@clocknext/sdk 0.3.0 → 0.5.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 +12 -9
- package/dist/index.cjs +114 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +388 -88
- package/dist/index.d.ts +388 -88
- package/dist/index.js +114 -44
- package/dist/index.js.map +1 -1
- package/package.json +13 -5
package/README.md
CHANGED
|
@@ -90,12 +90,16 @@ const cnk = new ClockNext({ apiKey, mode: "sync" });
|
|
|
90
90
|
- **Retries:** transient failures (network, timeout, `408/409/429/5xx`) are
|
|
91
91
|
retried with exponential backoff + equal jitter, honoring `Retry-After`.
|
|
92
92
|
Deterministic `4xx` (`400/401/404/422`) are never retried.
|
|
93
|
-
- **Replay safety:** reads (`GET`)
|
|
94
|
-
`
|
|
95
|
-
|
|
96
|
-
Other non-idempotent writes — e.g. `customers.create`, `plans.create`,
|
|
93
|
+
- **Replay safety:** reads (`GET`) and `DELETE` are retried automatically. Metering
|
|
94
|
+
signals (`signals.*`) opt into retries too — made safe by the idempotency key
|
|
95
|
+
below. Other non-idempotent writes — e.g. `customers.create`, `plans.create`,
|
|
97
96
|
`purchases.create` — are *not* auto-retried, so a transient blip can never
|
|
98
97
|
create a duplicate.
|
|
98
|
+
- **Idempotency:** every metering signal carries an idempotency key — auto-
|
|
99
|
+
generated per signal, or pass your own via `idempotencyKey` on the signal —
|
|
100
|
+
reused across all retries, so a retried send is **deduplicated by the server**
|
|
101
|
+
instead of double-counted. Pass a stable key (e.g. your own event id) to also
|
|
102
|
+
dedup across process restarts or at-least-once redelivery.
|
|
99
103
|
- **Flushing:** call `await cnk.flush()` before a serverless return, and
|
|
100
104
|
`await cnk.close()` on graceful shutdown to drain the buffer.
|
|
101
105
|
- **Durability:** the async buffer is **in-memory**. A signal already accepted
|
|
@@ -140,9 +144,8 @@ await cnk.customers.removeMember(c.id, members[0].id);
|
|
|
140
144
|
await cnk.customers.wallet(c.id, { limit: 50 }); // { balanceUsd, transactions }
|
|
141
145
|
await cnk.customers.addWalletEntry(c.id, { type: "CREDIT", amount: 25 }); // top up (DEBIT to spend)
|
|
142
146
|
|
|
143
|
-
// billing config
|
|
144
|
-
await cnk.customers.
|
|
145
|
-
await cnk.customers.setCreditTypes(c.id, ["cr_1", "cr_2"]);
|
|
147
|
+
// billing config — currency is set through update()
|
|
148
|
+
await cnk.customers.update(c.id, { currencyCode: "EUR" });
|
|
146
149
|
|
|
147
150
|
// grant / claw back balances (delta is signed)
|
|
148
151
|
await cnk.customers.adjustCredit(c.id, { creditId: "cr_1", delta: 100 });
|
|
@@ -200,7 +203,7 @@ await cnk.outcomes.create({
|
|
|
200
203
|
});
|
|
201
204
|
|
|
202
205
|
// unit type — a quantity-priced meter (FLAT / SLAB / VOLUME)
|
|
203
|
-
await cnk.units.create({ name: "Seats", pricingType: "FLAT", flatPrice: 5 });
|
|
206
|
+
await cnk.units.create({ name: "Seats", agentKey: "seats", pricingType: "FLAT", flatPrice: 5 });
|
|
204
207
|
```
|
|
205
208
|
|
|
206
209
|
### Unit metering
|
|
@@ -208,7 +211,7 @@ await cnk.units.create({ name: "Seats", pricingType: "FLAT", flatPrice: 5 });
|
|
|
208
211
|
Record a unit event (no tokens) and read a customer's unit balances:
|
|
209
212
|
|
|
210
213
|
```ts
|
|
211
|
-
await cnk.signals.unit({ customerId: c.id,
|
|
214
|
+
await cnk.signals.unit({ customerId: c.id, agentKey: "seats" });
|
|
212
215
|
await cnk.signals.unitUsage({ customerId: c.id });
|
|
213
216
|
```
|
|
214
217
|
|
package/dist/index.cjs
CHANGED
|
@@ -229,7 +229,7 @@ function sleep(ms) {
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
// src/http.ts
|
|
232
|
-
var SDK_VERSION = "0.
|
|
232
|
+
var SDK_VERSION = "0.5.0" ;
|
|
233
233
|
var Transport = class {
|
|
234
234
|
constructor(cfg) {
|
|
235
235
|
this.cfg = cfg;
|
|
@@ -339,13 +339,14 @@ var Credits = class {
|
|
|
339
339
|
});
|
|
340
340
|
return res.credits;
|
|
341
341
|
}
|
|
342
|
-
/** Create a credit type. `POST /api/v1/credits`. */
|
|
342
|
+
/** Create a credit type; returns the created credit. `POST /api/v1/credits`. */
|
|
343
343
|
async create(input) {
|
|
344
|
-
await this.transport.request({
|
|
344
|
+
const res = await this.transport.request({
|
|
345
345
|
method: "POST",
|
|
346
346
|
path: "/api/v1/credits",
|
|
347
347
|
body: input
|
|
348
348
|
});
|
|
349
|
+
return res.credit;
|
|
349
350
|
}
|
|
350
351
|
/** Fetch one credit type with usage stats. `GET /api/v1/credits/:id`. */
|
|
351
352
|
async get(id) {
|
|
@@ -355,13 +356,14 @@ var Credits = class {
|
|
|
355
356
|
});
|
|
356
357
|
return res.credit;
|
|
357
358
|
}
|
|
358
|
-
/** Update a credit type's definition. `PATCH /api/v1/credits/:id`. */
|
|
359
|
+
/** Update a credit type's definition; returns the updated credit. `PATCH /api/v1/credits/:id`. */
|
|
359
360
|
async update(id, input) {
|
|
360
|
-
await this.transport.request({
|
|
361
|
+
const res = await this.transport.request({
|
|
361
362
|
method: "PATCH",
|
|
362
363
|
path: `/api/v1/credits/${encodeURIComponent(id)}`,
|
|
363
364
|
body: input
|
|
364
365
|
});
|
|
366
|
+
return res.credit;
|
|
365
367
|
}
|
|
366
368
|
/** Activate or deactivate a credit type. */
|
|
367
369
|
async setActive(id, isActive) {
|
|
@@ -441,12 +443,12 @@ var Customers = class {
|
|
|
441
443
|
path: `/api/v1/customers/${encodeURIComponent(id)}`
|
|
442
444
|
});
|
|
443
445
|
}
|
|
444
|
-
/** A customer's usage history. `GET /api/v1/
|
|
446
|
+
/** A customer's usage history. `GET /api/v1/usage?customerId=`. */
|
|
445
447
|
async usage(id, params = {}) {
|
|
446
448
|
const res = await this.transport.request({
|
|
447
449
|
method: "GET",
|
|
448
|
-
path: `/api/v1/
|
|
449
|
-
query: { limit: params.limit }
|
|
450
|
+
path: `/api/v1/usage`,
|
|
451
|
+
query: { customerId: id, limit: params.limit }
|
|
450
452
|
});
|
|
451
453
|
return { customer: res.customer, totals: res.totals, logs: res.logs };
|
|
452
454
|
}
|
|
@@ -457,6 +459,37 @@ var Customers = class {
|
|
|
457
459
|
path: `/api/v1/customers/${encodeURIComponent(id)}/balances`
|
|
458
460
|
});
|
|
459
461
|
}
|
|
462
|
+
/**
|
|
463
|
+
* One credit balance for a customer, matched by `creditId` or `creditName`;
|
|
464
|
+
* `null` when the customer has no plan or no matching credit. Convenience
|
|
465
|
+
* over `balances()` + `Array.find` — it fetches the FULL balances under the
|
|
466
|
+
* hood (there is no server-side scoped read), so prefer `balances()` when you
|
|
467
|
+
* need several rows from the same customer.
|
|
468
|
+
*/
|
|
469
|
+
async creditBalance(customerId, match) {
|
|
470
|
+
const { credits } = await this.balances(customerId);
|
|
471
|
+
return credits.find(
|
|
472
|
+
(c) => match.creditId != null && c.creditId === match.creditId || match.creditName != null && c.creditName === match.creditName
|
|
473
|
+
) ?? null;
|
|
474
|
+
}
|
|
475
|
+
/** One outcome balance for a customer, matched by `outcomeId` or
|
|
476
|
+
* `outcomeName`; `null` when absent. See {@link creditBalance} for the
|
|
477
|
+
* fetch-then-filter caveat. */
|
|
478
|
+
async outcomeBalance(customerId, match) {
|
|
479
|
+
const { outcomes } = await this.balances(customerId);
|
|
480
|
+
return outcomes.find(
|
|
481
|
+
(o) => match.outcomeId != null && o.outcomeId === match.outcomeId || match.outcomeName != null && o.outcomeName === match.outcomeName
|
|
482
|
+
) ?? null;
|
|
483
|
+
}
|
|
484
|
+
/** One unit balance for a customer, matched by `unitId` or `unitName`;
|
|
485
|
+
* `null` when absent. See {@link creditBalance} for the fetch-then-filter
|
|
486
|
+
* caveat. */
|
|
487
|
+
async unitBalance(customerId, match) {
|
|
488
|
+
const { units } = await this.balances(customerId);
|
|
489
|
+
return units.find(
|
|
490
|
+
(u) => match.unitId != null && u.unitId === match.unitId || match.unitName != null && u.unitName === match.unitName
|
|
491
|
+
) ?? null;
|
|
492
|
+
}
|
|
460
493
|
/** The customer's current plan. `GET …/:id/plan`. */
|
|
461
494
|
plan(id) {
|
|
462
495
|
return this.transport.request({
|
|
@@ -464,14 +497,6 @@ var Customers = class {
|
|
|
464
497
|
path: `/api/v1/customers/${encodeURIComponent(id)}/plan`
|
|
465
498
|
});
|
|
466
499
|
}
|
|
467
|
-
/** Revenue / cost / profit / margin over a window. `GET …/:id/revenue`. */
|
|
468
|
-
revenue(id, params = {}) {
|
|
469
|
-
return this.transport.request({
|
|
470
|
-
method: "GET",
|
|
471
|
-
path: `/api/v1/customers/${encodeURIComponent(id)}/revenue`,
|
|
472
|
-
query: { range: params.range }
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
500
|
// --- Members -------------------------------------------------------------
|
|
476
501
|
/** List a customer's members. `GET …/:id/members`. */
|
|
477
502
|
async listMembers(id) {
|
|
@@ -526,22 +551,9 @@ var Customers = class {
|
|
|
526
551
|
body: input
|
|
527
552
|
});
|
|
528
553
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
method: "PUT",
|
|
533
|
-
path: `/api/v1/customers/${encodeURIComponent(id)}/currency`,
|
|
534
|
-
body: { currencyCode }
|
|
535
|
-
});
|
|
536
|
-
}
|
|
537
|
-
/** Set which credit types the customer is billed against. `PUT …/:id/credits`. */
|
|
538
|
-
async setCreditTypes(id, creditIds) {
|
|
539
|
-
await this.transport.request({
|
|
540
|
-
method: "PUT",
|
|
541
|
-
path: `/api/v1/customers/${encodeURIComponent(id)}/credits`,
|
|
542
|
-
body: { creditIds }
|
|
543
|
-
});
|
|
544
|
-
}
|
|
554
|
+
// Note: a customer's currency is set through `update(id, { currencyCode })`
|
|
555
|
+
// (PATCH …/:id) — there is no separate currency endpoint. Credit-type
|
|
556
|
+
// assignment has no public API.
|
|
545
557
|
// --- Balance adjustments -------------------------------------------------
|
|
546
558
|
/** Grant or claw back a credit balance. `POST …/:id/credits/adjust`. */
|
|
547
559
|
async adjustCredit(id, input) {
|
|
@@ -650,13 +662,14 @@ var Outcomes = class {
|
|
|
650
662
|
});
|
|
651
663
|
return res.outcome;
|
|
652
664
|
}
|
|
653
|
-
/** Activate or deactivate an outcome type. */
|
|
665
|
+
/** Activate or deactivate an outcome type; returns the updated outcome. */
|
|
654
666
|
async setActive(id, isActive) {
|
|
655
|
-
await this.transport.request({
|
|
667
|
+
const res = await this.transport.request({
|
|
656
668
|
method: "PATCH",
|
|
657
669
|
path: `/api/v1/outcomes/${encodeURIComponent(id)}`,
|
|
658
670
|
body: { isActive }
|
|
659
671
|
});
|
|
672
|
+
return res.outcome;
|
|
660
673
|
}
|
|
661
674
|
/** Delete an outcome type. */
|
|
662
675
|
async delete(id) {
|
|
@@ -830,6 +843,17 @@ var Purchases = class {
|
|
|
830
843
|
}
|
|
831
844
|
};
|
|
832
845
|
|
|
846
|
+
// src/idempotency.ts
|
|
847
|
+
function newKey() {
|
|
848
|
+
const c = globalThis.crypto;
|
|
849
|
+
if (typeof c?.randomUUID === "function") return c.randomUUID();
|
|
850
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (ch) => {
|
|
851
|
+
const r = Math.random() * 16 | 0;
|
|
852
|
+
const v = ch === "x" ? r : r & 3 | 8;
|
|
853
|
+
return v.toString(16);
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
|
|
833
857
|
// src/resources/signals.ts
|
|
834
858
|
function signalToWire(signal) {
|
|
835
859
|
const body = {
|
|
@@ -838,17 +862,15 @@ function signalToWire(signal) {
|
|
|
838
862
|
model: signal.model
|
|
839
863
|
};
|
|
840
864
|
if (signal.member) body.member = signal.member;
|
|
841
|
-
if (signal.status) body.status = signal.status;
|
|
842
865
|
if (signal.custom) body.custom = signal.custom;
|
|
843
866
|
const t = signal.tokens;
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
if (t.cache != null) body.cacheTokens = t.cache;
|
|
848
|
-
}
|
|
867
|
+
body.inputTokens = t.input;
|
|
868
|
+
body.outputTokens = t.output;
|
|
869
|
+
if (t.cache != null) body.cacheTokens = t.cache;
|
|
849
870
|
if (signal.type === "credit" || signal.type === "outcome") {
|
|
850
871
|
body.agentKey = signal.agentKey;
|
|
851
872
|
}
|
|
873
|
+
if (signal.idempotencyKey) body.idempotencyKey = signal.idempotencyKey;
|
|
852
874
|
return body;
|
|
853
875
|
}
|
|
854
876
|
var Signals = class {
|
|
@@ -862,6 +884,7 @@ var Signals = class {
|
|
|
862
884
|
cfg;
|
|
863
885
|
async track(signal, opts = {}) {
|
|
864
886
|
const body = signalToWire(signal);
|
|
887
|
+
if (body.idempotencyKey == null) body.idempotencyKey = newKey();
|
|
865
888
|
const sendNow = opts.wait || this.cfg.mode === "sync";
|
|
866
889
|
if (!sendNow) {
|
|
867
890
|
this.flusher.enqueue({ body, signal });
|
|
@@ -879,6 +902,23 @@ var Signals = class {
|
|
|
879
902
|
usageLog: res.usageLog ?? null
|
|
880
903
|
};
|
|
881
904
|
}
|
|
905
|
+
/**
|
|
906
|
+
* Validate + price a signal WITHOUT recording it (server `dryRun`). Returns
|
|
907
|
+
* the projected usage log so an integration can preflight before sending real
|
|
908
|
+
* traffic — no inbox row, wallet / outcome writes, or webhooks fire.
|
|
909
|
+
*/
|
|
910
|
+
async verify(signal) {
|
|
911
|
+
const body = signalToWire(signal);
|
|
912
|
+
body.dryRun = true;
|
|
913
|
+
const res = await this.transport.request({
|
|
914
|
+
method: "POST",
|
|
915
|
+
path: "/api/v1/usage",
|
|
916
|
+
body,
|
|
917
|
+
// A dry run writes nothing, so it is always safe to retry.
|
|
918
|
+
retry: true
|
|
919
|
+
});
|
|
920
|
+
return res.usageLog ?? null;
|
|
921
|
+
}
|
|
882
922
|
/** Meter a named credit. */
|
|
883
923
|
credit(input, opts) {
|
|
884
924
|
return this.track({ ...input, type: "credit" }, opts);
|
|
@@ -901,9 +941,9 @@ var Signals = class {
|
|
|
901
941
|
return { customer: res.customer, totals: res.totals, logs: res.logs };
|
|
902
942
|
}
|
|
903
943
|
/**
|
|
904
|
-
* Record one unit-metered event (a fixed-price meter with no tokens),
|
|
905
|
-
* to a catalog unit by
|
|
906
|
-
* `POST /api/v1/units`.
|
|
944
|
+
* Record one unit-metered event (a fixed-price meter with no tokens),
|
|
945
|
+
* addressed to a catalog unit by its `agentKey`. Sent immediately — unit
|
|
946
|
+
* events are not buffered. `POST /api/v1/units`.
|
|
907
947
|
*/
|
|
908
948
|
async unit(input) {
|
|
909
949
|
return this.transport.request({
|
|
@@ -911,7 +951,7 @@ var Signals = class {
|
|
|
911
951
|
path: "/api/v1/units",
|
|
912
952
|
body: {
|
|
913
953
|
customerId: input.customerId,
|
|
914
|
-
|
|
954
|
+
agentKey: input.agentKey,
|
|
915
955
|
...input.member ? { member: input.member } : {}
|
|
916
956
|
},
|
|
917
957
|
// Best-effort metering opts into retries on transient failures.
|
|
@@ -1019,6 +1059,33 @@ var Webhooks = class {
|
|
|
1019
1059
|
}
|
|
1020
1060
|
};
|
|
1021
1061
|
|
|
1062
|
+
// src/resources/workspace.ts
|
|
1063
|
+
var Workspace = class {
|
|
1064
|
+
constructor(transport) {
|
|
1065
|
+
this.transport = transport;
|
|
1066
|
+
}
|
|
1067
|
+
transport;
|
|
1068
|
+
/** Identify the org + key behind the request, incl. `sandbox` | `live`. */
|
|
1069
|
+
me() {
|
|
1070
|
+
return this.transport.request({
|
|
1071
|
+
method: "GET",
|
|
1072
|
+
path: "/api/v1/me"
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
/** The org's enabled models with live prices. `{ active: true }` returns only
|
|
1076
|
+
* the models that can be metered right now. */
|
|
1077
|
+
async models(params = {}) {
|
|
1078
|
+
const res = await this.transport.request({
|
|
1079
|
+
method: "GET",
|
|
1080
|
+
path: "/api/v1/models",
|
|
1081
|
+
query: {
|
|
1082
|
+
active: params.active === void 0 ? void 0 : String(params.active)
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
return res.models;
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1022
1089
|
// src/client.ts
|
|
1023
1090
|
var ClockNext = class {
|
|
1024
1091
|
/** Record + read usage signals (credit / wallet / outcome / unit). */
|
|
@@ -1043,6 +1110,8 @@ var ClockNext = class {
|
|
|
1043
1110
|
webhooks;
|
|
1044
1111
|
/** Mint customer-portal embed tokens. */
|
|
1045
1112
|
portal;
|
|
1113
|
+
/** Identify the workspace (sandbox/live) + list its models. `/me`, `/models`. */
|
|
1114
|
+
workspace;
|
|
1046
1115
|
cfg;
|
|
1047
1116
|
flusher;
|
|
1048
1117
|
constructor(config) {
|
|
@@ -1065,6 +1134,7 @@ var ClockNext = class {
|
|
|
1065
1134
|
this.units = new Units(transport);
|
|
1066
1135
|
this.webhooks = new Webhooks(transport);
|
|
1067
1136
|
this.portal = new Portal(transport);
|
|
1137
|
+
this.workspace = new Workspace(transport);
|
|
1068
1138
|
}
|
|
1069
1139
|
/** Number of signals currently buffered (async mode). */
|
|
1070
1140
|
get pending() {
|