@tacet-ink/journal-core 0.3.1 → 0.3.2
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 +6 -6
- package/dist/client/argon2.d.ts +13 -2
- package/dist/client/argon2.js +9 -3
- package/dist/client/note-crypto.d.ts +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/server/env.d.ts +6 -0
- package/dist/server/ratelimit.d.ts +14 -0
- package/dist/server/ratelimit.js +54 -1
- package/docs/format-spec.md +5 -5
- package/docs/vectors/bip39.json +1 -1
- package/docs/vectors/families.json +77 -77
- package/docs/vectors/kat.json +1 -1
- package/package.json +3 -2
- package/scripts/verify-core-crypto.ts +147 -12
- package/src/client/argon2.ts +10 -3
- package/src/client/note-crypto.ts +3 -1
- package/src/index.ts +1 -1
- package/src/server/env.ts +7 -0
- package/src/server/ratelimit.ts +48 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
```sh
|
|
19
19
|
git clone https://github.com/tacet-ink/journal-core.git && cd journal-core
|
|
20
20
|
npm ci --ignore-scripts
|
|
21
|
-
npm run verify #
|
|
21
|
+
npm run verify # 521 assertions, all green, run against the real modules (no mocks)
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
```ts
|
|
@@ -77,7 +77,7 @@ run: `node --experimental-strip-types example.ts`
|
|
|
77
77
|
| `src/client/keys.ts` | 品牌前綴 localStorage 命名空間 |
|
|
78
78
|
| `src/server/auth.ts` | 零知識 auth 核心:PH1→PH2 login 動線、入庫格式驗證、包裹欄組成對(pair 檢查 pickKeyPackage);PH2 UNIQUE 衝突與 session 撤銷=呼叫端職責 |
|
|
79
79
|
| `src/server/ladder.ts` | 密語正規化 v3 世代 ladder 表原語(ph2_ladder:以舊 ph2 查列/遷移入表 upsert 值主權威)+幽靈守衛查表接線已內建 loginRouteCore——fork 只需配 `ladderLookup` store+0011 migration(未配置=單查行為零變) |
|
|
80
|
-
| `src/server/ratelimit.ts` | per-IP fixed-window 限流(單句 UPSERT…RETURNING;D1 計數,跨 isolate
|
|
80
|
+
| `src/server/ratelimit.ts` | per-IP fixed-window 限流(單句 UPSERT…RETURNING;D1 計數,跨 isolate 有效;IPv6 鍵經 `normalizeRateIp` 聚合至 /64;IPv4-mapped 位址回點分 IPv4 鍵(=原生同 /32 鍵)) |
|
|
81
81
|
| `src/server/cors.ts`/`hash.ts` | 共用 CORS/雜湊工具(`src/server/env.ts` 為內部 Env 介面,不入 exports) |
|
|
82
82
|
|
|
83
83
|
發行形(0.3.0 起):`dist/`=tsc 原生 emit 的 ESM `.js`+`.d.ts`,`exports` 全部子路徑指向 dist;
|
|
@@ -124,7 +124,7 @@ jr3d/jr4d 兩入參數同輸入,域分離由 info 承載;凍結 KAT 兩 blob
|
|
|
124
124
|
|
|
125
125
|
```sh
|
|
126
126
|
npm ci --ignore-scripts
|
|
127
|
-
npm run verify #
|
|
127
|
+
npm run verify # 521 斷言對真模組(禁鏡像;限流單句 UPSERT…RETURNING 直載真 SQLite):roundtrip/AAD 防搬移/
|
|
128
128
|
# extractable/時代隔離/跨前綴家族隔離/payload 竄改/RFC 9106 KAT/BIP39 @scure 對照 200 組/
|
|
129
129
|
# server-side(inboundCipher/validWrappedKey/pickKeyPackage/checkRate/timingSafeEq)+ pinlock 全函式
|
|
130
130
|
# + 密語正規化 v3 世代(normalizePassphrase/jr4w./jr4d./PH1 v3 鹽域分離+帶內版本化舊契約向量)
|
|
@@ -136,7 +136,7 @@ npm run verify # 506 斷言對真模組(禁鏡像;限流單句 UPSERT…RE
|
|
|
136
136
|
# +KAT 凍結向量 [17](HKDF info 域世代分離+v3/raw 入口契約四 blob)
|
|
137
137
|
# +包裹前綴快檢 [13-2](錯前綴試探免付 KDF:五入口行為+計時帽+計數錨)
|
|
138
138
|
# +CI 供應鏈面 [21](npm ci --ignore-scripts+dependabot runner fromJSON 分流+腳本摘除+NIT-1 錨+r2 MINOR-1 URL 直傳;
|
|
139
|
-
#
|
|
139
|
+
# 521 帳=repo 樹——發行包內無 .github=[21] 四條 workflow 錨收斂為 1 顯性 SKIP 行→518,SKIP 顯形非靜默)
|
|
140
140
|
# +發行 tarball 治理面 [22](pack 白名單零 open-card-pr.sh)
|
|
141
141
|
# +格式規格+測試向量 [26](docs/format-spec.md+docs/vectors 存在性/位元組 shape/欄位名對規格/凍結 KAT 逐字對閘)
|
|
142
142
|
# +高階 Vault API [27](roundtrip 三形/新舊前綴解鎖/upgrade/換密語/復原/VaultError 毒化/載體覆寫+Worker 回退鏈/vendor 孿生)
|
|
@@ -317,7 +317,7 @@ carrier: `node:crypto` ≥ Node 24.7 / hash-wasm in browsers, RFC 9106 test vect
|
|
|
317
317
|
@scure/bip39), brand-namespaced key storage (`keys.ts`). Server: zero-knowledge auth core with a
|
|
318
318
|
PH1→PH2 login flow, inbound cipher/package validation and paired key-package selection
|
|
319
319
|
(`auth.ts`; PH2-UNIQUE conflict and session revocation are caller-owned wiring), per-IP
|
|
320
|
-
fixed-window rate limiting on D1 (`ratelimit.ts`), shared CORS/hash utilities. Since 0.3.0 the
|
|
320
|
+
(IPv6 keyed per /64 prefix; IPv4-mapped addresses un-map to the native dotted IPv4 key) fixed-window rate limiting on D1 (`ratelimit.ts`), shared CORS/hash utilities. Since 0.3.0 the
|
|
321
321
|
package ships `dist/` (tsc-emitted ESM `.js` + `.d.ts`); every `exports` subpath resolves to dist,
|
|
322
322
|
and `src/` is included for reading and auditing only.
|
|
323
323
|
|
|
@@ -331,7 +331,7 @@ prefix family (`families.json`), known-answer vectors (RFC 9106 Argon2id, the ga
|
|
|
331
331
|
`scripts/generate-vectors.ts` against the real primitives and committed as frozen evidence; the
|
|
332
332
|
verify gate checks their presence and shape (section [26]) and does not regenerate them.
|
|
333
333
|
|
|
334
|
-
**Verification.** `npm run verify` runs
|
|
334
|
+
**Verification.** `npm run verify` runs 521 assertions against the real modules (no mocks):
|
|
335
335
|
roundtrips, AAD tamper-evidence, extractability rules, era isolation, cross-prefix family
|
|
336
336
|
isolation, payload tampering, RFC 9106 KAT, a 200-vector BIP39 cross-check, and the
|
|
337
337
|
login ladder-guard behavior suite (external audit #8), and presence/shape checks for the format
|
package/dist/client/argon2.d.ts
CHANGED
|
@@ -35,6 +35,17 @@
|
|
|
35
35
|
*/
|
|
36
36
|
/** RFC 9106 無 secret/ad 標準向量(Argon2id v=0x13, t=3, m=32, p=4, T=32, pwd=32B 0x01, salt=16B 0x00)。 */
|
|
37
37
|
export declare const ARGON_RFC9106_EXPECTED = "72d2a36fd5c266bcc96121b24937bc253338cdfcbd273713655748c54b4dd503";
|
|
38
|
+
/** RFC 9106 KAT 參數單一真相(verifyArgonKat/scripts/generate-vectors.ts 寫 kat.json/閘 [26] 對帳共用)。
|
|
39
|
+
* tagLen=派生本體固定輸出長(ARGON_TAG_LEN);pwd=passwordLen 位元組 passwordByte、salt=saltLen 位元組 0x00。 */
|
|
40
|
+
export declare const ARGON_RFC9106_PARAMS: Readonly<{
|
|
41
|
+
readonly memoryKib: 32;
|
|
42
|
+
readonly iterations: 3;
|
|
43
|
+
readonly parallelism: 4;
|
|
44
|
+
readonly tagLen: 32;
|
|
45
|
+
readonly passwordByte: 1;
|
|
46
|
+
readonly passwordLen: 32;
|
|
47
|
+
readonly saltLen: 16;
|
|
48
|
+
}>;
|
|
38
49
|
/**
|
|
39
50
|
* PH1 v2 固定域鹽(登入憑證 Argon2id 派生,2026-09-10 安全路線第 4 步)。
|
|
40
51
|
*
|
|
@@ -116,7 +127,7 @@ export declare function getArgonLoader(): ArgonLoader | null;
|
|
|
116
127
|
export declare function verifyArgonKat(): Promise<boolean>;
|
|
117
128
|
/** KEK = Argon2id(pass, salt, m=64MiB, t=3, p=1) → AES-GCM key(nonextractable)。 */
|
|
118
129
|
export declare function deriveKekArgon(password: string, salt: Uint8Array): Promise<CryptoKey>;
|
|
119
|
-
/** jr3w. 包裹:payload =
|
|
130
|
+
/** jr3w. 包裹:payload = iv[12] ‖ AES-GCM(KEK, hex(noteKey), aad='notekey')(鹽外置家族,嚴格 92B);salt 不入 payload,由呼叫端存 users.salt。 */
|
|
120
131
|
export declare function wrapNoteKey3(cfg: Argon3Config, noteKey: CryptoKey, passphrase: string): Promise<{
|
|
121
132
|
wrapped: string;
|
|
122
133
|
salt: string;
|
|
@@ -127,7 +138,7 @@ export declare function wrapNoteKey3(cfg: Argon3Config, noteKey: CryptoKey, pass
|
|
|
127
138
|
* 家族守衛(wrap3 未配置回 null)、salt1 hex-形檢查與 KEK 派生——payload 形是鹽外置家族(iv‖ct),
|
|
128
139
|
* own 92B 檢已隨收口摘除(jr3s 同;鹽內嵌族 own 108B 保留)。 */
|
|
129
140
|
export declare function unwrapNoteKey3(cfg: Argon3Config, wrapped: string, passphrase: string, saltHex: string): Promise<CryptoKey | null>;
|
|
130
|
-
/** jr4w. 包裹(v3 密語):payload =
|
|
141
|
+
/** jr4w. 包裹(v3 密語):payload = iv[12] ‖ GCM(KEK, hex(noteKey), aad='notekey')(鹽外置家族,嚴格 92B);salt 不入 payload,呼叫端存 users.salt。 */
|
|
131
142
|
export declare function wrapNoteKey4(cfg: Argon3Config, noteKey: CryptoKey, passphrase: string): Promise<{
|
|
132
143
|
wrapped: string;
|
|
133
144
|
salt: string;
|
package/dist/client/argon2.js
CHANGED
|
@@ -45,6 +45,11 @@ function deriveInput(passphrase) {
|
|
|
45
45
|
}
|
|
46
46
|
/** RFC 9106 無 secret/ad 標準向量(Argon2id v=0x13, t=3, m=32, p=4, T=32, pwd=32B 0x01, salt=16B 0x00)。 */
|
|
47
47
|
export const ARGON_RFC9106_EXPECTED = '72d2a36fd5c266bcc96121b24937bc253338cdfcbd273713655748c54b4dd503';
|
|
48
|
+
/** RFC 9106 KAT 參數單一真相(verifyArgonKat/scripts/generate-vectors.ts 寫 kat.json/閘 [26] 對帳共用)。
|
|
49
|
+
* tagLen=派生本體固定輸出長(ARGON_TAG_LEN);pwd=passwordLen 位元組 passwordByte、salt=saltLen 位元組 0x00。 */
|
|
50
|
+
export const ARGON_RFC9106_PARAMS = Object.freeze({
|
|
51
|
+
memoryKib: 32, iterations: 3, parallelism: 4, tagLen: 32, passwordByte: 0x01, passwordLen: 32, saltLen: 16,
|
|
52
|
+
});
|
|
48
53
|
/**
|
|
49
54
|
* PH1 v2 固定域鹽(登入憑證 Argon2id 派生,2026-09-10 安全路線第 4 步)。
|
|
50
55
|
*
|
|
@@ -145,7 +150,8 @@ async function deriveArgon2id(password, salt, memoryKib, passes, parallelism) {
|
|
|
145
150
|
}
|
|
146
151
|
/** 標準向量 KAT:驗證當前載體的 Argon2id 實作正確性(驗證閘起手式)。 */
|
|
147
152
|
export async function verifyArgonKat() {
|
|
148
|
-
const
|
|
153
|
+
const k = ARGON_RFC9106_PARAMS;
|
|
154
|
+
const raw = await deriveArgon2id(new Uint8Array(k.passwordLen).fill(k.passwordByte), new Uint8Array(k.saltLen), k.memoryKib, k.iterations, k.parallelism);
|
|
149
155
|
return toHex(raw) === ARGON_RFC9106_EXPECTED;
|
|
150
156
|
}
|
|
151
157
|
// ── jr3w. 單因子包裹 ──────────────────────────────────────────────────────────
|
|
@@ -156,7 +162,7 @@ async function argonKekFromRaw(raw) {
|
|
|
156
162
|
export async function deriveKekArgon(password, salt) {
|
|
157
163
|
return argonKekFromRaw(await deriveArgon2id(password, salt, ARGON_MEMORY_KIB, ARGON_ITERATIONS, ARGON_PARALLELISM));
|
|
158
164
|
}
|
|
159
|
-
/** jr3w. 包裹:payload =
|
|
165
|
+
/** jr3w. 包裹:payload = iv[12] ‖ AES-GCM(KEK, hex(noteKey), aad='notekey')(鹽外置家族,嚴格 92B);salt 不入 payload,由呼叫端存 users.salt。 */
|
|
160
166
|
export async function wrapNoteKey3(cfg, noteKey, passphrase) {
|
|
161
167
|
if (!cfg.wrap3)
|
|
162
168
|
throw new Error('ERR_JR3W_NOT_CONFIGURED');
|
|
@@ -193,7 +199,7 @@ export async function unwrapNoteKey3(cfg, wrapped, passphrase, saltHex) {
|
|
|
193
199
|
// 92B、AAD 'notekey'、嚴格長度全在 openNoteKey 本體),唯一差異=入口吃 normalizePassphrase(pass)
|
|
194
200
|
// 與 cfg.wrap4 前綴。舊 payload(jr1w./jr3w.)永遠可解=raw 派生;新 payload 只由 jr4w. 產生。
|
|
195
201
|
// pinSaltPrefix 沿用 cfg 既有欄(Argon3 家族 PIN 鹽域,非 jr3d. 專屬)。
|
|
196
|
-
/** jr4w. 包裹(v3 密語):payload =
|
|
202
|
+
/** jr4w. 包裹(v3 密語):payload = iv[12] ‖ GCM(KEK, hex(noteKey), aad='notekey')(鹽外置家族,嚴格 92B);salt 不入 payload,呼叫端存 users.salt。 */
|
|
197
203
|
export async function wrapNoteKey4(cfg, noteKey, passphrase) {
|
|
198
204
|
if (!cfg.wrap4)
|
|
199
205
|
throw new Error('ERR_JR4W_NOT_CONFIGURED');
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* import 當下就必須 extractable=true。noteKey 全部產生路徑(generate/unwrap×3)
|
|
25
25
|
* 一律顯式 true;KEK/guest key 保持 nonextractable(只加解密、永不 export)。
|
|
26
26
|
*/
|
|
27
|
+
import type { KeyStore } from './keys.js';
|
|
27
28
|
export interface NoteCryptoConfig {
|
|
28
29
|
/** guest 時代 KDF 前綴,如 'journal-note-u1'。 */
|
|
29
30
|
guestKdfPrefix: string;
|
|
@@ -72,7 +73,7 @@ export interface NoteCryptoConfig {
|
|
|
72
73
|
/** 本機 IDB 密文前綴(jr1d.,notes store stored 形)。未配置 = local API 拒絕(未配置行為不變)。 */
|
|
73
74
|
cipherLocal?: string;
|
|
74
75
|
/** localStorage key store(品牌前綴由 keys.ts 管理)。 */
|
|
75
|
-
store:
|
|
76
|
+
store: KeyStore;
|
|
76
77
|
}
|
|
77
78
|
export declare const PBKDF2_ITERATIONS = 600000;
|
|
78
79
|
export declare const PIN_PBKDF2_ITERATIONS = 2000000;
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { makeKeyStore, type KeyStore, type KeyStoreConfig } from './client/keys.
|
|
|
11
11
|
export { wordlist } from './client/wordlist.ts';
|
|
12
12
|
/** 伺服器端原語(Cloudflare Workers + D1)。 */
|
|
13
13
|
export { makeInboundCipher, isCipherFor, validWrappedKey, validHash64, validSalt, pickKeyPackage, loginRouteCore, errResponse, AUTH_RATE, type AuthRow, type AuthStore, type CipherFormats, type KeyPackage, } from './server/auth.ts';
|
|
14
|
-
export { checkRate, type RateWindow } from './server/ratelimit.ts';
|
|
14
|
+
export { checkRate, normalizeRateIp, type RateWindow } from './server/ratelimit.ts';
|
|
15
15
|
export { corsResponse, CORS_HEADERS } from './server/cors.ts';
|
|
16
16
|
export { sha256Hex, hashString, generateSessionToken, timingSafeEq, } from './server/hash.ts';
|
|
17
17
|
export type { Env } from './server/env.ts';
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ export { makeKeyStore } from './client/keys.js';
|
|
|
11
11
|
export { wordlist } from './client/wordlist.js';
|
|
12
12
|
/** 伺服器端原語(Cloudflare Workers + D1)。 */
|
|
13
13
|
export { makeInboundCipher, isCipherFor, validWrappedKey, validHash64, validSalt, pickKeyPackage, loginRouteCore, errResponse, AUTH_RATE, } from './server/auth.js';
|
|
14
|
-
export { checkRate } from './server/ratelimit.js';
|
|
14
|
+
export { checkRate, normalizeRateIp } from './server/ratelimit.js';
|
|
15
15
|
export { corsResponse, CORS_HEADERS } from './server/cors.js';
|
|
16
16
|
export { sha256Hex, hashString, generateSessionToken, timingSafeEq, } from './server/hash.js';
|
|
17
17
|
export { PH2_LADDER_TABLE, PH2_LADDER_KIND_LEGACY, PH2_LADDER_KIND_V2, } from './server/ladder.js';
|
package/dist/server/env.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* env.ts — core 對 Worker Env 的最小介面。各 fork 的 Env 以此為基底擴充。
|
|
3
|
+
*
|
|
4
|
+
* D1Database 型別以模組 import 承載(非全域 triple-slash reference):
|
|
5
|
+
* 消費者未帶 @cloudflare/workers-types 時,吃 client 面 .d.ts 不會被 env.d.ts 的
|
|
6
|
+
* 全域型別缺席炸掉(types= reference 在 types 套件缺席=TS2688 全檔紅;import type
|
|
7
|
+
* 缺席=該欄型別 any 化+TS2304 只在吃 env.d.ts 的檔案現形=影響面收斂)。
|
|
3
8
|
*/
|
|
9
|
+
import type { D1Database } from '@cloudflare/workers-types';
|
|
4
10
|
export interface Env {
|
|
5
11
|
DB: D1Database;
|
|
6
12
|
}
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
* 放行判準:RETURNING 的 count <= max(窗口內超額後的每一發都回實際 count,
|
|
15
15
|
* 由呼叫端以 count <= max 拒絕——計數不封頂,超額仍逐發累計)。
|
|
16
16
|
*
|
|
17
|
+
* 限流鍵(2026-10-11 定案):checkRate 入口經 normalizeRateIp 正規化——IPv6 聚合至 /64
|
|
18
|
+
* (單一終端可自由輪換 /64 內尾段=完整地址當鍵即每發新鍵、限流失效);IPv4-mapped
|
|
19
|
+
* 位址回點分 IPv4(=原生 IPv4 同 /32 鍵,review r1 MAJOR-1 收口);IPv4 與非 IPv6 形原樣。
|
|
20
|
+
*
|
|
17
21
|
* 呼叫端須自建表(無 GC 機制:舊 IP 列不清,表隨活躍 IP 數緩慢增長——
|
|
18
22
|
* 每列 ~40 bytes,量級可忽略;如需回收見下方 GC 註記):
|
|
19
23
|
* CREATE TABLE IF NOT EXISTS {table} (ip TEXT PRIMARY KEY, window_start INTEGER, count INTEGER);
|
|
@@ -29,4 +33,14 @@ export interface RateWindow {
|
|
|
29
33
|
export declare const SQL_RATE_BUMP = "INSERT INTO {table} (ip, window_start, count) VALUES (?1, ?2, 1)\n ON CONFLICT(ip) DO UPDATE SET\n count = CASE WHEN window_start <= ?3 THEN 1 ELSE count + 1 END,\n window_start = CASE WHEN window_start <= ?3 THEN ?2 ELSE window_start END\n RETURNING count AS n";
|
|
30
34
|
/** @internal(0.1.6 公開面走 index.ts checkRate)。放行判準單一真相:窗口內超額後照實回實際 count,呼叫端以 count <= max 拒絕。 */
|
|
31
35
|
export declare function isRateAllowed(count: number, max: number): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* 限流鍵正規化(單一真相;純函式、零 throw):
|
|
38
|
+
* - IPv6 → /64 前綴+`/64` 標記(RFC 5952 規範形:hextet 前導零摘除、小寫、零串壓縮),
|
|
39
|
+
* 例 `2001:0DB8:0000:000A::1` → `2001:db8:0:a::/64`、`::1` → `::/64`;zone id(`%…`)先剝除。
|
|
40
|
+
* 介面 ID 歸零後末 4 組恆零=最長零串恆為尾串(前綴內零串至多 3 組),故壓縮形=前綴摘尾零組+`::`。
|
|
41
|
+
* - IPv4-mapped(::/96 前綴+映射尾組,RFC 4291 §2.5.5.2)→ 點分 IPv4 原鍵——mapped 位址就是 IPv4
|
|
42
|
+
* 節點的表示法,回 /32 與原生同鍵(hex 尾組 twin `::ffff:c000:201` 同鍵 192.0.2.1;禁併入 ::/64 全域桶)。
|
|
43
|
+
* - IPv4/`'unknown'`/空串/無法解析形:原樣返回(鍵照樣進表=可見面,不靜默丟鍵)。
|
|
44
|
+
*/
|
|
45
|
+
export declare function normalizeRateIp(ip: string): string;
|
|
32
46
|
export declare function checkRate(env: Env, w: RateWindow, ip: string): Promise<boolean>;
|
package/dist/server/ratelimit.js
CHANGED
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
* 放行判準:RETURNING 的 count <= max(窗口內超額後的每一發都回實際 count,
|
|
15
15
|
* 由呼叫端以 count <= max 拒絕——計數不封頂,超額仍逐發累計)。
|
|
16
16
|
*
|
|
17
|
+
* 限流鍵(2026-10-11 定案):checkRate 入口經 normalizeRateIp 正規化——IPv6 聚合至 /64
|
|
18
|
+
* (單一終端可自由輪換 /64 內尾段=完整地址當鍵即每發新鍵、限流失效);IPv4-mapped
|
|
19
|
+
* 位址回點分 IPv4(=原生 IPv4 同 /32 鍵,review r1 MAJOR-1 收口);IPv4 與非 IPv6 形原樣。
|
|
20
|
+
*
|
|
17
21
|
* 呼叫端須自建表(無 GC 機制:舊 IP 列不清,表隨活躍 IP 數緩慢增長——
|
|
18
22
|
* 每列 ~40 bytes,量級可忽略;如需回收見下方 GC 註記):
|
|
19
23
|
* CREATE TABLE IF NOT EXISTS {table} (ip TEXT PRIMARY KEY, window_start INTEGER, count INTEGER);
|
|
@@ -58,6 +62,55 @@ async function bumpRate(env, w, ip) {
|
|
|
58
62
|
return true;
|
|
59
63
|
}
|
|
60
64
|
}
|
|
65
|
+
const HEXTET = /^[0-9a-f]{1,4}$/i;
|
|
66
|
+
const IPV4_OCTET = /^(0|[1-9]\d{0,2})$/;
|
|
67
|
+
/** IPv6 文字形 → 8 組 hextet 數值(RFC 4291 §2.2:`::` 至多一處、嵌入 IPv4 僅居末);非法形回 null。 */
|
|
68
|
+
function parseIpv6(s) {
|
|
69
|
+
const halves = s.split('::');
|
|
70
|
+
if (halves.length > 2)
|
|
71
|
+
return null;
|
|
72
|
+
const parts = halves.map((h) => (h === '' ? [] : h.split(':')));
|
|
73
|
+
const last = parts[parts.length - 1];
|
|
74
|
+
const v4 = [];
|
|
75
|
+
if (last.length > 0 && last[last.length - 1].includes('.')) {
|
|
76
|
+
const q = last.pop().split('.');
|
|
77
|
+
if (q.length !== 4 || !q.every((o) => IPV4_OCTET.test(o) && Number(o) <= 255))
|
|
78
|
+
return null;
|
|
79
|
+
v4.push(Number(q[0]) * 256 + Number(q[1]), Number(q[2]) * 256 + Number(q[3]));
|
|
80
|
+
}
|
|
81
|
+
if (!parts.every((p) => p.every((g) => HEXTET.test(g))))
|
|
82
|
+
return null;
|
|
83
|
+
const n = parts.reduce((a, p) => a + p.length, 0) + v4.length;
|
|
84
|
+
const hex = (p) => p.map((g) => parseInt(g, 16));
|
|
85
|
+
if (halves.length === 1)
|
|
86
|
+
return n === 8 ? [...hex(parts[0]), ...v4] : null;
|
|
87
|
+
if (n > 7)
|
|
88
|
+
return null;
|
|
89
|
+
return [...hex(parts[0]), ...new Array(8 - n).fill(0), ...hex(parts[1]), ...v4];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 限流鍵正規化(單一真相;純函式、零 throw):
|
|
93
|
+
* - IPv6 → /64 前綴+`/64` 標記(RFC 5952 規範形:hextet 前導零摘除、小寫、零串壓縮),
|
|
94
|
+
* 例 `2001:0DB8:0000:000A::1` → `2001:db8:0:a::/64`、`::1` → `::/64`;zone id(`%…`)先剝除。
|
|
95
|
+
* 介面 ID 歸零後末 4 組恆零=最長零串恆為尾串(前綴內零串至多 3 組),故壓縮形=前綴摘尾零組+`::`。
|
|
96
|
+
* - IPv4-mapped(::/96 前綴+映射尾組,RFC 4291 §2.5.5.2)→ 點分 IPv4 原鍵——mapped 位址就是 IPv4
|
|
97
|
+
* 節點的表示法,回 /32 與原生同鍵(hex 尾組 twin `::ffff:c000:201` 同鍵 192.0.2.1;禁併入 ::/64 全域桶)。
|
|
98
|
+
* - IPv4/`'unknown'`/空串/無法解析形:原樣返回(鍵照樣進表=可見面,不靜默丟鍵)。
|
|
99
|
+
*/
|
|
100
|
+
export function normalizeRateIp(ip) {
|
|
101
|
+
if (typeof ip !== 'string' || !ip.includes(':'))
|
|
102
|
+
return ip;
|
|
103
|
+
const pct = ip.indexOf('%');
|
|
104
|
+
const g = parseIpv6(pct >= 0 ? ip.slice(0, pct) : ip);
|
|
105
|
+
if (!g)
|
|
106
|
+
return ip;
|
|
107
|
+
if (g[0] === 0 && g[1] === 0 && g[2] === 0 && g[3] === 0 && g[4] === 0 && g[5] === 0xffff)
|
|
108
|
+
return [g[6] >> 8, g[6] & 255, g[7] >> 8, g[7] & 255].join('.');
|
|
109
|
+
const net = g.slice(0, 4);
|
|
110
|
+
while (net.length > 0 && net[net.length - 1] === 0)
|
|
111
|
+
net.pop();
|
|
112
|
+
return net.map((h) => h.toString(16)).join(':') + '::/64';
|
|
113
|
+
}
|
|
61
114
|
export function checkRate(env, w, ip) {
|
|
62
|
-
return bumpRate(env, w, ip);
|
|
115
|
+
return bumpRate(env, w, normalizeRateIp(ip));
|
|
63
116
|
}
|
package/docs/format-spec.md
CHANGED
|
@@ -351,7 +351,7 @@ PH1 鹽是 per-product 固定域鹽(PH2 UNIQUE 約束);v2/v3 鹽域值
|
|
|
351
351
|
| `inputs` | 產生時的輸入(密語、PIN、identity、config 欄、外置鹽 `salt_hex`/`salt1_hex`、`note_key_hex` 或 `plaintext`) |
|
|
352
352
|
| `wire` | 完整線上字串(`prefix + b64(payload)`) |
|
|
353
353
|
| `payload_len` | Base64 解碼後位元組數 |
|
|
354
|
-
| `fields` | payload 欄位切分陣列,每項 `name`(`pinSalt`/`iv`/`ct`/`tag`)、`offset`、`len`、`hex`;`len` 總和=`payload_len` |
|
|
354
|
+
| `fields` | payload 欄位切分陣列,每項 `fields[].name`(`pinSalt`/`iv`/`ct`/`tag`)、`fields[].offset`、`fields[].len`、`fields[].hex`;`fields[].len` 總和=`payload_len` |
|
|
355
355
|
|
|
356
356
|
金鑰包裹家族的 `inputs.note_key_hex` 固定 `c3`×32(驗證閘 KAT 母型),以此 wire 依本規格解包須得回之;
|
|
357
357
|
content 家族以 `inputs.plaintext`/`inputs.aad` 解密須得回原文。
|
|
@@ -360,10 +360,10 @@ content 家族以 `inputs.plaintext`/`inputs.aad` 解密須得回原文。
|
|
|
360
360
|
|
|
361
361
|
| 欄位 | 意義 |
|
|
362
362
|
| --- | --- |
|
|
363
|
-
| `rfc9106` | Argon2id RFC 9106 §5.3
|
|
364
|
-
| `frozen_blobs` | 驗證閘凍結 blob([14] `jr2w.`、[17] `jr3w.`/`jr3d.`/`jr4w.`/`jr4d.`、[20] `jr1r.`/`jr1w.` 復原舊契約):`prefix`、`wire
|
|
365
|
-
| `credentials` | §5 憑證派生樣本:`kind`、`input`、`salt`、`expected_hex` |
|
|
363
|
+
| `rfc9106` | Argon2id RFC 9106 §5.3 參數:`rfc9106.algorithm`、`rfc9106.version`、`rfc9106.memory_kib`、`rfc9106.iterations`、`rfc9106.parallelism`、`rfc9106.tag_len`、`rfc9106.password_hex`、`rfc9106.salt_hex`、`rfc9106.secret`/`rfc9106.associated_data`(恆 null),與 `rfc9106.expected_tag_hex` |
|
|
364
|
+
| `frozen_blobs` | 驗證閘凍結 blob([14] `jr2w.`、[17] `jr3w.`/`jr3d.`/`jr4w.`/`jr4d.`、[20] `jr1r.`/`jr1w.` 復原舊契約):`frozen_blobs[].prefix`、`frozen_blobs[].wire`、`frozen_blobs[].payload_len`、`frozen_blobs[].inputs`(解包輸入)、`frozen_blobs[].expected_note_key_hex`、`frozen_blobs[].source`(閘段號) |
|
|
365
|
+
| `credentials` | §5 憑證派生樣本:`credentials[].kind`、`credentials[].input`、`credentials[].input_nfkc`(僅 `ph1_v3`:`input` 經 `normalizePassphrase` 後的值)、`credentials[].salt`、`credentials[].expected_hex` |
|
|
366
366
|
|
|
367
367
|
### 7.3 `bip39.json`——BIP39 抽樣
|
|
368
368
|
|
|
369
|
-
`samples`(陣列)每筆:`entropy_hex`、`checksum_hex`、`words`(24 詞陣列)、`source`(`seeded:<n>`/`edge`)。
|
|
369
|
+
`samples`(陣列)每筆:`samples[].entropy_hex`、`samples[].checksum_hex`、`samples[].words`(24 詞陣列)、`samples[].source`(`seeded:<n>`/`edge`)。
|
package/docs/vectors/bip39.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"format": "journal-core-vectors/1",
|
|
3
|
-
"package_version": "0.3.
|
|
3
|
+
"package_version": "0.3.1",
|
|
4
4
|
"spec": "docs/format-spec.md",
|
|
5
5
|
"generator": "scripts/generate-vectors.ts",
|
|
6
6
|
"note": "Frozen evidence generated once by calling the real primitives; not recomputed in CI (random iv/salt). Verify gate [26] checks presence and shape only.",
|