@tacet-ink/journal-core 0.2.4 → 0.3.0

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.
@@ -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,149 @@ 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 實測 492——A2 [26] +8);帳面語意契約(逐段「案 X/帳」
146
+ // 對 secRan 執行值顯形):執行 < 帳 的段=本環境條件態未行使(非 skip 態),--only 期望算術對齊本帳。
147
+ // [23] 執行 18 帳 21=[23]④ lookup 未配置(0.2.3 單查恆等)/[23]⑤ fail-open(表缺席)兩情境
148
+ // 在本機樹由 [8](登入憑證 Argon2id 派生)內登入 lane 的訊號面真行使(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(負向斷言本體在 [25] 段
152
+ //——secCases 全摘即 RED);反向缺席段=帳面誠實「案 0/N · skip」。
153
+ // secCases 載體三群:[24]/[25] self-account+--only 段存在面+teeth(段帳由 secRan 執行自記)。
154
+ 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: 4, 26: 8 };
155
+ function parseOnly(spec: string): { err: string | null; set: number[] } {
156
+ const set = new Set<number>();
157
+ const specStr = spec.trim();
158
+ if (specStr === '') return { err: 'ERR_ONLY_SPEC_EMPTY', set: [] };
159
+ for (const rawPart of specStr.split(',')) {
160
+ const part = rawPart.trim();
161
+ if (part === '') return { err: 'ERR_ONLY_SPEC_EMPTY_TOKEN', set: [] };
162
+ const span = part.match(/^([0-9]+)-([0-9]+)$/);
163
+ if (span) {
164
+ const lo = Number(span[1]); const hi = Number(span[2]);
165
+ if (!(hi >= lo) || !secCases[lo] || !secCases[hi]) return { err: 'ERR_ONLY_SPEC_RANGE', set: [] };
166
+ for (let s = lo; s <= hi; s++) if (secCases[s]) set.add(s);
167
+ continue;
168
+ }
169
+ if (!/^[0-9]+$/.test(part)) return { err: 'ERR_ONLY_SPEC_TOKEN', set: [] };
170
+ const num = Number(part);
171
+ if (!secCases[num]) return { err: 'ERR_ONLY_SPEC_UNKNOWN_SEC', set: [] };
172
+ set.add(num);
173
+ }
174
+ return { err: null, set: [...set].sort((a, b) => a - b) };
175
+ }
176
+ const argvArr = (globalThis as unknown as { process?: { argv?: string[] } }).process?.argv ?? [];
177
+ const argvOnlyIdx = argvArr.indexOf('--only');
178
+ const onlySpecRaw = argvOnlyIdx >= 0 ? argvArr[argvOnlyIdx + 1] ?? '' : null;
179
+ const onlyParsed = onlySpecRaw !== null ? parseOnly(onlySpecRaw) : null;
180
+ if (onlyParsed?.err) {
181
+ console.error('FAIL: ' + onlyParsed.err + ' — --only 段號語法契約:--only 16,17,18(CSV)/--only 16-18(範圍)/混形 16,17-19;--only 缺席=全跑。');
182
+ process.exit(1);
183
+ }
184
+ const onlySet = new Set<number>(onlyParsed?.set ?? []);
185
+ const onlyMode = (): boolean => onlySet.size > 0;
186
+ // runner 自證段(24/25)隨每輪在場(毫秒級;only 態帳面顯形契約的行使面——非選段制)。
187
+ // 全跑態 secEnter 恆 true(n ≥1 且 n ≠ 22——secOpen 行在場即實段;secCases 人工帳冊退役=
188
+ // 靜默不跑面收口:新段只加 secOpen 即跑,段帳由 secRan 自記)。22=慣例 skip 段(bannerless
189
+ // 斷言群帳入 [13]的慣例面;[24] 契約明文在案);n < 1=非法(非段位)。--only 態照選段集+自證段。
190
+ const secEnter = (n: number): boolean =>
191
+ (!onlyMode() ? (n >= 1 && n !== 22) : (onlySet.has(n) || n === 24 || n === 25));
192
+ const onlySpecDisp = (): string => (onlyMode() ? (onlyParsed?.set ?? []).join(',') : '');
193
+ const rangeCompress = (nums: number[]): string => {
194
+ const out: string[] = [];
195
+ let i = 0;
196
+ while (i < nums.length) {
197
+ let j = i;
198
+ while (j + 1 < nums.length && nums[j + 1] === nums[j] + 1) j++;
199
+ out.push(nums[i] === nums[j] ? String(nums[i]) : nums[i] + '-' + nums[j]);
200
+ i = j + 1;
201
+ }
202
+ return out.join(',');
203
+ };
204
+ const onlyTag = (): string => (onlyMode() ? '(--ONLY ' + rangeCompress(onlyParsed?.set ?? []) + ')' : '');
205
+ let secCur = 0;
206
+ const secRan = new Map<number, number>();
207
+ const secOpen = (n: number, text: string): void => {
208
+ secCur = n;
209
+ if (onlyMode() ? onlySet.has(n) : true) console.log(onlyMode() ? text : '\n' + text);
210
+ };
211
+
212
+ // ── setup 恆跑向量群(--only 態也重生——跨段消費常數上移;值與原段位逐位等值,僅求值時點上移)──
213
+ // 依賴帳(--only 分段的承載面——每行原段位標註):note/identityA/held/noteKey/cipherGuest/cipherBound ←[2];
214
+ // pass/recToken/wrapped/salt/wrappedRec/unwrapped/recUnwrapped/localIdentity ←[3];origRaw ←[4];
215
+ // TACET2/passD/pinD/dual ←[6];TACET3/w3/dual3 ←[7];ph1v2a ←[8];bip ←[9];
216
+ // TACET_ATTACH/attachAad/attachPayload/attachCt ←[10];TACET4/pass4/passD4 ←[15]([17]KAT17/[19] 跨段消費)。
217
+ const note = '今天寫了一點東西。';
218
+ const identityA = 'acct-aaaaaaaaaaaaaaaa';
219
+ const held = makeHeldKey();
220
+ const noteKey = await generateNoteKey();
221
+ const cipherGuest = await encryptNote(TACET, held, note, 'noteId:n1', { current: () => identityA }); // held 空態=guest 時代(原 [2] 順序——guest 密文先於 held 設值)
222
+ held.set(noteKey);
223
+ const cipherBound = await encryptNote(TACET, held, note, 'noteId:n1', { current: () => identityA });
224
+ const pass = 'correct-horse-battery-staple-42';
225
+ const recToken = generateRecToken();
226
+ const { wrapped, salt } = await wrapNoteKey(TACET, noteKey, pass);
227
+ const wrappedRec = await (await import('../src/client/note-crypto.ts')).wrapNoteKeyWithRecToken(TACET, noteKey, recToken, identityA);
228
+ const unwrapped = await unwrapNoteKey(TACET, wrapped, pass, salt);
229
+ const recUnwrapped = await unwrapNoteKeyWithRecToken(TACET, wrappedRec, recToken, identityA);
230
+ const origRaw = hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey)));
231
+ const localIdentity = 'acct-localwrap-test';
232
+ const TACET2: NoteCryptoConfig = { ...TACET, wrapDual: 'jr2w.', pinSaltPrefix: 'tacet-note-pin1:' };
233
+ const passD = 'dual-factor-passphrase-42';
234
+ const pinD = '2580ab';
235
+ const dual = await wrapNoteKeyDual(TACET2, noteKey, passD, pinD);
236
+ const TACET3: Argon3Config = { wrap3: 'jr3w.', wrapDual3: 'jr3d.', pinSalt3Prefix: 'tacet-note-pin3:' };
237
+ const w3 = await wrapNoteKey3(TACET3, noteKey, pass);
238
+ const dual3 = await wrapNoteKeyDual3(TACET3, noteKey, passD, pinD);
239
+ const ph1v2a = await derivePh1Argon('probe-determinism-pass-42');
240
+ const bip = await import('../src/client/bip39.ts');
241
+ const TACET_ATTACH: NoteCryptoConfig = { ...TACET, cipherAttach: 'jr1c.' };
242
+ const attachAad = 'jr1a:note-abc:att-001';
243
+ const attachPayload = JSON.stringify({ v: 1, kind: 'img', mime: 'image/jpeg', w: 2048, h: 1365, b64: b64(new Uint8Array(2048)) });
244
+ const attachCt = await encryptAttach(TACET_ATTACH, noteKey, attachPayload, attachAad);
245
+ const TACET4: Argon3Config = { wrap4: 'jr4w.', wrapDual4: 'jr4d.', pinSalt3Prefix: 'tacet-note-pin3:' };
246
+ const guest = await crypto.subtle.importKey('raw', enc.encode('seed').slice(0), 'PBKDF2', false, ['deriveKey']); // 型別哨兵:guest 非此路徑——跨段消費([5]/[11]/[13]/[18])
247
+ void guest;
248
+ const pass4 = 'v3-generation-passphrase-42';
249
+ const passD4 = 'dual-factor-passphrase-42'; // NFKC → passD 的全形形
250
+ // srcOf:源碼窗讀取 helper(原 [13] 段內宣告——跨 [14]-[23] 消費=setup 恆跑群;2026-10-09 段級化揚升)
251
+ async function srcOf(rel: string): Promise<string> {
252
+ // node 內建模組動態存取(structured type,零 node types 依賴——argon2.ts getBuiltinModule 母型同構;
253
+ // TS2591 types 帽下 import('node:fs') 靜態/動態皆炸=此繞法)
254
+ const fs = (globalThis as unknown as {
255
+ process?: { getBuiltinModule?: (id: string) => { readFileSync?: (p: string | URL, enc: string) => string } | undefined };
256
+ }).process?.getBuiltinModule?.('node:fs');
257
+ if (!fs?.readFileSync) throw new Error('ERR_FS_UNAVAILABLE');
258
+ return fs.readFileSync(new URL(rel, import.meta.url), 'utf8');
259
+ }
260
+
261
+ const TACET_GUESTOK: NoteCryptoConfig = { ...TACET }; // cipherGuest='jr1g.' 在場,專測 identity 面
262
+ const emptyIdp = { current: () => '' };
263
+ const dual4 = await wrapNoteKeyDual4(TACET4, noteKey, passD4, pinD); // 全形 wrap=NFKC 載體(cp1=0xFF44——ASCII 化即本面 RED)
136
264
  // ── 1. 基礎向量 ─────────────────────────────────────────────────────────────
