@clocknext/sdk 0.3.0 → 0.4.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.4.0" ;
233
233
  var Transport = class {
234
234
  constructor(cfg) {
235
235
  this.cfg = cfg;
@@ -526,22 +526,9 @@ var Customers = class {
526
526
  body: input
527
527
  });
528
528
  }
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
- }
529
+ // Note: a customer's currency is set through `update(id, { currencyCode })`
530
+ // (PATCH …/:id) — there is no separate currency endpoint. Credit-type
531
+ // assignment has no public API.
545
532
  // --- Balance adjustments -------------------------------------------------
546
533
  /** Grant or claw back a credit balance. `POST …/:id/credits/adjust`. */
547
534
  async adjustCredit(id, input) {
@@ -830,6 +817,17 @@ var Purchases = class {
830
817
  }
831
818
  };
832
819
 
820
+ // src/idempotency.ts
821
+ function newKey() {
822
+ const c = globalThis.crypto;
823
+ if (typeof c?.randomUUID === "function") return c.randomUUID();
824
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (ch) => {
825
+ const r = Math.random() * 16 | 0;
826
+ const v = ch === "x" ? r : r & 3 | 8;
827
+ return v.toString(16);
828
+ });
829
+ }
830
+
833
831
  // src/resources/signals.ts
834
832
  function signalToWire(signal) {
835
833
  const body = {
@@ -838,17 +836,15 @@ function signalToWire(signal) {
838
836
  model: signal.model
839
837
  };
840
838
  if (signal.member) body.member = signal.member;
841
- if (signal.status) body.status = signal.status;
842
839
  if (signal.custom) body.custom = signal.custom;
843
840
  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
- }
841
+ body.inputTokens = t.input;
842
+ body.outputTokens = t.output;
843
+ if (t.cache != null) body.cacheTokens = t.cache;
849
844
  if (signal.type === "credit" || signal.type === "outcome") {
850
845
  body.agentKey = signal.agentKey;
851
846
  }
847
+ if (signal.idempotencyKey) body.idempotencyKey = signal.idempotencyKey;
852
848
  return body;
853
849
  }
854
850
  var Signals = class {
@@ -862,6 +858,7 @@ var Signals = class {
862
858
  cfg;
863
859
  async track(signal, opts = {}) {
864
860
  const body = signalToWire(signal);
861
+ if (body.idempotencyKey == null) body.idempotencyKey = newKey();
865
862
  const sendNow = opts.wait || this.cfg.mode === "sync";
866
863
  if (!sendNow) {
867
864
  this.flusher.enqueue({ body, signal });
@@ -879,6 +876,23 @@ var Signals = class {
879
876
  usageLog: res.usageLog ?? null
880
877
  };
881
878
  }
879
+ /**
880
+ * Validate + price a signal WITHOUT recording it (server `dryRun`). Returns
881
+ * the projected usage log so an integration can preflight before sending real
882
+ * traffic — no inbox row, wallet / outcome writes, or webhooks fire.
883
+ */
884
+ async verify(signal) {
885
+ const body = signalToWire(signal);
886
+ body.dryRun = true;
887
+ const res = await this.transport.request({
888
+ method: "POST",
889
+ path: "/api/v1/usage",
890
+ body,
891
+ // A dry run writes nothing, so it is always safe to retry.
892
+ retry: true
893
+ });
894
+ return res.usageLog ?? null;
895
+ }
882
896
  /** Meter a named credit. */
883
897
  credit(input, opts) {
884
898
  return this.track({ ...input, type: "credit" }, opts);
@@ -901,9 +915,9 @@ var Signals = class {
901
915
  return { customer: res.customer, totals: res.totals, logs: res.logs };
902
916
  }
903
917
  /**
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`.
918
+ * Record one unit-metered event (a fixed-price meter with no tokens),
919
+ * addressed to a catalog unit by its `agentKey`. Sent immediately — unit
920
+ * events are not buffered. `POST /api/v1/units`.
907
921
  */
908
922
  async unit(input) {
909
923
  return this.transport.request({
@@ -911,7 +925,7 @@ var Signals = class {
911
925
  path: "/api/v1/units",
912
926
  body: {
913
927
  customerId: input.customerId,
914
- unit: input.unit,
928
+ agentKey: input.agentKey,
915
929
  ...input.member ? { member: input.member } : {}
916
930
  },
917
931
  // Best-effort metering opts into retries on transient failures.
@@ -1019,6 +1033,33 @@ var Webhooks = class {
1019
1033
  }
1020
1034
  };
1021
1035
 
1036
+ // src/resources/workspace.ts
1037
+ var Workspace = class {
1038
+ constructor(transport) {
1039
+ this.transport = transport;
1040
+ }
1041
+ transport;
1042
+ /** Identify the org + key behind the request, incl. `sandbox` | `live`. */
1043
+ me() {
1044
+ return this.transport.request({
1045
+ method: "GET",
1046
+ path: "/api/v1/me"
1047
+ });
1048
+ }
1049
+ /** The org's enabled models with live prices. `{ active: true }` returns only
1050
+ * the models that can be metered right now. */
1051
+ async models(params = {}) {
1052
+ const res = await this.transport.request({
1053
+ method: "GET",
1054
+ path: "/api/v1/models",
1055
+ query: {
1056
+ active: params.active === void 0 ? void 0 : String(params.active)
1057
+ }
1058
+ });
1059
+ return res.models;
1060
+ }
1061
+ };
1062
+
1022
1063
  // src/client.ts
1023
1064
  var ClockNext = class {
1024
1065
  /** Record + read usage signals (credit / wallet / outcome / unit). */
@@ -1043,6 +1084,8 @@ var ClockNext = class {
1043
1084
  webhooks;
1044
1085
  /** Mint customer-portal embed tokens. */
1045
1086
  portal;
1087
+ /** Identify the workspace (sandbox/live) + list its models. `/me`, `/models`. */
1088
+ workspace;
1046
1089
  cfg;
1047
1090
  flusher;
1048
1091
  constructor(config) {
@@ -1065,6 +1108,7 @@ var ClockNext = class {
1065
1108
  this.units = new Units(transport);
1066
1109
  this.webhooks = new Webhooks(transport);
1067
1110
  this.portal = new Portal(transport);
1111
+ this.workspace = new Workspace(transport);
1068
1112
  }
1069
1113
  /** Number of signals currently buffered (async mode). */
1070
1114
  get pending() {