@atribu/sdk 0.2.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/LICENSE +21 -0
  3. package/README.md +431 -0
  4. package/dist/admin/index.cjs +326 -0
  5. package/dist/admin/index.cjs.map +1 -0
  6. package/dist/admin/index.d.cts +46 -0
  7. package/dist/admin/index.d.ts +46 -0
  8. package/dist/admin/index.js +323 -0
  9. package/dist/admin/index.js.map +1 -0
  10. package/dist/api.d-BXINTQo6.d.cts +3547 -0
  11. package/dist/api.d-BXINTQo6.d.ts +3547 -0
  12. package/dist/errors-D3ApBz8J.d.cts +86 -0
  13. package/dist/errors-D3ApBz8J.d.ts +86 -0
  14. package/dist/index.cjs +549 -0
  15. package/dist/index.cjs.map +1 -0
  16. package/dist/index.d.cts +198 -0
  17. package/dist/index.d.ts +198 -0
  18. package/dist/index.js +536 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/next/index.cjs +153 -0
  21. package/dist/next/index.cjs.map +1 -0
  22. package/dist/next/index.d.cts +43 -0
  23. package/dist/next/index.d.ts +43 -0
  24. package/dist/next/index.js +151 -0
  25. package/dist/next/index.js.map +1 -0
  26. package/dist/oauth/index.cjs +299 -0
  27. package/dist/oauth/index.cjs.map +1 -0
  28. package/dist/oauth/index.d.cts +117 -0
  29. package/dist/oauth/index.d.ts +117 -0
  30. package/dist/oauth/index.js +291 -0
  31. package/dist/oauth/index.js.map +1 -0
  32. package/dist/test/index.cjs +443 -0
  33. package/dist/test/index.cjs.map +1 -0
  34. package/dist/test/index.d.cts +321 -0
  35. package/dist/test/index.d.ts +321 -0
  36. package/dist/test/index.js +437 -0
  37. package/dist/test/index.js.map +1 -0
  38. package/dist/types-Dc6tIN_V.d.cts +101 -0
  39. package/dist/types-Dc6tIN_V.d.ts +101 -0
  40. package/dist/webhooks/index.cjs +97 -0
  41. package/dist/webhooks/index.cjs.map +1 -0
  42. package/dist/webhooks/index.d.cts +35 -0
  43. package/dist/webhooks/index.d.ts +35 -0
  44. package/dist/webhooks/index.js +94 -0
  45. package/dist/webhooks/index.js.map +1 -0
  46. package/package.json +102 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,41 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@atribu/sdk` will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.2.0] — 2026-05-15
11
+
12
+ DX polish bundle. No breaking changes to v0.1.0 — every new feature is opt-in or additive.
13
+
14
+ ### Added
15
+
16
+ - **Opt-in retry layer**: `new AtribuClient({...}).withRetry({ maxAttempts, backoff, baseDelayMs, jitter })`. Respects the typed retry hint — only retries `retry` / `retry_after` actions, never `do_not_retry` / `fix_and_retry` / `refresh_token`. Honors `Retry-After` exactly (no jitter); applies jittered exponential / fixed backoff otherwise.
17
+ - **`isRetryableError(err)`** helper for consumers building their own retry logic.
18
+ - **`@atribu/sdk/test` subpath** with MSW v2 handlers (`atribuMockHandlers()`) covering every OAuth-consumer endpoint, plus `eventFixtures` and `responseFixtures` for driving handler tests with realistic event shapes. Deep-merge overrides for any field.
19
+ - **Compile-time type tests** via `vitest/expectTypeOf` — 12 narrowing assertions on the webhook event union, message content union, error hierarchy, retry-hint shape, and client surface. Fail-fast on accidental type-shape drift.
20
+ - **OpenTelemetry recipe** in README — drop-in `tracedFetch` wrapper threading `traceparent`, surfacing `X-Request-Id` as a span attribute, recording HTTP status / errors. Works with any OTel-compatible tracer (Datadog APM, Sentry, native).
21
+
22
+ ### Changed
23
+
24
+ - Resources now accept a `HttpClientLike` interface instead of the concrete `HttpClient` class so the retry wrapper stacks cleanly on top. Pure internal refactor, public API unchanged.
25
+ - `msw` added as optional peer dependency (for `@atribu/sdk/test` users only).
26
+
27
+ ## [0.1.0] — 2026-05-15
28
+
29
+ Initial pre-1.0 release. OAuth-consumer surface only — analytics endpoints land in a separate SDK once Atribu's add-on monetization path is built.
30
+
31
+ ### Added
32
+
33
+ - `AtribuClient` with typed `messages.send`, `comments.reply`, `comments.privateReply`, `webhooks.subscriptions.{list,create,update,delete,rotateSecret,test}`, `webhooks.deliveries.replay`.
34
+ - `@atribu/sdk/webhooks` — `verifyWebhook` via Web Crypto (Node + Edge runtimes), rotation grace, timestamp tolerance, constant-time HMAC compare.
35
+ - `@atribu/sdk/oauth` — `buildAuthorizeUrl`, `exchangeCode`, `revokeToken`, `signIdTokenHint` (jose), `generateCodeVerifier`, `computeCodeChallenge` (PKCE RFC 7636).
36
+ - `@atribu/sdk/admin` — `AtribuAdminClient` with `oauthApps.{create,update,suspend,rotateClientSecret,rotateJwtSecret}`.
37
+ - `@atribu/sdk/next` — `withAtribuWebhook` HOF for Next.js App Router route handlers.
38
+ - Typed error hierarchy: `AtribuError`, `AtribuApiError`, `AtribuOauthError`, `AtribuWebhookError`, `AtribuTransportError`, `AtribuConfigError`.
39
+ - Retry hint surfaced on every `AtribuApiError` (`retry`, `retry_after`, `refresh_token`, `fix_and_retry`, `do_not_retry`). SDK does not auto-retry.
40
+ - `Idempotency-Key` automatically sent on every mutating call.
41
+ - OpenAPI-driven request/response types — zero drift from server.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Atribu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,431 @@
1
+ # @atribu/sdk
2
+
3
+ Official TypeScript SDK for the Atribu OAuth-consumer API. Send WhatsApp + Instagram messages on behalf of users who authorized your app, verify signed webhook deliveries, and run the consumer-side OAuth 2.0 flow without rolling your own JWT/PKCE crypto.
4
+
5
+ Runs on Node 18+, Bun, Deno, Vercel Edge, and Cloudflare Workers via Web Crypto. No browser support — API keys don't belong in client-side JS.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @atribu/sdk
11
+ # optional, only needed for the OAuth /oauth helpers:
12
+ npm install jose
13
+ ```
14
+
15
+ ## Quickstart — send a message
16
+
17
+ ```ts
18
+ import { AtribuClient } from "@atribu/sdk";
19
+
20
+ const atribu = new AtribuClient({
21
+ apiKey: process.env.ATRIBU_API_KEY!, // "atb_live_..."
22
+ // baseUrl: "http://localhost:3000", // for local dev
23
+ });
24
+
25
+ const result = await atribu.messages.send({
26
+ connection_id: "11111111-1111-1111-1111-111111111111",
27
+ channel: "whatsapp",
28
+ to: "+15551234567",
29
+ content: { type: "text", text: "Hi! Thanks for reaching out." },
30
+ });
31
+
32
+ console.log(result.provider_message_id);
33
+ ```
34
+
35
+ ## Verifying inbound webhooks
36
+
37
+ ```ts
38
+ // app/api/atribu-webhook/route.ts (Next.js App Router)
39
+ import { withAtribuWebhook } from "@atribu/sdk/next";
40
+
41
+ export const POST = withAtribuWebhook({
42
+ secret: process.env.ATRIBU_WEBHOOK_SECRET!,
43
+ previousSecret: process.env.ATRIBU_WEBHOOK_PREVIOUS_SECRET, // optional, during rotation
44
+ onEvent: async (event) => {
45
+ if (event.type === "message.received" && event.provider === "whatsapp") {
46
+ console.log("new WA message:", event.data.text);
47
+ // event.data is typed: { wa_message_id, from, to, contact_name, type, text, raw }
48
+ }
49
+ },
50
+ });
51
+ ```
52
+
53
+ Or roll your own handler:
54
+
55
+ ```ts
56
+ import { verifyWebhook } from "@atribu/sdk/webhooks";
57
+
58
+ export async function POST(req: Request) {
59
+ const rawBody = await req.text();
60
+ const signature = req.headers.get("x-atribu-signature");
61
+ try {
62
+ const event = await verifyWebhook({
63
+ rawBody,
64
+ signature,
65
+ secret: process.env.ATRIBU_WEBHOOK_SECRET!,
66
+ });
67
+ // handle event ...
68
+ return new Response(null, { status: 200 });
69
+ } catch {
70
+ return new Response("invalid signature", { status: 401 });
71
+ }
72
+ }
73
+ ```
74
+
75
+ Atribu signs every delivery as `X-Atribu-Signature: t=<unix>,v1=<hex_hmac_sha256>` over `<t>.<rawBody>` (Stripe-style). The verifier:
76
+
77
+ - enforces a timestamp tolerance (default 5 minutes) to defend against replay
78
+ - constant-time compares the HMAC
79
+ - accepts the `previousSecret` during rotation grace so you can dual-verify safely
80
+
81
+ The unique `event.id` plus the `X-Atribu-Delivery-Id` header give you idempotency keys for safe redelivery.
82
+
83
+ ## Consumer-side OAuth flow
84
+
85
+ If you're building an app that connects your end-users' WhatsApp/Instagram accounts via Atribu (Zernio-style), the `/oauth` subpath has every helper you need.
86
+
87
+ ```ts
88
+ import {
89
+ signIdTokenHint,
90
+ buildAuthorizeUrl,
91
+ generateCodeVerifier,
92
+ computeCodeChallenge,
93
+ exchangeCode,
94
+ revokeToken,
95
+ } from "@atribu/sdk/oauth";
96
+
97
+ // 1. Redirect the user to Atribu's consent page
98
+ const idTokenHint = await signIdTokenHint({
99
+ jwtSigningSecret: process.env.ATRIBU_APP_JWT_SECRET!,
100
+ subject: user.id,
101
+ email: user.email,
102
+ expiresIn: "5m",
103
+ });
104
+
105
+ const codeVerifier = generateCodeVerifier();
106
+ const codeChallenge = await computeCodeChallenge(codeVerifier);
107
+
108
+ const authorizeUrl = buildAuthorizeUrl({
109
+ clientId: "vitrina",
110
+ redirectUri: "https://vitrina.app/integrations/atribu/callback",
111
+ provider: "whatsapp",
112
+ scope: "whatsapp",
113
+ state: csrfToken,
114
+ idTokenHint,
115
+ codeChallenge,
116
+ codeChallengeMethod: "S256",
117
+ });
118
+
119
+ // 2. In your /callback handler:
120
+ const { accessToken, connectionId, scope, profileId } = await exchangeCode({
121
+ clientId: "vitrina",
122
+ clientSecret: process.env.ATRIBU_APP_CLIENT_SECRET!,
123
+ code: callbackQuery.code,
124
+ redirectUri: "https://vitrina.app/integrations/atribu/callback",
125
+ codeVerifier,
126
+ });
127
+
128
+ // store accessToken (it's an Atribu API key) + connectionId per user
129
+
130
+ // 3. To revoke later (e.g. user disconnects):
131
+ await revokeToken({
132
+ clientId: "vitrina",
133
+ clientSecret: process.env.ATRIBU_APP_CLIENT_SECRET!,
134
+ token: accessToken,
135
+ });
136
+ ```
137
+
138
+ ## Managing webhook subscriptions
139
+
140
+ ```ts
141
+ // the OAuth flow creates a subscription automatically; manage it after:
142
+ const subs = await atribu.webhooks.subscriptions.list();
143
+
144
+ await atribu.webhooks.subscriptions.update(subs[0].id, {
145
+ url: "https://vitrina.app/api/atribu-webhook-v2",
146
+ events: ["message.received", "message.delivery"],
147
+ providers: ["whatsapp", "instagram"],
148
+ });
149
+
150
+ // rotate the HMAC secret — capture the new secret and deploy dual-verify BEFORE rotating
151
+ const rotated = await atribu.webhooks.subscriptions.rotateSecret(subs[0].id, { grace_days: 14 });
152
+ console.log("new webhook secret (shown once):", rotated.secret);
153
+
154
+ // fire a synthetic event to test your handler
155
+ await atribu.webhooks.subscriptions.test(subs[0].id);
156
+
157
+ // re-deliver a dead webhook
158
+ await atribu.webhooks.deliveries.replay(deadDeliveryId);
159
+ ```
160
+
161
+ ## IG comment replies
162
+
163
+ ```ts
164
+ // public reply on the comment thread
165
+ await atribu.comments.reply({
166
+ comment_id: "ig_comment_id",
167
+ connection_id: connectionId,
168
+ text: "Thanks for the message! DMing you details.",
169
+ });
170
+
171
+ // private DM to the user who left the comment
172
+ await atribu.comments.privateReply({
173
+ comment_id: "ig_comment_id",
174
+ connection_id: connectionId,
175
+ text: "Here are the details you asked about ...",
176
+ });
177
+ ```
178
+
179
+ ## Opt-in retry
180
+
181
+ The SDK doesn't retry automatically (hiding retries amplifies load on a failing server and obscures backpressure). Opt in per-client when you want it:
182
+
183
+ ```ts
184
+ import { AtribuClient } from "@atribu/sdk";
185
+
186
+ const client = new AtribuClient({ apiKey: process.env.ATRIBU_API_KEY! })
187
+ .withRetry({
188
+ maxAttempts: 3, // initial + 2 retries
189
+ backoff: "exponential", // or "fixed" or "none"
190
+ baseDelayMs: 500, // first retry waits ~500ms (+ jitter)
191
+ maxDelayMs: 30_000,
192
+ jitter: 0.3, // 0..1, multiplies delay by 1+random*jitter
193
+ });
194
+
195
+ await client.messages.send({ ... }); // retries on 5xx / 408 / 429 / network glitches
196
+ ```
197
+
198
+ What gets retried:
199
+
200
+ | Condition | Behavior |
201
+ |---|---|
202
+ | 5xx, 408, `AtribuTransportError` (network) | Exponential backoff, jittered |
203
+ | 429 / 503 with `Retry-After` | Honors the server's instruction exactly (no jitter) |
204
+ | 401 (`refresh_token` hint) | **Not retried** — you need to refresh credentials, not retry |
205
+ | 422 (`fix_and_retry` hint) | **Not retried** — your input is bad; retrying won't help |
206
+ | 403 (`do_not_retry` hint) | **Not retried** — permission denied |
207
+
208
+ `.withRetry()` returns a *new* client; the original is untouched. Chain it once at client construction and you're done.
209
+
210
+ If you want to inspect retryability without using the wrapper:
211
+
212
+ ```ts
213
+ import { isRetryableError } from "@atribu/sdk";
214
+
215
+ try {
216
+ await client.messages.send({ ... });
217
+ } catch (err) {
218
+ if (isRetryableError(err)) {
219
+ // queue this for retry on your side
220
+ } else {
221
+ throw err;
222
+ }
223
+ }
224
+ ```
225
+
226
+ ## Testing with `@atribu/sdk/test`
227
+
228
+ A drop-in MSW v2 setup so you can mock every Atribu endpoint with three lines:
229
+
230
+ ```ts
231
+ // Install once: npm i -D msw
232
+ import { afterAll, afterEach, beforeAll } from "vitest";
233
+ import { setupServer } from "msw/node";
234
+ import { atribuMockHandlers, fixtures } from "@atribu/sdk/test";
235
+
236
+ const server = setupServer(...atribuMockHandlers());
237
+
238
+ beforeAll(() => server.listen());
239
+ afterEach(() => server.resetHandlers());
240
+ afterAll(() => server.close());
241
+ ```
242
+
243
+ Every endpoint defaults to a realistic happy-path response. Override specific endpoints:
244
+
245
+ ```ts
246
+ const server = setupServer(
247
+ ...atribuMockHandlers({
248
+ messages: {
249
+ send: {
250
+ status: 422,
251
+ body: { error: { code: "validation_error", message: "bad content", status: 422 } },
252
+ },
253
+ },
254
+ }),
255
+ );
256
+ ```
257
+
258
+ Or generate event payloads for your own webhook handler tests:
259
+
260
+ ```ts
261
+ import { eventFixtures } from "@atribu/sdk/test";
262
+
263
+ const event = eventFixtures.whatsappMessageReceived({
264
+ data: { text: "Custom test message", from: "+15551111111" },
265
+ });
266
+
267
+ // drive your handler with it
268
+ await myWebhookHandler(event);
269
+ ```
270
+
271
+ Available fixtures: `eventFixtures.{whatsappMessageReceived, whatsappMessageDelivery, instagramFbLoginMessage, instagramPostback, instagramIgLoginChange, instagramMessageDelivery}`. All deep-merge their overrides into a sensible default.
272
+
273
+ ## Error handling
274
+
275
+ Every API error is an `AtribuApiError` with a typed `code` + machine-readable `retry` hint. **The SDK never retries automatically** — your queue / job system decides whether to act on the hint.
276
+
277
+ ```ts
278
+ import { AtribuApiError } from "@atribu/sdk";
279
+
280
+ try {
281
+ await atribu.messages.send({ ... });
282
+ } catch (err) {
283
+ if (err instanceof AtribuApiError) {
284
+ switch (err.retry.action) {
285
+ case "retry": // 5xx, transient
286
+ return queue.retry(job, { delay: 5_000 });
287
+ case "retry_after": // 429 with Retry-After
288
+ return queue.retry(job, { delay: err.retry.retryAfterMs });
289
+ case "refresh_token": // 401
290
+ return refreshOAuthAndRetry();
291
+ case "fix_and_retry": // 422, 4xx caller error
292
+ return logger.error("bad payload", { err, requestId: err.requestId });
293
+ case "do_not_retry": // 403, etc.
294
+ return logger.error("permanent failure", { err, requestId: err.requestId });
295
+ }
296
+ }
297
+ throw err;
298
+ }
299
+ ```
300
+
301
+ | Error class | When it's thrown |
302
+ | --- | --- |
303
+ | `AtribuApiError` | The server returned a non-2xx response for a `/api/v1/*` route. Has `code`, `status`, `requestId`, `retry`, `responseBody`. |
304
+ | `AtribuOauthError` | The OAuth `/oauth/token` or `/oauth/revoke` endpoint returned an RFC-shaped error. Has `code`, `description`, `status`. |
305
+ | `AtribuWebhookError` | A webhook signature failed verification. Has `code: "missing_signature" | "malformed_header" | "expired_timestamp" | "invalid_signature"`. |
306
+ | `AtribuTransportError` | The HTTP request itself failed (network, abort, timeout). Has `cause`. |
307
+ | `AtribuConfigError` | The SDK was configured incorrectly (missing API key, no `fetch` available, etc.). |
308
+
309
+ Every successful request and every `AtribuApiError` surfaces the server's `X-Request-Id` header on `err.requestId` so you can grep server logs for "why".
310
+
311
+ ## Configuration
312
+
313
+ ```ts
314
+ new AtribuClient({
315
+ apiKey: string, // required
316
+ baseUrl?: string, // default "https://www.atribu.app"
317
+ fetch?: typeof fetch, // default globalThis.fetch
318
+ timeoutMs?: number, // default 30000
319
+ userAgent?: string, // appended after the SDK UA
320
+ defaultIdempotencyKeyGenerator?: () => string,
321
+ });
322
+ ```
323
+
324
+ - **Custom `fetch`**: useful for instrumentation (Datadog APM, OTel), for testing (msw), or for runtimes where `globalThis.fetch` isn't the default.
325
+ - **Timeout**: aborts the request via `AbortController` after the budget. Surface as `AtribuTransportError` with `name: "AbortError"` on the cause.
326
+ - **Idempotency keys**: every mutating call automatically sends `Idempotency-Key: <uuid>`. Override per-call via `{ idempotencyKey }` or globally with `defaultIdempotencyKeyGenerator`.
327
+ - **User-Agent**: format `@atribu/sdk/<version> (<runtime>)`. Server uses it for per-version analytics.
328
+
329
+ ## OpenTelemetry tracing
330
+
331
+ The custom-`fetch` hook is enough — wrap your tracer around it and every SDK call becomes a child span. No SDK changes needed:
332
+
333
+ ```ts
334
+ import { trace, SpanStatusCode, context, propagation } from "@opentelemetry/api";
335
+ import { AtribuClient } from "@atribu/sdk";
336
+
337
+ const tracer = trace.getTracer("my-app");
338
+
339
+ const tracedFetch: typeof fetch = async (input, init) => {
340
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
341
+ const method = init?.method ?? "GET";
342
+
343
+ return tracer.startActiveSpan(`atribu.${method.toLowerCase()} ${new URL(url).pathname}`, async (span) => {
344
+ span.setAttributes({
345
+ "http.method": method,
346
+ "http.url": url,
347
+ "peer.service": "atribu-api",
348
+ });
349
+
350
+ // Inject W3C traceparent headers so server-side spans link up.
351
+ const headers = new Headers(init?.headers);
352
+ propagation.inject(context.active(), headers, {
353
+ set: (carrier, key, value) => carrier.set(key, value),
354
+ });
355
+
356
+ try {
357
+ const res = await fetch(input, { ...init, headers });
358
+ span.setAttribute("http.status_code", res.status);
359
+ const requestId = res.headers.get("x-request-id");
360
+ if (requestId) span.setAttribute("atribu.request_id", requestId);
361
+ if (res.status >= 400) {
362
+ span.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${res.status}` });
363
+ }
364
+ return res;
365
+ } catch (err) {
366
+ span.recordException(err as Error);
367
+ span.setStatus({ code: SpanStatusCode.ERROR });
368
+ throw err;
369
+ } finally {
370
+ span.end();
371
+ }
372
+ });
373
+ };
374
+
375
+ const atribu = new AtribuClient({
376
+ apiKey: process.env.ATRIBU_API_KEY!,
377
+ fetch: tracedFetch,
378
+ });
379
+ ```
380
+
381
+ Atribu's `X-Request-Id` response header lands as an attribute on every span — same key the server logs use, so you can pivot from a span to the server log with a single grep. Works the same with Datadog APM (`@datadog/dd-trace`), Sentry tracing, or any other fetch-instrumentation pattern.
382
+
383
+ ## Admin client (Atribu staff)
384
+
385
+ If you're staff managing consumer apps, the `/admin` subpath wraps the `/api/v1/admin/oauth-apps/*` surface. Requires the `ATRIBU_ADMIN_SECRET`.
386
+
387
+ ```ts
388
+ import { AtribuAdminClient } from "@atribu/sdk/admin";
389
+
390
+ const admin = new AtribuAdminClient({
391
+ adminSecret: process.env.ATRIBU_ADMIN_SECRET!,
392
+ });
393
+
394
+ const app = await admin.oauthApps.create({
395
+ client_id: "vitrina",
396
+ name: "Vitrina",
397
+ redirect_uris: ["https://vitrina.app/integrations/atribu/callback"],
398
+ allowed_scopes: ["whatsapp", "instagram"],
399
+ created_by: "your-user-uuid",
400
+ });
401
+
402
+ // secrets are shown ONCE — store them safely
403
+ console.log(app.client_secret, app.jwt_signing_secret);
404
+
405
+ await admin.oauthApps.rotateClientSecret(app.id, { grace_days: 14 });
406
+ await admin.oauthApps.suspend(app.id); // kill-switch — revokes every api_key minted by this app
407
+ ```
408
+
409
+ ## Runtime support
410
+
411
+ | Runtime | Supported | Notes |
412
+ | --- | --- | --- |
413
+ | Node 18+ | ✅ | Uses `globalThis.fetch` + Web Crypto |
414
+ | Node 16 | ❌ | No native fetch; install `undici` and inject |
415
+ | Bun | ✅ | |
416
+ | Deno | ✅ | Import via `npm:@atribu/sdk` |
417
+ | Vercel Edge | ✅ | |
418
+ | Cloudflare Workers | ✅ | |
419
+ | Browser | ❌ by design | API keys belong on a server |
420
+
421
+ ## Versioning
422
+
423
+ SDK versions follow semver. Pre-1.0 (`0.x`) means the surface may still evolve based on real consumer feedback. Once Vitrina has shipped and the surface is stable, we'll bump to `1.0.0`.
424
+
425
+ ## Repository
426
+
427
+ Source: [github.com/atribu/atribu/tree/main/packages/sdk](https://github.com/atribu/atribu/tree/main/packages/sdk)
428
+
429
+ ## License
430
+
431
+ MIT