@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.
@@ -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
+
14
+ import type { HashWasmArgon2id } from './argon2.ts';
15
+ import { hashWasmArgon2Factory } from './vendor/hash-wasm-argon2.ts';
16
+
17
+ /** Worker 最小介面(瀏覽器 Worker 直接相容;node worker_threads 等可包轉接層注入)。 */
18
+ export interface ArgonWorkerLike {
19
+ postMessage(message: unknown, transfer?: Transferable[]): void;
20
+ terminate(): void;
21
+ onmessage: ((ev: { data: unknown }) => void) | null;
22
+ onerror: ((ev: unknown) => void) | null;
23
+ }
24
+
25
+ /** Worker 源碼字串(self.onmessage 形;瀏覽器 DedicatedWorkerGlobalScope 語意)。 */
26
+ export function argonWorkerSource(): string {
27
+ return [
28
+ "'use strict';",
29
+ 'const factory = (' + hashWasmArgon2Factory.toString() + ');',
30
+ 'let mod = null;',
31
+ 'let q = Promise.resolve();',
32
+ 'self.onmessage = (ev) => {',
33
+ ' const m = ev.data;',
34
+ ' q = q.then(async () => {',
35
+ ' try {',
36
+ ' if (!mod) mod = factory();',
37
+ " 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' });",
38
+ ' const out = new Uint8Array(r);',
39
+ ' self.postMessage({ id: m.id, ok: true, out }, [out.buffer]);',
40
+ ' } catch (e) {',
41
+ ' self.postMessage({ id: m.id, ok: false, error: String((e && e.message) || e) });',
42
+ ' }',
43
+ ' });',
44
+ '};',
45
+ '',
46
+ ].join('\n');
47
+ }
48
+
49
+ /** 瀏覽器預設 Worker 工廠:blob URL(CSP 需 worker-src blob:;不允許時建構拋出或 error 事件→回退鏈接手)。 */
50
+ export function createBlobArgonWorker(): ArgonWorkerLike {
51
+ const url = URL.createObjectURL(new Blob([argonWorkerSource()], { type: 'text/javascript' }));
52
+ let w: Worker;
53
+ try {
54
+ w = new Worker(url);
55
+ } catch (e) {
56
+ URL.revokeObjectURL(url);
57
+ throw e;
58
+ }
59
+ const like: ArgonWorkerLike = {
60
+ postMessage: (m, t) => w.postMessage(m, t ?? []),
61
+ terminate: () => { w.terminate(); URL.revokeObjectURL(url); },
62
+ onmessage: null,
63
+ onerror: null,
64
+ };
65
+ w.onmessage = (ev) => like.onmessage?.({ data: ev.data });
66
+ w.onerror = (ev) => like.onerror?.(ev);
67
+ w.onmessageerror = (ev) => like.onerror?.(ev);
68
+ return like;
69
+ }
70
+
71
+ interface Pending {
72
+ resolve: (v: Uint8Array) => void;
73
+ reject: (e: unknown) => void;
74
+ }
75
+
76
+ /** Worker 載體:回 hash-wasm 同形 argon2id(argon2.ts ArgonLoader 契約)+terminate。 */
77
+ export function makeWorkerArgon2id(createWorker: () => ArgonWorkerLike): { argon2id: HashWasmArgon2id; terminate(): void } {
78
+ let worker: ArgonWorkerLike | null = null;
79
+ let dead: unknown = null;
80
+ let seq = 0;
81
+ const pending = new Map<number, Pending>();
82
+ let queue: Promise<unknown> = Promise.resolve();
83
+
84
+ const die = (e: unknown): void => {
85
+ if (!dead) dead = e ?? new Error('ERR_ARGON_WORKER_DEAD');
86
+ for (const p of pending.values()) p.reject(dead);
87
+ pending.clear();
88
+ if (worker) { try { worker.terminate(); } catch { /* 已終止 */ } }
89
+ worker = null;
90
+ };
91
+
92
+ const boot = (): ArgonWorkerLike => {
93
+ if (worker) return worker;
94
+ const w = createWorker();
95
+ w.onmessage = (ev) => {
96
+ const d = ev.data as { id?: number; ok?: boolean; out?: Uint8Array; error?: string } | null;
97
+ const p = d && typeof d.id === 'number' ? pending.get(d.id) : undefined;
98
+ if (!p || !d) return;
99
+ pending.delete(d.id!);
100
+ if (d.ok && d.out instanceof Uint8Array) p.resolve(d.out);
101
+ else {
102
+ const err = new Error('ERR_ARGON_WORKER: ' + (d.error ?? 'bad response'));
103
+ p.reject(err);
104
+ die(err); // ok:false=worker 內 wasm 失效(CSP wasm-eval/記憶體)——整載體 dead,交回退鏈
105
+ }
106
+ };
107
+ w.onerror = (ev) => die(new Error('ERR_ARGON_WORKER_ERROR', { cause: ev }));
108
+ worker = w;
109
+ return w;
110
+ };
111
+
112
+ const run = (params: Parameters<HashWasmArgon2id>[0]): Promise<Uint8Array> =>
113
+ new Promise<Uint8Array>((resolve, reject) => {
114
+ if (dead) { reject(dead); return; }
115
+ let w: ArgonWorkerLike;
116
+ try { w = boot(); } catch (e) { die(e); reject(dead); return; }
117
+ const id = ++seq;
118
+ pending.set(id, { resolve, reject });
119
+ try {
120
+ w.postMessage({
121
+ id,
122
+ password: params.password,
123
+ salt: params.salt,
124
+ iterations: params.iterations,
125
+ parallelism: params.parallelism,
126
+ memorySize: params.memorySize,
127
+ hashLength: params.hashLength,
128
+ });
129
+ } catch (e) {
130
+ pending.delete(id);
131
+ die(e);
132
+ reject(dead);
133
+ }
134
+ });
135
+
136
+ const argon2id: HashWasmArgon2id = (params) => {
137
+ const job = queue.then(() => run(params));
138
+ queue = job.catch(() => undefined);
139
+ return job.then((out) => {
140
+ if (params.outputType === 'binary') return out;
141
+ if (params.outputType === 'hex') return Array.from(out).map((b) => b.toString(16).padStart(2, '0')).join('');
142
+ throw new Error('ERR_ARGON_WORKER_OUTPUT_TYPE');
143
+ });
144
+ };
145
+
146
+ return { argon2id, terminate: () => die(new Error('ERR_ARGON_WORKER_TERMINATED')) };
147
+ }
@@ -140,7 +140,7 @@ export interface Argon3Config {
140
140
  wrapDual4?: string;
141
141
  }
