# @atlasauth/backend 中文文档

规范地址：https://npmcn.com/package/%40atlasauth/backend/

版本：0\.55\.0
npm 元数据更新时间：2026-11-15T17:55:38.887Z
中文译文更新时间：2026-11-15T18:55:24.575Z
软件包许可：未声明

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

原始 npm 页面：https://www.npmjs.com/package/%40atlasauth/backend
原文对照：https://npmcn.com/package/%40atlasauth/backend/?lang=original
项目主页：https://github.com/alphajew420/ssoly/tree/main/packages/backend#readme
源码仓库：https://github.com/alphajew420/ssoly

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

- 包体积（解压后）：4.4 MB，838 个文件
- 模块格式：CommonJS
- TypeScript 类型：包内自带
- 运行依赖：5 个
- peer 依赖：@atlasauth/pca ^0\.4\.3，@opentelemetry/api ^1\.9\.0
- 首次发布：2026-09-13T21:19:11.569Z
- 最新版本发布：2026-11-15T17:55:38.689Z
- 近 30 天下载量：7536（2026-10-07 至 2026-11-05，npm 官方下载统计）
- 已发布版本数：71（近 12 个月 71 个）

## 中文简介

用于节点、德诺、邦和边缘运行时间的服务器侧图集。 它有三样东西:

## 安装

```sh
npm install @atlasauth/backend
```

也可以使用：`pnpm add @atlasauth/backend`、`yarn add @atlasauth/backend`、`bun add @atlasauth/backend`

## README

# @ atlasauth/ 后端

用于节点、德诺、邦和边缘运行时间的服务器侧图集。 它有三样东西:

-   核实 阿特拉斯会话令牌、webhooks和后通道登录令牌
-   运行主机- 直接( OpenID Connect) 登录的服务器侧面
-   为后端 API 提供一个输入客户端, 使用您的密钥( W)`sk_...`(中文(简体) )
-   给您一个平台 API 的打字客户端, 使用您的平台密钥( Name`psk_...`(中文(简体) )

什么都不需要,不过 `fetch`。 。 。 .

```sh
npm install @atlasauth/backend
```

## 校验会话

```ts
import { AtlasBackend } from '@atlasauth/backend';

const atlas = new AtlasBackend({ issuer: 'https://clerk.example.com' });
const result = await atlas.verify(sessionToken);
if (result.ok) console.log(result.claims.sub);
```

JWKS是从发行商找到并被缓存的. 共享主机上的例子也通过 `instanceId` (或 `publishableKey`) (中文(简体) ).

## 用重定向( OpenID 连接) 签名

`oidc` 运行主机-重定向集成的依赖方侧:状态、非ce和PKCE、流程饼干、代码交换和ID-token检查(仅RS256, `iss`, (中文(简体) ), `aud` =你的客户身份, `nonce`, (中文(简体) ), `exp`) (中文(简体) ).

```ts
import { oidc, AtlasOidcError } from '@atlasauth/backend';

const config = {
  issuer: 'https://bright-fox-42-fapi.atlasauth.net', // https:// + frontend_api_host
  clientId: process.env.ATLAS_CLIENT_ID!,
  redirectUri: 'https://app.example.com/auth/callback',
  secret: process.env.OIDC_COOKIE_SECRET!, // 32+ random characters; seals the flow cookie
};

// Start: set the flow cookie, redirect.
const { url, cookie } = await oidc.start({ ...config, scope: 'openid profile email' });

// Callback: one call does every check.
const identity = await oidc.callback({
  ...config,
  clientSecret: process.env.ATLAS_CLIENT_SECRET!,
  query: request.url, // or URLSearchParams, or req.query
  cookie: request.headers.get('cookie'),
});
// identity: { sub, namespacedSub, email, emailVerified, name, idToken, accessToken, refreshToken?, expiresAt, claims, clearCookie }

// Sign out: end your session, then end the Atlas one.
const logout = await oidc.logoutUrl({ ...config, postLogoutRedirectUri: 'https://app.example.com/', idTokenHint });
```

-   流饼干是 `httpOnly`, (中文(简体) ), `SameSite=Lax`, (中文(简体) ), `Secure` (a项除外) `http://localhost` ),被瞄准回调路径,持续10分钟. 与 `secret` 它被加密(AES-GCM),所以不能被读取或被伪造. 无 `secret` 它只是编码。 总是通过一个生产.
-   `namespacedSub` 实值 `${issuer}#${sub}`。如果您接受不止一个实例,则其上的关键用户,所以两个实例的用户从未碰撞过.
-   `clientAuth` 实值 `client_secret_basic` (以秘密示弃), `client_secret_post`,或 `none` (公共客户端,仅限PKCE).
-   `new AtlasBackend({ issuer }).oidc` 使用相同的方法 `issuer` 填报.
-   你自己的密码交换了吗? `oidc.verifyIdToken(idToken, { issuer, clientId, nonce })` 运行只是象征性的支票.

