@hearth-auth/sdk

中文文档
4.6.4发布于 2026/11/01TypeScriptES Module构建来源已验证

的TypeScript客户端 炉 身份的API。

中文使用文档
文档目录43 节
  1. 安装
  2. 快速启动
  3. 观众
  4. 服务器端登录(使用PKCE的授权码)
  5. 刷新令牌
  6. 其他补助
  7. 浏览器应用程序
  8. RBAC的能力
  9. hasPermission(permission: string): Promise<boolean>
  10. hasRole(role: string): Promise<boolean>
  11. inGroup(group: string): Promise<boolean>
  12. inOrg(org: string): Promise<boolean>
  13. client.permissions(): Promise<MePermissionsResponse>
  14. 反应集成
  15. UserInfo和live权限
  16. Session-version饲料
  17. JWKS和发现
  18. 管理API
  19. 用户
  20. 领域
  21. 客户端、角色和组
  22. 组织
  23. 生成客户端
  24. 错误处理
  25. 开发引导(仅限开发)
  26. 引用类型
  27. 故障排除
  28. 权限下发方式(HEA-922/923)
  29. 嵌入式(默认)
  30. 决策(每个请求服务器检查)
  31. 内省(通过/内省实时RBAC)
  32. 服务器中间件(Express和fasttify)
  33. Next.js
  34. 路由器API路由
  35. 应用路由器路由处理程序
  36. middleware.ts (边缘运行时)
  37. Agent鉴权(M5)
  38. Agent CRUD + API密钥
  39. dpop绑定令牌(RFC 9449)
  40. RFC 8693令牌交换(OBO / act chain)
  41. 衰减授权令牌- AATs(阶段D)
  42. 交易令牌(单次使用A2A, 60s TTL)
  43. 标准草案的跟踪

灶台TypeScript SDK

的TypeScript客户端 炉 身份的API。

SDK规范: 此SDK必须符合 灶台SDK通用规范.

安装

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/sdkHearthClient,令牌验证,OAuth流,Express/ fasttify中间件,管理客户端,React钩子,浏览器验证
@hearth-auth/sdk/nextjswithHearthAuth (Pages Router)和 getHearthClaims (应用路由器路由处理程序)
@hearth-auth/sdk/nextjs/edgehearthEdgeMiddleware 用于 middleware.ts 在边缘运行时

快速启动

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.

// 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。

刷新令牌

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.

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.

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保护。

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

hasRole(role: string): Promise<boolean>

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

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

inGroup(group: string): Promise<boolean>

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

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

inOrg(org: string): Promise<boolean>

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

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发布以来所做的任何角色/组分配。

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

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


反应集成

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

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.

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.

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

Session-version饲料

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

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和发现

// 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.

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时的原始文本)。

用户

// Create a user
const user = await admin.createUser({
  email: "[email protected]",
  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>");

领域

// 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设置相同。

组织

// 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, TokenNotYetValidErrorexp / nbf 或者 iat 在时钟倾斜的窗外
TokenInvalidError错误的签名,错误的算法,畸形的JWT
TokenIssuerError, TokenAudienceErroriss /不匹配 aud 不包含配置的 audience
IntrospectionError自省请求失败或返回非json
OAuthFlowError令牌、用户信息、权限或会话版本请求失败(statusCode, errorCode)
AuthorizationModeMismatchError内省回应了一种模式 expectedMode
RequiredActionError, SessionVersionRevokedError, SessionVersionCacheStaleError查看他们的文档评论

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

AdminClient 和 HearthApiClient 扔 HearthError 关于非2xx的回应: status 为HTTP状态码, body 解析后的JSON(或原始文本)。

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。

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);

引用类型

// 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 第5节给出了完整的错误分类。


权限下发方式(HEA-922/923)

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

嵌入式(默认)

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

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

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.

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 当服务器响应一个模式时 与中间件所期望的不同。

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 呼叫。
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() });
});
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路由

// 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上,不会调用处理程序。

应用路由器路由处理程序

// 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 (边缘运行时)

// 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密钥

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密钥对,使其不能被令牌窃贼重放:

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)

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)

// 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)

// 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 (首席技术官)。当草案提交到RFC或新修订发布时,在 他叫- 1409.

草案炉特性检查时
draft-oauth-ai-agents-on-behalf-of-user-02鄂博 on_behalf_of 索赔新修订或RFC发布
draft-niyikiza-oauth-attenuating-agent-tokensAAT发动机(/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 共发布 105 个版本,首次发布于 2026/06/23;过去 12 个月发布了 105 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
4.6.4latest2026/11/012 天前
4.6.32026/11/012 天前
4.6.22026/11/012 天前
4.6.12026/10/312 天前
4.6.02026/10/313 天前
4.5.182026/10/313 天前

关于 @hearth-auth/sdk 的常见问题

@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 和提交记录判断。