@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 CHANGED
@@ -17,8 +17,8 @@
17
17
 
18
18
  ```sh
19
19
  git clone https://github.com/tacet-ink/journal-core.git && cd journal-core
20
- npm ci
21
- npm run verify # 432 assertions, all green, run against the real modules (no mocks)
20
+ npm ci --ignore-scripts
21
+ npm run verify # 491 assertions, all green, run against the real modules (no mocks)
22
22
  ```
23
23
 
24
24
  ```ts
@@ -76,7 +76,7 @@ run: `node --experimental-strip-types example.ts`
76
76
  | `src/client/bip39.ts` | 復原套件 24 詞 ⇄ hex64 轉寫層(BIP39,零依賴自製) |
77
77
  | `src/client/keys.ts` | 品牌前綴 localStorage 命名空間 |
78
78
  | `src/server/auth.ts` | 零知識 auth 核心:PH1→PH2 login 動線、入庫格式驗證、包裹欄組成對(pair 檢查 pickKeyPackage);PH2 UNIQUE 衝突與 session 撤銷=呼叫端職責 |
79
- | `src/server/ladder.ts` | 密語正規化 v3 世代 ladder 表原語(ph2_ladder:以舊 ph2 查列/遷移入表 upsert 值主權威)——幽靈守衛的資料承載位(查表接線在 fork route) |
79
+ | `src/server/ladder.ts` | 密語正規化 v3 世代 ladder 表原語(ph2_ladder:以舊 ph2 查列/遷移入表 upsert 值主權威)+幽靈守衛查表接線已內建 loginRouteCore——fork 只需配 `ladderLookup` store+0011 migration(未配置=單查行為零變) |
80
80
  | `src/server/ratelimit.ts` | per-IP fixed-window 限流(單句 UPSERT…RETURNING;D1 計數,跨 isolate 有效) |
81
81
  | `src/server/cors.ts`/`hash.ts` | 共用 CORS/雜湊工具(`src/server/env.ts` 為內部 Env 介面,不入 exports) |
82
82
 
@@ -118,18 +118,28 @@ jr3d/jr4d 兩入參數同輸入,域分離由 info 承載;凍結 KAT 兩 blob
118
118
  ## 驗證(Verification)
119
119
 
120
120
  ```sh
121
- npm run verify # 432 斷言對真模組(禁鏡像;限流單句 UPSERT…RETURNING 直載真 SQLite):roundtrip/AAD 防搬移/
121
+ npm ci --ignore-scripts
122
+ npm run verify # 491 斷言對真模組(禁鏡像;限流單句 UPSERT…RETURNING 直載真 SQLite):roundtrip/AAD 防搬移/
122
123
  # extractable/時代隔離/跨前綴家族隔離/payload 竄改/RFC 9106 KAT/BIP39 @scure 對照 200 組/
123
124
  # server-side(inboundCipher/validWrappedKey/pickKeyPackage/checkRate/timingSafeEq)+ pinlock 全函式
124
125
  # + 密語正規化 v3 世代(normalizePassphrase/jr4w./jr4d./PH1 v3 鹽域分離+帶內版本化舊契約向量)
125
126
  # + 本機包裹專用前綴(wrapLocal=jr1l. opt-in+讀舊寫新自癒)
126
127
  # +復原套件專用前綴與 HKDF 世代 [20](wrapRec=jr1r.+recKekHkdf opt-in+讀舊寫新雙試+KAT)
128
+ # +loginRouteCore ladder 查表守衛 [23](外審 #8 幽靈帳:現值 miss → ladder → 兩面 miss 才建幽靈;真 node:sqlite 直驅七情境+拷貝樹毒化接線摘除案)
127
129
  # +ladder 表與重取鹽面 [19](ph2_ladder 原語/upsert 值主權威行為面/unwrapNoteKeyDual4WithSalt)
128
130
  # +常駐毒化矩陣 [16][18](/tmp 拷貝樹突變重跑=毒化證據隨每執行重建)
129
131
  # +KAT 凍結向量 [17](HKDF info 域世代分離+v3/raw 入口契約四 blob)
