@clocknext/sdk 0.12.0 → 0.13.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.
@@ -58,8 +58,9 @@ async function main() {
58
58
  tokens: { input: 800, output: 5000 },
59
59
  });
60
60
 
61
- // 4. Outcome run — every step shares a `runId`; mark the last one `complete`
62
- // to bill the run. `agentKey` is the OutcomeStep's key, which you read off
61
+ // 4. Outcome run — every step shares a `runId`; a separate, dedicated call
62
+ // (`completeOutcome`) closes and bills the run — never a flag on a step.
63
+ // `agentKey` is the OutcomeStep's key, which you read off
63
64
  // `cnk.outcomes.list()` the same way.
64
65
  // `outcomes.list()` returns catalogue rows WITHOUT their steps; the step
65
66
  // agent keys — which is what a signal reports against — are on the detail.
@@ -72,9 +73,10 @@ async function main() {
72
73
  model: model.modelId,
73
74
  agentKey: outcome.steps[0].agentKey,
74
75
  runId,
75
- complete: true, // closes the RUN; a composite still needs its own close
76
76
  tokens: { input: 600, output: 150 },
77
77
  });
78
+ // Closes the RUN; a composite still needs its own `completeComposite()` close.
79
+ await cnk.signals.completeOutcome({ customerId: customer.id, runId });
78
80
  }
79
81
 
80
82
  // 5. Flush the background buffer, then read the signals back. Ingestion is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clocknext/sdk",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Official ClockNext SDK — record usage signals and manage plans, purchases, invoices, customers, and billing from your backend.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/config.ts CHANGED
@@ -14,9 +14,13 @@ const DEFAULT_BASE_URL = "https://payments.clocknext.com";
14
14
  *
15
15
  * In the default `async` mode a signal is sent by the background flusher, long
16
16
  * after `signals.credit()` resolved `{ status: "buffered" }`. There is no
17
- * promise left to reject into, so a rejected signal — an unknown `agentKey`, a
18
- * model the org has not enabled, a customer with no active plan — has nowhere
19
- * to surface except these hooks.
17
+ * promise left to reject into, so a failed REQUEST — a bad API key, an
18
+ * unreachable host, a timeout — has nowhere to surface except these hooks.
19
+ *
20
+ * They do not cover a signal the server accepted (`202`) and then refused while
21
+ * pricing it: that request succeeded as far as this SDK can tell. Those
22
+ * rejections are recorded on the workspace's signal log in the ClockNext
23
+ * dashboard, which is owner-only by design.
20
24
  *
21
25
  * They used to default to nothing at all, which meant a misconfigured
22
26
  * integration compiled, ran, returned success-shaped results, and metered
