@factorialco/auth

中文文档
0.3.0发布于 2026/07/28MITTypeScriptESM + CJS构建来源已验证近 30 天下载 5.1万

Verify Factorial ID tokens and request OAuth grants.

原始使用文档
文档目录15 节
  1. Install
  2. Quick start
  3. OAuth token client
  4. Actor identity primitives
  5. ActorRef
  6. IdentityChain
  7. How they relate
  8. Error handling
  9. Caching & key rotation
  10. Integration patterns
  11. Server request handler (Express-style)
  12. Edge / Web Request
  13. Authenticated identity from access-token claims
  14. Compatibility
  15. Contributing

@factorialco/auth

Verify Factorial ID tokens and request OAuth grants from its token endpoint.

Library-agnostic: it depends only on jose and the global fetch, so it runs on Node.js (>= 22.14) and on edge / serverless runtimes.

  • Resolves the issuer + JWKS URI via OIDC discovery (cached, with stale fallback).
  • Verifies tokens (ES256 by default): signature, iss, aud, exp, nbf.
  • Refetches the JWKS automatically on key rotation (unknown kid).
  • Returns strict, typed, immutable claim objects.
  • Requests platform, delegated, and refreshed OAuth tokens through discovered endpoints.
  • Ships actor-identity value objects: ActorRef, ActorType, IdentityChain, ActType.
  • Provides a framework-agnostic extractBearerToken helper.

Install

pnpm add @factorialco/auth
# or: npm install / yarn add / bun add

Quick start

import { FactorialAuth } from '@factorialco/auth'

const auth = new FactorialAuth({
  oidcDiscoveryUrl: 'https://id.factorialhr.com/.well-known/openid-configuration',
  audience: 'your-client-id',
})

// Decode and verify an access token
const accessTokenClaim = await auth.decodeAccessToken(token)
console.log(accessTokenClaim.sub, accessTokenClaim.cid)

// Decode and verify an id token
const idTokenClaim = await auth.decodeIdToken(token)
console.log(idTokenClaim.sub, idTokenClaim.email)

Construction is cheap and synchronous — no network calls happen until the first decode or token-client call. Discovery and JWKS documents are fetched lazily and then cached (see Caching & key rotation).

OAuth token client

Configure client credentials on the same FactorialAuth instance. Credentials are only required when a token-client operation is called:

const auth = new FactorialAuth({
  oidcDiscoveryUrl: process.env.FACTORIAL_OIDC_DISCOVERY_URL!,
  audience: 'factorial',
  clientId: process.env.FACTORIAL_OAUTH_CLIENT_ID,
  clientSecret: process.env.FACTORIAL_OAUTH_CLIENT_SECRET,
})

const platform = await auth.tokenClient.platformToken({
  audience: 'factorial-backend',
  cell: 'eu1',
})

const delegated = await auth.tokenClient.delegatedToken({
  subjectToken,
  audience: 'factorial-backend',
})

const refreshed = await auth.tokenClient.refreshToken(refreshToken)

The token client parses the OAuth response but does not decode, verify, persist, refresh automatically, or revoke returned grants. Applications retain ownership of those lifecycle and authorization decisions.

When the token endpoint answers with an OAuth error, the thrown TokenRequestError exposes the HTTP status and the OAuth error code as oauthError, so callers can branch without parsing the message. Redirects from the token endpoint are never followed.

Actor identity primitives

ActorRef and IdentityChain are small value objects for carrying authenticated actor identity across application boundaries. They are authentication primitives, not authorization contexts and not event transport objects.

ActorRef

ActorRef is an immutable reference to one concrete actor.

It answers: "which actor is this?"

import { ActorRef } from '@factorialco/auth'

ActorRef.employee('123')
ActorRef.company('42')
ActorRef.system('domain-events-audit-log-creator')

An actor ref serializes to a URI-like string:

f:act:employee:123

Use an ActorRef when code needs a compact, stable actor identifier without loading a model, carrying permissions, or encoding tenant scope. The id is always a string and must be opaque: never put emails, names, API keys, tokens, IPs, or other sensitive values in it.

IdentityChain

IdentityChain is a recursive chain of actor refs.

It answers: "who is the effective authenticated actor, and who is acting through whom?"

The chain root is the effective actor for the current action. If the action happened through a become/act flow, act points to the actor chain that initiated it, and actType describes the relationship between those two nodes.

import { ActorRef, ActType, IdentityChain } from '@factorialco/auth'

const employee = ActorRef.employee('123')
const admin = ActorRef.employee('999')

const identityChain = IdentityChain.create({
  actor: employee,
  act: IdentityChain.create({ actor: admin }),
  actType: ActType.AdminBecome,
})

identityChain.actor // employee
identityChain.act?.actor // admin

The serialized shape is string-keyed and recursive (the wire form uses the snake_case act_type):

identityChain.toObject()

// {
//   actor: "f:act:employee:123",
//   act: { actor: "f:act:employee:999" },
//   act_type: "admin_become",
// }

Use IdentityChain.parse only for trusted server-side metadata. Deserialization is capped at 3 nodes by default to keep parsing conservative; pass maxDepth to change it:

IdentityChain.parse(serializedObject)
IdentityChain.parse(serializedObject, { maxDepth: 5 })

How they relate

ActorRef is the leaf value. It identifies one actor and nothing else.

IdentityChain composes actor refs into the authenticated relationship graph. This keeps actor references stable and simple while preserving become/act context explicitly. Do not encode impersonation chains into an actor ref string; keep the actor ref as the subject and use IdentityChain.act plus IdentityChain.actType for the relationship.

For example, an admin becoming an employee is represented as:

employee actor ref -> act(admin actor ref), actType: admin_become

