@tacet-ink/journal-core 0.1.4 → 0.1.6

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.
@@ -14,10 +14,15 @@ import {
14
14
  generateNoteKey,
15
15
  generateRecToken,
16
16
  storeLocalWrap,
17
+ clearLocalWrap,
18
+ buildBindPayload,
19
+ openNoteKey,
17
20
  unwrapNoteKey,
18
21
  unwrapNoteKeyWithRecToken,
19
22
  wrapNoteKey,
20
23
  wrapNoteKeyWithRecToken,
24
+ wrapNoteKeyShare,
25
+ unwrapNoteKeyShare,
21
26
  recTokenHash,
22
27
  wrapNoteKeyDual,
23
28
  unwrapNoteKeyDual,
@@ -26,6 +31,11 @@ import {
26
31
  decryptAttach,
27
32
  encryptLocal,
28
33
  decryptLocal,
34
+ hexToBytes,
35
+ encryptWithKey,
36
+ decryptWithKey,
37
+ b64 as modB64,
38
+ unb64 as unb64Mod,
29
39
  } from '../src/client/note-crypto.ts';
30
40
  import {
31
41
  verifyArgonKat,
@@ -43,6 +53,24 @@ import {
43
53
  } from '../src/client/argon2.ts';
44
54
  import { makeKeyStore } from '../src/client/keys.ts';
45
55
  import type { NoteCryptoConfig } from '../src/client/note-crypto.ts';
56
+ import {
57
+ wrapNoteKeyPinLock,
58
+ unwrapNoteKeyPinLock,
59
+ type PinLockConfig,
60
+ } from '../src/client/pinlock.ts';
61
+ import {
62
+ makeInboundCipher,
63
+ isCipherFor,
64
+ validWrappedKey,
65
+ validHash64,
66
+ validSalt,
67
+ pickKeyPackage,
68
+ AUTH_RATE,
69
+ type CipherFormats,
70
+ } from '../src/server/auth.ts';
71
+ import { checkRate, isRateAllowed, SQL_RATE_BUMP, type RateWindow } from '../src/server/ratelimit.ts';
72
+ import { generateSessionToken, timingSafeEq } from '../src/server/hash.ts';
73
+ import { CORS_HEADERS, corsResponse } from '../src/server/cors.ts';
46
74
 
47
75
  const enc = new TextEncoder();
48
76
 
@@ -113,7 +141,8 @@ await A('AAD 防搬移:跨身份 guest 解密失敗回 null',
113
141
  // 舊版明文相容層:無前綴 = 原樣返回
114
142
  await A('舊明文原樣返回', (await decryptNote(TACET, held, '純舊明文', 'x', { current: () => identityA })) === '純舊明文');
115
143
 
116
- // ── 3. 包裹三件套(pass / rec / 本機) ───────────────────────────────────────
144
+ // ── 3. 包裹三件套(pass / rec / 本機;2026-09-24 效能審查節——buildBindPayload 兩段
145
+ // PBKDF2 600k 已由卡C t_7361b68c 收 Promise.all 並行,node 實測 133-152ms→68-84ms) ──
117
146
 
118
147
  console.log('\n[3] 金鑰包裹(passphrase / 復原 / 本機)');
119
148
  const pass = 'correct-horse-battery-staple-42';
@@ -206,13 +235,9 @@ await A('dual unwrapNoteKeyDual 拒收 jr1w 字串', (await unwrapNoteKeyDual(TA
206
235
  await A('既有 jr1w 包裹不受 dual 並存影響', (await unwrapNoteKey(TACET2, wrapped, pass, salt)) !== null);
207
236
 
208
237
  // payload 竄改 → 一律 null(自描述完整性:pinSalt/hksalt/iv/ct 任一區)
209
- function unb64(text: string): Uint8Array {
210
- const bin = atob(text);
211
- const bytes = new Uint8Array(bin.length);
212
- for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
213
- return bytes;
214
- }
215
- const payload2 = unb64(dual.wrapped.slice('jr2w.'.length));
238
+ // 卡C 收口:本地複製退場、直呼模組 unb64(單一真相;t_7361b68c [14] 導入面;
239
+ // round 2 NIT-5:轉手 wrapper 本身退場——呼叫端直用模組 unb64)。
240
+ const payload2 = unb64Mod(dual.wrapped.slice('jr2w.'.length));
216
241
  const tamperedAt = async (idx: number): Promise<boolean> => {
217
242
  const copy = payload2.slice();
218
243
  copy[idx] ^= 0x01;
@@ -260,7 +285,7 @@ await A('jr3w unwrap 拒收 jr1w 字串', (await unwrapNoteKey3(TACET3, wrapped,
260
285
  await A('jr1w unwrapNoteKey 拒收 jr3w 字串', (await unwrapNoteKey(TACET2, w3.wrapped, pass, w3.salt)) === null);
261
286
  await A('既有 jr1w 包裹照解(遷移契約:舊前綴不解散)', (await unwrapNoteKey(TACET2, wrapped, pass, salt)) !== null);
262
287
  // jr3w payload 竄改 → GCM 驗證失敗 → null
263
- const payload3 = unb64(w3.wrapped.slice('jr3w.'.length));
288
+ const payload3 = unb64Mod(w3.wrapped.slice('jr3w.'.length));
264
289
  const tampered3 = async (idx: number): Promise<boolean> => {
265
290
  const copy = payload3.slice();
266
291
  copy[idx] ^= 0x01;
@@ -438,7 +463,47 @@ await A('附件錯鑰匙 → null',
438
463
 
439
464
  // ── 11. 本機 IDB 密文(jr1d. cipherLocal opt-in)+ guest 選配拒絕面 ──────────
440
465
 
441
- console.log('\n[11] 本機 IDB 密文(jr1d. cipherLocal opt-in)+ guest 選配');
466
+ console.log('\n[11] 本機 IDB 密文(jr1d. cipherLocal opt-in)+ guest 選配拒絕面');
467
+ // 分享包裹(jrsw.,wrapShare 選配)+ jr3s. roundtrip(t_7710c766 目標 8:閘本有 roundtrip 斷言缺席)
468
+ const TACET_SHARE: NoteCryptoConfig = { ...TACET, wrapShare: 'jrsw.' };
469
+ const shareWrapped = await wrapNoteKeyShare(TACET_SHARE, noteKey, 'share-pass-42');
470
+ await A('分享包裹前綴 jrsw.', shareWrapped.wrapped.startsWith('jrsw.'));
471
+ await A('分享 salt = 16B hex', /^[0-9a-f]{32}$/.test(shareWrapped.salt));
472
+ const shareUnwrapped = await unwrapNoteKeyShare(TACET_SHARE, shareWrapped.wrapped, 'share-pass-42', shareWrapped.salt);
473
+ await A('jrsw unwrap 等值 noteKey(extractable 再 export)',
474
+ shareUnwrapped !== null && hex(new Uint8Array(await crypto.subtle.exportKey('raw', shareUnwrapped))) === origRaw);
475
+ await A('分享錯密語 → null', (await unwrapNoteKeyShare(TACET_SHARE, shareWrapped.wrapped, 'wrong-share-pass', shareWrapped.salt)) === null);
476
+ await A('分享錯 salt → null', (await unwrapNoteKeyShare(TACET_SHARE, shareWrapped.wrapped, 'share-pass-42', 'zz'.repeat(16))) === null);
477
+ await A('分享 malformed salt(zz 字元)→ null(t_7710c766:salt1 hex 形檢查)',
478
+ (await unwrapNoteKeyShare(TACET_SHARE, shareWrapped.wrapped, 'share-pass-42', '0'.repeat(31) + 'z')) === null);
479
+ await A('分享 payload 長度不符 → null',
480
+ (await unwrapNoteKeyShare(TACET_SHARE, shareWrapped.wrapped.slice(0, -3) + 'abc', 'share-pass-42', shareWrapped.salt)) === null);
481
+ await A('未配置 wrapShare → 分享解包 null(opt-in 拒絕面)',
482
+ (await unwrapNoteKeyShare(TACET, shareWrapped.wrapped, 'share-pass-42', shareWrapped.salt)) === null);
483
+ await A('本機包裹 roundtrip 經 loadLocalWrap 通路(clearLocalWrap 在場面)',
484
+ (async () => { await storeLocalWrap(TACET, localIdentity, noteKey); return true; })());
485
+ await A('clearLocalWrap 摘除本機包裹', (() => { clearLocalWrap(TACET, localIdentity); return TACET.store.get(TACET.store.noteKeyWrap(localIdentity)) === null; })());
486
+ await A('buildBindPayload 四件套輸出面(wrapped/wrappedRec/recTokenHash 同 wrapNoteKey 直呼)',
487
+ (async () => {
488
+ const p = await buildBindPayload(TACET, noteKey, pass, recToken, identityA);
489
+ return p.wrapped.startsWith('jr1w.') && p.wrappedRec.startsWith('jr1w.') &&
490
+ /^[0-9a-f]{32}$/.test(p.salt) && /^[0-9a-f]{64}$/.test(p.recTokenHash);
491
+ })());
492
+ await A('buildBindPayload unwrap 等值 noteKey(wrapped 面救得回)',
493
+ (async () => {
494
+ const p = await buildBindPayload(TACET, noteKey, pass, recToken, identityA);
495
+ const k = await unwrapNoteKey(TACET, p.wrapped, pass, p.salt);
496
+ return k !== null && hex(new Uint8Array(await crypto.subtle.exportKey('raw', k))) === origRaw;
497
+ })());
498
+ const bindPayloadProbe = await buildBindPayload(TACET, noteKey, pass, recToken, identityA);
499
+ await A('[14] buildBindPayload wrappedRec 去重帳(p.wrappedRec ≠ p.wrapped+rec 套件 unwrapNoteKeyWithRecToken 救回 origRaw)',
500
+ (async () => {
501
+ if (bindPayloadProbe.wrappedRec === bindPayloadProbe.wrapped) return false;
502
+ const kRec = await unwrapNoteKeyWithRecToken(TACET, bindPayloadProbe.wrappedRec, recToken, identityA);
503
+ return kRec !== null && hex(new Uint8Array(await crypto.subtle.exportKey('raw', kRec))) === origRaw;
504
+ })());
505
+ await A('[14] buildBindPayload recTokenHash 去重帳(p.recTokenHash === recTokenHash(recToken) 恆等;錯綁 passphrase 即 RED)',
506
+ bindPayloadProbe.recTokenHash === (await recTokenHash(recToken)) && bindPayloadProbe.recTokenHash !== (await recTokenHash(pass)));
442
507
  const bareCfgLocal: NoteCryptoConfig = { ...TACET }; // 無 cipherLocal 欄
443
508
  let localCfgThrow = '';
444
509
  try { await encryptLocal(bareCfgLocal, noteKey, '{}', 'jr1:n1'); } catch (e) { localCfgThrow = (e as Error).message; }
@@ -506,6 +571,559 @@ await A('配置 guest → decryptNote 舊明文相容層原樣(既有契約不
506
571
  await A('既有 guest 配置 roundtrip 不受選配收編影響',
507
572
  (await decryptNote(TACET, makeHeldKey(), cipherGuest, 'noteId:n1', { current: () => identityA })) === note);
508
573
 
574
+ // guest 空 identity 拒絕面(t_7710c766 目標 3):K_u = SHA-256(prefix ‖ '') 靜默產出「空帳號金鑰」是誤配炸彈
575
+ const TACET_GUESTOK: NoteCryptoConfig = { ...TACET }; // cipherGuest='jr1g.' 在場,專測 identity 面
576
+ const emptyIdp = { current: () => '' };
577
+ let emptyIdMsg: string | null = null;
578
+ try { await encryptNote(TACET_GUESTOK, makeHeldKey(), note, 'noteId:n1', emptyIdp); } catch (e) { emptyIdMsg = (e as Error).message; }
579
+ await A("guest 加密面空 identity → throw ERR_NO_IDENTITY(falsy 一體:空字串/undefined/null)",
580
+ emptyIdMsg === 'ERR_NO_IDENTITY');
581
+ // 空白 ' ' 是 truthy:K_u 照派生=加密面照常(呼叫端責任面);誠實界線=「非 falsy 不擋」的行為證據
582
+ let wsCipher = '';
583
+ try { wsCipher = await encryptNote(TACET_GUESTOK, makeHeldKey(), note, 'noteId:n1', { current: () => ' ' }); } catch { /* 不該發生 */ }
584
+ await A('guest 空白 identity 是 truthy → 照派生不擋(非 falsy 不拒的誠實界線;行為面)', wsCipher.startsWith('jr1g.'));
585
+ await A('guest 解密面空 identity → null(對稱面 fail-closed)',
586
+ (await decryptNote(TACET_GUESTOK, makeHeldKey(), cipherGuest, 'noteId:n1', emptyIdp)) === null);
587
+ await A('guest 空 identity 的既有 roundtrip 恆正常(identity 在場零新拒絕面)',
588
+ (await decryptNote(TACET_GUESTOK, makeHeldKey(), cipherGuest, 'noteId:n1', { current: () => identityA })) === note);
589
+
590
+ // 畸形配置 ''(空字串)正規化面(follow-up ②,t_44239f5f):空字串前綴=startsWith('') 恆真,
591
+ // bound 列會被誤導 guest 分支回 null。正規化後:加密面與未配置同拒、解密面 bound 照解、
592
+ // guest 密文(他家族形)null——與「未配置」行為完全同構。
593
+ const TACET_EMPTYGUEST: NoteCryptoConfig = { ...TACET, cipherGuest: '' };
594
+ await A("畸形 guest 前綴 '' → encryptNote guest 路徑同 ERR_GUEST_NOT_CONFIGURED(truthiness 對稱)",
595
+ (await (async () => { try { await encryptNote(TACET_EMPTYGUEST, makeHeldKey(), note, 'noteId:n1', { current: () => identityA }); return null; } catch (e) { return (e as Error).message; } })()) === 'ERR_GUEST_NOT_CONFIGURED');
596
+ await A("畸形 guest 前綴 '' → decryptNote bound 密文照解(不誤導 guest 分支回 null)",
597
+ (await decryptNote(TACET_EMPTYGUEST, heldBound, cipherBound, 'noteId:n1', { current: () => identityA })) === note);
598
+ await A("畸形 guest 前綴 '' → decryptNote guest 密文 null(guest 家族整面拒絕)",
599
+ (await decryptNote(TACET_EMPTYGUEST, makeHeldKey(), cipherGuest, 'noteId:n1', { current: () => identityA })) === null);
600
+ await A("畸形 guest 前綴 '' → decryptNote 舊明文相容層同「未配置」形(null,不當明文)",
601
+ (await decryptNote(TACET_EMPTYGUEST, makeHeldKey(), '純舊明文', 'x', { current: () => identityA })) === null);
602
+
603
+ // ── 12. 伺服器端原語(t_7710c766 目標 8:makeInboundCipher/validWrappedKey/pickKeyPackage/
604
+ // checkRate/timingSafeEq)+ 本機鎖定全函式(pinlock jr1p.,目標 8) ──────────
605
+
606
+ console.log('\n[12] 伺服器端原語(auth/ratelimit/hash)+ 本機鎖定(pinlock)');
607
+ // ratelimit 行為面(t_b91ed07f;r2 MAJOR-2 收口):真 node:sqlite 直載 UPSERT…RETURNING 單句,
608
+ // 行為斷言全數「真生產函式 checkRate 直驅」——D1 形 shim 包 node:sqlite 只做 D1 run() 的
609
+ // {results} 回帶形,無測試端 glue 副本。表名直插 SQL=注入面驗證;放行判準 count<=max
610
+ // 單一真相。同 IP 真併發不 undercount 的 D1 活線證據見卡 comments(wrangler dev 探針:
611
+ // UPSERT…RETURNING 於 wrangler 4.129.0 workerd 逐發回真值;併發首發兩發計數 1→2)。
612
+ type RateRow = { ip: string; window_start: number; count: number };
613
+ const RATE: RateWindow = { table: 'login_rate', windowMs: 60_000, max: 3 };
614
+ const makeRateDb = (): {
615
+ db: {
616
+ exec: (s: string) => void;
617
+ prepare: (s: string) => { all: (...p: unknown[]) => Array<Record<string, unknown>>; get: (...p: unknown[]) => Record<string, unknown> | undefined };
618
+ };
619
+ row: (ip: string) => RateRow | undefined;
620
+ runRaw: (sql: string) => void;
621
+ check: (ip: string) => Promise<boolean>;
622
+ } => {
623
+ const { DatabaseSync } = (globalThis as unknown as {
624
+ process?: { getBuiltinModule?: (id: string) => { DatabaseSync?: unknown } };
625
+ }).process?.getBuiltinModule?.('node:sqlite') ?? {};
626
+ if (!DatabaseSync) throw new Error('ERR_SQLITE_UNAVAILABLE');
627
+ const db = new (DatabaseSync as new (p: string) => {
628
+ exec: (s: string) => void;
629
+ prepare: (s: string) => { all: (...p: unknown[]) => Array<Record<string, unknown>>; get: (...p: unknown[]) => Record<string, unknown> | undefined };
630
+ })(':memory:');
631
+ db.exec('CREATE TABLE login_rate (ip TEXT PRIMARY KEY, window_start INTEGER, count INTEGER)');
632
+ return {
633
+ db,
634
+ row: (ip: string) =>
635
+ db.prepare('SELECT * FROM login_rate WHERE ip = ?').get(ip) as RateRow | undefined,
636
+ runRaw: (sql) => db.exec(sql),
637
+ // 真 checkRate 直驅:?1/?2/?3 有名參數語意在 node:sqlite 與 workerd 一致(端對端探針實證,見卡 comments)。
638
+ check: (ip) =>
639
+ checkRate(
640
+ {
641
+ DB: {
642
+ prepare: (sql: string) => ({
643
+ bind: (...p: unknown[]) => ({
644
+ run: async () => ({ results: db.prepare(sql).all(...p), meta: {} }),
645
+ }),
646
+ }),
647
+ },
648
+ } as unknown as Parameters<typeof checkRate>[0],
649
+ RATE,
650
+ ip,
651
+ ),
652
+ };
653
+ };
654
+ await A('rate:真 checkRate 窗內首發 pass(D1-shim 直驅;RETURNING n=1)', await (async () => {
655
+ const r = makeRateDb();
656
+ return (await r.check('ip-1')) === true && JSON.stringify(r.row('ip-1'))?.includes('"count":1');
657
+ })());
658
+ await A('rate:同 IP 連發衝突臂計數 1→2(UPSERT 單句原子——同鍵衝突逐發累計不 undercount;真併發面由 D1 活線探針承載見卡 comments)', await (async () => {
659
+ const r = makeRateDb();
660
+ return (await r.check('ip-c')) === true && (await r.check('ip-c')) === true && r.row('ip-c')?.count === 2;
661
+ })());
662
+ await A('rate:同 IP 三發 pass、第四發 reject(真 checkRate;放行判準 count<=max)', await (async () => {
663
+ const r = makeRateDb();
664
+ const seq = [await r.check('ip-3'), await r.check('ip-3'), await r.check('ip-3'), await r.check('ip-3')];
665
+ return seq[0] === true && seq[1] === true && seq[2] === true && seq[3] === false;
666
+ })());
667
+ await A('rate:真 checkRate 窗口過期整窗重置(舊窗 seed→重置發放行且 n=1 不吃 max 帽;window_start 換新至真時鐘)', await (async () => {
668
+ const r = makeRateDb();
669
+ r.runRaw("INSERT INTO login_rate (ip, window_start, count) VALUES ('ip-w', 1000, 99)");
670
+ const ok = (await r.check('ip-w')) === true;
671
+ const row = r.row('ip-w');
672
+ return ok && row?.count === 1 && (row?.window_start ?? 0) > 1_700_000_000_000;
673
+ })());
674
+ await A('rate:真 checkRate count 不封頂(超額發照實累計 6——審計可見非靜默吞)', await (async () => {
675
+ const r = makeRateDb();
676
+ for (let i = 0; i < 6; i++) await r.check('ip-m');
677
+ return r.row('ip-m')?.count === 6;
678
+ })());
679
+ await A('rate:真 checkRate 他 IP 隔離(計數互不干擾)', await (async () => {
680
+ const r = makeRateDb();
681
+ await r.check('ip-a');
682
+ await r.check('ip-b'); await r.check('ip-b');
683
+ return r.row('ip-a')?.count === 1 && r.row('ip-b')?.count === 2;
684
+ })());
685
+ await A('rate:checkRate 呼叫端形(env.DB.prepare→bind→run;餵 D1 的 SQL 直插面)+fail-open', await (async () => {
686
+ const calls: string[] = [];
687
+ const env12 = { DB: {
688
+ prepare: (sql: string) => ({
689
+ bind: (..._p: unknown[]) => ({ run: async () => { calls.push(sql); throw new Error('D1_DOWN'); } }),
690
+ }),
691
+ } };
692
+ const ok = await checkRate(env12 as unknown as Parameters<typeof checkRate>[0], RATE, 'ip-fail');
693
+ return ok === true && calls.length === 1 && calls[0].startsWith('INSERT INTO') && calls[0].includes('RETURNING');
694
+ })());
695
+ await A('rate:表名注入拒絕(直插 SQL 面)', await (async () => {
696
+ try { await checkRate({ DB: { prepare: () => { throw new Error('SHOULD_NOT_PREPARE'); } } } as unknown as Parameters<typeof checkRate>[0], { table: 'x; DROP TABLE users--', windowMs: 1000, max: 3 }, 'ip'); return false; }
697
+ catch (e) { return String((e as Error).message).startsWith('ERR_RATE_TABLE_NAME'); }
698
+ })());
699
+ await A('rate:表名 \\w 合法(字母數字底線)', await (async () => {
700
+ try { await checkRate({ DB: { prepare: () => { throw new Error('D1_DOWN_FAILOPEN'); } } } as unknown as Parameters<typeof checkRate>[0], { table: 'login_rate_2', windowMs: 1000, max: 3 }, 'ip'); return true; }
701
+ catch (e) { return String((e as Error).message).startsWith('ERR_RATE_TABLE_NAME') === false && String((e as Error).message).includes('D1_DOWN_FAILOPEN'); }
702
+ })());
703
+ await A('rate:RETURNING 異形回應守衛 fail-open(results 空/alias 斷裂——與儲存故障同向,r2 MINOR-3 不反向 deny)', await (async () => {
704
+ const mkEnv = (run: () => Promise<unknown>) =>
705
+ ({ DB: { prepare: () => ({ bind: () => ({ run }) }) } } as unknown as Parameters<typeof checkRate>[0]);
706
+ const empty = await checkRate(mkEnv(async () => ({ results: [] })), RATE, 'ip');
707
+ const alias = await checkRate(mkEnv(async () => ({ results: [{ m: 1 }] })), RATE, 'ip');
708
+ return empty === true && alias === true;
709
+ })());
710
+ await A('rate:SQL_RATE_BUMP 常數形(UPSERT…RETURNING…count AS n;{table} 佔位)',
711
+ /^INSERT INTO \{table\} \(ip, window_start, count\) VALUES \(\?1, \?2, 1\)\n\s+ON CONFLICT\(ip\) DO UPDATE SET\n\s+count\s+= CASE WHEN window_start <= \?3 THEN 1 ELSE count \+ 1 END,\n\s+window_start = CASE WHEN window_start <= \?3 THEN \?2 ELSE window_start END\n\s+RETURNING count AS n$/.test(SQL_RATE_BUMP));
712
+ await A('AUTH_RATE 契約(60s 窗 max 10)', AUTH_RATE.windowMs === 60_000 && AUTH_RATE.max === 10);
713
+ const sessA = generateSessionToken();
714
+ const sessB = generateSessionToken();
715
+ await A('generateSessionToken hex64', /^[0-9a-f]{64}$/.test(sessA));
716
+ await A('generateSessionToken 兩次相異', sessA !== sessB);
717
+ await A('timingSafeEq 等值 true', timingSafeEq(sessA, sessA));
718
+ await A('timingSafeEq 異值 false', timingSafeEq(sessA, sessB) === false);
719
+ await A('timingSafeEq 長度異 false', timingSafeEq(sessA, sessA.slice(0, 32)) === false);
720
+ await A('timingSafeEq 空串等值', timingSafeEq('', ''));
721
+ await A('timingSafeEq 空串異值 false', !timingSafeEq('', 'x'));
722
+
723
+ const formats: CipherFormats = { cipherPrefixes: ['jr1u.', 'jr1b.'], wrapPrefix: 'jr1w.', cipherMax: 5000 };
724
+ const inbound = makeInboundCipher(formats);
725
+ await A('inboundCipher:前綴密文原樣入庫', inbound('jr1b.' + b64(new Uint8Array(32))) === 'jr1b.' + b64(new Uint8Array(32)));
726
+ await A('inboundCipher:超限密文丟棄(截斷必壞)', inbound('jr1b.' + b64(new Uint8Array(4000))) === null);
727
+ await A('inboundCipher:超限明文截到上限照收', inbound('字'.repeat(7000))?.length === 5000);
728
+ await A('inboundCipher:b64 慣例照收', inbound(b64(new Uint8Array(64))) !== null);
729
+ await A('inboundCipher:控制字元垃圾丟棄', inbound('bad\x00junk') === null);
730
+ await A('inboundCipher:真明文照收', inbound('純明文') === '純明文');
731
+ await A('inboundCipher:非字串 null', inbound(42) === null);
732
+ await A('isCipherFor:家族前綴 true/非家族 false',
733
+ isCipherFor('jr1b.abc', formats) && isCipherFor('jr1u.abc', formats) && !isCipherFor('jr2x.abc', formats));
734
+ await A('isCipherFor 空字首組態 → false(\x27\x27 態 startsWith(\x27\x27) 恆真 accept-all——run 574 MINOR-3:cipherPrefixRe 守衛不覆蓋直讀 tuple 面,同向收口;非字串 false 契約照舊)',
735
+ (() => {
736
+ const emptyTuple: CipherFormats = { cipherPrefixes: ['', ''], wrapPrefix: 'jr1w.', cipherMax: 5000 };
737
+ const mixed: CipherFormats = { cipherPrefixes: ['', 'jr1b.'], wrapPrefix: 'jr1w.', cipherMax: 5000 };
738
+ return isCipherFor('jr1g.abc', emptyTuple) === false && isCipherFor('純明文', emptyTuple) === false &&
739
+ isCipherFor('jr1g.abc', mixed) === false && // 混合形:空槽摘除、真前綴面承載
740
+ isCipherFor('jr1b.abc', mixed) === true && // 真前綴面零誤傷
741
+ isCipherFor(null, formats) === false; // 非字串 false 面(typed 形合法態;運行時 typeof 守衛承載 unknown 呼叫端)
742
+ })());
743
+ await A('makeInboundCipher regex 跳脫全字面(r2 MAJOR-1 辨別形重推——探針新舊碼輸出一手實測,兩向分歧各一)',
744
+ (() => {
745
+ // 舊碼 .replace('.','\\.') 只跳第一個點,前綴含 regex 符號(| ( ))時家族 regex 失真,
746
+ // 兩方向輸出分歧(一手對照探針帳面:tmp probe-d1.cjs,main 舊碼 vs 分支新碼):
747
+ // ①完整前綴形『a.b|c()』+A*600(真家族密文標記):正碼辨得前綴 → >max 密文丟棄 null;
748
+ // 舊碼 regex 辨不出自家前綴 → 誤落明文截斷(>max 密文截半損毀=舊碼 slice(0,500))。
749
+ // ②點前截形『a.b』+A*600:舊碼 a\.b 殘臂(從頭起 b64 全程)誤咬 → 丟棄 null;
750
+ // 正碼前綴不完整非家族 → 明文截斷照收 slice(0,500)。兩形缺一即探針無齒(r1 教訓)。
751
+ const f: CipherFormats = { cipherPrefixes: ['a.b|c()', 'jr1b.'], wrapPrefix: 'jr1w.', cipherMax: 500 };
752
+ const m = makeInboundCipher(f);
753
+ const full = 'a.b|c()' + 'A'.repeat(600);
754
+ const dotHead = 'a.b' + 'A'.repeat(600);
755
+ return m(full) === null && m(dotHead) === dotHead.slice(0, 500);
756
+ })());
757
+ // N-7(t_580f9c54):cipherPrefixRe 空字首組態毒形——never-match 守衛的行為面。
758
+ // run 574 r2 校正(新舊碼組裝對照一手實測 /tmp/t580f9c54-r2/n7-corrected-probe.mjs):舊碼
759
+ // `[]` 與 `['']` 組裝出 byte-identical `^()[A-Za-z0-9+/]+={0,2}$`(join('|') 同為空串),
760
+ // `['','']` 僅多空首選交替 `^(|)…`=同面——真洞=無點純 b64 串(≤200)在舊碼
761
+ // validWrappedKey/pickKeyPackage 放行(NON-NULL);帶點前綴向量('jr1w.'+b64)舊碼本就 null
762
+ // ('.' 在 b64 charset 外)=帳面無承載。r1 版敘事(charset 恆假自帶防線/空槽首選交替洞)經
763
+ // run 574 一手實證俱誤——本段已按實測帳重寫(revert-poison 案為守衛還原面的直接承載)。
764
+ // 守衛後(usable 過濾+never-match):家族/re 面整面拒絕;inboundCipher 輸出面 zero delta
765
+ // (家族路摘除後長路徑 b64≤max 短路進 legacy 慣例層、超限丟棄、明文相容層照收——出口與舊碼
766
+ // RE 路徑恆等,probe 33 組合實證);混合『['','jr1b.']』形只摘空槽、真前綴家族面照活(降級非全拒)。
767
+ await A('N-7 真洞還原面:無點純 b64 ≤200 空字首組態 → null(cipherPrefixRe never-match——守衛摘除即翻紅;三組態)',
768
+ (() => {
769
+ const pureB64 = b64(new Uint8Array(32)); // 無點 b64(44 字符)——舊碼本就放行=真洞載體(帶點向量舊碼本就 null,無承載)
770
+ const emptyTuple = ['', ''];
771
+ return validWrappedKey(pureB64, []) === null &&
772
+ validWrappedKey(pureB64, emptyTuple) === null &&
773
+ validWrappedKey(pureB64, '') === null; // string 形空前綴同面
774
+ })());
775
+ await A('N-7 pickKeyPackage 空字首組態 → 整組放棄(validWrappedKey never-match 傳播——wrapped/salt 同 null)',
776
+ (() => {
777
+ const p = pickKeyPackage({ wrapped: b64(new Uint8Array(32)), salt: 'a'.repeat(32) }, ['', '']);
778
+ return p.wrapped === null && p.salt === null;
779
+ })());
780
+ await A('inboundCipher 空字首組態:輸出面 zero delta(family RE 摘除後長路徑 b64≤max 落 legacy 慣例層、超限丟棄、明文相容層照收——probe 33 組合實證舊≡新,僅路徑歸層從家族 RE 移到 b64 慣例層)',
781
+ (() => {
782
+ const mEmpty = makeInboundCipher({ cipherPrefixes: ['', ''], wrapPrefix: 'jr1w.', cipherMax: 500 });
783
+ const mOneEmpty = makeInboundCipher({ cipherPrefixes: ['', 'jr1b.'], wrapPrefix: 'jr1w.', cipherMax: 500 });
784
+ const plain = '純明文日記內容';
785
+ const pureB64 = b64(new Uint8Array(32)); // 44 字符 >40=legacy b64 慣例面(家族路拒絕後的唯一去處)
786
+ return mEmpty(plain) === plain.slice(0, 500) && // 明文相容層照收(家族守衛不誤傷 prefix 無關面)
787
+ mEmpty(pureB64) === pureB64 && // 家族路摘除後 b64 落 legacy 慣例層(與舊碼 RE+慣例短路出口恆等)
788
+ mOneEmpty('jr1b.' + b64(new Uint8Array(32))) === 'jr1b.' + b64(new Uint8Array(32)) && // 混合形只摘空槽——真前綴家族面照活
789
+ mOneEmpty(plain) === plain.slice(0, 500);
790
+ })()); // 真前綴放行面對照=下兩案「validWrappedKey:合法包裹/pickKeyPackage:成對成立」(同體放行帳,不另立重複案)
791
+ await A('validWrappedKey:合法包裹 true/超長 null/非法前綴 null',
792
+ validWrappedKey('jr1w.' + b64(new Uint8Array(32)), 'jr1w.') !== null &&
793
+ validWrappedKey('jr1w.' + b64(new Uint8Array(300)), 'jr1w.') === null &&
794
+ validWrappedKey('jr9x.' + b64(new Uint8Array(32)), 'jr1w.') === null);
795
+ await A('validHash64/validSalt 形狀', validHash64('a'.repeat(64)) !== null && validHash64('zz') === null && validSalt('a'.repeat(32)) !== null && validSalt('zz') === null);
796
+ const pkgOk = pickKeyPackage({ wrapped: 'jr1w.' + b64(new Uint8Array(32)), salt: 'a'.repeat(32) }, 'jr1w.');
797
+ await A('pickKeyPackage:成對成立', pkgOk.wrapped !== null && pkgOk.salt !== null);
798
+ await A('pickKeyPackage:缺 salt 整組放棄',
799
+ (() => { const p = pickKeyPackage({ wrapped: 'jr1w.' + b64(new Uint8Array(32)) }, 'jr1w.'); return p.wrapped === null && p.salt === null; })());
800
+ await A('pickKeyPackage:缺 wrapped 整組放棄',
801
+ (() => { const p = pickKeyPackage({ salt: 'a'.repeat(32) }, 'jr1w.'); return p.wrapped === null && p.salt === null; })());
802
+ await A('pickKeyPackage:錯前綴整組放棄',
803
+ (() => { const p = pickKeyPackage({ wrapped: 'jr9x.' + b64(new Uint8Array(32)), salt: 'a'.repeat(32) }, 'jr1w.'); return p.wrapped === null; })());
804
+
805
+ // 本機鎖定(jr1p.):全函式 roundtrip+嚴格檢查面(t_7710c766:pinlock 閘本有斷言缺席)
806
+ const PINLOCK: PinLockConfig = { pinLock: 'jr1p.', pinLockSaltPrefix: 'tacet-pinlock-v1:', pinLockAad: 'notekey-pinlock' };
807
+ const pinWrapped = await wrapNoteKeyPinLock(PINLOCK, noteKey, '2580ab');
808
+ await A('jr1p 包裹前綴 jr1p.', pinWrapped.startsWith('jr1p.'));
809
+ const pinUnwrapped = await unwrapNoteKeyPinLock(PINLOCK, pinWrapped, '2580ab');
810
+ await A('jr1p unwrap 等值 noteKey(extractable 再 export)',
811
+ pinUnwrapped !== null && hex(new Uint8Array(await crypto.subtle.exportKey('raw', pinUnwrapped))) === origRaw);
812
+ await A('jr1p 錯 PIN → null', (await unwrapNoteKeyPinLock(PINLOCK, pinWrapped, '999999')) === null);
813
+ await A('jr1p 空 PIN unwrap → null(解包面誠實拒絕)', (await unwrapNoteKeyPinLock(PINLOCK, pinWrapped, ' ')) === null);
814
+ await A('jr1p PIN 大小寫/空白正規化等價', (await unwrapNoteKeyPinLock(PINLOCK, pinWrapped, ' 2580AB ')) !== null);
815
+ await A('jr1p 空_PIN wrap 拒絕形', await (async () => {
816
+ try { await wrapNoteKeyPinLock(PINLOCK, noteKey, ''); return false; } catch (e) { return (e as Error).message === 'ERR_PINLOCK_EMPTY'; }
817
+ })());
818
+ await A('jr1p 未配置 cfg → wrap 拒絕(opt-in 律)', await (async () => {
819
+ try { await wrapNoteKeyPinLock({}, noteKey, '2580ab'); return false; } catch { return true; }
820
+ })());
821
+ await A('jr1p 未配置 cfg → unwrap null',
822
+ (await unwrapNoteKeyPinLock({ pinLock: 'jr1p.' }, pinWrapped, '2580ab')) === null);
823
+ await A('jr1p payload 竄改 pinSalt 區 → null(自描述完整性)', await (async () => {
824
+ const bytes = unb64Mod(pinWrapped.slice('jr1p.'.length)); bytes[3] ^= 0x01;
825
+ return (await unwrapNoteKeyPinLock(PINLOCK, 'jr1p.' + b64(bytes), '2580ab')) === null;
826
+ })());
827
+ await A('jr1p payload 竄改 ct 尾 → null', await (async () => {
828
+ const bytes = unb64Mod(pinWrapped.slice('jr1p.'.length)); bytes[bytes.length - 1] ^= 0x01;
829
+ return (await unwrapNoteKeyPinLock(PINLOCK, 'jr1p.' + b64(bytes), '2580ab')) === null;
830
+ })());
831
+ await A('jr1p 跨家族:jr1w. 字串餵 pinlock → null',
832
+ (await unwrapNoteKeyPinLock(PINLOCK, wrapped, '2580ab')) === null);
833
+ await A('jr1p 跨家族:jr1p. 字串餵 jr1w unwrap → null',
834
+ (await unwrapNoteKey(TACET2, pinWrapped, pass, salt)) === null);
835
+
836
+ // ── 13. 新契約面(t_7710c766:hex fail-closed/salt hex 形收口/PH1 鹽注入/錯誤碼語意分離) ──
837
+
838
+ console.log('\n[13] unwrap 驗證統一新契約(fail-closed + 語意分離)');
839
+ // hexToBytes fail-closed(目標 #2;note-crypto 與 bip39 同步——bip39 面是 HEX64_RE 前置自守)
840
+ await A('hexToBytes 合法輸出不變(舊碼同值;新向量)', hexToBytes('00ff10').join(',') === '0,255,16');
841
+ 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]); })());
842
+ await A("hexToBytes 非法字元 'zz' → throw ERR_BAD_HEX", await (async () => {
843
+ try { hexToBytes('zz'); return false; } catch (e) { return (e as Error).message === 'ERR_BAD_HEX'; }
844
+ })());
845
+ await A('hexToBytes 奇數長度 → throw', await (async () => {
846
+ try { hexToBytes('abc'); return false; } catch (e) { return (e as Error).message === 'ERR_BAD_HEX'; }
847
+ })());
848
+ await A('hexToBytes 空字串 → throw', await (async () => {
849
+ try { hexToBytes(''); return false; } catch (e) { return (e as Error).message === 'ERR_BAD_HEX'; }
850
+ })());
851
+ await A(`hexToBytes 全形正規化('${'ff'}')→ Bytes [255] 等值`, (() => { try { return Array.from(hexToBytes('ff')).join(',') === '255'; } catch { return false; } })());
852
+ await A(`hexToBytes 全形數字('2f')→ Bytes 等值(NFKC 窗殲滅——round 1 MINOR-4:parse 原始串 NaN 歸零殘形)`, (() => { try { return Array.from(hexToBytes('2f')).join(',') === '47'; } catch { return false; } })());
853
+ await A('bip39 hexToBytes 同步(HEX64_RE 自守前提下的舊向量不變)', (await bip.recTokenToWords('ab'.repeat(32)))?.length === 24);
854
+ await A('unwrap:鹽 hex 非法字元 → 假錯誤誘餌面 null(原碼靜默歸零會錯誤炸出成功路徑)',
855
+ (await unwrapNoteKey(TACET2, dual.wrapped, passD, 'a'.repeat(31) + 'z')) === null);
856
+ await A('unwrapNoteKeyDual:salt1 hex 非法字元 ' + 'z' + ' → null(鹽內嵌族 own 檢查照舊——收口本體=鹽外置族)',
857
+ (await unwrapNoteKeyDual(TACET2, dual.wrapped, passD, pinD, 'a'.repeat(31) + 'z')) === null);
858
+ await A('unwrapNoteKeyDual:salt1 hex 奇數長 → null', (await unwrapNoteKeyDual(TACET2, dual.wrapped, passD, pinD, 'a'.repeat(31))) === null);
859
+ await A('unwrapNoteKey3:salt hex 形檢查照舊(既有契約不變)',
860
+ (await unwrapNoteKey3(TACET3, w3.wrapped, pass, 'a'.repeat(31) + 'z')) === null);
861
+ // 舊碼(無 rawHex 長度/形檢)下 32-hex 假 noteKey 與 'zz' 前綴零化 hex 皆 NON-NULL=假金鑰生產器。
862
+ const craftKek = (async (): Promise<CryptoKey> => {
863
+ // deriveKek 鏡像(未匯出):PBKDF2-SHA256(pass, salt, 600k) → AES-GCM-256。
864
+ const keyMat = await crypto.subtle.importKey('raw', enc.encode(pass), 'PBKDF2', false, ['deriveKey']);
865
+ return crypto.subtle.deriveKey({ name: 'PBKDF2', salt: hexToBytes(salt) as BufferSource, iterations: PBKDF2_ITERATIONS, hash: 'SHA-256' }, keyMat, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
866
+ })();
867
+ // 鍛造面(格式嚴格契約的可觀察承載;毒化前提):KEK 已知者可造任意形 payload——
868
+ // 舊碼(無 rawHex 長度/形檢)下 'zz' 前綴零化 hex 與 32-hex 假 noteKey 皆 NON-NULL=假金鑰生產器。
869
+ // round 1 審查 MAJOR-1 校正:鍛造 encrypt 必帶 additionalData('notekey')=與 decryptWithKey
870
+ // 同 AAD——缺 AAD 的鍛造 GCM 層恆拒=rawHex 形檢從未執行(斷言空轉)。
871
+ // craftKek 鏡像真 deriveKek(同 pass 同 salt 同 600k)=鍛造鏈與真 unwrap 同 KEK:
872
+ // 有效形鍛造真解開(NON-NULL 自證鏈活)、畸形形由各檢查點拒——機制面直接可觀察。
873
+ // AAD 單一化(r2 NIT-a):F_AAD 是四鍛造向量唯一 AAD 源(對 decryptWithKey 同 AAD 律的 helper 收口)。
874
+ const F_AAD: BufferSource = enc.encode('notekey');
875
+ await A('MAJOR-1 機制面:帶 AAD 鍛造 ct 可解回 rawHex(形檢層可達=斷言活)', await (async () => {
876
+ const kekC = await craftKek;
877
+ const ivC = crypto.getRandomValues(new Uint8Array(12));
878
+ const ctC = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv: ivC, additionalData: F_AAD }, kekC, enc.encode('zz' + 'a'.repeat(62)) as BufferSource));
879
+ return (await decryptWithKey(kekC, new Uint8Array([...ivC, ...ctC]), 'notekey')) === 'zz' + 'a'.repeat(62);
880
+ })());
881
+ await A('MAJOR-1 空轉形:同 payload 無 AAD 鍛造 → decryptWithKey null(GCM 層恆拒=round 1 空轉病理)', await (async () => {
882
+ const kekC = await craftKek;
883
+ const ivC = crypto.getRandomValues(new Uint8Array(12));
884
+ const ctC = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv: ivC }, kekC, enc.encode('zz' + 'a'.repeat(62)) as BufferSource));
885
+ return (await decryptWithKey(kekC, new Uint8Array([...ivC, ...ctC]), 'notekey')) === null;
886
+ })());
887
+ await A('鍛造有效形 92B payload(64-hex 假 noteKey)→ NON-NULL(鍛造鏈自證:真解到 importAesGcm=假金鑰真產出)', await (async () => {
888
+ const kekC = await craftKek;
889
+ const ivC = crypto.getRandomValues(new Uint8Array(12));
890
+ const ctC = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv: ivC, additionalData: F_AAD }, kekC, enc.encode('ab'.repeat(32)) as BufferSource));
891
+ const p = new Uint8Array(12 + ctC.byteLength); p.set(ivC, 0); p.set(ctC, 12);
892
+ const k = await unwrapNoteKey(TACET, 'jr1w.' + b64(p), pass, salt);
893
+ if (k === null) return false;
894
+ const raw = new Uint8Array(await crypto.subtle.exportKey('raw', k));
895
+ return raw.length === 32 && raw.every((b: number) => b === 0xab);
896
+ })());
897
+ await A('鍛造 92B payload 的零化 hex rawHex(zz 前綴)→ null(rawHex HEX_RE 形檢+hexToBytes fail-closed 雙層——單摘任一被次層接住,P2 同摘 6F 才翻;帶 AAD 真解後形檢拒)', await (async () => {
898
+ const kekC = await craftKek;
899
+ const ivC = crypto.getRandomValues(new Uint8Array(12));
900
+ const ctC = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv: ivC, additionalData: F_AAD }, kekC, enc.encode('zz' + 'a'.repeat(62)) as BufferSource));
901
+ const p = new Uint8Array(12 + ctC.byteLength); p.set(ivC, 0); p.set(ctC, 12);
902
+ return (await unwrapNoteKey(TACET, 'jr1w.' + b64(p), pass, salt)) === null;
903
+ })());
904
+ // 短 payload 鍛造(60B:iv+ct(hex32假 noteKey))→ null:openNoteKey 嚴格長度檢的可觀察承載
905
+ //(僅 hexToBytes fail-closed 擋不住「合法 hex 的假 32B 金鑰」——長度檢獨立承載)。
906
+ await A('鍛造 60B payload(合法 hex 32B 假 noteKey,帶 AAD)→ null(openNoteKey 嚴格 92B 長度檢+rawHex 64B 長度雙層——P1b 同摘才翻)', await (async () => {
907
+ const kekC = await craftKek;
908
+ const ivC = crypto.getRandomValues(new Uint8Array(12));
909
+ const ctC = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv: ivC, additionalData: F_AAD }, kekC, enc.encode('ab'.repeat(16)) as BufferSource));
910
+ const p = new Uint8Array(12 + ctC.byteLength); p.set(ivC, 0); p.set(ctC, 12);
911
+ return (await unwrapNoteKey(TACET, 'jr1w.' + b64(p), pass, salt)) === null;
912
+ })());
913
+ await A('encryptWithKey 產物餵 unwrap(長度恆不符 60B)→ null(短 payload 鍛造面對照)', await (async () => {
914
+ const kekC = await craftKek;
915
+ const short = await encryptWithKey(kekC, 'ab'.repeat(16), 'notekey'); // iv12+ct48 = 60B ≠ 92B
916
+ return (await unwrapNoteKey(TACET, 'jr1w.' + short, pass, salt)) === null;
917
+ })());
918
+ // guest 空 identity 自洽密文面(P5 的行為承載):手工以 K_u(prefix‖'') 造密文——
919
+ // 舊碼(守衛缺席)下 deriveGuestKey('')=同一把「空帳號金鑰」→ 解開=NON-NULL 假相;
920
+ // 新契約下解密面守衛先擋=null。這是解密面守衛唯一真咬的行為向量。
921
+ await A('guest 空 identity 自洽密文 → decryptNote null(「空帳號金鑰」整族拒絕=解密面守衛承載)', await (async () => {
922
+ const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', enc.encode('tacet-note-u1')));
923
+ const kuEmpty = await crypto.subtle.importKey('raw', digest, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
924
+ const cipherEmpty = 'jr1g.' + await encryptWithKey(kuEmpty, note, 'noteId:n1');
925
+ return (await decryptNote(TACET_GUESTOK, makeHeldKey(), cipherEmpty, 'noteId:n1', emptyIdp)) === null;
926
+ })());
927
+ // 錯誤碼語意分離(目標 6):空 PIN ≠ 未配置;jr3d 兩 case 同碼分流
928
+ await A('wrapNoteKeyDual 空 PIN → ERR_PIN_EMPTY(非 NOT_CONFIGURED)', await (async () => {
929
+ try { await wrapNoteKeyDual(TACET2, noteKey, passD, ''); return false; } catch (e) { return (e as Error).message === 'ERR_PIN_EMPTY'; }
930
+ })());
931
+ await A('wrapNoteKeyDual 空 PIN 全形空白正規化後 → ERR_PIN_EMPTY', await (async () => {
932
+ try { await wrapNoteKeyDual(TACET2, noteKey, passD, ' '); return false; } catch (e) { return (e as Error).message === 'ERR_PIN_EMPTY'; }
933
+ })());
934
+ await A('wrapNoteKeyDual 未配置 wrapDual → ERR_DUAL_NOT_CONFIGURED(契約不變)', await (async () => {
935
+ try { await wrapNoteKeyDual(TACET, noteKey, passD, pinD); return false; } catch (e) { return (e as Error).message === 'ERR_DUAL_NOT_CONFIGURED'; }
936
+ })());
937
+ await A('wrapNoteKeyDual3 空 PIN → ERR_PIN_EMPTY(t_7710c766:兩 case 同碼分流)', await (async () => {
938
+ try { await wrapNoteKeyDual3(TACET3, noteKey, passD, ''); return false; } catch (e) { return (e as Error).message === 'ERR_PIN_EMPTY'; }
939
+ })());
940
+ await A('wrapNoteKeyDual3 未配置 wrapDual3 → ERR_JR3W_NOT_CONFIGURED(契約不變)', await (async () => {
941
+ try { await wrapNoteKeyDual3({}, noteKey, passD, pinD); return false; } catch (e) { return (e as Error).message === 'ERR_JR3W_NOT_CONFIGURED'; }
942
+ })());
943
+ // PH1 鹽注入(目標 4):預設舊值零行為變更
944
+ await A('derivePh1Argon 預設鹽零行為變更(同 pass 同值)',
945
+ (await derivePh1Argon('probe-determinism-pass-42')) === ph1v2a);
946
+ async function srcOf(rel: string): Promise<string> {
947
+ // node 內建模組動態存取(structured type,零 node types 依賴——argon2.ts getBuiltinModule 母型同構;
948
+ // TS2591 types 帽下 import('node:fs') 靜態/動態皆炸=此繞法)
949
+ const fs = (globalThis as unknown as {
950
+ process?: { getBuiltinModule?: (id: string) => { readFileSync?: (p: string, enc: string) => string } | undefined };
951
+ }).process?.getBuiltinModule?.('node:fs');
952
+ if (!fs?.readFileSync) throw new Error('ERR_FS_UNAVAILABLE');
953
+ return fs.readFileSync(new URL(rel, import.meta.url).pathname, 'utf8');
954
+ }
955
+ await A('derivePh1Argon 預設分支源碼面 = encode(PH1_V2_SALT)(鹽預設單一真相錨;同-pass 同值斷言對預設值漂移無承載力)',
956
+ /saltArg \?\? new TextEncoder\(\)\.encode\(PH1_V2_SALT\)/.test(await srcOf('../src/client/argon2.ts')));
957
+ await A('derivePh1Argon 自選鹽 → 不同派生值', (await derivePh1Argon('probe-determinism-pass-42', new Uint8Array(12))) !== ph1v2a);
958
+ await A('derivePh1Argon 自選鹽 hex64 形', /^[0-9a-f]{64}$/.test(await derivePh1Argon('x', new Uint8Array(12))));
959
+ await A('PH1_V2_SALT re-export 在場(index barrel;其他產品可見可注入同源鹽)', PH1_V2_SALT === 'tacet-ph1-v1');
960
+ // index barrel 新匯出(目標 5):clearLocalWrap/buildBindPayload(review「缺匯出」條誤差已校正=本卡以閘承載)
961
+ await A('index barrel 匯出 clearLocalWrap/buildBindPayload', await (async () => {
962
+ const src = await import('../src/index.ts');
963
+ return typeof src.clearLocalWrap === 'function' && typeof src.buildBindPayload === 'function';
964
+ })());
965
+ // PinLockConfig 死欄退場(t_7710c766):型別面真齒=excess-property 探針(round 1 審查 MINOR-3:
966
+ // 舊形 typeof cfg==='object' 恆真空轉)。r2 MINOR-2 實測校正(t_580f9c54;行號帳由 P12 毒化 tsc 輸出承載
967
+ // 而非註解寫死——新增段推移行號=註解行號帳漂移,NIT-a 同病禁再犯):required 復活形=TS2741(PINLOCK
968
+ // 常數面)+TS2345(未配置兩案),optional 復活形=舊形探針未帶該欄=excess-property 檢照不到=tsc 0 綠——robust 形=pristine 就在探針字面量帶 `noteKeyExtractable:
969
+ // undefined as never`+前置 @ts-expect-error:欄位復活任何形(required/optional/false 字面量)都多出待位錯誤=TS2578 紅;
970
+ // pristine 態錯誤恰由 expect-error 吸收=全 repo tsc 0 綠(探針字面量缺 false 形欄不另生錯=零加值面除名)。
971
+ // 本面綠態=core+tacet 摘欄後編譯綠。@ts-expect-error 常駐在場(robust 形,見上註);pristine 態恰由其吸收。
972
+ // @ts-expect-error — 死欄退場待位錯誤面(pristine 恰此一待位錯由本指令吸收;欄位復活任何形=本指令反噬 TS2578=P12 案紅)
973
+ const _cfgProbe: PinLockConfig = { pinLock: 'jr1p.', pinLockSaltPrefix: 'p:', pinLockAad: 'notekey-pinlock', noteKeyExtractable: undefined as never };
974
+ void _cfgProbe;
975
+ // 壞 b64 面向量(openNoteKey 誠實契約「任何不符恆回 null 不拋」的可觀察承載——round 1 MINOR-5):
976
+ await A('鍛造壞 base64 面字符 → unwrapNoteKey null(openNoteKey unb64 拋點吞收=不拋契約;家族面毒化形=頂層 crash,本面包 try/catch 收乾淨 ✗)', await (async () => {
977
+ try {
978
+ return (await unwrapNoteKey(TACET, 'jr1w.' + '!!not-base64!!', pass, salt)) === null;
979
+ } catch { return false; } // 吞收契約破形(本體或呼叫端漏 catch)=本面 false 紅,非 runner crash
980
+ })());
981
+ await A('openNoteKey 直接呼叫:壞 base64 → null 不拋(本體吞收點)', await (async () => {
982
+ const kekC = await craftKek;
983
+ return (await openNoteKey('jr1w.' + '!!not-base64!!', kekC, 'notekey', 'jr1w.')) === null;
984
+ })());
985
+
986
+ // ── 14. 效能形契約(卡C t_7361b68c:零行為變更——輸出 byte 等價是合約;錨面咬「形」) ──
987
+ //
988
+ // 效能批的閘承載物理:執行時間不能進閘(機器相依)——咬「並行形在場+串行殘留歸零」
989
+ // 靜態錨+「Argon 串行刻意保留」負向+b64 輸出等價行為面+Max-Age/sideEffects 字面+
990
+ // ikm 組裝序行為向量(KAT14 凍結 blob)。執行帳(node 26.8.1 實測,2026-10-04):
991
+ // buildBindPayload 串行 133-152ms→並行 68-84ms;dual 串行 234→並行 179ms median;
992
+ // b64 4MiB 132-181ms→18ms。瀏覽器帳=review 222ms→27ms。
993
+
994
+ console.log('\n[14] 效能形契約(buildBindPayload 並行/b64 分塊/argon 禁並行/CORS Max-Age/sideEffects)');
995
+
996
+ const noteCryptoSrc14 = await srcOf('../src/client/note-crypto.ts');
997
+ const argon2Src14 = await srcOf('../src/client/argon2.ts');
998
+ const corsSrc14 = await srcOf('../src/server/cors.ts');
999
+ await A('[14] buildBindPayload 兩段共用 Promise.all 並行錨(wrapNoteKey+wrapNoteKeyWithRecToken)',
1000
+ /await Promise\.all\(\[\r?\n\s*wrapNoteKey\(cfg, noteKey, passphrase\),\r?\n\s*wrapNoteKeyWithRecToken\(cfg, noteKey, recToken, identity\)/.test(noteCryptoSrc14));
1001
+ await A('[14] deriveKek2 pass/pin 兩段 PBKDF2 共用 Promise.all 並行錨(RHS 指令序面)',
1002
+ /await Promise\.all\(\[\r?\n\s*derivePbkdf2Bits\(passphrase, salt1, PBKDF2_ITERATIONS\),\r?\n\s*derivePbkdf2Bits\(pin,/.test(noteCryptoSrc14));
1003
+ await A('[14] deriveKek2Argon 正向串行錨(pass/pin 兩段 deriveArgon2id 串行在場)',
1004
+ /const passBits = await deriveArgon2id\(/.test(argon2Src14) && /const pinBits = await deriveArgon2id\(/.test(argon2Src14));
1005
+ await A('[14] argon2 destructure 並行形歸零(舊串行面對稱收口——r2 MAJOR-1 防逆向回歸)',
1006
+ argon2Src14.split('const [passBits, pinBits] = await Promise.all([').length === 1);
1007
+ await A('[14] buildBindPayload 舊串行形歸零(await wrapNoteKeyWithRecToken 串行殘留)',
1008
+ !noteCryptoSrc14.includes('await wrapNoteKeyWithRecToken'));
1009
+ await A('[14] deriveKek2 舊串行形歸零(串行 passBits 殘留)',
1010
+ !/const passBits = await derivePbkdf2Bits/.test(noteCryptoSrc14));
1011
+ // r2 MAJOR-1:並行錨升左手側——destructure 與 Promise.all 相鄰形恰一,對調滑接(pin‖pass)
1012
+ // 即 RED(RHS 指令序不變=原本體並行錨不動,兩錨分工:RHS 面/組件序面)。
1013
+ await A('[14] deriveKek2 destructure 左手側恰一(const [passBits, pinBits] + Promise.all 相鄰形——swap 滑接即 RED)',
1014
+ noteCryptoSrc14.split('const [passBits, pinBits] = await Promise.all([').length === 2);
1015
+ // MAJOR-1 行為面:bea6ab8 逐值凍結 jr2w. blob(手工 pristine 組裝=passBits@0‖pinBits@32 序)——
1016
+ // destructure 對調滑接產 pin‖pass ikm=本 blob 不可解(reviewer rev-harm 雙向一手:pristine 自解
1017
+ // true/互解 null;r2 樹 A/C/B/D/E/F 六腿同帳,探針存證 kanban 工作區)。凍結定值自足:
1018
+ // salt1 'a1'×16/pinSalt 0xb2+i×16/iv 0x44+i×12/raw 'c3'×32/pass/pin 明寫——重放=按定值
1019
+ // 重建 KEK2(HKDF info 'journal-kek2-v1:jr2w.'、aad 'notekey2')比對 unwrap=raw。
1020
+ const KAT14 = {
1021
+ blob: 'jr2w.srO0tba3uLm6u7y9vr/AwURFRkdISUpLTE1OTy8+hmx+AhMYCgPVBH1Q1q1UnjPF9uvN/ie4DoYqh4LGkMTHvHDy/Sku/lMRvoRWfuqSuIlX0rEpVU8HfMajsaYtzaz1zHFQELEIuzTsM6+S',
1022
+ salt1: 'a1'.repeat(16),
1023
+ pass: 'correct-horse-battery-staple-42',
1024
+ pin: '482913',
1025
+ raw: 'c3'.repeat(32), // 計算式構造(手打對數面禁止——r2 首跑 1 FAIL 即手打 raw 對數錯的實證)
1026
+ };
1027
+ await A('[14] jr2w. ikm 組裝序行為向量(bea6ab8 凍結 blob unwrap=raw——destructure 對調滑接不可解)',
1028
+ (async () => {
1029
+ const k = await unwrapNoteKeyDual(TACET2, KAT14.blob, KAT14.pass, KAT14.pin, KAT14.salt1);
1030
+ if (k === null) return false;
1031
+ return hex(new Uint8Array(await crypto.subtle.exportKey('raw', k))) === KAT14.raw;
1032
+ })());
1033
+ await A('[14] argon 側禁並行警示在場(deriveKek2Argon 串行刻意——hash-wasm 共享記憶體池)',
1034
+ /禁並行/.test(argon2Src14) && !argon2Src14.includes('await Promise.all'));
1035
+ await A('[14] b64 分塊 apply 錨+舊逐位元組形歸零(fromCharCode(bytes[i]) 殘留)',
1036
+ noteCryptoSrc14.includes('String.fromCharCode.apply(null, bytes.subarray(i, i + 8192)') &&
1037
+ !noteCryptoSrc14.includes('String.fromCharCode(bytes[i])'));
1038
+ // b64 行為面:分塊後輸出必逐位元組等價(本閘起手 b64 是獨立複製=黃金對照);邊界尺寸
1039
+ //(0/1/7/8191/8192/8193)咬窗緣、65539 非 8192 倍數咬長尾、4MiB 咬分塊多輪+unb64 全往返。
1040
+ const b64Ref14 = (bytes: Uint8Array): string => {
1041
+ let bin = '';
1042
+ for (let i = 0; i < bytes.byteLength; i++) bin += String.fromCharCode(bytes[i]);
1043
+ return btoa(bin);
1044
+ };
1045
+ {
1046
+ let boundsEq = true;
1047
+ for (const size of [0, 1, 7, 8191, 8192, 8193]) {
1048
+ const buf = new Uint8Array(size);
1049
+ for (let i = 0; i < size; i++) buf[i] = (i * 7 + 3) & 0xff;
1050
+ if (modB64(buf) !== b64Ref14(buf)) boundsEq = false;
1051
+ }
1052
+ await A('[14] b64 輸出等價:邊界尺寸 0/1/7/8191/8192/8193 逐位元組對照', boundsEq);
1053
+ }
1054
+ await A('[14] b64 輸出等價:65539(非 8192 倍數長尾)逐位元組對照', (() => {
1055
+ const buf = new Uint8Array(65539);
1056
+ for (let i = 0; i < buf.length; i++) buf[i] = (i * 13 + 5) & 0xff;
1057
+ return modB64(buf) === b64Ref14(buf);
1058
+ })());
1059
+ await A('[14] b64 輸出等價:4MiB 多區塊+unb64 roundtrip 逐位元組',
1060
+ (() => {
1061
+ const buf = new Uint8Array(4 * 1024 * 1024);
1062
+ for (let i = 0; i < buf.length; i++) buf[i] = i & 0xff;
1063
+ const s = modB64(buf);
1064
+ if (s !== b64Ref14(buf)) return false;
1065
+ const back = unb64Mod(s);
1066
+ if (back.byteLength !== buf.byteLength) return false;
1067
+ for (let i = 0; i < buf.length; i++) if (back[i] !== buf[i]) return false;
1068
+ return true;
1069
+ })());
1070
+ await A('[14] CORS_HEADERS 帶 Access-Control-Max-Age(preflight 快取 86400)',
1071
+ CORS_HEADERS['Access-Control-Max-Age'] === '86400' && /'Access-Control-Max-Age': '86400'/.test(corsSrc14));
1072
+ await A('[14] CORS 其餘三頭零變(ACAO */Methods GET, POST, OPTIONS/Headers Content-Type, Authorization)',
1073
+ CORS_HEADERS['Access-Control-Allow-Origin'] === '*' &&
1074
+ CORS_HEADERS['Access-Control-Allow-Methods'] === 'GET, POST, OPTIONS' &&
1075
+ CORS_HEADERS['Access-Control-Allow-Headers'] === 'Content-Type, Authorization');
1076
+ await A('[14] corsResponse 仍完整轉發四頭(corsResponse 面帶 Max-Age)',
1077
+ (() => {
1078
+ const h = new Headers({ 'x-probe': '1' });
1079
+ const out = corsResponse(new Response('ok', { headers: h }));
1080
+ return Object.entries(CORS_HEADERS).every(([k, v]) => out.headers.get(k) === v) && out.headers.get('x-probe') === '1';
1081
+ })());
1082
+ {
1083
+ const fsPkg14 = (globalThis as unknown as {
1084
+ process?: { getBuiltinModule?: (id: string) => { readFileSync?: (p: string, enc: string) => string } | undefined };
1085
+ }).process?.getBuiltinModule?.('node:fs');
1086
+ const pkg14 = JSON.parse(fsPkg14!.readFileSync!(new URL('../package.json', import.meta.url).pathname, 'utf8')) as { sideEffects?: boolean; version: string };
1087
+ await A('[14] package.json sideEffects:false(tree-shake server 模組+BIP39 詞表)', pkg14.sideEffects === false);
1088
+ // sideEffects 前提(round 2 MINOR-1 升級):src 標目 **遞迴** 枚舉(硬編碼清單退場——新模組
1089
+ // 自動入掃),頂層語句白名單化+**宣告行初始化式呼叫面檢查**(const x = fn() 形=執行點;
1090
+ // Object.freeze(...)/純常數結構白名單——wordlist.ts:7 即此形)。
1091
+ await A('[14] sideEffects 前提:src 頂層零副作用語句+宣告行初始化式零裸呼叫(Object.freeze 白名單)',
1092
+ await (async () => {
1093
+ const fs14 = (globalThis as unknown as {
1094
+ process?: { getBuiltinModule?: (id: string) => { readFileSync?: (p: string, enc: string) => string; readdirSync?: (p: string) => string[] } | undefined };
1095
+ }).process?.getBuiltinModule?.('node:fs');
1096
+ if (!fs14?.readFileSync || !fs14?.readdirSync) return false;
1097
+ const urlOf = (p: string) => new URL(`../${p}`, import.meta.url).pathname;
1098
+ const listTs = (dir: string): string[] => fs14.readdirSync!(urlOf(dir)).flatMap((e: string) => {
1099
+ const full = `${dir}/${e}`;
1100
+ let isDir = false;
1101
+ try { fs14.readdirSync!(urlOf(full)); isDir = true; } catch { isDir = false; }
1102
+ if (isDir) return listTs(full);
1103
+ return e.endsWith('.ts') ? [full] : [];
1104
+ });
1105
+ const files = listTs('src');
1106
+ if (files.length < 12) return false; // 枚舉自守衛:src 樹不該少於既有 12 模組
1107
+ let offenders = 0;
1108
+ for (const f of files) {
1109
+ const lines = fs14.readFileSync!(urlOf(f), 'utf8').split('\n');
1110
+ for (const line of lines) {
1111
+ if (/^[a-zA-Z(/{\[]/.test(line)) {
1112
+ // 頂層語句白名單:import/export+註解+純宣告(const/let/var/function/async/
1113
+ // interface/type/enum/class/declare)——其餘字母開頭頂層=執行面 side effect。
1114
+ if (/^(import\b|export\b|\/\*\*|\*|\/\/|const\b|let\b|var\b|function\b|async\b|interface\b|type\b|enum\b|class\b|declare\b)/.test(line)) {
1115
+ // 宣告行初始化面:const x = <呼叫形>()=模組載入即執行(NIT MINOR-1 缺口)。
1116
+ // 白名單:Object.freeze(純凍結常數)。其餘呼叫形初始化=offender。
1117
+ if (/^(const\b|let\b|var\b|export const\b).*=.*\b[A-Za-z_$][\w$]*\(\s*[^)]/.test(line) &&
1118
+ !/Object\.freeze\(/.test(line)) offenders++;
1119
+ } else offenders++;
1120
+ }
1121
+ }
1122
+ }
1123
+ return offenders === 0;
1124
+ })());
1125
+ }
1126
+
509
1127
  // ── 決議 ────────────────────────────────────────────────────────────────────
510
1128
 
511
1129
  console.log(`\n${passed} 斷言全綠` + (failures.length ? `;${failures.length} 失敗` : ''));