@tacet-ink/journal-core 0.3.0 → 0.3.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.
@@ -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';
@@ -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-10 實測 506——A3 [27] +14);帳面語意契約(逐段「案 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: 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, 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();
@@ -2525,7 +2540,7 @@ secOpen(21, '[21] CI 供應鏈面(npm ci --ignore-scripts+dependabot runner
2525
2540
  await A('[21] publish.yml:npm ci --ignore-scripts(tag 驅動無 actor 面;typecheck+verify 先行=腳本面零在場)',
2526
2541
  srcPy21.includes('npm ci --ignore-scripts'));
2527
2542
  } else {
2528
- await A('[21] CI 源碼面:.github 缺席環境(tarball 發行樹常態)=顯性 SKIP(workflow 斷言 4 收斂為 1 SKIP 行——消費端計數帳 489)',
2543
+ await A('[21] CI 源碼面:.github 缺席環境(tarball 發行樹常態)=顯性 SKIP(workflow 斷言 4 收斂為 1 SKIP 行——消費端計數帳 503)',
2529
2544
  true, 'no .github dir — consumption tree face');
2530
2545
  }
2531
2546
  // 摘除自證(t_760f44e8 治理掃蕩補漏;fs 原語缺席環境=顯性 FAIL 形自守衛——非 silent true)
@@ -2790,7 +2805,7 @@ secOpen(23, '[23] loginRouteCore ladder 查表守衛(外審 #8 幽靈帳——
2790
2805
  }
2791
2806
 
2792
2807
  };
2793
- // ── 26. 格式規格+測試向量(docs/format-spec.md+docs/vectors/*.json;0.3.0 批 A2) ──
2808
+ // ── 26. 格式規格+測試向量(docs/format-spec.md+docs/vectors/*.json) ──
2794
2809
  //
2795
2810
  // 單向契約:源碼 → scripts/generate-vectors.ts → JSON(凍結證據,commit 入 repo)→ 本段只檢存在+shape。
2796
2811
  // 本段不依賴產生器、不重算 KDF(向量含隨機 iv/鹽=重算即漂移);BIP39 是確定性轉寫,逐組對照現行原語。
@@ -2866,6 +2881,309 @@ secOpen(26, '[26] 格式規格+測試向量(docs/format-spec.md+docs/vecto
2866
2881
  }
2867
2882
  }
2868
2883
 
