@clocknext/sdk 0.1.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 +161 -0
- package/dist/index.cjs +596 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +580 -0
- package/dist/index.d.ts +580 -0
- package/dist/index.js +582 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# @clocknext/sdk
|
|
2
|
+
|
|
3
|
+
Official ClockNext SDK for Node/TypeScript. Record **usage signals** and manage
|
|
4
|
+
**customers** from your backend.
|
|
5
|
+
|
|
6
|
+
> **Server-side only.** Your `cnk_…` API key has org-wide write access — never
|
|
7
|
+
> ship it to a browser. For embedding the customer portal in a browser, mint a
|
|
8
|
+
> short-lived token with `cnk.portal.createToken()`.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @clocknext/sdk
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Requires Node ≥ 18 (native `fetch`). Zero runtime dependencies.
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { ClockNext } from "@clocknext/sdk";
|
|
22
|
+
|
|
23
|
+
const cnk = new ClockNext({ apiKey: process.env.CLOCKNEXT_API_KEY! });
|
|
24
|
+
|
|
25
|
+
await cnk.signals.credit({
|
|
26
|
+
customerId: "cus_123",
|
|
27
|
+
model: "llama-3.3-70b-versatile", // catalog model id (OrgModel.modelId)
|
|
28
|
+
key: "api_credit", // the credit's stable key
|
|
29
|
+
member: "alice@acme.com",
|
|
30
|
+
tokens: { input: 1200, output: 340, cache: 0 },
|
|
31
|
+
custom: { feature: "summarize" },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Before a serverless function returns (or on shutdown):
|
|
35
|
+
await cnk.flush();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Recording signals
|
|
39
|
+
|
|
40
|
+
There are three signal types, matching ClockNext's billable primitives. Each has
|
|
41
|
+
a typed helper that enforces the right required fields at compile time.
|
|
42
|
+
|
|
43
|
+
`model` is always the **catalog model id** enabled in your org (matched
|
|
44
|
+
case-insensitively against `OrgModel.modelId`). Credits and outcomes are
|
|
45
|
+
referenced by their **stable `key`** (assigned when you create the credit /
|
|
46
|
+
outcome step), not by display name.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// Credit — metered against a credit (by key); charged at the model's real cost + your margin
|
|
50
|
+
await cnk.signals.credit({ customerId, model: "llama-3.3-70b-versatile", key: "api_credit", tokens: { input: 1200, output: 340 } });
|
|
51
|
+
|
|
52
|
+
// Wallet — debits the customer's USD wallet at model cost
|
|
53
|
+
await cnk.signals.wallet({ customerId, model: "openai/gpt-oss-120b", tokens: { input: 800, output: 5000 } });
|
|
54
|
+
|
|
55
|
+
// Outcome — advances one step of a workflow (key = the OutcomeStep's key)
|
|
56
|
+
await cnk.signals.outcome({ customerId, model: "llama-3.3-70b-versatile", key: "resolved_ticket.triage" });
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
> Usage `/api/v1/usage` responses use the envelope
|
|
60
|
+
> `{ statusCode, statusDetail, result }`; customer endpoints currently use
|
|
61
|
+
> `{ ok, … }`. The SDK unwraps both for you.
|
|
62
|
+
|
|
63
|
+
### Async vs sync
|
|
64
|
+
|
|
65
|
+
By default the client is in **`async`** mode: `track()` buffers the signal and
|
|
66
|
+
returns immediately (`{ queued: true }`), and a background flusher sends batches
|
|
67
|
+
on a size/interval trigger. This keeps billing telemetry off your LLM hot path.
|
|
68
|
+
|
|
69
|
+
To get the computed result back (and any real-time allowance rejection), either:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// per call
|
|
73
|
+
const res = await cnk.signals.credit({ … }, { wait: true });
|
|
74
|
+
console.log(res.usageLog);
|
|
75
|
+
|
|
76
|
+
// or globally
|
|
77
|
+
const cnk = new ClockNext({ apiKey, mode: "sync" });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
> In `async` mode, server-side rejections (e.g. insufficient allowance, `422`)
|
|
81
|
+
> surface via the `onError` hook or `flush()`, not from the `track()` call.
|
|
82
|
+
|
|
83
|
+
### Reliability
|
|
84
|
+
|
|
85
|
+
- **Idempotency:** every signal carries an idempotency key (auto-generated once,
|
|
86
|
+
reused across retries) so a transport retry can't double-count. Pass your own
|
|
87
|
+
`idempotencyKey` for deterministic, cross-process dedup. *(Server-side dedup
|
|
88
|
+
enforcement is rolling out — see the SDK plan.)*
|
|
89
|
+
- **Retries:** transient failures (network, timeout, `408/409/429/5xx`) are
|
|
90
|
+
retried with exponential backoff + jitter, honoring `Retry-After`. Deterministic
|
|
91
|
+
`4xx` (`400/401/404/422`) are never retried.
|
|
92
|
+
- **Flushing:** call `await cnk.flush()` before a serverless return, and
|
|
93
|
+
`await cnk.close()` on graceful shutdown to drain the buffer.
|
|
94
|
+
|
|
95
|
+
## Customers
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const c = await cnk.customers.create({ name: "Acme", email: "ops@acme.com" });
|
|
99
|
+
const one = await cnk.customers.get(c.id);
|
|
100
|
+
const page = await cnk.customers.list({ limit: 50, q: "ac" });
|
|
101
|
+
await cnk.customers.update(c.id, { notes: "VIP" });
|
|
102
|
+
await cnk.customers.delete(c.id);
|
|
103
|
+
|
|
104
|
+
// iterate every customer (auto-follows the cursor)
|
|
105
|
+
for await (const cust of cnk.customers.iterate()) { /* … */ }
|
|
106
|
+
|
|
107
|
+
// insights
|
|
108
|
+
await cnk.customers.usage(c.id, { limit: 50 });
|
|
109
|
+
await cnk.customers.balances(c.id);
|
|
110
|
+
await cnk.customers.plan(c.id);
|
|
111
|
+
await cnk.customers.revenue(c.id, { range: "30d" });
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Customer portal token
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const { token, expiresAt } = await cnk.portal.createToken({ customerId: "cus_123", ttlSeconds: 600 });
|
|
118
|
+
// render <iframe src={`https://app.clocknext.com/portal/embed?token=${token}`} />
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Errors
|
|
122
|
+
|
|
123
|
+
All failures throw typed errors you can branch on:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { AllowanceError, AuthError, RateLimitError } from "@clocknext/sdk";
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await cnk.signals.credit({ … }, { wait: true });
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (err instanceof AllowanceError) {/* upsell */}
|
|
132
|
+
else if (err instanceof AuthError) {/* bad key */}
|
|
133
|
+
else if (err instanceof RateLimitError) {/* back off */}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`ValidationError (400)` · `AuthError (401)` · `NotFoundError (404)` ·
|
|
138
|
+
`PlanError (422)` · `AllowanceError (422)` · `ConflictError (409)` ·
|
|
139
|
+
`RateLimitError (429)` · `ServerError (5xx)` · `NetworkError` — all extend
|
|
140
|
+
`ClockNextError` (`.status`, `.retryable`, `.idempotencyKey`).
|
|
141
|
+
|
|
142
|
+
## Configuration
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
new ClockNext({
|
|
146
|
+
apiKey: "cnk_…", // required
|
|
147
|
+
baseUrl: "https://app.clocknext.com",
|
|
148
|
+
mode: "async", // "async" | "sync"
|
|
149
|
+
timeoutMs: 10_000,
|
|
150
|
+
batch: { maxSize: 20, maxIntervalMs: 2000, maxConcurrency: 5, maxQueueSize: 10_000 },
|
|
151
|
+
retry: { maxAttempts: 5, baseDelayMs: 200, maxDelayMs: 10_000 },
|
|
152
|
+
onError: (err, signal) => {},
|
|
153
|
+
onFlush: (count) => {},
|
|
154
|
+
onRetry: ({ attempt, delayMs }) => {},
|
|
155
|
+
onDrop: (signal, reason) => {},
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT
|