@vibiz/app 0.1.0 → 0.2.1
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 +181 -2
- package/dist/index.js +111 -6
- package/package.json +1 -1
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
|
-
/**
|
|
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
|
-
/**
|
|
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(
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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.
|