137
265
 
138
- console.log('\n[1] PH1 / PBKDF2 / hash-ladder 基礎');
266
+ secOpen(1, '[1] PH1 / PBKDF2 / hash-ladder 基礎'); if (secEnter(1)) {
139
267
  await A('PH1 = SHA-256(pass) 穩定', (await ph1Of('test-pass-123')) === (await ph1Of('test-pass-123')));
140
268
  await A('PH1 hex64 格式', /^[0-9a-f]{64}$/.test(await ph1Of('x')));
141
269
  await A('PBKDF2_ITERATIONS = 600k', PBKDF2_ITERATIONS === 600_000);
142
270
 
143
271
  // ── 2. roundtrip(guest 時代 + 綁定時代,AAD 由呼叫端) ───────────────────────
144
272
 
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 });
273
+ };
274
+ secOpen(2, '[2] roundtrip / AAD 防搬移'); if (secEnter(2)) {
152
275
  await A('guest 時代前綴 jr1g.', cipherGuest.startsWith('jr1g.'));
153
276
  await A('guest roundtrip', (await decryptNote(TACET, held, cipherGuest, 'noteId:n1', { current: () => identityA })) === note);
154
277
 
155
- const noteKey = await generateNoteKey();
156
- held.set(noteKey);
157
- const cipherBound = await encryptNote(TACET, held, note, 'noteId:n1', { current: () => identityA });
158
278
  await A('綁定時代前綴 jr1b.', cipherBound.startsWith('jr1b.'));
159
279
  await A('綁定 roundtrip', (await decryptNote(TACET, held, cipherBound, 'noteId:n1', { current: () => identityA })) === note);
160
280
 
@@ -170,24 +290,19 @@ await A('舊明文原樣返回', (await decryptNote(TACET, held, '純舊明文',
170
290
  // ── 3. 包裹三件套(pass / rec / 本機;2026-09-24 效能節——buildBindPayload 兩段
171
291
  // PBKDF2 600k 已收 Promise.all 並行,node 實測 133-152ms→68-84ms) ──
172
292
 
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);
293
+ };
294
+ secOpen(3, '[3] 金鑰包裹(passphrase / 復原 / 本機)'); if (secEnter(3)) {
178
295
  const recTokenHashValue = await recTokenHash(recToken);
179
296
  await A('wrapped 前綴 jr1w.', wrapped.startsWith('jr1w.'));
180
297
  await A('salt = 16B hex', /^[0-9a-f]{32}$/.test(salt));
181
298
  await A('wrappedRec 前綴 jr1w.', wrappedRec.startsWith('jr1w.'));
182
299
  await A('recTokenHash hex64', /^[0-9a-f]{64}$/.test(recTokenHashValue));
183
300
 
184
- const unwrapped = await unwrapNoteKey(TACET, wrapped, pass, salt);
185
301
  await A('pass unwrap 救回 noteKey(同 pass 同 salt)',
186
302
  unwrapped !== null && hex(new Uint8Array(await crypto.subtle.exportKey('raw', unwrapped))) ===
187
303
  hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey))));
188
304
  await A('錯誤 pass unwrap → null', (await unwrapNoteKey(TACET, wrapped, 'wrong-pass', salt)) === null);
189
305
 
190
- const recUnwrapped = await unwrapNoteKeyWithRecToken(TACET, wrappedRec, recToken, identityA);
191
306
  await A('rec unwrap 救回 noteKey',
192
307
  recUnwrapped !== null && hex(new Uint8Array(await crypto.subtle.exportKey('raw', recUnwrapped))) ===
193
308
  hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey))));
@@ -202,14 +317,14 @@ await A('改密語:新包裹解得回同 noteKey',
202
317
  hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey))));
203
318
 
204
319
  // 本機包裹
205
- const localIdentity = 'acct-localwrap-test';
206
320
  const heldLocal = makeHeldKey();
207
321
  heldLocal.set(noteKey);
208
322
  await storeLocalWrap(TACET, localIdentity, noteKey);
209
323
 
210
324
  // ── 4. extractable 鐵律(2026-09-06 iOS 實機炸點回歸) ────────────────────────
211
325
 
