@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.
package/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
  ```sh
19
19
  git clone https://github.com/tacet-ink/journal-core.git && cd journal-core
20
20
  npm ci --ignore-scripts
21
- npm run verify # 492 assertions, all green, run against the real modules (no mocks)
21
+ npm run verify # 521 assertions, all green, run against the real modules (no mocks)
22
22
  ```
23
23
 
24
24
  ```ts
@@ -77,7 +77,7 @@ run: `node --experimental-strip-types example.ts`
77
77
  | `src/client/keys.ts` | 品牌前綴 localStorage 命名空間 |
78
78
  | `src/server/auth.ts` | 零知識 auth 核心:PH1→PH2 login 動線、入庫格式驗證、包裹欄組成對(pair 檢查 pickKeyPackage);PH2 UNIQUE 衝突與 session 撤銷=呼叫端職責 |
79
79
  | `src/server/ladder.ts` | 密語正規化 v3 世代 ladder 表原語(ph2_ladder:以舊 ph2 查列/遷移入表 upsert 值主權威)+幽靈守衛查表接線已內建 loginRouteCore——fork 只需配 `ladderLookup` store+0011 migration(未配置=單查行為零變) |
80
- | `src/server/ratelimit.ts` | per-IP fixed-window 限流(單句 UPSERT…RETURNING;D1 計數,跨 isolate 有效) |
80
+ | `src/server/ratelimit.ts` | per-IP fixed-window 限流(單句 UPSERT…RETURNING;D1 計數,跨 isolate 有效;IPv6 鍵經 `normalizeRateIp` 聚合至 /64;IPv4-mapped 位址回點分 IPv4 鍵(=原生同 /32 鍵)) |
81
81
  | `src/server/cors.ts`/`hash.ts` | 共用 CORS/雜湊工具(`src/server/env.ts` 為內部 Env 介面,不入 exports) |
82
82
 
83
83
  發行形(0.3.0 起):`dist/`=tsc 原生 emit 的 ESM `.js`+`.d.ts`,`exports` 全部子路徑指向 dist;
@@ -124,7 +124,7 @@ jr3d/jr4d 兩入參數同輸入,域分離由 info 承載;凍結 KAT 兩 blob
124
124
 
125
125
  ```sh
126
126
  npm ci --ignore-scripts
127
- npm run verify # 492 斷言對真模組(禁鏡像;限流單句 UPSERT…RETURNING 直載真 SQLite):roundtrip/AAD 防搬移/
127
+ npm run verify # 521 斷言對真模組(禁鏡像;限流單句 UPSERT…RETURNING 直載真 SQLite):roundtrip/AAD 防搬移/
128
128
  # extractable/時代隔離/跨前綴家族隔離/payload 竄改/RFC 9106 KAT/BIP39 @scure 對照 200 組/
129
129
  # server-side(inboundCipher/validWrappedKey/pickKeyPackage/checkRate/timingSafeEq)+ pinlock 全函式
130
130
  # + 密語正規化 v3 世代(normalizePassphrase/jr4w./jr4d./PH1 v3 鹽域分離+帶內版本化舊契約向量)
@@ -136,9 +136,10 @@ npm run verify # 492 斷言對真模組(禁鏡像;限流單句 UPSERT…RE
136
136
  # +KAT 凍結向量 [17](HKDF info 域世代分離+v3/raw 入口契約四 blob)
137
137
  # +包裹前綴快檢 [13-2](錯前綴試探免付 KDF:五入口行為+計時帽+計數錨)
138
138
  # +CI 供應鏈面 [21](npm ci --ignore-scripts+dependabot runner fromJSON 分流+腳本摘除+NIT-1 錨+r2 MINOR-1 URL 直傳;
139
- # 492 帳=repo 樹——發行包內無 .github=[21] 四條 workflow 錨收斂為 1 顯性 SKIP 行→489,SKIP 顯形非靜默)
139
+ # 521 帳=repo 樹——發行包內無 .github=[21] 四條 workflow 錨收斂為 1 顯性 SKIP 行→518,SKIP 顯形非靜默)
140
140
  # +發行 tarball 治理面 [22](pack 白名單零 open-card-pr.sh)
141
141
  # +格式規格+測試向量 [26](docs/format-spec.md+docs/vectors 存在性/位元組 shape/欄位名對規格/凍結 KAT 逐字對閘)