失败投出 `AtlasOidcError` 拥有稳定 `code`数字 :

| `code` | 含义 |
| --- | --- |
| `state_mismatch` | 没有流饼干,不一样 `state`或者是超过10分钟的水流 再来一次. |
| `access_denied` | Atlas 以 OAuth 重定向返回 `error`。准确值为 `oauthError` (单位:千美元)`access_denied`, (中文(简体) ), `login_required`(中文(简体) ). |
| `issuer_mismatch` | 召回的电话 `iss` 参数名称为不同的服务器. |
| `invalid_request` | 召回没有 `code`。 。 。 . |
| `token_exchange_failed` | 符号端点拒绝代码或客户端( S)`oauthError`, (中文(简体) ), `status`) (中文(简体) ). |
| `invalid_token` | ID 符号没有通过检查: 算法, 签名, `iss`, (中文(简体) ), `aud`编号 :`azp`, (中文(简体) ), `nonce`, (中文(简体) ), `exp`编号 :`iat` 或者说 `at_hash`。 。 。 . |
| `discovery_failed` | 发现文件或签名密钥无法被取出或与发行人不符. |
| `reauthentication_required` | `start` 请求 `maxAge` 和标识符的 `auth_time` 较旧(或缺失). |
| `subject_mismatch` | `start` 请求 `expectedSub` (一个步骤)和一个不同的用户报名. |

每一个错误来自 `callback` 携带 `clearCookie`: 随错误页一起发送.

