@vibiz/app 0.1.0 → 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.d.ts CHANGED
@@ -6,6 +6,9 @@
6
6
  * await vibiz.email.send({ to, subject, text });
7
7
  * await vibiz.user(userId).track('Upgraded', { properties: { plan: 'pro' } });
8
8
  * const openai = new OpenAI(vibiz.ai); // the AI gateway, OpenAI-compatible
9
+ * const { url } = await vibiz.files.upload(bytes, { name: 'avatar.png' });
10
+ * await vibiz.contacts.upsert({ email, first_name, lists: ['Users'] }); // newsletters, onboarding
11
+ * await vibiz.contacts.track(email, 'upgraded', { plan: 'pro' }); // starts automations
9
12
  *
10
13
  * The key (`VIBIZ_APP_KEY`, `vz_app_…`) belongs to one project and one
11
14
  * environment. It is a server secret: this client refuses to run in a
@@ -45,6 +48,8 @@ export interface SendEmail {
45
48
  export interface ActionResult<O> {
46
49
  output: O;
47
50
  cost_micro_usd: number | null;
51
+ /** The run in the project's web data history (console ▸ Web data). */
52
+ run_id?: string;
48
53
  }
49
54
  export interface ChatMessage {
50
55
  role: 'system' | 'user' | 'assistant' | 'tool';
@@ -76,6 +81,8 @@ export interface Payment {
76
81
  brand: string;
77
82
  last4: string;
78
83
  } | null;
84
+ /** `own`: charged on the owner's own Stripe account (docs/PAYMENTS.md "Your own account"). */
85
+ account: 'vibiz' | 'own';
79
86
  dispute: {
80
87
  status: 'needs_response' | 'under_review' | 'won' | 'lost';
81
88
  amount_cents: number;
@@ -91,6 +98,8 @@ export interface Subscription {
91
98
  user: string | null;
92
99
  customer_email: string | null;
93
100
  price_id: string | null;
101
+ /** `own`: billed on the owner's own Stripe account. The app's code is the same either way. */
102
+ account: 'vibiz' | 'own';
94
103
  current_period_end: string | null;
95
104
  cancel_at_period_end: boolean;
96
105
  created_at: string;
@@ -191,9 +200,87 @@ export interface WebhookEvent<T = unknown> {
191
200
  * Pass the raw body, exactly as received. Throws on anything wrong.
192
201
  */
193
202
  export declare function verifyWebhook<T = unknown>(rawBody: string, header: string | null | undefined, secret?: string | undefined, nowSeconds?: number): WebhookEvent<T>;
203
+ /** Someone the project emails: newsletters, announcements, automations (docs: email). */
204
+ export interface ContactInput {
205
+ email: string;
206
+ first_name?: string | null;
207
+ last_name?: string | null;
208
+ /** Merged into what is stored; `null` removes a key. Use them in emails ({{plan|free}}) and segments. */
209
+ properties?: Record<string, string | number | boolean | null>;
210
+ /** List ids or names; a name that doesn't exist yet is created. */
211
+ lists?: string[];
212
+ /** `false` unsubscribes (from `lists` if given, else from everything). */
213
+ subscribed?: boolean;
214
+ /** Send a confirmation email first; they're `pending` until they click it. */
215
+ double_opt_in?: boolean;
216
+ /** How they agreed, kept as the record: method (`app_signup`), the words, IP, user agent. */
217
+ consent?: {
218
+ method?: string;
219
+ text?: string;
220
+ ip?: string;
221
+ user_agent?: string;
222
+ at?: string;
223
+ };
224
+ }
225
+ export interface Contact {
226
+ object: 'contact';
227
+ id: string;
228
+ email: string;
229
+ first_name: string | null;
230
+ last_name: string | null;
231
+ properties: Record<string, unknown>;
232
+ status: 'subscribed' | 'pending' | 'unsubscribed' | 'bounced' | 'complained' | 'dormant';
233
+ lists?: Array<{
234
+ id: string;
235
+ name: string;
236
+ status: 'subscribed' | 'unsubscribed';
237
+ }>;
238
+ created_at: string;
239
+ /** Development keys never touch the real list: `false` there, and nothing is stored. */
240
+ recorded?: false;
241
+ /** Bounced or marked mail as spam before: gets no marketing email. */
242
+ suppressed?: true;
243
+ confirmation_sent?: true;
244
+ }
245
+ /** A file in the project's Files (docs: files). */
246
+ export interface AppFile {
247
+ object: 'file';
248
+ id: string;
249
+ project_id: string;
250
+ environment: 'production' | 'development';
251
+ name: string;
252
+ folder: string | null;
253
+ content_type: string;
254
+ kind: 'image' | 'video' | 'audio' | 'other';
255
+ size: number;
256
+ sha256: string;
257
+ source: 'upload' | 'ai';
258
+ private: boolean;
259
+ /** Permanent for public files; a signed link valid one hour for private ones. */
260
+ url: string;
261
+ url_expires_at: string | null;
262
+ /** Images; null otherwise or unknown. */
263
+ width: number | null;
264
+ height: number | null;
265
+ /** Video and audio; null when unknown. */
266
+ duration_seconds: number | null;
267
+ tags: string[];
268
+ note: string | null;
269
+ ai: {
270
+ model: string;
271
+ prompt: string;
272
+ cost_usd: number;
273
+ call_id: string | null;
274
+ } | null;
275
+ created_at: string;
276
+ }
194
277
  export declare class VibizApp {
195
278
  private readonly options;
196
279
  private loaded;
280
+ /** When `webhooks.receive` last fetched the signing secret again. */
281
+ private secretRefreshedAt;
282
+ /** The signing secret as Vibiz last gave it to `webhooks.receive`. */
283
+ private webhookSecret;
197
284
  constructor(options?: VibizAppOptions);
198
285
  /** The key, read when first needed, so `loadEnv()` or a late env still works. */
199
286
  private get key();
@@ -251,7 +338,11 @@ export declare class VibizApp {
251
338
  status: string;
252
339
  }>>;
253
340
  };
254
- /** Any action (`web.scrape`, `web.search`, `places.search`, `email.send`…), billed to the project. */
341
+ /**
342
+ * Any action, billed to the project: web data (`web.scrape`, `web.search`, `web.crawl`,
343
+ * `web.screenshot`, `web.design`, `ads.library`, `places.search`, `reviews`, `social.profile`,
344
+ * `social.posts`, `ecommerce.product`, `linkedin.company`) or `email.send`.
345
+ */
255
346
  readonly actions: {
256
347
  run: <O = unknown>(name: string, input: unknown) => Promise<ActionResult<O>>;
257
348
  };
@@ -309,9 +400,97 @@ export declare class VibizApp {
309
400
  body?: unknown;
310
401
  }) => Promise<ConnectorResponse<T>>;
311
402
  };
312
- /** Webhooks from Vibiz: `verify(rawBody, header)` checks the signature with VIBIZ_WEBHOOK_SECRET. */
403
+ /**
404
+ * The project's Files (docs: files), in this key's environment: uploads
405
+ * (avatars, product images, downloads) with a permanent link to put in a
406
+ * page or a post. AI images and speech made with this key land here too.
407
+ */
408
+ readonly files: {
409
+ /** Upload bytes or a Blob (at most 50 MB) → the file, with its `url`. */
410
+ upload: (file: Uint8Array | Blob, opts: {
411
+ name: string;
412
+ contentType?: string;
413
+ folder?: string;
414
+ private?: boolean;
415
+ }) => Promise<AppFile>;
416
+ /** Newest first; `q` matches the name, the AI prompt, tags and note. */
417
+ list: (opts?: {
418
+ type?: AppFile["kind"];
419
+ source?: AppFile["source"];
420
+ q?: string;
421
+ folder?: string;
422
+ /** An ISO time or date, or relative: `30m`, `4h`, `2d`, `1w`. */
423
+ since?: string;
424
+ until?: string;
425
+ model?: string;
426
+ tag?: string;
427
+ limit?: number;
428
+ cursor?: string;
429
+ }) => Promise<{
430
+ data: AppFile[];
431
+ has_more: boolean;
432
+ next_cursor: string | null;
433
+ total_files: number;
434
+ total_bytes: number;
435
+ }>;
436
+ /** The newest file matching the filters (`not_found` when none). */
437
+ latest: (opts?: {
438
+ type?: AppFile["kind"];
439
+ source?: AppFile["source"];
440
+ model?: string;
441
+ tag?: string;
442
+ }) => Promise<AppFile>;
443
+ get: (id: string) => Promise<AppFile>;
444
+ /** Delete it: its link stops working. */
445
+ delete: (id: string) => Promise<{
446
+ object: "file";
447
+ id: string;
448
+ deleted: true;
449
+ }>;
450
+ };
451
+ /**
452
+ * The project's contacts (docs: email ▸ Contacts): who gets its newsletters and
453
+ * automations. Add users at signup, keep their properties current, and track
454
+ * what they do to start automations (`track(email, 'upgraded')`). Nobody who
455
+ * unsubscribed, bounced or complained is subscribed again by an upsert. A
456
+ * development key answers but stores nothing.
457
+ */
458
+ readonly contacts: {
459
+ upsert: (input: ContactInput) => Promise<Contact>;
460
+ /** Something the contact did: segments and automations (event triggers, exit events) read it. */
461
+ track: (email: string, event: string, properties?: Record<string, string | number | boolean | null>) => Promise<{
462
+ object: "contact_event";
463
+ recorded: boolean;
464
+ reason?: string;
465
+ }>;
466
+ /** They asked to stop (your own settings page): no more marketing email from this project. */
467
+ unsubscribe: (email: string) => Promise<{
468
+ object: "contact";
469
+ email: string;
470
+ found: boolean;
471
+ }>;
472
+ get: (email: string) => Promise<Contact>;
473
+ /** Forget them (an account deleted). An address that left stays suppressed. */
474
+ delete: (email: string) => Promise<{
475
+ object: "contact";
476
+ deleted: boolean;
477
+ }>;
478
+ };
479
+ /**
480
+ * Webhooks from Vibiz. `receive(request)` is all a route needs with only
481
+ * VIBIZ_APP_KEY on the host: it reads the raw body and the signature, and
482
+ * fetches the signing secret with the app key when the env doesn't hold it
483
+ * (again once if a signature doesn't match, in case it was rotated).
484
+ * `verify(rawBody, header)` is the synchronous check with VIBIZ_WEBHOOK_SECRET.
485
+ */
313
486
  readonly webhooks: {
314
487
  verify: <T = unknown>(rawBody: string, header: string | null | undefined, secret?: string) => WebhookEvent<T>;
488
+ receive: <T = unknown>(request: {
489
+ text(): Promise<string>;
490
+ headers: {
491
+ get(name: string): string | null;
492
+ };
493
+ }) => Promise<WebhookEvent<T>>;
315
494
  };
316
495
  /**
317
496
  * A server-side event in the project's analytics, next to the browser's.
package/dist/index.js CHANGED
@@ -145,7 +145,11 @@ function sameString(a, b) {
145
145
  function verifyWebhook(rawBody, header, secret = runtimeEnv().VIBIZ_WEBHOOK_SECRET, nowSeconds = Math.floor(Date.now() / 1e3)) {
146
146
  const bad = (why) => new VibizAppError(400, "invalid_signature", `Webhook refused: ${why}.`);
147
147
  if (!secret) {
148
- throw new VibizAppError(500, "not_configured", "No VIBIZ_WEBHOOK_SECRET in the env.");
148
+ throw new VibizAppError(
149
+ 500,
150
+ "not_configured",
151
+ "No VIBIZ_WEBHOOK_SECRET in the env. With only VIBIZ_APP_KEY, use `await vibiz.webhooks.receive(request)`."
152
+ );
149
153
  }
150
154
  if (!header) throw bad("no Vibiz-Signature header");
151
155
  let t;
@@ -173,9 +177,19 @@ function userHeader(user) {
173
177
  for (const b of new TextEncoder().encode(user)) bin += String.fromCharCode(b);
174
178
  return `b64u:${btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")}`;
175
179
  }
180
+ var query = (q) => {
181
+ const p = new URLSearchParams();
182
+ for (const [k, v] of Object.entries(q)) if (v !== void 0 && v !== "") p.set(k, String(v));
183
+ const s = p.toString();
184
+ return s ? `?${s}` : "";
185
+ };
176
186
  var VibizApp = class _VibizApp {
177
187
  options;
178
188
  loaded = null;
189
+ /** When `webhooks.receive` last fetched the signing secret again. */
190
+ secretRefreshedAt = 0;
191
+ /** The signing secret as Vibiz last gave it to `webhooks.receive`. */
192
+ webhookSecret;
179
193
  constructor(options = {}) {
180
194
  this.options = options;
181
195
  }
@@ -207,14 +221,16 @@ var VibizApp = class _VibizApp {
207
221
  );
208
222
  }
209
223
  const res = await (this.options.fetch ?? fetch)(`${this.url}/v1${path}`, {
210
- method: init.method ?? (init.body === void 0 ? "GET" : "POST"),
224
+ method: init.method ?? (init.body === void 0 && !init.raw ? "GET" : "POST"),
211
225
  headers: {
212
226
  authorization: `Bearer ${this.key}`,
213
227
  accept: "application/json",
214
228
  ...init.body !== void 0 ? { "content-type": "application/json" } : {},
229
+ ...init.raw ? { "content-type": init.contentType ?? "application/octet-stream" } : {},
215
230
  ...this.options.user ? { "x-vibiz-user": userHeader(this.options.user) } : {}
216
231
  },
217
- ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {}
232
+ ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
233
+ ...init.raw ? { body: init.raw } : {}
218
234
  });
219
235
  const text = await res.text();
220
236
  let data = null;
@@ -285,7 +301,11 @@ var VibizApp = class _VibizApp {
285
301
  email = {
286
302
  send: (input) => this.actions.run("email.send", input)
287
303
  };
288
- /** Any action (`web.scrape`, `web.search`, `places.search`, `email.send`…), billed to the project. */
304
+ /**
305
+ * Any action, billed to the project: web data (`web.scrape`, `web.search`, `web.crawl`,
306
+ * `web.screenshot`, `web.design`, `ads.library`, `places.search`, `reviews`, `social.profile`,
307
+ * `social.posts`, `ecommerce.product`, `linkedin.company`) or `email.send`.
308
+ */
289
309
  actions = {
290
310
  run: (name, input) => this.request(`/app/actions/${encodeURIComponent(name)}`, {
291
311
  body: { input }
@@ -355,9 +375,94 @@ var VibizApp = class _VibizApp {
355
375
  body: { operation, ...input }
356
376
  })
357
377
  };
358
- /** Webhooks from Vibiz: `verify(rawBody, header)` checks the signature with VIBIZ_WEBHOOK_SECRET. */
378
+ /**
379
+ * The project's Files (docs: files), in this key's environment: uploads
380
+ * (avatars, product images, downloads) with a permanent link to put in a
381
+ * page or a post. AI images and speech made with this key land here too.
382
+ */
383
+ files = {
384
+ /** Upload bytes or a Blob (at most 50 MB) → the file, with its `url`. */
385
+ upload: async (file, opts) => {
386
+ const raw = file instanceof Uint8Array ? file : new Uint8Array(await file.arrayBuffer());
387
+ const contentType = opts.contentType ?? (file instanceof Blob && file.type ? file.type : void 0);
388
+ return this.request(
389
+ `/app/files${query({ name: opts.name, folder: opts.folder, private: opts.private ? 1 : void 0 })}`,
390
+ { raw, ...contentType ? { contentType } : {} }
391
+ );
392
+ },
393
+ /** Newest first; `q` matches the name, the AI prompt, tags and note. */
394
+ list: (opts = {}) => this.request(`/app/files${query(opts)}`),
395
+ /** The newest file matching the filters (`not_found` when none). */
396
+ latest: (opts = {}) => this.request(`/app/files/latest${query(opts)}`),
397
+ get: (id) => this.request(`/app/files/${encodeURIComponent(id)}`),
398
+ /** Delete it: its link stops working. */
399
+ delete: (id) => this.request(
400
+ `/app/files/${encodeURIComponent(id)}`,
401
+ { method: "DELETE" }
402
+ )
403
+ };
404
+ /**
405
+ * The project's contacts (docs: email ▸ Contacts): who gets its newsletters and
406
+ * automations. Add users at signup, keep their properties current, and track
407
+ * what they do to start automations (`track(email, 'upgraded')`). Nobody who
408
+ * unsubscribed, bounced or complained is subscribed again by an upsert. A
409
+ * development key answers but stores nothing.
410
+ */
411
+ contacts = {
412
+ upsert: (input) => this.request("/app/contacts", { body: input }),
413
+ /** Something the contact did: segments and automations (event triggers, exit events) read it. */
414
+ track: (email, event, properties) => this.request(
415
+ "/app/contacts/events",
416
+ {
417
+ body: { email, event, ...properties ? { properties } : {} }
418
+ }
419
+ ),
420
+ /** They asked to stop (your own settings page): no more marketing email from this project. */
421
+ unsubscribe: (email) => this.request(
422
+ "/app/contacts/unsubscribe",
423
+ {
424
+ body: { email }
425
+ }
426
+ ),
427
+ get: (email) => this.request(`/app/contacts/${encodeURIComponent(email)}`),
428
+ /** Forget them (an account deleted). An address that left stays suppressed. */
429
+ delete: (email) => this.request(
430
+ `/app/contacts/${encodeURIComponent(email)}`,
431
+ {
432
+ method: "DELETE"
433
+ }
434
+ )
435
+ };
436
+ /**
437
+ * Webhooks from Vibiz. `receive(request)` is all a route needs with only
438
+ * VIBIZ_APP_KEY on the host: it reads the raw body and the signature, and
439
+ * fetches the signing secret with the app key when the env doesn't hold it
440
+ * (again once if a signature doesn't match, in case it was rotated).
441
+ * `verify(rawBody, header)` is the synchronous check with VIBIZ_WEBHOOK_SECRET.
442
+ */
359
443
  webhooks = {
360
- verify: (rawBody, header, secret) => verifyWebhook(rawBody, header, secret)
444
+ verify: (rawBody, header, secret) => verifyWebhook(rawBody, header, secret),
445
+ receive: async (request) => {
446
+ const raw = await request.text();
447
+ const header = request.headers.get("vibiz-signature");
448
+ const secret = async (refresh = false) => {
449
+ if (!refresh) {
450
+ const known = this.webhookSecret ?? runtimeEnv().VIBIZ_WEBHOOK_SECRET;
451
+ if (known) return known;
452
+ }
453
+ this.webhookSecret = (await this.loadEnv({ refresh })).VIBIZ_WEBHOOK_SECRET;
454
+ return this.webhookSecret;
455
+ };
456
+ try {
457
+ return verifyWebhook(raw, header, await secret());
458
+ } catch (e) {
459
+ const reason = e instanceof VibizAppError ? e.code : "";
460
+ if (reason !== "invalid_signature" && reason !== "not_configured") throw e;
461
+ if (Date.now() - this.secretRefreshedAt < 6e4) throw e;
462
+ this.secretRefreshedAt = Date.now();
463
+ return verifyWebhook(raw, header, await secret(true));
464
+ }
465
+ }
361
466
  };
362
467
  /**
363
468
  * A server-side event in the project's analytics, next to the browser's.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibiz/app",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "One key for your app: env, AI, email, actions and events from Vibiz Stack.",
5
5
  "keywords": [
6
6
  "vibiz",