@tacet-ink/journal-core 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
  ```sh
19
19
  git clone https://github.com/tacet-ink/journal-core.git && cd journal-core
20
20
  npm ci --ignore-scripts
21
- npm run verify # 475 assertions, all green, run against the real modules (no mocks)
21
+ npm run verify # 491 assertions, all green, run against the real modules (no mocks)
22
22
  ```
23
23
 
24
24
  ```ts
@@ -119,7 +119,7 @@ jr3d/jr4d 兩入參數同輸入,域分離由 info 承載;凍結 KAT 兩 blob
119
119
 
120
120
  ```sh
121
121
  npm ci --ignore-scripts
122
- npm run verify # 475 斷言對真模組(禁鏡像;限流單句 UPSERT…RETURNING 直載真 SQLite):roundtrip/AAD 防搬移/
122
+ npm run verify # 491 斷言對真模組(禁鏡像;限流單句 UPSERT…RETURNING 直載真 SQLite):roundtrip/AAD 防搬移/
123
123
  # extractable/時代隔離/跨前綴家族隔離/payload 竄改/RFC 9106 KAT/BIP39 @scure 對照 200 組/
124
124
  # server-side(inboundCipher/validWrappedKey/pickKeyPackage/checkRate/timingSafeEq)+ pinlock 全函式
125
125
  # + 密語正規化 v3 世代(normalizePassphrase/jr4w./jr4d./PH1 v3 鹽域分離+帶內版本化舊契約向量)
@@ -131,7 +131,7 @@ npm run verify # 475 斷言對真模組(禁鏡像;限流單句 UPSERT…RE
131
131
  # +KAT 凍結向量 [17](HKDF info 域世代分離+v3/raw 入口契約四 blob)
132
132
  # +包裹前綴快檢 [13-2](錯前綴試探免付 KDF:五入口行為+計時帽+計數錨)
133
133
  # +CI 供應鏈面 [21](npm ci --ignore-scripts+dependabot runner fromJSON 分流+腳本摘除+NIT-1 錨+r2 MINOR-1 URL 直傳;
134
- # 475 帳=repo 樹——發行包內無 .github=[21] 四條 workflow 錨收斂為 1 顯性 SKIP 行→472,SKIP 顯形非靜默)
134
+ # 491 帳=repo 樹——發行包內無 .github=[21] 四條 workflow 錨收斂為 1 顯性 SKIP 行→480,SKIP 顯形非靜默)
135
135
  # +發行 tarball 治理面 [22](pack 白名單零 open-card-pr.sh)
