@tacet-ink/journal-core 0.3.0 → 0.3.2

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.
@@ -69,6 +69,8 @@ import {
69
69
  derivePh1ArgonV3,
70
70
  PH1_V2_SALT,
71
71
  PH1_V3_SALT,
72
+ ARGON_RFC9106_EXPECTED,
73
+ setArgonLoader,
72
74
  type Argon3Config,
73
75
  } from '../src/client/argon2.ts';
74
76
  import { makeKeyStore } from '../src/client/keys.ts';
@@ -88,7 +90,7 @@ import {
88
90
  AUTH_RATE,
89
91
  type CipherFormats,
90
92
  } from '../src/server/auth.ts';
91
- import { checkRate, isRateAllowed, SQL_RATE_BUMP, type RateWindow } from '../src/server/ratelimit.ts';
93
+ import { checkRate, isRateAllowed, normalizeRateIp, SQL_RATE_BUMP, type RateWindow } from '../src/server/ratelimit.ts';
92
94
  import { generateSessionToken, timingSafeEq } from '../src/server/hash.ts';
93
95
  import { CORS_HEADERS, corsResponse } from '../src/server/cors.ts';
94
96
  import {
@@ -97,6 +99,19 @@ import {
97
99
  PH2_LADDER_KIND_V2,
98
100
  makePh2LadderStore,
99
101
  } from '../src/server/ladder.ts';
102
+ import {
103
+ createVault,
104
+ unlockVault,
105
+ recoverVault,
106
+ VaultError,
107
+ isVaultError,
108
+ currentArgonCarrier,
109
+ resetArgonCarrier,
110
+ argonWorkerSource,
111
+ type VaultOptions,
112
+ type ArgonWorkerLike,
113
+ } from '../src/client/vault.ts';
114
+ import { hashWasmArgon2Factory, HASH_WASM_ARGON2_SHA256, HASH_WASM_VERSION } from '../src/client/vendor/hash-wasm-argon2.ts';
100
115
 
101
116
  type Row19 = { account_id: string; ph2_kind: string };
102
117
  type Env19 = { DB: { prepare(sql: string): { bind(...params: unknown[]): { first(): Promise<Row19 | null>; run(): Promise<void> } } } };
@@ -142,7 +157,7 @@ async function A(name: string, cond: boolean | Promise<boolean>, detail = ''): P
142
157
  // 帳面契約:--only 態總帳顯形「本輪 X/選段帳 N(N 案非本輪載)」+OK 標記帶 (--ONLY …) 尾碼;skip 段帳面恆列「未跑非通過」;[24]/[25] runner 自證段隨每輪在場。
143
158
  // 段標記 helper(secOpen):全跑態輸出位元組恆等原 console.log('\n[N] …');--only 態僅選中段輸出段頭。
144
159
  // 毒化矩陣內層 rerun(execFileSync+POISON_GATE_INNER=1)不傳遞 --only=內層恆全帳(POISON_GATE_INNER 語意不變)。
145
- // 段帳=執行帳(healthy-repo 全跑 runtime 真值,2026-10-10 實測 492——A2 [26] +8);帳面語意契約(逐段「案 X/帳」
160
+ // 段帳=執行帳(healthy-repo 全跑 runtime 真值,2026-10-11 實測 521——[12] 限流鍵 /64 正規化 +14+IPv4-mapped 面 +1);帳面語意契約(逐段「案 X/帳」
146
161
  // 對 secRan 執行值顯形):執行 < 帳 的段=本環境條件態未行使(非 skip 態),--only 期望算術對齊本帳。
147
162
  // [23] 執行 18 帳 21=[23]④ lookup 未配置(0.2.3 單查恆等)/[23]⑤ fail-open(表缺席)兩情境
148
163
  // 在本機樹由 [8](登入憑證 Argon2id 派生)內登入 lane 的訊號面真行使(try/catch 異常面)——車道分流非帳面虛報。
@@ -151,7 +166,7 @@ async function A(name: string, cond: boolean | Promise<boolean>, detail = ''): P
151
166
  // for (const [k] of secRan) if (!secCases[k] && k !== 22) FAIL(負向斷言本體在 [25] 段
152
167
  //——secCases 全摘即 RED);反向缺席段=帳面誠實「案 0/N · skip」。
153
168
  // secCases 載體三群:[24]/[25] self-account+--only 段存在面+teeth(段帳由 secRan 執行自記)。
154
- const secCases: Record<number, number> = { 1: 3, 2: 7, 3: 9, 4: 4, 5: 3, 6: 20, 7: 27, 8: 8, 9: 22, 10: 5, 11: 41, 12: 50, 13: 39, 14: 18, 15: 59, 16: 15, 17: 14, 18: 39, 19: 17, 20: 47, 21: 10, 23: 18, 24: 5, 25: 4, 26: 8 };
169
+ const secCases: Record<number, number> = { 1: 3, 2: 7, 3: 9, 4: 4, 5: 3, 6: 20, 7: 27, 8: 8, 9: 22, 10: 5, 11: 41, 12: 65, 13: 39, 14: 18, 15: 59, 16: 15, 17: 14, 18: 39, 19: 17, 20: 47, 21: 10, 23: 18, 24: 5, 25: 4, 26: 8, 27: 14 };
155
170
  function parseOnly(spec: string): { err: string | null; set: number[] } {
156
171
  const set = new Set<number>();
157
172
  const specStr = spec.trim();
@@ -758,7 +773,7 @@ secOpen(12, '[12] 伺服器端原語(auth/ratelimit/hash)+ 本機鎖定(
758
773
  // UPSERT…RETURNING 於 wrangler 4.129.0 workerd 逐發回真值;併發首發兩發計數 1→2)。
759
774
  type RateRow = { ip: string; window_start: number; count: number };
760
775
  const RATE: RateWindow = { table: 'login_rate', windowMs: 60_000, max: 3 };
761
- const makeRateDb = (): {
776
+ const makeRateDb = (cr: typeof checkRate = checkRate): {
762
777
  db: {
763
778
  exec: (s: string) => void;
764
779
  prepare: (s: string) => { all: (...p: unknown[]) => Array<Record<string, unknown>>; get: (...p: unknown[]) => Record<string, unknown> | undefined };
@@ -783,7 +798,7 @@ const makeRateDb = (): {
783
798
  runRaw: (sql) => db.exec(sql),
784
799
  // 真 checkRate 直驅:?1/?2/?3 有名參數語意在 node:sqlite 與 workerd 一致(端對端探針實證,見卡 comments)。
785
800
  check: (ip) =>
786
- checkRate(
801
+ cr(
787
802
  {
788
803
  DB: {
789
804
  prepare: (sql: string) => ({
@@ -856,6 +871,120 @@ await A('rate:RETURNING 異形回應守衛 fail-open(results 空/alias 斷
856
871
  })());
857
872
  await A('rate:SQL_RATE_BUMP 常數形(UPSERT…RETURNING…count AS n;{table} 佔位)',
858
873
  /^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));
874
+ // 限流鍵正規化(t_b30cbc5f):checkRate 入口 normalizeRateIp——IPv6 聚合至 /64(尾段漂移不再每發新鍵)。
875
+ // v6 面謂詞以模組參數化:同一謂詞本體先對真模組(綠)、再對拷貝樹毒化模組(IPv6 聚合分支摘除)須逐案真翻紅。
876
+ type RateMod12 = { checkRate: typeof checkRate; normalizeRateIp: typeof normalizeRateIp };
877
+ const rateReal12: RateMod12 = { checkRate, normalizeRateIp };
878
+ const v6Cases12: Array<[string, (m: RateMod12) => Promise<boolean>]> = [
879
+ ['rate:IPv6 同 /64 尾段漂移兩地址(2001:db8:5678::1/2001:db8:5678:0:ffff:ffff:ffff:9)經真 checkRate 共用鍵 2001:db8:5678::/64 計數 2(裸地址不入表)', async (m) => {
880
+ const r = makeRateDb(m.checkRate);
881
+ await r.check('2001:db8:5678::1'); await r.check('2001:db8:5678:0:ffff:ffff:ffff:9');
882
+ return r.row('2001:db8:5678::/64')?.count === 2 && r.row('2001:db8:5678::1') === undefined;
883
+ }],
884
+ ['rate:IPv6 同 /64 輪換四個介面 ID → 三發 pass、第四發 reject(尾段漂移不再繞過 max)', async (m) => {
885
+ const r = makeRateDb(m.checkRate);
886
+ const seq: boolean[] = [];
887
+ for (const ip of ['2001:db8:77::1', '2001:db8:77::2', '2001:db8:77:0:a:b:c:d', '2001:db8:77::ffff']) seq.push(await r.check(ip));
888
+ return seq.join() === 'true,true,true,false';
889
+ }],
890
+ ['rate:IPv6 跨 /64 分鍵(2001:db8:5678::1/2001:db8:9999::1 → 兩 /64 鍵各計 1)', async (m) => {
891
+ const r = makeRateDb(m.checkRate);
892
+ await r.check('2001:db8:5678::1'); await r.check('2001:db8:9999::1');
893
+ return r.row('2001:db8:5678::/64')?.count === 1 && r.row('2001:db8:9999::/64')?.count === 1;
894
+ }],
895
+ ['rate:IPv6 同 /48 異 /64 分鍵(第 4 hextet 屬前綴:2001:db8:5678::1 → 2001:db8:5678::/64、2001:db8:5678:ffff::9 → 2001:db8:5678:ffff::/64 各計 1——聚合粒度恰 /64 非 /48)', async (m) => {
896
+ const r = makeRateDb(m.checkRate);
897
+ await r.check('2001:db8:5678::1'); await r.check('2001:db8:5678:ffff::9');
898
+ return r.row('2001:db8:5678::/64')?.count === 1 && r.row('2001:db8:5678:ffff::/64')?.count === 1;
899
+ }],
900
+ ['normalizeRateIp RFC 5952 形:2001:0DB8:0000:000A::1 → 2001:db8:0:a::/64(前導零摘、小寫、內部單零組不壓縮、尾零串 ::);2001:db8:a::1 → 2001:db8:a::/64 異鍵(零組不吞=前綴不碰撞)', async (m) =>
901
+ m.normalizeRateIp('2001:0DB8:0000:000A::1') === '2001:db8:0:a::/64' && m.normalizeRateIp('2001:db8:a::1') === '2001:db8:a::/64'],
902
+ ['normalizeRateIp 邊界:::1/0000:0000:0000:0000:1:2:3:4(全零前綴)/:: → 恆 ::/64(IPv4-mapped 位址另行回點分 IPv4,見下案)', async (m) =>
903
+ ['::1', '0000:0000:0000:0000:1:2:3:4', '::'].every((ip) => m.normalizeRateIp(ip) === '::/64')],
904
+ ['rate:normalizeRateIp IPv4-mapped 回點分 IPv4 鍵(/32 契約):::ffff:192.0.2.1 → 192.0.2.1;hex 尾組同主機 twin ::ffff:c000:201 同鍵 192.0.2.1(mapped 裸形絕不入表);兩台相異 mapped 主機經真 checkRate 各自鍵各計 1(不共 ::/64 桶)', async (m) => {
905
+ const r = makeRateDb(m.checkRate);
906
+ await r.check('::ffff:192.0.2.1'); await r.check('::ffff:c000:201'); await r.check('::ffff:192.0.2.2');
907
+ return m.normalizeRateIp('::ffff:192.0.2.1') === '192.0.2.1'
908
+ && m.normalizeRateIp('::ffff:c000:201') === '192.0.2.1'
909
+ && r.row('192.0.2.1')?.count === 2
910
+ && r.row('192.0.2.2')?.count === 1
911
+ && r.row('::ffff:192.0.2.1') === undefined && r.row('::ffff:c000:201') === undefined
912
+ && r.row('::/64') === undefined;
913
+ }],
914
+ ['normalizeRateIp zone id 剝除+輸出小寫+嵌入 IPv4 末組:fe80::1%eth0 → fe80::/64;FE80::ABCD → fe80::/64;1:2:3:4:5:6:1.2.3.4 → 1:2:3:4::/64', async (m) =>
915
+ m.normalizeRateIp('fe80::1%eth0') === 'fe80::/64' && m.normalizeRateIp('FE80::ABCD') === 'fe80::/64'
916
+ && m.normalizeRateIp('1:2:3:4:5:6:1.2.3.4') === '1:2:3:4::/64'],
917
+ ];
918
+ const plainCases12: Array<[string, (m: RateMod12) => Promise<boolean>]> = [
919
+ ['rate:IPv4 原樣鍵(192.0.2.1 經真 checkRate 鍵面=self、計數 1)', async (m) => {
920
+ const r = makeRateDb(m.checkRate);
921
+ await r.check('192.0.2.1');
922
+ return m.normalizeRateIp('192.0.2.1') === '192.0.2.1' && r.row('192.0.2.1')?.count === 1;
923
+ }],
924
+ ["rate:'unknown'/空串原樣鍵(經真 checkRate 照樣進表=可見面,不靜默丟鍵)", async (m) => {
925
+ const r = makeRateDb(m.checkRate);
926
+ await r.check('unknown'); await r.check('');
927
+ return m.normalizeRateIp('unknown') === 'unknown' && m.normalizeRateIp('') === ''
928
+ && r.row('unknown')?.count === 1 && r.row('')?.count === 1;
929
+ }],
930
+ ['normalizeRateIp 無法解析形原樣且零 throw(IPv4 五段/IPv6 單段 2001/缺組/雙 ::/:::/非 hex/9 組/:: 外 8 組/嵌入 IPv4 越界/方括號/前導空白/非字串)', async (m) => {
931
+ const bad = ['1.2.3.4.5', '2001', '2001:db8', '1::2::3', ':::', '2001:db8::g', '1:2:3:4:5:6:7:8:9', '1:2:3:4:5:6:7:8::', '::ffff:999.0.0.1', '[2001:db8::1]', ' 2001:db8::1'];
932
+ try {
933
+ return bad.every((ip) => m.normalizeRateIp(ip) === ip)
934
+ && (m.normalizeRateIp(undefined as unknown as string) as unknown) === undefined;
935
+ } catch { return false; }
936
+ }],
937
+ ];
938
+ for (const [name, pred] of [...v6Cases12, ...plainCases12]) await A(name, await pred(rateReal12));
939
+ await A('index barrel 匯出 normalizeRateIp(=ratelimit 子路徑同一函式——單一真相)',
940
+ (await import('../src/index.ts')).normalizeRateIp === normalizeRateIp);
941
+ // 毒化(拷貝樹 in-gate,[23] 母型):IPv6 聚合分支入口 `if (!g) return ip;` → `return ip;`(IPv6 一律原樣成鍵)
942
+ // → v6 面謂詞逐案真翻紅(false 非 crash)、非 v6 面照綠(毒面恰此面翻)。毒化跑在 /tmp 拷貝=本 repo 樹零接觸、樹出生即棄。
943
+ {
944
+ const gb12 = (id: string): unknown =>
945
+ (globalThis as unknown as { process?: { getBuiltinModule?: (i: string) => unknown } }).process?.getBuiltinModule?.(id);
946
+ const fs12 = gb12('node:fs') as {
947
+ mkdtempSync: (p: string) => string;
948
+ writeFileSync: (p: string, s: string, e: string) => void;
949
+ rmSync: (p: string, o?: unknown) => void;
950
+ existsSync: (p: string) => boolean;
951
+ } | undefined;
952
+ const os12 = gb12('node:os') as { tmpdir: () => string } | undefined;
953
+ const path12 = gb12('node:path') as { join: (...p: string[]) => string } | undefined;
954
+ const url12 = gb12('node:url') as { pathToFileURL: (p: string) => { href: string } } | undefined;
955
+ const needle12 = ' if (!g) return ip;\n';
956
+ const src12 = await srcOf('../src/server/ratelimit.ts');
957
+ const cnt12 = src12.split(needle12).length - 1;
958
+ await A('rate 毒化錨恰 1(IPv6 聚合分支入口單點承載)', cnt12 === 1, 'cnt=' + cnt12);
959
+ if (!fs12 || !os12 || !path12 || !url12) {
960
+ await A('rate 毒化面(原語缺席環境)=顯性 RED 行,毒化證據缺席顯形非 silent true', false, 'node fs/os/path/url 原語缺席');
961
+ } else if (cnt12 === 1) {
962
+ const tree12 = fs12.mkdtempSync(path12.join(os12.tmpdir(), 't12-ratekey-'));
963
+ const flipped12: string[] = [];
964
+ const held12: string[] = [];
965
+ let plainOk12 = false;
966
+ try {
967
+ fs12.writeFileSync(path12.join(tree12, 'ratelimit.ts'), src12.replace(needle12, ' return ip;\n'), 'utf8');
968
+ fs12.writeFileSync(path12.join(tree12, 'env.ts'), await srcOf('../src/server/env.ts'), 'utf8');
969
+ const modP12 = (await import(url12.pathToFileURL(path12.join(tree12, 'ratelimit.ts')).href)) as RateMod12;
970
+ for (const [name, pred] of v6Cases12) {
971
+ let res: string;
972
+ try { res = String(await pred(modP12)); } catch (e) { res = 'crash: ' + String((e as Error).message).slice(0, 80); }
973
+ (res === 'false' ? flipped12 : held12).push(name.slice(0, 40) + ' → ' + res);
974
+ }
975
+ plainOk12 = (await Promise.all(plainCases12.map(([, pred]) => pred(modP12)))).every((x) => x);
976
+ } catch (e) {
977
+ held12.push('poison import crash: ' + String((e as Error).message).slice(0, 120));
978
+ } finally {
979
+ fs12.rmSync(tree12, { recursive: true, force: true });
980
+ }
981
+ await A('rate 毒化(IPv6 聚合分支摘除)→ v6 面謂詞逐案真翻紅(' + v6Cases12.length + ' 案全 false、零 crash)',
982
+ held12.length === 0 && flipped12.length === v6Cases12.length,
983
+ 'flipped=' + flipped12.length + '/' + v6Cases12.length + (held12.length ? ';未翻:' + held12.join('|') : ''));
984
+ await A('rate 毒化面精準:非 v6 面照綠(IPv4/unknown/無效形——毒面恰 IPv6 聚合面翻)+毒化樹出生即棄(本 repo 樹零接觸)',
985
+ plainOk12 && !fs12.existsSync(tree12));
986
+ }
987
+ }
859
988
  await A('AUTH_RATE 契約(60s 窗 max 10)', AUTH_RATE.windowMs === 60_000 && AUTH_RATE.max === 10);
860
989
  const sessA = generateSessionToken();
861
990
  const sessB = generateSessionToken();
@@ -2525,7 +2654,7 @@ secOpen(21, '[21] CI 供應鏈面(npm ci --ignore-scripts+dependabot runner
2525
2654
  await A('[21] publish.yml:npm ci --ignore-scripts(tag 驅動無 actor 面;typecheck+verify 先行=腳本面零在場)',
2526
2655
  srcPy21.includes('npm ci --ignore-scripts'));
2527
2656
  } else {
2528
- await A('[21] CI 源碼面:.github 缺席環境(tarball 發行樹常態)=顯性 SKIP(workflow 斷言 4 收斂為 1 SKIP 行——消費端計數帳 489)',
2657
+ await A('[21] CI 源碼面:.github 缺席環境(tarball 發行樹常態)=顯性 SKIP(workflow 斷言 4 收斂為 1 SKIP 行——消費端計數帳 503)',
2529
2658
  true, 'no .github dir — consumption tree face');
2530
2659
  }
2531
2660
  // 摘除自證(t_760f44e8 治理掃蕩補漏;fs 原語缺席環境=顯性 FAIL 形自守衛——非 silent true)
@@ -2790,12 +2919,12 @@ secOpen(23, '[23] loginRouteCore ladder 查表守衛(外審 #8 幽靈帳——
2790
2919
  }
2791
2920
 
2792
2921
  };
2793
- // ── 26. 格式規格+測試向量(docs/format-spec.md+docs/vectors/*.json;0.3.0 批 A2) ──
2922
+ // ── 26. 格式規格+測試向量(docs/format-spec.md+docs/vectors/*.json) ──
2794
2923
  //
2795
2924
  // 單向契約:源碼 → scripts/generate-vectors.ts → JSON(凍結證據,commit 入 repo)→ 本段只檢存在+shape。
2796
2925
  // 本段不依賴產生器、不重算 KDF(向量含隨機 iv/鹽=重算即漂移);BIP39 是確定性轉寫,逐組對照現行原語。
2797
2926
  // teeth:payload 位元組長度=b64 解碼真值、fields 拼接=payload 逐位元組、布局長度=規格表列、
2798
- // 欄位名逐一在規格檔、凍結 blob 逐字=本閘 KAT 字面([14]/[17]/[20])、RFC 9106 期望值=argon2.ts 常數。
2927
+ // 欄位名限定形逐一在規格對應節、凍結 blob 逐字=本閘 KAT 字面([14]/[17]/[20])、RFC 9106 期望值+參數=argon2.ts 常數。
2799
2928
  secOpen(26, '[26] 格式規格+測試向量(docs/format-spec.md+docs/vectors 存在性+shape)'); if (secEnter(26)) {
2800
2929
  {
2801
2930
  const readJson26 = async (rel: string): Promise<Record<string, unknown> | null> => {
@@ -2836,16 +2965,37 @@ secOpen(26, '[26] 格式規格+測試向量(docs/format-spec.md+docs/vecto
2836
2965
  const head = v.layout === 'salt-embedded' ? v.fields[0]?.name === 'pinSalt' && v.fields[0]?.len === 16 : v.fields[0]?.name === 'iv';
2837
2966
  return want === v.payload_len && head && specRows26.some((l) => l.startsWith('| `' + v.prefix + '`') && l.includes('| ' + v.layout + ' | ' + lenCol + ' |'));
2838
2967
  }));
2839
- const KKEYS26 = ['rfc9106', 'frozen_blobs', 'credentials', 'expected_tag_hex', 'expected_note_key_hex', 'expected_hex', 'samples', 'entropy_hex', 'checksum_hex', 'words', 'source'];
2840
- await A('[26] 欄位名對規格:families 每筆欄名+fields 子欄(name/offset/len/hex)+kat/bip39 欄名,逐一以 `欄名` 形出現在 format-spec.md',
2841
- spec26.length > 0 && [...HEAD26, ...VKEYS26, 'name', 'offset', 'len', 'hex', ...KKEYS26].every((k) => spec26.includes('`' + k + '`'))
2842
- && KKEYS26.slice(0, 3).every((k) => !!kat26 && k in kat26) && !!bip26 && 'samples' in bip26);
2968
+ // 欄位名限定形(通用字 name/len/hex/source… 在規格檔隨處可見=裸字檢查無咬力):頂層欄=§7 頭表/§7.1/§7.2 對應節
2969
+ // 的表列 `| \`欄\` |`;巢狀欄=限定形 `父[].欄`/`rfc9106.欄` 逐字在規格檔。欄名集取自向量檔實際鍵(新增欄未入規格=紅)。
2970
+ const sec26 = (from: string, to: string): string => { const i = spec26.indexOf(from); const j = to ? spec26.indexOf(to, i + 1) : spec26.length; return i < 0 || j < 0 ? '' : spec26.slice(i, j); };
2971
+ const s7h26 = sec26('## 7. ', '### 7.1 '), s71_26 = sec26('### 7.1 ', '### 7.2 '), s72_26 = sec26('### 7.2 ', '### 7.3 '), s73_26 = sec26('### 7.3 ', '');
2972
+ const row26 = (sec: string, k: string): boolean => sec.includes('\n| `' + k + '` |');
2973
+ const keysOf26 = (xs: unknown): string[] => [...new Set((Array.isArray(xs) ? xs : []).flatMap((x) => (x && typeof x === 'object' ? Object.keys(x) : [])))];
2974
+ const fieldKeys26 = keysOf26(vecs26.flatMap((v) => (Array.isArray(v.fields) ? v.fields : [])));
2975
+ const rfcKeys26 = Object.keys((kat26?.rfc9106 ?? {}) as object);
2976
+ const frozenKeys26 = keysOf26(kat26?.frozen_blobs), credKeys26 = keysOf26(kat26?.credentials), sampleKeys26 = keysOf26(bip26?.samples);
2977
+ // 反空轉下限:實際鍵集須涵蓋閘面讀取的欄(鍵集取自資料=資料缺欄時下限擋住空集合真值)
2978
+ const floor26 = (ks: string[], need: string[]): boolean => need.every((k) => ks.includes(k));
2979
+ await A('[26] 欄位名對規格(限定形):頂層欄=§7 頭表/§7.1 vectors 表/§7.2 kat 表逐列;巢狀欄逐一以 `fields[].欄`/`rfc9106.欄`/`frozen_blobs[].欄`/`credentials[].欄`/`samples[].欄` 出現在對應節(欄名集取自向量檔實鍵+閘讀欄下限)',
2980
+ spec26.length > 0 && !!kat26 && !!bip26
2981
+ && HEAD26.every((k) => row26(s7h26, k)) && keysOf26(vecs26).every((k) => row26(s71_26, k)) && VKEYS26.every((k) => row26(s71_26, k))
2982
+ && ['rfc9106', 'frozen_blobs', 'credentials'].every((k) => k in kat26! && row26(s72_26, k)) && 'samples' in bip26! && s73_26.includes('`samples`')
2983
+ && floor26(fieldKeys26, ['name', 'offset', 'len', 'hex']) && fieldKeys26.every((k) => s71_26.includes('`fields[].' + k + '`'))
2984
+ && floor26(rfcKeys26, ['memory_kib', 'iterations', 'parallelism', 'tag_len', 'password_hex', 'salt_hex', 'expected_tag_hex']) && rfcKeys26.every((k) => s72_26.includes('`rfc9106.' + k + '`'))
2985
+ && floor26(frozenKeys26, ['prefix', 'wire', 'payload_len', 'expected_note_key_hex', 'source']) && frozenKeys26.every((k) => s72_26.includes('`frozen_blobs[].' + k + '`'))
2986
+ && floor26(credKeys26, ['kind', 'expected_hex']) && credKeys26.every((k) => s72_26.includes('`credentials[].' + k + '`'))
2987
+ && floor26(sampleKeys26, ['entropy_hex', 'checksum_hex', 'words', 'source']) && sampleKeys26.every((k) => s73_26.includes('`samples[].' + k + '`')));
2843
2988
  const gate26 = await srcOf('./verify-core-crypto.ts');
2844
2989
  const frozen26 = (Array.isArray(kat26?.frozen_blobs) ? kat26!.frozen_blobs : []) as { prefix: string; wire: string; payload_len: number; expected_note_key_hex: string; source: string }[];
2845
2990
  const rfc26 = (kat26?.rfc9106 ?? {}) as Record<string, unknown>;
2846
2991
  const creds26 = (Array.isArray(kat26?.credentials) ? kat26!.credentials : []) as { kind: string; expected_hex: string }[];
2847
- await A('[26] kat 對帳:RFC 9106 expected_tag_hex=ARGON_RFC9106_EXPECTED(argon2.ts 常數);凍結 blob 7 筆逐字=本閘 KAT 字面([14]/[17]/[20])且 payload_len=解碼真長;憑證樣本 5 筆 hex64',
2848
- (await import('../src/client/argon2.ts')).ARGON_RFC9106_EXPECTED === rfc26.expected_tag_hex
2992
+ const argon26 = await import('../src/client/argon2.ts');
2993
+ const kp26 = argon26.ARGON_RFC9106_PARAMS;
2994
+ await A('[26] kat 對帳:RFC 9106 expected_tag_hex=ARGON_RFC9106_EXPECTED、參數 m/t/p/tag/pwd/salt=ARGON_RFC9106_PARAMS(argon2.ts 常數——verifyArgonKat/產生器同源);凍結 blob 7 筆逐字=本閘 KAT 字面([14]/[17]/[20])且 payload_len=解碼真長;憑證樣本 5 筆 hex64',
2995
+ argon26.ARGON_RFC9106_EXPECTED === rfc26.expected_tag_hex
2996
+ && rfc26.memory_kib === kp26.memoryKib && rfc26.iterations === kp26.iterations && rfc26.parallelism === kp26.parallelism
2997
+ && rfc26.tag_len === kp26.tagLen && kp26.tagLen === argon26.ARGON_TAG_LEN
2998
+ && rfc26.password_hex === kp26.passwordByte.toString(16).padStart(2, '0').repeat(kp26.passwordLen) && rfc26.salt_hex === '00'.repeat(kp26.saltLen)
2849
2999
  && frozen26.length === 7 && frozen26.every((b) => gate26.includes("'" + b.wire + "'") && b.wire.startsWith(b.prefix)
2850
3000
  && unb64Mod(b.wire.slice(b.prefix.length)).length === b.payload_len && /^[0-9a-f]{64}$/.test(b.expected_note_key_hex))
2851
3001
  && creds26.length === 5 && creds26.every((c) => /^[0-9a-f]{64}$/.test(c.expected_hex)));
@@ -2866,6 +3016,309 @@ secOpen(26, '[26] 格式規格+測試向量(docs/format-spec.md+docs/vecto
2866
3016
  }
2867
3017
  }
2868
3018
 
3019
+ // ── 27. 高階 Vault API(src/client/vault.ts+argon-auto.ts+argon-worker.ts+vendor hash-wasm) ──
3020
+ //
3021
+ // 行為面(對真模組):create→encrypt→decrypt 三資料形 roundtrip、AAD=`<app>:<recordId>` 以低階原語
3022
+ // 手解對帳、unlockVault 新前綴(jr4w.)/舊前綴(jr3w. Argon、jr1w. PBKDF2)、upgrade 同鑰驗證重包、
3023
+ // changePassphrase 週期(含 PIN jr4d. 進出)、復原套件(jr1r.)、VaultError 毒化(搬列 AAD/竄改/
3024
+ // 形不符/配置缺欄)、載體覆寫顯形(setArgonLoader 注入優先+錯值載體 KAT 拒用)、真 Worker 載體
3025
+ //(node worker_threads 轉接層跑 argonWorkerSource 原文——瀏覽器 blob 形同源碼)、回退鏈(Worker 失效→
3026
+ // inline+onFallback;無回退鏈=KDF_UNSUPPORTED;派生期載體失效歸因不誤報 WRAP_MISMATCH)、vendor 孿生
3027
+ //(去縮排原文 sha256=檔內常數=閘釘選值)、零 runtime deps+exports 子路徑+分層單向。
3028
+ // 收尾恆 resetArgonCarrier()+setArgonLoader(null)——後續段零載體殘留。
3029
+ secOpen(27, '[27] 高階 Vault API(roundtrip 三資料形/新舊前綴解鎖/upgrade/換密語/復原/VaultError 毒化/載體覆寫+Worker 回退鏈/vendor 孿生)'); if (secEnter(27)) {
3030
+ {
3031
+ const codeOf27 = async (p: Promise<unknown>): Promise<string> => {
3032
+ try { await p; return 'OK'; } catch (e) { return e instanceof VaultError ? e.code : 'NON-VAULT:' + String((e as Error)?.message ?? e); }
3033
+ };
3034
+ const fam27 = (h: { family: unknown }): string | null => h.family as string | null; // getter 讀值(禁 TS 跨 await 窄化)
3035
+ const bytesEq27 = (a: unknown, b: Uint8Array): boolean =>
3036
+ a instanceof Uint8Array && a.length === b.length && a.every((x, i) => x === b[i]);
3037
+ const CFG27: VaultOptions = {
3038
+ app: 'tacet-vault27',
3039
+ cipher: 'jr1v.',
3040
+ argon: { wrap4: 'jr4w.', wrap3: 'jr3w.', wrapDual4: 'jr4d.', wrapDual3: 'jr3d.', pinSalt3Prefix: 'tacet-note-pin3:' },
3041
+ legacy: { wrap: 'jr1w.', wrapDual: 'jr2w.', pinSaltPrefix: 'tacet-note-pin1:' },
3042
+ recovery: { wrapRec: 'jr1r.', recKekHkdf: 'tacet-vault27' },
3043
+ };
3044
+ const PASS27 = 'correct horse battery 27';
3045
+ try {
3046
+ // ① roundtrip 三資料形
3047
+ const c27 = await createVault(PASS27, CFG27);
3048
+ const v27 = c27.vault;
3049
+ const str27 = '默·日記 — 🌙 line\nbreak';
3050
+ const obj27 = { title: '標題', tags: ['a', 'b'], n: 3, ok: true, nil: null, nested: { x: [1, { y: 'z' }] } };
3051
+ const bytes27 = new Uint8Array([0, 1, 2, 254, 255, 0, 128]);
3052
+ const bS27 = await v27.encrypt('rec-s', str27);
3053
+ const bJ27 = await v27.encrypt('rec-j', obj27);
3054
+ const bB27 = await v27.encrypt('rec-b', bytes27);
3055
+ const bE27 = await v27.encrypt('rec-e', new Uint8Array(0));
3056
+ const bES27 = await v27.encrypt('rec-es', '');
3057
+ await A('[27] create→encrypt→decrypt roundtrip 三資料形(字串含 Unicode/JSON 巢狀物件/Uint8Array 含 0x00+空陣列+空字串)還原同型;blob 前綴=cfg.cipher;同明文兩次密文異(隨機 iv)',
3058
+ [bS27, bJ27, bB27, bE27, bES27].every((b) => b.startsWith('jr1v.'))
3059
+ && (await v27.decrypt('rec-s', bS27)) === str27
3060
+ && JSON.stringify(await v27.decrypt('rec-j', bJ27)) === JSON.stringify(obj27)
3061
+ && bytesEq27(await v27.decrypt('rec-b', bB27), bytes27)
3062
+ && bytesEq27(await v27.decrypt('rec-e', bE27), new Uint8Array(0))
3063
+ && (await v27.decrypt('rec-es', bES27)) === ''
3064
+ && (await v27.encrypt('rec-s', str27)) !== bS27);
3065
+
3066
+ // ② 寫面形+AAD 域(低階原語手解對帳)
3067
+ const rec27 = c27.serverRecord;
3068
+ const nk27 = await unwrapNoteKey4(CFG27.argon, rec27.wrapped, PASS27, rec27.salt);
3069
+ const openRaw27 = async (key: CryptoKey | null, blob: string, aad: string): Promise<Uint8Array | null> => {
3070
+ if (!key) return null;
3071
+ try {
3072
+ const p = unb64Mod(blob.slice(5));
3073
+ return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv: p.slice(0, 12) as BufferSource, additionalData: enc.encode(aad) as BufferSource }, key, p.slice(12) as BufferSource));
3074
+ } catch { return null; }
3075
+ };
3076
+ const rawS27 = await openRaw27(nk27, bS27, 'tacet-vault27:rec-s');
3077
+ const kit27 = c27.recovery;
3078
+ await A('[27] 寫面形:serverRecord jr4w.+salt hex32、family wrap4、needsUpgrade false、carrier node;低階 unwrapNoteKey4 解得同鑰——AAD=`<app>:<recordId>`(tag 0x73+UTF-8 本體)、裸 recordId/他 app 域恆解不開;復原套件 recToken hex64+wrappedRec jr1r.+recTokenHash=SHA-256(recToken)',
3079
+ rec27.wrapped.startsWith('jr4w.') && /^[0-9a-f]{32}$/.test(rec27.salt) && v27.family === 'wrap4' && v27.needsUpgrade() === false && v27.carrier === 'node'
3080
+ && !!rawS27 && rawS27[0] === 0x73 && new TextDecoder().decode(rawS27.subarray(1)) === str27
3081
+ && (await openRaw27(nk27, bS27, 'rec-s')) === null && (await openRaw27(nk27, bS27, 'other-app:rec-s')) === null
3082
+ && !!kit27 && /^[0-9a-f]{64}$/.test(kit27.recToken) && kit27.wrappedRec.startsWith('jr1r.') && kit27.recTokenHash === await sha(kit27.recToken));
3083
+
3084
+ // ③ unlockVault 新前綴
3085
+ const u27 = await unlockVault(PASS27, rec27, CFG27);
3086
+ await A('[27] unlockVault 新前綴(jr4w.):另一 handle 解前 handle 三資料形密文;family wrap4、needsUpgrade false',
3087
+ u27.family === 'wrap4' && !u27.needsUpgrade()
3088
+ && (await u27.decrypt('rec-s', bS27)) === str27 && JSON.stringify(await u27.decrypt('rec-j', bJ27)) === JSON.stringify(obj27)
3089
+ && bytesEq27(await u27.decrypt('rec-b', bB27), bytes27));
3090
+
3091
+ // ④ unlockVault 舊前綴(jr3w. Argon/jr1w. PBKDF2)——同一 noteKey 兩包
3092
+ const OLD27 = 'old pass 27';
3093
+ const nkOld27 = await generateNoteKey();
3094
+ const r3_27 = await wrapNoteKey3(CFG27.argon, nkOld27, OLD27);
3095
+ const r1_27 = await wrapNoteKey(TACET, nkOld27, OLD27);
3096
+ const u3_27 = await unlockVault(OLD27, r3_27, CFG27);
3097
+ const u1_27 = await unlockVault(OLD27, r1_27, CFG27);
3098
+ const bOld27 = await u3_27.encrypt('rec-old', 'old-era');
3099
+ await A('[27] unlockVault 舊前綴各一:jr3w.(family wrap3)/jr1w.(family wrap,PBKDF2 舊世代讀面=carrier null 不需 Argon)皆 needsUpgrade true;兩 handle 同鑰互解+低階 noteKey 手解同 AAD 域',
3100
+ u3_27.family === 'wrap3' && u1_27.family === 'wrap' && u3_27.needsUpgrade() && u1_27.needsUpgrade() && u1_27.carrier === null && u3_27.carrier === 'node'
3101
+ && (await u1_27.decrypt('rec-old', bOld27)) === 'old-era'
3102
+ && new TextDecoder().decode((await openRaw27(nkOld27, bOld27, 'tacet-vault27:rec-old'))?.subarray(1)) === 'old-era');
3103
+
3104
+ // ⑤ upgrade:錯密語拒(狀態不動)→ 正確密語重包 jr4w.;舊包裹仍可解(舊前綴語意不動);雙因子 jr3d. → jr4d.
3105
+ const upWrong27 = await codeOf27(u3_27.upgrade('not the pass'));
3106
+ const stillOld27 = u3_27.family === 'wrap3' && u3_27.serverRecord?.wrapped === r3_27.wrapped;
3107
+ const up27 = await u3_27.upgrade(OLD27);
3108
+ const uUp27 = await unlockVault(OLD27, up27, CFG27);
3109
+ const uOld27b = await unlockVault(OLD27, r3_27, CFG27);
3110
+ const upIdem27 = await u3_27.upgrade('ignored when current');
3111
+ const r3d27 = await wrapNoteKeyDual3(CFG27.argon, nkOld27, OLD27, '2468');
3112
+ const uD27 = await unlockVault(OLD27, r3d27, { ...CFG27, pin: '2468' });
3113
+ const dualBefore27 = fam27(uD27) === 'wrapDual3' && uD27.needsUpgrade(); // 升級前快照(斷言式在全部動作後求值)
3114
+ const upDualNoPin27 = await codeOf27(uD27.upgrade(OLD27));
3115
+ const upD27 = await uD27.upgrade(OLD27, { pin: '2468' });
3116
+ await A('[27] needsUpgrade/upgrade 真值行:錯密語=ERR_VAULT_WRAP_MISMATCH 且狀態不動;正確=新前綴 jr4w. 包同一 noteKey(新包裹解舊密文)、needsUpgrade 翻 false、已最新再呼=原包裹欄原樣;舊 jr3w. 包裹照解;jr3d. 缺 PIN=ERR_VAULT_PIN_REQUIRED、帶 PIN → jr4d.(family wrapDual4)',
3117
+ upWrong27 === 'ERR_VAULT_WRAP_MISMATCH' && stillOld27
3118
+ && up27.wrapped.startsWith('jr4w.') && fam27(u3_27) === 'wrap4' && !u3_27.needsUpgrade()
3119
+ && (await uUp27.decrypt('rec-old', bOld27)) === 'old-era' && (await uOld27b.decrypt('rec-old', bOld27)) === 'old-era'
3120
+ && upIdem27.wrapped === up27.wrapped
3121
+ && dualBefore27 && upDualNoPin27 === 'ERR_VAULT_PIN_REQUIRED'
3122
+ && upD27.wrapped.startsWith('jr4d.') && fam27(uD27) === 'wrapDual4' && !uD27.needsUpgrade());
3123
+
3124
+ // ⑥ changePassphrase 週期(含 PIN 進出)
3125
+ const NEW27 = 'new pass 27';
3126
+ const cp27 = await v27.changePassphrase(NEW27);
3127
+ const uNew27 = await unlockVault(NEW27, cp27, CFG27);
3128
+ const oldOnNew27 = await codeOf27(unlockVault(PASS27, cp27, CFG27));
3129
+ const oldRecStill27 = await unlockVault(PASS27, rec27, CFG27);
3130
+ const cpD27 = await v27.changePassphrase(NEW27, { pin: '1357' });
3131
+ const famD27 = v27.family;
3132
+ const noPin27 = await codeOf27(unlockVault(NEW27, cpD27, CFG27));
3133
+ const uPin27 = await unlockVault(NEW27, cpD27, { ...CFG27, pin: '1357' });
3134
+ const cpNoPin27 = await codeOf27(v27.changePassphrase(NEW27));
3135
+ const cpBack27 = await v27.changePassphrase(NEW27, { pin: null });
3136
+ await A('[27] changePassphrase 週期:同一 noteKey 重包 jr4w.(新密語解舊密文、舊密語對新包裹=WRAP_MISMATCH、舊包裹欄不被本 API 觸及);pin 字串→jr4d.(無 PIN 解鎖=PIN_REQUIRED、帶 PIN 解);雙因子省略 pin=PIN_REQUIRED;pin:null 回 jr4w.;空密語=BAD_PASSPHRASE、空白 PIN=BAD_PIN',
3137
+ cp27.wrapped.startsWith('jr4w.') && cp27.wrapped !== rec27.wrapped && (await uNew27.decrypt('rec-s', bS27)) === str27
3138
+ && oldOnNew27 === 'ERR_VAULT_WRAP_MISMATCH' && (await oldRecStill27.decrypt('rec-s', bS27)) === str27
3139
+ && cpD27.wrapped.startsWith('jr4d.') && famD27 === 'wrapDual4' && noPin27 === 'ERR_VAULT_PIN_REQUIRED'
3140
+ && (await uPin27.decrypt('rec-j', bJ27) as { title?: string }).title === '標題'
3141
+ && cpNoPin27 === 'ERR_VAULT_PIN_REQUIRED' && cpBack27.wrapped.startsWith('jr4w.') && fam27(v27) === 'wrap4'
3142
+ && (await codeOf27(v27.changePassphrase(''))) === 'ERR_VAULT_BAD_PASSPHRASE'
3143
+ && (await codeOf27(v27.changePassphrase(NEW27, { pin: ' ' }))) === 'ERR_VAULT_BAD_PIN');
3144
+
3145
+ // ⑦ VaultError 毒化(記錄密文面)
3146
+ const tamper27 = (() => { const p = unb64Mod(bS27.slice(5)); p[p.length - 1] ^= 0x01; return 'jr1v.' + modB64(p); })();
3147
+ const errObj27 = await (async () => { try { await v27.decrypt('rec-OTHER', bS27); return null; } catch (e) { return e; } })();
3148
+ const cyc27: Record<string, unknown> = {}; cyc27.self = cyc27;
3149
+ await A('[27] VaultError 毒化(記錄面):錯 recordId(AAD 搬列)/末位元翻轉竄改/他 vault 金鑰=ERR_VAULT_DECRYPT;他前綴/壞 base64/過短=ERR_VAULT_BAD_BLOB;空 recordId=BAD_RECORD_ID;undefined/ArrayBuffer/Uint16Array/BigInt/循環物件=BAD_DATA;錯誤物件 instanceof VaultError+name+isVaultError 分流',
3150
+ errObj27 instanceof VaultError && (errObj27 as VaultError).name === 'VaultError' && isVaultError(errObj27, 'ERR_VAULT_DECRYPT') && !isVaultError(errObj27, 'ERR_VAULT_BAD_BLOB')
3151
+ && (await codeOf27(v27.decrypt('rec-s', tamper27))) === 'ERR_VAULT_DECRYPT'
3152
+ && (await codeOf27(u3_27.decrypt('rec-s', bS27))) === 'ERR_VAULT_DECRYPT'
3153
+ && (await codeOf27(v27.decrypt('rec-s', 'jr1b.' + bS27.slice(5)))) === 'ERR_VAULT_BAD_BLOB'
3154
+ && (await codeOf27(v27.decrypt('rec-s', 'jr1v.!!not-base64!!'))) === 'ERR_VAULT_BAD_BLOB'
3155
+ && (await codeOf27(v27.decrypt('rec-s', 'jr1v.' + modB64(new Uint8Array(20))))) === 'ERR_VAULT_BAD_BLOB'
3156
+ && (await codeOf27(v27.encrypt('', 'x'))) === 'ERR_VAULT_BAD_RECORD_ID'
3157
+ && (await codeOf27(v27.decrypt('', bS27))) === 'ERR_VAULT_BAD_RECORD_ID'
3158
+ && (await codeOf27(v27.encrypt('r', undefined as never))) === 'ERR_VAULT_BAD_DATA'
3159
+ && (await codeOf27(v27.encrypt('r', new ArrayBuffer(4) as never))) === 'ERR_VAULT_BAD_DATA'
3160
+ && (await codeOf27(v27.encrypt('r', new Uint16Array(2) as never))) === 'ERR_VAULT_BAD_DATA'
3161
+ && (await codeOf27(v27.encrypt('r', 10n as never))) === 'ERR_VAULT_BAD_DATA'
3162
+ && (await codeOf27(v27.encrypt('r', cyc27 as never))) === 'ERR_VAULT_BAD_DATA');
3163
+
3164
+ // ⑧ 配置/serverRecord 拒(opt-in 未配置即拒、不猜)
3165
+ const noW4_27 = { ...CFG27, argon: { ...CFG27.argon, wrap4: undefined } };
3166
+ await A('[27] VaultError 拒(配置/包裹欄面):serverRecord 缺 salt/缺 wrapped/salt 大寫/null=BAD_RECORD;未知前綴=WRAP_UNKNOWN;缺 argon.wrap4/缺 app/缺 cipher/前綴互為前綴/recovery 半配置/PIN 寫面未配置=ERR_VAULT_CONFIG;空密語=BAD_PASSPHRASE',
3167
+ (await codeOf27(unlockVault(PASS27, { wrapped: rec27.wrapped } as never, CFG27))) === 'ERR_VAULT_BAD_RECORD'
3168
+ && (await codeOf27(unlockVault(PASS27, { salt: rec27.salt } as never, CFG27))) === 'ERR_VAULT_BAD_RECORD'
3169
+ && (await codeOf27(unlockVault(PASS27, { wrapped: rec27.wrapped, salt: rec27.salt.toUpperCase() }, CFG27))) === 'ERR_VAULT_BAD_RECORD'
3170
+ && (await codeOf27(unlockVault(PASS27, null as never, CFG27))) === 'ERR_VAULT_BAD_RECORD'
3171
+ && (await codeOf27(unlockVault(PASS27, { wrapped: 'jr9w.' + rec27.wrapped.slice(5), salt: rec27.salt }, CFG27))) === 'ERR_VAULT_WRAP_UNKNOWN'
3172
+ && (await codeOf27(createVault(PASS27, noW4_27))) === 'ERR_VAULT_CONFIG'
3173
+ && (await codeOf27(createVault(PASS27, { ...CFG27, app: '' }))) === 'ERR_VAULT_CONFIG'
3174
+ && (await codeOf27(createVault(PASS27, { ...CFG27, cipher: '' }))) === 'ERR_VAULT_CONFIG'
3175
+ && (await codeOf27(createVault(PASS27, { ...CFG27, cipher: 'jr4' }))) === 'ERR_VAULT_CONFIG'
3176
+ && (await codeOf27(createVault(PASS27, { ...CFG27, recovery: { wrapRec: 'jr1r.', recKekHkdf: '' } }))) === 'ERR_VAULT_CONFIG'
3177
+ && (await codeOf27(createVault(PASS27, { ...CFG27, argon: { wrap4: 'jr4w.' }, pin: '1234' }))) === 'ERR_VAULT_CONFIG'
3178
+ && (await codeOf27(createVault('', CFG27))) === 'ERR_VAULT_BAD_PASSPHRASE');
3179
+
3180
+ // ⑨ 復原套件
3181
+ const rv27 = await recoverVault(kit27!.recToken, kit27!.wrappedRec, CFG27);
3182
+ const rvFamNull27 = rv27.family === null && rv27.serverRecord === null && rv27.carrier === null && !rv27.needsUpgrade();
3183
+ const rvUp27 = await codeOf27(rv27.upgrade(PASS27));
3184
+ const rvCp27 = await rv27.changePassphrase('recovered 27');
3185
+ const rvU27 = await unlockVault('recovered 27', rvCp27, CFG27);
3186
+ const wrongTok27 = (kit27!.recToken[0] === 'a' ? 'b' : 'a') + kit27!.recToken.slice(1);
3187
+ await A('[27] 復原套件:recoverVault(recToken, wrappedRec) 解舊密文;family/serverRecord/carrier 皆 null(HKDF 不需 Argon)、upgrade=BAD_RECORD、changePassphrase 設新密語後 unlock 可解;錯 recToken/非 hex64/他前綴=ERR_VAULT_RECOVERY;未配置 recovery=ERR_VAULT_CONFIG',
3188
+ (await rv27.decrypt('rec-s', bS27)) === str27 && rvFamNull27 && rvUp27 === 'ERR_VAULT_BAD_RECORD'
3189
+ && rvCp27.wrapped.startsWith('jr4w.') && (await rvU27.decrypt('rec-b', bB27) as Uint8Array).length === bytes27.length
3190
+ && (await codeOf27(recoverVault(wrongTok27, kit27!.wrappedRec, CFG27))) === 'ERR_VAULT_RECOVERY'
3191
+ && (await codeOf27(recoverVault('xyz', kit27!.wrappedRec, CFG27))) === 'ERR_VAULT_RECOVERY'
3192
+ && (await codeOf27(recoverVault(kit27!.recToken, 'jr1w.' + kit27!.wrappedRec.slice(5), CFG27))) === 'ERR_VAULT_RECOVERY'
3193
+ && (await codeOf27(recoverVault(kit27!.recToken, kit27!.wrappedRec, { ...CFG27, recovery: undefined }))) === 'ERR_VAULT_CONFIG');
3194
+
3195
+ // ⑩ 載體覆寫顯形(setArgonLoader 注入恆優先;錯值載體 KAT 拒用)
3196
+ let cnt27 = 0;
3197
+ const fac27 = hashWasmArgon2Factory();
3198
+ const countLoader27 = () => { cnt27++; return Promise.resolve(fac27); };
3199
+ setArgonLoader(countLoader27);
3200
+ const cInj27 = await createVault(PASS27, { ...CFG27, recovery: undefined });
3201
+ const injKind27 = cInj27.vault.carrier;
3202
+ const injCur27 = currentArgonCarrier();
3203
+ const uFromNode27 = await unlockVault(PASS27, rec27, CFG27); // 注入載體解 node 載體所寫
3204
+ setArgonLoader(null);
3205
+ const uFromInj27 = await unlockVault(PASS27, cInj27.serverRecord, CFG27); // node 載體解注入載體所寫
3206
+ const zeroLoader27 = () => Promise.resolve({ argon2id: (async () => new Uint8Array(32)) as never });
3207
+ setArgonLoader(zeroLoader27);
3208
+ const badKat27 = await codeOf27(createVault(PASS27, CFG27));
3209
+ setArgonLoader(null);
3210
+ await A('[27] 載體覆寫顯形:setArgonLoader 注入=carrier/currentArgonCarrier 皆 injected 且注入載體實被呼叫;注入↔node 互解(逐位元一致);撤注入回 node;錯值載體(恆零輸出)=RFC 9106 KAT 拒用 ERR_VAULT_KDF_UNSUPPORTED(zero-fallback:不降級)',
3211
+ injKind27 === 'injected' && injCur27 === 'injected' && cnt27 > 0
3212
+ && (await uFromNode27.decrypt('rec-s', bS27)) === str27 && uFromInj27.family === 'wrap4' && uFromInj27.carrier === 'node'
3213
+ && currentArgonCarrier() === 'node' && badKat27 === 'ERR_VAULT_KDF_UNSUPPORTED');
3214
+
3215
+ // ⑪ 真 Worker 載體(node worker_threads 轉接層跑 argonWorkerSource 原文)
3216
+ const wt27 = (globalThis as unknown as { process?: { getBuiltinModule?: (id: string) => unknown } }).process?.getBuiltinModule?.('node:worker_threads') as
3217
+ { Worker?: new (src: string, o: { eval: boolean }) => { postMessage(m: unknown, t?: unknown): void; terminate(): Promise<number>; on(ev: string, f: (x: unknown) => void): void } } | undefined;
3218
+ if (!wt27?.Worker) {
3219
+ await A('[27] 真 Worker 載體(worker_threads 缺席環境)=顯性 FAIL 行(非 silent true)', false, 'node:worker_threads 缺席');
3220
+ } else {
3221
+ const SHIM27 = "const { parentPort } = require('node:worker_threads'); globalThis.self = { postMessage: (m, t) => parentPort.postMessage(m, t), set onmessage(f) { parentPort.on('message', (d) => f({ data: d })); } };\n";
3222
+ let made27 = 0;
3223
+ const nodeWorker27 = (): ArgonWorkerLike => {
3224
+ made27++;
3225
+ const w = new wt27.Worker!(SHIM27 + argonWorkerSource(), { eval: true });
3226
+ const like: ArgonWorkerLike = { postMessage: (m, t) => w.postMessage(m, t), terminate: () => { void w.terminate(); }, onmessage: null, onerror: null };
3227
+ w.on('message', (d) => like.onmessage?.({ data: d }));
3228
+ w.on('error', (e) => like.onerror?.(e));
3229
+ return like;
3230
+ };
3231
+ const wOpts27 = { chain: ['worker', 'inline'] as const, createWorker: nodeWorker27 };
3232
+ const cW27 = await createVault(PASS27, { ...CFG27, recovery: undefined, carrier: wOpts27 });
3233
+ const wKind27 = cW27.vault.carrier;
3234
+ const wCur27 = currentArgonCarrier();
3235
+ const uW27 = await unlockVault(PASS27, rec27, { ...CFG27, carrier: wOpts27 }); // Worker 解 node 所寫
3236
+ resetArgonCarrier();
3237
+ const afterReset27 = currentArgonCarrier();
3238
+ const uWn27 = await unlockVault(PASS27, cW27.serverRecord, CFG27); // node 解 Worker 所寫
3239
+ await A('[27] 真 Worker 載體:chain [worker,inline]+worker_threads 轉接 argonWorkerSource 原文=carrier worker(單一 Worker 重用);Worker↔node 互解逐位元一致;resetArgonCarrier 撤自裝載體回 node',
3240
+ wKind27 === 'worker' && wCur27 === 'worker' && made27 === 1 && uW27.carrier === 'worker'
3241
+ && (await uW27.decrypt('rec-s', bS27)) === str27 && uWn27.family === 'wrap4' && uWn27.carrier === 'node' && afterReset27 === 'node');
3242
+ }
3243
+
3244
+ // ⑫ 回退鏈+失效歸因
3245
+ const events27: { from: string; to: string }[] = [];
3246
+ const broken27 = (): ArgonWorkerLike => { throw new Error('CSP worker-src blocked (simulated)'); };
3247
+ const fb27 = await unlockVault(PASS27, rec27, { ...CFG27, carrier: { chain: ['worker', 'inline'], createWorker: broken27, onFallback: (e) => events27.push(e) } });
3248
+ const fbKind27 = fb27.carrier;
3249
+ const fbCur27 = currentArgonCarrier();
3250
+ resetArgonCarrier();
3251
+ const noFb27 = await codeOf27(unlockVault(PASS27, rec27, { ...CFG27, carrier: { chain: ['worker'], createWorker: broken27 } }));
3252
+ resetArgonCarrier();
3253
+ // 健康 KAT(32 KiB)、64 MiB 派生即 ok:false 的 flaky Worker:無回退=KDF_UNSUPPORTED(非 WRAP_MISMATCH);有回退=成功+事件
3254
+ const flaky27 = (): ArgonWorkerLike => {
3255
+ const fac = hashWasmArgon2Factory();
3256
+ const like: ArgonWorkerLike = {
3257
+ postMessage: (m) => {
3258
+ const q = m as { id: number; memorySize: number; password: string | Uint8Array; salt: Uint8Array; iterations: number; parallelism: number; hashLength: number };
3259
+ void (async () => {
3260
+ if (q.memorySize > 1024) { like.onmessage?.({ data: { id: q.id, ok: false, error: 'OOM (simulated)' } }); return; }
3261
+ const out = await fac.argon2id({ ...q, outputType: 'binary' }) as Uint8Array;
3262
+ like.onmessage?.({ data: { id: q.id, ok: true, out } });
3263
+ })();
3264
+ },
3265
+ terminate: () => {},
3266
+ onmessage: null,
3267
+ onerror: null,
3268
+ };
3269
+ return like;
3270
+ };
3271
+ const attrib27 = await codeOf27(unlockVault(PASS27, rec27, { ...CFG27, carrier: { chain: ['worker'], createWorker: flaky27 } }));
3272
+ resetArgonCarrier();
3273
+ const events27b: unknown[] = [];
3274
+ const fl27 = await unlockVault(PASS27, rec27, { ...CFG27, carrier: { chain: ['worker', 'inline'], createWorker: flaky27, onFallback: (e) => events27b.push(e) } });
3275
+ const flKind27 = fl27.carrier === 'worker' ? currentArgonCarrier() : fl27.carrier;
3276
+ resetArgonCarrier();
3277
+ const absent27 = await codeOf27(unlockVault(PASS27, rec27, { ...CFG27, carrier: { chain: ['worker'] } })); // node 無全域 Worker=載體缺席
3278
+ const emptyChain27 = await codeOf27(createVault(PASS27, { ...CFG27, carrier: { chain: [] } }));
3279
+ resetArgonCarrier();
3280
+ await A('[27] 回退鏈顯形:Worker 建構失敗→inline(onFallback 1 次 worker→inline、carrier 翻 inline);chain 無 inline=ERR_VAULT_KDF_UNSUPPORTED;派生期 Worker 失效(KAT 過、64 MiB 失敗)無回退=KDF_UNSUPPORTED 不誤報 WRAP_MISMATCH、有回退=成功+事件;載體缺席(node 無全域 Worker、chain [worker])=KDF_UNSUPPORTED;空 chain=CONFIG',
3281
+ fbKind27 === 'inline' && fbCur27 === 'inline' && events27.length === 1 && events27[0].from === 'worker' && events27[0].to === 'inline'
3282
+ && noFb27 === 'ERR_VAULT_KDF_UNSUPPORTED'
3283
+ && attrib27 === 'ERR_VAULT_KDF_UNSUPPORTED'
3284
+ && (await fl27.decrypt('rec-s', bS27)) === str27 && flKind27 === 'inline' && events27b.length === 1
3285
+ && absent27 === 'ERR_VAULT_KDF_UNSUPPORTED' && emptyChain27 === 'ERR_VAULT_CONFIG');
3286
+ } catch (e) {
3287
+ await A('[27] 行為面未預期拋出(應全數經 VaultError 分流或斷言承載)', false, String((e as Error)?.stack ?? e).slice(0, 400));
3288
+ } finally {
3289
+ resetArgonCarrier();
3290
+ setArgonLoader(null);
3291
+ }
3292
+
3293
+ // ⑬ vendor 孿生+零 runtime deps
3294
+ const vendSrc27 = await srcOf('../src/client/vendor/hash-wasm-argon2.ts');
3295
+ const vb27 = vendSrc27.indexOf(' // VENDOR-BEGIN\n');
3296
+ const ve27 = vendSrc27.indexOf(' // VENDOR-END\n');
3297
+ const restored27 = vb27 > 0 && ve27 > vb27
3298
+ ? vendSrc27.slice(vb27 + ' // VENDOR-BEGIN\n'.length, ve27).split('\n').map((l) => (l.startsWith(' ') ? l.slice(2) : l)).join('\n')
3299
+ : '';
3300
+ const HASH_WASM_PIN27 = 'dcec617a2e1b700fa132d1583a186cb70611113395e869f2dd6cc82b415d3094'; // hash-wasm@4.12.0 dist/argon2.umd.min.js
3301
+ const kat27 = await hashWasmArgon2Factory().argon2id({ password: new Uint8Array(32).fill(1), salt: new Uint8Array(16), iterations: 3, parallelism: 4, memorySize: 32, hashLength: 32, outputType: 'hex' });
3302
+ const pkg27 = JSON.parse(await srcOf('../package.json')) as { dependencies?: unknown; devDependencies?: Record<string, string>; files?: string[]; exports?: Record<string, { types?: string; default?: string }> };
3303
+ await A('[27] vendor 孿生:去兩格縮排原文 sha256=HASH_WASM_ARGON2_SHA256=閘釘選值(hash-wasm@4.12.0);vendor inline 載體 RFC 9106 KAT 一致且零全域寫入;零 runtime deps(無 dependencies 欄)+hash-wasm 僅 devDependencies+產生器 tarball 排除',
3304
+ restored27.length > 20000 && await sha(restored27) === HASH_WASM_ARGON2_SHA256 && HASH_WASM_ARGON2_SHA256 === HASH_WASM_PIN27 && HASH_WASM_VERSION === '4.12.0'
3305
+ && kat27 === ARGON_RFC9106_EXPECTED && (globalThis as unknown as { hashwasm?: unknown }).hashwasm === undefined
3306
+ && pkg27.dependencies === undefined && typeof pkg27.devDependencies?.['hash-wasm'] === 'string'
3307
+ && !!pkg27.files?.includes('!scripts/vendor-hash-wasm.cjs'));
3308
+
3309
+ // ⑭ exports 子路徑+分層單向+靜態錨
3310
+ const vaultSrc27 = await srcOf('../src/client/vault.ts');
3311
+ const autoSrc27 = await srcOf('../src/client/argon-auto.ts');
3312
+ const lowSrcs27 = [await srcOf('../src/client/note-crypto.ts'), await srcOf('../src/client/argon2.ts')];
3313
+ await A('[27] exports `./client/vault` 指 dist(types .d.ts+default .js 雙形);分層單向(note-crypto.ts/argon2.ts 零 import vault/argon-auto/argon-worker);AAD 構形錨 `app + \':\' + recordId` 恰 1;argon-auto 零 PBKDF2 碼面(無 \'PBKDF2\' 字面、無 note-crypto import——zero-fallback 錨)',
3314
+ pkg27.exports?.['./client/vault']?.types === './dist/client/vault.d.ts' && pkg27.exports?.['./client/vault']?.default === './dist/client/vault.js'
3315
+ && lowSrcs27.every((s) => !/from '\.\/(vault|argon-auto|argon-worker)/.test(s))
3316
+ && vaultSrc27.includes('new TextEncoder().encode(app + \':\' + recordId)')
3317
+ && vaultSrc27.split('TextEncoder().encode(app + \':\' + recordId)').length - 1 === 1
3318
+ && !/['"]PBKDF2['"]|from '\.\/note-crypto/.test(autoSrc27));
3319
+ }
3320
+ }
3321
+
2869
3322
  secOpen(24, '[24] runner 契約自證(--only 解析契約 fail-closed+段級帳面顯形)'); if (secEnter(24)) {
2870
3323
  await A('[24] --only 解析契約(全跑態):spec 缺席=onlySet 空集=secEnter 恆真(CI 形契約不變——npm run verify 全帳;--only 態本面恆真跳過=帳面由次面承載)',
2871
3324
  !onlyMode() ? (onlySpecRaw === null && parseOnly('').err === 'ERR_ONLY_SPEC_EMPTY' && secEnter(1) && secEnter(23)) : true);
@@ -2886,8 +3339,8 @@ secOpen(24, '[24] runner 契約自證(--only 解析契約 fail-closed+段級
2886
3339
  }
2887
3340
 
2888
3341
  secOpen(25, '[25] 段級運行樣本:secEnter 真值表+tag 形帳+secCases 段帳'); if (secEnter(25)) {
2889
- await A('[25] secEnter 真值表:bannerless 段全跑態行為(27 未在 secOrder 段帳=secRan 零位面——帳面逐段只列 secOrder;n=0 恆 false);全跑態實段 1/15/23 全真',
2890
- !onlyMode() ? (secEnter(0) === false && (secRan.get(27) ?? 0) === 0 && secEnter(1) && secEnter(15) && secEnter(23)) : true);
3342
+ await A('[25] secEnter 真值表:bannerless 段全跑態行為(99 非段位=不在 secOrder 段帳=secRan 零位面——帳面逐段只列 secOrder;n=0 恆 false;原哨兵 27 隨 A3 [27] 實段落地改 99);全跑態實段 1/15/23 全真',
3343
+ !onlyMode() ? (secEnter(0) === false && (secRan.get(99) ?? 0) === 0 && secEnter(1) && secEnter(15) && secEnter(23)) : true);
2891
3344
  await A('[25] tag/spec 形帳:--only 態 (--ONLY …) 括形+spec 遞增 CSV 非空;全跑態 spec 空+tag 裸',
2892
3345
  onlyMode() ? (onlyTag().startsWith('(--ONLY ') && onlyTag().endsWith(')') && onlySpecDisp().length > 0)
2893
3346
  : (onlySpecDisp() === '' && onlyTag() === ''));
@@ -2898,7 +3351,7 @@ secOpen(25, '[25] 段級運行樣本:secEnter 真值表+tag 形帳+secCase
2898
3351
  }
2899
3352
 
2900
3353
  // ── 段級帳面(誠實帳——skip 段恆列帳「未跑非通過」;全跑態逐段案數對 secCases 帳面)──
2901
- const secOrder = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 24, 25, 26];
3354
+ const secOrder = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 24, 25, 26, 27];
2902
3355
  const secTotal = secOrder.reduce((a, n) => a + (secCases[n] ?? 0), 0);
2903
3356
  const ranSecs = secOrder.filter((n) => (secRan.get(n) ?? 0) > 0);
2904
3357
  const selTotal = secOrder.filter((n) => !onlyMode() || onlySet.has(n) || n === 24 || n === 25).reduce((a, n) => a + (secCases[n] ?? 0), 0);