> This page is for 平台, version V4 (default).
> For other versions, use one of these documentation indexes:
> - V4 (default): https://next.developer.frame.io/platform/v4/llms.txt
> - V4 实验版: https://next.developer.frame.io/platform/v4-experimental/llms.txt
> - 旧版: https://next.developer.frame.io/platform/v2/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://next.developer.frame.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://next.developer.frame.io/_mcp/server.

# Frame.io TypeScript SDK — 身份验证指南

本指南介绍如何使用 **Frame.io TypeScript SDK** (`frameio`) 通过 Frame.io API 进行身份验证。 Frame.io V4 API 使用 [Adobe Identity Management Service (IMS)](https://developer.adobe.com/developer-console/docs/guides/authentication/)，Adobe 的 OAuth 2.0 身份平台。 这是面向 TypeScript/JavaScript 开发者的独立参考。 下方的所有代码示例和流程仅适用于 `frameio` 包。

---

## TypeScript SDK 中的身份验证类型

TypeScript SDK 支持四种 OAuth 身份验证类，以及直接使用令牌的方式：

| 方法                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | 用例                                | 是否需要用户交互？ | 是否需要客户端密钥？ |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | --------- | ---------- |
| **静态令牌**                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | 快速脚本、测试，或者您已拥有令牌                  | 否         | 否          |
| **服务器到服务器**                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | 后端服务、定时任务、自动化                     | 否         | 是          |
| **Web 应用程序**                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | 服务器端应用程序（Express、Fastify、Next.js） | 是         | 是          |
| **SPA (PKCE)**                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | 无法存储密钥的浏览器应用程序                    | 是         | 否          |
| **原生应用程序 (PKCE)**                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | 使用自定义 URI 方案进行重定向的桌面/移动应用程序       | 是         | 否          |
| **服务器到服务器**方式让您的应用程序以服务帐户身份运行，无需用户交互。此方式仅适用于通过 [Adobe Admin Console](https://adminconsole.adobe.com/) 管理的 Frame.io V4 帐户。**Web 应用程序**和 **SPA** 方式让您的应用程序以特定用户的身份运行。两者底层都使用 Adobe IMS：用户授权您的应用程序后，SDK 会将生成的代码交换为令牌。TypeScript SDK 为您处理 IMS 的 `/authorize/v2` 和 `/token/v3` 流程。对于 Web 应用程序，您需要客户端密钥；对于 SPA，则改用 [PKCE](https://datatracker.ietf.org/doc/html/rfc7636)。**原生应用程序**遵循与 SPA 相同的 PKCE 流程，但使用 Adobe 为您的原生应用程序凭据分配的 `adobe+<hash>://callback</hash>` 重定向 URI。这让您的应用程序能够在授权完成后，于操作系统层面拦截该重定向。 |                                   |           |            |

---

## 服务帐户用户

使用服务器到服务器的身份验证时，您的应用程序充当**服务帐户用户**，这是一种可以代表服务执行操作的独特帐户类型。 这些内容在 Frame.io 中对其他用户可见：当服务帐户执行操作时，其名称会显示在 UI 中。 您可以通过 [Adobe Admin Console](https://adminconsole.adobe.com/) 和 [Developer Console](https://developer.adobe.com/console) 授予和撤销服务帐户访问权限。 服务帐户名称通过 Frame.io UI 进行管理。 默认情况下，您的第一个 S2S 连接名为 **Service Account User**，第二个名为 **Service Account User 2**，以此类推。

> **Info**
>
> 有关更多信息，请参阅 [使用 Frame.io 服务器到服务器的支持完成自动化设置](https://helpx.adobe.com/enterprise/using/automate-using-frame-io.html)。

---

## 快速入门

### 前提条件

1. [Adobe Developer Console](https://developer.adobe.com/console) 提供的 **凭据**：

* **客户端 ID** — 所有 OAuth 流均需要 - **客户端密钥** — 服务器到服务器的流和 Web 应用程序流需要 - **重定向 URI** — Web 应用程序、SPA 和原生应用程序流需要；必须注册在 Adobe 项目中

2. **安装 SDK：**

```bash
npm install frameio
```

### 选择方法

* **无用户参与？** 使用**服务器到服务器** (`ServerToServerAuth`)。
* **有用户参与且您可以存储密钥？** 使用 **Web 应用程序** (`WebAppAuth`)。
* **有用户参与但您无法存储密钥？** 对于浏览器应用程序，使用 **SPA** (`SPAAuth`)；对于桌面/移动应用程序，使用 **原生应用程序** (`NativeAppAuth`)。

---

## 访问令牌

如果您已有访问令牌（来自其他 OAuth 系统或之前的交换，例如通过我们的 [API Explorer](/platform/api-reference/accounts/index?explorer=true)获得），那么您可以直接传递：

```typescript
import { FrameioClient } from "frameio";

const client = new FrameioClient({ token: "YOUR_ACCESS_TOKEN" });
```

这是最简单的方法，但令牌最终会过期，SDK 不会为您刷新令牌。

### 旧版开发者令牌

对于尚未通过 [Adobe Admin Console](https://adminconsole.adobe.com/) 进行管理的 V4 迁移帐户，您可以继续使用来自 [Frame.io 开发者网站](https://developer.frame.io/app/tokens)的旧版开发者令牌。 您必须包含 `x-frameio-legacy-token-auth` 标头并将其设置为 `true`：

```typescript
import { FrameioClient } from "frameio";

const client = new FrameioClient({
    token: "YOUR_LEGACY_DEVELOPER_TOKEN",
    headers: { "x-frameio-legacy-token-auth": "true" },
});
```

旧版开发者令牌不会过期，但它们是一种过渡机制。 对于新的集成和生产工作负载，我们推荐使用下面的其中一个 OAuth 2.0 流。 有关详细信息，请参阅[迁移指南](/platform/docs/resources/migration#authentication)。

---

## 服务器到服务器（客户端凭据）

将此用于需要 Frame.io 访问权限且无需用户交互的后端服务和脚本。 此流仅适用于通过 [Adobe Admin Console](https://adminconsole.adobe.com/) 管理的 Frame.io V4 帐户。 您的应用程序以[服务帐户用户](#service-account-users)的身份进行身份验证，无需人工干预。

```typescript
import { FrameioClient, ServerToServerAuth } from "frameio";

const auth = new ServerToServerAuth({
    clientId: "YOUR_CLIENT_ID",
    clientSecret: "YOUR_CLIENT_SECRET",
});

const client = new FrameioClient({ token: () => auth.getToken() });
```

就是这样。 `auth.getToken()` 是 SDK 在每个请求中都会调用的异步函数。 如果当前令牌仍然有效，它就会立即返回。 如果即将过期，它会先获取一个新令牌，过程完全透明。

### 工作原理

您的客户端凭据（客户端 ID + 密钥）**永不过期**。 您只需出于安全维护目的手动轮换它们即可。 S2S 可为您提供实际上永久、不间断的 API 访问权限，无需手动干预。

底层原理：

1. 对于第一次 API 调用，`getToken()` 使用 `client_credentials` 授权凭证从 Adobe IMS 请求新的访问令牌。
2. 令牌会缓存在内存中。 单个访问令牌会过期（通常为 24 小时），但这会为您自动处理。
3. 当缓存的令牌在刷新缓冲时间内时（默认：过期时间前 60 秒），SDK 会使用相同的客户端凭据自动获取新的令牌。
4. 不涉及刷新令牌。 客户端凭据本身就是长期有效的密钥，始终可用于生成新的访问令牌。

### 显式身份验证

如果您想主动获取令牌（例如，在启动时因错误凭据而快速失败）：

```typescript
const auth = new ServerToServerAuth({ clientId: "...", clientSecret: "..." });
await auth.authenticate(); // throws AuthenticationError if credentials are invalid
const client = new FrameioClient({ token: () => auth.getToken() });
```

---

## Web 应用程序（授权代码）

将此用于用户使用其 Adobe ID 登录的服务器端应用程序。 此流需要客户端密钥，必须安全地将其存储在您的服务器上。

#### 将用户重定向到 Adobe IMS

```typescript
    import { WebAppAuth } from "frameio";
    import crypto from "crypto";

    const auth = new WebAppAuth({
        clientId: "YOUR_CLIENT_ID",
        clientSecret: "YOUR_CLIENT_SECRET",
        redirectUri: "https://yourapp.com/callback",
    });

    // Generate a cryptographically random state value to prevent CSRF attacks
    const state = crypto.randomBytes(32).toString("hex");

    const authorizationUrl = auth.getAuthorizationUrl({ state });
    // Store `state` in the user's session, then redirect them to `authorizationUrl`
```

#### 处理回调

当 Adobe IMS 将用户重定向回您的 `redirectUri` 时，提取 `code` 和 `state` 参数。 验证状态是否与您存储的内容匹配，然后将代码交换为令牌：

```typescript
    // In your callback handler (e.g. an Express route):
    await auth.exchangeCode(req.query.code as string);
```

这会将授权代码交换为访问令牌和刷新令牌，并将两者都存储在内部。

#### 使用客户端

```typescript
    import { FrameioClient } from "frameio";

    const client = new FrameioClient({ token: () => auth.getToken() });
```

就是这样。 从现在开始，`getToken()` 会自动管理令牌生命周期。 当访问令牌即将过期时，SDK 会使用刷新令牌获取新令牌。 无需用户交互。

### 完整的表达式示例

```typescript
import crypto from "crypto";
import express from "express";
import session from "express-session";
import { FrameioClient, WebAppAuth } from "frameio";

const auth = new WebAppAuth({
    clientId: "YOUR_CLIENT_ID",
    clientSecret: "YOUR_CLIENT_SECRET",
    redirectUri: "http://localhost:3000/callback",
});

const app = express();
app.use(session({ secret: crypto.randomBytes(32).toString("hex"), resave: false, saveUninitialized: false }));

app.get("/login", (req, res) => {
    const state = crypto.randomBytes(32).toString("hex");
    (req.session as any).oauthState = state;
    res.redirect(auth.getAuthorizationUrl({ state }));
});

app.get("/callback", async (req, res) => {
    if (req.query.state !== (req.session as any).oauthState) {
        return res.status(403).send("Invalid state parameter");
    }

    await auth.exchangeCode(req.query.code as string);

    const client = new FrameioClient({ token: () => auth.getToken() });
    const accounts = await client.accounts.index();
    res.json(accounts);
});

app.listen(3000);
```

---

## 单页面应用程序 / PKCE（授权代码 + PKCE）

将其用于无法安全存储客户端密钥的基于浏览器的应用程序、桌面应用程序或 CLI 工具。 此流使用 [PKCE (RFC 7636)](https://datatracker.ietf.org/doc/html/rfc7636) 来保护授权代码交换。

#### 生成授权 URL

```typescript
    import { SPAAuth } from "frameio";

    const auth = new SPAAuth({
        clientId: "YOUR_CLIENT_ID",
        redirectUri: "https://yourapp.com/callback",
    });

    const state = crypto.randomUUID();
    const result = await auth.getAuthorizationUrl({ state });
    // result.url            -> redirect the user here
    // result.codeVerifier   -> store this securely until the callback
```

`getAuthorizationUrl` 返回一个 `AuthorizationUrlResult`，其中包含完整 URL（嵌入 PKCE `code_challenge`）以及您在下一步中需要的 `codeVerifier`。

#### 使用验证器交换代码

当用户被重定向回来时：

```typescript
    await auth.exchangeCode({
        code: "CODE_FROM_CALLBACK",
        codeVerifier: result.codeVerifier,
    });
```

#### 使用客户端

```typescript
    import { FrameioClient } from "frameio";

    const client = new FrameioClient({ token: () => auth.getToken() });
```

就是这样。 刷新操作与 Web 应用程序相同 — SDK 会自动使用刷新令牌。 区别在于，刷新期间不会发送客户端密钥，因为 SPA 流专为公共客户端而设计。

> **Warning**
>
> `codeVerifier` 必须安全地存储在授权请求和代码交换之间的客户端。 在浏览器应用程序中使用 `sessionStorage` 或等效方式。

---

## 原生应用程序（授权代码 + PKCE）

将其用于桌面和移动应用程序。 在 [Adobe Developer Console](https://developer.adobe.com/console) 中创建原生应用程序凭据时，Adobe 会为您分配一个格式为 `adobe+<hash>://callback</hash>` 的重定向 URI——您需要在操作系统级别注册您的应用程序来处理该自定义 URI 方案。 本地开发也支持环回重定向 (`http://127.0.0.1:<port>/callback</port>`)。 该流与 SPA 相同 — 使用 PKCE 且没有客户端密钥。

```typescript
import { NativeAppAuth } from "frameio";

const auth = new NativeAppAuth({
    clientId: "YOUR_CLIENT_ID",
    redirectUri: "adobe+abc123def456://callback", // from your Adobe Developer Console Native App credential
    // Also supports loopback: "http://127.0.0.1:8080/callback"
});

const { url, codeVerifier } = await auth.getAuthorizationUrl({
    state: crypto.randomUUID(),
});

// Open system browser to `url`
// Listen for redirect on your custom URI scheme or loopback server

await auth.exchangeCode({ code: "CODE_FROM_REDIRECT", codeVerifier });
const client = new FrameioClient({ token: () => auth.getToken() });
```

### 重定向 URI 规则

Adobe 在两个点执行重定向 URI 规则：在 [Adobe Developer Console](https://developer.adobe.com/console) 中注册凭据时，以及当 `redirect_uri` 参数访问 `/authorize/v2` 端点时。 您在此 SDK 中传递给 `redirectUri` 的值必须与您在凭据上注册的其中一个“重定向 URI 模式”匹配，否则 Adobe 将重定向到凭据上的默认重定向 URI。

* **Web 应用程序** 和 **SPA** 凭据需要 HTTPS。
* **原生应用程序**凭据使用非 HTTPS 重定向 — 通常是 Developer Console 中显示的凭据 `adobe+<hash>://callback</hash>` URI。

有关您的凭据接受的确切模式，请参阅 [Adobe Developer Console](https://developer.adobe.com/console)。

> **Note**
>
> Python SDK 不包含原生应用程序凭据类，因为 Python 没有用于注册自定义
>
> URI 方案处理程序的标准方法。 TypeScript SDK 支持包括原生应用程序在内的所有四种凭据类型。

---

## 手动令牌刷新

对于 Web 应用程序、SPA 和原生应用程序流，SDK 通过 `getToken()` 自动刷新令牌。 如果您需要显式控制，可以直接调用 `refresh()`：

```typescript
await auth.refresh(); // fetches a new access token using the refresh token
```

当您希望在关键操作之前强制刷新而不是依赖自动刷新缓冲区时，这样做非常有用。

`refresh()` 可用于 `WebAppAuth`、`SPAAuth` 和 `NativeAppAuth` 上。 如果没有可用的刷新令牌，则会抛出 `ConfigurationError`（即您必须先调用 `exchangeCode()`）。 `ServerToServerAuth` 没有 `refresh()` 方法 — 而是使用 `authenticate()` 通过客户端凭据获取新令牌。

---

## 令牌持久性

所有身份验证类都支持 `exportTokens()` 和 `importTokens()` 来在重新启动时持久化令牌状态。 对于 Web 应用程序、SPA 和原生应用程序流程，这尤其重要，因为访问令牌和刷新令牌默认保存在内存中 — 如果您的应用程序重新启动，用户需要重新进行身份验证，除非您对这些令牌进行持久化。 对于服务器到服务器，持久性是可选的（客户端凭据始终可以生成新令牌），但导入缓存的令牌可以避免启动时出现额外的往返。

### 导出和导入

```typescript
// After exchangeCode(), save the token state
const tokenData = auth.exportTokens();
// tokenData is: { access_token: "...", refresh_token: "...", expires_at: 1234567890.0 }
// Save it to your database, file, or secret store

// On next startup, restore it
auth.importTokens(tokenData);
const client = new FrameioClient({ token: () => auth.getToken() });
// The SDK will automatically refresh if the token is near expiry
```

> **Warning**
>
> 安全存储导出的令牌。 它们包含用于授权凭证 API 访问权限的访问令牌和刷新令牌。 避免将令牌写入
>
> 生产中的纯文本文件。

### 使用 `onTokenRefreshed` 自动持久化

要在每次刷新时自动持久化令牌，请使用 `onTokenRefreshed` 回调：

```typescript
import fs from "fs/promises";

const TOKEN_FILE = "tokens.json";

const auth = new WebAppAuth({
    clientId: "...",
    clientSecret: "...",
    redirectUri: "...",
    onTokenRefreshed: (tokens) => {
        fs.writeFile(TOKEN_FILE, JSON.stringify(tokens));
    },
});

// On startup, restore if available
try {
    const saved = JSON.parse(await fs.readFile(TOKEN_FILE, "utf-8"));
    auth.importTokens(saved);
} catch {
    // No saved tokens — user will need to authenticate
}
```

该回调接收与 `exportTokens()` 相同结构的数据，并在每次成功刷新令牌后触发。

---

## 撤销令牌

要注销用户并通过 Adobe IMS 使其令牌失效：

```typescript
await auth.revoke();
```

这会向 Adobe IMS 发出两个尽力撤销请求（一个用于访问令牌，一个用于刷新令牌，两者并行），然后清除所有本地令牌状态。对于机密客户端 (`WebAppAuth`)，撤销请求使用 HTTP Basic Auth；对于公共客户端（`SPAAuth`、`NativeAppAuth`），`client_id` 作为查询参数发送。 撤销错误会被记录但不会被抛出。 撤销后，用户将需要重新进行身份验证。

---

## 错误处理

所有身份验证错误都继承自 `FrameioAuthError`，因此您可以广泛捕获它们或处理特定用例：

```typescript
import {
    FrameioAuthError,
    AuthenticationError,
    TokenExpiredError,
    ConfigurationError,
    NetworkError,
    RateLimitError,
} from "frameio";

try {
    await auth.exchangeCode("...");
} catch (error) {
    if (error instanceof TokenExpiredError) {
        // The refresh token has expired; redirect the user to sign in again
    } else if (error instanceof AuthenticationError) {
        // Token exchange failed
        console.error(`Error: ${error.errorCode} - ${error.errorDescription}`);
    } else if (error instanceof NetworkError) {
        // Timeout or connection failure (after retries)
    } else if (error instanceof RateLimitError) {
        // 429 from Adobe IMS; retry after error.retryAfter seconds
    } else if (error instanceof FrameioAuthError) {
        // Catch-all for any other auth error
    }
}
```

### 错误参考

| 例外                    | 引发的时间                                                          |
| --------------------- | -------------------------------------------------------------- |
| `ConfigurationError`  | 配置缺失或无效（例如 `clientId` 为空、非 HTTPS 重定向 URI、非 HTTPS `imsBaseUrl`） |
| `AuthenticationError` | Adobe IMS 拒绝令牌交换或刷新（具有 `.errorCode` 和 `.errorDescription`）     |
| `TokenExpiredError`   | 刷新令牌本身已过期；用户必须重新进行身份验证                                         |
| `NetworkError`        | HTTP 超时或完成所有重试后仍出现连接故障                                         |
| `RateLimitError`      | Adobe IMS 返回 429；请查看 `.retryAfter` 获取退避指导                      |
| `PKCEError`           | 可供消费者在 PKCE 流中使用；不会由 SDK 内部抛出                                  |

### 在生产中处理过期的刷新令牌

> **Warning**
>
> 对于 Web 应用程序、SPA 和原生应用程序流，刷新令牌最终会过期。当这种情况发生时，`getToken()` 会引发 `TokenExpiredError`。 您应该捕获这个错误，并再次将用户重定向到授权流。

```typescript
import { TokenExpiredError } from "frameio";

try {
    const client = new FrameioClient({ token: () => auth.getToken() });
    const assets = await client.files.list({ projectId: "..." });
} catch (error) {
    if (error instanceof TokenExpiredError) {
        // Clear persisted tokens and redirect user to login
        await auth.revoke();
        return res.redirect("/login");
    }
}
```

---

## 配置参考

这些参数具有合理的默认值，很少需要设置。 如果您确实需要自定义行为（指向暂存 IMS、注入自定义 `fetch`、微调超时或连接日志记录器），请在构造授权类时将其中任何一个作为可选参数传递：

| 参数                 | 默认                               | 描述                                                                                                                               |
| ------------------ | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `scopes`           | 特定于流的默认值                         | 以空格分隔的 OAuth 权限范围。 S2S 默认为 `openid AdobeID frame.s2s.all`；面向用户的流默认为 `openid email profile offline_access additional_info.roles`。 |
| `imsBaseUrl`       | `https://ims-na1.adobelogin.com` | Adobe IMS 基础 URL。 针对暂存或非生产环境的覆盖设置。 必须使用 HTTPS。                                                                                   |
| `fetch`            | `globalThis.fetch`               | 用于代理、mTLS 或自定义 HTTP 处理的自定义 `fetch` 实施。                                                                                           |
| `timeout`          | `30000`                          | 令牌端点调用的 HTTP 请求超时时间（以毫秒为单位）。                                                                                                     |
| `maxRetries`       | `2`                              | 瞬态故障（5xx、超时）的最大重试次数。 速率限制重试 (429) 单独跟踪。                                                                                          |
| `refreshBuffer`    | `60`                             | 令牌过期时间前触发主动刷新的秒数。                                                                                                                |
| `onTokenRefreshed` | `undefined`                      | 每次成功刷新令牌后触发的回调。 接收包含 `access_token`、`refresh_token` 和 `expires_at` 的对象。                                                          |
| `logger`           | 无操作（静默）                          | 采用 `debug`、`info`、`warn`、`error` 方法的记录器实例（例如 `console`、`pino`、`winston`）。                                                        |

### 暂存环境

通过重写 `imsBaseUrl` 指向暂存 Adobe IMS 实例。 如果您需要以编程方式引用生产值，SDK 还可以导出 `DEFAULT_IMS_BASE_URL` (`https://ims-na1.adobelogin.com`)。

```typescript
const auth = new ServerToServerAuth({
    clientId: "...",
    clientSecret: "...",
    imsBaseUrl: "https://ims-na1-stg1.adobelogin.com",
});
```

### 自定义获取

对于代理支持或自定义 TLS 配置：

```typescript
import { ProxyAgent } from "undici";

const proxyAgent = new ProxyAgent("http://corporate-proxy:8080");

const auth = new ServerToServerAuth({
    clientId: "...",
    clientSecret: "...",
    fetch: (url, init) => fetch(url, { ...init, dispatcher: proxyAgent }),
});
```

---

## 并发安全性

TypeScript SDK 可安全用于并发使用。 当多个 `getToken()` 调用同时发生且需要刷新时，只会触发一个刷新请求。 其他调用会等待相同的 promise，最终接收相同的结果。 无需外部锁定。 这种去重技术利用 JavaScript 的单线程事件循环和共享的 `Promise` — 如果刷新已在进行中，并发调用者会加入其中，而不是启动第二个请求。 如果在刷新操作期间调用 `revoke()`，刷新会因 `AuthenticationError` 被拒绝，同时令牌也会保持清除状态，撤销始终优先。