Client SDK 指南
本文定义 TypeScript SDK contract,是 package boundary 与 host-integration rule 的权威来源。apps/webui 下的 application-specific code 是参考实现,不是 public API。
Package Families
| Package | 职责 |
|---|---|
@securitydept/client | Foundation traits:environment、transport、state/resource、event、cancellation、span、tracing、injection、protocol 和 URL helper。 |
@securitydept/client-react / @securitydept/client-angular | foundation trait 的 framework bridge。 |
@securitydept/basic-auth-context-client* | Basic-Auth boundary observation、login navigation 与本地投影清理。 |
@securitydept/session-context-client* | cookie-session refresh、user-info、login 和 logout integration。 |
@securitydept/token-set-context-client* | token-set OIDC mode、lifecycle orchestration、registry、access-token substrate 和 framework adapter。 |
* 表示 core package 加上可选的 React 或 Angular package。workspace 中标为 private 的 package 是 tooling、test 或 reference-app dependency,不是受支持的 npm product。
Public Imports
只能使用声明的 package export。已发布的 @securitydept/client subpath 为 ./web、./webext、./server、./rx、./test。token-set 的 focused subpath 为:
./orchestration./frontend-oidc-mode./backend-oidc-mode./access-token-substrate./registry./test
sdks/ts/public-surface-inventory.json 与每个 package 的 exports 字段是可执行的 source of truth。不得 import source file、reference-app glue 或未记录的 internal path。
稳定性和变更纪律
public-surface inventory 为每个已发布 package 和 subpath 指定稳定性等级。0.x 不意味着所有 public surface 都可以随意破坏。
| 稳定性 | 变更纪律 |
|---|---|
| stable | 先弃用。移除前至少保留一个 minor release,并提供 migration note。 |
| provisional | breaking change 必须有 migration note 和 review 可见的理由。 |
| experimental | 可以立即 breaking;在对 adopter 有帮助时记录简短 migration note。 |
当前的总体划分是:@securitydept/client 与 core context-client root 为 stable;framework adapter、host-specific subpath 和 token-set focused subpath 通常为 provisional;@securitydept/client/test 是 experimental。单个 export 的等级以 inventory 为准。
受支持的 Host Entrypoint
| Entrypoint | 适用场景 | 边界 |
|---|---|---|
@securitydept/client/web | native browser page。 | 从显式 Web input 创建 router、page lifecycle、popup 与 browser storage trait。 |
@securitydept/client/webext | browser-extension UI、background 或 shared extension code。 | 适配 extension routing/storage API,不把 extension global 暴露给 core client。 |
@securitydept/client/server | request-scoped server 或 SSR integration。 | 构造 request-scoped environment 和 transport,不假定 browser global 存在。 |
@securitydept/client/rx | 内部 SDK 或 host-side RxJS composition。 | 提供 RxJS-backed SDK implementation 与 command/resource utility;public trait 自身提供 observable interop。 |
@securitydept/client/test | SDK test 和 example。 | experimental test-only helper,不是 adopter runtime dependency。 |
@securitydept/token-set-context-client/test | Token Set client、registry entry 和 registry fixture。 | dependency-light test-only helper,不依赖 test runner 或 framework。 |
在 composition root 使用 createEnvironmentForNativeWeb、对应的 WebExtension creator 或 createEnvironmentForServer。这些 helper 都不会为 client 做隐式 host detection。
显式 Environment
client 在 construction 时接收 FoundationEnvironment。必需基线为:
interface FoundationEnvironment {
injector: SecuritydeptInjector;
transport: BaseTransportTrait;
time: TimeTrait;
realmStorage: SyncStorageTrait;
span: SpanTrait;
tracing: TracingTrait;
}persistentStorage、sessionStorage、router、pageLifecycle、popup、idleCallback 是 optional capability。client/helper 不得静默获取缺失的 host capability。应使用 @securitydept/client/web、webext、server 中的 host-specific environment creator,或在 composition root 显式提供 capability。
transport 是 neutral base transport。authenticated transport 从 base transport 加 replaying authorization-header signal 派生;它不是第二个 environment field,也不让 environment 持有 auth state。
Public API 设计规则
- public function 接受 optional input 时使用 options object;不要增加含义不明确的 positional second parameter。
- enum-like string domain 定义为具名
constobject 及其派生 type。public contract 和重复 telemetry vocabulary 使用具名 constant,不要散落 raw string。 - 如果一个 short、single-use helper 只增加跳转,则保持 inline;只有复用、建立稳定边界或能显著澄清复杂逻辑时才提取。
- required host capability 必须显式传入。helper 可以用
null或undefined表示 optional capability unavailable,但不得静默获取 browser、storage 或 environment fallback。 - 所有已发布 package 都声明
sideEffects: false。不得依赖 import-time global patch 或 implicit polyfill;在 composition root 显式配置 host polyfill 和 global integration。
State、Event 和 Cancellation
公开 reactive contract 是 SDK trait:
ReadableSignalTrait<T>提供同步 current state 和 observable interop。ResourceTrait<T>提供 typed loading/value/error lifecycle。EventStreamTrait<T>是 read-only event sequence;EventSubjectTrait<T>只用于 producer side。CancellationTokenTrait是具有 observable interop 的 cooperative cancellation。
consumer 接收 read-only signal 与 event stream。实现可直接使用 RxJS:这些 trait 已提供 observable interop,@securitydept/client/rx 提供 RxJS-backed implementation 与 command/resource utility。不要为了内部“纯度”再套一层只转发 subscription 的 wrapper,也不要把 RxJS Observable 作为 SDK public contract 暴露。
Client lifecycle event stream 是 hot、non-replay 的边沿流。current/stale state 属于 ReadableSignalTrait / ResourceTrait;诊断历史属于显式命名的 trace timeline 或 event-history adapter。late subscriber 不得因订阅 event stream 而重复触发历史 toast、metric 等副作用。
failure variant 必须在 required error field 中携带所属 context 生成的 ClientError。consumer 可以通过 from(client.events).pipe(filter(isClientErrorEvent)) 派生默认 message stream,无需建立第二个 error subject。operation 抛错时,Resource failure、failure event 与 rejected operation 保留同一个 error instance。撤销恢复可以发布 failure event,同时解析为未认证状态。完整 error 是进程内 programming contract,不得直接序列化;logging/tracing 使用 describeError(error) 派生 secret-safe field,UI adapter 使用 readErrorPresentationDescriptor(error)。
当 public trait API 需要 RxJS NEVER 或 EMPTY 语义时,使用 createNeverEventStream() 与 createEmptyEventStream()。
Signal 性能与验证
RxSignal 保留单一 Map 快照内核,使用原始值存储、按需 Rx 值镜像和原生 dirty 回调。Public Signal API、现有 lossless Rx 语义、同步深度优先回调顺序及公开/外部 dirty observable 协议保持支持;watchStream 保留原有调度、重放与取消行为。
运行 mise exec -- pnpm bench:sdks 执行 Vitest 耗时基准,运行 mise exec -- pnpm test:sdks 执行 SDK 测试(包含基准正确性、保留内存及回收诊断)。基准命令可直接追加 -t diamond 等筛选参数;测试命令追加 -- -t memory 将筛选交给 Turbo 下的测试。基准专用依赖隔离在 sdks/ts/benchmarks 私有 workspace 包中,生成结果只写入忽略的 temp/。测量方法和比较边界见基准指南。
Span 和 Tracing
SpanTrait 是 tracing、error 与未来 event correlation 共享的 foundation context capability,不属于 telemetry 或 event 的子能力。context propagation 保持显式:client fork client frame,每个 operation 再 fork operation frame。shared attribute 使用具名的 client.name、client.id、operation.name;provider detail 通过 setAttributes(attributes, { providerId }) 写入。provider read 默认看到自身 attribute 加 shared attribute,同时保留 root-to-node frame,不做跨层扁平合并。attribute value 使用 immutable SpanAttributeValue contract;span core 依赖 readonly contract,只在状态变化时复制顶层 record,不检查、复制或冻结嵌套值。attribute 通过 getAttributes() 或 getRootToNodeAttributes() 读取。
tracing 使用 TRACING_SPAN_ATTRIBUTE_PROVIDER_ID 写入 protocol/lifecycle detail。TracingEvent 保留 live span reference;createTraceTimelineStore() 这类 buffering subscriber 在自己的消费边界读取并保存 tracing-visible root-to-node frame。timeline 使用 SDK 内部固定容量 ring buffer,不要求额外 runtime package;non-replay latestEntry stream 在记录时发布 entry,在 clear 后发布 null,entries getter 仅在 consumer 需要完整 timeline 时创建 readonly array snapshot。operation creator option 使用 traceAttributes,不再使用旧的通用 fields input。
ClientError 首次捕获 error-visible span path,并保留原 error instance 与 subclass。snapshot 只包含 shared attribute 和 CLIENT_ERROR_SPAN_ATTRIBUTE_PROVIDER_ID attribute,不包含 tracing-only detail。readErrorPresentationDescriptor() 默认读取最深层 client identity 与 operation name,因此通用 message stream 无需维护 event-type mapping 就能输出来源明确的 UI 文案。通过 contextFormatter: null 可关闭该前缀,也可注入返回本地化 context label 的 formatter。
trace 只描述当前动作。不要为了重建 span tree 已有的 nesting 再维护全局 source/outcome enum。freshness 是 refresh boundary fact,不是所有 auth event/trace 的默认字段。
Auth Context Clients
Basic Auth
BasicAuthContextClient 建模 zone-aware challenge boundary。它不管理或撤销 browser credential cache。client 暴露 boundary snapshot、operation 和 event;login navigation 仍是显式的 host/router action。其 logout() 只清除当前内存 boundary projection,不发起网络请求。
Session
SessionContextClient 建模 server-owned cookie session。它读取 session user-info、暴露 resource state,并发起 login/logout navigation;不暴露 server 的 OIDC credential 或 session-storage implementation。
Token Set
token-set client 只有一个 in-memory snapshot authority。其 lifecycle 有四个稳定角色:
- workflow source 提供 startup、page resume、refresh timing 等输入。
- closed planner 将 restore、freshness、refresh outcome 求值为 discriminated final candidate。
- base client 串行化 top-level workflow,并提交恰好一个 determination。
- mode client 实现 protocol-specific OIDC work;registry 组合 client 并路由 callback。
start() 是 canonical initial lifecycle entry。restorePersistedState() 仍保留为 explicit manual persistence re-sync command,例如 cross-tab 或 host-driven synchronization;它不是通用 readiness API。由 registry 构造的 client 应使用 registry readiness API。
auth event 是 direct discriminated public union,event type 决定 payload shape。consumer 不得再依赖已废弃的 AuthCheck*、TokenSetAuthFlowReason、TokenSetAuthFlowOutcome 或 payload-map contract。只有 refresh event 携带 freshness 和 refresh-material fact。任何 auth event 不得包含 raw access token、refresh material 或 authorization header。
Frontend OIDC 运行时能力
浏览器 Frontend OIDC 应使用 HTTPS 或可信 localhost/loopback origin。普通 HTTP 域名和局域网 IP 不提供原生 crypto.subtle;修改 external_base_url 不会改变页面的安全上下文。参阅安全上下文规则。
client 创建时检查 globalThis.crypto.getRandomValues,以及 pkceEnabled 为 true(默认)时的 crypto.subtle.digest;构造授权 URL 前再次检查。缺失能力抛出 configuration ClientError:isSecureContext === false 时为 FrontendOidcModeErrorCode.InsecureContext(frontend_oidc.runtime.insecure_context),否则为 WebCryptoUnavailable(frontend_oidc.runtime.web_crypto_unavailable)。错误消息列出缺失函数与处理方式。host 提供兼容 API 后,不会仅因上下文不安全而拒绝;检查只确认函数存在,不验证实现正确性或算法支持。
如果必须使用不支持的 runtime,需要在创建 SDK environment、span、registry 或 client 之前,为 globalThis.crypto 安装兼容 polyfill。仅向应用代码传入另一个 crypto object,不会改变 oauth4webapi 对全局 API 的使用。函数和模块需求如下:
| 使用方 | 必需能力 |
|---|---|
SDK client/registry/span 的 UUID v7;oauth4webapi generateRandomState、generateRandomNonce、generateRandomCodeVerifier | 密码学安全的 crypto.getRandomValues |
Frontend OIDC 的 buildAuthorizeUrl、authorizeUrl、loginWithRedirect、loginWithPopup,开启 PKCE 时 | 支持 SHA-256 的 crypto.subtle.digest |
| 直接使用 oauth4webapi 的 JWK thumbprint、DPoP hash、hybrid code/ID-token hash 校验 | digest;算法取决于操作与签名算法 |
直接使用 oauth4webapi JWT assertion(PrivateKeyJwt、ClientSecretJwt) | sign;secret-based assertion 还需要 importKey |
| 直接使用 oauth4webapi 的 application-level JWT/JARM/access-token/DPoP 签名校验 | importKey、verify |
直接使用 oauth4webapi 的 DPoP proof 与 generateKeyPair | 按操作需要 exportKey、sign、generateKey |
扩展路径还需要兼容的 CryptoKey object/全局 constructor 及所选算法。SecurityDept 当前封装使用 authorization-code response、None/ClientSecretPost 和 PKCE,并未启用这些直接扩展、签名校验 helper 或 JWE 解密。自定义 jweDecrypt callback 应自行满足额外密码学需求。参阅 oauth4webapi 实现;workspace 锁定 3.8.6,但使用方可以安装其他兼容的 3.x 版本。扩展流程或升级时应重新核对需求。
SDK 不内置 crypto 降级。不要用 Math.random 替代安全随机数、通过关闭 PKCE 规避环境问题,或伪造 isSecureContext。polyfill 只提供计算能力,不保证 HTTP 页面传输的机密性与真实性。生产环境仍须使用 HTTPS;CORS、mixed content、Secure cookie 与 IdP callback 规则是独立限制。loopback 的 allowInsecureRequests 只放宽 oauth4webapi 的 endpoint 协议策略。
刷新错误恢复
两个 OIDC 模式的配置均支持 refreshErrorPolicy,由 BaseOidcModeClient 统一实现。默认值为 "revokeAsUnauthenticated":刷新令牌被确认撤销后返回 null、清除持久化凭据,并发布 resolved 的未认证状态。Registry 工厂仍可正常就绪,受保护路由守卫可以开始交互登录并保留目标 URL;客户端自身不会因撤销而直接导航。
使用 "revokeAsUnauthenticatedOnInit" 可仅恢复 start() 还原持久化会话期间的刷新错误;使用 "throw" 保留操作抛错语义。显式调用 restorePersistedState() 属于手动操作。无论选择哪种策略,已确认撤销的凭据都会清除。
同步或异步处理函数接收 { error, operation, trigger, clientId, cancellationToken },返回 "unauthenticated" 或 "throw"。operation 为 "restorePersistedState"、"refresh";trigger 为 "initialization"、"manual"、"refreshTimer"、"pageResume"。这些契约及命名常量从 @securitydept/token-set-context-client/orchestration 导出。
只有协议层识别的 TokenSetAuthorizationRevocationError(invalid_grant 或符合条件的 Bearer invalid_token challenge)允许恢复为未认证,处理函数也受此约束。普通 401、网络、配置、协议、存储及取消错误不会被重分类;回调和授权码交换错误不适用此策略。处理函数失败或被取消时仍抛错,但会清除已确认撤销的材料。现有生命周期失败事件和 tracing 保留原协议错误;鉴权事件不会向晚订阅者重放。
Framework Adapters
React 与 Angular package 将 canonical client ownership 适配到框架的 context/injection/lifecycle,不得创建竞争的 auth-state authority。router adapter 把 framework router 适配到 RouterTrait,不定义 product route 或 UI。
- React 中,在 Fiber 之外调用
createEnvironmentForReact(...),通过SecuritydeptProvider安装其 injector,再以useSecuritydeptContext()读取 SDK dependency。@securitydept/client-react/tanstack-router负责 TanStack Router adaptation。 - Angular 中,使用
provideEnvironment(...)组合 Angular router、HttpClient、injector 和 destruction lifecycle。@securitydept/client-angular负责相应的 router/transport adapter。 - token-set host 用
provideTokenSetClientRegistry(...)注册 keyed client。registry 的authEvents与errors流会聚合 ready-client auth event 及所有 client/factory error,host 无需自行 flatten per-client resource。React 通过useTokenSetClientRegistry()与 callback hook 读取 registry;Angular 提供同一个 core registry,并可使用 callback component、route-root helper 与 guard factory。 - route helper 只表达 requirement,并将求值交给 registry/client lifecycle。application 仍拥有 route tree、redirect、UI 和 user-facing copy。
router navigation mode 决定由哪一种 host mechanism 接收目标。Angular adapter 会把两种 mechanism 组合为单一 RouterTrait:在 CreateRouterForAngularOptions(或 provideEnvironment({ routerForAngularCreateOptions: ... }))中,将 native-web 的 navigation、location、history、window option 与 Angular router 扁平放在同一个 options object。push/replace 仍交给 Angular Router;包括 OIDC authorization URL 在内的 external request 交给内部组合的 native-web router。application 不需要再注册或选择第二个 environment router。
TanStack React Router adapter 将 push/replace request 映射为 router.navigate({ to, ... }),external request 则映射为 router.navigate({ href })。不要把 absolute IdP URL 传给 to;TanStack 会将 to 当作 internal route destination。
Lifecycle 和 Error 规则
- 直接拥有的 client 在 required capability 完成 composition 后调用
start();registry-owned client 使用 registry readiness API。 - 消费 public read-only state/event surface;在 owning framework/container destroy 时调用
dispose()。 - 向可取消 operation 传递 cancellation token;cancellation 是 cooperative 的,必须在 async boundary 检查。
- public failure 使用所属 context 生成、带有 safe code/source/recovery metadata 的
ClientError。不得在 UI error 或 event 中暴露 secret-bearing transport、token、provider payload。
兼容性
任何 public change 都必须更新 package export、inventory、focused documentation、test 和 TS SDK 迁移记录,并遵守上述稳定性纪律。只要不会保留误导或不安全的模型,优先选择 additive migration path。不要仅为了掩盖 ownership correction 而添加 alias。