@clocknext/sdk 0.11.0 → 0.12.1

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.cjs CHANGED
@@ -1,17 +1,145 @@
1
1
  'use strict';
2
2
 
3
+ // src/errors.ts
4
+ var ClockNextError = class extends Error {
5
+ /** HTTP status, when the failure came from a server response. */
6
+ status;
7
+ /** Whether the SDK considers this failure worth retrying. */
8
+ retryable;
9
+ /**
10
+ * The server's machine-readable code, when it sent one. Branch on this in
11
+ * preference to `.message`, which is human copy and may be reworded.
12
+ */
13
+ code;
14
+ /**
15
+ * The idempotency key of the request that failed, for metering calls.
16
+ *
17
+ * This is what makes a dead-letter retry safe: re-send the failed signal with
18
+ * this SAME key and the server deduplicates it instead of billing twice. A
19
+ * fresh key would double-bill, so never generate one when replaying.
20
+ */
21
+ idempotencyKey;
22
+ constructor(message, opts = {}) {
23
+ super(message);
24
+ this.name = new.target.name;
25
+ this.status = opts.status;
26
+ this.retryable = opts.retryable ?? false;
27
+ this.code = opts.code;
28
+ this.idempotencyKey = opts.idempotencyKey;
29
+ Object.setPrototypeOf(this, new.target.prototype);
30
+ }
31
+ };
32
+ var AuthError = class extends ClockNextError {
33
+ };
34
+ var ForbiddenError = class extends ClockNextError {
35
+ };
36
+ var ValidationError = class extends ClockNextError {
37
+ };
38
+ var NotFoundError = class extends ClockNextError {
39
+ };
40
+ var PlanError = class extends ClockNextError {
41
+ };
42
+ var AllowanceError = class extends ClockNextError {
43
+ };
44
+ var ConflictError = class extends ClockNextError {
45
+ };
46
+ var RateLimitError = class extends ClockNextError {
47
+ retryAfterMs;
48
+ constructor(message, opts = {}) {
49
+ super(message, {
50
+ status: 429,
51
+ retryable: true,
52
+ idempotencyKey: opts.idempotencyKey
53
+ });
54
+ this.retryAfterMs = opts.retryAfterMs;
55
+ }
56
+ };
57
+ var ServerError = class extends ClockNextError {
58
+ };
59
+ var NetworkError = class extends ClockNextError {
60
+ constructor(message, opts = {}) {
61
+ super(message, { retryable: true, idempotencyKey: opts.idempotencyKey });
62
+ }
63
+ };
64
+ var FIELD_PATHS = [
65
+ [/\busage\.inputTokens\b/g, "tokens.input"],
66
+ [/\busage\.outputTokens\b/g, "tokens.output"],
67
+ [/\busage\.cacheTokens\b/g, "tokens.cache"],
68
+ [/\busage\.model\b/g, "model"],
69
+ // A missing/invalid `usage` block as a whole: the caller's equivalent is
70
+ // `tokens` (plus `model`, which the SDK hoists to the top level).
71
+ [/^usage:/, "tokens:"]
72
+ ];
73
+ function translateFieldPaths(message) {
74
+ let out = message;
75
+ for (const [pattern, replacement] of FIELD_PATHS) {
76
+ out = out.replace(pattern, replacement);
77
+ }
78
+ return out;
79
+ }
80
+ var ALLOWANCE_MESSAGE = /\binsufficient\b|\bno allowance left\b|\bno slots left\b|\boutcomes remaining\b|\btop up the (?:allowance|balance)\b/i;
81
+ function isAllowanceFailure(message, code) {
82
+ if (code) return code === "ALLOWANCE_EXHAUSTED";
83
+ return ALLOWANCE_MESSAGE.test(message);
84
+ }
85
+ var TRANSIENT_CONFLICT = /could not resolve the idempotent request/i;
86
+ function errorFromResponse(status, message, retryAfterMs, meta = {}) {
87
+ const raw = message || `Request failed with status ${status}.`;
88
+ const { code, idempotencyKey } = meta;
89
+ const opts = { status, code, idempotencyKey };
90
+ if (status === 400) {
91
+ return new ValidationError(translateFieldPaths(raw), opts);
92
+ }
93
+ if (status === 401) return new AuthError(raw, opts);
94
+ if (status === 403) return new ForbiddenError(raw, opts);
95
+ if (status === 404) return new NotFoundError(raw, opts);
96
+ if (status === 408) return new ClockNextError(raw, { ...opts, retryable: true });
97
+ if (status === 409) {
98
+ return new ConflictError(raw, {
99
+ ...opts,
100
+ retryable: code === "IDEMPOTENCY_UNRESOLVED" || TRANSIENT_CONFLICT.test(raw)
101
+ });
102
+ }
103
+ if (status === 422) {
104
+ return isAllowanceFailure(raw, code) ? new AllowanceError(raw, opts) : new PlanError(raw, opts);
105
+ }
106
+ if (status === 429) return new RateLimitError(raw, { retryAfterMs, idempotencyKey });
107
+ if (status >= 500) return new ServerError(raw, { ...opts, retryable: true });
108
+ return new ClockNextError(raw, opts);
109
+ }
110
+
3
111
  // src/config.ts
