@tacet-ink/journal-core 0.3.3 → 0.3.5
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 +146 -36
- package/dist/client/bip39.d.ts +33 -0
- package/dist/client/bip39.js +12 -0
- package/dist/client/note-crypto.js +6 -0
- package/dist/client/vault-error.d.ts +4 -1
- package/dist/client/vault-error.js +9 -2
- package/dist/client/vault.d.ts +94 -15
- package/dist/client/vault.js +155 -27
- package/package.json +1 -6
- package/scripts/prepare-core-pkg.cjs +0 -74
- package/scripts/verify-core-crypto.ts +0 -3387
- package/src/client/argon-auto.ts +0 -224
- package/src/client/argon-worker.ts +0 -147
- package/src/client/argon2.ts +0 -532
- package/src/client/bip39.ts +0 -145
- package/src/client/keys.ts +0 -27
- package/src/client/note-crypto.ts +0 -719
- package/src/client/pinlock.ts +0 -110
- package/src/client/vault-error.ts +0 -51
- package/src/client/vault.ts +0 -486
- package/src/client/vendor/hash-wasm-argon2.ts +0 -27
- package/src/client/wordlist.ts +0 -264
- package/src/index.ts +0 -104
- package/src/server/auth.ts +0 -225
- package/src/server/cors.ts +0 -22
- package/src/server/env.ts +0 -13
- package/src/server/hash.ts +0 -28
- package/src/server/ladder.ts +0 -76
- package/src/server/ratelimit.ts +0 -124
package/dist/client/vault.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* vault.ts — 高階 Vault API:createVault/unlockVault/recoverVault → VaultHandle。
|
|
2
|
+
* vault.ts — 高階 Vault API:createVault/unlockVault/recoverVault → VaultHandle+零設定前綴推導 resolver。
|
|
3
3
|
*
|
|
4
4
|
* 定位:低階原語(note-crypto.ts/argon2.ts)的**呼叫端**——additive-only,零改動原語語意。
|
|
5
5
|
* 一個 vault=一把隨機 256-bit noteKey(generateNoteKey)+伺服器存的包裹欄(wrapped+salt)
|
|
@@ -20,15 +20,86 @@
|
|
|
20
20
|
* tag:0x73 's'=字串(body=UTF-8)/0x6a 'j'=JSON(body=UTF-8(JSON.stringify))/0x62 'b'=位元組(body=原樣)。
|
|
21
21
|
*/
|
|
22
22
|
import { generateNoteKey, generateRecToken, recTokenHash, wrapNoteKeyWithRecToken, unwrapNoteKeyWithRecToken, unwrapNoteKey, unwrapNoteKeyDual, normalizePin, b64, unb64, IV_LEN, } from './note-crypto.js';
|
|
23
|
-
import { wrapNoteKey4, unwrapNoteKey4, wrapNoteKeyDual4, unwrapNoteKeyDual4, unwrapNoteKey3, unwrapNoteKeyDual3, } from './argon2.js';
|
|
23
|
+
import { wrapNoteKey4, unwrapNoteKey4, wrapNoteKeyDual4, unwrapNoteKeyDual4, unwrapNoteKey3, unwrapNoteKeyDual3, derivePh1ArgonV3, } from './argon2.js';
|
|
24
|
+
import { recTokenToWords, wordsToRecToken, normalizeWordInput } from './bip39.js';
|
|
24
25
|
import { VaultError, isVaultError } from './vault-error.js';
|
|
25
26
|
import { ensureArgonCarrier, argonCarrierFailures } from './argon-auto.js';
|
|
26
27
|
export { VaultError, isVaultError };
|
|
27
28
|
export { ensureArgonCarrier, currentArgonCarrier, resetArgonCarrier, DEFAULT_ARGON_CHAIN, } from './argon-auto.js';
|
|
28
29
|
export { argonWorkerSource, createBlobArgonWorker } from './argon-worker.js';
|
|
30
|
+
/**
|
|
31
|
+
* resolveVaultConfig(input):零設定前綴推導(resolver)。
|
|
32
|
+
* 輸入 `{ app, ...任意顯式欄位 }`,輸出完整 VaultConfig+defaulted 旗標:
|
|
33
|
+
* `defaulted`=真時 cipher/argon/legacy/recovery 四組**整組**由 app 決定性推導(10 欄,recovery opt-in 時含)——
|
|
34
|
+
* 命名自定(語意=resolveVaultConfig:`{app}`+零設定前綴推導):前綴零手填、家族尾碼恆固定(契約三要素 app+尾碼+佈局;
|
|
35
|
+
* 帶內版本化:尾碼代即版本代,升級=新尾碼;家族佈局不隨字面——README 前綴契約節同源)。
|
|
36
|
+
* `defaulted`=假時輸入即顯式全配置(回歸 diff 恆 0——validateConfig 逐對檢查照守)。
|
|
37
|
+
*
|
|
38
|
+
* 推導表(app 契約 `^[A-Za-z0-9_-]+$`——前綴字面=線上 ASCII 契約;**契約閘=零設定(推導)路徑專載**:
|
|
39
|
+
* 零設定路徑上 app 在場但含契約外字元=ERR_VAULT_CONFIG fail-closed 不猜;顯式全配置路徑
|
|
40
|
+
* app 字面照 0.3.3 既有契約零驗形(非 ASCII/帶點 app 走顯式全配置照舊)——契約閘不跨路徑加寬):
|
|
41
|
+
* cipher='foo1v.'/wrap4='foo4w.'/wrapDual4='foo4d.'/pinSalt3Prefix='foo3s:'(鹽域 ':' 同 tacet 形)
|
|
42
|
+
* wrap3='foo3w.'/wrapDual3='foo3d.'/legacy wrap='foo1w.'/wrapDual='foo2w.'/pinSaltPrefix='foo1s.'
|
|
43
|
+
* recovery(`recovery: true` 時)wrapRec='foo1r.'/recKekHkdf='fookek1'(KEK info 域帶 k 代——刻意
|
|
44
|
+
* 異於 wrapRec 字面:HKDF info 域與前綴域恆非前綴關係,鹽外置 payload 邊界不重疊);
|
|
45
|
+
* wrap3/wrapDual3 舊世代讀面同由 app 推導(`app + '3w.'`/`app + '3d.'`——app 族半顯式 legacy 槽的推導殘部)。
|
|
46
|
+
* `foo`=示例形;任兩推導值互不為前綴(startsWith 唯一性前提);尾碼代=家族表 README 行。
|
|
47
|
+
*
|
|
48
|
+
* 顯式路徑判別:`argon` 或 `cipher` 任一在場=顯式全配置路徑(resolver 不再推導任何欄位——
|
|
49
|
+
* 顯式欄位優先、缺欄照 validateConfig 拒=opt-in 母型;app 契約閘不跨路徑加寬——**契約閘=
|
|
50
|
+
* 零設定路徑專載**(顯式全配置路徑 app 字面照 0.3.3 契約零驗;`resolveVaultConfig(opts)`
|
|
51
|
+
* 三入口單點接線共用)。
|
|
52
|
+
* recovery 物件形同判(呼叫端自管面)。半顯式態(argon 組推導+legacy 物件槽顯式欄位共存):
|
|
53
|
+
* 推導值與顯式值同進前綴互斥檢查(validateConfig——例 `legacy: { wrap: 'foo4w.bait' }` 與推導
|
|
54
|
+
* wrap4 'foo4w.' 互為前綴=CONFIG 拒;歧義配置不猜。
|
|
55
|
+
*/
|
|
56
|
+
export function resolveVaultConfig(input) {
|
|
57
|
+
if (!input || typeof input !== 'object')
|
|
58
|
+
throw new VaultError('ERR_VAULT_CONFIG', 'input missing');
|
|
59
|
+
const appIn = input.app;
|
|
60
|
+
const opts = input;
|
|
61
|
+
const explicit = opts.argon !== undefined || opts.cipher !== undefined
|
|
62
|
+
|| (opts.recovery !== undefined && typeof opts.recovery === 'object');
|
|
63
|
+
if (explicit)
|
|
64
|
+
return { config: input, defaulted: false };
|
|
65
|
+
// app 契約閘=零設定(推導)路徑專載——契約閘不跨路徑加寬(顯式全配置路徑 app 字面照 0.3.3
|
|
66
|
+
// 既有契約零驗;非契約 app 誤入零設定=推導值帶契約外字元入前綴域=fail-closed 不猜)。
|
|
67
|
+
if (typeof appIn !== 'string' || !APP_RE.test(appIn)) {
|
|
68
|
+
throw new VaultError('ERR_VAULT_CONFIG', 'app contract (^[A-Za-z0-9_-]+$)');
|
|
69
|
+
}
|
|
70
|
+
const app = appIn;
|
|
71
|
+
let legacy;
|
|
72
|
+
if (opts.legacy === true) {
|
|
73
|
+
legacy = { wrap: app + '1w.', wrapDual: app + '2w.', pinSaltPrefix: app + '1s.' };
|
|
74
|
+
}
|
|
75
|
+
else if (opts.legacy === undefined || opts.legacy === false) {
|
|
76
|
+
legacy = { wrap: false };
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const l = opts.legacy;
|
|
80
|
+
legacy = {
|
|
81
|
+
wrap: l.wrap === undefined ? app + '1w.' : l.wrap,
|
|
82
|
+
wrapDual: l.wrapDual ?? app + '2w.',
|
|
83
|
+
pinSaltPrefix: l.pinSaltPrefix ?? app + '1s.',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const recovery = opts.recovery === true ? { wrapRec: app + '1r.', recKekHkdf: app + 'kek1' } : undefined;
|
|
87
|
+
return {
|
|
88
|
+
config: {
|
|
89
|
+
app,
|
|
90
|
+
cipher: app + '1v.',
|
|
91
|
+
argon: { wrap4: app + '4w.', wrapDual4: app + '4d.', pinSalt3Prefix: app + '3s:', wrap3: app + '3w.', wrapDual3: app + '3d.' },
|
|
92
|
+
legacy,
|
|
93
|
+
recovery,
|
|
94
|
+
},
|
|
95
|
+
defaulted: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
29
98
|
// ── 配置驗證 ────────────────────────────────────────────────────────────────
|
|
30
99
|
const HEX32_RE = /^[0-9a-f]{32}$/;
|
|
31
100
|
const HEX64_RE = /^[0-9a-f]{64}$/;
|
|
101
|
+
/** app 契約(前綴字面=線上 ASCII 契約):字母/數字/底線/減號。 */
|
|
102
|
+
const APP_RE = /^[A-Za-z0-9_-]+$/;
|
|
32
103
|
const nonEmpty = (v) => typeof v === 'string' && v.length > 0;
|
|
33
104
|
/** 讀面次序(新→舊):jr4 家族優先,舊世代依序。 */
|
|
34
105
|
const FAMILIES = [
|
|
@@ -36,8 +107,8 @@ const FAMILIES = [
|
|
|
36
107
|
{ family: 'wrapDual4', dual: true, current: true, argon: true, prefix: (c) => (c.argon.pinSalt3Prefix ? c.argon.wrapDual4 : undefined) },
|
|
37
108
|
{ family: 'wrap3', dual: false, current: false, argon: true, prefix: (c) => c.argon.wrap3 },
|
|
38
109
|
{ family: 'wrapDual3', dual: true, current: false, argon: true, prefix: (c) => (c.argon.pinSalt3Prefix ? c.argon.wrapDual3 : undefined) },
|
|
39
|
-
{ family: 'wrap', dual: false, current: false, argon: false, prefix: (c) => c.legacy?.wrap },
|
|
40
|
-
{ family: 'wrapDual', dual: true, current: false, argon: false, prefix: (c) => (c.legacy?.pinSaltPrefix ? c.legacy?.wrapDual : undefined) },
|
|
110
|
+
{ family: 'wrap', dual: false, current: false, argon: false, prefix: (c) => (c.legacy?.wrap === false ? undefined : c.legacy?.wrap) },
|
|
111
|
+
{ family: 'wrapDual', dual: true, current: false, argon: false, prefix: (c) => (c.legacy?.wrap === false ? undefined : (c.legacy?.pinSaltPrefix ? c.legacy?.wrapDual : undefined)) },
|
|
41
112
|
];
|
|
42
113
|
function specOf(family) {
|
|
43
114
|
return FAMILIES.find((f) => f.family === family);
|
|
@@ -51,6 +122,17 @@ function validateConfig(c) {
|
|
|
51
122
|
throw new VaultError('ERR_VAULT_CONFIG', 'cipher');
|
|
52
123
|
if (!c.argon || !nonEmpty(c.argon.wrap4))
|
|
53
124
|
throw new VaultError('ERR_VAULT_CONFIG', 'argon.wrap4');
|
|
125
|
+
if (c.legacy !== undefined) {
|
|
126
|
+
if (typeof c.legacy !== 'object' || Array.isArray(c.legacy))
|
|
127
|
+
throw new VaultError('ERR_VAULT_CONFIG', 'legacy');
|
|
128
|
+
const lv = c.legacy;
|
|
129
|
+
if (lv.wrap !== undefined && lv.wrap !== false && !nonEmpty(lv.wrap))
|
|
130
|
+
throw new VaultError('ERR_VAULT_CONFIG', 'legacy.wrap');
|
|
131
|
+
if (lv.wrapDual !== undefined && !nonEmpty(lv.wrapDual))
|
|
132
|
+
throw new VaultError('ERR_VAULT_CONFIG', 'legacy.wrapDual');
|
|
133
|
+
if (lv.pinSaltPrefix !== undefined && !nonEmpty(lv.pinSaltPrefix))
|
|
134
|
+
throw new VaultError('ERR_VAULT_CONFIG', 'legacy.pinSaltPrefix');
|
|
135
|
+
}
|
|
54
136
|
const prefixes = [c.cipher];
|
|
55
137
|
for (const f of FAMILIES) {
|
|
56
138
|
const p = f.prefix(c);
|
|
@@ -58,6 +140,11 @@ function validateConfig(c) {
|
|
|
58
140
|
prefixes.push(p);
|
|
59
141
|
}
|
|
60
142
|
if (c.recovery !== undefined) {
|
|
143
|
+
// 物件形契約(resolver `recovery: true` opt-in 推導前的原始形=boolean 進位——顯式路徑直通時 true/false 恆拒:
|
|
144
|
+
// true=推導語意非顯式規格;false=「無復原」語意不入 validateConfig 面)。
|
|
145
|
+
if (c.recovery === true || c.recovery === false || typeof c.recovery !== 'object' || Array.isArray(c.recovery)) {
|
|
146
|
+
throw new VaultError('ERR_VAULT_CONFIG', 'recovery');
|
|
147
|
+
}
|
|
61
148
|
const r = c.recovery;
|
|
62
149
|
// 兩欄一體(ERR_REC_CFG_PARTIAL 母型):部分配置拒——禁寫出跨契約 blob。
|
|
63
150
|
if (!r || !nonEmpty(r.wrapRec) || !nonEmpty(r.recKekHkdf))
|
|
@@ -91,17 +178,19 @@ function checkDualWritable(c) {
|
|
|
91
178
|
* store 面不被觸及(vault 無儲存),以惰性空殼滿足型別。 */
|
|
92
179
|
const NO_STORE = { get: () => null, set: () => { }, remove: () => { }, noteKeyWrap: (k) => k };
|
|
93
180
|
function legacyCfg(c) {
|
|
181
|
+
const lw = c.legacy?.wrap === false ? '' : c.legacy?.wrap ?? '';
|
|
94
182
|
return {
|
|
95
183
|
guestKdfPrefix: '',
|
|
96
184
|
recSaltPrefix: '',
|
|
97
185
|
cipherBound: c.cipher,
|
|
98
|
-
wrap:
|
|
186
|
+
wrap: lw,
|
|
99
187
|
wrapDual: c.legacy?.wrapDual,
|
|
100
188
|
pinSaltPrefix: c.legacy?.pinSaltPrefix,
|
|
101
189
|
store: NO_STORE,
|
|
102
190
|
};
|
|
103
191
|
}
|
|
104
|
-
/** 復原面專用 cfg:wrap='' =舊契約回落腿關閉(identity 綁域的 jr1w. 復原套件不屬本 API)。
|
|
192
|
+
/** 復原面專用 cfg:wrap='' =舊契約回落腿關閉(identity 綁域的 jr1w. 復原套件不屬本 API)。
|
|
193
|
+
* 呼叫端先收口:boolean 形 recovery 已由 validateConfig 拒(createVault)/recoverVault 入口拒——本函式唯物件形。 */
|
|
105
194
|
function recoveryCfg(c) {
|
|
106
195
|
const r = c.recovery;
|
|
107
196
|
return {
|
|
@@ -325,42 +414,67 @@ function makeHandle(c, noteKey, st, carrierOpts) {
|
|
|
325
414
|
};
|
|
326
415
|
}
|
|
327
416
|
// ── 入口 ────────────────────────────────────────────────────────────────────
|
|
328
|
-
/**
|
|
417
|
+
/**
|
|
418
|
+
* deriveVaultPh1(passphrase, ph1Salt):高階 Vault API 的 PH1 登入憑證派生(hex64)。
|
|
419
|
+
* 本體委派 `derivePh1ArgonV3(pass, ph1Salt)`(PH1 v3:Argon2id(normalizePassphrase(pass), 鹽, m=64MiB, t=3, p=1)→hex64)
|
|
420
|
+
* ——語意單一真相同在 argon2.ts,本函式只是 vault 子路徑的入口收口位(呼叫端免跨 import argon2)。
|
|
421
|
+
*
|
|
422
|
+
* ⚠️ 契約三行(README PH1 節同款):
|
|
423
|
+
* 1. 鹽一經選定恆固定(per-product)——PH2 UNIQUE 約束:同一密語必須恆生同一 PH2;
|
|
424
|
+
* per-user 鹽會摧毀它。改鹽=帶內版本化換鹽尾碼重遷移,禁原地改語意。
|
|
425
|
+
* 2. `PH1_V3_SALT`('tacet-ph1-v2')禁當 saltArg 餵 v2 派生面 `derivePh1Argon`——
|
|
426
|
+
* 反之亦然:同鹽域值跨面餵入=NFKC-effective 密語與 v2 raw 密語同值撞 PH2(鹽撞位警告)。
|
|
427
|
+
* 3. 其他產品用自有鹽域——禁硬編 tacet 鹽當預設;必須顯式帶入 `ph1Salt`。
|
|
428
|
+
*/
|
|
429
|
+
export async function deriveVaultPh1(passphrase, ph1Salt) {
|
|
430
|
+
return derivePh1ArgonV3(passphrase, ph1Salt);
|
|
431
|
+
}
|
|
432
|
+
/** 新 vault:隨機 noteKey+最新家族包裹(pin=<app>4d.,否則 <app>4w.)+選配復原套件(<app>1r.)。
|
|
433
|
+
* 前綴零手填態(resolver):`{ app }` 最小設定——全前綴由 app 決定性推導(resolveVaultConfig;
|
|
434
|
+
* `recovery: true` opt-in、PIN 帶值=同族尾碼推導 <app>4d. 不拒);顯式 `argon`/`cipher` 在場
|
|
435
|
+
* =顯式全配置路徑(零設定語意退出——缺欄照 validateConfig 拒)。輸入形 VaultOptions | VaultConfigInput。 */
|
|
329
436
|
export function createVault(passphrase, opts) {
|
|
330
437
|
return translate(async () => {
|
|
331
|
-
|
|
438
|
+
const o = resolveVaultConfig(opts);
|
|
439
|
+
const cfg = o.config;
|
|
440
|
+
validateConfig(cfg);
|
|
332
441
|
checkPassphrase(passphrase);
|
|
333
442
|
const pin = opts.pin === undefined ? null : opts.pin;
|
|
334
443
|
if (pin !== null) {
|
|
335
444
|
checkPin(pin);
|
|
336
|
-
checkDualWritable(
|
|
445
|
+
checkDualWritable(cfg);
|
|
337
446
|
}
|
|
338
447
|
const carrier = await ensureArgonCarrier(opts.carrier);
|
|
339
448
|
const noteKey = await generateNoteKey();
|
|
340
|
-
const w = await wrapCurrent(
|
|
449
|
+
const w = await wrapCurrent(cfg, noteKey, passphrase, pin);
|
|
341
450
|
let recovery = null;
|
|
342
451
|
if (opts.recovery) {
|
|
343
452
|
const recToken = generateRecToken();
|
|
453
|
+
const words = (await recTokenToWords(recToken)); // generateRecToken 恆 hex64——null 不可能
|
|
344
454
|
// identity 不入 HKDF 世代 KEK(identity-free by design)——空字串僅滿足簽名。
|
|
345
|
-
const wrappedRec = await wrapNoteKeyWithRecToken(recoveryCfg(
|
|
346
|
-
recovery = { recToken, wrappedRec, recTokenHash: await recTokenHash(recToken) };
|
|
455
|
+
const wrappedRec = await wrapNoteKeyWithRecToken(recoveryCfg(cfg), noteKey, recToken, '');
|
|
456
|
+
recovery = { recToken, words, wrappedRec, recTokenHash: await recTokenHash(recToken) };
|
|
347
457
|
}
|
|
348
|
-
const vault = makeHandle(
|
|
458
|
+
const vault = makeHandle(cfg, noteKey, { rec: w.rec, family: w.family, carrier }, opts.carrier);
|
|
349
459
|
return { vault, serverRecord: { ...w.rec }, recovery };
|
|
350
460
|
});
|
|
351
461
|
}
|
|
352
462
|
/** 解鎖:serverRecord(wrapped+salt 成對)按前綴辨識家族(新→舊次序),舊世代照解;
|
|
353
|
-
*
|
|
463
|
+
* 前綴零手填態(resolver):整組前綴缺省=全前綴由 app 推導(resolveVaultConfig);顯式欄位
|
|
464
|
+
* 在場=顯式全配置路徑(零推導)。needsUpgrade() 為真時呼叫端可於同一動線
|
|
465
|
+
* upgrade(passphrase) 靜默重包(Argon 遷移契約同構)。輸入形 VaultOptions | VaultConfigInput。 */
|
|
354
466
|
export function unlockVault(passphrase, serverRecord, opts) {
|
|
355
467
|
return translate(async () => {
|
|
356
|
-
|
|
468
|
+
const o = resolveVaultConfig(opts);
|
|
469
|
+
const cfg = o.config;
|
|
470
|
+
validateConfig(cfg);
|
|
357
471
|
const r = serverRecord;
|
|
358
472
|
if (!r || typeof r !== 'object' || !nonEmpty(r.wrapped) || typeof r.salt !== 'string' || !HEX32_RE.test(r.salt)) {
|
|
359
473
|
throw new VaultError('ERR_VAULT_BAD_RECORD');
|
|
360
474
|
}
|
|
361
475
|
checkPassphrase(passphrase);
|
|
362
476
|
const rec = { wrapped: r.wrapped, salt: r.salt };
|
|
363
|
-
const f = detectFamily(
|
|
477
|
+
const f = detectFamily(cfg, rec.wrapped);
|
|
364
478
|
if (!f)
|
|
365
479
|
throw new VaultError('ERR_VAULT_WRAP_UNKNOWN');
|
|
366
480
|
if (f.dual) {
|
|
@@ -369,23 +483,37 @@ export function unlockVault(passphrase, serverRecord, opts) {
|
|
|
369
483
|
checkPin(opts.pin);
|
|
370
484
|
}
|
|
371
485
|
const carrier = f.argon ? await ensureArgonCarrier(opts.carrier) : null;
|
|
372
|
-
const noteKey = await unwrapOrThrow(f,
|
|
373
|
-
return makeHandle(
|
|
486
|
+
const noteKey = await unwrapOrThrow(f, cfg, rec, passphrase, opts.pin ?? '');
|
|
487
|
+
return makeHandle(cfg, noteKey, { rec, family: f.family, carrier }, opts.carrier);
|
|
374
488
|
});
|
|
375
489
|
}
|
|
376
|
-
/** 復原:recToken(hex64)+wrappedRec
|
|
490
|
+
/** 復原:recToken(hex64)+wrappedRec(<app>1r.)→ handle(無包裹欄——呼叫端隨即 changePassphrase 設新密語)。
|
|
491
|
+
* 前綴零手填態同上:recovery 未配置=ERR_VAULT_CONFIG(零鹽選擇不可靜默——opt-in 鐵律;resolver 對
|
|
492
|
+
* `recovery: true` 推導零鹽契約、物件形=顯式零動)。輸入形 VaultOptions | VaultConfigInput。 */
|
|
377
493
|
export function recoverVault(recToken, wrappedRec, opts) {
|
|
378
494
|
return translate(async () => {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (
|
|
383
|
-
throw new VaultError('
|
|
384
|
-
|
|
495
|
+
const o = resolveVaultConfig(opts);
|
|
496
|
+
const cfg = o.config;
|
|
497
|
+
validateConfig(cfg);
|
|
498
|
+
if (!cfg.recovery || cfg.recovery === true)
|
|
499
|
+
throw new VaultError('ERR_VAULT_CONFIG', 'recovery'); // true=resolver opt-in 形——顯式路徑(argon/cipher 在場)未推導=與未配置同拒
|
|
500
|
+
let token;
|
|
501
|
+
if (typeof recToken === 'string' && HEX64_RE.test(recToken)) {
|
|
502
|
+
token = recToken; // hex64 既有契約原樣
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
// 詞形入參(24 詞空格單字串/24 詞陣列):正規化→轉回 hex64→後續恆走原驗證路;
|
|
506
|
+
// 任何不符(錯詞 checksum/錯數/詞表外)=ERR_VAULT_RECOVERY 照舊(不洩漏哪類錯)。
|
|
507
|
+
const hex = await wordsToRecToken(normalizeWordInput(recToken));
|
|
508
|
+
if (hex === null || !HEX64_RE.test(hex))
|
|
509
|
+
throw new VaultError('ERR_VAULT_RECOVERY', 'recToken');
|
|
510
|
+
token = hex;
|
|
511
|
+
}
|
|
512
|
+
if (typeof wrappedRec !== 'string' || !wrappedRec.startsWith(cfg.recovery.wrapRec))
|
|
385
513
|
throw new VaultError('ERR_VAULT_RECOVERY', 'wrappedRec prefix');
|
|
386
|
-
const noteKey = await unwrapNoteKeyWithRecToken(recoveryCfg(
|
|
514
|
+
const noteKey = await unwrapNoteKeyWithRecToken(recoveryCfg(cfg), wrappedRec, token, '');
|
|
387
515
|
if (!noteKey)
|
|
388
516
|
throw new VaultError('ERR_VAULT_RECOVERY');
|
|
389
|
-
return makeHandle(
|
|
517
|
+
return makeHandle(cfg, noteKey, { rec: null, family: null, carrier: null }, opts.carrier);
|
|
390
518
|
});
|
|
391
519
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tacet-ink/journal-core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Zero-knowledge journal core — e2e crypto, passphrase-only auth, transport, ratelimit. The server never sees plaintext.",
|
|
5
5
|
"homepage": "https://tacet.ink",
|
|
6
6
|
"repository": {
|
|
@@ -89,11 +89,6 @@
|
|
|
89
89
|
"files": [
|
|
90
90
|
"dist",
|
|
91
91
|
"docs",
|
|
92
|
-
"src",
|
|
93
|
-
"scripts",
|
|
94
|
-
"!scripts/generate-vectors.ts",
|
|
95
|
-
"!scripts/vendor-hash-wasm.cjs",
|
|
96
|
-
"!scripts/dts-fixture.cjs",
|
|
97
92
|
"migrations",
|
|
98
93
|
"README.md",
|
|
99
94
|
"LICENSE"
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// t_c18a95ce 修復 A:prepare-core-pkg.cjs 斷鏈防線——@scure/bip39 鏡相依閉包 vendor 進 .core-pkg
|
|
2
|
-
// 方案 (a) 真 vendor(裁定):以 REF_DEPS 為根、聲明面 dependencies 遞迴窮舉閉包 → .core-pkg/node_modules
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
|
|
6
|
-
const ROOT = path.join(__dirname, '..');
|
|
7
|
-
const SRC_DIR = path.join(ROOT, 'node_modules', '@tacet-ink', 'journal-core');
|
|
8
|
-
const VENDOR_DIR = path.join(ROOT, '.core-pkg');
|
|
9
|
-
const LINK_TARGET = '../../.core-pkg';
|
|
10
|
-
|
|
11
|
-
// 斷鏈封閉集:閘 scripts 參照的 devDeps @scure 與其宣告 runtime deps(@noble/hashes——
|
|
12
|
-
// bip39 的 sha256/pbkdf2 面,npm 平坦安裝在根 node_modules,無嵌套副本)→ 遞迴鏡相依
|
|
13
|
-
// 閉包全 vendor:只 vendor @scure 本體時,vendored bip39 內部 import '@noble/hashes/*'
|
|
14
|
-
// 走父層查不到=[19] 面在消費者手術環境恆紅(鏡相依閉包非單件)。
|
|
15
|
-
// — prepare 時閉包全 vendor,realpath 與 SRC_DIR 同構逃出 node_modules 禁區。
|
|
16
|
-
const REF_DEPS = ['@scure/bip39'];
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const st = fs.lstatSync(SRC_DIR);
|
|
20
|
-
if (st.isSymbolicLink()) {
|
|
21
|
-
const cur = fs.readlinkSync(SRC_DIR);
|
|
22
|
-
if (cur.replace(/\/+$/, '') === LINK_TARGET) {
|
|
23
|
-
if (!fs.existsSync(VENDOR_DIR)) {
|
|
24
|
-
console.error('ABORT: symlink 已在場但 .core-pkg 缺席:rm node_modules 後重跑 npm ci(plain)');
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
27
|
-
console.log('CORE-PKG-PREPARE-OK (idempotent)');
|
|
28
|
-
process.exit(0);
|
|
29
|
-
}
|
|
30
|
-
console.error('ABORT: node_modules 條目是指向他人的 symlink: ' + cur);
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|
|
33
|
-
} catch (e) {
|
|
34
|
-
if (e && e.code !== 'ENOENT') { console.error('ABORT: ' + e.message); process.exit(1); }
|
|
35
|
-
console.error('ABORT: node_modules/@tacet-ink/journal-core 缺席:先跑 npm ci(plain,非 --ignore-scripts)');
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// 真樹還原 → 複製 → symlink 手術
|
|
40
|
-
fs.rmSync(VENDOR_DIR, { recursive: true, force: true });
|
|
41
|
-
fs.cpSync(SRC_DIR, VENDOR_DIR, { recursive: true });
|
|
42
|
-
// 參照依賴 vendor:.core-pkg/node_modules/<ref>(realpath 在 .core-pkg 樹內=
|
|
43
|
-
// strip-types 判準 realpath 之外;斷鏈防線——tarball 環境(.core-pkg/node_modules 自帶)
|
|
44
|
-
// 與本樹手術環境(外部 vendor 直拷)同構=閘 [16] pristine 全綠)。
|
|
45
|
-
const vendoredRefs = [];
|
|
46
|
-
const vendoredSet = new Set();
|
|
47
|
-
const vendorQueue = [...REF_DEPS];
|
|
48
|
-
while (vendorQueue.length) {
|
|
49
|
-
const ref = vendorQueue.pop();
|
|
50
|
-
if (vendoredSet.has(ref)) continue;
|
|
51
|
-
vendoredSet.add(ref);
|
|
52
|
-
const refAbs = path.join(ROOT, 'node_modules', ...ref.split('/'));
|
|
53
|
-
if (!fs.existsSync(refAbs)) {
|
|
54
|
-
console.error('ABORT: 參照閉包缺席(' + ref + '):npm ci 未裝齊參照組依賴?');
|
|
55
|
-
process.exit(1);
|
|
56
|
-
}
|
|
57
|
-
fs.cpSync(refAbs, path.join(VENDOR_DIR, 'node_modules', ...ref.split('/')), { recursive: true });
|
|
58
|
-
vendoredRefs.push(ref);
|
|
59
|
-
// runtime deps 閉包遞迴(聲明面為準;嵌套副本由 cpSync 遞迴一併帶入,root 派為同構直拷)
|
|
60
|
-
try {
|
|
61
|
-
const depPkg = JSON.parse(fs.readFileSync(path.join(refAbs, 'package.json'), 'utf8'));
|
|
62
|
-
const deps = depPkg.dependencies && typeof depPkg.dependencies === 'object' ? Object.keys(depPkg.dependencies) : [];
|
|
63
|
-
vendorQueue.push(...deps);
|
|
64
|
-
} catch { /* 無法讀 package.json 的 vendored 件仍依序入帳——封閉集以聲明面窮舉為準 */ }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const a = fs.readFileSync(path.join(SRC_DIR, 'package.json'), 'utf8');
|
|
68
|
-
const b = fs.readFileSync(path.join(VENDOR_DIR, 'package.json'), 'utf8');
|
|
69
|
-
if (a !== b) { console.error('ABORT: .core-pkg 複製不一致'); process.exit(1); }
|
|
70
|
-
fs.rmSync(SRC_DIR, { recursive: true, force: true });
|
|
71
|
-
fs.symlinkSync(LINK_TARGET, SRC_DIR, 'dir');
|
|
72
|
-
|
|
73
|
-
const ver = b.match(/"version": "([^"]+)"/)[1];
|
|
74
|
-
console.log('CORE-PKG-PREPARE-OK ' + ver + '(vendored: ' + vendoredRefs.join(' ') + ')');
|