136
136
  ```
137
137
 
@@ -311,7 +311,7 @@ PH1→PH2 login flow, inbound cipher/package validation and paired key-package s
311
311
  (`auth.ts`; PH2-UNIQUE conflict and session revocation are caller-owned wiring), per-IP
312
312
  fixed-window rate limiting on D1 (`ratelimit.ts`), shared CORS/hash utilities.
313
313
 
314
- **Verification.** `npm run verify` runs 475 assertions against the real modules (no mocks):
314
+ **Verification.** `npm run verify` runs 491 assertions against the real modules (no mocks):
315
315
  roundtrips, AAD tamper-evidence, extractability rules, era isolation, cross-prefix family
316
316
  isolation, payload tampering, RFC 9106 KAT, a 200-vector BIP39 cross-check, and the
317
317
  login ladder-guard behavior suite (external audit #8).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tacet-ink/journal-core",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Zero-knowledge journal core — e2e crypto, passphrase-only auth, transport, ratelimit. The server never sees plaintext.",
5
5
  "homepage": "https://tacet.ink",
6
6
  "repository": {
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * verify-core-crypto.ts — core 抽取的驗證閘(對真模組,禁鏡像——extractable 教訓)。
3
3
  * 執行:node --experimental-strip-types scripts/verify-core-crypto.ts
4
- * 全綠輸出 CORE-CRYPTO-OK;任何失敗 exit 1。
4
+ * 全綠輸出 CORE-CRYPTO-VERIFY-OK(N assertions)(整行=唯一消費錨——下游驗收條款禁前綴子字串
5
+ * 比對,防段數漂移誤判;--only 態改 CORE-CRYPTO-VERIFY-OK(N assertions)(--ONLY 16-18) 顯形);任何失敗 exit 1。
6
+ * [24] 段級運行(2026-10-09):`--only 16,17,18`/`--only 16-18`(或混形)=只跑指定段;setup(頂層向量群+段標記行)恆跑;
7
+ * 段依賴常數由 setup 重生(無跨段狀態依賴——各段消費自己的區塊常數)。省時是這卡的真價值:審查輪重跑 [16]-[18] 不再等 KDF 群全套。
5
8
  *
6
9
  * 2026-10-09(0.2.4):[23] loginRouteCore ladder 查表守衛段(外審 #8 幽靈帳)——真 node:sqlite
7
10
  * shim 直驅七情境(現值 hit/ladder hit 回舊帳/兩面 miss 建幽靈/未配置單查零變/fail-open)
@@ -129,32 +132,148 @@ let passed = 0;
129
132
  const failures: string[] = [];
130
133
  async function A(name: string, cond: boolean | Promise<boolean>, detail = ''): Promise<void> {
131
134
  const ok = cond instanceof Promise ? await cond : cond;
132
- if (ok) { passed++; console.log(` ✓ ${name}`); }
133
- else { failures.push(name); console.error(` ✗ ${name}${detail ? ' — ' + detail : ''}`); }
135
+ if (ok) { passed++; secRan.set(secCur, (secRan.get(secCur) ?? 0) + 1); console.log(` ✓ ${name}`); }
136
+ else { failures.push(name); secRan.set(secCur, (secRan.get(secCur) ?? 0) + 1); console.error(` ✗ ${name}${detail ? ' — ' + detail : ''}`); }
134
137
  }
135
138
 
139
+ // ── [24] runner:段級 --only 運行(2026-10-09,t_3ec0936e)——段號語法契約+帳面誠實契約的主場 ──
140
+ // 語法契約([24] parse 面自證):--only <CSV>|<a-b>|<混形>;輸出單調遞增去重(16,17,18 ≡ 16-18);
141
+ // 空值/非數字/未定段/倒序範圍=ERR_ONLY_* fail-closed exit 1。--only 缺席=預設恆全跑(CI/deploy 契約不動)。
142
+ // 帳面契約:--only 態總帳顯形「本輪 X/選段帳 N(N 案非本輪載)」+OK 標記帶 (--ONLY …) 尾碼;skip 段帳面恆列「未跑非通過」;[24]/[25] runner 自證段隨每輪在場。
143
+ // 段標記 helper(secOpen):全跑態輸出位元組恆等原 console.log('\n[N] …');--only 態僅選中段輸出段頭。
144
+ // 毒化矩陣內層 rerun(execFileSync+POISON_GATE_INNER=1)不傳遞 --only=內層恆全帳(POISON_GATE_INNER 語意不變)。
145
+ // 段帳=執行帳(healthy-repo 全跑 runtime 真值,2026-10-10 實測 483);帳面語意契約(逐段「案 X/帳」
146
+ // 對 secRan 執行值顯形):執行 < 帳 的段=本環境條件態未行使(非 skip 態),--only 期望算術對齊本帳。
147
+ // [23] 執行 18 帳 21=[23]④ lookup 未配置(0.2.3 單查恆等)/[23]⑤ fail-open(表缺席)兩情境
148
+ // 在本機樹由 [8] PH1_v2_SALT 訊號面真行使(try/catch 異常面)——車道分流非帳面虛報。
149
+ // secEnter 跑行為脫鉤 secCases;secCases 鍵只承載段存在/帳面/--only 判準的登記義務
150
+ //(secOpen(22,…) 無段標記行=慣例 skip 段)。缺帳防線(teeth):secRan 實跑段必已登記——
151
+ // for (const [k] of secRan) if (!secCases[k] && k !== 22) FAIL;反向缺席段=帳面誠實「案 0/N · skip」。
152
+ // secCases 只餘 [24]/[25] self-account 與 --only 段存在面+teeth 三載體(段帳由 secRan 執行自記)。
153
+ const secCases: Record<number, number> = { 1: 3, 2: 7, 3: 9, 4: 4, 5: 3, 6: 20, 7: 27, 8: 8, 9: 22, 10: 5, 11: 41, 12: 50, 13: 39, 14: 18, 15: 59, 16: 15, 17: 14, 18: 39, 19: 17, 20: 47, 21: 10, 23: 18, 24: 5, 25: 3 };
154
+ function parseOnly(spec: string): { err: string | null; set: number[] } {
155
+ const set = new Set<number>();
156
+ const specStr = spec.trim();
157
+ if (specStr === '') return { err: 'ERR_ONLY_SPEC_EMPTY', set: [] };
158
+ for (const rawPart of specStr.split(',')) {
159
+ const part = rawPart.trim();
160
+ if (part === '') return { err: 'ERR_ONLY_SPEC_EMPTY_TOKEN', set: [] };
161
+ const span = part.match(/^([0-9]+)-([0-9]+)$/);
162
+ if (span) {
163
+ const lo = Number(span[1]); const hi = Number(span[2]);
164
+ if (!(hi >= lo) || !secCases[lo] || !secCases[hi]) return { err: 'ERR_ONLY_SPEC_RANGE', set: [] };
165
+ for (let s = lo; s <= hi; s++) if (secCases[s]) set.add(s);
166
+ continue;
167
+ }
168
+ if (!/^[0-9]+$/.test(part)) return { err: 'ERR_ONLY_SPEC_TOKEN', set: [] };
169
+ const num = Number(part);
170
+ if (!secCases[num]) return { err: 'ERR_ONLY_SPEC_UNKNOWN_SEC', set: [] };
171
+ set.add(num);
172
+ }
173
+ return { err: null, set: [...set].sort((a, b) => a - b) };
174
+ }
175
+ const argvArr = (globalThis as unknown as { process?: { argv?: string[] } }).process?.argv ?? [];
176
+ const argvOnlyIdx = argvArr.indexOf('--only');
177
+ const onlySpecRaw = argvOnlyIdx >= 0 ? argvArr[argvOnlyIdx + 1] ?? '' : null;
178
+ const onlyParsed = onlySpecRaw !== null ? parseOnly(onlySpecRaw) : null;
179
+ if (onlyParsed?.err) {
180
+ console.error('FAIL: ' + onlyParsed.err + ' — --only 段號語法契約:--only 16,17,18(CSV)/--only 16-18(範圍)/混形 16,17-19;--only 缺席=全跑。');
181
+ process.exit(1);
182
+ }
183
+ const onlySet = new Set<number>(onlyParsed?.set ?? []);
184
+ const onlyMode = (): boolean => onlySet.size > 0;
185
+ // runner 自證段(24/25)隨每輪在場(毫秒級;only 態帳面顯形契約的行使面——非選段制)。
186
+ // 全跑態 secEnter 恆 true(n ≥1 且 n ≠ 22——secOpen 行在場即實段;secCases 人工帳冊退役=
187
+ // 靜默不跑面收口:新段只加 secOpen 即跑,段帳由 secRan 自記)。22=慣例 skip 段(bannerless
188
+ // 斷言群帳入 [13]的慣例面;[24] 契約明文在案);n < 1=非法(非段位)。--only 態照選段集+自證段。
189
+ const secEnter = (n: number): boolean =>
190
+ (!onlyMode() ? (n >= 1 && n !== 22) : (onlySet.has(n) || n === 24 || n === 25));
191
+ const onlySpecDisp = (): string => (onlyMode() ? (onlyParsed?.set ?? []).join(',') : '');
192
+ const rangeCompress = (nums: number[]): string => {
193
+ const out: string[] = [];
194
+ let i = 0;
195
+ while (i < nums.length) {
196
+ let j = i;
197
+ while (j + 1 < nums.length && nums[j + 1] === nums[j] + 1) j++;
198
+ out.push(nums[i] === nums[j] ? String(nums[i]) : nums[i] + '-' + nums[j]);
199
+ i = j + 1;
200
+ }
201
+ return out.join(',');
202
+ };
203
+ const onlyTag = (): string => (onlyMode() ? '(--ONLY ' + rangeCompress(onlyParsed?.set ?? []) + ')' : '');
204
+ let secCur = 0;
205
+ const secRan = new Map<number, number>();
206
+ const secOpen = (n: number, text: string): void => {
207
+ secCur = n;
208
+ if (onlyMode() ? onlySet.has(n) : true) console.log(onlyMode() ? text : '\n' + text);
209
+ };
210
+
211
+ // ── setup 恆跑向量群(--only 態也重生——跨段消費常數上移;值與原段位逐位等值,僅求值時點上移)──
212
+ // 依賴帳(--only 分段的承載面——每行原段位標註):note/identityA/held/noteKey/cipherGuest/cipherBound ←[2];
213
+ // pass/recToken/wrapped/salt/wrappedRec/unwrapped/recUnwrapped/localIdentity ←[3];origRaw ←[4];
214
+ // TACET2/passD/pinD/dual ←[6];TACET3/w3/dual3 ←[7];ph1v2a ←[8];bip ←[9];
215
+ // TACET_ATTACH/attachAad/attachPayload/attachCt ←[10];TACET4/pass4/passD4 ←[15]([17]KAT17/[19] 跨段消費)。
216
+ const note = '今天寫了一點東西。';
217
+ const identityA = 'acct-aaaaaaaaaaaaaaaa';
218
+ const held = makeHeldKey();
219
+ const noteKey = await generateNoteKey();
220
+ const cipherGuest = await encryptNote(TACET, held, note, 'noteId:n1', { current: () => identityA }); // held 空態=guest 時代(原 [2] 順序——guest 密文先於 held 設值)
221
+ held.set(noteKey);
222
+ const cipherBound = await encryptNote(TACET, held, note, 'noteId:n1', { current: () => identityA });
223
+ const pass = 'correct-horse-battery-staple-42';
224
+ const recToken = generateRecToken();
225
+ const { wrapped, salt } = await wrapNoteKey(TACET, noteKey, pass);
226
+ const wrappedRec = await (await import('../src/client/note-crypto.ts')).wrapNoteKeyWithRecToken(TACET, noteKey, recToken, identityA);
227
+ const unwrapped = await unwrapNoteKey(TACET, wrapped, pass, salt);
228
+ const recUnwrapped = await unwrapNoteKeyWithRecToken(TACET, wrappedRec, recToken, identityA);
229
+ const origRaw = hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey)));
230
+ const localIdentity = 'acct-localwrap-test';
231
+ const TACET2: NoteCryptoConfig = { ...TACET, wrapDual: 'jr2w.', pinSaltPrefix: 'tacet-note-pin1:' };
232
+ const passD = 'dual-factor-passphrase-42';
233
+ const pinD = '2580ab';
234
+ const dual = await wrapNoteKeyDual(TACET2, noteKey, passD, pinD);
235
+ const TACET3: Argon3Config = { wrap3: 'jr3w.', wrapDual3: 'jr3d.', pinSalt3Prefix: 'tacet-note-pin3:' };
236
+ const w3 = await wrapNoteKey3(TACET3, noteKey, pass);
237
+ const dual3 = await wrapNoteKeyDual3(TACET3, noteKey, passD, pinD);
238
+ const ph1v2a = await derivePh1Argon('probe-determinism-pass-42');
239
+ const bip = await import('../src/client/bip39.ts');
240
+ const TACET_ATTACH: NoteCryptoConfig = { ...TACET, cipherAttach: 'jr1c.' };
241
+ const attachAad = 'jr1a:note-abc:att-001';
242
+ const attachPayload = JSON.stringify({ v: 1, kind: 'img', mime: 'image/jpeg', w: 2048, h: 1365, b64: b64(new Uint8Array(2048)) });
243
+ const attachCt = await encryptAttach(TACET_ATTACH, noteKey, attachPayload, attachAad);
244
+ const TACET4: Argon3Config = { wrap4: 'jr4w.', wrapDual4: 'jr4d.', pinSalt3Prefix: 'tacet-note-pin3:' };
245
+ const guest = await crypto.subtle.importKey('raw', enc.encode('seed').slice(0), 'PBKDF2', false, ['deriveKey']); // 型別哨兵:guest 非此路徑——跨段消費([5]/[11]/[13]/[18])
246
+ void guest;
247
+ const pass4 = 'v3-generation-passphrase-42';
248
+ const passD4 = 'dual-factor-passphrase-42'; // NFKC → passD 的全形形
249
+ // srcOf:源碼窗讀取 helper(原 [13] 段內宣告——跨 [14]-[23] 消費=setup 恆跑群;2026-10-09 段級化揚升)
250
+ async function srcOf(rel: string): Promise<string> {
251
+ // node 內建模組動態存取(structured type,零 node types 依賴——argon2.ts getBuiltinModule 母型同構;
252
+ // TS2591 types 帽下 import('node:fs') 靜態/動態皆炸=此繞法)
253
+ const fs = (globalThis as unknown as {
254
+ process?: { getBuiltinModule?: (id: string) => { readFileSync?: (p: string | URL, enc: string) => string } | undefined };
255
+ }).process?.getBuiltinModule?.('node:fs');
256
+ if (!fs?.readFileSync) throw new Error('ERR_FS_UNAVAILABLE');
257
+ return fs.readFileSync(new URL(rel, import.meta.url), 'utf8');
258
+ }
259
+
260
+ const TACET_GUESTOK: NoteCryptoConfig = { ...TACET }; // cipherGuest='jr1g.' 在場,專測 identity 面
261
+ const emptyIdp = { current: () => '' };
262
+ const dual4 = await wrapNoteKeyDual4(TACET4, noteKey, passD4, pinD); // 全形 wrap=NFKC 載體(cp1=0xFF44——ASCII 化即本面 RED)
136
263
  // ── 1. 基礎向量 ─────────────────────────────────────────────────────────────
137
264
 
138
- console.log('\n[1] PH1 / PBKDF2 / hash-ladder 基礎');
265
+ secOpen(1, '[1] PH1 / PBKDF2 / hash-ladder 基礎'); if (secEnter(1)) {
139
266
  await A('PH1 = SHA-256(pass) 穩定', (await ph1Of('test-pass-123')) === (await ph1Of('test-pass-123')));
140
267
  await A('PH1 hex64 格式', /^[0-9a-f]{64}$/.test(await ph1Of('x')));
141
268
  await A('PBKDF2_ITERATIONS = 600k', PBKDF2_ITERATIONS === 600_000);
142
269
 
143
270
  // ── 2. roundtrip(guest 時代 + 綁定時代,AAD 由呼叫端) ───────────────────────
144
271
 
145
- console.log('\n[2] roundtrip / AAD 防搬移');
146
- const note = '今天寫了一點東西。';
147
- const identityA = 'acct-aaaaaaaaaaaaaaaa';
148
- const guest = await crypto.subtle.importKey('raw', enc.encode('seed').slice(0), 'PBKDF2', false, ['deriveKey']); // 型別哨兵:guest 非此路徑
149
- void guest;
150
- const held = makeHeldKey();
151
- const cipherGuest = await encryptNote(TACET, held, note, 'noteId:n1', { current: () => identityA });
272
+ };
273
+ secOpen(2, '[2] roundtrip / AAD 防搬移'); if (secEnter(2)) {
152
274
  await A('guest 時代前綴 jr1g.', cipherGuest.startsWith('jr1g.'));
153
275
  await A('guest roundtrip', (await decryptNote(TACET, held, cipherGuest, 'noteId:n1', { current: () => identityA })) === note);
154
276
 
155
- const noteKey = await generateNoteKey();
156
- held.set(noteKey);
157
- const cipherBound = await encryptNote(TACET, held, note, 'noteId:n1', { current: () => identityA });
158
277
  await A('綁定時代前綴 jr1b.', cipherBound.startsWith('jr1b.'));
159
278
  await A('綁定 roundtrip', (await decryptNote(TACET, held, cipherBound, 'noteId:n1', { current: () => identityA })) === note);
160
279
 
@@ -170,24 +289,19 @@ await A('舊明文原樣返回', (await decryptNote(TACET, held, '純舊明文',
170
289
  // ── 3. 包裹三件套(pass / rec / 本機;2026-09-24 效能節——buildBindPayload 兩段
171
290
  // PBKDF2 600k 已收 Promise.all 並行,node 實測 133-152ms→68-84ms) ──
172
291
 
173
- console.log('\n[3] 金鑰包裹(passphrase / 復原 / 本機)');
174
- const pass = 'correct-horse-battery-staple-42';
175
- const recToken = generateRecToken();
176
- const { wrapped, salt } = await wrapNoteKey(TACET, noteKey, pass);
177
- const wrappedRec = await (await import('../src/client/note-crypto.ts')).wrapNoteKeyWithRecToken(TACET, noteKey, recToken, identityA);
292
+ };
293
+ secOpen(3, '[3] 金鑰包裹(passphrase / 復原 / 本機)'); if (secEnter(3)) {
178
294
  const recTokenHashValue = await recTokenHash(recToken);
179
295
  await A('wrapped 前綴 jr1w.', wrapped.startsWith('jr1w.'));
180
296
  await A('salt = 16B hex', /^[0-9a-f]{32}$/.test(salt));
181
297
  await A('wrappedRec 前綴 jr1w.', wrappedRec.startsWith('jr1w.'));
182
298
  await A('recTokenHash hex64', /^[0-9a-f]{64}$/.test(recTokenHashValue));
183
299
 
184
- const unwrapped = await unwrapNoteKey(TACET, wrapped, pass, salt);
185
300
  await A('pass unwrap 救回 noteKey(同 pass 同 salt)',
186
301
  unwrapped !== null && hex(new Uint8Array(await crypto.subtle.exportKey('raw', unwrapped))) ===
187
302
  hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey))));
188
303
  await A('錯誤 pass unwrap → null', (await unwrapNoteKey(TACET, wrapped, 'wrong-pass', salt)) === null);
189
304
 
190
- const recUnwrapped = await unwrapNoteKeyWithRecToken(TACET, wrappedRec, recToken, identityA);
191
305
  await A('rec unwrap 救回 noteKey',
192
306
  recUnwrapped !== null && hex(new Uint8Array(await crypto.subtle.exportKey('raw', recUnwrapped))) ===
193
307
  hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey))));
@@ -202,14 +316,14 @@ await A('改密語:新包裹解得回同 noteKey',
202
316
  hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey))));
203
317
 
204
318
  // 本機包裹
205
- const localIdentity = 'acct-localwrap-test';
206
319
  const heldLocal = makeHeldKey();
207
320
  heldLocal.set(noteKey);
208
321
  await storeLocalWrap(TACET, localIdentity, noteKey);
209
322
 
210
323
  // ── 4. extractable 鐵律(2026-09-06 iOS 實機炸點回歸) ────────────────────────
211
324
 
212
- console.log('\n[4] noteKey extractable 鐵律');
325
+ };
326
+ secOpen(4, '[4] noteKey extractable 鐵律'); if (secEnter(4)) {
213
327
  const rawNote = crypto.getRandomValues(new Uint8Array(32));
214
328
  const nonExtractable = await crypto.subtle.importKey('raw', rawNote, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
215
329
  let threwNotExtractable = false;
@@ -224,7 +338,6 @@ const extractable = await crypto.subtle.importKey('raw', rawNote, { name: 'AES-G
224
338
  await A('修法機制:extractable key exportKey 等值 roundtrip',
225
339
  hex(new Uint8Array(await crypto.subtle.exportKey('raw', extractable))) === hex(rawNote));
226
340
  // unwrap 出的 key(全部三路徑)都要能再包裹:與 noteKey 原值比對(非 rawNote——那是本段新建隨機值)
227
- const origRaw = hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey)));
228
341
  await A('unwrap 出的 noteKey 可再 exportKey(等值 noteKey)',
229
342
  hex(new Uint8Array(await crypto.subtle.exportKey('raw', unwrapped!))) === origRaw);
230
343
  await A('rec unwrap 出的 noteKey 可再 exportKey(等值 noteKey)',
@@ -232,7 +345,8 @@ await A('rec unwrap 出的 noteKey 可再 exportKey(等值 noteKey)',
232
345
 
233
346
  // ── 5. 時代隔離:guest 與 bound 前綴互不誤判 ─────────────────────────────────
234
347
 
235
- console.log('\n[5] 時代隔離');
348
+ };
349
+ secOpen(5, '[5] 時代隔離'); if (secEnter(5)) {
236
350
  await A('guest cipher 不以 jr1b. 開頭', !cipherGuest.startsWith('jr1b.'));
237
351
  await A('bound cipher 不以 jr1g. 開頭', !cipherBound.startsWith('jr1g.'));
238
352
  await A('無 held key 的 bound 解密 → null',
@@ -240,11 +354,8 @@ await A('無 held key 的 bound 解密 → null',
240
354
 
241
355
  // ── 6. 雙因子合鑰(jr2w.,2026-09-09 PIN 第二因子) ─────────────────────────
242
356
 
243
- console.log('\n[6] 雙因子合鑰 KEK2(jr2w.)');
244
- const TACET2: NoteCryptoConfig = { ...TACET, wrapDual: 'jr2w.', pinSaltPrefix: 'tacet-note-pin1:' };
245
- const passD = 'dual-factor-passphrase-42';
246
- const pinD = '2580ab';
247
- const dual = await wrapNoteKeyDual(TACET2, noteKey, passD, pinD);
357
+ };
358
+ secOpen(6, '[6] 雙因子合鑰 KEK2(jr2w.)'); if (secEnter(6)) {
248
359
  await A('wrapped2 前綴 jr2w.', dual.wrapped.startsWith('jr2w.'));
249
360
  await A('dual salt1 = 16B hex', /^[0-9a-f]{32}$/.test(dual.salt));
250
361
  const unwrapped2 = await unwrapNoteKeyDual(TACET2, dual.wrapped, passD, pinD, dual.salt);
@@ -293,13 +404,12 @@ await A('未配置 wrapDual 拒絕 dual 包裹', (await unwrapNoteKeyDual(TACET,
293
404
  // Argon2id 記憶體困難(64MiB)使每猜成本上升 1-2 個數量級。遷移契約 = 成功 unwrap
294
405
  // 後客戶端靜默 re-wrap,舊前綴照解(下面有並存斷言)。
295
406
 
296
- console.log('\n[7] Argon2id 包裹(jr3w./jr3d.)');
297
- const TACET3: Argon3Config = { wrap3: 'jr3w.', wrapDual3: 'jr3d.', pinSalt3Prefix: 'tacet-note-pin3:' };
407
+ };
408
+ secOpen(7, '[7] Argon2id 包裹(jr3w./jr3d.)'); if (secEnter(7)) {
298
409
  await A('RFC 9106 無 secret/ad 標準向量(當前載體正確性)', await verifyArgonKat());
299
410
  await A('Argon 參數契約 m=64MiB t=3 p=1 tag=32',
300
411
  ARGON_MEMORY_KIB === 65536 && ARGON_ITERATIONS === 3 && ARGON_PARALLELISM === 1 && ARGON_TAG_LEN === 32);
301
412
 
302
- const w3 = await wrapNoteKey3(TACET3, noteKey, pass);
303
413
  await A('wrapped 前綴 jr3w.', w3.wrapped.startsWith('jr3w.'));
304
414
  await A('jr3w salt = 16B hex', /^[0-9a-f]{32}$/.test(w3.salt));
305
415
  const unwrapped3 = await unwrapNoteKey3(TACET3, w3.wrapped, pass, w3.salt);
@@ -323,7 +433,6 @@ await A('jr3w payload 竄改 iv 區 → null', await tampered3(3));
323
433
  await A('jr3w payload 竄改 ct 尾 → null', await tampered3(payload3.length - 1));
324
434
 
325
435
  // jr3d 雙因子(PIN 第二因子,Argon2id 版)
326
- const dual3 = await wrapNoteKeyDual3(TACET3, noteKey, passD, pinD);
327
436
  await A('dual3 前綴 jr3d.', dual3.wrapped.startsWith('jr3d.'));
328
437
  await A('dual3 salt1 = 16B hex', /^[0-9a-f]{32}$/.test(dual3.salt));
329
438
  const unwrappedD3 = await unwrapNoteKeyDual3(TACET3, dual3.wrapped, passD, pinD, dual3.salt);
@@ -360,9 +469,9 @@ await A('未配置 wrapDual3 → unwrap 拒收 jr3d',
360
469
  // 每猜成本從 ~ns(SHA-256×2)升到 ~0.1-0.18s CPU=×10⁴-10⁶。
361
470
  // 固定域鹽是被迫設計(per-user 鹽會摧毀 PH2 UNIQUE=幽靈帳號機制基礎);改參數 = 換鹽尾碼(v2/v3)重遷移。
362
471
 
363
- console.log('\n[8] PH1 v2(登入憑證 Argon2id 派生)');
472
+ };
473
+ secOpen(8, '[8] PH1 v2(登入憑證 Argon2id 派生)'); if (secEnter(8)) {
364
474
  await A('RFC 9106 KAT 先行(載體正確性,本節所有 Argon 斷言的前提)', await verifyArgonKat());
365
- const ph1v2a = await derivePh1Argon('probe-determinism-pass-42');
366
475
  await A('ph1v2 確定性(同 pass 兩跑逐位元同)', ph1v2a === (await derivePh1Argon('probe-determinism-pass-42')));
367
476
  await A('ph1v2 hex64 形(validHash64 可收)', /^[0-9a-f]{64}$/.test(ph1v2a));
368
477
  // 與 legacy ph1(SHA-256 形)必然不同值=雙查表兩鍵語意成立
@@ -384,8 +493,8 @@ await A('ph1v2 非恆等(pass ≠ 輸出)', ph1v2a !== 'probe-determinism-pa
384
493
 
385
494
  // ── 9. BIP39 復原套件契約(words↔hex64 轉寫層;線上契約 recToken hex64 不變) ──
386
495
 
387
- console.log('\n[9] BIP39 復原套件(24 詞 ⇄ entropy 32B ⇄ hex64)');
388
- const bip = await import('../src/client/bip39.ts');
496
+ };
497
+ secOpen(9, '[9] BIP39 復原套件(24 詞 ⇄ entropy 32B ⇄ hex64)'); if (secEnter(9)) {
389
498
  const wl = await import('../src/client/wordlist.ts');
390
499
 
391
500
  const kitWords = await bip.generateBip39Words();
@@ -483,16 +592,13 @@ await A('words 派生 hex64 unwrap 救回 noteKey',
483
592
 
484
593
  // ── 10. 附件密文(jr1c.,image attachments;opt-in 未配置即拒) ───────────────
485
594
 
486
- console.log('\n[10] 附件密文(jr1c. cipherAttach opt-in)');
595
+ };
596
+ secOpen(10, '[10] 附件密文(jr1c. cipherAttach opt-in)'); if (secEnter(10)) {
487
597
  const bareCfgAttach: NoteCryptoConfig = { ...TACET }; // 無 cipherAttach 欄
488
598
  let attachCfgThrow = '';
489
599
  try { await encryptAttach(bareCfgAttach, noteKey, '{"v":1}', 'jr1a:n1:a1'); } catch (e) { attachCfgThrow = (e as Error).message; }
490
600
  await A('未配置 cipherAttach → encryptAttach throw', attachCfgThrow === 'ERR_ATTACH_NOT_CONFIGURED', attachCfgThrow);
491
601
 
492
- const TACET_ATTACH: NoteCryptoConfig = { ...TACET, cipherAttach: 'jr1c.' };
493
- const attachAad = 'jr1a:note-abc:att-001';
494
- const attachPayload = JSON.stringify({ v: 1, kind: 'img', mime: 'image/jpeg', w: 2048, h: 1365, b64: b64(new Uint8Array(2048)) });
495
- const attachCt = await encryptAttach(TACET_ATTACH, noteKey, attachPayload, attachAad);
496
602
  await A('附件前綴 jr1c.', attachCt.startsWith('jr1c.'));
497
603
  await A('附件 roundtrip(顯式 noteKey)',
498
604
  (await decryptAttach(TACET_ATTACH, noteKey, attachCt, attachAad)) === attachPayload);
@@ -503,7 +609,8 @@ await A('附件錯鑰匙 → null',
503
609
 
504
610
  // ── 11. 本機 IDB 密文(jr1d. cipherLocal opt-in)+ guest 選配拒絕面 ──────────
505
611
 
506
- console.log('\n[11] 本機 IDB 密文(jr1d. cipherLocal opt-in)+ guest 選配拒絕面');
612
+ };
613
+ secOpen(11, '[11] 本機 IDB 密文(jr1d. cipherLocal opt-in)+ guest 選配拒絕面'); if (secEnter(11)) {
507
614
  // 分享包裹(jrsw.,wrapShare 選配)+ jr3s. roundtrip(閘本有 roundtrip 斷言缺席)
508
615
  const TACET_SHARE: NoteCryptoConfig = { ...TACET, wrapShare: 'jrsw.' };
509
616
  const shareWrapped = await wrapNoteKeyShare(TACET_SHARE, noteKey, 'share-pass-42');
@@ -612,8 +719,6 @@ await A('既有 guest 配置 roundtrip 不受選配化影響',
612
719
  (await decryptNote(TACET, makeHeldKey(), cipherGuest, 'noteId:n1', { current: () => identityA })) === note);
613
720
 
614
721
  // guest 空 identity 拒絕面:K_u = SHA-256(prefix ‖ '') 靜默產出「空帳號金鑰」是誤配炸彈
615
- const TACET_GUESTOK: NoteCryptoConfig = { ...TACET }; // cipherGuest='jr1g.' 在場,專測 identity 面
616
- const emptyIdp = { current: () => '' };
617
722
  let emptyIdMsg: string | null = null;
618
723
  try { await encryptNote(TACET_GUESTOK, makeHeldKey(), note, 'noteId:n1', emptyIdp); } catch (e) { emptyIdMsg = (e as Error).message; }
619
724
  await A("guest 加密面空 identity → throw ERR_NO_IDENTITY(falsy 一體:空字串/undefined/null)",
@@ -643,7 +748,8 @@ await A("畸形 guest 前綴 '' → decryptNote 舊明文相容層同「未配
643
748
  // ── 12. 伺服器端原語(makeInboundCipher/validWrappedKey/pickKeyPackage/
644
749
  // checkRate/timingSafeEq)+ 本機鎖定全函式(pinlock jr1p.) ──────────
645
750
 
646
- console.log('\n[12] 伺服器端原語(auth/ratelimit/hash)+ 本機鎖定(pinlock)');
751
+ };
752
+ secOpen(12, '[12] 伺服器端原語(auth/ratelimit/hash)+ 本機鎖定(pinlock)'); if (secEnter(12)) {
647
753
  // ratelimit 行為面(UPSERT…RETURNING 單句設計收口):真 node:sqlite 直載 UPSERT…RETURNING 單句,
648
754
  // 行為斷言全數「真生產函式 checkRate 直驅」——D1 形 shim 包 node:sqlite 只做 D1 run() 的
649
755
  // {results} 回帶形,無測試端 glue 副本。表名直插 SQL=注入面驗證;放行判準 count<=max
@@ -875,7 +981,8 @@ await A('jr1p 跨家族:jr1p. 字串餵 jr1w unwrap → null',
875
981
 
876
982
  // ── 13. 新契約面(hex fail-closed/salt hex 形收口/PH1 鹽注入/錯誤碼語意分離) ──
877
983
 
878
- console.log('\n[13] unwrap 驗證統一新契約(fail-closed + 語意分離)');
984
+ };
985
+ secOpen(13, '[13] unwrap 驗證統一新契約(fail-closed + 語意分離)'); if (secEnter(13)) {
879
986
  // hexToBytes fail-closed(note-crypto 與 bip39 同步——bip39 面是 HEX64_RE 前置自守)
880
987
  await A('hexToBytes 合法輸出不變(舊碼同值;新向量)', hexToBytes('00ff10').join(',') === '0,255,16');
881
988
  await A('hexToBytes 大小寫正規化(新行為,舊碼同值)', (() => { const a = Array.from(hexToBytes('AB')); const b = Array.from(hexToBytes('ab')); return a.length === b.length && a.every((v, i) => v === b[i]); })());
@@ -1033,15 +1140,6 @@ await A('wrapNoteKeyDual3 未配置 wrapDual3 → ERR_JR3W_NOT_CONFIGURED(契
1033
1140
  // PH1 鹽注入:預設舊值零行為變更
1034
1141
  await A('derivePh1Argon 預設鹽零行為變更(同 pass 同值)',
1035
1142
  (await derivePh1Argon('probe-determinism-pass-42')) === ph1v2a);
1036
- async function srcOf(rel: string): Promise<string> {
1037
- // node 內建模組動態存取(structured type,零 node types 依賴——argon2.ts getBuiltinModule 母型同構;
1038
- // TS2591 types 帽下 import('node:fs') 靜態/動態皆炸=此繞法)
1039
- const fs = (globalThis as unknown as {
1040
- process?: { getBuiltinModule?: (id: string) => { readFileSync?: (p: string | URL, enc: string) => string } | undefined };
1041
- }).process?.getBuiltinModule?.('node:fs');
1042
- if (!fs?.readFileSync) throw new Error('ERR_FS_UNAVAILABLE');
1043
- return fs.readFileSync(new URL(rel, import.meta.url), 'utf8');
1044
- }
1045
1143
  await A('derivePh1Argon 預設分支源碼面 = encode(PH1_V2_SALT)(鹽預設單一真相錨;同-pass 同值斷言對預設值漂移無承載力)',
1046
1144
  /saltArg \?\? new TextEncoder\(\)\.encode\(PH1_V2_SALT\)/.test(await srcOf('../src/client/argon2.ts')));
1047
1145
  await A('derivePh1Argon 自選鹽 → 不同派生值', (await derivePh1Argon('probe-determinism-pass-42', new Uint8Array(12))) !== ph1v2a);
@@ -1095,7 +1193,8 @@ await A('[22] 發行 tarball 零 open-card-pr.sh(治理掃蕩面——pack 白
1095
1193
  // buildBindPayload 串行 133-152ms→並行 68-84ms;dual 串行 234→並行 179ms median;
1096
1194
  // b64 4MiB 132-181ms→18ms。瀏覽器實測 222ms→27ms。
1097
1195
 
1098
- console.log('\n[14] 效能形契約(buildBindPayload 並行/b64 分塊/argon 禁並行/CORS Max-Age/sideEffects)');
1196
+ };
1197
+ secOpen(14, '[14] 效能形契約(buildBindPayload 並行/b64 分塊/argon 禁並行/CORS Max-Age/sideEffects)'); if (secEnter(14)) {
1099
1198
 
1100
1199
  const noteCryptoSrc14 = await srcOf('../src/client/note-crypto.ts');
1101
1200
  const argon2Src14 = await srcOf('../src/client/argon2.ts');
@@ -1236,13 +1335,11 @@ await A('[14] corsResponse 仍完整轉發四頭(corsResponse 面帶 Max-Age
1236
1335
  // 函式體零改=raw 契約永久不變——本節同字面雙家族對照向量(PASS12x/PASS12x)就是
1237
1336
  // 「免換前綴合約證明」的承載面:同組字串在舊家族恆異 KEK(raw 直派生)、在 v3 家族恆同 KEK(NFKC 收容)。
1238
1337
 
1239
- console.log('\n[15] 密語正規化 v3 世代(normalizePassphrase/jr4w./jr4d./PH1 v3)');
1338
+ };
1339
+ secOpen(15, '[15] 密語正規化 v3 世代(normalizePassphrase/jr4w./jr4d./PH1 v3)'); if (secEnter(15)) {
1240
1340
 
1241
1341
  const noteCryptoSrc15 = await srcOf('../src/client/note-crypto.ts');
1242
1342
  const argon2Src15 = await srcOf('../src/client/argon2.ts');
1243
- const TACET4: Argon3Config = { wrap4: 'jr4w.', wrapDual4: 'jr4d.', pinSalt3Prefix: 'tacet-note-pin3:' };
1244
- const pass4 = 'v3-generation-passphrase-42';
1245
- const passD4 = 'dual-factor-passphrase-42'; // NFKC → passD 的全形形
1246
1343
  const passD4_NFKC = normalizePassphrase(passD4); // 與 passD 恆等(NFKC 收容)——P2 毒化翻面本體(毒形即收口形計數錨翻+毒翻 = NFKC-effective 帳面翻,雙承載)
1247
1344
 
1248
1345
  // 函式契約面(單一真相本體:normalizePassphrase 與 normalizePin 並存不混)
@@ -1275,7 +1372,6 @@ await A('jr4w 錯 pass → null', (await unwrapNoteKey4(TACET4, w4.wrapped, 'wro
1275
1372
  await A('jr4w 錯 salt → null', (await unwrapNoteKey4(TACET4, w4.wrapped, pass4, 'ab'.repeat(16))) === null);
1276
1373
 
1277
1374
  // jr4d. 雙因子(v3 密語+PIN 照舊)——wrap 端吃全形(NFKC 載體),unwrap 端兩向都咬
1278
- const dual4 = await wrapNoteKeyDual4(TACET4, noteKey, passD4, pinD); // 全形 wrap=NFKC 載體(cp1=0xFF44——ASCII 化即本面 RED)
1279
1375
  await A('dual4 前綴 jr4d.', dual4.wrapped.startsWith('jr4d.'));
1280
1376
  await A('dual4 salt1 = 16B hex', /^[0-9a-f]{32}$/.test(dual4.salt));
1281
1377
  const unwrappedD4 = await unwrapNoteKeyDual4(TACET4, dual4.wrapped, passD, pinD, dual4.salt);
@@ -1413,7 +1509,8 @@ await A('index barrel 匯出 v3 世代七識別字(normalizePassphrase/jr4
1413
1509
  // v3 家族入口 normalize 收口形恰 3×3+PBKDF2 raw 恰 1——毒化形即計數變異,本 repo 樹直接斷言。
1414
1510
  // 每案結束後還原/清樹(出生即棄);designated 面帳寫在案例名內。
1415
1511
 
1416
- console.log('\n[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒)');
1512
+ };
1513
+ secOpen(16, '[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒)'); if (secEnter(16)) {
1417
1514
  {
1418
1515
  const getBuiltin = (id: string): unknown =>
1419
1516
  (globalThis as unknown as { process?: { getBuiltinModule?: (i: string) => unknown } }).process?.getBuiltinModule?.(id);
@@ -1641,6 +1738,7 @@ console.log('\n[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒
1641
1738
  }
1642
1739
  }
1643
1740
  }
1741
+ };
1644
1742
 
1645
1743
  // ── 17. KAT 凍結向量:HKDF info 域世代分離(修正承載面) ──────────────
1646
1744
  //
@@ -1679,7 +1777,7 @@ const KAT17 = {
1679
1777
  {
1680
1778
  const katRawUnlock = async (key: CryptoKey | null): Promise<boolean> =>
1681
1779
  key !== null && (await hex(new Uint8Array(await crypto.subtle.exportKey('raw', key)))) === KAT17.raw;
1682
- console.log('\n[17] KAT 凍結向量:HKDF info 域世代分離+v3/raw 入口契約(jr3w./jr3d./jr4w./jr4d. 四 blob)');
1780
+ secOpen(17, '[17] KAT 凍結向量:HKDF info 域世代分離+v3/raw 入口契約(jr3w./jr3d./jr4w./jr4d. 四 blob)'); if (secEnter(17)) {
1683
1781
  await A('[17] jr3w 凍結 blob:全形 RAW unwrap = 原始 noteKey(舊家族 raw 契約——KAT 承載)',
1684
1782
  katRawUnlock(await unwrapNoteKey3(TACET3, KAT17.jr3w, KAT17.pass3Raw, KAT17.s3)));
1685
1783
  await A('[17] jr3w 凍結 blob:normalized 慣用形 → null(normalized 面恆拒——raw 族零正規化承載)',
@@ -1708,6 +1806,7 @@ const KAT17 = {
1708
1806
  (await unwrapNoteKeyDual3(TACET3, KAT17.jr4d, passD, KAT17.pin, KAT17.sd4)) === null);
1709
1807
  await A('[17] family isolation:jr3d blob 餵 jr4d 路徑 → null(KEK2 info 域分離對稱面)',
1710
1808
  (await unwrapNoteKeyDual4(TACET4, KAT17.jr3d, passD, KAT17.pin, KAT17.sd3)) === null);
1809
+ };
1711
1810
  }
1712
1811
 
1713
1812
  // ── 18. 本機包裹專用前綴(wrapLocal opt-in+讀舊寫新自癒;jr1l.) ──
@@ -1722,7 +1821,7 @@ const KAT17 = {
1722
1821
  // ③未配置態讀取面:照走舊形 cfg.wrap(行為不變),且不自癒重寫(heal 綁在新前綴面)。
1723
1822
  // KEK 不動=deriveGuestKey(identity 派生、passphrase-free——session 期免重打密語的機制
1724
1823
  // 原樣);本段只界「前綴」面(payload 布局的 KDF/prefix 契約由前綴界定——帶內版本化母型)。
1725
- console.log('\n[18] 本機包裹專用前綴(wrapLocal=jr1l. opt-in+讀舊寫新自癒)');
1824
+ secOpen(18, '[18] 本機包裹專用前綴(wrapLocal=jr1l. opt-in+讀舊寫新自癒)'); if (secEnter(18)) {
1726
1825
  {
1727
1826
  // section 專用 Map store:node 端 localStorage 死位(keys.ts try/catch 恆 null)——stored 面
1728
1827
  // 行為斷言要可觀察可重放,結構 KeyStore 注入(同 cfg 注入母型;TACET 基座 fixture 不動)。
@@ -1984,6 +2083,7 @@ console.log('\n[18] 本機包裹專用前綴(wrapLocal=jr1l. opt-in+讀舊
1984
2083
  }
1985
2084
  }
1986
2085
  }
2086
+ };
1987
2087
 
1988
2088
  // ── 19. ladder 表與重取鹽面((a+) 案) ──────────────
1989
2089
  //
@@ -1994,7 +2094,7 @@ console.log('\n[18] 本機包裹專用前綴(wrapLocal=jr1l. opt-in+讀舊
1994
2094
  // 直驅 makePh2LadderStore:upsert 恰一列、同一舊值最新持有者勝、無 DELETE 語句面)。
1995
2095
  // 表名/kind 常數的契約面(PH2_LADDER_TABLE='ph2_ladder'、KIND legacy/v2)由本段斷言直接咬住
1996
2096
  //(fork migration 0011 與 core 原語同字面——漂移=fork schema 與 core 原語脫鉤=表缺席 fail-open 面)。
1997
- console.log('\n[19] ladder 表與重取鹽面(unwrapNoteKeyDual4WithSalt+ph2_ladder)');
2097
+ secOpen(19, '[19] ladder 表與重取鹽面(unwrapNoteKeyDual4WithSalt+ph2_ladder)'); if (secEnter(19)) {
1998
2098
  {
1999
2099
  const { DatabaseSync } = (globalThis as unknown as {
2000
2100
  process?: { getBuiltinModule?: (id: string) => { DatabaseSync?: unknown } };
@@ -2087,6 +2187,7 @@ console.log('\n[19] ladder 表與重取鹽面(unwrapNoteKeyDual4WithSalt+ph2
2087
2187
  await A('[19] WithSalt 未配置 cfg(wrapDual4 缺)→ null(opt-in 鐵律同向)',
2088
2188
  (await unwrapNoteKeyDual4WithSalt({ pinSalt3Prefix: 'tacet-note-pin3:' }, rewrapD4.wrapped, passD, pinD, rewrapD4.salt)) === null);
2089
2189
  }
2190
+ };
2090
2191
  // ── 20. 復原套件專用前綴+HKDF 派生世代 ──
2091
2192
  //
2092
2193
  // 裁定:採搭車——wrappedRec 專用前綴+KEK_rec 改
@@ -2108,7 +2209,7 @@ console.log('\n[19] ladder 表與重取鹽面(unwrapNoteKeyDual4WithSalt+ph2
2108
2209
  // 涵蓋;摘雙試=遷移率裁定非日曆)。兩欄皆缺席(舊 cfg)=單試舊面(行為不變)。
2109
2210
  // ⑤四象限 write→read 矩陣(檢查對齊 M1):每象限「自 wrap 可自 unwrap 或 wrap 拒絕」
2110
2211
  // +超集 cfg 可讀子集 blob(升級路徑保護)+B01 歷史缺陷形恆拒(升 KDF=換前綴)。
2111
- console.log('\n[20] 復原套件專用前綴(wrapRec=jr1r. opt-in)+KEK_rec HKDF 世代(recKekHkdf opt-in,兩欄一體配對門)');
2212
+ secOpen(20, '[20] 復原套件專用前綴(wrapRec=jr1r. opt-in)+KEK_rec HKDF 世代(recKekHkdf opt-in,兩欄一體配對門)'); if (secEnter(20)) {
2112
2213
  {
2113
2214
  const recSalt20 = 'a1b2c3d4e5f60718293a4b5c6d7e8f90';
2114
2215
  const TACET5: NoteCryptoConfig = { ...TACET, wrapRec: 'jr1r.', recKekHkdf: 'jr1r.', recKekSalt: recSalt20 };
@@ -2392,6 +2493,7 @@ console.log('\n[20] 復原套件專用前綴(wrapRec=jr1r. opt-in)+KEK_rec
2392
2493
  trees20.length === 6 && trees20.every((t) => !existsSync20(t)));
2393
2494
  }
2394
2495
  }
2496
+ };
2395
2497
 
2396
2498
  // ── 21. CI 供應鏈面(npm ci --ignore-scripts+Dependabot runner 分流)──
2397
2499
  //
@@ -2399,7 +2501,7 @@ console.log('\n[20] 復原套件專用前綴(wrapRec=jr1r. opt-in)+KEK_rec
2399
2501
  // --ignore-scripts 是安裝面防線(postinstall/preinstall/prepare 全免執行);
2400
2502
  // Dependabot PR 走 GitHub-hosted runner 隔離(self-hosted runner 不接機器人分支)。
2401
2503
  // 靜態錨面:workflow YAML 本體的接線(job-if C1 母型+actor 分流+runs-on fromJSON 陣列形)。
2402
- console.log('\n[21] CI 供應鏈面(npm ci --ignore-scripts+dependabot runner 分流)');
2504
+ secOpen(21, '[21] CI 供應鏈面(npm ci --ignore-scripts+dependabot runner 分流)'); if (secEnter(21)) {
2403
2505
  {
2404
2506
  // 環境鍵=.github 目錄存在(存在性鍵非檔名硬讀——r1 MINOR-3):閘 src 檔在 files 白名單
2405
2507
  // 而 .github 不出包——tarball 消費樹硬讀 workflows=未守衛 ENOENT,rc=1 尾段全吞。
@@ -2422,7 +2524,7 @@ console.log('\n[21] CI 供應鏈面(npm ci --ignore-scripts+dependabot runne
2422
2524
  await A('[21] publish.yml:npm ci --ignore-scripts(tag 驅動無 actor 面;typecheck+verify 先行=腳本面零在場)',
2423
2525
  srcPy21.includes('npm ci --ignore-scripts'));
2424
2526
  } else {
2425
- await A('[21] CI 源碼面:.github 缺席環境(tarball 發行樹常態)=顯性 SKIP(workflow 斷言 4 收斂為 1 SKIP 行——消費端計數帳 475−3=472)',
2527
+ await A('[21] CI 源碼面:.github 缺席環境(tarball 發行樹常態)=顯性 SKIP(workflow 斷言 4 收斂為 1 SKIP 行——消費端計數帳 479)',
2426
2528
  true, 'no .github dir — consumption tree face');
2427
2529
  }
2428
2530
  // 摘除自證(t_760f44e8 治理掃蕩補漏;fs 原語缺席環境=顯性 FAIL 形自守衛——非 silent true)
@@ -2447,8 +2549,9 @@ console.log('\n[21] CI 供應鏈面(npm ci --ignore-scripts+dependabot runne
2447
2549
  && !/readFileSync\(new URL\([^)]*\)\.pathname/.test(gateSrc21));
2448
2550
  }
2449
2551
  }
2552
+ };
2450
2553
 
2451
- console.log('\n[23] loginRouteCore ladder 查表守衛(外審 #8 幽靈帳——現值 miss → ladder → 兩面 miss 才 createUser)');
2554
+ secOpen(23, '[23] loginRouteCore ladder 查表守衛(外審 #8 幽靈帳——現值 miss → ladder → 兩面 miss 才 createUser)'); if (secEnter(23)) {
2452
2555
  {
2453
2556
  // getBuiltin23:node 內建模組動態存取(本段自備 helper——[19]/[21] 同構;structured type 零 node types 依賴)
2454
2557
  const getBuiltin23 = (id: string): unknown =>
@@ -2660,7 +2763,7 @@ console.log('\n[23] loginRouteCore ladder 查表守衛(外審 #8 幽靈帳—
2660
2763
  const storeP = {
2661
2764
  findByIdentityQuery: async () => null,
2662
2765
  createUser: async () => 'acct-ghost-23',
2663
- getByUserKey: async () => null,
2766
+ getByUserKey: async (key: string) => (tP.db.prepare(`SELECT ${USER_COLS23} FROM users WHERE account_id = ?`).get(key) as unknown) || null,
2664
2767
  revokeAllSessions: async () => {},
2665
2768
  insertSession: async () => {},
2666
2769
  ladderLookup: async () => 'acct-23old',
@@ -2685,10 +2788,60 @@ console.log('\n[23] loginRouteCore ladder 查表守衛(外審 #8 幽靈帳—
2685
2788
  }
2686
2789
  }
2687
2790
 
2791
+ };
2792
+ secOpen(24, '[24] runner 契約自證(--only 解析契約 fail-closed+段級帳面顯形)'); if (secEnter(24)) {
2793
+ await A('[24] --only 解析契約(全跑態):spec 缺席=onlySet 空集=secEnter 恆真(CI 形契約不變——npm run verify 全帳;--only 態本面恆真跳過=帳面由次面承載)',
2794
+ !onlyMode() ? (onlySpecRaw === null && parseOnly('').err === 'ERR_ONLY_SPEC_EMPTY' && secEnter(1) && secEnter(23)) : true);
2795
+ await A('[24] --only 解析契約(CSV/範圍/混形三形同集合):16,17,18 ≡ 16-18(同集合);16,17-19 展開 16..19;單調遞增去重 18,16-17,16 → [16,17,18]',
2796
+ JSON.stringify(parseOnly('16,17,18').set) === JSON.stringify([16, 17, 18])
2797
+ && JSON.stringify(parseOnly('16-18').set) === JSON.stringify([16, 17, 18])
2798
+ && JSON.stringify(parseOnly('16,17-19').set) === JSON.stringify([16, 17, 18, 19])
2799
+ && JSON.stringify(parseOnly('18,16-17,16').set) === JSON.stringify([16, 17, 18]));
2800
+ await A('[24] --only 解析契約(fail-closed 分流):空值/非數字 token/慣例 skip 段 22/未定段(0/99)/倒序範圍=ERR_ONLY_*(exit 1 由頂層守衛承載——parse 面證 err 分流真值)',
2801
+ parseOnly('').err === 'ERR_ONLY_SPEC_EMPTY' && parseOnly('abc').err === 'ERR_ONLY_SPEC_TOKEN'
2802
+ && parseOnly('0').err === 'ERR_ONLY_SPEC_UNKNOWN_SEC' && parseOnly('22').err === 'ERR_ONLY_SPEC_UNKNOWN_SEC'
2803
+ && parseOnly('99').err === 'ERR_ONLY_SPEC_UNKNOWN_SEC' && parseOnly('2-1').err === 'ERR_ONLY_SPEC_RANGE'
2804
+ && parseOnly('16,,17').err === 'ERR_ONLY_SPEC_EMPTY_TOKEN');
2805
+ await A('[24] 段級帳面顯形契約:全跑態 onlySpecDisp 空+onlyTag 光禿;--only 態 spec CSV 遞增+(--ONLY …) 尾碼形+secRan 本段已入帳',
2806
+ onlyMode() ? ((secRan.get(24) ?? 0) >= 3 && onlySpecDisp() === (onlyParsed?.set ?? []).join(',') && onlyTag().startsWith('(--ONLY ')) : (onlySpecDisp() === '' && onlyTag() === ''));
2807
+ await A('[24] skip 慣例面:secEnter(22)=false(22 慣例 skip 段——secOpen 行缺席+bannerless 斷言群帳入 [13] 的慣例面;secEnter 全跑態 n≥1 且 n≠22 即真=段落帳冊退役)',
2808
+ secEnter(22) === false);
2809
+ }
2810
+
2811
+ secOpen(25, '[25] 段級運行樣本:secEnter 真值表+tag 形帳+secCases 段帳'); if (secEnter(25)) {
2812
+ await A('[25] secEnter 真值表:bannerless 段全跑態行為(26 未在 secOrder 段帳=secRan 零位面——帳面逐段只列 secOrder;n=0 恆 false);全跑態實段 1/15/23 全真',
2813
+ !onlyMode() ? (secEnter(0) === false && (secRan.get(26) ?? 0) === 0 && secEnter(1) && secEnter(15) && secEnter(23)) : true);
2814
+ await A('[25] tag/spec 形帳:--only 態 (--ONLY …) 括形+spec 遞增 CSV 非空;全跑態 spec 空+tag 裸',
2815
+ onlyMode() ? (onlyTag().startsWith('(--ONLY ') && onlyTag().endsWith(')') && onlySpecDisp().length > 0)
2816
+ : (onlySpecDisp() === '' && onlyTag() === ''));
2817
+ await A('[25] secCases 段帳登記義務(負向斷言——teeth):secRan 實跑段(本機樹 [23]⑤ fail-open 表缺席異常面照行)必已登記;secCases 缺鍵且非 22 段=RED(secCases 全摘禁令的行為面承載)',
2818
+ (() => { for (const [k25] of secRan) if (!secCases[k25] && k25 !== 22) return false; return true; })());
2819
+ await A('[25] self-account:secCases[25] = 3 恰此三案(段帳面 self-proving——帳漂移即本面 RED;secCases 載體面:[24]/[25] 自證段+--only 段存在面+teeth 三載體)',
2820
+ secCases[25] === 3 && (secRan.get(25) ?? 0) === 3);
2821
+ }
2822
+
2823
+ // ── 段級帳面(誠實帳——skip 段恆列帳「未跑非通過」;全跑態逐段案數對 secCases 帳面)──
2824
+ const secOrder = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 24, 25];
2825
+ const secTotal = secOrder.reduce((a, n) => a + (secCases[n] ?? 0), 0);
2826
+ const ranSecs = secOrder.filter((n) => (secRan.get(n) ?? 0) > 0);
2827
+ const selTotal = secOrder.filter((n) => !onlyMode() || onlySet.has(n) || n === 24 || n === 25).reduce((a, n) => a + (secCases[n] ?? 0), 0);
2828
+ const skipSecs = secOrder.filter((n) => !ranSecs.includes(n));
2829
+ console.log('\n──── 段級帳面 ────');
2830
+ for (const nSec of secOrder) {
2831
+ const ranN = secRan.get(nSec) ?? 0;
2832
+ const rowLabel = ranSecs.includes(nSec)
2833
+ ? (failures.length === 0 ? 'ok' : '見 ✗(失敗 ' + failures.length + '——詳上)')
2834
+ : 'skip(--only 態未選段——本輪未跑非通過)';
2835
+ console.log('[' + nSec + '] 案 ' + ranN + '/' + (secCases[nSec] ?? 0) + ' · ' + rowLabel);
2836
+ }
2837
+
2688
2838
  console.log(`\n${passed} 斷言全綠` + (failures.length ? `;${failures.length} 失敗` : ''));
2839
+ if (onlyMode()) {
2840
+ console.log('--only 態:段 ' + onlySpecDisp() + ' 運行,本輪 ' + passed + '/' + selTotal + '(含 runner 自證段 ' + ((secCases[24] ?? 0) + (secCases[25] ?? 0)) + ';總帳 ' + secTotal + ' 案非本輪載;skip ' + skipSecs.length + ' 段——本輪未跑非通過)');
2841
+ }
2689
2842
  if (failures.length) {
2690
2843
  console.error('失敗項:', failures);
2691
2844
  process.exit(1);
2692
2845
  }
2693
2846
 
2694
- console.log('CORE-CRYPTO-VERIFY-OK');
2847
+ console.log('CORE-CRYPTO-VERIFY-OK(' + passed + ' assertions)' + onlyTag());
@@ -145,8 +145,9 @@ export interface AuthStore {
145
145
  * ladder hit → getByUserKey → 可續 buildSession),兩面皆 miss 才 createUser。
146
146
  * 未配置(optional undefined)=查表面退場(現行單查行為零變——向後相容:
147
147
  * fork 未配 ladder(0011 未上)時行為與 0.2.3 恆等,sennight/vestige 未來接 core 也零炸)。
148
- * 錯誤處理照 tacet store.ts 母型:ladder 表缺席(0011 未上)=fail-open 視同 miss
149
- *(lookup 實作自行 catch;本函式不吞 store 拋出的非缺席類錯誤——缺席面由 fork 實作裁定)。
148
+ * 錯誤處理照 tacet store.ts 母型:lookup 拋任何錯(0011 未上的 ladder 表缺席、ladder store
149
+ * 停機故障)=fail-open 視同 miss,不炸登入(ladder store 停機中登入=建幽靈——fail-open
150
+ * 語意的已知代價;接線端 ladderGuard 統一 catch 只包 lookup)+非 lookup 錯誤照常外拋。
150
151
  */
151
152
  ladderLookup?(oldPh2: string): Promise<string | null>;
152
153
  /** ladder 入表(遷移線用;upsert冪等)。 */
@@ -19,8 +19,9 @@
19
19
  * 零知識照舊:ladder 只存 ph2 hash 與 account_id,零 PII、零密語材料。
20
20
  *
21
21
  * 職責分工(fork 對位面):表名/migration 由 fork migrations 承載(0011-ph2-ladder.sql,
22
- * PK=ph2 值主權威形);本層提供「以舊 ph2 查列」與「遷移時入表」原語,查表接線
23
- * (現值 miss → ladder → 建幽靈)在 fork route(tacet loginRoute,v2-first 裁定)。
22
+ * PK=ph2 值主權威形);本層提供「以舊 ph2 查列」與「遷移時入表」原語。查表守衛自 0.2.4 起
23
+ * 內建 loginRouteCore(現值 miss → ladder → 兩面 miss 才建幽靈;lookup 未配置=零呼叫
24
+ * 行為零變)——自帶 login route 的 fork(tacet loginRoute,v2-first 裁定)續走自身接線。
24
25
  */
25
26
 
26
27
  import type { Env } from './env.ts';