covara 0.11.2 → 0.13.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.
Files changed (101) hide show
  1. package/README.md +4 -1
  2. package/dist/abuse/budget.d.ts +41 -0
  3. package/dist/abuse/budget.d.ts.map +1 -0
  4. package/dist/abuse/budget.js +122 -0
  5. package/dist/abuse/budget.js.map +1 -0
  6. package/dist/abuse/captcha.d.ts +45 -0
  7. package/dist/abuse/captcha.d.ts.map +1 -0
  8. package/dist/abuse/captcha.js +80 -0
  9. package/dist/abuse/captcha.js.map +1 -0
  10. package/dist/abuse/config.d.ts +150 -0
  11. package/dist/abuse/config.d.ts.map +1 -0
  12. package/dist/abuse/config.js +33 -0
  13. package/dist/abuse/config.js.map +1 -0
  14. package/dist/abuse/enforce.d.ts +44 -0
  15. package/dist/abuse/enforce.d.ts.map +1 -0
  16. package/dist/abuse/enforce.js +198 -0
  17. package/dist/abuse/enforce.js.map +1 -0
  18. package/dist/abuse/middleware.d.ts +26 -0
  19. package/dist/abuse/middleware.d.ts.map +1 -0
  20. package/dist/abuse/middleware.js +115 -0
  21. package/dist/abuse/middleware.js.map +1 -0
  22. package/dist/auth/routes.d.ts +12 -0
  23. package/dist/auth/routes.d.ts.map +1 -1
  24. package/dist/auth/routes.js +28 -0
  25. package/dist/auth/routes.js.map +1 -1
  26. package/dist/cli/templates/agents.d.ts.map +1 -1
  27. package/dist/cli/templates/agents.js +2 -0
  28. package/dist/cli/templates/agents.js.map +1 -1
  29. package/dist/cli/templates/source.d.ts +4 -4
  30. package/dist/cli/templates/source.d.ts.map +1 -1
  31. package/dist/cli/templates/source.js +5 -5
  32. package/dist/client/captcha.d.ts +41 -0
  33. package/dist/client/captcha.d.ts.map +1 -0
  34. package/dist/client/captcha.js +110 -0
  35. package/dist/client/captcha.js.map +1 -0
  36. package/dist/client/index.d.ts +18 -0
  37. package/dist/client/index.d.ts.map +1 -1
  38. package/dist/client/index.js +4 -0
  39. package/dist/client/index.js.map +1 -1
  40. package/dist/client/pow.d.ts +15 -0
  41. package/dist/client/pow.d.ts.map +1 -0
  42. package/dist/client/pow.js +9 -0
  43. package/dist/client/pow.js.map +1 -0
  44. package/dist/client/react.d.ts +29 -1
  45. package/dist/client/react.d.ts.map +1 -1
  46. package/dist/client/react.js +77 -1
  47. package/dist/client/react.js.map +1 -1
  48. package/dist/client/transport.d.ts +13 -1
  49. package/dist/client/transport.d.ts.map +1 -1
  50. package/dist/client/transport.js +131 -16
  51. package/dist/client/transport.js.map +1 -1
  52. package/dist/client/types.d.ts +25 -0
  53. package/dist/client/types.d.ts.map +1 -1
  54. package/dist/index.d.ts +12 -2
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +9 -2
  57. package/dist/index.js.map +1 -1
  58. package/dist/pow/core.d.ts +55 -0
  59. package/dist/pow/core.d.ts.map +1 -0
  60. package/dist/pow/core.js +220 -0
  61. package/dist/pow/core.js.map +1 -0
  62. package/dist/pow/server.d.ts +71 -0
  63. package/dist/pow/server.d.ts.map +1 -0
  64. package/dist/pow/server.js +152 -0
  65. package/dist/pow/server.js.map +1 -0
  66. package/dist/resource/error.d.ts +17 -0
  67. package/dist/resource/error.d.ts.map +1 -1
  68. package/dist/resource/error.js +46 -0
  69. package/dist/resource/error.js.map +1 -1
  70. package/dist/resource/filter.d.ts +1 -0
  71. package/dist/resource/filter.d.ts.map +1 -1
  72. package/dist/resource/filter.js +101 -4
  73. package/dist/resource/filter.js.map +1 -1
  74. package/dist/resource/hook.d.ts.map +1 -1
  75. package/dist/resource/hook.js +73 -3
  76. package/dist/resource/hook.js.map +1 -1
  77. package/dist/resource/procedures.d.ts +3 -2
  78. package/dist/resource/procedures.d.ts.map +1 -1
  79. package/dist/resource/procedures.js +1 -0
  80. package/dist/resource/procedures.js.map +1 -1
  81. package/dist/resource/relation-registry.d.ts +20 -0
  82. package/dist/resource/relation-registry.d.ts.map +1 -0
  83. package/dist/resource/relation-registry.js +12 -0
  84. package/dist/resource/relation-registry.js.map +1 -0
  85. package/dist/resource/search.d.ts +4 -0
  86. package/dist/resource/search.d.ts.map +1 -1
  87. package/dist/resource/search.js +27 -8
  88. package/dist/resource/search.js.map +1 -1
  89. package/dist/resource/secure-query.d.ts.map +1 -1
  90. package/dist/resource/secure-query.js +13 -0
  91. package/dist/resource/secure-query.js.map +1 -1
  92. package/dist/resource/subscription.d.ts.map +1 -1
  93. package/dist/resource/subscription.js +16 -0
  94. package/dist/resource/subscription.js.map +1 -1
  95. package/dist/resource/types.d.ts +18 -5
  96. package/dist/resource/types.d.ts.map +1 -1
  97. package/dist/server/app.d.ts +2 -0
  98. package/dist/server/app.d.ts.map +1 -1
  99. package/dist/server/app.js +4 -0
  100. package/dist/server/app.js.map +1 -1
  101. package/package.json +1 -1