4
112
  var DEFAULT_BASE_URL = "https://payments.clocknext.com";
113
+ function defaultOnError(error, signal) {
114
+ const what = signal ? `${signal.type} signal` : "request";
115
+ const who = signal ? ` customer=${signal.customerId}` : "";
116
+ const key = signal?.idempotencyKey != null ? ` idempotencyKey=${signal.idempotencyKey}` : "";
117
+ console.error(
118
+ `[clocknext] ${what} permanently failed and was NOT recorded.${who}${key}`,
119
+ error
120
+ );
121
+ console.error(
122
+ "[clocknext] Pass `onError` to handle this yourself, or `onError: () => {}` to silence it."
123
+ );
124
+ }
125
+ function defaultOnDrop(signal, reason) {
126
+ if (reason === "send_failed") return;
127
+ const why = reason === "queue_full" ? "the local queue is full \u2014 raise `batch.maxQueueSize` or flush more often" : "the client is closed \u2014 this signal was tracked after `close()`";
128
+ console.error(
129
+ `[clocknext] dropped a ${signal.type} signal without sending it: ${why}.`,
130
+ `customer=${signal.customerId}`
131
+ );
132
+ }
5
133
  function num(value, fallback, min) {
6
134
  return typeof value === "number" && Number.isFinite(value) && value >= min ? value : fallback;
7
135
  }
8
136
  function resolveConfig(config) {
9
137
  if (!config.apiKey || typeof config.apiKey !== "string") {
10
- throw new Error("ClockNext: `apiKey` is required.");
138
+ throw new ValidationError("ClockNext: `apiKey` is required.");
11
139
  }
12
140
  const fetchImpl = config.fetch ?? (typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : void 0);
13
141
  if (!fetchImpl) {
14
- throw new Error(
142
+ throw new ValidationError(
15
143
  "ClockNext: no global `fetch` found. Use Node 18+, or pass a `fetch` implementation."
16
144
  );
17
145
  }
@@ -33,10 +161,11 @@ function resolveConfig(config) {
33
161
  },
34
162
  fetch: fetchImpl,
35
163
  logger: config.logger ?? {},
36
- onError: config.onError,
164
+ // Both default to loud. See the note above `defaultOnError`.
165
+ onError: config.onError ?? defaultOnError,
37
166
  onFlush: config.onFlush,
38
167
  onRetry: config.onRetry,
39
- onDrop: config.onDrop
168
+ onDrop: config.onDrop ?? defaultOnDrop
40
169
  };
41
170
  }
42
171
 
