# 登录

Mini App 可以用 Micromeet 账号登录正在使用 Cowork 的人，再从它自己的后端以这个人的身份调用 Micromeet。页面始终拿不到密码、token 或 Mini App 的密钥：Cowork 只给页面一个一次性的 code，由 Mini App 的后端拿它换 token。

只有 Micromeet 签过名、而且至少有一个验证过的服务器域名的已安装 Mini App 能登录（见[打包与签名](https://docs.micromeet.ai/zh/miniapp-sdk/build.md)）。内置 Mini App 没有这个模块。

## MAESTROSDK.auth.login(params?) <Badge type="warning" text="尚未发布" />

向 Cowork 要一个一次性 code，对应这个 Mini App 和当前登录 Cowork 的人。

### 参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `scopes` | `string[]` | 否 | Mini App 要的权限，例如 `['profile:read']`。不传就是 Micromeet 允许这个 Mini App 的全部权限 |

第一次调用时，Cowork 给这个人看一张卡：Mini App 的名字和开发者、它要的权限、将要使用的账号；按钮是 **授权** 和 **取消**，默认是取消。卡只出现在这个人正在看的标签页上。授权过一次之后，同样的权限再调用直接拿到 code，不再弹卡。

### 返回值

`Promise<{ ok: true; code: string; region: 'SG' | 'HK' | 'ID'; expiresIn: number } | { ok: false; error: string }>`

code 只能用一次，5 分钟内有效（`expiresIn` 单位为秒）。把 `code` 和 `region` 发给你的后端。

| `error` | 含义 |
|---|---|
| `unsigned` | 包没有 Micromeet 的签名，或页面不是已安装的 Mini App |
| `tampered` | 安装后 Mini App 的文件被改过；Cowork 已关闭它 |
| `host_signed_out` | 没有人登录 Cowork，或页面是为另一个账号打开的。如果这正是用户在看的标签页，Cowork 会打开它的登录界面 |
| `user_cancelled` | 用户取消了、标签页在后台，或卡开着时换了账号 |
| `app_not_available` | 这个 AppID 在本区没有登记，或已停用 |
| `version_not_available` | 这个版本没有登记或已停用：请更新 Mini App |
| `no_server_domain` | 这个版本没有验证过的服务器域名，或其中有域名已不再有效（见[打包与签名](https://docs.micromeet.ai/zh/miniapp-sdk/build.md)） |
| `scope_denied` | 要了 Micromeet 没允许这个 Mini App 的权限，或 `scopes` 不是字符串列表 |
| `rate_limited` | 一分钟内要的 code 太多；稍后再试 |
| `network` | Cowork 连不上 Micromeet |

## MAESTROSDK.auth.status() <Badge type="warning" text="尚未发布" />

`Promise<{ signed: boolean; host: 'cowork'; hostSignedIn: boolean }>`。包的签名有效且文件都没被改过时 `signed` 为 `true`；有人登录 Cowork 时 `hostSignedIn` 为 `true`。可以据此决定是否显示登录按钮。

## 在 Mini App 的后端

后端保存 Micromeet 登记 AppID 时给的 client secret，Mini App 登记在几个区就有几个；不要放进页面或包里。后端运行在 Mini App 验证过的某个服务器域名上（见[打包与签名](https://docs.micromeet.ai/zh/miniapp-sdk/build.md)）。一步步的例子见[开发登录 Mini App](https://docs.micromeet.ai/zh/miniapp-sdk/sign-in-guide.md)。

### 页面发来的请求

页面的 origin 是 `miniapp://<appId>`。回答它的请求时带上 `Access-Control-Allow-Origin: miniapp://<appId>`，预检请求（`OPTIONS`）也要允许它用到的方法和请求头；否则请求在页面里就失败了，到不了你的代码。Micromeet 的 token 留在后端，给页面你自己的会话。

### 后端工具

`@micromeet-ai/miniapp-sdk/server`：ESM 和 CommonJS 都能用，需要 Node.js 18 或更新版本。

```js
import { createMiniappAuthClient, MiniappAuthError } from '@micromeet-ai/miniapp-sdk/server';

const auth = createMiniappAuthClient({ appId: 'ai.example.app', clientSecret: process.env.MINIAPP_SECRET_HK });
const tokens = await auth.exchangeCode({ code, region });
const me = await auth.profile({ accessToken: tokens.accessToken, region: tokens.region });
```

| 选项 | 必填 | 说明 |
|---|---|---|
| `appId` | 是 | AppID |
| `clientSecret` | 是 | 这个 client 要调用的那个区的 secret；每个区建一个 client |
| `coreUrl` | 否 | `(region) => 地址`：调用哪个 Micromeet。默认按区调用正式环境 |
| `fetch` | 否 | 替换全局的 `fetch` |
| `timeoutMs` | 否 | 每个请求的超时，默认 15000 |

| 函数 | 返回 |
|---|---|
| `exchangeCode({ code, region })` | token：`accessToken`、`expiresIn`（秒）、`refreshToken`、`scope`（列表）、`user: { ref, name, email }`、`institution: { iid, name }`、`region` |
| `refresh({ refreshToken, region })` | 一组新的 token；旧的 refresh token 失效，保存新的 |
| `revoke({ refreshToken, region })` | 无；让这个人退出 Mini App 的登录 |
| `profile({ accessToken, region })` | `{ ref, name, email, institution: { iid, name }, region }`（`profile:read`） |
| `call({ accessToken, region, path, body? })` | 向接受 Mini App token 的 Micromeet 接口发 `POST`，返回它的 `data` |

出错时每个函数都抛出 `MiniappAuthError`，带 Micromeet 的 `code` 和 HTTP `status`（见下面的「错误」）。

### Token

| Token | 有效期 | 用途 |
|---|---|---|
| Access token | 15 分钟 | 调用接受 Mini App token 的接口时放在 `Authorization: Bearer` |
| Refresh token | 30 天 | `refresh` 换一组新的；每个 refresh token 只能用一次 |

- 之后的每次调用都用 code 带来的那个区；token 只在它的区有效。
- token 代表这个人，以及他登录时在 Cowork 里选中的机构。
- `user.ref` 是这个人在这个 Mini App 里的固定编号（换一个 Mini App 就是另一个）：用它当你的用户键。
- 改密码、退出所有设备、离开机构、版本停用或 Mini App 停用后，token 在下一次请求时失效。

### Scope 与接口

| Scope | 接口 | 返回 |
|---|---|---|
| `profile:read` | `POST /share/miniapp/profile` | `{ ref, name, email, institution: { iid, name }, region }` |

token 只能用在这张表里的接口上，只限这个人授权的 scope，也不会超出这个人在 Micromeet 里本来能做的事。

### 错误

| `code` | HTTP | 发生了什么 |
|---|---|---|
| `invalid_client` | 401 | 这个区的 AppID 或 secret 不对，或 secret 已被更换 |
| `invalid_grant` | 400 | code 已用过、超过 5 分钟或不是这个 AppID 的；refresh token 已用过、已撤销或超过 30 天；或这个人改了密码、退出了所有设备、离开了机构：重新登录 |
| `app_not_available` | 400 | 这个 Mini App 在这个区已停用 |
| `version_not_available` | 400 | 这个人登录时用的版本已停用：请更新 Mini App |
| `needLogin` | 401 | 调用接口时：access token 过期或已失效。调用 `refresh`，失败就重新登录 |
| `forbidden` | 403 | 调用接口时：token 没有这个接口的 scope，或这个人在 Micromeet 里不能做这件事 |
| `network` | — | 后端工具连不上 Micromeet（`status` 为 0） |
| `params_invalid` | 400 或 — | `grant_type` 不对；或后端工具收到未知的区、不是接口的 `path`（`status` 为 0，不发请求） |

### HTTP 调用

其他语言的后端做同样的调用。每个都是向 code 所在区的 Micromeet 发 `POST`，请求体是 JSON：

| 区 | 地址 |
|---|---|
| SG | `https://crms-api.micromeet.ai` |
| HK | `https://crms-api-hk.micromeet.ai` |
| ID | `https://crms-api-id.micromeet.ai` |

| 路径 | 请求体 | 回答里的 `data` |
|---|---|---|
| `/miniapp-open/auth/token` | `{ "grant_type": "authorization_code", "app_id", "client_secret", "code" }` | `{ access_token, token_type: "Bearer", expires_in, refresh_token, scope, user: { ref, name, email }, institution: { iid, name }, region }`；`scope` 用空格分隔 |
| `/miniapp-open/auth/token` | `{ "grant_type": "refresh_token", "app_id", "client_secret", "refresh_token" }` | 同上 |
| `/miniapp-open/auth/revoke` | `{ "app_id", "client_secret", "refresh_token" }` | `{ revoked: true }` |
| `/share/miniapp/profile` | `{}`，带 `Authorization: Bearer <access_token>` | `{ ref, name, email, institution: { iid, name }, region }` |

成功时回答 `{ "code": 0, "success": true, "data": … }`；失败时回答 `{ "code": "<错误>", "msg": "…", "success": false }`，HTTP 状态见上面的「错误」。

## 相关

- [打包与签名](https://docs.micromeet.ai/zh/miniapp-sdk/build.md): 打包 Mini App，交给 Micromeet 签名
- [概览](https://docs.micromeet.ai/zh/miniapp-sdk/overview.md): Mini App SDK 的全部模块