`start` 还要 `uiLocales`, (中文(简体) ), `maxAge` (回调后执行) `auth_time`, (中文(简体) ), `returnTo` (同一地点路径,返回为 `identity.returnTo`和 `expectedSub`。该身份带有 `sid` (Atlas会议d)和 `authTime`。 。 。 .

### `@atlasauth/backend/rp`: 请求 – 任何运行时间的响应处理器

同样的流量 `(Request) => Response` 处理器,加分级,会话保持动态和退出。 此入口只装入 Web 标准和 `jose`,所以它运行 在节点18+,德诺,邦和工人.

```ts
import { createRelyingParty } from '@atlasauth/backend/rp';

const rp = createRelyingParty({
  issuer: process.env.ATLAS_ISSUER!,
  clientId: process.env.ATLAS_CLIENT_ID!,
  clientSecret: process.env.ATLAS_CLIENT_SECRET!,
  redirectUri: 'https://app.example.com/auth/callback',
  secret: process.env.SESSION_SECRET!,      // 32+ characters
  secretKey: process.env.ATLAS_SECRET_KEY,  // for touchIfDue and server-side revoke
  postLogoutRedirectUri: 'https://app.example.com/',
});

const startResponse = await rp.start(request);                  // 302 to /oauth2/authorize
const { user, tokens, response } = await rp.callback(request);  // user.sub, user.sid, user.auth_time, ...
const stepUp = await rp.requireRecentAuth(request, 300, { sub: user.sub, authTime: user.auth_time }); // null or a 302
const touch = await rp.touchIfDue(user.sid!, lastTouchedAt);    // 'skipped' | 'touched' | 'ended'
const signOut = await rp.signOut({ idToken: tokens.id_token, sid: user.sid });
```

或用一条线将三条路线都接通 `rp.handle(request)` (签名、召回和退出); `null` 对任何其他路径,给予 `onSignedIn(user, tokens, request, result)` 财务报告和审定财务报表 `onSignOut(request)` 在配置中. `forwardParams: ['ref']` 将您登录的 URL 的额外查询参数传递到 `/oauth2/authorize`。 。 。 .

还出口 `LogoutTokenVerifier`, (中文(简体) ), `fetchBackchannelLogout` 财务报告和审定财务报表 `verifyAtlasWebhook` 编号 : `verifyAtlasWebhookRequest` (网易密码).

同一切入点单独公布于 [`@atlasauth/rp`](https://www.npmjs.com/package/@atlasauth/rp),其唯一依赖是 `jose`。 。 。 .

### Next.js (App路由器) (英语)

```ts
// app/auth/login/route.ts
import { oidc } from '@atlasauth/backend';
import { config } from '@/lib/atlas';

export async function GET() {
  const { url, cookie } = await oidc.start(config);
  return new Response(null, { status: 302, headers: { location: url, 'set-cookie': cookie } });
}

// app/auth/callback/route.ts
import { oidc, AtlasOidcError } from '@atlasauth/backend';
import { config } from '@/lib/atlas';

export async function GET(request: Request) {
  try {
    const user = await oidc.callback({
      ...config,
      clientSecret: process.env.ATLAS_CLIENT_SECRET!,
      query: request.url,
      cookie: request.headers.get('cookie'),
    });
    const headers = new Headers({ location: '/' });
    headers.append('set-cookie', user.clearCookie);
    headers.append('set-cookie', await createAppSession(user)); // your own session cookie
    return new Response(null, { status: 302, headers });
  } catch (error) {
    if (!(error instanceof AtlasOidcError)) throw error;
    return new Response(`Sign-in failed (${error.code})`, {
      status: 400,
      headers: error.clearCookie ? { 'set-cookie': error.clearCookie } : {},
    });
  }
}
```

### 快递

```ts
import express from 'express';
import { oidc, AtlasOidcError } from '@atlasauth/backend';

const app = express();

app.get('/auth/login', async (req, res) => {
  const { url, cookie } = await oidc.start(config);
  res.append('Set-Cookie', cookie).redirect(url);
});

app.get('/auth/callback', async (req, res) => {
  try {
    const user = await oidc.callback({
      ...config,
      clientSecret: process.env.ATLAS_CLIENT_SECRET!,
      query: req.originalUrl,
      cookie: req.headers.cookie,
    });
    res.append('Set-Cookie', user.clearCookie);
    await startYourSession(req, res, user.namespacedSub);
    res.redirect('/');
  } catch (error) {
    if (!(error instanceof AtlasOidcError)) throw error;
    if (error.clearCookie) res.append('Set-Cookie', error.clearCookie);
    res.status(400).send(`Sign-in failed (${error.code})`);
  }
});
```

## 测试离线

`createTestIssuer()` 薄荷会话令牌和带有丢出密钥的ID令牌及其 `fetch` 服务于发现, JWKS 和一个符号端点, 因此一个完全的重定向的登录在没有网络的单元测试中运行 .

```ts
import { createTestIssuer, oidc, TEST_TOKEN_TAMPERS } from '@atlasauth/backend';

const fx = await createTestIssuer({ clientSecret: 'test-secret' });
const config = { issuer: fx.issuer, clientId: fx.clientId, redirectUri: 'https://app.example.com/auth/callback', secret: '…32+ chars…', fetchImpl: fx.fetch };

const { url, cookie } = await oidc.start(config);
const { callbackUrl } = await fx.authorize(url, { sub: 'user_1', claims: { email: 'a@example.com', email_verified: true } });
const user = await oidc.callback({ ...config, clientSecret: 'test-secret', query: callbackUrl, cookie });

// Negative cases: wrong_aud, wrong_iss, foreign_key, expired, wrong_nonce, alg_none.
for (const tamper of TEST_TOKEN_TAMPERS) {
  const { url, cookie } = await oidc.start(config);
  const { callbackUrl } = await fx.authorize(url, { tamper });
  await expect(oidc.callback({ ...config, clientSecret: 'test-secret', query: callbackUrl, cookie })).rejects.toMatchObject({ code: 'invalid_token' });
}
```

`fx.mintIdToken(claims, { aud, nonce, tamper })` 财务报告和审定财务报表 `fx.mintSession(claims, { tamper })` 薄荷单币. `@atlasauth/testing` 再出口 `createTestIssuer`。 。 。 .

## 管理客户端( 密钥)

```ts
import { createAtlasClient, paginate } from '@atlasauth/backend';

const atlas = createAtlasClient({ secretKey: process.env.ATLAS_SECRET_KEY! });

const user = await atlas.users.create({ email_address: 'ada@example.com' });
for await (const u of paginate(atlas.users.list)) console.log(u.id);
```

每个后端API操作都有方法,按资源分组. 这些团体是:

| 区域 | 命名空间 |
| --- | --- |
| 用户和会话 | `users`, (中文(简体) ), `emailAddresses`, (中文(简体) ), `sessions`, (中文(简体) ), `signInTokens`, (中文(简体) ), `actorTokens` |
| 组织 | `organizations`, (中文(简体) ), `invitations` |
| 作用和授权 | `roles`, (中文(简体) ), `permissions`, (中文(简体) ), `fga`, (中文(简体) ), `openfga` |
| 访问和注册控制 | `waitlist`, (中文(简体) ), `allowlist`, (中文(简体) ), `blocklist`, (中文(简体) ), `domains`, (中文(简体) ), `redirectUrls` |
| 实例设置 | `instance`, (中文(简体) ), `instanceSecurity`, (中文(简体) ), `design`, (中文(简体) ), `branding`, (中文(简体) ), `localizations` |
| 模板和信件 | `emailTemplates`, (中文(简体) ), `smsTemplates`, (中文(简体) ), `messaging`, (中文(简体) ), `notifications` |
| 社会和企业注册 | `oauthProviders`, (中文(简体) ), `connectors`, (中文(简体) ), `ssoConnections`, (中文(简体) ), `ssoOnboarding`, (中文(简体) ), `orgLdapConnections`, (中文(简体) ), `scimTokens`, (中文(简体) ), `scimProvisioning`, (中文(简体) ), `legacyAuthConnection` |
| 证书和标志 | `oauthClients`, (中文(简体) ), `resourceServers`, (中文(简体) ), `jwtTemplates`, (中文(简体) ), `apiKeys`, (中文(简体) ), `machines`, (中文(简体) ), `tokens`, (中文(简体) ), `sessionExchangeApps`, (中文(简体) ), `tickets` |
| 网络选择和审计 | `webhooks`, (中文(简体) ), `auditLogs`, (中文(简体) ), `auditEvents`, (中文(简体) ), `events`, (中文(简体) ), `usage`, (中文(简体) ), `configChangelog`, (中文(简体) ), `logStreams` |
| 警卫 | `attackProtection`, (中文(简体) ), `rateLimitPolicy`, (中文(简体) ), `riskBasedMfa`, (中文(简体) ), `botSignals`, (中文(简体) ), `captcha`, (中文(简体) ), `networkAcls`, (中文(简体) ), `managedWaf`, (中文(简体) ), `radiusClients` |
| 账单和应享权利 | `billing`, (中文(简体) ), `entitlementOverrides`, (中文(简体) ), `organizationSettingsSchema` |
| 隐私 | `dataSubjectRequests`, (中文(简体) ), `importExport` |
| 代理人和整编人员 | `actions`, (中文(简体) ), `pca`, (中文(简体) ), `mcp`, (中文(简体) ), `mcpConnector`, (中文(简体) ), `ltiPlatforms` |
| 开发工具 | `testMode`, (中文(简体) ), `diagnostics` |

### 检查您的设置

```ts
const report = await atlas.instance.preflight();
for (const f of report.findings) console.log(f.severity, f.message, '→', f.fix);
```

`atlas.doctor()` (或 `npx atlas-doctor` 与 `ATLAS_SECRET_KEY` set)检查密钥,时钟 skew对Atlas,后端API,JWKS,服务状态和实例配置,并从未丢出:

```ts
const report = await atlas.doctor();
if (!report.ok) console.error(formatDoctorReport(report));
```

### 退缩和可观察性

429(任何方法)或503(GET、PUT、DELETE、或带有一等钥匙的书写)两次重试,等待 `Retry-After` 或与焦躁的指数后退。 给它 `retry: { maxRetries, baseDelayMs, maxDelayMs }` 或通过 `retry: false`; 当服务器要求等待的时间比 `maxDelayMs`,则 `AtlasApiError` 被抛出 `retryAfterMs` 设置.

`createAtlasClient({ secretKey, otel: true })` 记录每个请求的客户间距+ `atlas.client.requests`, (中文(简体) ), `atlas.client.retries` 财务报告和审定财务报表 `atlas.client.request.duration`通过全球 `@opentelemetry/api` (可选的对等依赖; 未设置, 没有加载) 。 过 `otel: { tracer, meter }` 用你自己的东西.

### 设计登录页面

```ts
import { readFile } from 'node:fs/promises';

const logo = await atlas.design.assets.upload(await readFile('logo.png'), {
  kind: 'logo',
  contentType: 'image/png',
});
await atlas.design.update({
  palette: { primary: '#4f46e5' },
  logo: { assetId: logo.asset_id },
});
// Per-organization override; null removes a setting.
await atlas.design.organizations.update('org_123', { palette: { primary: null } });
```

上传发送带有内容类型的原始图像字节 。 A 类 `Blob` 带来自己的类型.

### API 密钥支出上限

给终端用户 API 每个单位和期间的密钥封顶(`request`, (中文(简体) ), `second`, (中文(简体) ), `minute`, (中文(简体) ), `day`, (中文(简体) ), `month` 或者说 `total`· ; `{ unit: 'requests', amount: 600, period: 'minute' }` 每笔电话费的限额是每笔电话费的分数。 超过上限的借项被拒绝而不记录任何情况;它与 `allowed: false` 而不是扔东西.

```ts
const key = await atlas.apiKeys.create({
  subject_type: 'user',
  subject_id: 'user_123',
  limits: [
    { unit: 'requests', amount: 1000, period: 'day' },
    { unit: 'usd_cents', amount: 5000, period: 'month' },
  ],
});

const debit = await atlas.apiKeys.recordUsage(key.id, {
  unit: 'usd_cents',
  amount: 12,
  idempotency_key: 'req_8f2c', // a retry returns the first outcome without counting again
});
if (!debit.allowed) {
  // debit.code === 'LIMIT_EXCEEDED'; debit.exceeded lists the caps that refused it
}

const { usage } = await atlas.apiKeys.getUsage(key.id); // used / remaining / resets_at per cap
await atlas.apiKeys.update(key.id, { limits: null }); // remove the caps
```

`apiKeys.verify(secret)` 返回密钥 `name`, (中文(简体) ), `scopes`, (中文(简体) ), `effective_scopes` (通过实例范围的影响加以扩大), `constraints`, (中文(简体) ), `limits` 和当前 `usage`。 。 。 .

### 镜像 Atlas 数据输入您的数据库

`createMirror` 遵循更改种子(先是snapshot,然后是命令更改,在您的位置被修剪后自动重拍)并写入内存, Redis, Postgres 或您自己的 Drizzle 或 Prisma 表格(`@atlasauth/backend/mirror/drizzle`, (中文(简体) ), `@atlasauth/backend/mirror/prisma`) (中文(简体) ).

```ts
import { createMirror, memoryMirrorStore } from '@atlasauth/backend';

const mirror = createMirror({ client: atlas, resources: ['user', 'organization'], store: memoryMirrorStore() });
await mirror.sync(); // or mirror.start() to poll
```

### 本地 API 密钥镜像

`createKeyMirror` 保留您自己的表格权威: 薄荷和在阿特拉斯的导入, 对照sha256进行本地验证, 没有网络呼叫, 本地先取消, 并提取从其它处提取的撤销信息 .

```ts
import { createKeyMirror, memoryKeyMirrorStore } from '@atlasauth/backend';

const mirror = createKeyMirror({ store: memoryKeyMirrorStore(), apiKeys: atlas.apiKeys });
const { secret } = await mirror.mint({ subject_type: 'organization', subject_id: 'org_123', scopes: ['calls:write'] });
const result = await mirror.verify(presented, { requiredScopes: ['calls:write'] }); // local, no network call
await mirror.revoke(keyId); // local first, then Atlas
setInterval(() => void mirror.sync(), 15_000); // revocation feed
// await mirror.applyWebhook(event) for verified api_key.* webhooks
```

回来 `KeyMirrorStore` (单位:千美元)`put`, (中文(简体) ), `get`, (中文(简体) ), `getByHash`, (中文(简体) ), `markRevoked`, (中文(简体) ), `getCursor`, (中文(简体) ), `setCursor`)与自制的桌子.

### 会话保存

如果您的应用程序只在某些页面上加载了Atlas的前端SDK,则在其他页面上所花的时间可以算作是无活性. 从后端触摸会话以滑动其不活动窗口 :

```ts
const s = await atlas.sessions.touch(sessionId);
// s.idle_expires_at is the new inactivity deadline; s.extended is false when a touch in the last minute already did it
```

触摸从未延续过会话的绝对到期,也从未恢复过会话的结束( )`409 SESSION_REVOKED`, (中文(简体) ), `SESSION_IDLE_EXPIRED` 或者说 `SESSION_EXPIRED`) (中文(简体) ). 它每分钟写一次 所以每次请求都安全.

### 严格的请求机构

默认情况下, Atlas 忽略了一个实体字段, 端点不接受 。 它列出在 `Atlas-Unknown-Fields` 响应头.

```ts
const atlas = createAtlasClient({
  secretKey,
  strict: true, // refuse unknown fields with 422 UNKNOWN_FIELD instead
  onUnknownFields: ({ method, path, fields }) => console.warn(method, path, 'ignored', fields),
});

// Override for some calls:
await atlas.withOptions({ strict: false }).users.update('user_123', body);
```

### 错误

每次失败的调用 `AtlasApiError`,已经 `status`, (中文(简体) ), `code` 和全文 `errors` 列表中选择一个选项. `AtlasErrorCodes` 名称 :

```ts
import { AtlasApiError, AtlasErrorCodes } from '@atlasauth/backend';

try {
  await atlas.notifications.send(body);
} catch (e) {
  if (e instanceof AtlasApiError && e.hasCode(AtlasErrorCodes.EMAIL_DELIVERY_NOT_CONFIGURED)) {
    // set up an email provider first
  }
}
```

## 平台客户端( 平台密钥)

```ts
import { createAtlasPlatformClient } from '@atlasauth/backend';

const platform = createAtlasPlatformClient({ platformKey: process.env.ATLAS_PLATFORM_KEY! });

const app = await platform.apps.create({ name: 'Acme' });
const signIn = await platform.atlasIdApps.create({
  name: 'Acme',
  redirect_uris: ['https://acme.example/callback'],
  post_logout_redirect_uris: ['https://acme.example/'],
  backchannel_logout_uri: 'https://acme.example/backchannel-logout',
  branding: { theme: 'dark', accent_color: '#4f46e5' },
});
console.log(signIn.client_id, signIn.client_secret); // the secret is shown once
```

平台客户端有这些命名空间:

-   `apps`: 创建、更新、存档、传输、主张和删除应用程序、管理其实例、旋转和列出机密
-   `accountlessApps`
-   `atlasIdApps`: 与 Atlas 应用签名,包括设计预览和秘密旋转
-   `designAssets`

它接受同样的 `strict`, (中文(简体) ), `onUnknownFields` 财务报告和审定财务报表 `withOptions` 设置.

### 域名移动、 登出扇并管理电子邮件

```ts
// Move every URL from the old origin to the new one. dry_run first shows the diff.
const preview = await atlas.instance.moveOrigin({ from: 'https://old.example.com', to: 'https://example.com', dry_run: true });
for (const c of preview.changes) console.log(c.object, c.field, c.before, '->', c.after);

// Send the same logout_token your OIDC apps get to another service.
await atlas.logoutNotifyEndpoints.create({ url: 'https://api.example.com/atlas/logout' });

// Send email from your own domain through Atlas, then publish the returned DNS records.
const managed = await atlas.messaging.managedEmail.create({ from_address: 'Acme <no-reply@example.com>' });
console.log(managed.records);
```

在平台客户端上, `atlasIdApps.findUsers(id, email)` 回答一个应用程序的隐私请求, `atlasIdApps.revokeGrant(id, grantId)` 撤销一个人的许可 `atlasIdPublisher` 索赔和核实 在同意屏幕上显示的出版商名称.

## 后通道登录

当用户在Atlas系统中签名时, Atlas POSTs a `logout_token` 给您的 `backchannel_logout_uri`。在本地会话结束前进行校验 :

```ts
import { LogoutTokenVerifier, memoryLogoutReplayStore } from '@atlasauth/backend';

const logoutTokens = new LogoutTokenVerifier({
  issuer: 'https://atlasauth.net', // or your instance's issuer
  clientId: process.env.ATLAS_CLIENT_ID!,
  replayStore: memoryLogoutReplayStore(), // use a shared store across processes
});

// Framework-agnostic handler: (form body) -> { status, headers }
export async function handleBackchannelLogout(form: URLSearchParams) {
  const headers = { 'Cache-Control': 'no-store' };
  const token = form.get('logout_token');
  if (!token) return { status: 400, headers };

  const result = await logoutTokens.verify(token);
  if (!result.ok) {
    // retryable: our side failed for now (the issuer's keys could not be fetched).
    // 503 + Retry-After makes Atlas send it again; 400 is final and never retried.
    return result.retryable
      ? { status: 503, headers: { ...headers, 'Retry-After': '30' } }
      : { status: 400, headers };
  }

  const { sid, sub } = result.claims;
  if (sid) await sessions.deleteByAtlasSid(sid); // your session store
  else if (sub) await sessions.deleteAllForUser(sub);
  return { status: 200, headers };
}
```

核查员检查如下:

-   rS256 签名
-   `iss`,该 `aud` 包含您的客户端 id
-   `iat`,它必须是存在的, 不在未来和不太老 `exp` 如果有的话
-   `jti`
-   后通道登录事件
-   那个 `sid` 或者说 `sub` 当前
-   没有 `nonce`
-   那个 `typ` 实值 `logout+jwt`,当标题有一个

对于由您自己签名的标志, `new AtlasBackend({ issuer }).verifyLogoutToken(token, { clientId })` 重用实例缓存的密钥 .

要拒绝在多个服务器上重放, 请执行 `LogoutReplayStore.remember(jti, expiresAtMs)` 在一个共享的商店,例如 Redis `SET NX PX`。 。 。 .

阿特拉斯重审一个得到的交付 `5xx`, (中文(简体) ), `408`, (中文(简体) ), `429` 5秒5分30分2小时5小时10小时后 任何其他 `4xx` 最终决定。 阿特拉斯系统从未遵循过方向:a `3xx` 答复记录为 `backchannel_redirect_not_followed`因此,在域移动更新后 `backchannel_logout_uri` 到最后的URL。 回答自己的短暂失败( 数据库已下调) `503` 我也一样.

要检查路线终点到终点, `oauthClients.testBackchannelLogout(id)` 发送一个签名的测试令牌( s)`sid` 开始 `sid_test_`,额外索赔 `atlas_test: true`并返回所回答的路线; `oauthClients.listBackchannelDeliveries(id)` 列出每次尝试.

## 网络呼号

`verifyWebhook` 请检查Atlas发送到您的 Webhook 端点的事件的签名 。 您管理端点 `atlas.webhooks.endpoints`: 创建,更新,揭示或旋转一个秘密,发送测试事件,并重报过去交付.

这个 `atlas-signature` 信头可以携带一个以上的签名,以空格分隔(`v1,<sig> v1,<sig>`) (中文(简体) ). 虽然一个端点秘密会旋转,但阿特拉斯可能会用旧的和新的秘密在每次交货上签名. `verifyWebhook` 当任何一种签名符合您的秘密时, 接受发送.

### 网络加密上的标准 Webhooks

您的应用程序通过 Atlas 发布的事件遵循标准 Webhooks . `verifyStandardWebhookWeb({ body, headers, secret })` (和 `verifyStandardWebhookRequest(request, { secret })`)仅用Web Crypto验证它们,因此它们运行在Workers,Deno,Bun以及浏览器和节点上. `signAtlasWebhook` 财务报告和审定财务报表 `signStandardWebhook` 从 `@atlasauth/rp/testing` 制作正确的签名 交付给您的接受者测试.

### 类型事件

`verifyWebhook` 返回一个 `AtlasWebhookEvent`,一个工会结束 `type`,所以一个 `switch` 缩进 `data`数字 :

```ts
const { verified, event } = verifyWebhook({ body: rawBody, headers, secret });
if (!verified || !event) return res.sendStatus(400);
switch (event.type) {
  case 'user.created':
    await createUser(event.data.id, event.data.primary_email_address);
    break;
  case 'organization_membership.updated':
    console.log(event.data.role, event.data.role_source); // RoleSource | null
    break;
  default:
    // an event type newer than this package version
}
```

`ATLAS_WEBHOOK_EVENT_TYPES` 列出每一种类型. `RoleSource` 实值 `"manual"`, (中文(简体) ), `"scim"`, (中文(简体) ), `"directory_sync"`, (中文(简体) ), `"sso:<connection id>"` 或者说 `"ldap:<connection id>"`。 。 。 .

## 将用户映射到数据库

`syncLocalUser` 为已签名的Atlas用户查找或创建行。 校验会话后拨打:

```ts
const { user, created, linked } = await syncLocalUser(auth.claims, {
  findBySub: (sub) => db.user.findUnique({ where: { atlasSub: sub } }),
  findByEmail: (email) => db.user.findUnique({ where: { email } }),
  subOf: (row) => row.atlasSub,
  isLocalEmailVerified: (row) => row.emailVerified,
  link: (row, sub) => db.user.update({ where: { id: row.id }, data: { atlasSub: sub } }),
  create: ({ sub, email, email_verified, external_id }) =>
    db.user.create({ data: { atlasSub: sub, email, emailVerified: email_verified, externalId: external_id } }),
});
```

-   发现一行 `sub` 先说.
-   已有行只在符号上注明时通过电子邮件链接 `email_verified: true` 和 `isLocalEmailVerified(row)` 这是真的。 如果将一个地址联系起来,没有人证明,那么登记地址的任何人都会先记账.
-   已经绑定到不同 Atlas 用户的行永远不会重新连接 : `AtlasLinkConflictError` 被抛出.
-   在 Atlas 子上设置一个独特的索引 。 当两个先要求比赛,失败者 `create` 失败了 `syncLocalUser` 返回胜利者的行. `isDuplicateError` 决定何为失败( 默认: Postgres) `23505`,或包含"重复"或"独特"的信息.

信物携带 `email`, (中文(简体) ), `email_verified` 财务报告和审定财务报表 `external_id` 当实例在会话符中包含电子邮件请求时.

## 需要外交部

```ts
if (!mfaSatisfied(auth.claims)) return res.status(403).json({ error: 'mfa_required' });
```

`mfaSatisfied` 信物上写着的是真的 `mfa_satisfied: true`,或者当 `mfa` 没错, `acr` 实值 `urn:atlas:acr:mfa` 或者说 `amr` 包含 `mfa`,或会议通过企业SSO(`sso_connection_id`) (中文(简体) ). `usedMultiFactor` 财务报告和审定财务报表 `authenticatedWithin` 带 `SessionClaims` 我也一样.

一个组织外务省的政策是单独的一条条: `mfaOrgSatisfied(auth.claims, orgId)` 当组织不需要外交部时,或标志上写着 `org_mfa_ok`. 保险单只扣下组织的债权,因此从未读过的申请不会被停止;从自己的表格中授权的应用程序必须调用 `mfaOrgSatisfied` 自己来 它只描述该标志的活跃组织;对于其他任何组织,用户属于,呼叫 `client.organizations.mfaStatus(orgId, { sessionId })`。 。 。 .

## 中国证监会

对于饼干认证表和 `fetch` 电话:

```ts
const secret = process.env.ATLAS_CSRF_SECRET ?? deriveCsrfSecret(process.env.ATLAS_SECRET_KEY!);
const token = csrfToken({ sid: auth.claims.sid, secret }); // set as the __atlas_csrf cookie, render into the page
// on a POST:
if (!(await verifyCsrf(request, { sid: auth.claims.sid, secret }))) return new Response(null, { status: 403 });
```

符号绑定在会话上(`sid`) (中文(简体) ). `verifyCsrf` 从 `x-atlas-csrf` 标题或一个 `atlas_csrf` 构成字段,它必须等于 `__atlas_csrf` 饼干 形式体是从克隆人读取的,所以你的处理器仍然可以读取. 和下一个Js, `atlasProxy({ csrf: true })` 内 `@atlasauth/nextjs` 做这一切为你.

## 移交归属

随着转诊归属的开始,阿特拉斯系统在您转诊平台的每次互动注册之前都会报告 `user.created` 发送。 答案落在用户身上: `referral` (他们已回避他们,或已回避), `referral_code` 财务报告和审定财务报表 `referral_link` (用户自己的代码和链接,当他们是一个裁判).

```ts
await atlas.referrals.update({
  enabled: true,
  provider: 'webhook', // the Atlas referral protocol; or a built-in vendor adapter
  endpoint_url: 'https://referrals.example.com/atlas',
  signing_secret: process.env.REFERRAL_SIGNING_SECRET,
  auto_enroll_users: true,
  link_template: 'https://app.example.com/join?ref={code}',
  security: { on_high_risk: 'hold' },
});
const test = await atlas.referrals.test(); // { ok, http_status, latency_ms, error? }
const failed = await atlas.referrals.listCalls({ status: 'failed', limit: 20 });
await atlas.referrals.retryCall(failed.data[0]!.id);

const user = await atlas.users.get('user_123');
user.referral?.referrer_user_id;
await atlas.users.update('user_123', { referral_code: 'ada-1' }); // 409 REFERRAL_CODE_TAKEN if held
await atlas.users.flagReferral('user_123', { reason: 'self-referral' });
```

浏览器抓取 : `browser_sources` 列出浏览器读取的脚本或 cookie 的推荐程序( 更新时不包含返回提供者的默认, 由 `browser_sources_default`),以及 `consent_provider` 选择决定(`auto`一个经理,或 `app`) (中文(简体) ).

无法满足供应商的需求 `422 REFERRALS_NOT_CONFIGURED`。当平台回复延迟时,阿特拉斯发送 `user.referral_attributed` 网络hook (`UserReferralAttributedEvent`) (中文(简体) ). 飞行前可报告 `referrals_enabled_unverified` 财务报告和审定财务报表 `referrals_without_bot_protection`。 。 。 .

### 接收推荐协议( 移交平台)

执行Atlas查询协议的查询平台与 `verifyReferralRequest`每一次通话都是 `POST` 它的JSON身体上写着它是什么行动`op`: `signup`, (中文(简体) ), `erase`, (中文(简体) ), `export`, (中文(简体) ), `partner` 或者说 `signal`。通过呼叫到达的路径:一个签名的机构,其 `op` 拒绝匹配终点(`op_mismatch`) (中文(简体) ).

```ts
import { verifyReferralRequest } from '@atlasauth/backend';

app.post('/atlas/:endpoint', express.raw({ type: '*/*' }), async (req, res) => {
  const { verified, call } = verifyReferralRequest({
    body: req.body,
    headers: req.headers,
    secret: process.env.REFERRAL_SIGNING_SECRET!,
    path: req.path, // or expectedOp: 'signup' on a handler for one endpoint
  });
  if (!verified || !call) return res.sendStatus(401);
  switch (call.op) {
    case 'signup': // ReferralSignupPayload → answer ReferralSignupResponse
      return res.json({ referral: await match(call.payload), partner: await enroll(call.payload) });
    case 'export': // the connection test sends test: true
      return call.test ? res.sendStatus(404) : res.json(await exportFor(call.payload.customerRef));
    default: // erase, partner, signal
      await handle(call);
      return res.sendStatus(204);
  }
});
```

一个电话的重试会保留它 `atlas-id` (单位:千美元)`deliveryId`) (中文(简体) ). 报名是独家的 `customerRef`:回答重试的方式相同.