@@ -66,20 +195,25 @@ var Flusher = class {
66
195
  timer = null;
67
196
  draining = null;
68
197
  closed = false;
69
- /** Buffer one signal. Drops (with onDrop) when closed or the queue is full. */
198
+ /**
199
+ * Buffer one signal. Drops (with onDrop) when closed or the queue is full.
200
+ * Returns whether it was actually buffered, so `track()` can report a drop
201
+ * honestly instead of answering "buffered" for a signal it threw away.
202
+ */
70
203
  enqueue(item) {
71
204
  if (this.closed) {
72
- this.cfg.onDrop?.(item.signal, "send_failed");
73
- return;
205
+ this.cfg.onDrop?.(item.signal, "closed");
206
+ return false;
74
207
  }
75
208
  if (this.queue.size() >= this.cfg.batch.maxQueueSize) {
76
209
  this.cfg.logger.warn?.("[clocknext] queue full \u2014 dropping signal");
77
210
  this.cfg.onDrop?.(item.signal, "queue_full");
78
- return;
211
+ return false;
79
212
  }
80
213
  this.queue.push(item);
81
214
  if (this.queue.size() >= this.cfg.batch.maxSize) void this.flush();
82
215
  else this.arm();
216
+ return true;
83
217
  }
84
218
  /** Arm the interval timer if it isn't already running. */
85
219
  arm() {
@@ -90,21 +224,37 @@ var Flusher = class {
90
224
  }, this.cfg.batch.maxIntervalMs);
91
225
  this.timer.unref?.();
92
226
  }