package/src/index.ts CHANGED
@@ -44,6 +44,8 @@ export type {
44
44
  CreditInput,
45
45
  WalletInput,
46
46
  OutcomeInput,
47
+ OutcomeCompletionInput,
48
+ OutcomeCompletionResult,
47
49
  TrackResult,
48
50
  UsageList,
49
51
  SerializedUsageLog,
@@ -87,6 +89,7 @@ export type {
87
89
  CreditAdjustInput,
88
90
  OutcomeAdjustInput,
89
91
  UnitAdjustInput,
92
+ CompositeAdjustInput,
90
93
  // plans
91
94
  Plan,
92
95
  PlanDetail,
@@ -2,6 +2,7 @@ import type { Transport } from "../http.js";
2
2
  import type {
3
3
  AddMemberInput,
4
4
  ApiCustomer,
5
+ CompositeAdjustInput,
5
6
  CreateCustomerInput,
6
7
  CreditAdjustInput,
7
8
  CreditBalance,
@@ -261,13 +262,18 @@ export class Customers {
261
262
 
262
263
  // --- Balance adjustments -------------------------------------------------
263
264
 
264
- /** Grant or claw back a credit balance. `POST …/:id/credits/adjust`. */
265
- async adjustCredit(id: string, input: CreditAdjustInput): Promise<void> {
266
- await this.transport.request({
265
+ /** Grant or claw back a credit balance. Returns the remaining balance.
266
+ * `POST …/:id/credits/adjust`. */
267
+ async adjustCredit(
268
+ id: string,
269
+ input: CreditAdjustInput,
270
+ ): Promise<{ remaining: number }> {
271
+ const res = await this.transport.request<{ remaining: number }>({
267
272
  method: "POST",
268
273
  path: `/api/v1/customers/${encodeURIComponent(id)}/credits/adjust`,
269
274
  body: input,
270
275
  });
276
+ return { remaining: res.remaining };
271
277
  }
272
278
 
273
279
  /** Grant or claw back an outcome balance. Returns the remaining balance. */
@@ -295,4 +301,21 @@ export class Customers {
295
301
  });
296
302
  return { remaining: res.remaining };
297
303
  }
304
+
305
+ /**
306
+ * Grant or claw back slots on a customer's ADVANCE composite pool for the
307
+ * current billing cycle. Returns the resulting `remaining` slot count.
308
+ * `POST …/:id/composites/adjust`.
309
+ */
310
+ async adjustComposite(
311
+ id: string,
312
+ input: CompositeAdjustInput,
313
+ ): Promise<{ remaining: number }> {
314
+ const res = await this.transport.request<{ remaining: number }>({
315
+ method: "POST",
316
+ path: `/api/v1/customers/${encodeURIComponent(id)}/composites/adjust`,
317
+ body: input,
318
+ });
319
+ return { remaining: res.remaining };
320
+ }
298
321
  }
@@ -5,6 +5,8 @@ import type {
5
5
  CompositeCompletionInput,
6
6
  CompositeCompletionResult,
7
7
  CreditInput,
8
+ OutcomeCompletionInput,
9
+ OutcomeCompletionResult,
8
10
  OutcomeInput,
9
11
  ResolvedConfig,
10
12
  SerializedUnitUsage,
@@ -37,10 +39,11 @@ const SIGNAL_PATHS: Record<SignalType, string> = {
37
39
  wallet: "/api/v1/signal/wallet",
38
40
  };
39
41
 
40
- /** Unit reports and composite completions are not part of the `Signal` union
41
- * (no model, no tokens), so they carry their paths separately. */
42
+ /** Unit reports and completion pings are not part of the `Signal` union (no
43
+ * model, no tokens), so they carry their paths separately. */
42
44
  const UNIT_PATH = "/api/v1/signal/unit";
43
45
  const COMPOSITE_COMPLETE_PATH = "/api/v1/signal/composite/complete";
46
+ const OUTCOME_COMPLETE_PATH = "/api/v1/signal/outcome/complete";
44
47
 
45
48
  /**
46
49
  * Builds the request body for a typed Signal.
@@ -82,10 +85,12 @@ export function signalToWire(signal: Signal): Record<string, unknown> {
82
85
  body.agentKey = signal.agentKey;
83
86
  }
84
87
 
85
- // Outcome runs are grouped by `runId`; `complete` ends the run.
88
+ // Outcome runs are grouped by `runId`. Ending the run is NOT a flag on this
89
+ // body: `POST /api/v1/signal/outcome` (what this body is sent to) accepts
90
+ // ONLY the step shape and rejects one that also carries `complete` — see
91
+ // `signals.completeOutcome()`, its own dedicated route.
86
92
  if (signal.type === "outcome") {
87
93
  body.runId = signal.runId;
88
- if (signal.complete != null) body.complete = signal.complete;
89
94
  }
90
95
 
91
96
  // Composite tag, nested under its `refId`. Never set for wallet — the server
@@ -136,8 +141,15 @@ function handles(res: SignalResponse) {
136
141
  *
137
142
  * Because of that, a signal the server later REJECTS (unknown `agentKey`, a
138
143
  * model the org has not enabled, no active plan) cannot come back through this
139
- * promise. It surfaces on the `onError` hook, and `list()` is how you confirm
140
- * one actually landed — ingestion being asynchronous, the only way to.
144
+ * promise — and does not reach `onError` either, since from here that request
145
+ * succeeded. Such a rejection has no programmatic channel at all: it is
146
+ * recorded on the workspace's signal log in the ClockNext dashboard, which is
147
+ * owner-only by design.
148
+ *
149
+ * `list()` is the partial check available from code. It returns priced usage
150
+ * logs with a `status`, so it confirms a signal landed and whether pricing
151
+ * succeeded — but a signal refused BEFORE pricing never becomes a usage log
152
+ * and does not appear there.
141
153
  */
142
154
  export class Signals {
143
155
  constructor(
@@ -235,6 +247,56 @@ export class Signals {
235
247
  return this.track({ ...input, type: "outcome" }, opts);
236
248
  }
237
249
 
250
+ /**
251
+ * Close ONE outcome run — declares the workflow finished and bills it. The
252
+ * outcome mirror of {@link Signals.completeComposite}, on its own dedicated
253
+ * route: `POST /api/v1/signal/outcome/complete`.
254
+ *
255
+ * An outcome bills per COMPLETION, never per step, and `signals.outcome()`
256
+ * never closes one — it accepts only the step shape. Declaring the workflow
257
+ * finished is always this separate, explicit, contentless call.
258
+ *
259
+ * This closes the RUN only. If a step of this run also carried a
260
+ * `composite`, that occurrence stays open and unbilled until you separately
261
+ * call {@link Signals.completeComposite} — the two completions are
262
+ * independent, and neither implies the other.
263
+ *
264
+ * Sent immediately, never buffered. A genuinely second, independent call for
265
+ * an already-closed run is a PERMANENT error (it would bill the run twice),
266
+ * not a safe no-op — see {@link OutcomeCompletionResult}.
267
+ */
268
+ async completeOutcome(
269
+ input: OutcomeCompletionInput,
270
+ ): Promise<OutcomeCompletionResult> {
271
+ const idempotencyKey = input.idempotencyKey ?? newKey();
272
+ const res = await this.transport.request<
273
+ SignalResponse & {
274
+ status?: "COMPLETED" | "ALREADY_COMPLETED";
275
+ usageLog?: OutcomeCompletionResult["usageLog"];
276
+ }
277
+ >({
278
+ method: "POST",
279
+ path: OUTCOME_COMPLETE_PATH,
280
+ idempotencyKey,
281
+ body: {
282
+ customerId: input.customerId,
283
+ runId: input.runId,
284
+ complete: true,
285
+ ...(input.member ? { member: input.member } : {}),
286
+ idempotencyKey,
287
+ },
288
+ retry: true,
289
+ });
290
+ const queued = res.queued === true;
291
+ return {
292
+ delivery: queued ? "accepted" : "recorded",
293
+ queued,
294
+ ...(res.status != null ? { status: res.status } : {}),
295
+ usageLog: res.usageLog ?? null,
296
+ ...handles(res),
297
+ };
298
+ }
299
+
238
300
  /**
239
301
  * Recent usage logs + totals for a customer. `GET /api/v1/usage`.
240
302
  *
@@ -316,13 +378,13 @@ export class Signals {
316
378
  * arrive in any order and the occurrence stays open and unbilled until you
317
379
  * say otherwise. Put it on the line before your own `return`.
318
380
  *
319
- * EVERY composite needs this, outcome-backed ones included. An outcome's own
320
- * `complete: true` closes its RUN and nothing else — it does not close the
321
- * composite occurrence the run was tagged into, whatever mix of credits,
322
- * outcomes and units that composite wraps. (Earlier versions of this SDK
323
- * documented the opposite. Believing it means the occurrence stays ONGOING
324
- * forever and is never billed, so if you tag outcome signals with a
325
- * `composite`, you must still call this.)
381
+ * EVERY composite needs this, outcome-backed ones included. `completeOutcome()`
382
+ * closes its RUN and nothing else — it does not close the composite
383
+ * occurrence the run was tagged into, whatever mix of credits, outcomes and
384
+ * units that composite wraps. (Earlier versions of this SDK documented the
385
+ * opposite. Believing it means the occurrence stays ONGOING forever and is
386
+ * never billed, so if you tag outcome signals with a `composite`, you must
387
+ * still call this.)
326
388
  *
327
389
  * Closing twice is safe: the second call answers `ALREADY_COMPLETED` and
328
390
  * cannot bill again. Calling it EARLY is also safe — before every entitlement
package/src/types.ts CHANGED
@@ -140,7 +140,14 @@ export interface WalletSignal extends SignalBase {
140
140
  /**
141
141
  * Records one step of a multi-call outcome run. The `agentKey` identifies the
142
142
  * step (and the outcome); `runId` groups every signal of a single run, so a step
143
- * may repeat freely. The run bills once, when the caller marks it `complete`.
143
+ * may repeat freely. The run bills once, when the caller closes it with
144
+ * `signals.completeOutcome()` — a SEPARATE, dedicated call, never a flag on a
145
+ * step signal.
146
+ *
147
+ * There is no `complete` field here. `POST /api/v1/signal/outcome` (what this
148
+ * type posts to) now accepts ONLY the step shape and rejects a body that also
149
+ * carries `complete` — see {@link OutcomeCompletionInput} /
150
+ * `signals.completeOutcome()`, its own dedicated route.
144
151
  */
145
152
  export interface OutcomeSignal extends SignalBase {
146
153
  type: "outcome";
@@ -152,17 +159,6 @@ export interface OutcomeSignal extends SignalBase {
152
159
  * outcome). Opaque to ClockNext — any string you can correlate; unique per org.
153
160
  */
154
161
  runId: string;
155
- /**
156
- * Set `true` on the LAST step signal to declare the run finished — completion
157
- * is never inferred from step coverage. Sending it twice is a safe no-op and
158
- * can't bill again.
159
- *
160
- * This closes the RUN only. If the signal also carries a `composite`, that
161
- * occurrence stays open and unbilled until you call
162
- * `signals.completeComposite()` — the two completions are separate, and
163
- * neither implies the other.
164
- */
165
- complete?: boolean;
166
162
  /** Optional composite this signal belongs to. See {@link CompositeTag}. */
167
163
  composite?: CompositeTag;
168
164
  }
@@ -174,6 +170,58 @@ export type CreditInput = Omit<CreditSignal, "type">;
174
170
  export type WalletInput = Omit<WalletSignal, "type">;
175
171
  export type OutcomeInput = Omit<OutcomeSignal, "type">;
176
172
 
173
+ /**
174
+ * Input for `signals.completeOutcome()` — closes ONE outcome run, the outcome
175
+ * mirror of {@link CompositeCompletionInput}. No `agentKey`, no `usage`: this
176
+ * closes an EXISTING run that an earlier `signals.outcome()` step already
177
+ * opened, so there is no step behind this call and nothing here to price.
178
+ *
179
+ * This closes the RUN only. If a step of this run also carried a `composite`,
180
+ * that occurrence stays open and unbilled until you separately call
181
+ * `signals.completeComposite()` — the two completions are independent, and
182
+ * neither implies the other.
183
+ */
184
+ export interface OutcomeCompletionInput {
185
+ customerId: string;
186
+ /** The same `runId` the run's step signals were sent with. */
187
+ runId: string;
188
+ /** Optional member (email) this completion is attributed to. */
189
+ member?: string;
190
+ /** Idempotency key, auto-generated if omitted. Sending `complete` twice for
191
+ * the same run is already a safe no-op server-side, so this only collapses
192
+ * a retry into one attempt rather than protecting against a double bill. */
193
+ idempotencyKey?: string;
194
+ }
195
+
196
+ /**
197
+ * The result of `signals.completeOutcome()`.
198
+ *
199
+ * Unlike {@link CompositeCompletionResult}, `ALREADY_COMPLETED` is NOT what a
200
+ * normal second ping for the same run gets back — that is rejected outright
201
+ * (a permanent error: the run has already been billed, and completing it again
202
+ * would bill it twice). `ALREADY_COMPLETED` is reserved for the much rarer case
203
+ * of two concurrent deliveries of the exact same request (e.g. a client-side
204
+ * retry racing the original), which really is a harmless echo.
205
+ */
206
+ export interface OutcomeCompletionResult {
207
+ /** How far the ping got. Never `"buffered"` — completions are always sent
208
+ * immediately. See {@link TrackResult.status}. */
209
+ delivery: Extract<SignalDelivery, "accepted" | "recorded">;
210
+ /** True when the ping was accepted but not yet applied (`202`). Prefer
211
+ * {@link OutcomeCompletionResult.delivery}. */
212
+ queued: boolean;
213
+ /** `COMPLETED` on the first close, `ALREADY_COMPLETED` only on the rare
214
+ * concurrent-replay case described above. Absent when the ping was queued. */
215
+ status?: "COMPLETED" | "ALREADY_COMPLETED";
216
+ /** The priced usage log, when the server closed the run inline. `null` on a
217
+ * `202` hand-off. */
218
+ usageLog?: SerializedUsageLog | null;
219
+ /** The durable inbox id — see {@link TrackResult.rawId}. */
220
+ rawId?: string;
221
+ /** The queue message id, present only on a `202` hand-off. */
222
+ messageId?: string;
223
+ }
224
+
177
225
  // ---------------------------------------------------------------------------
178
226
  // Server response shapes (mirrors src/lib/usage/log.ts)
179
227
  // ---------------------------------------------------------------------------
@@ -278,8 +326,10 @@ export interface TrackResult {
278
326
  * `"accepted"` — the server has written it to its durable inbox (`202`)
279
327
  * and will price it shortly. It can no longer be lost, but
280
328
  * it is not billed yet and `usageLog` is null. A rejection
281
- * discovered during pricing will NOT come back to you here —
282
- * read it back with `signals.list()`.
329
+ * discovered during pricing will NOT come back to you here,
330
+ * nor on `onError`. `signals.list()` shows what got priced;
331
+ * the dashboard's signal log — owner-only, by design — is
332
+ * where an individual rejection says why.
283
333
  * `"recorded"` — the server priced it inline and `usageLog` is populated.
284
334
  * `"dropped"` — the SDK threw it away without sending: the client is
285
335
  * closed, or the local queue is full. `onDrop` fired with
@@ -976,7 +1026,9 @@ export interface WalletEntryInput {
976
1026
  }
977
1027
 
978
1028
  /** Grant (positive) or claw back (negative) a credit balance. `delta` must be a
979
- * non-zero whole number. The credit is addressed by its stable `agentKey`. */
1029
+ * non-zero whole number. The credit is addressed by its stable `agentKey`. A
1030
+ * reduce is never rejected or floored for going below 0 — the resulting
1031
+ * balance may read negative, same as a real-usage overdraft already can. */
980
1032
  export interface CreditAdjustInput {
981
1033
  /** The credit's stable agent key (the same key used to record usage). */
982
1034
  agentKey: string;
@@ -984,10 +1036,12 @@ export interface CreditAdjustInput {
984
1036
  notes?: string;
985
1037
  }
986
1038
 
987
- /** Grant/claw an outcome balance. `delta` is a non-zero whole number; floored at
988
- * 0. The outcome is addressed here by a STEP's `agentKey`, which resolves to its
1039
+ /** Grant/claw an outcome balance. `delta` is a non-zero whole number. The
1040
+ * outcome is addressed here by a STEP's `agentKey`, which resolves to its
989
1041
  * parent outcome — NOT by the outcome's own `agentKey` ({@link Outcome.agentKey}),
990
- * which this endpoint does not accept. */
1042
+ * which this endpoint does not accept. A reduce is never rejected or floored
1043
+ * for going below 0 — the resulting balance may read negative, same as a
1044
+ * real-usage overdraft already can. */
991
1045
  export interface OutcomeAdjustInput {
992
1046
  /** An outcome STEP's stable agent key — identifies the parent outcome. Passing
993
1047
  * the outcome's own `agentKey` here fails with `422`. */
@@ -996,8 +1050,10 @@ export interface OutcomeAdjustInput {
996
1050
  notes?: string;
997
1051
  }
998
1052
 
999
- /** Grant/claw a unit balance. `delta` is non-zero (fractional allowed); floored
1000
- * at 0. The unit is addressed by its stable `agentKey`. */
1053
+ /** Grant/claw a unit balance. `delta` is non-zero (fractional allowed). The
1054
+ * unit is addressed by its stable `agentKey`. A reduce is never rejected or
1055
+ * floored for going below 0 — the resulting balance may read negative, same
1056
+ * as a real-usage overdraft already can. */
1001
1057
  export interface UnitAdjustInput {
1002
1058
  /** The unit's stable agent key (the same key used to record usage). */
1003
1059
  agentKey: string;
@@ -1005,6 +1061,26 @@ export interface UnitAdjustInput {
1005
1061
  notes?: string;
1006
1062
  }
1007
1063
 
1064
+ /**
1065
+ * Grant (positive) or claw back (negative) slots on a customer's ADVANCE
1066
+ * composite (pricing metric) pool for the CURRENT billing cycle. `delta` is a
1067
+ * non-zero whole number. A reduce is never rejected or floored for going below
1068
+ * 0 — same as credit/outcome/unit adjustments; the resulting pool may read
1069
+ * negative. Fails if the customer has no active ADVANCE purchase component
1070
+ * selling this composite.
1071
+ */
1072
+ export interface CompositeAdjustInput {
1073
+ /**
1074
+ * The composite's stable identifier — its `refId` (see {@link Composite.refId}),
1075
+ * the same value signals tag it with, NOT `agentKey` (a composite has no
1076
+ * `agentKey` column of its own). Named `agentKey` on the wire only so the
1077
+ * four adjust endpoints share one request shape.
1078
+ */
1079
+ agentKey: string;
1080
+ delta: number;
1081
+ notes?: string;
1082
+ }
1083
+
1008
1084
  // ---------------------------------------------------------------------------
1009
1085
  // Entitlement catalogs: Credits, Outcomes, Units
1010
1086
  // ---------------------------------------------------------------------------