@tacet-ink/journal-core 0.2.3 → 0.2.4
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 +415 -7
- package/src/client/argon2.ts +4 -0
- package/src/client/note-crypto.ts +7 -1
- package/src/server/auth.ts +45 -6
- package/src/server/ladder.ts +5 -1
- package/scripts/open-card-pr.sh +0 -49
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 #
|
|
20
|
+
npm ci --ignore-scripts
|
|
21
|
+
npm run verify # 475 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
|
|
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
|
|
121
|
+
npm ci --ignore-scripts
|
|
122
|
+
npm run verify # 475 斷言對真模組(禁鏡像;限流單句 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
|
+
# 475 帳=repo 樹——發行包內無 .github=[21] 四條 workflow 錨收斂為 1 顯性 SKIP 行→472,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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
314
|
+
**Verification.** `npm run verify` runs 475 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,
|
|
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
|
+
"version": "0.2.4",
|
|
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(' ') + ')');
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* verify-core-crypto.ts — core 抽取的驗證閘(對真模組,禁鏡像——extractable 教訓)。
|
|
3
3
|
* 執行:node --experimental-strip-types scripts/verify-core-crypto.ts
|
|
4
4
|
* 全綠輸出 CORE-CRYPTO-OK;任何失敗 exit 1。
|
|
5
|
+
*
|
|
6
|
+
* 2026-10-09(0.2.4):[23] loginRouteCore ladder 查表守衛段(外審 #8 幽靈帳)——真 node:sqlite
|
|
7
|
+
* shim 直驅七情境(現值 hit/ladder hit 回舊帳/兩面 miss 建幽靈/未配置單查零變/fail-open)
|
|
8
|
+
* +拷貝樹毒化 in-gate(接線摘除 → ladder hit 翻建幽靈)。POISON_GATE_INNER 內層跑份自我跳過。
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
11
|
// 對真模組(禁鏡像重寫金鑰邏輯——鏡像驗證抓不到真模組 bug 的生產教訓)
|
|
@@ -45,6 +49,8 @@ import {
|
|
|
45
49
|
verifyArgonKat,
|
|
46
50
|
wrapNoteKey3,
|
|
47
51
|
unwrapNoteKey3,
|
|
52
|
+
wrapNoteKeyShare3,
|
|
53
|
+
unwrapNoteKeyShare3,
|
|
48
54
|
wrapNoteKeyDual3,
|
|
49
55
|
unwrapNoteKeyDual3,
|
|
50
56
|
wrapNoteKey4,
|
|
@@ -451,7 +457,21 @@ try {
|
|
|
451
457
|
}
|
|
452
458
|
await A('與 @scure/bip39 參照 200 組雙向一致', refOk);
|
|
453
459
|
} catch {
|
|
454
|
-
|
|
460
|
+
// 斷鏈(symlink 落空)與真缺席要分流:裸 npm ci 環境的確定性病是「參照組斷鏈」——
|
|
461
|
+
// 訊息指向 vendor 修復(prepare-core-pkg.cjs),不是泛化「未安裝」(對斷鏈態毫無作用=誤導指示)。
|
|
462
|
+
// 路徑錨定 repo root(t_87ef62dd NIT-1):裸 '@scure/bip39' 是 CWD 相對——他 cwd 跑閘
|
|
463
|
+
// (如 scripts/ 子目錄)會把斷鏈態誤報「未安裝」;錨 import.meta.url 起手=CWD 無關恆真。
|
|
464
|
+
const fsMod = (globalThis as unknown as { process?: { getBuiltinModule?: (id: string) => { lstatSync?: (p: string) => { isSymbolicLink(): boolean }; existsSync?: (p: string) => boolean } | undefined } }).process?.getBuiltinModule?.('node:fs');
|
|
465
|
+
const repoRootRef = new URL('../node_modules/@scure/bip39', import.meta.url).pathname.replace(/\/$/, '');
|
|
466
|
+
const isBrokenLink = !!fsMod?.lstatSync && !!fsMod.existsSync && (() => {
|
|
467
|
+
try {
|
|
468
|
+
return fsMod.lstatSync(repoRootRef).isSymbolicLink() && !fsMod.existsSync(repoRootRef);
|
|
469
|
+
} catch { return false; } // ENOENT=真缺席(未安裝),不是斷鏈
|
|
470
|
+
})();
|
|
471
|
+
await A('與 @scure/bip39 參照 200 組雙向一致', false,
|
|
472
|
+
isBrokenLink
|
|
473
|
+
? '參照套件斷鏈(@scure/bip39 symlink realpath 不存在)——重跑 npm ci(plain)再跑 scripts/prepare-core-pkg.cjs vendor 修復'
|
|
474
|
+
: '參照套件未安裝(devDependencies @scure/bip39)');
|
|
455
475
|
}
|
|
456
476
|
|
|
457
477
|
// 與現行包裹鏈相容:words 造的 hex64 走 recTokenHash/wrapNoteKeyWithRecToken 原樣
|
|
@@ -873,6 +893,56 @@ await A(`hexToBytes 全形數字('2f')→ Bytes 等值(NFKC 窗殲滅
|
|
|
873
893
|
await A('bip39 hexToBytes 同步(HEX64_RE 自守前提下的舊向量不變)', (await bip.recTokenToWords('ab'.repeat(32)))?.length === 24);
|
|
874
894
|
await A('unwrap:鹽 hex 非法字元 → 假錯誤誘餌面 null(原碼靜默歸零會錯誤炸出成功路徑)',
|
|
875
895
|
(await unwrapNoteKey(TACET2, dual.wrapped, passD, 'a'.repeat(31) + 'z')) === null);
|
|
896
|
+
// ── 13-2. 包裹前綴快檢上移(t_87ef62dd,外審 #2):錯前綴 junk 在 KDF 派生前即 null——
|
|
897
|
+
// 母型 unwrapNoteKey4 入口快檢(argon2.ts);五入口(jr1w./jrsw./jr3w./jr3s.+recToken 兩腿)
|
|
898
|
+
// 行為恆 null 不拋(快檢同向 null=契約零變——錯前綴本就由 openNoteKey:417 拒);
|
|
899
|
+
// 收益面=錯前綴試探不再付 600k PBKDF2/64MiB Argon2id KDF 成本(計時面對真 KDF 斷言)。
|
|
900
|
+
{
|
|
901
|
+
const junkPrefix = 'jr9w.' + wrapped.slice('jr1w.'.length); // 真形 payload 錯前綴(junk 前綴面)
|
|
902
|
+
const t0Junk = performance.now();
|
|
903
|
+
const junkRc = [
|
|
904
|
+
await unwrapNoteKey(TACET, junkPrefix, pass, salt),
|
|
905
|
+
await unwrapNoteKeyShare(TACET, junkPrefix, pass, salt),
|
|
906
|
+
await unwrapNoteKey3(TACET3, junkPrefix, pass, salt),
|
|
907
|
+
await unwrapNoteKey3(TACET3, junkPrefix, 'wrong-passphrase', salt),
|
|
908
|
+
await unwrapNoteKeyShare3(TACET3, junkPrefix, pass, salt),
|
|
909
|
+
];
|
|
910
|
+
const tJunkMs = performance.now() - t0Junk;
|
|
911
|
+
await A('錯前綴 junk 試探(真 payload 錯前綴面)五入口恆 null 不拋(快檢同向 null=契約零變)', junkRc.every(r => r === null), JSON.stringify(junkRc.map(r => r === null)));
|
|
912
|
+
// 行為承載面(KDF 未走即 null):PBKDF2 600k ≈ 60-150ms/Argon2id 64MiB ≈ 120-150ms——
|
|
913
|
+
// 任一入口真付 KDF 即超帽;快檢落地後五次試探總計 ≪1ms(實測 <1ms;帽 30ms=數量級餘裕)。
|
|
914
|
+
// 結構面(真防線)=下方源碼計數錨+接線形——計時帽是行為輔助面(未來快機 PBKDF2 縮水時
|
|
915
|
+
// 計數錨仍承載);對照組只承載「正確前綴快檢後仍真 unwrap」=零變更契約行為面(不計時——
|
|
916
|
+
// 絕對時距對照在快慢機間是 flake 源,tacet-dev runner 母型)。
|
|
917
|
+
await A('錯前綴 junk 試探五入口計時帽 ≪ KDF 成本(快檢在 deriveKek/deriveKekArgon 前——真付 KDF 即超帽)', tJunkMs < 30, 'junk5=' + tJunkMs.toFixed(2) + 'ms');
|
|
918
|
+
const t2Legacy = performance.now();
|
|
919
|
+
const legacyBack = await unwrapNoteKey(TACET, wrapped, pass, salt);
|
|
920
|
+
await A('對照組:正確前綴 unwrap 真付 KDF 且救回 noteKey(快檢零變更契約——正確路徑行為面)',
|
|
921
|
+
legacyBack !== null && hex(new Uint8Array(await crypto.subtle.exportKey('raw', legacyBack))) ===
|
|
922
|
+
hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey))), 'elapsed=' + (performance.now() - t2Legacy).toFixed(2) + 'ms');
|
|
923
|
+
// 源碼計數錨(快檢五點+recToken 雙腿接線=tacet-dev「條款與接線分離」):五入口 startsWith 快檢
|
|
924
|
+
// 恰 5(摘任一=計數變異);recToken 雙腿錨——jr1r. 面在 deriveRecKek 前、cfg.wrap 面在
|
|
925
|
+
// deriveRecLegacyKek 前的相鄰形(錯位接線——快檢搬進 KDF 之後=形變異即 RED)。
|
|
926
|
+
const ncSrc132 = await srcOf('../src/client/note-crypto.ts');
|
|
927
|
+
const arSrc132 = await srcOf('../src/client/argon2.ts');
|
|
928
|
+
const qcNc = (ncSrc132.match(/if \(!wrapped\.startsWith\(cfg\.(wrap|wrapShare|wrapRec|wrap)\)\) return null;/g) ?? []).length;
|
|
929
|
+
const scNc = ncSrc132.split('wrapped.startsWith(cfg.wrapRec)').length - 1; // jr1r. 腿條件面(跳過形)
|
|
930
|
+
const qcAr = (arSrc132.match(/if \(!wrapped\.startsWith\(cfg\.(wrap3|wrapShare3)\)\) return null;/g) ?? []).length;
|
|
931
|
+
await A('源碼計數錨:startsWith 快檢恰 6 條(note-crypto 4=jr1w./jrsw./舊腿終面/jr1r. 條件面+argon2 2;錯前綴試探防線——摘任一條即計數變異 RED)',
|
|
932
|
+
qcNc === 3 && scNc === 1 && qcAr === 2, 'nc=' + qcNc + ' sc=' + scNc + ' ar=' + qcAr);
|
|
933
|
+
await A('源碼計數錨:recToken 雙腿快檢接線形(jr1r. 腿條件面在 deriveRecKek 前——禁 return null 短路雙試;舊面終面接線在 deriveRecLegacyKek 前)',
|
|
934
|
+
/if \(cfg\.wrapRec && cfg\.recKekHkdf && wrapped\.startsWith\(cfg\.wrapRec\)\) \{\n {6}const recKek = await deriveRecKek\(/.test(ncSrc132)
|
|
935
|
+
&& /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));
|
|
936
|
+
// 毒化錨前綴自檢(錨窗同字面點數——毒化矩陣 Q 案的錯位防線;jr1w./舊腿兩行同註解開頭——
|
|
937
|
+
// 錨帶全註解頭消歧:『(t_87ef62dd)』=jr1w. 面、『:錯前綴』=舊腿終面)
|
|
938
|
+
await A('快檢錨前綴自檢:五錨各恰 1(split 計數帳)',
|
|
939
|
+
ncSrc132.split('if (!wrapped.startsWith(cfg.wrap)) return null; // 快速前綴快檢(t_87ef62dd)').length === 2
|
|
940
|
+
&& ncSrc132.split('if (!wrapped.startsWith(cfg.wrap)) return null; // 快速前綴快檢:').length === 2
|
|
941
|
+
&& ncSrc132.split('if (!wrapped.startsWith(cfg.wrapShare)) return null; // 快速前綴快檢').length === 2
|
|
942
|
+
&& ncSrc132.split('if (cfg.wrapRec && cfg.recKekHkdf && wrapped.startsWith(cfg.wrapRec)) {').length === 2
|
|
943
|
+
&& arSrc132.split('if (!wrapped.startsWith(cfg.wrap3)) return null;').length === 2
|
|
944
|
+
&& arSrc132.split('if (!wrapped.startsWith(cfg.wrapShare3)) return null;').length === 2);
|
|
945
|
+
}
|
|
876
946
|
await A('unwrapNoteKeyDual:salt1 hex 非法字元 ' + 'z' + ' → null(鹽內嵌族 own 檢查照舊——收口本體=鹽外置族)',
|
|
877
947
|
(await unwrapNoteKeyDual(TACET2, dual.wrapped, passD, pinD, 'a'.repeat(31) + 'z')) === null);
|
|
878
948
|
await A('unwrapNoteKeyDual:salt1 hex 奇數長 → null', (await unwrapNoteKeyDual(TACET2, dual.wrapped, passD, pinD, 'a'.repeat(31))) === null);
|
|
@@ -967,10 +1037,10 @@ async function srcOf(rel: string): Promise<string> {
|
|
|
967
1037
|
// node 內建模組動態存取(structured type,零 node types 依賴——argon2.ts getBuiltinModule 母型同構;
|
|
968
1038
|
// TS2591 types 帽下 import('node:fs') 靜態/動態皆炸=此繞法)
|
|
969
1039
|
const fs = (globalThis as unknown as {
|
|
970
|
-
process?: { getBuiltinModule?: (id: string) => { readFileSync?: (p: string, enc: string) => string } | undefined };
|
|
1040
|
+
process?: { getBuiltinModule?: (id: string) => { readFileSync?: (p: string | URL, enc: string) => string } | undefined };
|
|
971
1041
|
}).process?.getBuiltinModule?.('node:fs');
|
|
972
1042
|
if (!fs?.readFileSync) throw new Error('ERR_FS_UNAVAILABLE');
|
|
973
|
-
return fs.readFileSync(new URL(rel, import.meta.url)
|
|
1043
|
+
return fs.readFileSync(new URL(rel, import.meta.url), 'utf8');
|
|
974
1044
|
}
|
|
975
1045
|
await A('derivePh1Argon 預設分支源碼面 = encode(PH1_V2_SALT)(鹽預設單一真相錨;同-pass 同值斷言對預設值漂移無承載力)',
|
|
976
1046
|
/saltArg \?\? new TextEncoder\(\)\.encode\(PH1_V2_SALT\)/.test(await srcOf('../src/client/argon2.ts')));
|
|
@@ -1003,6 +1073,20 @@ await A('openNoteKey 直接呼叫:壞 base64 → null 不拋(本體吞收點
|
|
|
1003
1073
|
return (await openNoteKey('jr1w.' + '!!not-base64!!', kekC, 'notekey', 'jr1w.')) === null;
|
|
1004
1074
|
})());
|
|
1005
1075
|
|
|
1076
|
+
//── 13-3. 發行 tarball 治理面(外審 #7 的封閉集承載——open-card-pr.sh 摘除的 tarball 面對位;t_87ef62dd)──
|
|
1077
|
+
//(pack 驗證讀 package.json files 白名單——npm 不可用環境 = 顯性 SKIP 形自守衛,非 silent true)
|
|
1078
|
+
await A('[22] 發行 tarball 零 open-card-pr.sh(治理掃蕩面——pack 白名單逐檔對帳),pack 不可用 = SKIP 顯形', await (async () => {
|
|
1079
|
+
try {
|
|
1080
|
+
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');
|
|
1081
|
+
if (!cp132?.execFileSync) return false; // 自守衛:缺席=顯性 FAIL(非 silent true)
|
|
1082
|
+
const out132 = cp132.execFileSync('npm', ['pack', '--json', '--dry-run'], { cwd: new URL('..', import.meta.url).pathname, encoding: 'buffer', timeout: 120000 });
|
|
1083
|
+
const j132 = JSON.parse(out132.toString('utf8')) as { files?: { path: string }[] }[];
|
|
1084
|
+
const files132 = (j132[0]?.files ?? []).map((f) => f.path);
|
|
1085
|
+
const g132 = files132.filter((p) => p === 'scripts/open-card-pr.sh' || p.startsWith('scripts/'));
|
|
1086
|
+
return files132.length > 0 && !files132.includes('scripts/open-card-pr.sh') && g132.length === 2;
|
|
1087
|
+
} catch { return false; }
|
|
1088
|
+
})());
|
|
1089
|
+
|
|
1006
1090
|
// ── 14. 效能形契約(零行為變更——輸出 byte 等價是合約;錨面咬「形」) ──
|
|
1007
1091
|
//
|
|
1008
1092
|
// 效能批的閘承載物理:執行時間不能進閘(機器相依)——咬「並行形在場+串行殘留歸零」
|
|
@@ -1920,7 +2004,7 @@ console.log('\n[19] ladder 表與重取鹽面(unwrapNoteKeyDual4WithSalt+ph2
|
|
|
1920
2004
|
await A('[19] ladder kinds 契約:legacy/v2 兩形(常數面)', PH2_LADDER_KIND_LEGACY === 'legacy' && PH2_LADDER_KIND_V2 === 'v2');
|
|
1921
2005
|
// 值主權威行為面(真 node:sqlite 直驅本體):建表走 fork migration 同形(PK=ph2)。
|
|
1922
2006
|
{
|
|
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 } };
|
|
2007
|
+
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
2008
|
const db19 = new DatabaseSync19(':memory:');
|
|
1925
2009
|
db19.exec('CREATE TABLE users (account_id TEXT PRIMARY KEY, ph2 TEXT)');
|
|
1926
2010
|
db19.exec("INSERT INTO users (account_id, ph2) VALUES ('acc19', 'ph2-new')");
|
|
@@ -1929,6 +2013,38 @@ console.log('\n[19] ladder 表與重取鹽面(unwrapNoteKeyDual4WithSalt+ph2
|
|
|
1929
2013
|
const ddlPos19 = migSrc19.indexOf('CREATE TABLE IF NOT EXISTS ph2_ladder');
|
|
1930
2014
|
const ddl19 = migSrc19.slice(ddlPos19, migSrc19.indexOf(';', ddlPos19) + 1);
|
|
1931
2015
|
db19.exec(ddl19.replace(/IF NOT EXISTS /g, ''));
|
|
2016
|
+
// 0012 對位面(t_87ef62dd):0011 上線後的索引修正 migration——0011 帶冗餘
|
|
2017
|
+
// idx_ph2_ladder_ph2(PK=ph2 同鍵 duplicate)且 account_id 零索引;0012 DROP+CREATE
|
|
2018
|
+
// 修正面在 fork 動線於 0011 之後執行(本段照序:建 0011 表+0011 原索引 → 0012)。
|
|
2019
|
+
{
|
|
2020
|
+
const idxPos19 = migSrc19.indexOf('CREATE INDEX IF NOT EXISTS idx_ph2_ladder_ph2');
|
|
2021
|
+
if (idxPos19 >= 0) db19.exec(migSrc19.slice(idxPos19, migSrc19.indexOf(';', idxPos19) + 1)); // 0011 冗餘索引原樣(0012 要 DROP 的缺陷面)
|
|
2022
|
+
const src12 = await srcOf('../migrations/0012-fix-ph2-ladder-index.sql');
|
|
2023
|
+
await A('[19] 0011 對位錨:冗餘 idx_ph2_ladder_ph2 在場(0012 DROP 面=0011 冗餘索引原樣)',
|
|
2024
|
+
idxPos19 >= 0 && src12.includes('DROP INDEX IF EXISTS idx_ph2_ladder_ph2'));
|
|
2025
|
+
const stmts12 = ['DROP INDEX IF EXISTS idx_ph2_ladder_ph2;', 'CREATE INDEX IF NOT EXISTS idx_ph2_ladder_account ON ph2_ladder(account_id);'];
|
|
2026
|
+
await A('[19] 0012 語句面:DROP 冗餘+CREATE account_id 兩句(報告原樣;DROP 在前——殘留冗餘即 RED)',
|
|
2027
|
+
src12.split('DROP INDEX IF EXISTS idx_ph2_ladder_ph2;').length === 2
|
|
2028
|
+
&& src12.split('CREATE INDEX IF NOT EXISTS idx_ph2_ladder_account ON ph2_ladder(account_id);').length === 2
|
|
2029
|
+
&& src12.indexOf('DROP INDEX') < src12.indexOf('CREATE INDEX'));
|
|
2030
|
+
await A('[19] 0012 非 UNIQUE 契約(一帳多舊值設計——ladder 多歷列並存;UNIQUE 面殘留即 RED)',
|
|
2031
|
+
/CREATE (UNIQUE )?INDEX/.test(src12) && !src12.includes('UNIQUE INDEX'));
|
|
2032
|
+
for (const st12 of stmts12) db19.exec(st12);
|
|
2033
|
+
const idxAfter = db19.prepare('SELECT name FROM sqlite_master WHERE type = \'index\' AND tbl_name = \'ph2_ladder\'').all() as { name: string }[];
|
|
2034
|
+
const idxNames = idxAfter.map((r) => r.name).filter((n) => n !== 'sqlite_autoindex_ph2_ladder_1');
|
|
2035
|
+
await A('[19] 0012 pragma 動線:冗餘 ph2 index 摘除+account_id index 在場(n 前後查——索引集恰 {idx_ph2_ladder_account})',
|
|
2036
|
+
idxNames.length === 1 && idxNames[0] === 'idx_ph2_ladder_account', JSON.stringify(idxNames));
|
|
2037
|
+
await A('[19] 0012 冪等:0012 語句重跑零炸(IF EXISTS/IF NOT EXISTS 面——fork 重入動線)',
|
|
2038
|
+
await (async () => { try { db19.exec(stmts12.join('\n')); return true; } catch { return false; } })());
|
|
2039
|
+
// EXPLAIN QUERY PLAN:account_id 查詢走 SEARCH(非 SCAN——修復收益的直接承載面);
|
|
2040
|
+
// ph2 查面走 autoindex(PK)=冗餘索引摘除後照 SEARCH。
|
|
2041
|
+
const planAcc = String((db19.prepare('EXPLAIN QUERY PLAN SELECT * FROM ph2_ladder WHERE account_id = ?').get('acc19') as { detail?: string } | null)?.detail ?? '');
|
|
2042
|
+
const planPh2 = String((db19.prepare('EXPLAIN QUERY PLAN SELECT account_id FROM ph2_ladder WHERE ph2 = ?').get('old-19') as { detail?: string } | null)?.detail ?? '');
|
|
2043
|
+
await A('[19] 0012 EXPLAIN 動線:account_id 查詢 SEARCH ph2_ladder (idx_ph2_ladder_account)(非 SCAN)',
|
|
2044
|
+
planAcc.includes('SEARCH ph2_ladder USING INDEX idx_ph2_ladder_account') && !/(^|\s)SCAN\s/.test(planAcc), planAcc);
|
|
2045
|
+
await A('[19] 0012 EXPLAIN PK 面:ph2 查詢仍 SEARCH autoindex(冗餘索引摘除零傷——非 SCAN)',
|
|
2046
|
+
planPh2.includes('SEARCH') && !/(^|\s)SCAN\s/.test(planPh2), planPh2);
|
|
2047
|
+
}
|
|
1932
2048
|
const env19 = {
|
|
1933
2049
|
DB: {
|
|
1934
2050
|
prepare(sql: string) {
|
|
@@ -2111,14 +2227,14 @@ console.log('\n[20] 復原套件專用前綴(wrapRec=jr1r. opt-in)+KEK_rec
|
|
|
2111
2227
|
await A('[20] 源碼窗:配對門接線形恰 1(wrapRec 或 recKekHkdf 在場即入門——單欄短路復活即形變異)',
|
|
2112
2228
|
noteCryptoSrc20.split('if (cfg.wrapRec || cfg.recKekHkdf) {').length === 2
|
|
2113
2229
|
&& 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);
|
|
2230
|
+
await A('[20] 源碼窗:HKDF 專責接線恰 1(兩欄齊備守衛+t_87ef62dd 快檢條件面——派生雙模/C2 借形復活/短路回歸即變異)',
|
|
2231
|
+
noteCryptoSrc20.split('if (cfg.wrapRec && cfg.recKekHkdf && wrapped.startsWith(cfg.wrapRec)) {').length === 2);
|
|
2116
2232
|
await A('[20] 源碼窗:PBKDF2 fallback 殘留歸零+deriveRecKek 呼叫面恰 3(非空洞計數:fallback 形歸零+def+兩呼叫端在場——C2 收口面)',
|
|
2117
2233
|
noteCryptoSrc20.split('if (!cfg.recKekHkdf) return deriveKek').length === 1
|
|
2118
2234
|
&& noteCryptoSrc20.split('deriveRecKek(').length === 4);
|
|
2119
2235
|
await A('[20] 源碼窗:deriveRecLegacyKek def+呼叫恰 3(split 帳=出現+1;legacy 本體單一真相——寫面+讀面同源)',
|
|
2120
2236
|
noteCryptoSrc20.split('deriveRecLegacyKek(').length === 4);
|
|
2121
|
-
await A('[20] 源碼窗:cfg.wrapRec 敘述形恰 2(配對門 XOR
|
|
2237
|
+
await A('[20] 源碼窗:cfg.wrapRec 敘述形恰 2(配對門 XOR 一對——借用面復活即計數變異;t_87ef62dd 快檢面零咬:腿接線是 startsWith 閉括號形非 XOR 敘述形)',
|
|
2122
2238
|
noteCryptoSrc20.split('cfg.wrapRec ||').length === 3
|
|
2123
2239
|
&& noteCryptoSrc20.split('!cfg.wrapRec ||').length === 2);
|
|
2124
2240
|
|
|
@@ -2277,6 +2393,298 @@ console.log('\n[20] 復原套件專用前綴(wrapRec=jr1r. opt-in)+KEK_rec
|
|
|
2277
2393
|
}
|
|
2278
2394
|
}
|
|
2279
2395
|
|
|
2396
|
+
// ── 21. CI 供應鏈面(npm ci --ignore-scripts+Dependabot runner 分流)──
|
|
2397
|
+
//
|
|
2398
|
+
// 外審 #1 的落地裁定(t_87ef62dd):public 密碼學套件 repo 的 CI 安裝不吃依賴腳本——
|
|
2399
|
+
// --ignore-scripts 是安裝面防線(postinstall/preinstall/prepare 全免執行);
|
|
2400
|
+
// Dependabot PR 走 GitHub-hosted runner 隔離(self-hosted runner 不接機器人分支)。
|
|
2401
|
+
// 靜態錨面:workflow YAML 本體的接線(job-if C1 母型+actor 分流+runs-on fromJSON 陣列形)。
|
|
2402
|
+
console.log('\n[21] CI 供應鏈面(npm ci --ignore-scripts+dependabot runner 分流)');
|
|
2403
|
+
{
|
|
2404
|
+
// 環境鍵=.github 目錄存在(存在性鍵非檔名硬讀——r1 MINOR-3):閘 src 檔在 files 白名單
|
|
2405
|
+
// 而 .github 不出包——tarball 消費樹硬讀 workflows=未守衛 ENOENT,rc=1 尾段全吞。
|
|
2406
|
+
// 目錄缺席=顯性 SKIP 行(消費端常態;[16]/[18] 毒化樹 pristine 基準由此同保線);
|
|
2407
|
+
// 目錄在場而檔案缺席=srcOf 硬 FAIL(repo 面牙齒不退位——軟讀文件名會把 repo 內
|
|
2408
|
+
// 摘 workflow 校準成靜默綠=掉牙)。
|
|
2409
|
+
const fs21 = (globalThis as unknown as {
|
|
2410
|
+
process?: { getBuiltinModule?: (id: string) => { existsSync?: (p: string | URL) => boolean } | undefined };
|
|
2411
|
+
}).process?.getBuiltinModule?.('node:fs');
|
|
2412
|
+
const ghDir21 = !!fs21?.existsSync && fs21.existsSync(new URL('../.github/', import.meta.url));
|
|
2413
|
+
if (ghDir21) {
|
|
2414
|
+
const srcVy21 = await srcOf('../.github/workflows/verify.yml');
|
|
2415
|
+
const srcPy21 = await srcOf('../.github/workflows/publish.yml');
|
|
2416
|
+
await A('[21] verify.yml:npm ci --ignore-scripts(安裝面防線——依賴腳本零在場)',
|
|
2417
|
+
srcVy21.includes('npm ci --ignore-scripts'));
|
|
2418
|
+
await A('[21] verify.yml:job-if 同 repo 保衛(C1 母型——fork/他 repo branch 不上 runner;dependabot 分支照跑不擋=分流入 RUN 面——r1 MAJOR-1)',
|
|
2419
|
+
/if: github\.event_name != 'pull_request' \|\| github\.event\.pull_request\.head\.repo\.full_name == github\.repository/.test(srcVy21));
|
|
2420
|
+
await A('[21] verify.yml:runs-on dependabot 分流=fromJSON 陣列形(actor→["ubuntu-latest"];fallback fromJSON 兩標籤組 ["self-hosted","journal-core"]——裸逗號字串 fallback=StringToken 單一 literal 標籤缺陷形零承載,r2 MAJOR-1)',
|
|
2421
|
+
/runs-on: \$\{\{ fromJSON\(github\.event_name == 'pull_request' && github\.actor == 'dependabot\[bot\]' && '\["ubuntu-latest"\]' \|\| '\["self-hosted","journal-core"\]'\) \}\}/.test(srcVy21));
|
|
2422
|
+
await A('[21] publish.yml:npm ci --ignore-scripts(tag 驅動無 actor 面;typecheck+verify 先行=腳本面零在場)',
|
|
2423
|
+
srcPy21.includes('npm ci --ignore-scripts'));
|
|
2424
|
+
} else {
|
|
2425
|
+
await A('[21] CI 源碼面:.github 缺席環境(tarball 發行樹常態)=顯性 SKIP(workflow 斷言 4 收斂為 1 SKIP 行——消費端計數帳 475−3=472)',
|
|
2426
|
+
true, 'no .github dir — consumption tree face');
|
|
2427
|
+
}
|
|
2428
|
+
// 摘除自證(t_760f44e8 治理掃蕩補漏;fs 原語缺席環境=顯性 FAIL 形自守衛——非 silent true)
|
|
2429
|
+
await A('[21] open-card-pr.sh 摘除(主機路徑+GITHUB_TOKEN 線索面;fs 原語缺席=顯性 FAIL 自守衛)',
|
|
2430
|
+
!!fs21?.existsSync && !fs21.existsSync(new URL('../scripts/open-card-pr.sh', import.meta.url)));
|
|
2431
|
+
// NIT-1 自證(isBrokenLink repo-root 錨定):根錨路徑在場形(本 repo 常態 @scure 在場)
|
|
2432
|
+
// +CWD 無關面(probe 眼:lstat 走 import.meta.url 起手——cwd=任意時仍指 repo)。
|
|
2433
|
+
// 缺陷形(裸 '@scure/bip39' 殘留)=源碼負向咬(regex 敘述形)。
|
|
2434
|
+
{
|
|
2435
|
+
const gateSrc21 = await srcOf('../scripts/verify-core-crypto.ts');
|
|
2436
|
+
await A('[21] NIT-1:isBrokenLink 路徑錨 = import.meta.url 起手(repo root 錨定——CWD 相對 lstat 誤報面收口)',
|
|
2437
|
+
/const repoRootRef = new URL\('\.\.\/node_modules\/@scure\/bip39', import\.meta\.url\)/.test(gateSrc21));
|
|
2438
|
+
await A('[21] NIT-1:裸 CWD 相對 lstat 殘留歸零(缺陷形負向——lstatSync\(\'@scure 直接形零殘留)',
|
|
2439
|
+
!/lstatSync\('@scure\/bip39'\)/.test(gateSrc21) && !/existsSync\('@scure\/bip39'\)/.test(gateSrc21));
|
|
2440
|
+
await A('[21] NIT-1:isBrokenLink 判準本體在場(lstat isSymbolicLink && !existsSync——斷鏈 vs 真缺席分流保線)',
|
|
2441
|
+
/isSymbolicLink\(\) && !fsMod\.existsSync\(repoRootRef\)/.test(gateSrc21));
|
|
2442
|
+
await A('[21] r2 MINOR-1:fs 直讀守衛=URL 物件直傳(existsSync .github 在場面+srcOf readFileSync rel 面——file: URL 原生接線,零 .pathname 中介)',
|
|
2443
|
+
/fs21\.existsSync\(new URL\('\.\.\/\.github\/', import\.meta\.url\)\)/.test(gateSrc21)
|
|
2444
|
+
&& /fs\.readFileSync\(new URL\(rel, import\.meta\.url\), 'utf8'\)/.test(gateSrc21));
|
|
2445
|
+
await A('[21] r2 MINOR-1:fs 原語直讀 .pathname 形零殘留(缺陷形負向——URL %編碼失真→existsSync 恆 false→SKIP 分枝靜默退位面收口)',
|
|
2446
|
+
!/existsSync\(new URL\([^)]*\)\.pathname\)/.test(gateSrc21)
|
|
2447
|
+
&& !/readFileSync\(new URL\([^)]*\)\.pathname/.test(gateSrc21));
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
console.log('\n[23] loginRouteCore ladder 查表守衛(外審 #8 幽靈帳——現值 miss → ladder → 兩面 miss 才 createUser)');
|
|
2452
|
+
{
|
|
2453
|
+
// getBuiltin23:node 內建模組動態存取(本段自備 helper——[19]/[21] 同構;structured type 零 node types 依賴)
|
|
2454
|
+
const getBuiltin23 = (id: string): unknown =>
|
|
2455
|
+
(globalThis as unknown as { process?: { getBuiltinModule?: (i: string) => unknown } }).process?.getBuiltinModule?.(id);
|
|
2456
|
+
const { DatabaseSync } = (getBuiltin23('node:sqlite') as { DatabaseSync?: unknown } | undefined) ?? {};
|
|
2457
|
+
if (!DatabaseSync) throw new Error('ERR_SQLITE_UNAVAILABLE');
|
|
2458
|
+
const { loginRouteCore } = await import('../src/server/auth.ts');
|
|
2459
|
+
const enc23 = new TextEncoder();
|
|
2460
|
+
const hex23 = (u8: Uint8Array): string => Array.from(u8).map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
2461
|
+
const ncCrypto23 = (getBuiltin23('node:crypto') as { createHash?: (a: string) => { update: (s: string) => { digest: (e: string) => string } } } | undefined) ?? { createHash: () => { throw new Error('ERR_CRYPTO_UNAVAILABLE'); } };
|
|
2462
|
+
const sha256Sync23 = (s: string): string => ncCrypto23.createHash!('sha256').update(s).digest('hex');
|
|
2463
|
+
const sha23 = async (s: string): Promise<string> => hex23(new Uint8Array(await crypto.subtle.digest('SHA-256', enc23.encode(s))));
|
|
2464
|
+
// PH1 hex64 樣本(直入 login body):loginRouteCore 對 body 做 sha256Hex=ph2——fixture 種子
|
|
2465
|
+
// 必須種 sha256(ph1)(雜湊後落表);種 ph1 本身進 ph2 欄=miss by construction(種錯層)。
|
|
2466
|
+
const ph1a23 = sha256Sync23('jr23-current-ph1');
|
|
2467
|
+
const ph1b23 = sha256Sync23('jr23-old-ph1');
|
|
2468
|
+
const ph1c23 = sha256Sync23('jr23-ghost-ph1');
|
|
2469
|
+
const ph1d23 = sha256Sync23('jr23-nolad-ph1');
|
|
2470
|
+
const ph1e23 = sha256Sync23('jr23-failo-ph1');
|
|
2471
|
+
const ph1x23 = sha256Sync23('jr23-sessi-ph1');
|
|
2472
|
+
const rowA23 = sha256Sync23(ph1a23);
|
|
2473
|
+
const row23old = sha256Sync23('jr23-v3cur-ph1');
|
|
2474
|
+
const row23x = sha256Sync23(ph1x23);
|
|
2475
|
+
|
|
2476
|
+
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 } => {
|
|
2477
|
+
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:');
|
|
2478
|
+
db.exec('CREATE TABLE login_rate (ip TEXT PRIMARY KEY, window_start INTEGER, count INTEGER)');
|
|
2479
|
+
db.exec('CREATE TABLE users (account_id TEXT PRIMARY KEY, ph2 TEXT UNIQUE, salt TEXT, wrapped_key TEXT, wrapped_rec TEXT, rec_hash TEXT)');
|
|
2480
|
+
return {
|
|
2481
|
+
db,
|
|
2482
|
+
env23: {
|
|
2483
|
+
DB: {
|
|
2484
|
+
prepare: (sql: string) => ({
|
|
2485
|
+
bind: (...params: unknown[]) => ({
|
|
2486
|
+
run: async () => {
|
|
2487
|
+
let results: unknown[] = [];
|
|
2488
|
+
try { results = db.prepare(sql).all(...params) as unknown[]; }
|
|
2489
|
+
catch { try { db.prepare(sql).run(...params); } catch {} }
|
|
2490
|
+
return { results, meta: {} };
|
|
2491
|
+
},
|
|
2492
|
+
}),
|
|
2493
|
+
}),
|
|
2494
|
+
},
|
|
2495
|
+
} as unknown as never,
|
|
2496
|
+
};
|
|
2497
|
+
};
|
|
2498
|
+
const USER_COLS23 = 'account_id AS userKey, ph2, salt, wrapped_key AS wrapped, wrapped_rec AS wrappedRec, rec_hash AS recHash';
|
|
2499
|
+
|
|
2500
|
+
// ① 現值 hit:單查命中,ladder 零呼叫零建號(現值 hit 短路在 findByIdentityQuery)
|
|
2501
|
+
{
|
|
2502
|
+
const t = mkDb23();
|
|
2503
|
+
const calls23 = { ladder: 0, create: 0 };
|
|
2504
|
+
const store23 = {
|
|
2505
|
+
findByIdentityQuery: async (ph2: string) => (t.db.prepare(`SELECT ${USER_COLS23} FROM users WHERE ph2 = ?`).get(ph2) as unknown) || null,
|
|
2506
|
+
createUser: async () => { calls23.create++; return 'acct-ghost-23'; },
|
|
2507
|
+
getByUserKey: async () => null,
|
|
2508
|
+
revokeAllSessions: async () => {},
|
|
2509
|
+
insertSession: async () => {},
|
|
2510
|
+
ladderLookup: async () => { calls23.ladder++; return null; },
|
|
2511
|
+
};
|
|
2512
|
+
t.db.prepare("INSERT INTO users (account_id, ph2, salt, wrapped_key) VALUES ('acct-23a', ?, 's23', 'wr23')").run(rowA23);
|
|
2513
|
+
const res23 = await loginRouteCore(t.env23 as never, store23 as never, 'ip-23-1', ph1a23);
|
|
2514
|
+
const j23 = JSON.parse(await res23.text()) as { userKey: string; status: string };
|
|
2515
|
+
await A('[23] 現值 hit 回 200 ready(userKey 恰 acct-23a——登入 body 相容現值契約)', res23.status === 200 && j23.status === 'ready' && j23.userKey === 'acct-23a');
|
|
2516
|
+
await A('[23] 現值 hit:ladderLookup 零呼叫(短路在 findByIdentityQuery)', calls23.ladder === 0);
|
|
2517
|
+
await A('[23] 現值 hit:createUser 零呼叫(零幽靈面)', calls23.create === 0);
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
// ② ladder hit(現值 miss)=回舊帳不建幽靈;getByUserKey 讀現值(ph2Kind 語意照 ladder.ts);
|
|
2521
|
+
// session 歸宿舊帳 userKey
|
|
2522
|
+
{
|
|
2523
|
+
const t = mkDb23();
|
|
2524
|
+
const calls23 = { ladder: 0, create: 0, byKey: [] as string[] };
|
|
2525
|
+
let sess23: { tokenHash: string; userKey: string; expiresAt: number } | null = null;
|
|
2526
|
+
const store23 = {
|
|
2527
|
+
findByIdentityQuery: async (ph2: string) => (t.db.prepare(`SELECT ${USER_COLS23} FROM users WHERE ph2 = ?`).get(ph2) as unknown) || null,
|
|
2528
|
+
createUser: async () => { calls23.create++; return 'acct-ghost-23'; },
|
|
2529
|
+
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; },
|
|
2530
|
+
revokeAllSessions: async () => {},
|
|
2531
|
+
insertSession: async (tokenHash: string, userKey: string, expiresAt: number) => { sess23 = { tokenHash, userKey, expiresAt }; },
|
|
2532
|
+
ladderLookup: async () => { calls23.ladder++; return 'acct-23old'; },
|
|
2533
|
+
};
|
|
2534
|
+
t.db.prepare("INSERT INTO users (account_id, ph2, salt, wrapped_key) VALUES ('acct-23old', ?, 's23', 'wr23')").run(row23old);
|
|
2535
|
+
const res23 = await loginRouteCore(t.env23 as never, store23 as never, 'ip-23-2', ph1b23);
|
|
2536
|
+
const j23 = JSON.parse(await res23.text()) as { userKey: string; status: string };
|
|
2537
|
+
await A('[23] ladder hit → userKey=舊帳 acct-23old(不建幽靈)', j23.userKey === 'acct-23old' && calls23.create === 0);
|
|
2538
|
+
await A('[23] ladder hit:ladderLookup 恰 1 呼叫(守衛接線在場)', calls23.ladder === 1);
|
|
2539
|
+
await A('[23] ladder hit 語意照 ladder.ts:回帳走 getByUserKey(現值面承載——非表列直接回)', calls23.byKey.length === 1 && calls23.byKey[0] === 'acct-23old');
|
|
2540
|
+
await A('[23] ladder hit → ready 分流(帳戶 ph2 在場)', j23.status === 'ready');
|
|
2541
|
+
await A('[23] ladder hit → session 落在舊帳 userKey(tokenSha256+30 天)', sess23 !== null && (sess23 as { userKey: string }).userKey === 'acct-23old'
|
|
2542
|
+
&& /^[0-9a-f]{64}$/.test((sess23 as { tokenHash: string }).tokenHash)
|
|
2543
|
+
&& (sess23 as { expiresAt: number }).expiresAt > Date.now() + 29 * 86400e3);
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
// ③ 兩面 miss → 建 1 幽靈(ph2 恆等=sha256(登入 PH1))
|
|
2547
|
+
{
|
|
2548
|
+
const t = mkDb23();
|
|
2549
|
+
let inserted23: string | null = null;
|
|
2550
|
+
const calls23 = { ladder: 0 };
|
|
2551
|
+
const store23 = {
|
|
2552
|
+
findByIdentityQuery: async (ph2: string) => (t.db.prepare(`SELECT ${USER_COLS23} FROM users WHERE ph2 = ?`).get(ph2) as unknown) || null,
|
|
2553
|
+
createUser: async (ph2: string) => { inserted23 = ph2; return 'acct-ghost-23'; },
|
|
2554
|
+
getByUserKey: async () => null,
|
|
2555
|
+
revokeAllSessions: async () => {},
|
|
2556
|
+
insertSession: async () => {},
|
|
2557
|
+
ladderLookup: async () => { calls23.ladder++; return null; },
|
|
2558
|
+
};
|
|
2559
|
+
const res23 = await loginRouteCore(t.env23 as never, store23 as never, 'ip-23-3', ph1c23);
|
|
2560
|
+
const j23 = JSON.parse(await res23.text()) as { userKey: string; status: string };
|
|
2561
|
+
await A('[23] 兩面 miss → 建 1 幽靈(createUser 恰 1——幽靈語意照舊)', j23.userKey === 'acct-ghost-23');
|
|
2562
|
+
await A('[23] 幽靈 ph2 = sha256Hex(登入 PH1)(subtle 交叉驗——種子層對帳)', inserted23 !== null && inserted23 === await sha23(ph1c23));
|
|
2563
|
+
await A('[23] 兩面 miss:ladderLookup 有呼叫(配置在場=守衛有接線)+require_binding 分流', calls23.ladder === 1 && j23.status === 'require_binding');
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
// ④ lookup 未配置(optional undefined)=0.2.3 單查行為零變(ladder 零呼叫——向後相容鐵律)
|
|
2567
|
+
{
|
|
2568
|
+
const t = mkDb23();
|
|
2569
|
+
const calls23 = { create: 0 };
|
|
2570
|
+
const store23 = {
|
|
2571
|
+
findByIdentityQuery: async (ph2: string) => (t.db.prepare(`SELECT ${USER_COLS23} FROM users WHERE ph2 = ?`).get(ph2) as unknown) || null,
|
|
2572
|
+
createUser: async () => { calls23.create++; return 'acct-ghost-23'; },
|
|
2573
|
+
getByUserKey: async () => null,
|
|
2574
|
+
revokeAllSessions: async () => {},
|
|
2575
|
+
insertSession: async () => {},
|
|
2576
|
+
};
|
|
2577
|
+
const res23 = await loginRouteCore(t.env23 as never, store23 as never, 'ip-23-4', ph1d23);
|
|
2578
|
+
const j23 = JSON.parse(await res23.text()) as { userKey: string };
|
|
2579
|
+
await A('[23] ladderLookup 未配置 → 直接建幽靈(0.2.3 單查行為恆等——optional 退場鐵律)', j23.userKey === 'acct-ghost-23' && calls23.create === 1);
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
// ⑤ ladder store 拋錯(表缺席)=fail-open 視同 miss,不炸登入
|
|
2583
|
+
{
|
|
2584
|
+
const t = mkDb23();
|
|
2585
|
+
let createCalls23 = 0;
|
|
2586
|
+
const store23 = {
|
|
2587
|
+
findByIdentityQuery: async (ph2: string) => (t.db.prepare(`SELECT ${USER_COLS23} FROM users WHERE ph2 = ?`).get(ph2) as unknown) || null,
|
|
2588
|
+
createUser: async () => { createCalls23++; return 'acct-ghost-23'; },
|
|
2589
|
+
getByUserKey: async () => null,
|
|
2590
|
+
revokeAllSessions: async () => {},
|
|
2591
|
+
insertSession: async () => {},
|
|
2592
|
+
ladderLookup: async () => { throw new Error('no such table: ph2_ladder'); },
|
|
2593
|
+
};
|
|
2594
|
+
const res23 = await loginRouteCore(t.env23 as never, store23 as never, 'ip-23-5', ph1e23);
|
|
2595
|
+
const j23 = JSON.parse(await res23.text()) as { userKey: string };
|
|
2596
|
+
await A('[23] ladder 拋錯 fail-open=登入不炸、續建幽靈(tacet store.ts 母型上位)', res23.status === 200 && j23.userKey === 'acct-ghost-23' && createCalls23 === 1);
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
// ⑥ ladder hit:帳戶現值已旋轉(ph2 異值欄面)——status 分流照 user?.ph2 實值
|
|
2600
|
+
{
|
|
2601
|
+
const t = mkDb23();
|
|
2602
|
+
const store23 = {
|
|
2603
|
+
findByIdentityQuery: async (ph2: string) => (t.db.prepare(`SELECT ${USER_COLS23} FROM users WHERE ph2 = ?`).get(ph2) as unknown) || null,
|
|
2604
|
+
createUser: async () => 'acct-ghost-23',
|
|
2605
|
+
getByUserKey: async (key: string) => (t.db.prepare(`SELECT ${USER_COLS23} FROM users WHERE account_id = ?`).get(key) as unknown) || null,
|
|
2606
|
+
revokeAllSessions: async () => {},
|
|
2607
|
+
insertSession: async () => {},
|
|
2608
|
+
ladderLookup: async () => 'acct-23old',
|
|
2609
|
+
};
|
|
2610
|
+
t.db.prepare("INSERT INTO users (account_id, ph2, salt, wrapped_key) VALUES ('acct-23old', 'ph2-rotated-current-23', 's23', 'wr23')").run();
|
|
2611
|
+
const res23 = await loginRouteCore(t.env23 as never, store23 as never, 'ip-23-6', ph1b23);
|
|
2612
|
+
const j23 = JSON.parse(await res23.text()) as { userKey: string; status: string };
|
|
2613
|
+
await A('[23] ladder hit(現值已旋轉異值欄)→ userKey=舊帳、status=ready(user?.ph2 在場分流本體契約)', j23.userKey === 'acct-23old' && j23.status === 'ready');
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
// ⑦ 毒化(拷貝樹 in-gate,t_0ab6e760 母型):ladderGuard 接線摘除 → ladder hit 案翻建幽靈。
|
|
2617
|
+
// 毒化跑在 /tmp 拷貝樹=本 repo 樹 byte 不動;還原 byte-exact+樹出生即棄。
|
|
2618
|
+
// POISON_GATE_INNER sentinel([16]/[18]/[20] 遞迴防線母型):內層跑份自我跳過毒化節。
|
|
2619
|
+
{
|
|
2620
|
+
const inner23 = (getBuiltin23('node:process') as { env?: Record<string, string | undefined> } | undefined)?.env?.POISON_GATE_INNER === '1';
|
|
2621
|
+
if (inner23) {
|
|
2622
|
+
await A('[23] 毒化證據:內層跑份自我跳過(POISON_GATE_INNER sentinel——遞迴防線母型)', true, 'POISON_GATE_INNER=1');
|
|
2623
|
+
} else {
|
|
2624
|
+
const fs23 = (getBuiltin23('node:fs') as {
|
|
2625
|
+
mkdtempSync?: (p: string) => string;
|
|
2626
|
+
mkdirSync?: (p: string, o?: unknown) => void;
|
|
2627
|
+
copyFileSync?: (a: string | URL, b: string) => void;
|
|
2628
|
+
readdirSync?: (p: string | URL) => string[];
|
|
2629
|
+
readFileSync?: (p: string | URL, e: string) => string;
|
|
2630
|
+
writeFileSync?: (p: string, s: string, e: string) => void;
|
|
2631
|
+
rmSync?: (p: string, o?: unknown) => void;
|
|
2632
|
+
} | undefined) ?? undefined;
|
|
2633
|
+
const path23x = (getBuiltin23('node:path') as { join?: (...p: string[]) => string } | undefined) ?? undefined;
|
|
2634
|
+
const os23 = (getBuiltin23('node:os') as { tmpdir?: () => string } | undefined) ?? undefined;
|
|
2635
|
+
const url23 = (getBuiltin23('node:url') as { pathToFileURL?: (p: string) => { href: string } } | undefined) ?? undefined;
|
|
2636
|
+
if (!fs23?.mkdtempSync || !path23x?.join || !os23?.tmpdir || !url23?.pathToFileURL) {
|
|
2637
|
+
// 原語缺席環境=顯性 SKIP 行(非 silent true——t_87ef62dd 環境鍵母型)
|
|
2638
|
+
await A('[23] 毒化面(原語缺席環境)=顯性 SKIP 行,毒化證據缺席顯形非 silent true',
|
|
2639
|
+
false, 'node fs/path/os/url 原語缺席——毒化面 skip(消費端精簡 Node 形;repo 樹常態不見此行)');
|
|
2640
|
+
} else {
|
|
2641
|
+
// 拷貝樹源=本閘的套件根(import.meta.url 起手——repo 樹與消費樹同構,零硬編碼路徑)
|
|
2642
|
+
const pkgRootUrl23 = new URL('../', import.meta.url);
|
|
2643
|
+
const srcDirUrl23 = new URL('src/server/', pkgRootUrl23);
|
|
2644
|
+
const tree23 = fs23.mkdtempSync!(path23x.join(os23.tmpdir(), 't23-ladder-'));
|
|
2645
|
+
fs23.mkdirSync!(path23x.join(tree23, 'src/server'), { recursive: true });
|
|
2646
|
+
const filesSer23 = fs23.readdirSync!(srcDirUrl23).filter((f) => f.endsWith('.ts'));
|
|
2647
|
+
for (const f of filesSer23) fs23.copyFileSync!(new URL('src/server/' + f, pkgRootUrl23), path23x.join(tree23, 'src/server', f));
|
|
2648
|
+
await A('[23] 毒化樹拷貝:src/server 全檔在場(auth.ts+依賴鏈四檔≥5)', filesSer23.length >= 5 && filesSer23.includes('auth.ts'), 'n=' + filesSer23.length);
|
|
2649
|
+
// 毒化錨(唯一):接線面 `?? (await ladderGuard(store, ph2))` 摘除 → 單查直路
|
|
2650
|
+
//(removal-form——毒面=ladder hit 案翻建幽靈;未配置案照綠=行為面恰此面翻)。
|
|
2651
|
+
const needle23 = '(await store.findByIdentityQuery(ph2)) ?? (await ladderGuard(store, ph2))';
|
|
2652
|
+
const srcAuth23 = fs23.readFileSync!(new URL('src/server/auth.ts', pkgRootUrl23), 'utf8');
|
|
2653
|
+
const cnt23 = srcAuth23.split(needle23).length - 1;
|
|
2654
|
+
await A('[23] 毒化錨恰 1(split 計數——接線形單點承載)', cnt23 === 1, 'cnt=' + cnt23);
|
|
2655
|
+
if (cnt23 === 1) {
|
|
2656
|
+
fs23.writeFileSync!(path23x.join(tree23, 'src/server/auth.ts'), srcAuth23.split(needle23).join('await store.findByIdentityQuery(ph2)'), 'utf8');
|
|
2657
|
+
try {
|
|
2658
|
+
const modP = (await import(url23.pathToFileURL(path23x.join(tree23, 'src/server/auth.ts')).href + '?poison23=1')) as unknown as { loginRouteCore: typeof loginRouteCore };
|
|
2659
|
+
const tP = mkDb23();
|
|
2660
|
+
const storeP = {
|
|
2661
|
+
findByIdentityQuery: async () => null,
|
|
2662
|
+
createUser: async () => 'acct-ghost-23',
|
|
2663
|
+
getByUserKey: async () => null,
|
|
2664
|
+
revokeAllSessions: async () => {},
|
|
2665
|
+
insertSession: async () => {},
|
|
2666
|
+
ladderLookup: async () => 'acct-23old',
|
|
2667
|
+
};
|
|
2668
|
+
tP.db.prepare("INSERT INTO users (account_id, ph2, salt, wrapped_key) VALUES ('acct-23old', ?, 's23', 'wr23')").run(row23old);
|
|
2669
|
+
const resP = await modP.loginRouteCore(tP.env23 as never, storeP as never, 'ip-23-P', ph1b23);
|
|
2670
|
+
const jP = JSON.parse(await resP.text()) as { userKey: string; status: string };
|
|
2671
|
+
await A('[23] 毒化(ladderGuard 接線摘除)→ ladder hit 案翻建幽靈(行為面真翻——designated)',
|
|
2672
|
+
jP.userKey === 'acct-ghost-23', 'userKey=' + jP.userKey);
|
|
2673
|
+
} catch (e) {
|
|
2674
|
+
await A('[23] 毒化重演(行為面真翻)', false, 'crash: ' + String((e as Error).message).slice(0, 160));
|
|
2675
|
+
}
|
|
2676
|
+
fs23.writeFileSync!(path23x.join(tree23, 'src/server/auth.ts'), srcAuth23, 'utf8');
|
|
2677
|
+
const restored23 = fs23.readFileSync!(path23x.join(tree23, 'src/server/auth.ts'), 'utf8') === srcAuth23;
|
|
2678
|
+
fs23.rmSync!(tree23, { recursive: true, force: true });
|
|
2679
|
+
await A('[23] 毒化還原 byte-exact+樹出生即棄(本 repo 樹零接觸——零殘毒面)', restored23);
|
|
2680
|
+
} else {
|
|
2681
|
+
fs23.rmSync!(tree23, { recursive: true, force: true });
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2280
2688
|
console.log(`\n${passed} 斷言全綠` + (failures.length ? `;${failures.length} 失敗` : ''));
|
|
2281
2689
|
if (failures.length) {
|
|
2282
2690
|
console.error('失敗項:', failures);
|
package/src/client/argon2.ts
CHANGED
|
@@ -249,12 +249,14 @@ export async function wrapNoteKey3(cfg: Argon3Config, noteKey: CryptoKey, passph
|
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
/** jr3w. 解包:任何不符(前綴/salt 形/長度/AAD)回 null 不拋;成功 → noteKey extractable=true(鐵律 4)。
|
|
252
|
+
* 快速前綴快檢在場(deriveKekArgon 前——t_87ef62dd;unwrapNoteKey4 母型):錯前綴 junk 免付 Argon。
|
|
252
253
|
* 收口 openNoteKey:嚴格 92B+rawHex 形+hex fail-closed 全在共用核心;本函式只做
|
|
253
254
|
* 家族守衛(wrap3 未配置回 null)、salt1 hex-形檢查與 KEK 派生——payload 形是鹽外置家族(iv‖ct),
|
|
254
255
|
* own 92B 檢已隨收口摘除(jr3s 同;鹽內嵌族 own 108B 保留)。 */
|
|
255
256
|
export async function unwrapNoteKey3(cfg: Argon3Config, wrapped: string, passphrase: string, saltHex: string): Promise<CryptoKey | null> {
|
|
256
257
|
try {
|
|
257
258
|
if (!cfg.wrap3) return null;
|
|
259
|
+
if (!wrapped.startsWith(cfg.wrap3)) return null; // 快速前綴快檢(t_87ef62dd):錯前綴 junk 免付 64 MiB Argon 成本
|
|
258
260
|
const salt = hexToBytes(saltHex);
|
|
259
261
|
if (salt.length !== SALT_LEN || !/^[0-9a-f]{32}$/.test(saltHex)) return null;
|
|
260
262
|
const kek = await deriveKekArgon(passphrase, salt);
|
|
@@ -478,11 +480,13 @@ export async function wrapNoteKeyShare3(cfg: Argon3Config, noteKey: CryptoKey, s
|
|
|
478
480
|
}
|
|
479
481
|
|
|
480
482
|
/** jr3s. 解包:salt 取自 GET /shares/:hash 回應(與 jr1w 同形);任何不符回 null,不拋。
|
|
483
|
+
* 快速前綴快檢在場(deriveKekArgon 前——t_87ef62dd;unwrapNoteKey4 母型):錯前綴 junk 免付 Argon。
|
|
481
484
|
* 收口 openNoteKey:鹽外置家族(payload 嚴格 92B+rawHex 形+hex fail-closed
|
|
482
485
|
* 全在共用核心);本函式只做家族守衛、salt hex-形檢查與 KEK 派生——own 92B 檢已隨收口摘除。 */
|
|
483
486
|
export async function unwrapNoteKeyShare3(cfg: Argon3Config, wrapped: string, sharePass: string, saltHex: string): Promise<CryptoKey | null> {
|
|
484
487
|
try {
|
|
485
488
|
if (!cfg.wrapShare3) return null;
|
|
489
|
+
if (!wrapped.startsWith(cfg.wrapShare3)) return null; // 快速前綴快檢(t_87ef62dd):錯前綴 junk 免付 64 MiB Argon 成本
|
|
486
490
|
const salt = hexToBytes(saltHex);
|
|
487
491
|
if (salt.length !== SALT_LEN || !/^[0-9a-f]{32}$/.test(saltHex)) return null;
|
|
488
492
|
const kek = await deriveKekArgon(sharePass, salt);
|
|
@@ -439,6 +439,7 @@ export async function wrapNoteKey(cfg: NoteCryptoConfig, noteKey: CryptoKey, pas
|
|
|
439
439
|
export async function unwrapNoteKey(cfg: NoteCryptoConfig, wrapped: string, passphrase: string, saltHex: string): Promise<CryptoKey | null> {
|
|
440
440
|
try {
|
|
441
441
|
if (!cfg.wrap) return null;
|
|
442
|
+
if (!wrapped.startsWith(cfg.wrap)) return null; // 快速前綴快檢(t_87ef62dd):錯前綴 junk 免付 600k PBKDF2 成本
|
|
442
443
|
const salt = hexToBytes(saltHex);
|
|
443
444
|
if (salt.length !== 16 || !HEX32_RE.test(saltHex)) return null;
|
|
444
445
|
const kek = await deriveKek(passphrase, salt);
|
|
@@ -468,6 +469,7 @@ export async function wrapNoteKeyShare(cfg: NoteCryptoConfig, noteKey: CryptoKey
|
|
|
468
469
|
export async function unwrapNoteKeyShare(cfg: NoteCryptoConfig, wrapped: string, sharePass: string, saltHex: string): Promise<CryptoKey | null> {
|
|
469
470
|
try {
|
|
470
471
|
if (!cfg.wrapShare) return null;
|
|
472
|
+
if (!wrapped.startsWith(cfg.wrapShare)) return null; // 快速前綴快檢(t_87ef62dd):錯前綴 junk 免付 600k PBKDF2 成本
|
|
471
473
|
const salt = hexToBytes(saltHex);
|
|
472
474
|
if (salt.length !== 16 || !HEX32_RE.test(saltHex)) return null;
|
|
473
475
|
const kek = await deriveKek(sharePass, salt);
|
|
@@ -525,7 +527,10 @@ export async function unwrapNoteKeyWithRecToken(cfg: NoteCryptoConfig, wrapped:
|
|
|
525
527
|
try {
|
|
526
528
|
// 次序契約:jr1r. 面只在兩欄齊備時試(HKDF 專責派生——cfg.wrap 面零 HKDF 腿,
|
|
527
529
|
// jr1w. 前綴只 carrying passphrase-PBKDF2 派生:「一個前綴一份契約」第二違例收口)。
|
|
528
|
-
|
|
530
|
+
// 快速前綴快檢(t_87ef62dd):jr1r. 腿接線=條件面(錯前綴跳過 HKDF 腿)——
|
|
531
|
+
// 禁 return null 形(會短路「新世代讀舊寫新」的舊面回落雙試=[20] 過渡保護契約);
|
|
532
|
+
// 舊面是終面(回落後無再退)=return null 快檢形恰當(錯前綴免付 600k PBKDF2)。
|
|
533
|
+
if (cfg.wrapRec && cfg.recKekHkdf && wrapped.startsWith(cfg.wrapRec)) {
|
|
529
534
|
const recKek = await deriveRecKek(cfg, recToken);
|
|
530
535
|
const fresh = await openNoteKey(wrapped, recKek, 'notekey-rec', cfg.wrapRec);
|
|
531
536
|
if (fresh) return fresh;
|
|
@@ -533,6 +538,7 @@ export async function unwrapNoteKeyWithRecToken(cfg: NoteCryptoConfig, wrapped:
|
|
|
533
538
|
// 舊面回落(過渡保護):cfg.wrap 在場即試(部分配置讀面照走——寫面已被配對門
|
|
534
539
|
// 拒,讀面寬容=舊 blob 永遠可解的承載面)。
|
|
535
540
|
if (!cfg.wrap) return null;
|
|
541
|
+
if (!wrapped.startsWith(cfg.wrap)) return null; // 快速前綴快檢:錯前綴 junk 免付 600k PBKDF2 成本
|
|
536
542
|
const legacyKek = await deriveRecLegacyKek(cfg, recToken, identity);
|
|
537
543
|
return await openNoteKey(wrapped, legacyKek, 'notekey', cfg.wrap);
|
|
538
544
|
} catch {
|
package/src/server/auth.ts
CHANGED
|
@@ -8,14 +8,16 @@
|
|
|
8
8
|
* - 入庫前密文/包裹格式驗證(inboundCipher/isCipherFor/validWrappedKey/
|
|
9
9
|
* pickKeyPackage 等,前綴由 config 注入)。
|
|
10
10
|
* - /auth/login 核心動線(loginRouteCore):rate-limit 閘 → PH1 驗形 →
|
|
11
|
-
* PH2 → store
|
|
11
|
+
* PH2 → store 查詢(現值 miss → ladder 查表守衛——0.2.4 內建,optional 未配置
|
|
12
|
+
* 即退場)/建號 → session token 簽發(30 天)。
|
|
12
13
|
*
|
|
13
14
|
* 呼叫端(fork)職責——本檔只定義介面,機制在呼叫端實作:
|
|
14
15
|
* - identity 生成:AuthStore.createUser 由各 fork 自決(隨機 account_id 或
|
|
15
16
|
* 客戶端帶入語意皆可);PH2 UNIQUE 衝突(兩個同時首登同密語)是 store
|
|
16
17
|
* 責任——tacet 參考實作:UNIQUE 失敗重查既有列回 userKey(非 5xx)。
|
|
17
18
|
* - hash-ladder 雙軌(舊式 PH1 直比 → 升級寫回 PH2)與密語重設後的 session
|
|
18
|
-
* 撤銷時機:fork 呼叫端接線(revokeAllSessions
|
|
19
|
+
* 撤銷時機:fork 呼叫端接線(revokeAllSessions 介面已備);ladder 查表守衛已
|
|
20
|
+
* 內建 loginRouteCore(optional——fork 只需配 ladderLookup+0011 migration,接線零寫)。
|
|
19
21
|
* - 常數時間比較 timingSafeEq:./hash.ts 匯出(login 動線經 PH2 查詢不直比)。
|
|
20
22
|
*
|
|
21
23
|
* 不變量(勿破壞):
|
|
@@ -136,9 +138,16 @@ export interface AuthStore {
|
|
|
136
138
|
/** v3 重鑰:舊值入 ladder 表+ph2/wrapped/salt/wrappedRec/rec_hash
|
|
137
139
|
* 五欄同列覆蓋(rec 欄在場才覆蓋——四欄形契約面不動;fork 實作三語句同批寫入)。 */
|
|
138
140
|
rekeyWithLadder?(userKey: string, oldPh2: string, oldKind: 'legacy' | 'v2', ph2: string, pkg: KeyPackage, recPkg: string | null, recHash: string | null): Promise<void>;
|
|
139
|
-
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
141
|
+
/**
|
|
142
|
+
* ladder 查表:以「舊 ph2 值」(v2/legacy 形)查 ladder 表,命中回帳戶 id。
|
|
143
|
+
* loginRouteCore 內建接線(0.2.4 外審 #8 幽靈守衛上位):現值 miss → ladderLookup →
|
|
144
|
+
* 命中=回舊帳(不建幽靈;ph2Kind 語意照 ladder.ts 檔頭註解——core 端回帳序:
|
|
145
|
+
* ladder hit → getByUserKey → 可續 buildSession),兩面皆 miss 才 createUser。
|
|
146
|
+
* 未配置(optional undefined)=查表面退場(現行單查行為零變——向後相容:
|
|
147
|
+
* fork 未配 ladder(0011 未上)時行為與 0.2.3 恆等,sennight/vestige 未來接 core 也零炸)。
|
|
148
|
+
* 錯誤處理照 tacet store.ts 母型:ladder 表缺席(0011 未上)=fail-open 視同 miss
|
|
149
|
+
*(lookup 實作自行 catch;本函式不吞 store 拋出的非缺席類錯誤——缺席面由 fork 實作裁定)。
|
|
150
|
+
*/
|
|
142
151
|
ladderLookup?(oldPh2: string): Promise<string | null>;
|
|
143
152
|
/** ladder 入表(遷移線用;upsert冪等)。 */
|
|
144
153
|
ladderInsert?(accountId: string, oldPh2: string, oldKind: 'legacy' | 'v2'): Promise<void>;
|
|
@@ -167,12 +176,42 @@ export function errResponse(code: string, status: number): Response {
|
|
|
167
176
|
|
|
168
177
|
// ── /auth/login:PH1 進站 → PH2 查詢(UNIQUE)→ session ─────────────────────
|
|
169
178
|
|
|
179
|
+
/**
|
|
180
|
+
* loginRouteCore ladder 查表守衛(0.2.4 外審 #8 幽靈帳真洞收口——tacet fork 母型上位):
|
|
181
|
+
* 現值(PH2 UNIQUE 查表)miss 時,先查 ladder 表(ph2_ladder:遷移線落下的「舊 ph2 值」),
|
|
182
|
+
* 命中=回舊帳不建幽靈;ladder 也 miss(或未配置)=才 createUser。
|
|
183
|
+
* - ladder hit 語意照 ladder.ts 檔頭註解:表列只承載舊值,帳戶現行憑證面以
|
|
184
|
+
* `getByUserKey` 讀到的 users 現值為準(v3 帳的 salt/wrapped 已是 v3 形);
|
|
185
|
+
* ladder hit → getByUserKey 落查無列(帳戶已刪、行殘留)=誠實 miss 續走建幽靈。
|
|
186
|
+
* - lookup 未配置(optional undefined)=零呼叫,行為與 0.2.3 單查恆等(向後相容鐵律);
|
|
187
|
+
* insert 是遷移線(rekeyWithLadder)職責、delete 已綁 delete-account(fork 面)——
|
|
188
|
+
* 本守衛只動查表,零寫入面。
|
|
189
|
+
* - lookup 拋錯(表缺席/儲存故障)=fail-open 視同 miss,不炸登入(tacet store.ts 母型;
|
|
190
|
+
* 靜默面照 tacet 慣例只 console.error)。
|
|
191
|
+
* - 零知識照舊:ladder 只存 ph2 hash 與 account_id,本函式零新洩漏面。
|
|
192
|
+
*/
|
|
193
|
+
async function ladderGuard(
|
|
194
|
+
store: AuthStore,
|
|
195
|
+
ph2: string,
|
|
196
|
+
): Promise<AuthRow | null> {
|
|
197
|
+
if (!store.ladderLookup) return null;
|
|
198
|
+
let ladderAcc: string | null = null;
|
|
199
|
+
try {
|
|
200
|
+
ladderAcc = await store.ladderLookup(ph2);
|
|
201
|
+
} catch (e) {
|
|
202
|
+
console.error('[ph2-ladder] lookup failed (fail-open):', e);
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
if (!ladderAcc) return null;
|
|
206
|
+
return store.getByUserKey(ladderAcc);
|
|
207
|
+
}
|
|
208
|
+
|
|
170
209
|
export async function loginRouteCore(env: Env & Record<string, unknown>, store: AuthStore, ip: string, ph1In: unknown): Promise<Response> {
|
|
171
210
|
if (!(await checkRate(env, AUTH_RATE, ip))) return errResponse('ERR_RATE_LIMITED', 429);
|
|
172
211
|
const ph1 = validHash64(ph1In);
|
|
173
212
|
if (!ph1) return errResponse('ERR_BAD_REQUEST', 400);
|
|
174
213
|
const ph2 = await sha256Hex(ph1);
|
|
175
|
-
const user = await store.findByIdentityQuery(ph2);
|
|
214
|
+
const user = (await store.findByIdentityQuery(ph2)) ?? (await ladderGuard(store, ph2));
|
|
176
215
|
const userKey: string = user ? user.userKey : await store.createUser(ph2);
|
|
177
216
|
const token = generateSessionToken();
|
|
178
217
|
const expiresAt = Date.now() + 30 * 24 * 60 * 60 * 1000; // PWA:30 天
|
package/src/server/ladder.ts
CHANGED
|
@@ -35,7 +35,11 @@ export type Ph2LadderKind = 'legacy' | 'v2';
|
|
|
35
35
|
export const PH2_LADDER_KIND_LEGACY = 'legacy';
|
|
36
36
|
export const PH2_LADDER_KIND_V2 = 'v2';
|
|
37
37
|
|
|
38
|
-
/** ladder 行形(fork schema 對位面)。
|
|
38
|
+
/** ladder 行形(fork schema 對位面)。
|
|
39
|
+
* 欄名映射註解(讀帳面——schema 欄名 vs Row 形名差異;t_87ef62dd #13 只補註解不改名,
|
|
40
|
+
* 禁 consumer breaking):schema 欄是 `upgraded_at`(D1/SQLite 欄面——「舊形被覆蓋的時刻」),
|
|
41
|
+
* Row 形名 `createdAt`(fork 消費面語意——「該舊值歷列建立時刻」);同一值、兩層命名——
|
|
42
|
+
* findByOldPh2 讀取面做 upgraded_at→createdAt 的逐欄映射,改任一層名都是 breaking(禁動)。 */
|
|
39
43
|
export interface Ph2LadderRow {
|
|
40
44
|
accountId: string;
|
|
41
45
|
ph2Kind: string;
|
package/scripts/open-card-pr.sh
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# open-card-pr.sh — journal-core 同 repo card branch PR 產生器。
|
|
3
|
-
#
|
|
4
|
-
# Usage: open-card-pr.sh <branch> <title> <body-file>
|
|
5
|
-
# branch card branch(head),例:card/<id>
|
|
6
|
-
# title PR 標題
|
|
7
|
-
# body-file PR body 純文字檔(完整讀入)
|
|
8
|
-
#
|
|
9
|
-
# 行為:純 curl POST /repos/tacet-ink/journal-core/pulls
|
|
10
|
-
# (head=<branch>、base=main、Token 取自 /srv/dropbox/csoft/tacet/.env 的
|
|
11
|
-
# GITHUB_TOKEN)。成功時 print PR 編號+URL;僅 print,不做其他事
|
|
12
|
-
# (不 push branch、不合併、不關閉)。
|
|
13
|
-
set -euo pipefail
|
|
14
|
-
|
|
15
|
-
REPO_URL="https://api.github.com/repos/tacet-ink/journal-core/pulls"
|
|
16
|
-
# 位置面:預設為本機絕對路徑(host-specific);其他環境以 ENV_FILE 覆寫
|
|
17
|
-
: "${ENV_FILE:=/srv/dropbox/csoft/tacet/.env}"
|
|
18
|
-
|
|
19
|
-
BRANCH="${1:?usage: open-card-pr.sh <branch> <title> <body-file>}"
|
|
20
|
-
TITLE="${2:?missing PR title}"
|
|
21
|
-
BODY_FILE="${3:?missing body file}"
|
|
22
|
-
|
|
23
|
-
[ -f "$BODY_FILE" ] || { echo "body file not found: $BODY_FILE" >&2; exit 1; }
|
|
24
|
-
|
|
25
|
-
TOKEN="$(grep '^GITHUB_TOKEN=' "$ENV_FILE" | head -1 | cut -d= -f2-)"
|
|
26
|
-
[ -n "$TOKEN" ] || { echo "GITHUB_TOKEN not found in $ENV_FILE" >&2; exit 1; }
|
|
27
|
-
|
|
28
|
-
PAYLOAD="$(jq -n --arg title "$TITLE" --arg head "$BRANCH" --arg base main \
|
|
29
|
-
--rawfile body "$BODY_FILE" \
|
|
30
|
-
'{title: $title, head: $head, base: $base, body: $body}')"
|
|
31
|
-
|
|
32
|
-
RESP="$(curl -sS -X POST \
|
|
33
|
-
-H "Authorization: token ${TOKEN}" \
|
|
34
|
-
-H "Accept: application/vnd.github+json" \
|
|
35
|
-
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
36
|
-
-d "$PAYLOAD" \
|
|
37
|
-
"$REPO_URL")"
|
|
38
|
-
|
|
39
|
-
NUM="$(printf '%s' "$RESP" | jq -r '.number // empty')"
|
|
40
|
-
if [ -n "$NUM" ]; then
|
|
41
|
-
URL="$(printf '%s' "$RESP" | jq -r '.html_url')"
|
|
42
|
-
echo "PR number: $NUM"
|
|
43
|
-
echo "PR url: $URL"
|
|
44
|
-
else
|
|
45
|
-
# 失敗(422 常見=branch 已有 PR/無 diff):print API 訊息不做事
|
|
46
|
-
printf '%s' "$RESP" | jq -r 'if .message then .message
|
|
47
|
-
else . end' >&2
|
|
48
|
-
exit 1
|
|
49
|
-
fi
|