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