@vibiz/sdk 0.0.0-stage → 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.
package/dist/index.js ADDED
@@ -0,0 +1,1440 @@
1
+ // packages/sdk/src/client.ts
2
+ var VibizApiError = class extends Error {
3
+ constructor(status, code, message, details) {
4
+ super(message);
5
+ this.status = status;
6
+ this.code = code;
7
+ this.details = details;
8
+ this.name = "VibizApiError";
9
+ }
10
+ status;
11
+ code;
12
+ details;
13
+ };
14
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
15
+ var Vibiz = class {
16
+ constructor(options) {
17
+ this.options = options;
18
+ if (!options.baseUrl) {
19
+ throw new Error("No Vibiz API URL: pass baseUrl (CLI: --api, or VIBIZ_API_URL).");
20
+ }
21
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
22
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
23
+ }
24
+ options;
25
+ baseUrl;
26
+ fetchImpl;
27
+ async send(path, init = {}) {
28
+ const headers = { authorization: `Bearer ${this.options.apiKey}` };
29
+ if (init.body !== void 0) headers["content-type"] = "application/json";
30
+ if (init.raw) headers["content-type"] = init.contentType ?? "application/octet-stream";
31
+ if (init.idempotencyKey) headers["idempotency-key"] = init.idempotencyKey;
32
+ if (this.options.user) headers["x-vibiz-user"] = endUserHeader(this.options.user);
33
+ return this.fetchImpl(`${this.baseUrl}/v1${path}`, {
34
+ method: init.method ?? "GET",
35
+ headers,
36
+ body: init.form ?? init.raw ?? (init.body !== void 0 ? JSON.stringify(init.body) : void 0)
37
+ });
38
+ }
39
+ async fail(res) {
40
+ const json = await res.json().catch(() => null);
41
+ const e = json?.error;
42
+ throw new VibizApiError(
43
+ res.status,
44
+ e?.code ?? "http_error",
45
+ e?.message ?? `HTTP ${res.status}`,
46
+ e?.details
47
+ );
48
+ }
49
+ /** A raw response (audio, a stream), failing like `request` on an error status. */
50
+ async stream(path, init = {}) {
51
+ const res = await this.send(path, init);
52
+ if (!res.ok) return this.fail(res);
53
+ return res;
54
+ }
55
+ async request(path, init = {}) {
56
+ const res = await this.send(path, init);
57
+ if (res.status === 204) return void 0;
58
+ const json = await res.json().catch(() => null);
59
+ if (!res.ok) {
60
+ const e = json?.error;
61
+ throw new VibizApiError(
62
+ res.status,
63
+ e?.code ?? "http_error",
64
+ e?.message ?? `HTTP ${res.status}`,
65
+ e?.details
66
+ );
67
+ }
68
+ return json;
69
+ }
70
+ postAction(id, postId, action, body, env) {
71
+ return this.request(
72
+ `/projects/${id}/social/posts/${encodeURIComponent(postId)}/${action}${envQuery(env)}`,
73
+ { method: "POST", body }
74
+ );
75
+ }
76
+ projects = {
77
+ create: (input, opts = {}) => this.request("/projects", {
78
+ method: "POST",
79
+ body: input,
80
+ ...opts
81
+ }),
82
+ list: () => this.request("/projects").then((r) => r.data),
83
+ get: (id) => this.request(`/projects/${id}`),
84
+ delete: (id) => this.request(`/projects/${id}`, {
85
+ method: "DELETE"
86
+ }),
87
+ /** Undo a delete within 7 days: data was kept, the site is provisioned again. */
88
+ restore: (id) => this.request(`/projects/${id}/restore`, {
89
+ method: "POST"
90
+ }),
91
+ addResource: (id, type) => this.request(`/projects/${id}/resources`, {
92
+ method: "POST",
93
+ body: { type }
94
+ }),
95
+ /** Change a resource's settings (merged), e.g. `('replay', { sample_rate: 0.1 })`. */
96
+ configureResource: (id, type, config) => this.request(`/projects/${id}/resources/${type}`, {
97
+ method: "PATCH",
98
+ body: { config }
99
+ }),
100
+ /** Run a project-scoped action, such as `email.send`. */
101
+ runAction: (id, name, input, opts = {}) => this.request(
102
+ `/projects/${id}/actions/${name}`,
103
+ { method: "POST", body: { input }, ...opts }
104
+ ),
105
+ /** The production env. Owner keys only: an agent key gets `forbidden` (use `dev`). */
106
+ env: (id) => this.request(`/projects/${id}/env`).then((r) => r.env),
107
+ /** The production env's variable names, without values. Any key. */
108
+ envNames: (id) => this.request(`/projects/${id}/env?names=1`).then((r) => r.names),
109
+ /**
110
+ * The names plus the console links where an owner copies the values
111
+ * (`console_url`) or just the production app key (`app_key_console_url`).
112
+ */
113
+ envLinks: (id) => this.request(
114
+ `/projects/${id}/env?names=1`
115
+ ),
116
+ /**
117
+ * The development environment: its own database branch (schema, no
118
+ * production rows), its own sign-in and AI key. What agents and laptops use.
119
+ */
120
+ dev: {
121
+ get: (id) => this.request(`/projects/${id}/dev`),
122
+ start: (id) => this.request(`/projects/${id}/dev`, { method: "POST", body: {} }),
123
+ env: (id) => this.request(`/projects/${id}/dev/env`).then((r) => r.env),
124
+ remove: (id) => this.request(`/projects/${id}/dev`, { method: "DELETE" }),
125
+ /** Start it if needed and wait until ready, then return its env. */
126
+ ensure: async (id, opts = {}) => {
127
+ const deadline = Date.now() + (opts.timeoutMs ?? 5 * 6e4);
128
+ let state = await this.projects.dev.get(id);
129
+ if (state.status === "none" || state.status === "failed") {
130
+ state = await this.projects.dev.start(id);
131
+ }
132
+ while (state.status !== "ready") {
133
+ if (state.status === "failed") {
134
+ const bad = state.resources.find((r) => r.status === "failed");
135
+ throw new VibizApiError(
136
+ 409,
137
+ "dependency_not_ready",
138
+ bad?.error ?? "The development environment failed."
139
+ );
140
+ }
141
+ if (Date.now() > deadline) {
142
+ throw new VibizApiError(
143
+ 408,
144
+ "timeout",
145
+ "Timed out waiting for the development environment."
146
+ );
147
+ }
148
+ await sleep(opts.intervalMs ?? 2e3);
149
+ state = await this.projects.dev.get(id);
150
+ }
151
+ return this.projects.dev.env(id);
152
+ }
153
+ },
154
+ /** The app key (`vz_app_…`), per environment. The secret itself only ever travels in the env. */
155
+ appKeys: {
156
+ list: (id) => this.request(`/projects/${id}/app-keys`).then((r) => r.data),
157
+ /** Production's needs an owner key. The old key works for `grace_seconds` (default 3600). */
158
+ rotate: (id, opts = {}) => this.request(`/projects/${id}/app-keys/rotate`, { method: "POST", body: opts })
159
+ },
160
+ /** Hosts outside Vibiz that receive the env server to server. Connecting needs an owner key. */
161
+ hosts: {
162
+ list: (id) => this.request(`/projects/${id}/hosts`).then((r) => r.data),
163
+ add: (id, input) => this.request(`/projects/${id}/hosts`, { method: "POST", body: input }),
164
+ sync: (id) => this.request(`/projects/${id}/hosts/sync`, {
165
+ method: "POST",
166
+ body: {}
167
+ }).then((r) => r.data),
168
+ remove: (id, targetId, opts = {}) => this.request(
169
+ `/projects/${id}/hosts/${targetId}${opts.keepValues ? "?keep_values=1" : ""}`,
170
+ { method: "DELETE" }
171
+ )
172
+ },
173
+ /**
174
+ * The app's own database. `production` needs an owner key (it holds the
175
+ * app's users' data); agents read `development`, the default for them.
176
+ */
177
+ database: {
178
+ tables: (id, env) => this.request(
179
+ `/projects/${id}/database${env ? `?env=${env}` : ""}`
180
+ ),
181
+ /** Read-only: one SELECT / WITH / EXPLAIN / SHOW statement, 5 s, 500 rows. */
182
+ query: (id, sql, env) => this.request(`/projects/${id}/database/query`, {
183
+ method: "POST",
184
+ body: { sql, ...env ? { env } : {} }
185
+ })
186
+ },
187
+ /**
188
+ * How people sign in to the app (docs/SIGN-IN.md). `production` needs an
189
+ * owner key; agents work on `development`, the default for them.
190
+ */
191
+ auth: {
192
+ get: (id, env) => this.request(`/projects/${id}/auth/settings${env ? `?env=${env}` : ""}`),
193
+ update: (id, patch, env) => this.request(`/projects/${id}/auth/settings${env ? `?env=${env}` : ""}`, {
194
+ method: "PATCH",
195
+ body: patch
196
+ }),
197
+ /** Turn a social provider on, with shared keys or the customer's own OAuth app. */
198
+ setProvider: (id, provider, input, env) => this.request(
199
+ `/projects/${id}/auth/providers/${provider}${env ? `?env=${env}` : ""}`,
200
+ { method: "PUT", body: input }
201
+ ),
202
+ removeProvider: (id, provider, env) => this.request(
203
+ `/projects/${id}/auth/providers/${provider}${env ? `?env=${env}` : ""}`,
204
+ { method: "DELETE" }
205
+ ),
206
+ /** Owner only: a test message through the SMTP sender already set. */
207
+ testEmail: (id, to, env) => this.request(
208
+ `/projects/${id}/auth/test-email${env ? `?env=${env}` : ""}`,
209
+ { method: "POST", body: { to } }
210
+ )
211
+ },
212
+ /**
213
+ * The business behind the project (docs/COMPANY.md). `env` defaults like
214
+ * the API: production for owners, development for agents. Production
215
+ * submit, signing, actions and documents need an owner key.
216
+ */
217
+ company: {
218
+ get: (id, env) => this.request(`/projects/${id}/company${envQuery(env)}`),
219
+ /** Create or replace the draft. */
220
+ draft: (id, draft, env) => this.request(`/projects/${id}/company${envQuery(env)}`, {
221
+ method: "PUT",
222
+ body: draft
223
+ }),
224
+ /** Change part of the draft: top-level keys replace, `null` clears one. */
225
+ update: (id, patch, env) => this.request(`/projects/${id}/company${envQuery(env)}`, {
226
+ method: "PATCH",
227
+ body: patch
228
+ }),
229
+ remove: (id, env) => this.request(
230
+ `/projects/${id}/company${envQuery(env)}`,
231
+ { method: "DELETE" }
232
+ ),
233
+ /**
234
+ * Production files for real and charges the price: an owner key, `confirm: true`,
235
+ * and the `quote.total_cents` and `updated_at` of the draft that was reviewed
236
+ * (a draft or price changed since is refused with a 409, not filed).
237
+ */
238
+ submit: (id, input, env) => this.request(`/projects/${id}/company/submit${envQuery(env)}`, {
239
+ method: "POST",
240
+ body: input
241
+ }),
242
+ /**
243
+ * Pay a live filing by card instead of from the balance (owner keys): the same
244
+ * body as `submit`; it is filed when the payment arrives (docs/BILLING.md "Company").
245
+ */
246
+ checkout: (id, input) => this.request(`/projects/${id}/company/checkout?env=production`, {
247
+ method: "POST",
248
+ body: input
249
+ }),
250
+ /** A fresh signing link; show it to the signer right away, it voids the previous one. */
251
+ sign: (id, document, env) => this.request(
252
+ `/projects/${id}/company/signatures/${document}${envQuery(env)}`,
253
+ { method: "POST", body: {} }
254
+ ),
255
+ /** Answer a required action: new names, or the founder's own US address. */
256
+ resolve: (id, actionId, input, env) => this.request(
257
+ `/projects/${id}/company/actions/${encodeURIComponent(actionId)}${envQuery(env)}`,
258
+ { method: "POST", body: input }
259
+ ),
260
+ /** A short-lived download link. */
261
+ document: (id, documentId, env) => this.request(
262
+ `/projects/${id}/company/documents/${encodeURIComponent(documentId)}${envQuery(env)}`
263
+ ),
264
+ refresh: (id, env) => this.request(`/projects/${id}/company/refresh${envQuery(env)}`, {
265
+ method: "POST",
266
+ body: {}
267
+ }),
268
+ /** Development only: complete formation or the EIN at the sandbox. */
269
+ simulate: (id, step) => this.request(`/projects/${id}/company/simulate?env=development`, {
270
+ method: "POST",
271
+ body: { step }
272
+ })
273
+ },
274
+ /**
275
+ * Social (docs/SOCIAL.md): the project's own social and ad accounts. `env`
276
+ * defaults like the API: production for owners, development (a sandbox:
277
+ * nothing is posted or spent) for agents. On production an agent's posts
278
+ * and ad changes wait for the owner (`pending_approval`); an owner sends
279
+ * `confirm: true`.
280
+ */
281
+ social: {
282
+ get: (id, env) => this.request(`/projects/${id}/social${envQuery(env)}`),
283
+ accounts: (id, opts = {}) => this.request(
284
+ `/projects/${id}/social/accounts${qs({ env: opts.env, kind: opts.kind, platform: opts.platform })}`
285
+ ),
286
+ /**
287
+ * Production (the default): a link (1 h) for the owner to open, approve and
288
+ * sign in to the platform. Development: the test account, made at once.
289
+ */
290
+ connect: (id, platform, opts = {}) => this.request(
291
+ `/projects/${id}/social/connect${envQuery(opts.env)}`,
292
+ {
293
+ method: "POST",
294
+ body: { platform, ...opts.ads ? { ads: true } : {} }
295
+ }
296
+ ),
297
+ refreshAccounts: (id, env) => this.request(
298
+ `/projects/${id}/social/accounts/refresh${envQuery(env)}`,
299
+ { method: "POST", body: {} }
300
+ ),
301
+ disconnect: (id, accountId, env) => this.request(
302
+ `/projects/${id}/social/accounts/${encodeURIComponent(accountId)}${envQuery(env)}`,
303
+ { method: "DELETE" }
304
+ ),
305
+ accountAnalytics: (id, accountId, opts = {}) => this.request(
306
+ `/projects/${id}/social/accounts/${encodeURIComponent(accountId)}/analytics${qs(opts)}`
307
+ ),
308
+ /** Upload a file (bytes and their content type) → a URL to post with. */
309
+ upload: (id, file, env) => this.request(
310
+ `/projects/${id}/social/media${qs({ env, filename: file.filename })}`,
311
+ { method: "POST", raw: file.bytes, contentType: file.contentType }
312
+ ),
313
+ posts: (id, opts = {}) => this.request(`/projects/${id}/social/posts${qs(opts)}`),
314
+ post: (id, postId, env) => this.request(
315
+ `/projects/${id}/social/posts/${encodeURIComponent(postId)}${envQuery(env)}`
316
+ ),
317
+ create: (id, input, env) => this.request(`/projects/${id}/social/posts${envQuery(env)}`, {
318
+ method: "POST",
319
+ body: input
320
+ }),
321
+ publish: (id, postId, input = {}, env) => this.postAction(id, postId, "publish", input, env),
322
+ approve: (id, postId, input = {}, env) => this.postAction(id, postId, "approve", input, env),
323
+ reject: (id, postId, env) => this.postAction(id, postId, "reject", {}, env),
324
+ retry: (id, postId, input = {}, env) => this.postAction(id, postId, "retry", input, env),
325
+ refresh: (id, postId, env) => this.postAction(id, postId, "refresh", {}, env),
326
+ /** Cancel a draft, a waiting or a scheduled post. */
327
+ cancel: (id, postId, env) => this.request(
328
+ `/projects/${id}/social/posts/${encodeURIComponent(postId)}${envQuery(env)}`,
329
+ { method: "DELETE" }
330
+ ),
331
+ postAnalytics: (id, postId, env) => this.request(
332
+ `/projects/${id}/social/posts/${encodeURIComponent(postId)}/analytics${envQuery(env)}`
333
+ ),
334
+ campaigns: (id, opts = {}) => this.request(
335
+ `/projects/${id}/social/ads/campaigns${qs(opts)}`
336
+ ),
337
+ adChanges: (id, opts = {}) => this.request(
338
+ `/projects/${id}/social/ads/changes${qs(opts)}`
339
+ ),
340
+ adChange: (id, input, env) => this.request(`/projects/${id}/social/ads/changes${envQuery(env)}`, {
341
+ method: "POST",
342
+ body: input
343
+ }),
344
+ approveAdChange: (id, changeId, env) => this.request(
345
+ `/projects/${id}/social/ads/changes/${encodeURIComponent(changeId)}/approve${envQuery(env)}`,
346
+ { method: "POST", body: {} }
347
+ ),
348
+ rejectAdChange: (id, changeId, env) => this.request(
349
+ `/projects/${id}/social/ads/changes/${encodeURIComponent(changeId)}/reject${envQuery(env)}`,
350
+ { method: "POST", body: {} }
351
+ )
352
+ },
353
+ /**
354
+ * Payments (docs/PAYMENTS.md): Vibiz is the merchant, so an app sells from
355
+ * its first call. `env` defaults like the API: production for owners,
356
+ * development for agents. On production, refunds, cancels, withdrawing,
357
+ * payout setup and webhook settings need an owner key.
358
+ */
359
+ payments: {
360
+ get: (id, env) => this.request(`/projects/${id}/payments${envQuery(env)}`),
361
+ update: (id, patch, env) => this.request(`/projects/${id}/payments${envQuery(env)}`, {
362
+ method: "PATCH",
363
+ body: patch
364
+ }),
365
+ /** A new signing secret, shown once (also put in the app's env as VIBIZ_WEBHOOK_SECRET). */
366
+ rotateWebhookSecret: (id, env) => this.request(
367
+ `/projects/${id}/payments/webhook-secret${envQuery(env)}`,
368
+ { method: "POST", body: {} }
369
+ ),
370
+ deliveries: (id, env) => this.request(
371
+ `/projects/${id}/payments/deliveries${envQuery(env)}`
372
+ ),
373
+ products: {
374
+ list: (id, env) => this.request(
375
+ `/projects/${id}/payments/products${envQuery(env)}`
376
+ ),
377
+ create: (id, input, env) => this.request(`/projects/${id}/payments/products${envQuery(env)}`, {
378
+ method: "POST",
379
+ body: input
380
+ }),
381
+ get: (id, productId, env) => this.request(
382
+ `/projects/${id}/payments/products/${encodeURIComponent(productId)}${envQuery(env)}`
383
+ ),
384
+ update: (id, productId, patch, env) => this.request(
385
+ `/projects/${id}/payments/products/${encodeURIComponent(productId)}${envQuery(env)}`,
386
+ { method: "PATCH", body: patch }
387
+ ),
388
+ /** Archive: kept on record, no longer sold. */
389
+ remove: (id, productId, env) => this.request(
390
+ `/projects/${id}/payments/products/${encodeURIComponent(productId)}${envQuery(env)}`,
391
+ { method: "DELETE" }
392
+ ),
393
+ addPrice: (id, productId, price, env) => this.request(
394
+ `/projects/${id}/payments/products/${encodeURIComponent(productId)}/prices${envQuery(env)}`,
395
+ { method: "POST", body: price }
396
+ ),
397
+ updatePrice: (id, priceId, patch, env) => this.request(
398
+ `/projects/${id}/payments/prices/${encodeURIComponent(priceId)}${envQuery(env)}`,
399
+ { method: "PATCH", body: patch }
400
+ )
401
+ },
402
+ checkout: (id, input, env) => this.request(`/projects/${id}/payments/checkout${envQuery(env)}`, {
403
+ method: "POST",
404
+ body: input
405
+ }),
406
+ list: (id, opts = {}) => this.request(
407
+ `/projects/${id}/payments/payments${paymentsQuery(opts)}`
408
+ ),
409
+ payment: (id, paymentId, env) => this.request(
410
+ `/projects/${id}/payments/payments/${encodeURIComponent(paymentId)}${envQuery(env)}`
411
+ ),
412
+ /** The balance (available + pending) must cover it: `409 insufficient_balance` otherwise. */
413
+ refund: (id, paymentId, input = {}, env) => this.request(
414
+ `/projects/${id}/payments/payments/${encodeURIComponent(paymentId)}/refund${envQuery(env)}`,
415
+ { method: "POST", body: input }
416
+ ),
417
+ subscriptions: (id, opts = {}) => this.request(
418
+ `/projects/${id}/payments/subscriptions${paymentsQuery(opts)}`
419
+ ),
420
+ cancel: (id, subscriptionId, opts = {}, env) => this.request(
421
+ `/projects/${id}/payments/subscriptions/${encodeURIComponent(subscriptionId)}/cancel${envQuery(env)}`,
422
+ { method: "POST", body: opts }
423
+ ),
424
+ ledger: (id, opts = {}) => this.request(
425
+ `/projects/${id}/payments/ledger${paymentsQuery(opts)}`
426
+ ),
427
+ /** For the embedded verification, bank and payout UI (Connect.js). Short-lived. */
428
+ payoutsSession: (id, input = {}, env) => this.request(`/projects/${id}/payments/payouts/session${envQuery(env)}`, {
429
+ method: "POST",
430
+ body: input
431
+ }),
432
+ /** Default: everything available. */
433
+ withdraw: (id, input = {}, env) => this.request(`/projects/${id}/payments/withdraw${envQuery(env)}`, {
434
+ method: "POST",
435
+ body: input
436
+ }),
437
+ withdrawals: (id, env) => this.request(
438
+ `/projects/${id}/payments/withdrawals${envQuery(env)}`
439
+ ),
440
+ refresh: (id, env) => this.request(`/projects/${id}/payments/refresh${envQuery(env)}`, {
441
+ method: "POST",
442
+ body: {}
443
+ }),
444
+ /** Development only: a real test-mode payment, dispute or refund, or release the hold. */
445
+ simulate: (id, input) => this.request(
446
+ `/projects/${id}/payments/simulate?env=development`,
447
+ { method: "POST", body: input }
448
+ ),
449
+ /**
450
+ * The owner's own Stripe account instead of payments through Vibiz
451
+ * (docs/PAYMENTS.md "Your own account"). `link` makes a link for the
452
+ * owner (any key; production unless `env` says): give them `url`, only
453
+ * they can complete it. `disconnect` (owner only) goes back to Vibiz.
454
+ */
455
+ ownAccount: {
456
+ link: (id, env = "production") => this.request(`/projects/${id}/payments/own-account/link?env=${env}`, {
457
+ method: "POST",
458
+ body: {}
459
+ }),
460
+ disconnect: (id, env) => this.request(`/projects/${id}/payments/own-account${envQuery(env)}`, {
461
+ method: "DELETE"
462
+ })
463
+ }
464
+ },
465
+ /**
466
+ * Custom connectors (docs/CONNECTORS.md): any HTTP API from one manifest,
467
+ * called through Vibiz, which holds the key. `env` defaults like the API:
468
+ * production for owners, development for agents. Production keys, and
469
+ * approving an agent's dangerous call, need an owner key.
470
+ */
471
+ connectors: {
472
+ list: (id) => this.request(`/projects/${id}/connectors`).then((r) => r.data),
473
+ get: (id, name) => this.request(`/projects/${id}/connectors/${encodeURIComponent(name)}`),
474
+ /** From a manifest, a short form (`operations: ["GET /posts/{id}"]`) or an import's draft. */
475
+ create: (id, manifest) => this.request(`/projects/${id}/connectors`, { method: "POST", body: manifest }),
476
+ /** Preview an OpenAPI 3 spec (url or the document): nothing is saved. */
477
+ import: (id, input) => this.request(`/projects/${id}/connectors/import`, {
478
+ method: "POST",
479
+ body: input
480
+ }),
481
+ update: (id, name, patch) => this.request(`/projects/${id}/connectors/${encodeURIComponent(name)}`, {
482
+ method: "PATCH",
483
+ body: patch
484
+ }),
485
+ delete: (id, name) => this.request(`/projects/${id}/connectors/${encodeURIComponent(name)}`, {
486
+ method: "DELETE"
487
+ }),
488
+ /** Set values (`null` removes). Production: owner keys. Values are never read back. */
489
+ setSecrets: (id, name, values, env) => this.request(
490
+ `/projects/${id}/connectors/${encodeURIComponent(name)}/secrets${envQuery(env)}`,
491
+ { method: "PUT", body: { values } }
492
+ ),
493
+ check: (id, name, env) => this.request(
494
+ `/projects/${id}/connectors/${encodeURIComponent(name)}/check${envQuery(env)}`,
495
+ { method: "POST", body: {} }
496
+ ),
497
+ /** One operation. A dangerous one (or a production write) by an agent key answers 403 approval_required. */
498
+ call: (id, name, input, opts = {}) => this.request(
499
+ `/projects/${id}/connectors/${encodeURIComponent(name)}/call${envQuery(opts.env)}`,
500
+ {
501
+ method: "POST",
502
+ body: input,
503
+ ...opts.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {}
504
+ }
505
+ ),
506
+ calls: (id, name, opts = {}) => this.request(
507
+ `/projects/${id}/connectors/${encodeURIComponent(name)}/calls${paymentsQuery(opts)}`
508
+ ),
509
+ approvals: (id, opts = {}) => this.request(
510
+ `/projects/${id}/connectors/approvals${paymentsQuery(opts)}`
511
+ ).then((r) => r.data),
512
+ approval: (id, approvalId) => this.request(
513
+ `/projects/${id}/connectors/approvals/${encodeURIComponent(approvalId)}`
514
+ ),
515
+ /** Owner keys. */
516
+ approve: (id, approvalId) => this.request(
517
+ `/projects/${id}/connectors/approvals/${encodeURIComponent(approvalId)}/approve`,
518
+ { method: "POST", body: {} }
519
+ ),
520
+ /** Owner keys. */
521
+ deny: (id, approvalId) => this.request(
522
+ `/projects/${id}/connectors/approvals/${encodeURIComponent(approvalId)}/deny`,
523
+ { method: "POST", body: {} }
524
+ )
525
+ },
526
+ /**
527
+ * The owner's own domains: served by Vibiz hosting (with HTTPS once DNS
528
+ * points at it) or trusted for a host elsewhere. Adding and removing need
529
+ * an owner key; any key reads status and asks for a re-check.
530
+ */
531
+ domains: {
532
+ list: (id) => this.request(`/projects/${id}/domains`),
533
+ /** `hosting` defaults to `vibiz` when the project has Vibiz hosting, else `elsewhere`. */
534
+ add: (id, domain, opts = {}) => this.request(`/projects/${id}/domains`, {
535
+ method: "POST",
536
+ body: { domain, ...opts }
537
+ }),
538
+ /** Check DNS and ownership again now. */
539
+ verify: (id, domain) => this.request(`/projects/${id}/domains/${encodeURIComponent(domain)}/verify`, {
540
+ method: "POST",
541
+ body: {}
542
+ }),
543
+ remove: (id, domain) => this.request(`/projects/${id}/domains/${encodeURIComponent(domain)}`, {
544
+ method: "DELETE"
545
+ }),
546
+ /**
547
+ * Owner keys: buy a domain from the account's credits; it serves on this
548
+ * project once registered. An agent key answers `owner_only` with the
549
+ * console link to hand the owner.
550
+ */
551
+ buy: (id, input) => this.request(`/projects/${id}/domains/buy`, { method: "POST", body: input })
552
+ },
553
+ /** Poll until every resource is ready, or throw on the first failure. */
554
+ waitUntilReady: async (id, opts = {}) => {
555
+ const deadline = Date.now() + (opts.timeoutMs ?? 10 * 6e4);
556
+ for (; ; ) {
557
+ const p = await this.projects.get(id);
558
+ const failed = p.resources.find((r) => r.status === "failed");
559
+ if (failed) {
560
+ throw new VibizApiError(
561
+ 409,
562
+ failed.error?.code ?? "resource_failed",
563
+ failed.error?.message ?? `${failed.type} failed`
564
+ );
565
+ }
566
+ if (p.resources.every((r) => r.status === "ready")) return p;
567
+ if (Date.now() > deadline)
568
+ throw new VibizApiError(408, "timeout", "Timed out waiting for the project.");
569
+ await sleep(opts.intervalMs ?? 2e3);
570
+ }
571
+ }
572
+ };
573
+ files = {
574
+ missing: (shas) => this.request("/files/missing", {
575
+ method: "POST",
576
+ body: { shas }
577
+ }).then((r) => r.missing),
578
+ upload: (sha, bytes) => this.request(`/files/${sha}`, { method: "PUT", raw: bytes })
579
+ };
580
+ /**
581
+ * A project's Files (docs: files): uploads and every AI-generated image,
582
+ * video and speech, kept permanently with a stable link. `env` defaults to
583
+ * production. (`files` above is the deploy upload cache.)
584
+ */
585
+ projectFiles = {
586
+ list: (projectId, opts = {}) => this.request(`/projects/${projectId}/files${qs({ ...opts })}`),
587
+ /** The newest file matching the filters (404 `not_found` when none). */
588
+ latest: (projectId, opts = {}) => this.request(`/projects/${projectId}/files/latest${qs({ ...opts })}`),
589
+ get: (projectId, fileId, opts = {}) => this.request(
590
+ `/projects/${projectId}/files/${encodeURIComponent(fileId)}${envQuery(opts.env)}`
591
+ ),
592
+ /** Upload bytes (or a Blob) as one file; at most 50 MB. */
593
+ upload: async (projectId, file, opts) => {
594
+ const bytes = file instanceof Uint8Array ? file : new Uint8Array(await file.arrayBuffer());
595
+ const contentType = opts.contentType ?? (file instanceof Blob && file.type ? file.type : "application/octet-stream");
596
+ return this.request(
597
+ `/projects/${projectId}/files${qs({
598
+ env: opts.env,
599
+ name: opts.name,
600
+ folder: opts.folder,
601
+ private: opts.private ? 1 : void 0
602
+ })}`,
603
+ { method: "POST", raw: bytes, contentType }
604
+ );
605
+ },
606
+ /** Save a file from a public URL (at most 50 MB). */
607
+ import: (projectId, input, opts = {}) => this.request(`/projects/${projectId}/files/import${envQuery(opts.env)}`, {
608
+ method: "POST",
609
+ body: input
610
+ }),
611
+ /** Rename, move to a folder (`null`: none), make private, tag or note. */
612
+ update: (projectId, fileId, patch, opts = {}) => this.request(
613
+ `/projects/${projectId}/files/${encodeURIComponent(fileId)}${envQuery(opts.env)}`,
614
+ { method: "PATCH", body: patch }
615
+ ),
616
+ /** Delete a file: its link stops working. */
617
+ delete: (projectId, fileId, opts = {}) => this.request(
618
+ `/projects/${projectId}/files/${encodeURIComponent(fileId)}${envQuery(opts.env)}`,
619
+ { method: "DELETE" }
620
+ ),
621
+ /** The file's bytes (private files too). */
622
+ download: async (projectId, fileId, opts = {}) => {
623
+ const res = await this.stream(
624
+ `/projects/${projectId}/files/${encodeURIComponent(fileId)}/content${envQuery(opts.env)}`
625
+ );
626
+ return new Uint8Array(await res.arrayBuffer());
627
+ }
628
+ };
629
+ /** Take a project elsewhere, or keep a backup (docs: export). */
630
+ exports = {
631
+ /** Start packing: `202` with the export `queued`. */
632
+ create: (projectId, input = {}) => this.request(`/projects/${projectId}/exports`, {
633
+ method: "POST",
634
+ body: input
635
+ }),
636
+ list: (projectId) => this.request(`/projects/${projectId}/exports`).then((r) => r.data),
637
+ get: (projectId, exportId) => this.request(`/projects/${projectId}/exports/${encodeURIComponent(exportId)}`),
638
+ waitUntilDone: async (projectId, exportId, opts = {}) => {
639
+ const deadline = Date.now() + (opts.timeoutMs ?? 30 * 6e4);
640
+ for (; ; ) {
641
+ const e = await this.exports.get(projectId, exportId);
642
+ opts.onProgress?.(e);
643
+ if (e.status !== "queued" && e.status !== "running") return e;
644
+ if (Date.now() > deadline)
645
+ throw new VibizApiError(408, "timeout", "Timed out waiting for the export.");
646
+ await sleep(opts.intervalMs ?? 2e3);
647
+ }
648
+ },
649
+ /** One of its files, from its signed link (no key sent). */
650
+ download: async (url) => {
651
+ const res = await this.fetchImpl(url);
652
+ if (!res.ok) return this.fail(res);
653
+ return res;
654
+ },
655
+ /**
656
+ * The environment's secret values as `.env` text. Owner keys only; the
657
+ * call itself is the confirmation, so only make it when the person asked.
658
+ */
659
+ envFile: async (projectId, exportId) => {
660
+ const res = await this.stream(
661
+ `/projects/${projectId}/exports/${encodeURIComponent(exportId)}/env?confirm=1`
662
+ );
663
+ return res.text();
664
+ }
665
+ };
666
+ deployments = {
667
+ create: (projectId, input, opts = {}) => this.request(
668
+ `/projects/${projectId}/deployments`,
669
+ {
670
+ method: "POST",
671
+ body: input,
672
+ ...opts
673
+ }
674
+ ),
675
+ list: (projectId) => this.request(`/projects/${projectId}/deployments`).then(
676
+ (r) => r.data
677
+ ),
678
+ get: (id) => this.request(`/deployments/${id}`),
679
+ promote: (id) => this.request(
680
+ `/deployments/${id}/promote`,
681
+ { method: "POST" }
682
+ ),
683
+ waitUntilDone: async (id, opts = {}) => {
684
+ const deadline = Date.now() + (opts.timeoutMs ?? 20 * 6e4);
685
+ for (; ; ) {
686
+ const d = await this.deployments.get(id);
687
+ if (d.status === "ready" || d.status === "failed" || d.status === "canceled") return d;
688
+ if (Date.now() > deadline)
689
+ throw new VibizApiError(408, "timeout", "Timed out waiting for the deployment.");
690
+ await sleep(opts.intervalMs ?? 3e3);
691
+ }
692
+ }
693
+ };
694
+ analytics = {
695
+ /** Visitors, pageviews, top pages, sources, countries, events and a timeseries. */
696
+ get: (projectId, opts = {}) => this.request(
697
+ `/projects/${projectId}/analytics?period=${opts.period ?? "7d"}`
698
+ )
699
+ };
700
+ /** Session replay. Start from `insights`; open single sessions with `get`. */
701
+ replays = {
702
+ /** Rule-based findings over recorded sessions: what broke and where people got stuck. */
703
+ insights: (projectId, opts = {}) => this.request(
704
+ `/projects/${projectId}/replays/insights?period=${opts.period ?? "7d"}`
705
+ ),
706
+ /** Recorded sessions, most worth watching first. */
707
+ list: (projectId, opts = {}) => {
708
+ const q = new URLSearchParams();
709
+ for (const [k, v] of Object.entries(opts))
710
+ if (v !== void 0 && v !== false) q.set(k, String(v));
711
+ const qs2 = q.toString();
712
+ return this.request(`/projects/${projectId}/replays${qs2 ? `?${qs2}` : ""}`);
713
+ },
714
+ /** Metadata, signals, completeness and the timeline of notable moments. */
715
+ get: (id) => this.request(`/replays/${id}`),
716
+ /** The raw rrweb events (masked at the source). Pages with `from_seq` / `max_chunks`. */
717
+ events: (id, opts = {}) => {
718
+ const q = new URLSearchParams();
719
+ if (opts.time) q.set("time", opts.time);
720
+ if (opts.from_seq !== void 0) q.set("from_seq", String(opts.from_seq));
721
+ if (opts.max_chunks !== void 0) q.set("max_chunks", String(opts.max_chunks));
722
+ const qs2 = q.toString();
723
+ return this.request(
724
+ `/replays/${id}/events${qs2 ? `?${qs2}` : ""}`
725
+ );
726
+ },
727
+ /** A link to the hosted player for a person, valid for `expires_in` seconds (default 1 h, max 24 h). */
728
+ share: (id, opts = {}) => this.request(`/replays/${id}/share`, {
729
+ method: "POST",
730
+ body: opts
731
+ }),
732
+ delete: (id) => this.request(`/replays/${id}`, { method: "DELETE" })
733
+ };
734
+ operations = {
735
+ get: (id) => this.request(`/operations/${id}`)
736
+ };
737
+ actions = {
738
+ run: (name, input) => this.request(`/actions/${name}`, {
739
+ method: "POST",
740
+ body: { input }
741
+ }),
742
+ /** The web data actions (scrape, crawl, search, ads library…) with their inputs. */
743
+ list: () => this.request("/actions").then((r) => r.data),
744
+ /** Past runs, newest first, and one run with its output. */
745
+ runs: {
746
+ list: (q = {}) => this.request(
747
+ `/actions/runs${qs({ project: q.project, action: q.action, since: q.since, cursor: q.cursor, limit: q.limit })}`
748
+ ),
749
+ get: (id) => this.request(`/actions/runs/${encodeURIComponent(id)}`)
750
+ }
751
+ };
752
+ /**
753
+ * A site's design (docs: design): tokens, component styles, layout,
754
+ * screenshots and exports (Tailwind v4, shadcn/ui, CSS variables, a brief).
755
+ * With `projectId` the spec, brief and screenshots are saved to its Files.
756
+ */
757
+ design = {
758
+ extract: (input, opts = {}) => opts.projectId ? this.projects.runAction(opts.projectId, "web.design", input) : this.actions.run("web.design", input)
759
+ };
760
+ email = {
761
+ /** Send from the project's address (`info@<slug>.vibiz.app` by default). */
762
+ send: (projectId, input, opts = {}) => this.projects.runAction(projectId, "email.send", input, opts),
763
+ /**
764
+ * Business email's settings (docs: email ▸ Broadcasts): the sending address
765
+ * (`news.<slug>`), footer, double opt-in, tracking, signup form, and the
766
+ * owner's monthly allowance for agent-sent mail. `setup` starts the sender.
767
+ */
768
+ marketing: {
769
+ get: (projectId) => this.request(`${mkt(projectId)}/marketing`),
770
+ update: (projectId, input) => this.request(`${mkt(projectId)}/marketing`, {
771
+ method: "PATCH",
772
+ body: input
773
+ }),
774
+ setup: (projectId) => this.request(`${mkt(projectId)}/marketing/setup`, {
775
+ method: "POST",
776
+ body: {}
777
+ })
778
+ },
779
+ lists: {
780
+ list: (projectId) => this.request(`${mkt(projectId)}/lists`),
781
+ create: (projectId, input) => this.request(`${mkt(projectId)}/lists`, { method: "POST", body: input }),
782
+ update: (projectId, list, input) => this.request(`${mkt(projectId)}/lists/${encodeURIComponent(list)}`, {
783
+ method: "PATCH",
784
+ body: input
785
+ }),
786
+ delete: (projectId, list) => this.request(
787
+ `${mkt(projectId)}/lists/${encodeURIComponent(list)}`,
788
+ { method: "DELETE" }
789
+ )
790
+ },
791
+ /** Contacts: by id or email. Upserting never re-subscribes someone who left. */
792
+ contacts: {
793
+ list: (projectId, q = {}) => this.request(`${mkt(projectId)}/contacts${qs(q)}`),
794
+ get: (projectId, contact) => this.request(`${mkt(projectId)}/contacts/${encodeURIComponent(contact)}`),
795
+ upsert: (projectId, input) => this.request(`${mkt(projectId)}/contacts`, { method: "POST", body: input }),
796
+ update: (projectId, contact, input) => this.request(`${mkt(projectId)}/contacts/${encodeURIComponent(contact)}`, {
797
+ method: "PATCH",
798
+ body: input
799
+ }),
800
+ delete: (projectId, contact) => this.request(
801
+ `${mkt(projectId)}/contacts/${encodeURIComponent(contact)}`,
802
+ { method: "DELETE" }
803
+ ),
804
+ /** A CSV from any newsletter tool. `dry_run` first: it reports, writes nothing. */
805
+ import: (projectId, input) => this.request(`${mkt(projectId)}/contacts/import`, {
806
+ method: "POST",
807
+ body: input
808
+ }),
809
+ imports: (projectId) => this.request(
810
+ `${mkt(projectId)}/contacts/imports`
811
+ ),
812
+ /** Contacts as CSV text. */
813
+ export: async (projectId, q = {}) => {
814
+ const res = await this.stream(`${mkt(projectId)}/contacts/export${qs(q)}`);
815
+ return res.text();
816
+ },
817
+ /** Something the contact did: segments and automations read it. */
818
+ track: (projectId, input) => this.request(`${mkt(projectId)}/events`, { method: "POST", body: input })
819
+ },
820
+ segments: {
821
+ list: (projectId) => this.request(`${mkt(projectId)}/segments`),
822
+ create: (projectId, input) => this.request(`${mkt(projectId)}/segments`, { method: "POST", body: input }),
823
+ update: (projectId, segment, input) => this.request(`${mkt(projectId)}/segments/${encodeURIComponent(segment)}`, {
824
+ method: "PATCH",
825
+ body: input
826
+ }),
827
+ delete: (projectId, segment) => this.request(
828
+ `${mkt(projectId)}/segments/${encodeURIComponent(segment)}`,
829
+ { method: "DELETE" }
830
+ ),
831
+ /** How many subscribed contacts a filter matches, and ten of them. */
832
+ preview: (projectId, filter) => this.request(
833
+ `${mkt(projectId)}/segments/preview`,
834
+ { method: "POST", body: { filter } }
835
+ )
836
+ },
837
+ /** An email as a contact would get it (a sample contact, or `contact_id`), untracked. */
838
+ render: (projectId, input) => this.request(`${mkt(projectId)}/render`, { method: "POST", body: input }),
839
+ /**
840
+ * Newsletters and announcements. Anyone drafts, reviews and sends tests (an
841
+ * agent's go to the owners); sending is the owner's, or an agent's within the
842
+ * owner's monthly allowance (otherwise `owner_only` with the review link).
843
+ */
844
+ broadcasts: {
845
+ list: (projectId, q = {}) => this.request(`${mkt(projectId)}/broadcasts${qs(q)}`),
846
+ get: (projectId, id) => this.request(`${mkt(projectId)}/broadcasts/${encodeURIComponent(id)}`),
847
+ create: (projectId, input) => this.request(`${mkt(projectId)}/broadcasts`, { method: "POST", body: input }),
848
+ update: (projectId, id, input) => this.request(`${mkt(projectId)}/broadcasts/${encodeURIComponent(id)}`, {
849
+ method: "PATCH",
850
+ body: input
851
+ }),
852
+ delete: (projectId, id) => this.request(
853
+ `${mkt(projectId)}/broadcasts/${encodeURIComponent(id)}`,
854
+ { method: "DELETE" }
855
+ ),
856
+ /** Everything to fix before sending, the audience, and the cost against the balance. */
857
+ review: (projectId, id) => this.request(
858
+ `${mkt(projectId)}/broadcasts/${encodeURIComponent(id)}/review`
859
+ ),
860
+ /** A free, untracked `[Test]` (default: to the account's owners). */
861
+ test: (projectId, id, input = {}) => this.request(
862
+ `${mkt(projectId)}/broadcasts/${encodeURIComponent(id)}/test`,
863
+ {
864
+ method: "POST",
865
+ body: input
866
+ }
867
+ ),
868
+ /** Now, or at `scheduled_at` (ISO). */
869
+ send: (projectId, id, input = {}) => this.request(`${mkt(projectId)}/broadcasts/${encodeURIComponent(id)}/send`, {
870
+ method: "POST",
871
+ body: input
872
+ }),
873
+ cancel: (projectId, id) => this.request(`${mkt(projectId)}/broadcasts/${encodeURIComponent(id)}/cancel`, {
874
+ method: "POST",
875
+ body: {}
876
+ }),
877
+ /** Owner only, after a pause. */
878
+ resume: (projectId, id) => this.request(`${mkt(projectId)}/broadcasts/${encodeURIComponent(id)}/resume`, {
879
+ method: "POST",
880
+ body: {}
881
+ }),
882
+ /** `status`: a send status, or `opened`, `clicked`, `unsubscribed`; `mailbox`: gmail, outlook… */
883
+ recipients: (projectId, id, q = {}) => this.request(
884
+ `${mkt(projectId)}/broadcasts/${encodeURIComponent(id)}/recipients${qs(q)}`
885
+ )
886
+ },
887
+ /** Onboarding sequences and the like: a trigger, then waits, emails and branches. */
888
+ automations: {
889
+ list: (projectId) => this.request(`${mkt(projectId)}/automations`),
890
+ get: (projectId, id) => this.request(`${mkt(projectId)}/automations/${encodeURIComponent(id)}`),
891
+ create: (projectId, input) => this.request(`${mkt(projectId)}/automations`, { method: "POST", body: input }),
892
+ update: (projectId, id, input) => this.request(`${mkt(projectId)}/automations/${encodeURIComponent(id)}`, {
893
+ method: "PATCH",
894
+ body: input
895
+ }),
896
+ delete: (projectId, id) => this.request(
897
+ `${mkt(projectId)}/automations/${encodeURIComponent(id)}`,
898
+ { method: "DELETE" }
899
+ ),
900
+ activate: (projectId, id) => this.request(
901
+ `${mkt(projectId)}/automations/${encodeURIComponent(id)}/activate`,
902
+ {
903
+ method: "POST",
904
+ body: {}
905
+ }
906
+ ),
907
+ pause: (projectId, id) => this.request(`${mkt(projectId)}/automations/${encodeURIComponent(id)}/pause`, {
908
+ method: "POST",
909
+ body: {}
910
+ }),
911
+ enrollments: (projectId, id, q = {}) => this.request(
912
+ `${mkt(projectId)}/automations/${encodeURIComponent(id)}/enrollments${qs(q)}`
913
+ ),
914
+ /** One email step as a free `[Test]` (default: to the owners). */
915
+ test: (projectId, id, input) => this.request(
916
+ `${mkt(projectId)}/automations/${encodeURIComponent(id)}/test`,
917
+ {
918
+ method: "POST",
919
+ body: input
920
+ }
921
+ )
922
+ }
923
+ };
924
+ /**
925
+ * The AI gateway: chat, images, speech and transcription, metered against
926
+ * the account's (or the project's) spend. Works with the account key or a
927
+ * project AI key (`VIBIZ_AI_API_KEY`). OpenAI's SDK works too: point it at
928
+ * `<api>/v1/ai`.
929
+ */
930
+ ai = {
931
+ /** One completion. `model` defaults to `vibiz/fast`. */
932
+ chat: (input, opts = {}) => this.request("/ai/chat/completions", {
933
+ method: "POST",
934
+ body: { ...input, stream: false },
935
+ ...opts
936
+ }),
937
+ /** A streamed completion, chunk by chunk. Breaking out of the loop stops the model. */
938
+ chatStream: (input) => {
939
+ const open = () => this.stream("/ai/chat/completions", { method: "POST", body: { ...input, stream: true } });
940
+ return {
941
+ async *[Symbol.asyncIterator]() {
942
+ const res = await open();
943
+ const reader = res.body.getReader();
944
+ const decoder = new TextDecoder();
945
+ let buffer = "";
946
+ try {
947
+ for (; ; ) {
948
+ const { done, value } = await reader.read();
949
+ if (done) return;
950
+ buffer += decoder.decode(value, { stream: true });
951
+ for (let end = buffer.indexOf("\n\n"); end >= 0; end = buffer.indexOf("\n\n")) {
952
+ const block = buffer.slice(0, end);
953
+ buffer = buffer.slice(end + 2);
954
+ const data = block.startsWith("data: ") ? block.slice(6) : "";
955
+ if (!data) continue;
956
+ if (data === "[DONE]") return;
957
+ const parsed = JSON.parse(data);
958
+ if (parsed.error) {
959
+ throw new VibizApiError(
960
+ 502,
961
+ parsed.error.code,
962
+ parsed.error.message,
963
+ parsed.error.details
964
+ );
965
+ }
966
+ yield parsed;
967
+ }
968
+ }
969
+ } finally {
970
+ await reader.cancel().catch(() => {
971
+ });
972
+ }
973
+ }
974
+ };
975
+ },
976
+ embed: (input) => this.request(
977
+ "/ai/embeddings",
978
+ { method: "POST", body: input }
979
+ ),
980
+ image: (input, opts = {}) => this.request("/ai/images/generations", { method: "POST", body: input, ...opts }),
981
+ /** Text to speech: the audio bytes (mp3 by default). */
982
+ speech: async (input) => {
983
+ const res = await this.stream("/ai/audio/speech", { method: "POST", body: input });
984
+ return new Uint8Array(await res.arrayBuffer());
985
+ },
986
+ /** Speech to text. */
987
+ transcribe: (input) => {
988
+ const form = new FormData();
989
+ const blob = input.file instanceof Blob ? input.file : new Blob([input.file]);
990
+ form.set("file", blob, input.filename ?? "audio.mp3");
991
+ if (input.model) form.set("model", input.model);
992
+ if (input.language) form.set("language", input.language);
993
+ return this.request("/ai/audio/transcriptions", { method: "POST", form });
994
+ },
995
+ /**
996
+ * The Vibiz models and the lab models, with prices. `all` adds every
997
+ * language model you can call by id; `q` searches id, name and maker.
998
+ */
999
+ models: (opts = {}) => {
1000
+ const qs2 = new URLSearchParams();
1001
+ if (opts.all) qs2.set("all", "1");
1002
+ if (opts.q) qs2.set("q", opts.q);
1003
+ const tail = qs2.size ? `?${qs2}` : "";
1004
+ return this.request(`/ai/models${tail}`).then((r) => r.data);
1005
+ },
1006
+ jobs: {
1007
+ create: (input, opts = {}) => this.request("/ai/jobs", { method: "POST", body: input, ...opts }),
1008
+ get: (id) => this.request(`/ai/jobs/${id}`),
1009
+ cancel: (id) => this.request(`/ai/jobs/${id}/cancel`, { method: "POST" }),
1010
+ wait: async (id, opts = {}) => {
1011
+ const deadline = Date.now() + (opts.timeoutMs ?? 20 * 6e4);
1012
+ for (; ; ) {
1013
+ const job = await this.ai.jobs.get(id);
1014
+ if (job.status !== "queued" && job.status !== "running") return job;
1015
+ if (Date.now() > deadline)
1016
+ throw new VibizApiError(408, "timeout", "Timed out waiting for the job.");
1017
+ await sleep(opts.intervalMs ?? 2e3);
1018
+ }
1019
+ }
1020
+ }
1021
+ };
1022
+ /** A project's AI key and settings (the `ai` resource). Account key only. */
1023
+ projectAi = {
1024
+ get: (projectId) => this.request(`/projects/${projectId}/ai`),
1025
+ update: (projectId, patch) => this.request(`/projects/${projectId}/ai`, { method: "PATCH", body: patch }),
1026
+ /** A new key; the old one keeps working for `graceSeconds` (default 3600). */
1027
+ rotateKey: (projectId, opts = {}) => this.request(`/projects/${projectId}/ai/keys/rotate`, {
1028
+ method: "POST",
1029
+ body: opts.graceSeconds === void 0 ? {} : { grace_seconds: opts.graceSeconds }
1030
+ }),
1031
+ revokeKey: (projectId) => this.request(`/projects/${projectId}/ai/keys/revoke`, { method: "POST" })
1032
+ };
1033
+ /**
1034
+ * Your own AI provider keys (docs/AI.md "Your own keys"): calls they can
1035
+ * serve run only on them (never falling back to Vibiz) and cost 0 here.
1036
+ * Pass `projectId` for a project's override. Setting and removing need an
1037
+ * owner key. Secrets are write-only: no method returns one.
1038
+ */
1039
+ aiKeys = {
1040
+ list: (opts = {}) => this.request(`${aiKeysBase(opts.projectId)}`),
1041
+ get: (provider, opts = {}) => this.request(`${aiKeysBase(opts.projectId)}/${provider}`),
1042
+ /** Checked with a cheap call to the provider first; refused with the reason if it fails. */
1043
+ set: (provider, input, opts = {}) => this.request(`${aiKeysBase(opts.projectId)}/${provider}`, {
1044
+ method: "PUT",
1045
+ body: input
1046
+ }),
1047
+ /** Which capabilities the key serves; the rest runs on Vibiz. */
1048
+ update: (provider, patch, opts = {}) => this.request(`${aiKeysBase(opts.projectId)}/${provider}`, {
1049
+ method: "PATCH",
1050
+ body: patch
1051
+ }),
1052
+ test: (provider, opts = {}) => this.request(`${aiKeysBase(opts.projectId)}/${provider}/test`, {
1053
+ method: "POST",
1054
+ body: {}
1055
+ }),
1056
+ remove: (provider, opts = {}) => this.request(`${aiKeysBase(opts.projectId)}/${provider}`, {
1057
+ method: "DELETE"
1058
+ })
1059
+ };
1060
+ /** What the account used, what it cost, and how close it is to its limits. */
1061
+ usage = {
1062
+ get: (opts = {}) => this.request(`/usage${usageQuery(opts)}`),
1063
+ project: (projectId, opts = {}) => this.request(`/projects/${projectId}/usage${usageQuery(opts)}`)
1064
+ };
1065
+ /**
1066
+ * Buying domains (docs: buying a domain): search and quote with any key;
1067
+ * the domains the account owns, auto-renew, renewals, the transfer code and
1068
+ * the owner details (the registrant, saved once) need an owner key.
1069
+ */
1070
+ domains = {
1071
+ search: (q, opts = {}) => this.request(`/domains/search${qs({ q, project_id: opts.projectId })}`),
1072
+ quote: (domain, opts = {}) => this.request(
1073
+ `/domains/quote${qs({ domain, years: opts.years, project_id: opts.projectId })}`
1074
+ ),
1075
+ list: () => this.request("/domains"),
1076
+ get: (domain) => this.request(`/domains/${encodeURIComponent(domain)}`),
1077
+ update: (domain, input) => this.request(`/domains/${encodeURIComponent(domain)}`, {
1078
+ method: "PATCH",
1079
+ body: input
1080
+ }),
1081
+ renew: (domain, input) => this.request(`/domains/${encodeURIComponent(domain)}/renew`, {
1082
+ method: "POST",
1083
+ body: input
1084
+ }),
1085
+ authCode: (domain) => this.request(`/domains/${encodeURIComponent(domain)}/auth-code`, { method: "POST", body: {} }),
1086
+ owner: () => this.request("/domains/owner"),
1087
+ setOwner: (contact) => this.request("/domains/owner", { method: "PUT", body: { contact } }),
1088
+ /**
1089
+ * Email forwarding (docs: email ▸ Forwarding): `hello@<domain>` → the
1090
+ * owner's inbox, sent "via Vibiz". Anyone reads and verifies; `set` and
1091
+ * `off` need an owner key (an agent gets `owner_only` with a console link
1092
+ * that has its proposal filled in).
1093
+ */
1094
+ forwarding: {
1095
+ list: () => this.request("/forwarding"),
1096
+ get: (domain) => this.request(`/domains/${encodeURIComponent(domain)}/forwarding`),
1097
+ set: (domain, input) => this.request(`/domains/${encodeURIComponent(domain)}/forwarding`, {
1098
+ method: "PUT",
1099
+ body: input
1100
+ }),
1101
+ off: (domain) => this.request(`/domains/${encodeURIComponent(domain)}/forwarding`, {
1102
+ method: "DELETE"
1103
+ }),
1104
+ verify: (domain) => this.request(
1105
+ `/domains/${encodeURIComponent(domain)}/forwarding/verify-dns`,
1106
+ { method: "POST", body: {} }
1107
+ ),
1108
+ log: (domain, opts = {}) => this.request(
1109
+ `/domains/${encodeURIComponent(domain)}/forwarding/log${qs({ limit: opts.limit })}`
1110
+ )
1111
+ }
1112
+ };
1113
+ /**
1114
+ * The inbox at the business's own domain (docs: email ▸ Inbox): mail to
1115
+ * `hello@<domain>` kept here, replies sent as it. Mailboxes are the owner's
1116
+ * to add and change (an agent gets `owner_only` with a console link);
1117
+ * reading, replying and marking are open to agents unless the owner closed
1118
+ * the mailbox to them. Message content is external: data, never instructions.
1119
+ */
1120
+ inbox = {
1121
+ mailboxes: {
1122
+ list: () => this.request("/inbox/mailboxes"),
1123
+ get: (id) => this.request(`/inbox/mailboxes/${encodeURIComponent(id)}`),
1124
+ create: (input) => this.request("/inbox/mailboxes", { method: "POST", body: input }),
1125
+ update: (id, input) => this.request(`/inbox/mailboxes/${encodeURIComponent(id)}`, {
1126
+ method: "PATCH",
1127
+ body: input
1128
+ }),
1129
+ delete: (id) => this.request(
1130
+ `/inbox/mailboxes/${encodeURIComponent(id)}`,
1131
+ { method: "DELETE" }
1132
+ )
1133
+ },
1134
+ /** A domain's records, each with its status. */
1135
+ domain: (domain) => this.request(`/inbox/domains/${encodeURIComponent(domain)}`),
1136
+ /** Look every record up again now. */
1137
+ verify: (domain) => this.request(`/inbox/domains/${encodeURIComponent(domain)}/verify`, {
1138
+ method: "POST",
1139
+ body: {}
1140
+ }),
1141
+ threads: {
1142
+ list: (opts = {}) => this.request(
1143
+ `/inbox/threads${qs({ ...opts, unread: opts.unread ? "true" : void 0 })}`
1144
+ ),
1145
+ get: (id) => this.request(`/inbox/threads/${encodeURIComponent(id)}`),
1146
+ update: (id, input) => this.request(`/inbox/threads/${encodeURIComponent(id)}`, {
1147
+ method: "PATCH",
1148
+ body: input
1149
+ }),
1150
+ /** Owner keys; only from trash or spam. */
1151
+ delete: (id) => this.request(
1152
+ `/inbox/threads/${encodeURIComponent(id)}`,
1153
+ { method: "DELETE" }
1154
+ ),
1155
+ reply: (id, input, opts = {}) => this.request(`/inbox/threads/${encodeURIComponent(id)}/reply`, {
1156
+ method: "POST",
1157
+ body: input,
1158
+ ...opts.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {}
1159
+ })
1160
+ },
1161
+ messages: {
1162
+ update: (id, input) => this.request(`/inbox/messages/${encodeURIComponent(id)}`, {
1163
+ method: "PATCH",
1164
+ body: input
1165
+ })
1166
+ },
1167
+ /** A new message (or a forward, with `forward_message_id`) from a mailbox. */
1168
+ send: (input, opts = {}) => this.request("/inbox/send", {
1169
+ method: "POST",
1170
+ body: input,
1171
+ ...opts.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {}
1172
+ }),
1173
+ /** A file to attach (kept a day until sent); returns its id. */
1174
+ upload: (file) => this.request(`/inbox/files${qs({ name: file.filename })}`, {
1175
+ method: "POST",
1176
+ raw: file.bytes,
1177
+ contentType: file.contentType
1178
+ }),
1179
+ settings: {
1180
+ get: () => this.request("/inbox/settings"),
1181
+ /** Owner keys: the new-mail digest. */
1182
+ update: (input) => this.request("/inbox/settings", { method: "PATCH", body: input })
1183
+ }
1184
+ };
1185
+ /**
1186
+ * The account's own credits (docs/BILLING.md): the balance, the card,
1187
+ * auto-recharge and history. A top-up is a hosted page a person pays on.
1188
+ */
1189
+ billing = {
1190
+ get: () => this.request("/billing"),
1191
+ /** Owner keys: auto-recharge and the daily safety cap. */
1192
+ update: (input) => this.request("/billing", { method: "PATCH", body: input }),
1193
+ /** A payment page for `amount_cents` (500–100000); hand its `url` to a person. */
1194
+ checkout: (input) => this.request("/billing/checkout", { method: "POST", body: input }),
1195
+ checkoutStatus: (id) => this.request(`/billing/checkout/${encodeURIComponent(id)}`),
1196
+ /** Owner keys: a page to save or replace the card. */
1197
+ saveCard: () => this.request("/billing/card", { method: "POST", body: {} }),
1198
+ removeCard: () => this.request("/billing/card", { method: "DELETE" }),
1199
+ ledger: (opts = {}) => this.request(
1200
+ `/billing/ledger${qs({ kind: opts.kind?.join(","), cursor: opts.cursor, limit: opts.limit })}`
1201
+ ),
1202
+ charges: (opts = {}) => this.request(`/billing/charges${qs(opts)}`)
1203
+ };
1204
+ /** Request counts, error rate and latency by capability. */
1205
+ metrics = {
1206
+ get: (opts = {}) => this.request(`/metrics?period=${opts.period ?? "30d"}`)
1207
+ };
1208
+ /** Named keys, one per teammate or agent, so spend shows per person. */
1209
+ apiKeys = {
1210
+ list: () => this.request("/api-keys").then((r) => r.data),
1211
+ /** The secret is returned once, here. */
1212
+ /** `agent` by default: builds and runs, never reads production secrets. Needs an owner key. */
1213
+ create: (name, opts = {}) => this.request("/api-keys", {
1214
+ method: "POST",
1215
+ body: { name, ...opts.access ? { access: opts.access } : {} }
1216
+ }),
1217
+ revoke: (id) => this.request(`/api-keys/${id}`, { method: "DELETE" })
1218
+ };
1219
+ /** A short-lived link to the hosted usage dashboard (admin scope needs an admin key). */
1220
+ dashboard = {
1221
+ link: (opts = {}) => this.request("/dashboard/link", { method: "POST", body: opts })
1222
+ };
1223
+ /** The platform view. Admin keys only. */
1224
+ admin = {
1225
+ metrics: (opts = {}) => this.request(`/admin/metrics?period=${opts.period ?? "30d"}`),
1226
+ usage: (opts = {}) => this.request(`/admin/usage${usageQuery(opts)}`),
1227
+ tenantUsage: (tenantId, opts = {}) => this.request(`/admin/tenants/${tenantId}/usage${usageQuery(opts)}`)
1228
+ };
1229
+ /** Company formation: what can be formed where, fees, industries, the disclaimer. */
1230
+ company = {
1231
+ catalog: (env) => this.request(`/company/catalog${envQuery(env)}`)
1232
+ };
1233
+ /** The hosts Vibiz can connect to, and the fields each asks for. */
1234
+ hostKinds() {
1235
+ return this.request("/hosts").then((r) => r.data);
1236
+ }
1237
+ capabilities() {
1238
+ return this.request("/capabilities");
1239
+ }
1240
+ /** The account and key this client acts as. */
1241
+ whoami() {
1242
+ return this.request("/whoami");
1243
+ }
1244
+ /** Revoke the key this client uses (`vibiz logout` for a key made by a browser login). */
1245
+ logout() {
1246
+ return this.request("/cli/logout", {
1247
+ method: "POST"
1248
+ });
1249
+ }
1250
+ };
1251
+ var mkt = (projectId) => `/projects/${encodeURIComponent(projectId)}/email`;
1252
+ var envQuery = (env) => env ? `?env=${env}` : "";
1253
+ var qs = (q) => {
1254
+ const p = new URLSearchParams();
1255
+ for (const [k, v] of Object.entries(q))
1256
+ if (v !== void 0 && v !== null && v !== "") p.set(k, String(v));
1257
+ const s = p.toString();
1258
+ return s ? `?${s}` : "";
1259
+ };
1260
+ function paymentsQuery(opts) {
1261
+ const q = new URLSearchParams();
1262
+ for (const [k, v] of Object.entries(opts)) if (v !== void 0 && v !== "") q.set(k, String(v));
1263
+ const qs2 = q.toString();
1264
+ return qs2 ? `?${qs2}` : "";
1265
+ }
1266
+ function usageQuery(opts) {
1267
+ const q = new URLSearchParams();
1268
+ for (const [k, v] of Object.entries(opts)) {
1269
+ if (v === void 0 || v === null || v === "") continue;
1270
+ q.set(k, Array.isArray(v) ? v.join(",") : String(v));
1271
+ }
1272
+ const qs2 = q.toString();
1273
+ return qs2 ? `?${qs2}` : "";
1274
+ }
1275
+ function endUserHeader(user) {
1276
+ if (/^[\x20-\x7e]*$/.test(user) && !user.startsWith("b64u:")) return user;
1277
+ const bytes = new TextEncoder().encode(user);
1278
+ let bin = "";
1279
+ for (const b of bytes) bin += String.fromCharCode(b);
1280
+ return `b64u:${btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")}`;
1281
+ }
1282
+ var aiKeysBase = (projectId) => projectId ? `/projects/${encodeURIComponent(projectId)}/ai-keys` : "/ai-keys";
1283
+
1284
+ // packages/sdk/src/deploy.ts
1285
+ import { createHash } from "node:crypto";
1286
+ import { readdir, readFile } from "node:fs/promises";
1287
+ import { homedir } from "node:os";
1288
+ import { join, parse, relative, resolve, sep } from "node:path";
1289
+ var IGNORED = /* @__PURE__ */ new Set([
1290
+ "vibiz.json",
1291
+ "node_modules",
1292
+ ".git",
1293
+ ".next",
1294
+ ".vercel",
1295
+ ".turbo",
1296
+ ".vibiz",
1297
+ "dist",
1298
+ ".DS_Store"
1299
+ ]);
1300
+ var isEnvFile = (name) => name === ".env" || name.startsWith(".env.");
1301
+ var isSecretFile = (name) => /^id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/.test(name) || /\.(pem|key|p12|pfx|keystore|jks)$/i.test(name) || [".npmrc", ".pypirc", ".netrc", ".git-credentials", "credentials.json", ".htpasswd"].includes(
1302
+ name
1303
+ );
1304
+ var PRIVATE_DIRS = [".ssh", ".aws", ".gnupg", ".kube", ".docker", ".config", "Library"];
1305
+ async function assertDeployableRoot(root) {
1306
+ const abs = resolve(root);
1307
+ const home = homedir();
1308
+ if (abs === parse(abs).root || abs === home || home.startsWith(`${abs}${sep}`)) {
1309
+ throw new Error(`Refusing to deploy ${abs}: point at the app's own folder.`);
1310
+ }
1311
+ const names = new Set(await readdir(abs));
1312
+ const found = PRIVATE_DIRS.filter((d) => names.has(d));
1313
+ if (found.length) {
1314
+ throw new Error(
1315
+ `Refusing to deploy ${abs}: it contains ${found.join(", ")}, which looks like a home or config folder.`
1316
+ );
1317
+ }
1318
+ }
1319
+ async function collectFiles(root) {
1320
+ const out = [];
1321
+ async function walk(dir) {
1322
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
1323
+ if (IGNORED.has(entry.name) || isEnvFile(entry.name) || isSecretFile(entry.name)) continue;
1324
+ const absolute = join(dir, entry.name);
1325
+ if (entry.isDirectory()) await walk(absolute);
1326
+ else if (entry.isFile()) {
1327
+ const bytes = await readFile(absolute);
1328
+ out.push({
1329
+ absolute,
1330
+ path: relative(root, absolute).split(sep).join("/"),
1331
+ sha: createHash("sha1").update(bytes).digest("hex"),
1332
+ size: bytes.byteLength
1333
+ });
1334
+ }
1335
+ }
1336
+ }
1337
+ await walk(root);
1338
+ return out.sort((a, b) => a.path.localeCompare(b.path));
1339
+ }
1340
+ async function deployDirectory(vibiz, projectId, root, opts = {}) {
1341
+ await assertDeployableRoot(root);
1342
+ const files = await collectFiles(root);
1343
+ if (files.length === 0) throw new Error(`No files to deploy in ${root}`);
1344
+ const missing = new Set(await vibiz.files.missing(files.map((f) => f.sha)));
1345
+ const toUpload = files.filter((f) => missing.has(f.sha));
1346
+ opts.onProgress?.(`${files.length} files, ${toUpload.length} to upload`);
1347
+ const uploadedShas = /* @__PURE__ */ new Set();
1348
+ for (const f of toUpload) {
1349
+ if (uploadedShas.has(f.sha)) continue;
1350
+ await vibiz.files.upload(f.sha, await readFile(f.absolute));
1351
+ uploadedShas.add(f.sha);
1352
+ }
1353
+ const { deployment } = await vibiz.deployments.create(projectId, {
1354
+ files: files.map(({ path, sha, size }) => ({ path, sha, size })),
1355
+ target: opts.target ?? "production"
1356
+ });
1357
+ return { deployment, uploaded: uploadedShas.size, total: files.length };
1358
+ }
1359
+
1360
+ // packages/sdk/src/env.ts
1361
+ import { chmod, readFile as readFile2, writeFile } from "node:fs/promises";
1362
+ function dotenvValue(value) {
1363
+ if (!/[\r\n]/.test(value)) {
1364
+ if (!/["\\]/.test(value)) return `"${value}"`;
1365
+ for (const q of ["'", "`"]) if (!value.includes(q)) return `${q}${value}${q}`;
1366
+ } else if (!value.includes('"') && !value.includes("\\n")) {
1367
+ return `"${value.replace(/\r?\n/g, "\\n")}"`;
1368
+ }
1369
+ throw new Error("This value has every kind of quote and cannot be written to a .env file.");
1370
+ }
1371
+ function mergeEnvFile(existing, env) {
1372
+ const pending = new Map(Object.entries(env));
1373
+ const lines = existing === "" ? [] : existing.replace(/\n$/, "").split("\n");
1374
+ const out = lines.map((line) => {
1375
+ const key = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line)?.[1];
1376
+ if (!key || !pending.has(key)) return line;
1377
+ const value = pending.get(key);
1378
+ pending.delete(key);
1379
+ return `${key}=${dotenvValue(value)}`;
1380
+ });
1381
+ for (const [k, v] of pending) out.push(`${k}=${dotenvValue(v)}`);
1382
+ return `${out.join("\n")}
1383
+ `;
1384
+ }
1385
+ async function writeEnvFile(path, env) {
1386
+ const existing = await readFile2(path, "utf8").catch(() => "");
1387
+ await writeFile(path, mergeEnvFile(existing, env), { mode: 384 });
1388
+ await chmod(path, 384);
1389
+ }
1390
+
1391
+ // packages/sdk/src/format.ts
1392
+ function formatUsd(usd) {
1393
+ if (usd === 0) return "$0.00";
1394
+ const sign = usd < 0 ? "-" : "";
1395
+ const a = Math.abs(usd);
1396
+ if (a < 1e-6) return `${sign}<$0.000001`;
1397
+ if (a < 0.01) return `${sign}$${String(Number(a.toPrecision(2)))}`;
1398
+ return `${sign}$${a.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
1399
+ }
1400
+ var compact = (n) => n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e4 ? `${(n / 1e3).toFixed(1)}k` : `${Math.round(n * 100) / 100}`;
1401
+ var SHORT = {
1402
+ image: ["img", "img"],
1403
+ video: ["video", "videos"],
1404
+ character: ["char", "chars"],
1405
+ audio_second: ["s audio", "s audio"],
1406
+ call: ["call", "calls"],
1407
+ email: ["email", "emails"],
1408
+ build: ["build", "builds"],
1409
+ deploy: ["deploy", "deploys"],
1410
+ stack_day: ["stack-day", "stack-days"],
1411
+ event: ["event", "events"],
1412
+ session: ["session", "sessions"],
1413
+ gb_day: ["GB-day", "GB-days"]
1414
+ };
1415
+ function formatUnits(units) {
1416
+ const parts = [];
1417
+ const tin = units.input_tokens ?? 0;
1418
+ const tout = units.output_tokens ?? 0;
1419
+ if (tin || tout) {
1420
+ parts.push(tout ? `${compact(tin)} in \xB7 ${compact(tout)} out tok` : `${compact(tin)} tok`);
1421
+ }
1422
+ for (const [u, n] of Object.entries(units)) {
1423
+ if (!n || u === "input_tokens" || u === "output_tokens") continue;
1424
+ const [one, many] = SHORT[u] ?? [u.replace(/_/g, " "), u.replace(/_/g, " ")];
1425
+ parts.push(`${compact(n)} ${n === 1 ? one : many}`);
1426
+ }
1427
+ return parts.join(" \xB7 ") || "\u2013";
1428
+ }
1429
+ export {
1430
+ Vibiz,
1431
+ VibizApiError,
1432
+ assertDeployableRoot,
1433
+ collectFiles,
1434
+ deployDirectory,
1435
+ endUserHeader,
1436
+ formatUnits,
1437
+ formatUsd,
1438
+ mergeEnvFile,
1439
+ writeEnvFile
1440
+ };