2884
+ // ── 27. 高階 Vault API(src/client/vault.ts+argon-auto.ts+argon-worker.ts+vendor hash-wasm) ──
2885
+ //
2886
+ // 行為面(對真模組):create→encrypt→decrypt 三資料形 roundtrip、AAD=`<app>:<recordId>` 以低階原語
2887
+ // 手解對帳、unlockVault 新前綴(jr4w.)/舊前綴(jr3w. Argon、jr1w. PBKDF2)、upgrade 同鑰驗證重包、
2888
+ // changePassphrase 週期(含 PIN jr4d. 進出)、復原套件(jr1r.)、VaultError 毒化(搬列 AAD/竄改/
2889
+ // 形不符/配置缺欄)、載體覆寫顯形(setArgonLoader 注入優先+錯值載體 KAT 拒用)、真 Worker 載體
2890
+ //(node worker_threads 轉接層跑 argonWorkerSource 原文——瀏覽器 blob 形同源碼)、回退鏈(Worker 失效→
2891
+ // inline+onFallback;無回退鏈=KDF_UNSUPPORTED;派生期載體失效歸因不誤報 WRAP_MISMATCH)、vendor 孿生
2892
+ //(去縮排原文 sha256=檔內常數=閘釘選值)、零 runtime deps+exports 子路徑+分層單向。
2893
+ // 收尾恆 resetArgonCarrier()+setArgonLoader(null)——後續段零載體殘留。
2894
+ secOpen(27, '[27] 高階 Vault API(roundtrip 三資料形/新舊前綴解鎖/upgrade/換密語/復原/VaultError 毒化/載體覆寫+Worker 回退鏈/vendor 孿生)'); if (secEnter(27)) {
2895
+ {
2896
+ const codeOf27 = async (p: Promise<unknown>): Promise<string> => {
2897
+ try { await p; return 'OK'; } catch (e) { return e instanceof VaultError ? e.code : 'NON-VAULT:' + String((e as Error)?.message ?? e); }
2898
+ };
2899
+ const fam27 = (h: { family: unknown }): string | null => h.family as string | null; // getter 讀值(禁 TS 跨 await 窄化)
2900
+ const bytesEq27 = (a: unknown, b: Uint8Array): boolean =>
2901
+ a instanceof Uint8Array && a.length === b.length && a.every((x, i) => x === b[i]);
2902
+ const CFG27: VaultOptions = {
2903
+ app: 'tacet-vault27',
2904
+ cipher: 'jr1v.',
2905
+ argon: { wrap4: 'jr4w.', wrap3: 'jr3w.', wrapDual4: 'jr4d.', wrapDual3: 'jr3d.', pinSalt3Prefix: 'tacet-note-pin3:' },
2906
+ legacy: { wrap: 'jr1w.', wrapDual: 'jr2w.', pinSaltPrefix: 'tacet-note-pin1:' },
2907
+ recovery: { wrapRec: 'jr1r.', recKekHkdf: 'tacet-vault27' },
2908
+ };
2909
+ const PASS27 = 'correct horse battery 27';
2910
+ try {
2911
+ // ① roundtrip 三資料形
2912
+ const c27 = await createVault(PASS27, CFG27);
2913
+ const v27 = c27.vault;
2914
+ const str27 = '默·日記 — 🌙 line\nbreak';
2915
+ const obj27 = { title: '標題', tags: ['a', 'b'], n: 3, ok: true, nil: null, nested: { x: [1, { y: 'z' }] } };
2916
+ const bytes27 = new Uint8Array([0, 1, 2, 254, 255, 0, 128]);
2917
+ const bS27 = await v27.encrypt('rec-s', str27);
2918
+ const bJ27 = await v27.encrypt('rec-j', obj27);
2919
+ const bB27 = await v27.encrypt('rec-b', bytes27);
2920
+ const bE27 = await v27.encrypt('rec-e', new Uint8Array(0));
2921
+ const bES27 = await v27.encrypt('rec-es', '');
2922
+ await A('[27] create→encrypt→decrypt roundtrip 三資料形(字串含 Unicode/JSON 巢狀物件/Uint8Array 含 0x00+空陣列+空字串)還原同型;blob 前綴=cfg.cipher;同明文兩次密文異(隨機 iv)',
2923
+ [bS27, bJ27, bB27, bE27, bES27].every((b) => b.startsWith('jr1v.'))
2924
+ && (await v27.decrypt('rec-s', bS27)) === str27
2925
+ && JSON.stringify(await v27.decrypt('rec-j', bJ27)) === JSON.stringify(obj27)
2926
+ && bytesEq27(await v27.decrypt('rec-b', bB27), bytes27)
2927
+ && bytesEq27(await v27.decrypt('rec-e', bE27), new Uint8Array(0))
2928
+ && (await v27.decrypt('rec-es', bES27)) === ''
2929
+ && (await v27.encrypt('rec-s', str27)) !== bS27);
2930
+
2931
+ // ② 寫面形+AAD 域(低階原語手解對帳)
2932
+ const rec27 = c27.serverRecord;
2933
+ const nk27 = await unwrapNoteKey4(CFG27.argon, rec27.wrapped, PASS27, rec27.salt);
2934
+ const openRaw27 = async (key: CryptoKey | null, blob: string, aad: string): Promise<Uint8Array | null> => {
2935
+ if (!key) return null;
2936
+ try {
2937
+ const p = unb64Mod(blob.slice(5));
2938
+ 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));
2939
+ } catch { return null; }
2940
+ };
2941
+ const rawS27 = await openRaw27(nk27, bS27, 'tacet-vault27:rec-s');
2942
+ const kit27 = c27.recovery;
2943
+ 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)',
2944
+ rec27.wrapped.startsWith('jr4w.') && /^[0-9a-f]{32}$/.test(rec27.salt) && v27.family === 'wrap4' && v27.needsUpgrade() === false && v27.carrier === 'node'
2945
+ && !!rawS27 && rawS27[0] === 0x73 && new TextDecoder().decode(rawS27.subarray(1)) === str27
2946
+ && (await openRaw27(nk27, bS27, 'rec-s')) === null && (await openRaw27(nk27, bS27, 'other-app:rec-s')) === null
2947
+ && !!kit27 && /^[0-9a-f]{64}$/.test(kit27.recToken) && kit27.wrappedRec.startsWith('jr1r.') && kit27.recTokenHash === await sha(kit27.recToken));
2948
+
2949
+ // ③ unlockVault 新前綴
2950
+ const u27 = await unlockVault(PASS27, rec27, CFG27);
2951
+ await A('[27] unlockVault 新前綴(jr4w.):另一 handle 解前 handle 三資料形密文;family wrap4、needsUpgrade false',
2952
+ u27.family === 'wrap4' && !u27.needsUpgrade()
2953
+ && (await u27.decrypt('rec-s', bS27)) === str27 && JSON.stringify(await u27.decrypt('rec-j', bJ27)) === JSON.stringify(obj27)
2954
+ && bytesEq27(await u27.decrypt('rec-b', bB27), bytes27));
2955
+
2956
+ // ④ unlockVault 舊前綴(jr3w. Argon/jr1w. PBKDF2)——同一 noteKey 兩包
2957
+ const OLD27 = 'old pass 27';
2958
+ const nkOld27 = await generateNoteKey();
2959
+ const r3_27 = await wrapNoteKey3(CFG27.argon, nkOld27, OLD27);
2960
+ const r1_27 = await wrapNoteKey(TACET, nkOld27, OLD27);
2961
+ const u3_27 = await unlockVault(OLD27, r3_27, CFG27);
2962
+ const u1_27 = await unlockVault(OLD27, r1_27, CFG27);
2963
+ const bOld27 = await u3_27.encrypt('rec-old', 'old-era');
2964
+ await A('[27] unlockVault 舊前綴各一:jr3w.(family wrap3)/jr1w.(family wrap,PBKDF2 舊世代讀面=carrier null 不需 Argon)皆 needsUpgrade true;兩 handle 同鑰互解+低階 noteKey 手解同 AAD 域',
2965
+ u3_27.family === 'wrap3' && u1_27.family === 'wrap' && u3_27.needsUpgrade() && u1_27.needsUpgrade() && u1_27.carrier === null && u3_27.carrier === 'node'
2966
+ && (await u1_27.decrypt('rec-old', bOld27)) === 'old-era'
2967
+ && new TextDecoder().decode((await openRaw27(nkOld27, bOld27, 'tacet-vault27:rec-old'))?.subarray(1)) === 'old-era');
2968
+
2969
+ // ⑤ upgrade:錯密語拒(狀態不動)→ 正確密語重包 jr4w.;舊包裹仍可解(舊前綴語意不動);雙因子 jr3d. → jr4d.
2970
+ const upWrong27 = await codeOf27(u3_27.upgrade('not the pass'));
2971
+ const stillOld27 = u3_27.family === 'wrap3' && u3_27.serverRecord?.wrapped === r3_27.wrapped;
2972
+ const up27 = await u3_27.upgrade(OLD27);
2973
+ const uUp27 = await unlockVault(OLD27, up27, CFG27);
2974
+ const uOld27b = await unlockVault(OLD27, r3_27, CFG27);
2975
+ const upIdem27 = await u3_27.upgrade('ignored when current');
2976
+ const r3d27 = await wrapNoteKeyDual3(CFG27.argon, nkOld27, OLD27, '2468');
2977
+ const uD27 = await unlockVault(OLD27, r3d27, { ...CFG27, pin: '2468' });
2978
+ const dualBefore27 = fam27(uD27) === 'wrapDual3' && uD27.needsUpgrade(); // 升級前快照(斷言式在全部動作後求值)
2979
+ const upDualNoPin27 = await codeOf27(uD27.upgrade(OLD27));
2980
+ const upD27 = await uD27.upgrade(OLD27, { pin: '2468' });
2981
+ await A('[27] needsUpgrade/upgrade 真值行:錯密語=ERR_VAULT_WRAP_MISMATCH 且狀態不動;正確=新前綴 jr4w. 包同一 noteKey(新包裹解舊密文)、needsUpgrade 翻 false、已最新再呼=原包裹欄原樣;舊 jr3w. 包裹照解;jr3d. 缺 PIN=ERR_VAULT_PIN_REQUIRED、帶 PIN → jr4d.(family wrapDual4)',
2982
+ upWrong27 === 'ERR_VAULT_WRAP_MISMATCH' && stillOld27
2983
+ && up27.wrapped.startsWith('jr4w.') && fam27(u3_27) === 'wrap4' && !u3_27.needsUpgrade()
2984
+ && (await uUp27.decrypt('rec-old', bOld27)) === 'old-era' && (await uOld27b.decrypt('rec-old', bOld27)) === 'old-era'
2985
+ && upIdem27.wrapped === up27.wrapped
2986
+ && dualBefore27 && upDualNoPin27 === 'ERR_VAULT_PIN_REQUIRED'
2987
+ && upD27.wrapped.startsWith('jr4d.') && fam27(uD27) === 'wrapDual4' && !uD27.needsUpgrade());
2988
+
2989
+ // ⑥ changePassphrase 週期(含 PIN 進出)
2990
+ const NEW27 = 'new pass 27';
2991
+ const cp27 = await v27.changePassphrase(NEW27);
2992
+ const uNew27 = await unlockVault(NEW27, cp27, CFG27);
2993
+ const oldOnNew27 = await codeOf27(unlockVault(PASS27, cp27, CFG27));
2994
+ const oldRecStill27 = await unlockVault(PASS27, rec27, CFG27);
2995
+ const cpD27 = await v27.changePassphrase(NEW27, { pin: '1357' });
2996
+ const famD27 = v27.family;
2997
+ const noPin27 = await codeOf27(unlockVault(NEW27, cpD27, CFG27));
2998
+ const uPin27 = await unlockVault(NEW27, cpD27, { ...CFG27, pin: '1357' });
2999
+ const cpNoPin27 = await codeOf27(v27.changePassphrase(NEW27));
3000
+ const cpBack27 = await v27.changePassphrase(NEW27, { pin: null });
3001
+ 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',
3002
+ cp27.wrapped.startsWith('jr4w.') && cp27.wrapped !== rec27.wrapped && (await uNew27.decrypt('rec-s', bS27)) === str27
3003
+ && oldOnNew27 === 'ERR_VAULT_WRAP_MISMATCH' && (await oldRecStill27.decrypt('rec-s', bS27)) === str27
3004
+ && cpD27.wrapped.startsWith('jr4d.') && famD27 === 'wrapDual4' && noPin27 === 'ERR_VAULT_PIN_REQUIRED'
3005
+ && (await uPin27.decrypt('rec-j', bJ27) as { title?: string }).title === '標題'
3006
+ && cpNoPin27 === 'ERR_VAULT_PIN_REQUIRED' && cpBack27.wrapped.startsWith('jr4w.') && fam27(v27) === 'wrap4'
3007
+ && (await codeOf27(v27.changePassphrase(''))) === 'ERR_VAULT_BAD_PASSPHRASE'
3008
+ && (await codeOf27(v27.changePassphrase(NEW27, { pin: ' ' }))) === 'ERR_VAULT_BAD_PIN');
3009
+
3010
+ // ⑦ VaultError 毒化(記錄密文面)
3011
+ const tamper27 = (() => { const p = unb64Mod(bS27.slice(5)); p[p.length - 1] ^= 0x01; return 'jr1v.' + modB64(p); })();
3012
+ const errObj27 = await (async () => { try { await v27.decrypt('rec-OTHER', bS27); return null; } catch (e) { return e; } })();
3013
+ const cyc27: Record<string, unknown> = {}; cyc27.self = cyc27;
3014
+ 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 分流',
3015
+ errObj27 instanceof VaultError && (errObj27 as VaultError).name === 'VaultError' && isVaultError(errObj27, 'ERR_VAULT_DECRYPT') && !isVaultError(errObj27, 'ERR_VAULT_BAD_BLOB')
3016
+ && (await codeOf27(v27.decrypt('rec-s', tamper27))) === 'ERR_VAULT_DECRYPT'
3017
+ && (await codeOf27(u3_27.decrypt('rec-s', bS27))) === 'ERR_VAULT_DECRYPT'
3018
+ && (await codeOf27(v27.decrypt('rec-s', 'jr1b.' + bS27.slice(5)))) === 'ERR_VAULT_BAD_BLOB'
3019
+ && (await codeOf27(v27.decrypt('rec-s', 'jr1v.!!not-base64!!'))) === 'ERR_VAULT_BAD_BLOB'
3020
+ && (await codeOf27(v27.decrypt('rec-s', 'jr1v.' + modB64(new Uint8Array(20))))) === 'ERR_VAULT_BAD_BLOB'
3021
+ && (await codeOf27(v27.encrypt('', 'x'))) === 'ERR_VAULT_BAD_RECORD_ID'
3022
+ && (await codeOf27(v27.decrypt('', bS27))) === 'ERR_VAULT_BAD_RECORD_ID'
3023
+ && (await codeOf27(v27.encrypt('r', undefined as never))) === 'ERR_VAULT_BAD_DATA'
3024
+ && (await codeOf27(v27.encrypt('r', new ArrayBuffer(4) as never))) === 'ERR_VAULT_BAD_DATA'
3025
+ && (await codeOf27(v27.encrypt('r', new Uint16Array(2) as never))) === 'ERR_VAULT_BAD_DATA'
3026
+ && (await codeOf27(v27.encrypt('r', 10n as never))) === 'ERR_VAULT_BAD_DATA'
3027
+ && (await codeOf27(v27.encrypt('r', cyc27 as never))) === 'ERR_VAULT_BAD_DATA');
3028
+
3029
+ // ⑧ 配置/serverRecord 拒(opt-in 未配置即拒、不猜)
3030
+ const noW4_27 = { ...CFG27, argon: { ...CFG27.argon, wrap4: undefined } };
3031
+ await A('[27] VaultError 拒(配置/包裹欄面):serverRecord 缺 salt/缺 wrapped/salt 大寫/null=BAD_RECORD;未知前綴=WRAP_UNKNOWN;缺 argon.wrap4/缺 app/缺 cipher/前綴互為前綴/recovery 半配置/PIN 寫面未配置=ERR_VAULT_CONFIG;空密語=BAD_PASSPHRASE',
3032
+ (await codeOf27(unlockVault(PASS27, { wrapped: rec27.wrapped } as never, CFG27))) === 'ERR_VAULT_BAD_RECORD'
3033
+ && (await codeOf27(unlockVault(PASS27, { salt: rec27.salt } as never, CFG27))) === 'ERR_VAULT_BAD_RECORD'
3034
+ && (await codeOf27(unlockVault(PASS27, { wrapped: rec27.wrapped, salt: rec27.salt.toUpperCase() }, CFG27))) === 'ERR_VAULT_BAD_RECORD'
3035
+ && (await codeOf27(unlockVault(PASS27, null as never, CFG27))) === 'ERR_VAULT_BAD_RECORD'
3036
+ && (await codeOf27(unlockVault(PASS27, { wrapped: 'jr9w.' + rec27.wrapped.slice(5), salt: rec27.salt }, CFG27))) === 'ERR_VAULT_WRAP_UNKNOWN'
3037
+ && (await codeOf27(createVault(PASS27, noW4_27))) === 'ERR_VAULT_CONFIG'
3038
+ && (await codeOf27(createVault(PASS27, { ...CFG27, app: '' }))) === 'ERR_VAULT_CONFIG'
3039
+ && (await codeOf27(createVault(PASS27, { ...CFG27, cipher: '' }))) === 'ERR_VAULT_CONFIG'
3040
+ && (await codeOf27(createVault(PASS27, { ...CFG27, cipher: 'jr4' }))) === 'ERR_VAULT_CONFIG'
3041
+ && (await codeOf27(createVault(PASS27, { ...CFG27, recovery: { wrapRec: 'jr1r.', recKekHkdf: '' } }))) === 'ERR_VAULT_CONFIG'
3042
+ && (await codeOf27(createVault(PASS27, { ...CFG27, argon: { wrap4: 'jr4w.' }, pin: '1234' }))) === 'ERR_VAULT_CONFIG'
3043
+ && (await codeOf27(createVault('', CFG27))) === 'ERR_VAULT_BAD_PASSPHRASE');
3044
+
3045
+ // ⑨ 復原套件
3046
+ const rv27 = await recoverVault(kit27!.recToken, kit27!.wrappedRec, CFG27);
3047
+ const rvFamNull27 = rv27.family === null && rv27.serverRecord === null && rv27.carrier === null && !rv27.needsUpgrade();
3048
+ const rvUp27 = await codeOf27(rv27.upgrade(PASS27));
3049
+ const rvCp27 = await rv27.changePassphrase('recovered 27');
3050
+ const rvU27 = await unlockVault('recovered 27', rvCp27, CFG27);
3051
+ const wrongTok27 = (kit27!.recToken[0] === 'a' ? 'b' : 'a') + kit27!.recToken.slice(1);
3052
+ 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',
3053
+ (await rv27.decrypt('rec-s', bS27)) === str27 && rvFamNull27 && rvUp27 === 'ERR_VAULT_BAD_RECORD'
3054
+ && rvCp27.wrapped.startsWith('jr4w.') && (await rvU27.decrypt('rec-b', bB27) as Uint8Array).length === bytes27.length
3055
+ && (await codeOf27(recoverVault(wrongTok27, kit27!.wrappedRec, CFG27))) === 'ERR_VAULT_RECOVERY'
3056
+ && (await codeOf27(recoverVault('xyz', kit27!.wrappedRec, CFG27))) === 'ERR_VAULT_RECOVERY'
3057
+ && (await codeOf27(recoverVault(kit27!.recToken, 'jr1w.' + kit27!.wrappedRec.slice(5), CFG27))) === 'ERR_VAULT_RECOVERY'
3058
+ && (await codeOf27(recoverVault(kit27!.recToken, kit27!.wrappedRec, { ...CFG27, recovery: undefined }))) === 'ERR_VAULT_CONFIG');
3059
+
3060
+ // ⑩ 載體覆寫顯形(setArgonLoader 注入恆優先;錯值載體 KAT 拒用)
3061
+ let cnt27 = 0;
3062
+ const fac27 = hashWasmArgon2Factory();
3063
+ const countLoader27 = () => { cnt27++; return Promise.resolve(fac27); };
3064
+ setArgonLoader(countLoader27);
3065
+ const cInj27 = await createVault(PASS27, { ...CFG27, recovery: undefined });
3066
+ const injKind27 = cInj27.vault.carrier;
3067
+ const injCur27 = currentArgonCarrier();
3068
+ const uFromNode27 = await unlockVault(PASS27, rec27, CFG27); // 注入載體解 node 載體所寫
3069
+ setArgonLoader(null);
3070
+ const uFromInj27 = await unlockVault(PASS27, cInj27.serverRecord, CFG27); // node 載體解注入載體所寫
3071
+ const zeroLoader27 = () => Promise.resolve({ argon2id: (async () => new Uint8Array(32)) as never });
3072
+ setArgonLoader(zeroLoader27);
3073
+ const badKat27 = await codeOf27(createVault(PASS27, CFG27));
3074
+ setArgonLoader(null);
3075
+ await A('[27] 載體覆寫顯形:setArgonLoader 注入=carrier/currentArgonCarrier 皆 injected 且注入載體實被呼叫;注入↔node 互解(逐位元一致);撤注入回 node;錯值載體(恆零輸出)=RFC 9106 KAT 拒用 ERR_VAULT_KDF_UNSUPPORTED(zero-fallback:不降級)',
3076
+ injKind27 === 'injected' && injCur27 === 'injected' && cnt27 > 0
3077
+ && (await uFromNode27.decrypt('rec-s', bS27)) === str27 && uFromInj27.family === 'wrap4' && uFromInj27.carrier === 'node'
3078
+ && currentArgonCarrier() === 'node' && badKat27 === 'ERR_VAULT_KDF_UNSUPPORTED');
3079
+
3080
+ // ⑪ 真 Worker 載體(node worker_threads 轉接層跑 argonWorkerSource 原文)
3081
+ const wt27 = (globalThis as unknown as { process?: { getBuiltinModule?: (id: string) => unknown } }).process?.getBuiltinModule?.('node:worker_threads') as
3082
+ { 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;
3083
+ if (!wt27?.Worker) {
3084
+ await A('[27] 真 Worker 載體(worker_threads 缺席環境)=顯性 FAIL 行(非 silent true)', false, 'node:worker_threads 缺席');
3085
+ } else {
3086
+ 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";
3087
+ let made27 = 0;
3088
+ const nodeWorker27 = (): ArgonWorkerLike => {
3089
+ made27++;
3090
+ const w = new wt27.Worker!(SHIM27 + argonWorkerSource(), { eval: true });
3091
+ const like: ArgonWorkerLike = { postMessage: (m, t) => w.postMessage(m, t), terminate: () => { void w.terminate(); }, onmessage: null, onerror: null };
3092
+ w.on('message', (d) => like.onmessage?.({ data: d }));
3093
+ w.on('error', (e) => like.onerror?.(e));
3094
+ return like;
3095
+ };
3096
+ const wOpts27 = { chain: ['worker', 'inline'] as const, createWorker: nodeWorker27 };
3097
+ const cW27 = await createVault(PASS27, { ...CFG27, recovery: undefined, carrier: wOpts27 });
3098
+ const wKind27 = cW27.vault.carrier;
3099
+ const wCur27 = currentArgonCarrier();
3100
+ const uW27 = await unlockVault(PASS27, rec27, { ...CFG27, carrier: wOpts27 }); // Worker 解 node 所寫
3101
+ resetArgonCarrier();
3102
+ const afterReset27 = currentArgonCarrier();
3103
+ const uWn27 = await unlockVault(PASS27, cW27.serverRecord, CFG27); // node 解 Worker 所寫
3104
+ await A('[27] 真 Worker 載體:chain [worker,inline]+worker_threads 轉接 argonWorkerSource 原文=carrier worker(單一 Worker 重用);Worker↔node 互解逐位元一致;resetArgonCarrier 撤自裝載體回 node',
3105
+ wKind27 === 'worker' && wCur27 === 'worker' && made27 === 1 && uW27.carrier === 'worker'
3106
+ && (await uW27.decrypt('rec-s', bS27)) === str27 && uWn27.family === 'wrap4' && uWn27.carrier === 'node' && afterReset27 === 'node');
3107
+ }
3108
+
3109
+ // ⑫ 回退鏈+失效歸因
3110
+ const events27: { from: string; to: string }[] = [];
3111
+ const broken27 = (): ArgonWorkerLike => { throw new Error('CSP worker-src blocked (simulated)'); };
3112
+ const fb27 = await unlockVault(PASS27, rec27, { ...CFG27, carrier: { chain: ['worker', 'inline'], createWorker: broken27, onFallback: (e) => events27.push(e) } });
3113
+ const fbKind27 = fb27.carrier;
3114
+ const fbCur27 = currentArgonCarrier();
3115
+ resetArgonCarrier();
3116
+ const noFb27 = await codeOf27(unlockVault(PASS27, rec27, { ...CFG27, carrier: { chain: ['worker'], createWorker: broken27 } }));
3117
+ resetArgonCarrier();
3118
+ // 健康 KAT(32 KiB)、64 MiB 派生即 ok:false 的 flaky Worker:無回退=KDF_UNSUPPORTED(非 WRAP_MISMATCH);有回退=成功+事件
3119
+ const flaky27 = (): ArgonWorkerLike => {
3120
+ const fac = hashWasmArgon2Factory();
3121
+ const like: ArgonWorkerLike = {
3122
+ postMessage: (m) => {
3123
+ const q = m as { id: number; memorySize: number; password: string | Uint8Array; salt: Uint8Array; iterations: number; parallelism: number; hashLength: number };
3124
+ void (async () => {
3125
+ if (q.memorySize > 1024) { like.onmessage?.({ data: { id: q.id, ok: false, error: 'OOM (simulated)' } }); return; }
3126
+ const out = await fac.argon2id({ ...q, outputType: 'binary' }) as Uint8Array;
3127
+ like.onmessage?.({ data: { id: q.id, ok: true, out } });
3128
+ })();
3129
+ },
3130
+ terminate: () => {},
3131
+ onmessage: null,
3132
+ onerror: null,
3133
+ };
3134
+ return like;
3135
+ };
3136
+ const attrib27 = await codeOf27(unlockVault(PASS27, rec27, { ...CFG27, carrier: { chain: ['worker'], createWorker: flaky27 } }));
3137
+ resetArgonCarrier();
3138
+ const events27b: unknown[] = [];
3139
+ const fl27 = await unlockVault(PASS27, rec27, { ...CFG27, carrier: { chain: ['worker', 'inline'], createWorker: flaky27, onFallback: (e) => events27b.push(e) } });
3140
+ const flKind27 = fl27.carrier === 'worker' ? currentArgonCarrier() : fl27.carrier;
3141
+ resetArgonCarrier();
3142
+ const absent27 = await codeOf27(unlockVault(PASS27, rec27, { ...CFG27, carrier: { chain: ['worker'] } })); // node 無全域 Worker=載體缺席
3143
+ const emptyChain27 = await codeOf27(createVault(PASS27, { ...CFG27, carrier: { chain: [] } }));
3144
+ resetArgonCarrier();
3145
+ 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',
3146
+ fbKind27 === 'inline' && fbCur27 === 'inline' && events27.length === 1 && events27[0].from === 'worker' && events27[0].to === 'inline'
3147
+ && noFb27 === 'ERR_VAULT_KDF_UNSUPPORTED'
3148
+ && attrib27 === 'ERR_VAULT_KDF_UNSUPPORTED'
3149
+ && (await fl27.decrypt('rec-s', bS27)) === str27 && flKind27 === 'inline' && events27b.length === 1
3150
+ && absent27 === 'ERR_VAULT_KDF_UNSUPPORTED' && emptyChain27 === 'ERR_VAULT_CONFIG');
3151
+ } catch (e) {
3152
+ await A('[27] 行為面未預期拋出(應全數經 VaultError 分流或斷言承載)', false, String((e as Error)?.stack ?? e).slice(0, 400));
3153
+ } finally {
3154
+ resetArgonCarrier();
3155
+ setArgonLoader(null);
3156
+ }
3157
+
3158
+ // ⑬ vendor 孿生+零 runtime deps
3159
+ const vendSrc27 = await srcOf('../src/client/vendor/hash-wasm-argon2.ts');
3160
+ const vb27 = vendSrc27.indexOf(' // VENDOR-BEGIN\n');
3161
+ const ve27 = vendSrc27.indexOf(' // VENDOR-END\n');
3162
+ const restored27 = vb27 > 0 && ve27 > vb27
3163
+ ? vendSrc27.slice(vb27 + ' // VENDOR-BEGIN\n'.length, ve27).split('\n').map((l) => (l.startsWith(' ') ? l.slice(2) : l)).join('\n')
3164
+ : '';
3165
+ const HASH_WASM_PIN27 = 'dcec617a2e1b700fa132d1583a186cb70611113395e869f2dd6cc82b415d3094'; // hash-wasm@4.12.0 dist/argon2.umd.min.js
3166
+ 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' });
3167
+ const pkg27 = JSON.parse(await srcOf('../package.json')) as { dependencies?: unknown; devDependencies?: Record<string, string>; files?: string[]; exports?: Record<string, { types?: string; default?: string }> };
3168
+ 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 排除',
3169
+ restored27.length > 20000 && await sha(restored27) === HASH_WASM_ARGON2_SHA256 && HASH_WASM_ARGON2_SHA256 === HASH_WASM_PIN27 && HASH_WASM_VERSION === '4.12.0'
3170
+ && kat27 === ARGON_RFC9106_EXPECTED && (globalThis as unknown as { hashwasm?: unknown }).hashwasm === undefined
3171
+ && pkg27.dependencies === undefined && typeof pkg27.devDependencies?.['hash-wasm'] === 'string'
3172
+ && !!pkg27.files?.includes('!scripts/vendor-hash-wasm.cjs'));
3173
+
3174
+ // ⑭ exports 子路徑+分層單向+靜態錨
3175
+ const vaultSrc27 = await srcOf('../src/client/vault.ts');
3176
+ const autoSrc27 = await srcOf('../src/client/argon-auto.ts');
3177
+ const lowSrcs27 = [await srcOf('../src/client/note-crypto.ts'), await srcOf('../src/client/argon2.ts')];
3178
+ 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 錨)',
3179
+ pkg27.exports?.['./client/vault']?.types === './dist/client/vault.d.ts' && pkg27.exports?.['./client/vault']?.default === './dist/client/vault.js'
3180
+ && lowSrcs27.every((s) => !/from '\.\/(vault|argon-auto|argon-worker)/.test(s))
3181
+ && vaultSrc27.includes('new TextEncoder().encode(app + \':\' + recordId)')
3182
+ && vaultSrc27.split('TextEncoder().encode(app + \':\' + recordId)').length - 1 === 1
3183
+ && !/['"]PBKDF2['"]|from '\.\/note-crypto/.test(autoSrc27));
3184
+ }
3185
+ }
3186
+
2869
3187
  secOpen(24, '[24] runner 契約自證(--only 解析契約 fail-closed+段級帳面顯形)'); if (secEnter(24)) {
2870
3188
  await A('[24] --only 解析契約(全跑態):spec 缺席=onlySet 空集=secEnter 恆真(CI 形契約不變——npm run verify 全帳;--only 態本面恆真跳過=帳面由次面承載)',
2871
3189
  !onlyMode() ? (onlySpecRaw === null && parseOnly('').err === 'ERR_ONLY_SPEC_EMPTY' && secEnter(1) && secEnter(23)) : true);
@@ -2886,8 +3204,8 @@ secOpen(24, '[24] runner 契約自證(--only 解析契約 fail-closed+段級
2886
3204
  }
2887
3205
 
2888
3206
  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);
