@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.
Files changed (181) hide show
  1. package/LICENSE +185 -185
  2. package/NOTICE +16 -16
  3. package/README.md +974 -947
  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/purpose.d.ts +29 -0
  117. package/dist/purpose.d.ts.map +1 -0
  118. package/dist/purpose.js +60 -0
  119. package/dist/purpose.js.map +1 -0
  120. package/dist/resources/agents.js +3 -3
  121. package/dist/resources/agents.js.map +1 -1
  122. package/dist/resources/anomalies.d.ts +3 -1
  123. package/dist/resources/anomalies.d.ts.map +1 -1
  124. package/dist/resources/anomalies.js +7 -1
  125. package/dist/resources/anomalies.js.map +1 -1
  126. package/dist/resources/audit.js +1 -1
  127. package/dist/resources/audit.js.map +1 -1
  128. package/dist/resources/budgets.js +2 -2
  129. package/dist/resources/budgets.js.map +1 -1
  130. package/dist/resources/credentials.js +1 -1
  131. package/dist/resources/credentials.js.map +1 -1
  132. package/dist/resources/decisions.d.ts +69 -0
  133. package/dist/resources/decisions.d.ts.map +1 -0
  134. package/dist/resources/decisions.js +115 -0
  135. package/dist/resources/decisions.js.map +1 -0
  136. package/dist/resources/domains.js +2 -2
  137. package/dist/resources/domains.js.map +1 -1
  138. package/dist/resources/dpdp.js +4 -4
  139. package/dist/resources/dpdp.js.map +1 -1
  140. package/dist/resources/grants.js +2 -2
  141. package/dist/resources/grants.js.map +1 -1
  142. package/dist/resources/policies.js +3 -3
  143. package/dist/resources/policies.js.map +1 -1
  144. package/dist/resources/scim.js +5 -5
  145. package/dist/resources/scim.js.map +1 -1
  146. package/dist/resources/tokens.d.ts.map +1 -1
  147. package/dist/resources/tokens.js +5 -1
  148. package/dist/resources/tokens.js.map +1 -1
  149. package/dist/resources/vault.js +2 -2
  150. package/dist/resources/vault.js.map +1 -1
  151. package/dist/resources/webauthn.d.ts +6 -1
  152. package/dist/resources/webauthn.d.ts.map +1 -1
  153. package/dist/resources/webauthn.js +5 -1
  154. package/dist/resources/webauthn.js.map +1 -1
  155. package/dist/resources/webhooks.js +1 -1
  156. package/dist/resources/webhooks.js.map +1 -1
  157. package/dist/revocations/feed.d.ts +61 -0
  158. package/dist/revocations/feed.d.ts.map +1 -0
  159. package/dist/revocations/feed.js +282 -0
  160. package/dist/revocations/feed.js.map +1 -0
  161. package/dist/revocations/index.d.ts +20 -0
  162. package/dist/revocations/index.d.ts.map +1 -0
  163. package/dist/revocations/index.js +19 -0
  164. package/dist/revocations/index.js.map +1 -0
  165. package/dist/revocations/set.d.ts +59 -0
  166. package/dist/revocations/set.d.ts.map +1 -0
  167. package/dist/revocations/set.js +87 -0
  168. package/dist/revocations/set.js.map +1 -0
  169. package/dist/scopes.d.ts +26 -0
  170. package/dist/scopes.d.ts.map +1 -0
  171. package/dist/scopes.js +37 -0
  172. package/dist/scopes.js.map +1 -0
  173. package/dist/types.d.ts +121 -4
  174. package/dist/types.d.ts.map +1 -1
  175. package/dist/types.js +0 -1
  176. package/dist/types.js.map +1 -1
  177. package/dist/verify.d.ts +31 -2
  178. package/dist/verify.d.ts.map +1 -1
  179. package/dist/verify.js +236 -29
  180. package/dist/verify.js.map +1 -1
  181. package/package.json +92 -84
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Derive the counters a tool call is metered against. Mirrors the Python SDK's
3
+ * `grantex.caps._limits`.
4
+ *
5
+ * - The **manifest** (`caps` on the tool) declares tenant-wide counters per tool.
6
+ * - The **grant** (`caps` in its `urn:grantex:tools:v1` entry) declares counters
7
+ * for that grant only: per tool (`{"verify_business": {"per_hour": 50}}`)
8
+ * and a connector cost-unit budget (`{"cost_units": {"per_day": 5000}}`).
9
+ *
10
+ * Every declared cap is its own counter and all of them must hold. A call's
11
+ * cost is the sum of the manifest `cost_units` for the components it incurs
12
+ * (all declared components unless the caller names them).
13
+ */
14
+ import { CapsConfigurationError, MAX_COUNT } from './meter.js';
15
+ const WINDOWS = ['per_hour', 'per_day', 'per_case'];
16
+ const MAX_ID = 256;
17
+ export const CASE_REQUIRED = 'case_required';
18
+ export const INVALID_CASE_ID = 'invalid_case_id';
19
+ export const INVALID_COST_COMPONENT = 'invalid_cost_component';
20
+ export const MALFORMED_GRANT_CAPS = 'malformed_grant_caps';
21
+ /** Unambiguous counter identity; identical to the Python SDK's `counter_id`. */
22
+ export function counterId(...parts) {
23
+ return JSON.stringify(parts);
24
+ }
25
+ function isPlainObject(value) {
26
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
27
+ }
28
+ function windows(raw, where) {
29
+ if (!isPlainObject(raw) || Object.keys(raw).length === 0) {
30
+ throw new CapsConfigurationError(`${where} must be a non-empty object`, MALFORMED_GRANT_CAPS);
31
+ }
32
+ const out = {};
33
+ for (const [window, value] of Object.entries(raw)) {
34
+ if (!WINDOWS.includes(window)) {
35
+ throw new CapsConfigurationError(`${where} has unknown window ${JSON.stringify(window)}`, MALFORMED_GRANT_CAPS);
36
+ }
37
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0 || value > MAX_COUNT) {
38
+ throw new CapsConfigurationError(`${where}.${window} must be an integer between 0 and ${MAX_COUNT}`, MALFORMED_GRANT_CAPS);
39
+ }
40
+ out[window] = value;
41
+ }
42
+ return out;
43
+ }
44
+ /** Validate a grant entry's `caps`; `cost_units` is the connector budget. */
45
+ export function parseGrantCaps(caps) {
46
+ const out = new Map();
47
+ if (caps === undefined || caps === null)
48
+ return out;
49
+ if (!isPlainObject(caps))
50
+ throw new CapsConfigurationError('grant caps must be an object', MALFORMED_GRANT_CAPS);
51
+ for (const [name, value] of Object.entries(caps))
52
+ out.set(name, windows(value, `caps.${name}`));
53
+ return out;
54
+ }
55
+ /**
56
+ * The limits for one call of `tool`.
57
+ *
58
+ * @throws {CapsConfigurationError} with `subReason` `case_required`,
59
+ * `invalid_case_id`, `invalid_cost_component` or `malformed_grant_caps`.
60
+ */
61
+ export function buildCapLimits(options) {
62
+ const { connector, tool, spec, grantId, caseId, costComponents } = options;
63
+ if (caseId !== undefined && (typeof caseId !== 'string' || caseId.length === 0 || caseId.length > MAX_ID)) {
64
+ throw new CapsConfigurationError('caseId must be a non-empty string of at most 256 characters', INVALID_CASE_ID);
65
+ }
66
+ const declared = { ...(spec.costUnits ?? {}) };
67
+ const components = costComponents === undefined ? Object.keys(declared) : [...costComponents];
68
+ if (costComponents !== undefined
69
+ && (new Set(components).size !== components.length || components.some((c) => !Object.prototype.hasOwnProperty.call(declared, c)))) {
70
+ throw new CapsConfigurationError(`cost components ${JSON.stringify(components)} are not all declared by tool ${JSON.stringify(tool)}`, INVALID_COST_COMPONENT);
71
+ }
72
+ if (costComponents !== undefined && Object.keys(declared).length > 0 && components.length === 0) {
73
+ // A call to a tool that declares cost units incurs at least one of them;
74
+ // an empty list would skip the budget entirely.
75
+ throw new CapsConfigurationError(`tool ${JSON.stringify(tool)} declares cost units, so costComponents cannot be empty`, INVALID_COST_COMPONENT);
76
+ }
77
+ const cost = components.reduce((sum, c) => sum + declared[c], 0);
78
+ if (cost > MAX_COUNT) {
79
+ throw new CapsConfigurationError(`the cost of this call (${cost} units) exceeds ${MAX_COUNT}`, INVALID_COST_COMPONENT);
80
+ }
81
+ const grant = parseGrantCaps(options.grantCaps);
82
+ const limits = [];
83
+ const add = (scopeParts, caps, units, scope, kind) => {
84
+ for (const window of WINDOWS) {
85
+ const limit = caps[window];
86
+ if (limit === undefined)
87
+ continue;
88
+ const parts = [...scopeParts, kind, window];
89
+ if (window === 'per_case') {
90
+ if (caseId === undefined) {
91
+ throw new CapsConfigurationError(`${scope} per_case cap on ${connector}.${tool} needs a caseId`, CASE_REQUIRED);
92
+ }
93
+ parts.push(caseId);
94
+ }
95
+ limits.push({ counter: counterId(...parts), limit, window, units, scope, kind });
96
+ }
97
+ };
98
+ if (spec.caps !== undefined) {
99
+ add(['manifest', connector, tool], { per_hour: spec.caps.perHour, per_day: spec.caps.perDay, per_case: spec.caps.perCase }, 1, 'manifest', 'calls');
100
+ }
101
+ const toolCaps = grant.get(tool);
102
+ if (toolCaps !== undefined && tool !== 'cost_units')
103
+ add(['grant', grantId, connector, tool], toolCaps, 1, 'grant', 'calls');
104
+ const budget = grant.get('cost_units');
105
+ if (cost > 0 && budget !== undefined)
106
+ add(['grant', grantId, connector], budget, cost, 'grant', 'cost_units');
107
+ return limits;
108
+ }
109
+ //# sourceMappingURL=limits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"limits.js","sourceRoot":"","sources":["../../src/caps/limits.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * In-process caps backend. **For tests only.**
3
+ *
4
+ * Counters live in one process's memory: they are not shared between workers,
5
+ * are lost on restart, and would let a multi-process deployment exceed every
6
+ * cap. Use the Redis or Postgres backend in any deployment.
7
+ */
8
+ import { type CapsBackend, type ResolvedCapLimit } from './meter.js';
9
+ export declare class InMemoryCapsBackend implements CapsBackend {
10
+ #private;
11
+ reserve(tenantId: string, reservationId: string, limits: readonly ResolvedCapLimit[], nowMs: number | undefined): Promise<void>;
12
+ refund(tenantId: string, reservationId: string, limits: readonly ResolvedCapLimit[]): Promise<void>;
13
+ usage(tenantId: string, limit: ResolvedCapLimit, nowMs: number | undefined): Promise<number>;
14
+ }
15
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/caps/memory.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAA2C,KAAK,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAQ9G,qBAAa,mBAAoB,YAAW,WAAW;;IAkB/C,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,CAiBpI;IAEK,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAOxG;IAEK,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAEjG;CACF"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * In-process caps backend. **For tests only.**
3
+ *
4
+ * Counters live in one process's memory: they are not shared between workers,
5
+ * are lost on restart, and would let a multi-process deployment exceed every
6
+ * cap. Use the Redis or Postgres backend in any deployment.
7
+ */
8
+ import { CapExceededError, WINDOW_MS, tenantHash } from './meter.js';
9
+ export class InMemoryCapsBackend {
10
+ #counters = new Map();
11
+ async #entries(tenantId, limit, now) {
12
+ const id = `${await tenantHash(tenantId)}:${limit.key}`;
13
+ let entries = this.#counters.get(id);
14
+ if (entries === undefined) {
15
+ entries = [];
16
+ this.#counters.set(id, entries);
17
+ }
18
+ const window = WINDOW_MS[limit.window];
19
+ if (window > 0) {
20
+ const live = entries.filter((e) => e.at > now - window);
21
+ entries.splice(0, entries.length, ...live);
22
+ }
23
+ return entries;
24
+ }
25
+ async reserve(tenantId, reservationId, limits, nowMs) {
26
+ const now = nowMs ?? Date.now();
27
+ // Resolve every counter before checking, then check and record with no
28
+ // await in between: JavaScript runs this block without interleaving.
29
+ const all = [];
30
+ for (const limit of limits)
31
+ all.push(await this.#entries(tenantId, limit, now));
32
+ limits.forEach((limit, i) => {
33
+ const used = all[i].reduce((sum, e) => sum + e.units, 0);
34
+ if (used + limit.units > limit.limit) {
35
+ throw new CapExceededError({
36
+ limit: limit.limit, window: limit.window, used, requested: limit.units, scope: limit.scope, kind: limit.kind,
37
+ });
38
+ }
39
+ });
40
+ limits.forEach((limit, i) => {
41
+ all[i].push({ at: now, reservationId, units: limit.units });
42
+ });
43
+ }
44
+ async refund(tenantId, reservationId, limits) {
45
+ for (const limit of limits) {
46
+ const entries = this.#counters.get(`${await tenantHash(tenantId)}:${limit.key}`);
47
+ if (entries === undefined)
48
+ continue;
49
+ const kept = entries.filter((e) => e.reservationId !== reservationId);
50
+ entries.splice(0, entries.length, ...kept);
51
+ }
52
+ }
53
+ async usage(tenantId, limit, nowMs) {
54
+ return (await this.#entries(tenantId, limit, nowMs ?? Date.now())).reduce((sum, e) => sum + e.units, 0);
55
+ }
56
+ }
57
+ //# sourceMappingURL=memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.js","sourceRoot":"","sources":["../../src/caps/memory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,107 @@
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 declare const CAP_ERROR_CODE = "E1008";
6
+ export type CapWindow = 'per_hour' | 'per_day' | 'per_case';
7
+ /** `off` skips caps, `warn` allows over-cap calls and reports them, `enforce` denies them. */
8
+ export type CapsMode = 'off' | 'warn' | 'enforce';
9
+ export declare const CAPS_MODES: readonly CapsMode[];
10
+ /** Rolling window length per window name; `per_case` has no time window. */
11
+ export declare const WINDOW_MS: Readonly<Record<CapWindow, number>>;
12
+ export declare const MAX_COUNT = 2147483647;
13
+ /** One counter a call is metered against. */
14
+ export interface CapLimit {
15
+ /** Counter identity within a tenant; per-case counters must include the case. */
16
+ counter: string;
17
+ limit: number;
18
+ window: CapWindow;
19
+ /** Units this call consumes (default 1). */
20
+ units?: number;
21
+ /** `manifest` or `grant` (default `manifest`). */
22
+ scope?: string;
23
+ /** `calls` or `cost_units` (default `calls`). */
24
+ kind?: string;
25
+ }
26
+ /** A validated limit with defaults applied. */
27
+ export interface ResolvedCapLimit extends Required<CapLimit> {
28
+ /** Stable, fixed-length storage key (SHA-256 hex). */
29
+ key: string;
30
+ }
31
+ /** Units reserved for one call. Keep it to refund a call proven unsent. */
32
+ export interface Reservation {
33
+ reservationId: string;
34
+ tenantId: string;
35
+ limits: readonly ResolvedCapLimit[];
36
+ }
37
+ export interface CounterUsage {
38
+ limit: ResolvedCapLimit;
39
+ used: number;
40
+ remaining: number;
41
+ }
42
+ /** A reservation would exceed a cap. `code` is `E1008`. */
43
+ export declare class CapExceededError extends Error {
44
+ readonly code = "E1008";
45
+ readonly reason = "cap_exceeded";
46
+ readonly limit: number;
47
+ readonly window: CapWindow;
48
+ readonly used: number;
49
+ readonly requested: number;
50
+ readonly scope: string;
51
+ readonly kind: string;
52
+ constructor(args: {
53
+ limit: number;
54
+ window: CapWindow;
55
+ used: number;
56
+ requested: number;
57
+ scope: string;
58
+ kind: string;
59
+ });
60
+ }
61
+ /** The caps backend could not answer. Callers must treat the call as denied. */
62
+ export declare class MeterUnavailableError extends Error {
63
+ constructor(message: string, options?: {
64
+ cause?: unknown;
65
+ });
66
+ }
67
+ /** Limits or arguments are invalid. `subReason` names why. */
68
+ export declare class CapsConfigurationError extends Error {
69
+ readonly subReason: string;
70
+ constructor(message: string, subReason?: string);
71
+ }
72
+ /** Storage for counters. Implementations must be atomic across all limits. */
73
+ export interface CapsBackend {
74
+ /** Reserve every limit or none; throw `CapExceededError` naming the first limit exceeded. */
75
+ reserve(tenantId: string, reservationId: string, limits: readonly ResolvedCapLimit[], nowMs: number | undefined): Promise<void>;
76
+ /** Remove a reservation. Idempotent. */
77
+ refund(tenantId: string, reservationId: string, limits: readonly ResolvedCapLimit[], nowMs: number | undefined): Promise<void>;
78
+ /** Units currently held by the counter within its window. */
79
+ usage(tenantId: string, limit: ResolvedCapLimit, nowMs: number | undefined): Promise<number>;
80
+ }
81
+ /** Storage key for a counter; identical to the Python SDK's `CapLimit.key()`. */
82
+ export declare function counterKey(window: CapWindow, counter: string): Promise<string>;
83
+ /** Fixed-length tenant component for storage keys. */
84
+ export declare function tenantHash(tenantId: string): Promise<string>;
85
+ /** Validate a reservation's arguments and apply defaults. */
86
+ export declare function resolveLimits(tenantId: unknown, limits: readonly CapLimit[]): Promise<ResolvedCapLimit[]>;
87
+ /**
88
+ * Reserve call caps and cost-unit budgets before the call that incurs cost.
89
+ *
90
+ * `reserve()` checks every limit and records the call atomically, so
91
+ * concurrent callers can never exceed a cap. A failed call is **not** refunded
92
+ * by default: a timeout or error does not prove the provider did no work. Call
93
+ * `refundUnsent()` only when the provider call is known not to have been sent.
94
+ * Any backend failure throws `MeterUnavailableError`; callers must deny.
95
+ */
96
+ export declare class CapsMeter {
97
+ #private;
98
+ constructor(backend: CapsBackend, options?: {
99
+ clock?: () => number;
100
+ });
101
+ reserve(tenantId: string, limits: readonly CapLimit[]): Promise<Reservation>;
102
+ /** Release a reservation whose provider call was proven not sent. */
103
+ refundUnsent(reservation: Reservation): Promise<void>;
104
+ /** Current usage of each counter, for display (consent page, case view). */
105
+ usage(tenantId: string, limits: readonly CapLimit[]): Promise<CounterUsage[]>;
106
+ }
107
+ //# sourceMappingURL=meter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meter.d.ts","sourceRoot":"","sources":["../../src/caps/meter.ts"],"names":[],"mappings":"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"}
@@ -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"}