package/README.md CHANGED
@@ -88,6 +88,8 @@ function TodoList() {
88
88
  - **Security Headers** - HSTS, `X-Frame-Options`, MIME-sniffing protection, and more, auto-mounted by `createCovara`; opt-in CSP (so it never blocks your frontend)
89
89
  - **Authorization Scopes** - Row-level security with RSQL expressions, enforced across reads, writes, subscriptions, and search
90
90
  - **Field-level Read Masking** - `fields.readable` allowlist strips non-readable columns from every response and subscription event (cannot be bypassed via `?select=`)
91
+ - **Abuse Protection** - A cost-weighted **token-bucket budget** with **proof of work as the overflow valve** via `abuseProtection({ budget, pow })`: within budget, requests are served and debited; over budget, the server issues a stateless, request-bound, one-time-use **428 challenge** that the **client library solves and retries transparently** (solving pays the overdraft). Difficulty is programmatically controllable via a trust hook; an endpoint can also *always* require PoW. Disable PoW for a hard 429 instead. KV-backed with in-memory fallback; works alongside the conventional `rateLimit`. See [docs](https://kahveciderin.github.io/covara/docs/tooling/abuse-protection)
92
+ - **CAPTCHA** *(beta)* - Turnstile / hCaptcha / reCAPTCHA / custom providers as a per-endpoint gate (signup/login/…) or an alternative budget-overflow valve. The plain client surfaces a typed challenge; the React `<CovaraCaptcha/>` component renders the provider widget and retries transparently.
91
93
 
92
94
  ### File Storage
93
95
  - **Storage Adapters** - Local disk, S3, Cloudflare R2 (native binding or S3-compat), and in-memory behind one `StorageAdapter` interface
@@ -295,7 +297,8 @@ app.resource("/posts", postsTable, {
295
297
  // Optimistic locking (ETag / If-Match)
296
298
  etag: { versionField: "version" },
297
299
 
298
- // Authorization scopes (row-level security via RSQL)
300
+ // Authorization scopes (row-level security via RSQL). Scopes can traverse a
301
+ // declared relation as a join, e.g. rsql`organization.members.userId==${user.id}`
299
302
  auth: {
300
303
  public: { read: true },
301
304
  update: async (user) => rsql`authorId==${user.id}`,
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Token-bucket budget store. Each identity class has a `capacity` and a
3
+ * `refillPerMinute`; operations deduct their declared cost. Exhaustion is a
4
+ * hard rejection (the caller raises 429). Backed by the global KV (or an
5
+ * injected store) with an in-process memory fallback, mirroring the rate-limit
6
+ * store's KV-or-memory pattern.
7
+ */
8
+ import type { KVAdapter } from "../kv/index.js";
9
+ import type { BudgetClassConfig } from "./config.js";
10
+ export interface BudgetConsumeResult {
11
+ allowed: boolean;
12
+ remaining: number;
13
+ retryAfterMs: number;
14
+ }
15
+ export declare class BudgetStore {
16
+ private readonly injected?;
17
+ constructor(injected?: KVAdapter | undefined);
18
+ private get kv();
19
+ private load;
20
+ private save;
21
+ /**
22
+ * Inspect the bucket without deducting: report current (refilled) tokens,
23
+ * whether `cost` fits, the deficit, and the wait for a hard limit. Used to
24
+ * decide whether a request is within budget before any charge.
25
+ */
26
+ assess(key: string, cost: number, cfg: BudgetClassConfig, now?: number): Promise<BudgetConsumeResult & {
27
+ tokens: number;
28
+ sufficient: boolean;
29
+ deficit: number;
30
+ }>;
31
+ /**
32
+ * Debit `cost` from the bucket, flooring at zero (never negative). Used both
33
+ * for an in-budget charge and to settle the overdraft after a solved PoW
34
+ * challenge.
35
+ */
36
+ deduct(key: string, cost: number, cfg: BudgetClassConfig, now?: number): Promise<number>;
37
+ consume(key: string, cost: number, cfg: BudgetClassConfig, now?: number): Promise<BudgetConsumeResult>;
38
+ credit(key: string, amount: number, cfg: BudgetClassConfig, now?: number): Promise<void>;
39
+ }
40
+ export declare const clearBudgetMemoryForTests: () => void;
41
+ //# sourceMappingURL=budget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budget.d.ts","sourceRoot":"","sources":["../../src/abuse/budget.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAEtC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAIlD,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB;AA2BD,qBAAa,WAAW;IACV,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAAT,QAAQ,CAAC,EAAE,SAAS,YAAA;IAEjD,OAAO,KAAK,EAAE,GAEb;YAEa,IAAI;YAuBJ,IAAI;IAwBlB;;;;OAIG;IACG,MAAM,CACV,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,iBAAiB,EACtB,GAAG,SAAa,GACf,OAAO,CAAC,mBAAmB,GAAG;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAc1F;;;;OAIG;IACG,MAAM,CACV,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,iBAAiB,EACtB,GAAG,SAAa,GACf,OAAO,CAAC,MAAM,CAAC;IAQZ,OAAO,CACX,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,iBAAiB,EACtB,GAAG,SAAa,GACf,OAAO,CAAC,mBAAmB,CAAC;IAkBzB,MAAM,CACV,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,iBAAiB,EACtB,GAAG,SAAa,GACf,OAAO,CAAC,IAAI,CAAC;CAMjB;AAED,eAAO,MAAM,yBAAyB,QAAO,IAE5C,CAAC"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Token-bucket budget store. Each identity class has a `capacity` and a
3
+ * `refillPerMinute`; operations deduct their declared cost. Exhaustion is a
4
+ * hard rejection (the caller raises 429). Backed by the global KV (or an
5
+ * injected store) with an in-process memory fallback, mirroring the rate-limit
6
+ * store's KV-or-memory pattern.
7
+ */
8
+ import { getGlobalKV, hasGlobalKV } from "../kv/index.js";
9
+ const BUDGET_PREFIX = "covara:budget:";
10
+ const memoryBuckets = new Map();
11
+ const refill = (state, cfg, now) => {
12
+ const elapsedMs = Math.max(0, now - state.lastRefillMs);
13
+ const refilled = (elapsedMs / 60000) * cfg.refillPerMinute;
14
+ return {
15
+ tokens: Math.min(cfg.capacity, state.tokens + refilled),
16
+ lastRefillMs: now,
17
+ };
18
+ };
19
+ const retryAfterMsFor = (deficit, cfg) => {
20
+ if (cfg.refillPerMinute <= 0)
21
+ return Number.MAX_SAFE_INTEGER;
22
+ return Math.ceil((deficit / cfg.refillPerMinute) * 60000);
23
+ };
24
+ export class BudgetStore {
25
+ constructor(injected) {
26
+ this.injected = injected;
27
+ }
28
+ get kv() {
29
+ return this.injected ?? (hasGlobalKV() ? getGlobalKV() : null);
30
+ }
31
+ async load(key, cfg, now) {
32
+ const kv = this.kv;
33
+ if (!kv) {
34
+ const existing = memoryBuckets.get(key);
35
+ return existing
36
+ ? refill(existing, cfg, now)
37
+ : { tokens: cfg.capacity, lastRefillMs: now };
38
+ }
39
+ const data = await kv.hgetall(`${BUDGET_PREFIX}${key}`);
40
+ if (!data || data.tokens === undefined || data.lastRefillMs === undefined) {
41
+ return { tokens: cfg.capacity, lastRefillMs: now };
42
+ }
43
+ return refill({ tokens: parseFloat(data.tokens), lastRefillMs: parseInt(data.lastRefillMs, 10) }, cfg, now);
44
+ }
45
+ async save(key, state, cfg) {
46
+ const kv = this.kv;
47
+ if (!kv) {
48
+ memoryBuckets.set(key, state);
49
+ return;
50
+ }
51
+ const kvKey = `${BUDGET_PREFIX}${key}`;
52
+ await kv.hmset(kvKey, {
53
+ tokens: String(state.tokens),
54
+ lastRefillMs: String(state.lastRefillMs),
55
+ });
56
+ const secondsToFull = cfg.refillPerMinute > 0
57
+ ? Math.ceil(((cfg.capacity - state.tokens) / cfg.refillPerMinute) * 60) + 1
58
+ : 0;
59
+ if (secondsToFull > 0) {
60
+ await kv.expire(kvKey, secondsToFull);
61
+ }
62
+ }
63
+ /**
64
+ * Inspect the bucket without deducting: report current (refilled) tokens,
65
+ * whether `cost` fits, the deficit, and the wait for a hard limit. Used to
66
+ * decide whether a request is within budget before any charge.
67
+ */
68
+ async assess(key, cost, cfg, now = Date.now()) {
69
+ const state = await this.load(key, cfg, now);
70
+ const sufficient = state.tokens >= cost;
71
+ const deficit = Math.max(0, cost - state.tokens);
72
+ return {
73
+ allowed: sufficient,
74
+ sufficient,
75
+ tokens: state.tokens,
76
+ remaining: state.tokens,
77
+ deficit,
78
+ retryAfterMs: sufficient ? 0 : retryAfterMsFor(deficit, cfg),
79
+ };
80
+ }
81
+ /**
82
+ * Debit `cost` from the bucket, flooring at zero (never negative). Used both
83
+ * for an in-budget charge and to settle the overdraft after a solved PoW
84
+ * challenge.
85
+ */
86
+ async deduct(key, cost, cfg, now = Date.now()) {
87
+ if (cost <= 0)
88
+ return (await this.load(key, cfg, now)).tokens;
89
+ const state = await this.load(key, cfg, now);
90
+ state.tokens = Math.max(0, state.tokens - cost);
91
+ await this.save(key, state, cfg);
92
+ return state.tokens;
93
+ }
94
+ async consume(key, cost, cfg, now = Date.now()) {
95
+ const state = await this.load(key, cfg, now);
96
+ if (cost <= 0) {
97
+ return { allowed: true, remaining: state.tokens, retryAfterMs: 0 };
98
+ }
99
+ if (state.tokens >= cost) {
100
+ state.tokens -= cost;
101
+ await this.save(key, state, cfg);
102
+ return { allowed: true, remaining: state.tokens, retryAfterMs: 0 };
103
+ }
104
+ await this.save(key, state, cfg);
105
+ return {
106
+ allowed: false,
107
+ remaining: state.tokens,
108
+ retryAfterMs: retryAfterMsFor(cost - state.tokens, cfg),
109
+ };
110
+ }
111
+ async credit(key, amount, cfg, now = Date.now()) {
112
+ if (amount <= 0)
113
+ return;
114
+ const state = await this.load(key, cfg, now);
115
+ state.tokens = Math.min(cfg.capacity, state.tokens + amount);
116
+ await this.save(key, state, cfg);
117
+ }
118
+ }
119
+ export const clearBudgetMemoryForTests = () => {
120
+ memoryBuckets.clear();
121
+ };
122
+ //# sourceMappingURL=budget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budget.js","sourceRoot":"","sources":["../../src/abuse/budget.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,MAAM,CAAC;AAGhD,MAAM,aAAa,GAAG,gBAAgB,CAAC;AAavC,MAAM,aAAa,GAAG,IAAI,GAAG,EAAuB,CAAC;AAErD,MAAM,MAAM,GAAG,CACb,KAAkB,EAClB,GAAsB,EACtB,GAAW,EACE,EAAE;IACf,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,KAAK,CAAC,YAAY,CAAC,CAAC;IACxD,MAAM,QAAQ,GAAG,CAAC,SAAS,GAAG,KAAM,CAAC,GAAG,GAAG,CAAC,eAAe,CAAC;IAC5D,OAAO;QACL,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,MAAM,GAAG,QAAQ,CAAC;QACvD,YAAY,EAAE,GAAG;KAClB,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,eAAe,GAAG,CAAC,OAAe,EAAE,GAAsB,EAAU,EAAE;IAC1E,IAAI,GAAG,CAAC,eAAe,IAAI,CAAC;QAAE,OAAO,MAAM,CAAC,gBAAgB,CAAC;IAC7D,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,GAAG,GAAG,CAAC,eAAe,CAAC,GAAG,KAAM,CAAC,CAAC;AAC7D,CAAC,CAAC;AAEF,MAAM,OAAO,WAAW;IACtB,YAA6B,QAAoB;QAApB,aAAQ,GAAR,QAAQ,CAAY;IAAG,CAAC;IAErD,IAAY,EAAE;QACZ,OAAO,IAAI,CAAC,QAAQ,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACjE,CAAC;IAEO,KAAK,CAAC,IAAI,CAChB,GAAW,EACX,GAAsB,EACtB,GAAW;QAEX,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC;QACnB,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACxC,OAAO,QAAQ;gBACb,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,CAAC;gBAC5B,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC;QAClD,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,aAAa,GAAG,GAAG,EAAE,CAAC,CAAC;QACxD,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;YAC1E,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC;QACrD,CAAC;QACD,OAAO,MAAM,CACX,EAAE,MAAM,EAAE,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,YAAY,EAAE,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,CAAC,EAAE,EAClF,GAAG,EACH,GAAG,CACJ,CAAC;IACJ,CAAC;IAEO,KAAK,CAAC,IAAI,CAChB,GAAW,EACX,KAAkB,EAClB,GAAsB;QAEtB,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC;QACnB,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YAC9B,OAAO;QACT,CAAC;QACD,MAAM,KAAK,GAAG,GAAG,aAAa,GAAG,GAAG,EAAE,CAAC;QACvC,MAAM,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE;YACpB,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;YAC5B,YAAY,EAAE,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC;SACzC,CAAC,CAAC;QACH,MAAM,aAAa,GACjB,GAAG,CAAC,eAAe,GAAG,CAAC;YACrB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,eAAe,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC;YAC3E,CAAC,CAAC,CAAC,CAAC;QACR,IAAI,aAAa,GAAG,CAAC,EAAE,CAAC;YACtB,MAAM,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC;QACxC,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,MAAM,CACV,GAAW,EACX,IAAY,EACZ,GAAsB,EACtB,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;QAEhB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;QAC7C,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC;QACxC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;QACjD,OAAO;YACL,OAAO,EAAE,UAAU;YACnB,UAAU;YACV,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,SAAS,EAAE,KAAK,CAAC,MAAM;YACvB,OAAO;YACP,YAAY,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,OAAO,EAAE,GAAG,CAAC;SAC7D,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,MAAM,CACV,GAAW,EACX,IAAY,EACZ,GAAsB,EACtB,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;QAEhB,IAAI,IAAI,IAAI,CAAC;YAAE,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC;QAC9D,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;QAC7C,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;QAChD,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;QACjC,OAAO,KAAK,CAAC,MAAM,CAAC;IACtB,CAAC;IAED,KAAK,CAAC,OAAO,CACX,GAAW,EACX,IAAY,EACZ,GAAsB,EACtB,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;QAEhB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;QAC7C,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC;YACd,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;QACrE,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,IAAI,IAAI,EAAE,CAAC;YACzB,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC;YACrB,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;YACjC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;QACrE,CAAC;QACD,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;QACjC,OAAO;YACL,OAAO,EAAE,KAAK;YACd,SAAS,EAAE,KAAK,CAAC,MAAM;YACvB,YAAY,EAAE,eAAe,CAAC,IAAI,GAAG,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC;SACxD,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,MAAM,CACV,GAAW,EACX,MAAc,EACd,GAAsB,EACtB,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;QAEhB,IAAI,MAAM,IAAI,CAAC;YAAE,OAAO;QACxB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;QAC7C,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;QAC7D,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;IACnC,CAAC;CACF;AAED,MAAM,CAAC,MAAM,yBAAyB,GAAG,GAAS,EAAE;IAClD,aAAa,CAAC,KAAK,EAAE,CAAC;AACxB,CAAC,CAAC"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * CAPTCHA providers (BETA). Fetch-based token verification against the common
3
+ * managed providers plus a generic escape hatch. Verification is fail-closed:
4
+ * any network/parse error or a falsy provider result rejects the token.
5
+ *
6
+ * Runs on Node and Cloudflare Workers (uses the global `fetch`, no node deps).
7
+ */
8
+ export interface CaptchaVerifyContext {
9
+ ip?: string;
10
+ action?: string;
11
+ }
12
+ export interface CaptchaProvider {
13
+ /** "turnstile" | "hcaptcha" | "recaptcha" | "custom" (or any custom name). */
14
+ name: string;
15
+ /** Public site key, surfaced to the client so it can render the widget. */
16
+ siteKey?: string;
17
+ verify(token: string, ctx?: CaptchaVerifyContext): Promise<boolean>;
18
+ }
19
+ export interface TurnstileOptions {
20
+ secret: string;
21
+ siteKey?: string;
22
+ verifyUrl?: string;
23
+ }
24
+ export declare const turnstile: (options: TurnstileOptions) => CaptchaProvider;
25
+ export interface HcaptchaOptions {
26
+ secret: string;
27
+ siteKey?: string;
28
+ verifyUrl?: string;
29
+ }
30
+ export declare const hcaptcha: (options: HcaptchaOptions) => CaptchaProvider;
31
+ export interface RecaptchaOptions {
32
+ secret: string;
33
+ siteKey?: string;
34
+ /** v3 score threshold (0..1). When set, the score must be >= minScore. */
35
+ minScore?: number;
36
+ verifyUrl?: string;
37
+ }
38
+ export declare const recaptcha: (options: RecaptchaOptions) => CaptchaProvider;
39
+ export interface CustomCaptchaOptions {
40
+ name?: string;
41
+ siteKey?: string;
42
+ verify: (token: string, ctx?: CaptchaVerifyContext) => Promise<boolean> | boolean;
43
+ }
44
+ export declare const customCaptcha: (options: CustomCaptchaOptions) => CaptchaProvider;
45
+ //# sourceMappingURL=captcha.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captcha.d.ts","sourceRoot":"","sources":["../../src/abuse/captcha.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,MAAM,WAAW,oBAAoB;IACnC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACrE;AAkCD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,SAAS,GAAI,SAAS,gBAAgB,KAAG,eAUpD,CAAC;AAEH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,QAAQ,GAAI,SAAS,eAAe,KAAG,eAUlD,CAAC;AAEH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,SAAS,GAAI,SAAS,gBAAgB,KAAG,eAiBpD,CAAC;AAEH,MAAM,WAAW,oBAAoB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,oBAAoB,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;CACnF;AAED,eAAO,MAAM,aAAa,GAAI,SAAS,oBAAoB,KAAG,eAa5D,CAAC"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * CAPTCHA providers (BETA). Fetch-based token verification against the common
3
+ * managed providers plus a generic escape hatch. Verification is fail-closed:
4
+ * any network/parse error or a falsy provider result rejects the token.
5
+ *
6
+ * Runs on Node and Cloudflare Workers (uses the global `fetch`, no node deps).
7
+ */
8
+ import { getLogger } from "../server/logger.js";
9
+ const postSiteVerify = async (url, fields) => {
10
+ const body = new URLSearchParams();
11
+ for (const [key, value] of Object.entries(fields)) {
12
+ if (value !== undefined && value !== "")
13
+ body.set(key, value);
14
+ }
15
+ try {
16
+ const res = await fetch(url, {
17
+ method: "POST",
18
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
19
+ body,
20
+ });
21
+ if (!res.ok)
22
+ return null;
23
+ return (await res.json());
24
+ }
25
+ catch (error) {
26
+ getLogger().warn("CAPTCHA verification request failed", {
27
+ url,
28
+ error: error instanceof Error ? error.message : String(error),
29
+ });
30
+ return null;
31
+ }
32
+ };
33
+ export const turnstile = (options) => ({
34
+ name: "turnstile",
35
+ siteKey: options.siteKey,
36
+ async verify(token, ctx) {
37
+ const data = await postSiteVerify(options.verifyUrl ?? "https://challenges.cloudflare.com/turnstile/v0/siteverify", { secret: options.secret, response: token, remoteip: ctx?.ip });
38
+ return data?.success === true;
39
+ },
40
+ });
41
+ export const hcaptcha = (options) => ({
42
+ name: "hcaptcha",
43
+ siteKey: options.siteKey,
44
+ async verify(token, ctx) {
45
+ const data = await postSiteVerify(options.verifyUrl ?? "https://api.hcaptcha.com/siteverify", { secret: options.secret, response: token, remoteip: ctx?.ip });
46
+ return data?.success === true;
47
+ },
48
+ });
49
+ export const recaptcha = (options) => ({
50
+ name: "recaptcha",
51
+ siteKey: options.siteKey,
52
+ async verify(token, ctx) {
53
+ const data = await postSiteVerify(options.verifyUrl ?? "https://www.google.com/recaptcha/api/siteverify", { secret: options.secret, response: token, remoteip: ctx?.ip });
54
+ if (data?.success !== true)
55
+ return false;
56
+ if (options.minScore !== undefined && (data.score ?? 0) < options.minScore) {
57
+ return false;
58
+ }
59
+ if (ctx?.action && data.action && data.action !== ctx.action) {
60
+ return false;
61
+ }
62
+ return true;
63
+ },
64
+ });
65
+ export const customCaptcha = (options) => ({
66
+ name: options.name ?? "custom",
67
+ siteKey: options.siteKey,
68
+ async verify(token, ctx) {
69
+ try {
70
+ return await options.verify(token, ctx);
71
+ }
72
+ catch (error) {
73
+ getLogger().warn("Custom CAPTCHA verifier threw", {
74
+ error: error instanceof Error ? error.message : String(error),
75
+ });
76
+ return false;
77
+ }
78
+ },
79
+ });
80
+ //# sourceMappingURL=captcha.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captcha.js","sourceRoot":"","sources":["../../src/abuse/captcha.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Abuse-protection configuration: a token-bucket `budget` (cost-weighted rate
3
+ * limiting, hard-rejects with 429) and a `pow` proof-of-work gate, which are
4
+ * fully independent and may be enabled together. Costs and PoW opt-ins live on
5
+ * the individual endpoints; this module holds only the shared/global settings.
6
+ */
7
+ import type { Context } from "hono";
8
+ import type { KVAdapter } from "../kv/index.js";
9
+ import type { UserContext } from "../resource/types.js";
10
+ import type { PowAlgorithm } from "../pow/core.js";
11
+ import type { CaptchaProvider } from "./captcha.js";
12
+ export interface BudgetClassConfig {
13
+ capacity: number;
14
+ refillPerMinute: number;
15
+ }
16
+ export interface BudgetConfig {
17
+ enabled?: boolean;
18
+ /**
19
+ * Map a request to an identity class name. Defaults to "authenticated" for a
20
+ * logged-in user and "anonymous" otherwise.
21
+ */
22
+ classify?: (c: Context) => string;
23
+ /** Token bucket parameters per identity class. */
24
+ classes: Record<string, BudgetClassConfig>;
25
+ /**
26
+ * Derive the bucket key for a request within its class. Defaults to the user
27
+ * id (authenticated) or client IP (anonymous), namespaced by class.
28
+ */
29
+ keyGenerator?: (c: Context, className: string) => string;
30
+ /** Optional explicit KV store; defaults to the global KV, else in-memory. */
31
+ store?: KVAdapter;
32
+ }
33
+ export interface PowDifficultyContext {
34
+ c: Context;
35
+ user: UserContext | null;
36
+ ip: string;
37
+ /** Operation key, e.g. "read", "create", "rpc:generate", "auth.signup". */
38
+ operation: string;
39
+ resource?: string;
40
+ /** The statically configured difficulty before any hook adjustment. */
41
+ baseDifficulty: number;
42
+ /**
43
+ * Why the challenge is being considered:
44
+ * - "endpoint": the endpoint always requires PoW (its `pow` opt-in).
45
+ * - "budget": the caller is over budget, so PoW is the overflow valve.
46
+ */
47
+ reason: "endpoint" | "budget";
48
+ /** Budget cost of the operation (present when reason === "budget"). */
49
+ cost?: number;
50
+ /** Tokens available before the charge (present when reason === "budget"). */
51
+ available?: number;
52
+ /** How far over budget the request is, `cost - available` (budget reason). */
53
+ deficit?: number;
54
+ }
55
+ export interface PowConfig {
56
+ enabled?: boolean;
57
+ secret?: string;
58
+ /** Default leading-zero-bit difficulty when an endpoint enables PoW. */
59
+ difficulty?: number;
60
+ /**
61
+ * Programmatic difficulty control (IP/user trust). Return 0 to skip the
62
+ * challenge for trusted callers. Overrides the static difficulty.
63
+ */
64
+ getDifficulty?: (ctx: PowDifficultyContext) => number | Promise<number>;
65
+ challengeTtlMs?: number;
66
+ algorithm?: PowAlgorithm;
67
+ /** Optional explicit KV store for the replay cache; defaults to global KV. */
68
+ store?: KVAdapter;
69
+ /** Bind challenges to a hash of the request body (default true). */
70
+ bindBody?: boolean;
71
+ }
72
+ /** Which mechanism a budget-exhausted (overflow) request is challenged with. */
73
+ export type OverflowMechanism = "pow" | "captcha";
74
+ export interface CaptchaContext {
75
+ c: Context;
76
+ user: UserContext | null;
77
+ ip: string;
78
+ operation: string;
79
+ resource?: string;
80
+ reason: "endpoint" | "budget";
81
+ }
82
+ export interface CaptchaConfig {
83
+ /** BETA. The verification provider (turnstile/hcaptcha/recaptcha/custom). */
84
+ provider: CaptchaProvider;
85
+ enabled?: boolean;
86
+ /** Request header carrying the solved token (default "Covara-Captcha-Token"). */
87
+ tokenHeader?: string;
88
+ }
89
+ export interface AbuseProtectionInput {
90
+ budget?: BudgetConfig;
91
+ pow?: PowConfig;
92
+ /** CAPTCHA challenge support (BETA). */
93
+ captcha?: CaptchaConfig;
94
+ /** Default mechanism for budget-overflow challenges (default "pow"). */
95
+ overflow?: OverflowMechanism;
96
+ }
97
+ export interface AbuseProtectionConfig {
98
+ budget: BudgetConfig | null;
99
+ pow: PowConfig | null;
100
+ captcha: CaptchaConfig | null;
101
+ overflow: OverflowMechanism;
102
+ }
103
+ export declare const DEFAULT_POW_DIFFICULTY = 20;
104
+ export declare const DEFAULT_POW_TTL_MS = 120000;
105
+ /**
106
+ * Per-endpoint PoW opt-in (procedures, auth routes). `true` uses the
107
+ * global/default difficulty; an object overrides difficulty or supplies an
108
+ * endpoint-specific trust hook.
109
+ */
110
+ export type EndpointPowConfig = boolean | {
111
+ difficulty?: number;
112
+ getDifficulty?: (ctx: PowDifficultyContext) => number | Promise<number>;
113
+ };
114
+ /** Operations that can carry an inline budget cost or PoW gate on a resource. */
115
+ export type AbuseOperation = "read" | "create" | "update" | "delete" | "subscribe" | "count" | "aggregate" | "search";
116
+ /** Inline per-operation budget costs declared on a resource. */
117
+ export type ResourceCostConfig = Partial<Record<AbuseOperation, number>>;
118
+ /**
119
+ * Resource-level PoW opt-in. `true` gates the mutating operations
120
+ * (create/update/delete) at the default difficulty. An object can override the
121
+ * difficulty/trust hook and restrict (or widen) the gated `operations`.
122
+ */
123
+ export type ResourcePowConfig = boolean | {
124
+ difficulty?: number;
125
+ getDifficulty?: (ctx: PowDifficultyContext) => number | Promise<number>;
126
+ operations?: AbuseOperation[];
127
+ };
128
+ /**
129
+ * Per-endpoint CAPTCHA opt-in (procedures, auth routes). `true` always requires
130
+ * a CAPTCHA; an object can supply a reCAPTCHA v3 `action` or a `required` hook.
131
+ */
132
+ export type EndpointCaptchaConfig = boolean | {
133
+ action?: string;
134
+ required?: (ctx: CaptchaContext) => boolean | Promise<boolean>;
135
+ };
136
+ /** Resource-level CAPTCHA opt-in; `operations` restricts which it gates. */
137
+ export type ResourceCaptchaConfig = boolean | {
138
+ action?: string;
139
+ required?: (ctx: CaptchaContext) => boolean | Promise<boolean>;
140
+ operations?: AbuseOperation[];
141
+ };
142
+ export declare const abuseProtection: (input: AbuseProtectionInput) => AbuseProtectionConfig;
143
+ export declare const setGlobalAbuseProtection: (config: AbuseProtectionConfig | AbuseProtectionInput) => void;
144
+ export declare const getGlobalAbuseProtection: () => AbuseProtectionConfig | null;
145
+ export declare const hasGlobalAbuseProtection: () => boolean;
146
+ export declare const clearGlobalAbuseProtection: () => void;
147
+ export declare const isBudgetEnabled: (config: AbuseProtectionConfig | null) => boolean;
148
+ export declare const isPowEnabled: (config: AbuseProtectionConfig | null) => boolean;
149
+ export declare const isCaptchaEnabled: (config: AbuseProtectionConfig | null) => boolean;
150
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/abuse/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Abuse-protection configuration: a token-bucket `budget` (cost-weighted rate
3
+ * limiting, hard-rejects with 429) and a `pow` proof-of-work gate, which are
4
+ * fully independent and may be enabled together. Costs and PoW opt-ins live on
5
+ * the individual endpoints; this module holds only the shared/global settings.
6
+ */
7
+ export const DEFAULT_POW_DIFFICULTY = 20;
8
+ export const DEFAULT_POW_TTL_MS = 120000;
9
+ const normalize = (input) => ({
10
+ budget: input.budget ?? null,
11
+ pow: input.pow ?? null,
12
+ captcha: input.captcha ?? null,
13
+ overflow: input.overflow ?? "pow",
14
+ });
15
+ let globalConfig = null;
16
+ export const abuseProtection = (input) => {
17
+ const config = normalize(input);
18
+ setGlobalAbuseProtection(config);
19
+ return config;
20
+ };
21
+ export const setGlobalAbuseProtection = (config) => {
22
+ globalConfig = normalize(config);
23
+ };
24
+ export const getGlobalAbuseProtection = () => globalConfig;
25
+ export const hasGlobalAbuseProtection = () => globalConfig !== null;
26
+ export const clearGlobalAbuseProtection = () => {
27
+ globalConfig = null;
28
+ };
29
+ export const isBudgetEnabled = (config) => !!config?.budget && config.budget.enabled !== false &&
30
+ Object.keys(config.budget.classes ?? {}).length > 0;
31
+ export const isPowEnabled = (config) => !config?.pow || config.pow.enabled !== false;
32
+ export const isCaptchaEnabled = (config) => !!config?.captcha && config.captcha.enabled !== false;
33
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/abuse/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA4GH,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,CAAC;AACzC,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAO,CAAC;AA6D1C,MAAM,SAAS,GAAG,CAChB,KAAmD,EAC5B,EAAE,CAAC,CAAC;IAC3B,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,IAAI;IAC5B,GAAG,EAAE,KAAK,CAAC,GAAG,IAAI,IAAI;IACtB,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,IAAI;IAC9B,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,KAAK;CAClC,CAAC,CAAC;AAEH,IAAI,YAAY,GAAiC,IAAI,CAAC;AAEtD,MAAM,CAAC,MAAM,eAAe,GAAG,CAC7B,KAA2B,EACJ,EAAE;IACzB,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;IAChC,wBAAwB,CAAC,MAAM,CAAC,CAAC;IACjC,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,wBAAwB,GAAG,CACtC,MAAoD,EAC9C,EAAE;IACR,YAAY,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;AACnC,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAiC,EAAE,CACzE,YAAY,CAAC;AAEf,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAY,EAAE,CAAC,YAAY,KAAK,IAAI,CAAC;AAE7E,MAAM,CAAC,MAAM,0BAA0B,GAAG,GAAS,EAAE;IACnD,YAAY,GAAG,IAAI,CAAC;AACtB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,MAAoC,EAAW,EAAE,CAC/E,CAAC,CAAC,MAAM,EAAE,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,KAAK,KAAK;IACnD,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;AAEtD,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAoC,EAAW,EAAE,CAC5E,CAAC,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,GAAG,CAAC,OAAO,KAAK,KAAK,CAAC;AAE/C,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,MAAoC,EAAW,EAAE,CAChF,CAAC,CAAC,MAAM,EAAE,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,KAAK,KAAK,CAAC"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Unified abuse enforcement. A request's cost is charged against a token-bucket
3
+ * budget; when the budget is exhausted, proof of work is the overflow valve —
4
+ * the server issues a 428 challenge (difficulty scalable via a trust hook)
5
+ * instead of a hard rejection, and solving it pays the overdraft (the bucket is
6
+ * drained to zero). An endpoint can also *always* require PoW via its `pow`
7
+ * opt-in, independent of budget. When PoW is disabled, budget exhaustion falls
8
+ * back to a hard 429.
9
+ */
10
+ import type { Context } from "hono";
11
+ import { type EndpointCaptchaConfig, type EndpointPowConfig, type OverflowMechanism } from "./config.js";
12
+ export interface EnforceAbuseOptions {
13
+ operation: string;
14
+ resource?: string;
15
+ /** Budget cost of this operation (0/undefined = not charged). */
16
+ cost?: number;
17
+ /** Endpoint always-PoW opt-in (independent of budget). */
18
+ endpointPow?: EndpointPowConfig;
19
+ /** Endpoint always-CAPTCHA opt-in (independent of budget). */
20
+ endpointCaptcha?: EndpointCaptchaConfig;
21
+ /** Override the budget-overflow mechanism for this endpoint. */
22
+ overflow?: OverflowMechanism;
23
+ /**
24
+ * Defer the budget debit: don't charge on allow, instead return a `settle()`
25
+ * the caller invokes when it decides the request counts (e.g. a failed
26
+ * login). When false (default) the cost is debited before returning.
27
+ */
28
+ defer?: boolean;
29
+ }
30
+ export interface AbuseGateResult {
31
+ /**
32
+ * Debit the operation's cost (floored at zero). A no-op in immediate mode
33
+ * (already charged); in deferred mode the caller invokes it to commit the
34
+ * charge. Safe to call at most once.
35
+ */
36
+ settle(): Promise<void>;
37
+ }
38
+ /**
39
+ * Charge an operation against the budget, gating with proof of work as needed.
40
+ * Throws `PowRequiredError` (428) when a challenge is required and unsolved, or
41
+ * `RateLimitError` (429) when over budget and PoW is disabled.
42
+ */
43
+ export declare const enforceAbuse: (c: Context, options: EnforceAbuseOptions) => Promise<AbuseGateResult>;
44
+ //# sourceMappingURL=enforce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enforce.d.ts","sourceRoot":"","sources":["../../src/abuse/enforce.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAYpC,OAAO,EAQL,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EAGvB,MAAM,UAAU,CAAC;AAwBlB,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,8DAA8D;IAC9D,eAAe,CAAC,EAAE,qBAAqB,CAAC;IACxC,gEAAgE;IAChE,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AAYD;;;;GAIG;AACH,eAAO,MAAM,YAAY,GACvB,GAAG,OAAO,EACV,SAAS,mBAAmB,KAC3B,OAAO,CAAC,eAAe,CAiJzB,CAAC"}