3207
+ await A('[25] secEnter 真值表:bannerless 段全跑態行為(99 非段位=不在 secOrder 段帳=secRan 零位面——帳面逐段只列 secOrder;n=0 恆 false;原哨兵 27 隨 A3 [27] 實段落地改 99);全跑態實段 1/15/23 全真',
3208
+ !onlyMode() ? (secEnter(0) === false && (secRan.get(99) ?? 0) === 0 && secEnter(1) && secEnter(15) && secEnter(23)) : true);
2891
3209
  await A('[25] tag/spec 形帳:--only 態 (--ONLY …) 括形+spec 遞增 CSV 非空;全跑態 spec 空+tag 裸',
2892
3210
  onlyMode() ? (onlyTag().startsWith('(--ONLY ') && onlyTag().endsWith(')') && onlySpecDisp().length > 0)
2893
3211
  : (onlySpecDisp() === '' && onlyTag() === ''));
@@ -2898,7 +3216,7 @@ secOpen(25, '[25] 段級運行樣本:secEnter 真值表+tag 形帳+secCase
2898
3216
  }
2899
3217
 
2900
3218
  // ── 段級帳面(誠實帳——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];
3219
+ 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
3220
  const secTotal = secOrder.reduce((a, n) => a + (secCases[n] ?? 0), 0);
