import type { ApprovalHmacKey, PrincipalJwtKey } from "./auth-keys.js"; import { type ApprovalEnvelope } from "./approval-hmac.js"; /** * 验签允许的**时钟不一致容差**(秒)—— 本仓这条轴上的单一来源。 * * 它回答的是**一跳**:签发方(issuer)与判定这份 token 的那口钟可以差多少(exp/nbf 门的宽限)。 * * 🔴 它**不**回答「两台副本之间差多少」:S-280 的消费记录不再靠任何钟差上界活着 —— 受理窗的判定、 * 落地后的复核、到期清扫三处读的是**同一口钟**(消费记录店自己的钟,见 * `plugins/approval-nonce-store.ts` 的 `nowMs()`)。曾经由本常量推导出的那个「副本间钟差」常量已 * 整条删除:它是一条**猜出来的**上界,没有任何一条腿强制它,而正确的修法是根本不需要这个数。 */ export declare const PRINCIPAL_JWT_CLOCK_SKEW_SEC = 30; /** * [ref] D-G: verify a compact JWS principal token. Returns the crypto-verified principal (`sub`) + the * channel-binding (`bnd`) on success, or `null` on ANY failure (caller fail-closes — NEVER falls back to a header). * 🔴 Hardening: the JWS header `alg` MUST equal the kid's PINNED alg (rejects `alg:none`, symmetric, downgrade); * the kid MUST resolve to a configured key; iss/aud/exp/nbf (with a small skew) are enforced; `sub` MUST be present. */ export declare function verifyPrincipalJwt(token: string, keys: PrincipalJwtKey[], expect: { iss: string; aud: string; nowSec?: number; skewSec?: number; maxTtlSec?: number; }): { sub: string; bnd?: string; acceptedBeforeMs: number; } | null; /** The channel-binding value a direct client's principal JWT must carry in `cnf.bnd`: the hex SHA-256 of the * CLIENT-VISIBLE action identity (sessionId + boundCallId + boundInputHash — NOT the unexposed checkpointToken). * Binds the identity assertion to THIS specific suspended action so a captured JWT cannot be lifted onto another. */ /** * 直连门信封**第三槽**的 plan_review 半场:**决定所作用内容的哈希**。 * * 第三槽在两种门上是**同一条规则**「这份决定作用在哪份内容上」:动作门填被批工具调用的 `boundInputHash` * (core 铸);非动作门 `plan_review` 的 `edit` 填**修订后的计划**的哈希(计划随本次请求一起提交,签名器手上 * 就有原文),`approve` / `reject` 不作用于任何新内容 ⇒ null。B-121(test 黑盒发现,验真后修):此前 edit 也填 * null,于是为「只改 README」签出的证明可以拿去提交任意 `editedPlan`——签名只护住了 decision/reason,没护住 * 决定真正落地的那份内容。`cnf.bnd` 与 HMAC 都吃第三槽,所以 JWT 与信封同时绑住它;不匹配 ⇒ * `401 principal_unbound`(既有码,cnf.bnd 不符)。旧注记「计划哈希被拒」说的是 approve(worker 没有可哈希的 * 原文),不覆盖 edit。 * * 形:`sha256(utf8(editedPlan))` hex —— 与 `approvalBnd` 同摘要、同编码;逐字节,不归一化(签名器与 worker * 必须对同一串字节达成一致,任何"归一化"都是第二个写者)。 */ /** 良构 Unicode(无孤立代理项)。`u` 旗下配对代理项是一个码点、不匹配 `\p{Surrogate}`;只有孤立的才命中。 */ export declare function isWellFormedUnicode(s: string): boolean; export declare function editedPlanBinding(editedPlan: string): string; /** * JWT `cnf.bnd` = **「这份身份凭据被签发去决哪一张卡」**(动作身份),故只哈希前三位。 * * 🔴 **与 HMAC 信封的分工**(S-433 F1 立此,属主注在 `approval-hmac.ts` 顶):HMAC 绑的是**「决了什么」** * —— `decision` / `reason` / `permissionModeAfter` 这些**会改变后果**的字段全在那一把里。所以「同一张卡上 * 把 approve 的档偷偷调宽」只由 HMAC 拦;本函数**刻意不跟着加位**:决定内容改了 `cnf.bnd` 不该变(否则 * 一份合法 JWT 会被绑死在一个它签发时还不知道的值上,而 `permissionModeAfter` 恰恰是决裁那一刻才定的)。 */ export declare function approvalBnd(env: { sessionId: string; boundCallId?: string | null; boundInputHash?: string | null; }): string; /** * 一次性凭据:这份证明**必须被消费掉一次**才算数,以及它的消费记录该活到什么时候。 * 只有非动作门(plan_review)的证明带它 —— 动作门的一次性由 checkpointToken 给(见 {@link verifyDirectDoorProof})。 */ export interface SingleUseProof { /** 签名器为这一次决定新铸的随机值(= 信封第 2 位 `boundCallId`,同时进 `cnf.bnd` 与 HMAC)。 */ readonly nonce: string; /** * 这份证明**不再被受理**的时刻(epoch ms,**开区间上界**:`店钟 >= 它` ⇒ 验签已经不收它)。 * 一个值、两处用,所以它们不可能漂: * · 消费记录的到期列逐字落它 —— 清理腿的删除线就是**店钟**,没有任何余量、也不需要; * · 消费**落地之后**的复核线(再问一次店钟)—— 消费是一次跨网络的写,验签发生在它之前。 * * 🔴 「它」与「店钟」必须是同一口钟:本值由 `verifyPrincipalJwt` 从 `exp + skew` 算出,而那一关的 * `nowSec` 由调用方从**店钟**喂进来(`routes/approvals-assistant.ts`)。 */ readonly acceptedBeforeMs: number; } /** {@link verifyDirectDoorProof} 的判决。`singleUse` 在场 = 调用方必须在改任何状态**之前**消费它一次。 */ export type DirectDoorProofResult = { ok: true; principal: string; singleUse?: SingleUseProof; } | { ok: false; status: number; errorCode: string; error: string; }; /** * [ref] D-G: verify the WHOLE direct-door proof for one /decide — the signed principal JWT (identity) + its * cnf.bnd channel-binding + the HMAC envelope (integrity) — and return the CRYPTO-VERIFIED principal, or a typed * reject. 🔴 Fail-closed: every failure rejects (the caller must NOT fall back to the trusted header). Order: * verify principal → bind → mac-present → mac-valid (so a non-operator never gets a MAC oracle, an unverified * caller never reaches the operator list). * * 🔴 `opts.nowSec`(S-280 / 合并复审 R2):受理窗要按**哪口钟**判。非动作门(plan_review)的反重放是 * 一条**消费记录**,而那条记录的生死由**消费记录店的钟**决定 —— 所以那条腿必须把店钟喂进来 * (`floor(店钟/1000)`),否则「还受理 ⇒ 记录还在」就退化成「两台副本的钟差不多」这条没人声明过的假设。 * 缺席 = 本机墙钟,这对动作门(`/decide`)是对的:它的反重放是 checkpointToken 的一次性,不牵扯第二台 * 机器上的一张行。 * * 🔴 `opts.actionBinding`: the boundCallId/boundInputHash action-binding is `policy_ask`- * SPECIFIC (a pending TOOL call). A NON-ACTION gate — `plan_review` (and any review-pause) — `binds NO action` * (core checkpoint-store.ts: pendingAction `{kind:"plan_review"}`, editedPlan is a typed sibling that binds no * action), so the proof correctly DEGRADES to "verify WHO": JWT identity + cnf.bnd channel-binding + the HMAC over * {sessionId, nonce, , decision, reason}. Pass `actionBinding:false` for a non-action gate; the default * (true) keeps the /decide tool-approval contract byte-identical. The THIRD slot carries one rule on both gates — * "the content this decision acts on": the bound tool input for /decide, `editedPlanBinding(editedPlan)` for a * plan_review `edit` (B-121), null for approve/reject. Binding approve/reject to a hash of the EXISTING plan was * rejected (the plan artifact lives in a profile ref store; the worker has no TOCTOU original) — that ruling never * covered `edit`, whose content arrives with the request. */ export declare function verifyDirectDoorProof(proof: { jwt?: string; mac?: string; kid?: string; }, /** 🔴 **命名类型,不是结构等价的内联字面**(S-433 F1 [high] 的**根因**,验真后修):本位此前是一份手抄的 * 五字段内联形 —— 结构类型让**多带一个字段的调用方**照样编译通过,而下面那次手抄再把它**丢掉**,于是 * `approval-hmac.ts` 上新增的签名位在这条唯一的真实验签链上**静默失效**,tsc 一个字都不响。 * 用 {@link ApprovalEnvelope} 之后,信封的形只有**一个**声明点,新增一位自动到达验签器。 */ envelope: ApprovalEnvelope, config: { principalJwtPubkeys: PrincipalJwtKey[]; principalJwtIss?: string; principalJwtAud?: string; principalJwtMaxTtlSec?: number; approvalHmacKeys: ApprovalHmacKey[]; }, opts?: { actionBinding?: boolean; nowSec?: number; }): DirectDoorProofResult; //# sourceMappingURL=principal-jwt.d.ts.map