142
142
 
143
- interface HashWasmArgon2id {
143
+ export interface HashWasmArgon2id {
144
144
  (params: {
145
145
  password: string | Uint8Array;
146
146
  salt: Uint8Array;
@@ -152,7 +152,7 @@ interface HashWasmArgon2id {
152
152
  }): Promise<Uint8Array | string>;
153
153
  }
154
154
 
155
- type ArgonLoader = () => Promise<{ argon2id: HashWasmArgon2id }>;
155
+ export type ArgonLoader = () => Promise<{ argon2id: HashWasmArgon2id }>;
156
156
 
157
157
  let injectedLoader: ArgonLoader | null = null;
158
158
 
@@ -161,6 +161,11 @@ export function setArgonLoader(loader: ArgonLoader | null): void {
161
161
  injectedLoader = loader;
162
162
  }
163
163
 
164
+ /** 現行注入載體(唯讀查詢;未注入=null)。argon-auto.ts 據此判「呼叫端覆寫優先」——0.3.1 新增 export。 */
165
+ export function getArgonLoader(): ArgonLoader | null {
166
+ return injectedLoader;
167
+ }
168
+
164
169
  /**
165
170
  * Argon2id 派生 32B。雙載體:injectedLoader(瀏覽器 hash-wasm)優先;
166
171
  * 無注入時走 node:crypto 原生(僅 node 執行環境可達)。皆不可得即 throw(禁 fallback)。
@@ -0,0 +1,51 @@
1
+ /**
2
+ * vault-error.ts — 高階 Vault API 的型別化錯誤家族。
3
+ *
4
+ * 低階原語面不動(throw 'ERR_*' 字串錯誤+unwrap 恆 null 的混態照舊)——只在 vault.ts/
5
+ * argon-auto.ts 邊界轉譯:高階 API 一律 throw VaultError,呼叫端以 `code` 分流,不比對 message。
6
+ * 失敗面刻意不細分「密語錯」與「密文竄改」(同一 GCM 失敗——細分=oracle)。
7
+ */
8
+
9
+ export type VaultErrorCode =
10
+ /** 配置缺欄/畸形(app、cipher、argon.wrap4、recovery 兩欄一體…)——opt-in 未配置即拒。 */
11
+ | 'ERR_VAULT_CONFIG'
12
+ /** serverRecord 缺 wrapped/salt 欄或形不符(不猜)。 */
13
+ | 'ERR_VAULT_BAD_RECORD'
14
+ /** wrapped 前綴不屬任何已配置家族(未知世代/他產品前綴)。 */
15
+ | 'ERR_VAULT_WRAP_UNKNOWN'
16
+ /** 家族辨識成功但解包失敗:密語/PIN 錯或包裹竄改(刻意不分)。 */
17
+ | 'ERR_VAULT_WRAP_MISMATCH'
18
+ /** 雙因子家族(jr2w./jr3d./jr4d.)缺 PIN。 */
19
+ | 'ERR_VAULT_PIN_REQUIRED'
20
+ /** 密語空值/非字串。 */
21
+ | 'ERR_VAULT_BAD_PASSPHRASE'
22
+ /** PIN 正規化後為空。 */
23
+ | 'ERR_VAULT_BAD_PIN'
24
+ /** Argon2id 載體缺席或失效(zero-fallback:禁 PBKDF2/zeros 兜底)。 */
25
+ | 'ERR_VAULT_KDF_UNSUPPORTED'
26
+ /** recordId 非字串或空。 */
27
+ | 'ERR_VAULT_BAD_RECORD_ID'
28
+ /** encrypt 資料形不支援(undefined、BigInt、循環 JSON、非 Uint8Array 的 ArrayBuffer 族…)。 */
29
+ | 'ERR_VAULT_BAD_DATA'
30
+ /** 密文 blob 形不符(前綴/base64/長度/型別標記)。 */
31
+ | 'ERR_VAULT_BAD_BLOB'
32
+ /** 密文解密失敗:recordId(AAD)不符、金鑰不符或密文竄改(刻意不分)。 */
33
+ | 'ERR_VAULT_DECRYPT'
34
+ /** 復原套件形不符(recToken 非 hex64、wrappedRec 前綴不符)或解包失敗。 */
35
+ | 'ERR_VAULT_RECOVERY'
36
+ /** 未預期的低階錯誤(cause 帶原錯)。 */
37
+ | 'ERR_VAULT_INTERNAL';
38
+
39
+ export class VaultError extends Error {
40
+ readonly code: VaultErrorCode;
41
+ constructor(code: VaultErrorCode, message?: string, options?: { cause?: unknown }) {
42
+ super(message ? code + ': ' + message : code, options);
43
+ this.name = 'VaultError';
44
+ this.code = code;
45
+ }
46
+ }
47
+
48
+ /** 型別守衛:`isVaultError(e)` 或指定碼 `isVaultError(e, 'ERR_VAULT_DECRYPT')`。 */
49
+ export function isVaultError(e: unknown, code?: VaultErrorCode): e is VaultError {
50
+ return e instanceof VaultError && (code === undefined || e.code === code);
51
+ }