132
+ # +包裹前綴快檢 [13-2](錯前綴試探免付 KDF:五入口行為+計時帽+計數錨)
133
+ # +CI 供應鏈面 [21](npm ci --ignore-scripts+dependabot runner fromJSON 分流+腳本摘除+NIT-1 錨+r2 MINOR-1 URL 直傳;
134
+ # 491 帳=repo 樹——發行包內無 .github=[21] 四條 workflow 錨收斂為 1 顯性 SKIP 行→480,SKIP 顯形非靜默)
135
+ # +發行 tarball 治理面 [22](pack 白名單零 open-card-pr.sh)
130
136
  ```
131
137
 
132
138
  - 產品碼**零執行時依賴**(WebCrypto 原語);devDependencies 僅閘用(typescript、@scure/bip39 對照、workers types)。
139
+ - **node_modules 出廠態說明**:本 repo 的 `@scure/bip39`/`@noble/hashes` 由閘的 vendor 修復
140
+ 腿(`scripts/prepare-core-pkg.cjs`,plain `npm ci` 後執行)補齊 symlink 與 vendored 副本——
141
+ 與 devDependencies 安裝態等價(provenance 註記:現場樹的非 pristine 安裝形態屬此機制,
142
+ 非手工污染;reproduce 走 plain `npm ci` 後跑 prepare 腿即收斂)。
133
143
  - Argon2id 雙載體:node 走 `node:crypto`(Node 24.7+ 原生)、瀏覽器走 hash-wasm(wasm 內嵌),
134
144
  RFC 9106 標準向量逐位元一致;無載體即 throw(禁 fallback 鐵律)。
135
145
  - BIP39 原語自製零依賴,與 @scure/bip39 參照 200 組雙向對照(僅 entropy↔words 轉寫層,禁用其 seed 派生)。
@@ -180,20 +190,54 @@ PH1 v3(密語正規化世代):`derivePh1ArgonV3(pass, saltArg?)` 預設鹽
180
190
  raw 'PASS' 撞同一 ph2)。派生輸入吃 `normalizePassphrase`(NFKC-only;大小寫摺疊禁絕=
181
191
  大小寫差恆不同 ph2);v2/legacy 派生面零變更(舊帳戶憑證 raw 契約)。兩代 ph2 可並存查表=
182
192
  tacet 遷移層職責(v0.2.0 世代收口)。
193
+
194
+ server 登入動線(fork 端):`loginRouteCore(request-ish, store, ip, ph1)` 內建
195
+ ladder 查表守衛——現值 miss 時經 `AuthStore.ladderLookup`(optional)查 ph2_ladder,
196
+ 命中回舊帳不建幽靈、兩面 miss 才 `createUser`;未配置=行為與 0.2.3 單查恆等。
197
+ 接法:store 實作 `ladderLookup`(tacet 參考形=`SQL_FIND_PH2_LADDER_BY_OLD`+fail-open
198
+ catch)+套用 `migrations/0011-ph2-ladder.sql`(+0012 索引修正);查表零寫入面
199
+ (insert 在 `rekeyWithLadder` 遷移線、delete 在 fork 的 delete-account 批)。
183
200
  ⚠️ 鹽撞位警告:`PH1_V3_SALT` 禁當 `derivePh1Argon` 的 `saltArg` 餵入(反之亦然)——
184
201
  把 v3 鹽域值餵 v2 派生(或 fork 把同一 saltArg 帶到兩面)=NFKC-effective 密語與 v2 raw 密語
185
202
  同值撞 ph2,跨世代帳戶空間混合。v3 的 `saltArg` 必須異於本 product 的 v2 鹽域值。
186
203
 
187
204
  ### 例外契約(錯誤碼語意)
188
205
 
189
- - 空 identity(guest 派生):加密面拋 `ERR_NO_IDENTITY`、解密面回 `null`
190
- (K_u = SHA-256(prefix ‖ '') 會靜默產出「空帳號金鑰」=誤配炸彈,兩面皆 fail-closed)。
191
- - 空 PIN(`wrapNoteKeyDual`/`wrapNoteKeyDual3`/`wrapNoteKeyDual4`):拋 `ERR_PIN_EMPTY`——與
192
- 「功能未配置」(`ERR_DUAL_NOT_CONFIGURED`/`ERR_JR3W_NOT_CONFIGURED`/`ERR_JR4W_NOT_CONFIGURED`/
193
- `ERR_JR4D_NOT_CONFIGURED`)語意分離。
194
- - `hexToBytes` fail-closed:非法 hex(奇數長度/非 hex 字元/空字串)拋 `ERR_BAD_HEX`,
195
- 不再 parseInt 靜默歸零(假金鑰生產器)。unwrap 家族的 try/catch 承接=跨前綴鐵律
196
- (回 null 不拋)不變。
206
+ **客戶端(client/)**
207
+
208
+ | 錯誤碼 | 拋出面 |
209
+ | --- | --- |
210
+ | `ERR_NO_IDENTITY` | guest 加密面空 identity(解密面同面回 `null`) |
211
+ | `ERR_GUEST_NOT_CONFIGURED` | `encryptNote` 未配置 `cipherGuest` |
212
+ | `ERR_BAD_HEX` | `hexToBytes` 非法 hex(奇數長度/非 hex 字元/空字串) |
213
+ | `ERR_WRAP_NOT_CONFIGURED` | 包裹未配置(`sealNoteKey` 本體+`wrapNoteKey` 入口雙防線) |
214
+ | `ERR_WRAPTAMPER_LEN32` | `sealNoteKey` noteKey 非 32B(內部契約面) |
215
+ | `ERR_DUAL_NOT_CONFIGURED` | `wrapNoteKeyDual` 未配置 `wrapDual`/`pinSaltPrefix` |
216
+ | `ERR_SHARE_NOT_CONFIGURED` | `wrapNoteKeyShare` 未配置 `wrapShare` |
217
+ | `ERR_LOCAL_NOT_CONFIGURED` | `encryptLocal` 未配置 `cipherLocal` |
218
+ | `ERR_ATTACH_NOT_CONFIGURED` | `encryptAttach` 未配置 `cipherAttach` |
219
+ | `ERR_PIN_EMPTY` | `wrapNoteKeyDual/3/4` 空 PIN(≠ 未配置——語意分離) |
220
+ | `ERR_PINLOCK_NOT_CONFIGURED` | pinlock 未配置三欄任一 |
221
+ | `ERR_PINLOCK_EMPTY` | pinlock 空 PIN |
222
+ | `ERR_REC_CFG_PARTIAL` | `wrapRec`/`recKekHkdf` 部分配置(配對門拒寫) |
223
+ | `ERR_REC_KEK_SALT` | `recKekSalt` 非 16B hex(HKDF 世代鹽域 fail-closed) |
224
+ | `ERR_JR3W_NOT_CONFIGURED` | `wrapNoteKey3`/`wrapNoteKeyDual3`(+`share` 面)未配置 |
225
+ | `ERR_JR3S_NOT_CONFIGURED` | `wrapNoteKeyShare3` 未配置 `wrapShare3` |
226
+ | `ERR_JR4W_NOT_CONFIGURED` | `wrapNoteKey4` 未配置 `wrap4` |
227
+ | `ERR_JR4D_NOT_CONFIGURED` | `wrapNoteKeyDual4` 未配置 `wrapDual4` |
228
+ | `ERR_ARGON2_TAGLEN` | Argon2id 載體 tag 非 32B(載體契約面) |
229
+ | `ERR_ARGON2_UNAVAILABLE` | 無 Argon2id 載體(node<24.7 且無 hash-wasm 注入——禁 fallback) |
230
+
231
+ **伺服端(server/)+閘**
232
+
233
+ | 錯誤碼 | 拋出面 |
234
+ | --- | --- |
235
+ | `ERR_BAD_REQUEST` | auth 動線缺 PH1 等必填(400 面回應) |
236
+ | `ERR_RATE_LIMITED` | 限流命中(429 面回應) |
237
+ | `ERR_RATE_TABLE_NAME` | 限流表名非法字元(SQL 面 fail-closed) |
238
+ | `ERR_SQLITE_UNAVAILABLE` | 驗證閘環境 node:sqlite 缺席 |
239
+ | `ERR_FS_UNAVAILABLE` | 驗證閘環境 node:fs 缺席 |
240
+
197
241
  - `PinLockConfig`:解包輸出的 noteKey 恆 `extractable=true`(鐵律,非可調選項)——
198
242
  config 只帶三個前綴欄(pinLock/pinLockSaltPrefix/pinLockAad),缺任一即拒。
199
243
 
@@ -240,6 +284,16 @@ and salted hashes, and never learns your passphrase or the content of any note.
240
284
  - **Key hygiene.** Any key that must be exported/wrapped is imported with `extractable=true`;
241
285
  KEK and guest keys are permanently non-extractable.
242
286
 
287
+ **Server login wiring.** `loginRouteCore(store, ip, ph1)` ships with the ladder guard built in
288
+ (v0.2.4, external-review #8 ghost-account fix): on a current-value (PH2) miss it consults the
289
+ `ph2_ladder` table via the optional `AuthStore.ladderLookup` — a ladder hit returns the
290
+ existing account (no ghost creation); a ladder miss, an unconfigured lookup, or a
291
+ ladder-store error (fail-open, treated as a miss) all fall through to the legacy
292
+ single-lookup behavior of ≤0.2.3. Forks adopt the guard by configuring `ladderLookup`
293
+ on their store (the tacet reference: `d1AuthStore`) and applying `migrations/0011-ph2-ladder.sql`
294
+ — the lookup itself never writes (inserts stay on the `rekeyWithLadder` migration path,
295
+ deletes on the fork's delete-account flow).
296
+
243
297
  **Opt-in primitives.** Optional contracts (`cipherGuest` / `wrapDual` / `wrapShare` / `pinLock` /
244
298
  `cipherAttach` / `cipherLocal` / `wrap4` / `wrapDual4` / `wrapLocal` / `wrapRec` (paired with
245
299
  `recKekHkdf`: configured together or not at all; partial configuration is rejected on write) /
@@ -257,9 +311,10 @@ PH1→PH2 login flow, inbound cipher/package validation and paired key-package s
257
311
  (`auth.ts`; PH2-UNIQUE conflict and session revocation are caller-owned wiring), per-IP
258
312
  fixed-window rate limiting on D1 (`ratelimit.ts`), shared CORS/hash utilities.
259
313
 
260
- **Verification.** `npm run verify` runs 432 assertions against the real modules (no mocks):
314
+ **Verification.** `npm run verify` runs 491 assertions against the real modules (no mocks):
261
315
  roundtrips, AAD tamper-evidence, extractability rules, era isolation, cross-prefix family
262
- isolation, payload tampering, RFC 9106 KAT, and a 200-vector BIP39 cross-check.
316
+ isolation, payload tampering, RFC 9106 KAT, a 200-vector BIP39 cross-check, and the
317
+ login ladder-guard behavior suite (external audit #8).
263
318
 
264
319
  **Usage.** TypeScript source is published (exports point at `.ts`). If you install from npm you
265
320
  must bundle it (vite/esbuild etc.); Node's strip-types does not apply inside `node_modules`.
@@ -0,0 +1,18 @@
1
+ -- 0012-fix-ph2-ladder-index.sql:ph2_ladder 索引修正(外審報告 #3,t_87ef62dd)。
2
+ --
3
+ -- 前置:0011-ph2-ladder.sql(2026-10-05 已部署 prod=禁回改本卡對位說明)。
4
+ -- 0011 缺陷(外審複證屬實):
5
+ -- ①重複冗餘 index——idx_ph2_ladder_ph2 對 PRIMARY KEY(ph2) 逐位同構,
6
+ -- PK 本身即查表索引(findByOldPh2 WHERE ph2 = ? 走 PK),duplicate 浪費寫入面。
7
+ -- ②account_id 索引缺席——「一帳戶多舊值歷列」查詢(delete-account 清場對位、
8
+ -- 帳戶面維運)全表 SCAN;FK 欄零索引是 SQLite/D1 對位常見債。
9
+ -- 修正(報告原樣):DROP 冗餘 ph2 index+CREATE account_id index。
10
+ -- 非 UNIQUE——「一帳戶多舊值」是一帳多列設計(legacy→v2→v3 每段遷移各存一段舊值),
11
+ -- UNIQUE 索引會讓第二段遷移 INSERT 直接炸(與 ladder upsert 語意正交)。
12
+ -- 聚合量測語句(0011 母型)不動:LEFT JOIN 走 ph2 = PK 面照舊。
13
+ --
14
+ -- 契約:0011 已上線不分叉環境照舊;本 migration 純索引面、零行為變更、可重入。
15
+
16
+ DROP INDEX IF EXISTS idx_ph2_ladder_ph2;
17
+
18
+ CREATE INDEX IF NOT EXISTS idx_ph2_ladder_account ON ph2_ladder(account_id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tacet-ink/journal-core",
3
- "version": "0.2.3",
3
+ "version": "0.2.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": {
@@ -0,0 +1,74 @@
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(' ') + ')');