@tacet-ink/journal-core 0.2.0 → 0.2.1

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.
@@ -161,8 +161,8 @@ await A('AAD 防搬移:跨身份 guest 解密失敗回 null',
161
161
  // 舊版明文相容層:無前綴 = 原樣返回
162
162
  await A('舊明文原樣返回', (await decryptNote(TACET, held, '純舊明文', 'x', { current: () => identityA })) === '純舊明文');
163
163
 
164
- // ── 3. 包裹三件套(pass / rec / 本機;2026-09-24 效能審查節——buildBindPayload 兩段
165
- // PBKDF2 600k 已由卡C t_7361b68c 收 Promise.all 並行,node 實測 133-152ms→68-84ms) ──
164
+ // ── 3. 包裹三件套(pass / rec / 本機;2026-09-24 效能節——buildBindPayload 兩段
165
+ // PBKDF2 600k 已收 Promise.all 並行,node 實測 133-152ms→68-84ms) ──
166
166
 
167
167
  console.log('\n[3] 金鑰包裹(passphrase / 復原 / 本機)');
168
168
  const pass = 'correct-horse-battery-staple-42';
@@ -255,8 +255,8 @@ await A('dual unwrapNoteKeyDual 拒收 jr1w 字串', (await unwrapNoteKeyDual(TA
255
255
  await A('既有 jr1w 包裹不受 dual 並存影響', (await unwrapNoteKey(TACET2, wrapped, pass, salt)) !== null);
256
256
 
257
257
  // payload 竄改 → 一律 null(自描述完整性:pinSalt/hksalt/iv/ct 任一區)
258
- // 卡C 收口:本地複製退場、直呼模組 unb64(單一真相;t_7361b68c [14] 導入面;
259
- // round 2 NIT-5:轉手 wrapper 本身退場——呼叫端直用模組 unb64)。
258
+ // 收口:本地複製退場、直呼模組 unb64(單一真相;[14] 導入面;
259
+ // 轉手 wrapper 本身退場——呼叫端直用模組 unb64)。
260
260
  const payload2 = unb64Mod(dual.wrapped.slice('jr2w.'.length));
261
261
  const tamperedAt = async (idx: number): Promise<boolean> => {
262
262
  const copy = payload2.slice();
@@ -410,7 +410,7 @@ await A('25 詞 → null', (await bip.wordsToRecToken([...kitWords, kitWords[0]]
410
410
  await A('詞表外 → null', (await bip.wordsToRecToken(kitWords.slice(0, 23).concat('notaword').join(' '))) === null);
411
411
 
412
412
  // checksum 面:錯一詞(詞表內不同詞)→ 幾乎必被 checksum 抓;固定種子 64 樣本全抓。
413
- // (t_d1cf3846:真隨機取樣時 P(漏 ≥2)≈0.4%=偶發 62/64 flake 實證——改 xorshift 常數
413
+ // (真隨機取樣時 P(漏 ≥2)≈0.4%=偶發 62/64 flake 實證——改 xorshift 常數
414
414
  // 種子樣本集(bip39.seededSampleBytes),向量可重放、閘輸出逐輪恆定;種子窗 1..64
415
415
  // 經五窗 320 樣本探針驗證全抓後採用,非挑窗)
416
416
  const wlArr: readonly string[] = wl.wordlist;
@@ -484,7 +484,7 @@ await A('附件錯鑰匙 → null',
484
484
  // ── 11. 本機 IDB 密文(jr1d. cipherLocal opt-in)+ guest 選配拒絕面 ──────────
485
485
 
486
486
  console.log('\n[11] 本機 IDB 密文(jr1d. cipherLocal opt-in)+ guest 選配拒絕面');
487
- // 分享包裹(jrsw.,wrapShare 選配)+ jr3s. roundtrip(t_7710c766 目標 8:閘本有 roundtrip 斷言缺席)
487
+ // 分享包裹(jrsw.,wrapShare 選配)+ jr3s. roundtrip(閘本有 roundtrip 斷言缺席)
488
488
  const TACET_SHARE: NoteCryptoConfig = { ...TACET, wrapShare: 'jrsw.' };
489
489
  const shareWrapped = await wrapNoteKeyShare(TACET_SHARE, noteKey, 'share-pass-42');
490
490
  await A('分享包裹前綴 jrsw.', shareWrapped.wrapped.startsWith('jrsw.'));
@@ -494,7 +494,7 @@ await A('jrsw unwrap 等值 noteKey(extractable 再 export)',
494
494
  shareUnwrapped !== null && hex(new Uint8Array(await crypto.subtle.exportKey('raw', shareUnwrapped))) === origRaw);
495
495
  await A('分享錯密語 → null', (await unwrapNoteKeyShare(TACET_SHARE, shareWrapped.wrapped, 'wrong-share-pass', shareWrapped.salt)) === null);
496
496
  await A('分享錯 salt → null', (await unwrapNoteKeyShare(TACET_SHARE, shareWrapped.wrapped, 'share-pass-42', 'zz'.repeat(16))) === null);
497
- await A('分享 malformed salt(zz 字元)→ null(t_7710c766:salt1 hex 形檢查)',
497
+ await A('分享 malformed salt(zz 字元)→ null(salt1 hex 形檢查)',
498
498
  (await unwrapNoteKeyShare(TACET_SHARE, shareWrapped.wrapped, 'share-pass-42', '0'.repeat(31) + 'z')) === null);
499
499
  await A('分享 payload 長度不符 → null',
500
500
  (await unwrapNoteKeyShare(TACET_SHARE, shareWrapped.wrapped.slice(0, -3) + 'abc', 'share-pass-42', shareWrapped.salt)) === null);
@@ -570,7 +570,7 @@ await A('本機 payload 竄改 iv 區 → null', await tamperLocal(3));
570
570
  await A('篡改索引越界=顯性失敗(runner 自檢:靜默 no-op 即假紅)', await tamperLocal(-1) === false);
571
571
  await A('本機 payload 竄改 ct 尾 → null', await tamperLocal(59));
572
572
 
573
- // guest 選配未配置拒絕面(2026-09-19 收編:cipherGuest 轉選配;未配置即拒鐵律)
573
+ // guest 選配未配置拒絕面(2026-09-19 定案:cipherGuest 轉選配;未配置即拒鐵律)
574
574
  const TACET_NOGUEST: NoteCryptoConfig = { ...TACET, cipherGuest: undefined };
575
575
  const guestThrowsMsg = async (): Promise<string | null> => {
576
576
  try { await encryptNote(TACET_NOGUEST, makeHeldKey(), note, 'noteId:n1', { current: () => identityA }); return null; }
@@ -588,10 +588,10 @@ await A('未配置 guest → decryptNote 不明字串 null(guest 家族整面
588
588
  (await decryptNote(TACET_NOGUEST, makeHeldKey(), 'jr9x.somestring', 'x', { current: () => identityA })) === null);
589
589
  await A('配置 guest → decryptNote 舊明文相容層原樣(既有契約不變)',
590
590
  (await decryptNote(TACET, makeHeldKey(), '純舊明文', 'x', { current: () => identityA })) === '純舊明文');
591
- await A('既有 guest 配置 roundtrip 不受選配收編影響',
591
+ await A('既有 guest 配置 roundtrip 不受選配化影響',
592
592
  (await decryptNote(TACET, makeHeldKey(), cipherGuest, 'noteId:n1', { current: () => identityA })) === note);
593
593
 
594
- // guest 空 identity 拒絕面(t_7710c766 目標 3):K_u = SHA-256(prefix ‖ '') 靜默產出「空帳號金鑰」是誤配炸彈
594
+ // guest 空 identity 拒絕面:K_u = SHA-256(prefix ‖ '') 靜默產出「空帳號金鑰」是誤配炸彈
595
595
  const TACET_GUESTOK: NoteCryptoConfig = { ...TACET }; // cipherGuest='jr1g.' 在場,專測 identity 面
596
596
  const emptyIdp = { current: () => '' };
597
597
  let emptyIdMsg: string | null = null;
@@ -607,7 +607,7 @@ await A('guest 解密面空 identity → null(對稱面 fail-closed)',
607
607
  await A('guest 空 identity 的既有 roundtrip 恆正常(identity 在場零新拒絕面)',
608
608
  (await decryptNote(TACET_GUESTOK, makeHeldKey(), cipherGuest, 'noteId:n1', { current: () => identityA })) === note);
609
609
 
610
- // 畸形配置 ''(空字串)正規化面(follow-up ②,t_44239f5f):空字串前綴=startsWith('') 恆真,
610
+ // 畸形配置 ''(空字串)正規化面:空字串前綴=startsWith('') 恆真,
611
611
  // bound 列會被誤導 guest 分支回 null。正規化後:加密面與未配置同拒、解密面 bound 照解、
612
612
  // guest 密文(他家族形)null——與「未配置」行為完全同構。
613
613
  const TACET_EMPTYGUEST: NoteCryptoConfig = { ...TACET, cipherGuest: '' };
@@ -620,14 +620,14 @@ await A("畸形 guest 前綴 '' → decryptNote guest 密文 null(guest 家族
620
620
  await A("畸形 guest 前綴 '' → decryptNote 舊明文相容層同「未配置」形(null,不當明文)",
621
621
  (await decryptNote(TACET_EMPTYGUEST, makeHeldKey(), '純舊明文', 'x', { current: () => identityA })) === null);
622
622
 
623
- // ── 12. 伺服器端原語(t_7710c766 目標 8:makeInboundCipher/validWrappedKey/pickKeyPackage/
624
- // checkRate/timingSafeEq)+ 本機鎖定全函式(pinlock jr1p.,目標 8) ──────────
623
+ // ── 12. 伺服器端原語(makeInboundCipher/validWrappedKey/pickKeyPackage/
624
+ // checkRate/timingSafeEq)+ 本機鎖定全函式(pinlock jr1p.) ──────────
625
625
 
626
626
  console.log('\n[12] 伺服器端原語(auth/ratelimit/hash)+ 本機鎖定(pinlock)');
627
- // ratelimit 行為面(t_b91ed07f;r2 MAJOR-2 收口):真 node:sqlite 直載 UPSERT…RETURNING 單句,
627
+ // ratelimit 行為面(UPSERT…RETURNING 單句設計收口):真 node:sqlite 直載 UPSERT…RETURNING 單句,
628
628
  // 行為斷言全數「真生產函式 checkRate 直驅」——D1 形 shim 包 node:sqlite 只做 D1 run() 的
629
629
  // {results} 回帶形,無測試端 glue 副本。表名直插 SQL=注入面驗證;放行判準 count<=max
630
- // 單一真相。同 IP 真併發不 undercount 的 D1 活線證據見卡 comments(wrangler dev 探針:
630
+ // 單一真相。同 IP 真併發不 undercount 的 D1 活線證據由 wrangler dev 探針承載(
631
631
  // UPSERT…RETURNING 於 wrangler 4.129.0 workerd 逐發回真值;併發首發兩發計數 1→2)。
632
632
  type RateRow = { ip: string; window_start: number; count: number };
633
633
  const RATE: RateWindow = { table: 'login_rate', windowMs: 60_000, max: 3 };
@@ -720,7 +720,7 @@ await A('rate:表名 \\w 合法(字母數字底線)', await (async () => {
720
720
  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; }
721
721
  catch (e) { return String((e as Error).message).startsWith('ERR_RATE_TABLE_NAME') === false && String((e as Error).message).includes('D1_DOWN_FAILOPEN'); }
722
722
  })());
723
- await A('rate:RETURNING 異形回應守衛 fail-open(results 空/alias 斷裂——與儲存故障同向,r2 MINOR-3 不反向 deny)', await (async () => {
723
+ await A('rate:RETURNING 異形回應守衛 fail-open(results 空/alias 斷裂——與儲存故障同向,異形不反向 deny)', await (async () => {
724
724
  const mkEnv = (run: () => Promise<unknown>) =>
725
725
  ({ DB: { prepare: () => ({ bind: () => ({ run }) }) } } as unknown as Parameters<typeof checkRate>[0]);
726
726
  const empty = await checkRate(mkEnv(async () => ({ results: [] })), RATE, 'ip');
@@ -751,7 +751,7 @@ await A('inboundCipher:真明文照收', inbound('純明文') === '純明文')
751
751
  await A('inboundCipher:非字串 null', inbound(42) === null);
752
752
  await A('isCipherFor:家族前綴 true/非家族 false',
753
753
  isCipherFor('jr1b.abc', formats) && isCipherFor('jr1u.abc', formats) && !isCipherFor('jr2x.abc', formats));
754
- await A('isCipherFor 空字首組態 → false(\x27\x27 態 startsWith(\x27\x27) 恆真 accept-all——run 574 MINOR-3:cipherPrefixRe 守衛不覆蓋直讀 tuple 面,同向收口;非字串 false 契約照舊)',
754
+ await A('isCipherFor 空字首組態 → false(\x27\x27 態 startsWith(\x27\x27) 恆真 accept-all——cipherPrefixRe 守衛不覆蓋直讀 tuple 面,同向收口;非字串 false 契約照舊)',
755
755
  (() => {
756
756
  const emptyTuple: CipherFormats = { cipherPrefixes: ['', ''], wrapPrefix: 'jr1w.', cipherMax: 5000 };
757
757
  const mixed: CipherFormats = { cipherPrefixes: ['', 'jr1b.'], wrapPrefix: 'jr1w.', cipherMax: 5000 };
@@ -760,31 +760,31 @@ await A('isCipherFor 空字首組態 → false(\x27\x27 態 startsWith(\x27\x2
760
760
  isCipherFor('jr1b.abc', mixed) === true && // 真前綴面零誤傷
761
761
  isCipherFor(null, formats) === false; // 非字串 false 面(typed 形合法態;運行時 typeof 守衛承載 unknown 呼叫端)
762
762
  })());
763
- await A('makeInboundCipher regex 跳脫全字面(r2 MAJOR-1 辨別形重推——探針新舊碼輸出一手實測,兩向分歧各一)',
763
+ await A('makeInboundCipher regex 跳脫全字面(辨別形重推——探針新舊碼輸出一手實測,兩向分歧各一)',
764
764
  (() => {
765
765
  // 舊碼 .replace('.','\\.') 只跳第一個點,前綴含 regex 符號(| ( ))時家族 regex 失真,
766
766
  // 兩方向輸出分歧(一手對照探針帳面:tmp probe-d1.cjs,main 舊碼 vs 分支新碼):
767
767
  // ①完整前綴形『a.b|c()』+A*600(真家族密文標記):正碼辨得前綴 → >max 密文丟棄 null;
768
768
  // 舊碼 regex 辨不出自家前綴 → 誤落明文截斷(>max 密文截半損毀=舊碼 slice(0,500))。
769
769
  // ②點前截形『a.b』+A*600:舊碼 a\.b 殘臂(從頭起 b64 全程)誤咬 → 丟棄 null;
770
- // 正碼前綴不完整非家族 → 明文截斷照收 slice(0,500)。兩形缺一即探針無齒(r1 教訓)。
770
+ // 正碼前綴不完整非家族 → 明文截斷照收 slice(0,500)。兩形缺一即探針無齒。
771
771
  const f: CipherFormats = { cipherPrefixes: ['a.b|c()', 'jr1b.'], wrapPrefix: 'jr1w.', cipherMax: 500 };
772
772
  const m = makeInboundCipher(f);
773
773
  const full = 'a.b|c()' + 'A'.repeat(600);
774
774
  const dotHead = 'a.b' + 'A'.repeat(600);
775
775
  return m(full) === null && m(dotHead) === dotHead.slice(0, 500);
776
776
  })());
777
- // N-7(t_580f9c54):cipherPrefixRe 空字首組態毒形——never-match 守衛的行為面。
778
- // run 574 r2 校正(新舊碼組裝對照一手實測 /tmp/t580f9c54-r2/n7-corrected-probe.mjs):舊碼
777
+ // 空字首組態毒形防線:cipherPrefixRe never-match 守衛的行為面。
778
+ // 校正(新舊碼組裝對照一手實測):舊碼
779
779
  // `[]` 與 `['']` 組裝出 byte-identical `^()[A-Za-z0-9+/]+={0,2}$`(join('|') 同為空串),
780
780
  // `['','']` 僅多空首選交替 `^(|)…`=同面——真洞=無點純 b64 串(≤200)在舊碼
781
781
  // validWrappedKey/pickKeyPackage 放行(NON-NULL);帶點前綴向量('jr1w.'+b64)舊碼本就 null
782
- // ('.' 在 b64 charset 外)=帳面無承載。r1 版敘事(charset 恆假自帶防線/空槽首選交替洞)經
783
- // run 574 一手實證俱誤——本段已按實測帳重寫(revert-poison 案為守衛還原面的直接承載)。
782
+ // ('.' 在 b64 charset 外)=帳面無承載。初版敘事(charset 恆假自帶防線/空槽首選交替洞)經
783
+ // 一手實證俱誤——本段已按實測帳重寫(revert-poison 案為守衛還原面的直接承載)。
784
784
  // 守衛後(usable 過濾+never-match):家族/re 面整面拒絕;inboundCipher 輸出面 zero delta
785
785
  // (家族路摘除後長路徑 b64≤max 短路進 legacy 慣例層、超限丟棄、明文相容層照收——出口與舊碼
786
786
  // RE 路徑恆等,probe 33 組合實證);混合『['','jr1b.']』形只摘空槽、真前綴家族面照活(降級非全拒)。
787
- await A('N-7 真洞還原面:無點純 b64 ≤200 空字首組態 → null(cipherPrefixRe never-match——守衛摘除即翻紅;三組態)',
787
+ await A('空字首真洞還原面:無點純 b64 ≤200 空字首組態 → null(cipherPrefixRe never-match——守衛摘除即翻紅;三組態)',
788
788
  (() => {
789
789
  const pureB64 = b64(new Uint8Array(32)); // 無點 b64(44 字符)——舊碼本就放行=真洞載體(帶點向量舊碼本就 null,無承載)
790
790
  const emptyTuple = ['', ''];
@@ -792,7 +792,7 @@ await A('N-7 真洞還原面:無點純 b64 ≤200 空字首組態 → null(c
792
792
  validWrappedKey(pureB64, emptyTuple) === null &&
793
793
  validWrappedKey(pureB64, '') === null; // string 形空前綴同面
794
794
  })());
795
- await A('N-7 pickKeyPackage 空字首組態 → 整組放棄(validWrappedKey never-match 傳播——wrapped/salt 同 null)',
795
+ await A('空字首組態 pickKeyPackage → 整組放棄(validWrappedKey never-match 傳播——wrapped/salt 同 null)',
796
796
  (() => {
797
797
  const p = pickKeyPackage({ wrapped: b64(new Uint8Array(32)), salt: 'a'.repeat(32) }, ['', '']);
798
798
  return p.wrapped === null && p.salt === null;
@@ -822,7 +822,7 @@ await A('pickKeyPackage:缺 wrapped 整組放棄',
822
822
  await A('pickKeyPackage:錯前綴整組放棄',
823
823
  (() => { const p = pickKeyPackage({ wrapped: 'jr9x.' + b64(new Uint8Array(32)), salt: 'a'.repeat(32) }, 'jr1w.'); return p.wrapped === null; })());
824
824
 
825
- // 本機鎖定(jr1p.):全函式 roundtrip+嚴格檢查面(t_7710c766:pinlock 閘本有斷言缺席)
825
+ // 本機鎖定(jr1p.):全函式 roundtrip+嚴格檢查面(閘本有斷言缺席)
826
826
  const PINLOCK: PinLockConfig = { pinLock: 'jr1p.', pinLockSaltPrefix: 'tacet-pinlock-v1:', pinLockAad: 'notekey-pinlock' };
827
827
  const pinWrapped = await wrapNoteKeyPinLock(PINLOCK, noteKey, '2580ab');
828
828
  await A('jr1p 包裹前綴 jr1p.', pinWrapped.startsWith('jr1p.'));
@@ -853,10 +853,10 @@ await A('jr1p 跨家族:jr1w. 字串餵 pinlock → null',
853
853
  await A('jr1p 跨家族:jr1p. 字串餵 jr1w unwrap → null',
854
854
  (await unwrapNoteKey(TACET2, pinWrapped, pass, salt)) === null);
855
855
 
856
- // ── 13. 新契約面(t_7710c766:hex fail-closed/salt hex 形收口/PH1 鹽注入/錯誤碼語意分離) ──
856
+ // ── 13. 新契約面(hex fail-closed/salt hex 形收口/PH1 鹽注入/錯誤碼語意分離) ──
857
857
 
858
858
  console.log('\n[13] unwrap 驗證統一新契約(fail-closed + 語意分離)');
859
- // hexToBytes fail-closed(目標 #2;note-crypto 與 bip39 同步——bip39 面是 HEX64_RE 前置自守)
859
+ // hexToBytes fail-closed(note-crypto 與 bip39 同步——bip39 面是 HEX64_RE 前置自守)
860
860
  await A('hexToBytes 合法輸出不變(舊碼同值;新向量)', hexToBytes('00ff10').join(',') === '0,255,16');
861
861
  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]); })());
862
862
  await A("hexToBytes 非法字元 'zz' → throw ERR_BAD_HEX", await (async () => {
@@ -869,7 +869,7 @@ await A('hexToBytes 空字串 → throw', await (async () => {
869
869
  try { hexToBytes(''); return false; } catch (e) { return (e as Error).message === 'ERR_BAD_HEX'; }
870
870
  })());
871
871
  await A(`hexToBytes 全形正規化('${'ff'}')→ Bytes [255] 等值`, (() => { try { return Array.from(hexToBytes('ff')).join(',') === '255'; } catch { return false; } })());
872
- await A(`hexToBytes 全形數字('2f')→ Bytes 等值(NFKC 窗殲滅——round 1 MINOR-4:parse 原始串 NaN 歸零殘形)`, (() => { try { return Array.from(hexToBytes('2f')).join(',') === '47'; } catch { return false; } })());
872
+ await A(`hexToBytes 全形數字('2f')→ Bytes 等值(NFKC 窗殲滅——parse 原始串 NaN 歸零殘形)`, (() => { try { return Array.from(hexToBytes('2f')).join(',') === '47'; } catch { return false; } })());
873
873
  await A('bip39 hexToBytes 同步(HEX64_RE 自守前提下的舊向量不變)', (await bip.recTokenToWords('ab'.repeat(32)))?.length === 24);
874
874
  await A('unwrap:鹽 hex 非法字元 → 假錯誤誘餌面 null(原碼靜默歸零會錯誤炸出成功路徑)',
875
875
  (await unwrapNoteKey(TACET2, dual.wrapped, passD, 'a'.repeat(31) + 'z')) === null);
@@ -886,19 +886,19 @@ const craftKek = (async (): Promise<CryptoKey> => {
886
886
  })();
887
887
  // 鍛造面(格式嚴格契約的可觀察承載;毒化前提):KEK 已知者可造任意形 payload——
888
888
  // 舊碼(無 rawHex 長度/形檢)下 'zz' 前綴零化 hex 與 32-hex 假 noteKey 皆 NON-NULL=假金鑰生產器。
889
- // round 1 審查 MAJOR-1 校正:鍛造 encrypt 必帶 additionalData('notekey')=與 decryptWithKey
889
+ // 鍛造校正:encrypt 必帶 additionalData('notekey')=與 decryptWithKey
890
890
  // 同 AAD——缺 AAD 的鍛造 GCM 層恆拒=rawHex 形檢從未執行(斷言空轉)。
891
891
  // craftKek 鏡像真 deriveKek(同 pass 同 salt 同 600k)=鍛造鏈與真 unwrap 同 KEK:
892
892
  // 有效形鍛造真解開(NON-NULL 自證鏈活)、畸形形由各檢查點拒——機制面直接可觀察。
893
- // AAD 單一化(r2 NIT-a):F_AAD 是四鍛造向量唯一 AAD 源(對 decryptWithKey 同 AAD 律的 helper 收口)。
893
+ // AAD 單一化:F_AAD 是四鍛造向量唯一 AAD 源(對 decryptWithKey 同 AAD 律的 helper 收口)。
894
894
  const F_AAD: BufferSource = enc.encode('notekey');
895
- await A('MAJOR-1 機制面:帶 AAD 鍛造 ct 可解回 rawHex(形檢層可達=斷言活)', await (async () => {
895
+ await A('鍛造機制面:帶 AAD 鍛造 ct 可解回 rawHex(形檢層可達=斷言活)', await (async () => {
896
896
  const kekC = await craftKek;
897
897
  const ivC = crypto.getRandomValues(new Uint8Array(12));
898
898
  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));
899
899
  return (await decryptWithKey(kekC, new Uint8Array([...ivC, ...ctC]), 'notekey')) === 'zz' + 'a'.repeat(62);
900
900
  })());
901
- await A('MAJOR-1 空轉形:同 payload 無 AAD 鍛造 → decryptWithKey null(GCM 層恆拒=round 1 空轉病理)', await (async () => {
901
+ await A('鍛造空轉形:同 payload 無 AAD 鍛造 → decryptWithKey null(GCM 層恆拒=空轉病理)', await (async () => {
902
902
  const kekC = await craftKek;
903
903
  const ivC = crypto.getRandomValues(new Uint8Array(12));
904
904
  const ctC = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv: ivC }, kekC, enc.encode('zz' + 'a'.repeat(62)) as BufferSource));
@@ -944,7 +944,7 @@ await A('guest 空 identity 自洽密文 → decryptNote null(「空帳號金
944
944
  const cipherEmpty = 'jr1g.' + await encryptWithKey(kuEmpty, note, 'noteId:n1');
945
945
  return (await decryptNote(TACET_GUESTOK, makeHeldKey(), cipherEmpty, 'noteId:n1', emptyIdp)) === null;
946
946
  })());
947
- // 錯誤碼語意分離(目標 6):空 PIN ≠ 未配置;jr3d 兩 case 同碼分流
947
+ // 錯誤碼語意分離:空 PIN ≠ 未配置;jr3d 兩 case 同碼分流
948
948
  await A('wrapNoteKeyDual 空 PIN → ERR_PIN_EMPTY(非 NOT_CONFIGURED)', await (async () => {
949
949
  try { await wrapNoteKeyDual(TACET2, noteKey, passD, ''); return false; } catch (e) { return (e as Error).message === 'ERR_PIN_EMPTY'; }
950
950
  })());
@@ -954,13 +954,13 @@ await A('wrapNoteKeyDual 空 PIN 全形空白正規化後 → ERR_PIN_EMPTY', aw
954
954
  await A('wrapNoteKeyDual 未配置 wrapDual → ERR_DUAL_NOT_CONFIGURED(契約不變)', await (async () => {
955
955
  try { await wrapNoteKeyDual(TACET, noteKey, passD, pinD); return false; } catch (e) { return (e as Error).message === 'ERR_DUAL_NOT_CONFIGURED'; }
956
956
  })());
957
- await A('wrapNoteKeyDual3 空 PIN → ERR_PIN_EMPTY(t_7710c766:兩 case 同碼分流)', await (async () => {
957
+ await A('wrapNoteKeyDual3 空 PIN → ERR_PIN_EMPTY(兩 case 同碼分流)', await (async () => {
958
958
  try { await wrapNoteKeyDual3(TACET3, noteKey, passD, ''); return false; } catch (e) { return (e as Error).message === 'ERR_PIN_EMPTY'; }
959
959
  })());
960
960
  await A('wrapNoteKeyDual3 未配置 wrapDual3 → ERR_JR3W_NOT_CONFIGURED(契約不變)', await (async () => {
961
961
  try { await wrapNoteKeyDual3({}, noteKey, passD, pinD); return false; } catch (e) { return (e as Error).message === 'ERR_JR3W_NOT_CONFIGURED'; }
962
962
  })());
963
- // PH1 鹽注入(目標 4):預設舊值零行為變更
963
+ // PH1 鹽注入:預設舊值零行為變更
964
964
  await A('derivePh1Argon 預設鹽零行為變更(同 pass 同值)',
965
965
  (await derivePh1Argon('probe-determinism-pass-42')) === ph1v2a);
966
966
  async function srcOf(rel: string): Promise<string> {
@@ -977,14 +977,14 @@ await A('derivePh1Argon 預設分支源碼面 = encode(PH1_V2_SALT)(鹽預設
977
977
  await A('derivePh1Argon 自選鹽 → 不同派生值', (await derivePh1Argon('probe-determinism-pass-42', new Uint8Array(12))) !== ph1v2a);
978
978
  await A('derivePh1Argon 自選鹽 hex64 形', /^[0-9a-f]{64}$/.test(await derivePh1Argon('x', new Uint8Array(12))));
979
979
  await A('PH1_V2_SALT re-export 在場(index barrel;其他產品可見可注入同源鹽)', PH1_V2_SALT === 'tacet-ph1-v1');
980
- // index barrel 新匯出(目標 5):clearLocalWrap/buildBindPayload(review「缺匯出」條誤差已校正=本卡以閘承載)
980
+ // index barrel 新匯出:clearLocalWrap/buildBindPayload(「缺匯出」條誤差已校正——以閘承載)
981
981
  await A('index barrel 匯出 clearLocalWrap/buildBindPayload', await (async () => {
982
982
  const src = await import('../src/index.ts');
983
983
  return typeof src.clearLocalWrap === 'function' && typeof src.buildBindPayload === 'function';
984
984
  })());
985
- // PinLockConfig 死欄退場(t_7710c766):型別面真齒=excess-property 探針(round 1 審查 MINOR-3:
986
- // 舊形 typeof cfg==='object' 恆真空轉)。r2 MINOR-2 實測校正(t_580f9c54;行號帳由 P12 毒化 tsc 輸出承載
987
- // 而非註解寫死——新增段推移行號=註解行號帳漂移,NIT-a 同病禁再犯):required 復活形=TS2741(PINLOCK
985
+ // PinLockConfig 死欄退場:型別面真齒=excess-property 探針(
986
+ // 舊形 typeof cfg==='object' 恆真空轉——實測校正;行號帳由毒化 tsc 輸出承載
987
+ // 而非註解寫死——新增段推移行號=註解行號帳漂移):required 復活形=TS2741(PINLOCK
988
988
  // 常數面)+TS2345(未配置兩案),optional 復活形=舊形探針未帶該欄=excess-property 檢照不到=tsc 0 綠——robust 形=pristine 就在探針字面量帶 `noteKeyExtractable:
989
989
  // undefined as never`+前置 @ts-expect-error:欄位復活任何形(required/optional/false 字面量)都多出待位錯誤=TS2578 紅;
990
990
  // pristine 態錯誤恰由 expect-error 吸收=全 repo tsc 0 綠(探針字面量缺 false 形欄不另生錯=零加值面除名)。
@@ -992,7 +992,7 @@ await A('index barrel 匯出 clearLocalWrap/buildBindPayload', await (async () =
992
992
  // @ts-expect-error — 死欄退場待位錯誤面(pristine 恰此一待位錯由本指令吸收;欄位復活任何形=本指令反噬 TS2578=P12 案紅)
993
993
  const _cfgProbe: PinLockConfig = { pinLock: 'jr1p.', pinLockSaltPrefix: 'p:', pinLockAad: 'notekey-pinlock', noteKeyExtractable: undefined as never };
994
994
  void _cfgProbe;
995
- // 壞 b64 面向量(openNoteKey 誠實契約「任何不符恆回 null 不拋」的可觀察承載——round 1 MINOR-5):
995
+ // 壞 b64 面向量(openNoteKey 誠實契約「任何不符恆回 null 不拋」的可觀察承載):
996
996
  await A('鍛造壞 base64 面字符 → unwrapNoteKey null(openNoteKey unb64 拋點吞收=不拋契約;家族面毒化形=頂層 crash,本面包 try/catch 收乾淨 ✗)', await (async () => {
997
997
  try {
998
998
  return (await unwrapNoteKey(TACET, 'jr1w.' + '!!not-base64!!', pass, salt)) === null;
@@ -1003,13 +1003,13 @@ await A('openNoteKey 直接呼叫:壞 base64 → null 不拋(本體吞收點
1003
1003
  return (await openNoteKey('jr1w.' + '!!not-base64!!', kekC, 'notekey', 'jr1w.')) === null;
1004
1004
  })());
1005
1005
 
1006
- // ── 14. 效能形契約(卡C t_7361b68c:零行為變更——輸出 byte 等價是合約;錨面咬「形」) ──
1006
+ // ── 14. 效能形契約(零行為變更——輸出 byte 等價是合約;錨面咬「形」) ──
1007
1007
  //
1008
1008
  // 效能批的閘承載物理:執行時間不能進閘(機器相依)——咬「並行形在場+串行殘留歸零」
1009
1009
  // 靜態錨+「Argon 串行刻意保留」負向+b64 輸出等價行為面+Max-Age/sideEffects 字面+
1010
1010
  // ikm 組裝序行為向量(KAT14 凍結 blob)。執行帳(node 26.8.1 實測,2026-10-04):
1011
1011
  // buildBindPayload 串行 133-152ms→並行 68-84ms;dual 串行 234→並行 179ms median;
1012
- // b64 4MiB 132-181ms→18ms。瀏覽器帳=review 222ms→27ms。
1012
+ // b64 4MiB 132-181ms→18ms。瀏覽器實測 222ms→27ms。
1013
1013
 
1014
1014
  console.log('\n[14] 效能形契約(buildBindPayload 並行/b64 分塊/argon 禁並行/CORS Max-Age/sideEffects)');
1015
1015
 
@@ -1022,19 +1022,19 @@ await A('[14] deriveKek2 輸入順序錨(pass 段 raw 收口形+Promise.all
1022
1022
  /await Promise\.all\(\[\r?\n\s*derivePbkdf2Bits\(passphrase, salt1, PBKDF2_ITERATIONS\),\r?\n\s*derivePbkdf2Bits\(pin,/.test(noteCryptoSrc14));
1023
1023
  await A('[14] deriveKek2Argon 正向串行錨(pass/pin 兩段 deriveArgon2id 串行在場)',
1024
1024
  /const passBits = await deriveArgon2id\(/.test(argon2Src14) && /const pinBits = await deriveArgon2id\(/.test(argon2Src14));
1025
- await A('[14] argon2 destructure 並行形歸零(舊串行面對稱收口——r2 MAJOR-1 防逆向回歸)',
1025
+ await A('[14] argon2 destructure 並行形歸零(舊串行面對稱收口——防逆向回歸)',
1026
1026
  argon2Src14.split('const [passBits, pinBits] = await Promise.all([').length === 1);
1027
1027
  await A('[14] buildBindPayload 舊串行形歸零(await wrapNoteKeyWithRecToken 串行殘留)',
1028
1028
  !noteCryptoSrc14.includes('await wrapNoteKeyWithRecToken'));
1029
1029
  await A('[14] deriveKek2 舊串行形歸零(串行 passBits 殘留)',
1030
1030
  !/const passBits = await derivePbkdf2Bits/.test(noteCryptoSrc14));
1031
- // r2 MAJOR-1:並行錨升左手側——destructure 與 Promise.all 相鄰形恰一,對調滑接(pin‖pass)
1031
+ // 並行錨升左手側——destructure 與 Promise.all 相鄰形恰一,對調滑接(pin‖pass)
1032
1032
  // 即 RED(RHS 指令序不變=原本體並行錨不動,兩錨分工:RHS 面/組件序面)。
1033
1033
  await A('[14] deriveKek2 destructure 左手側恰一(const [passBits, pinBits] + Promise.all 相鄰形——swap 滑接即 RED)',
1034
1034
  noteCryptoSrc14.split('const [passBits, pinBits] = await Promise.all([').length === 2);
1035
- // MAJOR-1 行為面:bea6ab8 逐值凍結 jr2w. blob(手工 pristine 組裝=passBits@0‖pinBits@32 序)——
1036
- // destructure 對調滑接產 pin‖pass ikm=本 blob 不可解(reviewer rev-harm 雙向一手:pristine 自解
1037
- // true/互解 null;r2 樹 A/C/B/D/E/F 六腿同帳,探針存證 kanban 工作區)。凍結定值自足:
1035
+ // 行為面:逐值凍結 jr2w. blob(手工 pristine 組裝=passBits@0‖pinBits@32 序)——
1036
+ // destructure 對調滑接產 pin‖pass ikm=本 blob 不可解(pristine 自解
1037
+ // true/互解 null 雙向一手實證)。凍結定值自足:
1038
1038
  // salt1 'a1'×16/pinSalt 0xb2+i×16/iv 0x44+i×12/raw 'c3'×32/pass/pin 明寫——重放=按定值
1039
1039
  // 重建 KEK2(HKDF info 'journal-kek2-v1:jr2w.'、aad 'notekey2')比對 unwrap=raw。
1040
1040
  const KAT14 = {
@@ -1042,9 +1042,9 @@ const KAT14 = {
1042
1042
  salt1: 'a1'.repeat(16),
1043
1043
  pass: 'correct-horse-battery-staple-42',
1044
1044
  pin: '482913',
1045
- raw: 'c3'.repeat(32), // 計算式構造(手打對數面禁止——r2 首跑 1 FAIL 即手打 raw 對數錯的實證)
1045
+ raw: 'c3'.repeat(32), // 計算式構造(手打對數面禁止——首跑 1 FAIL 即手打 raw 對數錯的實證)
1046
1046
  };
1047
- await A('[14] jr2w. ikm 組裝序行為向量(bea6ab8 凍結 blob unwrap=raw——destructure 對調滑接不可解)',
1047
+ await A('[14] jr2w. ikm 組裝序行為向量(凍結 blob unwrap=raw——destructure 對調滑接不可解)',
1048
1048
  (async () => {
1049
1049
  const k = await unwrapNoteKeyDual(TACET2, KAT14.blob, KAT14.pass, KAT14.pin, KAT14.salt1);
1050
1050
  if (k === null) return false;
@@ -1105,7 +1105,7 @@ await A('[14] corsResponse 仍完整轉發四頭(corsResponse 面帶 Max-Age
1105
1105
  }).process?.getBuiltinModule?.('node:fs');
1106
1106
  const pkg14 = JSON.parse(fsPkg14!.readFileSync!(new URL('../package.json', import.meta.url).pathname, 'utf8')) as { sideEffects?: boolean; version: string };
1107
1107
  await A('[14] package.json sideEffects:false(tree-shake server 模組+BIP39 詞表)', pkg14.sideEffects === false);
1108
- // sideEffects 前提(round 2 MINOR-1 升級):src 標目 **遞迴** 枚舉(硬編碼清單退場——新模組
1108
+ // sideEffects 前提:src 標目 **遞迴** 枚舉(硬編碼清單退場——新模組
1109
1109
  // 自動入掃),頂層語句白名單化+**宣告行初始化式呼叫面檢查**(const x = fn() 形=執行點;
1110
1110
  // Object.freeze(...)/純常數結構白名單——wordlist.ts:7 即此形)。
1111
1111
  await A('[14] sideEffects 前提:src 頂層零副作用語句+宣告行初始化式零裸呼叫(Object.freeze 白名單)',
@@ -1132,7 +1132,7 @@ await A('[14] corsResponse 仍完整轉發四頭(corsResponse 面帶 Max-Age
1132
1132
  // 頂層語句白名單:import/export+註解+純宣告(const/let/var/function/async/
1133
1133
  // interface/type/enum/class/declare)——其餘字母開頭頂層=執行面 side effect。
1134
1134
  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)) {
1135
- // 宣告行初始化面:const x = <呼叫形>()=模組載入即執行(NIT MINOR-1 缺口)。
1135
+ // 宣告行初始化面:const x = <呼叫形>()=模組載入即執行(缺口面)。
1136
1136
  // 白名單:Object.freeze(純凍結常數)。其餘呼叫形初始化=offender。
1137
1137
  if (/^(const\b|let\b|var\b|export const\b).*=.*\b[A-Za-z_$][\w$]*\(\s*[^)]/.test(line) &&
1138
1138
  !/Object\.freeze\(/.test(line)) offenders++;
@@ -1144,10 +1144,10 @@ await A('[14] corsResponse 仍完整轉發四頭(corsResponse 面帶 Max-Age
1144
1144
  })());
1145
1145
  }
1146
1146
 
1147
- // ── 15. 密語正規化 v3 世代(v0.2.0 批卡①:normalizePassphrase+jr4w./jr4d.+PH1 v3) ──
1147
+ // ── 15. 密語正規化 v3 世代(normalizePassphrase+jr4w./jr4d.+PH1 v3) ──
1148
1148
  //
1149
1149
  // 帶內版本化母型實例:密語 KEK 輸入契約改變(raw → normalizePassphrase NFKC-only)=新前綴。
1150
- // 命名兩軸分離:卡D 文案「v3 家族」=PH1 規格代(derivePh1ArgonV3);前綴數字軸 jr4w./jr4d.
1150
+ // 命名兩軸分離:「v3 家族」=PH1 規格代(derivePh1ArgonV3);前綴數字軸 jr4w./jr4d.
1151
1151
  // =KDF 世代代(jr1w→jr2w→jr3w→jr4w,README 家族表對照)。舊家族(jr1w./jr2w./jr3w./jr3d.)
1152
1152
  // 函式體零改=raw 契約永久不變——本節同字面雙家族對照向量(PASS12x/PASS12x)就是
1153
1153
  // 「免換前綴合約證明」的承載面:同組字串在舊家族恆異 KEK(raw 直派生)、在 v3 家族恆同 KEK(NFKC 收容)。
@@ -1159,7 +1159,7 @@ const argon2Src15 = await srcOf('../src/client/argon2.ts');
1159
1159
  const TACET4: Argon3Config = { wrap4: 'jr4w.', wrapDual4: 'jr4d.', pinSalt3Prefix: 'tacet-note-pin3:' };
1160
1160
  const pass4 = 'v3-generation-passphrase-42';
1161
1161
  const passD4 = 'dual-factor-passphrase-42'; // NFKC → passD 的全形形
1162
- const passD4_NFKC = normalizePassphrase(passD4); // 與 passD 恆等(NFKC 收容)——P2 毒化翻面本體(r2 CRITICAL-1:毒形即收口形計數錨翻+毒翻 = NFKC-effective 帳面翻,雙承載)
1162
+ const passD4_NFKC = normalizePassphrase(passD4); // 與 passD 恆等(NFKC 收容)——P2 毒化翻面本體(毒形即收口形計數錨翻+毒翻 = NFKC-effective 帳面翻,雙承載)
1163
1163
 
1164
1164
  // 函式契約面(單一真相本體:normalizePassphrase 與 normalizePin 並存不混)
1165
1165
  await A('normalizePassphrase NFKC 契約:全形收容(pass=pass)', normalizePassphrase('pass') === 'pass');
@@ -1199,13 +1199,13 @@ const dual4NfkEq = unwrappedD4 !== null
1199
1199
  && hex(new Uint8Array(await crypto.subtle.exportKey('raw', unwrappedD4))) === origRaw; // 全形 wrap → NFKC 形:unwrap 真救回(null 恆 false=normalize 摘除不可藏)
1200
1200
  await A('dual4 unwrap 等值 noteKey(extractable 再 export;全形 wrap 面 NFKC 等價自證——not-null 恆 false 恆真盲區免疫)', dual4NfkEq);
1201
1201
  const dual4a = await wrapNoteKeyDual4(TACET4, noteKey, passD, pinD); // ASCII NFKC 慣用形 wrap(passD=passD4 的 NFKC 位——上式自證)
1202
- const dual4NfkRev = dual4a !== null && (await unwrapNoteKeyDual4(TACET4, dual4a.wrapped, passD4, pinD, dual4a.salt)) !== null; // ASCII 包 → 全形解(MAJOR-1 修真向:feed passD4;無 NFKC 即 null)
1202
+ const dual4NfkRev = dual4a !== null && (await unwrapNoteKeyDual4(TACET4, dual4a.wrapped, passD4, pinD, dual4a.salt)) !== null; // ASCII 包 → 全形解(修真向:feed passD4;無 NFKC 即 null)
1203
1203
  await A('dual4 pass 段 NFKC 反向等價(ASCII wrap → 全形 unwrap;PIN 段契約不動)', dual4NfkRev === true);
1204
1204
  await A('dual4 PIN 大小寫不敏感(normalizePin 契約 v3 世代照舊)', (await unwrapNoteKeyDual4(TACET4, dual4.wrapped, passD, '2580AB', dual4.salt)) !== null);
1205
1205
  await A('dual4 PIN 全形 NFKC 等價', (await unwrapNoteKeyDual4(TACET4, dual4.wrapped, passD, '2580AB', dual4.salt)) !== null);
1206
1206
  await A('dual4 錯 PIN → null', (await unwrapNoteKeyDual4(TACET4, dual4.wrapped, passD, '999999', dual4.salt)) === null);
1207
1207
  await A('dual4 空 PIN → null(unwrap 面)', (await unwrapNoteKeyDual4(TACET4, dual4.wrapped, passD, '', dual4.salt)) === null);
1208
- await A('wrapNoteKeyDual4 空 PIN → ERR_PIN_EMPTY(非 NOT_CONFIGURED——t_7710c766 分流 v3 世代照舊)', await (async () => {
1208
+ await A('wrapNoteKeyDual4 空 PIN → ERR_PIN_EMPTY(非 NOT_CONFIGURED——分流語意 v3 世代照舊)', await (async () => {
1209
1209
  try { await wrapNoteKeyDual4(TACET4, noteKey, passD, ''); return false; } catch (e) { return (e as Error).message === 'ERR_PIN_EMPTY'; }
1210
1210
  })());
1211
1211
  await A('dual4 payload 嚴格 108B(截短 → null——鹽內嵌族 own 檢查在場)', await (async () => {
@@ -1259,7 +1259,7 @@ await A('jr2w 凍結:全形 wrap 照解(raw 字面恆可解)',
1259
1259
  (await unwrapNoteKeyDual(TACET2, old2fw.wrapped, fwPass15, pinD, old2fw.salt)) !== null);
1260
1260
  // 未配置拒絕(opt-in 鐵律 v3 世代照舊)
1261
1261
  await A('未配置 wrap4 → wrapNoteKey4 拒絕', await (async () => { try { await wrapNoteKey4({}, noteKey, pass4); return false; } catch (e) { return (e as Error).message === 'ERR_JR4W_NOT_CONFIGURED'; } })());
1262
- await A('未配置 wrapDual4 → wrapNoteKeyDual4 拒絕(ERR_JR4D_NOT_CONFIGURED——語意分離同 t_7710c766)', await (async () => { try { await wrapNoteKeyDual4({}, noteKey, passD, pinD); return false; } catch (e) { return (e as Error).message === 'ERR_JR4D_NOT_CONFIGURED'; } })());
1262
+ await A('未配置 wrapDual4 → wrapNoteKeyDual4 拒絕(ERR_JR4D_NOT_CONFIGURED——錯誤碼語意分離)', await (async () => { try { await wrapNoteKeyDual4({}, noteKey, passD, pinD); return false; } catch (e) { return (e as Error).message === 'ERR_JR4D_NOT_CONFIGURED'; } })());
1263
1263
  await A('未配置 wrap4 → unwrap 拒收 jr4w', (await unwrapNoteKey4({ wrapDual4: 'jr4d.' }, w4.wrapped, pass4, w4.salt)) === null);
1264
1264
 
1265
1265
  // PH1 v3(normalizePassphrase 派生+新鹽域)
@@ -1274,7 +1274,7 @@ await A('PH1_V3_SALT = "tacet-ph1-v2"(鹽域值代沿 PH1_V2_SALT 先例;與
1274
1274
  PH1_V3_SALT === 'tacet-ph1-v2' && (PH1_V3_SALT as string) !== PH1_V2_SALT && !PH1_V3_SALT.startsWith('tacet-note') && !PH1_V3_SALT.startsWith('tacet-pinlock'));
1275
1275
  await A('derivePh1ArgonV3 預設分支源碼面 = encode(PH1_V3_SALT)(鹽預設單一真相錨;同 v2 母型)',
1276
1276
  /saltArg \?\? new TextEncoder\(\)\.encode\(PH1_V3_SALT\)/.test(argon2Src15));
1277
- await A('derivePh1ArgonV3 自選鹽 → 不同派生值(卡A #4 saltArg 慣例)',
1277
+ await A('derivePh1ArgonV3 自選鹽 → 不同派生值(saltArg 慣例)',
1278
1278
  (await derivePh1ArgonV3('probe-v3-pass-42', new Uint8Array(12))) !== ph1v3a);
1279
1279
  await A('derivePh1Argon v2 預設零動(raw 契約——舊世代派生面零變更)', (await derivePh1Argon('probe-determinism-pass-42')) === ph1v2a);
1280
1280
 
@@ -1308,14 +1308,14 @@ await A('index barrel 匯出 v3 世代七識別字(normalizePassphrase/jr4
1308
1308
 
1309
1309
  // ── 決議 ────────────────────────────────────────────────────────────────────
1310
1310
 
1311
- // ── 16. 常駐毒化矩陣(v0.2.0 批卡①自帶——對真模組行為斷言的毒化證據隨每執行重建) ──
1311
+ // ── 16. 常駐毒化矩陣(自帶——對真模組行為斷言的毒化證據隨每執行重建) ──
1312
1312
  //
1313
- // 母型(t_0ab6e760 閘常駐毒化面):對 /tmp 拷貝樹做突變後重跑同一組行為斷言——
1313
+ // 母型(閘常駐毒化面):對 /tmp 拷貝樹做突變後重跑同一組行為斷言——
1314
1314
  // 「毒化態恰翻面」meta 斷言承載外部 poison runner 的證據;毒化跑在拷貝樹=本 repo 樹零接觸。
1315
- // 期望值每毒型真跑實測(r2 本 run /tmp 拷貝樹一手實測,見各案 comment 歸屬帳);還原面由
1315
+ // 期望值每毒型真跑實測(/tmp 拷貝樹一手實測,見各案歸屬帳);還原面由
1316
1316
  // /tmp 樹的出生即棄承載(本 repo 樹 byte 不動)。每案還原後重跑回綠(還原正確性的行為帳)。
1317
1317
  //
1318
- // 案期望值(r2 修正後程式碼上 /tmp 毒化重測,本 run 一手帳——還原後重跑回綠帶):
1318
+ // 案期望值(修正後程式碼上 /tmp 毒化重測,本 run 一手帳——還原後重跑回綠帶):
1319
1319
  // P1 摘 jr4w unwrap 入口 normalize → nfail=4 帳(正規化等價面+KAT 慣用形帳面+兩 [15] 錨跟隨)
1320
1320
  // P2 本體夾帶 lowercase → nfail=11 帳(NFKC-effective 帳面翻面集;P3/P4/P5 針毒形零匹配=全格向量化)
1321
1321
  // P3 摘 derivePh1ArgonV3 入口 normalize → nfail=2 帳(ph1v3 NFKC 等價面+[15] 收口形錨)
@@ -1323,9 +1323,9 @@ await A('index barrel 匯出 v3 世代七識別字(normalizePassphrase/jr4
1323
1323
  // P5 摘 jr4d wrap 入口 normalize → nfail=5 帳(dual4 等值面+PIN 兩面+兩 [15] 錨)
1324
1324
  // O1 jr3w.wrap 入口接 normalize(禁手)→ nfail=4 帳(jr3w 凍結兩面+兩 [15] 錨)
1325
1325
  // O2 deriveKek2 pass 段接 normalize(禁手)→ nfail=2 帳([14] 輸入順序錨+jr2w 凍結 normalized 面)
1326
- // O3 normalizePin 摘 trim/lowercase → nfail=9 帳(>=帽——MINOR-3 韌性;PIN 契約毒化真 RED)
1326
+ // O3 normalizePin 摘 trim/lowercase → nfail=9 帳(>=帽——韌性;PIN 契約毒化真 RED)
1327
1327
  // O4 摘 jr4d unwrap 入口 normalize(反拉禁手)→ nfail=4 帳(NFKC 反向等價面+KAT 慣用形帳面+兩 [15] 錨)
1328
- // O1-src/O2-src/O4-src 源碼計數錨([15] 同形複寫——防單點誤删,NIT-3):
1328
+ // O1-src/O2-src/O4-src 源碼計數錨([15] 同形複寫——防單點誤删):
1329
1329
  // v3 家族入口 normalize 收口形恰 3×3+PBKDF2 raw 恰 1——毒化形即計數變異,本 repo 樹直接斷言。
1330
1330
  // 每案結束後還原/清樹(出生即棄);designated 面帳寫在案例名內。
1331
1331
 
@@ -1333,7 +1333,7 @@ console.log('\n[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒
1333
1333
  {
1334
1334
  const getBuiltin = (id: string): unknown =>
1335
1335
  (globalThis as unknown as { process?: { getBuiltinModule?: (i: string) => unknown } }).process?.getBuiltinModule?.(id);
1336
- const cpMod = getBuiltin('node:child_process') as { execFileSync?: (cmd: string, args: string[], opts: { cwd: string; encoding: string; stdio: unknown[]; env?: Record<string, string> }) => string } | undefined;
1336
+ const cpMod = getBuiltin('node:child_process') as { execFileSync?: (cmd: string, args: string[], opts: { cwd: string; encoding: string; stdio: unknown[]; env?: Record<string, string>; timeout?: number }) => string } | undefined;
1337
1337
  const osMod = getBuiltin('node:os') as { tmpdir?: () => string } | undefined;
1338
1338
  const nodeEnvOk = !!cpMod?.execFileSync && !!osMod?.tmpdir && typeof (globalThis as unknown as { process?: { getBuiltinModule?: unknown } }).process?.getBuiltinModule === 'function';
1339
1339
  const processMod = (globalThis as unknown as { process?: { env?: Record<string, string | undefined> } }).process;
@@ -1357,17 +1357,17 @@ console.log('\n[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒
1357
1357
  if (!fs16?.mkdtempSync || !fs16.cpSync || !fs16.readFileSync || !fs16.writeFileSync) {
1358
1358
  await A('[16] node:fs 原語在場(mkdtemp/cp/read/write)', false);
1359
1359
  } else {
1360
- const treePaths16: string[] = []; // MINOR-2 修正:殘留判準只看本 run 喚出的 tree16 路徑(跨執行/跨 host 零耦合)
1360
+ const treePaths16: string[] = []; // 殘留判準只看本 run 喚出的 tree16 路徑(跨執行/跨 host 零耦合)
1361
1361
  const tmpdir16 = osMod!.tmpdir!();
1362
1362
  const coreNM = path16.resolve(new URL('.', import.meta.url).pathname, '../node_modules');
1363
- const repoRoot16 = path16.resolve(new URL('.', import.meta.url).pathname, '..'); // MINOR-4:repo root 錨定(禁 cwd 依賴——自 repo 外呼叫 cp 樹也恆真)
1363
+ const repoRoot16 = path16.resolve(new URL('.', import.meta.url).pathname, '..'); // repo root 錨定(禁 cwd 依賴——自 repo 外呼叫 cp 樹也恆真)
1364
1364
  const gitIgnoreSkip = (src: string): boolean => src === '.git' || src.endsWith('/.git') || src.includes('/.git/') || src === 'node_modules' || src.endsWith('/node_modules') || src.includes('/node_modules/');
1365
1365
  let tree16 = '';
1366
1366
  const mkTree = (): void => {
1367
1367
  fs16!.rmSync!(tree16, { recursive: true, force: true });
1368
1368
  tree16 = fs16!.mkdtempSync!(path16.join(tmpdir16, 'core-poison-'));
1369
- treePaths16.push(tree16); // 本 run 喚出記帳(MINOR-2:收尾殘留判準的封閉集)
1370
- fs16!.cpSync!(repoRoot16, tree16, { recursive: true, filter: (src: string) => !gitIgnoreSkip(src) }); // MINOR-6+r2 MINOR-4:零 .git/零 node_modules 實拷(repo root 錨定——非 cwd;CI 冷複製成本歸零——毒化樹 node_modules 走下方 symlink)
1369
+ treePaths16.push(tree16); // 本 run 喚出記帳(收尾殘留判準的封閉集)
1370
+ fs16!.cpSync!(repoRoot16, tree16, { recursive: true, filter: (src: string) => !gitIgnoreSkip(src) }); // 零 .git/零 node_modules 實拷(repo root 錨定——非 cwd;CI 冷複製成本歸零——毒化樹 node_modules 走下方 symlink)
1371
1371
  fs16!.rmSync!(path16.join(tree16, 'node_modules'), { recursive: true, force: true });
1372
1372
  // node_modules 走 symlink(worktree 慣例 cp 語意不跟隨):本閘執行樹一定有 node_modules
1373
1373
  //(@scure 對照組+typescript)——毒化樹補 symlink 即可;缺席環境(bare npm ci 後無 install)
@@ -1377,7 +1377,7 @@ console.log('\n[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒
1377
1377
  const runGate16 = (): { rc: number; out: string } => {
1378
1378
  // 內層 gate 執行帶 POISON_GATE_INNER=1 sentinel——[16] 矩陣在內層自我跳過=遞迴防線
1379
1379
  //(拷貝樹的閘檔含本節全文;無 sentinel 會自拷貝再自跑=18+ 連鎖行程實證)。
1380
- try { return { rc: 0, out: exec16('node', ['--experimental-strip-types', 'scripts/verify-core-crypto.ts'], { cwd: tree16, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], env: { ...(processMod?.env ?? {}), POISON_GATE_INNER: '1' } }) }; }
1380
+ try { return { rc: 0, out: exec16('node', ['--experimental-strip-types', 'scripts/verify-core-crypto.ts'], { cwd: tree16, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], env: { ...(processMod?.env ?? {}), POISON_GATE_INNER: '1' }, timeout: 240000 }) }; }
1381
1381
  catch (e: unknown) {
1382
1382
  const err = e as { stdout?: string; stderr?: string; status?: number };
1383
1383
  return { rc: err.status ?? 1, out: (err.stdout ?? '') + (err.stderr ?? '') };
@@ -1405,7 +1405,7 @@ console.log('\n[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒
1405
1405
  " const kek = await deriveKekArgon(deriveInput(passphrase), salt);\n return await openNoteKey(wrapped, kek, 'notekey', cfg.wrap4);",
1406
1406
  " const kek = await deriveKekArgon(passphrase, salt);\n return await openNoteKey(wrapped, kek, 'notekey', cfg.wrap4);");
1407
1407
  const g = runGate16();
1408
- // P1 名單面 designated(r2 MAJOR-2,本 run 重測帳):jr4w 正規化等價面+[17] jr4w NFKC 慣用形
1408
+ // P1 名單面 designated(本 run 重測帳):jr4w 正規化等價面+[17] jr4w NFKC 慣用形
1409
1409
  // 帳面兩面真翻——摘除 unwrap 入口 normalize 時 normalized 形失去收容(dual4 等值面不翻=wrap 端
1410
1410
  // normalize 仍承載,P1 針只咬 unwrap 面——P5 已咬 wrap 面;兩針分工面帳)。
1411
1411
  const p1eqRed = g.out.split('\n').some((l: string) => l.includes('jr4w 正規化等價') && l.trimStart().startsWith('✗'));
@@ -1417,14 +1417,14 @@ console.log('\n[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒
1417
1417
  fs16!.rmSync!(tree16, { recursive: true, force: true });
1418
1418
  await A('[16] P1 還原回綠(出生即棄樹上還原;P2 共樹後不可達案由出生即棄承載)', g2.rc === 0, String(g2.rc));
1419
1419
  }
1420
- // P2 本體夾帶 lowercase → NFKC-effective 行為翻面集(r2 CRITICAL-1 重測:nfail=11 帳——非 crash 形)
1420
+ // P2 本體夾帶 lowercase → NFKC-effective 行為翻面集(重測:nfail=11 帳——非 crash 形)
1421
1421
  {
1422
1422
  mkTree();
1423
1423
  const mut = poison16('src/client/note-crypto.ts',
1424
1424
  "export function normalizePassphrase(passphrase: string): string {\n return passphrase.normalize('NFKC');\n}",
1425
1425
  "export function normalizePassphrase(passphrase: string): string {\n return passphrase.normalize('NFKC').toLowerCase();\n}");
1426
1426
  const g = runGate16();
1427
- // P2 名單面 designated(r2 CRITICAL-1 重測帳):NFKC-effective 帳面翻面集(分離向量二+並存不混+jr4w
1427
+ // P2 名單面 designated(重測帳):NFKC-effective 帳面翻面集(分離向量二+並存不混+jr4w
1428
1428
  // raw 大小寫不折疊+dual4 case/space+[15] NFKC 契約向量+ph1v3 大小寫恆異+[17] 大寫兩形 null 四面+本體
1429
1429
  // 單行形錨)=11 面帳;同 P3/P4/P5 毒形零匹配(全格向量化——毒化矩陣單點真值面)。
1430
1430
  const p2normRed = g.out.split('\n').some((l: string) => l.includes('分離向量二') && l.trimStart().startsWith('✗'));
@@ -1439,7 +1439,7 @@ console.log('\n[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒
1439
1439
  " deriveInput(passphrase),\n saltArg ?? new TextEncoder().encode(PH1_V3_SALT),",
1440
1440
  " passphrase,\n saltArg ?? new TextEncoder().encode(PH1_V3_SALT),");
1441
1441
  const g = runGate16();
1442
- // P3 名單面 designated(r2 MAJOR-2):ph1v3 NFKC 等價面(全形 vs ASCII 同 ph2)真翻。
1442
+ // P3 名單面 designated:ph1v3 NFKC 等價面(全形 vs ASCII 同 ph2)真翻。
1443
1443
  const p3red = g.out.split('\n').some((l: string) => l.includes('ph1v3 NFKC 等價') && l.trimStart().startsWith('✗'));
1444
1444
  await A('[16] P3 摘 derivePh1ArgonV3 入口 normalize → ph1v3 NFKC 等價面真翻(名單級承載)', mut.applied && g.rc === 1 && nfail(g.out) >= 1 && p3red, 'nfail=' + nfail(g.out));
1445
1445
  fs16!.rmSync!(tree16, { recursive: true, force: true });
@@ -1452,7 +1452,7 @@ console.log('\n[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒
1452
1452
  " const kek = await deriveKekArgon(deriveInput(passphrase), salt);\n const wrapped = await sealNoteKey(cfg.wrap4, noteKey, kek, 'notekey');",
1453
1453
  " const kek = await deriveKekArgon(passphrase, salt);\n const wrapped = await sealNoteKey(cfg.wrap4, noteKey, kek, 'notekey');");
1454
1454
  const g = runGate16();
1455
- // P4 名單面 designated(r2 MAJOR-2):jr4w 全形/ASCII 同密語面(全形 wrap 的 NFKC 收容)真翻。
1455
+ // P4 名單面 designated:jr4w 全形/ASCII 同密語面(全形 wrap 的 NFKC 收容)真翻。
1456
1456
  const p4red = g.out.split('\n').some((l: string) => l.includes('jr4w 全形/ASCII 同密語') && l.trimStart().startsWith('✗'));
1457
1457
  await A('[16] P4 摘 jr4w wrap 入口 normalize → 全形/ASCII 同密語面真翻(名單級承載)', mut.applied && g.rc === 1 && nfail(g.out) >= 1 && p4red, 'nfail=' + nfail(g.out));
1458
1458
  fs16!.rmSync!(tree16, { recursive: true, force: true });
@@ -1480,7 +1480,7 @@ console.log('\n[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒
1480
1480
  await A('[16] O3 normalizePin 本體摘 trim/lowercase → 行為翻 9 面(PIN 契約毒化承載)', mut.applied && g.rc === 1 && nfail(g.out) >= 9, 'nfail=' + nfail(g.out) + ' want >=9');
1481
1481
  fs16!.rmSync!(tree16, { recursive: true, force: true });
1482
1482
  }
1483
- // O2 deriveKek2 pass 段接 normalize(禁手形)→ 真翻面案(r2 MAJOR-2 名單級承載;本 run 重測帳=
1483
+ // O2 deriveKek2 pass 段接 normalize(禁手形)→ 真翻面案(名單級承載;本 run 重測帳=
1484
1484
  // [14] 輸入順序錨翻+jr2w 凍結 normalized 面翻,恰 2 面——兩面各自 designated,帳寫在案斷言內)。
1485
1485
  {
1486
1486
  mkTree();
@@ -1490,7 +1490,7 @@ console.log('\n[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒
1490
1490
  const g = runGate16();
1491
1491
  // O2 行為翻 2 面帳(本 run 毒化重測):[14] 輸入順序錨(毒形不匹配 raw 收口形恰-1 形)+jr2w 凍結
1492
1492
  // normalized 解翻面(毒化 wrap 也 normalize → ascii 形同 KEK)——名單級承載=[14] 輸入順序錨面(案內檢);
1493
- // nfail 精確 2 帳=P5/P1 形的錨翻面恰補位(姊妹卡新增 PIN/dual 覆蓋時 >= 帽韌性——MINOR-3)。
1493
+ // nfail 精確 2 帳=P5/P1 形的錨翻面恰補位(新案新增 PIN/dual 覆蓋時 >= 帽韌性)。
1494
1494
  const o2rawRed = g.out.split('\n').some((l: string) => l.includes('[14] deriveKek2 輸入順序錨') && l.trimStart().startsWith('✗'));
1495
1495
  await A('[16] O2 deriveKek2 pass 段接 normalize(禁手形)→ raw 收口形錨翻+jr2w 凍結面翻(nfail>=2;名單級承載)',
1496
1496
  mut.applied && g.rc === 1 && nfail(g.out) >= 2 && o2rawRed, 'rc=' + g.rc + ' nfail=' + nfail(g.out) + ' fails=' + realFails(g.out).map(s => s.slice(0, 44)).join(';;'));
@@ -1504,7 +1504,7 @@ console.log('\n[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒
1504
1504
  " const kek = await deriveKekArgon(passphrase, salt);\n const wrapped = await sealNoteKey(cfg.wrap3, noteKey, kek, 'notekey');",
1505
1505
  " const kek = await deriveKekArgon(deriveInput(passphrase), salt);\n const wrapped = await sealNoteKey(cfg.wrap3, noteKey, kek, 'notekey');");
1506
1506
  const g = runGate16();
1507
- // O1 名單面 designated(r2 MAJOR-2,本 run 重測帳):jr3w 凍結兩面(全形照解面翻+normalized 恆拒
1507
+ // O1 名單面 designated(本 run 重測帳):jr3w 凍結兩面(全形照解面翻+normalized 恆拒
1508
1508
  // 面翻)=raw 入口契約復活的行為齒;4 面帳=兩行為面+兩 [15] 錨跟隨。
1509
1509
  const o1fwRed = g.out.split('\n').some((l: string) => l.includes('jr3w 全形 wrap 照解') && l.trimStart().startsWith('✗'));
1510
1510
  const o1normRed = g.out.split('\n').some((l: string) => l.includes('jr3w 拒收 normalized 慣用形') && l.trimStart().startsWith('✗'));
@@ -1512,14 +1512,14 @@ console.log('\n[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒
1512
1512
  mut.applied && g.rc === 1 && nfail(g.out) >= 2 && o1fwRed && o1normRed, 'rc=' + g.rc + ' nfail=' + nfail(g.out) + ' fails=' + realFails(g.out).map(s => s.slice(0, 44)).join(';;'));
1513
1513
  fs16!.rmSync!(tree16, { recursive: true, force: true });
1514
1514
  }
1515
- // O4 unwrapNoteKeyDual4 入口接 raw(v3 反拉形)→ 「毒態」行為面翻轉樣本(MAJOR-2 面修正後真有齒)
1515
+ // O4 unwrapNoteKeyDual4 入口接 raw(v3 反拉形)→ 「毒態」行為面翻轉樣本(修正後真有齒)
1516
1516
  {
1517
1517
  mkTree();
1518
1518
  const mut = poison16('src/client/argon2.ts',
1519
1519
  " const kek2 = await deriveKek2Argon(cfg, deriveInput(passphrase), pinNorm, salt1, pinSalt, 'journal-kek2-v1:' + cfg.wrapDual4);\n const rawHex = await decryptWithKey(kek2, ivPrefixedCt, 'notekey2');",
1520
1520
  " const kek2 = await deriveKek2Argon(cfg, passphrase, pinNorm, salt1, pinSalt, 'journal-kek2-v1:' + cfg.wrapDual4);\n const rawHex = await decryptWithKey(kek2, ivPrefixedCt, 'notekey2');");
1521
1521
  const g = runGate16();
1522
- // O4 名單面 designated(r2 MAJOR-2,本 run 重測帳):dual4 pass 段 NFKC 反向等價面(MAJOR-1 修正後
1522
+ // O4 名單面 designated(本 run 重測帳):dual4 pass 段 NFKC 反向等價面(修正後
1523
1523
  // unwrap 餵 passD4 的真反向)+[17] jr4d NFKC 慣用形帳面——反拉 normalize 時 unwrap 餵 normalized 形
1524
1524
  // 失去收容 = 該面 null = 行為齒(非只 rc 計數);行為翻 4 面帳=反向等價面+兩 [15] 錨+[17] 帳面。
1525
1525
  const o4red = g.out.split('\n').some((l: string) => l.includes('dual4 pass 段 NFKC 反向等價') && l.trimStart().startsWith('✗'));
@@ -1533,7 +1533,7 @@ console.log('\n[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒
1533
1533
  {
1534
1534
  const argonSrcNow = await srcOf('../src/client/argon2.ts');
1535
1535
  const ncSrcNow = await srcOf('../src/client/note-crypto.ts');
1536
- // 與 [15] 同形計數錨=「複寫防單點誤删」(NIT-3:[16] 內層保留 O-案源碼錨——[15] 錨被未來誤删時本段仍咬)。
1536
+ // 與 [15] 同形計數錨=「複寫防單點誤删」([16] 內層保留 O-案源碼錨——[15] 錨被未來誤删時本段仍咬)。
1537
1537
  await A('[16] O1-src 計數錨([15] 同形複寫——防單點誤删):argon2 normalize 收口入口形恰 3(禁手形任何復活=計數變異即 RED)',
1538
1538
  argonSrcNow.split('deriveKekArgon(deriveInput(passphrase)').length === 3
1539
1539
  && argonSrcNow.split('deriveArgon2id(\n deriveInput(passphrase)').length === 2
@@ -1543,7 +1543,7 @@ console.log('\n[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒
1543
1543
  await A('[16] O4-src 計數錨([15] 同形複寫):jr4d unwrap 入口 normalize 形恰 2(wrap/unwrap 對稱;摘除或反拉即變異)',
1544
1544
  argonSrcNow.split('deriveKek2Argon(cfg, deriveInput(passphrase), pinNorm').length === 3);
1545
1545
  }
1546
- // 收尾:毒化樹清理自證(MINOR-2 修正:封閉集——只核對本 run 喚出的 tree16 路徑;跨執行/
1546
+ // 收尾:毒化樹清理自證(封閉集——只核對本 run 喚出的 tree16 路徑;跨執行/
1547
1547
  // 並行/host 殘留零耦合——其他 core-poison-* 不入本判準);每棵出生即棄 rmSync 強制在場。
1548
1548
  {
1549
1549
  const exists16 = fs16!.existsSync!;
@@ -1556,9 +1556,9 @@ console.log('\n[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒
1556
1556
  }
1557
1557
  }
1558
1558
 
1559
- // ── 17. KAT 凍結向量:HKDF info 域世代分離(CRITICAL-1 修正承載面) ──────────────
1559
+ // ── 17. KAT 凍結向量:HKDF info 域世代分離(修正承載面) ──────────────
1560
1560
  //
1561
- // 凍結常數計算式構造(t_7361b68c KAT14 母型——raw 'c3'.repeat(32) 寫 c3x32 帳面):
1561
+ // 凍結常數計算式構造(KAT 母型——raw 'c3'.repeat(32) 寫 c3x32 帳面):
1562
1562
  // jr3w./jr3d.=舊 raw 族凍結 blob(全形字面 RAW 派生承載;normalized 慣用形恆拒);
1563
1563
  // jr4w./jr4d.=v3 族凍結 blob(KEK 輸入契約=normalizePassphrase:normalized 慣用形可解、
1564
1564
  // raw 全形形恆拒)。KDF 派生輸入正規化收口(v3):入口 deriveInput(pass)=normalizePassphrase
@@ -1624,10 +1624,10 @@ const KAT17 = {
1624
1624
  (await unwrapNoteKeyDual4(TACET4, KAT17.jr3d, passD, KAT17.pin, KAT17.sd3)) === null);
1625
1625
  }
1626
1626
 
1627
- // ── 18. 本機包裹專用前綴(v0.2.0 批卡③:wrapLocal opt-in+讀舊寫新自癒;jr1l.) ──
1627
+ // ── 18. 本機包裹專用前綴(wrapLocal opt-in+讀舊寫新自癒;jr1l.) ──
1628
1628
  //
1629
1629
  // 舊實作借用 cfg.wrap(jr1w. passphrase 包裹前綴)寫本機包裹=「一個前綴一份契約」的第二
1630
- // 違例(wrappedRec 專用前綴歸卡①批面)。收口三面:
1630
+ // 違例(wrappedRec 專用前綴歸 v0.2.0 世代收口)。收口三面:
1631
1631
  // ①寫面專用前綴:storeLocalWrap 寫入恆 cfg.wrapLocal(jr1l.);未配置=退場無寫入
1632
1632
  // (cipherLocal opt-in 母型——未配置面零寫入零拋)。
1633
1633
  // ②讀舊寫新自癒:loadLocalWrap 先試新前綴(本體嚴格面);命中舊形(借用期 jr1w. blob)
@@ -1635,7 +1635,7 @@ const KAT17 = {
1635
1635
  // 不硬遷移。
1636
1636
  // ③未配置態讀取面:照走舊形 cfg.wrap(行為不變),且不自癒重寫(heal 綁在新前綴面)。
1637
1637
  // KEK 不動=deriveGuestKey(identity 派生、passphrase-free——session 期免重打密語的機制
1638
- // 原樣);本卡只界「前綴」面(payload 布局的 KDF/prefix 契約由前綴界定——帶內版本化母型)。
1638
+ // 原樣);本段只界「前綴」面(payload 布局的 KDF/prefix 契約由前綴界定——帶內版本化母型)。
1639
1639
  console.log('\n[18] 本機包裹專用前綴(wrapLocal=jr1l. opt-in+讀舊寫新自癒)');
1640
1640
  {
1641
1641
  // section 專用 Map store:node 端 localStorage 死位(keys.ts try/catch 恆 null)——stored 面
@@ -1759,8 +1759,9 @@ console.log('\n[18] 本機包裹專用前綴(wrapLocal=jr1l. opt-in+讀舊
1759
1759
  // ── 源碼窗靜態錨(本地段結構——毒化形即計數/窗錨變異) ──
1760
1760
  const ncSrc18 = await srcOf('../src/client/note-crypto.ts');
1761
1761
  const localWin18 = ncSrc18.slice(ncSrc18.indexOf('── 本機包裹'), ncSrc18.indexOf('── 日記密文入口'));
1762
- await A('[18] 源碼窗:本機段零 throw 面(ERR_WRAP_NOT_CONFIGURED 退場——未配置=無寫入無拋)',
1763
- localWin18.length > 1900 && !localWin18.includes('ERR_WRAP_NOT_CONFIGURED'));
1762
+ await A('[18] 源碼窗:窗尾 floor 三值帳(真窗 1984B/Δ_p18b≈70B→摘後 1933 持/餘裕 +74B——floor 是帳漂移承重件,隨窗漂移即帳錯)', localWin18.length > 1910);
1763
+ await A('[18] 源碼窗:本機段零 throw 面(ERR_WRAP_NOT_CONFIGURED 禁令——未配置=無寫入無拋;P18d 毒化咬復活)',
1764
+ !localWin18.includes('ERR_WRAP_NOT_CONFIGURED'));
1764
1765
  await A('[18] 源碼窗:寫面唯 cfg.wrapLocal seal+舊借形零殘留(v1 借用面復活即反)',
1765
1766
  localWin18.split('sealNoteKey(cfg.wrapLocal').length === 2 && !localWin18.includes('sealNoteKey(cfg.wrap,'));
1766
1767
  await A('[18] 源碼窗:回落三元恰 1(收口形——自癒腿與未配置態同走單一 cfg.wrap 面;借用形復活即 2+)+自癒接線(if legacy → storeLocalWrap identity legacy 恰 1)',
@@ -1769,7 +1770,7 @@ console.log('\n[18] 本機包裹專用前綴(wrapLocal=jr1l. opt-in+讀舊
1769
1770
  && /if \(legacy\) \{\s*await storeLocalWrap\(cfg, identity, legacy\);/.test(localWin18));
1770
1771
  await A('[18] 界面欄:wrapLocal? 宣告恰 1(note-crypto 全檔——NoteCryptoConfig 選配欄)',
1771
1772
  ncSrc18.split('wrapLocal?: string;').length === 2);
1772
- // ── [18] 常駐毒化矩陣(本機段結構毒三案——母型 [16]:/tmp 拷貝突變+fresh import+出生即棄) ──
1773
+ // ── [18] 常駐毒化矩陣(本機段結構毒三案——母型 [16] gate-rerun 形:/tmp 拷貝突變+內層整組閘重跑+出生即棄) ──
1773
1774
  {
1774
1775
  const getBuiltin18 = (id: string): unknown =>
1775
1776
  (globalThis as unknown as { process?: { getBuiltinModule?: (i: string) => unknown } }).process?.getBuiltinModule?.(id);
@@ -1795,89 +1796,89 @@ console.log('\n[18] 本機包裹專用前綴(wrapLocal=jr1l. opt-in+讀舊
1795
1796
  const fsx18 = getBuiltin18('node:fs') as { existsSync?: (p: string) => boolean } | undefined;
1796
1797
  return typeof fsx18?.existsSync === 'function' ? fsx18.existsSync(p) : false;
1797
1798
  };
1799
+ const cp18 = getBuiltin18('node:child_process') as { execFileSync?: (cmd: string, args: string[], opts: { cwd: string; encoding: string; stdio: unknown[]; env?: Record<string, string>; timeout: number }) => string } | undefined;
1800
+ const runGate18 = (workDir: string): { rc: number; out: string } => {
1801
+ // 內層 gate 執行帶 POISON_GATE_INNER=1 sentinel——三毒化節在內層自我跳過=遞迴防線
1802
+ //([16] runGate16 同形;拷貝樹的閘檔含三節全文,無 sentinel 會自拷貝再自跑)。
1803
+ try {
1804
+ const out = cp18!.execFileSync!('node', ['--experimental-strip-types', 'scripts/verify-core-crypto.ts'], { cwd: workDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], env: { ...((globalThis as unknown as { process?: { env?: Record<string, string | undefined> } }).process?.env ?? {}), POISON_GATE_INNER: '1' }, timeout: 240000 });
1805
+ return { rc: 0, out };
1806
+ } catch (e: unknown) {
1807
+ const err = e as { stdout?: string; stderr?: string; status?: number };
1808
+ return { rc: err.status ?? 1, out: (err.stdout ?? '') + (err.stderr ?? '') };
1809
+ }
1810
+ };
1811
+ // 毒化窗(同字面點數律——needlePA18/PB18/PC18/P18d 毒化前點數自檢在 withPoisonRun18 內)。
1812
+ // byte-exact 帳:srcBefore sha256 毒前釘死;srcAfter 同值=毒化零突變本樹。
1813
+ const sha18 = (s: string): string =>
1814
+ (getBuiltin18('node:crypto') as { createHash?: (a: string) => { update: (s: string) => { digest: (e: string) => string } } } | undefined)?.createHash?.('sha256')!.update(s)!.digest('hex')!;
1815
+ const srcBefore18 = await srcOf('../src/client/note-crypto.ts');
1816
+ const shaBefore18 = sha18(srcBefore18);
1798
1817
  const trees18: string[] = [];
1799
- const withPoison18 = async (applyS: (s: string) => string, restoreS: (s: string) => string, probe: (dir: string) => Promise<void>): Promise<void> => {
1818
+
1819
+ // 三案改 gate-rerun 形(改形照 [16] runGate16 母型):
1820
+ // 拷貝樹突變 src/client/note-crypto.ts 後重跑內層整組閘(POISON_GATE_INNER=1 遞迴
1821
+ // 防線——內層三毒化節自我跳過),數 [18] 名單 ✗ 真紅:designated-FAIL 帳=毒化不只
1822
+ // 翻面,更要咬「未來有人把 [18] 主斷言改弱」——弱化後毒化名單帳不齊即 RED。
1823
+ const withPoisonRun18 = async (label: string, needle: string, replacement: string, cases: string[], want: number): Promise<void> => {
1800
1824
  const dir = fs18!.mkdtempSync!(os18!.tmpdir!() + '/t18-lw-')!;
1801
1825
  trees18.push(dir);
1802
1826
  try {
1803
1827
  fs18!.cpSync!(repoRoot18, dir, { recursive: true, filter: (s: string) => !skip18(s) });
1804
1828
  const p = dir + '/src/client/note-crypto.ts';
1805
1829
  const s0 = fs18!.readFileSync!(p, 'utf8');
1806
- fs18!.writeFileSync!(p, applyS(s0), 'utf8');
1807
- await probe(dir);
1830
+ const cnt = s0.split(needle).length - 1;
1831
+ if (cnt !== 1) { await A(label + '(毒化 needle 對位自檢——錨缺席=runner 設計錯非閘病)', false, 'needle cnt=' + cnt); return; }
1832
+ fs18!.writeFileSync!(p, s0.split(needle).join(replacement), 'utf8');
1833
+ const g = runGate18(dir);
1834
+ const failLines18 = g.out.split('\n').filter((l: string) => l.trimStart().startsWith('✗') && l.includes('[18]'));
1835
+ const others18 = g.out.split('\n').filter((l: string) => l.trimStart().startsWith('✗') && !l.includes('[18]'));
1836
+ const designated18 = cases.filter((kw) => failLines18.some((l: string) => l.includes(kw)));
1837
+ const account18 = failLines18.map((l: string) => l.replace(/^\s*✗\s*/, '').slice(0, 40)).join(';;');
1838
+ await A(label + '→ [18] 名單真紅 ' + want + '(designated-FAIL 帳:實測 ' + designated18.length + '/' + want +
1839
+ ';' + account18.slice(0, 110) + ';others=' + others18.length + ')',
1840
+ g.rc === 1 && designated18.length === want && failLines18.length === want
1841
+ && others18.length === 1 && others18[0]!.includes('與 @scure/bip39 參照 200 組雙向一致')
1842
+ && cases.every((kw) => failLines18.filter((l: string) => l.includes(kw)).length === 1),
1843
+ 'rc=' + g.rc + ' n18=' + failLines18.length + ' d=' + designated18.length + ' others=' + others18.length);
1808
1844
  } finally {
1809
1845
  fs18!.rmSync!(dir, { recursive: true, force: true });
1810
1846
  }
1811
1847
  };
1812
- const freshImport18 = async (dir: string): Promise<Record<string, unknown>> => await import('file://' + dir + '/src/client/note-crypto.ts') as Record<string, unknown>;
1813
- const mapStore18 = () => { const m = new Map<string, string>(); return {
1814
- get: (k: string): string | null => (m.has(k) ? m.get(k)! : null),
1815
- set: (k: string, v: string): void => { m.set(k, v); },
1816
- remove: (k: string): void => { m.delete(k); },
1817
- noteKeyWrap: (soul: string): string => 't18p_notekey:' + soul,
1818
- }; };
1819
- const baseCfg18 = (store: ReturnType<typeof mapStore18>): NoteCryptoConfig => ({ ...TACET, wrapLocal: 'jr1l.', store: store as unknown as NoteCryptoConfig['store'] });
1820
-
1821
- // P18 摘 fresh-face(三元→null)→ stored jr1l. blob 通路讀死+自癒腿(舊形服務)仍活
1822
- await withPoison18(
1823
- (s: string) => s.replace(needlePA18, '\n const fresh = null;'),
1824
- (s: string) => s.replace('\n const fresh = null;', needlePA18),
1825
- async (dir: string) => {
1826
- const mod = (await freshImport18(dir)) as typeof import('../src/client/note-crypto.ts');
1827
- const FIX = mapStore18(); const CFG = baseCfg18(FIX);
1828
- const nk = await mod.generateNoteKey();
1829
- await mod.storeLocalWrap(CFG, 'acca2', nk);
1830
- const hp = mod.makeHeldKey(); hp.set(nk);
1831
- const ct = await mod.encryptNote(CFG, hp, 'plain-ct-18', 'aadz', { current: () => 'acca2y' });
1832
- const back = await mod.decryptNote(CFG, mod.makeHeldKey(), ct, 'aadz', { current: () => 'acca2' });
1833
- await A('[18] 毒化 P18 摘 fresh-face → 本機通路讀死(blob 不可讀——designated)', back === null, String(back));
1834
- const guestA = await mod.deriveGuestKey(CFG, 'acca2');
1835
- CFG.store.set(CFG.store.noteKeyWrap('acca2'), await mod.sealNoteKey('jr1w.', nk, guestA, 'notekey-local'));
1836
- const lback = await mod.decryptNote(CFG, mod.makeHeldKey(), ct, 'aadz', { current: () => 'acca2' });
1837
- await A('[18] 毒化 P18 自癒腿仍活(舊形 blob 照服務——單面隔離非互毀)', lback !== null, String(lback));
1838
- });
1839
- // P18b 摘自癒回寫 → 舊形讀回照解+stored 殘留 jr1w.(零重寫)
1840
- await withPoison18(
1841
- (s: string) => s.replace(needlePB18, ' return legacy;'),
1842
- (s: string) => s.replace(' return legacy;', needlePB18),
1843
- async (dir: string) => {
1844
- const mod = (await freshImport18(dir)) as typeof import('../src/client/note-crypto.ts');
1845
- const FIX = mapStore18(); const CFG = baseCfg18(FIX);
1846
- const nk = await mod.generateNoteKey();
1847
- const guest = await mod.deriveGuestKey(CFG, 'accb2');
1848
- const legacy = await mod.sealNoteKey('jr1w.', nk, guest, 'notekey-local');
1849
- CFG.store.set(CFG.store.noteKeyWrap('accb2'), legacy);
1850
- const hp = mod.makeHeldKey(); hp.set(nk);
1851
- const ct = await mod.encryptNote(CFG, hp, 'plain-ct-18', 'aadz', { current: () => 'accb2y' });
1852
- const back = await mod.decryptNote(CFG, mod.makeHeldKey(), ct, 'aadz', { current: () => 'accb2' });
1853
- await A('[18] 毒化 P18b 摘自癒回寫 → 舊形讀回照解(讀面不依賴回寫線)', back !== null, String(back));
1854
- await A('[18] 毒化 P18b stored 殘留 jr1w.(零重寫——heal 條款面是唯一重寫者)',
1855
- typeof legacy === 'string' && legacy.startsWith('jr1w.') && (CFG.store.get(CFG.store.noteKeyWrap('accb2')) ?? '') === legacy);
1856
- });
1857
- // P18c 寫面接回借用形(禁手)→ 寫面 fallback jr1w.(v1 借用面復活的行為翻轉)
1858
- await withPoison18(
1859
- (s: string) => s.replace(needlePC18, 'sealNoteKey(cfg.wrap,'),
1860
- (s: string) => s.replace('sealNoteKey(cfg.wrap,', needlePC18),
1861
- async (dir: string) => {
1862
- const mod = (await freshImport18(dir)) as typeof import('../src/client/note-crypto.ts');
1863
- const FIX = mapStore18(); const CFG = baseCfg18(FIX);
1864
- const nk = await mod.generateNoteKey();
1865
- await mod.storeLocalWrap(CFG, 'accc2', nk);
1866
- const blob = CFG.store.get(CFG.store.noteKeyWrap('accc2'));
1867
- await A('[18] 毒化 P18c 寫面接回借用形 → 寫面 fallback jr1w.(designated——寫面契約行為翻轉)',
1868
- typeof blob === 'string' && blob.startsWith('jr1w.'), String(blob ?? '').slice(0, 8));
1869
- });
1870
- // 還原自證:真樹 needle 計數在所有毒化後仍恰 1×3(byte-exact 還原的行程帳)
1848
+ // P18 摘 fresh-face(三元→null)→ stored jr1l. blob 通路讀死+源碼窗 floor 面翻(窗 shrink 1833<1910;throw 禁令面由 P18d 獨立承載)
1849
+ await withPoisonRun18('[18] 毒化 P18 gate-rerun(P18:摘 fresh-face 三元→null)',
1850
+ needlePA18, '\n const fresh = null;',
1851
+ ['空-held decrypt 經本機包裹通路', '自癒後二次讀回', '窗尾 floor 三值帳'], 3);
1852
+ // P18b 摘自癒回寫 → 讀面照解(通路讀面不依賴回寫線)+回寫線計數窗錨翻面
1853
+ await withPoisonRun18('[18] 毒化 P18b gate-rerun(P18b:摘自癒回寫)',
1854
+ needlePB18, ' return legacy;',
1855
+ ['自癒回寫恆新前綴', '自癒 blob unwrap 等值', '自癒 blob 舊形恆拒', '回落三元恰 1'], 4);
1856
+ // P18c 寫面接回借用形(禁手)→ 寫面 fallback jr1w.(v1 借用面復活的行為翻轉)+寫面唯一 seal 計數窗錨翻面
1857
+ await withPoisonRun18('[18] 毒化 P18c gate-rerun(P18c:寫面接回借用形)',
1858
+ needlePC18, 'sealNoteKey(cfg.wrap,',
1859
+ ['寫前綴 jr1l.', '寫入 payload unwrap', '自癒回寫恆新前綴', '自癒 blob unwrap 等值', '自癒 blob 舊形恆拒', '二次讀不自癒二次', '寫面唯 cfg.wrapLocal seal'], 7);
1860
+ // P18d 本機段禁令復活(ERR 半邊——長度中性+36B):storeLocalWrap 守衛 return→throw
1861
+ //(storeLocalWrap 自身 try/catch 吸收=行為面零變;可觀察面=本窗禁令錨,毒後窗 +36B)。
1862
+ await withPoisonRun18('[18] 毒化 P18d gate-rerun(P18d:storeLocalWrap 守衛復活 throw)',
1863
+ ' if (!cfg.wrapLocal) return;',
1864
+ " if (!cfg.wrapLocal) throw new Error('ERR_WRAP_NOT_CONFIGURED');",
1865
+ ['本機段零 throw 面'], 1);
1866
+ // 真樹 needle 對位錨+byte-exact:毒化全程跑
1867
+ // 拷貝樹,真樹 sha256 毒前毒後恆等=零突變帳;needle 恰 1×4 計數防線=誤改斷言窗本體
1868
+ // (含守衛行)即 RED——名實一致帳)。
1871
1869
  const srcAfter18 = await srcOf('../src/client/note-crypto.ts');
1872
- await A('[18] 毒化還原自證(真樹 needle 恰 1×3——毒化零殘留本樹)',
1873
- srcAfter18.split(needlePA18).length === 2 && srcAfter18.split(needlePB18).length === 2 && srcAfter18.split(needlePC18).length === 2);
1870
+ await A('[18] 還原 byte-exact 對照(真樹 sha256 毒前毒後恆等——' + shaBefore18.slice(0, 12) + '…)', sha18(srcAfter18) === shaBefore18);
1871
+ await A('[18] 真樹 needle 對位錨(恰 1×4——真樹零突變的行程帶)',
1872
+ srcAfter18.split(needlePA18).length === 2 && srcAfter18.split(needlePB18).length === 2
1873
+ && srcAfter18.split(needlePC18).length === 2
1874
+ && srcAfter18.split(' if (!cfg.wrapLocal) return;').length === 2);
1874
1875
  await A('[18] 毒化樹零殘留(本 run 喚出 ' + trees18.length + ' 棵全清——封閉集判準)',
1875
- trees18.length === 3 && trees18.every((t) => !fs18!.rmSync || !existsSync18(t)));
1876
+ trees18.length === 4 && trees18.every((t) => !fs18!.rmSync || !existsSync18(t)));
1876
1877
  }
1877
1878
  }
1878
1879
  }
1879
1880
 
1880
- // ── 19. ladder 表與重取鹽面(v0.2.0 批卡② t_ce216a63;(a+) 案) ──────────────
1881
+ // ── 19. ladder 表與重取鹽面((a+) 案) ──────────────
1881
1882
  //
1882
1883
  // 驗證策略(真 SQLite,node:sqlite):①ph2_ladder DDL(FK CASCADE/UNIQUE account_id);
1883
1884
  // ②重取鹽面 unwrapNoteKeyDual4WithSalt=雙腿重試形為(ladder 遷移旋轉 salt1 後,呼叫端
@@ -1947,6 +1948,311 @@ console.log('\n[19] ladder 表與重取鹽面(unwrapNoteKeyDual4WithSalt+ph2
1947
1948
  await A('[19] WithSalt 未配置 cfg(wrapDual4 缺)→ null(opt-in 鐵律同向)',
1948
1949
  (await unwrapNoteKeyDual4WithSalt({ pinSalt3Prefix: 'tacet-note-pin3:' }, rewrapD4.wrapped, passD, pinD, rewrapD4.salt)) === null);
1949
1950
  }
1951
+ // ── 20. 復原套件專用前綴+HKDF 派生世代 ──
1952
+ //
1953
+ // 裁定:採搭車——wrappedRec 專用前綴+KEK_rec 改
1954
+ // HKDF-SHA256(jr3d KEK2 HKDF 為先例);「一個前綴一份契約」第二違例(wrappedRec 共用
1955
+ // cfg.wrap+同 AAD)同步收口。收口五面(檢查對齊定案):
1956
+ // ①專用前綴 cfg.wrapRec(jr1r.;命名=語意後綴軸,沿 jr1l. 定案)+專屬 AAD
1957
+ // 'notekey-rec'(帶內版本化:payload 契約面綁派生世代)。
1958
+ // ②世代配對門(檢查對齊 C1/C2):wrapRec+recKekHkdf 兩欄一體(同缺同在)——
1959
+ // 部分配置(XOR)=ERR_REC_CFG_PARTIAL 拒寫(fail-closed 禁寫出「jr1r.×PBKDF2」
1960
+ // 寫得出讀不回 blob 與「jr1w.×HKDF」借用 blob);讀面部分配置照走舊面回落
1961
+ // (承諾面:配置形正確時舊 blob 永遠可解)。
1962
+ // ③KEK_rec 派生世代 cfg.recKekHkdf(HKDF 專責):HKDF-SHA256( ikm = recToken(256-bit
1963
+ // 實體因子——KDF 強度無意義,世代收的是派生域分離), salt = recKekSalt?
1964
+ // 16B hex(未配置=零鹽), info = 'journal-kek-rec-v1:' + recKekHkdf, L = 32 )。
1965
+ // info 合成名沿 argon2 家族慣例(HKDF info 呼叫端帶入定形)。
1966
+ // PBKDF2 舊派生=deriveRecLegacyKek 單一本體(寫面未配置世代+讀面回落同源)。
1967
+ // ④讀舊寫新雙試:兩欄齊備 unwrap=jr1r. 面(HKDF)先行、未中→舊面(cfg.wrap+
1968
+ // notekey+PBKDF2)回落(救回動線順向雙試=裁定過渡保護
1969
+ // 涵蓋;摘雙試=遷移率裁定非日曆)。兩欄皆缺席(舊 cfg)=單試舊面(行為不變)。
1970
+ // ⑤四象限 write→read 矩陣(檢查對齊 M1):每象限「自 wrap 可自 unwrap 或 wrap 拒絕」
1971
+ // +超集 cfg 可讀子集 blob(升級路徑保護)+B01 歷史缺陷形恆拒(升 KDF=換前綴)。
1972
+ console.log('\n[20] 復原套件專用前綴(wrapRec=jr1r. opt-in)+KEK_rec HKDF 世代(recKekHkdf opt-in,兩欄一體配對門)');
1973
+ {
1974
+ const recSalt20 = 'a1b2c3d4e5f60718293a4b5c6d7e8f90';
1975
+ const TACET5: NoteCryptoConfig = { ...TACET, wrapRec: 'jr1r.', recKekHkdf: 'jr1r.', recKekSalt: recSalt20 };
1976
+ const raw20 = hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey)));
1977
+ const eqNoteKey20 = async (k: CryptoKey | null): Promise<boolean> =>
1978
+ k !== null && hex(new Uint8Array(await crypto.subtle.exportKey('raw', k))) === raw20;
1979
+ const rawKat20 = '57c72afacd486194c41227f42311600e287a7d48baa7a3e9de32fa460c089699'; // KAT 凍結 noteKey(外探針實測入冊)
1980
+ const eqRawKat20 = async (k: CryptoKey | null): Promise<boolean> =>
1981
+ k !== null && hex(new Uint8Array(await crypto.subtle.exportKey('raw', k))) === rawKat20;
1982
+ const recToken20 = generateRecToken();
1983
+ const KAT_BLOB_R20 = 'jr1r.bkvBFdKjNVfidHcfn0WoyyRJLkajWhXF7WmPdc3Q9FCNNxDz+7PgN0x25BjuAZPFi1oP3t+8dblrnSQEltkieBh33xoyChcGKe1wEfaStQF6ITBNb6vBZKVXDEQ=';
1984
+
1985
+ // P1 寫面:兩欄齊備=jr1r.+專屬 AAD;cfg.wrap 腿(passphrase 包裹)不受牽連
1986
+ const freshRec20 = await wrapNoteKeyWithRecToken(TACET5, noteKey, recToken20, identityA);
1987
+ await A('[20] 新世代 wrappedRec 前綴 jr1r.(寫面契約——借用期 cfg.wrap 形=反)', freshRec20.startsWith('jr1r.'));
1988
+ await A('[20] KAT 自洽前置(live wrap 樣本可重放):jr1r. blob+同 identity → 原 noteKey',
1989
+ eqNoteKey20(await unwrapNoteKeyWithRecToken(TACET5, freshRec20, recToken20, identityA)));
1990
+ await A('[20] 寫面 jr1w. 腿不受牽連(passphrase 包裹照走本前綴)',
1991
+ (await wrapNoteKey(TACET5, noteKey, pass)).wrapped.startsWith('jr1w.'));
1992
+
1993
+ // P2 舊契約面凍結:未配置世代 wrap/unwrap(byte 不變承載——函式體零改面)
1994
+ const legacyRec20 = await wrapNoteKeyWithRecToken(TACET, noteKey, recToken20, identityA);
1995
+ await A('[20] 未配置世代 wrappedRec 前綴 jr1w.(契約面永不變——借用面凍結)', legacyRec20.startsWith('jr1w.'));
1996
+ await A('[20] 未配置世代 unwrap = 原 noteKey(PBKDF2 舊派生契約原樣)',
1997
+ eqNoteKey20(await unwrapNoteKeyWithRecToken(TACET, legacyRec20, recToken20, identityA)));
1998
+ await A('[20] KAT 凍結 blob(舊 PBKDF2 契約面):凍結 recToken+凍結 identity 鹽域+凍結 blob 逐位可解(凍結常數計算式構造——KAT 母型;對錨=凍結 noteKey raw 非本段活金鑰)',
1999
+ eqRawKat20(await unwrapNoteKeyWithRecToken(TACET, 'jr1w.a4xchJ+EPui8iQy5dKpQyGjalu0nlJMsDd0LaLyRrY3mt+6112Se4NvaibymRFHsMg1LgOYsBVy15t0CiU1ipf9g8rniqBmdf1R5LzVXQdNJDCzbAfubPUKc/FE=', '62ff0221e30b56740ec210cd36df2e9667841585c6d0fcd5200826b965aa36a5', 'kat-id-0000000000000000')));
2000
+ await A('[20] seal 未配置守衛:cfg.wrap 缺席 legacy 路徑直呼 → ERR_WRAP_NOT_CONFIGURED(呼叫端亦可先行拒絕——雙防線外層)',
2001
+ (async () => {
2002
+ const noWrap20 = { ...TACET, wrap: undefined } as unknown as NoteCryptoConfig;
2003
+ try { await wrapNoteKeyWithRecToken(noWrap20, noteKey, recToken20, identityA); return false; }
2004
+ catch { return true; }
2005
+ })());
2006
+ await A('[20] 帳面 92B 錨(鹽內嵌面契約):jr1r. payload 嚴格 92B(hex64 64B+iv 12+tag 16——HKDF 鹽屬「派生面內嵌 cfg、payload 零內嵌」,pinSalt 前綴不在 rec 鹽形契約內)',
2007
+ freshRec20.length > 'jr1r.'.length && unb64Mod(freshRec20.slice('jr1r.'.length)).length === 92);
2008
+
2009
+ // P3 新世代讀舊寫新(雙試)+家族互斥+KAT 世代帶
2010
+ await A('[20] 新世代讀舊 blob(過渡保護):舊 PBKDF2/AAD/前綴面回落雙試 → 原 noteKey',
2011
+ eqNoteKey20(await unwrapNoteKeyWithRecToken(TACET5, legacyRec20, recToken20, identityA)));
2012
+ await A('[20] 家族互斥:jr1r. blob 餵未配置世代 cfg → null(新面恆拒——家族隔離對稱面)',
2013
+ (await unwrapNoteKeyWithRecToken(TACET, freshRec20, recToken20, identityA)) === null);
2014
+ await A('[20] KAT 凍結 blob:jr1r. 逐位重放(凍結 recToken/salt/identity/blob 恆定——HKDF 世代派生確定性重驗;對錨=凍結 noteKey raw 非本段活金鑰)',
2015
+ eqRawKat20(await unwrapNoteKeyWithRecToken(TACET5, KAT_BLOB_R20, '62ff0221e30b56740ec210cd36df2e9667841585c6d0fcd5200826b965aa36a5', 'kat-id-0000000000000000')));
2016
+ await A('[20] KAT 世代齒(鹽入派生正向翻面):凍結 recToken/identity/blob 不變、鹽域換值 → null(鹽 A wrap→鹽 B unwrap 恆不重合——HKDF salt 有入派生)',
2017
+ (await unwrapNoteKeyWithRecToken({ ...TACET5, recKekSalt: 'ffeeddccbbaa99887766554433221100' }, KAT_BLOB_R20, '62ff0221e30b56740ec210cd36df2e9667841585c6d0fcd5200826b965aa36a5', 'kat-id-0000000000000000')) === null);
2018
+ await A('[20] KAT 世代齒(info 域帶毒化案同錨):HKDF info 尾綴換值(鹽/identity/blob 恆定)→ null(info 有入派生——R2 毒化翻轉的 clean 鏡)',
2019
+ (await unwrapNoteKeyWithRecToken({ ...TACET5, recKekHkdf: 'jr1r-x' }, KAT_BLOB_R20, '62ff0221e30b56740ec210cd36df2e9667841585c6d0fcd5200826b965aa36a5', 'kat-id-0000000000000000')) === null);
2020
+ await A('[20] HKDF 世代正交面(配對門收口後契約):僅 recKekHkdf(wrapRec 缺席)=配對門拒寫,讀面 jr1r. blob 照舊面回落零接納(cfg.wrap 在場=PBKDF2 腿零承載——HKDF 派生禁借 jr1w. 前綴)',
2021
+ (await unwrapNoteKeyWithRecToken({ ...TACET, recKekHkdf: 'jr1r.' }, freshRec20, recToken20, identityA)) === null);
2022
+
2023
+ // identity 參數語意(HKDF 族零接觸面——docstring identity-free by design 承載)
2024
+ const freshB20 = await wrapNoteKeyWithRecToken(TACET5, noteKey, recToken20, 'acct-bbbbbbbbbbbbbbbb');
2025
+ await A('[20] identity HKDF 族零接觸(docstring identity-free 契約自證化——誤接線即 RED)',
2026
+ freshB20.startsWith('jr1r.') && eqNoteKey20(await unwrapNoteKeyWithRecToken(TACET5, freshB20, recToken20, 'acct-cccccccccccccccc')));
2027
+ await A('[20] identity 互斥面(舊契約面):錯 identity unwrap → null(rec 鹽=recSaltPrefix‖identity 定值——同 recToken 異 KEK)',
2028
+ (await unwrapNoteKeyWithRecToken(TACET, legacyRec20, recToken20, 'acct-bbbbbbbbbbbbbbbb')) === null);
2029
+
2030
+ // P4 契約面守衛(形檢 fail-closed)
2031
+ await A('[20] 壞 recKekSalt fail-closed:unwrap → null(HKDF 腿 throw→catch,零 legacy 回落——cfg 誤配整面 fail-closed)',
2032
+ (await unwrapNoteKeyWithRecToken({ ...TACET5, recKekSalt: 'xyz123' }, freshRec20, recToken20, identityA)) === null);
2033
+ await A('[20] 壞 recKekSalt wrap 面拋 ERR_REC_KEK_SALT(fail-closed 拒寫——派生世代誤配即禁寫)',
2034
+ (async () => { try { await wrapNoteKeyWithRecToken({ ...TACET5, recKekSalt: 'xyz123' }, noteKey, recToken20, identityA); return false; } catch (e) { return (e as Error).message === 'ERR_REC_KEK_SALT'; } })());
2035
+
2036
+ // P5 世代配對門(檢查對齊 C1/C2 收口——兩欄一體,fail-closed 拒寫)
2037
+ await A('[20] 配對門 wrapRec-only(C1 象限):拋 ERR_REC_CFG_PARTIAL(禁寫「jr1r.×PBKDF2」寫得出讀不回 blob)',
2038
+ (async () => { try { await wrapNoteKeyWithRecToken({ ...TACET, wrapRec: 'jr1r.' }, noteKey, recToken20, identityA); return false; } catch (e) { return (e as Error).message === 'ERR_REC_CFG_PARTIAL'; } })());
2039
+ await A('[20] 配對門 recKekHkdf-only(C2 象限):拋 ERR_REC_CFG_PARTIAL(禁借 jr1w. 前綴 carrying HKDF 派生)',
2040
+ (async () => { try { await wrapNoteKeyWithRecToken({ ...TACET, recKekHkdf: 'jr1r.' }, noteKey, recToken20, identityA); return false; } catch (e) { return (e as Error).message === 'ERR_REC_CFG_PARTIAL'; } })());
2041
+ // B01 歷史缺陷形向量(收口後零接納面):HKDF-on-jr1w. 借形 blob——守衛形拒寫=
2042
+ // 無構造源(升級探針缺陷形),凍結重放式對借形 blob 的恆拒承載(對錨=null 非 noteKey)。
2043
+ const legacyB01_20 = 'jr1w.' + modB64(new Uint8Array(92).fill(7));
2044
+ await A('[20] 配對門錯誤序前於鹽形檢(部分配置形不驗鹽——門先收)',
2045
+ (async () => { try { await wrapNoteKeyWithRecToken({ ...TACET, recKekHkdf: 'jr1r.', recKekSalt: 'xyz123' }, noteKey, recToken20, identityA); return false; } catch (e) { return (e as Error).message === 'ERR_REC_CFG_PARTIAL'; } })());
2046
+ await A('[20] 配對門讀面寬容:wrapRec-only cfg 讀舊 jr1w. blob → 原 noteKey(部分配置禁寫不禁讀——舊 blob 永遠可解)',
2047
+ eqNoteKey20(await unwrapNoteKeyWithRecToken({ ...TACET, wrapRec: 'jr1r.' }, legacyRec20, recToken20, identityA)));
2048
+ await A('[20] 配對門讀面寬容(C1 歷史缺陷形救回面):wrapRec-only cfg 逐位重放凍結舊 blob(PBKDF2 鹽域)→ 凍結 noteKey',
2049
+ eqRawKat20(await unwrapNoteKeyWithRecToken({ ...TACET, wrapRec: 'jr1r.' }, 'jr1w.a4xchJ+EPui8iQy5dKpQyGjalu0nlJMsDd0LaLyRrY3mt+6112Se4NvaibymRFHsMg1LgOYsBVy15t0CiU1ipf9g8rniqBmdf1R5LzVXQdNJDCzbAfubPUKc/FE=', '62ff0221e30b56740ec210cd36df2e9667841585c6d0fcd5200826b965aa36a5', 'kat-id-0000000000000000')));
2050
+ await A('[20] 配對門讀面恆拒(B01 歷史缺陷形):recKekHkdf-only cfg 讀 HKDF-on-jr1w. 形 blob → null(借形缺陷 blob 零接納面;升 KDF=換前綴)',
2051
+ (await unwrapNoteKeyWithRecToken({ ...TACET, recKekHkdf: 'jr1r.', recKekSalt: recSalt20 }, legacyB01_20, recToken20, identityA)) === null);
2052
+
2053
+ // M1 四象限 write→read 矩陣(檢查對齊——四象限配置(wrapRec,recKekHkdf)逐格對帳面;
2054
+ // 每格「write:blob 前綴(或配對門拒)→ read:同 cfg 自解行為」+超集 cfg 讀子集 blob)。
2055
+ {
2056
+ const qCfg10 = (): NoteCryptoConfig => ({ ...TACET, wrapRec: 'jr1r.' });
2057
+ const qCfg01 = (): NoteCryptoConfig => ({ ...TACET, recKekHkdf: 'jr1r.', recKekSalt: recSalt20 });
2058
+ const wrapQ = async (c: NoteCryptoConfig): Promise<string> => {
2059
+ try { return await wrapNoteKeyWithRecToken(c, noteKey, recToken20, identityA); }
2060
+ catch (e) { return (e as Error).message; }
2061
+ };
2062
+ const b00q = await wrapQ({ ...TACET });
2063
+ const b10q = await wrapQ(qCfg10());
2064
+ const b01q = await wrapQ(qCfg01());
2065
+ const b11q = await wrapQ({ ...TACET5 });
2066
+ await A('[20] M1 write 面四格:B00→jr1w./B10→ERR_REC_CFG_PARTIAL/B01→ERR_REC_CFG_PARTIAL/B11→jr1r.(部分配置零寫出面——C1/C2 借形禁產)',
2067
+ b00q.startsWith('jr1w.') && b10q === 'ERR_REC_CFG_PARTIAL' && b01q === 'ERR_REC_CFG_PARTIAL' && b11q.startsWith('jr1r.'),
2068
+ b00q.slice(0, 5) + '/' + b10q + '/' + b01q + '/' + b11q.slice(0, 5));
2069
+ const r00q = await unwrapNoteKeyWithRecToken({ ...TACET }, b00q, recToken20, identityA);
2070
+ const r11q = await unwrapNoteKeyWithRecToken({ ...TACET5 }, b11q, recToken20, identityA);
2071
+ await A('[20] M1 read 面自解格:B00/B11 同 cfg 自 wrap 自 unwrap=原 noteKey(B10/B01 無 blob=write 面承載)',
2072
+ (await eqNoteKey20(r00q)) && (await eqNoteKey20(r11q)));
2073
+ await A('[20] M1 超集讀子集(升級路徑保護):B10/B01 cfg 皆可讀 B00 舊 blob(舊面回落)',
2074
+ (await eqNoteKey20(await unwrapNoteKeyWithRecToken(qCfg10(), b00q, recToken20, identityA)))
2075
+ && (await eqNoteKey20(await unwrapNoteKeyWithRecToken(qCfg01(), b00q, recToken20, identityA))));
2076
+ await A('[20] M1 超集讀子集:B11 cfg 讀 B00 blob(雙試回落=原 noteKey)、B00 cfg 讀 B11 blob(子集讀超集=null——家族隔離對稱面)',
2077
+ (await eqNoteKey20(await unwrapNoteKeyWithRecToken({ ...TACET5 }, b00q, recToken20, identityA)))
2078
+ && ((await unwrapNoteKeyWithRecToken({ ...TACET }, b11q, recToken20, identityA)) === null));
2079
+ }
2080
+
2081
+ // 源碼窗(計數錨——接線漂移即 RED;needle+1 式對照 [14][15][16] 母型;錨為敘述形且 docstring 零咬;
2082
+ // 檢查對齊 M1 後配對門落地——窗口全體重錨隨修正形;「HKDF 腿零借 cfg.wrap」的接線由 R2 贊毒化承載)
2083
+ const noteCryptoSrc20 = await srcOf('../src/client/note-crypto.ts');
2084
+ await A('[20] 源碼窗:專屬 AAD 接線恰 2(wrap seal 面與 unwrap open 面各恰 1——回收共用面復活/錯位接線即變異)',
2085
+ noteCryptoSrc20.split("kek, 'notekey-rec')").length === 2 && noteCryptoSrc20.split("'notekey-rec', cfg.wrapRec)").length === 2);
2086
+ await A('[20] 源碼窗:世代配對門恰 1(ERR_REC_CFG_PARTIAL 拋點,split 帳=出現+1——門摘除=拒寫面失效即 RED)',
2087
+ noteCryptoSrc20.split("throw new Error('ERR_REC_CFG_PARTIAL')").length === 2);
2088
+ await A('[20] 源碼窗:配對門接線形恰 1(wrapRec 或 recKekHkdf 在場即入門——單欄短路復活即形變異)',
2089
+ noteCryptoSrc20.split('if (cfg.wrapRec || cfg.recKekHkdf) {').length === 2
2090
+ && noteCryptoSrc20.split("if (!cfg.wrapRec || !cfg.recKekHkdf) throw new Error('ERR_REC_CFG_PARTIAL');").length === 2);
2091
+ await A('[20] 源碼窗:HKDF 專責接線恰 1(兩欄齊備守衛——派生雙模/C2 借形復活即變異)',
2092
+ noteCryptoSrc20.split('if (cfg.wrapRec && cfg.recKekHkdf) {').length === 2);
2093
+ await A('[20] 源碼窗:PBKDF2 fallback 殘留歸零+deriveRecKek 呼叫面恰 3(非空洞計數:fallback 形歸零+def+兩呼叫端在場——C2 收口面)',
2094
+ noteCryptoSrc20.split('if (!cfg.recKekHkdf) return deriveKek').length === 1
2095
+ && noteCryptoSrc20.split('deriveRecKek(').length === 4);
2096
+ await A('[20] 源碼窗:deriveRecLegacyKek def+呼叫恰 3(split 帳=出現+1;legacy 本體單一真相——寫面+讀面同源)',
2097
+ noteCryptoSrc20.split('deriveRecLegacyKek(').length === 4);
2098
+ await A('[20] 源碼窗:cfg.wrapRec 敘述形恰 2(配對門 XOR 一對——借用面復活即計數變異)',
2099
+ noteCryptoSrc20.split('cfg.wrapRec ||').length === 3
2100
+ && noteCryptoSrc20.split('!cfg.wrapRec ||').length === 2);
2101
+
2102
+ // R 系毒化(自毒化驗齒——/tmp 拷貝樹 fresh import,出生即棄;期望值真跑後定值,帳面直覺禁沿;
2103
+ // 檢查對齊:R2 重錨=info 字面→KAT 世代齒、R4 label 對齊、R5 新案=配對門(C1/C2 唯一閘承載))
2104
+ const getBuiltin20 = (id: string): unknown =>
2105
+ (globalThis as unknown as { process?: { getBuiltinModule?: (i: string) => unknown } }).process?.getBuiltinModule?.(id);
2106
+ const fs20 = getBuiltin20('node:fs') as {
2107
+ rmSync?: (p: string, o?: { recursive: boolean; force?: boolean }) => void;
2108
+ cpSync?: (a: string, b: string, o?: { recursive: boolean; filter?: (s: string) => boolean }) => void;
2109
+ readFileSync?: (p: string, e?: string) => string;
2110
+ writeFileSync?: (p: string, c: string, e?: string) => void;
2111
+ mkdtempSync?: (p: string) => string;
2112
+ existsSync?: (p: string) => boolean;
2113
+ } | undefined;
2114
+ const os20 = getBuiltin20('node:os') as { tmpdir?: () => string } | undefined;
2115
+ const inner20 = (getBuiltin20('node:process') as { env?: Record<string, string | undefined> } | undefined)?.env?.POISON_GATE_INNER === '1';
2116
+ const ready20 = !!os20?.tmpdir && !!fs20?.mkdtempSync && !!fs20?.cpSync && !!fs20?.readFileSync && !!fs20?.writeFileSync && !!fs20?.rmSync;
2117
+ const needleR1 = " return sealNoteKey(cfg.wrapRec, noteKey, kek, 'notekey-rec');";
2118
+ const needleR3 = " const legacyKek = await deriveRecLegacyKek(cfg, recToken, identity);";
2119
+ const needleR5 = "if (!cfg.wrapRec || !cfg.recKekHkdf) throw new Error('ERR_REC_CFG_PARTIAL');";
2120
+ const srcNow20 = await srcOf('../src/client/note-crypto.ts');
2121
+ await A('[20] 毒化錨前綴自檢(毒化窗同字面點數先行;split 計數帳=出現數+1)',
2122
+ srcNow20.split(needleR1).length === 2 && srcNow20.split(needleR3).length === 2
2123
+ && srcNow20.split("const info = 'journal-kek-rec-v1:'").length === 2 && srcNow20.split(needleR5).length === 2);
2124
+ if (inner20 || !ready20) {
2125
+ await A('[20] 毒化矩陣載體就緒(node 內建模組在場;內層遞迴由 sentinel 跳過=正常;缺席=顯性 FAIL)',
2126
+ inner20 && !ready20 ? false : inner20, 'env unavailable AND not inner');
2127
+ } else {
2128
+ const repoRoot20 = new URL('..', import.meta.url).pathname;
2129
+ const skip20 = (s: string): boolean => s.endsWith('/.git') || s.includes('/.git/') || s.includes('/node_modules') || s.split('/').pop() === 'node_modules';
2130
+ const trees20: string[] = [];
2131
+ const withPoison20 = async (applyS: (s: string) => string, probe: (dir: string) => Promise<void>): Promise<void> => {
2132
+ const dir = fs20!.mkdtempSync!(os20!.tmpdir!() + '/t20-')!;
2133
+ trees20.push(dir);
2134
+ try {
2135
+ fs20!.cpSync!(repoRoot20, dir, { recursive: true, filter: (s: string) => !skip20(s) });
2136
+ const p = dir + '/src/client/note-crypto.ts';
2137
+ const s0 = fs20!.readFileSync!(p, 'utf8');
2138
+ fs20!.writeFileSync!(p, applyS(s0), 'utf8');
2139
+ await probe(dir);
2140
+ } finally {
2141
+ fs20!.rmSync!(dir, { recursive: true, force: true });
2142
+ }
2143
+ };
2144
+ const freshImport20 = async (dir: string): Promise<Record<string, unknown>> => await import('file://' + dir + '/src/client/note-crypto.ts') as Record<string, unknown>;
2145
+ const baseCfg20 = (): NoteCryptoConfig => ({ ...TACET, wrapRec: 'jr1r.', recKekHkdf: 'jr1r.', recKekSalt: recSalt20 });
2146
+
2147
+ // R1 摘專屬 AAD(寫面接回共用 'notekey')→ jr1r. 新面 unwrap 死=單純換前綴假收口(designated)
2148
+ await withPoison20(
2149
+ (s: string) => s.replace(needleR1, " return sealNoteKey(cfg.wrapRec, noteKey, kek, 'notekey');"),
2150
+ async (dir: string) => {
2151
+ const mod = (await freshImport20(dir)) as typeof import('../src/client/note-crypto.ts');
2152
+ const cfg = baseCfg20();
2153
+ const nk = await mod.generateNoteKey();
2154
+ const rt = mod.generateRecToken();
2155
+ const blob = await mod.wrapNoteKeyWithRecToken(cfg, nk, rt, identityA);
2156
+ const back = await mod.unwrapNoteKeyWithRecToken(cfg, blob, rt, identityA);
2157
+ await A('[20] 毒化 R1 摘專屬 AAD → 新面 unwrap 死(blob 內 AAD 舊契約形——designated)', back === null, String(back !== null));
2158
+ });
2159
+ // R2 HKDF info 域摘除(重錨:毒化打 info 字面——info 變數吃 recKekHkdf 值本身、
2160
+ // 家族成名域零在場→KAT 凍結 blob unwrap 死=KAT 世代齒一次到位(designated))
2161
+ await withPoison20(
2162
+ (s: string) => s.replace("const info = 'journal-kek-rec-v1:' + hkdfTail;", 'const info = hkdfTail;'),
2163
+ async (dir: string) => {
2164
+ const mod = (await freshImport20(dir)) as typeof import('../src/client/note-crypto.ts');
2165
+ const back = await mod.unwrapNoteKeyWithRecToken(baseCfg20(), KAT_BLOB_R20, '62ff0221e30b56740ec210cd36df2e9667841585c6d0fcd5200826b965aa36a5', 'kat-id-0000000000000000');
2166
+ await A('[20] 毒化 R2 info 字面移除 → KAT blob unwrap 死(KAT 世代齒——info 有入派生的正向翻面——designated)', back === null, String(back !== null));
2167
+ });
2168
+ // R3 摘舊面雙試(legacy 派生錯鹽)→ 新世代讀舊 blob 死(過渡保護有齒)+舊 cfg 面同死(同一本體)
2169
+ await withPoison20(
2170
+ (s: string) => s.replace(needleR3, " const legacyKek = await deriveRecLegacyKek(cfg, recToken, identity + 'x');"),
2171
+ async (dir: string) => {
2172
+ const mod = (await freshImport20(dir)) as typeof import('../src/client/note-crypto.ts');
2173
+ const cfg = baseCfg20();
2174
+ const nk = await mod.generateNoteKey();
2175
+ const rt = mod.generateRecToken();
2176
+ const legacy = await mod.wrapNoteKeyWithRecToken(TACET, nk, rt, identityA);
2177
+ const back = await mod.unwrapNoteKeyWithRecToken(cfg, legacy, rt, identityA);
2178
+ await A('[20] 毒化 R3 摘舊面雙試 → 新世代讀舊 blob 死(designated——過渡保護有齒)', back === null, String(back !== null));
2179
+ const backOld = await mod.unwrapNoteKeyWithRecToken(TACET, legacy, rt, identityA);
2180
+ await A('[20] 毒化 R3 舊 cfg 面同死(legacy 派生本體單一真相——兩處呼叫同源)', backOld === null, String(backOld !== null));
2181
+ });
2182
+ // R4 前綴借用面復活(wrapRec 面接回 cfg.wrap)→ 寫面 fallback jr1w.(designated;契約翻轉齒由 P1 前綴斷言承載)
2183
+ await withPoison20(
2184
+ (s: string) => s.replace(needleR1, " return sealNoteKey(cfg.wrap, noteKey, kek, 'notekey-rec');"),
2185
+ async (dir: string) => {
2186
+ const mod = (await freshImport20(dir)) as typeof import('../src/client/note-crypto.ts');
2187
+ const cfg = baseCfg20();
2188
+ const nk = await mod.generateNoteKey();
2189
+ const rt = mod.generateRecToken();
2190
+ const blob = await mod.wrapNoteKeyWithRecToken(cfg, nk, rt, identityA);
2191
+ await A('[20] 毒化 R4 前綴借用面復活 → 寫面 fallback jr1w.(毒化執行證——契約翻轉齒由 P1 前綴斷言承載)',
2192
+ typeof blob === 'string' && blob.startsWith('jr1w.'), String(blob ?? '').slice(0, 8));
2193
+ });
2194
+ // R5 忠實回退(檢查對齊新增案——C1/C2 拒寫面的唯一閘承載;designated):
2195
+ // 門分支回寫生產形(wrapRec 面吃 legacy 派生+notekey-rec 契約),尾端
2196
+ // cfg.wrap 腿原樣留任(wrapRec 缺席續走 cfg.wrap+poisoned deriveRecKef HKDF)=
2197
+ // 兩歷史缺陷象限(C1=jr1r.×PBKDF2/C2=jr1w.×HKDF)寫出面精確重現——毒化代碼照寫、
2198
+ // 兩缺陷形復活並自解 null 的行為翻面承載。
2199
+ const poisonR5 = (s: string): string => s.replace(
2200
+ " if (cfg.wrapRec || cfg.recKekHkdf) {\n if (!cfg.wrapRec || !cfg.recKekHkdf) throw new Error('ERR_REC_CFG_PARTIAL');\n const kek = await deriveRecKek(cfg, recToken);\n return sealNoteKey(cfg.wrapRec, noteKey, kek, 'notekey-rec');\n }",
2201
+ ' if (cfg.wrapRec) {\n const kek = await deriveRecLegacyKek(cfg, recToken, identity);\n return sealNoteKey(cfg.wrapRec, noteKey, kek, "notekey-rec");\n }',
2202
+ );
2203
+ await withPoison20(poisonR5, async (dir: string) => {
2204
+ const mod = (await freshImport20(dir)) as typeof import('../src/client/note-crypto.ts');
2205
+ const nk = await mod.generateNoteKey();
2206
+ const rt = mod.generateRecToken();
2207
+ const blobC1 = await mod.wrapNoteKeyWithRecToken({ ...TACET, wrapRec: 'jr1r.' }, nk, rt, identityA);
2208
+ const blobC2 = await mod.wrapNoteKeyWithRecToken({ ...TACET, recKekHkdf: 'jr1r.', recKekSalt: recSalt20 }, nk, rt, identityA);
2209
+ const backC1 = await mod.unwrapNoteKeyWithRecToken({ ...TACET, wrapRec: 'jr1r.' }, blobC1, rt, identityA);
2210
+ await A('[20] 毒化 R5 忠實回退 → C1 歷史缺陷形復活(jr1r.×PBKDF2 blob 寫出+毒形讀面自解 null=B10 象限寫得出讀不回——designated)',
2211
+ typeof blobC1 === 'string' && blobC1.startsWith('jr1r.') && backC1 === null,
2212
+ String(blobC1 ?? '').slice(0, 5) + '/' + String(backC1 !== null));
2213
+ await A('[20] 毒化 R5 忠實回退 → C2 歷史缺陷形復活(jr1w. 前綴 carrying 派生 HKDF blob 寫出——designated;借形契約面重現)',
2214
+ typeof blobC2 === 'string' && blobC2.startsWith('jr1w.'), String(blobC2 ?? '').slice(0, 5));
2215
+ });
2216
+ // 檢核:R5 毒化代碼(忠實回退形)寫出的 C1 形 blob,在守衛形(未毒化)讀面下不可救——配對門價值的直接行為證(缺口行為化)
2217
+ {
2218
+ const dirP = fs20!.mkdtempSync!(os20!.tmpdir!() + '/t20r5-')!;
2219
+ trees20.push(dirP);
2220
+ try {
2221
+ fs20!.cpSync!(repoRoot20, dirP, { recursive: true, filter: (s: string) => !skip20(s) });
2222
+ const pP = dirP + '/src/client/note-crypto.ts';
2223
+ const sP = fs20!.readFileSync!(pP, 'utf8');
2224
+ if (poisonR5(sP) === sP) throw new Error('R5 anchor missing');
2225
+ fs20!.writeFileSync!(pP, poisonR5(sP), 'utf8');
2226
+ const modP = (await import('file://' + dirP + '/src/client/note-crypto.ts')) as typeof import('../src/client/note-crypto.ts');
2227
+ const nkP = await modP.generateNoteKey();
2228
+ const rtP = (await modP.generateRecToken());
2229
+ const rawP = hex(new Uint8Array(await crypto.subtle.exportKey('raw', nkP)));
2230
+ const blobC1 = await modP.wrapNoteKeyWithRecToken({ ...TACET, wrapRec: 'jr1r.' }, nkP, rtP, identityA);
2231
+ const rawEqP = async (k: CryptoKey | null): Promise<boolean> => k !== null && hex(new Uint8Array(await crypto.subtle.exportKey('raw', k))) === rawP;
2232
+ const back = await unwrapNoteKeyWithRecToken({ ...TACET, wrapRec: 'jr1r.' }, blobC1, rtP, identityA);
2233
+ const backUp = await unwrapNoteKeyWithRecToken(TACET5, blobC1, rtP, identityA);
2234
+ await A('[20] 配對門價值行為證:R5 毒化代碼寫出 C1 形 blob → 守衛形讀面自解 null(B10 象限缺口的行為化翻面)',
2235
+ back === null, String(back === null));
2236
+ await A('[20] 配對門價值行為證:C1 形 blob 在完整新世代 cfg 下讀面亦 null(HKDF KEK≠PBKDF2 KEK——升級不救 B10)',
2237
+ backUp === null && !(await rawEqP(back)),
2238
+ String(backUp === null) + '/' + String(await rawEqP(back)));
2239
+ } finally {
2240
+ fs20!.rmSync!(dirP, { recursive: true, force: true });
2241
+ }
2242
+ }
2243
+ // 還原自證:毒化樹零殘留(本 run 喚出 6 棵全清——封閉集判準;出生即棄拷貝樹+真樹 needle 計數自證)
2244
+ const srcAfter20 = (getBuiltin20('node:fs') as { readFileSync?: (p: string, e?: string) => string })!.readFileSync!(
2245
+ new URL('../src/client/note-crypto.ts', import.meta.url).pathname, 'utf8');
2246
+ await A('[20] 真樹 needle 對位錨(真樹零突變的行程帶)',
2247
+ srcAfter20.split(needleR1).length === 2 && srcAfter20.split(needleR3).length === 2
2248
+ && srcAfter20.split("const info = 'journal-kek-rec-v1:'").length === 2 && srcAfter20.split(needleR5).length === 2);
2249
+ const existsSync20 = (p: string): boolean =>
2250
+ typeof fs20!.existsSync === 'function' ? fs20!.existsSync!(p) : false;
2251
+ await A('[20] 毒化樹零殘留(本 run 喚出 ' + trees20.length + ' 棵全清——封閉集判準)',
2252
+ trees20.length === 6 && trees20.every((t) => !existsSync20(t)));
2253
+ }
2254
+ }
2255
+
1950
2256
  console.log(`\n${passed} 斷言全綠` + (failures.length ? `;${failures.length} 失敗` : ''));
1951
2257
  if (failures.length) {
1952
2258
  console.error('失敗項:', failures);