That lets application code choose the right projection for its own contract.

Never accept a client-supplied actor ref or identity chain as authoritative for writes. Build these objects from authenticated server-side context, credentials, or known system callers. An actor ref identifies the actor, not the tenant, so audit and domain-event flows must carry company or tenant scope separately.

Error handling

Every verification/decoding failure throws a subclass of AuthError. Catch AuthError to handle all of them, or a specific subclass for finer control. The tryDecode* methods swallow AuthError and return null; any other (unexpected) error propagates.

Error
├─ AuthError                       // base for everything this library throws
│  ├─ ConfigurationError           // invalid FactorialAuthConfig (thrown by the constructor)
│  ├─ OidcDiscoveryFetchError      // discovery endpoint unreachable / non-2xx / timeout
│  ├─ OidcDiscoveryParseError      // discovery document invalid JSON or missing issuer/jwks_uri
│  ├─ TokenRequestError            // OAuth token endpoint request failed
│  ├─ TokenResponseParseError      // OAuth token response malformed or unusable
│  ├─ JwksFetchError               // JWKS endpoint unreachable / non-2xx / timeout
│  ├─ JwksParseError               // JWKS invalid JSON or malformed key set
│  └─ TokenError                   // base for token-level failures
│     ├─ InvalidToken              // bad signature, malformed token, unknown kid, or bad claim shape
│     ├─ ExpiredToken              // exp check failed
│     ├─ InvalidIssuer             // iss did not match the discovery document
│     ├─ InvalidAudience           // aud did not match config.audience
│     └─ ImmatureToken             // nbf check failed (token used before it is valid)
├─ ActorRefError                   // malformed ActorRef string/id (NOT an AuthError)
└─ IdentityChainError              // malformed identity chain (NOT an AuthError)

Note: ActorRefError and IdentityChainError extend Error directly, not AuthError, and are not affected by tryDecode*.

import { FactorialAuth, ExpiredToken, TokenError, OidcDiscoveryFetchError } from '@factorialco/auth'

try {
  const claims = await auth.decodeAccessToken(token)
} catch (error) {
  if (error instanceof ExpiredToken) {
    // prompt a refresh
  } else if (error instanceof TokenError) {
    // any other invalid-token case -> 401
  } else if (error instanceof OidcDiscoveryFetchError) {
    // upstream/infra problem -> 503
  } else {
    throw error
  }
}

Caching & key rotation

  • The OIDC discovery document and the JWKS are cached in memory with a dual TTL: fresh for 10 minutes, then served stale for up to 1 hour if a refetch fails (so a transient discovery/JWKS outage doesn't break verification).
  • Concurrent first-time verifications trigger a single discovery fetch and a single JWKS fetch (in-flight requests are de-duplicated).
  • When a token carries a kid that isn't in the cached JWKS (key rotation), the JWKS is refetched once and the lookup retried. If the kid is still unknown, the token is rejected with InvalidToken.

Because caches live on the instance, create one FactorialAuth and reuse it for the lifetime of your process/worker rather than constructing one per request.

Integration patterns

Server request handler (Express-style)

import { FactorialAuth, extractBearerToken } from '@factorialco/auth'

const auth = new FactorialAuth({
  oidcDiscoveryUrl: process.env.FACTORIAL_OIDC_DISCOVERY_URL!,
  audience: process.env.FACTORIAL_AUDIENCE!,
})

app.use(async (req, res, next) => {
  const token = extractBearerToken(req.headers.authorization ?? null)

  if (token === null) {
    return res.status(401).json({ error: 'missing bearer token' })
  }

  const claims = await auth.tryDecodeAccessToken(token)

  if (claims === null) {
    return res.status(401).json({ error: 'invalid token' })
  }

  req.claims = claims
  next()
})

Edge / Web Request

const token = extractBearerToken(request.headers.get('authorization'))
const claims = token ? await auth.tryDecodeAccessToken(token) : null

Authenticated identity from access-token claims

Access-token claims derive actor references and recursive identity chains using the same employee, platform-principal, become, and delegation rules as the Ruby factorial-auth package:

const claims = await auth.decodeAccessToken(token)
const actor = claims.actorRef
const chain = claims.identityChain()

identityChain() throws IdentityChainError when the act chain is deeper than the allowed maximum or carries an unknown bt value — even on a token that decoded successfully.

Compatibility

  • Runtime: Node.js >= 22.14, and any edge/serverless runtime with global fetch and Web Crypto (Cloudflare Workers, Vercel Edge, Deno, Bun).
  • Modules: dual ESM (import) and CJS (require) entry points with bundled .d.ts.

Contributing

See CONTRIBUTING.md for local setup and development commands.

版本与发布记录

@factorialco/auth 共发布 3 个版本,首次发布于 2026/07/14;过去 12 个月发布了 3 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.3.0latest2026/07/283 个月前
0.2.02026/07/144 个月前
0.1.02026/07/144 个月前

关于 @factorialco/auth 的常见问题

@factorialco/auth 是什么?

Verify Factorial ID tokens and request OAuth grants.

如何安装 @factorialco/auth?

在项目目录运行 npm install @factorialco/auth,或使用 pnpm add / yarn add / bun add。

@factorialco/auth 支持 TypeScript 吗?

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

@factorialco/auth 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@factorialco/auth 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.14.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@factorialco/auth 可以商用吗?

@factorialco/auth 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@factorialco/auth 的下载量有多少?

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

@factorialco/auth 还在维护吗?

最新版本 0.3.0 发布于 2026/07/28(3 个月前);至今共发布 3 个版本,其中过去 12 个月发布了 3 个;首次发布于 2026/07/14。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。