@spendguard/sdk 0.5.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/CHANGELOG.md +190 -0
- package/LICENSE_NOTICES.md +127 -0
- package/README.md +151 -0
- package/dist/adapter-D9T3yEEw.d.ts +3441 -0
- package/dist/cache-DOnw8QtJ.d.ts +1164 -0
- package/dist/cache.d.ts +6 -0
- package/dist/cache.js +74 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.js +4815 -0
- package/dist/errors.d.ts +269 -0
- package/dist/errors.js +148 -0
- package/dist/ids.d.ts +69 -0
- package/dist/ids.js +61 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +5295 -0
- package/dist/otel.d.ts +118 -0
- package/dist/otel.js +84 -0
- package/dist/pricing/demo.d.ts +26 -0
- package/dist/pricing/demo.js +138 -0
- package/dist/pricing.d.ts +70 -0
- package/dist/pricing.js +92 -0
- package/dist/promptHash.d.ts +23 -0
- package/dist/promptHash.js +25 -0
- package/dist/proto.d.ts +609 -0
- package/dist/proto.js +3055 -0
- package/dist/retry.d.ts +121 -0
- package/dist/retry.js +92 -0
- package/dist/runPlan.d.ts +69 -0
- package/dist/runPlan.js +35 -0
- package/fixtures/cross-language/v1.json +327 -0
- package/package.json +123 -0
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root of the SpendGuard error hierarchy. Every error thrown by the SDK
|
|
3
|
+
* inherits from this class so adapters can route with one `instanceof` check.
|
|
4
|
+
*/
|
|
5
|
+
declare class SpendGuardError extends Error {
|
|
6
|
+
name: string;
|
|
7
|
+
constructor(message: string, opts?: {
|
|
8
|
+
cause?: unknown;
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Constructor-time configuration error. Thrown synchronously from
|
|
13
|
+
* `new SpendGuardClient(...)` or `SpendGuardClient.fromEnv()` when:
|
|
14
|
+
* - `socketPath` is missing and `SPENDGUARD_SOCKET_PATH` /
|
|
15
|
+
* `SPENDGUARD_SIDECAR_UDS` are unset.
|
|
16
|
+
* - `tenantId` is missing and `SPENDGUARD_TENANT_ID` is unset.
|
|
17
|
+
* - `otelTracer` and `onSpan` are both provided (mutually exclusive).
|
|
18
|
+
* - `SPENDGUARD_DECISION_TIMEOUT_MS` is not a finite non-negative integer.
|
|
19
|
+
*/
|
|
20
|
+
declare class SpendGuardConfigError extends SpendGuardError {
|
|
21
|
+
name: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Connection-layer failure: the UDS / gRPC channel could not be opened,
|
|
25
|
+
* the sidecar is unreachable, the deadline expired before reply, or the
|
|
26
|
+
* server cancelled mid-flight.
|
|
27
|
+
*
|
|
28
|
+
* Adapters typically map `SidecarUnavailable` → 503 upstream (the `statusCode`
|
|
29
|
+
* is the conventional HTTP analog; the SDK itself does not speak HTTP).
|
|
30
|
+
*
|
|
31
|
+
* Raised by `mapGrpcStatusToError` (SLICE 5) for:
|
|
32
|
+
* - gRPC `UNAVAILABLE` — channel torn down / sidecar gone.
|
|
33
|
+
* - gRPC `DEADLINE_EXCEEDED` — request timed out before reply.
|
|
34
|
+
* - gRPC `CANCELLED` — server (or client) cancelled mid-flight.
|
|
35
|
+
*
|
|
36
|
+
* The original `RpcError` is preserved in `cause` so adapters can dig into
|
|
37
|
+
* the underlying gRPC trailer metadata if they need to.
|
|
38
|
+
*
|
|
39
|
+
* SLICE 8 wires retry classification for the same cluster into this class
|
|
40
|
+
* via `_classify_rpc_error` parity.
|
|
41
|
+
*/
|
|
42
|
+
declare class SidecarUnavailable extends SpendGuardError {
|
|
43
|
+
name: string;
|
|
44
|
+
readonly statusCode: 503;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Connection-not-established error. Thrown when the caller invokes an RPC
|
|
48
|
+
* before `connect()` (or attempts to read `sessionId` before `handshake()`).
|
|
49
|
+
*/
|
|
50
|
+
declare class SpendGuardConnectionError extends SpendGuardError {
|
|
51
|
+
name: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Handshake-protocol failure: the sidecar replied with a protocol-version
|
|
55
|
+
* mismatch, a capability level the adapter cannot satisfy, or an unsigned
|
|
56
|
+
* announcement when one was required.
|
|
57
|
+
*
|
|
58
|
+
* SLICE 4 wires the handshake payload mapping; the class itself is locked here.
|
|
59
|
+
*/
|
|
60
|
+
declare class HandshakeError extends SpendGuardError {
|
|
61
|
+
name: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Init payload for `DecisionDenied` (and its subclasses). All decision-typed
|
|
65
|
+
* errors carry the audit chain coordinates so adapters can correlate with the
|
|
66
|
+
* sidecar's emitted CloudEvents.
|
|
67
|
+
*/
|
|
68
|
+
interface DecisionDeniedInit {
|
|
69
|
+
decisionId: string;
|
|
70
|
+
reasonCodes?: readonly string[];
|
|
71
|
+
auditDecisionEventId?: string;
|
|
72
|
+
matchedRuleIds?: readonly string[];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Decision-time denial. Thrown when the sidecar returns a non-`CONTINUE` /
|
|
76
|
+
* non-`DEGRADE` outcome. Subclasses discriminate by terminal kind:
|
|
77
|
+
* `DecisionStopped` (STOP / STOP_RUN_PROJECTION), `DecisionSkipped` (SKIP),
|
|
78
|
+
* `ApprovalRequired` (REQUIRE_APPROVAL).
|
|
79
|
+
*
|
|
80
|
+
* SLICE 4 wires the mapping from `DecisionResponse.decision` enum to the
|
|
81
|
+
* concrete subclass; this base class is locked here.
|
|
82
|
+
*/
|
|
83
|
+
declare class DecisionDenied extends SpendGuardError {
|
|
84
|
+
name: string;
|
|
85
|
+
readonly statusCode: 403;
|
|
86
|
+
readonly decisionId: string;
|
|
87
|
+
readonly reasonCodes: readonly string[];
|
|
88
|
+
readonly auditDecisionEventId?: string;
|
|
89
|
+
readonly matchedRuleIds: readonly string[];
|
|
90
|
+
constructor(message: string, init: DecisionDeniedInit, opts?: {
|
|
91
|
+
cause?: unknown;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Sidecar returned `STOP` or `STOP_RUN_PROJECTION`. Run loop must unwind
|
|
96
|
+
* without further LLM / tool calls. `reasonCodes` carry the contract-DSL
|
|
97
|
+
* matched-rule outcomes (`projection.run.over_threshold`, etc.).
|
|
98
|
+
*/
|
|
99
|
+
declare class DecisionStopped extends DecisionDenied {
|
|
100
|
+
name: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Sidecar returned `SKIP` — current step / call must be skipped but the run
|
|
104
|
+
* may continue. Treated as a terminal decision for the in-flight boundary
|
|
105
|
+
* only.
|
|
106
|
+
*/
|
|
107
|
+
declare class DecisionSkipped extends DecisionDenied {
|
|
108
|
+
name: string;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Init payload for `ApprovalRequired`. Carries the approval bookkeeping the
|
|
112
|
+
* adapter needs to drive the human-in-the-loop resume round-trip.
|
|
113
|
+
*/
|
|
114
|
+
interface ApprovalRequiredInit extends DecisionDeniedInit {
|
|
115
|
+
approvalRequestId: string;
|
|
116
|
+
approverRole?: string;
|
|
117
|
+
tenantId?: string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Minimal client shape that `ApprovalRequired.resume()` needs. Avoids a
|
|
121
|
+
* circular import between `errors.ts` and `client.ts`; the real
|
|
122
|
+
* `SpendGuardClient` satisfies this contract.
|
|
123
|
+
*/
|
|
124
|
+
interface ApprovalResumeClient {
|
|
125
|
+
resumeAfterApproval(req: {
|
|
126
|
+
approvalId: string;
|
|
127
|
+
tenantId: string;
|
|
128
|
+
decisionId: string;
|
|
129
|
+
}): Promise<unknown>;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Sidecar returned `REQUIRE_APPROVAL`. The adapter must surface the approval
|
|
133
|
+
* to a human operator (Slack / control plane / etc.) and call
|
|
134
|
+
* `await err.resume(client)` after the operator acts.
|
|
135
|
+
*
|
|
136
|
+
* `tenantId` is carried explicitly so the resume round-trip can scope the
|
|
137
|
+
* GetApprovalForResume lookup against tenant (per Python `client.py` round-2
|
|
138
|
+
* #9 part 2 PR 9d).
|
|
139
|
+
*/
|
|
140
|
+
declare class ApprovalRequired extends DecisionDenied {
|
|
141
|
+
name: string;
|
|
142
|
+
readonly approvalRequestId: string;
|
|
143
|
+
readonly approverRole?: string;
|
|
144
|
+
readonly tenantId?: string;
|
|
145
|
+
constructor(message: string, init: ApprovalRequiredInit, opts?: {
|
|
146
|
+
cause?: unknown;
|
|
147
|
+
});
|
|
148
|
+
/**
|
|
149
|
+
* Resume the decision after a human operator has acted. Delegates to
|
|
150
|
+
* `client.resumeAfterApproval(...)`.
|
|
151
|
+
*
|
|
152
|
+
* @throws ApprovalDeniedError when the operator rejected.
|
|
153
|
+
* @throws ApprovalLapsedError when the approval expired / was cancelled.
|
|
154
|
+
* @throws ApprovalBundleHotReloadedError when bundle rotated mid-approval.
|
|
155
|
+
*/
|
|
156
|
+
resume(client: ApprovalResumeClient): Promise<unknown>;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Operator explicitly denied the approval. Carries `approverSubject` and
|
|
160
|
+
* `approverReason` from the audit row so the adapter can surface them upstream.
|
|
161
|
+
*
|
|
162
|
+
* Reason codes are forced to include `approval_denied` as the first entry per
|
|
163
|
+
* Python parity.
|
|
164
|
+
*/
|
|
165
|
+
declare class ApprovalDeniedError extends DecisionDenied {
|
|
166
|
+
name: string;
|
|
167
|
+
readonly approverSubject?: string;
|
|
168
|
+
readonly approverReason?: string;
|
|
169
|
+
constructor(message: string, init: DecisionDeniedInit & {
|
|
170
|
+
approverSubject?: string;
|
|
171
|
+
approverReason?: string;
|
|
172
|
+
}, opts?: {
|
|
173
|
+
cause?: unknown;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Approval state was non-terminal at the time the adapter polled. State is
|
|
178
|
+
* one of `pending` / `expired` / `cancelled` / `unknown`; the reason codes
|
|
179
|
+
* include `approval_lapsed_<state>` per Python parity.
|
|
180
|
+
*/
|
|
181
|
+
declare class ApprovalLapsedError extends DecisionDenied {
|
|
182
|
+
name: string;
|
|
183
|
+
readonly state: "pending" | "expired" | "cancelled" | "unknown";
|
|
184
|
+
constructor(message: string, init: DecisionDeniedInit & {
|
|
185
|
+
state: "pending" | "expired" | "cancelled" | "unknown";
|
|
186
|
+
}, opts?: {
|
|
187
|
+
cause?: unknown;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Approval was issued under one contract-bundle hash but the sidecar's
|
|
192
|
+
* currently-installed bundle differs. The adapter MUST refuse to resume —
|
|
193
|
+
* re-evaluation may produce a different decision that the operator did not
|
|
194
|
+
* see when they approved.
|
|
195
|
+
*
|
|
196
|
+
* Raised by `mapGrpcStatusToError` (SLICE 5) when a release / reserve trip
|
|
197
|
+
* surfaces gRPC `FAILED_PRECONDITION` with the reason-code metadata field
|
|
198
|
+
* set to `BUNDLE_HOT_RELOADED`. When the bundle hashes are not carried in
|
|
199
|
+
* trailer metadata the constructor receives empty strings; adapters should
|
|
200
|
+
* treat that as "hashes unavailable" and re-fetch from the handshake cache.
|
|
201
|
+
*/
|
|
202
|
+
declare class ApprovalBundleHotReloadedError extends SpendGuardError {
|
|
203
|
+
name: string;
|
|
204
|
+
readonly originalBundleHash: string;
|
|
205
|
+
readonly currentBundleHash: string;
|
|
206
|
+
constructor(message: string, init: {
|
|
207
|
+
originalBundleHash: string;
|
|
208
|
+
currentBundleHash: string;
|
|
209
|
+
}, opts?: {
|
|
210
|
+
cause?: unknown;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Adapter's `publish_effect` step failed to apply the mutation patch. The
|
|
215
|
+
* adapter should call `client.safeConfirmApplyFailed(...)` to anchor the
|
|
216
|
+
* rollback in the audit chain.
|
|
217
|
+
*
|
|
218
|
+
* Raised by `mapGrpcStatusToError` (SLICE 5) when a release / reserve trip
|
|
219
|
+
* surfaces gRPC `FAILED_PRECONDITION` with the reason-code metadata field set
|
|
220
|
+
* to `IDEMPOTENCY_CONFLICT` (replay of a release with a different request
|
|
221
|
+
* body), `BUDGET_EXCEEDED` (reservation can no longer be released against the
|
|
222
|
+
* current ledger state), or any unknown FAILED_PRECONDITION reason — the
|
|
223
|
+
* latter is the conservative default so adapters never see a bare
|
|
224
|
+
* `SpendGuardError` for FAILED_PRECONDITION trips.
|
|
225
|
+
*/
|
|
226
|
+
declare class MutationApplyFailed extends SpendGuardError {
|
|
227
|
+
name: string;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Decision-time wrapper for arbitrary post-decision errors that don't fit the
|
|
231
|
+
* `DecisionDenied` hierarchy. Provided as a discriminated subtype so adapters
|
|
232
|
+
* that want a single `catch (e: SpendGuardDecisionError)` block work without
|
|
233
|
+
* pattern-matching on the structural `DecisionDenied` chain.
|
|
234
|
+
*
|
|
235
|
+
* SLICE 4 may extend this with concrete subclasses; in SLICE 3 it exists only
|
|
236
|
+
* as the discriminated-subtype contract the slice doc requires.
|
|
237
|
+
*/
|
|
238
|
+
declare class SpendGuardDecisionError extends SpendGuardError {
|
|
239
|
+
name: string;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Thrown by `PricingLookup.usdMicrosForCall` when a token bucket has a
|
|
243
|
+
* non-zero count but NO configured price for either the specific token kind
|
|
244
|
+
* or the default kind.
|
|
245
|
+
*
|
|
246
|
+
* Fail-closed rationale: the previous behavior silently coerced the missing
|
|
247
|
+
* price to `0`, booking a `$0` charge for the call and under-counting the
|
|
248
|
+
* budget — exactly the under-charge failure mode the guardrail exists to
|
|
249
|
+
* prevent. Unknown / new models (the most likely to be mispriced) were
|
|
250
|
+
* precisely the ones that escaped accounting. Refusing loudly forces the
|
|
251
|
+
* adapter to supply a price (or explicitly handle the gap) rather than leak
|
|
252
|
+
* spend. Carries the offending `provider` / `model` / `tokenKind` so the
|
|
253
|
+
* caller can pinpoint the missing pricing-table row.
|
|
254
|
+
*/
|
|
255
|
+
declare class PricingMissingError extends SpendGuardError {
|
|
256
|
+
name: string;
|
|
257
|
+
readonly provider: string;
|
|
258
|
+
readonly model: string;
|
|
259
|
+
readonly tokenKind: string;
|
|
260
|
+
constructor(args: {
|
|
261
|
+
provider: string;
|
|
262
|
+
model: string;
|
|
263
|
+
tokenKind: string;
|
|
264
|
+
}, opts?: {
|
|
265
|
+
cause?: unknown;
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export { ApprovalBundleHotReloadedError, ApprovalDeniedError, ApprovalLapsedError, ApprovalRequired, type ApprovalRequiredInit, type ApprovalResumeClient, DecisionDenied, type DecisionDeniedInit, DecisionSkipped, DecisionStopped, HandshakeError, MutationApplyFailed, PricingMissingError, SidecarUnavailable, SpendGuardConfigError, SpendGuardConnectionError, SpendGuardDecisionError, SpendGuardError };
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var SpendGuardError = class extends Error {
|
|
3
|
+
name = "SpendGuardError";
|
|
4
|
+
constructor(message, opts) {
|
|
5
|
+
super(message);
|
|
6
|
+
if (opts?.cause !== void 0) {
|
|
7
|
+
this.cause = opts.cause;
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(this, "name", {
|
|
10
|
+
value: this.name,
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
writable: true
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var SpendGuardConfigError = class extends SpendGuardError {
|
|
18
|
+
name = "SpendGuardConfigError";
|
|
19
|
+
};
|
|
20
|
+
var SidecarUnavailable = class extends SpendGuardError {
|
|
21
|
+
name = "SidecarUnavailable";
|
|
22
|
+
statusCode = 503;
|
|
23
|
+
};
|
|
24
|
+
var SpendGuardConnectionError = class extends SpendGuardError {
|
|
25
|
+
name = "SpendGuardConnectionError";
|
|
26
|
+
};
|
|
27
|
+
var HandshakeError = class extends SpendGuardError {
|
|
28
|
+
name = "HandshakeError";
|
|
29
|
+
};
|
|
30
|
+
var DecisionDenied = class extends SpendGuardError {
|
|
31
|
+
name = "DecisionDenied";
|
|
32
|
+
statusCode = 403;
|
|
33
|
+
decisionId;
|
|
34
|
+
reasonCodes;
|
|
35
|
+
auditDecisionEventId;
|
|
36
|
+
matchedRuleIds;
|
|
37
|
+
constructor(message, init, opts) {
|
|
38
|
+
super(message, opts);
|
|
39
|
+
this.decisionId = init.decisionId;
|
|
40
|
+
this.reasonCodes = init.reasonCodes ?? [];
|
|
41
|
+
if (init.auditDecisionEventId !== void 0) {
|
|
42
|
+
this.auditDecisionEventId = init.auditDecisionEventId;
|
|
43
|
+
}
|
|
44
|
+
this.matchedRuleIds = init.matchedRuleIds ?? [];
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
var DecisionStopped = class extends DecisionDenied {
|
|
48
|
+
name = "DecisionStopped";
|
|
49
|
+
};
|
|
50
|
+
var DecisionSkipped = class extends DecisionDenied {
|
|
51
|
+
name = "DecisionSkipped";
|
|
52
|
+
};
|
|
53
|
+
var ApprovalRequired = class extends DecisionDenied {
|
|
54
|
+
name = "ApprovalRequired";
|
|
55
|
+
approvalRequestId;
|
|
56
|
+
approverRole;
|
|
57
|
+
tenantId;
|
|
58
|
+
constructor(message, init, opts) {
|
|
59
|
+
super(message, init, opts);
|
|
60
|
+
this.approvalRequestId = init.approvalRequestId;
|
|
61
|
+
if (init.approverRole !== void 0) {
|
|
62
|
+
this.approverRole = init.approverRole;
|
|
63
|
+
}
|
|
64
|
+
if (init.tenantId !== void 0) {
|
|
65
|
+
this.tenantId = init.tenantId;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Resume the decision after a human operator has acted. Delegates to
|
|
70
|
+
* `client.resumeAfterApproval(...)`.
|
|
71
|
+
*
|
|
72
|
+
* @throws ApprovalDeniedError when the operator rejected.
|
|
73
|
+
* @throws ApprovalLapsedError when the approval expired / was cancelled.
|
|
74
|
+
* @throws ApprovalBundleHotReloadedError when bundle rotated mid-approval.
|
|
75
|
+
*/
|
|
76
|
+
async resume(client) {
|
|
77
|
+
return client.resumeAfterApproval({
|
|
78
|
+
approvalId: this.approvalRequestId,
|
|
79
|
+
tenantId: this.tenantId ?? "",
|
|
80
|
+
decisionId: this.decisionId
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
var ApprovalDeniedError = class extends DecisionDenied {
|
|
85
|
+
name = "ApprovalDeniedError";
|
|
86
|
+
approverSubject;
|
|
87
|
+
approverReason;
|
|
88
|
+
constructor(message, init, opts) {
|
|
89
|
+
super(
|
|
90
|
+
message,
|
|
91
|
+
{ ...init, reasonCodes: ["approval_denied", ...init.reasonCodes ?? []] },
|
|
92
|
+
opts
|
|
93
|
+
);
|
|
94
|
+
if (init.approverSubject !== void 0) {
|
|
95
|
+
this.approverSubject = init.approverSubject;
|
|
96
|
+
}
|
|
97
|
+
if (init.approverReason !== void 0) {
|
|
98
|
+
this.approverReason = init.approverReason;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var ApprovalLapsedError = class extends DecisionDenied {
|
|
103
|
+
name = "ApprovalLapsedError";
|
|
104
|
+
state;
|
|
105
|
+
constructor(message, init, opts) {
|
|
106
|
+
super(
|
|
107
|
+
message,
|
|
108
|
+
{ ...init, reasonCodes: [`approval_lapsed_${init.state}`, ...init.reasonCodes ?? []] },
|
|
109
|
+
opts
|
|
110
|
+
);
|
|
111
|
+
this.state = init.state;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
var ApprovalBundleHotReloadedError = class extends SpendGuardError {
|
|
115
|
+
name = "ApprovalBundleHotReloadedError";
|
|
116
|
+
originalBundleHash;
|
|
117
|
+
currentBundleHash;
|
|
118
|
+
constructor(message, init, opts) {
|
|
119
|
+
super(message, opts);
|
|
120
|
+
this.originalBundleHash = init.originalBundleHash;
|
|
121
|
+
this.currentBundleHash = init.currentBundleHash;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
var MutationApplyFailed = class extends SpendGuardError {
|
|
125
|
+
name = "MutationApplyFailed";
|
|
126
|
+
};
|
|
127
|
+
var SpendGuardDecisionError = class extends SpendGuardError {
|
|
128
|
+
name = "SpendGuardDecisionError";
|
|
129
|
+
};
|
|
130
|
+
var PricingMissingError = class extends SpendGuardError {
|
|
131
|
+
name = "PricingMissingError";
|
|
132
|
+
provider;
|
|
133
|
+
model;
|
|
134
|
+
tokenKind;
|
|
135
|
+
constructor(args, opts) {
|
|
136
|
+
super(
|
|
137
|
+
`no price configured for provider=${JSON.stringify(args.provider)} model=${JSON.stringify(args.model)} tokenKind=${JSON.stringify(args.tokenKind)} (neither the specific kind nor the default kind has a price); refusing to charge $0 \u2014 supply a price for this model or handle PricingMissingError`,
|
|
138
|
+
opts
|
|
139
|
+
);
|
|
140
|
+
this.provider = args.provider;
|
|
141
|
+
this.model = args.model;
|
|
142
|
+
this.tokenKind = args.tokenKind;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export { ApprovalBundleHotReloadedError, ApprovalDeniedError, ApprovalLapsedError, ApprovalRequired, DecisionDenied, DecisionSkipped, DecisionStopped, HandshakeError, MutationApplyFailed, PricingMissingError, SidecarUnavailable, SpendGuardConfigError, SpendGuardConnectionError, SpendGuardDecisionError, SpendGuardError };
|
|
147
|
+
//# sourceMappingURL=errors.js.map
|
|
148
|
+
//# sourceMappingURL=errors.js.map
|
package/dist/ids.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mint a UUIDv7 per RFC 9562 §5.7.
|
|
3
|
+
*
|
|
4
|
+
* Layout (128 bits, big-endian):
|
|
5
|
+
* - 48 bits unix epoch ms
|
|
6
|
+
* - 4 bits version (0b0111)
|
|
7
|
+
* - 12 bits random
|
|
8
|
+
* - 2 bits variant (0b10)
|
|
9
|
+
* - 62 bits random
|
|
10
|
+
*
|
|
11
|
+
* Two calls within the same ms are time-ordered to ms precision but
|
|
12
|
+
* randomised within the ms slot. Returned in canonical 36-char hex form
|
|
13
|
+
* (`xxxxxxxx-xxxx-7xxx-yxxx-xxxxxxxxxxxx`).
|
|
14
|
+
*/
|
|
15
|
+
declare function newUuid7(): string;
|
|
16
|
+
/**
|
|
17
|
+
* Deterministic idempotency key for a trigger boundary.
|
|
18
|
+
*
|
|
19
|
+
* Same `(tenantId, sessionId, runId, stepId, llmCallId, trigger)` →
|
|
20
|
+
* same key. A retry of the SAME logical step within an agent framework's
|
|
21
|
+
* run loop MUST reuse this so the sidecar's cache short-circuits + the
|
|
22
|
+
* ledger's `UNIQUE` returns Replay.
|
|
23
|
+
*
|
|
24
|
+
* Returns `"sg-"` + 32 hex chars (128-bit BLAKE2b digest).
|
|
25
|
+
*
|
|
26
|
+
* **Cross-language gate (P0 — review-standards §2.2)**: the TS output for
|
|
27
|
+
* any given input is byte-identical to Python
|
|
28
|
+
* `derive_idempotency_key(**kwargs)` (which uses `hashlib.blake2b(...,
|
|
29
|
+
* digest_size=16)`). The implementation here calls the audited
|
|
30
|
+
* `@noble/hashes` BLAKE2b primitive with `dkLen: 16` — the same 128-bit
|
|
31
|
+
* personalised-output mode Python's hashlib exposes. Verified byte-for-byte
|
|
32
|
+
* against `derive_idempotency_key(**kwargs)` for 7 fixtures in
|
|
33
|
+
* `tests/ids.test.ts` (FX1–FX7).
|
|
34
|
+
*/
|
|
35
|
+
declare function deriveIdempotencyKey(args: {
|
|
36
|
+
tenantId: string;
|
|
37
|
+
sessionId: string;
|
|
38
|
+
runId: string;
|
|
39
|
+
stepId: string;
|
|
40
|
+
llmCallId: string;
|
|
41
|
+
trigger: string;
|
|
42
|
+
}): string;
|
|
43
|
+
/**
|
|
44
|
+
* Derive a stable UUID (v4-shaped) from a content signature + scope namespace.
|
|
45
|
+
*
|
|
46
|
+
* Used for content-derived `decision_id` / `llm_call_id` slots where a retry
|
|
47
|
+
* of the SAME `Model.request()` MUST produce the same UUID across retries.
|
|
48
|
+
* Scope ("decision_id", "llm_call_id", etc.) namespaces the output so
|
|
49
|
+
* different identifier slots never collide for the same signature.
|
|
50
|
+
*
|
|
51
|
+
* Same `(signature, scope)` → same UUID. Byte-equivalent to Python
|
|
52
|
+
* `spendguard.ids.derive_uuid_from_signature(signature, scope=scope)`:
|
|
53
|
+
* BLAKE2b-128 over `f"{scope}|{signature}"`, with RFC 4122 v4 version (0x4)
|
|
54
|
+
* and variant (0b10) nibbles overlaid on bytes 6 + 8. Verified against
|
|
55
|
+
* 5 fixtures in `tests/ids.test.ts` (FXU1–FXU5).
|
|
56
|
+
*/
|
|
57
|
+
declare function deriveUuidFromSignature(signature: string, args: {
|
|
58
|
+
scope: string;
|
|
59
|
+
}): string;
|
|
60
|
+
/**
|
|
61
|
+
* Sidecar workload identity hint.
|
|
62
|
+
*
|
|
63
|
+
* The adapter asserts this in handshake; the sidecar verifies against
|
|
64
|
+
* SO_PEERCRED + signed manifest. Reads `SPENDGUARD_WORKLOAD_INSTANCE_ID`
|
|
65
|
+
* env var; returns empty string when unset.
|
|
66
|
+
*/
|
|
67
|
+
declare function workloadInstanceId(): string;
|
|
68
|
+
|
|
69
|
+
export { deriveIdempotencyKey, deriveUuidFromSignature, newUuid7, workloadInstanceId };
|
package/dist/ids.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
import { blake2b } from '@noble/hashes/blake2';
|
|
3
|
+
|
|
4
|
+
// src/ids.ts
|
|
5
|
+
function newUuid7() {
|
|
6
|
+
const tsMs = BigInt(Date.now()) & (1n << 48n) - 1n;
|
|
7
|
+
const randA = randomBytes(2).readUInt16BE(0) & 4095;
|
|
8
|
+
const randB = randomBytes(8);
|
|
9
|
+
randB[0] = randB[0] & 63 | 128;
|
|
10
|
+
const hi = tsMs << 16n | BigInt(randA);
|
|
11
|
+
const buf = Buffer.alloc(16);
|
|
12
|
+
buf.writeBigUInt64BE(hi, 0);
|
|
13
|
+
buf[6] = buf[6] & 15 | 112;
|
|
14
|
+
randB.copy(buf, 8);
|
|
15
|
+
return [
|
|
16
|
+
buf.toString("hex", 0, 4),
|
|
17
|
+
buf.toString("hex", 4, 6),
|
|
18
|
+
buf.toString("hex", 6, 8),
|
|
19
|
+
buf.toString("hex", 8, 10),
|
|
20
|
+
buf.toString("hex", 10, 16)
|
|
21
|
+
].join("-");
|
|
22
|
+
}
|
|
23
|
+
var FIELD_SEP = "";
|
|
24
|
+
function deriveIdempotencyKey(args) {
|
|
25
|
+
const canonical = [
|
|
26
|
+
"v1",
|
|
27
|
+
args.tenantId,
|
|
28
|
+
args.sessionId,
|
|
29
|
+
args.runId,
|
|
30
|
+
args.stepId,
|
|
31
|
+
args.llmCallId,
|
|
32
|
+
args.trigger
|
|
33
|
+
].join(FIELD_SEP);
|
|
34
|
+
const digest = blake2b(new TextEncoder().encode(canonical), { dkLen: 16 });
|
|
35
|
+
let hex = "";
|
|
36
|
+
for (let i = 0; i < digest.length; i++) {
|
|
37
|
+
hex += digest[i].toString(16).padStart(2, "0");
|
|
38
|
+
}
|
|
39
|
+
return `sg-${hex}`;
|
|
40
|
+
}
|
|
41
|
+
function deriveUuidFromSignature(signature, args) {
|
|
42
|
+
const canonical = `${args.scope}|${signature}`;
|
|
43
|
+
const digest = blake2b(new TextEncoder().encode(canonical), { dkLen: 16 });
|
|
44
|
+
const buf = Buffer.from(digest);
|
|
45
|
+
buf[6] = buf[6] & 15 | 64;
|
|
46
|
+
buf[8] = buf[8] & 63 | 128;
|
|
47
|
+
return [
|
|
48
|
+
buf.toString("hex", 0, 4),
|
|
49
|
+
buf.toString("hex", 4, 6),
|
|
50
|
+
buf.toString("hex", 6, 8),
|
|
51
|
+
buf.toString("hex", 8, 10),
|
|
52
|
+
buf.toString("hex", 10, 16)
|
|
53
|
+
].join("-");
|
|
54
|
+
}
|
|
55
|
+
function workloadInstanceId() {
|
|
56
|
+
return process.env.SPENDGUARD_WORKLOAD_INSTANCE_ID ?? "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { deriveIdempotencyKey, deriveUuidFromSignature, newUuid7, workloadInstanceId };
|
|
60
|
+
//# sourceMappingURL=ids.js.map
|
|
61
|
+
//# sourceMappingURL=ids.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { R as RunProjectionPolicy } from './cache-DOnw8QtJ.js';
|
|
2
|
+
export { A as ApplyFailedRequest, B as BudgetClaim, C as ClaimEstimate, a as CommitEstimatedRequest, b as CommitSessionDeltaOutcome, c as CommitSessionDeltaRequest, D as DEFAULT_CACHE_MAX_ENTRIES, d as DEFAULT_CACHE_TTL_MS, e as DEFAULT_CAPABILITY_LEVEL, f as DEFAULT_DECISION_TIMEOUT_MS, g as DEFAULT_HANDSHAKE_TIMEOUT_MS, h as DEFAULT_MAX_PENDING_SESSION_DELTAS, i as DEFAULT_PROTOCOL_VERSION, j as DEFAULT_PUBLISH_TIMEOUT_MS, k as DEFAULT_TRACE_TIMEOUT_MS, l as DecisionOutcome, E as EmitLlmCallPostRequest, H as HandshakeOutcome, I as IdempotencyCache, m as InMemoryIdempotencyCache, n as InMemoryIdempotencyCacheOptions, N as NoopIdempotencyCache, P as PendingSessionDelta, o as PricingFreeze, p as PublishOutcomeRequest, Q as QueryBudgetRequest, q as QueryBudgetResult, r as ReleaseOutcome, s as ReleaseRequest, t as ReleaseSessionOutcome, u as ReleaseSessionRequest, v as ReserveRequest, w as ReserveSessionOutcome, x as ReserveSessionRequest, y as ResolvedConfig, z as ResumeAfterApprovalRequest, S as SessionCommitOutcome, F as SessionDeltaCommitClient, G as SessionDeltaCommitInput, J as SessionPendingDeltaLimitError, K as SessionReleaseClient, L as SessionReleaseInput, M as SessionReservationHandle, O as SessionReservationHandleError, T as SessionReservationHandleOptions, U as SessionReservationHandleSnapshot, V as SessionReservationReleasedError, W as SessionReservationReplayMismatchError, X as SpanRecord, Y as SpendGuardClient, Z as SpendGuardClientConfig, _ as SpendGuardClientOptions, $ as UnitRef, a0 as buildCommitSessionDeltaRequest, a1 as buildReleaseSessionRequest, a2 as buildReserveSessionRequest } from './cache-DOnw8QtJ.js';
|
|
3
|
+
export { ApprovalBundleHotReloadedError, ApprovalDeniedError, ApprovalLapsedError, ApprovalRequired, ApprovalRequiredInit, ApprovalResumeClient, DecisionDenied, DecisionDeniedInit, DecisionSkipped, DecisionStopped, HandshakeError, MutationApplyFailed, PricingMissingError, SidecarUnavailable, SpendGuardConfigError, SpendGuardConnectionError, SpendGuardDecisionError, SpendGuardError } from './errors.js';
|
|
4
|
+
export { deriveIdempotencyKey, deriveUuidFromSignature, newUuid7, workloadInstanceId } from './ids.js';
|
|
5
|
+
export { computePromptHash } from './promptHash.js';
|
|
6
|
+
export { PriceKey, PriceTable, PricingLookup, USD_MICROS_PER_USD } from './pricing.js';
|
|
7
|
+
export { RunPlan, currentRunPlan, withRunPlan } from './runPlan.js';
|
|
8
|
+
export { OtelAttributeValue, OtelAttributes, SPENDGUARD_OTEL_ATTR, setOtelSpanAttributes, withOtelSpan } from './otel.js';
|
|
9
|
+
export { RpcErrorClassification, RunWithRetryOptions, TRANSIENT_STATUS_CODES, classifyRpcError, runWithRetry } from './retry.js';
|
|
10
|
+
import '@grpc/grpc-js';
|
|
11
|
+
import '@opentelemetry/api';
|
|
12
|
+
import './adapter-D9T3yEEw.js';
|
|
13
|
+
import '@protobuf-ts/runtime-rpc';
|
|
14
|
+
import '@protobuf-ts/runtime';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Default UDS path used by `SpendGuardClient.fromEnv()` when neither
|
|
18
|
+
* `SPENDGUARD_SOCKET_PATH` nor `SPENDGUARD_SIDECAR_UDS` is set. Matches the
|
|
19
|
+
* sidecar Helm chart's default volume mount per slice spec.
|
|
20
|
+
*/
|
|
21
|
+
declare const DEFAULT_SOCKET_PATH: "/var/run/spendguard/adapter.sock";
|
|
22
|
+
/**
|
|
23
|
+
* Resolved env-var snapshot. Every field is optional because the env may be
|
|
24
|
+
* sparsely populated; the constructor combines this snapshot with the explicit
|
|
25
|
+
* options and validates the union.
|
|
26
|
+
*
|
|
27
|
+
* `runProjectionDefault` is typed as the same `RunProjectionPolicy` literal
|
|
28
|
+
* union surfaced by `SpendGuardClientConfig` so the env path produces the
|
|
29
|
+
* same shape the explicit-options path produces (no `string` vs literal-union
|
|
30
|
+
* drift between the two entry points).
|
|
31
|
+
*/
|
|
32
|
+
interface ResolvedEnvConfig {
|
|
33
|
+
socketPath?: string;
|
|
34
|
+
tenantId?: string;
|
|
35
|
+
workloadInstanceId?: string;
|
|
36
|
+
decisionTimeoutMs?: number;
|
|
37
|
+
handshakeTimeoutMs?: number;
|
|
38
|
+
runProjectionDefault?: RunProjectionPolicy;
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Deployment profile, read verbatim from `SPENDGUARD_PROFILE`. The
|
|
42
|
+
* constructor uses this to gate the env-var disable path: a bare
|
|
43
|
+
* `SPENDGUARD_DISABLE` ONLY takes effect when `SPENDGUARD_PROFILE=demo`,
|
|
44
|
+
* mirroring the Rust signing service's `DisabledSigner::for_profile`
|
|
45
|
+
* (`services/signing/src/lib.rs`) so a single mis-set env var cannot defeat
|
|
46
|
+
* enforcement in production. Lowercased for case-insensitive comparison;
|
|
47
|
+
* `undefined` when unset.
|
|
48
|
+
*/
|
|
49
|
+
profile?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The published `@spendguard/sdk` version. Mirror of the Python SDK
|
|
54
|
+
* `spendguard.__version__` (which lives at `0.5.1` on PyPI as of 2026-05-31).
|
|
55
|
+
*
|
|
56
|
+
* SLICE 10 (`COV_S05_10`) wires the publish pipeline. First npm release is
|
|
57
|
+
* `0.5.0`, chosen to track the Python SDK line (PyPI is at 0.6.0).
|
|
58
|
+
*/
|
|
59
|
+
declare const VERSION: "0.5.0";
|
|
60
|
+
|
|
61
|
+
export { DEFAULT_SOCKET_PATH, type ResolvedEnvConfig, RunProjectionPolicy, VERSION };
|