mcp-usage-control-cloudflare 1.0.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/dist/index.js ADDED
@@ -0,0 +1,1487 @@
1
+ import { UsageStateError, assertUsageIdentifier, validateUsageBudgetEnvelope, validateUsageRequestEnvelope, validateUsageVectorEnvelope, validateUsageVectorGrowthEnvelope, emitUsageEvent, } from 'mcp-usage-control';
2
+ const RESERVATION_ID_PATTERN = /^cf1\.[a-f0-9]{64}$/;
3
+ const HASH_PATTERN = /^[a-f0-9]{64}$/;
4
+ const DEFAULT_PATH = '/v1/usage-store';
5
+ const MAX_GATEWAY_BODY_BYTES = 65_536;
6
+ /** Worker-local UsageStore backed by a Durable Object namespace binding. */
7
+ export class CloudflareUsageStore {
8
+ namespace;
9
+ options;
10
+ constructor(namespace, options = {}) {
11
+ this.namespace = namespace;
12
+ this.options = normalizeOptions(options);
13
+ }
14
+ async reserve(input) {
15
+ const prepared = await prepareReserve(input, this.options);
16
+ const envelope = await this.stub().reserve(prepared.command);
17
+ this.emitRecovery(envelope.recovery);
18
+ if (!envelope.ok)
19
+ throw mapStoreError(envelope.error);
20
+ return mapReserveReply(envelope.result, prepared, input.request, input.units);
21
+ }
22
+ async reserveVector(input) {
23
+ const prepared = await prepareVectorReserve(input, this.options);
24
+ const stub = this.stub();
25
+ if (!stub.reserveVector) {
26
+ throw new UsageStateError('Cloudflare Durable Object does not support atomic vector usage');
27
+ }
28
+ const envelope = await stub.reserveVector(prepared.command);
29
+ this.emitRecovery(envelope.recovery);
30
+ if (!envelope.ok)
31
+ throw mapStoreError(envelope.error);
32
+ return mapVectorReserveReply(envelope.result, prepared, input.request);
33
+ }
34
+ async growVectorReservation(input) {
35
+ const prepared = await prepareVectorGrowth(input, this.options.idempotencyTtlMs);
36
+ const stub = this.stub();
37
+ if (!stub.growVector) {
38
+ throw new UsageStateError('Cloudflare Durable Object does not support atomic vector usage');
39
+ }
40
+ const envelope = await stub.growVector(prepared.command);
41
+ this.emitRecovery(envelope.recovery);
42
+ if (!envelope.ok)
43
+ throw mapStoreError(envelope.error);
44
+ return mapVectorGrowthReply(envelope.result, input, prepared);
45
+ }
46
+ async settleVector(input) {
47
+ const prepared = await prepareVectorSettlement(input, this.options.idempotencyTtlMs);
48
+ const stub = this.stub();
49
+ if (!stub.settleVector) {
50
+ throw new UsageStateError('Cloudflare Durable Object does not support atomic vector usage');
51
+ }
52
+ const envelope = await stub.settleVector(prepared.command);
53
+ this.emitRecovery(envelope.recovery);
54
+ if (!envelope.ok)
55
+ throw mapStoreError(envelope.error);
56
+ return mapVectorSettlementReply(envelope.result, input, prepared);
57
+ }
58
+ async growReservation(input) {
59
+ const prepared = await prepareGrowth(input, this.options.idempotencyTtlMs);
60
+ const stub = this.stub();
61
+ if (!stub.grow)
62
+ throw new UsageStateError('Cloudflare Durable Object does not support progressive growth');
63
+ const envelope = await stub.grow(prepared.command);
64
+ this.emitRecovery(envelope.recovery);
65
+ if (!envelope.ok)
66
+ throw mapStoreError(envelope.error);
67
+ return mapGrowthReply(envelope.result, input, prepared);
68
+ }
69
+ async markLiable(input) {
70
+ assertReservationId(input.reservationId);
71
+ const envelope = await this.stub().markLiable({
72
+ reservationId: input.reservationId,
73
+ idempotencyTtlMs: this.options.idempotencyTtlMs,
74
+ });
75
+ this.emitRecovery(envelope.recovery);
76
+ if (!envelope.ok)
77
+ throw mapStoreError(envelope.error);
78
+ return { reservationId: input.reservationId, expiresAt: envelope.result.expiresAt };
79
+ }
80
+ async renew(input) {
81
+ assertReservationId(input.reservationId);
82
+ assertPositiveInteger(input.ttlMs, 'ttlMs');
83
+ const envelope = await this.stub().renew({
84
+ reservationId: input.reservationId,
85
+ ttlMs: input.ttlMs,
86
+ idempotencyTtlMs: this.options.idempotencyTtlMs,
87
+ });
88
+ this.emitRecovery(envelope.recovery);
89
+ if (!envelope.ok)
90
+ throw mapStoreError(envelope.error);
91
+ return { reservationId: input.reservationId, expiresAt: envelope.result.expiresAt };
92
+ }
93
+ async settle(input) {
94
+ assertReservationId(input.reservationId);
95
+ assertNonNegativeInteger(input.actualUnits, 'actualUnits');
96
+ const outcomeHash = await digest(input.outcome);
97
+ const envelope = await this.stub().settle({
98
+ reservationId: input.reservationId,
99
+ actualUnits: input.actualUnits,
100
+ outcomeHash,
101
+ idempotencyTtlMs: this.options.idempotencyTtlMs,
102
+ });
103
+ this.emitRecovery(envelope.recovery);
104
+ if (!envelope.ok)
105
+ throw mapStoreError(envelope.error);
106
+ return {
107
+ reservationId: input.reservationId,
108
+ reservedUnits: envelope.result.reservedUnits,
109
+ actualUnits: envelope.result.actualUnits,
110
+ releasedUnits: envelope.result.releasedUnits,
111
+ outcome: input.outcome,
112
+ };
113
+ }
114
+ stub() {
115
+ return resolveStub(this.namespace, this.options.domainName);
116
+ }
117
+ emitRecovery(report) {
118
+ emitRecovery(this.options.observer, report);
119
+ }
120
+ }
121
+ export class CloudflareUsageTransportError extends Error {
122
+ code;
123
+ status;
124
+ constructor(code,
125
+ /** Bounded HTTP status metadata when a response was received. Response bodies are never exposed. */
126
+ status) {
127
+ super('Cloudflare usage store transport failed');
128
+ this.code = code;
129
+ this.status = status;
130
+ this.name = 'CloudflareUsageTransportError';
131
+ }
132
+ }
133
+ /** Node/edge remote UsageStore for a separately deployed Cloudflare gateway Worker. */
134
+ export class RemoteCloudflareUsageStore {
135
+ options;
136
+ endpoint;
137
+ fetchImpl;
138
+ timeoutMs;
139
+ observer;
140
+ constructor(options) {
141
+ this.options = options;
142
+ this.endpoint = new URL(options.endpoint);
143
+ const isLocalHttp = this.endpoint.protocol === 'http:' &&
144
+ (this.endpoint.hostname === '127.0.0.1' || this.endpoint.hostname === 'localhost');
145
+ if (this.endpoint.protocol !== 'https:' && !isLocalHttp) {
146
+ throw new RangeError('Cloudflare usage store endpoint must use HTTPS (local HTTP is test-only)');
147
+ }
148
+ if (this.endpoint.username || this.endpoint.password) {
149
+ throw new RangeError('Cloudflare usage store endpoint must not embed credentials');
150
+ }
151
+ this.fetchImpl = options.fetch ?? fetch;
152
+ this.timeoutMs = options.timeoutMs ?? 10_000;
153
+ assertPositiveInteger(this.timeoutMs, 'timeoutMs');
154
+ assertPortableTimerDelay(this.timeoutMs, 'timeoutMs');
155
+ this.observer = options.observer;
156
+ }
157
+ async reserve(input) {
158
+ const prepared = await prepareReserve(input, {
159
+ cleanupBatchSize: 1,
160
+ idempotencyTtlMs: 1,
161
+ });
162
+ const envelope = await this.post({
163
+ version: 1,
164
+ method: 'reserve',
165
+ input: {
166
+ reservationId: prepared.command.reservationId,
167
+ units: prepared.command.units,
168
+ budgets: prepared.command.budgets,
169
+ ttlMs: prepared.command.ttlMs,
170
+ initialGrowthCursor: prepared.command.initialGrowthCursor,
171
+ },
172
+ });
173
+ this.emitRecovery(envelope.recovery);
174
+ if (!envelope.ok)
175
+ throw mapStoreError(envelope.error);
176
+ return mapReserveReply(envelope.result, prepared, input.request, input.units);
177
+ }
178
+ async reserveVector(input) {
179
+ const prepared = await prepareVectorReserve(input, { cleanupBatchSize: 1, idempotencyTtlMs: 1 });
180
+ const envelope = await this.post({
181
+ version: 1,
182
+ method: 'reserve_vector',
183
+ input: {
184
+ reservationId: prepared.command.reservationId,
185
+ dimensions: prepared.command.dimensions,
186
+ ttlMs: prepared.command.ttlMs,
187
+ initialGrowthCursor: prepared.command.initialGrowthCursor,
188
+ },
189
+ });
190
+ this.emitRecovery(envelope.recovery);
191
+ if (!envelope.ok)
192
+ throw mapStoreError(envelope.error);
193
+ return mapVectorReserveReply(envelope.result, prepared, input.request);
194
+ }
195
+ async growVectorReservation(input) {
196
+ const prepared = await prepareVectorGrowth(input, 1);
197
+ const envelope = await this.post({
198
+ version: 1,
199
+ method: 'grow_vector',
200
+ input: {
201
+ reservationId: prepared.command.reservationId,
202
+ incrementHash: prepared.command.incrementHash,
203
+ expectedGrowthCursor: prepared.command.expectedGrowthCursor,
204
+ dimensions: prepared.command.dimensions,
205
+ fingerprint: prepared.command.fingerprint,
206
+ nextGrowthCursor: prepared.command.nextGrowthCursor,
207
+ },
208
+ });
209
+ this.emitRecovery(envelope.recovery);
210
+ if (!envelope.ok)
211
+ throw mapStoreError(envelope.error);
212
+ return mapVectorGrowthReply(envelope.result, input, prepared);
213
+ }
214
+ async settleVector(input) {
215
+ const prepared = await prepareVectorSettlement(input, 1);
216
+ const envelope = await this.post({
217
+ version: 1,
218
+ method: 'settle_vector',
219
+ input: {
220
+ reservationId: prepared.command.reservationId,
221
+ actualByDimension: prepared.command.actualByDimension,
222
+ outcomeHash: prepared.command.outcomeHash,
223
+ },
224
+ });
225
+ this.emitRecovery(envelope.recovery);
226
+ if (!envelope.ok)
227
+ throw mapStoreError(envelope.error);
228
+ return mapVectorSettlementReply(envelope.result, input, prepared);
229
+ }
230
+ async growReservation(input) {
231
+ const prepared = await prepareGrowth(input, 1);
232
+ const envelope = await this.post({
233
+ version: 1,
234
+ method: 'grow',
235
+ input: {
236
+ reservationId: prepared.command.reservationId,
237
+ incrementHash: prepared.command.incrementHash,
238
+ expectedGrowthCursor: prepared.command.expectedGrowthCursor,
239
+ additionalUnits: prepared.command.additionalUnits,
240
+ budgets: prepared.command.budgets,
241
+ fingerprint: prepared.command.fingerprint,
242
+ nextGrowthCursor: prepared.command.nextGrowthCursor,
243
+ },
244
+ });
245
+ this.emitRecovery(envelope.recovery);
246
+ if (!envelope.ok)
247
+ throw mapStoreError(envelope.error);
248
+ return mapGrowthReply(envelope.result, input, prepared);
249
+ }
250
+ async markLiable(input) {
251
+ assertReservationId(input.reservationId);
252
+ const envelope = await this.post({
253
+ version: 1,
254
+ method: 'mark_liable',
255
+ input: { reservationId: input.reservationId },
256
+ });
257
+ this.emitRecovery(envelope.recovery);
258
+ if (!envelope.ok)
259
+ throw mapStoreError(envelope.error);
260
+ return { reservationId: input.reservationId, expiresAt: envelope.result.expiresAt };
261
+ }
262
+ async renew(input) {
263
+ assertReservationId(input.reservationId);
264
+ assertPositiveInteger(input.ttlMs, 'ttlMs');
265
+ const envelope = await this.post({
266
+ version: 1,
267
+ method: 'renew',
268
+ input: { reservationId: input.reservationId, ttlMs: input.ttlMs },
269
+ });
270
+ this.emitRecovery(envelope.recovery);
271
+ if (!envelope.ok)
272
+ throw mapStoreError(envelope.error);
273
+ return { reservationId: input.reservationId, expiresAt: envelope.result.expiresAt };
274
+ }
275
+ async settle(input) {
276
+ assertReservationId(input.reservationId);
277
+ assertNonNegativeInteger(input.actualUnits, 'actualUnits');
278
+ const outcomeHash = await digest(input.outcome);
279
+ const envelope = await this.post({
280
+ version: 1,
281
+ method: 'settle',
282
+ input: {
283
+ reservationId: input.reservationId,
284
+ actualUnits: input.actualUnits,
285
+ outcomeHash,
286
+ },
287
+ });
288
+ this.emitRecovery(envelope.recovery);
289
+ if (!envelope.ok)
290
+ throw mapStoreError(envelope.error);
291
+ return {
292
+ reservationId: input.reservationId,
293
+ reservedUnits: envelope.result.reservedUnits,
294
+ actualUnits: envelope.result.actualUnits,
295
+ releasedUnits: envelope.result.releasedUnits,
296
+ outcome: input.outcome,
297
+ };
298
+ }
299
+ async post(body) {
300
+ const controller = new AbortController();
301
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
302
+ try {
303
+ let resolvedHeaders;
304
+ try {
305
+ resolvedHeaders = await waitForAbort(resolveHeaders(this.options.headers), controller.signal);
306
+ }
307
+ catch (error) {
308
+ if (controller.signal.aborted)
309
+ throw new CloudflareUsageTransportError('timeout');
310
+ throw error;
311
+ }
312
+ const headers = new Headers(resolvedHeaders);
313
+ headers.set('content-type', 'application/json');
314
+ headers.set('accept', 'application/json');
315
+ let response;
316
+ try {
317
+ response = await waitForAbort(this.fetchImpl(this.endpoint, {
318
+ method: 'POST',
319
+ headers,
320
+ body: JSON.stringify(body),
321
+ signal: controller.signal,
322
+ }), controller.signal);
323
+ }
324
+ catch (error) {
325
+ if (controller.signal.aborted)
326
+ throw new CloudflareUsageTransportError('timeout');
327
+ if (error instanceof CloudflareUsageTransportError)
328
+ throw error;
329
+ throw new CloudflareUsageTransportError('network');
330
+ }
331
+ if (response.status === 401 || response.status === 403) {
332
+ throw new CloudflareUsageTransportError('unauthorized', response.status);
333
+ }
334
+ if (!response.ok)
335
+ throw new CloudflareUsageTransportError('remote', response.status);
336
+ let payload;
337
+ try {
338
+ payload = await waitForAbort(response.json(), controller.signal);
339
+ }
340
+ catch (error) {
341
+ if (controller.signal.aborted)
342
+ throw new CloudflareUsageTransportError('timeout');
343
+ if (error instanceof CloudflareUsageTransportError)
344
+ throw error;
345
+ throw new CloudflareUsageTransportError('protocol', response.status);
346
+ }
347
+ if (!isEnvelopeForRequest(payload, body)) {
348
+ throw new CloudflareUsageTransportError('protocol', response.status);
349
+ }
350
+ return payload;
351
+ }
352
+ finally {
353
+ clearTimeout(timer);
354
+ }
355
+ }
356
+ emitRecovery(report) {
357
+ emitRecovery(this.observer, report);
358
+ }
359
+ }
360
+ /**
361
+ * Creates a public-HTTP-compatible Worker handler for non-Cloudflare callers.
362
+ * Authentication is mandatory and must be supplied by the application.
363
+ */
364
+ export function createCloudflareUsageStoreGateway(options) {
365
+ const normalized = normalizeOptions(options);
366
+ const path = options.path ?? DEFAULT_PATH;
367
+ if (!path.startsWith('/'))
368
+ throw new RangeError('gateway path must start with /');
369
+ return async function handle(request) {
370
+ const url = new URL(request.url);
371
+ if (url.pathname !== path)
372
+ return responseJson({ error: 'not_found' }, 404);
373
+ if (request.method !== 'POST')
374
+ return responseJson({ error: 'method_not_allowed' }, 405);
375
+ let authorized = false;
376
+ try {
377
+ authorized = (await options.authorize(request)) === true;
378
+ }
379
+ catch {
380
+ authorized = false;
381
+ }
382
+ if (!authorized)
383
+ return responseJson({ error: 'unauthorized' }, 401);
384
+ const contentLength = Number(request.headers.get('content-length') ?? '0');
385
+ if (Number.isFinite(contentLength) && contentLength > MAX_GATEWAY_BODY_BYTES) {
386
+ return responseJson({ error: 'payload_too_large' }, 413);
387
+ }
388
+ let text;
389
+ try {
390
+ text = await request.text();
391
+ }
392
+ catch {
393
+ return responseJson({ error: 'invalid_request' }, 400);
394
+ }
395
+ if (new TextEncoder().encode(text).byteLength > MAX_GATEWAY_BODY_BYTES) {
396
+ return responseJson({ error: 'payload_too_large' }, 413);
397
+ }
398
+ let body;
399
+ try {
400
+ body = JSON.parse(text);
401
+ }
402
+ catch {
403
+ return responseJson({ error: 'invalid_request' }, 400);
404
+ }
405
+ if (!isHttpRequest(body))
406
+ return responseJson({ error: 'invalid_request' }, 400);
407
+ const stub = resolveStub(options.namespace, normalized.domainName);
408
+ try {
409
+ const envelope = await invokeGateway(stub, body, normalized);
410
+ return responseJson(envelope, 200);
411
+ }
412
+ catch {
413
+ // Never serialize raw Durable Object/runtime exceptions to remote callers.
414
+ return responseJson({ error: 'store_unavailable' }, 503);
415
+ }
416
+ };
417
+ }
418
+ async function invokeGateway(stub, body, options) {
419
+ switch (body.method) {
420
+ case 'reserve':
421
+ return stub.reserve({
422
+ ...body.input,
423
+ cleanupBatchSize: options.cleanupBatchSize,
424
+ idempotencyTtlMs: options.idempotencyTtlMs,
425
+ });
426
+ case 'reserve_vector':
427
+ if (!stub.reserveVector)
428
+ return failGatewayVectorUnsupported();
429
+ return stub.reserveVector({
430
+ ...body.input,
431
+ cleanupBatchSize: options.cleanupBatchSize,
432
+ idempotencyTtlMs: options.idempotencyTtlMs,
433
+ });
434
+ case 'grow':
435
+ if (!stub.grow)
436
+ return failGatewayGrowthUnsupported();
437
+ return stub.grow({
438
+ ...body.input,
439
+ idempotencyTtlMs: options.idempotencyTtlMs,
440
+ });
441
+ case 'grow_vector':
442
+ if (!stub.growVector)
443
+ return failGatewayVectorUnsupported();
444
+ return stub.growVector({
445
+ ...body.input,
446
+ idempotencyTtlMs: options.idempotencyTtlMs,
447
+ });
448
+ case 'mark_liable':
449
+ return stub.markLiable({
450
+ reservationId: body.input.reservationId,
451
+ idempotencyTtlMs: options.idempotencyTtlMs,
452
+ });
453
+ case 'renew':
454
+ return stub.renew({
455
+ reservationId: body.input.reservationId,
456
+ ttlMs: body.input.ttlMs,
457
+ idempotencyTtlMs: options.idempotencyTtlMs,
458
+ });
459
+ case 'settle':
460
+ return stub.settle({
461
+ reservationId: body.input.reservationId,
462
+ actualUnits: body.input.actualUnits,
463
+ outcomeHash: body.input.outcomeHash,
464
+ idempotencyTtlMs: options.idempotencyTtlMs,
465
+ });
466
+ case 'settle_vector':
467
+ if (!stub.settleVector)
468
+ return failGatewayVectorUnsupported();
469
+ return stub.settleVector({
470
+ reservationId: body.input.reservationId,
471
+ actualByDimension: body.input.actualByDimension,
472
+ outcomeHash: body.input.outcomeHash,
473
+ idempotencyTtlMs: options.idempotencyTtlMs,
474
+ });
475
+ }
476
+ }
477
+ function failGatewayGrowthUnsupported() {
478
+ return {
479
+ ok: false,
480
+ error: 'growth_not_supported',
481
+ recovery: {
482
+ aggregate: {
483
+ pendingCount: 0,
484
+ pendingUnits: 0,
485
+ liableCount: 0,
486
+ liableUnits: 0,
487
+ vectorPendingCount: 0,
488
+ vectorLiableCount: 0,
489
+ },
490
+ },
491
+ };
492
+ }
493
+ function failGatewayVectorUnsupported() {
494
+ return {
495
+ ok: false,
496
+ error: 'growth_not_supported',
497
+ recovery: {
498
+ aggregate: {
499
+ pendingCount: 0,
500
+ pendingUnits: 0,
501
+ liableCount: 0,
502
+ liableUnits: 0,
503
+ vectorPendingCount: 0,
504
+ vectorLiableCount: 0,
505
+ },
506
+ },
507
+ };
508
+ }
509
+ async function prepareReserve(input, options) {
510
+ validateRequestIdentity(input.request);
511
+ assertNonNegativeInteger(input.units, 'units');
512
+ assertPositiveInteger(input.ttlMs, 'ttlMs');
513
+ const budgets = canonicalizeBudgets(input.budgets);
514
+ const operationHash = await digest(JSON.stringify([
515
+ input.request.principal.tenantId ?? null,
516
+ input.request.principal.id,
517
+ input.request.tool,
518
+ input.request.operationId,
519
+ ]));
520
+ const reservationId = `cf1.${operationHash}`;
521
+ const budgetEntries = await Promise.all(budgets.map(async (budget) => ({ budget, id: await digest(budget.key) })));
522
+ const budgetById = new Map(budgetEntries.map(entry => [entry.id, entry.budget]));
523
+ const initialGrowthCursor = newGrowthCursor();
524
+ return {
525
+ reservationId,
526
+ budgets,
527
+ budgetById,
528
+ command: {
529
+ reservationId,
530
+ units: input.units,
531
+ budgets: budgetEntries.map(entry => ({ id: entry.id, limit: entry.budget.limit })),
532
+ ttlMs: input.ttlMs,
533
+ cleanupBatchSize: options.cleanupBatchSize,
534
+ idempotencyTtlMs: options.idempotencyTtlMs,
535
+ initialGrowthCursor,
536
+ },
537
+ };
538
+ }
539
+ async function prepareVectorReserve(input, options) {
540
+ validateRequestIdentity(input.request);
541
+ assertPositiveInteger(input.ttlMs, 'ttlMs');
542
+ const dimensions = canonicalizeUsageDimensions(input.dimensions);
543
+ const operationHash = await digest(JSON.stringify([
544
+ input.request.principal.tenantId ?? null,
545
+ input.request.principal.id,
546
+ input.request.tool,
547
+ input.request.operationId,
548
+ ]));
549
+ const reservationId = `cf1.${operationHash}`;
550
+ const encoded = await Promise.all(dimensions.map(async (dimension) => ({
551
+ dimension,
552
+ id: await digest(dimension.key),
553
+ budgets: await Promise.all(dimension.budgets.map(async (budget) => ({ budget, id: await digest(budget.key) }))),
554
+ })));
555
+ const dimensionById = new Map(encoded.map(entry => [entry.id, entry.dimension]));
556
+ const budgetById = new Map(encoded.flatMap(entry => entry.budgets.map(budget => [budget.id, budget.budget])));
557
+ return {
558
+ reservationId,
559
+ dimensions,
560
+ dimensionById,
561
+ budgetById,
562
+ command: {
563
+ reservationId,
564
+ dimensions: encoded.map(entry => ({
565
+ id: entry.id,
566
+ units: entry.dimension.units,
567
+ budgets: entry.budgets.map(budget => ({ id: budget.id, limit: budget.budget.limit })),
568
+ })),
569
+ ttlMs: input.ttlMs,
570
+ cleanupBatchSize: options.cleanupBatchSize,
571
+ idempotencyTtlMs: options.idempotencyTtlMs,
572
+ initialGrowthCursor: newGrowthCursor(),
573
+ },
574
+ };
575
+ }
576
+ async function prepareVectorGrowth(input, idempotencyTtlMs) {
577
+ assertReservationId(input.reservationId);
578
+ if (!input.incrementId)
579
+ throw new RangeError('incrementId must be a non-empty string');
580
+ if (!input.expectedGrowthCursor) {
581
+ throw new RangeError('expectedGrowthCursor must be a non-empty string');
582
+ }
583
+ assertPositiveInteger(idempotencyTtlMs, 'idempotencyTtlMs');
584
+ const dimensions = canonicalizeGrowthDimensions(input.dimensions);
585
+ const encoded = await Promise.all(dimensions.map(async (dimension) => ({
586
+ dimension,
587
+ id: await digest(dimension.key),
588
+ budgets: await Promise.all(dimension.budgets.map(async (budget) => ({ budget, id: await digest(budget.key) }))),
589
+ })));
590
+ const dimensionById = new Map(encoded.map(entry => [entry.id, entry.dimension]));
591
+ const budgetById = new Map(encoded.flatMap(entry => entry.budgets.map(budget => [budget.id, budget.budget])));
592
+ const encodedDimensions = encoded.map(entry => ({
593
+ id: entry.id,
594
+ additionalUnits: entry.dimension.additionalUnits,
595
+ budgets: entry.budgets.map(budget => ({ id: budget.id, limit: budget.budget.limit })),
596
+ }));
597
+ return {
598
+ dimensionById,
599
+ budgetById,
600
+ command: {
601
+ reservationId: input.reservationId,
602
+ incrementHash: await digest(input.incrementId),
603
+ expectedGrowthCursor: input.expectedGrowthCursor,
604
+ dimensions: encodedDimensions,
605
+ fingerprint: await digest(JSON.stringify(encodedDimensions.map(dimension => [
606
+ dimension.id,
607
+ dimension.additionalUnits,
608
+ dimension.budgets.map(budget => [budget.id, budget.limit]),
609
+ ]))),
610
+ nextGrowthCursor: newGrowthCursor(),
611
+ idempotencyTtlMs,
612
+ },
613
+ };
614
+ }
615
+ async function prepareVectorSettlement(input, idempotencyTtlMs) {
616
+ assertReservationId(input.reservationId);
617
+ assertPositiveInteger(idempotencyTtlMs, 'idempotencyTtlMs');
618
+ const actuals = canonicalizeActualDimensions(input.actualByDimension);
619
+ const encoded = await Promise.all(actuals.map(async (actual) => ({ actual, id: await digest(actual.key) })));
620
+ return {
621
+ dimensionKeyById: new Map(encoded.map(entry => [entry.id, entry.actual.key])),
622
+ command: {
623
+ reservationId: input.reservationId,
624
+ actualByDimension: encoded.map(entry => ({ id: entry.id, actualUnits: entry.actual.actualUnits })),
625
+ outcomeHash: await digest(input.outcome),
626
+ idempotencyTtlMs,
627
+ },
628
+ };
629
+ }
630
+ function mapVectorReserveReply(reply, prepared, request) {
631
+ if (!reply.accepted) {
632
+ if (reply.reason === 'duplicate_operation')
633
+ return { accepted: false, reason: reply.reason };
634
+ if (!reply.limitingDimensionId || !reply.limitingBudgetId || reply.remaining === undefined) {
635
+ throw new UsageStateError('Cloudflare vector quota reply was incomplete');
636
+ }
637
+ const dimension = prepared.dimensionById.get(reply.limitingDimensionId);
638
+ const budget = prepared.budgetById.get(reply.limitingBudgetId);
639
+ if (!dimension || !budget) {
640
+ throw new UsageStateError('Cloudflare vector quota reply referenced unknown identifiers');
641
+ }
642
+ return {
643
+ accepted: false,
644
+ reason: 'quota_exceeded',
645
+ limitingDimensionKey: dimension.key,
646
+ limitingBudgetKey: budget.key,
647
+ remaining: reply.remaining,
648
+ };
649
+ }
650
+ const remainingByBudget = reply.remainingByBudget.map(balance => {
651
+ const dimension = prepared.dimensionById.get(balance.dimensionId);
652
+ const budget = prepared.budgetById.get(balance.budgetId);
653
+ if (!dimension || !budget) {
654
+ throw new UsageStateError('Cloudflare vector reserve reply referenced unknown identifiers');
655
+ }
656
+ return { dimensionKey: dimension.key, budgetKey: budget.key, remaining: balance.remaining };
657
+ });
658
+ if (remainingByBudget.length !== prepared.budgetById.size) {
659
+ throw new UsageStateError('Cloudflare vector reserve reply omitted a budget balance');
660
+ }
661
+ return {
662
+ accepted: true,
663
+ reservation: {
664
+ id: prepared.reservationId,
665
+ operationId: request.operationId,
666
+ principalId: request.principal.id,
667
+ ...(request.principal.tenantId === undefined ? {} : { tenantId: request.principal.tenantId }),
668
+ ...(request.principal.plan === undefined ? {} : { plan: request.principal.plan }),
669
+ tool: request.tool,
670
+ dimensions: prepared.dimensions.map(dimension => ({
671
+ key: dimension.key,
672
+ budgetKeys: dimension.budgets.map(budget => budget.key),
673
+ reservedUnits: dimension.units,
674
+ })),
675
+ expiresAt: reply.expiresAt,
676
+ growthCursor: prepared.command.initialGrowthCursor,
677
+ },
678
+ remainingByBudget,
679
+ };
680
+ }
681
+ function mapVectorGrowthReply(reply, input, prepared) {
682
+ if (!reply.accepted) {
683
+ const dimension = prepared.dimensionById.get(reply.limitingDimensionId);
684
+ const budget = prepared.budgetById.get(reply.limitingBudgetId);
685
+ if (!dimension || !budget) {
686
+ throw new UsageStateError('Cloudflare vector growth denial referenced unknown identifiers');
687
+ }
688
+ return {
689
+ accepted: false,
690
+ reason: 'quota_exceeded',
691
+ replayed: reply.replayed,
692
+ reservationId: input.reservationId,
693
+ incrementId: input.incrementId,
694
+ growthCursor: reply.growthCursor,
695
+ limitingDimensionKey: dimension.key,
696
+ limitingBudgetKey: budget.key,
697
+ remaining: reply.remaining,
698
+ };
699
+ }
700
+ const mapReserved = (items) => items.map(item => {
701
+ const dimension = prepared.dimensionById.get(item.id);
702
+ if (!dimension)
703
+ throw new UsageStateError('Cloudflare vector growth reply referenced unknown dimension');
704
+ return { key: dimension.key, reservedUnits: item.reservedUnits };
705
+ });
706
+ const remainingByBudget = reply.remainingByBudget.map(balance => {
707
+ const dimension = prepared.dimensionById.get(balance.dimensionId);
708
+ const budget = prepared.budgetById.get(balance.budgetId);
709
+ if (!dimension || !budget) {
710
+ throw new UsageStateError('Cloudflare vector growth reply referenced unknown identifiers');
711
+ }
712
+ return { dimensionKey: dimension.key, budgetKey: budget.key, remaining: balance.remaining };
713
+ });
714
+ if (remainingByBudget.length !== prepared.budgetById.size) {
715
+ throw new UsageStateError('Cloudflare vector growth reply omitted a budget balance');
716
+ }
717
+ return {
718
+ accepted: true,
719
+ replayed: reply.replayed,
720
+ reservationId: input.reservationId,
721
+ incrementId: input.incrementId,
722
+ previousReservedByDimension: mapReserved(reply.previousReservedByDimension),
723
+ reservedByDimension: mapReserved(reply.reservedByDimension),
724
+ growthCursor: reply.growthCursor,
725
+ remainingByBudget,
726
+ };
727
+ }
728
+ function mapVectorSettlementReply(reply, input, prepared) {
729
+ return {
730
+ reservationId: input.reservationId,
731
+ dimensions: reply.dimensions.map(dimension => {
732
+ const key = prepared.dimensionKeyById.get(dimension.id);
733
+ if (!key)
734
+ throw new UsageStateError('Cloudflare vector settlement referenced unknown dimension');
735
+ return {
736
+ key,
737
+ reservedUnits: dimension.reservedUnits,
738
+ actualUnits: dimension.actualUnits,
739
+ releasedUnits: dimension.releasedUnits,
740
+ };
741
+ }),
742
+ outcome: input.outcome,
743
+ };
744
+ }
745
+ async function prepareGrowth(input, idempotencyTtlMs) {
746
+ assertReservationId(input.reservationId);
747
+ assertUsageIdentifier(input.incrementId, 'incrementId');
748
+ if (typeof input.expectedGrowthCursor !== 'string' ||
749
+ input.expectedGrowthCursor.length === 0) {
750
+ throw new RangeError('expectedGrowthCursor must be a non-empty string');
751
+ }
752
+ assertPositiveInteger(input.additionalUnits, 'additionalUnits');
753
+ assertPositiveInteger(idempotencyTtlMs, 'idempotencyTtlMs');
754
+ const budgets = canonicalizeBudgets(input.budgets);
755
+ const budgetEntries = await Promise.all(budgets.map(async (budget) => ({ budget, id: await digest(budget.key) })));
756
+ const budgetById = new Map(budgetEntries.map(entry => [entry.id, entry.budget]));
757
+ const encodedBudgets = budgetEntries.map(entry => ({ id: entry.id, limit: entry.budget.limit }));
758
+ return {
759
+ budgetById,
760
+ command: {
761
+ reservationId: input.reservationId,
762
+ incrementHash: await digest(input.incrementId),
763
+ expectedGrowthCursor: input.expectedGrowthCursor,
764
+ additionalUnits: input.additionalUnits,
765
+ budgets: encodedBudgets,
766
+ fingerprint: await digest(JSON.stringify([
767
+ input.additionalUnits,
768
+ encodedBudgets.map(budget => [budget.id, budget.limit]),
769
+ ])),
770
+ nextGrowthCursor: newGrowthCursor(),
771
+ idempotencyTtlMs,
772
+ },
773
+ };
774
+ }
775
+ function mapGrowthReply(reply, input, prepared) {
776
+ if (!reply.accepted) {
777
+ const budget = prepared.budgetById.get(reply.limitingBudgetId);
778
+ if (!budget)
779
+ throw new UsageStateError('Cloudflare growth quota reply referenced an unknown budget');
780
+ return {
781
+ accepted: false,
782
+ reason: 'quota_exceeded',
783
+ replayed: reply.replayed,
784
+ reservationId: input.reservationId,
785
+ incrementId: input.incrementId,
786
+ growthCursor: reply.growthCursor,
787
+ limitingBudgetKey: budget.key,
788
+ remaining: reply.remaining,
789
+ };
790
+ }
791
+ const remainingByBudget = reply.remainingByBudget.map(balance => {
792
+ const budget = prepared.budgetById.get(balance.id);
793
+ if (!budget)
794
+ throw new UsageStateError('Cloudflare growth reply referenced an unknown budget');
795
+ return { key: budget.key, remaining: balance.remaining };
796
+ });
797
+ if (remainingByBudget.length !== prepared.budgetById.size) {
798
+ throw new UsageStateError('Cloudflare growth reply omitted a budget balance');
799
+ }
800
+ return {
801
+ accepted: true,
802
+ replayed: reply.replayed,
803
+ reservationId: input.reservationId,
804
+ incrementId: input.incrementId,
805
+ previousReservedUnits: reply.previousReservedUnits,
806
+ reservedUnits: reply.reservedUnits,
807
+ growthCursor: reply.growthCursor,
808
+ remainingByBudget,
809
+ };
810
+ }
811
+ function mapReserveReply(reply, prepared, request, units) {
812
+ if (!reply.accepted) {
813
+ if (reply.reason === 'duplicate_operation')
814
+ return { accepted: false, reason: reply.reason };
815
+ if (!reply.limitingBudgetId || reply.remaining === undefined) {
816
+ throw new UsageStateError('Cloudflare quota reply was incomplete');
817
+ }
818
+ const budget = prepared.budgetById.get(reply.limitingBudgetId);
819
+ if (!budget)
820
+ throw new UsageStateError('Cloudflare quota reply referenced an unknown budget');
821
+ return {
822
+ accepted: false,
823
+ reason: 'quota_exceeded',
824
+ limitingBudgetKey: budget.key,
825
+ remaining: reply.remaining,
826
+ };
827
+ }
828
+ const remainingByBudget = reply.remainingByBudget.map(balance => {
829
+ const budget = prepared.budgetById.get(balance.id);
830
+ if (!budget)
831
+ throw new UsageStateError('Cloudflare reserve reply referenced an unknown budget');
832
+ return { key: budget.key, remaining: balance.remaining };
833
+ });
834
+ if (remainingByBudget.length !== prepared.budgets.length) {
835
+ throw new UsageStateError('Cloudflare reserve reply omitted a budget balance');
836
+ }
837
+ return {
838
+ accepted: true,
839
+ reservation: {
840
+ id: prepared.reservationId,
841
+ operationId: request.operationId,
842
+ principalId: request.principal.id,
843
+ ...(request.principal.tenantId === undefined ? {} : { tenantId: request.principal.tenantId }),
844
+ ...(request.principal.plan === undefined ? {} : { plan: request.principal.plan }),
845
+ tool: request.tool,
846
+ budgetKeys: prepared.budgets.map(budget => budget.key),
847
+ reservedUnits: units,
848
+ expiresAt: reply.expiresAt,
849
+ growthCursor: prepared.command.initialGrowthCursor,
850
+ },
851
+ remainingByBudget,
852
+ };
853
+ }
854
+ function emitRecovery(observer, report) {
855
+ if (report.direct) {
856
+ if ('vector' in report.direct) {
857
+ emitUsageEvent(observer, {
858
+ type: 'vector.reservation.recovered',
859
+ timestamp: Date.now(),
860
+ store: 'cloudflare',
861
+ recovery: report.direct.state === 'pending' ? 'pending_released' : 'liable_retained',
862
+ reservationId: report.direct.reservationId,
863
+ dimensionCount: report.direct.dimensionCount,
864
+ budgetCount: report.direct.budgetCount,
865
+ count: 1,
866
+ });
867
+ }
868
+ else {
869
+ emitUsageEvent(observer, {
870
+ type: 'reservation.recovered',
871
+ timestamp: Date.now(),
872
+ store: 'cloudflare',
873
+ recovery: report.direct.state === 'pending' ? 'pending_released' : 'liable_retained',
874
+ reservationId: report.direct.reservationId,
875
+ reservedUnits: report.direct.reservedUnits,
876
+ count: 1,
877
+ });
878
+ }
879
+ }
880
+ if (report.aggregate.pendingCount > 0) {
881
+ emitUsageEvent(observer, {
882
+ type: 'reservation.recovered',
883
+ timestamp: Date.now(),
884
+ store: 'cloudflare',
885
+ recovery: 'pending_released',
886
+ reservedUnits: report.aggregate.pendingUnits,
887
+ count: report.aggregate.pendingCount,
888
+ });
889
+ }
890
+ if (report.aggregate.liableCount > 0) {
891
+ emitUsageEvent(observer, {
892
+ type: 'reservation.recovered',
893
+ timestamp: Date.now(),
894
+ store: 'cloudflare',
895
+ recovery: 'liable_retained',
896
+ reservedUnits: report.aggregate.liableUnits,
897
+ count: report.aggregate.liableCount,
898
+ });
899
+ }
900
+ if ((report.aggregate.vectorPendingCount ?? 0) > 0) {
901
+ emitUsageEvent(observer, {
902
+ type: 'vector.reservation.recovered',
903
+ timestamp: Date.now(),
904
+ store: 'cloudflare',
905
+ recovery: 'pending_released',
906
+ count: report.aggregate.vectorPendingCount ?? 0,
907
+ });
908
+ }
909
+ if ((report.aggregate.vectorLiableCount ?? 0) > 0) {
910
+ emitUsageEvent(observer, {
911
+ type: 'vector.reservation.recovered',
912
+ timestamp: Date.now(),
913
+ store: 'cloudflare',
914
+ recovery: 'liable_retained',
915
+ count: report.aggregate.vectorLiableCount ?? 0,
916
+ });
917
+ }
918
+ }
919
+ function mapStoreError(code) {
920
+ switch (code) {
921
+ case 'settlement_conflict':
922
+ return new UsageStateError('Reservation was already settled with a different result');
923
+ case 'actual_units_exceed_reserved':
924
+ return new UsageStateError('actualUnits cannot exceed reservedUnits');
925
+ case 'growth_conflict':
926
+ return new UsageStateError('Growth increment was already attempted with different parameters');
927
+ case 'growth_stale_cursor':
928
+ return new UsageStateError('Growth cursor is stale or conflicts with reservation state');
929
+ case 'growth_budget_mismatch':
930
+ return new UsageStateError('Growth budgets must exactly match the reservation budget set');
931
+ case 'growth_not_supported':
932
+ return new UsageStateError('Reservation does not support requested growth/capability');
933
+ case 'vector_dimension_mismatch':
934
+ return new UsageStateError('Vector dimensions and budgets must exactly match the reservation');
935
+ case 'usage_mode_mismatch':
936
+ return new UsageStateError('Usage reservation mode does not match the requested operation');
937
+ case 'not_found_or_expired':
938
+ return new UsageStateError('Reservation not found or expired');
939
+ }
940
+ }
941
+ function resolveStub(namespace, domainName) {
942
+ if (namespace.getByName)
943
+ return namespace.getByName(domainName);
944
+ if (namespace.idFromName && namespace.get)
945
+ return namespace.get(namespace.idFromName(domainName));
946
+ throw new TypeError('Durable Object namespace must provide getByName() or idFromName()+get()');
947
+ }
948
+ function normalizeOptions(options) {
949
+ const domainName = options.domainName ?? 'default';
950
+ const cleanupBatchSize = options.cleanupBatchSize ?? 256;
951
+ const idempotencyTtlMs = options.idempotencyTtlMs ?? 86_400_000;
952
+ if (domainName.length === 0 || domainName.length > 128) {
953
+ throw new RangeError('domainName must be between 1 and 128 characters');
954
+ }
955
+ assertPositiveInteger(cleanupBatchSize, 'cleanupBatchSize');
956
+ assertPositiveInteger(idempotencyTtlMs, 'idempotencyTtlMs');
957
+ return {
958
+ domainName,
959
+ cleanupBatchSize,
960
+ idempotencyTtlMs,
961
+ ...(options.observer === undefined ? {} : { observer: options.observer }),
962
+ };
963
+ }
964
+ function canonicalizeUsageDimensions(dimensions) {
965
+ if (dimensions.length === 0)
966
+ throw new RangeError('dimensions must contain at least one dimension');
967
+ const normalized = dimensions.map(dimension => {
968
+ if (typeof dimension.key !== 'string' || dimension.key.length === 0)
969
+ throw new RangeError('dimension.key must be a non-empty string');
970
+ assertNonNegativeInteger(dimension.units, `dimension.units (${dimension.key})`);
971
+ return { key: dimension.key, units: dimension.units, budgets: canonicalizeBudgets(dimension.budgets) };
972
+ });
973
+ normalized.sort((a, b) => a.key.localeCompare(b.key));
974
+ validateVectorTopology(normalized);
975
+ return normalized;
976
+ }
977
+ function canonicalizeGrowthDimensions(dimensions) {
978
+ if (dimensions.length === 0)
979
+ throw new RangeError('dimensions must contain at least one dimension');
980
+ const normalized = dimensions.map(dimension => {
981
+ if (typeof dimension.key !== 'string' || dimension.key.length === 0)
982
+ throw new RangeError('dimension.key must be a non-empty string');
983
+ assertNonNegativeInteger(dimension.additionalUnits, `dimension.additionalUnits (${dimension.key})`);
984
+ return {
985
+ key: dimension.key,
986
+ additionalUnits: dimension.additionalUnits,
987
+ budgets: canonicalizeBudgets(dimension.budgets),
988
+ };
989
+ });
990
+ normalized.sort((a, b) => a.key.localeCompare(b.key));
991
+ validateVectorTopology(normalized);
992
+ if (!normalized.some(dimension => dimension.additionalUnits > 0)) {
993
+ throw new RangeError('vector growth must add units to at least one dimension');
994
+ }
995
+ return normalized;
996
+ }
997
+ function canonicalizeActualDimensions(actuals) {
998
+ if (actuals.length === 0)
999
+ throw new RangeError('actualByDimension must contain at least one dimension');
1000
+ const normalized = actuals.map(actual => {
1001
+ if (typeof actual.key !== 'string' || actual.key.length === 0)
1002
+ throw new RangeError('actual dimension key must be a non-empty string');
1003
+ assertNonNegativeInteger(actual.actualUnits, `actualUnits (${actual.key})`);
1004
+ return { key: actual.key, actualUnits: actual.actualUnits };
1005
+ });
1006
+ normalized.sort((a, b) => a.key.localeCompare(b.key));
1007
+ for (let index = 1; index < normalized.length; index += 1) {
1008
+ if (normalized[index - 1].key === normalized[index].key) {
1009
+ throw new RangeError(`duplicate dimension key: ${normalized[index].key}`);
1010
+ }
1011
+ }
1012
+ return normalized;
1013
+ }
1014
+ function validateVectorTopology(dimensions) {
1015
+ const dimensionKeys = new Set();
1016
+ const budgetKeys = new Set();
1017
+ for (const dimension of dimensions) {
1018
+ if (dimensionKeys.has(dimension.key))
1019
+ throw new RangeError(`duplicate dimension key: ${dimension.key}`);
1020
+ dimensionKeys.add(dimension.key);
1021
+ for (const budget of dimension.budgets) {
1022
+ if (budgetKeys.has(budget.key)) {
1023
+ throw new RangeError(`budget key cannot appear in multiple vector dimensions: ${budget.key}`);
1024
+ }
1025
+ budgetKeys.add(budget.key);
1026
+ }
1027
+ }
1028
+ }
1029
+ function canonicalizeBudgets(budgets) {
1030
+ if (budgets.length === 0)
1031
+ throw new RangeError('budgets must contain at least one budget');
1032
+ const normalized = budgets.map(budget => {
1033
+ if (typeof budget.key !== 'string' || budget.key.length === 0) {
1034
+ throw new RangeError('budget.key must be a non-empty string');
1035
+ }
1036
+ assertNonNegativeInteger(budget.limit, `budget.limit (${budget.key})`);
1037
+ return { key: budget.key, limit: budget.limit };
1038
+ });
1039
+ normalized.sort((a, b) => a.key.localeCompare(b.key));
1040
+ for (let index = 1; index < normalized.length; index += 1) {
1041
+ if (normalized[index - 1].key === normalized[index].key) {
1042
+ throw new RangeError(`duplicate budget key: ${normalized[index].key}`);
1043
+ }
1044
+ }
1045
+ return normalized;
1046
+ }
1047
+ function validateRequestIdentity(request) {
1048
+ validateUsageRequestEnvelope(request);
1049
+ }
1050
+ function assertReservationId(value) {
1051
+ if (!RESERVATION_ID_PATTERN.test(value))
1052
+ throw new UsageStateError('Invalid Cloudflare reservation ID');
1053
+ }
1054
+ function assertNonNegativeInteger(value, name) {
1055
+ if (!Number.isSafeInteger(value) || value < 0) {
1056
+ throw new RangeError(`${name} must be a non-negative safe integer`);
1057
+ }
1058
+ }
1059
+ function assertPortableTimerDelay(value, name) {
1060
+ if (value > 2_147_483_647) {
1061
+ throw new RangeError(`${name} must not exceed 2147483647ms`);
1062
+ }
1063
+ }
1064
+ function assertPositiveInteger(value, name) {
1065
+ if (!Number.isSafeInteger(value) || value <= 0) {
1066
+ throw new RangeError(`${name} must be a positive safe integer`);
1067
+ }
1068
+ }
1069
+ function newGrowthCursor() {
1070
+ return `g1.${crypto.randomUUID()}`;
1071
+ }
1072
+ async function digest(value) {
1073
+ const bytes = new TextEncoder().encode(value);
1074
+ const hash = await crypto.subtle.digest('SHA-256', bytes);
1075
+ return Array.from(new Uint8Array(hash), byte => byte.toString(16).padStart(2, '0')).join('');
1076
+ }
1077
+ async function resolveHeaders(headers) {
1078
+ if (typeof headers === 'function')
1079
+ return headers();
1080
+ return headers;
1081
+ }
1082
+ function waitForAbort(promise, signal) {
1083
+ if (signal.aborted)
1084
+ return Promise.reject(new Error('operation aborted'));
1085
+ return new Promise((resolve, reject) => {
1086
+ const onAbort = () => {
1087
+ cleanup();
1088
+ reject(new Error('operation aborted'));
1089
+ };
1090
+ const cleanup = () => signal.removeEventListener('abort', onAbort);
1091
+ signal.addEventListener('abort', onAbort, { once: true });
1092
+ promise.then(value => {
1093
+ cleanup();
1094
+ resolve(value);
1095
+ }, error => {
1096
+ cleanup();
1097
+ reject(error);
1098
+ });
1099
+ });
1100
+ }
1101
+ function isHttpRequest(value) {
1102
+ if (!isRecord(value) || value.version !== 1 || typeof value.method !== 'string' || !isRecord(value.input)) {
1103
+ return false;
1104
+ }
1105
+ const input = value.input;
1106
+ switch (value.method) {
1107
+ case 'reserve':
1108
+ return (isReservationId(input.reservationId) &&
1109
+ isNonNegativeInteger(input.units) &&
1110
+ isPositiveInteger(input.ttlMs) &&
1111
+ typeof input.initialGrowthCursor === 'string' &&
1112
+ input.initialGrowthCursor.length > 0 &&
1113
+ Array.isArray(input.budgets) &&
1114
+ input.budgets.length > 0 &&
1115
+ input.budgets.every(budget => isRecord(budget) &&
1116
+ typeof budget.id === 'string' &&
1117
+ HASH_PATTERN.test(budget.id) &&
1118
+ isNonNegativeInteger(budget.limit)));
1119
+ case 'reserve_vector':
1120
+ return (isReservationId(input.reservationId) &&
1121
+ isPositiveInteger(input.ttlMs) &&
1122
+ typeof input.initialGrowthCursor === 'string' &&
1123
+ input.initialGrowthCursor.length > 0 &&
1124
+ isHashedDimensions(input.dimensions, 'reserve'));
1125
+ case 'grow':
1126
+ return (isReservationId(input.reservationId) &&
1127
+ typeof input.incrementHash === 'string' &&
1128
+ HASH_PATTERN.test(input.incrementHash) &&
1129
+ typeof input.expectedGrowthCursor === 'string' &&
1130
+ input.expectedGrowthCursor.length > 0 &&
1131
+ isPositiveInteger(input.additionalUnits) &&
1132
+ Array.isArray(input.budgets) &&
1133
+ input.budgets.length > 0 &&
1134
+ input.budgets.every(budget => isRecord(budget) &&
1135
+ typeof budget.id === 'string' &&
1136
+ HASH_PATTERN.test(budget.id) &&
1137
+ isNonNegativeInteger(budget.limit)) &&
1138
+ typeof input.fingerprint === 'string' &&
1139
+ HASH_PATTERN.test(input.fingerprint) &&
1140
+ typeof input.nextGrowthCursor === 'string' &&
1141
+ input.nextGrowthCursor.length > 0);
1142
+ case 'grow_vector':
1143
+ return (isReservationId(input.reservationId) &&
1144
+ typeof input.incrementHash === 'string' &&
1145
+ HASH_PATTERN.test(input.incrementHash) &&
1146
+ typeof input.expectedGrowthCursor === 'string' &&
1147
+ input.expectedGrowthCursor.length > 0 &&
1148
+ isHashedDimensions(input.dimensions, 'growth') &&
1149
+ typeof input.fingerprint === 'string' &&
1150
+ HASH_PATTERN.test(input.fingerprint) &&
1151
+ typeof input.nextGrowthCursor === 'string' &&
1152
+ input.nextGrowthCursor.length > 0);
1153
+ case 'mark_liable':
1154
+ return isReservationId(input.reservationId);
1155
+ case 'renew':
1156
+ return isReservationId(input.reservationId) && isPositiveInteger(input.ttlMs);
1157
+ case 'settle':
1158
+ return (isReservationId(input.reservationId) &&
1159
+ isNonNegativeInteger(input.actualUnits) &&
1160
+ typeof input.outcomeHash === 'string' &&
1161
+ HASH_PATTERN.test(input.outcomeHash));
1162
+ case 'settle_vector':
1163
+ return (isReservationId(input.reservationId) &&
1164
+ Array.isArray(input.actualByDimension) &&
1165
+ input.actualByDimension.length > 0 &&
1166
+ input.actualByDimension.every(dimension => isRecord(dimension) &&
1167
+ typeof dimension.id === 'string' &&
1168
+ HASH_PATTERN.test(dimension.id) &&
1169
+ isNonNegativeInteger(dimension.actualUnits)) &&
1170
+ typeof input.outcomeHash === 'string' &&
1171
+ HASH_PATTERN.test(input.outcomeHash));
1172
+ default:
1173
+ return false;
1174
+ }
1175
+ }
1176
+ function isHashedDimensions(value, mode) {
1177
+ if (!Array.isArray(value) || value.length === 0)
1178
+ return false;
1179
+ return value.every(dimension => {
1180
+ if (!isRecord(dimension) || typeof dimension.id !== 'string' || !HASH_PATTERN.test(dimension.id)) {
1181
+ return false;
1182
+ }
1183
+ const units = mode === 'reserve' ? dimension.units : dimension.additionalUnits;
1184
+ if (!isNonNegativeInteger(units))
1185
+ return false;
1186
+ return (Array.isArray(dimension.budgets) &&
1187
+ dimension.budgets.length > 0 &&
1188
+ dimension.budgets.every(budget => isRecord(budget) &&
1189
+ typeof budget.id === 'string' &&
1190
+ HASH_PATTERN.test(budget.id) &&
1191
+ isNonNegativeInteger(budget.limit)));
1192
+ });
1193
+ }
1194
+ function isEnvelopeForRequest(value, request) {
1195
+ if (!isRecord(value) || typeof value.ok !== 'boolean' || !isRecoveryReport(value.recovery)) {
1196
+ return false;
1197
+ }
1198
+ if (!value.ok)
1199
+ return isStoreErrorCode(value.error);
1200
+ return isSuccessfulReplyForRequest(value.result, request);
1201
+ }
1202
+ function isStoreErrorCode(value) {
1203
+ return (typeof value === 'string' &&
1204
+ [
1205
+ 'not_found_or_expired',
1206
+ 'settlement_conflict',
1207
+ 'actual_units_exceed_reserved',
1208
+ 'growth_conflict',
1209
+ 'growth_stale_cursor',
1210
+ 'growth_budget_mismatch',
1211
+ 'growth_not_supported',
1212
+ 'vector_dimension_mismatch',
1213
+ 'usage_mode_mismatch',
1214
+ ].includes(value));
1215
+ }
1216
+ function isSuccessfulReplyForRequest(value, request) {
1217
+ switch (request.method) {
1218
+ case 'reserve':
1219
+ return isReserveReplyForRequest(value, request.input.budgets.map(budget => budget.id));
1220
+ case 'reserve_vector':
1221
+ return isVectorReserveReplyForRequest(value, request.input.dimensions);
1222
+ case 'grow':
1223
+ return isGrowReplyForRequest(value, request.input);
1224
+ case 'grow_vector':
1225
+ return isVectorGrowReplyForRequest(value, request.input.dimensions);
1226
+ case 'mark_liable':
1227
+ case 'renew':
1228
+ return isRecord(value) && isPositiveInteger(value.expiresAt);
1229
+ case 'settle':
1230
+ return isSettlementReplyForRequest(value, request.input.actualUnits);
1231
+ case 'settle_vector':
1232
+ return isVectorSettlementReplyForRequest(value, request.input.actualByDimension);
1233
+ }
1234
+ }
1235
+ function isReserveReplyForRequest(value, expectedBudgetIds) {
1236
+ if (!isRecord(value) || typeof value.accepted !== 'boolean')
1237
+ return false;
1238
+ if (value.accepted) {
1239
+ return (isPositiveInteger(value.expiresAt) &&
1240
+ isExactBudgetBalances(value.remainingByBudget, expectedBudgetIds));
1241
+ }
1242
+ if (value.reason === 'duplicate_operation') {
1243
+ return ((value.limitingBudgetId === undefined || isExpectedHash(value.limitingBudgetId, expectedBudgetIds)) &&
1244
+ (value.remaining === undefined || isNonNegativeInteger(value.remaining)));
1245
+ }
1246
+ return (value.reason === 'quota_exceeded' &&
1247
+ isExpectedHash(value.limitingBudgetId, expectedBudgetIds) &&
1248
+ isNonNegativeInteger(value.remaining));
1249
+ }
1250
+ function isVectorReserveReplyForRequest(value, dimensions) {
1251
+ if (!isRecord(value) || typeof value.accepted !== 'boolean')
1252
+ return false;
1253
+ if (value.accepted) {
1254
+ return (isPositiveInteger(value.expiresAt) &&
1255
+ isExactVectorBalances(value.remainingByBudget, dimensions));
1256
+ }
1257
+ if (value.reason === 'duplicate_operation') {
1258
+ return ((value.limitingDimensionId === undefined || isExpectedDimensionId(value.limitingDimensionId, dimensions)) &&
1259
+ (value.limitingBudgetId === undefined || isExpectedVectorBudgetId(value.limitingBudgetId, dimensions)) &&
1260
+ (value.remaining === undefined || isNonNegativeInteger(value.remaining)));
1261
+ }
1262
+ return (value.reason === 'quota_exceeded' &&
1263
+ isExpectedVectorPair(value.limitingDimensionId, value.limitingBudgetId, dimensions) &&
1264
+ isNonNegativeInteger(value.remaining));
1265
+ }
1266
+ function isGrowReplyForRequest(value, input) {
1267
+ if (!isRecord(value) ||
1268
+ typeof value.accepted !== 'boolean' ||
1269
+ typeof value.replayed !== 'boolean' ||
1270
+ !isNonEmptyString(value.growthCursor)) {
1271
+ return false;
1272
+ }
1273
+ const expectedBudgetIds = input.budgets.map(budget => budget.id);
1274
+ if (!value.accepted) {
1275
+ return (value.reason === 'quota_exceeded' &&
1276
+ isExpectedHash(value.limitingBudgetId, expectedBudgetIds) &&
1277
+ isNonNegativeInteger(value.remaining));
1278
+ }
1279
+ if (!isNonNegativeInteger(value.previousReservedUnits) ||
1280
+ !isNonNegativeInteger(value.reservedUnits) ||
1281
+ value.previousReservedUnits > Number.MAX_SAFE_INTEGER - input.additionalUnits ||
1282
+ value.reservedUnits !== value.previousReservedUnits + input.additionalUnits) {
1283
+ return false;
1284
+ }
1285
+ return isExactBudgetBalances(value.remainingByBudget, expectedBudgetIds);
1286
+ }
1287
+ function isVectorGrowReplyForRequest(value, dimensions) {
1288
+ if (!isRecord(value) ||
1289
+ typeof value.accepted !== 'boolean' ||
1290
+ typeof value.replayed !== 'boolean' ||
1291
+ !isNonEmptyString(value.growthCursor)) {
1292
+ return false;
1293
+ }
1294
+ if (!value.accepted) {
1295
+ return (value.reason === 'quota_exceeded' &&
1296
+ isExpectedVectorGrowthPair(value.limitingDimensionId, value.limitingBudgetId, dimensions) &&
1297
+ isNonNegativeInteger(value.remaining));
1298
+ }
1299
+ const expectedIds = dimensions.map(dimension => dimension.id);
1300
+ const previous = reservedDimensionMap(value.previousReservedByDimension, expectedIds);
1301
+ const current = reservedDimensionMap(value.reservedByDimension, expectedIds);
1302
+ if (!previous || !current)
1303
+ return false;
1304
+ for (const dimension of dimensions) {
1305
+ const before = previous.get(dimension.id);
1306
+ const after = current.get(dimension.id);
1307
+ if (before === undefined ||
1308
+ after === undefined ||
1309
+ before > Number.MAX_SAFE_INTEGER - dimension.additionalUnits ||
1310
+ after !== before + dimension.additionalUnits) {
1311
+ return false;
1312
+ }
1313
+ }
1314
+ return isExactVectorGrowthBalances(value.remainingByBudget, dimensions);
1315
+ }
1316
+ function isSettlementReplyForRequest(value, expectedActualUnits) {
1317
+ if (!isRecord(value) ||
1318
+ !isNonNegativeInteger(value.reservedUnits) ||
1319
+ !isNonNegativeInteger(value.actualUnits) ||
1320
+ !isNonNegativeInteger(value.releasedUnits) ||
1321
+ typeof value.replayed !== 'boolean') {
1322
+ return false;
1323
+ }
1324
+ return (value.actualUnits === expectedActualUnits &&
1325
+ value.reservedUnits >= value.actualUnits &&
1326
+ value.releasedUnits === value.reservedUnits - value.actualUnits);
1327
+ }
1328
+ function isVectorSettlementReplyForRequest(value, expectedActuals) {
1329
+ if (!isRecord(value) || typeof value.replayed !== 'boolean' || !Array.isArray(value.dimensions)) {
1330
+ return false;
1331
+ }
1332
+ if (value.dimensions.length !== expectedActuals.length)
1333
+ return false;
1334
+ const expected = new Map(expectedActuals.map(item => [item.id, item.actualUnits]));
1335
+ const seen = new Set();
1336
+ for (const raw of value.dimensions) {
1337
+ if (!isRecord(raw) ||
1338
+ !isExpectedHash(raw.id, expectedActuals.map(item => item.id)) ||
1339
+ seen.has(raw.id) ||
1340
+ !isNonNegativeInteger(raw.reservedUnits) ||
1341
+ !isNonNegativeInteger(raw.actualUnits) ||
1342
+ !isNonNegativeInteger(raw.releasedUnits)) {
1343
+ return false;
1344
+ }
1345
+ const id = raw.id;
1346
+ seen.add(id);
1347
+ const actual = expected.get(id);
1348
+ if (actual === undefined ||
1349
+ raw.actualUnits !== actual ||
1350
+ raw.reservedUnits < raw.actualUnits ||
1351
+ raw.releasedUnits !== raw.reservedUnits - raw.actualUnits) {
1352
+ return false;
1353
+ }
1354
+ }
1355
+ return seen.size === expected.size;
1356
+ }
1357
+ function isExactBudgetBalances(value, expectedIds) {
1358
+ if (!Array.isArray(value) || value.length !== expectedIds.length)
1359
+ return false;
1360
+ const expected = new Set(expectedIds);
1361
+ const seen = new Set();
1362
+ for (const raw of value) {
1363
+ if (!isRecord(raw) ||
1364
+ typeof raw.id !== 'string' ||
1365
+ !expected.has(raw.id) ||
1366
+ seen.has(raw.id) ||
1367
+ !isNonNegativeInteger(raw.remaining)) {
1368
+ return false;
1369
+ }
1370
+ seen.add(raw.id);
1371
+ }
1372
+ return seen.size === expected.size;
1373
+ }
1374
+ function isExactVectorBalances(value, dimensions) {
1375
+ return isExactVectorBalancePairs(value, dimensions.flatMap(dimension => dimension.budgets.map(budget => [dimension.id, budget.id])));
1376
+ }
1377
+ function isExactVectorGrowthBalances(value, dimensions) {
1378
+ return isExactVectorBalancePairs(value, dimensions.flatMap(dimension => dimension.budgets.map(budget => [dimension.id, budget.id])));
1379
+ }
1380
+ function isExactVectorBalancePairs(value, expectedPairs) {
1381
+ if (!Array.isArray(value) || value.length !== expectedPairs.length)
1382
+ return false;
1383
+ const expected = new Set(expectedPairs.map(([dimensionId, budgetId]) => `${dimensionId}\u0000${budgetId}`));
1384
+ const seen = new Set();
1385
+ for (const raw of value) {
1386
+ if (!isRecord(raw) ||
1387
+ typeof raw.dimensionId !== 'string' ||
1388
+ typeof raw.budgetId !== 'string' ||
1389
+ !isNonNegativeInteger(raw.remaining)) {
1390
+ return false;
1391
+ }
1392
+ const pair = `${raw.dimensionId}\u0000${raw.budgetId}`;
1393
+ if (!expected.has(pair) || seen.has(pair))
1394
+ return false;
1395
+ seen.add(pair);
1396
+ }
1397
+ return seen.size === expected.size;
1398
+ }
1399
+ function reservedDimensionMap(value, expectedIds) {
1400
+ if (!Array.isArray(value) || value.length !== expectedIds.length)
1401
+ return undefined;
1402
+ const expected = new Set(expectedIds);
1403
+ const result = new Map();
1404
+ for (const raw of value) {
1405
+ if (!isRecord(raw) ||
1406
+ typeof raw.id !== 'string' ||
1407
+ !expected.has(raw.id) ||
1408
+ result.has(raw.id) ||
1409
+ !isNonNegativeInteger(raw.reservedUnits)) {
1410
+ return undefined;
1411
+ }
1412
+ result.set(raw.id, raw.reservedUnits);
1413
+ }
1414
+ return result.size === expected.size ? result : undefined;
1415
+ }
1416
+ function isExpectedHash(value, expectedIds) {
1417
+ return typeof value === 'string' && HASH_PATTERN.test(value) && expectedIds.includes(value);
1418
+ }
1419
+ function isExpectedDimensionId(value, dimensions) {
1420
+ return typeof value === 'string' && dimensions.some(dimension => dimension.id === value);
1421
+ }
1422
+ function isExpectedVectorBudgetId(value, dimensions) {
1423
+ return (typeof value === 'string' &&
1424
+ dimensions.some(dimension => dimension.budgets.some(budget => budget.id === value)));
1425
+ }
1426
+ function isExpectedVectorPair(dimensionId, budgetId, dimensions) {
1427
+ if (typeof dimensionId !== 'string' || typeof budgetId !== 'string')
1428
+ return false;
1429
+ const dimension = dimensions.find(item => item.id === dimensionId);
1430
+ return dimension !== undefined && dimension.budgets.some(budget => budget.id === budgetId);
1431
+ }
1432
+ function isExpectedVectorGrowthPair(dimensionId, budgetId, dimensions) {
1433
+ if (typeof dimensionId !== 'string' || typeof budgetId !== 'string')
1434
+ return false;
1435
+ const dimension = dimensions.find(item => item.id === dimensionId);
1436
+ return dimension !== undefined && dimension.budgets.some(budget => budget.id === budgetId);
1437
+ }
1438
+ function isNonEmptyString(value) {
1439
+ return typeof value === 'string' && value.length > 0;
1440
+ }
1441
+ function isRecoveryReport(value) {
1442
+ if (!isRecord(value) || !isRecoverySummary(value.aggregate))
1443
+ return false;
1444
+ if (value.direct === undefined)
1445
+ return true;
1446
+ if (!isRecord(value.direct) ||
1447
+ !isReservationId(value.direct.reservationId) ||
1448
+ (value.direct.state !== 'pending' && value.direct.state !== 'liable')) {
1449
+ return false;
1450
+ }
1451
+ if (value.direct.vector === true) {
1452
+ return (isNonNegativeInteger(value.direct.dimensionCount) &&
1453
+ isNonNegativeInteger(value.direct.budgetCount));
1454
+ }
1455
+ return isNonNegativeInteger(value.direct.reservedUnits);
1456
+ }
1457
+ function isRecoverySummary(value) {
1458
+ return (isRecord(value) &&
1459
+ isNonNegativeInteger(value.pendingCount) &&
1460
+ isNonNegativeInteger(value.pendingUnits) &&
1461
+ isNonNegativeInteger(value.liableCount) &&
1462
+ isNonNegativeInteger(value.liableUnits) &&
1463
+ (value.vectorPendingCount === undefined || isNonNegativeInteger(value.vectorPendingCount)) &&
1464
+ (value.vectorLiableCount === undefined || isNonNegativeInteger(value.vectorLiableCount)));
1465
+ }
1466
+ function isRecord(value) {
1467
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
1468
+ }
1469
+ function isReservationId(value) {
1470
+ return typeof value === 'string' && RESERVATION_ID_PATTERN.test(value);
1471
+ }
1472
+ function isNonNegativeInteger(value) {
1473
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
1474
+ }
1475
+ function isPositiveInteger(value) {
1476
+ return typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
1477
+ }
1478
+ function responseJson(value, status) {
1479
+ return new Response(JSON.stringify(value), {
1480
+ status,
1481
+ headers: {
1482
+ 'content-type': 'application/json; charset=utf-8',
1483
+ 'cache-control': 'no-store',
1484
+ },
1485
+ });
1486
+ }
1487
+ //# sourceMappingURL=index.js.map