@grantex/sdk 0.6.0 → 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.
- package/LICENSE +185 -185
- package/NOTICE +16 -16
- package/README.md +974 -947
- package/dist/authorization-details.d.ts +86 -0
- package/dist/authorization-details.d.ts.map +1 -0
- package/dist/authorization-details.js +209 -0
- package/dist/authorization-details.js.map +1 -0
- package/dist/canonical.d.ts +47 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +163 -0
- package/dist/canonical.js.map +1 -0
- package/dist/caps/index.d.ts +19 -0
- package/dist/caps/index.d.ts.map +1 -0
- package/dist/caps/index.js +19 -0
- package/dist/caps/index.js.map +1 -0
- package/dist/caps/limits.d.ts +40 -0
- package/dist/caps/limits.d.ts.map +1 -0
- package/dist/caps/limits.js +109 -0
- package/dist/caps/limits.js.map +1 -0
- package/dist/caps/memory.d.ts +15 -0
- package/dist/caps/memory.d.ts.map +1 -0
- package/dist/caps/memory.js +57 -0
- package/dist/caps/memory.js.map +1 -0
- package/dist/caps/meter.d.ts +107 -0
- package/dist/caps/meter.d.ts.map +1 -0
- package/dist/caps/meter.js +200 -0
- package/dist/caps/meter.js.map +1 -0
- package/dist/caps/postgres.d.ts +64 -0
- package/dist/caps/postgres.d.ts.map +1 -0
- package/dist/caps/postgres.js +221 -0
- package/dist/caps/postgres.js.map +1 -0
- package/dist/caps/redis.d.ts +45 -0
- package/dist/caps/redis.d.ts.map +1 -0
- package/dist/caps/redis.js +173 -0
- package/dist/caps/redis.js.map +1 -0
- package/dist/client.d.ts +12 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +517 -57
- package/dist/client.js.map +1 -1
- package/dist/decisions/action.d.ts +51 -0
- package/dist/decisions/action.d.ts.map +1 -0
- package/dist/decisions/action.js +237 -0
- package/dist/decisions/action.js.map +1 -0
- package/dist/decisions/index.d.ts +11 -0
- package/dist/decisions/index.d.ts.map +1 -0
- package/dist/decisions/index.js +8 -0
- package/dist/decisions/index.js.map +1 -0
- package/dist/decisions/verify.d.ts +103 -0
- package/dist/decisions/verify.d.ts.map +1 -0
- package/dist/decisions/verify.js +277 -0
- package/dist/decisions/verify.js.map +1 -0
- package/dist/denials.d.ts +132 -0
- package/dist/denials.d.ts.map +1 -0
- package/dist/denials.js +129 -0
- package/dist/denials.js.map +1 -0
- package/dist/evidence/build.d.ts +63 -0
- package/dist/evidence/build.d.ts.map +1 -0
- package/dist/evidence/build.js +201 -0
- package/dist/evidence/build.js.map +1 -0
- package/dist/evidence/canonical.d.ts +3 -0
- package/dist/evidence/canonical.d.ts.map +1 -0
- package/dist/evidence/canonical.js +3 -0
- package/dist/evidence/canonical.js.map +1 -0
- package/dist/evidence/checks.d.ts +13 -0
- package/dist/evidence/checks.d.ts.map +1 -0
- package/dist/evidence/checks.js +406 -0
- package/dist/evidence/checks.js.map +1 -0
- package/dist/evidence/document.d.ts +8 -0
- package/dist/evidence/document.d.ts.map +1 -0
- package/dist/evidence/document.js +298 -0
- package/dist/evidence/document.js.map +1 -0
- package/dist/evidence/hashing.d.ts +37 -0
- package/dist/evidence/hashing.d.ts.map +1 -0
- package/dist/evidence/hashing.js +106 -0
- package/dist/evidence/hashing.js.map +1 -0
- package/dist/evidence/index.d.ts +24 -0
- package/dist/evidence/index.d.ts.map +1 -0
- package/dist/evidence/index.js +24 -0
- package/dist/evidence/index.js.map +1 -0
- package/dist/evidence/result.d.ts +88 -0
- package/dist/evidence/result.d.ts.map +1 -0
- package/dist/evidence/result.js +88 -0
- package/dist/evidence/result.js.map +1 -0
- package/dist/evidence/schema-1.0.d.ts +2 -0
- package/dist/evidence/schema-1.0.d.ts.map +1 -0
- package/dist/evidence/schema-1.0.js +1398 -0
- package/dist/evidence/schema-1.0.js.map +1 -0
- package/dist/evidence/schema.d.ts +13 -0
- package/dist/evidence/schema.d.ts.map +1 -0
- package/dist/evidence/schema.js +264 -0
- package/dist/evidence/schema.js.map +1 -0
- package/dist/evidence/signature.d.ts +23 -0
- package/dist/evidence/signature.d.ts.map +1 -0
- package/dist/evidence/signature.js +136 -0
- package/dist/evidence/signature.js.map +1 -0
- package/dist/evidence/trace.d.ts +23 -0
- package/dist/evidence/trace.d.ts.map +1 -0
- package/dist/evidence/trace.js +69 -0
- package/dist/evidence/trace.js.map +1 -0
- package/dist/evidence/verify.d.ts +29 -0
- package/dist/evidence/verify.d.ts.map +1 -0
- package/dist/evidence/verify.js +163 -0
- package/dist/evidence/verify.js.map +1 -0
- package/dist/http.d.ts +12 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +11 -5
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +16 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -2
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +178 -9
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +379 -16
- package/dist/manifest.js.map +1 -1
- package/dist/purpose.d.ts +29 -0
- package/dist/purpose.d.ts.map +1 -0
- package/dist/purpose.js +60 -0
- package/dist/purpose.js.map +1 -0
- package/dist/resources/agents.js +3 -3
- package/dist/resources/agents.js.map +1 -1
- package/dist/resources/anomalies.d.ts +3 -1
- package/dist/resources/anomalies.d.ts.map +1 -1
- package/dist/resources/anomalies.js +7 -1
- package/dist/resources/anomalies.js.map +1 -1
- package/dist/resources/audit.js +1 -1
- package/dist/resources/audit.js.map +1 -1
- package/dist/resources/budgets.js +2 -2
- package/dist/resources/budgets.js.map +1 -1
- package/dist/resources/credentials.js +1 -1
- package/dist/resources/credentials.js.map +1 -1
- package/dist/resources/decisions.d.ts +69 -0
- package/dist/resources/decisions.d.ts.map +1 -0
- package/dist/resources/decisions.js +115 -0
- package/dist/resources/decisions.js.map +1 -0
- package/dist/resources/domains.js +2 -2
- package/dist/resources/domains.js.map +1 -1
- package/dist/resources/dpdp.js +4 -4
- package/dist/resources/dpdp.js.map +1 -1
- package/dist/resources/grants.js +2 -2
- package/dist/resources/grants.js.map +1 -1
- package/dist/resources/policies.js +3 -3
- package/dist/resources/policies.js.map +1 -1
- package/dist/resources/scim.js +5 -5
- package/dist/resources/scim.js.map +1 -1
- package/dist/resources/tokens.d.ts.map +1 -1
- package/dist/resources/tokens.js +5 -1
- package/dist/resources/tokens.js.map +1 -1
- package/dist/resources/vault.js +2 -2
- package/dist/resources/vault.js.map +1 -1
- package/dist/resources/webauthn.d.ts +6 -1
- package/dist/resources/webauthn.d.ts.map +1 -1
- package/dist/resources/webauthn.js +5 -1
- package/dist/resources/webauthn.js.map +1 -1
- package/dist/resources/webhooks.js +1 -1
- package/dist/resources/webhooks.js.map +1 -1
- package/dist/revocations/feed.d.ts +61 -0
- package/dist/revocations/feed.d.ts.map +1 -0
- package/dist/revocations/feed.js +282 -0
- package/dist/revocations/feed.js.map +1 -0
- package/dist/revocations/index.d.ts +20 -0
- package/dist/revocations/index.d.ts.map +1 -0
- package/dist/revocations/index.js +19 -0
- package/dist/revocations/index.js.map +1 -0
- package/dist/revocations/set.d.ts +59 -0
- package/dist/revocations/set.d.ts.map +1 -0
- package/dist/revocations/set.js +87 -0
- package/dist/revocations/set.js.map +1 -0
- package/dist/scopes.d.ts +26 -0
- package/dist/scopes.d.ts.map +1 -0
- package/dist/scopes.js +37 -0
- package/dist/scopes.js.map +1 -0
- package/dist/types.d.ts +121 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -1
- package/dist/types.js.map +1 -1
- package/dist/verify.d.ts +31 -2
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +236 -29
- package/dist/verify.js.map +1 -1
- package/package.json +92 -84
|
@@ -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"}
|
|
@@ -0,0 +1,173 @@
|
|
|
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 { CapExceededError, WINDOW_MS, tenantHash } from './meter.js';
|
|
21
|
+
// grantex caps redis script v1 begin
|
|
22
|
+
export const RESERVE_SCRIPT = `
|
|
23
|
+
local now
|
|
24
|
+
if ARGV[1] ~= '' then
|
|
25
|
+
now = tonumber(ARGV[1])
|
|
26
|
+
else
|
|
27
|
+
local t = redis.call('TIME')
|
|
28
|
+
now = tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)
|
|
29
|
+
end
|
|
30
|
+
local rid = ARGV[2]
|
|
31
|
+
local n = tonumber(ARGV[3])
|
|
32
|
+
local case_ttl = tonumber(ARGV[4])
|
|
33
|
+
for i = 1, n do
|
|
34
|
+
local a = 4 + (i - 1) * 3
|
|
35
|
+
local limit = tonumber(ARGV[a + 1])
|
|
36
|
+
local window = tonumber(ARGV[a + 2])
|
|
37
|
+
local units = tonumber(ARGV[a + 3])
|
|
38
|
+
local zkey = KEYS[(i - 1) * 2 + 1]
|
|
39
|
+
local skey = KEYS[(i - 1) * 2 + 2]
|
|
40
|
+
local sum = tonumber(redis.call('GET', skey) or '0')
|
|
41
|
+
if window > 0 then
|
|
42
|
+
local expired = redis.call('ZRANGEBYSCORE', zkey, '-inf', now - window)
|
|
43
|
+
if #expired > 0 then
|
|
44
|
+
for _, member in ipairs(expired) do
|
|
45
|
+
sum = sum - tonumber(string.match(member, ':(%d+)$'))
|
|
46
|
+
end
|
|
47
|
+
if sum < 0 then sum = 0 end
|
|
48
|
+
redis.call('ZREMRANGEBYSCORE', zkey, '-inf', now - window)
|
|
49
|
+
redis.call('SET', skey, sum, 'KEEPTTL')
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
if sum + units > limit then
|
|
53
|
+
return {0, i, sum}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
for i = 1, n do
|
|
57
|
+
local a = 4 + (i - 1) * 3
|
|
58
|
+
local window = tonumber(ARGV[a + 2])
|
|
59
|
+
local units = tonumber(ARGV[a + 3])
|
|
60
|
+
local zkey = KEYS[(i - 1) * 2 + 1]
|
|
61
|
+
local skey = KEYS[(i - 1) * 2 + 2]
|
|
62
|
+
redis.call('ZADD', zkey, now, rid .. ':' .. units)
|
|
63
|
+
redis.call('INCRBY', skey, units)
|
|
64
|
+
local ttl = case_ttl
|
|
65
|
+
if window > 0 then ttl = window + 60000 end
|
|
66
|
+
if ttl > 0 then
|
|
67
|
+
redis.call('PEXPIRE', zkey, ttl)
|
|
68
|
+
redis.call('PEXPIRE', skey, ttl)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
return {1, 0, 0}
|
|
72
|
+
`;
|
|
73
|
+
export const REFUND_SCRIPT = `
|
|
74
|
+
local rid = ARGV[1]
|
|
75
|
+
local n = tonumber(ARGV[2])
|
|
76
|
+
for i = 1, n do
|
|
77
|
+
local units = ARGV[2 + i]
|
|
78
|
+
local zkey = KEYS[(i - 1) * 2 + 1]
|
|
79
|
+
local skey = KEYS[(i - 1) * 2 + 2]
|
|
80
|
+
if redis.call('ZREM', zkey, rid .. ':' .. units) == 1 then
|
|
81
|
+
local left = redis.call('DECRBY', skey, tonumber(units))
|
|
82
|
+
if left < 0 then redis.call('SET', skey, 0, 'KEEPTTL') end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
return 1
|
|
86
|
+
`;
|
|
87
|
+
export const USAGE_SCRIPT = `
|
|
88
|
+
local now
|
|
89
|
+
if ARGV[1] ~= '' then
|
|
90
|
+
now = tonumber(ARGV[1])
|
|
91
|
+
else
|
|
92
|
+
local t = redis.call('TIME')
|
|
93
|
+
now = tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)
|
|
94
|
+
end
|
|
95
|
+
local window = tonumber(ARGV[2])
|
|
96
|
+
local sum = tonumber(redis.call('GET', KEYS[2]) or '0')
|
|
97
|
+
if window > 0 then
|
|
98
|
+
for _, member in ipairs(redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', now - window)) do
|
|
99
|
+
sum = sum - tonumber(string.match(member, ':(%d+)$'))
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
if sum < 0 then sum = 0 end
|
|
103
|
+
return sum
|
|
104
|
+
`;
|
|
105
|
+
/** Adapter for an ioredis client (`client.eval(script, numKeys, ...keysAndArgs)`). */
|
|
106
|
+
export function ioredisRunner(client) {
|
|
107
|
+
return {
|
|
108
|
+
evalScript: (script, keys, args) => client.eval(script, keys.length, ...keys, ...args),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async function keysFor(tenantId, limit) {
|
|
112
|
+
const base = `grantex:caps:{${await tenantHash(tenantId)}}:${limit.key}`;
|
|
113
|
+
return [`${base}:z`, `${base}:s`];
|
|
114
|
+
}
|
|
115
|
+
function toInt(value) {
|
|
116
|
+
const n = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : Number.NaN;
|
|
117
|
+
if (!Number.isInteger(n))
|
|
118
|
+
throw new Error('unexpected caps script result');
|
|
119
|
+
return n;
|
|
120
|
+
}
|
|
121
|
+
export class RedisCapsBackend {
|
|
122
|
+
#redis;
|
|
123
|
+
#caseTtlMs;
|
|
124
|
+
/**
|
|
125
|
+
* `caseTtlSeconds` optionally expires per-case counters; by default they never
|
|
126
|
+
* expire, because an expired per-case counter would reset the cap.
|
|
127
|
+
*/
|
|
128
|
+
constructor(redis, options = {}) {
|
|
129
|
+
const ttl = options.caseTtlSeconds;
|
|
130
|
+
if (ttl !== undefined && (!Number.isInteger(ttl) || ttl <= 0)) {
|
|
131
|
+
throw new Error('caseTtlSeconds must be a positive integer');
|
|
132
|
+
}
|
|
133
|
+
this.#redis = redis;
|
|
134
|
+
this.#caseTtlMs = ttl === undefined ? 0 : ttl * 1000;
|
|
135
|
+
}
|
|
136
|
+
async reserve(tenantId, reservationId, limits, nowMs) {
|
|
137
|
+
const keys = [];
|
|
138
|
+
const args = [nowMs === undefined ? '' : String(nowMs), reservationId, limits.length, this.#caseTtlMs];
|
|
139
|
+
for (const limit of limits) {
|
|
140
|
+
keys.push(...(await keysFor(tenantId, limit)));
|
|
141
|
+
args.push(limit.limit, WINDOW_MS[limit.window], limit.units);
|
|
142
|
+
}
|
|
143
|
+
const result = await this.#redis.evalScript(RESERVE_SCRIPT, keys, args);
|
|
144
|
+
if (!Array.isArray(result) || result.length !== 3)
|
|
145
|
+
throw new Error('unexpected caps script result');
|
|
146
|
+
const [ok, index, used] = result.map(toInt);
|
|
147
|
+
if (ok === 1)
|
|
148
|
+
return;
|
|
149
|
+
const limit = limits[index - 1];
|
|
150
|
+
if (limit === undefined)
|
|
151
|
+
throw new Error('unexpected caps script result');
|
|
152
|
+
throw new CapExceededError({
|
|
153
|
+
limit: limit.limit, window: limit.window, used, requested: limit.units, scope: limit.scope, kind: limit.kind,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async refund(tenantId, reservationId, limits) {
|
|
157
|
+
const keys = [];
|
|
158
|
+
const args = [reservationId, limits.length];
|
|
159
|
+
for (const limit of limits) {
|
|
160
|
+
keys.push(...(await keysFor(tenantId, limit)));
|
|
161
|
+
args.push(limit.units);
|
|
162
|
+
}
|
|
163
|
+
await this.#redis.evalScript(REFUND_SCRIPT, keys, args);
|
|
164
|
+
}
|
|
165
|
+
async usage(tenantId, limit, nowMs) {
|
|
166
|
+
const result = await this.#redis.evalScript(USAGE_SCRIPT, await keysFor(tenantId, limit), [
|
|
167
|
+
nowMs === undefined ? '' : String(nowMs),
|
|
168
|
+
WINDOW_MS[limit.window],
|
|
169
|
+
]);
|
|
170
|
+
return toInt(result);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=redis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis.js","sourceRoot":"","sources":["../../src/caps/redis.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,UAAU,EAA2C,MAAM,YAAY,CAAC;AAE9G,qCAAqC;AACrC,MAAM,CAAC,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkD7B,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;CAa5B,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;CAiB3B,CAAC;AAQF,sFAAsF;AACtF,MAAM,UAAU,aAAa,CAAC,MAE7B;IACC,OAAO;QACL,UAAU,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;KACvF,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,OAAO,CAAC,QAAgB,EAAE,KAAuB;IAC9D,MAAM,IAAI,GAAG,iBAAiB,MAAM,UAAU,CAAC,QAAQ,CAAC,KAAK,KAAK,CAAC,GAAG,EAAE,CAAC;IACzE,OAAO,CAAC,GAAG,IAAI,IAAI,EAAE,GAAG,IAAI,IAAI,CAAC,CAAC;AACpC,CAAC;AAED,SAAS,KAAK,CAAC,KAAc;IAC3B,MAAM,CAAC,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;IACrG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;IAC3E,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,OAAO,gBAAgB;IAClB,MAAM,CAAoB;IAC1B,UAAU,CAAS;IAE5B;;;OAGG;IACH,YAAY,KAAwB,EAAE,OAAO,GAAgC,EAAE;QAC7E,MAAM,GAAG,GAAG,OAAO,CAAC,cAAc,CAAC;QACnC,IAAI,GAAG,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC;YAC9D,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;QAC/D,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC,UAAU,GAAG,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC;IACvD,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,QAAgB,EAAE,aAAqB,EAAE,MAAmC,EAAE,KAAyB;QACnH,MAAM,IAAI,GAAa,EAAE,CAAC;QAC1B,MAAM,IAAI,GAAwB,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QAC5H,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,OAAO,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;YAC/C,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QAC/D,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;QACxE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QACpG,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAA6B,CAAC;QACxE,IAAI,EAAE,KAAK,CAAC;YAAE,OAAO;QACrB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QAChC,IAAI,KAAK,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QAC1E,MAAM,IAAI,gBAAgB,CAAC;YACzB,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI;SAC7G,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,QAAgB,EAAE,aAAqB,EAAE,MAAmC;QACvF,MAAM,IAAI,GAAa,EAAE,CAAC;QAC1B,MAAM,IAAI,GAAwB,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QACjE,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,OAAO,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;YAC/C,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACzB,CAAC;QACD,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,aAAa,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAC1D,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,QAAgB,EAAE,KAAuB,EAAE,KAAyB;QAC9E,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,YAAY,EAAE,MAAM,OAAO,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE;YACxF,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;YACxC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC;SACxB,CAAC,CAAC;QACH,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC;IACvB,CAAC;CACF"}
|
package/dist/client.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ import { PassportsClient } from './resources/passports.js';
|
|
|
21
21
|
import { DpdpClient } from './resources/dpdp.js';
|
|
22
22
|
import { CommerceClient } from './resources/commerce.js';
|
|
23
23
|
import { ToolManifest, type EnforceOptions, type EnforceResult, type WrapToolOptions, type EnforceMiddlewareOptions } from './manifest.js';
|
|
24
|
+
import { RevocationFeed, type RevocationFeedState } from './revocations/index.js';
|
|
25
|
+
import { DecisionsClient } from './resources/decisions.js';
|
|
24
26
|
import type { AuthorizationRequest, AuthorizeParams, GrantexClientOptions, RateLimit, RotateKeyResponse, SignupParams, SignupResponse, UpdateDeveloperSettingsParams, UpdateDeveloperSettingsResponse } from './types.js';
|
|
25
27
|
export declare class Grantex {
|
|
26
28
|
#private;
|
|
@@ -46,6 +48,7 @@ export declare class Grantex {
|
|
|
46
48
|
readonly passports: PassportsClient;
|
|
47
49
|
readonly dpdp: DpdpClient;
|
|
48
50
|
readonly commerce: CommerceClient;
|
|
51
|
+
readonly decisions: DecisionsClient;
|
|
49
52
|
get lastRateLimit(): RateLimit | undefined;
|
|
50
53
|
constructor(options?: GrantexClientOptions);
|
|
51
54
|
/**
|
|
@@ -101,6 +104,15 @@ export declare class Grantex {
|
|
|
101
104
|
* if (!result.allowed) throw new Error(result.reason);
|
|
102
105
|
* ```
|
|
103
106
|
*/
|
|
107
|
+
/**
|
|
108
|
+
* The revocation feed this client uses for `revocationCheck: 'feed'`,
|
|
109
|
+
* started on first use. Call `stopRevocationFeed()` to close it.
|
|
110
|
+
*/
|
|
111
|
+
revocationFeed(): RevocationFeed;
|
|
112
|
+
/** What the revocation feed knows, and whether it is fresh enough to be trusted. */
|
|
113
|
+
revocationFeedState(): RevocationFeedState | undefined;
|
|
114
|
+
/** Stop following the revocation feed and release its connection. */
|
|
115
|
+
stopRevocationFeed(): Promise<void>;
|
|
104
116
|
enforce(options: EnforceOptions): Promise<EnforceResult>;
|
|
105
117
|
/**
|
|
106
118
|
* Wrap a LangChain StructuredTool with automatic Grantex scope enforcement.
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|