@clocknext/sdk 0.1.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 +161 -0
- package/dist/index.cjs +596 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +580 -0
- package/dist/index.d.ts +580 -0
- package/dist/index.js +582 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/config.ts
|
|
4
|
+
var DEFAULT_BASE_URL = "https://app.clocknext.com";
|
|
5
|
+
function num(value, fallback, min) {
|
|
6
|
+
return typeof value === "number" && Number.isFinite(value) && value >= min ? value : fallback;
|
|
7
|
+
}
|
|
8
|
+
function resolveConfig(config) {
|
|
9
|
+
if (!config.apiKey || typeof config.apiKey !== "string") {
|
|
10
|
+
throw new Error("ClockNext: `apiKey` is required.");
|
|
11
|
+
}
|
|
12
|
+
const fetchImpl = config.fetch ?? (typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : void 0);
|
|
13
|
+
if (!fetchImpl) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"ClockNext: no global `fetch` found. Use Node 18+, or pass a `fetch` implementation."
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
apiKey: config.apiKey,
|
|
20
|
+
baseUrl: (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
|
|
21
|
+
mode: config.mode ?? "async",
|
|
22
|
+
timeoutMs: num(config.timeoutMs, 1e4, 1),
|
|
23
|
+
batch: {
|
|
24
|
+
maxSize: num(config.batch?.maxSize, 20, 1),
|
|
25
|
+
maxIntervalMs: num(config.batch?.maxIntervalMs, 2e3, 0),
|
|
26
|
+
maxConcurrency: num(config.batch?.maxConcurrency, 5, 1),
|
|
27
|
+
maxQueueSize: num(config.batch?.maxQueueSize, 1e4, 1)
|
|
28
|
+
},
|
|
29
|
+
retry: {
|
|
30
|
+
maxAttempts: num(config.retry?.maxAttempts, 5, 1),
|
|
31
|
+
baseDelayMs: num(config.retry?.baseDelayMs, 200, 0),
|
|
32
|
+
maxDelayMs: num(config.retry?.maxDelayMs, 1e4, 0)
|
|
33
|
+
},
|
|
34
|
+
fetch: fetchImpl,
|
|
35
|
+
logger: config.logger ?? {},
|
|
36
|
+
onError: config.onError,
|
|
37
|
+
onFlush: config.onFlush,
|
|
38
|
+
onRetry: config.onRetry,
|
|
39
|
+
onDrop: config.onDrop
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/queue.ts
|
|
44
|
+
var InMemoryQueue = class {
|
|
45
|
+
items = [];
|
|
46
|
+
push(item) {
|
|
47
|
+
this.items.push(item);
|
|
48
|
+
}
|
|
49
|
+
take(n) {
|
|
50
|
+
return this.items.splice(0, n);
|
|
51
|
+
}
|
|
52
|
+
size() {
|
|
53
|
+
return this.items.length;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// src/flusher.ts
|
|
58
|
+
var Flusher = class {
|
|
59
|
+
constructor(transport, cfg) {
|
|
60
|
+
this.transport = transport;
|
|
61
|
+
this.cfg = cfg;
|
|
62
|
+
}
|
|
63
|
+
transport;
|
|
64
|
+
cfg;
|
|
65
|
+
queue = new InMemoryQueue();
|
|
66
|
+
timer = null;
|
|
67
|
+
draining = null;
|
|
68
|
+
closed = false;
|
|
69
|
+
/** Buffer one signal. Drops (with onDrop) when closed or the queue is full. */
|
|
70
|
+
enqueue(item) {
|
|
71
|
+
if (this.closed) {
|
|
72
|
+
this.cfg.onDrop?.(item.signal, "send_failed");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (this.queue.size() >= this.cfg.batch.maxQueueSize) {
|
|
76
|
+
this.cfg.logger.warn?.("[clocknext] queue full \u2014 dropping signal");
|
|
77
|
+
this.cfg.onDrop?.(item.signal, "queue_full");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.queue.push(item);
|
|
81
|
+
if (this.queue.size() >= this.cfg.batch.maxSize) void this.flush();
|
|
82
|
+
else this.arm();
|
|
83
|
+
}
|
|
84
|
+
/** Arm the interval timer if it isn't already running. */
|
|
85
|
+
arm() {
|
|
86
|
+
if (this.timer || this.closed) return;
|
|
87
|
+
this.timer = setTimeout(() => {
|
|
88
|
+
this.timer = null;
|
|
89
|
+
void this.flush();
|
|
90
|
+
}, this.cfg.batch.maxIntervalMs);
|
|
91
|
+
this.timer.unref?.();
|
|
92
|
+
}
|
|
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() {
|
|
98
|
+
if (this.timer) {
|
|
99
|
+
clearTimeout(this.timer);
|
|
100
|
+
this.timer = null;
|
|
101
|
+
}
|
|
102
|
+
if (!this.draining) {
|
|
103
|
+
this.draining = this.drain().finally(() => {
|
|
104
|
+
this.draining = null;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return this.draining;
|
|
108
|
+
}
|
|
109
|
+
/** Drain the buffer in concurrency-bounded waves until it's empty. */
|
|
110
|
+
async drain() {
|
|
111
|
+
let sent = 0;
|
|
112
|
+
while (this.queue.size() > 0) {
|
|
113
|
+
const wave = this.queue.take(this.cfg.batch.maxConcurrency);
|
|
114
|
+
const results = await Promise.all(wave.map((item) => this.send(item)));
|
|
115
|
+
sent += results.filter(Boolean).length;
|
|
116
|
+
}
|
|
117
|
+
if (sent > 0) this.cfg.onFlush?.(sent);
|
|
118
|
+
}
|
|
119
|
+
/** Send one item; never throws — failures surface via onError/onDrop. */
|
|
120
|
+
async send(item) {
|
|
121
|
+
try {
|
|
122
|
+
await this.transport.request({
|
|
123
|
+
method: "POST",
|
|
124
|
+
path: "/api/v1/usage",
|
|
125
|
+
body: item.body,
|
|
126
|
+
idempotencyKey: item.idempotencyKey
|
|
127
|
+
});
|
|
128
|
+
return true;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
this.cfg.onError?.(error, item.signal);
|
|
131
|
+
this.cfg.onDrop?.(item.signal, "send_failed");
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
get size() {
|
|
136
|
+
return this.queue.size();
|
|
137
|
+
}
|
|
138
|
+
/** Flush remaining signals, then refuse further enqueues. */
|
|
139
|
+
async close() {
|
|
140
|
+
this.closed = true;
|
|
141
|
+
await this.flush();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// src/errors.ts
|
|
146
|
+
var ClockNextError = class extends Error {
|
|
147
|
+
/** HTTP status, when the failure came from a server response. */
|
|
148
|
+
status;
|
|
149
|
+
/** The idempotency key of the offending signal, when applicable. */
|
|
150
|
+
idempotencyKey;
|
|
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.idempotencyKey = opts.idempotencyKey;
|
|
158
|
+
this.retryable = opts.retryable ?? false;
|
|
159
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
var AuthError = class extends ClockNextError {
|
|
163
|
+
};
|
|
164
|
+
var ValidationError = class extends ClockNextError {
|
|
165
|
+
};
|
|
166
|
+
var NotFoundError = class extends ClockNextError {
|
|
167
|
+
};
|
|
168
|
+
var PlanError = class extends ClockNextError {
|
|
169
|
+
};
|
|
170
|
+
var AllowanceError = class extends ClockNextError {
|
|
171
|
+
};
|
|
172
|
+
var ConflictError = class extends ClockNextError {
|
|
173
|
+
};
|
|
174
|
+
var RateLimitError = class extends ClockNextError {
|
|
175
|
+
retryAfterMs;
|
|
176
|
+
constructor(message, opts = {}) {
|
|
177
|
+
super(message, { status: 429, idempotencyKey: opts.idempotencyKey, retryable: true });
|
|
178
|
+
this.retryAfterMs = opts.retryAfterMs;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
var ServerError = class extends ClockNextError {
|
|
182
|
+
};
|
|
183
|
+
var NetworkError = class extends ClockNextError {
|
|
184
|
+
constructor(message, opts = {}) {
|
|
185
|
+
super(message, { idempotencyKey: opts.idempotencyKey, retryable: true });
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
function errorFromResponse(status, message, idempotencyKey, retryAfterMs) {
|
|
189
|
+
const msg = message || `Request failed with status ${status}.`;
|
|
190
|
+
const opts = { status, idempotencyKey };
|
|
191
|
+
if (status === 400) return new ValidationError(msg, opts);
|
|
192
|
+
if (status === 401) return new AuthError(msg, opts);
|
|
193
|
+
if (status === 404) return new NotFoundError(msg, opts);
|
|
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, { idempotencyKey, retryAfterMs });
|
|
199
|
+
if (status >= 500) return new ServerError(msg, { ...opts, retryable: true });
|
|
200
|
+
return new ClockNextError(msg, opts);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/retry.ts
|
|
204
|
+
function isRetryable(error) {
|
|
205
|
+
return error instanceof ClockNextError && error.retryable;
|
|
206
|
+
}
|
|
207
|
+
function parseRetryAfter(headers) {
|
|
208
|
+
const ms = headers.get("retry-after-ms");
|
|
209
|
+
if (ms) {
|
|
210
|
+
const n = Number(ms);
|
|
211
|
+
if (Number.isFinite(n) && n >= 0) return n;
|
|
212
|
+
}
|
|
213
|
+
const ra = headers.get("retry-after");
|
|
214
|
+
if (ra) {
|
|
215
|
+
const secs = Number(ra);
|
|
216
|
+
if (Number.isFinite(secs)) return Math.max(0, secs * 1e3);
|
|
217
|
+
const date = Date.parse(ra);
|
|
218
|
+
if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
|
|
219
|
+
}
|
|
220
|
+
return void 0;
|
|
221
|
+
}
|
|
222
|
+
function computeBackoff(attempt, opts, retryAfterMs, rand = Math.random) {
|
|
223
|
+
if (retryAfterMs != null) return Math.min(retryAfterMs, opts.maxDelayMs);
|
|
224
|
+
const raw = opts.baseDelayMs * Math.pow(2, Math.max(0, attempt - 1));
|
|
225
|
+
const capped = Math.min(raw, opts.maxDelayMs);
|
|
226
|
+
return Math.round(capped * (0.5 + rand() * 0.5));
|
|
227
|
+
}
|
|
228
|
+
function sleep(ms) {
|
|
229
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/http.ts
|
|
233
|
+
var SDK_VERSION = "0.1.0";
|
|
234
|
+
var Transport = class {
|
|
235
|
+
constructor(cfg) {
|
|
236
|
+
this.cfg = cfg;
|
|
237
|
+
}
|
|
238
|
+
cfg;
|
|
239
|
+
async request(opts) {
|
|
240
|
+
const url = this.buildUrl(opts.path, opts.query);
|
|
241
|
+
const replaySafe = opts.method === "GET" || opts.method === "DELETE" || opts.idempotencyKey != null;
|
|
242
|
+
const maxAttempts = opts.retry === false || !replaySafe ? 1 : this.cfg.retry.maxAttempts;
|
|
243
|
+
let attempt = 0;
|
|
244
|
+
while (true) {
|
|
245
|
+
attempt++;
|
|
246
|
+
try {
|
|
247
|
+
return await this.once(url, opts);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
const canRetry = attempt < maxAttempts && isRetryable(error);
|
|
250
|
+
if (!canRetry) throw error;
|
|
251
|
+
const retryAfterMs = error instanceof RateLimitError ? error.retryAfterMs : void 0;
|
|
252
|
+
const delayMs = computeBackoff(attempt, this.cfg.retry, retryAfterMs);
|
|
253
|
+
this.cfg.onRetry?.({ attempt, delayMs, error });
|
|
254
|
+
this.cfg.logger.debug?.(
|
|
255
|
+
`[clocknext] retry ${attempt}/${maxAttempts - 1} in ${delayMs}ms`,
|
|
256
|
+
error instanceof Error ? error.message : error
|
|
257
|
+
);
|
|
258
|
+
await sleep(delayMs);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** One attempt: fetch + timeout + envelope/status handling. */
|
|
263
|
+
async once(url, opts) {
|
|
264
|
+
const controller = new AbortController();
|
|
265
|
+
const timer = setTimeout(() => controller.abort(), this.cfg.timeoutMs);
|
|
266
|
+
let res;
|
|
267
|
+
try {
|
|
268
|
+
res = await this.cfg.fetch(url, {
|
|
269
|
+
method: opts.method,
|
|
270
|
+
headers: this.headers(opts),
|
|
271
|
+
body: opts.body === void 0 ? void 0 : JSON.stringify(opts.body),
|
|
272
|
+
signal: controller.signal
|
|
273
|
+
});
|
|
274
|
+
} catch (err) {
|
|
275
|
+
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)}`;
|
|
276
|
+
throw new NetworkError(message2, { idempotencyKey: opts.idempotencyKey });
|
|
277
|
+
} finally {
|
|
278
|
+
clearTimeout(timer);
|
|
279
|
+
}
|
|
280
|
+
const text = await res.text();
|
|
281
|
+
let json = void 0;
|
|
282
|
+
if (text) {
|
|
283
|
+
try {
|
|
284
|
+
json = JSON.parse(text);
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const obj = json ?? {};
|
|
289
|
+
const isNew = typeof obj === "object" && obj !== null && "statusDetail" in obj;
|
|
290
|
+
if (isNew) {
|
|
291
|
+
const detail = obj.statusDetail;
|
|
292
|
+
if (res.ok && detail?.status === "SUCCESS") {
|
|
293
|
+
return obj.result ?? {};
|
|
294
|
+
}
|
|
295
|
+
const message2 = detail?.message ?? `Request failed with status ${res.status}.`;
|
|
296
|
+
throw errorFromResponse(
|
|
297
|
+
res.status,
|
|
298
|
+
message2,
|
|
299
|
+
opts.idempotencyKey,
|
|
300
|
+
parseRetryAfter(res.headers)
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
const envelope = obj;
|
|
304
|
+
if (res.ok && envelope.ok !== false) {
|
|
305
|
+
return envelope;
|
|
306
|
+
}
|
|
307
|
+
const message = envelope.error ?? text ?? `Request failed with status ${res.status}.`;
|
|
308
|
+
throw errorFromResponse(
|
|
309
|
+
res.status,
|
|
310
|
+
message,
|
|
311
|
+
opts.idempotencyKey,
|
|
312
|
+
parseRetryAfter(res.headers)
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
headers(opts) {
|
|
316
|
+
const h = {
|
|
317
|
+
Authorization: `Bearer ${this.cfg.apiKey}`,
|
|
318
|
+
"User-Agent": `clocknext-sdk-js/${SDK_VERSION}`
|
|
319
|
+
};
|
|
320
|
+
if (opts.body !== void 0) h["Content-Type"] = "application/json";
|
|
321
|
+
if (opts.idempotencyKey) h["Idempotency-Key"] = opts.idempotencyKey;
|
|
322
|
+
return h;
|
|
323
|
+
}
|
|
324
|
+
buildUrl(path, query) {
|
|
325
|
+
let url = `${this.cfg.baseUrl}${path}`;
|
|
326
|
+
if (query) {
|
|
327
|
+
const params = new URLSearchParams();
|
|
328
|
+
for (const [k, v] of Object.entries(query)) {
|
|
329
|
+
if (v !== void 0 && v !== "") params.set(k, String(v));
|
|
330
|
+
}
|
|
331
|
+
const qs = params.toString();
|
|
332
|
+
if (qs) url += `?${qs}`;
|
|
333
|
+
}
|
|
334
|
+
return url;
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// src/resources/customers.ts
|
|
339
|
+
var Customers = class {
|
|
340
|
+
constructor(transport) {
|
|
341
|
+
this.transport = transport;
|
|
342
|
+
}
|
|
343
|
+
transport;
|
|
344
|
+
/** Create a customer. `POST /api/v1/customers`. */
|
|
345
|
+
async create(input) {
|
|
346
|
+
const res = await this.transport.request({
|
|
347
|
+
method: "POST",
|
|
348
|
+
path: "/api/v1/customers",
|
|
349
|
+
body: input
|
|
350
|
+
});
|
|
351
|
+
return res.customer;
|
|
352
|
+
}
|
|
353
|
+
/** List customers, most-recent first (cursor-paginated). `GET /api/v1/customers`. */
|
|
354
|
+
async list(params = {}) {
|
|
355
|
+
const res = await this.transport.request({
|
|
356
|
+
method: "GET",
|
|
357
|
+
path: "/api/v1/customers",
|
|
358
|
+
query: { limit: params.limit, q: params.q, cursor: params.cursor }
|
|
359
|
+
});
|
|
360
|
+
return { customers: res.customers, nextCursor: res.nextCursor };
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Async iterator over ALL customers, transparently following the cursor.
|
|
364
|
+
* `for await (const c of cnk.customers.iterate()) { … }`
|
|
365
|
+
*/
|
|
366
|
+
async *iterate(params = {}) {
|
|
367
|
+
let cursor;
|
|
368
|
+
do {
|
|
369
|
+
const page = await this.list({ ...params, cursor });
|
|
370
|
+
for (const c of page.customers) yield c;
|
|
371
|
+
cursor = page.nextCursor ?? void 0;
|
|
372
|
+
} while (cursor);
|
|
373
|
+
}
|
|
374
|
+
/** Fetch one customer. `GET /api/v1/customers/:id`. */
|
|
375
|
+
async get(id) {
|
|
376
|
+
const res = await this.transport.request({
|
|
377
|
+
method: "GET",
|
|
378
|
+
path: `/api/v1/customers/${encodeURIComponent(id)}`
|
|
379
|
+
});
|
|
380
|
+
return res.customer;
|
|
381
|
+
}
|
|
382
|
+
/** Update a customer (partial). `PATCH /api/v1/customers/:id`. */
|
|
383
|
+
async update(id, input) {
|
|
384
|
+
const res = await this.transport.request({
|
|
385
|
+
method: "PATCH",
|
|
386
|
+
path: `/api/v1/customers/${encodeURIComponent(id)}`,
|
|
387
|
+
body: input
|
|
388
|
+
});
|
|
389
|
+
return res.customer;
|
|
390
|
+
}
|
|
391
|
+
/** Delete a customer (members, invitations, usage logs cascade). */
|
|
392
|
+
async delete(id) {
|
|
393
|
+
await this.transport.request({
|
|
394
|
+
method: "DELETE",
|
|
395
|
+
path: `/api/v1/customers/${encodeURIComponent(id)}`
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
/** A customer's usage history. `GET /api/v1/customers/:id/usage`. */
|
|
399
|
+
async usage(id, params = {}) {
|
|
400
|
+
const res = await this.transport.request({
|
|
401
|
+
method: "GET",
|
|
402
|
+
path: `/api/v1/customers/${encodeURIComponent(id)}/usage`,
|
|
403
|
+
query: { limit: params.limit }
|
|
404
|
+
});
|
|
405
|
+
return { customer: res.customer, totals: res.totals, logs: res.logs };
|
|
406
|
+
}
|
|
407
|
+
/** Wallet / credit / outcome / unit balances. `GET …/:id/balances`. */
|
|
408
|
+
balances(id) {
|
|
409
|
+
return this.transport.request({
|
|
410
|
+
method: "GET",
|
|
411
|
+
path: `/api/v1/customers/${encodeURIComponent(id)}/balances`
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
/** The customer's current plan. `GET …/:id/plan`. */
|
|
415
|
+
plan(id) {
|
|
416
|
+
return this.transport.request({
|
|
417
|
+
method: "GET",
|
|
418
|
+
path: `/api/v1/customers/${encodeURIComponent(id)}/plan`
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
/** Revenue / cost / profit / margin over a window. `GET …/:id/revenue`. */
|
|
422
|
+
revenue(id, params = {}) {
|
|
423
|
+
return this.transport.request({
|
|
424
|
+
method: "GET",
|
|
425
|
+
path: `/api/v1/customers/${encodeURIComponent(id)}/revenue`,
|
|
426
|
+
query: { range: params.range }
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// src/resources/portal.ts
|
|
432
|
+
var Portal = class {
|
|
433
|
+
constructor(transport) {
|
|
434
|
+
this.transport = transport;
|
|
435
|
+
}
|
|
436
|
+
transport;
|
|
437
|
+
async createToken(input) {
|
|
438
|
+
const res = await this.transport.request({
|
|
439
|
+
method: "POST",
|
|
440
|
+
path: "/api/v1/portal/token",
|
|
441
|
+
body: {
|
|
442
|
+
customerId: input.customerId,
|
|
443
|
+
ttlSeconds: input.ttlSeconds,
|
|
444
|
+
memberEmail: input.memberEmail
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
return {
|
|
448
|
+
token: res.token,
|
|
449
|
+
expiresAt: res.expiresAt,
|
|
450
|
+
expiresIn: res.expiresIn
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// src/idempotency.ts
|
|
456
|
+
function newIdempotencyKey() {
|
|
457
|
+
const webcrypto = globalThis.crypto;
|
|
458
|
+
if (webcrypto?.randomUUID) return webcrypto.randomUUID();
|
|
459
|
+
const b = new Uint8Array(16);
|
|
460
|
+
if (webcrypto?.getRandomValues) {
|
|
461
|
+
webcrypto.getRandomValues(b);
|
|
462
|
+
} else {
|
|
463
|
+
for (let i = 0; i < 16; i++) b[i] = Math.floor(Math.random() * 256);
|
|
464
|
+
}
|
|
465
|
+
b[6] = b[6] & 15 | 64;
|
|
466
|
+
b[8] = b[8] & 63 | 128;
|
|
467
|
+
const hex = Array.from(b, (n) => n.toString(16).padStart(2, "0")).join("");
|
|
468
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/resources/signals.ts
|
|
472
|
+
function signalToWire(signal) {
|
|
473
|
+
const body = {
|
|
474
|
+
customerId: signal.customerId,
|
|
475
|
+
type: signal.type,
|
|
476
|
+
model: signal.model
|
|
477
|
+
};
|
|
478
|
+
if (signal.member) body.member = signal.member;
|
|
479
|
+
if (signal.status) body.status = signal.status;
|
|
480
|
+
if (signal.custom) body.custom = signal.custom;
|
|
481
|
+
if (signal.idempotencyKey) body.idempotencyKey = signal.idempotencyKey;
|
|
482
|
+
const t = signal.tokens;
|
|
483
|
+
if (t) {
|
|
484
|
+
if (t.input != null) body.inputTokens = t.input;
|
|
485
|
+
if (t.output != null) body.outputTokens = t.output;
|
|
486
|
+
if (t.cache != null) body.cacheTokens = t.cache;
|
|
487
|
+
}
|
|
488
|
+
if (signal.type === "credit" || signal.type === "outcome") {
|
|
489
|
+
body.key = signal.key;
|
|
490
|
+
}
|
|
491
|
+
return body;
|
|
492
|
+
}
|
|
493
|
+
var Signals = class {
|
|
494
|
+
constructor(transport, flusher, cfg) {
|
|
495
|
+
this.transport = transport;
|
|
496
|
+
this.flusher = flusher;
|
|
497
|
+
this.cfg = cfg;
|
|
498
|
+
}
|
|
499
|
+
transport;
|
|
500
|
+
flusher;
|
|
501
|
+
cfg;
|
|
502
|
+
async track(signal, opts = {}) {
|
|
503
|
+
const idempotencyKey = signal.idempotencyKey ?? newIdempotencyKey();
|
|
504
|
+
const body = signalToWire({ ...signal, idempotencyKey });
|
|
505
|
+
const sendNow = opts.wait || this.cfg.mode === "sync";
|
|
506
|
+
if (!sendNow) {
|
|
507
|
+
this.flusher.enqueue({ body, idempotencyKey, signal });
|
|
508
|
+
return { idempotencyKey, queued: true };
|
|
509
|
+
}
|
|
510
|
+
const res = await this.transport.request({
|
|
511
|
+
method: "POST",
|
|
512
|
+
path: "/api/v1/usage",
|
|
513
|
+
body,
|
|
514
|
+
idempotencyKey
|
|
515
|
+
});
|
|
516
|
+
return {
|
|
517
|
+
idempotencyKey,
|
|
518
|
+
queued: false,
|
|
519
|
+
usageLog: res.usageLog ?? null
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
/** Meter a named credit. */
|
|
523
|
+
credit(input, opts) {
|
|
524
|
+
return this.track({ ...input, type: "credit" }, opts);
|
|
525
|
+
}
|
|
526
|
+
/** Debit the customer's wallet at model cost. */
|
|
527
|
+
wallet(input, opts) {
|
|
528
|
+
return this.track({ ...input, type: "wallet" }, opts);
|
|
529
|
+
}
|
|
530
|
+
/** Advance one step of an outcome workflow. */
|
|
531
|
+
outcome(input, opts) {
|
|
532
|
+
return this.track({ ...input, type: "outcome" }, opts);
|
|
533
|
+
}
|
|
534
|
+
/** Recent usage logs + totals for a customer. `GET /api/v1/usage`. */
|
|
535
|
+
async list(params) {
|
|
536
|
+
const res = await this.transport.request({
|
|
537
|
+
method: "GET",
|
|
538
|
+
path: "/api/v1/usage",
|
|
539
|
+
query: { customerId: params.customerId, limit: params.limit }
|
|
540
|
+
});
|
|
541
|
+
return { customer: res.customer, totals: res.totals, logs: res.logs };
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
// src/client.ts
|
|
546
|
+
var ClockNext = class {
|
|
547
|
+
/** Record + read usage signals. */
|
|
548
|
+
signals;
|
|
549
|
+
/** Manage customers and read their insights. */
|
|
550
|
+
customers;
|
|
551
|
+
/** Mint customer-portal embed tokens. */
|
|
552
|
+
portal;
|
|
553
|
+
cfg;
|
|
554
|
+
flusher;
|
|
555
|
+
constructor(config) {
|
|
556
|
+
this.cfg = resolveConfig(config);
|
|
557
|
+
if (typeof window !== "undefined") {
|
|
558
|
+
this.cfg.logger.warn?.(
|
|
559
|
+
"[clocknext] Running in a browser exposes your secret API key. Use this SDK only on a server/backend."
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
const transport = new Transport(this.cfg);
|
|
563
|
+
this.flusher = new Flusher(transport, this.cfg);
|
|
564
|
+
this.signals = new Signals(transport, this.flusher, this.cfg);
|
|
565
|
+
this.customers = new Customers(transport);
|
|
566
|
+
this.portal = new Portal(transport);
|
|
567
|
+
}
|
|
568
|
+
/** Number of signals currently buffered (async mode). */
|
|
569
|
+
get pending() {
|
|
570
|
+
return this.flusher.size;
|
|
571
|
+
}
|
|
572
|
+
/** Force-send all buffered signals now. Await before a serverless return. */
|
|
573
|
+
flush() {
|
|
574
|
+
return this.flusher.flush();
|
|
575
|
+
}
|
|
576
|
+
/** Flush and stop the background timer. Call on graceful shutdown. */
|
|
577
|
+
close() {
|
|
578
|
+
return this.flusher.close();
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
exports.AllowanceError = AllowanceError;
|
|
583
|
+
exports.AuthError = AuthError;
|
|
584
|
+
exports.ClockNext = ClockNext;
|
|
585
|
+
exports.ClockNextError = ClockNextError;
|
|
586
|
+
exports.ConflictError = ConflictError;
|
|
587
|
+
exports.NetworkError = NetworkError;
|
|
588
|
+
exports.NotFoundError = NotFoundError;
|
|
589
|
+
exports.PlanError = PlanError;
|
|
590
|
+
exports.RateLimitError = RateLimitError;
|
|
591
|
+
exports.ServerError = ServerError;
|
|
592
|
+
exports.ValidationError = ValidationError;
|
|
593
|
+
exports.newIdempotencyKey = newIdempotencyKey;
|
|
594
|
+
exports.signalToWire = signalToWire;
|
|
595
|
+
//# sourceMappingURL=index.cjs.map
|
|
596
|
+
//# sourceMappingURL=index.cjs.map
|