2903
3221
  const ranSecs = secOrder.filter((n) => (secRan.get(n) ?? 0) > 0);
2904
3222
  const selTotal = secOrder.filter((n) => !onlyMode() || onlySet.has(n) || n === 24 || n === 25).reduce((a, n) => a + (secCases[n] ?? 0), 0);
@@ -0,0 +1,224 @@
1
+ /**
2
+ * argon-auto.ts — Argon2id 載體平台預設。
3
+ *
4
+ * 優先序(向後相容鐵律:setArgonLoader 照舊優先):
5
+ * 1. 呼叫端已 setArgonLoader 注入(非本模組自裝)='injected'——本模組零介入。
6
+ * 2. 回退鏈 chain(可覆寫;預設 ['node','worker','inline'])逐項取第一個可用者:
7
+ * - 'node':node:crypto.argon2 在場(argon2.ts 原生腿;本模組不注入)。
8
+ * - 'worker':Web Worker(argon-worker.ts 內嵌 blob 形);執行期失效=若 chain 於其後含
9
+ * 'inline' 則回退 inline(onFallback 通知+currentArgonCarrier 翻 'inline'=行為顯形),否則 throw。
10
+ * - 'inline':同執行緒 vendor hash-wasm(WebAssembly 在場即可)。
11
+ * 3. 皆不可用=ERR_VAULT_KDF_UNSUPPORTED(zero-fallback:禁 PBKDF2/zeros 兜底)。
12
+ * 裝載即 KAT:每個載體首用前跑 RFC 9106 KAT(verifyArgonKat)——錯值載體=拒用,不只是缺席才拒。
13
+ *
14
+ * 'worker'/'inline' 經 setArgonLoader 全域注入(argon2.ts 派生本體唯一入口):同頁其他 argon2.ts
15
+ * 呼叫端共享此載體;呼叫端之後自行 setArgonLoader 即覆寫(下次 ensure 判 'injected')。
16
+ * resetArgonCarrier() 撤除本模組自裝載體(不動呼叫端注入)並終止 Worker。
17
+ */
18
+
19
+ import { getArgonLoader, setArgonLoader, verifyArgonKat, type ArgonLoader, type HashWasmArgon2id } from './argon2.ts';
20
+ import { makeWorkerArgon2id, createBlobArgonWorker, type ArgonWorkerLike } from './argon-worker.ts';
21
+ import { hashWasmArgon2Factory } from './vendor/hash-wasm-argon2.ts';
22
+ import { VaultError } from './vault-error.ts';
23
+
24
+ export type ArgonCarrierKind = 'injected' | 'node' | 'worker' | 'inline';
25
+ export type ArgonCarrierChoice = 'node' | 'worker' | 'inline';
26
+ export type { ArgonWorkerLike };
27
+
28
+ export interface ArgonFallbackEvent {
29
+ from: 'worker';
30
+ to: 'inline';
31
+ error: unknown;
32
+ }
33
+
34
+ export interface ArgonCarrierOptions {
35
+ /** 回退鏈(預設 ['node','worker','inline'])。例:['worker']=只准 Worker(失效即拒,不回退)。 */
36
+ chain?: readonly ArgonCarrierChoice[];
37
+ /** Worker 工廠覆寫(預設瀏覽器 blob Worker;測試/特殊宿主注入轉接層)。 */
38
+ createWorker?: () => ArgonWorkerLike;
39
+ /** Worker → inline 回退通知(行為顯形;不提供=靜默回退但 currentArgonCarrier 仍翻 'inline')。 */
40
+ onFallback?: (ev: ArgonFallbackEvent) => void;
41
+ }
42
+
43
+ export const DEFAULT_ARGON_CHAIN: readonly ArgonCarrierChoice[] = ['node', 'worker', 'inline'];
44
+
45
+ function nodeArgonAvailable(): boolean {
46
+ const nodeCrypto = (globalThis as unknown as {
47
+ process?: { getBuiltinModule?: (id: string) => { argon2?: unknown } | undefined };
48
+ }).process?.getBuiltinModule?.('node:crypto');
49
+ return typeof nodeCrypto?.argon2 === 'function';
50
+ }
51
+
52
+ function workerAvailable(opts: ArgonCarrierOptions): boolean {
53
+ if (opts.createWorker) return true;
54
+ const g = globalThis as unknown as { Worker?: unknown; Blob?: unknown; URL?: { createObjectURL?: unknown } };
55
+ return typeof g.Worker === 'function' && typeof g.Blob === 'function' && typeof g.URL?.createObjectURL === 'function';
56
+ }
57
+
58
+ function wasmAvailable(): boolean {
59
+ return typeof (globalThis as unknown as { WebAssembly?: unknown }).WebAssembly === 'object';
60
+ }
61
+
62
+ // ── 狀態 ────────────────────────────────────────────────────────────────────
63
+
64
+ interface AutoState {
65
+ loader: ArgonLoader;
66
+ kind: 'worker' | 'inline';
67
+ chainKey: string;
68
+ createWorker: ArgonCarrierOptions['createWorker'];
69
+ onFallback: ArgonCarrierOptions['onFallback'];
70
+ dispose(): void;
71
+ }
72
+
73
+ let auto: AutoState | null = null;
74
+ let nodeVerified = false;
75
+ const verified = new WeakSet<ArgonLoader>();
76
+ let failures = 0;
77
+
78
+ /** 載體終端失效累計(拋出派生本體的失效才計;回退成功不計)。vault 以前後差分辨
79
+ * 「解包 null=密語錯」與「載體失效」(低階 unwrap 吞錯回 null,邊界還原真因)。 */
80
+ export function argonCarrierFailures(): number {
81
+ return failures;
82
+ }
83
+
84
+ /** 現行載體(未 ensure 過且無注入、非 node=null)。 */
85
+ export function currentArgonCarrier(): ArgonCarrierKind | null {
86
+ const cur = getArgonLoader();
87
+ if (cur) return auto && cur === auto.loader ? auto.kind : 'injected';
88
+ return nodeArgonAvailable() ? 'node' : null;
89
+ }
90
+
91
+ /** 撤除本模組自裝載體(呼叫端 setArgonLoader 注入不動)+終止 Worker。 */
92
+ export function resetArgonCarrier(): void {
93
+ if (!auto) return;
94
+ const a = auto;
95
+ auto = null;
96
+ if (getArgonLoader() === a.loader) setArgonLoader(null);
97
+ a.dispose();
98
+ }
99
+
100
+ // ── inline 載體(同執行緒 vendor hash-wasm;串行) ────────────────────────────
101
+
102
+ function makeInlineArgon2id(): HashWasmArgon2id {
103
+ let mod: { argon2id: HashWasmArgon2id } | null = null;
104
+ let queue: Promise<unknown> = Promise.resolve();
105
+ return (params) => {
106
+ const job = queue.then(() => {
107
+ if (!mod) mod = hashWasmArgon2Factory();
108
+ return mod.argon2id(params);
109
+ });
110
+ queue = job.catch(() => undefined);
111
+ return job;
112
+ };
113
+ }
114
+
115
+ function terminal(e: unknown, what: string): VaultError {
116
+ failures++;
117
+ return new VaultError('ERR_VAULT_KDF_UNSUPPORTED', what, { cause: e });
118
+ }
119
+
120
+ function installInline(chainKey: string, opts: ArgonCarrierOptions): AutoState {
121
+ const inline = makeInlineArgon2id();
122
+ const argon2id: HashWasmArgon2id = async (params) => {
123
+ try { return await inline(params); } catch (e) { throw terminal(e, 'inline carrier failed'); }
124
+ };
125
+ const mod = { argon2id };
126
+ const st: AutoState = {
127
+ loader: () => Promise.resolve(mod),
128
+ kind: 'inline',
129
+ chainKey,
130
+ createWorker: opts.createWorker,
131
+ onFallback: opts.onFallback,
132
+ dispose: () => {},
133
+ };
134
+ return st;
135
+ }
136
+
137
+ function installWorker(chainKey: string, opts: ArgonCarrierOptions, fallbackInline: boolean): AutoState {
138
+ const w = makeWorkerArgon2id(opts.createWorker ?? createBlobArgonWorker);
139
+ let inline: HashWasmArgon2id | null = null;
140
+ const st: AutoState = {
141
+ loader: () => Promise.resolve(mod),
142
+ kind: 'worker',
143
+ chainKey,
144
+ createWorker: opts.createWorker,
145
+ onFallback: opts.onFallback,
146
+ dispose: () => w.terminate(),
147
+ };
148
+ const argon2id: HashWasmArgon2id = async (params) => {
149
+ if (st.kind === 'worker') {
150
+ try {
151
+ return await w.argon2id(params);
152
+ } catch (e) {
153
+ if (!fallbackInline) throw terminal(e, 'worker carrier failed (chain has no inline fallback)');
154
+ // 回退:整載體翻 inline(不再試 Worker)——顯形:kind 翻面+onFallback 通知。
155
+ if (st.kind === 'worker') {
156
+ st.kind = 'inline';
157
+ w.terminate();
158
+ try { opts.onFallback?.({ from: 'worker', to: 'inline', error: e }); } catch { /* 通知面不影響派生 */ }
159
+ }
160
+ }
161
+ }
162
+ inline ??= makeInlineArgon2id();
163
+ try { return await inline(params); } catch (e) { throw terminal(e, 'inline carrier failed'); }
164
+ };
165
+ const mod = { argon2id };
166
+ return st;
167
+ }
168
+
169
+ async function katOrThrow(): Promise<void> {
170
+ let ok = false;
171
+ try { ok = await verifyArgonKat(); } catch (e) {
172
+ if (e instanceof VaultError) throw e;
173
+ throw terminal(e, 'carrier KAT threw');
174
+ }
175
+ if (!ok) throw terminal(null, 'carrier KAT mismatch (RFC 9106)');
176
+ }
177
+
178
+ /**
179
+ * 確保 Argon2id 載體可用並回報種類。冪等:同選項重呼沿用既裝載體(Worker 不重建);
180
+ * 選項(chain/createWorker/onFallback)改變=撤除重裝。首用載體必過 RFC 9106 KAT。
181
+ */
182
+ export async function ensureArgonCarrier(opts: ArgonCarrierOptions = {}): Promise<ArgonCarrierKind> {
183
+ const cur = getArgonLoader();
184
+ if (cur && (!auto || cur !== auto.loader)) {
185
+ // 呼叫端覆寫優先:自裝殘留(若有)撤除但不碰全域注入。
186
+ if (auto) { const a = auto; auto = null; a.dispose(); }
187
+ if (!verified.has(cur)) { await katOrThrow(); verified.add(cur); }
188
+ return 'injected';
189
+ }
190
+ const chain = opts.chain ?? DEFAULT_ARGON_CHAIN;
191
+ if (!Array.isArray(chain) || chain.length === 0 || !chain.every((c) => c === 'node' || c === 'worker' || c === 'inline')) {
192
+ throw new VaultError('ERR_VAULT_CONFIG', 'carrier.chain');
193
+ }
194
+ const chainKey = chain.join(',');
195
+ if (auto && cur === auto.loader && auto.chainKey === chainKey && auto.createWorker === opts.createWorker && auto.onFallback === opts.onFallback) {
196
+ return auto.kind;
197
+ }
198
+ resetArgonCarrier();
199
+ for (let i = 0; i < chain.length; i++) {
200
+ const choice = chain[i];
201
+ if (choice === 'node') {
202
+ if (!nodeArgonAvailable()) continue;
203
+ if (!nodeVerified) { await katOrThrow(); nodeVerified = true; }
204
+ return 'node';
205
+ }
206
+ if (choice === 'worker') {
207
+ if (!workerAvailable(opts)) continue;
208
+ const st = installWorker(chainKey, opts, chain.slice(i + 1).includes('inline') && wasmAvailable());
209
+ auto = st;
210
+ setArgonLoader(st.loader);
211
+ try { await katOrThrow(); } catch (e) { resetArgonCarrier(); throw e; }
212
+ return st.kind;
213
+ }
214
+ if (choice === 'inline') {
215
+ if (!wasmAvailable()) continue;
216
+ const st = installInline(chainKey, opts);
217
+ auto = st;
218
+ setArgonLoader(st.loader);
219
+ try { await katOrThrow(); } catch (e) { resetArgonCarrier(); throw e; }
220
+ return st.kind;
221
+ }
222
+ }
223
+ throw new VaultError('ERR_VAULT_KDF_UNSUPPORTED', 'no Argon2id carrier in chain [' + chainKey + ']');
224
+ }