@grantex/sdk 0.5.1 → 0.7.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 (185) hide show
  1. package/LICENSE +185 -0
  2. package/NOTICE +16 -0
  3. package/README.md +34 -2
  4. package/dist/authorization-details.d.ts +86 -0
  5. package/dist/authorization-details.d.ts.map +1 -0
  6. package/dist/authorization-details.js +209 -0
  7. package/dist/authorization-details.js.map +1 -0
  8. package/dist/canonical.d.ts +47 -0
  9. package/dist/canonical.d.ts.map +1 -0
  10. package/dist/canonical.js +163 -0
  11. package/dist/canonical.js.map +1 -0
  12. package/dist/caps/index.d.ts +19 -0
  13. package/dist/caps/index.d.ts.map +1 -0
  14. package/dist/caps/index.js +19 -0
  15. package/dist/caps/index.js.map +1 -0
  16. package/dist/caps/limits.d.ts +40 -0
  17. package/dist/caps/limits.d.ts.map +1 -0
  18. package/dist/caps/limits.js +109 -0
  19. package/dist/caps/limits.js.map +1 -0
  20. package/dist/caps/memory.d.ts +15 -0
  21. package/dist/caps/memory.d.ts.map +1 -0
  22. package/dist/caps/memory.js +57 -0
  23. package/dist/caps/memory.js.map +1 -0
  24. package/dist/caps/meter.d.ts +107 -0
  25. package/dist/caps/meter.d.ts.map +1 -0
  26. package/dist/caps/meter.js +200 -0
  27. package/dist/caps/meter.js.map +1 -0
  28. package/dist/caps/postgres.d.ts +64 -0
  29. package/dist/caps/postgres.d.ts.map +1 -0
  30. package/dist/caps/postgres.js +221 -0
  31. package/dist/caps/postgres.js.map +1 -0
  32. package/dist/caps/redis.d.ts +45 -0
  33. package/dist/caps/redis.d.ts.map +1 -0
  34. package/dist/caps/redis.js +173 -0
  35. package/dist/caps/redis.js.map +1 -0
  36. package/dist/client.d.ts +12 -0
  37. package/dist/client.d.ts.map +1 -1
  38. package/dist/client.js +517 -57
  39. package/dist/client.js.map +1 -1
  40. package/dist/decisions/action.d.ts +51 -0
  41. package/dist/decisions/action.d.ts.map +1 -0
  42. package/dist/decisions/action.js +237 -0
  43. package/dist/decisions/action.js.map +1 -0
  44. package/dist/decisions/index.d.ts +11 -0
  45. package/dist/decisions/index.d.ts.map +1 -0
  46. package/dist/decisions/index.js +8 -0
  47. package/dist/decisions/index.js.map +1 -0
  48. package/dist/decisions/verify.d.ts +103 -0
  49. package/dist/decisions/verify.d.ts.map +1 -0
  50. package/dist/decisions/verify.js +277 -0
  51. package/dist/decisions/verify.js.map +1 -0
  52. package/dist/denials.d.ts +132 -0
  53. package/dist/denials.d.ts.map +1 -0
  54. package/dist/denials.js +129 -0
  55. package/dist/denials.js.map +1 -0
  56. package/dist/evidence/build.d.ts +63 -0
  57. package/dist/evidence/build.d.ts.map +1 -0
  58. package/dist/evidence/build.js +201 -0
  59. package/dist/evidence/build.js.map +1 -0
  60. package/dist/evidence/canonical.d.ts +3 -0
  61. package/dist/evidence/canonical.d.ts.map +1 -0
  62. package/dist/evidence/canonical.js +3 -0
  63. package/dist/evidence/canonical.js.map +1 -0
  64. package/dist/evidence/checks.d.ts +13 -0
  65. package/dist/evidence/checks.d.ts.map +1 -0
  66. package/dist/evidence/checks.js +406 -0
  67. package/dist/evidence/checks.js.map +1 -0
  68. package/dist/evidence/document.d.ts +8 -0
  69. package/dist/evidence/document.d.ts.map +1 -0
  70. package/dist/evidence/document.js +298 -0
  71. package/dist/evidence/document.js.map +1 -0
  72. package/dist/evidence/hashing.d.ts +37 -0
  73. package/dist/evidence/hashing.d.ts.map +1 -0
  74. package/dist/evidence/hashing.js +106 -0
  75. package/dist/evidence/hashing.js.map +1 -0
  76. package/dist/evidence/index.d.ts +24 -0
  77. package/dist/evidence/index.d.ts.map +1 -0
  78. package/dist/evidence/index.js +24 -0
  79. package/dist/evidence/index.js.map +1 -0
  80. package/dist/evidence/result.d.ts +88 -0
  81. package/dist/evidence/result.d.ts.map +1 -0
  82. package/dist/evidence/result.js +88 -0
  83. package/dist/evidence/result.js.map +1 -0
  84. package/dist/evidence/schema-1.0.d.ts +2 -0
  85. package/dist/evidence/schema-1.0.d.ts.map +1 -0
  86. package/dist/evidence/schema-1.0.js +1398 -0
  87. package/dist/evidence/schema-1.0.js.map +1 -0
  88. package/dist/evidence/schema.d.ts +13 -0
  89. package/dist/evidence/schema.d.ts.map +1 -0
  90. package/dist/evidence/schema.js +264 -0
  91. package/dist/evidence/schema.js.map +1 -0
  92. package/dist/evidence/signature.d.ts +23 -0
  93. package/dist/evidence/signature.d.ts.map +1 -0
  94. package/dist/evidence/signature.js +136 -0
  95. package/dist/evidence/signature.js.map +1 -0
  96. package/dist/evidence/trace.d.ts +23 -0
  97. package/dist/evidence/trace.d.ts.map +1 -0
  98. package/dist/evidence/trace.js +69 -0
  99. package/dist/evidence/trace.js.map +1 -0
  100. package/dist/evidence/verify.d.ts +29 -0
  101. package/dist/evidence/verify.d.ts.map +1 -0
  102. package/dist/evidence/verify.js +163 -0
  103. package/dist/evidence/verify.js.map +1 -0
  104. package/dist/http.d.ts +12 -1
  105. package/dist/http.d.ts.map +1 -1
  106. package/dist/http.js +11 -5
  107. package/dist/http.js.map +1 -1
  108. package/dist/index.d.ts +16 -3
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +16 -2
  111. package/dist/index.js.map +1 -1
  112. package/dist/manifest.d.ts +178 -9
  113. package/dist/manifest.d.ts.map +1 -1
  114. package/dist/manifest.js +379 -16
  115. package/dist/manifest.js.map +1 -1
  116. package/dist/prepaid-wallets.d.ts +21 -0
  117. package/dist/prepaid-wallets.d.ts.map +1 -1
  118. package/dist/prepaid-wallets.js +6 -0
  119. package/dist/prepaid-wallets.js.map +1 -1
  120. package/dist/purpose.d.ts +29 -0
  121. package/dist/purpose.d.ts.map +1 -0
  122. package/dist/purpose.js +60 -0
  123. package/dist/purpose.js.map +1 -0
  124. package/dist/resources/agents.js +3 -3
  125. package/dist/resources/agents.js.map +1 -1
  126. package/dist/resources/anomalies.d.ts +3 -1
  127. package/dist/resources/anomalies.d.ts.map +1 -1
  128. package/dist/resources/anomalies.js +7 -1
  129. package/dist/resources/anomalies.js.map +1 -1
  130. package/dist/resources/audit.js +1 -1
  131. package/dist/resources/audit.js.map +1 -1
  132. package/dist/resources/budgets.js +2 -2
  133. package/dist/resources/budgets.js.map +1 -1
  134. package/dist/resources/credentials.js +1 -1
  135. package/dist/resources/credentials.js.map +1 -1
  136. package/dist/resources/decisions.d.ts +69 -0
  137. package/dist/resources/decisions.d.ts.map +1 -0
  138. package/dist/resources/decisions.js +115 -0
  139. package/dist/resources/decisions.js.map +1 -0
  140. package/dist/resources/domains.js +2 -2
  141. package/dist/resources/domains.js.map +1 -1
  142. package/dist/resources/dpdp.js +4 -4
  143. package/dist/resources/dpdp.js.map +1 -1
  144. package/dist/resources/grants.js +2 -2
  145. package/dist/resources/grants.js.map +1 -1
  146. package/dist/resources/policies.js +3 -3
  147. package/dist/resources/policies.js.map +1 -1
  148. package/dist/resources/scim.js +5 -5
  149. package/dist/resources/scim.js.map +1 -1
  150. package/dist/resources/tokens.d.ts.map +1 -1
  151. package/dist/resources/tokens.js +5 -1
  152. package/dist/resources/tokens.js.map +1 -1
  153. package/dist/resources/vault.js +2 -2
  154. package/dist/resources/vault.js.map +1 -1
  155. package/dist/resources/webauthn.d.ts +6 -1
  156. package/dist/resources/webauthn.d.ts.map +1 -1
  157. package/dist/resources/webauthn.js +5 -1
  158. package/dist/resources/webauthn.js.map +1 -1
  159. package/dist/resources/webhooks.js +1 -1
  160. package/dist/resources/webhooks.js.map +1 -1
  161. package/dist/revocations/feed.d.ts +61 -0
  162. package/dist/revocations/feed.d.ts.map +1 -0
  163. package/dist/revocations/feed.js +282 -0
  164. package/dist/revocations/feed.js.map +1 -0
  165. package/dist/revocations/index.d.ts +20 -0
  166. package/dist/revocations/index.d.ts.map +1 -0
  167. package/dist/revocations/index.js +19 -0
  168. package/dist/revocations/index.js.map +1 -0
  169. package/dist/revocations/set.d.ts +59 -0
  170. package/dist/revocations/set.d.ts.map +1 -0
  171. package/dist/revocations/set.js +87 -0
  172. package/dist/revocations/set.js.map +1 -0
  173. package/dist/scopes.d.ts +26 -0
  174. package/dist/scopes.d.ts.map +1 -0
  175. package/dist/scopes.js +37 -0
  176. package/dist/scopes.js.map +1 -0
  177. package/dist/types.d.ts +121 -4
  178. package/dist/types.d.ts.map +1 -1
  179. package/dist/types.js +0 -1
  180. package/dist/types.js.map +1 -1
  181. package/dist/verify.d.ts +31 -2
  182. package/dist/verify.d.ts.map +1 -1
  183. package/dist/verify.js +236 -29
  184. package/dist/verify.js.map +1 -1
  185. package/package.json +13 -4
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Caps meter: atomic reservation of call caps and cost-unit budgets.
3
+ * Mirrors the Python SDK's `grantex.caps`; both address the same counters.
4
+ */
5
+ export const CAP_ERROR_CODE = 'E1008';
6
+ export const CAPS_MODES = ['off', 'warn', 'enforce'];
7
+ /** Rolling window length per window name; `per_case` has no time window. */
8
+ export const WINDOW_MS = {
9
+ per_hour: 3_600_000,
10
+ per_day: 86_400_000,
11
+ per_case: 0,
12
+ };
13
+ export const MAX_COUNT = 2147483647;
14
+ const TENANT_MAX = 256;
15
+ const RESERVATION_RE = /^[A-Za-z0-9_-]{8,64}$/;
16
+ /** A reservation would exceed a cap. `code` is `E1008`. */
17
+ export class CapExceededError extends Error {
18
+ code = CAP_ERROR_CODE;
19
+ reason = 'cap_exceeded';
20
+ limit;
21
+ window;
22
+ used;
23
+ requested;
24
+ scope;
25
+ kind;
26
+ constructor(args) {
27
+ const noun = args.kind === 'calls' ? 'call' : 'cost unit';
28
+ super(`${CAP_ERROR_CODE} cap_exceeded: ${args.scope} ${args.window} ${noun} cap of ${args.limit} reached (${args.used} used, ${args.requested} requested)`);
29
+ this.name = 'CapExceededError';
30
+ this.limit = args.limit;
31
+ this.window = args.window;
32
+ this.used = args.used;
33
+ this.requested = args.requested;
34
+ this.scope = args.scope;
35
+ this.kind = args.kind;
36
+ }
37
+ }
38
+ /** The caps backend could not answer. Callers must treat the call as denied. */
39
+ export class MeterUnavailableError extends Error {
40
+ constructor(message, options) {
41
+ super(message, options);
42
+ this.name = 'MeterUnavailableError';
43
+ }
44
+ }
45
+ /** Limits or arguments are invalid. `subReason` names why. */
46
+ export class CapsConfigurationError extends Error {
47
+ subReason;
48
+ constructor(message, subReason = 'invalid_limits') {
49
+ super(message);
50
+ this.name = 'CapsConfigurationError';
51
+ this.subReason = subReason;
52
+ }
53
+ }
54
+ const encoder = new TextEncoder();
55
+ async function sha256Hex(input) {
56
+ const digest = await globalThis.crypto.subtle.digest('SHA-256', encoder.encode(input));
57
+ return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, '0')).join('');
58
+ }
59
+ /** Storage key for a counter; identical to the Python SDK's `CapLimit.key()`. */
60
+ export function counterKey(window, counter) {
61
+ return sha256Hex(`grantex-caps:v1\0${window}\0${counter}`);
62
+ }
63
+ /** Fixed-length tenant component for storage keys. */
64
+ export async function tenantHash(tenantId) {
65
+ return (await sha256Hex(`grantex-caps-tenant:v1\0${tenantId}`)).slice(0, 32);
66
+ }
67
+ function checkCount(value, what) {
68
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0 || value > MAX_COUNT) {
69
+ throw new CapsConfigurationError(`${what} must be an integer between 0 and ${MAX_COUNT}`);
70
+ }
71
+ return value;
72
+ }
73
+ /** Validate a reservation's arguments and apply defaults. */
74
+ export async function resolveLimits(tenantId, limits) {
75
+ if (typeof tenantId !== 'string' || tenantId.length === 0 || tenantId.length > TENANT_MAX) {
76
+ throw new CapsConfigurationError('tenantId must be a non-empty string of at most 256 characters');
77
+ }
78
+ const resolved = [];
79
+ const seen = new Set();
80
+ for (const limit of limits) {
81
+ if (typeof limit !== 'object' || limit === null)
82
+ throw new CapsConfigurationError('limits must be objects');
83
+ if (!Object.prototype.hasOwnProperty.call(WINDOW_MS, limit.window)) {
84
+ throw new CapsConfigurationError(`unknown cap window ${JSON.stringify(limit.window)}`);
85
+ }
86
+ if (typeof limit.counter !== 'string' || limit.counter.length === 0) {
87
+ throw new CapsConfigurationError('a cap counter must be a non-empty string');
88
+ }
89
+ checkCount(limit.limit, 'a cap limit');
90
+ const units = checkCount(limit.units ?? 1, 'reserved units');
91
+ const key = await counterKey(limit.window, limit.counter);
92
+ if (seen.has(key))
93
+ throw new CapsConfigurationError(`counter ${JSON.stringify(limit.counter)} appears twice in one reservation`);
94
+ seen.add(key);
95
+ resolved.push({
96
+ counter: limit.counter,
97
+ limit: limit.limit,
98
+ window: limit.window,
99
+ units,
100
+ scope: limit.scope ?? 'manifest',
101
+ kind: limit.kind ?? 'calls',
102
+ key,
103
+ });
104
+ }
105
+ return resolved;
106
+ }
107
+ function randomId() {
108
+ const bytes = new Uint8Array(16);
109
+ globalThis.crypto.getRandomValues(bytes);
110
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
111
+ }
112
+ function unavailable(err) {
113
+ const name = err instanceof Error ? err.name : typeof err;
114
+ return new MeterUnavailableError(`caps backend unavailable: ${name}`, { cause: err });
115
+ }
116
+ /**
117
+ * Reserve call caps and cost-unit budgets before the call that incurs cost.
118
+ *
119
+ * `reserve()` checks every limit and records the call atomically, so
120
+ * concurrent callers can never exceed a cap. A failed call is **not** refunded
121
+ * by default: a timeout or error does not prove the provider did no work. Call
122
+ * `refundUnsent()` only when the provider call is known not to have been sent.
123
+ * Any backend failure throws `MeterUnavailableError`; callers must deny.
124
+ */
125
+ export class CapsMeter {
126
+ #backend;
127
+ #clock;
128
+ constructor(backend, options = {}) {
129
+ this.#backend = backend;
130
+ this.#clock = options.clock;
131
+ }
132
+ #now() {
133
+ return this.#clock === undefined ? undefined : Math.trunc(this.#clock());
134
+ }
135
+ async reserve(tenantId, limits) {
136
+ const applied = (await resolveLimits(tenantId, limits)).filter((l) => l.units > 0);
137
+ for (const limit of applied) {
138
+ if (limit.limit === 0) {
139
+ // A cap of zero disables the tool, whatever the backend holds.
140
+ throw new CapExceededError({
141
+ limit: 0, window: limit.window, used: 0, requested: limit.units, scope: limit.scope, kind: limit.kind,
142
+ });
143
+ }
144
+ if (limit.units > limit.limit) {
145
+ // This call alone exceeds the cap; report what the counter holds.
146
+ let used;
147
+ try {
148
+ used = await this.#backend.usage(tenantId, limit, this.#now());
149
+ }
150
+ catch (err) {
151
+ throw unavailable(err);
152
+ }
153
+ throw new CapExceededError({
154
+ limit: limit.limit, window: limit.window, used, requested: limit.units, scope: limit.scope, kind: limit.kind,
155
+ });
156
+ }
157
+ }
158
+ const reservation = { reservationId: randomId(), tenantId, limits: applied };
159
+ if (applied.length === 0)
160
+ return reservation;
161
+ try {
162
+ await this.#backend.reserve(tenantId, reservation.reservationId, applied, this.#now());
163
+ }
164
+ catch (err) {
165
+ if (err instanceof CapExceededError)
166
+ throw err;
167
+ throw unavailable(err);
168
+ }
169
+ return reservation;
170
+ }
171
+ /** Release a reservation whose provider call was proven not sent. */
172
+ async refundUnsent(reservation) {
173
+ if (!RESERVATION_RE.test(reservation.reservationId))
174
+ throw new CapsConfigurationError('invalid reservation id');
175
+ if (reservation.limits.length === 0)
176
+ return;
177
+ try {
178
+ await this.#backend.refund(reservation.tenantId, reservation.reservationId, reservation.limits, this.#now());
179
+ }
180
+ catch (err) {
181
+ throw unavailable(err);
182
+ }
183
+ }
184
+ /** Current usage of each counter, for display (consent page, case view). */
185
+ async usage(tenantId, limits) {
186
+ const resolved = await resolveLimits(tenantId, limits);
187
+ try {
188
+ const out = [];
189
+ for (const limit of resolved) {
190
+ const used = await this.#backend.usage(tenantId, limit, this.#now());
191
+ out.push({ limit, used, remaining: Math.max(limit.limit - used, 0) });
192
+ }
193
+ return out;
194
+ }
195
+ catch (err) {
196
+ throw unavailable(err);
197
+ }
198
+ }
199
+ }
200
+ //# sourceMappingURL=meter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meter.js","sourceRoot":"","sources":["../../src/caps/meter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Postgres caps backend: row locks serialize reservations per counter.
3
+ *
4
+ * A reservation runs in one READ COMMITTED transaction: it makes sure a row
5
+ * exists per counter in `grantex_cap_counters`, locks those rows in a fixed
6
+ * order (so concurrent reservations cannot deadlock), prunes and sums the
7
+ * counter's reservations in `grantex_cap_reservations`, and inserts the new
8
+ * reservation only if every limit holds. Time comes from the database
9
+ * (`clock_timestamp()`) unless a clock is injected for tests. Same tables and
10
+ * statements as the Python SDK's `PostgresCapsBackend`.
11
+ *
12
+ * Rows carry a SHA-256-derived hash of the tenant id, not the tenant id, in
13
+ * `tenant_id`: isolation comes from that hash being part of every key and
14
+ * query, and row-level security policies written against plain tenant ids do
15
+ * not match these rows.
16
+ *
17
+ * Expired rows are deleted lazily when their counter is next reserved. Run
18
+ * `prune()` periodically (for example hourly) to delete expired reservations,
19
+ * per-case reservations older than `caseTtlSeconds` if one is set, and
20
+ * counters with no reservations left.
21
+ *
22
+ * `pool` is a `pg` Pool, or anything with the same `connect()` / `query()` /
23
+ * `release()` shape. Create the tables with `CAPS_SCHEMA_SQL` through your own
24
+ * migration tool, or `ensureSchema()`.
25
+ */
26
+ import { type CapsBackend, type ResolvedCapLimit } from './meter.js';
27
+ export declare const CAPS_SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS grantex_cap_counters (\n tenant_id TEXT NOT NULL,\n counter_key TEXT NOT NULL,\n PRIMARY KEY (tenant_id, counter_key)\n);\n\nCREATE TABLE IF NOT EXISTS grantex_cap_reservations (\n tenant_id TEXT NOT NULL,\n counter_key TEXT NOT NULL,\n reservation_id TEXT NOT NULL,\n units BIGINT NOT NULL CHECK (units >= 0),\n reserved_at TIMESTAMPTZ NOT NULL,\n expires_at TIMESTAMPTZ,\n PRIMARY KEY (tenant_id, counter_key, reservation_id),\n FOREIGN KEY (tenant_id, counter_key)\n REFERENCES grantex_cap_counters (tenant_id, counter_key)\n);\n\nALTER TABLE grantex_cap_reservations ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ;\n\nCREATE INDEX IF NOT EXISTS idx_grantex_cap_reservations_time\n ON grantex_cap_reservations (tenant_id, counter_key, reserved_at);\n\nCREATE INDEX IF NOT EXISTS idx_grantex_cap_reservations_expiry\n ON grantex_cap_reservations (expires_at)\n WHERE expires_at IS NOT NULL;\n";
28
+ export interface PgClientLike {
29
+ query(text: string, values?: readonly unknown[]): Promise<{
30
+ rows: Array<Record<string, unknown>>;
31
+ rowCount?: number | null;
32
+ }>;
33
+ /** `pg` destroys the connection instead of reusing it when given an error. */
34
+ release(err?: Error | boolean): void;
35
+ }
36
+ export interface PgPoolLike {
37
+ connect(): Promise<PgClientLike>;
38
+ }
39
+ export declare class PostgresCapsBackend implements CapsBackend {
40
+ #private;
41
+ /**
42
+ * `caseTtlSeconds` optionally lets `prune()` delete per-case reservations
43
+ * that old; by default they are kept, because deleting them would reset the
44
+ * per-case cap.
45
+ */
46
+ constructor(pool: PgPoolLike, options?: {
47
+ caseTtlSeconds?: number;
48
+ });
49
+ ensureSchema(): Promise<void>;
50
+ reserve(tenantId: string, reservationId: string, limits: readonly ResolvedCapLimit[], nowMs: number | undefined): Promise<void>;
51
+ refund(tenantId: string, reservationId: string, limits: readonly ResolvedCapLimit[]): Promise<void>;
52
+ usage(tenantId: string, limit: ResolvedCapLimit, nowMs: number | undefined): Promise<number>;
53
+ /**
54
+ * Delete expired reservations and counters left empty; returns the numbers
55
+ * deleted. Counters being reserved are locked and skipped, and a counter that
56
+ * still has reservations cannot be deleted (the foreign key refuses it,
57
+ * failing this run rather than losing a reservation; run it again).
58
+ */
59
+ prune(nowMs?: number): Promise<{
60
+ reservations: number;
61
+ counters: number;
62
+ }>;
63
+ }
64
+ //# sourceMappingURL=postgres.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/caps/postgres.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAA2C,KAAK,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9G,eAAO,MAAM,eAAe,48BA2B3B,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAC9H,8EAA8E;IAC9E,OAAO,CAAC,GAAG,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,IAAI,OAAO,CAAC,YAAY,CAAC,CAAC;CAClC;AAYD,qBAAa,mBAAoB,YAAW,WAAW;;IAIrD;;;;OAIG;IACH,YAAY,IAAI,EAAE,UAAU,EAAE,OAAO,GAAE;QAAE,cAAc,CAAC,EAAE,MAAM,CAAA;KAAO,EAOtE;IAEK,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CASlC;IAiEK,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,gBAAgB,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAuCpI;IAEK,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAaxG;IAEK,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CASjG;IAED;;;;;OAKG;IACG,KAAK,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAgB/E;CACF"}
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Postgres caps backend: row locks serialize reservations per counter.
3
+ *
4
+ * A reservation runs in one READ COMMITTED transaction: it makes sure a row
5
+ * exists per counter in `grantex_cap_counters`, locks those rows in a fixed
6
+ * order (so concurrent reservations cannot deadlock), prunes and sums the
7
+ * counter's reservations in `grantex_cap_reservations`, and inserts the new
8
+ * reservation only if every limit holds. Time comes from the database
9
+ * (`clock_timestamp()`) unless a clock is injected for tests. Same tables and
10
+ * statements as the Python SDK's `PostgresCapsBackend`.
11
+ *
12
+ * Rows carry a SHA-256-derived hash of the tenant id, not the tenant id, in
13
+ * `tenant_id`: isolation comes from that hash being part of every key and
14
+ * query, and row-level security policies written against plain tenant ids do
15
+ * not match these rows.
16
+ *
17
+ * Expired rows are deleted lazily when their counter is next reserved. Run
18
+ * `prune()` periodically (for example hourly) to delete expired reservations,
19
+ * per-case reservations older than `caseTtlSeconds` if one is set, and
20
+ * counters with no reservations left.
21
+ *
22
+ * `pool` is a `pg` Pool, or anything with the same `connect()` / `query()` /
23
+ * `release()` shape. Create the tables with `CAPS_SCHEMA_SQL` through your own
24
+ * migration tool, or `ensureSchema()`.
25
+ */
26
+ import { CapExceededError, WINDOW_MS, tenantHash } from './meter.js';
27
+ export const CAPS_SCHEMA_SQL = `
28
+ CREATE TABLE IF NOT EXISTS grantex_cap_counters (
29
+ tenant_id TEXT NOT NULL,
30
+ counter_key TEXT NOT NULL,
31
+ PRIMARY KEY (tenant_id, counter_key)
32
+ );
33
+
34
+ CREATE TABLE IF NOT EXISTS grantex_cap_reservations (
35
+ tenant_id TEXT NOT NULL,
36
+ counter_key TEXT NOT NULL,
37
+ reservation_id TEXT NOT NULL,
38
+ units BIGINT NOT NULL CHECK (units >= 0),
39
+ reserved_at TIMESTAMPTZ NOT NULL,
40
+ expires_at TIMESTAMPTZ,
41
+ PRIMARY KEY (tenant_id, counter_key, reservation_id),
42
+ FOREIGN KEY (tenant_id, counter_key)
43
+ REFERENCES grantex_cap_counters (tenant_id, counter_key)
44
+ );
45
+
46
+ ALTER TABLE grantex_cap_reservations ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ;
47
+
48
+ CREATE INDEX IF NOT EXISTS idx_grantex_cap_reservations_time
49
+ ON grantex_cap_reservations (tenant_id, counter_key, reserved_at);
50
+
51
+ CREATE INDEX IF NOT EXISTS idx_grantex_cap_reservations_expiry
52
+ ON grantex_cap_reservations (expires_at)
53
+ WHERE expires_at IS NOT NULL;
54
+ `;
55
+ const NOW_SQL = 'SELECT CASE WHEN $1::bigint IS NULL THEN clock_timestamp() ELSE to_timestamp($1::bigint / 1000.0) END AS now';
56
+ const LOCK_ATTEMPTS = 3;
57
+ function int(value) {
58
+ const n = typeof value === 'number' ? value : typeof value === 'string' || typeof value === 'bigint' ? Number(value) : Number.NaN;
59
+ if (!Number.isSafeInteger(n))
60
+ throw new Error('database returned an invalid usage');
61
+ return n;
62
+ }
63
+ export class PostgresCapsBackend {
64
+ #pool;
65
+ #caseTtlSeconds;
66
+ /**
67
+ * `caseTtlSeconds` optionally lets `prune()` delete per-case reservations
68
+ * that old; by default they are kept, because deleting them would reset the
69
+ * per-case cap.
70
+ */
71
+ constructor(pool, options = {}) {
72
+ const ttl = options.caseTtlSeconds;
73
+ if (ttl !== undefined && (!Number.isInteger(ttl) || ttl <= 0)) {
74
+ throw new Error('caseTtlSeconds must be a positive integer');
75
+ }
76
+ this.#pool = pool;
77
+ this.#caseTtlSeconds = ttl;
78
+ }
79
+ async ensureSchema() {
80
+ const client = await this.#pool.connect();
81
+ try {
82
+ for (const statement of CAPS_SCHEMA_SQL.split(';').map((s) => s.trim()).filter(Boolean)) {
83
+ await client.query(statement);
84
+ }
85
+ }
86
+ finally {
87
+ client.release();
88
+ }
89
+ }
90
+ static async #now(client, nowMs) {
91
+ const { rows } = await client.query(NOW_SQL, [nowMs ?? null]);
92
+ const row = rows[0];
93
+ if (row === undefined)
94
+ throw new Error('database returned no time');
95
+ return row['now'];
96
+ }
97
+ static async #used(client, tenant, key, windowMs, now, prune) {
98
+ let rows;
99
+ if (windowMs > 0) {
100
+ if (prune) {
101
+ await client.query('DELETE FROM grantex_cap_reservations WHERE tenant_id = $1 AND counter_key = $2 ' +
102
+ 'AND reserved_at <= $3::timestamptz - make_interval(secs => $4::double precision)', [tenant, key, now, windowMs / 1000]);
103
+ }
104
+ ({ rows } = await client.query('SELECT COALESCE(SUM(units), 0) AS used FROM grantex_cap_reservations ' +
105
+ 'WHERE tenant_id = $1 AND counter_key = $2 AND reserved_at > $3::timestamptz - make_interval(secs => $4::double precision)', [tenant, key, now, windowMs / 1000]));
106
+ }
107
+ else {
108
+ ({ rows } = await client.query('SELECT COALESCE(SUM(units), 0) AS used FROM grantex_cap_reservations WHERE tenant_id = $1 AND counter_key = $2', [tenant, key]));
109
+ }
110
+ const row = rows[0];
111
+ if (row === undefined)
112
+ throw new Error('database returned no usage');
113
+ return int(row['used']);
114
+ }
115
+ #expirySeconds(limit) {
116
+ const window = WINDOW_MS[limit.window];
117
+ if (window > 0)
118
+ return window / 1000;
119
+ return this.#caseTtlSeconds ?? null;
120
+ }
121
+ /** Run `work` in a READ COMMITTED transaction; a connection whose rollback fails is destroyed. */
122
+ async #transaction(work) {
123
+ const client = await this.#pool.connect();
124
+ let releaseError;
125
+ try {
126
+ await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
127
+ try {
128
+ const result = await work(client);
129
+ await client.query('COMMIT');
130
+ return result;
131
+ }
132
+ catch (err) {
133
+ try {
134
+ await client.query('ROLLBACK');
135
+ }
136
+ catch (rollbackError) {
137
+ // The original error propagates; the connection is not reused.
138
+ releaseError = rollbackError instanceof Error ? rollbackError : new Error(String(rollbackError));
139
+ }
140
+ throw err;
141
+ }
142
+ }
143
+ finally {
144
+ client.release(releaseError);
145
+ }
146
+ }
147
+ async reserve(tenantId, reservationId, limits, nowMs) {
148
+ const tenant = await tenantHash(tenantId);
149
+ const ordered = [...limits].sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
150
+ await this.#transaction(async (client) => {
151
+ for (const limit of ordered) {
152
+ let locked = false;
153
+ for (let attempt = 0; attempt < LOCK_ATTEMPTS && !locked; attempt += 1) {
154
+ await client.query('INSERT INTO grantex_cap_counters (tenant_id, counter_key) VALUES ($1, $2) ON CONFLICT DO NOTHING', [tenant, limit.key]);
155
+ const { rows } = await client.query('SELECT counter_key FROM grantex_cap_counters WHERE tenant_id = $1 AND counter_key = $2 FOR UPDATE', [tenant, limit.key]);
156
+ // No row: prune() removed an empty counter between the two statements.
157
+ locked = rows.length === 1;
158
+ }
159
+ if (!locked)
160
+ throw new Error('caps counter row could not be locked');
161
+ }
162
+ // Read the time after the locks are held, so a reservation that waited
163
+ // is judged against the moment it actually runs.
164
+ const now = await PostgresCapsBackend.#now(client, nowMs);
165
+ for (const limit of ordered) {
166
+ const used = await PostgresCapsBackend.#used(client, tenant, limit.key, WINDOW_MS[limit.window], now, true);
167
+ if (used + limit.units > limit.limit) {
168
+ throw new CapExceededError({
169
+ limit: limit.limit, window: limit.window, used, requested: limit.units, scope: limit.scope, kind: limit.kind,
170
+ });
171
+ }
172
+ }
173
+ for (const limit of ordered) {
174
+ await client.query('INSERT INTO grantex_cap_reservations (tenant_id, counter_key, reservation_id, units, reserved_at, expires_at) ' +
175
+ 'VALUES ($1, $2, $3, $4, $5::timestamptz, $5::timestamptz + make_interval(secs => $6::double precision))', [tenant, limit.key, reservationId, limit.units, now, this.#expirySeconds(limit)]);
176
+ }
177
+ });
178
+ }
179
+ async refund(tenantId, reservationId, limits) {
180
+ const tenant = await tenantHash(tenantId);
181
+ const client = await this.#pool.connect();
182
+ try {
183
+ for (const limit of limits) {
184
+ await client.query('DELETE FROM grantex_cap_reservations WHERE tenant_id = $1 AND counter_key = $2 AND reservation_id = $3', [tenant, limit.key, reservationId]);
185
+ }
186
+ }
187
+ finally {
188
+ client.release();
189
+ }
190
+ }
191
+ async usage(tenantId, limit, nowMs) {
192
+ const tenant = await tenantHash(tenantId);
193
+ const client = await this.#pool.connect();
194
+ try {
195
+ const now = await PostgresCapsBackend.#now(client, nowMs);
196
+ return await PostgresCapsBackend.#used(client, tenant, limit.key, WINDOW_MS[limit.window], now, false);
197
+ }
198
+ finally {
199
+ client.release();
200
+ }
201
+ }
202
+ /**
203
+ * Delete expired reservations and counters left empty; returns the numbers
204
+ * deleted. Counters being reserved are locked and skipped, and a counter that
205
+ * still has reservations cannot be deleted (the foreign key refuses it,
206
+ * failing this run rather than losing a reservation; run it again).
207
+ */
208
+ async prune(nowMs) {
209
+ return this.#transaction(async (client) => {
210
+ const now = await PostgresCapsBackend.#now(client, nowMs);
211
+ const expired = await client.query('DELETE FROM grantex_cap_reservations WHERE expires_at IS NOT NULL AND expires_at <= $1::timestamptz', [now]);
212
+ const empty = await client.query('DELETE FROM grantex_cap_counters WHERE (tenant_id, counter_key) IN (' +
213
+ ' SELECT c.tenant_id, c.counter_key FROM grantex_cap_counters c' +
214
+ ' WHERE NOT EXISTS (SELECT 1 FROM grantex_cap_reservations r' +
215
+ ' WHERE r.tenant_id = c.tenant_id AND r.counter_key = c.counter_key)' +
216
+ ' FOR UPDATE SKIP LOCKED)');
217
+ return { reservations: expired.rowCount ?? 0, counters: empty.rowCount ?? 0 };
218
+ });
219
+ }
220
+ }
221
+ //# sourceMappingURL=postgres.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../src/caps/postgres.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Redis caps backend: one Lua script reserves every limit atomically.
3
+ *
4
+ * Keys are tenant-scoped and share a hash tag, `grantex:caps:{<tenant>}:...`,
5
+ * so one reservation touches a single Redis Cluster slot. Each counter is a
6
+ * sorted set of `<reservation id>:<units>` members scored by reservation time
7
+ * plus a running total; rolling windows drop members older than the window
8
+ * before checking. Time comes from the Redis server (`TIME`) unless a clock is
9
+ * injected for tests.
10
+ *
11
+ * Requires Redis 6.0 or later (`SET ... KEEPTTL`). Run it with
12
+ * `maxmemory-policy noeviction`: an evicted counter would forget reservations
13
+ * and let calls exceed a cap. Per-hour and per-day keys expire one window plus
14
+ * 60 seconds after the last reservation on the counter; per-case keys only with
15
+ * `caseTtlSeconds`.
16
+ *
17
+ * The scripts are byte-identical to the Python SDK's (`grantex/caps/_redis.py`);
18
+ * a test in each SDK checks this.
19
+ */
20
+ import { type CapsBackend, type ResolvedCapLimit } from './meter.js';
21
+ export declare const RESERVE_SCRIPT = "\nlocal now\nif ARGV[1] ~= '' then\n now = tonumber(ARGV[1])\nelse\n local t = redis.call('TIME')\n now = tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)\nend\nlocal rid = ARGV[2]\nlocal n = tonumber(ARGV[3])\nlocal case_ttl = tonumber(ARGV[4])\nfor i = 1, n do\n local a = 4 + (i - 1) * 3\n local limit = tonumber(ARGV[a + 1])\n local window = tonumber(ARGV[a + 2])\n local units = tonumber(ARGV[a + 3])\n local zkey = KEYS[(i - 1) * 2 + 1]\n local skey = KEYS[(i - 1) * 2 + 2]\n local sum = tonumber(redis.call('GET', skey) or '0')\n if window > 0 then\n local expired = redis.call('ZRANGEBYSCORE', zkey, '-inf', now - window)\n if #expired > 0 then\n for _, member in ipairs(expired) do\n sum = sum - tonumber(string.match(member, ':(%d+)$'))\n end\n if sum < 0 then sum = 0 end\n redis.call('ZREMRANGEBYSCORE', zkey, '-inf', now - window)\n redis.call('SET', skey, sum, 'KEEPTTL')\n end\n end\n if sum + units > limit then\n return {0, i, sum}\n end\nend\nfor i = 1, n do\n local a = 4 + (i - 1) * 3\n local window = tonumber(ARGV[a + 2])\n local units = tonumber(ARGV[a + 3])\n local zkey = KEYS[(i - 1) * 2 + 1]\n local skey = KEYS[(i - 1) * 2 + 2]\n redis.call('ZADD', zkey, now, rid .. ':' .. units)\n redis.call('INCRBY', skey, units)\n local ttl = case_ttl\n if window > 0 then ttl = window + 60000 end\n if ttl > 0 then\n redis.call('PEXPIRE', zkey, ttl)\n redis.call('PEXPIRE', skey, ttl)\n end\nend\nreturn {1, 0, 0}\n";
22
+ export declare const REFUND_SCRIPT = "\nlocal rid = ARGV[1]\nlocal n = tonumber(ARGV[2])\nfor i = 1, n do\n local units = ARGV[2 + i]\n local zkey = KEYS[(i - 1) * 2 + 1]\n local skey = KEYS[(i - 1) * 2 + 2]\n if redis.call('ZREM', zkey, rid .. ':' .. units) == 1 then\n local left = redis.call('DECRBY', skey, tonumber(units))\n if left < 0 then redis.call('SET', skey, 0, 'KEEPTTL') end\n end\nend\nreturn 1\n";
23
+ export declare const USAGE_SCRIPT = "\nlocal now\nif ARGV[1] ~= '' then\n now = tonumber(ARGV[1])\nelse\n local t = redis.call('TIME')\n now = tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)\nend\nlocal window = tonumber(ARGV[2])\nlocal sum = tonumber(redis.call('GET', KEYS[2]) or '0')\nif window > 0 then\n for _, member in ipairs(redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', now - window)) do\n sum = sum - tonumber(string.match(member, ':(%d+)$'))\n end\nend\nif sum < 0 then sum = 0 end\nreturn sum\n";
24
+ /** Runs a Lua script. Adapt your client, for example `ioredisRunner(new Redis(url))`. */
25
+ export interface RedisScriptRunner {
26
+ evalScript(script: string, keys: readonly string[], args: readonly (string | number)[]): Promise<unknown>;
27
+ }
28
+ /** Adapter for an ioredis client (`client.eval(script, numKeys, ...keysAndArgs)`). */
29
+ export declare function ioredisRunner(client: {
30
+ eval(script: string, numKeys: number, ...keysAndArgs: (string | number)[]): Promise<unknown>;
31
+ }): RedisScriptRunner;
32
+ export declare class RedisCapsBackend implements CapsBackend {
33
+ #private;
34
+ /**
35
+ * `caseTtlSeconds` optionally expires per-case counters; by default they never
36
+ * expire, because an expired per-case counter would reset the cap.
37
+ */
38
+ constructor(redis: RedisScriptRunner, options?: {
39
+ caseTtlSeconds?: number;
40
+ });
41
+ reserve(tenantId: string, reservationId: string, limits: readonly ResolvedCapLimit[], nowMs: number | undefined): Promise<void>;
42
+ refund(tenantId: string, reservationId: string, limits: readonly ResolvedCapLimit[]): Promise<void>;
43
+ usage(tenantId: string, limit: ResolvedCapLimit, nowMs: number | undefined): Promise<number>;
44
+ }
45
+ //# sourceMappingURL=redis.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../src/caps/redis.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAA2C,KAAK,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9G,eAAO,MAAM,cAAc,i/CAkD1B,CAAC;AAEF,eAAO,MAAM,aAAa,qYAazB,CAAC;AAEF,eAAO,MAAM,YAAY,2eAiBxB,CAAC;AAGF,yFAAyF;AACzF,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC3G;AAED,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,MAAM,EAAE;IACpC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9F,GAAG,iBAAiB,CAIpB;AAaD,qBAAa,gBAAiB,YAAW,WAAW;;IAIlD;;;OAGG;IACH,YAAY,KAAK,EAAE,iBAAiB,EAAE,OAAO,GAAE;QAAE,cAAc,CAAC,EAAE,MAAM,CAAA;KAAO,EAO9E;IAEK,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,gBAAgB,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBpI;IAEK,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAQxG;IAEK,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAMjG;CACF"}