@tacet-ink/journal-core 0.1.0
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/LICENSE +21 -0
- package/README.md +107 -0
- package/package.json +40 -0
- package/scripts/verify-core-crypto.ts +414 -0
- package/src/client/argon2.ts +350 -0
- package/src/client/bip39.ts +118 -0
- package/src/client/keys.ts +27 -0
- package/src/client/note-crypto.ts +433 -0
- package/src/client/pinlock.ts +105 -0
- package/src/client/wordlist.ts +264 -0
- package/src/index.ts +83 -0
- package/src/server/auth.ts +146 -0
- package/src/server/cors.ts +18 -0
- package/src/server/env.ts +6 -0
- package/src/server/hash.ts +28 -0
- package/src/server/ratelimit.ts +60 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ratelimit.ts — per-IP 滑動視窗限流(D1 計數,跨 isolate 有效;併發安全版)。
|
|
3
|
+
*
|
|
4
|
+
* 教訓來源(生產實證):
|
|
5
|
+
* - in-memory Map 在多 isolate/多 colo 下無效(實測 24 發分散多機房全 200)
|
|
6
|
+
* - 原單句 UPDATE 計數在併發下輕微超額 → 改單條件寫入鏈:INSERT 競態輸家先試
|
|
7
|
+
* 「作用中未超額」計數 UPDATE(輸家直接掉到 reset 會把贏家的作用中視窗誤判成拒絕)
|
|
8
|
+
* - fail-open 裁定:限流儲存故障時放行(保護面不可反噬主功能),只擋明確超額
|
|
9
|
+
*
|
|
10
|
+
* 呼叫端須自建表:
|
|
11
|
+
* CREATE TABLE IF NOT EXISTS {table} (ip TEXT PRIMARY KEY, window_start INTEGER, count INTEGER);
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Env } from './env';
|
|
15
|
+
|
|
16
|
+
export interface RateWindow {
|
|
17
|
+
table: string;
|
|
18
|
+
windowMs: number;
|
|
19
|
+
max: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function bumpRate(env: Env, w: RateWindow, ip: string): Promise<boolean> {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
try {
|
|
25
|
+
// 快路徑:未超額的既有列直接 +1(單句原子,競態安全)
|
|
26
|
+
const upd = await env.DB.prepare(
|
|
27
|
+
`UPDATE ${w.table} SET count = count + 1
|
|
28
|
+
WHERE ip = ? AND window_start > ? AND count < ?`
|
|
29
|
+
).bind(ip, now - w.windowMs, w.max).run();
|
|
30
|
+
if ((upd.meta?.changes ?? 0) > 0) return true;
|
|
31
|
+
// 無列或窗口過期:INSERT;主鍵競態(同 IP 併發首發)→ 輸家先試「作用中未超額」
|
|
32
|
+
// 計數 UPDATE(外部審查 MAJOR 修復:輸家直接掉到 reset 會把贏家的作用中視窗誤判成
|
|
33
|
+
// 拒絕 → 空視窗併發首發 undercount 到只剩 1 次)。再 fallback 到重置式 UPDATE。
|
|
34
|
+
const ins = await env.DB.prepare(
|
|
35
|
+
`INSERT INTO ${w.table} (ip, window_start, count) VALUES (?, ?, 1)
|
|
36
|
+
ON CONFLICT(ip) DO NOTHING`
|
|
37
|
+
).bind(ip, now).run();
|
|
38
|
+
if ((ins.meta?.changes ?? 0) > 0) return true;
|
|
39
|
+
const loser = await env.DB.prepare(
|
|
40
|
+
`UPDATE ${w.table} SET count = count + 1
|
|
41
|
+
WHERE ip = ? AND window_start > ? AND count < ?`
|
|
42
|
+
).bind(ip, now - w.windowMs, w.max).run();
|
|
43
|
+
if ((loser.meta?.changes ?? 0) > 0) return true;
|
|
44
|
+
// 列已存在且未超額:窗口過期 → 重置;窗口內但剛才 count>=max 沒吃到 → 拒絕
|
|
45
|
+
const reset = await env.DB.prepare(
|
|
46
|
+
`UPDATE ${w.table} SET window_start = ?, count = 1
|
|
47
|
+
WHERE ip = ? AND window_start <= ?`
|
|
48
|
+
).bind(now, ip, now - w.windowMs).run();
|
|
49
|
+
if ((reset.meta?.changes ?? 0) > 0) return true;
|
|
50
|
+
return false;
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// fail-open 裁定(2026-09-06):儲存故障時放行,只擋明確超額
|
|
53
|
+
console.error(`[ratelimit] ${w.table} update failed:`, e);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function checkRate(env: Env, w: RateWindow, ip: string): Promise<boolean> {
|
|
59
|
+
return bumpRate(env, w, ip);
|
|
60
|
+
}
|