@clocknext/sdk 0.8.0 → 0.10.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/dist/index.js CHANGED
@@ -119,7 +119,8 @@ var Flusher = class {
119
119
  try {
120
120
  await this.transport.request({
121
121
  method: "POST",
122
- path: "/api/v1/usage",
122
+ // Per-item, not per-Flusher: one route per meter. See `QueuedSignal`.
123
+ path: item.path,
123
124
  body: item.body,
124
125
  // Best-effort metering opts into retries on transient failures.
125
126
  retry: true
@@ -227,7 +228,7 @@ function sleep(ms) {
227
228
  }
228
229
 
229
230
  // src/http.ts
230
- var SDK_VERSION = "0.8.0" ;
231
+ var SDK_VERSION = "0.10.0" ;
231
232
  var Transport = class {
232
233
  constructor(cfg) {
233
234
  this.cfg = cfg;
@@ -322,6 +323,36 @@ var Transport = class {
322
323
  }
323
324
  };
324
325
 
326
+ // src/resources/composites.ts
327
+ var Composites = class {
328
+ constructor(transport) {
329
+ this.transport = transport;
330
+ }
331
+ transport;
332
+ /** List composites. Pass `{ active: true }` for only the ones a signal can
333
+ * still be tagged with — the default list INCLUDES archived ones, which
334
+ * keep every row they already own. */
335
+ async list(params = {}) {
336
+ const res = await this.transport.request({
337
+ method: "GET",
338
+ path: "/api/v1/composites",
339
+ query: {
340
+ active: params.active === void 0 ? void 0 : String(params.active)
341
+ }
342
+ });
343
+ return res.composites;
344
+ }
345
+ /** Create a composite. `POST /api/v1/composites`. */
346
+ async create(input) {
347
+ const res = await this.transport.request({
348
+ method: "POST",
349
+ path: "/api/v1/composites",
350
+ body: input
351
+ });
352
+ return res.composite;
353
+ }
354
+ };
355
+
325
356
  // src/resources/credits.ts
326
357
  var Credits = class {
327
358
  constructor(transport) {
@@ -504,7 +535,7 @@ var Customers = class {
504
535
  });
505
536
  return res.members;
506
537
  }
507
- /** Add a member to a customer. Duplicate `employeeId` → ConflictError. */
538
+ /** Add a member to a customer. Duplicate `email` → ConflictError. */
508
539
  async addMember(id, input) {
509
540
  const res = await this.transport.request({
510
541
  method: "POST",
@@ -513,7 +544,7 @@ var Customers = class {
513
544
  });
514
545
  return res.member;
515
546
  }
516
- /** Update a member's profile (role is fixed). `PATCH …/:id/members/:memberId`. */
547
+ /** Update a member's profile. Duplicate `email` → ConflictError. `PATCH …/:id/members/:memberId`. */
517
548
  async updateMember(id, memberId, input) {
518
549
  const res = await this.transport.request({
519
550
  method: "PATCH",
@@ -522,12 +553,13 @@ var Customers = class {
522
553
  });
523
554
  return res.member;
524
555
  }
525
- /** Remove a member from a customer. `DELETE …/:id/members/:memberId`. */
556
+ /** Remove a member from a customer, returning who was removed. `DELETE …/:id/members/:memberId`. */
526
557
  async removeMember(id, memberId) {
527
- await this.transport.request({
558
+ const res = await this.transport.request({
528
559
  method: "DELETE",
529
560
  path: `/api/v1/customers/${encodeURIComponent(id)}/members/${encodeURIComponent(memberId)}`
530
561
  });
562
+ return res.member;
531
563
  }
532
564
  // --- Wallet & billing config --------------------------------------------
533
565
  /** The customer's wallet balance + transactions. `GET …/:id/wallet`. */
@@ -853,18 +885,28 @@ function newKey() {
853
885
  }
854
886
 
855
887
  // src/resources/signals.ts
888
+ var SIGNAL_PATHS = {
889
+ credit: "/api/v1/signal/credit",
890
+ outcome: "/api/v1/signal/outcome",
891
+ wallet: "/api/v1/signal/wallet"
892
+ };
893
+ var UNIT_PATH = "/api/v1/signal/unit";
894
+ var COMPOSITE_COMPLETE_PATH = "/api/v1/signal/composite/complete";
856
895
  function signalToWire(signal) {
896
+ const t = signal.tokens;
857
897
  const body = {
858
898
  customerId: signal.customerId,
859
- type: signal.type,
860
- model: signal.model
899
+ // `usage` is required on all three model-priced kinds. input/output are
900
+ // always sent; `cacheTokens` only when provided (the server defaults 0).
901
+ usage: {
902
+ model: signal.model,
903
+ inputTokens: t.input,
904
+ outputTokens: t.output,
905
+ ...t.cache != null ? { cacheTokens: t.cache } : {}
906
+ }
861
907
  };
862
908
  if (signal.member) body.member = signal.member;
863
909
  if (signal.custom) body.custom = signal.custom;
864
- const t = signal.tokens;
865
- body.inputTokens = t.input;
866
- body.outputTokens = t.output;
867
- if (t.cache != null) body.cacheTokens = t.cache;
868
910
  if (signal.type === "credit" || signal.type === "outcome") {
869
911
  body.agentKey = signal.agentKey;
870
912
  }
@@ -872,12 +914,18 @@ function signalToWire(signal) {
872
914
  body.runId = signal.runId;
873
915
  if (signal.complete != null) body.complete = signal.complete;
874
916
  }
875
- if (signal.type !== "wallet" && signal.metric) {
876
- body[signal.metric.ref] = signal.metric.value;
917
+ if (signal.type !== "wallet" && signal.composite) {
918
+ body.composite = { [signal.composite.ref]: signal.composite.value };
877
919
  }
878
920
  if (signal.idempotencyKey) body.idempotencyKey = signal.idempotencyKey;
879
921
  return body;
880
922
  }
923
+ function handles(res) {
924
+ return {
925
+ ...res.messageId != null ? { messageId: res.messageId } : {},
926
+ ...res.rawId != null ? { rawId: res.rawId } : {}
927
+ };
928
+ }
881
929
  var Signals = class {
882
930
  constructor(transport, flusher, cfg) {
883
931
  this.transport = transport;
@@ -888,7 +936,8 @@ var Signals = class {
888
936
  flusher;
889
937
  cfg;
890
938
  /**
891
- * Record one usage signal. `POST /api/v1/usage`.
939
+ * Record one usage signal, to the route its `type` selects:
940
+ * `POST /api/v1/signal/credit` | `/signal/outcome` | `/signal/wallet`.
892
941
  *
893
942
  * Returns as soon as the signal is buffered (async mode) or the request has
894
943
  * been answered (`{ wait: true }` / sync mode). See {@link TrackResult}: a
@@ -897,14 +946,15 @@ var Signals = class {
897
946
  async track(signal, opts = {}) {
898
947
  const body = signalToWire(signal);
899
948
  if (body.idempotencyKey == null) body.idempotencyKey = newKey();
949
+ const path = SIGNAL_PATHS[signal.type];
900
950
  const sendNow = opts.wait || this.cfg.mode === "sync";
901
951
  if (!sendNow) {
902
- this.flusher.enqueue({ body, signal });
952
+ this.flusher.enqueue({ path, body, signal });
903
953
  return { queued: true };
904
954
  }
905
955
  const res = await this.transport.request({
906
956
  method: "POST",
907
- path: "/api/v1/usage",
957
+ path,
908
958
  body,
909
959
  // Best-effort metering opts into retries on transient failures.
910
960
  retry: true
@@ -912,39 +962,52 @@ var Signals = class {
912
962
  return {
913
963
  queued: res.queued === true,
914
964
  usageLog: res.usageLog ?? null,
915
- ...res.messageId != null ? { messageId: res.messageId } : {}
965
+ ...handles(res)
916
966
  };
917
967
  }
918
968
  /**
919
969
  * Validate + price a signal WITHOUT recording it (server `dryRun`). Returns
920
970
  * the projected usage log so an integration can preflight before sending real
921
971
  * traffic — no inbox row, wallet / outcome writes, or webhooks fire.
972
+ *
973
+ * Only the model-priced kinds support this. A dry run is not offered for unit
974
+ * reports or composite completions, and the server REJECTS the flag on those
975
+ * routes rather than accepting and ignoring it — which is why there is no
976
+ * dry-run path through `unit()` or `completeComposite()`.
922
977
  */
923
978
  async verify(signal) {
924
979
  const body = signalToWire(signal);
925
980
  body.dryRun = true;
926
981
  const res = await this.transport.request({
927
982
  method: "POST",
928
- path: "/api/v1/usage",
983
+ path: SIGNAL_PATHS[signal.type],
929
984
  body,
930
985
  // A dry run writes nothing, so it is always safe to retry.
931
986
  retry: true
932
987
  });
933
988
  return res.usageLog ?? null;
934
989
  }
935
- /** Meter a named credit. */
990
+ /** Meter a named credit. `POST /api/v1/signal/credit`. */
936
991
  credit(input, opts) {
937
992
  return this.track({ ...input, type: "credit" }, opts);
938
993
  }
939
- /** Debit the customer's wallet at model cost. */
994
+ /** Debit the customer's wallet at model cost. `POST /api/v1/signal/wallet`. */
940
995
  wallet(input, opts) {
941
996
  return this.track({ ...input, type: "wallet" }, opts);
942
997
  }
943
- /** Advance one step of an outcome workflow. */
998
+ /** Advance one step of an outcome workflow. `POST /api/v1/signal/outcome`. */
944
999
  outcome(input, opts) {
945
1000
  return this.track({ ...input, type: "outcome" }, opts);
946
1001
  }
947
- /** Recent usage logs + totals for a customer. `GET /api/v1/usage`. */
1002
+ /**
1003
+ * Recent usage logs + totals for a customer. `GET /api/v1/usage`.
1004
+ *
1005
+ * Still the legacy path, deliberately. The `/api/v1/signal/*` family is
1006
+ * POST-only — it replaced how usage is INGESTED, not how it is read — so
1007
+ * `GET /api/v1/usage` has no successor and is not deprecated. It is also the
1008
+ * only way to prove a signal landed, which matters more now that ingestion
1009
+ * answers `202` for everything.
1010
+ */
948
1011
  async list(params) {
949
1012
  const res = await this.transport.request({
950
1013
  method: "GET",
@@ -956,22 +1019,94 @@ var Signals = class {
956
1019
  /**
957
1020
  * Record one unit-metered event (a fixed-price meter with no tokens),
958
1021
  * addressed to a catalog unit by its `agentKey`. Sent immediately — unit
959
- * events are not buffered. `POST /api/v1/units`.
1022
+ * events are not buffered. `POST /api/v1/signal/unit`.
1023
+ *
1024
+ * Two things changed with the move off `POST /api/v1/units`, and both show up
1025
+ * in the return type. The route is now durable (the report lands in an inbox
1026
+ * before it is priced, so a failure parks as Pending and can be retried
1027
+ * instead of losing a billable event) and it answers `202` when it queues —
1028
+ * so this resolves {@link UnitTrackResult}, not a bare `{ unitUsage }`.
1029
+ * Branch on `queued`.
960
1030
  */
961
1031
  async unit(input) {
962
- return this.transport.request({
1032
+ const res = await this.transport.request({
963
1033
  method: "POST",
964
- path: "/api/v1/units",
1034
+ path: UNIT_PATH,
965
1035
  body: {
966
1036
  customerId: input.customerId,
967
1037
  agentKey: input.agentKey,
968
- ...input.member ? { member: input.member } : {}
1038
+ ...input.quantity != null ? { quantity: input.quantity } : {},
1039
+ ...input.member ? { member: input.member } : {},
1040
+ ...input.composite ? { composite: { [input.composite.ref]: input.composite.value } } : {},
1041
+ // Assigned HERE, before the send, so the SAME key rides every
1042
+ // transport-level retry below. Without it a single 500 followed by a
1043
+ // success is TWO un-keyed reports, and the server dedups on the key —
1044
+ // so it bills the allowance down twice, up to `retry.maxAttempts`
1045
+ // times. The route gained idempotency precisely to stop that; not
1046
+ // using it by default would leave the double-charge reachable from the
1047
+ // client instead of the server.
1048
+ idempotencyKey: input.idempotencyKey ?? newKey()
969
1049
  },
970
1050
  // Best-effort metering opts into retries on transient failures.
971
1051
  retry: true
972
1052
  });
1053
+ return {
1054
+ queued: res.queued === true,
1055
+ unitUsage: res.unitUsage ?? null,
1056
+ ...handles(res)
1057
+ };
973
1058
  }
974
- /** A customer's unit balances + recent unit events. `GET /api/v1/units`. */
1059
+ /**
1060
+ * Close ONE occurrence of a composite — a distinct tag value.
1061
+ * `POST /api/v1/signal/composite/complete`.
1062
+ *
1063
+ * A composite bundles several credits / outcomes / units and is billed as one
1064
+ * thing. The occurrence opens on the first tagged signal and costs nothing;
1065
+ * this call is the moment it is billed. Only your code knows when the
1066
+ * workflow ended, so this is a DECLARATION, not a hint: tagged signals may
1067
+ * arrive in any order and the occurrence stays open and unbilled until you
1068
+ * say otherwise. Put it on the line before your own `return`.
1069
+ *
1070
+ * An OUTCOME never needs this — its own `complete: true` closes its run and
1071
+ * the matching occurrence together. Credits and units have no "done" signal
1072
+ * of their own, so this is theirs.
1073
+ *
1074
+ * Closing twice is safe: the second call answers `ALREADY_COMPLETED` and
1075
+ * cannot bill again. Calling it EARLY is also safe — before every entitlement
1076
+ * the composite is restricted to has reported, or while an ADVANCE pool is
1077
+ * full — the server parks the ping and applies it when the blocker clears,
1078
+ * rather than failing it.
1079
+ */
1080
+ async completeComposite(input) {
1081
+ const res = await this.transport.request({
1082
+ method: "POST",
1083
+ path: COMPOSITE_COMPLETE_PATH,
1084
+ body: {
1085
+ customerId: input.customerId,
1086
+ composite: { [input.composite.ref]: input.composite.value },
1087
+ // Auto-assigned like the others. A retry here could not double-BILL
1088
+ // even without it — closing a closed occurrence is a server-side
1089
+ // no-op — but that safety comes from occurrence-level dedup, not from
1090
+ // idempotency, and an un-keyed replay still writes a redundant inbox
1091
+ // row. Keying it collapses the retry into the one attempt it is.
1092
+ idempotencyKey: input.idempotencyKey ?? newKey()
1093
+ },
1094
+ retry: true
1095
+ });
1096
+ return {
1097
+ queued: res.queued === true,
1098
+ ...res.status != null ? { status: res.status } : {},
1099
+ ...res.composite !== void 0 ? { composite: res.composite } : {},
1100
+ ...res.value !== void 0 ? { value: res.value } : {},
1101
+ ...handles(res)
1102
+ };
1103
+ }
1104
+ /**
1105
+ * A customer's unit balances + recent unit events. `GET /api/v1/units`.
1106
+ *
1107
+ * Legacy path for the same reason as {@link list}: the signal family is
1108
+ * POST-only, so the read side has no successor and is not deprecated.
1109
+ */
975
1110
  unitUsage(params) {
976
1111
  return this.transport.request({
977
1112
  method: "GET",
@@ -1115,6 +1250,8 @@ var ClockNext = class {
1115
1250
  payments;
1116
1251
  /** Manage the credit-type catalog. */
1117
1252
  credits;
1253
+ /** Manage the composite catalog — bundles billed as one thing. */
1254
+ composites;
1118
1255
  /** Manage the outcome-type catalog. */
1119
1256
  outcomes;
1120
1257
  /** Manage the unit-type catalog. */
@@ -1143,6 +1280,7 @@ var ClockNext = class {
1143
1280
  this.invoices = new Invoices(transport);
1144
1281
  this.payments = new Payments(transport);
1145
1282
  this.credits = new Credits(transport);
1283
+ this.composites = new Composites(transport);
1146
1284
  this.outcomes = new Outcomes(transport);
1147
1285
  this.units = new Units(transport);
1148
1286
  this.webhooks = new Webhooks(transport);