@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 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`), idempotent replaces (`PUT`, e.g.
94
- `customers.setCurrency`), and `DELETE` are retried automatically. Metering
95
- signals (`signals.*`) opt into retries too, since metering is best-effort.
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.setCurrency(c.id, "EUR");
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, unit: "Seats" });
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.3.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/customers/:id/usage`. */
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/customers/${encodeURIComponent(id)}/usage`,
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
- /** Set the customer's currency. `PUT …/:id/currency`. */
530
- async setCurrency(id, currencyCode) {
531
- await this.transport.request({
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
- if (t) {
845
- if (t.input != null) body.inputTokens = t.input;
846
- if (t.output != null) body.outputTokens = t.output;
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), matched
905
- * to a catalog unit by name. Sent immediately — unit events are not buffered.
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
- unit: input.unit,
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() {