mbase-sdk 0.0.2 → 0.0.5

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/README.md CHANGED
@@ -12,20 +12,25 @@ npm install @meterbase/sdk
12
12
 
13
13
  ## Usage
14
14
 
15
- Two calls, in this order: **`check` → do the work → `track`.**
15
+ Two calls, in this order: **`check` → do the work → `track`** — or, for work
16
+ that must not be done twice, [`reserve`](#work-you-cannot-do-twice).
16
17
 
17
18
  ```ts
18
19
  import { Meterbase } from "@meterbase/sdk"
19
20
 
20
21
  const meterbase = new Meterbase({ apiKey: process.env.METERBASE_API_KEY! })
21
22
 
22
- const { allowed } = await meterbase.check({
23
+ const { allowed, reason, rate_limits } = await meterbase.check({
23
24
  customer_id: "acct_1", // the tenant's own id, not ours
24
25
  meter_id: "ai_tokens", // the meter's key, not ours
25
26
  quantity: 50_000, // what you are about to spend; defaults to 1
26
27
  })
27
28
 
28
- if (!allowed) throw new Error("Out of quota")
29
+ if (!allowed) throw new Error(`Refused: ${reason}`)
30
+
31
+ // Allowed, and still worth reading: every window comes back on every call.
32
+ const tight = rate_limits.find((w) => w.remaining < w.max / 10)
33
+ if (tight) console.warn(`Slowing down until ${tight.resets_at}`)
29
34
 
30
35
  const completion = await generate(prompt)
31
36
 
@@ -48,6 +53,16 @@ capacity is not a number. It is one figure and not a breakdown: `check` answers
48
53
  from a single cached integer, and itemising where the capacity came from would
49
54
  cost it that. For the per-grant detail, read a customer's allowances.
50
55
 
56
+ A check answers **two gates**, and `allowed` needs both: the capacity has to
57
+ cover the quantity, and every rate limit in force — _at most so much of this
58
+ meter per minute, hour or day_ — has to admit it. `reason` names the one that
59
+ said no, and is absent when the answer is yes. `rate_limits` reports every
60
+ window whether or not it refused, so a caller can ease off as its headroom
61
+ closes instead of finding the ceiling by hitting it; it is `[]` when no rule
62
+ applies. A limit is protective and not commercial, so it is never folded into
63
+ `available`: a customer well inside their plan can still be limited, and one on
64
+ `no_cap` is precisely the customer a runaway loop costs most.
65
+
51
66
  ### Recording usage
52
67
 
53
68
  `track` counts work that already happened — the tokens were spent, the image
@@ -60,7 +75,7 @@ const { event } = await meterbase.track({
60
75
  customer_id: "acct_1",
61
76
  meter_id: "ai_tokens",
62
77
  quantity: 250_000,
63
- idempotency_key: "req_9f3a", // generated when you omit it
78
+ idempotency_key: "0199e0c0-8f3a-7c21-9d44-6b2e91a0f5c3", // a UUIDv7; generated when you omit it
64
79
  })
65
80
 
66
81
  event.replayed // false the first time, true for a retry of the same call
@@ -75,16 +90,37 @@ bill drawn from a period is never moved by a report that arrives after it.
75
90
 
76
91
  #### Idempotency
77
92
 
78
- `customer_id` + `idempotency_key` identifies one logical event.
79
-
80
- | The retry sends | The engine answers |
81
- | -------------------------------- | --------------------------------------------------------- |
82
- | the same key, meter and quantity | the original event with `replayed: true`, writing nothing |
83
- | the same key, a different event | `409 idempotency_conflict`, naming the original |
84
-
85
- The timestamp is deliberately not compared: every call is stamped with its own
86
- clock, so a retry carries a later one, and refusing that would punish the
87
- ordinary retry. The original's timestamp comes back unchanged.
93
+ `idempotency_key` identifies one logical event, and **is** that event's id.
94
+ It has to be a **UUIDv7**, whose first 48 bits are the millisecond it was
95
+ minted. That timestamp is not decoration: the engine admits a key only while
96
+ it is within **one hour** of the engine's clock, and keeps the claim behind it
97
+ until exactly that hour is up. A retry is therefore recognised for as long as
98
+ it is accepted at all.
99
+
100
+ | The call sends | The engine answers |
101
+ | ------------------------------------- | ------------------------------------------------- |
102
+ | a fresh key | the recorded event, `replayed: false` |
103
+ | a key it has already seen | that event with `replayed: true`, writing nothing |
104
+ | a key more than an hour old, or ahead | `422 too_late`, writing nothing |
105
+ | anything that is not a UUIDv7 | `422 invalid_idempotency_key`, writing nothing |
106
+
107
+ A key it has already seen replays **whatever the rest of the body says**:
108
+ there is one claim behind the key and nothing to compare a retry against, so
109
+ a changed quantity is a replay rather than an error. The upside is that a
110
+ replay costs one lookup and stores nothing.
111
+
112
+ **The hour is a retry budget.** Retry inside it and your call is recognised.
113
+ Past it, `too_late` — and do not then resend under a fresh key, because the
114
+ original may have been recorded. Nothing is written on either refusal: both
115
+ are checked before the engine touches anything.
116
+
117
+ **Clocks matter now.** The key is minted here, from this machine's clock, so
118
+ a device running more than an hour off would have every call refused. When
119
+ this SDK mints the key it handles that itself: `too_late` carries the engine's
120
+ `server_time`, the client corrects its offset and retries once, and you never
121
+ see the error. When **you** supply the key, you get `TooLateError` with
122
+ `serverTime` on it, because only you can know whether that key names work that
123
+ was already recorded.
88
124
 
89
125
  **The SDK generates a key when you omit one, and reuses it across that call's
90
126
  retries** — including its own, since `track` is the one `POST` it will replay.
@@ -93,6 +129,68 @@ request id — so that a retry from further out than this SDK replays too. Keys
93
129
  are retained 35 days; a retry after that records a second event, which is not a
94
130
  case this API sets out to serve.
95
131
 
132
+ ### Work you cannot do twice
133
+
134
+ `check` → work → `track` has a hole: two concurrent callers both pass the gate,
135
+ both do the work, and both record it. For an API call that costs nothing that
136
+ is an accepted trade. For an LLM completion, an image, a transcode, it is the
137
+ whole problem — the customer is over their cap and the money is already spent.
138
+
139
+ `reserve` closes it. It makes the same decision `check` makes and then **holds**
140
+ the capacity, so two reserves for the same last units cannot both be granted:
141
+
142
+ ```ts
143
+ const hold = await meterbase.reserve({
144
+ customer_id: "acct_1",
145
+ meter_id: "ai_tokens",
146
+ quantity: 50_000, // what you are about to spend; defaults to 1
147
+ expires_in_seconds: 60, // how long to hold it; defaults to 60, at most 900
148
+ })
149
+
150
+ if (!hold.allowed) throw new Error(`Refused: ${hold.reason}`)
151
+
152
+ try {
153
+ const completion = await generate(prompt)
154
+ await hold.commit(completion.usage.total_tokens) // records what it cost
155
+ } catch (error) {
156
+ await hold.release() // nothing happened, so nothing is recorded
157
+ throw error
158
+ }
159
+ ```
160
+
161
+ **The rule for choosing: can you afford to do the work twice? `check`. No?
162
+ `reserve`.** A reserve costs a database transaction where a check costs a cached
163
+ read — refusals included — so it is not the call to put in front of cheap work.
164
+
165
+ A reserve answers everything `check` answers, plus the hold: `reservation_id`,
166
+ the `quantity` it set aside, and `expires_at`. `available` is already net of it.
167
+ A refusal holds nothing, so it carries no id and no expiry, and `allowed`
168
+ narrows the type — after `if (!hold.allowed)`, `commit` and `release` are there.
169
+
170
+ `commit` is `track` under the hold's own id, which is what makes it a commit
171
+ rather than a second event beside it. Pass what the work actually cost and the
172
+ difference comes back; omit it to record what was held. Going over is allowed —
173
+ the hold only ever guaranteed what it reserved.
174
+
175
+ `release` gives the capacity back and records nothing. It is built for the
176
+ `catch` block, so it resolves with `null` instead of throwing when there is
177
+ nothing left to release — already committed, already released — because
178
+ throwing there would mask the error you are already handling.
179
+ `meterbase.release({ reservation_id })` throws that 404, as every other call
180
+ does.
181
+
182
+ **Nothing is lost if your process dies.** A hold expires on its own, and the
183
+ capacity comes back with nothing having run. A commit that arrives after
184
+ `expires_at` is still recorded — you lose the guarantee, not the event — and
185
+ the SDK warns when that happens, which is the only feedback there is on an
186
+ `expires_in_seconds` guessed too short.
187
+
188
+ `reservation_id` is a UUIDv7 on the same terms as `idempotency_key`, generated
189
+ when you omit it, and it becomes the event's id once the hold is committed. One
190
+ id names the hold, the claim and the event. An id is for one reserve and its
191
+ retries: once its hold is committed or released, sending it again reserves
192
+ afresh rather than finding anything.
193
+
96
194
  ### Customers
97
195
 
98
196
  ```ts
@@ -362,18 +460,19 @@ try {
362
460
  }
363
461
  ```
364
462
 
365
- | Class | When |
366
- | ---------------------------------- | ---------------------------------------------------------------------------------------- |
367
- | `AuthenticationError` | 401 — unknown, revoked or malformed key |
368
- | `PermissionDeniedError` | 403 — the key may not use this route |
369
- | `NotFoundError` | 404 |
370
- | `ConflictError` | 409 — a key or external id is taken |
371
- | `IdempotencyConflictError` | 409 — a `ConflictError` naming the event that key already recorded, on `existingEventId` |
372
- | `InvalidRequestError` | 422 — a field failed validation |
373
- | `CycleChangeRequiresResetError` | 422 — an `InvalidRequestError`: the two plans' cycles differ, so use `restart` |
374
- | `RateLimitError` | 429 |
375
- | `ServerError` | 5xx |
376
- | `ConnectionError` / `TimeoutError` | the request never got an answer |
463
+ | Class | When |
464
+ | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
465
+ | `AuthenticationError` | 401 — unknown, revoked or malformed key |
466
+ | `PermissionDeniedError` | 403 — the key may not use this route |
467
+ | `NotFoundError` | 404 |
468
+ | `ConflictError` | 409 — a key or external id is taken |
469
+ | `InvalidRequestError` | 422 — a field failed validation |
470
+ | `CycleChangeRequiresResetError` | 422 — an `InvalidRequestError`: the two plans' cycles differ, so use `restart` |
471
+ | `InvalidIdempotencyKeyError` | 422 — an `InvalidRequestError`: the key was not a UUIDv7 |
472
+ | `TooLateError` | 422 — an `InvalidRequestError`: the key is over an hour from the engine's clock, which it carries on `serverTime`. Nothing was recorded |
473
+ | `RateLimitError` | 429 |
474
+ | `ServerError` | 5xx |
475
+ | `ConnectionError` / `TimeoutError` | the request never got an answer |
377
476
 
378
477
  `APIError` carries `status`, `code` and the parsed `body`. Branch on `code`,
379
478
  which is stable; `message` is for humans and may change.
@@ -405,7 +504,8 @@ because a retried create could produce a second row.
405
504
  SDK fixes that key **before** the retry loop starts: every attempt of one call
406
505
  names the same logical event, so the engine replays it instead of counting the
407
506
  work twice. If you retry `track` yourself, pass your own `idempotency_key` and
408
- keep it the same across attempts for exactly that reason.
507
+ keep it the same across attempts for exactly that reason — and do it inside
508
+ the hour the engine holds that key for.
409
509
 
410
510
  ## Types
411
511
 
@@ -481,11 +581,12 @@ It asserts the whole lifecycle of a customer, a meter and a plan; that a plan's
481
581
  allowances become real entitlement a `check` can see, with the recurring cap
482
582
  and the minted bonus summed into `available`; that tracked usage takes exactly
483
583
  that much away, that a replay takes nothing, and that a reused key naming a
484
- different event is refused; that a scheduled plan change is dated ahead and
485
- reads as `pending`, that cancelling it supersedes it, that a `restart`
486
- re-anchors the customer, and that two plans on different cycles refuse a
487
- scheduled change; that soft deletes and archiving behave as
488
- documented; that duplicates really answer
584
+ different event is refused; that a rate limit set on a meter comes back on
585
+ every `check` as a live window, and refuses with the allowance untouched; that
586
+ a scheduled plan change is dated ahead and reads as `pending`, that cancelling
587
+ it supersedes it, that a `restart` re-anchors the customer, and that two plans
588
+ on different cycles refuse a scheduled change; that soft deletes and archiving
589
+ behave as documented; that duplicates really answer
489
590
  `409 customer_external_id_taken` / `409 meter_key_taken`; and — via
490
591
  `Record<keyof T, true>` field maps — that the engine's fields still match the
491
592
  SDK's types exactly, in both directions.
package/dist/index.cjs CHANGED
@@ -29,16 +29,19 @@ var NotFoundError = class extends APIError {
29
29
  };
30
30
  var ConflictError = class extends APIError {
31
31
  };
32
- var IdempotencyConflictError = class extends ConflictError {
33
- /** The event that key already recorded. Undefined only if the engine sent a
34
- * body this could not be read out of. */
35
- existingEventId;
32
+ var InvalidRequestError = class extends APIError {
33
+ };
34
+ var InvalidIdempotencyKeyError = class extends InvalidRequestError {
35
+ };
36
+ var TooLateError = class extends InvalidRequestError {
37
+ /** The engine's clock when it refused. */
38
+ serverTime;
36
39
  constructor(args) {
37
40
  super(args);
38
- this.existingEventId = existingEventId(args.body);
41
+ this.serverTime = serverTime(args.body);
39
42
  }
40
43
  };
41
- var InvalidRequestError = class extends APIError {
44
+ var ReservationMismatchError = class extends InvalidRequestError {
42
45
  };
43
46
  var CycleChangeRequiresResetError = class extends InvalidRequestError {
44
47
  };
@@ -65,21 +68,34 @@ function errorFromResponse(args) {
65
68
  case 404:
66
69
  return new NotFoundError(args);
67
70
  case 409:
68
- return args.code === "idempotency_conflict" ? new IdempotencyConflictError(args) : new ConflictError(args);
71
+ return new ConflictError(args);
69
72
  case 422:
70
- return args.code === "cycle_change_requires_reset" ? new CycleChangeRequiresResetError(args) : new InvalidRequestError(args);
73
+ switch (args.code) {
74
+ case "cycle_change_requires_reset":
75
+ return new CycleChangeRequiresResetError(args);
76
+ case "invalid_idempotency_key":
77
+ return new InvalidIdempotencyKeyError(args);
78
+ case "reservation_mismatch":
79
+ return new ReservationMismatchError(args);
80
+ case "too_late":
81
+ return new TooLateError(args);
82
+ default:
83
+ return new InvalidRequestError(args);
84
+ }
71
85
  case 429:
72
86
  return new RateLimitError(args);
73
87
  default:
74
88
  return args.status >= 500 ? new ServerError(args) : new APIError(args);
75
89
  }
76
90
  }
77
- function existingEventId(body) {
91
+ function serverTime(body) {
78
92
  if (typeof body !== "object" || body === null) return void 0;
79
93
  const error = body.error;
80
94
  if (typeof error !== "object" || error === null) return void 0;
81
- const id = error.existing_event_id;
82
- return typeof id === "string" ? id : void 0;
95
+ const at = error.server_time;
96
+ if (typeof at !== "string") return void 0;
97
+ const parsed = new Date(at);
98
+ return Number.isNaN(parsed.getTime()) ? void 0 : parsed;
83
99
  }
84
100
 
85
101
  // src/client.ts
@@ -93,6 +109,21 @@ var Client = class {
93
109
  #timeout;
94
110
  #maxRetries;
95
111
  #fetch;
112
+ /**
113
+ * How far the engine's clock is ahead of this machine's, in milliseconds.
114
+ * Idempotency keys are minted here and the engine refuses one dated more
115
+ * than an hour from its own clock, so without this a device with a wrong
116
+ * clock would fail every call forever.
117
+ */
118
+ #clockOffset = 0;
119
+ /** The engine's clock, as well as this client knows it. */
120
+ now() {
121
+ return Date.now() + this.#clockOffset;
122
+ }
123
+ /** Off by up to one round trip, which a window in hours does not notice. */
124
+ observeServerTime(at) {
125
+ this.#clockOffset = at.getTime() - Date.now();
126
+ }
96
127
  constructor(options) {
97
128
  if (!options.apiKey) {
98
129
  throw new MeterbaseError(
@@ -637,6 +668,12 @@ var Meterbase = class {
637
668
  *
638
669
  * This is the gate, and the only call that ever refuses. Run it before the
639
670
  * work; record the work with `track` afterwards.
671
+ *
672
+ * Two gates answer it, and `allowed` needs both: the customer's capacity has
673
+ * to cover the quantity, and every rate-limit window in force has to admit
674
+ * it. `reason` names the one that said no. `rate_limits` reports each window
675
+ * whether or not it refused, so a caller can ease off as its headroom closes
676
+ * instead of discovering the ceiling by hitting it.
640
677
  */
641
678
  check(params, options) {
642
679
  return this.#client.request({
@@ -652,24 +689,70 @@ var Meterbase = class {
652
689
  * customer's capacity is recorded, drives `available` to 0, and the next
653
690
  * `check` says no. The answer is a receipt, not a verdict.
654
691
  *
655
- * `idempotency_key` is generated when omitted, so retrying this call — the
656
- * SDK's own retries included — replays the original rather than counting
657
- * the same work twice.
692
+ * `idempotency_key` is a UUIDv7, generated when omitted, so retrying this
693
+ * call — the SDK's own retries included — replays the original rather than
694
+ * counting the same work twice. The engine keeps that claim for an hour,
695
+ * which is how long a retry stays recognisable.
658
696
  */
659
697
  // `async` so that generating the key cannot throw synchronously out of a
660
698
  // method that otherwise only ever rejects: one call, one way to fail.
661
699
  async track(params, options) {
700
+ return this.#keyed(
701
+ IDEMPOTENCY_WINDOW_MS,
702
+ params.idempotency_key,
703
+ (key) => this.#client.request({
704
+ ...options,
705
+ method: "POST",
706
+ path: "/v1/usage/track",
707
+ // Fixed before the retry loop is entered: every attempt of this call
708
+ // carries the same key, which is what makes replaying it safe.
709
+ body: { ...params, idempotency_key: key },
710
+ idempotent: true
711
+ })
712
+ );
713
+ }
714
+ /**
715
+ * Holds capacity for work that cannot be done twice, or refuses. The same
716
+ * decision `check` makes, and then a hold on the capacity until the work
717
+ * is committed, released, or expires.
718
+ *
719
+ * The rule for choosing: can you afford to do the work twice? `check`. No?
720
+ * `reserve`. A reserve costs a transaction where a check costs a cached
721
+ * read, refusals included, so it is not a per-request gate on cheap work.
722
+ *
723
+ * Two concurrent reserves for the same customer and meter cannot both be
724
+ * granted the same capacity; that is the whole point, and it is what
725
+ * `check` → work → `track` could never promise.
726
+ */
727
+ async reserve(params, options) {
728
+ const response = await this.#keyed(
729
+ RESERVE_WINDOW_MS,
730
+ params.reservation_id,
731
+ (id) => this.#client.request({
732
+ ...options,
733
+ method: "POST",
734
+ path: "/v1/usage/reserve",
735
+ body: { ...params, reservation_id: id },
736
+ // A retry under the same id finds the hold it already made rather
737
+ // than making a second one.
738
+ idempotent: true
739
+ })
740
+ );
741
+ return this.#hold(params, response);
742
+ }
743
+ /**
744
+ * Gives a hold back: the work did not happen, so nothing is recorded.
745
+ *
746
+ * Throws `NotFoundError` when the id names nothing — committed, released
747
+ * already, or never made, which are one absence with one meaning. Prefer
748
+ * `hold.release()`, which treats that as the success it is.
749
+ */
750
+ release(params, options) {
662
751
  return this.#client.request({
663
752
  ...options,
664
753
  method: "POST",
665
- path: "/v1/usage/track",
666
- // Fixed here, once, before the retry loop is entered: every attempt of
667
- // this call carries the same key, which is what makes replaying it safe.
668
- body: {
669
- ...params,
670
- idempotency_key: params.idempotency_key ?? idempotencyKey()
671
- },
672
- idempotent: true
754
+ path: "/v1/usage/release",
755
+ body: params
673
756
  });
674
757
  }
675
758
  /** Reports which workspace this key acts for. */
@@ -680,19 +763,110 @@ var Meterbase = class {
680
763
  path: "/v1/whoami"
681
764
  });
682
765
  }
766
+ /**
767
+ * Sends a call that carries a caller-minted UUIDv7, minting one when the
768
+ * caller supplied none and re-minting once if this machine's clock put it
769
+ * outside the engine's window.
770
+ *
771
+ * A key the caller supplied is never re-minted: this cannot know whether it
772
+ * names work already recorded, and replacing it could count that work
773
+ * twice.
774
+ */
775
+ async #keyed(window, supplied, send) {
776
+ if (supplied !== void 0) return send(supplied);
777
+ const key = idempotencyKey(this.#client.now());
778
+ const startedAt = Date.now();
779
+ try {
780
+ return await send(key);
781
+ } catch (error) {
782
+ if (!(error instanceof TooLateError) || error.serverTime === void 0) {
783
+ throw error;
784
+ }
785
+ const elapsed = Date.now() - startedAt;
786
+ const serverAtStart = error.serverTime.getTime() - elapsed;
787
+ if (Math.abs(serverAtStart - mintedAtMs(key)) <= window) throw error;
788
+ this.#client.observeServerTime(error.serverTime);
789
+ return await send(idempotencyKey(this.#client.now()));
790
+ }
791
+ }
792
+ /** Puts `commit` and `release` on a granted hold; a refusal is left as it is. */
793
+ #hold(params, response) {
794
+ if (!response.allowed) return response;
795
+ const id = response.reservation_id;
796
+ const expiresAt = response.expires_at;
797
+ if (id === void 0 || expiresAt === void 0) {
798
+ throw new MeterbaseError(
799
+ "The engine granted a reservation without an id or an expiry, so there is no hold to commit or release. The capacity it set aside comes back on its own."
800
+ );
801
+ }
802
+ const expiresAtMs = Date.parse(expiresAt);
803
+ return {
804
+ ...response,
805
+ allowed: true,
806
+ reservation_id: id,
807
+ quantity: response.quantity ?? params.quantity ?? 1,
808
+ expires_at: expiresAt,
809
+ commit: (quantity, commitOptions) => {
810
+ if (Number.isFinite(expiresAtMs) && this.#client.now() > expiresAtMs && typeof globalThis.console?.warn === "function") {
811
+ globalThis.console.warn(
812
+ `Meterbase: committing reservation ${id} after it expired at ${expiresAt}. The event is still recorded, but the capacity was no longer held \u2014 raise expires_in_seconds for this work.`
813
+ );
814
+ }
815
+ return this.track(
816
+ {
817
+ customer_id: params.customer_id,
818
+ meter_id: params.meter_id,
819
+ quantity,
820
+ // The hold's own id, so the track commits it rather than
821
+ // recording a second event beside it.
822
+ idempotency_key: id
823
+ },
824
+ commitOptions
825
+ );
826
+ },
827
+ release: async (releaseOptions) => {
828
+ try {
829
+ return await this.release({ reservation_id: id }, releaseOptions);
830
+ } catch (error) {
831
+ if (error instanceof NotFoundError) return null;
832
+ throw error;
833
+ }
834
+ }
835
+ };
836
+ }
683
837
  };
684
- function idempotencyKey() {
838
+ var IDEMPOTENCY_WINDOW_MS = 60 * 60 * 1e3;
839
+ var RESERVE_WINDOW_MS = 30 * 60 * 1e3;
840
+ function mintedAtMs(key) {
841
+ return Number.parseInt(key.replace(/-/g, "").slice(0, 12), 16);
842
+ }
843
+ function idempotencyKey(atMs) {
685
844
  const webcrypto = globalThis.crypto;
686
- if (typeof webcrypto?.randomUUID === "function") {
687
- return webcrypto.randomUUID();
688
- }
689
- if (typeof webcrypto?.getRandomValues === "function") {
690
- const bytes = webcrypto.getRandomValues(new Uint8Array(16));
691
- return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
845
+ if (typeof webcrypto?.getRandomValues !== "function") {
846
+ throw new MeterbaseError(
847
+ "No crypto to generate an idempotency key with: pass `idempotency_key` yourself (a UUIDv7), or run somewhere `crypto.getRandomValues` exists."
848
+ );
692
849
  }
693
- throw new MeterbaseError(
694
- "No crypto to generate an idempotency key with: pass `idempotency_key` yourself, or run somewhere `crypto.getRandomValues` exists."
695
- );
850
+ const bytes = webcrypto.getRandomValues(new Uint8Array(16));
851
+ const ms = Math.max(0, Math.trunc(atMs));
852
+ const high = Math.floor(ms / 4294967296);
853
+ const low = ms >>> 0;
854
+ bytes[0] = high >>> 8 & 255;
855
+ bytes[1] = high & 255;
856
+ bytes[2] = low >>> 24 & 255;
857
+ bytes[3] = low >>> 16 & 255;
858
+ bytes[4] = low >>> 8 & 255;
859
+ bytes[5] = low & 255;
860
+ bytes[6] = bytes[6] & 15 | 112;
861
+ bytes[8] = bytes[8] & 63 | 128;
862
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
863
+ return [
864
+ hex.slice(0, 8),
865
+ hex.slice(8, 12),
866
+ hex.slice(12, 16),
867
+ hex.slice(16, 20),
868
+ hex.slice(20)
869
+ ].join("-");
696
870
  }
697
871
 
698
872
  exports.APIError = APIError;
@@ -700,15 +874,17 @@ exports.AuthenticationError = AuthenticationError;
700
874
  exports.ConflictError = ConflictError;
701
875
  exports.ConnectionError = ConnectionError;
702
876
  exports.CycleChangeRequiresResetError = CycleChangeRequiresResetError;
703
- exports.IdempotencyConflictError = IdempotencyConflictError;
877
+ exports.InvalidIdempotencyKeyError = InvalidIdempotencyKeyError;
704
878
  exports.InvalidRequestError = InvalidRequestError;
705
879
  exports.Meterbase = Meterbase;
706
880
  exports.MeterbaseError = MeterbaseError;
707
881
  exports.NotFoundError = NotFoundError;
708
882
  exports.PermissionDeniedError = PermissionDeniedError;
709
883
  exports.RateLimitError = RateLimitError;
884
+ exports.ReservationMismatchError = ReservationMismatchError;
710
885
  exports.ServerError = ServerError;
711
886
  exports.TimeoutError = TimeoutError;
887
+ exports.TooLateError = TooLateError;
712
888
  exports.errorFromResponse = errorFromResponse;
713
889
  //# sourceMappingURL=index.cjs.map
714
890
  //# sourceMappingURL=index.cjs.map