142
+ # +高階 Vault API [27](roundtrip 三形/新舊前綴解鎖/upgrade/換密語/復原/VaultError 毒化/載體覆寫+Worker 回退鏈/vendor 孿生)
142
143
  ```
143
144
 
144
145
  - 產品碼**零執行時依賴**(WebCrypto 原語);devDependencies 僅閘用(typescript、@scure/bip39 對照、workers types)。
@@ -316,7 +317,7 @@ carrier: `node:crypto` ≥ Node 24.7 / hash-wasm in browsers, RFC 9106 test vect
316
317
  @scure/bip39), brand-namespaced key storage (`keys.ts`). Server: zero-knowledge auth core with a
317
318
  PH1→PH2 login flow, inbound cipher/package validation and paired key-package selection
318
319
  (`auth.ts`; PH2-UNIQUE conflict and session revocation are caller-owned wiring), per-IP
319
- fixed-window rate limiting on D1 (`ratelimit.ts`), shared CORS/hash utilities. Since 0.3.0 the
320
+ (IPv6 keyed per /64 prefix; IPv4-mapped addresses un-map to the native dotted IPv4 key) fixed-window rate limiting on D1 (`ratelimit.ts`), shared CORS/hash utilities. Since 0.3.0 the
320
321
  package ships `dist/` (tsc-emitted ESM `.js` + `.d.ts`); every `exports` subpath resolves to dist,
321
322
  and `src/` is included for reading and auditing only.
322
323
 
@@ -330,7 +331,7 @@ prefix family (`families.json`), known-answer vectors (RFC 9106 Argon2id, the ga
330
331
  `scripts/generate-vectors.ts` against the real primitives and committed as frozen evidence; the
331
332
  verify gate checks their presence and shape (section [26]) and does not regenerate them.
332
333
 
333
- **Verification.** `npm run verify` runs 492 assertions against the real modules (no mocks):
334
+ **Verification.** `npm run verify` runs 521 assertions against the real modules (no mocks):
334
335
  roundtrips, AAD tamper-evidence, extractability rules, era isolation, cross-prefix family
335
336
  isolation, payload tampering, RFC 9106 KAT, a 200-vector BIP39 cross-check, and the
336
337
  login ladder-guard behavior suite (external audit #8), and presence/shape checks for the format
@@ -0,0 +1,47 @@
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
+ import { type ArgonWorkerLike } from './argon-worker.ts';
19
+ export type ArgonCarrierKind = 'injected' | 'node' | 'worker' | 'inline';
20
+ export type ArgonCarrierChoice = 'node' | 'worker' | 'inline';
21
+ export type { ArgonWorkerLike };
22
+ export interface ArgonFallbackEvent {
23
+ from: 'worker';
24
+ to: 'inline';
25
+ error: unknown;
26
+ }
27
+ export interface ArgonCarrierOptions {
28
+ /** 回退鏈(預設 ['node','worker','inline'])。例:['worker']=只准 Worker(失效即拒,不回退)。 */
29
+ chain?: readonly ArgonCarrierChoice[];
30
+ /** Worker 工廠覆寫(預設瀏覽器 blob Worker;測試/特殊宿主注入轉接層)。 */
31
+ createWorker?: () => ArgonWorkerLike;
32
+ /** Worker → inline 回退通知(行為顯形;不提供=靜默回退但 currentArgonCarrier 仍翻 'inline')。 */
33
+ onFallback?: (ev: ArgonFallbackEvent) => void;
34
+ }
35
+ export declare const DEFAULT_ARGON_CHAIN: readonly ArgonCarrierChoice[];
36
+ /** 載體終端失效累計(拋出派生本體的失效才計;回退成功不計)。vault 以前後差分辨
37
+ * 「解包 null=密語錯」與「載體失效」(低階 unwrap 吞錯回 null,邊界還原真因)。 */
38
+ export declare function argonCarrierFailures(): number;
39
+ /** 現行載體(未 ensure 過且無注入、非 node=null)。 */
40
+ export declare function currentArgonCarrier(): ArgonCarrierKind | null;
41
+ /** 撤除本模組自裝載體(呼叫端 setArgonLoader 注入不動)+終止 Worker。 */
42
+ export declare function resetArgonCarrier(): void;
43
+ /**
44
+ * 確保 Argon2id 載體可用並回報種類。冪等:同選項重呼沿用既裝載體(Worker 不重建);
45
+ * 選項(chain/createWorker/onFallback)改變=撤除重裝。首用載體必過 RFC 9106 KAT。
46
+ */
47
+ export declare function ensureArgonCarrier(opts?: ArgonCarrierOptions): Promise<ArgonCarrierKind>;
@@ -0,0 +1,226 @@
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
+ import { getArgonLoader, setArgonLoader, verifyArgonKat } from './argon2.js';
19
+ import { makeWorkerArgon2id, createBlobArgonWorker } from './argon-worker.js';
20
+ import { hashWasmArgon2Factory } from './vendor/hash-wasm-argon2.js';
21
+ import { VaultError } from './vault-error.js';
22
+ export const DEFAULT_ARGON_CHAIN = ['node', 'worker', 'inline'];
23
+ function nodeArgonAvailable() {
24
+ const nodeCrypto = globalThis.process?.getBuiltinModule?.('node:crypto');
25
+ return typeof nodeCrypto?.argon2 === 'function';
26
+ }
27
+ function workerAvailable(opts) {
28
+ if (opts.createWorker)
29
+ return true;
30
+ const g = globalThis;
31
+ return typeof g.Worker === 'function' && typeof g.Blob === 'function' && typeof g.URL?.createObjectURL === 'function';
32
+ }
33
+ function wasmAvailable() {
34
+ return typeof globalThis.WebAssembly === 'object';
35
+ }
36
+ let auto = null;
37
+ let nodeVerified = false;
38
+ const verified = new WeakSet();
39
+ let failures = 0;
40
+ /** 載體終端失效累計(拋出派生本體的失效才計;回退成功不計)。vault 以前後差分辨
41
+ * 「解包 null=密語錯」與「載體失效」(低階 unwrap 吞錯回 null,邊界還原真因)。 */
42
+ export function argonCarrierFailures() {
43
+ return failures;
44
+ }
45
+ /** 現行載體(未 ensure 過且無注入、非 node=null)。 */
46
+ export function currentArgonCarrier() {
47
+ const cur = getArgonLoader();
48
+ if (cur)
49
+ return auto && cur === auto.loader ? auto.kind : 'injected';
50
+ return nodeArgonAvailable() ? 'node' : null;
51
+ }
52
+ /** 撤除本模組自裝載體(呼叫端 setArgonLoader 注入不動)+終止 Worker。 */
53
+ export function resetArgonCarrier() {
54
+ if (!auto)
55
+ return;
56
+ const a = auto;
57
+ auto = null;
58
+ if (getArgonLoader() === a.loader)
59
+ setArgonLoader(null);
60
+ a.dispose();
61
+ }
62
+ // ── inline 載體(同執行緒 vendor hash-wasm;串行) ────────────────────────────
63
+ function makeInlineArgon2id() {
64
+ let mod = null;
65
+ let queue = Promise.resolve();
66
+ return (params) => {
67
+ const job = queue.then(() => {
68
+ if (!mod)
69
+ mod = hashWasmArgon2Factory();
70
+ return mod.argon2id(params);
71
+ });
72
+ queue = job.catch(() => undefined);
73
+ return job;
74
+ };
75
+ }
76
+ function terminal(e, what) {
77
+ failures++;
78
+ return new VaultError('ERR_VAULT_KDF_UNSUPPORTED', what, { cause: e });
79
+ }
80
+ function installInline(chainKey, opts) {
81
+ const inline = makeInlineArgon2id();
82
+ const argon2id = async (params) => {
83
+ try {
84
+ return await inline(params);
85
+ }
86
+ catch (e) {
87
+ throw terminal(e, 'inline carrier failed');
88
+ }
89
+ };
90
+ const mod = { argon2id };
91
+ const st = {
92
+ loader: () => Promise.resolve(mod),
93
+ kind: 'inline',
94
+ chainKey,
95
+ createWorker: opts.createWorker,
96
+ onFallback: opts.onFallback,
97
+ dispose: () => { },
98
+ };
99
+ return st;
100
+ }
101
+ function installWorker(chainKey, opts, fallbackInline) {
102
+ const w = makeWorkerArgon2id(opts.createWorker ?? createBlobArgonWorker);
103
+ let inline = null;
104
+ const st = {
105
+ loader: () => Promise.resolve(mod),
106
+ kind: 'worker',
107
+ chainKey,
108
+ createWorker: opts.createWorker,
109
+ onFallback: opts.onFallback,
110
+ dispose: () => w.terminate(),
111
+ };
112
+ const argon2id = async (params) => {
113
+ if (st.kind === 'worker') {
114
+ try {
115
+ return await w.argon2id(params);
116
+ }
117
+ catch (e) {
118
+ if (!fallbackInline)
119
+ throw terminal(e, 'worker carrier failed (chain has no inline fallback)');
120
+ // 回退:整載體翻 inline(不再試 Worker)——顯形:kind 翻面+onFallback 通知。
121
+ if (st.kind === 'worker') {
122
+ st.kind = 'inline';
123
+ w.terminate();
124
+ try {
125
+ opts.onFallback?.({ from: 'worker', to: 'inline', error: e });
126
+ }
127
+ catch { /* 通知面不影響派生 */ }
128
+ }
129
+ }
130
+ }
131
+ inline ??= makeInlineArgon2id();
132
+ try {
133
+ return await inline(params);
134
+ }
135
+ catch (e) {
136
+ throw terminal(e, 'inline carrier failed');
137
+ }
138
+ };
139
+ const mod = { argon2id };
140
+ return st;
141
+ }
142
+ async function katOrThrow() {
143
+ let ok = false;
144
+ try {
145
+ ok = await verifyArgonKat();
146
+ }
147
+ catch (e) {
148
+ if (e instanceof VaultError)
149
+ throw e;
150
+ throw terminal(e, 'carrier KAT threw');
151
+ }
152
+ if (!ok)
153
+ throw terminal(null, 'carrier KAT mismatch (RFC 9106)');
154
+ }
155
+ /**
156
+ * 確保 Argon2id 載體可用並回報種類。冪等:同選項重呼沿用既裝載體(Worker 不重建);
157
+ * 選項(chain/createWorker/onFallback)改變=撤除重裝。首用載體必過 RFC 9106 KAT。
158
+ */
159
+ export async function ensureArgonCarrier(opts = {}) {
160
+ const cur = getArgonLoader();
161
+ if (cur && (!auto || cur !== auto.loader)) {
162
+ // 呼叫端覆寫優先:自裝殘留(若有)撤除但不碰全域注入。
163
+ if (auto) {
164
+ const a = auto;
165
+ auto = null;
166
+ a.dispose();
167
+ }
168
+ if (!verified.has(cur)) {
169
+ await katOrThrow();
170
+ verified.add(cur);
171
+ }
172
+ return 'injected';
173
+ }
174
+ const chain = opts.chain ?? DEFAULT_ARGON_CHAIN;
175
+ if (!Array.isArray(chain) || chain.length === 0 || !chain.every((c) => c === 'node' || c === 'worker' || c === 'inline')) {
176
+ throw new VaultError('ERR_VAULT_CONFIG', 'carrier.chain');
177
+ }
178
+ const chainKey = chain.join(',');
179
+ if (auto && cur === auto.loader && auto.chainKey === chainKey && auto.createWorker === opts.createWorker && auto.onFallback === opts.onFallback) {
180
+ return auto.kind;
181
+ }
182
+ resetArgonCarrier();
183
+ for (let i = 0; i < chain.length; i++) {
184
+ const choice = chain[i];
185
+ if (choice === 'node') {
186
+ if (!nodeArgonAvailable())
187
+ continue;
188
+ if (!nodeVerified) {
189
+ await katOrThrow();
190
+ nodeVerified = true;
191
+ }
192
+ return 'node';
193
+ }
194
+ if (choice === 'worker') {
195
+ if (!workerAvailable(opts))
196
+ continue;
197
+ const st = installWorker(chainKey, opts, chain.slice(i + 1).includes('inline') && wasmAvailable());
198
+ auto = st;
199
+ setArgonLoader(st.loader);
200
+ try {
201
+ await katOrThrow();
202
+ }
203
+ catch (e) {
204
+ resetArgonCarrier();
205
+ throw e;
206
+ }
207
+ return st.kind;
208
+ }
209
+ if (choice === 'inline') {
210
+ if (!wasmAvailable())
211
+ continue;
212
+ const st = installInline(chainKey, opts);
213
+ auto = st;
214
+ setArgonLoader(st.loader);
215
+ try {
216
+ await katOrThrow();
217
+ }
218
+ catch (e) {
219
+ resetArgonCarrier();
220
+ throw e;
221
+ }
222
+ return st.kind;
223
+ }
224
+ }
225
+ throw new VaultError('ERR_VAULT_KDF_UNSUPPORTED', 'no Argon2id carrier in chain [' + chainKey + ']');
226
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * argon-worker.ts — Argon2id Web Worker 載體:登入不卡畫面。
3
+ *
4
+ * Worker 本體=內嵌 blob 形(免 bundle 步):vendor 工廠 hashWasmArgon2Factory 的原文
5
+ *(Function#toString)+訊息膠水組成 Worker 源碼字串 → Blob → object URL → new Worker。
6
+ * 協定:main → { id, password, salt, iterations, parallelism, memorySize, hashLength };
7
+ * worker → { id, ok: true, out: Uint8Array } | { id, ok: false, error }。
8
+ * 串行:main 端單一佇列(hash-wasm Argon2 禁並行——argon2.ts deriveKek2Argon 註同理),
9
+ * worker 端亦以 promise 鏈串行(雙防線)。
10
+ * 失敗態:建構拋出/error 事件/ok:false 回應=本載體失效(dead),佇列內與後續請求一律 reject——
11
+ * 回退與否由 argon-auto.ts 回退鏈決定(本檔不自帶回退,行為顯形)。
12
+ */
13
+ import type { HashWasmArgon2id } from './argon2.ts';
14
+ /** Worker 最小介面(瀏覽器 Worker 直接相容;node worker_threads 等可包轉接層注入)。 */
15
+ export interface ArgonWorkerLike {
16
+ postMessage(message: unknown, transfer?: Transferable[]): void;
17
+ terminate(): void;
18
+ onmessage: ((ev: {
19
+ data: unknown;
20
+ }) => void) | null;
21
+ onerror: ((ev: unknown) => void) | null;
22
+ }
23
+ /** Worker 源碼字串(self.onmessage 形;瀏覽器 DedicatedWorkerGlobalScope 語意)。 */
24
+ export declare function argonWorkerSource(): string;
25
+ /** 瀏覽器預設 Worker 工廠:blob URL(CSP 需 worker-src blob:;不允許時建構拋出或 error 事件→回退鏈接手)。 */
26
+ export declare function createBlobArgonWorker(): ArgonWorkerLike;
27
+ /** Worker 載體:回 hash-wasm 同形 argon2id(argon2.ts ArgonLoader 契約)+terminate。 */
28
+ export declare function makeWorkerArgon2id(createWorker: () => ArgonWorkerLike): {
29
+ argon2id: HashWasmArgon2id;
30
+ terminate(): void;
31
+ };
@@ -0,0 +1,147 @@
1
+ /**
2
+ * argon-worker.ts — Argon2id Web Worker 載體:登入不卡畫面。
3
+ *
4
+ * Worker 本體=內嵌 blob 形(免 bundle 步):vendor 工廠 hashWasmArgon2Factory 的原文
5
+ *(Function#toString)+訊息膠水組成 Worker 源碼字串 → Blob → object URL → new Worker。
6
+ * 協定:main → { id, password, salt, iterations, parallelism, memorySize, hashLength };
7
+ * worker → { id, ok: true, out: Uint8Array } | { id, ok: false, error }。
8
+ * 串行:main 端單一佇列(hash-wasm Argon2 禁並行——argon2.ts deriveKek2Argon 註同理),
9
+ * worker 端亦以 promise 鏈串行(雙防線)。
10
+ * 失敗態:建構拋出/error 事件/ok:false 回應=本載體失效(dead),佇列內與後續請求一律 reject——
11
+ * 回退與否由 argon-auto.ts 回退鏈決定(本檔不自帶回退,行為顯形)。
12
+ */
13
+ import { hashWasmArgon2Factory } from './vendor/hash-wasm-argon2.js';
14
+ /** Worker 源碼字串(self.onmessage 形;瀏覽器 DedicatedWorkerGlobalScope 語意)。 */
15
+ export function argonWorkerSource() {
16
+ return [
17
+ "'use strict';",
18
+ 'const factory = (' + hashWasmArgon2Factory.toString() + ');',
19
+ 'let mod = null;',
20
+ 'let q = Promise.resolve();',
21
+ 'self.onmessage = (ev) => {',
22
+ ' const m = ev.data;',
23
+ ' q = q.then(async () => {',
24
+ ' try {',
25
+ ' if (!mod) mod = factory();',
26
+ " const r = await mod.argon2id({ password: m.password, salt: m.salt, iterations: m.iterations, parallelism: m.parallelism, memorySize: m.memorySize, hashLength: m.hashLength, outputType: 'binary' });",
27
+ ' const out = new Uint8Array(r);',
28
+ ' self.postMessage({ id: m.id, ok: true, out }, [out.buffer]);',
29
+ ' } catch (e) {',
30
+ ' self.postMessage({ id: m.id, ok: false, error: String((e && e.message) || e) });',
31
+ ' }',
32
+ ' });',
33
+ '};',
34
+ '',
35
+ ].join('\n');
36
+ }
37
+ /** 瀏覽器預設 Worker 工廠:blob URL(CSP 需 worker-src blob:;不允許時建構拋出或 error 事件→回退鏈接手)。 */
38
+ export function createBlobArgonWorker() {
39
+ const url = URL.createObjectURL(new Blob([argonWorkerSource()], { type: 'text/javascript' }));
40
+ let w;
41
+ try {
42
+ w = new Worker(url);
43
+ }
44
+ catch (e) {
45
+ URL.revokeObjectURL(url);
46
+ throw e;
47
+ }
48
+ const like = {
49
+ postMessage: (m, t) => w.postMessage(m, t ?? []),
50
+ terminate: () => { w.terminate(); URL.revokeObjectURL(url); },
51
+ onmessage: null,
52
+ onerror: null,
53
+ };
54
+ w.onmessage = (ev) => like.onmessage?.({ data: ev.data });
55
+ w.onerror = (ev) => like.onerror?.(ev);
56
+ w.onmessageerror = (ev) => like.onerror?.(ev);
57
+ return like;
58
+ }
59
+ /** Worker 載體:回 hash-wasm 同形 argon2id(argon2.ts ArgonLoader 契約)+terminate。 */
60
+ export function makeWorkerArgon2id(createWorker) {
61
+ let worker = null;
62
+ let dead = null;
63
+ let seq = 0;
64
+ const pending = new Map();
65
+ let queue = Promise.resolve();
66
+ const die = (e) => {
67
+ if (!dead)
68
+ dead = e ?? new Error('ERR_ARGON_WORKER_DEAD');
69
+ for (const p of pending.values())
70
+ p.reject(dead);
71
+ pending.clear();
72
+ if (worker) {
73
+ try {
74
+ worker.terminate();
75
+ }
76
+ catch { /* 已終止 */ }
77
+ }
78
+ worker = null;
79
+ };
80
+ const boot = () => {
81
+ if (worker)
82
+ return worker;
83
+ const w = createWorker();
84
+ w.onmessage = (ev) => {
85
+ const d = ev.data;
86
+ const p = d && typeof d.id === 'number' ? pending.get(d.id) : undefined;
87
+ if (!p || !d)
88
+ return;
89
+ pending.delete(d.id);
90
+ if (d.ok && d.out instanceof Uint8Array)
91
+ p.resolve(d.out);
92
+ else {
93
+ const err = new Error('ERR_ARGON_WORKER: ' + (d.error ?? 'bad response'));
94
+ p.reject(err);
95
+ die(err); // ok:false=worker 內 wasm 失效(CSP wasm-eval/記憶體)——整載體 dead,交回退鏈
96
+ }
97
+ };
98
+ w.onerror = (ev) => die(new Error('ERR_ARGON_WORKER_ERROR', { cause: ev }));
99
+ worker = w;
100
+ return w;
101
+ };
102
+ const run = (params) => new Promise((resolve, reject) => {
103
+ if (dead) {
104
+ reject(dead);
105
+ return;
106
+ }
107
+ let w;
108
+ try {
109
+ w = boot();
110
+ }
111
+ catch (e) {
112
+ die(e);
113
+ reject(dead);
114
+ return;
115
+ }
116
+ const id = ++seq;
117
+ pending.set(id, { resolve, reject });
118
+ try {
119
+ w.postMessage({
120
+ id,
121
+ password: params.password,
122
+ salt: params.salt,
123
+ iterations: params.iterations,
124
+ parallelism: params.parallelism,
125
+ memorySize: params.memorySize,
126
+ hashLength: params.hashLength,
127
+ });
128
+ }
129
+ catch (e) {
130
+ pending.delete(id);
131
+ die(e);
132
+ reject(dead);
133
+ }
134
+ });
135
+ const argon2id = (params) => {
136
+ const job = queue.then(() => run(params));
137
+ queue = job.catch(() => undefined);
138
+ return job.then((out) => {
139
+ if (params.outputType === 'binary')
140
+ return out;
141
+ if (params.outputType === 'hex')
142
+ return Array.from(out).map((b) => b.toString(16).padStart(2, '0')).join('');
143
+ throw new Error('ERR_ARGON_WORKER_OUTPUT_TYPE');
144
+ });
145
+ };
146
+ return { argon2id, terminate: () => die(new Error('ERR_ARGON_WORKER_TERMINATED')) };
147
+ }
@@ -35,6 +35,17 @@
35
35
  */
36
36
  /** RFC 9106 無 secret/ad 標準向量(Argon2id v=0x13, t=3, m=32, p=4, T=32, pwd=32B 0x01, salt=16B 0x00)。 */
37
37
  export declare const ARGON_RFC9106_EXPECTED = "72d2a36fd5c266bcc96121b24937bc253338cdfcbd273713655748c54b4dd503";
38
+ /** RFC 9106 KAT 參數單一真相(verifyArgonKat/scripts/generate-vectors.ts 寫 kat.json/閘 [26] 對帳共用)。
39
+ * tagLen=派生本體固定輸出長(ARGON_TAG_LEN);pwd=passwordLen 位元組 passwordByte、salt=saltLen 位元組 0x00。 */
40
+ export declare const ARGON_RFC9106_PARAMS: Readonly<{
41
+ readonly memoryKib: 32;
42
+ readonly iterations: 3;
43
+ readonly parallelism: 4;
44
+ readonly tagLen: 32;
45
+ readonly passwordByte: 1;
46
+ readonly passwordLen: 32;
47
+ readonly saltLen: 16;
48
+ }>;
38
49
  /**
39
50
  * PH1 v2 固定域鹽(登入憑證 Argon2id 派生,2026-09-10 安全路線第 4 步)。
40
51
  *
@@ -94,7 +105,7 @@ export interface Argon3Config {
94
105
  /** 密語正規化 v3 世代雙因子包裹前綴(jr4d.,pass 段正規化+PIN 段照舊)。未配置 = 拒絕。 */
95
106
  wrapDual4?: string;
96
107
  }
97
- interface HashWasmArgon2id {
108
+ export interface HashWasmArgon2id {
98
109
  (params: {
99
110
  password: string | Uint8Array;
100
111
  salt: Uint8Array;
@@ -105,16 +116,18 @@ interface HashWasmArgon2id {
105
116
  outputType: 'binary' | 'hex' | 'encoded';
106
117
  }): Promise<Uint8Array | string>;
107
118
  }
108
- type ArgonLoader = () => Promise<{
119
+ export type ArgonLoader = () => Promise<{
109
120
  argon2id: HashWasmArgon2id;
110
121
  }>;
111
122
  /** 載體注入(瀏覽器端由接線層指定 hash-wasm;node 端不注入走 node:crypto 原生)。 */
112
123
  export declare function setArgonLoader(loader: ArgonLoader | null): void;
124
+ /** 現行注入載體(唯讀查詢;未注入=null)。argon-auto.ts 據此判「呼叫端覆寫優先」——0.3.1 新增 export。 */
125
+ export declare function getArgonLoader(): ArgonLoader | null;
113
126
  /** 標準向量 KAT:驗證當前載體的 Argon2id 實作正確性(驗證閘起手式)。 */
114
127
  export declare function verifyArgonKat(): Promise<boolean>;
115
128
  /** KEK = Argon2id(pass, salt, m=64MiB, t=3, p=1) → AES-GCM key(nonextractable)。 */
116
129
  export declare function deriveKekArgon(password: string, salt: Uint8Array): Promise<CryptoKey>;
117
- /** jr3w. 包裹:payload = salt[16] ‖ iv[12] ‖ AES-GCM(KEK, hex(noteKey), aad='notekey');salt 由呼叫端存 users.salt。 */
130
+ /** jr3w. 包裹:payload = iv[12] ‖ AES-GCM(KEK, hex(noteKey), aad='notekey')(鹽外置家族,嚴格 92B);salt 不入 payload,由呼叫端存 users.salt。 */
118
131
  export declare function wrapNoteKey3(cfg: Argon3Config, noteKey: CryptoKey, passphrase: string): Promise<{
119
132
  wrapped: string;
120
133
  salt: string;
@@ -125,7 +138,7 @@ export declare function wrapNoteKey3(cfg: Argon3Config, noteKey: CryptoKey, pass
125
138
  * 家族守衛(wrap3 未配置回 null)、salt1 hex-形檢查與 KEK 派生——payload 形是鹽外置家族(iv‖ct),
126
139
  * own 92B 檢已隨收口摘除(jr3s 同;鹽內嵌族 own 108B 保留)。 */
127
140
  export declare function unwrapNoteKey3(cfg: Argon3Config, wrapped: string, passphrase: string, saltHex: string): Promise<CryptoKey | null>;
128
- /** jr4w. 包裹(v3 密語):payload = salt[16] ‖ iv[12] ‖ GCM(KEK, hex(noteKey), aad='notekey');salt 呼叫端存 users.salt。 */
141
+ /** jr4w. 包裹(v3 密語):payload = iv[12] ‖ GCM(KEK, hex(noteKey), aad='notekey')(鹽外置家族,嚴格 92B);salt 不入 payload,呼叫端存 users.salt。 */
129
142
  export declare function wrapNoteKey4(cfg: Argon3Config, noteKey: CryptoKey, passphrase: string): Promise<{
130
143
  wrapped: string;
131
144
  salt: string;
@@ -163,4 +176,3 @@ export declare function wrapNoteKeyShare3(cfg: Argon3Config, noteKey: CryptoKey,
163
176
  export declare function unwrapNoteKeyShare3(cfg: Argon3Config, wrapped: string, sharePass: string, saltHex: string): Promise<CryptoKey | null>;
164
177
  /** jr4d. 解包(重取 salt1 面):pinSalt 內嵌自描述,salt1 由呼叫端重取後帶入(ladder 遷移後的新値);任何不符回 null,不拋。 */
165
178
  export declare function unwrapNoteKeyDual4WithSalt(cfg: Argon3Config, wrapped: string, passphrase: string, pin: string, salt1Hex: string): Promise<CryptoKey | null>;
166
- export {};
@@ -45,6 +45,11 @@ function deriveInput(passphrase) {
45
45
  }
46
46
  /** RFC 9106 無 secret/ad 標準向量(Argon2id v=0x13, t=3, m=32, p=4, T=32, pwd=32B 0x01, salt=16B 0x00)。 */
47
47
  export const ARGON_RFC9106_EXPECTED = '72d2a36fd5c266bcc96121b24937bc253338cdfcbd273713655748c54b4dd503';
48
+ /** RFC 9106 KAT 參數單一真相(verifyArgonKat/scripts/generate-vectors.ts 寫 kat.json/閘 [26] 對帳共用)。
49
+ * tagLen=派生本體固定輸出長(ARGON_TAG_LEN);pwd=passwordLen 位元組 passwordByte、salt=saltLen 位元組 0x00。 */
50
+ export const ARGON_RFC9106_PARAMS = Object.freeze({
51
+ memoryKib: 32, iterations: 3, parallelism: 4, tagLen: 32, passwordByte: 0x01, passwordLen: 32, saltLen: 16,
52
+ });
48
53
  /**
49
54
  * PH1 v2 固定域鹽(登入憑證 Argon2id 派生,2026-09-10 安全路線第 4 步)。
50
55
  *
@@ -103,6 +108,10 @@ let injectedLoader = null;
103
108
  export function setArgonLoader(loader) {
104
109
  injectedLoader = loader;
105
110
  }
111
+ /** 現行注入載體(唯讀查詢;未注入=null)。argon-auto.ts 據此判「呼叫端覆寫優先」——0.3.1 新增 export。 */
112
+ export function getArgonLoader() {
113
+ return injectedLoader;
114
+ }
106
115
  /**
107
116
  * Argon2id 派生 32B。雙載體:injectedLoader(瀏覽器 hash-wasm)優先;
108
117
  * 無注入時走 node:crypto 原生(僅 node 執行環境可達)。皆不可得即 throw(禁 fallback)。
@@ -141,7 +150,8 @@ async function deriveArgon2id(password, salt, memoryKib, passes, parallelism) {
141
150
  }
142
151
  /** 標準向量 KAT:驗證當前載體的 Argon2id 實作正確性(驗證閘起手式)。 */
143
152
  export async function verifyArgonKat() {
144
- const raw = await deriveArgon2id(new Uint8Array(32).fill(1), new Uint8Array(16), 32, 3, 4);
153
+ const k = ARGON_RFC9106_PARAMS;
154
+ const raw = await deriveArgon2id(new Uint8Array(k.passwordLen).fill(k.passwordByte), new Uint8Array(k.saltLen), k.memoryKib, k.iterations, k.parallelism);
145
155
  return toHex(raw) === ARGON_RFC9106_EXPECTED;
146
156
  }
147
157
  // ── jr3w. 單因子包裹 ──────────────────────────────────────────────────────────
@@ -152,7 +162,7 @@ async function argonKekFromRaw(raw) {
152
162
  export async function deriveKekArgon(password, salt) {
153
163
  return argonKekFromRaw(await deriveArgon2id(password, salt, ARGON_MEMORY_KIB, ARGON_ITERATIONS, ARGON_PARALLELISM));
154
164
  }
155
- /** jr3w. 包裹:payload = salt[16] ‖ iv[12] ‖ AES-GCM(KEK, hex(noteKey), aad='notekey');salt 由呼叫端存 users.salt。 */
165
+ /** jr3w. 包裹:payload = iv[12] ‖ AES-GCM(KEK, hex(noteKey), aad='notekey')(鹽外置家族,嚴格 92B);salt 不入 payload,由呼叫端存 users.salt。 */
156
166
  export async function wrapNoteKey3(cfg, noteKey, passphrase) {
157
167
  if (!cfg.wrap3)
158
168
  throw new Error('ERR_JR3W_NOT_CONFIGURED');
@@ -189,7 +199,7 @@ export async function unwrapNoteKey3(cfg, wrapped, passphrase, saltHex) {
189
199
  // 92B、AAD 'notekey'、嚴格長度全在 openNoteKey 本體),唯一差異=入口吃 normalizePassphrase(pass)
190
200
  // 與 cfg.wrap4 前綴。舊 payload(jr1w./jr3w.)永遠可解=raw 派生;新 payload 只由 jr4w. 產生。
191
201
  // pinSaltPrefix 沿用 cfg 既有欄(Argon3 家族 PIN 鹽域,非 jr3d. 專屬)。
192
- /** jr4w. 包裹(v3 密語):payload = salt[16] ‖ iv[12] ‖ GCM(KEK, hex(noteKey), aad='notekey');salt 呼叫端存 users.salt。 */
202
+ /** jr4w. 包裹(v3 密語):payload = iv[12] ‖ GCM(KEK, hex(noteKey), aad='notekey')(鹽外置家族,嚴格 92B);salt 不入 payload,呼叫端存 users.salt。 */
193
203
  export async function wrapNoteKey4(cfg, noteKey, passphrase) {
194
204
  if (!cfg.wrap4)
195
205
  throw new Error('ERR_JR4W_NOT_CONFIGURED');
@@ -24,6 +24,7 @@
24
24
  * import 當下就必須 extractable=true。noteKey 全部產生路徑(generate/unwrap×3)
25
25
  * 一律顯式 true;KEK/guest key 保持 nonextractable(只加解密、永不 export)。
26
26
  */
27
+ import type { KeyStore } from './keys.js';
27
28
  export interface NoteCryptoConfig {
28
29
  /** guest 時代 KDF 前綴,如 'journal-note-u1'。 */
29
30
  guestKdfPrefix: string;
@@ -72,7 +73,7 @@ export interface NoteCryptoConfig {
72
73
  /** 本機 IDB 密文前綴(jr1d.,notes store stored 形)。未配置 = local API 拒絕(未配置行為不變)。 */
73
74
  cipherLocal?: string;
74
75
  /** localStorage key store(品牌前綴由 keys.ts 管理)。 */
75
- store: import('./keys').KeyStore;
76
+ store: KeyStore;
76
77
  }
77
78
  export declare const PBKDF2_ITERATIONS = 600000;
78
79
  export declare const PIN_PBKDF2_ITERATIONS = 2000000;