import type { Pool as MysqlPool } from "mysql2/promise"; import type { Pool as PgPool } from "pg"; import { type SqlDriver } from "./sql-driver.js"; /** 表名(单数形;`test/schema-naming-invariants.test.ts` 的闭集词表钉着)。 */ export declare const APPROVAL_NONCE_TABLE = "approval_nonce"; /** * nonce 的**字符上限**。与 `reason` 的上限同姿势:**拒而不截**(截过的字节签名器复现不了 ⇒ MAC 恒不匹配, * 把一个可诊断的 400 变成费解的 401)。190 = 本仓键列的标准宽度(`VARCHAR(190)`);以 JS 的 UTF-16 长度 * 设限是**保守**方向(星平面字符在 JS 里算 2、在两个引擎的 VARCHAR 里算 1),永远不会撞到列宽。 */ export declare const MAX_APPROVAL_NONCE_CHARS = 190; export interface ApprovalNonceStore { /** * **这只店为它的记录计时的那口钟**(epoch ms)—— 见头注「到期 —— 一口钟」。 * * 它是消费记录这条命上**唯一**的参照:受理窗的判定、落地后的复核、到期清扫三处全读它,于是 * 「还受理 ⇒ 记录还在」是结构性的,不靠「两台副本的钟差不多」这条没人声明过的假设。 * * ⚠️ **要求:单调不减**。同源只挡「两只钟各走各的」,挡不住「这一只往回走」—— 钟回拨会让一条已被 * 清扫的记录所对应的证明重新过验签(头注「由构造」那段的残余,🚧 见 `approval-nonce-store.test.ts`)。 * 现役三实现都**不**自己强制这一条(Memory/File = `Date.now()`,SQL = 库钟),登记在案。 * 🔴 读不出来 ⇒ **响亮抛**([ref] fail-closed):调用方必须把它当不确定失败,**绝不**掉回本地墙钟。 */ nowMs(): Promise; /** * 消费 `(sessionId, nonce)` —— **恰好一次**。 * @param expiresAtMs 这份证明还能被受理的最后一刻(epoch ms,**按 {@link nowMs} 这口钟**)。 * @returns `true` = 本次调用消费掉了它(首次);`false` = 它**已经**被消费过(= 重放)。 */ consume(sessionId: string, nonce: string, expiresAtMs: number): Promise; /** * GC:删掉 `expires_at_ms <= ` **{@link nowMs}** 的记录,返回删除行数(维护 tick 调)。 * * 🔴 **一个时钟入参都没有**,而且这不是省事:形参会让删除线由**跑清理的那台副本**说了算,而它跟 * 验签那台不是同一口钟 —— 那正是本店要消灭的那条依赖(与票据店 [ref] 删掉 `now: () => number` * 同一条律:留着那只形参会让人以为「注一只假钟就能改记录的判决」)。测试要制造过期,把 * `consume` 的 `expiresAtMs` 相对 {@link nowMs} 往前挪即可 —— 那也是生产上唯一能让记录过期的方式。 */ reapExpired(): Promise; } export declare class SqlApprovalNonceStore implements ApprovalNonceStore { private readonly db; constructor(db: SqlDriver); private q; /** 库钟({@link sqlDbNowMs})—— 一组无状态副本唯一共享的那口钟。读不出 ⇒ 响亮抛。 */ nowMs(): Promise; consume(sessionId: string, nonce: string, expiresAtMs: number): Promise; reapExpired(): Promise; } /** MySQL 协议腿的构造绑定(与兄弟店同形:类名/构造元数不变,消费方不必动)。 */ export declare class TiDBApprovalNonceStore extends SqlApprovalNonceStore { constructor(pool: MysqlPool); } /** PostgreSQL 腿的构造绑定。 */ export declare class PgApprovalNonceStore extends SqlApprovalNonceStore { constructor(pool: PgPool); } export declare class MemoryApprovalNonceStore implements ApprovalNonceStore { private readonly rows; /** 单进程腿:店 = 这个进程,所以店钟就是本进程的墙钟(验签、复核、清扫本来就同一只钟,无第二口)。 */ nowMs(): Promise; consume(sessionId: string, nonce: string, expiresAtMs: number): Promise; reapExpired(): Promise; } /** * local 车道的持久腿:**单个**追加日志 + 内存索引 + 到期时的原子压实。 * * 为什么不是「按会话一个文件」(兄弟 `FileApprovalExemptionStore` 的形):那只店的行**随会话长存**, * 按会话分片才能按会话删;本店的行**至多活 ~150 秒**、且从不按会话查询 —— 分片只会在数据根里长出 * 一堆几行就作废的小文件。单文件 + 压实与行的生命周期同构。 * * 崩溃语义:`append` 走 fsync。丢一条记录 = 那个 nonce 在它剩下的 TTL 里可被重放一次(与「整台副本重启」 * 的既有暴露同级,且 SQL 车道不受此影响);**绝不**能出现的是反向 —— 记录还在却被读成不在,那要靠 * `readJsonlRecords` 的断尾安全(半行丢弃)兜住。 */ export declare class FileApprovalNonceStore implements ApprovalNonceStore { private readonly path; private readonly tmpDir; /** key → 盘上那条记录**本身**(不是它的到期时刻):压实要把幸存者原样写回去,而把 `(sessionId, nonce)` * 从复合键里**解析**出来是一条没必要的反向铸形 —— 记录留着就不必解析。 */ private readonly rows; private log; constructor(root: string); private logFor; /** local 车道:店 = 这台机器上的这个数据根,所以店钟就是本进程的墙钟(理由同 Memory 腿)。 */ nowMs(): Promise; consume(sessionId: string, nonce: string, expiresAtMs: number): Promise; reapExpired(): Promise; /** 释放追加 fd(`LocalBackend.close` → 优雅重启重开)。 * 刻意**不包 try/catch**:core 的 `AppendLog.close()` 自己就是全函数(`releaseFd` 内部吞掉 `closeSync` * 的错),再包一层空 catch 只是一段永不执行的静默降级码 —— 兄弟 File 店那一层是历史包袱,不复制。 */ dispose(): void; } //# sourceMappingURL=approval-nonce-store.d.ts.map