# @hearth-auth/sdk 中文文档

规范地址：https://npmcn.com/package/%40hearth-auth/sdk/

版本：4\.6\.4
npm 元数据更新时间：2026-11-01T05:06:48.347Z
中文译文更新时间：2026-11-01T08:25:48.520Z
软件包许可：未声明

本文由上游 README 机器翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40hearth-auth/sdk
原文对照：https://npmcn.com/package/%40hearth-auth/sdk/?lang=original
项目主页：https://github.com/hearth-auth/hearth#readme
源码仓库：https://github.com/hearth-auth/hearth

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：415 kB，56 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- 运行依赖：2 个
- peer 依赖：next \>=14\.0\.0，react \>=17\.0\.0 \<20\.0\.0
- 首次发布：2026-06-22T18:00:14.378Z
- 最新版本发布：2026-11-01T05:06:47.991Z
- 已发布版本数：105（近 12 个月 105 个）

## 中文简介

的TypeScript客户端 [炉](https://github.com/hearth-auth/hearth) 身份的API。

## 安装

```sh
npm install @hearth-auth/sdk
```

也可以使用：`pnpm add @hearth-auth/sdk`、`yarn add @hearth-auth/sdk`、`bun add @hearth-auth/sdk`

## README

# 灶台TypeScript SDK

的TypeScript客户端 [炉](https://github.com/hearth-auth/hearth) 身份的API。

> **SDK规范:** 此SDK必须符合 [灶台SDK通用规范](https://npmcn.com/package/openspec/specs/sdk-support-contract/spec.md).

## 安装

```bash
npm install @hearth-auth/sdk
# or
yarn add @hearth-auth/sdk
# or
pnpm add @hearth-auth/sdk
```

**同行依赖性:** 两个可选的。反应(`>=17 <20`)只需要用于 `HearthProvider` / `useHasPermission` 钩子。Next.js (`>=14`)只在导入时才需要 `@hearth-auth/sdk/nextjs` 或者 `@hearth-auth/sdk/nextjs/edge`.

| 导入路径 | 它给了你什么 |
| --- | --- |
| `@hearth-auth/sdk` | `HearthClient`，令牌验证，OAuth流，Express/ fasttify中间件，管理客户端，React钩子，浏览器验证 |
| `@hearth-auth/sdk/nextjs` | `withHearthAuth` （Pages Router）和 `getHearthClaims` （应用路由器路由处理程序） |
| `@hearth-auth/sdk/nextjs/edge` | `hearthEdgeMiddleware` 用于 `middleware.ts` 在边缘运行时 |

* * *

## 快速启动

```typescript
import { createHearth, HearthClient } from "@hearth-auth/sdk";

// Server-side client: discovery, token verification, OAuth flows
const client = new HearthClient({
  issuerUrl: "https://hearth.example.com",
  clientId: "<client-id>",
  clientSecret: "<client-secret>", // confidential clients only
  realmId: "<your-realm-id>",
  audience: "https://api.example.com", // expected `aud`; default "hearth"
});

const claims = await client.verifyToken(accessToken); // throws on a bad token
claims.subject();
claims.hasPermission("docs.write");

// RBAC facade — verifies the token, then checks its claims
const hearth = createHearth({
  baseUrl: "https://hearth.example.com",
  realmId: "<your-realm-id>",
  issuerUrl: "https://hearth.example.com/realms/<realm-slug>",
  getToken: () => currentAccessToken,
});
await hearth.hasPermission("docs.write");
```

`HearthClient` 读取其中的每个端点URL `{issuerUrl}/.well-known/openid-configuration` 第一次使用并缓存它。 `httpTimeout` （默认为10000毫秒）适用于它发出的每个请求。呼叫 `client.invalidateCache()` 删除缓存的发现文档、JWKS和自省客户端。

`createHearth` 为令牌提供了一个RBAC facade `getToken()` 的回报。在读取声明之前，每次检查都会根据领域JWKS（获取一次，然后缓存）验证该令牌。

### 观众

`verifyToken` 总是检查 `aud` （rfc 9068§4）；检查不能关闭。预期的受众是 `audience` 选项,默认 `"hearth"` (出口 `DEFAULT_AUDIENCE`)：当客户端没有命名任何资源时，用户灶台将自动生成。注册为受保护资源的API设置其资源URI。客户端ID不是受众：只根据客户端ID检查ID令牌（OIDC Core）。一个空 `audience` 抛出 `ConfigurationError`;一个令牌 `aud` 不包含预期的观众投掷 `TokenAudienceError`. `JwksClient` 选择相同的选项，并且 `VerifyOptions.audience` 凌驾于它之上 `verify()` 呼叫。

* * *

## 服务器端登录（使用PKCE的授权码）

`beginLogin` 使用新的PKCE验证器构建授权URL `state`。将两者都保存在服务器端会话中；回调时，勾选 `state` 并调用 `completeLogin`.

```typescript
// GET /login
const { authorizationUrl, state, codeVerifier } = await client.beginLogin(
  "https://app.example.com/callback",
  "openid profile email", // default: "openid"
);
session.oauth = { state, codeVerifier };
res.redirect(authorizationUrl);

// GET /callback
const url = new URL(req.url, "https://app.example.com");
if (url.searchParams.get("state") !== session.oauth.state) throw new Error("state mismatch");
const tokens = await client.completeLogin(
  url.searchParams.get("code")!,
  session.oauth.codeVerifier,
  "https://app.example.com/callback",
);
// tokens.access_token, tokens.expires_in, tokens.refresh_token?, tokens.id_token?
```

`completeLogin(code, verifier, redirectUri)` 是 `exchangeCode(code, redirectUri, { codeVerifier })`。呼叫 `exchangeCode` 在您自己构建授权URL时直接使用。

发送 `client_id` 在form body中，加上 `client_secret` 当配置一个时。公共客户离开 `clientSecret` 并依赖于PKCE。

### 刷新令牌

```typescript
const refreshed = await client.refreshTokens(tokens.refresh_token!);
// Store refreshed.refresh_token when present — Hearth rotates refresh tokens.
```

传递第二个参数以请求更窄的范围。

### 其他补助

| 方法 | 格兰特 |
| --- | --- |
| `clientCredentials(scope?)` | 客户端凭证（RFC 6749§4.4） |
| `startDeviceFlow(scope?)`， `pollDeviceToken(deviceCode, interval)` | 设备授权（RFC 8628） |
| `requestMagicLink(email)`， `exchangeMagicLink(token)` | 无密码魔法链接（需要） `realmId`） |

每个令牌端点失败都会抛出 `OAuthFlowError` 与 `statusCode` 和OAuth `errorCode` (例如 `invalid_grant`）. 网络故障或超时 `statusCode` 0.

### 浏览器应用程序

单页应用程序没有服务器会话来保存验证器。使用 `createHearthAuth`，或从中构建流 `generateCodeVerifier`， `generateCodeChallenge` 和 `buildAuthorizationUrl`.

```typescript
import { createHearthAuth, HearthApiClient } from "@hearth-auth/sdk";

const auth = createHearthAuth(
  new HearthApiClient({ baseUrl: "https://hearth.example.com", realmId: "<realm-id>" }),
  {
    clientId: "<client-id>",
    redirectUri: "https://app.example.com/callback",
    hearthUrl: "https://hearth.example.com",
    realmSlug: "<realm-slug>",
    // storage: "sessionStorage" (default) | "localStorage" | "memory" | { getItem, setItem, removeItem }
    // storageKeyPrefix: "hearth_" (default)
  },
);

await auth.startLogin();
// on the callback page: await auth.handleCallback(code, state);
auth.getAccessToken(); // also getRefreshToken(), getIdToken(), isAuthenticated(), clearTokens()
```

访问令牌保留在内存中。刷新令牌、ID令牌和PKCE验证器 `state` 在飞行中登录到配置的 `storage`， `sessionStorage` 默认情况下（范围为一个选项卡，关闭时清除）。 `"memory"` 在登录重定向后无效。在创建时，facade被移除 `hearth_refresh_token` 和 `hearth_id_token` 从 `localStorage` 除非 `localStorage` 是已配置的存储。

* * *

## RBAC的能力

返回的令牌 `getToken()` 在读取声明之前：针对领域JWKS的EdDSA签名(从 `issuerUrl` 一次，然后缓存)，加上 `exp`， `nbf`， `iat`， `iss` (必须等于 `issuerUrl`), `aud` (必须包含 `audience`、默认 `"hearth"`）. 当令牌不存在或未进行验证时，每个谓词都会解析 `false`． 与 `sessionVersions` 启用后，谓词可以拒绝 `SessionVersionRevokedError` 或者 `SessionVersionCacheStaleError`.

```typescript
const hearth = createHearth({
  baseUrl: "https://hearth.example.com",
  realmId: "<your-realm-id>",
  issuerUrl: "https://hearth.example.com/realms/<realm-slug>", // required: the realm issuer
  audience: "hearth",                                           // optional; this is the default
  getToken: () => auth.getAccessToken(), // may also return a Promise
});
```

### `hasPermission(permission: string): Promise<boolean>`

解决了 `true` 如果令牌验证并且它 `permissions` 要求包含 `permission`。将其用于特性门和API保护。

```typescript
if (await hearth.hasPermission("docs.versions.read")) {
  renderVersionHistory();
}
```

### `hasRole(role: string): Promise<boolean>`

解决了 `true` 如果令牌验证并且它 `roles` 要求包含 `role`。对于UI个性化和粗粒度访问非常有用。

```typescript
if (await hearth.hasRole("billing-admin")) {
  renderBillingPanel();
}
```

### `inGroup(group: string): Promise<boolean>`

解决了 `true` 如果令牌验证并且它 `groups` 声明包含组段塞。

```typescript
if (await hearth.inGroup("engineering")) {
  renderInternalToolingLink();
}
```

### `inOrg(org: string): Promise<boolean>`

解决了 `true` 如果令牌验证并且它 `oid` claim等于给定的组织ID。

```typescript
if (await hearth.inOrg("6f1d0c1e-3b9a-4c2e-9d7f-1a2b3c4d5e6f") /* the org's id */) {
  renderAcmeContent();
}
```

### `client.permissions(): Promise<MePermissionsResponse>`

调用 `GET /v1/me/permissions` 并返回 **freshly-resolved** 来自服务器的RBAC声明集。与上面的谓词不同，这反映了自JWT发布以来所做的任何角色/组分配。

```typescript
const { roles, groups, permissions } = await hearth.client.permissions();
```

使用 `client.permissions()` 当您需要发布后的准确性时（例如，在管理操作之后）。对于其他检查，首选本地谓词—一旦缓存了JWKS，它们就不会进行网络调用。

* * *

## 反应集成

React钩子是从main中导出的 `@hearth-auth/sdk` 包中。不需要导入子路径。

```tsx
import {
  createHearth,
  HearthProvider,
  useHasPermission,
  useHasRole,
  useInGroup,
  useInOrg,
} from "@hearth-auth/sdk";

// 1. Create the facade once at app startup
const hearth = createHearth({
  baseUrl: "https://hearth.example.com",
  realmId: "<your-realm-id>",
  issuerUrl: "https://hearth.example.com/realms/<realm-slug>",
  getToken: () => auth.getAccessToken(), // auth = createHearthAuth(...)
});

// 2. Mount the provider at the root of your React tree
function App() {
  return (
    <HearthProvider client={hearth}>
      <Router />
    </HearthProvider>
  );
}

// 3. Use hooks anywhere in the tree — no prop drilling
function NavBar() {
  const canEdit   = useHasPermission("docs.write");
  const isAdmin   = useHasRole("admin");
  const inEng     = useInGroup("engineering");
  const isAcme    = useInOrg("6f1d0c1e-3b9a-4c2e-9d7f-1a2b3c4d5e6f");

  return (
    <nav>
      {canEdit   && <a href="/editor">Editor</a>}
      {isAdmin   && <a href="/admin">Admin</a>}
      {inEng     && <a href="/internal">Internal tools</a>}
      {isAcme    && <a href="/acme">Acme portal</a>}
    </nav>
  );
}
```

钩子返回 `boolean`。每个在每次渲染后运行facade的异步检查并返回最后的结果： `false` 直到检查解决，并且 `false` 当它拒绝的时候。所有钩子返回 `false` 当没有 `HearthProvider` 已挂载，使它们可以在没有提供程序的情况下安全地调用测试。

* * *

## UserInfo和live权限

`userinfo` 调用被发现的 `userinfo_endpoint`。它返回由授予范围过滤的OIDC声明： `sub` 总是存在； `name` 需要 `profile` 范围; `email` 和 `email_verified` 需要 `email`.

```typescript
const info = await client.userinfo(accessToken);
// info.sub, info.name?, info.email?, info.email_verified?, plus any other released claim
```

`mePermissions` 调用 `GET /v1/me/permissions` 并返回用户现在在服务器上的角色、组和权限，包括发出令牌后所做的更改。它需要 `realmId`.

```typescript
const { roles, groups, permissions } = await client.mePermissions(accessToken);
```

### Session-version饲料

`svSnapshot` 和 `svDelta` 读取会话版本提要（RFC HEA-930），该提要允许资源服务器查看会话撤销，而无需自省每个令牌。都需要 `realmId` 和一个服务令牌 `hearth.sv_feed` 范围。

```typescript
const snap = await client.svSnapshot(serviceToken); // { current_seq, versions: { [sessionId]: minSv } }
const delta = await client.svDelta(serviceToken, snap.current_seq, 500); // null when nothing changed
```

`SessionVersionCache` 为您运行此循环并检查令牌 `sv` 没有网络呼叫索赔。

* * *

## JWKS和发现

```typescript
// The discovery document (cached after the first call)
const discovery = await client.discover();

// Verify an access token: EdDSA signature against the realm JWKS, then exp,
// nbf, iat (not in the future), iss (must equal issuerUrl) and aud (must
// contain `audience`, default "hearth").
const claims = await client.verifyToken(accessToken);
claims.subject();          // sub
claims.scopes();           // scope split into an array
claims.requiredActions();  // required_actions, [] when absent
claims.raw();              // the whole payload, frozen
```

缓存JWKS；在未知的地方 `kid` 在令牌被拒绝之前，它被再次获取一次。 `client.jwksClient()` 返回底层 `JwksClient` 如果你直接需要的话。

* * *

## 管理API

`AdminClient` 包装了 `/admin/*` 端点。的承载令牌构造它 `hearth.admin` 许可。空参数抛出 `ConfigurationError`.

```typescript
import { AdminClient } from "@hearth-auth/sdk";

const admin = new AdminClient("https://hearth.example.com", "<realm-id>", accessToken);
```

每个list方法 `{ limit?, cursor? }` 并返回 `{ items, next_cursor }`。通过 `next_cursor` 在 `cursor` 直到它是 `null`。每个非2xx响应都会抛出 `HearthError` 与 `status` 和 `body` （解析JSON，或者正文不是JSON时的原始文本）。

### 用户

```typescript
// Create a user
const user = await admin.createUser({
  email: "alice@example.com",
  displayName: "Alice",
});

// List users (paginated)
const page = await admin.listUsers({ limit: 50 });
// page.items: User[], page.next_cursor: string | null

// Get a user by ID
const user = await admin.getUser("<user-id>");

// Update a user
const updated = await admin.updateUser("<user-id>", {
  displayName: "Alice Smith",
  status: "USER_STATUS_ACTIVE", // the proto enum name; "active" is refused
});

// Delete a user
await admin.deleteUser("<user-id>");
```

### 领域

```typescript
// Realms are provisioned via hearth.yaml, not the admin API — there is no
// createRealm() or updateRealm() (the server returns 405).

// List realms (paginated)
const page = await admin.listRealms({ limit: 20 });

// Get a realm by ID
const realm = await admin.getRealm("<realm-id>");

// Delete a realm (cascades users, sessions, clients, assignments)
await admin.deleteRealm("<realm-id>");
```

### 客户端、角色和组

`createClient`， `getClient`， `updateClient`， `regenerateClientSecret`， `deleteClient`， `listClients`角色和组的create/get/update/delete/list设置相同。

### 组织

```typescript
// Create an organization (slug is immutable after creation)
const org = await admin.createOrganization({
  slug: "acme",
  display_name: "Acme Corp",
  mfa_required: true, // members need MFA even where the realm does not
});

// List, get, update, delete
const page = await admin.listOrganizations({ limit: 50 });
const same = await admin.getOrganization(org.id);
await admin.updateOrganization(org.id, { status: "suspended" });
await admin.deleteOrganization(org.id);

// Extra org roles of one member (the user must already be a member)
await admin.addMemberRole(org.id, "<user-id>", "billing");
const roles = await admin.listMemberRoles(org.id, "<user-id>"); // ["billing"]
await admin.removeMemberRole(org.id, "<user-id>", "billing");
```

`Organization`， `CreateOrganizationParams` 和 `UpdateOrganizationParams` 是导出的类型。

### 生成客户端

路由和路径参数来自于从灶台的OpenAPI文档(`src/generated/admin/schema.ts`,通过 `openapi-typescript`我打电话过去 `openapi-fetch`）. 用 `make sdk-admin-gen` 从存储库根；永远不要手工编辑。

* * *

## 错误处理

SDK本身引发的错误会扩展 `HearthSdkError`：

| 错误 | 当 |
| --- | --- |
| `ConfigurationError` | 缺少必要的设置(`clientId`， `realmId`（发现端点）或 `audience` 是空的 |
| `DiscoveryError` | 无法获取发现文档或发现文档无效 |
| `JWKSFetchError` | 无法获取JWKS |
| `TokenVerificationError` | 下面每个令牌失败的基类 |
| `TokenExpiredError`， `TokenNotYetValidError` | `exp` / `nbf` 或者 `iat` 在时钟倾斜的窗外 |
| `TokenInvalidError` | 错误的签名，错误的算法，畸形的JWT |
| `TokenIssuerError`， `TokenAudienceError` | `iss` /不匹配 `aud` 不包含配置的 `audience` |
| `IntrospectionError` | 自省请求失败或返回非json |
| `OAuthFlowError` | 令牌、用户信息、权限或会话版本请求失败(`statusCode`， `errorCode`） |
| `AuthorizationModeMismatchError` | 内省回应了一种模式 `expectedMode` |
| `RequiredActionError`， `SessionVersionRevokedError`， `SessionVersionCacheStaleError` | 查看他们的文档评论 |

错误消息中的任何jwt形状的字符串都被替换为 `[redacted]`，因此记录错误并不记录令牌。

`AdminClient` 和 `HearthApiClient` 扔 `HearthError` 关于非2xx的回应： `status` 为HTTP状态码， `body` 解析后的JSON（或原始文本）。

```typescript
import { HearthError, OAuthFlowError, TokenVerificationError } from "@hearth-auth/sdk";

try {
  await client.verifyToken(token);
} catch (err) {
  if (err instanceof TokenVerificationError) return res.status(401).end();
  throw err;
}
```

* * *

## 开发引导（仅限开发）

引导端点创建领域、管理用户、会话、分配 `realm.admin` 角色，并返回令牌。它只在灶台运行时可用 `--dev`。在生产环境中，它返回404。

```typescript
import { AdminClient, HearthApiClient } from "@hearth-auth/sdk";

const { realm_id, user_id, access_token, refresh_token } =
  await HearthApiClient.bootstrap("http://127.0.0.1:8420");

// Use realm_id and access_token to make subsequent requests
const admin = new AdminClient("http://127.0.0.1:8420", realm_id, access_token);
```

* * *

## 引用类型

```typescript
// HearthClientConfig — constructor argument for HearthClient
interface HearthClientConfig {
  issuerUrl: string;            // e.g. "https://hearth.example.com"; endpoints are discovered from it
  clientId?: string;            // needed for login flows, introspection
  audience?: string;            // expected `aud` on verifyToken; default "hearth"; always checked
  clientSecret?: string;        // confidential clients only
  realmId?: string;             // sent as X-Realm-ID; needed by authorize, mePermissions, sv feed, magic link
  httpTimeout?: number;         // ms, default 10 000
  jwksTtl?: number;             // ms, default 5 minutes
  introspectionEndpoint?: string;
  expectedMode?: "embedded" | "introspection" | "decision";
}

// HearthOptions — argument to createHearth()
interface HearthOptions {
  baseUrl: string;
  realmId: string;
  issuerUrl: string;            // the realm issuer, e.g. "https://hearth.example.com/realms/acme"
  audience?: string;            // expected `aud`; default "hearth"
  // called on every predicate check
  getToken: () => string | null | undefined | Promise<string | null | undefined>;
  sessionVersions?: SessionVersionConfig;
}

// HearthFacade — returned by createHearth(); each predicate verifies the token first
interface HearthFacade {
  hasPermission(permission: string): Promise<boolean>;
  hasRole(role: string): Promise<boolean>;
  inGroup(group: string): Promise<boolean>;
  inOrg(org: string): Promise<boolean>;
  sessionVersionCacheAge(): number;
  stop(): void;
  client: { permissions(): Promise<MePermissionsResponse> };
}

// AuthConfig — second argument to createHearthAuth(client, config)
interface AuthConfig {
  clientId: string;
  redirectUri: string;
  hearthUrl: string;            // e.g. "https://hearth.example.com"
  realmSlug: string;            // e.g. "acme"
  storage?: "sessionStorage" | "localStorage" | "memory" | TokenStorage; // default "sessionStorage"
  storageKeyPrefix?: string;    // default "hearth_"
}

// HearthBrowserAuth — returned by createHearthAuth()
interface HearthBrowserAuth {
  startLogin(): Promise<void>;
  handleCallback(code: string, state: string): Promise<void>;
  refreshAccessToken(): Promise<void>;
  logout(): Promise<void>;
  getAccessToken(): string | null; // held in memory
  getRefreshToken(): string | null;
  getIdToken(): string | null;
  isAuthenticated(): boolean;
  clearTokens(): void;
}

// AuthorizeParams
interface AuthorizeParams {
  clientId: string;
  redirectUri: string;
  scope: string;
  state: string;
  userId: string;
  responseType?: string;       // default: "code"
  codeChallenge?: string;      // S256 challenge; required for PKCE
  codeChallengeMethod?: string; // "S256"
  nonce?: string;              // echoed in the ID token
}

// TokenExchangeParams
interface TokenExchangeParams {
  clientId: string;
  code: string;
  redirectUri: string;
  codeVerifier?: string; // required when codeChallenge was sent on authorize
}

// TokenResponse
interface TokenResponse {
  access_token: string;
  token_type: string;      // "Bearer"
  expires_in: number;      // seconds
  refresh_token?: string;  // absent for client credentials
  id_token?: string;       // present when `openid` was granted
  scope?: string;
}

// LoginBeginResult — returned by beginLogin()
interface LoginBeginResult {
  authorizationUrl: string;
  state: string;
  codeVerifier: string;
}

// UserInfoResponse
interface UserInfoResponse {
  sub: string;
  name?: string;
  email?: string;
  email_verified?: boolean;
  preferred_username?: string;
  [claim: string]: unknown;
}

// MePermissionsResponse — from GET /v1/me/permissions
interface MePermissionsResponse {
  roles: string[];
  groups: string[];
  permissions: string[];
  scope: string;
}

// User
interface User {
  id: string;
  email: string;
  display_name: string;
  status: string;
  created_at?: number; // Unix epoch seconds
  updated_at?: number;
}

// Realm
interface Realm {
  id: string;
  name: string;
  status: string;
  config: Record<string, unknown> | null;
  created_at?: number;
  updated_at?: number;
}

// OAuthClient — returned by registerClient()
interface OAuthClient {
  client_id: string;
  client_name: string;
  redirect_uris: string[];
  grant_types: string[];
  created_at?: number;
}

// PageResponse<T> — paginated list
interface PageResponse<T> {
  items: T[];
  next_cursor: string | null; // pass as cursor on the next request, or null if last page
}

// HearthError
class HearthError extends Error {
  status: number;   // HTTP status code
  body: unknown;    // parsed JSON error body
}
```

## 故障排除

**`DiscoveryError`** ——验证 `issuerUrl` 是可到达的，并返回一个有效的 `/.well-known/openid-configuration`.

**`JWKSFetchError`** -检查到JWKS端点的网络连接。在返回此错误之前，SDK会在缓存丢失时重试一次。

**`TokenExpiredError`** -令牌的 `exp` 索赔已成为过去。刷新令牌或重新验证。

**`TokenInvalidError`** —JWT签名不匹配JWKS中的任何密钥。如果服务器最近轮换了密钥，SDK将自动重新获取一次；持续失败表示键不匹配。

**`TokenAudienceError`** -令牌的 `aud` 声明中不包含配置的 `audience` (默认 `"hearth"`）. 集 `audience` 到发出令牌的资源URI；客户端ID不是受众。

**`AuthorizationModeMismatchError`** ——服务器回应道 `access_token_authorization` 模式 这与SDK不同 `expectedMode` 配置或 `mode` 传递给 `requirePermission`. 验证 `OAuthClient` admin设置匹配资源服务器的SDK配置。

看到 [openspec /规格/ sdk-support-contract / spec.md](https://npmcn.com/package/openspec/specs/sdk-support-contract/spec.md) 第5节给出了完整的错误分类。

* * *

## 权限下发方式（HEA-922/923）

灶台支持将RBAC数据传递到资源服务器的三种模式。选一个 注册OAuth客户端；SDK验证您保持一致。

### 嵌入式(默认)

RBAC申索(`permissions`， `roles`， `groups`)在发布时嵌入JWT中。这个 检查器首先验证令牌——EdDSA签名与领域的JWKS相对应 `exp`， `nbf`， `iat`， `iss` 和 `aud` ——然后再读声明。的JWKS 缓存，因此在第一个请求之后，每次检查都没有网络流量。

不验证返回的令牌 `false`;检查者从不相信未经验证的人 有效载荷。

```typescript
import { HearthClient, requirePermission } from "@hearth-auth/sdk";

const client = new HearthClient({
  issuerUrl: "https://auth.example.com",
  audience: "https://api.example.com", // expected `aud`; default "hearth"
});

const check = requirePermission("docs.write", { mode: "embedded", client });

// Verifies the token, then reads its permissions claim.
const allowed = await check(accessToken);
```

> **注意安全。** 在v1.1之前，这个检查器调用 `decodeJwt` 相信一切 载荷说，所以 `alg: none` 令牌携带 `permissions: ["admin.write"]` 是 接受。如果你用的是旧版本，那就升级。

### 决策（每个请求服务器检查）

JWT只携带身份声明。SDK调用 `POST /oauth/authorize` 在每一张支票上。 失败关闭：任何网络或服务器错误返回 `false`.

```typescript
import { HearthClient, requirePermission } from "@hearth-auth/sdk";

const client = new HearthClient({
  issuerUrl: "https://auth.example.com",
  realmId: "<realm-id>",
});

// Low-level: call authorize() directly
const allowed = await client.authorize(accessToken, "docs.write");

// Middleware factory
const check = requirePermission("docs.write", { mode: "decision", client });
const allowed2 = await check(accessToken);
```

### 内省（通过/内省实时RBAC）

JWT只携带身份声明。SDK调用 `POST /introspect` 并读取实时RBAC 响应。抛出 `AuthorizationModeMismatchError` 当服务器响应一个模式时 与中间件所期望的不同。

```typescript
import { HearthClient, requirePermission } from "@hearth-auth/sdk";

const client = new HearthClient({
  issuerUrl: "https://auth.example.com",
  clientId: "<client-id>",
  clientSecret: "<client-secret>",
  // optional: validate the server echoes the expected mode
  expectedMode: "introspection",
});

const check = requirePermission("docs.write", { mode: "introspection", client });
const allowed = await check(accessToken);
```

> **设计约束**SDK绝对不能从一种模式切换到另一种模式 是否 `permissions` 存在于JWT中。这个 `mode` 必须始终显式设置。 缺少 `permissions` 声称在 `embedded` 模式表示用户没有权限，而不是 SDK应该尝试网络调用。

* * *

## 服务器中间件（Express和fasttify）

`hearthMiddleware` 和 `hearthFastifyHook` 验证每个请求上的承载令牌，应用可选的范围、角色和权限保护，并附加已验证的 `Claims`。两者都有相同的选择：

| 选项 | 意义 |
| --- | --- |
| `client` | 这个 `HearthClient` 用…验证为每个进程创建一个缓存，以便共享JWKS缓存。 |
| `mode` | `"embedded"`， `"introspection"` 或者 `"decision"`。默认值: `client.expectedMode`,然后 `"embedded"`. |
| `required` | 默认的 `true`。当 `false`，没有令牌或未验证令牌的请求将在没有声明的情况下通过。 |
| `requiredScope`， `requiredRole` | 在每个模式下根据已验证的JWT进行检查。 |
| `requiredPermission` | 检查每 `mode` (见下文)。 |
| `organizationId`， `resource` | 随决策模式发送 `POST /oauth/authorize` 呼叫。 |

```typescript
import express from "express";
import { HearthClient, hearthMiddleware } from "@hearth-auth/sdk";

const client = new HearthClient({ issuerUrl: "https://hearth.example.com" });
const app = express();

app.get("/docs", hearthMiddleware({ client, requiredPermission: "docs.read" }), (req, res) => {
  res.json({ sub: req.hearthClaims!.subject() });
});
```

```typescript
import Fastify from "fastify";
import { hearthFastifyHook } from "@hearth-auth/sdk";

const app = Fastify();
app.addHook("onRequest", hearthFastifyHook({ client, requiredRole: "editor" }));
// request.hearthClaims is set in route handlers
```

回复:

| 情况 | 状态 |
| --- | --- |
| 没有不记名令牌 `required`)，或者令牌不进行验证 | 401 |
| `token_type` 是 `required_action` (即使 `required` 是 `false`） | 401 |
| 自省模式：令牌不再活动 | 401 |
| 缺少范围、角色或权限；决策模式否定；自省失败或响应其他模式 | 403 |

每个401都有 `WWW-Authenticate: Bearer realm="hearth"`。正文是JSON格式： `{ "error": "unauthorized" | "forbidden", "error_description": "..." }`.

如何 `requiredPermission` 检查:

-   **嵌入式** ——摘自 `permissions` 已验证的JWT的声明。没有网络呼叫。没有声明的令牌没有权限；中间件永远不会退回到另一种模式。
-   **自省** ——来自现场 `permissions` 返回的 `POST /introspect`。需要 `clientId` 和 `clientSecret` 在客户端。
-   **决定** - - - - - - `POST /oauth/authorize` 决定;JWT声明被忽略。需要 `realmId` 在客户端。

不能服务于该模式的客户端使工厂抛出 `ConfigurationError` 在启动时，而不是在第一个请求时。

对于另一个框架，调用 `authenticateRequest(authorizationHeader, options)`． 它返回 `{ ok: true, claims }` 或者 `{ ok: false, status, headers, body }` 寄给你。

* * *

## Next.js

### 路由器API路由

```typescript
// pages/api/profile.ts
import { withHearthAuth } from "@hearth-auth/sdk/nextjs";
import { hearth } from "../../lib/hearth"; // a module-scope HearthClient

export default withHearthAuth(
  (req, res) => {
    res.json({ sub: req.hearthClaims!.subject() });
  },
  { client: hearth, requiredPermission: "profile.read" },
);
```

`withHearthAuth` 的选项相同 `hearthMiddleware`。在401或403上，不会调用处理程序。

### 应用路由器路由处理程序

```typescript
// app/api/profile/route.ts
import { NextResponse } from "next/server";
import { getHearthClaims } from "@hearth-auth/sdk/nextjs";
import { hearth } from "@/lib/hearth";

export async function GET(request: Request) {
  const claims = await getHearthClaims(request, hearth);
  if (!claims) return NextResponse.json({ error: "unauthorized" }, { status: 401 });
  return NextResponse.json({ sub: claims.subject() });
}
```

`getHearthClaims` 返回 `null` 当没有承载令牌时，令牌不进行验证，或者它是一个 `required_action` 令牌。

### `middleware.ts` (边缘运行时)

```typescript
// middleware.ts
import { NextResponse, type NextRequest } from "next/server";
import { HearthClient } from "@hearth-auth/sdk";
import { hearthEdgeMiddleware } from "@hearth-auth/sdk/nextjs/edge";

const guard = hearthEdgeMiddleware({
  client: new HearthClient({ issuerUrl: process.env.HEARTH_ISSUER_URL! }),
  requiredScope: "api",
});

export async function middleware(request: NextRequest) {
  return (await guard(request)) ?? NextResponse.next();
}

export const config = { matcher: ["/api/:path*"] };
```

卫兵决定 `undefined` 当请求可以继续时，或转换为401/403 JSON `Response`。它只使用 `fetch` 和Web Crypto，所以它在Edge Runtime上运行。在模块范围内创建它，以便发现文档和JWKS在隔离的生命周期内保持缓存。

* * *

## Agent鉴权（M5）

灶台通过一组REST端点和OAuth扩展支持AI代理身份和授权。使与 `agent_auth.capabilities.identity = true` (加上 `advanced = true` （aat和交易令牌） `hearth.yaml`.

### Agent CRUD + API密钥

```typescript
const client = new HearthClient({ baseUrl, realmId });

// Create an agent
const agent = await client.post("/v1/agents", {
  realm_id: realmId,
  display_name: "my-agent",
  capabilities: ["urn:hearth:capability:docs:read"],
});

// Issue an API key (long-lived bearer token for the agent)
const { api_key } = await client.post(`/v1/agents/${agent.agent_id}/credentials/keys`, {
  description: "production key",
});
```

### dpop绑定令牌（RFC 9449）

将访问令牌绑定到EC密钥对，使其不能被令牌窃贼重放：

```typescript
import { generateKeyPairSync, sign, createHash, randomUUID } from "node:crypto";

const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
const pub = publicKey.export({ format: "jwk" });

// JWK thumbprint per RFC 7638 (lex-sorted required members)
const canonical = JSON.stringify({ crv: pub.crv, kty: pub.kty, x: pub.x, y: pub.y });
const thumbprint = createHash("sha256").update(canonical).digest("base64url");

function makeDPopProof(htm: string, htu: string, nonce?: string): string {
  const header = { alg: "ES256", jwk: { crv: "EC", kty: "EC", x: pub.x, y: pub.y }, typ: "dpop+jwt" };
  const claims: Record<string, unknown> = {
    htm, htu, iat: Math.floor(Date.now() / 1000), jti: randomUUID(),
  };
  if (nonce) claims.nonce = nonce;
  const b64u = (v: unknown) => Buffer.from(JSON.stringify(v)).toString("base64url");
  const input = `${b64u(header)}.${b64u(claims)}`;
  const sig = sign("SHA256", Buffer.from(input), { key: privateKey, dsaEncoding: "ieee-p1363" });
  return `${input}.${sig.toString("base64url")}`;
}

// 1st request — server always returns DPoP-Nonce
const resp1 = await fetch(tokenUrl, { method: "POST", headers: { DPoP: makeDPopProof("POST", tokenUrl) }, body });
const nonce = resp1.headers.get("dpop-nonce")!;

// 2nd request — include nonce; receive AT with cnf.jkt binding
const resp2 = await fetch(tokenUrl, { method: "POST", headers: { DPoP: makeDPopProof("POST", tokenUrl, nonce) }, body });
const { access_token } = await resp2.json();
// Decoded AT claims will contain: cnf: { jkt: "<thumbprint>" }
```

### RFC 8693令牌交换（OBO / act chain）

```typescript
const body = new URLSearchParams({
  grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
  subject_token: subjectToken,
  subject_token_type: "urn:ietf:params:oauth:token-type:access_token",
  requested_token_type: "urn:ietf:params:oauth:token-type:access_token",
  scope: "openid",
});
const resp = await fetch(`${baseUrl}/token`, { method: "POST", body, headers: { Authorization: `Basic ${creds}` } });
const { access_token } = await resp.json();
// Exchanged token contains: act: { sub: "<actor-client-id>" }  (RFC 8693 §4.1)
```

### 衰减授权令牌- AATs（阶段D）

```typescript
// Issue a root AAT for an agent
const rootAat = await client.post("/v1/aats", {
  realm_id: realmId,
  agent_id: agentId,
  tools: [
    { tool_name: "read_docs", constraints: null },
    { tool_name: "search_files", constraints: null },
  ],
  expires_in_secs: 3600,
});

// Derive a child AAT with narrowed scope (child tools ⊆ parent tools)
const childAat = await client.post("/v1/aats/derive", {
  realm_id: realmId,
  parent_token: rootAat.token,
  tools: [{ tool_name: "read_docs", constraints: null }],
  expires_in_secs: 300,
});
```

### 交易令牌（单次使用A2A， 60s TTL）

```typescript
// Issue a single-use transaction token binding agent-a → agent-b
const txn = await client.post("/v1/transaction-tokens", {
  realm_id: realmId,
  requesting_agent_id: agentAId,
  target_agent_id: agentBId,
  txn_id: `txn-${crypto.randomUUID()}`,
});

// Consume (single-use — second call returns 409)
await client.post("/v1/transaction-tokens/consume", {
  realm_id: realmId,
  token: txn.token,
});
```

### 标准草案的跟踪

下面的IETF草案支持代理认证表面。复核汇票垫款的指定业主为 **[@therecluse26](https://github.com/therecluse26)** (首席技术官)。当草案提交到RFC或新修订发布时，在 [他叫- 1409](https://npmcn.com/HEA/issues/HEA-1409).

| 草案 | 炉特性 | 检查时 |
| --- | --- | --- |
| `draft-oauth-ai-agents-on-behalf-of-user-02` | 鄂博 `on_behalf_of` 索赔 | 新修订或RFC发布 |
| `draft-niyikiza-oauth-attenuating-agent-tokens` | AAT发动机(`/v1/aats`） | 新修订或RFC发布 |
| `draft-oauth-transaction-tokens-for-agents` | 交易令牌(`/v1/transaction-tokens`） | 新修订或RFC发布 |
| `draft-prakash-aip` | 代理身份模型，代理卡 | 新修订或RFC发布 |
| OpenID社保基金/制定 | DPoP JKT黑名单+风险信号 | 当CAEP SSF规格稳定时 |

## 常见问题

### @hearth-auth/sdk 是什么？

的TypeScript客户端 炉 身份的API。

### 如何安装 @hearth-auth/sdk？

在项目目录运行 npm install @hearth-auth/sdk，或使用 pnpm add / yarn add / bun add。

### @hearth-auth/sdk 支持 TypeScript 吗？

支持。包内自带 TypeScript 类型声明（package\.json 声明了 types 入口），安装后即可获得类型提示，无需再安装 @types 包。

### @hearth-auth/sdk 支持 ESM 还是 CommonJS？

以 ES Module 形式发布，推荐使用 import 引入。在 CommonJS 项目中，Node\.js 20\.19\+ / 22\.12\+ 可以直接 require\(\) 不含顶层 await 的 ES Module，更早的版本需要改用动态 import\(\)。

### @hearth-auth/sdk 可以商用吗？

@hearth-auth/sdk 使用 未声明 许可。package\.json 没有声明许可证。没有许可证的代码默认保留全部权利，商用前请联系作者或查看仓库中的 LICENSE 文件。以上为概括，不构成法律意见，请以许可原文为准。

### @hearth-auth/sdk 还在维护吗？

最新版本 4\.6\.4 发布于 2026/11/01（2 天前）；至今共发布 105 个版本，其中过去 12 个月发布了 105 个；首次发布于 2026/06/23。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

