@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/README.md +518 -264
- package/dist/index.cjs +257 -138
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +221 -56
- package/dist/index.d.ts +221 -56
- package/dist/index.js +257 -139
- package/dist/index.js.map +1 -1
- package/examples/node-basic.ts +93 -0
- package/examples/serverless-flush.ts +47 -0
- package/package.json +3 -1
- package/src/client.ts +107 -0
- package/src/config.ts +112 -0
- package/src/errors.ts +215 -0
- package/src/filters.ts +33 -0
- package/src/flusher.ts +144 -0
- package/src/http.ts +178 -0
- package/src/idempotency.ts +33 -0
- package/src/index.ts +165 -0
- package/src/queue.ts +48 -0
- package/src/resources/composites.ts +47 -0
- package/src/resources/credits.ts +76 -0
- package/src/resources/customers.ts +298 -0
- package/src/resources/invoices.ts +51 -0
- package/src/resources/outcomes.ts +82 -0
- package/src/resources/payments.ts +30 -0
- package/src/resources/plans.ts +76 -0
- package/src/resources/portal.ts +31 -0
- package/src/resources/purchases.ts +75 -0
- package/src/resources/signals.ts +386 -0
- package/src/resources/units.ts +76 -0
- package/src/resources/webhooks.ts +37 -0
- package/src/resources/workspace.ts +33 -0
- package/src/retry.ts +56 -0
- package/src/types.ts +1599 -0
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
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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, "
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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.
|
|
331
|
+
var SDK_VERSION = "0.12.0" ;
|
|
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
|
|
291
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
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,48 +1068,31 @@ var Signals = class {
|
|
|
947
1068
|
*/
|
|
948
1069
|
async track(signal, opts = {}) {
|
|
949
1070
|
const body = signalToWire(signal);
|
|
950
|
-
|
|
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
|
-
|
|
1090
|
+
status: queued ? "accepted" : "recorded",
|
|
1091
|
+
queued,
|
|
966
1092
|
usageLog: res.usageLog ?? null,
|
|
967
1093
|
...handles(res)
|
|
968
1094
|
};
|
|
969
1095
|
}
|
|
970
|
-
/**
|
|
971
|
-
* Validate + price a signal WITHOUT recording it (server `dryRun`). Returns
|
|
972
|
-
* the projected usage log so an integration can preflight before sending real
|
|
973
|
-
* traffic — no inbox row, wallet / outcome writes, or webhooks fire.
|
|
974
|
-
*
|
|
975
|
-
* Only the model-priced kinds support this. A dry run is not offered for unit
|
|
976
|
-
* reports or composite completions, and the server REJECTS the flag on those
|
|
977
|
-
* routes rather than accepting and ignoring it — which is why there is no
|
|
978
|
-
* dry-run path through `unit()` or `completeComposite()`.
|
|
979
|
-
*/
|
|
980
|
-
async verify(signal) {
|
|
981
|
-
const body = signalToWire(signal);
|
|
982
|
-
body.dryRun = true;
|
|
983
|
-
const res = await this.transport.request({
|
|
984
|
-
method: "POST",
|
|
985
|
-
path: SIGNAL_PATHS[signal.type],
|
|
986
|
-
body,
|
|
987
|
-
// A dry run writes nothing, so it is always safe to retry.
|
|
988
|
-
retry: true
|
|
989
|
-
});
|
|
990
|
-
return res.usageLog ?? null;
|
|
991
|
-
}
|
|
992
1096
|
/** Meter a named credit. `POST /api/v1/signal/credit`. */
|
|
993
1097
|
credit(input, opts) {
|
|
994
1098
|
return this.track({ ...input, type: "credit" }, opts);
|
|
@@ -1031,9 +1135,11 @@ var Signals = class {
|
|
|
1031
1135
|
* Branch on `queued`.
|
|
1032
1136
|
*/
|
|
1033
1137
|
async unit(input) {
|
|
1138
|
+
const idempotencyKey = input.idempotencyKey ?? newKey();
|
|
1034
1139
|
const res = await this.transport.request({
|
|
1035
1140
|
method: "POST",
|
|
1036
1141
|
path: UNIT_PATH,
|
|
1142
|
+
idempotencyKey,
|
|
1037
1143
|
body: {
|
|
1038
1144
|
customerId: input.customerId,
|
|
1039
1145
|
agentKey: input.agentKey,
|
|
@@ -1047,13 +1153,15 @@ var Signals = class {
|
|
|
1047
1153
|
// times. The route gained idempotency precisely to stop that; not
|
|
1048
1154
|
// using it by default would leave the double-charge reachable from the
|
|
1049
1155
|
// client instead of the server.
|
|
1050
|
-
idempotencyKey
|
|
1156
|
+
idempotencyKey
|
|
1051
1157
|
},
|
|
1052
1158
|
// Best-effort metering opts into retries on transient failures.
|
|
1053
1159
|
retry: true
|
|
1054
1160
|
});
|
|
1161
|
+
const queued = res.queued === true;
|
|
1055
1162
|
return {
|
|
1056
|
-
|
|
1163
|
+
status: queued ? "accepted" : "recorded",
|
|
1164
|
+
queued,
|
|
1057
1165
|
unitUsage: res.unitUsage ?? null,
|
|
1058
1166
|
...handles(res)
|
|
1059
1167
|
};
|
|
@@ -1069,9 +1177,13 @@ var Signals = class {
|
|
|
1069
1177
|
* arrive in any order and the occurrence stays open and unbilled until you
|
|
1070
1178
|
* say otherwise. Put it on the line before your own `return`.
|
|
1071
1179
|
*
|
|
1072
|
-
*
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
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.)
|
|
1075
1187
|
*
|
|
1076
1188
|
* Closing twice is safe: the second call answers `ALREADY_COMPLETED` and
|
|
1077
1189
|
* cannot bill again. Calling it EARLY is also safe — before every entitlement
|
|
@@ -1080,9 +1192,11 @@ var Signals = class {
|
|
|
1080
1192
|
* rather than failing it.
|
|
1081
1193
|
*/
|
|
1082
1194
|
async completeComposite(input) {
|
|
1195
|
+
const idempotencyKey = input.idempotencyKey ?? newKey();
|
|
1083
1196
|
const res = await this.transport.request({
|
|
1084
1197
|
method: "POST",
|
|
1085
1198
|
path: COMPOSITE_COMPLETE_PATH,
|
|
1199
|
+
idempotencyKey,
|
|
1086
1200
|
body: {
|
|
1087
1201
|
customerId: input.customerId,
|
|
1088
1202
|
composite: { [input.composite.ref]: input.composite.value },
|
|
@@ -1091,12 +1205,14 @@ var Signals = class {
|
|
|
1091
1205
|
// no-op — but that safety comes from occurrence-level dedup, not from
|
|
1092
1206
|
// idempotency, and an un-keyed replay still writes a redundant inbox
|
|
1093
1207
|
// row. Keying it collapses the retry into the one attempt it is.
|
|
1094
|
-
idempotencyKey
|
|
1208
|
+
idempotencyKey
|
|
1095
1209
|
},
|
|
1096
1210
|
retry: true
|
|
1097
1211
|
});
|
|
1212
|
+
const queued = res.queued === true;
|
|
1098
1213
|
return {
|
|
1099
|
-
|
|
1214
|
+
delivery: queued ? "accepted" : "recorded",
|
|
1215
|
+
queued,
|
|
1100
1216
|
...res.status != null ? { status: res.status } : {},
|
|
1101
1217
|
...res.composite !== void 0 ? { composite: res.composite } : {},
|
|
1102
1218
|
...res.value !== void 0 ? { value: res.value } : {},
|
|
@@ -1124,14 +1240,16 @@ var Units = class {
|
|
|
1124
1240
|
this.transport = transport;
|
|
1125
1241
|
}
|
|
1126
1242
|
transport;
|
|
1127
|
-
/** 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`. */
|
|
1128
1246
|
async list(params = {}) {
|
|
1129
1247
|
const res = await this.transport.request({
|
|
1130
1248
|
method: "GET",
|
|
1131
1249
|
path: "/api/v1/units-catalog",
|
|
1132
|
-
query: { active:
|
|
1250
|
+
query: { active: activeQuery(params.active) }
|
|
1133
1251
|
});
|
|
1134
|
-
return res.units;
|
|
1252
|
+
return filterByActive(res.units, params.active);
|
|
1135
1253
|
}
|
|
1136
1254
|
/** Create a unit type. `POST /api/v1/units-catalog`. */
|
|
1137
1255
|
async create(input) {
|
|
@@ -1223,16 +1341,16 @@ var Workspace = class {
|
|
|
1223
1341
|
});
|
|
1224
1342
|
}
|
|
1225
1343
|
/** The org's enabled models with live prices. `{ active: true }` returns only
|
|
1226
|
-
* 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`. */
|
|
1227
1347
|
async models(params = {}) {
|
|
1228
1348
|
const res = await this.transport.request({
|
|
1229
1349
|
method: "GET",
|
|
1230
1350
|
path: "/api/v1/models",
|
|
1231
|
-
query: {
|
|
1232
|
-
active: params.active === void 0 ? void 0 : String(params.active)
|
|
1233
|
-
}
|
|
1351
|
+
query: { active: activeQuery(params.active) }
|
|
1234
1352
|
});
|
|
1235
|
-
return res.models;
|
|
1353
|
+
return filterByActive(res.models, params.active);
|
|
1236
1354
|
}
|
|
1237
1355
|
};
|
|
1238
1356
|
|
|
@@ -1308,6 +1426,7 @@ exports.AuthError = AuthError;
|
|
|
1308
1426
|
exports.ClockNext = ClockNext;
|
|
1309
1427
|
exports.ClockNextError = ClockNextError;
|
|
1310
1428
|
exports.ConflictError = ConflictError;
|
|
1429
|
+
exports.ForbiddenError = ForbiddenError;
|
|
1311
1430
|
exports.NetworkError = NetworkError;
|
|
1312
1431
|
exports.NotFoundError = NotFoundError;
|
|
1313
1432
|
exports.PlanError = PlanError;
|