/** * O4 设备换绑 wire + S-4 additive 投影(DEBTS [ref];设计真源 = sema-internal * `server/designs/2026-08-30-o4-device-rebind-wire-v2.md`,冻结稿)—— `/v1/devices/*` 管理面的 * **首批三动词**(capabilities.ts 那条「车C 的 `/v1/devices/*`」预留注所指的族,本文件开族): * * · `GET /v1/devices` —— 设备列表(owner 域过滤;explicit operator 全量)。行 = * `{ deviceId, name?, status: "online"|"offline", lastSeenAt? }`(v2 稿 §4 冻结形;`status` 由 * **hub 在场性**判 —— v1 部署形 = 单副本(基线稿 §4.6),汇聚端的活连接集就是本副本的 presence * 真源;`name` = displayName,空串诚实缺席;`lastSeenAt` ISO-8601)。**吊销行不列**:revoked 是 * 终态(重注册 = 新 deviceId),列出来只会被壳当换绑目标然后撞 404。 * · `GET /v1/devices/sessions/:rootSessionId` —— S-4 绑定读面(壳换绑 UX 的全部材料): * `{ rootSessionId, boundDevice: { deviceId, rev, boundAt } }`。稿 §4 允许「并入 session 读面」或 * 「独立读面」两形,选**独立读面**:sessions 的 GET 面承诺「无参请求字节不变」(routes/sessions.ts * 的 additive contract 注),且 audit 投影由 boot/session-faces 组装、不持 device 店 —— 并入即跨 * 装配缝加一次店读;独立读面把 device wire 族收在一个文件里,户口与门序自洽。 * · `POST /v1/devices/sessions/:rootSessionId/rebind` —— 显式换绑动词(v2 稿 §2 门序五步,逐字): * ① gatedPrincipal + owner 门(非属主 404 反枚举同串,[ref] 纪律) * ② 目标设备在场门(不存在 / 不是该域的 / 已吊销 ⇒ 404 **同串** —— 设备 id 也是不可枚举面) * ③ 无活跃 run 门(**CAS 前判、CAS 语句不兼职**;durable 判据 = run store 的 session claim * `getActiveTaskId`,hub 内存在途计数不作门 —— 内存态跨副本不可见;拒 = 409 * `device.rebind_active_run` 体带 `activeTaskId`) * ④ 前态 CAS(店内单事务:`rev` 恒变列 + `session_rebound` 审计行;输 = 409 * `device.rebind_rev_conflict` 体带 `currentRev` —— [ref] currentPending 哲学,壳一跳重定位) * ⑤ 赢 = 200 `{ rootSessionId, deviceId, rev, workspaceCarryover: "none" }`。 * 幂等形:toDeviceId ≡ 当前 ∧ expectedRev ≡ 当前 rev ⇒ 200 回显不加 rev(真幂等,不写审计)。 * `workspaceCarryover: "none"` 是 **wire 冻结字面**(v2 稿 §5):换绑只改路由,不搬字节 —— 壳据 * 此渲「新设备上是全新工作区」的诚实披露;两个 200 形恒带。 * * ── 调用方域(owner domain)的唯一铸造点 ───────────────────────────────────────────────────────── * device lane 的三表自持 O11 复合身份 `{owner_tenant, owner_subject}`(device-store.ts 头注;全局 * principal 一个字节不动)。基线稿 §11-O11 的落法:「认证需在 SSO 校验点把 tenant+subject 传到 device * placement 校验腿」——对位到本仓的真源即:subject = `gatedPrincipal`(verified 链),tenant = * `ssoVerifiedScope`(registry-JWT 的 `scope` claim,security.ts:同为签名真源、非自报头)。两者齐备 * 才构成一个 device 域;缺 tenant claim 的凭据(纯 principal 头 / 静态 token 直连)**判不出域** ⇒ * 非 operator 一律响亮 403(fail-closed + loud,[ref]:静默空列表是假健康)——基线稿对 enrollment 的 * 同向裁定是「缺 tenant/global scope 明确拒注册」。enrollment/管理端点与**首绑写协议**都从 * 这同一个函数取域(S-470 起它住在 `http/device-placement.ts` —— 提交腿也要用,放在路由域里是层序倒挂), * 不得手抄第二份派生。 * * ── 反枚举 404 的单一铸造点 ────────────────────────────────────────────────────────────────────── * 「绑定不在 / 不是你的 / 目标设备不在 / 不是该域的 / 已吊销」五臂共用 {@link sendDeviceNotFound} —— * 判别位在一个函数里就消失,memory `anti-enum-404-ambiguity` 的教训(判别位留在上层 = 迟早有一个 * 调用点忘记归一)在结构上关掉。码/文案与 device-enrollment.ts 的 `rejectNotFound` 同串 * (`not_found.device` + "device not found")。 * * ── 门与失败方向 ──────────────────────────────────────────────────────────────────────────────── * 活跃 run 门的 store 读**不吞错**(server.ts 有两处 `getActiveTaskId(...).catch(() => undefined)`, * 那是观测面;这里是门 —— store 打不通就 500,绝不当「没有活跃 run」放行,[ref])。run store 缺席 = * 501 `capability.run_store_required`(门无判据即拒,不是跳过)。 * * billable = false 全族(纯管理/读面,零模型工作);刻意不吃 drain 503(读面与管理动作在排空期 * 照常有效 —— 换绑本身正是「设备要换了」的运维动作)。错误码登记:附录 A + error-code-catalog-live * (send 点全部写**字面量** + `satisfies DeviceErrorCode` 钉闭集 —— 目录门扫的是字面量,闭集纪律由 * 编译器执行,两道门同时成立;device-store.ts「禁手抄」条的本义是禁**脱离闭集**的字面量)。 */ import type { IncomingMessage, ServerResponse } from "node:http"; import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js"; export declare function handleDevices(req: IncomingMessage, res: ServerResponse, match: RouteMatch, ctx: RouteCtx): Promise; export declare const DEVICES_ROUTES: readonly [{ readonly id: "devices-list"; readonly path: "/v1/devices"; readonly credentialGated: readonly ["GET"]; readonly methods: readonly ["GET"]; }, { readonly id: "device-enroll-tokens"; readonly path: "/v1/devices/enroll-tokens"; readonly credentialGated: readonly ["POST"]; readonly methods: readonly ["POST"]; }, { readonly id: "device-enroll"; readonly path: "/v1/devices/enroll"; readonly credentialGated: readonly ["POST"]; readonly methods: readonly ["POST"]; }, { readonly id: "device-revoke"; readonly pattern: RegExp; readonly label: "/v1/devices/:deviceId/revoke"; readonly methods: readonly ["POST"]; }, { readonly id: "device-session-rebind"; readonly pattern: RegExp; readonly label: "/v1/devices/sessions/:id/rebind"; readonly methods: readonly ["POST"]; }, { readonly id: "device-session"; readonly pattern: RegExp; readonly label: "/v1/devices/sessions/:id"; readonly credentialGated: readonly ["GET"]; readonly methods: readonly ["GET"]; }, { readonly prefix: "/v1/devices/"; readonly credentialGated: readonly ["POST"]; }]; /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */ export type DevicesRouteId = RouteIdsOf; //# sourceMappingURL=devices.d.ts.map