93
- /**
94
- * Send everything currently buffered. Overlapping calls share the single
95
- * in-progress drain, so total in-flight sends never exceed `maxConcurrency`.
96
- */
97
- flush() {
227
+ /** Cancel the interval timer if one is armed. */
228
+ clearTimer() {
98
229
  if (this.timer) {
99
230
  clearTimeout(this.timer);
100
231
  this.timer = null;
101
232
  }
102
- if (!this.draining) {
103
- this.draining = this.drain().finally(() => {
104
- this.draining = null;
105
- });
106
- }
107
- return this.draining;
233
+ }
234
+ /**
235
+ * Send everything buffered at the moment this resolves.
236
+ *
237
+ * Overlapping calls CHAIN onto the in-flight drain rather than sharing it.
238
+ * Sharing looks equivalent and is not: a drain that has already passed its
239
+ * final `size() > 0` check cannot see a signal enqueued a moment later, so
240
+ * handing that same promise back would resolve `flush()` — and therefore
241
+ * `close()` — with the buffer non-empty. Since `close()` has already set
242
+ * `closed`, `arm()` will not re-arm, and the timer it did arm is `unref`'d,
243
+ * so the process is free to exit and that signal is simply lost. Chaining
244
+ * guarantees at least one full drain begins after this call.
245
+ *
246
+ * Concurrency is unaffected: drains run one after another, and each one
247
+ * sends at most `maxConcurrency` signals at a time.
248
+ */
249
+ flush() {
250
+ this.clearTimer();
251
+ const prior = this.draining ?? Promise.resolve();
252
+ const tracked = prior.catch(() => {
253
+ }).then(() => this.drain()).finally(() => {
254
+ if (this.draining === tracked) this.draining = null;
255
+ });
256
+ this.draining = tracked;
257
+ return tracked;
108
258
  }
109
259
  /** Drain the buffer in concurrency-bounded waves until it's empty. */
110
260
  async drain() {
@@ -125,7 +275,10 @@ var Flusher = class {
125
275
  path: item.path,
126
276
  body: item.body,
127
277
  // Best-effort metering opts into retries on transient failures.
128
- retry: true
278
+ retry: true,
279
+ // Rides onto any thrown error so `onError` can replay this exact
280
+ // signal without minting a new key (which would bill it twice).
281
+ idempotencyKey: typeof item.body.idempotencyKey === "string" ? item.body.idempotencyKey : void 0
129
282
  });
130
283
  return true;
131
284
  } catch (error) {
@@ -141,65 +294,10 @@ var Flusher = class {
141
294
  async close() {
142
295
  this.closed = true;
143
296
  await this.flush();
297
+ this.clearTimer();
144
298
  }
145
299
  };
146
300
 
147
- // src/errors.ts
148
- var ClockNextError = class extends Error {
149
- /** HTTP status, when the failure came from a server response. */
150
- status;
151
- /** Whether the SDK considers this failure worth retrying. */
152
- retryable;
153
- constructor(message, opts = {}) {
154
- super(message);
155
- this.name = new.target.name;
156
- this.status = opts.status;
157
- this.retryable = opts.retryable ?? false;
158
- Object.setPrototypeOf(this, new.target.prototype);
159
- }
160
- };
161
- var AuthError = class extends ClockNextError {
162
- };
163
- var ValidationError = class extends ClockNextError {
164
- };
165
- var NotFoundError = class extends ClockNextError {
166
- };
167
- var PlanError = class extends ClockNextError {
168
- };
169
- var AllowanceError = class extends ClockNextError {
170
- };
171
- var ConflictError = class extends ClockNextError {
172
- };
173
- var RateLimitError = class extends ClockNextError {
174
- retryAfterMs;
175
- constructor(message, opts = {}) {
176
- super(message, { status: 429, retryable: true });
177
- this.retryAfterMs = opts.retryAfterMs;
178
- }
179
- };
180
- var ServerError = class extends ClockNextError {
181
- };
182
- var NetworkError = class extends ClockNextError {
183
- constructor(message) {
184
- super(message, { retryable: true });
185
- }
186
- };
187
- function errorFromResponse(status, message, retryAfterMs) {
188
- const msg = message || `Request failed with status ${status}.`;
189
- const opts = { status };
190
- if (status === 400) return new ValidationError(msg, opts);
191
- if (status === 401) return new AuthError(msg, opts);
192
- if (status === 404) return new NotFoundError(msg, opts);
193
- if (status === 408) return new ClockNextError(msg, { ...opts, retryable: true });
194
- if (status === 409) return new ConflictError(msg, { ...opts, retryable: true });
195
- if (status === 422) {
196
- return /insufficient/i.test(msg) ? new AllowanceError(msg, opts) : new PlanError(msg, opts);
197
- }
198
- if (status === 429) return new RateLimitError(msg, { retryAfterMs });
199
- if (status >= 500) return new ServerError(msg, { ...opts, retryable: true });
200
- return new ClockNextError(msg, opts);
201
- }
202
-
203
301
  // src/retry.ts
204
302
  function isRetryable(error) {
205
303
  return error instanceof ClockNextError && error.retryable;
@@ -230,7 +328,7 @@ function sleep(ms) {
230
328
  }
231
329
 
232
330
  // src/http.ts
233
- var SDK_VERSION = "0.11.0" ;
331
+ var SDK_VERSION = "0.12.1" ;
234
332
  var Transport = class {
235
333
  constructor(cfg) {
236
334
  this.cfg = cfg;
@@ -274,7 +372,7 @@ var Transport = class {
274
372
  });
275
373
  } catch (err) {
276
374
  const message2 = controller.signal.aborted ? `Request to ${opts.path} timed out after ${this.cfg.timeoutMs}ms.` : `Network error calling ${opts.path}: ${err instanceof Error ? err.message : String(err)}`;
277
- throw new NetworkError(message2);
375
+ throw new NetworkError(message2, { idempotencyKey: opts.idempotencyKey });
278
376
  } finally {
279
377
  clearTimeout(timer);
280
378
  }
@@ -287,21 +385,30 @@ var Transport = class {
287
385
  }
288
386
  }
289
387
  const obj = json ?? {};
290
- const isNew = typeof obj === "object" && obj !== null && "statusDetail" in obj;
291
- if (isNew) {
388
+ const meta = { idempotencyKey: opts.idempotencyKey };
389
+ const isEnveloped = typeof obj === "object" && obj !== null && "statusDetail" in obj;
390
+ if (isEnveloped) {
292
391
  const detail = obj.statusDetail;
293
392
  if (res.ok && detail?.status === "SUCCESS") {
294
393
  return obj.result ?? {};
295
394
  }
296
395
  const message2 = detail?.message ?? `Request failed with status ${res.status}.`;
297
- throw errorFromResponse(res.status, message2, parseRetryAfter(res.headers));
396
+ throw errorFromResponse(res.status, message2, parseRetryAfter(res.headers), {
397
+ ...meta,
398
+ code: detail?.code
399
+ });
298
400
  }
299
401
  const envelope = obj;
300
402
  if (res.ok && envelope.ok !== false) {
301
403
  return envelope;
302
404
  }
303
405
  const message = envelope.error ?? text ?? `Request failed with status ${res.status}.`;
304
- throw errorFromResponse(res.status, message, parseRetryAfter(res.headers));
406
+ throw errorFromResponse(
407
+ res.status,
408
+ message,
409
+ parseRetryAfter(res.headers),
410
+ meta
411
+ );
305
412
  }
306
413
  headers(opts) {
307
414
  const h = {
@@ -325,6 +432,14 @@ var Transport = class {
325
432
  }
326
433
  };
327
434
 
435
+ // src/filters.ts
436
+ function activeQuery(active) {
437
+ return active === true ? "true" : void 0;
438
+ }
439
+ function filterByActive(rows, active) {
440
+ return active === false ? rows.filter((row) => !row.isActive) : rows;
441
+ }
442
+
328
443
  // src/resources/composites.ts
329
444
  var Composites = class {
330
445
  constructor(transport) {
@@ -333,16 +448,16 @@ var Composites = class {
333
448
  transport;
334
449
  /** List composites. Pass `{ active: true }` for only the ones a signal can
335
450
  * still be tagged with — the default list INCLUDES archived ones, which
336
- * keep every row they already own. */
451
+ * keep every row they already own.
452
+ * `{ active: false }` returns only archived ones; omitting it returns
453
+ * both. See `activeQuery` — the API itself only supports `true`. */
337
454
  async list(params = {}) {
338
455
  const res = await this.transport.request({
339
456
  method: "GET",
340
457
  path: "/api/v1/composites",
341
- query: {
342
- active: params.active === void 0 ? void 0 : String(params.active)
343
- }
458
+ query: { active: activeQuery(params.active) }
344
459
  });
345
- return res.composites;
460
+ return filterByActive(res.composites, params.active);
346
461
  }
347
462
  /** Create a composite. `POST /api/v1/composites`. */
348
463
  async create(input) {
@@ -361,14 +476,16 @@ var Credits = class {
361
476
  this.transport = transport;
362
477
  }
363
478
  transport;
364
- /** List credit types. Pass `{ active: true }` to only return active ones. */
479
+ /** List credit types. Pass `{ active: true }` to only return active ones.
480
+ * `{ active: false }` returns only archived ones; omitting it returns
481
+ * both. See `activeQuery` — the API itself only supports `true`. */
365
482
  async list(params = {}) {
366
483
  const res = await this.transport.request({
367
484
  method: "GET",
368
485
  path: "/api/v1/credits",
369
- query: { active: params.active === void 0 ? void 0 : String(params.active) }
486
+ query: { active: activeQuery(params.active) }
370
487
  });
371
- return res.credits;
488
+ return filterByActive(res.credits, params.active);
372
489
  }
373
490
  /** Create a credit type; returns the created credit. `POST /api/v1/credits`. */
374
491
  async create(input) {
@@ -659,14 +776,16 @@ var Outcomes = class {
659
776
  this.transport = transport;
660
777
  }
661
778
  transport;
662
- /** List outcome types. Pass `{ active: true }` to only return active ones. */
779
+ /** List outcome types. Pass `{ active: true }` to only return active ones.
780
+ * `{ active: false }` returns only archived ones; omitting it returns
781
+ * both. See `activeQuery` — the API itself only supports `true`. */
663
782
  async list(params = {}) {
664
783
  const res = await this.transport.request({
665
784
  method: "GET",
666
785
  path: "/api/v1/outcomes",
667
- query: { active: params.active === void 0 ? void 0 : String(params.active) }
786
+ query: { active: activeQuery(params.active) }
668
787
  });
669
- return res.outcomes;
788
+ return filterByActive(res.outcomes, params.active);
670
789
  }
671
790
  /** Create an outcome type with its steps. `POST /api/v1/outcomes`. */
672
791
  async create(input) {
@@ -742,14 +861,16 @@ var Plans = class {
742
861
  this.transport = transport;
743
862
  }
744
863
  transport;
745
- /** List plans, oldest-first. Pass `{ active: true }` to only return active plans. */
864
+ /** List plans, oldest-first. Pass `{ active: true }` to only return active plans.
865
+ * `{ active: false }` returns only archived ones; omitting it returns
866
+ * both. See `activeQuery` — the API itself only supports `true`. */
746
867
  async list(params = {}) {
747
868
  const res = await this.transport.request({
748
869
  method: "GET",
749
870
  path: "/api/v1/plans",
750
- query: { active: params.active === void 0 ? void 0 : String(params.active) }
871
+ query: { active: activeQuery(params.active) }
751
872
  });
752
- return res.plans;
873
+ return filterByActive(res.plans, params.active);
753
874
  }
754
875
  /** Create a plan. `POST /api/v1/plans`. */
755
876
  async create(input) {
@@ -947,22 +1068,27 @@ var Signals = class {
947
1068
  */
948
1069
  async track(signal, opts = {}) {
949
1070
  const body = signalToWire(signal);
950
- if (body.idempotencyKey == null) body.idempotencyKey = newKey();
1071
+ const idempotencyKey = typeof body.idempotencyKey === "string" ? body.idempotencyKey : newKey();
1072
+ body.idempotencyKey = idempotencyKey;
1073
+ const tracked = { ...signal, idempotencyKey };
951
1074
  const path = SIGNAL_PATHS[signal.type];
952
1075
  const sendNow = opts.wait || this.cfg.mode === "sync";
953
1076
  if (!sendNow) {
954
- this.flusher.enqueue({ path, body, signal });
955
- return { queued: true };
1077
+ const buffered = this.flusher.enqueue({ path, body, signal: tracked });
1078
+ return buffered ? { status: "buffered", queued: true } : { status: "dropped", queued: false };
956
1079
  }
957
1080
  const res = await this.transport.request({
958
1081
  method: "POST",
959
1082
  path,
960
1083
  body,
961
1084
  // Best-effort metering opts into retries on transient failures.
962
- retry: true
1085
+ retry: true,
1086
+ idempotencyKey
963
1087
  });
1088
+ const queued = res.queued === true;
964
1089
  return {
965
- queued: res.queued === true,
1090
+ status: queued ? "accepted" : "recorded",
1091
+ queued,
966
1092
  usageLog: res.usageLog ?? null,
967
1093
  ...handles(res)
968
1094
  };
@@ -1009,9 +1135,11 @@ var Signals = class {
1009
1135
  * Branch on `queued`.
1010
1136
  */
1011
1137
  async unit(input) {
1138
+ const idempotencyKey = input.idempotencyKey ?? newKey();
1012
1139
  const res = await this.transport.request({
1013
1140
  method: "POST",
1014
1141
  path: UNIT_PATH,
1142
+ idempotencyKey,
1015
1143
  body: {
1016
1144
  customerId: input.customerId,
1017
1145
  agentKey: input.agentKey,
@@ -1025,13 +1153,15 @@ var Signals = class {
1025
1153
  // times. The route gained idempotency precisely to stop that; not
1026
1154
  // using it by default would leave the double-charge reachable from the
1027
1155
  // client instead of the server.
1028
- idempotencyKey: input.idempotencyKey ?? newKey()
1156
+ idempotencyKey
1029
1157
  },
1030
1158
  // Best-effort metering opts into retries on transient failures.
1031
1159
  retry: true
1032
1160
  });
1161
+ const queued = res.queued === true;
1033
1162
  return {
1034
- queued: res.queued === true,
1163
+ status: queued ? "accepted" : "recorded",
1164
+ queued,
1035
1165
  unitUsage: res.unitUsage ?? null,
1036
1166
  ...handles(res)
1037
1167
  };
@@ -1047,9 +1177,13 @@ var Signals = class {
1047
1177
  * arrive in any order and the occurrence stays open and unbilled until you
1048
1178
  * say otherwise. Put it on the line before your own `return`.
1049
1179
  *
1050
- * An OUTCOME never needs this — its own `complete: true` closes its run and
1051
- * the matching occurrence together. Credits and units have no "done" signal
1052
- * of their own, so this is theirs.
1180
+ * EVERY composite needs this, outcome-backed ones included. An outcome's own
1181
+ * `complete: true` closes its RUN and nothing else — it does not close the
1182
+ * composite occurrence the run was tagged into, whatever mix of credits,
1183
+ * outcomes and units that composite wraps. (Earlier versions of this SDK
1184
+ * documented the opposite. Believing it means the occurrence stays ONGOING
1185
+ * forever and is never billed, so if you tag outcome signals with a
1186
+ * `composite`, you must still call this.)
1053
1187
  *
1054
1188
  * Closing twice is safe: the second call answers `ALREADY_COMPLETED` and
1055
1189
  * cannot bill again. Calling it EARLY is also safe — before every entitlement
@@ -1058,9 +1192,11 @@ var Signals = class {
1058
1192
  * rather than failing it.
1059
1193
  */
1060
1194
  async completeComposite(input) {
1195
+ const idempotencyKey = input.idempotencyKey ?? newKey();
1061
1196
  const res = await this.transport.request({
1062
1197
  method: "POST",
1063
1198
  path: COMPOSITE_COMPLETE_PATH,
1199
+ idempotencyKey,
1064
1200
  body: {
1065
1201
  customerId: input.customerId,
1066
1202
  composite: { [input.composite.ref]: input.composite.value },
@@ -1069,12 +1205,14 @@ var Signals = class {
1069
1205
  // no-op — but that safety comes from occurrence-level dedup, not from
1070
1206
  // idempotency, and an un-keyed replay still writes a redundant inbox
1071
1207
  // row. Keying it collapses the retry into the one attempt it is.
1072
- idempotencyKey: input.idempotencyKey ?? newKey()
1208
+ idempotencyKey
1073
1209
  },
1074
1210
  retry: true
1075
1211
  });
1212
+ const queued = res.queued === true;
1076
1213
  return {
1077
- queued: res.queued === true,
1214
+ delivery: queued ? "accepted" : "recorded",
1215
+ queued,
1078
1216
  ...res.status != null ? { status: res.status } : {},
1079
1217
  ...res.composite !== void 0 ? { composite: res.composite } : {},
1080
1218
  ...res.value !== void 0 ? { value: res.value } : {},
@@ -1102,14 +1240,16 @@ var Units = class {
1102
1240
  this.transport = transport;
1103
1241
  }
1104
1242
  transport;
1105
- /** List unit types. Pass `{ active: true }` to only return active ones. */
1243
+ /** List unit types. Pass `{ active: true }` to only return active ones.
1244
+ * `{ active: false }` returns only archived ones; omitting it returns
1245
+ * both. See `activeQuery` — the API itself only supports `true`. */
1106
1246
  async list(params = {}) {
1107
1247
  const res = await this.transport.request({
1108
1248
  method: "GET",
1109
1249
  path: "/api/v1/units-catalog",
1110
- query: { active: params.active === void 0 ? void 0 : String(params.active) }
1250
+ query: { active: activeQuery(params.active) }
1111
1251
  });
1112
- return res.units;
1252
+ return filterByActive(res.units, params.active);
1113
1253
  }
1114
1254
  /** Create a unit type. `POST /api/v1/units-catalog`. */
1115
1255
  async create(input) {
@@ -1201,16 +1341,16 @@ var Workspace = class {
1201
1341
  });
1202
1342
  }
1203
1343
  /** The org's enabled models with live prices. `{ active: true }` returns only
1204
- * the models that can be metered right now. */
1344
+ * the models that can be metered right now.
1345
+ * `{ active: false }` returns only archived ones; omitting it returns
1346
+ * both. See `activeQuery` — the API itself only supports `true`. */
1205
1347
  async models(params = {}) {
1206
1348
  const res = await this.transport.request({
1207
1349
  method: "GET",
1208
1350
  path: "/api/v1/models",
1209
- query: {
1210
- active: params.active === void 0 ? void 0 : String(params.active)
1211
- }
1351
+ query: { active: activeQuery(params.active) }
1212
1352
  });
1213
- return res.models;
1353
+ return filterByActive(res.models, params.active);
1214
1354
  }
1215
1355
  };
1216
1356
 
@@ -1286,6 +1426,7 @@ exports.AuthError = AuthError;
1286
1426
  exports.ClockNext = ClockNext;
1287
1427
  exports.ClockNextError = ClockNextError;
1288
1428
  exports.ConflictError = ConflictError;
1429
+ exports.ForbiddenError = ForbiddenError;
1289
1430
  exports.NetworkError = NetworkError;
1290
1431
  exports.NotFoundError = NotFoundError;
1291
1432
  exports.PlanError = PlanError;