mbase-sdk 0.0.2 → 0.0.4

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.d.cts CHANGED
@@ -29,6 +29,10 @@ type Request = RequestOptions & {
29
29
  };
30
30
  declare class Client {
31
31
  #private;
32
+ /** The engine's clock, as well as this client knows it. */
33
+ now(): number;
34
+ /** Off by up to one round trip, which a window in hours does not notice. */
35
+ observeServerTime(at: Date): void;
32
36
  constructor(options: MeterbaseOptions);
33
37
  request<T>(req: Request): Promise<T>;
34
38
  }
@@ -164,9 +168,47 @@ type CheckParams = {
164
168
  * asks whether there is any capacity at all. */
165
169
  quantity?: number;
166
170
  };
171
+ /**
172
+ * Which gate turned the request away. `entitlement` is named ahead of
173
+ * `rate_limit` when both refuse: running out of plan is the one the customer
174
+ * can act on, and being told about the window instead would send them to
175
+ * support over the wrong thing.
176
+ *
177
+ * Open on purpose. The engine's contract says this list will grow, and a
178
+ * closed union would turn every code the engine adds into a build the caller
179
+ * has to wait on the SDK for. Branch on the codes you know; anything else is
180
+ * still a refusal, and still means do not do the work.
181
+ */
182
+ type CheckReason = "entitlement" | "rate_limit" | (string & {});
183
+ /**
184
+ * One rate-limit window in force for this customer and meter.
185
+ *
186
+ * Windows are fixed and clock-aligned — a cell starts at a multiple of the
187
+ * window counted from the epoch — which is what lets `used` be a literal count
188
+ * of what the current cell holds and `resets_at` a clock time. A sliding
189
+ * window would owe the reader a tooltip for both.
190
+ */
191
+ type CheckRateLimit = {
192
+ /** A whole number of seconds from `60` to `2592000`: a minute to 30 days. */
193
+ window_seconds: number;
194
+ max: number;
195
+ used: number;
196
+ /** `max - used`, floored at 0. */
197
+ remaining: number;
198
+ /** `used + quantity > max`: this request does not fit in this window. */
199
+ exceeded: boolean;
200
+ /** End of the current cell, when `used` goes back to 0. Waiting for it is
201
+ * the fix for a window that refused — unless the quantity is larger than
202
+ * `max`, which never fits however long the caller waits. */
203
+ resets_at: string;
204
+ };
167
205
  type CheckResult = {
206
+ /** Both gates: the capacity covers the quantity **and** every rate-limit
207
+ * window admits it. */
168
208
  allowed: boolean;
169
- /** Unlimited for this meter. `available` is `null`. */
209
+ /** Unlimited for this meter. `available` is `null`. Rate limits still
210
+ * apply — a customer who is never refused on capacity is the one a runaway
211
+ * loop costs most. */
170
212
  no_cap: boolean;
171
213
  /**
172
214
  * The whole capacity: the plan's unused entitlement for the current period,
@@ -178,6 +220,20 @@ type CheckResult = {
178
220
  * per-grant detail, read `customers.allowances.list`.
179
221
  */
180
222
  available: number | null;
223
+ /** Which gate said no. Absent when allowed, so its presence is the denial
224
+ * and reading it is never a branch on a truthy string. */
225
+ reason?: CheckReason;
226
+ /**
227
+ * Every window in force, refused or not, which is why the engine reports
228
+ * them on every call: a caller that only heard about a limit when it was
229
+ * refused could not slow down before it, only after.
230
+ *
231
+ * A limit is protective, not commercial, so it sits beside `available` and
232
+ * is never folded into it — a customer well inside their plan can still be
233
+ * limited, and the two are fixed by different people for different reasons.
234
+ * `[]` when no rule applies, which is the ordinary case.
235
+ */
236
+ rate_limits: CheckRateLimit[];
181
237
  };
182
238
  /**
183
239
  * `track` records work that already happened, so it never refuses on capacity
@@ -432,7 +488,7 @@ type WhoAmI = {
432
488
  * `source: "plan"` is the engine's own, minted when a plan is assigned, and
433
489
  * cannot be created here.
434
490
  */
