@tacet-ink/journal-core 0.2.3 → 0.2.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 +69 -14
- package/migrations/0012-fix-ph2-ladder-index.sql +18 -0
- package/package.json +1 -1
- package/scripts/prepare-core-pkg.cjs +74 -0
- package/scripts/verify-core-crypto.ts +635 -74
- package/src/client/argon2.ts +4 -0
- package/src/client/note-crypto.ts +7 -1
- package/src/server/auth.ts +46 -6
- package/src/server/ladder.ts +8 -3
- package/scripts/open-card-pr.sh +0 -49
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* verify-core-crypto.ts — core 抽取的驗證閘(對真模組,禁鏡像——extractable 教訓)。
|
|
3
3
|
* 執行:node --experimental-strip-types scripts/verify-core-crypto.ts
|
|
4
|
-
* 全綠輸出 CORE-CRYPTO-OK
|
|
4
|
+
* 全綠輸出 CORE-CRYPTO-VERIFY-OK(N assertions)(整行=唯一消費錨——下游驗收條款禁前綴子字串
|
|
5
|
+
* 比對,防段數漂移誤判;--only 態改 CORE-CRYPTO-VERIFY-OK(N assertions)(--ONLY 16-18) 顯形);任何失敗 exit 1。
|
|
6
|
+
* [24] 段級運行(2026-10-09):`--only 16,17,18`/`--only 16-18`(或混形)=只跑指定段;setup(頂層向量群+段標記行)恆跑;
|
|
7
|
+
* 段依賴常數由 setup 重生(無跨段狀態依賴——各段消費自己的區塊常數)。省時是這卡的真價值:審查輪重跑 [16]-[18] 不再等 KDF 群全套。
|
|
8
|
+
*
|
|
9
|
+
* 2026-10-09(0.2.4):[23] loginRouteCore ladder 查表守衛段(外審 #8 幽靈帳)——真 node:sqlite
|
|
10
|
+
* shim 直驅七情境(現值 hit/ladder hit 回舊帳/兩面 miss 建幽靈/未配置單查零變/fail-open)
|
|
11
|
+
* +拷貝樹毒化 in-gate(接線摘除 → ladder hit 翻建幽靈)。POISON_GATE_INNER 內層跑份自我跳過。
|
|
5
12
|
*/
|
|
6
13
|
|
|
7
14
|
// 對真模組(禁鏡像重寫金鑰邏輯——鏡像驗證抓不到真模組 bug 的生產教訓)
|
|
@@ -45,6 +52,8 @@ import {
|
|
|
45
52
|
verifyArgonKat,
|
|
46
53
|
wrapNoteKey3,
|
|
47
54
|
unwrapNoteKey3,
|
|
55
|
+
wrapNoteKeyShare3,
|
|
56
|
+
unwrapNoteKeyShare3,
|
|
48
57
|
wrapNoteKeyDual3,
|
|
49
58
|
unwrapNoteKeyDual3,
|
|
50
59
|
wrapNoteKey4,
|
|
@@ -123,32 +132,148 @@ let passed = 0;
|
|
|
123
132
|
const failures: string[] = [];
|
|
124
133
|
async function A(name: string, cond: boolean | Promise<boolean>, detail = ''): Promise<void> {
|
|
125
134
|
const ok = cond instanceof Promise ? await cond : cond;
|
|
126
|
-
if (ok) { passed++; console.log(` ✓ ${name}`); }
|
|
127
|
-
else { failures.push(name); console.error(` ✗ ${name}${detail ? ' — ' + detail : ''}`); }
|
|
135
|
+
if (ok) { passed++; secRan.set(secCur, (secRan.get(secCur) ?? 0) + 1); console.log(` ✓ ${name}`); }
|
|
136
|
+
else { failures.push(name); secRan.set(secCur, (secRan.get(secCur) ?? 0) + 1); console.error(` ✗ ${name}${detail ? ' — ' + detail : ''}`); }
|
|
128
137
|
}
|
|
129
138
|
|
|
139
|
+
// ── [24] runner:段級 --only 運行(2026-10-09,t_3ec0936e)——段號語法契約+帳面誠實契約的主場 ──
|
|
140
|
+
// 語法契約([24] parse 面自證):--only <CSV>|<a-b>|<混形>;輸出單調遞增去重(16,17,18 ≡ 16-18);
|
|
141
|
+
// 空值/非數字/未定段/倒序範圍=ERR_ONLY_* fail-closed exit 1。--only 缺席=預設恆全跑(CI/deploy 契約不動)。
|
|
142
|
+
// 帳面契約:--only 態總帳顯形「本輪 X/選段帳 N(N 案非本輪載)」+OK 標記帶 (--ONLY …) 尾碼;skip 段帳面恆列「未跑非通過」;[24]/[25] runner 自證段隨每輪在場。
|
|
143
|
+
// 段標記 helper(secOpen):全跑態輸出位元組恆等原 console.log('\n[N] …');--only 態僅選中段輸出段頭。
|
|
144
|
+
// 毒化矩陣內層 rerun(execFileSync+POISON_GATE_INNER=1)不傳遞 --only=內層恆全帳(POISON_GATE_INNER 語意不變)。
|
|
145
|
+
// 段帳=執行帳(healthy-repo 全跑 runtime 真值,2026-10-10 實測 483);帳面語意契約(逐段「案 X/帳」
|
|
146
|
+
// 對 secRan 執行值顯形):執行 < 帳 的段=本環境條件態未行使(非 skip 態),--only 期望算術對齊本帳。
|
|
147
|
+
// [23] 執行 18 帳 21=[23]④ lookup 未配置(0.2.3 單查恆等)/[23]⑤ fail-open(表缺席)兩情境
|
|
148
|
+
// 在本機樹由 [8] PH1_v2_SALT 訊號面真行使(try/catch 異常面)——車道分流非帳面虛報。
|
|
149
|
+
// secEnter 跑行為脫鉤 secCases;secCases 鍵只承載段存在/帳面/--only 判準的登記義務
|
|
150
|
+
//(secOpen(22,…) 無段標記行=慣例 skip 段)。缺帳防線(teeth):secRan 實跑段必已登記——
|
|
151
|
+
// for (const [k] of secRan) if (!secCases[k] && k !== 22) FAIL;反向缺席段=帳面誠實「案 0/N · skip」。
|
|
152
|
+
// secCases 只餘 [24]/[25] self-account 與 --only 段存在面+teeth 三載體(段帳由 secRan 執行自記)。
|
|
153
|
+
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: 3 };
|
|
154
|
+
function parseOnly(spec: string): { err: string | null; set: number[] } {
|
|
155
|
+
const set = new Set<number>();
|
|
156
|
+
const specStr = spec.trim();
|
|
157
|
+
if (specStr === '') return { err: 'ERR_ONLY_SPEC_EMPTY', set: [] };
|
|
158
|
+
for (const rawPart of specStr.split(',')) {
|
|
159
|
+
const part = rawPart.trim();
|
|
160
|
+
if (part === '') return { err: 'ERR_ONLY_SPEC_EMPTY_TOKEN', set: [] };
|
|
161
|
+
const span = part.match(/^([0-9]+)-([0-9]+)$/);
|
|
162
|
+
if (span) {
|
|
163
|
+
const lo = Number(span[1]); const hi = Number(span[2]);
|
|
164
|
+
if (!(hi >= lo) || !secCases[lo] || !secCases[hi]) return { err: 'ERR_ONLY_SPEC_RANGE', set: [] };
|
|
165
|
+
for (let s = lo; s <= hi; s++) if (secCases[s]) set.add(s);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!/^[0-9]+$/.test(part)) return { err: 'ERR_ONLY_SPEC_TOKEN', set: [] };
|
|
169
|
+
const num = Number(part);
|
|
170
|
+
if (!secCases[num]) return { err: 'ERR_ONLY_SPEC_UNKNOWN_SEC', set: [] };
|
|
171
|
+
set.add(num);
|
|
172
|
+
}
|
|
173
|
+
return { err: null, set: [...set].sort((a, b) => a - b) };
|
|
174
|
+
}
|
|
175
|
+
const argvArr = (globalThis as unknown as { process?: { argv?: string[] } }).process?.argv ?? [];
|
|
176
|
+
const argvOnlyIdx = argvArr.indexOf('--only');
|
|
177
|
+
const onlySpecRaw = argvOnlyIdx >= 0 ? argvArr[argvOnlyIdx + 1] ?? '' : null;
|
|
178
|
+
const onlyParsed = onlySpecRaw !== null ? parseOnly(onlySpecRaw) : null;
|
|
179
|
+
if (onlyParsed?.err) {
|
|
180
|
+
console.error('FAIL: ' + onlyParsed.err + ' — --only 段號語法契約:--only 16,17,18(CSV)/--only 16-18(範圍)/混形 16,17-19;--only 缺席=全跑。');
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
const onlySet = new Set<number>(onlyParsed?.set ?? []);
|
|
184
|
+
const onlyMode = (): boolean => onlySet.size > 0;
|
|
185
|
+
// runner 自證段(24/25)隨每輪在場(毫秒級;only 態帳面顯形契約的行使面——非選段制)。
|
|
186
|
+
// 全跑態 secEnter 恆 true(n ≥1 且 n ≠ 22——secOpen 行在場即實段;secCases 人工帳冊退役=
|
|
187
|
+
// 靜默不跑面收口:新段只加 secOpen 即跑,段帳由 secRan 自記)。22=慣例 skip 段(bannerless
|
|
188
|
+
// 斷言群帳入 [13]的慣例面;[24] 契約明文在案);n < 1=非法(非段位)。--only 態照選段集+自證段。
|
|
189
|
+
const secEnter = (n: number): boolean =>
|
|
190
|
+
(!onlyMode() ? (n >= 1 && n !== 22) : (onlySet.has(n) || n === 24 || n === 25));
|
|
191
|
+
const onlySpecDisp = (): string => (onlyMode() ? (onlyParsed?.set ?? []).join(',') : '');
|
|
192
|
+
const rangeCompress = (nums: number[]): string => {
|
|
193
|
+
const out: string[] = [];
|
|
194
|
+
let i = 0;
|
|
195
|
+
while (i < nums.length) {
|
|
196
|
+
let j = i;
|
|
197
|
+
while (j + 1 < nums.length && nums[j + 1] === nums[j] + 1) j++;
|
|
198
|
+
out.push(nums[i] === nums[j] ? String(nums[i]) : nums[i] + '-' + nums[j]);
|
|
199
|
+
i = j + 1;
|
|
200
|
+
}
|
|
201
|
+
return out.join(',');
|
|
202
|
+
};
|
|
203
|
+
const onlyTag = (): string => (onlyMode() ? '(--ONLY ' + rangeCompress(onlyParsed?.set ?? []) + ')' : '');
|
|
204
|
+
let secCur = 0;
|
|
205
|
+
const secRan = new Map<number, number>();
|
|
206
|
+
const secOpen = (n: number, text: string): void => {
|
|
207
|
+
secCur = n;
|
|
208
|
+
if (onlyMode() ? onlySet.has(n) : true) console.log(onlyMode() ? text : '\n' + text);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// ── setup 恆跑向量群(--only 態也重生——跨段消費常數上移;值與原段位逐位等值,僅求值時點上移)──
|
|
212
|
+
// 依賴帳(--only 分段的承載面——每行原段位標註):note/identityA/held/noteKey/cipherGuest/cipherBound ←[2];
|
|
213
|
+
// pass/recToken/wrapped/salt/wrappedRec/unwrapped/recUnwrapped/localIdentity ←[3];origRaw ←[4];
|
|
214
|
+
// TACET2/passD/pinD/dual ←[6];TACET3/w3/dual3 ←[7];ph1v2a ←[8];bip ←[9];
|
|
215
|
+
// TACET_ATTACH/attachAad/attachPayload/attachCt ←[10];TACET4/pass4/passD4 ←[15]([17]KAT17/[19] 跨段消費)。
|
|
216
|
+
const note = '今天寫了一點東西。';
|
|
217
|
+
const identityA = 'acct-aaaaaaaaaaaaaaaa';
|
|
218
|
+
const held = makeHeldKey();
|
|
219
|
+
const noteKey = await generateNoteKey();
|
|
220
|
+
const cipherGuest = await encryptNote(TACET, held, note, 'noteId:n1', { current: () => identityA }); // held 空態=guest 時代(原 [2] 順序——guest 密文先於 held 設值)
|
|
221
|
+
held.set(noteKey);
|
|
222
|
+
const cipherBound = await encryptNote(TACET, held, note, 'noteId:n1', { current: () => identityA });
|
|
223
|
+
const pass = 'correct-horse-battery-staple-42';
|
|
224
|
+
const recToken = generateRecToken();
|
|
225
|
+
const { wrapped, salt } = await wrapNoteKey(TACET, noteKey, pass);
|
|
226
|
+
const wrappedRec = await (await import('../src/client/note-crypto.ts')).wrapNoteKeyWithRecToken(TACET, noteKey, recToken, identityA);
|
|
227
|
+
const unwrapped = await unwrapNoteKey(TACET, wrapped, pass, salt);
|
|
228
|
+
const recUnwrapped = await unwrapNoteKeyWithRecToken(TACET, wrappedRec, recToken, identityA);
|
|
229
|
+
const origRaw = hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey)));
|
|
230
|
+
const localIdentity = 'acct-localwrap-test';
|
|
231
|
+
const TACET2: NoteCryptoConfig = { ...TACET, wrapDual: 'jr2w.', pinSaltPrefix: 'tacet-note-pin1:' };
|
|
232
|
+
const passD = 'dual-factor-passphrase-42';
|
|
233
|
+
const pinD = '2580ab';
|
|
234
|
+
const dual = await wrapNoteKeyDual(TACET2, noteKey, passD, pinD);
|
|
235
|
+
const TACET3: Argon3Config = { wrap3: 'jr3w.', wrapDual3: 'jr3d.', pinSalt3Prefix: 'tacet-note-pin3:' };
|
|
236
|
+
const w3 = await wrapNoteKey3(TACET3, noteKey, pass);
|
|
237
|
+
const dual3 = await wrapNoteKeyDual3(TACET3, noteKey, passD, pinD);
|
|
238
|
+
const ph1v2a = await derivePh1Argon('probe-determinism-pass-42');
|
|
239
|
+
const bip = await import('../src/client/bip39.ts');
|
|
240
|
+
const TACET_ATTACH: NoteCryptoConfig = { ...TACET, cipherAttach: 'jr1c.' };
|
|
241
|
+
const attachAad = 'jr1a:note-abc:att-001';
|
|
242
|
+
const attachPayload = JSON.stringify({ v: 1, kind: 'img', mime: 'image/jpeg', w: 2048, h: 1365, b64: b64(new Uint8Array(2048)) });
|
|
243
|
+
const attachCt = await encryptAttach(TACET_ATTACH, noteKey, attachPayload, attachAad);
|
|
244
|
+
const TACET4: Argon3Config = { wrap4: 'jr4w.', wrapDual4: 'jr4d.', pinSalt3Prefix: 'tacet-note-pin3:' };
|
|
245
|
+
const guest = await crypto.subtle.importKey('raw', enc.encode('seed').slice(0), 'PBKDF2', false, ['deriveKey']); // 型別哨兵:guest 非此路徑——跨段消費([5]/[11]/[13]/[18])
|
|
246
|
+
void guest;
|
|
247
|
+
const pass4 = 'v3-generation-passphrase-42';
|
|
248
|
+
const passD4 = 'dual-factor-passphrase-42'; // NFKC → passD 的全形形
|
|
249
|
+
// srcOf:源碼窗讀取 helper(原 [13] 段內宣告——跨 [14]-[23] 消費=setup 恆跑群;2026-10-09 段級化揚升)
|
|
250
|
+
async function srcOf(rel: string): Promise<string> {
|
|
251
|
+
// node 內建模組動態存取(structured type,零 node types 依賴——argon2.ts getBuiltinModule 母型同構;
|
|
252
|
+
// TS2591 types 帽下 import('node:fs') 靜態/動態皆炸=此繞法)
|
|
253
|
+
const fs = (globalThis as unknown as {
|
|
254
|
+
process?: { getBuiltinModule?: (id: string) => { readFileSync?: (p: string | URL, enc: string) => string } | undefined };
|
|
255
|
+
}).process?.getBuiltinModule?.('node:fs');
|
|
256
|
+
if (!fs?.readFileSync) throw new Error('ERR_FS_UNAVAILABLE');
|
|
257
|
+
return fs.readFileSync(new URL(rel, import.meta.url), 'utf8');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const TACET_GUESTOK: NoteCryptoConfig = { ...TACET }; // cipherGuest='jr1g.' 在場,專測 identity 面
|
|
261
|
+
const emptyIdp = { current: () => '' };
|
|
262
|
+
const dual4 = await wrapNoteKeyDual4(TACET4, noteKey, passD4, pinD); // 全形 wrap=NFKC 載體(cp1=0xFF44——ASCII 化即本面 RED)
|
|
130
263
|
// ── 1. 基礎向量 ─────────────────────────────────────────────────────────────
|
|
131
264
|
|
|
132
|
-
|
|
265
|
+
secOpen(1, '[1] PH1 / PBKDF2 / hash-ladder 基礎'); if (secEnter(1)) {
|
|
133
266
|
await A('PH1 = SHA-256(pass) 穩定', (await ph1Of('test-pass-123')) === (await ph1Of('test-pass-123')));
|
|
134
267
|
await A('PH1 hex64 格式', /^[0-9a-f]{64}$/.test(await ph1Of('x')));
|
|
135
268
|
await A('PBKDF2_ITERATIONS = 600k', PBKDF2_ITERATIONS === 600_000);
|
|
136
269
|
|
|
137
270
|
// ── 2. roundtrip(guest 時代 + 綁定時代,AAD 由呼叫端) ───────────────────────
|
|
138
271
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const identityA = 'acct-aaaaaaaaaaaaaaaa';
|
|
142
|
-
const guest = await crypto.subtle.importKey('raw', enc.encode('seed').slice(0), 'PBKDF2', false, ['deriveKey']); // 型別哨兵:guest 非此路徑
|
|
143
|
-
void guest;
|
|
144
|
-
const held = makeHeldKey();
|
|
145
|
-
const cipherGuest = await encryptNote(TACET, held, note, 'noteId:n1', { current: () => identityA });
|
|
272
|
+
};
|
|
273
|
+
secOpen(2, '[2] roundtrip / AAD 防搬移'); if (secEnter(2)) {
|
|
146
274
|
await A('guest 時代前綴 jr1g.', cipherGuest.startsWith('jr1g.'));
|
|
147
275
|
await A('guest roundtrip', (await decryptNote(TACET, held, cipherGuest, 'noteId:n1', { current: () => identityA })) === note);
|
|
148
276
|
|
|
149
|
-
const noteKey = await generateNoteKey();
|
|
150
|
-
held.set(noteKey);
|
|
151
|
-
const cipherBound = await encryptNote(TACET, held, note, 'noteId:n1', { current: () => identityA });
|
|
152
277
|
await A('綁定時代前綴 jr1b.', cipherBound.startsWith('jr1b.'));
|
|
153
278
|
await A('綁定 roundtrip', (await decryptNote(TACET, held, cipherBound, 'noteId:n1', { current: () => identityA })) === note);
|
|
154
279
|
|
|
@@ -164,24 +289,19 @@ await A('舊明文原樣返回', (await decryptNote(TACET, held, '純舊明文',
|
|
|
164
289
|
// ── 3. 包裹三件套(pass / rec / 本機;2026-09-24 效能節——buildBindPayload 兩段
|
|
165
290
|
// PBKDF2 600k 已收 Promise.all 並行,node 實測 133-152ms→68-84ms) ──
|
|
166
291
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const recToken = generateRecToken();
|
|
170
|
-
const { wrapped, salt } = await wrapNoteKey(TACET, noteKey, pass);
|
|
171
|
-
const wrappedRec = await (await import('../src/client/note-crypto.ts')).wrapNoteKeyWithRecToken(TACET, noteKey, recToken, identityA);
|
|
292
|
+
};
|
|
293
|
+
secOpen(3, '[3] 金鑰包裹(passphrase / 復原 / 本機)'); if (secEnter(3)) {
|
|
172
294
|
const recTokenHashValue = await recTokenHash(recToken);
|
|
173
295
|
await A('wrapped 前綴 jr1w.', wrapped.startsWith('jr1w.'));
|
|
174
296
|
await A('salt = 16B hex', /^[0-9a-f]{32}$/.test(salt));
|
|
175
297
|
await A('wrappedRec 前綴 jr1w.', wrappedRec.startsWith('jr1w.'));
|
|
176
298
|
await A('recTokenHash hex64', /^[0-9a-f]{64}$/.test(recTokenHashValue));
|
|
177
299
|
|
|
178
|
-
const unwrapped = await unwrapNoteKey(TACET, wrapped, pass, salt);
|
|
179
300
|
await A('pass unwrap 救回 noteKey(同 pass 同 salt)',
|
|
180
301
|
unwrapped !== null && hex(new Uint8Array(await crypto.subtle.exportKey('raw', unwrapped))) ===
|
|
181
302
|
hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey))));
|
|
182
303
|
await A('錯誤 pass unwrap → null', (await unwrapNoteKey(TACET, wrapped, 'wrong-pass', salt)) === null);
|
|
183
304
|
|
|
184
|
-
const recUnwrapped = await unwrapNoteKeyWithRecToken(TACET, wrappedRec, recToken, identityA);
|
|
185
305
|
await A('rec unwrap 救回 noteKey',
|
|
186
306
|
recUnwrapped !== null && hex(new Uint8Array(await crypto.subtle.exportKey('raw', recUnwrapped))) ===
|
|
187
307
|
hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey))));
|
|
@@ -196,14 +316,14 @@ await A('改密語:新包裹解得回同 noteKey',
|
|
|
196
316
|
hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey))));
|
|
197
317
|
|
|
198
318
|
// 本機包裹
|
|
199
|
-
const localIdentity = 'acct-localwrap-test';
|
|
200
319
|
const heldLocal = makeHeldKey();
|
|
201
320
|
heldLocal.set(noteKey);
|
|
202
321
|
await storeLocalWrap(TACET, localIdentity, noteKey);
|
|
203
322
|
|
|
204
323
|
// ── 4. extractable 鐵律(2026-09-06 iOS 實機炸點回歸) ────────────────────────
|
|
205
324
|
|
|
206
|
-
|
|
325
|
+
};
|
|
326
|
+
secOpen(4, '[4] noteKey extractable 鐵律'); if (secEnter(4)) {
|
|
207
327
|
const rawNote = crypto.getRandomValues(new Uint8Array(32));
|
|
208
328
|
const nonExtractable = await crypto.subtle.importKey('raw', rawNote, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
|
|
209
329
|
let threwNotExtractable = false;
|
|
@@ -218,7 +338,6 @@ const extractable = await crypto.subtle.importKey('raw', rawNote, { name: 'AES-G
|
|
|
218
338
|
await A('修法機制:extractable key exportKey 等值 roundtrip',
|
|
219
339
|
hex(new Uint8Array(await crypto.subtle.exportKey('raw', extractable))) === hex(rawNote));
|
|
220
340
|
// unwrap 出的 key(全部三路徑)都要能再包裹:與 noteKey 原值比對(非 rawNote——那是本段新建隨機值)
|
|
221
|
-
const origRaw = hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey)));
|
|
222
341
|
await A('unwrap 出的 noteKey 可再 exportKey(等值 noteKey)',
|
|
223
342
|
hex(new Uint8Array(await crypto.subtle.exportKey('raw', unwrapped!))) === origRaw);
|
|
224
343
|
await A('rec unwrap 出的 noteKey 可再 exportKey(等值 noteKey)',
|
|
@@ -226,7 +345,8 @@ await A('rec unwrap 出的 noteKey 可再 exportKey(等值 noteKey)',
|
|
|
226
345
|
|
|
227
346
|
// ── 5. 時代隔離:guest 與 bound 前綴互不誤判 ─────────────────────────────────
|
|
228
347
|
|
|
229
|
-
|
|
348
|
+
};
|
|
349
|
+
secOpen(5, '[5] 時代隔離'); if (secEnter(5)) {
|
|
230
350
|
await A('guest cipher 不以 jr1b. 開頭', !cipherGuest.startsWith('jr1b.'));
|
|
231
351
|
await A('bound cipher 不以 jr1g. 開頭', !cipherBound.startsWith('jr1g.'));
|
|
232
352
|
await A('無 held key 的 bound 解密 → null',
|
|
@@ -234,11 +354,8 @@ await A('無 held key 的 bound 解密 → null',
|
|
|
234
354
|
|
|
235
355
|
// ── 6. 雙因子合鑰(jr2w.,2026-09-09 PIN 第二因子) ─────────────────────────
|
|
236
356
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const passD = 'dual-factor-passphrase-42';
|
|
240
|
-
const pinD = '2580ab';
|
|
241
|
-
const dual = await wrapNoteKeyDual(TACET2, noteKey, passD, pinD);
|
|
357
|
+
};
|
|
358
|
+
secOpen(6, '[6] 雙因子合鑰 KEK2(jr2w.)'); if (secEnter(6)) {
|
|
242
359
|
await A('wrapped2 前綴 jr2w.', dual.wrapped.startsWith('jr2w.'));
|
|
243
360
|
await A('dual salt1 = 16B hex', /^[0-9a-f]{32}$/.test(dual.salt));
|
|
244
361
|
const unwrapped2 = await unwrapNoteKeyDual(TACET2, dual.wrapped, passD, pinD, dual.salt);
|
|
@@ -287,13 +404,12 @@ await A('未配置 wrapDual 拒絕 dual 包裹', (await unwrapNoteKeyDual(TACET,
|
|
|
287
404
|
// Argon2id 記憶體困難(64MiB)使每猜成本上升 1-2 個數量級。遷移契約 = 成功 unwrap
|
|
288
405
|
// 後客戶端靜默 re-wrap,舊前綴照解(下面有並存斷言)。
|
|
289
406
|
|
|
290
|
-
|
|
291
|
-
|
|
407
|
+
};
|
|
408
|
+
secOpen(7, '[7] Argon2id 包裹(jr3w./jr3d.)'); if (secEnter(7)) {
|
|
292
409
|
await A('RFC 9106 無 secret/ad 標準向量(當前載體正確性)', await verifyArgonKat());
|
|
293
410
|
await A('Argon 參數契約 m=64MiB t=3 p=1 tag=32',
|
|
294
411
|
ARGON_MEMORY_KIB === 65536 && ARGON_ITERATIONS === 3 && ARGON_PARALLELISM === 1 && ARGON_TAG_LEN === 32);
|
|
295
412
|
|
|
296
|
-
const w3 = await wrapNoteKey3(TACET3, noteKey, pass);
|
|
297
413
|
await A('wrapped 前綴 jr3w.', w3.wrapped.startsWith('jr3w.'));
|
|
298
414
|
await A('jr3w salt = 16B hex', /^[0-9a-f]{32}$/.test(w3.salt));
|
|
299
415
|
const unwrapped3 = await unwrapNoteKey3(TACET3, w3.wrapped, pass, w3.salt);
|
|
@@ -317,7 +433,6 @@ await A('jr3w payload 竄改 iv 區 → null', await tampered3(3));
|
|
|
317
433
|
await A('jr3w payload 竄改 ct 尾 → null', await tampered3(payload3.length - 1));
|
|
318
434
|
|
|
319
435
|
// jr3d 雙因子(PIN 第二因子,Argon2id 版)
|
|
320
|
-
const dual3 = await wrapNoteKeyDual3(TACET3, noteKey, passD, pinD);
|
|
321
436
|
await A('dual3 前綴 jr3d.', dual3.wrapped.startsWith('jr3d.'));
|
|
322
437
|
await A('dual3 salt1 = 16B hex', /^[0-9a-f]{32}$/.test(dual3.salt));
|
|
323
438
|
const unwrappedD3 = await unwrapNoteKeyDual3(TACET3, dual3.wrapped, passD, pinD, dual3.salt);
|
|
@@ -354,9 +469,9 @@ await A('未配置 wrapDual3 → unwrap 拒收 jr3d',
|
|
|
354
469
|
// 每猜成本從 ~ns(SHA-256×2)升到 ~0.1-0.18s CPU=×10⁴-10⁶。
|
|
355
470
|
// 固定域鹽是被迫設計(per-user 鹽會摧毀 PH2 UNIQUE=幽靈帳號機制基礎);改參數 = 換鹽尾碼(v2/v3)重遷移。
|
|
356
471
|
|
|
357
|
-
|
|
472
|
+
};
|
|
473
|
+
secOpen(8, '[8] PH1 v2(登入憑證 Argon2id 派生)'); if (secEnter(8)) {
|
|
358
474
|
await A('RFC 9106 KAT 先行(載體正確性,本節所有 Argon 斷言的前提)', await verifyArgonKat());
|
|
359
|
-
const ph1v2a = await derivePh1Argon('probe-determinism-pass-42');
|
|
360
475
|
await A('ph1v2 確定性(同 pass 兩跑逐位元同)', ph1v2a === (await derivePh1Argon('probe-determinism-pass-42')));
|
|
361
476
|
await A('ph1v2 hex64 形(validHash64 可收)', /^[0-9a-f]{64}$/.test(ph1v2a));
|
|
362
477
|
// 與 legacy ph1(SHA-256 形)必然不同值=雙查表兩鍵語意成立
|
|
@@ -378,8 +493,8 @@ await A('ph1v2 非恆等(pass ≠ 輸出)', ph1v2a !== 'probe-determinism-pa
|
|
|
378
493
|
|
|
379
494
|
// ── 9. BIP39 復原套件契約(words↔hex64 轉寫層;線上契約 recToken hex64 不變) ──
|
|
380
495
|
|
|
381
|
-
|
|
382
|
-
|
|
496
|
+
};
|
|
497
|
+
secOpen(9, '[9] BIP39 復原套件(24 詞 ⇄ entropy 32B ⇄ hex64)'); if (secEnter(9)) {
|
|
383
498
|
const wl = await import('../src/client/wordlist.ts');
|
|
384
499
|
|
|
385
500
|
const kitWords = await bip.generateBip39Words();
|
|
@@ -451,7 +566,21 @@ try {
|
|
|
451
566
|
}
|
|
452
567
|
await A('與 @scure/bip39 參照 200 組雙向一致', refOk);
|
|
453
568
|
} catch {
|
|
454
|
-
|
|
569
|
+
// 斷鏈(symlink 落空)與真缺席要分流:裸 npm ci 環境的確定性病是「參照組斷鏈」——
|
|
570
|
+
// 訊息指向 vendor 修復(prepare-core-pkg.cjs),不是泛化「未安裝」(對斷鏈態毫無作用=誤導指示)。
|
|
571
|
+
// 路徑錨定 repo root(t_87ef62dd NIT-1):裸 '@scure/bip39' 是 CWD 相對——他 cwd 跑閘
|
|
572
|
+
// (如 scripts/ 子目錄)會把斷鏈態誤報「未安裝」;錨 import.meta.url 起手=CWD 無關恆真。
|
|
573
|
+
const fsMod = (globalThis as unknown as { process?: { getBuiltinModule?: (id: string) => { lstatSync?: (p: string) => { isSymbolicLink(): boolean }; existsSync?: (p: string) => boolean } | undefined } }).process?.getBuiltinModule?.('node:fs');
|
|
574
|
+
const repoRootRef = new URL('../node_modules/@scure/bip39', import.meta.url).pathname.replace(/\/$/, '');
|
|
575
|
+
const isBrokenLink = !!fsMod?.lstatSync && !!fsMod.existsSync && (() => {
|
|
576
|
+
try {
|
|
577
|
+
return fsMod.lstatSync(repoRootRef).isSymbolicLink() && !fsMod.existsSync(repoRootRef);
|
|
578
|
+
} catch { return false; } // ENOENT=真缺席(未安裝),不是斷鏈
|
|
579
|
+
})();
|
|
580
|
+
await A('與 @scure/bip39 參照 200 組雙向一致', false,
|
|
581
|
+
isBrokenLink
|
|
582
|
+
? '參照套件斷鏈(@scure/bip39 symlink realpath 不存在)——重跑 npm ci(plain)再跑 scripts/prepare-core-pkg.cjs vendor 修復'
|
|
583
|
+
: '參照套件未安裝(devDependencies @scure/bip39)');
|
|
455
584
|
}
|
|
456
585
|
|
|
457
586
|
// 與現行包裹鏈相容:words 造的 hex64 走 recTokenHash/wrapNoteKeyWithRecToken 原樣
|
|
@@ -463,16 +592,13 @@ await A('words 派生 hex64 unwrap 救回 noteKey',
|
|
|
463
592
|
|
|
464
593
|
// ── 10. 附件密文(jr1c.,image attachments;opt-in 未配置即拒) ───────────────
|
|
465
594
|
|
|
466
|
-
|
|
595
|
+
};
|
|
596
|
+
secOpen(10, '[10] 附件密文(jr1c. cipherAttach opt-in)'); if (secEnter(10)) {
|
|
467
597
|
const bareCfgAttach: NoteCryptoConfig = { ...TACET }; // 無 cipherAttach 欄
|
|
468
598
|
let attachCfgThrow = '';
|
|
469
599
|
try { await encryptAttach(bareCfgAttach, noteKey, '{"v":1}', 'jr1a:n1:a1'); } catch (e) { attachCfgThrow = (e as Error).message; }
|
|
470
600
|
await A('未配置 cipherAttach → encryptAttach throw', attachCfgThrow === 'ERR_ATTACH_NOT_CONFIGURED', attachCfgThrow);
|
|
471
601
|
|
|
472
|
-
const TACET_ATTACH: NoteCryptoConfig = { ...TACET, cipherAttach: 'jr1c.' };
|
|
473
|
-
const attachAad = 'jr1a:note-abc:att-001';
|
|
474
|
-
const attachPayload = JSON.stringify({ v: 1, kind: 'img', mime: 'image/jpeg', w: 2048, h: 1365, b64: b64(new Uint8Array(2048)) });
|
|
475
|
-
const attachCt = await encryptAttach(TACET_ATTACH, noteKey, attachPayload, attachAad);
|
|
476
602
|
await A('附件前綴 jr1c.', attachCt.startsWith('jr1c.'));
|
|
477
603
|
await A('附件 roundtrip(顯式 noteKey)',
|
|
478
604
|
(await decryptAttach(TACET_ATTACH, noteKey, attachCt, attachAad)) === attachPayload);
|
|
@@ -483,7 +609,8 @@ await A('附件錯鑰匙 → null',
|
|
|
483
609
|
|
|
484
610
|
// ── 11. 本機 IDB 密文(jr1d. cipherLocal opt-in)+ guest 選配拒絕面 ──────────
|
|
485
611
|
|
|
486
|
-
|
|
612
|
+
};
|
|
613
|
+
secOpen(11, '[11] 本機 IDB 密文(jr1d. cipherLocal opt-in)+ guest 選配拒絕面'); if (secEnter(11)) {
|
|
487
614
|
// 分享包裹(jrsw.,wrapShare 選配)+ jr3s. roundtrip(閘本有 roundtrip 斷言缺席)
|
|
488
615
|
const TACET_SHARE: NoteCryptoConfig = { ...TACET, wrapShare: 'jrsw.' };
|
|
489
616
|
const shareWrapped = await wrapNoteKeyShare(TACET_SHARE, noteKey, 'share-pass-42');
|
|
@@ -592,8 +719,6 @@ await A('既有 guest 配置 roundtrip 不受選配化影響',
|
|
|
592
719
|
(await decryptNote(TACET, makeHeldKey(), cipherGuest, 'noteId:n1', { current: () => identityA })) === note);
|
|
593
720
|
|
|
594
721
|
// guest 空 identity 拒絕面:K_u = SHA-256(prefix ‖ '') 靜默產出「空帳號金鑰」是誤配炸彈
|
|
595
|
-
const TACET_GUESTOK: NoteCryptoConfig = { ...TACET }; // cipherGuest='jr1g.' 在場,專測 identity 面
|
|
596
|
-
const emptyIdp = { current: () => '' };
|
|
597
722
|
let emptyIdMsg: string | null = null;
|
|
598
723
|
try { await encryptNote(TACET_GUESTOK, makeHeldKey(), note, 'noteId:n1', emptyIdp); } catch (e) { emptyIdMsg = (e as Error).message; }
|
|
599
724
|
await A("guest 加密面空 identity → throw ERR_NO_IDENTITY(falsy 一體:空字串/undefined/null)",
|
|
@@ -623,7 +748,8 @@ await A("畸形 guest 前綴 '' → decryptNote 舊明文相容層同「未配
|
|
|
623
748
|
// ── 12. 伺服器端原語(makeInboundCipher/validWrappedKey/pickKeyPackage/
|
|
624
749
|
// checkRate/timingSafeEq)+ 本機鎖定全函式(pinlock jr1p.) ──────────
|
|
625
750
|
|
|
626
|
-
|
|
751
|
+
};
|
|
752
|
+
secOpen(12, '[12] 伺服器端原語(auth/ratelimit/hash)+ 本機鎖定(pinlock)'); if (secEnter(12)) {
|
|
627
753
|
// ratelimit 行為面(UPSERT…RETURNING 單句設計收口):真 node:sqlite 直載 UPSERT…RETURNING 單句,
|
|
628
754
|
// 行為斷言全數「真生產函式 checkRate 直驅」——D1 形 shim 包 node:sqlite 只做 D1 run() 的
|
|
629
755
|
// {results} 回帶形,無測試端 glue 副本。表名直插 SQL=注入面驗證;放行判準 count<=max
|
|
@@ -855,7 +981,8 @@ await A('jr1p 跨家族:jr1p. 字串餵 jr1w unwrap → null',
|
|
|
855
981
|
|
|
856
982
|
// ── 13. 新契約面(hex fail-closed/salt hex 形收口/PH1 鹽注入/錯誤碼語意分離) ──
|
|
857
983
|
|
|
858
|
-
|
|
984
|
+
};
|
|
985
|
+
secOpen(13, '[13] unwrap 驗證統一新契約(fail-closed + 語意分離)'); if (secEnter(13)) {
|
|
859
986
|
// hexToBytes fail-closed(note-crypto 與 bip39 同步——bip39 面是 HEX64_RE 前置自守)
|
|
860
987
|
await A('hexToBytes 合法輸出不變(舊碼同值;新向量)', hexToBytes('00ff10').join(',') === '0,255,16');
|
|
861
988
|
await A('hexToBytes 大小寫正規化(新行為,舊碼同值)', (() => { const a = Array.from(hexToBytes('AB')); const b = Array.from(hexToBytes('ab')); return a.length === b.length && a.every((v, i) => v === b[i]); })());
|
|
@@ -873,6 +1000,56 @@ await A(`hexToBytes 全形數字('2f')→ Bytes 等值(NFKC 窗殲滅
|
|
|
873
1000
|
await A('bip39 hexToBytes 同步(HEX64_RE 自守前提下的舊向量不變)', (await bip.recTokenToWords('ab'.repeat(32)))?.length === 24);
|
|
874
1001
|
await A('unwrap:鹽 hex 非法字元 → 假錯誤誘餌面 null(原碼靜默歸零會錯誤炸出成功路徑)',
|
|
875
1002
|
(await unwrapNoteKey(TACET2, dual.wrapped, passD, 'a'.repeat(31) + 'z')) === null);
|
|
1003
|
+
// ── 13-2. 包裹前綴快檢上移(t_87ef62dd,外審 #2):錯前綴 junk 在 KDF 派生前即 null——
|
|
1004
|
+
// 母型 unwrapNoteKey4 入口快檢(argon2.ts);五入口(jr1w./jrsw./jr3w./jr3s.+recToken 兩腿)
|
|
1005
|
+
// 行為恆 null 不拋(快檢同向 null=契約零變——錯前綴本就由 openNoteKey:417 拒);
|
|
1006
|
+
// 收益面=錯前綴試探不再付 600k PBKDF2/64MiB Argon2id KDF 成本(計時面對真 KDF 斷言)。
|
|
1007
|
+
{
|
|
1008
|
+
const junkPrefix = 'jr9w.' + wrapped.slice('jr1w.'.length); // 真形 payload 錯前綴(junk 前綴面)
|
|
1009
|
+
const t0Junk = performance.now();
|
|
1010
|
+
const junkRc = [
|
|
1011
|
+
await unwrapNoteKey(TACET, junkPrefix, pass, salt),
|
|
1012
|
+
await unwrapNoteKeyShare(TACET, junkPrefix, pass, salt),
|
|
1013
|
+
await unwrapNoteKey3(TACET3, junkPrefix, pass, salt),
|
|
1014
|
+
await unwrapNoteKey3(TACET3, junkPrefix, 'wrong-passphrase', salt),
|
|
1015
|
+
await unwrapNoteKeyShare3(TACET3, junkPrefix, pass, salt),
|
|
1016
|
+
];
|
|
1017
|
+
const tJunkMs = performance.now() - t0Junk;
|
|
1018
|
+
await A('錯前綴 junk 試探(真 payload 錯前綴面)五入口恆 null 不拋(快檢同向 null=契約零變)', junkRc.every(r => r === null), JSON.stringify(junkRc.map(r => r === null)));
|
|
1019
|
+
// 行為承載面(KDF 未走即 null):PBKDF2 600k ≈ 60-150ms/Argon2id 64MiB ≈ 120-150ms——
|
|
1020
|
+
// 任一入口真付 KDF 即超帽;快檢落地後五次試探總計 ≪1ms(實測 <1ms;帽 30ms=數量級餘裕)。
|
|
1021
|
+
// 結構面(真防線)=下方源碼計數錨+接線形——計時帽是行為輔助面(未來快機 PBKDF2 縮水時
|
|
1022
|
+
// 計數錨仍承載);對照組只承載「正確前綴快檢後仍真 unwrap」=零變更契約行為面(不計時——
|
|
1023
|
+
// 絕對時距對照在快慢機間是 flake 源,tacet-dev runner 母型)。
|
|
1024
|
+
await A('錯前綴 junk 試探五入口計時帽 ≪ KDF 成本(快檢在 deriveKek/deriveKekArgon 前——真付 KDF 即超帽)', tJunkMs < 30, 'junk5=' + tJunkMs.toFixed(2) + 'ms');
|
|
1025
|
+
const t2Legacy = performance.now();
|
|
1026
|
+
const legacyBack = await unwrapNoteKey(TACET, wrapped, pass, salt);
|
|
1027
|
+
await A('對照組:正確前綴 unwrap 真付 KDF 且救回 noteKey(快檢零變更契約——正確路徑行為面)',
|
|
1028
|
+
legacyBack !== null && hex(new Uint8Array(await crypto.subtle.exportKey('raw', legacyBack))) ===
|
|
1029
|
+
hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey))), 'elapsed=' + (performance.now() - t2Legacy).toFixed(2) + 'ms');
|
|
1030
|
+
// 源碼計數錨(快檢五點+recToken 雙腿接線=tacet-dev「條款與接線分離」):五入口 startsWith 快檢
|
|
1031
|
+
// 恰 5(摘任一=計數變異);recToken 雙腿錨——jr1r. 面在 deriveRecKek 前、cfg.wrap 面在
|
|
1032
|
+
// deriveRecLegacyKek 前的相鄰形(錯位接線——快檢搬進 KDF 之後=形變異即 RED)。
|
|
1033
|
+
const ncSrc132 = await srcOf('../src/client/note-crypto.ts');
|
|
1034
|
+
const arSrc132 = await srcOf('../src/client/argon2.ts');
|
|
1035
|
+
const qcNc = (ncSrc132.match(/if \(!wrapped\.startsWith\(cfg\.(wrap|wrapShare|wrapRec|wrap)\)\) return null;/g) ?? []).length;
|
|
1036
|
+
const scNc = ncSrc132.split('wrapped.startsWith(cfg.wrapRec)').length - 1; // jr1r. 腿條件面(跳過形)
|
|
1037
|
+
const qcAr = (arSrc132.match(/if \(!wrapped\.startsWith\(cfg\.(wrap3|wrapShare3)\)\) return null;/g) ?? []).length;
|
|
1038
|
+
await A('源碼計數錨:startsWith 快檢恰 6 條(note-crypto 4=jr1w./jrsw./舊腿終面/jr1r. 條件面+argon2 2;錯前綴試探防線——摘任一條即計數變異 RED)',
|
|
1039
|
+
qcNc === 3 && scNc === 1 && qcAr === 2, 'nc=' + qcNc + ' sc=' + scNc + ' ar=' + qcAr);
|
|
1040
|
+
await A('源碼計數錨:recToken 雙腿快檢接線形(jr1r. 腿條件面在 deriveRecKek 前——禁 return null 短路雙試;舊面終面接線在 deriveRecLegacyKek 前)',
|
|
1041
|
+
/if \(cfg\.wrapRec && cfg\.recKekHkdf && wrapped\.startsWith\(cfg\.wrapRec\)\) \{\n {6}const recKek = await deriveRecKek\(/.test(ncSrc132)
|
|
1042
|
+
&& /if \(!cfg\.wrap\) return null;\n {4}if \(!wrapped\.startsWith\(cfg\.wrap\)\) return null; \/\/ 快速前綴快檢:錯前綴 junk 免付 600k PBKDF2 成本\n {4}const legacyKek = await deriveRecLegacyKek\(/.test(ncSrc132));
|
|
1043
|
+
// 毒化錨前綴自檢(錨窗同字面點數——毒化矩陣 Q 案的錯位防線;jr1w./舊腿兩行同註解開頭——
|
|
1044
|
+
// 錨帶全註解頭消歧:『(t_87ef62dd)』=jr1w. 面、『:錯前綴』=舊腿終面)
|
|
1045
|
+
await A('快檢錨前綴自檢:五錨各恰 1(split 計數帳)',
|
|
1046
|
+
ncSrc132.split('if (!wrapped.startsWith(cfg.wrap)) return null; // 快速前綴快檢(t_87ef62dd)').length === 2
|
|
1047
|
+
&& ncSrc132.split('if (!wrapped.startsWith(cfg.wrap)) return null; // 快速前綴快檢:').length === 2
|
|
1048
|
+
&& ncSrc132.split('if (!wrapped.startsWith(cfg.wrapShare)) return null; // 快速前綴快檢').length === 2
|
|
1049
|
+
&& ncSrc132.split('if (cfg.wrapRec && cfg.recKekHkdf && wrapped.startsWith(cfg.wrapRec)) {').length === 2
|
|
1050
|
+
&& arSrc132.split('if (!wrapped.startsWith(cfg.wrap3)) return null;').length === 2
|
|
1051
|
+
&& arSrc132.split('if (!wrapped.startsWith(cfg.wrapShare3)) return null;').length === 2);
|
|
1052
|
+
}
|
|
876
1053
|
await A('unwrapNoteKeyDual:salt1 hex 非法字元 ' + 'z' + ' → null(鹽內嵌族 own 檢查照舊——收口本體=鹽外置族)',
|
|
877
1054
|
(await unwrapNoteKeyDual(TACET2, dual.wrapped, passD, pinD, 'a'.repeat(31) + 'z')) === null);
|
|
878
1055
|
await A('unwrapNoteKeyDual:salt1 hex 奇數長 → null', (await unwrapNoteKeyDual(TACET2, dual.wrapped, passD, pinD, 'a'.repeat(31))) === null);
|
|
@@ -963,15 +1140,6 @@ await A('wrapNoteKeyDual3 未配置 wrapDual3 → ERR_JR3W_NOT_CONFIGURED(契
|
|
|
963
1140
|
// PH1 鹽注入:預設舊值零行為變更
|
|
964
1141
|
await A('derivePh1Argon 預設鹽零行為變更(同 pass 同值)',
|
|
965
1142
|
(await derivePh1Argon('probe-determinism-pass-42')) === ph1v2a);
|
|
966
|
-
async function srcOf(rel: string): Promise<string> {
|
|
967
|
-
// node 內建模組動態存取(structured type,零 node types 依賴——argon2.ts getBuiltinModule 母型同構;
|
|
968
|
-
// TS2591 types 帽下 import('node:fs') 靜態/動態皆炸=此繞法)
|
|
969
|
-
const fs = (globalThis as unknown as {
|
|
970
|
-
process?: { getBuiltinModule?: (id: string) => { readFileSync?: (p: string, enc: string) => string } | undefined };
|
|
971
|
-
}).process?.getBuiltinModule?.('node:fs');
|
|
972
|
-
if (!fs?.readFileSync) throw new Error('ERR_FS_UNAVAILABLE');
|
|
973
|
-
return fs.readFileSync(new URL(rel, import.meta.url).pathname, 'utf8');
|
|
974
|
-
}
|
|
975
1143
|
await A('derivePh1Argon 預設分支源碼面 = encode(PH1_V2_SALT)(鹽預設單一真相錨;同-pass 同值斷言對預設值漂移無承載力)',
|
|
976
1144
|
/saltArg \?\? new TextEncoder\(\)\.encode\(PH1_V2_SALT\)/.test(await srcOf('../src/client/argon2.ts')));
|
|
977
1145
|
await A('derivePh1Argon 自選鹽 → 不同派生值', (await derivePh1Argon('probe-determinism-pass-42', new Uint8Array(12))) !== ph1v2a);
|
|
@@ -1003,6 +1171,20 @@ await A('openNoteKey 直接呼叫:壞 base64 → null 不拋(本體吞收點
|
|
|
1003
1171
|
return (await openNoteKey('jr1w.' + '!!not-base64!!', kekC, 'notekey', 'jr1w.')) === null;
|
|
1004
1172
|
})());
|
|
1005
1173
|
|
|
1174
|
+
//── 13-3. 發行 tarball 治理面(外審 #7 的封閉集承載——open-card-pr.sh 摘除的 tarball 面對位;t_87ef62dd)──
|
|
1175
|
+
//(pack 驗證讀 package.json files 白名單——npm 不可用環境 = 顯性 SKIP 形自守衛,非 silent true)
|
|
1176
|
+
await A('[22] 發行 tarball 零 open-card-pr.sh(治理掃蕩面——pack 白名單逐檔對帳),pack 不可用 = SKIP 顯形', await (async () => {
|
|
1177
|
+
try {
|
|
1178
|
+
const cp132 = (globalThis as unknown as { process?: { getBuiltinModule?: (id: string) => { execFileSync?: (c: string, a: string[], o: Record<string, unknown>) => { toString(enc: string): string } } } }).process?.getBuiltinModule?.('node:child_process');
|
|
1179
|
+
if (!cp132?.execFileSync) return false; // 自守衛:缺席=顯性 FAIL(非 silent true)
|
|
1180
|
+
const out132 = cp132.execFileSync('npm', ['pack', '--json', '--dry-run'], { cwd: new URL('..', import.meta.url).pathname, encoding: 'buffer', timeout: 120000 });
|
|
1181
|
+
const j132 = JSON.parse(out132.toString('utf8')) as { files?: { path: string }[] }[];
|
|
1182
|
+
const files132 = (j132[0]?.files ?? []).map((f) => f.path);
|
|
1183
|
+
const g132 = files132.filter((p) => p === 'scripts/open-card-pr.sh' || p.startsWith('scripts/'));
|
|
1184
|
+
return files132.length > 0 && !files132.includes('scripts/open-card-pr.sh') && g132.length === 2;
|
|
1185
|
+
} catch { return false; }
|
|
1186
|
+
})());
|
|
1187
|
+
|
|
1006
1188
|
// ── 14. 效能形契約(零行為變更——輸出 byte 等價是合約;錨面咬「形」) ──
|
|
1007
1189
|
//
|
|
1008
1190
|
// 效能批的閘承載物理:執行時間不能進閘(機器相依)——咬「並行形在場+串行殘留歸零」
|
|
@@ -1011,7 +1193,8 @@ await A('openNoteKey 直接呼叫:壞 base64 → null 不拋(本體吞收點
|
|
|
1011
1193
|
// buildBindPayload 串行 133-152ms→並行 68-84ms;dual 串行 234→並行 179ms median;
|
|
1012
1194
|
// b64 4MiB 132-181ms→18ms。瀏覽器實測 222ms→27ms。
|
|
1013
1195
|
|
|
1014
|
-
|
|
1196
|
+
};
|
|
1197
|
+
secOpen(14, '[14] 效能形契約(buildBindPayload 並行/b64 分塊/argon 禁並行/CORS Max-Age/sideEffects)'); if (secEnter(14)) {
|
|
1015
1198
|
|
|
1016
1199
|
const noteCryptoSrc14 = await srcOf('../src/client/note-crypto.ts');
|
|
1017
1200
|
const argon2Src14 = await srcOf('../src/client/argon2.ts');
|
|
@@ -1152,13 +1335,11 @@ await A('[14] corsResponse 仍完整轉發四頭(corsResponse 面帶 Max-Age
|
|
|
1152
1335
|
// 函式體零改=raw 契約永久不變——本節同字面雙家族對照向量(PASS12x/PASS12x)就是
|
|
1153
1336
|
// 「免換前綴合約證明」的承載面:同組字串在舊家族恆異 KEK(raw 直派生)、在 v3 家族恆同 KEK(NFKC 收容)。
|
|
1154
1337
|
|
|
1155
|
-
|
|
1338
|
+
};
|
|
1339
|
+
secOpen(15, '[15] 密語正規化 v3 世代(normalizePassphrase/jr4w./jr4d./PH1 v3)'); if (secEnter(15)) {
|
|
1156
1340
|
|
|
1157
1341
|
const noteCryptoSrc15 = await srcOf('../src/client/note-crypto.ts');
|
|
1158
1342
|
const argon2Src15 = await srcOf('../src/client/argon2.ts');
|
|
1159
|
-
const TACET4: Argon3Config = { wrap4: 'jr4w.', wrapDual4: 'jr4d.', pinSalt3Prefix: 'tacet-note-pin3:' };
|
|
1160
|
-
const pass4 = 'v3-generation-passphrase-42';
|
|
1161
|
-
const passD4 = 'dual-factor-passphrase-42'; // NFKC → passD 的全形形
|
|
1162
1343
|
const passD4_NFKC = normalizePassphrase(passD4); // 與 passD 恆等(NFKC 收容)——P2 毒化翻面本體(毒形即收口形計數錨翻+毒翻 = NFKC-effective 帳面翻,雙承載)
|
|
1163
1344
|
|
|
1164
1345
|
// 函式契約面(單一真相本體:normalizePassphrase 與 normalizePin 並存不混)
|
|
@@ -1191,7 +1372,6 @@ await A('jr4w 錯 pass → null', (await unwrapNoteKey4(TACET4, w4.wrapped, 'wro
|
|
|
1191
1372
|
await A('jr4w 錯 salt → null', (await unwrapNoteKey4(TACET4, w4.wrapped, pass4, 'ab'.repeat(16))) === null);
|
|
1192
1373
|
|
|
1193
1374
|
// jr4d. 雙因子(v3 密語+PIN 照舊)——wrap 端吃全形(NFKC 載體),unwrap 端兩向都咬
|
|
1194
|
-
const dual4 = await wrapNoteKeyDual4(TACET4, noteKey, passD4, pinD); // 全形 wrap=NFKC 載體(cp1=0xFF44——ASCII 化即本面 RED)
|
|
1195
1375
|
await A('dual4 前綴 jr4d.', dual4.wrapped.startsWith('jr4d.'));
|
|
1196
1376
|
await A('dual4 salt1 = 16B hex', /^[0-9a-f]{32}$/.test(dual4.salt));
|
|
1197
1377
|
const unwrappedD4 = await unwrapNoteKeyDual4(TACET4, dual4.wrapped, passD, pinD, dual4.salt);
|
|
@@ -1329,7 +1509,8 @@ await A('index barrel 匯出 v3 世代七識別字(normalizePassphrase/jr4
|
|
|
1329
1509
|
// v3 家族入口 normalize 收口形恰 3×3+PBKDF2 raw 恰 1——毒化形即計數變異,本 repo 樹直接斷言。
|
|
1330
1510
|
// 每案結束後還原/清樹(出生即棄);designated 面帳寫在案例名內。
|
|
1331
1511
|
|
|
1332
|
-
|
|
1512
|
+
};
|
|
1513
|
+
secOpen(16, '[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒)'); if (secEnter(16)) {
|
|
1333
1514
|
{
|
|
1334
1515
|
const getBuiltin = (id: string): unknown =>
|
|
1335
1516
|
(globalThis as unknown as { process?: { getBuiltinModule?: (i: string) => unknown } }).process?.getBuiltinModule?.(id);
|
|
@@ -1557,6 +1738,7 @@ console.log('\n[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒
|
|
|
1557
1738
|
}
|
|
1558
1739
|
}
|
|
1559
1740
|
}
|
|
1741
|
+
};
|
|
1560
1742
|
|
|
1561
1743
|
// ── 17. KAT 凍結向量:HKDF info 域世代分離(修正承載面) ──────────────
|
|
1562
1744
|
//
|
|
@@ -1595,7 +1777,7 @@ const KAT17 = {
|
|
|
1595
1777
|
{
|
|
1596
1778
|
const katRawUnlock = async (key: CryptoKey | null): Promise<boolean> =>
|
|
1597
1779
|
key !== null && (await hex(new Uint8Array(await crypto.subtle.exportKey('raw', key)))) === KAT17.raw;
|
|
1598
|
-
|
|
1780
|
+
secOpen(17, '[17] KAT 凍結向量:HKDF info 域世代分離+v3/raw 入口契約(jr3w./jr3d./jr4w./jr4d. 四 blob)'); if (secEnter(17)) {
|
|
1599
1781
|
await A('[17] jr3w 凍結 blob:全形 RAW unwrap = 原始 noteKey(舊家族 raw 契約——KAT 承載)',
|
|
1600
1782
|
katRawUnlock(await unwrapNoteKey3(TACET3, KAT17.jr3w, KAT17.pass3Raw, KAT17.s3)));
|
|
1601
1783
|
await A('[17] jr3w 凍結 blob:normalized 慣用形 → null(normalized 面恆拒——raw 族零正規化承載)',
|
|
@@ -1624,6 +1806,7 @@ const KAT17 = {
|
|
|
1624
1806
|
(await unwrapNoteKeyDual3(TACET3, KAT17.jr4d, passD, KAT17.pin, KAT17.sd4)) === null);
|
|
1625
1807
|
await A('[17] family isolation:jr3d blob 餵 jr4d 路徑 → null(KEK2 info 域分離對稱面)',
|
|
1626
1808
|
(await unwrapNoteKeyDual4(TACET4, KAT17.jr3d, passD, KAT17.pin, KAT17.sd3)) === null);
|
|
1809
|
+
};
|
|
1627
1810
|
}
|
|
1628
1811
|
|
|
1629
1812
|
// ── 18. 本機包裹專用前綴(wrapLocal opt-in+讀舊寫新自癒;jr1l.) ──
|
|
@@ -1638,7 +1821,7 @@ const KAT17 = {
|
|
|
1638
1821
|
// ③未配置態讀取面:照走舊形 cfg.wrap(行為不變),且不自癒重寫(heal 綁在新前綴面)。
|
|
1639
1822
|
// KEK 不動=deriveGuestKey(identity 派生、passphrase-free——session 期免重打密語的機制
|
|
1640
1823
|
// 原樣);本段只界「前綴」面(payload 布局的 KDF/prefix 契約由前綴界定——帶內版本化母型)。
|
|
1641
|
-
|
|
1824
|
+
secOpen(18, '[18] 本機包裹專用前綴(wrapLocal=jr1l. opt-in+讀舊寫新自癒)'); if (secEnter(18)) {
|
|
1642
1825
|
{
|
|
1643
1826
|
// section 專用 Map store:node 端 localStorage 死位(keys.ts try/catch 恆 null)——stored 面
|
|
1644
1827
|
// 行為斷言要可觀察可重放,結構 KeyStore 注入(同 cfg 注入母型;TACET 基座 fixture 不動)。
|
|
@@ -1900,6 +2083,7 @@ console.log('\n[18] 本機包裹專用前綴(wrapLocal=jr1l. opt-in+讀舊
|
|
|
1900
2083
|
}
|
|
1901
2084
|
}
|
|
1902
2085
|
}
|
|
2086
|
+
};
|
|
1903
2087
|
|
|
1904
2088
|
// ── 19. ladder 表與重取鹽面((a+) 案) ──────────────
|
|
1905
2089
|
//
|
|
@@ -1910,7 +2094,7 @@ console.log('\n[18] 本機包裹專用前綴(wrapLocal=jr1l. opt-in+讀舊
|
|
|
1910
2094
|
// 直驅 makePh2LadderStore:upsert 恰一列、同一舊值最新持有者勝、無 DELETE 語句面)。
|
|
1911
2095
|
// 表名/kind 常數的契約面(PH2_LADDER_TABLE='ph2_ladder'、KIND legacy/v2)由本段斷言直接咬住
|
|
1912
2096
|
//(fork migration 0011 與 core 原語同字面——漂移=fork schema 與 core 原語脫鉤=表缺席 fail-open 面)。
|
|
1913
|
-
|
|
2097
|
+
secOpen(19, '[19] ladder 表與重取鹽面(unwrapNoteKeyDual4WithSalt+ph2_ladder)'); if (secEnter(19)) {
|
|
1914
2098
|
{
|
|
1915
2099
|
const { DatabaseSync } = (globalThis as unknown as {
|
|
1916
2100
|
process?: { getBuiltinModule?: (id: string) => { DatabaseSync?: unknown } };
|
|
@@ -1920,7 +2104,7 @@ console.log('\n[19] ladder 表與重取鹽面(unwrapNoteKeyDual4WithSalt+ph2
|
|
|
1920
2104
|
await A('[19] ladder kinds 契約:legacy/v2 兩形(常數面)', PH2_LADDER_KIND_LEGACY === 'legacy' && PH2_LADDER_KIND_V2 === 'v2');
|
|
1921
2105
|
// 值主權威行為面(真 node:sqlite 直驅本體):建表走 fork migration 同形(PK=ph2)。
|
|
1922
2106
|
{
|
|
1923
|
-
const DatabaseSync19 = DatabaseSync as new (p: string) => { exec: (s: string) => void; prepare: (s: string) => { run: (...p: unknown[]) => { changes: number | bigint }; get: (...p: unknown[]) => unknown } };
|
|
2107
|
+
const DatabaseSync19 = DatabaseSync as new (p: string) => { exec: (s: string) => void; prepare: (s: string) => { run: (...p: unknown[]) => { changes: number | bigint }; get: (...p: unknown[]) => unknown; all: (...p: unknown[]) => unknown[] } };
|
|
1924
2108
|
const db19 = new DatabaseSync19(':memory:');
|
|
1925
2109
|
db19.exec('CREATE TABLE users (account_id TEXT PRIMARY KEY, ph2 TEXT)');
|
|
1926
2110
|
db19.exec("INSERT INTO users (account_id, ph2) VALUES ('acc19', 'ph2-new')");
|
|
@@ -1929,6 +2113,38 @@ console.log('\n[19] ladder 表與重取鹽面(unwrapNoteKeyDual4WithSalt+ph2
|
|
|
1929
2113
|
const ddlPos19 = migSrc19.indexOf('CREATE TABLE IF NOT EXISTS ph2_ladder');
|
|
1930
2114
|
const ddl19 = migSrc19.slice(ddlPos19, migSrc19.indexOf(';', ddlPos19) + 1);
|
|
1931
2115
|
db19.exec(ddl19.replace(/IF NOT EXISTS /g, ''));
|
|
2116
|
+
// 0012 對位面(t_87ef62dd):0011 上線後的索引修正 migration——0011 帶冗餘
|
|
2117
|
+
// idx_ph2_ladder_ph2(PK=ph2 同鍵 duplicate)且 account_id 零索引;0012 DROP+CREATE
|
|
2118
|
+
// 修正面在 fork 動線於 0011 之後執行(本段照序:建 0011 表+0011 原索引 → 0012)。
|
|
2119
|
+
{
|
|
2120
|
+
const idxPos19 = migSrc19.indexOf('CREATE INDEX IF NOT EXISTS idx_ph2_ladder_ph2');
|
|
2121
|
+
if (idxPos19 >= 0) db19.exec(migSrc19.slice(idxPos19, migSrc19.indexOf(';', idxPos19) + 1)); // 0011 冗餘索引原樣(0012 要 DROP 的缺陷面)
|
|
2122
|
+
const src12 = await srcOf('../migrations/0012-fix-ph2-ladder-index.sql');
|
|
2123
|
+
await A('[19] 0011 對位錨:冗餘 idx_ph2_ladder_ph2 在場(0012 DROP 面=0011 冗餘索引原樣)',
|
|
2124
|
+
idxPos19 >= 0 && src12.includes('DROP INDEX IF EXISTS idx_ph2_ladder_ph2'));
|
|
2125
|
+
const stmts12 = ['DROP INDEX IF EXISTS idx_ph2_ladder_ph2;', 'CREATE INDEX IF NOT EXISTS idx_ph2_ladder_account ON ph2_ladder(account_id);'];
|
|
2126
|
+
await A('[19] 0012 語句面:DROP 冗餘+CREATE account_id 兩句(報告原樣;DROP 在前——殘留冗餘即 RED)',
|
|
2127
|
+
src12.split('DROP INDEX IF EXISTS idx_ph2_ladder_ph2;').length === 2
|
|
2128
|
+
&& src12.split('CREATE INDEX IF NOT EXISTS idx_ph2_ladder_account ON ph2_ladder(account_id);').length === 2
|
|
2129
|
+
&& src12.indexOf('DROP INDEX') < src12.indexOf('CREATE INDEX'));
|
|
2130
|
+
await A('[19] 0012 非 UNIQUE 契約(一帳多舊值設計——ladder 多歷列並存;UNIQUE 面殘留即 RED)',
|
|
2131
|
+
/CREATE (UNIQUE )?INDEX/.test(src12) && !src12.includes('UNIQUE INDEX'));
|
|
2132
|
+
for (const st12 of stmts12) db19.exec(st12);
|
|
2133
|
+
const idxAfter = db19.prepare('SELECT name FROM sqlite_master WHERE type = \'index\' AND tbl_name = \'ph2_ladder\'').all() as { name: string }[];
|
|
2134
|
+
const idxNames = idxAfter.map((r) => r.name).filter((n) => n !== 'sqlite_autoindex_ph2_ladder_1');
|
|
2135
|
+
await A('[19] 0012 pragma 動線:冗餘 ph2 index 摘除+account_id index 在場(n 前後查——索引集恰 {idx_ph2_ladder_account})',
|
|
2136
|
+
idxNames.length === 1 && idxNames[0] === 'idx_ph2_ladder_account', JSON.stringify(idxNames));
|
|
2137
|
+
await A('[19] 0012 冪等:0012 語句重跑零炸(IF EXISTS/IF NOT EXISTS 面——fork 重入動線)',
|
|
2138
|
+
await (async () => { try { db19.exec(stmts12.join('\n')); return true; } catch { return false; } })());
|
|
2139
|
+
// EXPLAIN QUERY PLAN:account_id 查詢走 SEARCH(非 SCAN——修復收益的直接承載面);
|
|
2140
|
+
// ph2 查面走 autoindex(PK)=冗餘索引摘除後照 SEARCH。
|
|
2141
|
+
const planAcc = String((db19.prepare('EXPLAIN QUERY PLAN SELECT * FROM ph2_ladder WHERE account_id = ?').get('acc19') as { detail?: string } | null)?.detail ?? '');
|
|
2142
|
+
const planPh2 = String((db19.prepare('EXPLAIN QUERY PLAN SELECT account_id FROM ph2_ladder WHERE ph2 = ?').get('old-19') as { detail?: string } | null)?.detail ?? '');
|
|
2143
|
+
await A('[19] 0012 EXPLAIN 動線:account_id 查詢 SEARCH ph2_ladder (idx_ph2_ladder_account)(非 SCAN)',
|
|
2144
|
+
planAcc.includes('SEARCH ph2_ladder USING INDEX idx_ph2_ladder_account') && !/(^|\s)SCAN\s/.test(planAcc), planAcc);
|
|
2145
|
+
await A('[19] 0012 EXPLAIN PK 面:ph2 查詢仍 SEARCH autoindex(冗餘索引摘除零傷——非 SCAN)',
|
|
2146
|
+
planPh2.includes('SEARCH') && !/(^|\s)SCAN\s/.test(planPh2), planPh2);
|
|
2147
|
+
}
|
|
1932
2148
|
const env19 = {
|
|
1933
2149
|
DB: {
|
|
1934
2150
|
prepare(sql: string) {
|
|
@@ -1971,6 +2187,7 @@ console.log('\n[19] ladder 表與重取鹽面(unwrapNoteKeyDual4WithSalt+ph2
|
|
|
1971
2187
|
await A('[19] WithSalt 未配置 cfg(wrapDual4 缺)→ null(opt-in 鐵律同向)',
|
|
1972
2188
|
(await unwrapNoteKeyDual4WithSalt({ pinSalt3Prefix: 'tacet-note-pin3:' }, rewrapD4.wrapped, passD, pinD, rewrapD4.salt)) === null);
|
|
1973
2189
|
}
|
|
2190
|
+
};
|
|
1974
2191
|
// ── 20. 復原套件專用前綴+HKDF 派生世代 ──
|
|
1975
2192
|
//
|
|
1976
2193
|
// 裁定:採搭車——wrappedRec 專用前綴+KEK_rec 改
|
|
@@ -1992,7 +2209,7 @@ console.log('\n[19] ladder 表與重取鹽面(unwrapNoteKeyDual4WithSalt+ph2
|
|
|
1992
2209
|
// 涵蓋;摘雙試=遷移率裁定非日曆)。兩欄皆缺席(舊 cfg)=單試舊面(行為不變)。
|
|
1993
2210
|
// ⑤四象限 write→read 矩陣(檢查對齊 M1):每象限「自 wrap 可自 unwrap 或 wrap 拒絕」
|
|
1994
2211
|
// +超集 cfg 可讀子集 blob(升級路徑保護)+B01 歷史缺陷形恆拒(升 KDF=換前綴)。
|
|
1995
|
-
|
|
2212
|
+
secOpen(20, '[20] 復原套件專用前綴(wrapRec=jr1r. opt-in)+KEK_rec HKDF 世代(recKekHkdf opt-in,兩欄一體配對門)'); if (secEnter(20)) {
|
|
1996
2213
|
{
|
|
1997
2214
|
const recSalt20 = 'a1b2c3d4e5f60718293a4b5c6d7e8f90';
|
|
1998
2215
|
const TACET5: NoteCryptoConfig = { ...TACET, wrapRec: 'jr1r.', recKekHkdf: 'jr1r.', recKekSalt: recSalt20 };
|
|
@@ -2111,14 +2328,14 @@ console.log('\n[20] 復原套件專用前綴(wrapRec=jr1r. opt-in)+KEK_rec
|
|
|
2111
2328
|
await A('[20] 源碼窗:配對門接線形恰 1(wrapRec 或 recKekHkdf 在場即入門——單欄短路復活即形變異)',
|
|
2112
2329
|
noteCryptoSrc20.split('if (cfg.wrapRec || cfg.recKekHkdf) {').length === 2
|
|
2113
2330
|
&& noteCryptoSrc20.split("if (!cfg.wrapRec || !cfg.recKekHkdf) throw new Error('ERR_REC_CFG_PARTIAL');").length === 2);
|
|
2114
|
-
await A('[20] 源碼窗:HKDF 專責接線恰 1
|
|
2115
|
-
noteCryptoSrc20.split('if (cfg.wrapRec && cfg.recKekHkdf) {').length === 2);
|
|
2331
|
+
await A('[20] 源碼窗:HKDF 專責接線恰 1(兩欄齊備守衛+t_87ef62dd 快檢條件面——派生雙模/C2 借形復活/短路回歸即變異)',
|
|
2332
|
+
noteCryptoSrc20.split('if (cfg.wrapRec && cfg.recKekHkdf && wrapped.startsWith(cfg.wrapRec)) {').length === 2);
|
|
2116
2333
|
await A('[20] 源碼窗:PBKDF2 fallback 殘留歸零+deriveRecKek 呼叫面恰 3(非空洞計數:fallback 形歸零+def+兩呼叫端在場——C2 收口面)',
|
|
2117
2334
|
noteCryptoSrc20.split('if (!cfg.recKekHkdf) return deriveKek').length === 1
|
|
2118
2335
|
&& noteCryptoSrc20.split('deriveRecKek(').length === 4);
|
|
2119
2336
|
await A('[20] 源碼窗:deriveRecLegacyKek def+呼叫恰 3(split 帳=出現+1;legacy 本體單一真相——寫面+讀面同源)',
|
|
2120
2337
|
noteCryptoSrc20.split('deriveRecLegacyKek(').length === 4);
|
|
2121
|
-
await A('[20] 源碼窗:cfg.wrapRec 敘述形恰 2(配對門 XOR
|
|
2338
|
+
await A('[20] 源碼窗:cfg.wrapRec 敘述形恰 2(配對門 XOR 一對——借用面復活即計數變異;t_87ef62dd 快檢面零咬:腿接線是 startsWith 閉括號形非 XOR 敘述形)',
|
|
2122
2339
|
noteCryptoSrc20.split('cfg.wrapRec ||').length === 3
|
|
2123
2340
|
&& noteCryptoSrc20.split('!cfg.wrapRec ||').length === 2);
|
|
2124
2341
|
|
|
@@ -2276,11 +2493,355 @@ console.log('\n[20] 復原套件專用前綴(wrapRec=jr1r. opt-in)+KEK_rec
|
|
|
2276
2493
|
trees20.length === 6 && trees20.every((t) => !existsSync20(t)));
|
|
2277
2494
|
}
|
|
2278
2495
|
}
|
|
2496
|
+
};
|
|
2497
|
+
|
|
2498
|
+
// ── 21. CI 供應鏈面(npm ci --ignore-scripts+Dependabot runner 分流)──
|
|
2499
|
+
//
|
|
2500
|
+
// 外審 #1 的落地裁定(t_87ef62dd):public 密碼學套件 repo 的 CI 安裝不吃依賴腳本——
|
|
2501
|
+
// --ignore-scripts 是安裝面防線(postinstall/preinstall/prepare 全免執行);
|
|
2502
|
+
// Dependabot PR 走 GitHub-hosted runner 隔離(self-hosted runner 不接機器人分支)。
|
|
2503
|
+
// 靜態錨面:workflow YAML 本體的接線(job-if C1 母型+actor 分流+runs-on fromJSON 陣列形)。
|
|
2504
|
+
secOpen(21, '[21] CI 供應鏈面(npm ci --ignore-scripts+dependabot runner 分流)'); if (secEnter(21)) {
|
|
2505
|
+
{
|
|
2506
|
+
// 環境鍵=.github 目錄存在(存在性鍵非檔名硬讀——r1 MINOR-3):閘 src 檔在 files 白名單
|
|
2507
|
+
// 而 .github 不出包——tarball 消費樹硬讀 workflows=未守衛 ENOENT,rc=1 尾段全吞。
|
|
2508
|
+
// 目錄缺席=顯性 SKIP 行(消費端常態;[16]/[18] 毒化樹 pristine 基準由此同保線);
|
|
2509
|
+
// 目錄在場而檔案缺席=srcOf 硬 FAIL(repo 面牙齒不退位——軟讀文件名會把 repo 內
|
|
2510
|
+
// 摘 workflow 校準成靜默綠=掉牙)。
|
|
2511
|
+
const fs21 = (globalThis as unknown as {
|
|
2512
|
+
process?: { getBuiltinModule?: (id: string) => { existsSync?: (p: string | URL) => boolean } | undefined };
|
|
2513
|
+
}).process?.getBuiltinModule?.('node:fs');
|
|
2514
|
+
const ghDir21 = !!fs21?.existsSync && fs21.existsSync(new URL('../.github/', import.meta.url));
|
|
2515
|
+
if (ghDir21) {
|
|
2516
|
+
const srcVy21 = await srcOf('../.github/workflows/verify.yml');
|
|
2517
|
+
const srcPy21 = await srcOf('../.github/workflows/publish.yml');
|
|
2518
|
+
await A('[21] verify.yml:npm ci --ignore-scripts(安裝面防線——依賴腳本零在場)',
|
|
2519
|
+
srcVy21.includes('npm ci --ignore-scripts'));
|
|
2520
|
+
await A('[21] verify.yml:job-if 同 repo 保衛(C1 母型——fork/他 repo branch 不上 runner;dependabot 分支照跑不擋=分流入 RUN 面——r1 MAJOR-1)',
|
|
2521
|
+
/if: github\.event_name != 'pull_request' \|\| github\.event\.pull_request\.head\.repo\.full_name == github\.repository/.test(srcVy21));
|
|
2522
|
+
await A('[21] verify.yml:runs-on dependabot 分流=fromJSON 陣列形(actor→["ubuntu-latest"];fallback fromJSON 兩標籤組 ["self-hosted","journal-core"]——裸逗號字串 fallback=StringToken 單一 literal 標籤缺陷形零承載,r2 MAJOR-1)',
|
|
2523
|
+
/runs-on: \$\{\{ fromJSON\(github\.event_name == 'pull_request' && github\.actor == 'dependabot\[bot\]' && '\["ubuntu-latest"\]' \|\| '\["self-hosted","journal-core"\]'\) \}\}/.test(srcVy21));
|
|
2524
|
+
await A('[21] publish.yml:npm ci --ignore-scripts(tag 驅動無 actor 面;typecheck+verify 先行=腳本面零在場)',
|
|
2525
|
+
srcPy21.includes('npm ci --ignore-scripts'));
|
|
2526
|
+
} else {
|
|
2527
|
+
await A('[21] CI 源碼面:.github 缺席環境(tarball 發行樹常態)=顯性 SKIP(workflow 斷言 4 收斂為 1 SKIP 行——消費端計數帳 479)',
|
|
2528
|
+
true, 'no .github dir — consumption tree face');
|
|
2529
|
+
}
|
|
2530
|
+
// 摘除自證(t_760f44e8 治理掃蕩補漏;fs 原語缺席環境=顯性 FAIL 形自守衛——非 silent true)
|
|
2531
|
+
await A('[21] open-card-pr.sh 摘除(主機路徑+GITHUB_TOKEN 線索面;fs 原語缺席=顯性 FAIL 自守衛)',
|
|
2532
|
+
!!fs21?.existsSync && !fs21.existsSync(new URL('../scripts/open-card-pr.sh', import.meta.url)));
|
|
2533
|
+
// NIT-1 自證(isBrokenLink repo-root 錨定):根錨路徑在場形(本 repo 常態 @scure 在場)
|
|
2534
|
+
// +CWD 無關面(probe 眼:lstat 走 import.meta.url 起手——cwd=任意時仍指 repo)。
|
|
2535
|
+
// 缺陷形(裸 '@scure/bip39' 殘留)=源碼負向咬(regex 敘述形)。
|
|
2536
|
+
{
|
|
2537
|
+
const gateSrc21 = await srcOf('../scripts/verify-core-crypto.ts');
|
|
2538
|
+
await A('[21] NIT-1:isBrokenLink 路徑錨 = import.meta.url 起手(repo root 錨定——CWD 相對 lstat 誤報面收口)',
|
|
2539
|
+
/const repoRootRef = new URL\('\.\.\/node_modules\/@scure\/bip39', import\.meta\.url\)/.test(gateSrc21));
|
|
2540
|
+
await A('[21] NIT-1:裸 CWD 相對 lstat 殘留歸零(缺陷形負向——lstatSync\(\'@scure 直接形零殘留)',
|
|
2541
|
+
!/lstatSync\('@scure\/bip39'\)/.test(gateSrc21) && !/existsSync\('@scure\/bip39'\)/.test(gateSrc21));
|
|
2542
|
+
await A('[21] NIT-1:isBrokenLink 判準本體在場(lstat isSymbolicLink && !existsSync——斷鏈 vs 真缺席分流保線)',
|
|
2543
|
+
/isSymbolicLink\(\) && !fsMod\.existsSync\(repoRootRef\)/.test(gateSrc21));
|
|
2544
|
+
await A('[21] r2 MINOR-1:fs 直讀守衛=URL 物件直傳(existsSync .github 在場面+srcOf readFileSync rel 面——file: URL 原生接線,零 .pathname 中介)',
|
|
2545
|
+
/fs21\.existsSync\(new URL\('\.\.\/\.github\/', import\.meta\.url\)\)/.test(gateSrc21)
|
|
2546
|
+
&& /fs\.readFileSync\(new URL\(rel, import\.meta\.url\), 'utf8'\)/.test(gateSrc21));
|
|
2547
|
+
await A('[21] r2 MINOR-1:fs 原語直讀 .pathname 形零殘留(缺陷形負向——URL %編碼失真→existsSync 恆 false→SKIP 分枝靜默退位面收口)',
|
|
2548
|
+
!/existsSync\(new URL\([^)]*\)\.pathname\)/.test(gateSrc21)
|
|
2549
|
+
&& !/readFileSync\(new URL\([^)]*\)\.pathname/.test(gateSrc21));
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
};
|
|
2553
|
+
|
|
2554
|
+
secOpen(23, '[23] loginRouteCore ladder 查表守衛(外審 #8 幽靈帳——現值 miss → ladder → 兩面 miss 才 createUser)'); if (secEnter(23)) {
|
|
2555
|
+
{
|
|
2556
|
+
// getBuiltin23:node 內建模組動態存取(本段自備 helper——[19]/[21] 同構;structured type 零 node types 依賴)
|
|
2557
|
+
const getBuiltin23 = (id: string): unknown =>
|
|
2558
|
+
(globalThis as unknown as { process?: { getBuiltinModule?: (i: string) => unknown } }).process?.getBuiltinModule?.(id);
|
|
2559
|
+
const { DatabaseSync } = (getBuiltin23('node:sqlite') as { DatabaseSync?: unknown } | undefined) ?? {};
|
|
2560
|
+
if (!DatabaseSync) throw new Error('ERR_SQLITE_UNAVAILABLE');
|
|
2561
|
+
const { loginRouteCore } = await import('../src/server/auth.ts');
|
|
2562
|
+
const enc23 = new TextEncoder();
|
|
2563
|
+
const hex23 = (u8: Uint8Array): string => Array.from(u8).map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
2564
|
+
const ncCrypto23 = (getBuiltin23('node:crypto') as { createHash?: (a: string) => { update: (s: string) => { digest: (e: string) => string } } } | undefined) ?? { createHash: () => { throw new Error('ERR_CRYPTO_UNAVAILABLE'); } };
|
|
2565
|
+
const sha256Sync23 = (s: string): string => ncCrypto23.createHash!('sha256').update(s).digest('hex');
|
|
2566
|
+
const sha23 = async (s: string): Promise<string> => hex23(new Uint8Array(await crypto.subtle.digest('SHA-256', enc23.encode(s))));
|
|
2567
|
+
// PH1 hex64 樣本(直入 login body):loginRouteCore 對 body 做 sha256Hex=ph2——fixture 種子
|
|
2568
|
+
// 必須種 sha256(ph1)(雜湊後落表);種 ph1 本身進 ph2 欄=miss by construction(種錯層)。
|
|
2569
|
+
const ph1a23 = sha256Sync23('jr23-current-ph1');
|
|
2570
|
+
const ph1b23 = sha256Sync23('jr23-old-ph1');
|
|
2571
|
+
const ph1c23 = sha256Sync23('jr23-ghost-ph1');
|
|
2572
|
+
const ph1d23 = sha256Sync23('jr23-nolad-ph1');
|
|
2573
|
+
const ph1e23 = sha256Sync23('jr23-failo-ph1');
|
|
2574
|
+
const ph1x23 = sha256Sync23('jr23-sessi-ph1');
|
|
2575
|
+
const rowA23 = sha256Sync23(ph1a23);
|
|
2576
|
+
const row23old = sha256Sync23('jr23-v3cur-ph1');
|
|
2577
|
+
const row23x = sha256Sync23(ph1x23);
|
|
2578
|
+
|
|
2579
|
+
const mkDb23 = (): { db: { exec: (s: string) => void; prepare: (s: string) => { run: (...p: unknown[]) => { changes: number }; get: (...p: unknown[]) => unknown; all: (...p: unknown[]) => unknown[] } }; env23: unknown } => {
|
|
2580
|
+
const db = new (DatabaseSync as new (p: string) => { exec: (s: string) => void; prepare: (s: string) => { run: (...p: unknown[]) => { changes: number }; get: (...p: unknown[]) => unknown; all: (...p: unknown[]) => unknown[] } })(':memory:');
|
|
2581
|
+
db.exec('CREATE TABLE login_rate (ip TEXT PRIMARY KEY, window_start INTEGER, count INTEGER)');
|
|
2582
|
+
db.exec('CREATE TABLE users (account_id TEXT PRIMARY KEY, ph2 TEXT UNIQUE, salt TEXT, wrapped_key TEXT, wrapped_rec TEXT, rec_hash TEXT)');
|
|
2583
|
+
return {
|
|
2584
|
+
db,
|
|
2585
|
+
env23: {
|
|
2586
|
+
DB: {
|
|
2587
|
+
prepare: (sql: string) => ({
|
|
2588
|
+
bind: (...params: unknown[]) => ({
|
|
2589
|
+
run: async () => {
|
|
2590
|
+
let results: unknown[] = [];
|
|
2591
|
+
try { results = db.prepare(sql).all(...params) as unknown[]; }
|
|
2592
|
+
catch { try { db.prepare(sql).run(...params); } catch {} }
|
|
2593
|
+
return { results, meta: {} };
|
|
2594
|
+
},
|
|
2595
|
+
}),
|
|
2596
|
+
}),
|
|
2597
|
+
},
|
|
2598
|
+
} as unknown as never,
|
|
2599
|
+
};
|
|
2600
|
+
};
|
|
2601
|
+
const USER_COLS23 = 'account_id AS userKey, ph2, salt, wrapped_key AS wrapped, wrapped_rec AS wrappedRec, rec_hash AS recHash';
|
|
2602
|
+
|
|
2603
|
+
// ① 現值 hit:單查命中,ladder 零呼叫零建號(現值 hit 短路在 findByIdentityQuery)
|
|
2604
|
+
{
|
|
2605
|
+
const t = mkDb23();
|
|
2606
|
+
const calls23 = { ladder: 0, create: 0 };
|
|
2607
|
+
const store23 = {
|
|
2608
|
+
findByIdentityQuery: async (ph2: string) => (t.db.prepare(`SELECT ${USER_COLS23} FROM users WHERE ph2 = ?`).get(ph2) as unknown) || null,
|
|
2609
|
+
createUser: async () => { calls23.create++; return 'acct-ghost-23'; },
|
|
2610
|
+
getByUserKey: async () => null,
|
|
2611
|
+
revokeAllSessions: async () => {},
|
|
2612
|
+
insertSession: async () => {},
|
|
2613
|
+
ladderLookup: async () => { calls23.ladder++; return null; },
|
|
2614
|
+
};
|
|
2615
|
+
t.db.prepare("INSERT INTO users (account_id, ph2, salt, wrapped_key) VALUES ('acct-23a', ?, 's23', 'wr23')").run(rowA23);
|
|
2616
|
+
const res23 = await loginRouteCore(t.env23 as never, store23 as never, 'ip-23-1', ph1a23);
|
|
2617
|
+
const j23 = JSON.parse(await res23.text()) as { userKey: string; status: string };
|
|
2618
|
+
await A('[23] 現值 hit 回 200 ready(userKey 恰 acct-23a——登入 body 相容現值契約)', res23.status === 200 && j23.status === 'ready' && j23.userKey === 'acct-23a');
|
|
2619
|
+
await A('[23] 現值 hit:ladderLookup 零呼叫(短路在 findByIdentityQuery)', calls23.ladder === 0);
|
|
2620
|
+
await A('[23] 現值 hit:createUser 零呼叫(零幽靈面)', calls23.create === 0);
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
// ② ladder hit(現值 miss)=回舊帳不建幽靈;getByUserKey 讀現值(ph2Kind 語意照 ladder.ts);
|
|
2624
|
+
// session 歸宿舊帳 userKey
|
|
2625
|
+
{
|
|
2626
|
+
const t = mkDb23();
|
|
2627
|
+
const calls23 = { ladder: 0, create: 0, byKey: [] as string[] };
|
|
2628
|
+
let sess23: { tokenHash: string; userKey: string; expiresAt: number } | null = null;
|
|
2629
|
+
const store23 = {
|
|
2630
|
+
findByIdentityQuery: async (ph2: string) => (t.db.prepare(`SELECT ${USER_COLS23} FROM users WHERE ph2 = ?`).get(ph2) as unknown) || null,
|
|
2631
|
+
createUser: async () => { calls23.create++; return 'acct-ghost-23'; },
|
|
2632
|
+
getByUserKey: async (key: string) => { calls23.byKey.push(key); return (t.db.prepare(`SELECT ${USER_COLS23} FROM users WHERE account_id = ?`).get(key) as unknown) || null; },
|
|
2633
|
+
revokeAllSessions: async () => {},
|
|
2634
|
+
insertSession: async (tokenHash: string, userKey: string, expiresAt: number) => { sess23 = { tokenHash, userKey, expiresAt }; },
|
|
2635
|
+
ladderLookup: async () => { calls23.ladder++; return 'acct-23old'; },
|
|
2636
|
+
};
|
|
2637
|
+
t.db.prepare("INSERT INTO users (account_id, ph2, salt, wrapped_key) VALUES ('acct-23old', ?, 's23', 'wr23')").run(row23old);
|
|
2638
|
+
const res23 = await loginRouteCore(t.env23 as never, store23 as never, 'ip-23-2', ph1b23);
|
|
2639
|
+
const j23 = JSON.parse(await res23.text()) as { userKey: string; status: string };
|
|
2640
|
+
await A('[23] ladder hit → userKey=舊帳 acct-23old(不建幽靈)', j23.userKey === 'acct-23old' && calls23.create === 0);
|
|
2641
|
+
await A('[23] ladder hit:ladderLookup 恰 1 呼叫(守衛接線在場)', calls23.ladder === 1);
|
|
2642
|
+
await A('[23] ladder hit 語意照 ladder.ts:回帳走 getByUserKey(現值面承載——非表列直接回)', calls23.byKey.length === 1 && calls23.byKey[0] === 'acct-23old');
|
|
2643
|
+
await A('[23] ladder hit → ready 分流(帳戶 ph2 在場)', j23.status === 'ready');
|
|
2644
|
+
await A('[23] ladder hit → session 落在舊帳 userKey(tokenSha256+30 天)', sess23 !== null && (sess23 as { userKey: string }).userKey === 'acct-23old'
|
|
2645
|
+
&& /^[0-9a-f]{64}$/.test((sess23 as { tokenHash: string }).tokenHash)
|
|
2646
|
+
&& (sess23 as { expiresAt: number }).expiresAt > Date.now() + 29 * 86400e3);
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
// ③ 兩面 miss → 建 1 幽靈(ph2 恆等=sha256(登入 PH1))
|
|
2650
|
+
{
|
|
2651
|
+
const t = mkDb23();
|
|
2652
|
+
let inserted23: string | null = null;
|
|
2653
|
+
const calls23 = { ladder: 0 };
|
|
2654
|
+
const store23 = {
|
|
2655
|
+
findByIdentityQuery: async (ph2: string) => (t.db.prepare(`SELECT ${USER_COLS23} FROM users WHERE ph2 = ?`).get(ph2) as unknown) || null,
|
|
2656
|
+
createUser: async (ph2: string) => { inserted23 = ph2; return 'acct-ghost-23'; },
|
|
2657
|
+
getByUserKey: async () => null,
|
|
2658
|
+
revokeAllSessions: async () => {},
|
|
2659
|
+
insertSession: async () => {},
|
|
2660
|
+
ladderLookup: async () => { calls23.ladder++; return null; },
|
|
2661
|
+
};
|
|
2662
|
+
const res23 = await loginRouteCore(t.env23 as never, store23 as never, 'ip-23-3', ph1c23);
|
|
2663
|
+
const j23 = JSON.parse(await res23.text()) as { userKey: string; status: string };
|
|
2664
|
+
await A('[23] 兩面 miss → 建 1 幽靈(createUser 恰 1——幽靈語意照舊)', j23.userKey === 'acct-ghost-23');
|
|
2665
|
+
await A('[23] 幽靈 ph2 = sha256Hex(登入 PH1)(subtle 交叉驗——種子層對帳)', inserted23 !== null && inserted23 === await sha23(ph1c23));
|
|
2666
|
+
await A('[23] 兩面 miss:ladderLookup 有呼叫(配置在場=守衛有接線)+require_binding 分流', calls23.ladder === 1 && j23.status === 'require_binding');
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
// ④ lookup 未配置(optional undefined)=0.2.3 單查行為零變(ladder 零呼叫——向後相容鐵律)
|
|
2670
|
+
{
|
|
2671
|
+
const t = mkDb23();
|
|
2672
|
+
const calls23 = { create: 0 };
|
|
2673
|
+
const store23 = {
|
|
2674
|
+
findByIdentityQuery: async (ph2: string) => (t.db.prepare(`SELECT ${USER_COLS23} FROM users WHERE ph2 = ?`).get(ph2) as unknown) || null,
|
|
2675
|
+
createUser: async () => { calls23.create++; return 'acct-ghost-23'; },
|
|
2676
|
+
getByUserKey: async () => null,
|
|
2677
|
+
revokeAllSessions: async () => {},
|
|
2678
|
+
insertSession: async () => {},
|
|
2679
|
+
};
|
|
2680
|
+
const res23 = await loginRouteCore(t.env23 as never, store23 as never, 'ip-23-4', ph1d23);
|
|
2681
|
+
const j23 = JSON.parse(await res23.text()) as { userKey: string };
|
|
2682
|
+
await A('[23] ladderLookup 未配置 → 直接建幽靈(0.2.3 單查行為恆等——optional 退場鐵律)', j23.userKey === 'acct-ghost-23' && calls23.create === 1);
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
// ⑤ ladder store 拋錯(表缺席)=fail-open 視同 miss,不炸登入
|
|
2686
|
+
{
|
|
2687
|
+
const t = mkDb23();
|
|
2688
|
+
let createCalls23 = 0;
|
|
2689
|
+
const store23 = {
|
|
2690
|
+
findByIdentityQuery: async (ph2: string) => (t.db.prepare(`SELECT ${USER_COLS23} FROM users WHERE ph2 = ?`).get(ph2) as unknown) || null,
|
|
2691
|
+
createUser: async () => { createCalls23++; return 'acct-ghost-23'; },
|
|
2692
|
+
getByUserKey: async () => null,
|
|
2693
|
+
revokeAllSessions: async () => {},
|
|
2694
|
+
insertSession: async () => {},
|
|
2695
|
+
ladderLookup: async () => { throw new Error('no such table: ph2_ladder'); },
|
|
2696
|
+
};
|
|
2697
|
+
const res23 = await loginRouteCore(t.env23 as never, store23 as never, 'ip-23-5', ph1e23);
|
|
2698
|
+
const j23 = JSON.parse(await res23.text()) as { userKey: string };
|
|
2699
|
+
await A('[23] ladder 拋錯 fail-open=登入不炸、續建幽靈(tacet store.ts 母型上位)', res23.status === 200 && j23.userKey === 'acct-ghost-23' && createCalls23 === 1);
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
// ⑥ ladder hit:帳戶現值已旋轉(ph2 異值欄面)——status 分流照 user?.ph2 實值
|
|
2703
|
+
{
|
|
2704
|
+
const t = mkDb23();
|
|
2705
|
+
const store23 = {
|
|
2706
|
+
findByIdentityQuery: async (ph2: string) => (t.db.prepare(`SELECT ${USER_COLS23} FROM users WHERE ph2 = ?`).get(ph2) as unknown) || null,
|
|
2707
|
+
createUser: async () => 'acct-ghost-23',
|
|
2708
|
+
getByUserKey: async (key: string) => (t.db.prepare(`SELECT ${USER_COLS23} FROM users WHERE account_id = ?`).get(key) as unknown) || null,
|
|
2709
|
+
revokeAllSessions: async () => {},
|
|
2710
|
+
insertSession: async () => {},
|
|
2711
|
+
ladderLookup: async () => 'acct-23old',
|
|
2712
|
+
};
|
|
2713
|
+
t.db.prepare("INSERT INTO users (account_id, ph2, salt, wrapped_key) VALUES ('acct-23old', 'ph2-rotated-current-23', 's23', 'wr23')").run();
|
|
2714
|
+
const res23 = await loginRouteCore(t.env23 as never, store23 as never, 'ip-23-6', ph1b23);
|
|
2715
|
+
const j23 = JSON.parse(await res23.text()) as { userKey: string; status: string };
|
|
2716
|
+
await A('[23] ladder hit(現值已旋轉異值欄)→ userKey=舊帳、status=ready(user?.ph2 在場分流本體契約)', j23.userKey === 'acct-23old' && j23.status === 'ready');
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
// ⑦ 毒化(拷貝樹 in-gate,t_0ab6e760 母型):ladderGuard 接線摘除 → ladder hit 案翻建幽靈。
|
|
2720
|
+
// 毒化跑在 /tmp 拷貝樹=本 repo 樹 byte 不動;還原 byte-exact+樹出生即棄。
|
|
2721
|
+
// POISON_GATE_INNER sentinel([16]/[18]/[20] 遞迴防線母型):內層跑份自我跳過毒化節。
|
|
2722
|
+
{
|
|
2723
|
+
const inner23 = (getBuiltin23('node:process') as { env?: Record<string, string | undefined> } | undefined)?.env?.POISON_GATE_INNER === '1';
|
|
2724
|
+
if (inner23) {
|
|
2725
|
+
await A('[23] 毒化證據:內層跑份自我跳過(POISON_GATE_INNER sentinel——遞迴防線母型)', true, 'POISON_GATE_INNER=1');
|
|
2726
|
+
} else {
|
|
2727
|
+
const fs23 = (getBuiltin23('node:fs') as {
|
|
2728
|
+
mkdtempSync?: (p: string) => string;
|
|
2729
|
+
mkdirSync?: (p: string, o?: unknown) => void;
|
|
2730
|
+
copyFileSync?: (a: string | URL, b: string) => void;
|
|
2731
|
+
readdirSync?: (p: string | URL) => string[];
|
|
2732
|
+
readFileSync?: (p: string | URL, e: string) => string;
|
|
2733
|
+
writeFileSync?: (p: string, s: string, e: string) => void;
|
|
2734
|
+
rmSync?: (p: string, o?: unknown) => void;
|
|
2735
|
+
} | undefined) ?? undefined;
|
|
2736
|
+
const path23x = (getBuiltin23('node:path') as { join?: (...p: string[]) => string } | undefined) ?? undefined;
|
|
2737
|
+
const os23 = (getBuiltin23('node:os') as { tmpdir?: () => string } | undefined) ?? undefined;
|
|
2738
|
+
const url23 = (getBuiltin23('node:url') as { pathToFileURL?: (p: string) => { href: string } } | undefined) ?? undefined;
|
|
2739
|
+
if (!fs23?.mkdtempSync || !path23x?.join || !os23?.tmpdir || !url23?.pathToFileURL) {
|
|
2740
|
+
// 原語缺席環境=顯性 SKIP 行(非 silent true——t_87ef62dd 環境鍵母型)
|
|
2741
|
+
await A('[23] 毒化面(原語缺席環境)=顯性 SKIP 行,毒化證據缺席顯形非 silent true',
|
|
2742
|
+
false, 'node fs/path/os/url 原語缺席——毒化面 skip(消費端精簡 Node 形;repo 樹常態不見此行)');
|
|
2743
|
+
} else {
|
|
2744
|
+
// 拷貝樹源=本閘的套件根(import.meta.url 起手——repo 樹與消費樹同構,零硬編碼路徑)
|
|
2745
|
+
const pkgRootUrl23 = new URL('../', import.meta.url);
|
|
2746
|
+
const srcDirUrl23 = new URL('src/server/', pkgRootUrl23);
|
|
2747
|
+
const tree23 = fs23.mkdtempSync!(path23x.join(os23.tmpdir(), 't23-ladder-'));
|
|
2748
|
+
fs23.mkdirSync!(path23x.join(tree23, 'src/server'), { recursive: true });
|
|
2749
|
+
const filesSer23 = fs23.readdirSync!(srcDirUrl23).filter((f) => f.endsWith('.ts'));
|
|
2750
|
+
for (const f of filesSer23) fs23.copyFileSync!(new URL('src/server/' + f, pkgRootUrl23), path23x.join(tree23, 'src/server', f));
|
|
2751
|
+
await A('[23] 毒化樹拷貝:src/server 全檔在場(auth.ts+依賴鏈四檔≥5)', filesSer23.length >= 5 && filesSer23.includes('auth.ts'), 'n=' + filesSer23.length);
|
|
2752
|
+
// 毒化錨(唯一):接線面 `?? (await ladderGuard(store, ph2))` 摘除 → 單查直路
|
|
2753
|
+
//(removal-form——毒面=ladder hit 案翻建幽靈;未配置案照綠=行為面恰此面翻)。
|
|
2754
|
+
const needle23 = '(await store.findByIdentityQuery(ph2)) ?? (await ladderGuard(store, ph2))';
|
|
2755
|
+
const srcAuth23 = fs23.readFileSync!(new URL('src/server/auth.ts', pkgRootUrl23), 'utf8');
|
|
2756
|
+
const cnt23 = srcAuth23.split(needle23).length - 1;
|
|
2757
|
+
await A('[23] 毒化錨恰 1(split 計數——接線形單點承載)', cnt23 === 1, 'cnt=' + cnt23);
|
|
2758
|
+
if (cnt23 === 1) {
|
|
2759
|
+
fs23.writeFileSync!(path23x.join(tree23, 'src/server/auth.ts'), srcAuth23.split(needle23).join('await store.findByIdentityQuery(ph2)'), 'utf8');
|
|
2760
|
+
try {
|
|
2761
|
+
const modP = (await import(url23.pathToFileURL(path23x.join(tree23, 'src/server/auth.ts')).href + '?poison23=1')) as unknown as { loginRouteCore: typeof loginRouteCore };
|
|
2762
|
+
const tP = mkDb23();
|
|
2763
|
+
const storeP = {
|
|
2764
|
+
findByIdentityQuery: async () => null,
|
|
2765
|
+
createUser: async () => 'acct-ghost-23',
|
|
2766
|
+
getByUserKey: async (key: string) => (tP.db.prepare(`SELECT ${USER_COLS23} FROM users WHERE account_id = ?`).get(key) as unknown) || null,
|
|
2767
|
+
revokeAllSessions: async () => {},
|
|
2768
|
+
insertSession: async () => {},
|
|
2769
|
+
ladderLookup: async () => 'acct-23old',
|
|
2770
|
+
};
|
|
2771
|
+
tP.db.prepare("INSERT INTO users (account_id, ph2, salt, wrapped_key) VALUES ('acct-23old', ?, 's23', 'wr23')").run(row23old);
|
|
2772
|
+
const resP = await modP.loginRouteCore(tP.env23 as never, storeP as never, 'ip-23-P', ph1b23);
|
|
2773
|
+
const jP = JSON.parse(await resP.text()) as { userKey: string; status: string };
|
|
2774
|
+
await A('[23] 毒化(ladderGuard 接線摘除)→ ladder hit 案翻建幽靈(行為面真翻——designated)',
|
|
2775
|
+
jP.userKey === 'acct-ghost-23', 'userKey=' + jP.userKey);
|
|
2776
|
+
} catch (e) {
|
|
2777
|
+
await A('[23] 毒化重演(行為面真翻)', false, 'crash: ' + String((e as Error).message).slice(0, 160));
|
|
2778
|
+
}
|
|
2779
|
+
fs23.writeFileSync!(path23x.join(tree23, 'src/server/auth.ts'), srcAuth23, 'utf8');
|
|
2780
|
+
const restored23 = fs23.readFileSync!(path23x.join(tree23, 'src/server/auth.ts'), 'utf8') === srcAuth23;
|
|
2781
|
+
fs23.rmSync!(tree23, { recursive: true, force: true });
|
|
2782
|
+
await A('[23] 毒化還原 byte-exact+樹出生即棄(本 repo 樹零接觸——零殘毒面)', restored23);
|
|
2783
|
+
} else {
|
|
2784
|
+
fs23.rmSync!(tree23, { recursive: true, force: true });
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
};
|
|
2792
|
+
secOpen(24, '[24] runner 契約自證(--only 解析契約 fail-closed+段級帳面顯形)'); if (secEnter(24)) {
|
|
2793
|
+
await A('[24] --only 解析契約(全跑態):spec 缺席=onlySet 空集=secEnter 恆真(CI 形契約不變——npm run verify 全帳;--only 態本面恆真跳過=帳面由次面承載)',
|
|
2794
|
+
!onlyMode() ? (onlySpecRaw === null && parseOnly('').err === 'ERR_ONLY_SPEC_EMPTY' && secEnter(1) && secEnter(23)) : true);
|
|
2795
|
+
await A('[24] --only 解析契約(CSV/範圍/混形三形同集合):16,17,18 ≡ 16-18(同集合);16,17-19 展開 16..19;單調遞增去重 18,16-17,16 → [16,17,18]',
|
|
2796
|
+
JSON.stringify(parseOnly('16,17,18').set) === JSON.stringify([16, 17, 18])
|
|
2797
|
+
&& JSON.stringify(parseOnly('16-18').set) === JSON.stringify([16, 17, 18])
|
|
2798
|
+
&& JSON.stringify(parseOnly('16,17-19').set) === JSON.stringify([16, 17, 18, 19])
|
|
2799
|
+
&& JSON.stringify(parseOnly('18,16-17,16').set) === JSON.stringify([16, 17, 18]));
|
|
2800
|
+
await A('[24] --only 解析契約(fail-closed 分流):空值/非數字 token/慣例 skip 段 22/未定段(0/99)/倒序範圍=ERR_ONLY_*(exit 1 由頂層守衛承載——parse 面證 err 分流真值)',
|
|
2801
|
+
parseOnly('').err === 'ERR_ONLY_SPEC_EMPTY' && parseOnly('abc').err === 'ERR_ONLY_SPEC_TOKEN'
|
|
2802
|
+
&& parseOnly('0').err === 'ERR_ONLY_SPEC_UNKNOWN_SEC' && parseOnly('22').err === 'ERR_ONLY_SPEC_UNKNOWN_SEC'
|
|
2803
|
+
&& parseOnly('99').err === 'ERR_ONLY_SPEC_UNKNOWN_SEC' && parseOnly('2-1').err === 'ERR_ONLY_SPEC_RANGE'
|
|
2804
|
+
&& parseOnly('16,,17').err === 'ERR_ONLY_SPEC_EMPTY_TOKEN');
|
|
2805
|
+
await A('[24] 段級帳面顯形契約:全跑態 onlySpecDisp 空+onlyTag 光禿;--only 態 spec CSV 遞增+(--ONLY …) 尾碼形+secRan 本段已入帳',
|
|
2806
|
+
onlyMode() ? ((secRan.get(24) ?? 0) >= 3 && onlySpecDisp() === (onlyParsed?.set ?? []).join(',') && onlyTag().startsWith('(--ONLY ')) : (onlySpecDisp() === '' && onlyTag() === ''));
|
|
2807
|
+
await A('[24] skip 慣例面:secEnter(22)=false(22 慣例 skip 段——secOpen 行缺席+bannerless 斷言群帳入 [13] 的慣例面;secEnter 全跑態 n≥1 且 n≠22 即真=段落帳冊退役)',
|
|
2808
|
+
secEnter(22) === false);
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
secOpen(25, '[25] 段級運行樣本:secEnter 真值表+tag 形帳+secCases 段帳'); if (secEnter(25)) {
|
|
2812
|
+
await A('[25] secEnter 真值表:bannerless 段全跑態行為(26 未在 secOrder 段帳=secRan 零位面——帳面逐段只列 secOrder;n=0 恆 false);全跑態實段 1/15/23 全真',
|
|
2813
|
+
!onlyMode() ? (secEnter(0) === false && (secRan.get(26) ?? 0) === 0 && secEnter(1) && secEnter(15) && secEnter(23)) : true);
|
|
2814
|
+
await A('[25] tag/spec 形帳:--only 態 (--ONLY …) 括形+spec 遞增 CSV 非空;全跑態 spec 空+tag 裸',
|
|
2815
|
+
onlyMode() ? (onlyTag().startsWith('(--ONLY ') && onlyTag().endsWith(')') && onlySpecDisp().length > 0)
|
|
2816
|
+
: (onlySpecDisp() === '' && onlyTag() === ''));
|
|
2817
|
+
await A('[25] secCases 段帳登記義務(負向斷言——teeth):secRan 實跑段(本機樹 [23]⑤ fail-open 表缺席異常面照行)必已登記;secCases 缺鍵且非 22 段=RED(secCases 全摘禁令的行為面承載)',
|
|
2818
|
+
(() => { for (const [k25] of secRan) if (!secCases[k25] && k25 !== 22) return false; return true; })());
|
|
2819
|
+
await A('[25] self-account:secCases[25] = 3 恰此三案(段帳面 self-proving——帳漂移即本面 RED;secCases 載體面:[24]/[25] 自證段+--only 段存在面+teeth 三載體)',
|
|
2820
|
+
secCases[25] === 3 && (secRan.get(25) ?? 0) === 3);
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
// ── 段級帳面(誠實帳——skip 段恆列帳「未跑非通過」;全跑態逐段案數對 secCases 帳面)──
|
|
2824
|
+
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];
|
|
2825
|
+
const secTotal = secOrder.reduce((a, n) => a + (secCases[n] ?? 0), 0);
|
|
2826
|
+
const ranSecs = secOrder.filter((n) => (secRan.get(n) ?? 0) > 0);
|
|
2827
|
+
const selTotal = secOrder.filter((n) => !onlyMode() || onlySet.has(n) || n === 24 || n === 25).reduce((a, n) => a + (secCases[n] ?? 0), 0);
|
|
2828
|
+
const skipSecs = secOrder.filter((n) => !ranSecs.includes(n));
|
|
2829
|
+
console.log('\n──── 段級帳面 ────');
|
|
2830
|
+
for (const nSec of secOrder) {
|
|
2831
|
+
const ranN = secRan.get(nSec) ?? 0;
|
|
2832
|
+
const rowLabel = ranSecs.includes(nSec)
|
|
2833
|
+
? (failures.length === 0 ? 'ok' : '見 ✗(失敗 ' + failures.length + '——詳上)')
|
|
2834
|
+
: 'skip(--only 態未選段——本輪未跑非通過)';
|
|
2835
|
+
console.log('[' + nSec + '] 案 ' + ranN + '/' + (secCases[nSec] ?? 0) + ' · ' + rowLabel);
|
|
2836
|
+
}
|
|
2279
2837
|
|
|
2280
2838
|
console.log(`\n${passed} 斷言全綠` + (failures.length ? `;${failures.length} 失敗` : ''));
|
|
2839
|
+
if (onlyMode()) {
|
|
2840
|
+
console.log('--only 態:段 ' + onlySpecDisp() + ' 運行,本輪 ' + passed + '/' + selTotal + '(含 runner 自證段 ' + ((secCases[24] ?? 0) + (secCases[25] ?? 0)) + ';總帳 ' + secTotal + ' 案非本輪載;skip ' + skipSecs.length + ' 段——本輪未跑非通過)');
|
|
2841
|
+
}
|
|
2281
2842
|
if (failures.length) {
|
|
2282
2843
|
console.error('失敗項:', failures);
|
|
2283
2844
|
process.exit(1);
|
|
2284
2845
|
}
|
|
2285
2846
|
|
|
2286
|
-
console.log('CORE-CRYPTO-VERIFY-OK');
|
|
2847
|
+
console.log('CORE-CRYPTO-VERIFY-OK(' + passed + ' assertions)' + onlyTag());
|