# 平台应用授权与 PKCE

Bundle 安装的环境托管平台应用通过同源配置、统一网页授权和 PKCE 获取 appClientToken。

`ENVIRONMENT_HOSTED_APP` 必须通过 `@baijimu/platform-application-sdk` 使用当前部署提供的统一网页授权页。
公共 SaaS 当前地址是：

```text
https://console.baijimu.com/#/platform-app-authorize
```

这个地址只是公共 SaaS 的部署值，不是 SDK 默认值。SDK 从当前托管应用同源的
`/.well-known/baijimu-platform-application.json` 读取各环境登记的授权地址。
历史 `TRUSTED_EXTERNAL_APP` 已被当前启动和平台授权链路拒绝，外部站点必须使用自己的后台授权；
需要平台能力的应用先迁移为环境托管并升级 Bundle。
应用不能自己复制网页授权页、复制一份 `sdk.js`，也不能直接调用内部授权接口并传入 `userId`。
平台授权页负责确认登录态、工作区访问权，以及平台应用是否来自当前工作区已启用的精确 Bundle 安装；
标准 SDK 负责发起授权请求、处理回跳和兑换 token。

这里的 token 兑换属于 App Gateway 平台应用授权链路。完整接入配置见
[App Gateway 接入与配置](/development/bundle-development/platform-application-development/app-gateway-configuration/)。

## 完整流程

```text
工作区打开 accessUrl
  -> SDK 读取 installId
  -> SDK 生成 codeVerifier、codeChallenge 和 state
  -> SDK 跳转部署配置中的平台网页授权页
  -> 平台校验工作区成员、Bundle 安装和版本声明
  -> 授权页回跳 redirectUri，携带一次性 code
  -> SDK 校验 state
  -> SDK 使用 code + codeVerifier 兑换并校验 appClientToken
  -> SDK 携带 Bearer token 调用授权范围内的接口
```

这一步没有独立的平台应用 consent 确认。能力范围来自当前 Bundle 安装锁定的平台应用版本；扩大能力时必须创建新版本并升级 Bundle，而不是让前端提交一份新的授权范围。

## 分享入口与未安装提示

使用 `baijimu platform-app entry <platformAppId> --bundle-id <bundleId> --json` 查询当前环境的通用入口。
不要直接把带某个工作区 `installId` 的应用启动地址分享给其他用户。标准分享地址是：

```text
https://console.baijimu.com/#/bundle-app/{bundleId}/{platformAppId}
```

接收者登录后选择有权访问的工作区。Manager 会按 `bundleId + platformAppId` 核验该工作区的精确安装资源：

- 已安装且应用已启用：直接进入工作区应用入口。
- 未安装：明确提示需要安装该 Bundle，并进入只显示目标 Bundle 的安装流程；安装完成后返回原分享入口。
- 已物化但未启用：不签发授权码，提示工作区管理员检查 Bundle 安装状态。
- 当前成员不能安装：保留安装需求，由工作区所有者或管理员执行。

公开访问应用页面不代表拥有安装权限；`installId` 只是安装选择器，不能用于证明访问权或换取平台 token。

## 第一步：生成 PKCE 参数

应用只调用 `client.bootstrap()`。SDK 会为每次授权生成新的高熵 `codeVerifier`，并使用 SHA-256 计算
Base64 URL 编码的 `codeChallenge`：

```text
codeChallenge = BASE64URL(SHA256(codeVerifier))
codeChallengeMethod = S256
```

SDK 把 `codeVerifier` 和 `state` 临时保存在当前浏览器会话中，不会把 `codeVerifier` 放进 URL。

## 第二步：跳转网页授权页

公共 SaaS 授权页使用 Hash Router，因此 SDK 会把授权参数写在 `#/platform-app-authorize` 后面的查询
字符串中：

```text
https://console.baijimu.com/#/platform-app-authorize
  ?installId=pia_xxx
  &redirectUri=https%3A%2F%2Fapp.example.com%2F
  &codeChallenge=...
  &codeChallengeMethod=S256
  &state=...
```

参数含义：

| 参数                    | 是否必填 | 说明                   |
| --------------------- | ---- | -------------------- |
| `installId`           | 是    | 当前工作区安装记录            |
| `redirectUri`         | 是    | 授权完成后的回跳地址，必须与应用入口同源 |
| `codeChallenge`       | 是    | PKCE challenge       |
| `codeChallengeMethod` | 是    | 使用 `S256`            |
| `state`               | 是    | 防止请求伪造和回跳串线的随机值      |

旧参数 `prompt` 和 `authorizationRequestToken` 已退役，应用不得发送。

## 第三步：处理回跳

网页授权成功后回跳 `redirectUri`，并附加：

```text
?code=一次性授权码&installId=pia_xxx&state=原始state
```

回跳携带的是短期、一次性 `code`，不是最终 token。SDK 会先验证 `state` 与发起授权时保存的值一致，
再执行兑换。

## 第四步：兑换 token

SDK 调用当前部署配置中的 App Gateway。公共 SaaS 对应的请求示例是：

```http
POST https://api.baijimu.com/app-gateway/api/workspace-runtime/apps/{installId}/token/exchange
Content-Type: application/json

{
  "code": "authorization-code",
  "codeVerifier": "original-code-verifier"
}
```

兑换成功后 SDK 会校验：

- 返回的 `workspaceId` 是有效工作区
- 返回的 `runtimeAppId` 与当前安装绑定的默认 Runtime 应用一致
- token 对应的 `installId` 与启动安装一致
- token 尚未过期

App Gateway 将 token 放在响应的 `data.token` 中。应用通过同源配置初始化标准 SDK，不能把公共 SaaS 地址
或 URL 中未验证的参数作为授权服务地址：

```ts
import { createPlatformApplicationClientFromHostedConfiguration } from "@baijimu/platform-application-sdk";

const client = await createPlatformApplicationClientFromHostedConfiguration(applicationDefinition.id);
```

`appClientToken` 没有 `PLATFORM_APPLICATION` 或 `RUNTIME_APP` 两套 scope；所有通过
Bundle 安装的平台应用都使用同一 Runtime 绑定 token 契约。

授权码只能使用一次。兑换失败、token 到期或服务返回未授权时，应清理当前安装的本地会话并重新开始授权流程。

> **不要把 token 放在回跳 URL**
>
> URL 会进入浏览器历史、Referer、代理和访问日志。网页授权页只回传一次性授权码，最终 `appClientToken` 必须通过后端 token exchange 响应取得。