435
- declare class CustomerAllowances {
491
+ declare class CustomerAllowances$1 {
436
492
  #private;
437
493
  constructor(client: Client);
438
494
  grant(customerId: string, params: AllowanceGrantParams, options?: RequestOptions): Promise<Allowance>;
@@ -453,7 +509,7 @@ declare class CustomerAllowances {
453
509
  * that picking one does not mean knowing what `effective` and `reconciliation`
454
510
  * do to a half-used period.
455
511
  */
456
- declare class CustomerPlan {
512
+ declare class CustomerPlan$1 {
457
513
  #private;
458
514
  constructor(client: Client);
459
515
  /**
@@ -542,12 +598,12 @@ declare class CustomerPlan {
542
598
  history(customerId: string, options?: RequestOptions): Promise<List<Assignment>>;
543
599
  }
544
600
 
545
- declare class Customers {
601
+ declare class Customers$1 {
546
602
  #private;
547
603
  /** The plan they hold, and the trail of instructions that got them there. */
548
- readonly plan: CustomerPlan;
604
+ readonly plan: CustomerPlan$1;
549
605
  /** Grants, which sit on top of whatever the plan gives. */
550
- readonly allowances: CustomerAllowances;
606
+ readonly allowances: CustomerAllowances$1;
551
607
  constructor(client: Client);
552
608
  create(params: CustomerCreateParams, options?: RequestOptions): Promise<Customer>;
553
609
  /** Lean: a listing does not embed the plan. Read one customer for that. */
@@ -569,7 +625,7 @@ declare class Customers {
569
625
  delete(id: string, options?: RequestOptions): Promise<Customer>;
570
626
  }
571
627
 
572
- declare class Meters {
628
+ declare class Meters$1 {
573
629
  #private;
574
630
  constructor(client: Client);
575
631
  create(params: MeterCreateParams, options?: RequestOptions): Promise<Meter>;
@@ -584,16 +640,16 @@ declare class Meters {
584
640
  * A plan's entitlement, versioned. There is no update and no delete: an edit
585
641
  * appends the next version, so which amount applied when stays derivable.
586
642
  */
587
- declare class PlanAllowances {
643
+ declare class PlanAllowances$1 {
588
644
  #private;
589
645
  constructor(client: Client);
590
646
  set(planId: string, params: PlanAllowanceSetParams, options?: RequestOptions): Promise<PlanAllowance>;
591
647
  /** Every version of every meter, newest first — history, not just current. */
592
648
  list(planId: string, params?: PlanAllowanceListParams, options?: RequestOptions): Promise<List<PlanAllowance>>;
593
649
  }
594
- declare class Plans {
650
+ declare class Plans$1 {
595
651
  #private;
596
- readonly allowances: PlanAllowances;
652
+ readonly allowances: PlanAllowances$1;
597
653
  constructor(client: Client);
598
654
  /** A new plan grants nothing; attach entitlement with `allowances.set`. */
599
655
  create(params: PlanCreateParams, options?: RequestOptions): Promise<Plan>;
@@ -634,22 +690,30 @@ declare class NotFoundError extends APIError {
634
690
  }
635
691
  declare class ConflictError extends APIError {
636
692
  }
693
+ declare class InvalidRequestError extends APIError {
694
+ }
695
+ /**
696
+ * `422 invalid_idempotency_key`: the key was not a UUID version 7.
697
+ * `crypto.randomUUID()` mints a v4, so omit `idempotency_key` and let this
698
+ * SDK mint the right thing.
699
+ */
700
+ declare class InvalidIdempotencyKeyError extends InvalidRequestError {
701
+ }
637
702
  /**
638
- * `409 idempotency_conflict`: the key is in use for a different meter or
639
- * quantity. A caller told their key is taken asks what it recorded next, so
640
- * the engine names the original and this carries it.
703
+ * `422 too_late`: the key's timestamp is more than an hour from the engine's
704
+ * clock, so a retry can no longer be told from a new event. **Nothing was
705
+ * recorded** — the check runs before any write.
641
706
  *
642
- * A `ConflictError` still, so `catch (e) { if (e instanceof ConflictError) }`
643
- * keeps working.
707
+ * The usual cause is this machine's clock. When this SDK minted the key it
708
+ * corrects the offset from `serverTime` and retries once, so you only see
709
+ * this for a key you supplied. If that key is a retry of a call made over an
710
+ * hour ago, do not resend it under a new one: it may already be recorded.
644
711
  */
645
- declare class IdempotencyConflictError extends ConflictError {
646
- /** The event that key already recorded. Undefined only if the engine sent a
647
- * body this could not be read out of. */
648
- readonly existingEventId: string | undefined;
712
+ declare class TooLateError extends InvalidRequestError {
713
+ /** The engine's clock when it refused. */
714
+ readonly serverTime: Date | undefined;
649
715
  constructor(args: ConstructorParameters<typeof APIError>[0]);
650
716
  }
651
- declare class InvalidRequestError extends APIError {
652
- }
653
717
  /**
654
718
  * `422 cycle_change_requires_reset`: the two plans measure different cycles,
655
719
  * so a `next_cycle` change has no shared boundary to wait for and a `prorate`
@@ -697,6 +761,12 @@ declare class Meterbase {
697
761
  *
698
762
  * This is the gate, and the only call that ever refuses. Run it before the
699
763
  * work; record the work with `track` afterwards.
764
+ *
765
+ * Two gates answer it, and `allowed` needs both: the customer's capacity has
766
+ * to cover the quantity, and every rate-limit window in force has to admit
767
+ * it. `reason` names the one that said no. `rate_limits` reports each window
768
+ * whether or not it refused, so a caller can ease off as its headroom closes
769
+ * instead of discovering the ceiling by hitting it.
700
770
  */
701
771
  check(params: CheckParams, options?: RequestOptions): Promise<CheckResult>;
702
772
  /**
@@ -705,13 +775,26 @@ declare class Meterbase {
705
775
  * customer's capacity is recorded, drives `available` to 0, and the next
706
776
  * `check` says no. The answer is a receipt, not a verdict.
707
777
  *
708
- * `idempotency_key` is generated when omitted, so retrying this call — the
709
- * SDK's own retries included — replays the original rather than counting
710
- * the same work twice.
778
+ * `idempotency_key` is a UUIDv7, generated when omitted, so retrying this
779
+ * call — the SDK's own retries included — replays the original rather than
780
+ * counting the same work twice. The engine keeps that claim for an hour,
781
+ * which is how long a retry stays recognisable.
711
782
  */
712
783
  track(params: TrackParams, options?: RequestOptions): Promise<TrackResult>;
713
784
  /** Reports which workspace this key acts for. */
714
785
  whoami(options?: RequestOptions): Promise<WhoAmI>;
715
786
  }
787
+ /**
788
+ * The resource namespaces, type-only: none is constructible without the
789
+ * unexported Client. Instance types rather than `export type { Customers }`,
790
+ * which the declaration bundler re-emits as a value export — promising a
791
+ * runtime binding the bundle never has.
792
+ */
793
+ type CustomerAllowances = InstanceType<typeof CustomerAllowances$1>;
794
+ type CustomerPlan = InstanceType<typeof CustomerPlan$1>;
795
+ type Customers = InstanceType<typeof Customers$1>;
796
+ type Meters = InstanceType<typeof Meters$1>;
797
+ type PlanAllowances = InstanceType<typeof PlanAllowances$1>;
798
+ type Plans = InstanceType<typeof Plans$1>;
716
799
 
717
- export { APIError, type Allowance, type AllowanceGrantParams, type AllowanceListParams, type AllowanceSource, type ApplyMode, type AssignPlanParams, type Assignment, AuthenticationError, type CheckParams, type CheckResult, ConflictError, ConnectionError, type Customer, CustomerAllowances, type CustomerCreateParams, type CustomerListParams, CustomerPlan, type CustomerUpdateParams, type CustomerWithPlan, Customers, CycleChangeRequiresResetError, type EmbeddedPlan, IdempotencyConflictError, InvalidRequestError, type List, type Meter, type MeterCreateParams, type MeterListParams, type MeterUpdateParams, Meterbase, MeterbaseError, type MeterbaseOptions, Meters, NotFoundError, PermissionDeniedError, type Plan, type PlanAllowance, type PlanAllowanceKind, type PlanAllowanceListParams, type PlanAllowanceSetParams, PlanAllowances, type PlanChangeNowParams, type PlanChangeParams, type PlanCreateParams, type PlanCycle, type PlanListParams, type PlanMoveParams, type PlanUpdateParams, Plans, RateLimitError, type Reconciliation, type RecordedReconciliation, type RequestOptions, ServerError, TimeoutError, type TrackParams, type TrackResult, type UsageEvent, type Validity, type WhoAmI, errorFromResponse };
800
+ export { APIError, type Allowance, type AllowanceGrantParams, type AllowanceListParams, type AllowanceSource, type ApplyMode, type AssignPlanParams, type Assignment, AuthenticationError, type CheckParams, type CheckRateLimit, type CheckReason, type CheckResult, ConflictError, ConnectionError, type Customer, type CustomerAllowances, type CustomerCreateParams, type CustomerListParams, type CustomerPlan, type CustomerUpdateParams, type CustomerWithPlan, type Customers, CycleChangeRequiresResetError, type EmbeddedPlan, InvalidIdempotencyKeyError, InvalidRequestError, type List, type Meter, type MeterCreateParams, type MeterListParams, type MeterUpdateParams, Meterbase, MeterbaseError, type MeterbaseOptions, type Meters, NotFoundError, PermissionDeniedError, type Plan, type PlanAllowance, type PlanAllowanceKind, type PlanAllowanceListParams, type PlanAllowanceSetParams, type PlanAllowances, type PlanChangeNowParams, type PlanChangeParams, type PlanCreateParams, type PlanCycle, type PlanListParams, type PlanMoveParams, type PlanUpdateParams, type Plans, RateLimitError, type Reconciliation, type RecordedReconciliation, type RequestOptions, ServerError, TimeoutError, TooLateError, type TrackParams, type TrackResult, type UsageEvent, type Validity, type WhoAmI, errorFromResponse };
package/dist/index.d.ts CHANGED
@@ -29,6 +29,10 @@ type Request = RequestOptions & {
29
29
  };
30
30
  declare class Client {
31
31
  #private;
32
+ /** The engine's clock, as well as this client knows it. */
33
+ now(): number;
34
+ /** Off by up to one round trip, which a window in hours does not notice. */
35
+ observeServerTime(at: Date): void;
32
36
  constructor(options: MeterbaseOptions);
33
37
  request<T>(req: Request): Promise<T>;
34
38
  }
@@ -164,9 +168,47 @@ type CheckParams = {
164
168
  * asks whether there is any capacity at all. */
165
169
  quantity?: number;
166
170
  };
171
+ /**
172
+ * Which gate turned the request away. `entitlement` is named ahead of
173
+ * `rate_limit` when both refuse: running out of plan is the one the customer
174
+ * can act on, and being told about the window instead would send them to
175
+ * support over the wrong thing.
176
+ *
177
+ * Open on purpose. The engine's contract says this list will grow, and a
178
+ * closed union would turn every code the engine adds into a build the caller
179
+ * has to wait on the SDK for. Branch on the codes you know; anything else is
180
+ * still a refusal, and still means do not do the work.
181
+ */
182
+ type CheckReason = "entitlement" | "rate_limit" | (string & {});
183
+ /**
184
+ * One rate-limit window in force for this customer and meter.
185
+ *
186
+ * Windows are fixed and clock-aligned — a cell starts at a multiple of the
187
+ * window counted from the epoch — which is what lets `used` be a literal count
188
+ * of what the current cell holds and `resets_at` a clock time. A sliding
189
+ * window would owe the reader a tooltip for both.
190
+ */
191
+ type CheckRateLimit = {
192
+ /** A whole number of seconds from `60` to `2592000`: a minute to 30 days. */
193
+ window_seconds: number;
194
+ max: number;
195
+ used: number;
196
+ /** `max - used`, floored at 0. */
197
+ remaining: number;
198
+ /** `used + quantity > max`: this request does not fit in this window. */
199
+ exceeded: boolean;
200
+ /** End of the current cell, when `used` goes back to 0. Waiting for it is
201
+ * the fix for a window that refused — unless the quantity is larger than
202
+ * `max`, which never fits however long the caller waits. */
203
+ resets_at: string;
204
+ };
167
205
  type CheckResult = {
206
+ /** Both gates: the capacity covers the quantity **and** every rate-limit
207
+ * window admits it. */
168
208
  allowed: boolean;
169
- /** Unlimited for this meter. `available` is `null`. */
209
+ /** Unlimited for this meter. `available` is `null`. Rate limits still
210
+ * apply — a customer who is never refused on capacity is the one a runaway
211
+ * loop costs most. */
170
212
  no_cap: boolean;
171
213
  /**
172
214
  * The whole capacity: the plan's unused entitlement for the current period,
@@ -178,6 +220,20 @@ type CheckResult = {
178
220
  * per-grant detail, read `customers.allowances.list`.
179
221
  */
180
222
  available: number | null;
223
+ /** Which gate said no. Absent when allowed, so its presence is the denial
224
+ * and reading it is never a branch on a truthy string. */
225
+ reason?: CheckReason;
226
+ /**
227
+ * Every window in force, refused or not, which is why the engine reports
228
+ * them on every call: a caller that only heard about a limit when it was
229
+ * refused could not slow down before it, only after.
230
+ *
231
+ * A limit is protective, not commercial, so it sits beside `available` and
232
+ * is never folded into it — a customer well inside their plan can still be
233
+ * limited, and the two are fixed by different people for different reasons.
234
+ * `[]` when no rule applies, which is the ordinary case.
235
+ */
236
+ rate_limits: CheckRateLimit[];
181
237
  };
182
238
  /**
183
239
  * `track` records work that already happened, so it never refuses on capacity
@@ -432,7 +488,7 @@ type WhoAmI = {
432
488
  * `source: "plan"` is the engine's own, minted when a plan is assigned, and
433
489
  * cannot be created here.
434
490
  */
435
- declare class CustomerAllowances {
491
+ declare class CustomerAllowances$1 {
436
492
  #private;
437
493
  constructor(client: Client);
438
494
  grant(customerId: string, params: AllowanceGrantParams, options?: RequestOptions): Promise<Allowance>;
@@ -453,7 +509,7 @@ declare class CustomerAllowances {
453
509
  * that picking one does not mean knowing what `effective` and `reconciliation`
454
510
  * do to a half-used period.
455
511
  */
456
- declare class CustomerPlan {
512
+ declare class CustomerPlan$1 {
457
513
  #private;
458
514
  constructor(client: Client);
459
515
  /**
@@ -542,12 +598,12 @@ declare class CustomerPlan {
542
598
  history(customerId: string, options?: RequestOptions): Promise<List<Assignment>>;
543
599
  }
544
600
 
545
- declare class Customers {
601
+ declare class Customers$1 {
546
602
  #private;
547
603
  /** The plan they hold, and the trail of instructions that got them there. */
548
- readonly plan: CustomerPlan;
604
+ readonly plan: CustomerPlan$1;
549
605
  /** Grants, which sit on top of whatever the plan gives. */
550
- readonly allowances: CustomerAllowances;
606
+ readonly allowances: CustomerAllowances$1;
551
607
  constructor(client: Client);
552
608
  create(params: CustomerCreateParams, options?: RequestOptions): Promise<Customer>;
553
609
  /** Lean: a listing does not embed the plan. Read one customer for that. */
@@ -569,7 +625,7 @@ declare class Customers {
569
625
  delete(id: string, options?: RequestOptions): Promise<Customer>;
570
626
  }
571
627
 
572
- declare class Meters {
628
+ declare class Meters$1 {
573
629
  #private;
574
630
  constructor(client: Client);
575
631
  create(params: MeterCreateParams, options?: RequestOptions): Promise<Meter>;
@@ -584,16 +640,16 @@ declare class Meters {
584
640
  * A plan's entitlement, versioned. There is no update and no delete: an edit
585
641
  * appends the next version, so which amount applied when stays derivable.
586
642
  */
587
- declare class PlanAllowances {
643
+ declare class PlanAllowances$1 {
588
644
  #private;
589
645
  constructor(client: Client);
590
646
  set(planId: string, params: PlanAllowanceSetParams, options?: RequestOptions): Promise<PlanAllowance>;
591
647
  /** Every version of every meter, newest first — history, not just current. */
592
648
  list(planId: string, params?: PlanAllowanceListParams, options?: RequestOptions): Promise<List<PlanAllowance>>;
593
649
  }
594
- declare class Plans {
650
+ declare class Plans$1 {
595
651
  #private;
596
- readonly allowances: PlanAllowances;
652
+ readonly allowances: PlanAllowances$1;
597
653
  constructor(client: Client);
598
654
  /** A new plan grants nothing; attach entitlement with `allowances.set`. */
599
655
  create(params: PlanCreateParams, options?: RequestOptions): Promise<Plan>;
@@ -634,22 +690,30 @@ declare class NotFoundError extends APIError {
634
690
  }
635
691
  declare class ConflictError extends APIError {
636
692
  }
693
+ declare class InvalidRequestError extends APIError {
694
+ }
695
+ /**
696
+ * `422 invalid_idempotency_key`: the key was not a UUID version 7.
697
+ * `crypto.randomUUID()` mints a v4, so omit `idempotency_key` and let this
698
+ * SDK mint the right thing.
699
+ */
700
+ declare class InvalidIdempotencyKeyError extends InvalidRequestError {
701
+ }
637
702
  /**
638
- * `409 idempotency_conflict`: the key is in use for a different meter or
639
- * quantity. A caller told their key is taken asks what it recorded next, so
640
- * the engine names the original and this carries it.
703
+ * `422 too_late`: the key's timestamp is more than an hour from the engine's
704
+ * clock, so a retry can no longer be told from a new event. **Nothing was
705
+ * recorded** — the check runs before any write.
641
706
  *
642
- * A `ConflictError` still, so `catch (e) { if (e instanceof ConflictError) }`
643
- * keeps working.
707
+ * The usual cause is this machine's clock. When this SDK minted the key it
708
+ * corrects the offset from `serverTime` and retries once, so you only see
709
+ * this for a key you supplied. If that key is a retry of a call made over an
710
+ * hour ago, do not resend it under a new one: it may already be recorded.
644
711
  */
645
- declare class IdempotencyConflictError extends ConflictError {
646
- /** The event that key already recorded. Undefined only if the engine sent a
647
- * body this could not be read out of. */
648
- readonly existingEventId: string | undefined;
712
+ declare class TooLateError extends InvalidRequestError {
713
+ /** The engine's clock when it refused. */
714
+ readonly serverTime: Date | undefined;
649
715
  constructor(args: ConstructorParameters<typeof APIError>[0]);
650
716
  }
651
- declare class InvalidRequestError extends APIError {
652
- }
653
717
  /**
654
718
  * `422 cycle_change_requires_reset`: the two plans measure different cycles,
655
719
  * so a `next_cycle` change has no shared boundary to wait for and a `prorate`
@@ -697,6 +761,12 @@ declare class Meterbase {
697
761
  *
698
762
  * This is the gate, and the only call that ever refuses. Run it before the
699
763
  * work; record the work with `track` afterwards.
764
+ *
765
+ * Two gates answer it, and `allowed` needs both: the customer's capacity has
766
+ * to cover the quantity, and every rate-limit window in force has to admit
767
+ * it. `reason` names the one that said no. `rate_limits` reports each window
768
+ * whether or not it refused, so a caller can ease off as its headroom closes
769
+ * instead of discovering the ceiling by hitting it.
700
770
  */
701
771
  check(params: CheckParams, options?: RequestOptions): Promise<CheckResult>;
702
772
  /**
@@ -705,13 +775,26 @@ declare class Meterbase {
705
775
  * customer's capacity is recorded, drives `available` to 0, and the next
706
776
  * `check` says no. The answer is a receipt, not a verdict.
707
777
  *
708
- * `idempotency_key` is generated when omitted, so retrying this call — the
709
- * SDK's own retries included — replays the original rather than counting
710
- * the same work twice.
778
+ * `idempotency_key` is a UUIDv7, generated when omitted, so retrying this
779
+ * call — the SDK's own retries included — replays the original rather than
780
+ * counting the same work twice. The engine keeps that claim for an hour,
781
+ * which is how long a retry stays recognisable.
711
782
  */
712
783
  track(params: TrackParams, options?: RequestOptions): Promise<TrackResult>;
713
784
  /** Reports which workspace this key acts for. */
714
785
  whoami(options?: RequestOptions): Promise<WhoAmI>;
715
786
  }
787
+ /**
788
+ * The resource namespaces, type-only: none is constructible without the
789
+ * unexported Client. Instance types rather than `export type { Customers }`,
790
+ * which the declaration bundler re-emits as a value export — promising a
791
+ * runtime binding the bundle never has.
792
+ */
793
+ type CustomerAllowances = InstanceType<typeof CustomerAllowances$1>;
794
+ type CustomerPlan = InstanceType<typeof CustomerPlan$1>;
795
+ type Customers = InstanceType<typeof Customers$1>;
796
+ type Meters = InstanceType<typeof Meters$1>;
797
+ type PlanAllowances = InstanceType<typeof PlanAllowances$1>;
798
+ type Plans = InstanceType<typeof Plans$1>;
716
799
 
717
- export { APIError, type Allowance, type AllowanceGrantParams, type AllowanceListParams, type AllowanceSource, type ApplyMode, type AssignPlanParams, type Assignment, AuthenticationError, type CheckParams, type CheckResult, ConflictError, ConnectionError, type Customer, CustomerAllowances, type CustomerCreateParams, type CustomerListParams, CustomerPlan, type CustomerUpdateParams, type CustomerWithPlan, Customers, CycleChangeRequiresResetError, type EmbeddedPlan, IdempotencyConflictError, InvalidRequestError, type List, type Meter, type MeterCreateParams, type MeterListParams, type MeterUpdateParams, Meterbase, MeterbaseError, type MeterbaseOptions, Meters, NotFoundError, PermissionDeniedError, type Plan, type PlanAllowance, type PlanAllowanceKind, type PlanAllowanceListParams, type PlanAllowanceSetParams, PlanAllowances, type PlanChangeNowParams, type PlanChangeParams, type PlanCreateParams, type PlanCycle, type PlanListParams, type PlanMoveParams, type PlanUpdateParams, Plans, RateLimitError, type Reconciliation, type RecordedReconciliation, type RequestOptions, ServerError, TimeoutError, type TrackParams, type TrackResult, type UsageEvent, type Validity, type WhoAmI, errorFromResponse };
800
+ export { APIError, type Allowance, type AllowanceGrantParams, type AllowanceListParams, type AllowanceSource, type ApplyMode, type AssignPlanParams, type Assignment, AuthenticationError, type CheckParams, type CheckRateLimit, type CheckReason, type CheckResult, ConflictError, ConnectionError, type Customer, type CustomerAllowances, type CustomerCreateParams, type CustomerListParams, type CustomerPlan, type CustomerUpdateParams, type CustomerWithPlan, type Customers, CycleChangeRequiresResetError, type EmbeddedPlan, InvalidIdempotencyKeyError, InvalidRequestError, type List, type Meter, type MeterCreateParams, type MeterListParams, type MeterUpdateParams, Meterbase, MeterbaseError, type MeterbaseOptions, type Meters, NotFoundError, PermissionDeniedError, type Plan, type PlanAllowance, type PlanAllowanceKind, type PlanAllowanceListParams, type PlanAllowanceSetParams, type PlanAllowances, type PlanChangeNowParams, type PlanChangeParams, type PlanCreateParams, type PlanCycle, type PlanListParams, type PlanMoveParams, type PlanUpdateParams, type Plans, RateLimitError, type Reconciliation, type RecordedReconciliation, type RequestOptions, ServerError, TimeoutError, TooLateError, type TrackParams, type TrackResult, type UsageEvent, type Validity, type WhoAmI, errorFromResponse };
package/dist/index.js CHANGED
@@ -27,17 +27,18 @@ var NotFoundError = class extends APIError {
27
27
  };
28
28
  var ConflictError = class extends APIError {
29
29
  };
30
- var IdempotencyConflictError = class extends ConflictError {
31
- /** The event that key already recorded. Undefined only if the engine sent a
32
- * body this could not be read out of. */
33
- existingEventId;
30
+ var InvalidRequestError = class extends APIError {
31
+ };
32
+ var InvalidIdempotencyKeyError = class extends InvalidRequestError {
33
+ };
34
+ var TooLateError = class extends InvalidRequestError {
35
+ /** The engine's clock when it refused. */
36
+ serverTime;
34
37
  constructor(args) {
35
38
  super(args);
36
- this.existingEventId = existingEventId(args.body);
39
+ this.serverTime = serverTime(args.body);
37
40
  }
38
41
  };
39
- var InvalidRequestError = class extends APIError {
40
- };
41
42
  var CycleChangeRequiresResetError = class extends InvalidRequestError {
42
43
  };
43
44
  var RateLimitError = class extends APIError {
@@ -63,21 +64,32 @@ function errorFromResponse(args) {
63
64
  case 404:
64
65
  return new NotFoundError(args);
65
66
  case 409:
66
- return args.code === "idempotency_conflict" ? new IdempotencyConflictError(args) : new ConflictError(args);
67
+ return new ConflictError(args);
67
68
  case 422:
68
- return args.code === "cycle_change_requires_reset" ? new CycleChangeRequiresResetError(args) : new InvalidRequestError(args);
69
+ switch (args.code) {
70
+ case "cycle_change_requires_reset":
71
+ return new CycleChangeRequiresResetError(args);
72
+ case "invalid_idempotency_key":
73
+ return new InvalidIdempotencyKeyError(args);
74
+ case "too_late":
75
+ return new TooLateError(args);
76
+ default:
77
+ return new InvalidRequestError(args);
78
+ }
69
79
  case 429:
70
80
  return new RateLimitError(args);
71
81
  default:
72
82
  return args.status >= 500 ? new ServerError(args) : new APIError(args);
73
83
  }
74
84
  }
75
- function existingEventId(body) {
85
+ function serverTime(body) {
76
86
  if (typeof body !== "object" || body === null) return void 0;
77
87
  const error = body.error;
78
88
  if (typeof error !== "object" || error === null) return void 0;
79
- const id = error.existing_event_id;
80
- return typeof id === "string" ? id : void 0;
89
+ const at = error.server_time;
90
+ if (typeof at !== "string") return void 0;
91
+ const parsed = new Date(at);
92
+ return Number.isNaN(parsed.getTime()) ? void 0 : parsed;
81
93
  }
82
94
 
83
95
  // src/client.ts
@@ -91,6 +103,21 @@ var Client = class {
91
103
  #timeout;
92
104
  #maxRetries;
93
105
  #fetch;
106
+ /**
107
+ * How far the engine's clock is ahead of this machine's, in milliseconds.
108
+ * Idempotency keys are minted here and the engine refuses one dated more
109
+ * than an hour from its own clock, so without this a device with a wrong
110
+ * clock would fail every call forever.
111
+ */
112
+ #clockOffset = 0;
113
+ /** The engine's clock, as well as this client knows it. */
114
+ now() {
115
+ return Date.now() + this.#clockOffset;
116
+ }
117
+ /** Off by up to one round trip, which a window in hours does not notice. */
118
+ observeServerTime(at) {
119
+ this.#clockOffset = at.getTime() - Date.now();
120
+ }
94
121
  constructor(options) {
95
122
  if (!options.apiKey) {
96
123
  throw new MeterbaseError(
@@ -635,6 +662,12 @@ var Meterbase = class {
635
662
  *
636
663
  * This is the gate, and the only call that ever refuses. Run it before the
637
664
  * work; record the work with `track` afterwards.
665
+ *
666
+ * Two gates answer it, and `allowed` needs both: the customer's capacity has
667
+ * to cover the quantity, and every rate-limit window in force has to admit
668
+ * it. `reason` names the one that said no. `rate_limits` reports each window
669
+ * whether or not it refused, so a caller can ease off as its headroom closes
670
+ * instead of discovering the ceiling by hitting it.
638
671
  */
639
672
  check(params, options) {
640
673
  return this.#client.request({
@@ -650,25 +683,42 @@ var Meterbase = class {
650
683
  * customer's capacity is recorded, drives `available` to 0, and the next
651
684
  * `check` says no. The answer is a receipt, not a verdict.
652
685
  *
653
- * `idempotency_key` is generated when omitted, so retrying this call — the
654
- * SDK's own retries included — replays the original rather than counting
655
- * the same work twice.
686
+ * `idempotency_key` is a UUIDv7, generated when omitted, so retrying this
687
+ * call — the SDK's own retries included — replays the original rather than
688
+ * counting the same work twice. The engine keeps that claim for an hour,
689
+ * which is how long a retry stays recognisable.
656
690
  */
657
691
  // `async` so that generating the key cannot throw synchronously out of a
658
692
  // method that otherwise only ever rejects: one call, one way to fail.
659
693
  async track(params, options) {
660
- return this.#client.request({
694
+ const send = (key2) => this.#client.request({
661
695
  ...options,
662
696
  method: "POST",
663
697
  path: "/v1/usage/track",
664
- // Fixed here, once, before the retry loop is entered: every attempt of
665
- // this call carries the same key, which is what makes replaying it safe.
666
- body: {
667
- ...params,
668
- idempotency_key: params.idempotency_key ?? idempotencyKey()
669
- },
698
+ // Fixed before the retry loop is entered: every attempt of this call
699
+ // carries the same key, which is what makes replaying it safe.
700
+ body: { ...params, idempotency_key: key2 },
670
701
  idempotent: true
671
702
  });
703
+ if (params.idempotency_key !== void 0) {
704
+ return send(params.idempotency_key);
705
+ }
706
+ const key = idempotencyKey(this.#client.now());
707
+ const startedAt = Date.now();
708
+ try {
709
+ return await send(key);
710
+ } catch (error) {
711
+ if (!(error instanceof TooLateError) || error.serverTime === void 0) {
712
+ throw error;
713
+ }
714
+ const elapsed = Date.now() - startedAt;
715
+ const serverAtStart = error.serverTime.getTime() - elapsed;
716
+ if (Math.abs(serverAtStart - mintedAtMs(key)) <= IDEMPOTENCY_WINDOW_MS) {
717
+ throw error;
718
+ }
719
+ this.#client.observeServerTime(error.serverTime);
720
+ return await send(idempotencyKey(this.#client.now()));
721
+ }
672
722
  }
673
723
  /** Reports which workspace this key acts for. */
674
724
  whoami(options) {
@@ -679,20 +729,39 @@ var Meterbase = class {
679
729
  });
680
730
  }
681
731
  };
682
- function idempotencyKey() {
732
+ var IDEMPOTENCY_WINDOW_MS = 60 * 60 * 1e3;
733
+ function mintedAtMs(key) {
734
+ return Number.parseInt(key.replace(/-/g, "").slice(0, 12), 16);
735
+ }
736
+ function idempotencyKey(atMs) {
683
737
  const webcrypto = globalThis.crypto;
684
- if (typeof webcrypto?.randomUUID === "function") {
685
- return webcrypto.randomUUID();
686
- }
687
- if (typeof webcrypto?.getRandomValues === "function") {
688
- const bytes = webcrypto.getRandomValues(new Uint8Array(16));
689
- return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
738
+ if (typeof webcrypto?.getRandomValues !== "function") {
739
+ throw new MeterbaseError(
740
+ "No crypto to generate an idempotency key with: pass `idempotency_key` yourself (a UUIDv7), or run somewhere `crypto.getRandomValues` exists."
741
+ );
690
742
  }
691
- throw new MeterbaseError(
692
- "No crypto to generate an idempotency key with: pass `idempotency_key` yourself, or run somewhere `crypto.getRandomValues` exists."
693
- );
743
+ const bytes = webcrypto.getRandomValues(new Uint8Array(16));
744
+ const ms = Math.max(0, Math.trunc(atMs));
745
+ const high = Math.floor(ms / 4294967296);
746
+ const low = ms >>> 0;
747
+ bytes[0] = high >>> 8 & 255;
748
+ bytes[1] = high & 255;
749
+ bytes[2] = low >>> 24 & 255;
750
+ bytes[3] = low >>> 16 & 255;
751
+ bytes[4] = low >>> 8 & 255;
752
+ bytes[5] = low & 255;
753
+ bytes[6] = bytes[6] & 15 | 112;
754
+ bytes[8] = bytes[8] & 63 | 128;
755
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
756
+ return [
757
+ hex.slice(0, 8),
758
+ hex.slice(8, 12),
759
+ hex.slice(12, 16),
760
+ hex.slice(16, 20),
761
+ hex.slice(20)
762
+ ].join("-");
694
763
  }
695
764
 
696
- export { APIError, AuthenticationError, ConflictError, ConnectionError, CycleChangeRequiresResetError, IdempotencyConflictError, InvalidRequestError, Meterbase, MeterbaseError, NotFoundError, PermissionDeniedError, RateLimitError, ServerError, TimeoutError, errorFromResponse };
765
+ export { APIError, AuthenticationError, ConflictError, ConnectionError, CycleChangeRequiresResetError, InvalidIdempotencyKeyError, InvalidRequestError, Meterbase, MeterbaseError, NotFoundError, PermissionDeniedError, RateLimitError, ServerError, TimeoutError, TooLateError, errorFromResponse };
697
766
  //# sourceMappingURL=index.js.map
698
767
  //# sourceMappingURL=index.js.map