### 双向同步

提供商与 `inbound` 能力可以将注册后所做的转录更改发回Atlas。 设置报告 `inbound.url` (交给平台), `inbound.secret_set`, (中文(简体) ), `inbound.endpoint_id` 财务报告和审定财务报表 `inbound.last_event` (单位:千美元)`{ at, type, outcome }`) (中文(简体) ). 将平台事件保密 `inbound_secret` (只写字,16至512个字符) `inbound_endpoint_id`数字 :

```ts
await atlas.referrals.update({ inbound_secret: process.env.REFERRAL_INBOUND_SECRET, inbound_endpoint_id: 'we_123' });
const { inbound } = await atlas.referrals.get(); // inbound.last_event?.outcome: 'applied', 'stale', …
```

Atlas查询协议上的一个平台 `referral.updated` 事件为 `inbound.url`,如Atlas Webhook 与输入密件(或连接在未设置密件时是签名密件)一样签名. `signReferralUpdatedEvent` 构建它:

```ts
import { signReferralUpdatedEvent } from '@atlasauth/backend';

const e = signReferralUpdatedEvent({
  inboundUrl: settings.inbound.url,
  secret: process.env.REFERRAL_SIGNING_SECRET!,
  event: { customerRef: 'user_123', referralId: 'ref_1', status: 'qualified' },
});
await fetch(e.request());
```

## 常见问题

### @atlasauth/backend 是什么？

用于节点、德诺、邦和边缘运行时间的服务器侧图集。 它有三样东西:

### 如何安装 @atlasauth/backend？

在项目目录运行 npm install @atlasauth/backend，或使用 pnpm add / yarn add / bun add。它还提供命令行工具 atlas-doctor，可以用 npx atlas-doctor 直接运行。

### @atlasauth/backend 支持 TypeScript 吗？

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

### @atlasauth/backend 支持 ESM 还是 CommonJS？

以 CommonJS 形式发布，可以直接 require\(\)；在 ES Module 项目中也可以用 import 引入（Node\.js 会把 module\.exports 作为默认导出）。

### @atlasauth/backend 可以商用吗？

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

### @atlasauth/backend 的下载量有多少？

根据 npm 官方下载统计，@atlasauth/backend 在 2026/10/07 – 2026/11/05 这 30 天内被下载 7,536 次，日均约 251 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入，适合横向比较热度，不能直接等同于使用人数。

### @atlasauth/backend 还在维护吗？

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

