/** * device lane 准入链(design/device-executor-lane-v2 §4.3.2 提交期准入 + §7-T5 enrollment 五件 + * §7-T6 反枚举)—— **纯函数层**:没有 HTTP、没有 WS、没有 core 装配,只有「店 + 判据 → typed 判决」。 * 端点(`/v1/devices/*`、`GET /v1/device/ws`)由车A-3/车C 接;它们**必须**经由这里的判决,不得自建判据。 * * ── 五件(§7-T5,逐条落在哪儿)──────────────────────────────────────────────────────────────────── * ① TTL —— `DeviceStore.consumeEnrollToken` 的 CAS 谓词 `expires_at > NOW(3)`(**DB 时钟**,§4.6-F18)。 * ② 绑定 principal —— 同一条 CAS 谓词里 `target_tenant/target_subject` 逐字节等值;不匹配 = 拒。 * ③ 消费一次性 CAS —— `consumed_at IS NULL` 谓词 + `rev=rev+1`;**affected=0 时只有同 pubkey 放行**。 * ④ 重用审计 —— 异 pubkey ⇒ `device_enroll_token_reuse` 审计行(告警级);principal 不符 ⇒ * `device_identity_mismatch` 审计行。两条都在**拒绝之前**写,失败即整条请求失败(fail-closed: * 审计写不进去 ≠ 放行)。 * ⑤ 限速 —— {@link DeviceEnrollRateLimiter},注入式。缺席 = **不限速**,所以端点车必须传一个 * (§7-T5 ⑤ 双限速);本层不替它兜底,也不假装限了。 * * ── 反枚举(§4.8 + memory `anti-enum-404-ambiguity`)────────────────────────────────────────────── * enrollment 的**全部**拒绝理由(token 不存在 / 过期 / principal 不符 / 被别的 pubkey 用过)在 wire * 上是**同一个** `device.enrollment_invalid`;设备读面的「不存在」与「不是你的」是同一个 * `not_found.device`。细分只进审计。 * * ── 吊销在途语义(§8-R4)────────────────────────────────────────────────────────────────────────── * `revoked` 是终态:①绑定的 session 后续提交 = `device.revoked`(admitSession 步 2,**先于** presence * 判定);②吊销同事务把连接租约作废并把 `conn_generation` 推进一格 ⇒ 在途连接的每一次带世代谓词的写 * 都会影响 0 行,设备侧因此立刻失权(§4.6 步 2);③重新 enrollment = 新 deviceId,不复活旧行。 */ import { type DeviceErrorCode, type DeviceOwner, type DeviceRejectCode, type DeviceEnrollTokenRow, type DeviceRow, type DeviceSessionRow, type DeviceStore, type NewDeviceInput } from "./device-store.js"; /** typed 拒绝 —— 端点层只负责把它翻成 HTTP(status 已在体内,不许调用方另算)。 */ export interface DeviceRejection { /** [ref]:含两个借用位(`limit.rate_exceeded` 429 / `draining` 503);enrollment 本文件只铸前者, * `draining` 的唯一铸造点在 device-ws-hub 的排空/停机两臂。 */ code: DeviceRejectCode; status: number; message: string; /** 限流/排空专用:`Retry-After` 的秒值(附录 A 的 429/503 族纪律)。 */ retryAfterSec?: number; } export type DeviceOutcome = { ok: true; value: T; } | { ok: false; reject: DeviceRejection; }; /** * 限速座(§7-T5 ⑤)。**fail-closed**:`check` 抛错 = 拒绝(不是放行)—— 限速器不可用时放行,正是 * token 撞库最想要的那一刻。 */ export interface DeviceEnrollRateLimiter { check(key: string): Promise<{ allowed: boolean; retryAfterSec?: number; }>; } export interface EnrollDeviceInput { /** 明文 token(**只**在这里出现,落库前立刻摘要)。 */ token: string; /** 请求方**验证过**的复合身份(SSO/凭据链产出,恒不取自可自造头)。 */ requester: DeviceOwner; /** 🔴 `deviceId` 与 `owner` **不在**这里:前者恒由 server 铸(调用方选 id = 让请求方挑自己的设备身份, * 与 §6「server 铸 dev_」相悖),后者恒取自**验证过的** requester —— 两个字段都不接受请求体供给。 * 测试要确定性 id ⇒ 注入 {@link DeviceEnrollmentDeps.mintId},不是从入参开一个口子。 */ device: Omit; } export interface EnrollDeviceValue { device: DeviceRow; /** `true` = 幂等重放(响应丢失重试),`false` = 本次真注册。wire 上两者同为 200 + 同 deviceId。 */ replayed: boolean; } export interface AdmitSessionInput { rootSessionId: string; requester: DeviceOwner; /** 提交体携带的 deviceId(§4.3.2:仅根任务携;缺席 = 沿用绑定)。 */ requestedDeviceId?: string; } export interface AdmitSessionValue { deviceId: string; device: DeviceRow; /** * 判决**当刻**的连接世代(§4.6 fencing token)。 * * 🔴 **准入判决是一个快照,不是一把锁**(codex 交叉轮 F4,判真但归属下一车):`admitSession` 返回后、 * 调用方真正下发指令前,一次 `revokeDevice` 可以提交 —— 它会作废租约并把 `conn_generation` 推进一格, * 但它**追不回**已经基于旧判决发出去的指令。这不是本层能关掉的窗(§6 注③ 刻意不落指令表,所以持久层 * 没有「派发」这个写点可以带谓词);设计给的答案是**在下发边界重新断言世代**(§4.6 步 2/3:每一次带 * `conn_generation = ?` 谓词的写影响 0 行 = 本连接已失权 ⇒ 停发并断连)。 * 本字段就是那个断言要用的值 —— 车A-3 的 WS 汇聚端**必须**拿它去 fence,拿不到就不要下发。 * 残余诚实:窗口上界 = 心跳周期(吊销后设备侧最迟一个心跳失败即失权),§8-R4 对已发指令的账面处置是 * 「在途结果帧丢弃 + 审计,对应调用按 outcome-unknown 收尾」。 */ connGeneration: number; } export interface IssueEnrollTokenRequest { /** 签发对象 = 请求方**验证过**的复合身份。v1 只能为自己签(operator 代他人签发是另一件事, * 它要一条「谁可以代谁」的授权面,不在本动词里偷偷发生)。 */ target: DeviceOwner; /** 审计列 `created_by`(= 发起这次签发的已验证 principal 串)。 */ createdBy: string; /** 有效期**时长**(整秒,自 **DB now** 起算)。属主 = 部署旋钮 `DEVICE_ENROLL_TOKEN_TTL_SEC`; * 缺席 ⇒ {@link DEFAULT_ENROLL_TOKEN_TTL_SEC}。 */ ttlSeconds?: number; } export interface IssuedEnrollToken { /** 🔴 明文 token —— **只在这一次**存在于内存与响应体里;库里只有 sha256 摘要。 */ token: string; row: DeviceEnrollTokenRow; } /** §7-T5 ① 的 15 分钟。**唯一默认点**(config 层刻意不抄第二份 —— 缺席即此值,见 config-types 的键注)。 */ export declare const DEFAULT_ENROLL_TOKEN_TTL_SEC = 900; export interface DeviceEnrollment { /** §7-T5 五件的第 ①②③ 件的**写入侧**:绑定复合身份 + TTL + 一次性摘要落库(明文一现)。 */ issueEnrollToken(input: IssueEnrollTokenRequest): Promise>; enroll(input: EnrollDeviceInput): Promise>; bindPlacement(input: { rootSessionId: string; deviceId: string; requester: DeviceOwner; }): Promise>; admitSession(input: AdmitSessionInput): Promise>; revoke(input: { deviceId: string; requester: DeviceOwner; revokedBy: string; reason?: string | null; }): Promise>; } export interface DeviceEnrollmentDeps { store: DeviceStore; /** 缺席 = 不限速(端点车必须传;见文件头注 ⑤)。 */ rateLimiter?: DeviceEnrollRateLimiter; /** 测试可注入确定性铸造;默认 `mintDeviceId()`。 */ mintId?: () => string; } /** 一条 typed 拒绝的**构造点**:状态档恒取 {@link DEVICE_ERROR_STATUS} 单表,调用方只给码与文案 —— 不许各处手抄「码 ↔ 状态」配对。 */ export declare function buildDeviceRejection(errorCode: DeviceErrorCode, message: string): DeviceRejection; export declare function createDeviceEnrollment(deps: DeviceEnrollmentDeps): DeviceEnrollment; /** * 进程内固定窗限速(§7-T5 ⑤ 的**默认**实现)。 * * 🔴 诚实边界:这是**每副本**的窗,不在多副本部署下声称有全局上界。device 车道的生产接线恒用它 * (`boot/device-lane.ts` 的 `DEVICE_ENROLL_RATE_POLICY`)—— 曾经的「适配全局 RateGate」一支已删:那只限速器的 * 限额值跟全局 `RATE_LIMIT_RPM` 走,缺省 0 = 关断,等于让出厂形的注册口不限速。 */ export declare function createFixedWindowRateLimiter(opts: { limit: number; windowMs: number; now?: () => number; }): DeviceEnrollRateLimiter; //# sourceMappingURL=device-enrollment.d.ts.map