212
- console.log('\n[4] noteKey extractable 鐵律');
326
+ };
327
+ secOpen(4, '[4] noteKey extractable 鐵律'); if (secEnter(4)) {
213
328
  const rawNote = crypto.getRandomValues(new Uint8Array(32));
214
329
  const nonExtractable = await crypto.subtle.importKey('raw', rawNote, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
215
330
  let threwNotExtractable = false;
@@ -224,7 +339,6 @@ const extractable = await crypto.subtle.importKey('raw', rawNote, { name: 'AES-G
224
339
  await A('修法機制:extractable key exportKey 等值 roundtrip',
225
340
  hex(new Uint8Array(await crypto.subtle.exportKey('raw', extractable))) === hex(rawNote));
226
341
  // unwrap 出的 key(全部三路徑)都要能再包裹:與 noteKey 原值比對(非 rawNote——那是本段新建隨機值)
227
- const origRaw = hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey)));
228
342
  await A('unwrap 出的 noteKey 可再 exportKey(等值 noteKey)',
229
343
  hex(new Uint8Array(await crypto.subtle.exportKey('raw', unwrapped!))) === origRaw);
230
344
  await A('rec unwrap 出的 noteKey 可再 exportKey(等值 noteKey)',
@@ -232,7 +346,8 @@ await A('rec unwrap 出的 noteKey 可再 exportKey(等值 noteKey)',
232
346
 
233
347
  // ── 5. 時代隔離:guest 與 bound 前綴互不誤判 ─────────────────────────────────
234
348
 
235
- console.log('\n[5] 時代隔離');
349
+ };
350
+ secOpen(5, '[5] 時代隔離'); if (secEnter(5)) {
236
351
  await A('guest cipher 不以 jr1b. 開頭', !cipherGuest.startsWith('jr1b.'));
237
352
  await A('bound cipher 不以 jr1g. 開頭', !cipherBound.startsWith('jr1g.'));
238
353
  await A('無 held key 的 bound 解密 → null',
@@ -240,11 +355,8 @@ await A('無 held key 的 bound 解密 → null',
240
355
 
241
356
  // ── 6. 雙因子合鑰(jr2w.,2026-09-09 PIN 第二因子) ─────────────────────────
242
357
 
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);
358
+ };
359
+ secOpen(6, '[6] 雙因子合鑰 KEK2(jr2w.)'); if (secEnter(6)) {
248
360
  await A('wrapped2 前綴 jr2w.', dual.wrapped.startsWith('jr2w.'));
249
361
  await A('dual salt1 = 16B hex', /^[0-9a-f]{32}$/.test(dual.salt));
250
362
  const unwrapped2 = await unwrapNoteKeyDual(TACET2, dual.wrapped, passD, pinD, dual.salt);
@@ -293,13 +405,12 @@ await A('未配置 wrapDual 拒絕 dual 包裹', (await unwrapNoteKeyDual(TACET,
293
405
  // Argon2id 記憶體困難(64MiB)使每猜成本上升 1-2 個數量級。遷移契約 = 成功 unwrap
294
406
  // 後客戶端靜默 re-wrap,舊前綴照解(下面有並存斷言)。
295
407
 
296
- console.log('\n[7] Argon2id 包裹(jr3w./jr3d.)');
297
- const TACET3: Argon3Config = { wrap3: 'jr3w.', wrapDual3: 'jr3d.', pinSalt3Prefix: 'tacet-note-pin3:' };
408
+ };
409
+ secOpen(7, '[7] Argon2id 包裹(jr3w./jr3d.)'); if (secEnter(7)) {
298
410
  await A('RFC 9106 無 secret/ad 標準向量(當前載體正確性)', await verifyArgonKat());
299
411
  await A('Argon 參數契約 m=64MiB t=3 p=1 tag=32',
300
412
  ARGON_MEMORY_KIB === 65536 && ARGON_ITERATIONS === 3 && ARGON_PARALLELISM === 1 && ARGON_TAG_LEN === 32);
301
413
 
302
- const w3 = await wrapNoteKey3(TACET3, noteKey, pass);
303
414
  await A('wrapped 前綴 jr3w.', w3.wrapped.startsWith('jr3w.'));
304
415
  await A('jr3w salt = 16B hex', /^[0-9a-f]{32}$/.test(w3.salt));
305
416
  const unwrapped3 = await unwrapNoteKey3(TACET3, w3.wrapped, pass, w3.salt);
@@ -323,7 +434,6 @@ await A('jr3w payload 竄改 iv 區 → null', await tampered3(3));
323
434
  await A('jr3w payload 竄改 ct 尾 → null', await tampered3(payload3.length - 1));
324
435
 
325
436
  // jr3d 雙因子(PIN 第二因子,Argon2id 版)
326
- const dual3 = await wrapNoteKeyDual3(TACET3, noteKey, passD, pinD);
327
437
  await A('dual3 前綴 jr3d.', dual3.wrapped.startsWith('jr3d.'));
328
438
  await A('dual3 salt1 = 16B hex', /^[0-9a-f]{32}$/.test(dual3.salt));
329
439
  const unwrappedD3 = await unwrapNoteKeyDual3(TACET3, dual3.wrapped, passD, pinD, dual3.salt);
@@ -360,9 +470,9 @@ await A('未配置 wrapDual3 → unwrap 拒收 jr3d',
360
470
  // 每猜成本從 ~ns(SHA-256×2)升到 ~0.1-0.18s CPU=×10⁴-10⁶。
361
471
  // 固定域鹽是被迫設計(per-user 鹽會摧毀 PH2 UNIQUE=幽靈帳號機制基礎);改參數 = 換鹽尾碼(v2/v3)重遷移。
362
472
 
363
- console.log('\n[8] PH1 v2(登入憑證 Argon2id 派生)');
473
+ };
474
+ secOpen(8, '[8] PH1 v2(登入憑證 Argon2id 派生)'); if (secEnter(8)) {
364
475
  await A('RFC 9106 KAT 先行(載體正確性,本節所有 Argon 斷言的前提)', await verifyArgonKat());
365
- const ph1v2a = await derivePh1Argon('probe-determinism-pass-42');
366
476
  await A('ph1v2 確定性(同 pass 兩跑逐位元同)', ph1v2a === (await derivePh1Argon('probe-determinism-pass-42')));
367
477
  await A('ph1v2 hex64 形(validHash64 可收)', /^[0-9a-f]{64}$/.test(ph1v2a));
368
478
  // 與 legacy ph1(SHA-256 形)必然不同值=雙查表兩鍵語意成立
@@ -384,8 +494,8 @@ await A('ph1v2 非恆等(pass ≠ 輸出)', ph1v2a !== 'probe-determinism-pa
384
494
 
385
495
  // ── 9. BIP39 復原套件契約(words↔hex64 轉寫層;線上契約 recToken hex64 不變) ──
386
496
 
387
- console.log('\n[9] BIP39 復原套件(24 詞 ⇄ entropy 32B ⇄ hex64)');
388
- const bip = await import('../src/client/bip39.ts');
497
+ };
498
+ secOpen(9, '[9] BIP39 復原套件(24 詞 ⇄ entropy 32B ⇄ hex64)'); if (secEnter(9)) {
389
499
  const wl = await import('../src/client/wordlist.ts');
390
500
 
391
501
  const kitWords = await bip.generateBip39Words();
@@ -483,16 +593,13 @@ await A('words 派生 hex64 unwrap 救回 noteKey',
483
593
 
484
594
  // ── 10. 附件密文(jr1c.,image attachments;opt-in 未配置即拒) ───────────────
485
595
 
486
- console.log('\n[10] 附件密文(jr1c. cipherAttach opt-in)');
596
+ };
597
+ secOpen(10, '[10] 附件密文(jr1c. cipherAttach opt-in)'); if (secEnter(10)) {
487
598
  const bareCfgAttach: NoteCryptoConfig = { ...TACET }; // 無 cipherAttach 欄
488
599
  let attachCfgThrow = '';
489
600
  try { await encryptAttach(bareCfgAttach, noteKey, '{"v":1}', 'jr1a:n1:a1'); } catch (e) { attachCfgThrow = (e as Error).message; }
490
601
  await A('未配置 cipherAttach → encryptAttach throw', attachCfgThrow === 'ERR_ATTACH_NOT_CONFIGURED', attachCfgThrow);
491
602
 
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
603
  await A('附件前綴 jr1c.', attachCt.startsWith('jr1c.'));
497
604
  await A('附件 roundtrip(顯式 noteKey)',
498
605
  (await decryptAttach(TACET_ATTACH, noteKey, attachCt, attachAad)) === attachPayload);
@@ -503,7 +610,8 @@ await A('附件錯鑰匙 → null',
503
610
 
504
611
  // ── 11. 本機 IDB 密文(jr1d. cipherLocal opt-in)+ guest 選配拒絕面 ──────────
505
612
 
506
- console.log('\n[11] 本機 IDB 密文(jr1d. cipherLocal opt-in)+ guest 選配拒絕面');
613
+ };
614
+ secOpen(11, '[11] 本機 IDB 密文(jr1d. cipherLocal opt-in)+ guest 選配拒絕面'); if (secEnter(11)) {
507
615
  // 分享包裹(jrsw.,wrapShare 選配)+ jr3s. roundtrip(閘本有 roundtrip 斷言缺席)
508
616
  const TACET_SHARE: NoteCryptoConfig = { ...TACET, wrapShare: 'jrsw.' };
509
617
  const shareWrapped = await wrapNoteKeyShare(TACET_SHARE, noteKey, 'share-pass-42');
@@ -612,8 +720,6 @@ await A('既有 guest 配置 roundtrip 不受選配化影響',
612
720
  (await decryptNote(TACET, makeHeldKey(), cipherGuest, 'noteId:n1', { current: () => identityA })) === note);
613
721
 
614
722
  // guest 空 identity 拒絕面:K_u = SHA-256(prefix ‖ '') 靜默產出「空帳號金鑰」是誤配炸彈
615
- const TACET_GUESTOK: NoteCryptoConfig = { ...TACET }; // cipherGuest='jr1g.' 在場,專測 identity 面
616
- const emptyIdp = { current: () => '' };
617
723
  let emptyIdMsg: string | null = null;
618
724
  try { await encryptNote(TACET_GUESTOK, makeHeldKey(), note, 'noteId:n1', emptyIdp); } catch (e) { emptyIdMsg = (e as Error).message; }
619
725
  await A("guest 加密面空 identity → throw ERR_NO_IDENTITY(falsy 一體:空字串/undefined/null)",
@@ -643,7 +749,8 @@ await A("畸形 guest 前綴 '' → decryptNote 舊明文相容層同「未配
643
749
  // ── 12. 伺服器端原語(makeInboundCipher/validWrappedKey/pickKeyPackage/
644
750
  // checkRate/timingSafeEq)+ 本機鎖定全函式(pinlock jr1p.) ──────────
645
751
 
646
- console.log('\n[12] 伺服器端原語(auth/ratelimit/hash)+ 本機鎖定(pinlock)');
752
+ };
753
+ secOpen(12, '[12] 伺服器端原語(auth/ratelimit/hash)+ 本機鎖定(pinlock)'); if (secEnter(12)) {
647
754
  // ratelimit 行為面(UPSERT…RETURNING 單句設計收口):真 node:sqlite 直載 UPSERT…RETURNING 單句,
648
755
  // 行為斷言全數「真生產函式 checkRate 直驅」——D1 形 shim 包 node:sqlite 只做 D1 run() 的
649
756
  // {results} 回帶形,無測試端 glue 副本。表名直插 SQL=注入面驗證;放行判準 count<=max
@@ -875,7 +982,8 @@ await A('jr1p 跨家族:jr1p. 字串餵 jr1w unwrap → null',
875
982
 
876
983
  // ── 13. 新契約面(hex fail-closed/salt hex 形收口/PH1 鹽注入/錯誤碼語意分離) ──
877
984
 
878
- console.log('\n[13] unwrap 驗證統一新契約(fail-closed + 語意分離)');
985
+ };
986
+ secOpen(13, '[13] unwrap 驗證統一新契約(fail-closed + 語意分離)'); if (secEnter(13)) {
879
987
  // hexToBytes fail-closed(note-crypto 與 bip39 同步——bip39 面是 HEX64_RE 前置自守)
880
988
  await A('hexToBytes 合法輸出不變(舊碼同值;新向量)', hexToBytes('00ff10').join(',') === '0,255,16');
881
989
  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 +1141,6 @@ await A('wrapNoteKeyDual3 未配置 wrapDual3 → ERR_JR3W_NOT_CONFIGURED(契
1033
1141
  // PH1 鹽注入:預設舊值零行為變更
1034
1142
  await A('derivePh1Argon 預設鹽零行為變更(同 pass 同值)',
1035
1143
  (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
1144
  await A('derivePh1Argon 預設分支源碼面 = encode(PH1_V2_SALT)(鹽預設單一真相錨;同-pass 同值斷言對預設值漂移無承載力)',
1046
1145
  /saltArg \?\? new TextEncoder\(\)\.encode\(PH1_V2_SALT\)/.test(await srcOf('../src/client/argon2.ts')));
1047
1146
  await A('derivePh1Argon 自選鹽 → 不同派生值', (await derivePh1Argon('probe-determinism-pass-42', new Uint8Array(12))) !== ph1v2a);
@@ -1095,7 +1194,8 @@ await A('[22] 發行 tarball 零 open-card-pr.sh(治理掃蕩面——pack 白
1095
1194
  // buildBindPayload 串行 133-152ms→並行 68-84ms;dual 串行 234→並行 179ms median;
1096
1195
  // b64 4MiB 132-181ms→18ms。瀏覽器實測 222ms→27ms。
1097
1196
 
1098
- console.log('\n[14] 效能形契約(buildBindPayload 並行/b64 分塊/argon 禁並行/CORS Max-Age/sideEffects)');
1197
+ };
1198
+ secOpen(14, '[14] 效能形契約(buildBindPayload 並行/b64 分塊/argon 禁並行/CORS Max-Age/sideEffects)'); if (secEnter(14)) {
1099
1199
 
1100
1200
  const noteCryptoSrc14 = await srcOf('../src/client/note-crypto.ts');
1101
1201
  const argon2Src14 = await srcOf('../src/client/argon2.ts');
@@ -1236,13 +1336,11 @@ await A('[14] corsResponse 仍完整轉發四頭(corsResponse 面帶 Max-Age
1236
1336
  // 函式體零改=raw 契約永久不變——本節同字面雙家族對照向量(PASS12x/PASS12x)就是
1237
1337
  // 「免換前綴合約證明」的承載面:同組字串在舊家族恆異 KEK(raw 直派生)、在 v3 家族恆同 KEK(NFKC 收容)。
1238
1338
 
1239
- console.log('\n[15] 密語正規化 v3 世代(normalizePassphrase/jr4w./jr4d./PH1 v3)');
1339
+ };
1340
+ secOpen(15, '[15] 密語正規化 v3 世代(normalizePassphrase/jr4w./jr4d./PH1 v3)'); if (secEnter(15)) {
1240
1341
 
1241
1342
  const noteCryptoSrc15 = await srcOf('../src/client/note-crypto.ts');
1242
1343
  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
1344
  const passD4_NFKC = normalizePassphrase(passD4); // 與 passD 恆等(NFKC 收容)——P2 毒化翻面本體(毒形即收口形計數錨翻+毒翻 = NFKC-effective 帳面翻,雙承載)
1247
1345
 
1248
1346
  // 函式契約面(單一真相本體:normalizePassphrase 與 normalizePin 並存不混)
@@ -1275,7 +1373,6 @@ await A('jr4w 錯 pass → null', (await unwrapNoteKey4(TACET4, w4.wrapped, 'wro
1275
1373
  await A('jr4w 錯 salt → null', (await unwrapNoteKey4(TACET4, w4.wrapped, pass4, 'ab'.repeat(16))) === null);
1276
1374
 
1277
1375
  // jr4d. 雙因子(v3 密語+PIN 照舊)——wrap 端吃全形(NFKC 載體),unwrap 端兩向都咬
1278
- const dual4 = await wrapNoteKeyDual4(TACET4, noteKey, passD4, pinD); // 全形 wrap=NFKC 載體(cp1=0xFF44——ASCII 化即本面 RED)
1279
1376
  await A('dual4 前綴 jr4d.', dual4.wrapped.startsWith('jr4d.'));
1280
1377
  await A('dual4 salt1 = 16B hex', /^[0-9a-f]{32}$/.test(dual4.salt));
1281
1378
  const unwrappedD4 = await unwrapNoteKeyDual4(TACET4, dual4.wrapped, passD, pinD, dual4.salt);
@@ -1413,7 +1510,8 @@ await A('index barrel 匯出 v3 世代七識別字(normalizePassphrase/jr4
1413
1510
  // v3 家族入口 normalize 收口形恰 3×3+PBKDF2 raw 恰 1——毒化形即計數變異,本 repo 樹直接斷言。
1414
1511
  // 每案結束後還原/清樹(出生即棄);designated 面帳寫在案例名內。
1415
1512
 
1416
- console.log('\n[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒)');
1513
+ };
1514
+ secOpen(16, '[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒)'); if (secEnter(16)) {
1417
1515
  {
1418
1516
  const getBuiltin = (id: string): unknown =>
1419
1517
  (globalThis as unknown as { process?: { getBuiltinModule?: (i: string) => unknown } }).process?.getBuiltinModule?.(id);
@@ -1641,6 +1739,7 @@ console.log('\n[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒
1641
1739
  }
1642
1740
  }
1643
1741
  }
1742
+ };
1644
1743
 
1645
1744
  // ── 17. KAT 凍結向量:HKDF info 域世代分離(修正承載面) ──────────────
1646
1745
  //
@@ -1679,7 +1778,7 @@ const KAT17 = {
1679
1778
  {
1680
1779
  const katRawUnlock = async (key: CryptoKey | null): Promise<boolean> =>
1681
1780
  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)');
1781
+ secOpen(17, '[17] KAT 凍結向量:HKDF info 域世代分離+v3/raw 入口契約(jr3w./jr3d./jr4w./jr4d. 四 blob)'); if (secEnter(17)) {
1683
1782
  await A('[17] jr3w 凍結 blob:全形 RAW unwrap = 原始 noteKey(舊家族 raw 契約——KAT 承載)',
1684
1783
  katRawUnlock(await unwrapNoteKey3(TACET3, KAT17.jr3w, KAT17.pass3Raw, KAT17.s3)));
1685
1784
  await A('[17] jr3w 凍結 blob:normalized 慣用形 → null(normalized 面恆拒——raw 族零正規化承載)',
@@ -1708,6 +1807,7 @@ const KAT17 = {
1708
1807
  (await unwrapNoteKeyDual3(TACET3, KAT17.jr4d, passD, KAT17.pin, KAT17.sd4)) === null);
1709
1808
  await A('[17] family isolation:jr3d blob 餵 jr4d 路徑 → null(KEK2 info 域分離對稱面)',
1710
1809
  (await unwrapNoteKeyDual4(TACET4, KAT17.jr3d, passD, KAT17.pin, KAT17.sd3)) === null);
1810
+ };
1711
1811
  }
1712
1812
 
1713
1813
  // ── 18. 本機包裹專用前綴(wrapLocal opt-in+讀舊寫新自癒;jr1l.) ──
@@ -1722,7 +1822,7 @@ const KAT17 = {
1722
1822
  // ③未配置態讀取面:照走舊形 cfg.wrap(行為不變),且不自癒重寫(heal 綁在新前綴面)。
1723
1823
  // KEK 不動=deriveGuestKey(identity 派生、passphrase-free——session 期免重打密語的機制
1724
1824
  // 原樣);本段只界「前綴」面(payload 布局的 KDF/prefix 契約由前綴界定——帶內版本化母型)。
1725
- console.log('\n[18] 本機包裹專用前綴(wrapLocal=jr1l. opt-in+讀舊寫新自癒)');
1825
+ secOpen(18, '[18] 本機包裹專用前綴(wrapLocal=jr1l. opt-in+讀舊寫新自癒)'); if (secEnter(18)) {
1726
1826
  {
1727
1827
  // section 專用 Map store:node 端 localStorage 死位(keys.ts try/catch 恆 null)——stored 面
1728
1828
  // 行為斷言要可觀察可重放,結構 KeyStore 注入(同 cfg 注入母型;TACET 基座 fixture 不動)。
@@ -1984,6 +2084,7 @@ console.log('\n[18] 本機包裹專用前綴(wrapLocal=jr1l. opt-in+讀舊
1984
2084
  }
1985
2085
  }
1986
2086
  }
2087
+ };
1987
2088
 
1988
2089
  // ── 19. ladder 表與重取鹽面((a+) 案) ──────────────
1989
2090
  //
@@ -1994,7 +2095,7 @@ console.log('\n[18] 本機包裹專用前綴(wrapLocal=jr1l. opt-in+讀舊
1994
2095
  // 直驅 makePh2LadderStore:upsert 恰一列、同一舊值最新持有者勝、無 DELETE 語句面)。
1995
2096
  // 表名/kind 常數的契約面(PH2_LADDER_TABLE='ph2_ladder'、KIND legacy/v2)由本段斷言直接咬住
1996
2097
  //(fork migration 0011 與 core 原語同字面——漂移=fork schema 與 core 原語脫鉤=表缺席 fail-open 面)。
1997
- console.log('\n[19] ladder 表與重取鹽面(unwrapNoteKeyDual4WithSalt+ph2_ladder)');
2098
+ secOpen(19, '[19] ladder 表與重取鹽面(unwrapNoteKeyDual4WithSalt+ph2_ladder)'); if (secEnter(19)) {
1998
2099
  {
1999
2100
  const { DatabaseSync } = (globalThis as unknown as {
2000
2101
  process?: { getBuiltinModule?: (id: string) => { DatabaseSync?: unknown } };
@@ -2087,6 +2188,7 @@ console.log('\n[19] ladder 表與重取鹽面(unwrapNoteKeyDual4WithSalt+ph2
2087
2188
  await A('[19] WithSalt 未配置 cfg(wrapDual4 缺)→ null(opt-in 鐵律同向)',
2088
2189
  (await unwrapNoteKeyDual4WithSalt({ pinSalt3Prefix: 'tacet-note-pin3:' }, rewrapD4.wrapped, passD, pinD, rewrapD4.salt)) === null);
2089
2190
  }
2191
+ };
2090
2192
  // ── 20. 復原套件專用前綴+HKDF 派生世代 ──
2091
2193
  //
2092
2194
  // 裁定:採搭車——wrappedRec 專用前綴+KEK_rec 改
@@ -2108,7 +2210,7 @@ console.log('\n[19] ladder 表與重取鹽面(unwrapNoteKeyDual4WithSalt+ph2
2108
2210
  // 涵蓋;摘雙試=遷移率裁定非日曆)。兩欄皆缺席(舊 cfg)=單試舊面(行為不變)。
2109
2211
  // ⑤四象限 write→read 矩陣(檢查對齊 M1):每象限「自 wrap 可自 unwrap 或 wrap 拒絕」
2110
2212
  // +超集 cfg 可讀子集 blob(升級路徑保護)+B01 歷史缺陷形恆拒(升 KDF=換前綴)。
2111
- console.log('\n[20] 復原套件專用前綴(wrapRec=jr1r. opt-in)+KEK_rec HKDF 世代(recKekHkdf opt-in,兩欄一體配對門)');
2213
+ secOpen(20, '[20] 復原套件專用前綴(wrapRec=jr1r. opt-in)+KEK_rec HKDF 世代(recKekHkdf opt-in,兩欄一體配對門)'); if (secEnter(20)) {
2112
2214
  {
2113
2215
  const recSalt20 = 'a1b2c3d4e5f60718293a4b5c6d7e8f90';
2114
2216
  const TACET5: NoteCryptoConfig = { ...TACET, wrapRec: 'jr1r.', recKekHkdf: 'jr1r.', recKekSalt: recSalt20 };
@@ -2392,6 +2494,7 @@ console.log('\n[20] 復原套件專用前綴(wrapRec=jr1r. opt-in)+KEK_rec
2392
2494
  trees20.length === 6 && trees20.every((t) => !existsSync20(t)));
2393
2495
  }
2394
2496
  }
2497
+ };
2395
2498
 
2396
2499
  // ── 21. CI 供應鏈面(npm ci --ignore-scripts+Dependabot runner 分流)──
2397
2500
  //
@@ -2399,7 +2502,7 @@ console.log('\n[20] 復原套件專用前綴(wrapRec=jr1r. opt-in)+KEK_rec
2399
2502
  // --ignore-scripts 是安裝面防線(postinstall/preinstall/prepare 全免執行);
2400
2503
  // Dependabot PR 走 GitHub-hosted runner 隔離(self-hosted runner 不接機器人分支)。
2401
2504
  // 靜態錨面:workflow YAML 本體的接線(job-if C1 母型+actor 分流+runs-on fromJSON 陣列形)。
2402
- console.log('\n[21] CI 供應鏈面(npm ci --ignore-scripts+dependabot runner 分流)');
2505
+ secOpen(21, '[21] CI 供應鏈面(npm ci --ignore-scripts+dependabot runner 分流)'); if (secEnter(21)) {
2403
2506
  {
2404
2507
  // 環境鍵=.github 目錄存在(存在性鍵非檔名硬讀——r1 MINOR-3):閘 src 檔在 files 白名單
2405
2508
  // 而 .github 不出包——tarball 消費樹硬讀 workflows=未守衛 ENOENT,rc=1 尾段全吞。
@@ -2422,7 +2525,7 @@ console.log('\n[21] CI 供應鏈面(npm ci --ignore-scripts+dependabot runne
2422
2525
  await A('[21] publish.yml:npm ci --ignore-scripts(tag 驅動無 actor 面;typecheck+verify 先行=腳本面零在場)',
2423
2526
  srcPy21.includes('npm ci --ignore-scripts'));
2424
2527
  } else {
2425
- await A('[21] CI 源碼面:.github 缺席環境(tarball 發行樹常態)=顯性 SKIP(workflow 斷言 4 收斂為 1 SKIP 行——消費端計數帳 475−3=472)',
2528
+ await A('[21] CI 源碼面:.github 缺席環境(tarball 發行樹常態)=顯性 SKIP(workflow 斷言 4 收斂為 1 SKIP 行——消費端計數帳 489)',
2426
2529
  true, 'no .github dir — consumption tree face');
2427
2530
  }
2428
2531
  // 摘除自證(t_760f44e8 治理掃蕩補漏;fs 原語缺席環境=顯性 FAIL 形自守衛——非 silent true)
@@ -2447,8 +2550,9 @@ console.log('\n[21] CI 供應鏈面(npm ci --ignore-scripts+dependabot runne
2447
2550
  && !/readFileSync\(new URL\([^)]*\)\.pathname/.test(gateSrc21));
2448
2551
  }
2449
2552
  }
2553
+ };
2450
2554
 
2451
- console.log('\n[23] loginRouteCore ladder 查表守衛(外審 #8 幽靈帳——現值 miss → ladder → 兩面 miss 才 createUser)');
2555
+ secOpen(23, '[23] loginRouteCore ladder 查表守衛(外審 #8 幽靈帳——現值 miss → ladder → 兩面 miss 才 createUser)'); if (secEnter(23)) {
2452
2556
  {
2453
2557
  // getBuiltin23:node 內建模組動態存取(本段自備 helper——[19]/[21] 同構;structured type 零 node types 依賴)
2454
2558
  const getBuiltin23 = (id: string): unknown =>
@@ -2660,7 +2764,7 @@ console.log('\n[23] loginRouteCore ladder 查表守衛(外審 #8 幽靈帳—
2660
2764
  const storeP = {
2661
2765
  findByIdentityQuery: async () => null,
2662
2766
  createUser: async () => 'acct-ghost-23',
2663
- getByUserKey: async () => null,
2767
+ getByUserKey: async (key: string) => (tP.db.prepare(`SELECT ${USER_COLS23} FROM users WHERE account_id = ?`).get(key) as unknown) || null,
2664
2768
  revokeAllSessions: async () => {},
2665
2769
  insertSession: async () => {},
2666
2770
  ladderLookup: async () => 'acct-23old',
@@ -2685,10 +2789,136 @@ console.log('\n[23] loginRouteCore ladder 查表守衛(外審 #8 幽靈帳—
2685
2789
  }
2686
2790
  }
2687
2791
 
2792
+ };
2793
+ // ── 26. 格式規格+測試向量(docs/format-spec.md+docs/vectors/*.json;0.3.0 批 A2) ──
2794
+ //
2795
+ // 單向契約:源碼 → scripts/generate-vectors.ts → JSON(凍結證據,commit 入 repo)→ 本段只檢存在+shape。
2796
+ // 本段不依賴產生器、不重算 KDF(向量含隨機 iv/鹽=重算即漂移);BIP39 是確定性轉寫,逐組對照現行原語。
2797
+ // teeth:payload 位元組長度=b64 解碼真值、fields 拼接=payload 逐位元組、布局長度=規格表列、
2798
+ // 欄位名逐一在規格檔、凍結 blob 逐字=本閘 KAT 字面([14]/[17]/[20])、RFC 9106 期望值=argon2.ts 常數。
2799
+ secOpen(26, '[26] 格式規格+測試向量(docs/format-spec.md+docs/vectors 存在性+shape)'); if (secEnter(26)) {
2800
+ {
2801
+ const readJson26 = async (rel: string): Promise<Record<string, unknown> | null> => {
2802
+ try { return JSON.parse(await srcOf(rel)) as Record<string, unknown>; } catch { return null; }
2803
+ };
2804
+ const spec26 = await srcOf('../docs/format-spec.md').catch(() => '');
2805
+ const fam26 = await readJson26('../docs/vectors/families.json');
2806
+ const kat26 = await readJson26('../docs/vectors/kat.json');
2807
+ const bip26 = await readJson26('../docs/vectors/bip39.json');
2808
+ type Vec26 = { prefix: string; family: string; layout: string; kdf: string; aad: string; inputs: Record<string, unknown>; wire: string; payload_len: number; fields: { name: string; offset: number; len: number; hex: string }[] };
2809
+ const vecs26 = (Array.isArray(fam26?.vectors) ? fam26!.vectors : []) as Vec26[];
2810
+ const HEAD26 = ['format', 'package_version', 'spec', 'generator', 'note'];
2811
+ await A('[26] 規格檔+三向量檔在場且 JSON parse;頂層共通欄齊(format=journal-core-vectors/1、spec/generator 路徑字面——產生器不被閘依賴,發行包不帶產生器)',
2812
+ spec26.length > 0 && !!fam26 && !!kat26 && !!bip26
2813
+ && [fam26, kat26, bip26].every((j) => HEAD26.every((k) => typeof j![k] === 'string') && j!.format === 'journal-core-vectors/1'
2814
+ && j!.spec === 'docs/format-spec.md' && j!.generator === 'scripts/generate-vectors.ts'));
2815
+ // 規格表前綴集(§1 家族逐條表:| `prefix` | 家族 | 布局 | payload | KDF | AAD |)
2816
+ const specRows26 = spec26.split('\n').filter((l) => /^\| `jr[0-9a-z]{2}\.`(([^|]*))? \|/.test(l) && /\| (content|salt-external|salt-embedded) \|/.test(l));
2817
+ const specPrefixes26 = new Set(specRows26.map((l) => l.match(/^\| `(jr[0-9a-z]{2}\.)`/)![1]));
2818
+ const vecPrefixes26 = new Set(vecs26.map((v) => v.prefix));
2819
+ await A('[26] 家族覆蓋雙向:規格家族表 16 列前綴集=families.json 前綴集(15 前綴;jr1w. 兩世代各一組)',
2820
+ specRows26.length === 16 && vecs26.length === 16 && specPrefixes26.size === 15
2821
+ && [...specPrefixes26].every((p) => vecPrefixes26.has(p)) && [...vecPrefixes26].every((p) => specPrefixes26.has(p)));
2822
+ const VKEYS26 = ['prefix', 'family', 'layout', 'kdf', 'aad', 'inputs', 'wire', 'payload_len', 'fields'];
2823
+ await A('[26] families 位元組 shape:payload_len=wire b64 解碼真長;fields offset 連續、len 總和=payload_len、hex 拼接=payload 逐位元組',
2824
+ vecs26.length > 0 && vecs26.every((v) => {
2825
+ if (!VKEYS26.every((k) => k in v) || !v.wire.startsWith(v.prefix)) return false;
2826
+ let payload: Uint8Array;
2827
+ try { payload = unb64Mod(v.wire.slice(v.prefix.length)); } catch { return false; }
2828
+ let off = 0;
2829
+ for (const f of v.fields) { if (f.offset !== off || f.hex.length !== f.len * 2) return false; off += f.len; }
2830
+ return payload.length === v.payload_len && off === v.payload_len && v.fields.map((f) => f.hex).join('') === hex(payload);
2831
+ }));
2832
+ await A('[26] 布局長度=規格:salt-external 92/salt-embedded 108(pinSalt 頭 16)/content 28+UTF8(plaintext);且規格家族表該前綴列含同布局+同長度',
2833
+ vecs26.length > 0 && vecs26.every((v) => {
2834
+ const want = v.layout === 'salt-external' ? 92 : v.layout === 'salt-embedded' ? 108 : v.layout === 'content' ? 28 + enc.encode(String(v.inputs.plaintext)).length : -1;
2835
+ const lenCol = v.layout === 'content' ? '28+n' : String(want);
2836
+ const head = v.layout === 'salt-embedded' ? v.fields[0]?.name === 'pinSalt' && v.fields[0]?.len === 16 : v.fields[0]?.name === 'iv';
2837
+ return want === v.payload_len && head && specRows26.some((l) => l.startsWith('| `' + v.prefix + '`') && l.includes('| ' + v.layout + ' | ' + lenCol + ' |'));
2838
+ }));
2839
+ const KKEYS26 = ['rfc9106', 'frozen_blobs', 'credentials', 'expected_tag_hex', 'expected_note_key_hex', 'expected_hex', 'samples', 'entropy_hex', 'checksum_hex', 'words', 'source'];
2840
+ await A('[26] 欄位名對規格:families 每筆欄名+fields 子欄(name/offset/len/hex)+kat/bip39 欄名,逐一以 `欄名` 形出現在 format-spec.md',
2841
+ spec26.length > 0 && [...HEAD26, ...VKEYS26, 'name', 'offset', 'len', 'hex', ...KKEYS26].every((k) => spec26.includes('`' + k + '`'))
2842
+ && KKEYS26.slice(0, 3).every((k) => !!kat26 && k in kat26) && !!bip26 && 'samples' in bip26);
2843
+ const gate26 = await srcOf('./verify-core-crypto.ts');
2844
+ const frozen26 = (Array.isArray(kat26?.frozen_blobs) ? kat26!.frozen_blobs : []) as { prefix: string; wire: string; payload_len: number; expected_note_key_hex: string; source: string }[];
2845
+ const rfc26 = (kat26?.rfc9106 ?? {}) as Record<string, unknown>;
2846
+ const creds26 = (Array.isArray(kat26?.credentials) ? kat26!.credentials : []) as { kind: string; expected_hex: string }[];
2847
+ await A('[26] kat 對帳:RFC 9106 expected_tag_hex=ARGON_RFC9106_EXPECTED(argon2.ts 常數);凍結 blob 7 筆逐字=本閘 KAT 字面([14]/[17]/[20])且 payload_len=解碼真長;憑證樣本 5 筆 hex64',
2848
+ (await import('../src/client/argon2.ts')).ARGON_RFC9106_EXPECTED === rfc26.expected_tag_hex
2849
+ && frozen26.length === 7 && frozen26.every((b) => gate26.includes("'" + b.wire + "'") && b.wire.startsWith(b.prefix)
2850
+ && unb64Mod(b.wire.slice(b.prefix.length)).length === b.payload_len && /^[0-9a-f]{64}$/.test(b.expected_note_key_hex))
2851
+ && creds26.length === 5 && creds26.every((c) => /^[0-9a-f]{64}$/.test(c.expected_hex)));
2852
+ const samples26 = (Array.isArray(bip26?.samples) ? bip26!.samples : []) as { entropy_hex: string; checksum_hex: string; words: string[]; source: string }[];
2853
+ await A('[26] bip39 抽樣 ≥32 組:entropy hex64+24 詞+checksum=SHA-256(entropy)[0];逐組對照現行轉寫原語(recTokenToWords 逐詞相等——確定性轉寫非 KDF 重算)',
2854
+ samples26.length >= 32 && (await (async () => {
2855
+ for (const s of samples26) {
2856
+ if (!/^[0-9a-f]{64}$/.test(s.entropy_hex) || s.words?.length !== 24) return false;
2857
+ const cs = new Uint8Array(await crypto.subtle.digest('SHA-256', hexToBytes(s.entropy_hex) as BufferSource))[0];
2858
+ if (cs.toString(16).padStart(2, '0') !== s.checksum_hex) return false;
2859
+ if ((await bip.recTokenToWords(s.entropy_hex))?.join(' ') !== s.words.join(' ')) return false;
2860
+ }
2861
+ return true;
2862
+ })()));
2863
+ const pkg26 = JSON.parse(await srcOf('../package.json')) as { files?: string[] };
2864
+ await A('[26] 發行面:package.json files 含 docs(tarball 帶規格+向量——消費端可對帳)+產生器排除(!scripts/generate-vectors.ts——[22] scripts 封閉集 2 檔不動)',
2865
+ Array.isArray(pkg26.files) && pkg26.files.includes('docs') && pkg26.files.includes('!scripts/generate-vectors.ts'));
2866
+ }
2867
+ }
2868
+
2869
+ secOpen(24, '[24] runner 契約自證(--only 解析契約 fail-closed+段級帳面顯形)'); if (secEnter(24)) {
2870
+ await A('[24] --only 解析契約(全跑態):spec 缺席=onlySet 空集=secEnter 恆真(CI 形契約不變——npm run verify 全帳;--only 態本面恆真跳過=帳面由次面承載)',
2871
+ !onlyMode() ? (onlySpecRaw === null && parseOnly('').err === 'ERR_ONLY_SPEC_EMPTY' && secEnter(1) && secEnter(23)) : true);
2872
+ await A('[24] --only 解析契約(CSV/範圍/混形三形同集合):16,17,18 ≡ 16-18(同集合);16,17-19 展開 16..19;單調遞增去重 18,16-17,16 → [16,17,18]',
2873
+ JSON.stringify(parseOnly('16,17,18').set) === JSON.stringify([16, 17, 18])
2874
+ && JSON.stringify(parseOnly('16-18').set) === JSON.stringify([16, 17, 18])
2875
+ && JSON.stringify(parseOnly('16,17-19').set) === JSON.stringify([16, 17, 18, 19])
2876
+ && JSON.stringify(parseOnly('18,16-17,16').set) === JSON.stringify([16, 17, 18]));
2877
+ await A('[24] --only 解析契約(fail-closed 分流):空值/非數字 token/慣例 skip 段 22/未定段(0/99)/倒序範圍=ERR_ONLY_*(exit 1 由頂層守衛承載——parse 面證 err 分流真值)',
2878
+ parseOnly('').err === 'ERR_ONLY_SPEC_EMPTY' && parseOnly('abc').err === 'ERR_ONLY_SPEC_TOKEN'
2879
+ && parseOnly('0').err === 'ERR_ONLY_SPEC_UNKNOWN_SEC' && parseOnly('22').err === 'ERR_ONLY_SPEC_UNKNOWN_SEC'
2880
+ && parseOnly('99').err === 'ERR_ONLY_SPEC_UNKNOWN_SEC' && parseOnly('2-1').err === 'ERR_ONLY_SPEC_RANGE'
2881
+ && parseOnly('16,,17').err === 'ERR_ONLY_SPEC_EMPTY_TOKEN');
2882
+ await A('[24] 段級帳面顯形契約:全跑態 onlySpecDisp 空+onlyTag 光禿;--only 態 spec CSV 遞增+(--ONLY …) 尾碼形+secRan 本段已入帳',
2883
+ onlyMode() ? ((secRan.get(24) ?? 0) >= 3 && onlySpecDisp() === (onlyParsed?.set ?? []).join(',') && onlyTag().startsWith('(--ONLY ')) : (onlySpecDisp() === '' && onlyTag() === ''));
2884
+ await A('[24] skip 慣例面:secEnter(22)=false(22 慣例 skip 段——secOpen 行缺席+bannerless 斷言群帳入 [13] 的慣例面;secEnter 全跑態 n≥1 且 n≠22 即真=段落帳冊退役)',
2885
+ secEnter(22) === false);
2886
+ }
2887
+
2888
+ secOpen(25, '[25] 段級運行樣本:secEnter 真值表+tag 形帳+secCases 段帳'); if (secEnter(25)) {
2889
+ await A('[25] secEnter 真值表:bannerless 段全跑態行為(27 未在 secOrder 段帳=secRan 零位面——帳面逐段只列 secOrder;n=0 恆 false);全跑態實段 1/15/23 全真',
2890
+ !onlyMode() ? (secEnter(0) === false && (secRan.get(27) ?? 0) === 0 && secEnter(1) && secEnter(15) && secEnter(23)) : true);
2891
+ await A('[25] tag/spec 形帳:--only 態 (--ONLY …) 括形+spec 遞增 CSV 非空;全跑態 spec 空+tag 裸',
2892
+ onlyMode() ? (onlyTag().startsWith('(--ONLY ') && onlyTag().endsWith(')') && onlySpecDisp().length > 0)
2893
+ : (onlySpecDisp() === '' && onlyTag() === ''));
2894
+ await A('[25] secCases 段帳登記義務(負向斷言——teeth):secRan 實跑段(本機樹 [23]⑤ fail-open 表缺席異常面照行)必已登記;secCases 缺鍵且非 22 段=RED(secCases 全摘禁令的行為面承載)',
2895
+ (() => { for (const [k25] of secRan) if (!secCases[k25] && k25 !== 22) return false; return true; })());
2896
+ await A('[25] self-account:secCases[25] = 4 恰此四案(段帳面 self-proving——帳漂移即本面 RED;secCases 載體面:[24]/[25] 自證段+--only 段存在面+teeth 三載體)',
2897
+ secCases[25] === 4 && (secRan.get(25) ?? 0) === 3);
2898
+ }
2899
+
2900
+ // ── 段級帳面(誠實帳——skip 段恆列帳「未跑非通過」;全跑態逐段案數對 secCases 帳面)──
2901
+ 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, 26];
2902
+ const secTotal = secOrder.reduce((a, n) => a + (secCases[n] ?? 0), 0);
2903
+ const ranSecs = secOrder.filter((n) => (secRan.get(n) ?? 0) > 0);
2904
+ const selTotal = secOrder.filter((n) => !onlyMode() || onlySet.has(n) || n === 24 || n === 25).reduce((a, n) => a + (secCases[n] ?? 0), 0);
2905
+ const skipSecs = secOrder.filter((n) => !ranSecs.includes(n));
2906
+ console.log('\n──── 段級帳面 ────');
2907
+ for (const nSec of secOrder) {
2908
+ const ranN = secRan.get(nSec) ?? 0;
2909
+ const rowLabel = ranSecs.includes(nSec)
2910
+ ? (failures.length === 0 ? 'ok' : '見 ✗(失敗 ' + failures.length + '——詳上)')
2911
+ : 'skip(--only 態未選段——本輪未跑非通過)';
2912
+ console.log('[' + nSec + '] 案 ' + ranN + '/' + (secCases[nSec] ?? 0) + ' · ' + rowLabel);
2913
+ }
2914
+
2688
2915
  console.log(`\n${passed} 斷言全綠` + (failures.length ? `;${failures.length} 失敗` : ''));
2916
+ if (onlyMode()) {
2917
+ console.log('--only 態:段 ' + onlySpecDisp() + ' 運行,本輪 ' + passed + '/' + selTotal + '(含 runner 自證段 ' + ((secCases[24] ?? 0) + (secCases[25] ?? 0)) + ';總帳 ' + secTotal + ' 案非本輪載;skip ' + skipSecs.length + ' 段——本輪未跑非通過)');
2918
+ }
2689
2919
  if (failures.length) {
2690
2920
  console.error('失敗項:', failures);
2691
2921
  process.exit(1);
2692
2922
  }
2693
2923
 
2694
- console.log('CORE-CRYPTO-VERIFY-OK');
2924
+ console.log('CORE-CRYPTO-VERIFY-OK(' + passed + ' assertions)' + onlyTag());