@vibiz/app 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/README.md +22 -2
- package/dist/index.d.ts +505 -0
- package/dist/index.js +500 -0
- package/package.json +38 -4
package/README.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @vibiz/app
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
One key for your app on [Vibiz Stack](https://app.vibiz.app): its env, AI, email, actions and
|
|
4
|
+
server-side events.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install @vibiz/app
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { vibiz } from '@vibiz/app';
|
|
12
|
+
|
|
13
|
+
await vibiz.loadEnv(); // hosts that hold only VIBIZ_APP_KEY
|
|
14
|
+
const openai = new OpenAI(vibiz.ai); // OpenAI-compatible AI gateway
|
|
15
|
+
await vibiz.email.send({ to, subject, text });
|
|
16
|
+
await vibiz.user(userId).track('Upgraded', { properties: { plan: 'pro' } });
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`VIBIZ_APP_KEY` is in your app's env already when it runs on Vibiz hosting or a connected host.
|
|
20
|
+
It is a server secret: never ship it to a browser.
|
|
21
|
+
|
|
22
|
+
No dependencies. Node 20+, Bun, Deno and edge runtimes; ESM with TypeScript types.
|
|
23
|
+
Docs: https://app.vibiz.app/docs/app-sdk
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vibiz/app: everything a running app needs from Vibiz Stack, on one key.
|
|
3
|
+
*
|
|
4
|
+
* import { vibiz } from '@vibiz/app';
|
|
5
|
+
* await vibiz.loadEnv(); // hosts that hold only VIBIZ_APP_KEY
|
|
6
|
+
* await vibiz.email.send({ to, subject, text });
|
|
7
|
+
* await vibiz.user(userId).track('Upgraded', { properties: { plan: 'pro' } });
|
|
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
|
|
12
|
+
*
|
|
13
|
+
* The key (`VIBIZ_APP_KEY`, `vz_app_…`) belongs to one project and one
|
|
14
|
+
* environment. It is a server secret: this client refuses to run in a
|
|
15
|
+
* browser. No dependencies; Node 18+, Bun, Deno and edge runtimes.
|
|
16
|
+
*/
|
|
17
|
+
export interface VibizAppOptions {
|
|
18
|
+
/** Default: `VIBIZ_APP_KEY`. */
|
|
19
|
+
key?: string;
|
|
20
|
+
/** Default: `VIBIZ_API_URL`, then https://app.vibiz.app. */
|
|
21
|
+
url?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Your own user's id (a stable pseudonymous id, not an email), for spend and
|
|
24
|
+
* events per user. `vibiz.user(id)` sets it per request.
|
|
25
|
+
*/
|
|
26
|
+
user?: string;
|
|
27
|
+
fetch?: typeof fetch;
|
|
28
|
+
/** The key is a server secret. Set only if you understand who can read it. */
|
|
29
|
+
dangerouslyAllowBrowser?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** An error from Vibiz: the public `code` (`rate_limited`, `invalid_request`…) and HTTP status. */
|
|
32
|
+
export declare class VibizAppError extends Error {
|
|
33
|
+
readonly status: number;
|
|
34
|
+
readonly code: string;
|
|
35
|
+
readonly details: unknown;
|
|
36
|
+
constructor(status: number, code: string, message: string, details?: unknown);
|
|
37
|
+
}
|
|
38
|
+
export interface SendEmail {
|
|
39
|
+
to: string;
|
|
40
|
+
subject: string;
|
|
41
|
+
text?: string;
|
|
42
|
+
html?: string;
|
|
43
|
+
/** The mailbox on the project's mail domain: `info` → `info@<project>.vibiz.app`. */
|
|
44
|
+
from?: string;
|
|
45
|
+
from_name?: string;
|
|
46
|
+
reply_to?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface ActionResult<O> {
|
|
49
|
+
output: O;
|
|
50
|
+
cost_micro_usd: number | null;
|
|
51
|
+
/** The run in the project's web data history (console ▸ Web data). */
|
|
52
|
+
run_id?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface ChatMessage {
|
|
55
|
+
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
56
|
+
content: string;
|
|
57
|
+
}
|
|
58
|
+
export interface TrackOptions {
|
|
59
|
+
/** Defaults to the client's user. */
|
|
60
|
+
user?: string;
|
|
61
|
+
path?: string;
|
|
62
|
+
properties?: Record<string, string | number | boolean | null>;
|
|
63
|
+
}
|
|
64
|
+
/** A payment, as the API returns it (docs/PAYMENTS.md). */
|
|
65
|
+
export interface Payment {
|
|
66
|
+
object: 'payment';
|
|
67
|
+
id: string;
|
|
68
|
+
status: 'succeeded' | 'refunded' | 'partially_refunded' | 'disputed' | 'failed';
|
|
69
|
+
amount_cents: number;
|
|
70
|
+
refunded_cents: number;
|
|
71
|
+
fee_cents: number;
|
|
72
|
+
net_cents: number;
|
|
73
|
+
currency: 'usd';
|
|
74
|
+
description: string | null;
|
|
75
|
+
user: string | null;
|
|
76
|
+
customer_email: string | null;
|
|
77
|
+
price_id: string | null;
|
|
78
|
+
subscription_id: string | null;
|
|
79
|
+
metadata: Record<string, string>;
|
|
80
|
+
card: {
|
|
81
|
+
brand: string;
|
|
82
|
+
last4: string;
|
|
83
|
+
} | null;
|
|
84
|
+
/** `own`: charged on the owner's own Stripe account (docs/PAYMENTS.md "Your own account"). */
|
|
85
|
+
account: 'vibiz' | 'own';
|
|
86
|
+
dispute: {
|
|
87
|
+
status: 'needs_response' | 'under_review' | 'won' | 'lost';
|
|
88
|
+
amount_cents: number;
|
|
89
|
+
reason: string;
|
|
90
|
+
} | null;
|
|
91
|
+
available_at: string;
|
|
92
|
+
created_at: string;
|
|
93
|
+
}
|
|
94
|
+
export interface Subscription {
|
|
95
|
+
object: 'subscription';
|
|
96
|
+
id: string;
|
|
97
|
+
status: 'active' | 'trialing' | 'past_due' | 'canceled' | 'incomplete' | 'unpaid';
|
|
98
|
+
user: string | null;
|
|
99
|
+
customer_email: string | null;
|
|
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';
|
|
103
|
+
current_period_end: string | null;
|
|
104
|
+
cancel_at_period_end: boolean;
|
|
105
|
+
created_at: string;
|
|
106
|
+
}
|
|
107
|
+
export interface Price {
|
|
108
|
+
object: 'price';
|
|
109
|
+
id: string;
|
|
110
|
+
product_id: string;
|
|
111
|
+
active: boolean;
|
|
112
|
+
unit_amount_cents: number;
|
|
113
|
+
currency: 'usd';
|
|
114
|
+
type: 'one_time' | 'recurring';
|
|
115
|
+
interval: 'day' | 'week' | 'month' | 'year' | null;
|
|
116
|
+
interval_count: number | null;
|
|
117
|
+
lookup_key: string | null;
|
|
118
|
+
}
|
|
119
|
+
export interface Product {
|
|
120
|
+
object: 'product';
|
|
121
|
+
id: string;
|
|
122
|
+
name: string;
|
|
123
|
+
description: string | null;
|
|
124
|
+
active: boolean;
|
|
125
|
+
prices: Price[];
|
|
126
|
+
created_at: string;
|
|
127
|
+
}
|
|
128
|
+
export interface CheckoutInput {
|
|
129
|
+
/** One of these… */
|
|
130
|
+
price_id?: string;
|
|
131
|
+
lookup_key?: string;
|
|
132
|
+
/** …or an ad-hoc one-time item. */
|
|
133
|
+
amount_cents?: number;
|
|
134
|
+
name?: string;
|
|
135
|
+
quantity?: number;
|
|
136
|
+
/** Your own user's id: their payments and subscriptions are found by it. */
|
|
137
|
+
user?: string;
|
|
138
|
+
customer_email?: string;
|
|
139
|
+
success_url: string;
|
|
140
|
+
cancel_url: string;
|
|
141
|
+
/** Up to 20 keys, echoed on the payment and in webhooks. */
|
|
142
|
+
metadata?: Record<string, string>;
|
|
143
|
+
}
|
|
144
|
+
/** An event Vibiz sent to your webhook, verified. */
|
|
145
|
+
/** A connector as the app sees it: what it may call, never a key. */
|
|
146
|
+
export interface AppConnector {
|
|
147
|
+
object: 'connector';
|
|
148
|
+
name: string;
|
|
149
|
+
label: string;
|
|
150
|
+
ready: boolean;
|
|
151
|
+
pricing: {
|
|
152
|
+
billed_by: string;
|
|
153
|
+
url: string | null;
|
|
154
|
+
};
|
|
155
|
+
operations: Array<{
|
|
156
|
+
id: string;
|
|
157
|
+
method: string;
|
|
158
|
+
path: string;
|
|
159
|
+
description: string;
|
|
160
|
+
risk: 'read' | 'write' | 'dangerous';
|
|
161
|
+
params: Record<string, unknown>;
|
|
162
|
+
body: Record<string, unknown> | null;
|
|
163
|
+
}>;
|
|
164
|
+
}
|
|
165
|
+
/** An API's answer through a connector: its status (a 404 there is `ok: false`), headers and data. */
|
|
166
|
+
export interface ConnectorResponse<T = unknown> {
|
|
167
|
+
object: 'connector_response';
|
|
168
|
+
call_id: string;
|
|
169
|
+
connector: string;
|
|
170
|
+
operation: string;
|
|
171
|
+
environment: 'production' | 'development';
|
|
172
|
+
status: number;
|
|
173
|
+
ok: boolean;
|
|
174
|
+
headers: Record<string, string>;
|
|
175
|
+
content_type: string | null;
|
|
176
|
+
data: T;
|
|
177
|
+
data_base64?: string;
|
|
178
|
+
truncated: boolean;
|
|
179
|
+
untrusted: true;
|
|
180
|
+
notice: string;
|
|
181
|
+
pricing: {
|
|
182
|
+
billed_by: string;
|
|
183
|
+
url: string | null;
|
|
184
|
+
};
|
|
185
|
+
duration_ms: number;
|
|
186
|
+
}
|
|
187
|
+
export interface WebhookEvent<T = unknown> {
|
|
188
|
+
id: string;
|
|
189
|
+
object: 'event';
|
|
190
|
+
type: string;
|
|
191
|
+
created: number;
|
|
192
|
+
environment: 'production' | 'development';
|
|
193
|
+
data: {
|
|
194
|
+
object: T;
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Check a webhook from Vibiz: `Vibiz-Signature: t=<unix>,v1=<hex HMAC-SHA256 of
|
|
199
|
+
* "${t}.${body}">` with your `VIBIZ_WEBHOOK_SECRET`, at most 5 minutes old.
|
|
200
|
+
* Pass the raw body, exactly as received. Throws on anything wrong.
|
|
201
|
+
*/
|
|
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
|
+
}
|
|
277
|
+
export declare class VibizApp {
|
|
278
|
+
private readonly options;
|
|
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;
|
|
284
|
+
constructor(options?: VibizAppOptions);
|
|
285
|
+
/** The key, read when first needed, so `loadEnv()` or a late env still works. */
|
|
286
|
+
private get key();
|
|
287
|
+
private get url();
|
|
288
|
+
/** A client that attributes spend and events to one of your users. */
|
|
289
|
+
user(id: string): VibizApp;
|
|
290
|
+
private request;
|
|
291
|
+
/** Which project and environment this key is. */
|
|
292
|
+
whoami(): Promise<{
|
|
293
|
+
project_id: string;
|
|
294
|
+
environment: "production" | "development";
|
|
295
|
+
}>;
|
|
296
|
+
/**
|
|
297
|
+
* Fetch the app's env from Vibiz and put it into `process.env` (existing
|
|
298
|
+
* variables win unless `override`). For hosts that hold only VIBIZ_APP_KEY:
|
|
299
|
+
* call it once at boot, before anything reads the env. Fetched once per
|
|
300
|
+
* process; `refresh: true` fetches again (after a rotation).
|
|
301
|
+
*/
|
|
302
|
+
loadEnv(opts?: {
|
|
303
|
+
override?: boolean;
|
|
304
|
+
refresh?: boolean;
|
|
305
|
+
}): Promise<Record<string, string>>;
|
|
306
|
+
/**
|
|
307
|
+
* The AI gateway, OpenAI-compatible: `new OpenAI(vibiz.ai)` or the Vercel AI
|
|
308
|
+
* SDK's `createOpenAI(vibiz.ai)`. Models: `vibiz/fast`, `vibiz/smart`…
|
|
309
|
+
*/
|
|
310
|
+
get ai(): {
|
|
311
|
+
readonly baseURL: string;
|
|
312
|
+
readonly apiKey: string;
|
|
313
|
+
/** One chat completion, without an SDK. */
|
|
314
|
+
chat: (input: {
|
|
315
|
+
messages: ChatMessage[];
|
|
316
|
+
model?: string;
|
|
317
|
+
max_tokens?: number;
|
|
318
|
+
temperature?: number;
|
|
319
|
+
}) => Promise<{
|
|
320
|
+
choices: {
|
|
321
|
+
message: {
|
|
322
|
+
role: string;
|
|
323
|
+
content: string | null;
|
|
324
|
+
};
|
|
325
|
+
}[];
|
|
326
|
+
usage?: {
|
|
327
|
+
prompt_tokens: number;
|
|
328
|
+
completion_tokens: number;
|
|
329
|
+
};
|
|
330
|
+
cost_micro_usd?: number;
|
|
331
|
+
}>;
|
|
332
|
+
};
|
|
333
|
+
/** Email from the project's own address (needs the `email` resource). */
|
|
334
|
+
readonly email: {
|
|
335
|
+
send: (input: SendEmail) => Promise<ActionResult<{
|
|
336
|
+
id: string;
|
|
337
|
+
from: string;
|
|
338
|
+
status: string;
|
|
339
|
+
}>>;
|
|
340
|
+
};
|
|
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
|
+
*/
|
|
346
|
+
readonly actions: {
|
|
347
|
+
run: <O = unknown>(name: string, input: unknown) => Promise<ActionResult<O>>;
|
|
348
|
+
};
|
|
349
|
+
/**
|
|
350
|
+
* Payments (docs/PAYMENTS.md): Vibiz is the merchant, so this works from the
|
|
351
|
+
* first call, before the owner has verified anything. Refunds and
|
|
352
|
+
* withdrawals are the owner's, in the console.
|
|
353
|
+
*/
|
|
354
|
+
readonly payments: {
|
|
355
|
+
/** A hosted checkout page for a price (one-time or a subscription) or an ad-hoc item. */
|
|
356
|
+
checkout: (input: CheckoutInput) => Promise<{
|
|
357
|
+
object: "checkout";
|
|
358
|
+
id: string;
|
|
359
|
+
url: string;
|
|
360
|
+
expires_at: string;
|
|
361
|
+
}>;
|
|
362
|
+
/** The user's live subscription (active, trialing or past due), or null. */
|
|
363
|
+
subscription: (user: string) => Promise<Subscription | null>;
|
|
364
|
+
/** Whether the user has an active (or trialing) subscription, optionally to one price id or lookup key. */
|
|
365
|
+
hasActive: (user: string, price?: string) => Promise<boolean>;
|
|
366
|
+
/** A link where the user manages their subscription and card. */
|
|
367
|
+
portal: (user: string, opts: {
|
|
368
|
+
return_url: string;
|
|
369
|
+
}) => Promise<{
|
|
370
|
+
object: "portal_session";
|
|
371
|
+
url: string;
|
|
372
|
+
}>;
|
|
373
|
+
/** The catalog: products with their prices. */
|
|
374
|
+
products: () => Promise<Product[]>;
|
|
375
|
+
/** Payments, newest first (`user` keeps one of yours). */
|
|
376
|
+
payments: (opts?: {
|
|
377
|
+
user?: string;
|
|
378
|
+
cursor?: string;
|
|
379
|
+
limit?: number;
|
|
380
|
+
}) => Promise<{
|
|
381
|
+
data: Payment[];
|
|
382
|
+
has_more: boolean;
|
|
383
|
+
next_cursor: string | null;
|
|
384
|
+
}>;
|
|
385
|
+
/** Cancel a user's subscription (at the end of the period by default). */
|
|
386
|
+
cancel: (subscriptionId: string, opts?: {
|
|
387
|
+
at_period_end?: boolean;
|
|
388
|
+
}) => Promise<Subscription>;
|
|
389
|
+
};
|
|
390
|
+
/**
|
|
391
|
+
* Your connectors (docs/CONNECTORS.md): call an API through Vibiz, which
|
|
392
|
+
* holds the key for this app's environment. The answer's `data` is the
|
|
393
|
+
* API's own content. A dangerous operation needs the owner's permission.
|
|
394
|
+
*/
|
|
395
|
+
readonly connectors: {
|
|
396
|
+
/** The connectors with their enabled operations and parameters. */
|
|
397
|
+
list: () => Promise<AppConnector[]>;
|
|
398
|
+
call: <T = unknown>(name: string, operation: string, input?: {
|
|
399
|
+
params?: Record<string, unknown>;
|
|
400
|
+
body?: unknown;
|
|
401
|
+
}) => Promise<ConnectorResponse<T>>;
|
|
402
|
+
};
|
|
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
|
+
*/
|
|
486
|
+
readonly webhooks: {
|
|
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>>;
|
|
494
|
+
};
|
|
495
|
+
/**
|
|
496
|
+
* A server-side event in the project's analytics, next to the browser's.
|
|
497
|
+
* Never throws: analytics must not break the request that tracks it.
|
|
498
|
+
* Returns whether it was recorded (development keys don't record).
|
|
499
|
+
*/
|
|
500
|
+
track(name: string, opts?: TrackOptions): Promise<boolean>;
|
|
501
|
+
}
|
|
502
|
+
/** Create a client with explicit options. */
|
|
503
|
+
export declare const createVibiz: (options?: VibizAppOptions) => VibizApp;
|
|
504
|
+
/** The default client: VIBIZ_APP_KEY and VIBIZ_API_URL from the environment, read on use. */
|
|
505
|
+
export declare const vibiz: VibizApp;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
// packages/app/src/index.ts
|
|
2
|
+
var runtimeEnv = () => globalThis.process?.env ?? {};
|
|
3
|
+
var DEFAULT_URL = "https://app.vibiz.app";
|
|
4
|
+
var VibizAppError = class extends Error {
|
|
5
|
+
status;
|
|
6
|
+
code;
|
|
7
|
+
details;
|
|
8
|
+
constructor(status, code, message, details) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "VibizAppError";
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.details = details;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var WEBHOOK_TOLERANCE_S = 300;
|
|
17
|
+
var K = new Uint32Array([
|
|
18
|
+
1116352408,
|
|
19
|
+
1899447441,
|
|
20
|
+
3049323471,
|
|
21
|
+
3921009573,
|
|
22
|
+
961987163,
|
|
23
|
+
1508970993,
|
|
24
|
+
2453635748,
|
|
25
|
+
2870763221,
|
|
26
|
+
3624381080,
|
|
27
|
+
310598401,
|
|
28
|
+
607225278,
|
|
29
|
+
1426881987,
|
|
30
|
+
1925078388,
|
|
31
|
+
2162078206,
|
|
32
|
+
2614888103,
|
|
33
|
+
3248222580,
|
|
34
|
+
3835390401,
|
|
35
|
+
4022224774,
|
|
36
|
+
264347078,
|
|
37
|
+
604807628,
|
|
38
|
+
770255983,
|
|
39
|
+
1249150122,
|
|
40
|
+
1555081692,
|
|
41
|
+
1996064986,
|
|
42
|
+
2554220882,
|
|
43
|
+
2821834349,
|
|
44
|
+
2952996808,
|
|
45
|
+
3210313671,
|
|
46
|
+
3336571891,
|
|
47
|
+
3584528711,
|
|
48
|
+
113926993,
|
|
49
|
+
338241895,
|
|
50
|
+
666307205,
|
|
51
|
+
773529912,
|
|
52
|
+
1294757372,
|
|
53
|
+
1396182291,
|
|
54
|
+
1695183700,
|
|
55
|
+
1986661051,
|
|
56
|
+
2177026350,
|
|
57
|
+
2456956037,
|
|
58
|
+
2730485921,
|
|
59
|
+
2820302411,
|
|
60
|
+
3259730800,
|
|
61
|
+
3345764771,
|
|
62
|
+
3516065817,
|
|
63
|
+
3600352804,
|
|
64
|
+
4094571909,
|
|
65
|
+
275423344,
|
|
66
|
+
430227734,
|
|
67
|
+
506948616,
|
|
68
|
+
659060556,
|
|
69
|
+
883997877,
|
|
70
|
+
958139571,
|
|
71
|
+
1322822218,
|
|
72
|
+
1537002063,
|
|
73
|
+
1747873779,
|
|
74
|
+
1955562222,
|
|
75
|
+
2024104815,
|
|
76
|
+
2227730452,
|
|
77
|
+
2361852424,
|
|
78
|
+
2428436474,
|
|
79
|
+
2756734187,
|
|
80
|
+
3204031479,
|
|
81
|
+
3329325298
|
|
82
|
+
]);
|
|
83
|
+
function sha256(data) {
|
|
84
|
+
const h = [
|
|
85
|
+
1779033703,
|
|
86
|
+
3144134277,
|
|
87
|
+
1013904242,
|
|
88
|
+
2773480762,
|
|
89
|
+
1359893119,
|
|
90
|
+
2600822924,
|
|
91
|
+
528734635,
|
|
92
|
+
1541459225
|
|
93
|
+
];
|
|
94
|
+
const blocks = Math.ceil((data.length + 9) / 64);
|
|
95
|
+
const buf = new Uint8Array(blocks * 64);
|
|
96
|
+
buf.set(data);
|
|
97
|
+
buf[data.length] = 128;
|
|
98
|
+
const view = new DataView(buf.buffer);
|
|
99
|
+
view.setUint32(buf.length - 8, Math.floor(data.length / 536870912));
|
|
100
|
+
view.setUint32(buf.length - 4, data.length << 3 >>> 0);
|
|
101
|
+
const w = new Array(64).fill(0);
|
|
102
|
+
const rotr = (x, n) => x >>> n | x << 32 - n;
|
|
103
|
+
for (let b = 0; b < blocks; b++) {
|
|
104
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(b * 64 + i * 4);
|
|
105
|
+
for (let i = 16; i < 64; i++) {
|
|
106
|
+
const a15 = w[i - 15] ?? 0;
|
|
107
|
+
const a2 = w[i - 2] ?? 0;
|
|
108
|
+
const s0 = rotr(a15, 7) ^ rotr(a15, 18) ^ a15 >>> 3;
|
|
109
|
+
const s1 = rotr(a2, 17) ^ rotr(a2, 19) ^ a2 >>> 10;
|
|
110
|
+
w[i] = (w[i - 16] ?? 0) + s0 + (w[i - 7] ?? 0) + s1 >>> 0;
|
|
111
|
+
}
|
|
112
|
+
const v = [...h];
|
|
113
|
+
for (let i = 0; i < 64; i++) {
|
|
114
|
+
const [a = 0, bb = 0, c = 0, d = 0, e = 0, f = 0, g = 0, hh = 0] = v;
|
|
115
|
+
const t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + (K[i] ?? 0) + (w[i] ?? 0) >>> 0;
|
|
116
|
+
const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & bb ^ a & c ^ bb & c) >>> 0;
|
|
117
|
+
v.splice(0, 8, t1 + t2 >>> 0, a, bb, c, d + t1 >>> 0, e, f, g);
|
|
118
|
+
}
|
|
119
|
+
for (let i = 0; i < 8; i++) h[i] = (h[i] ?? 0) + (v[i] ?? 0) >>> 0;
|
|
120
|
+
}
|
|
121
|
+
const digest = new Uint8Array(32);
|
|
122
|
+
const dv = new DataView(digest.buffer);
|
|
123
|
+
for (let i = 0; i < 8; i++) dv.setUint32(i * 4, h[i] ?? 0);
|
|
124
|
+
return digest;
|
|
125
|
+
}
|
|
126
|
+
function hmacSha256Hex(key, message) {
|
|
127
|
+
const enc = new TextEncoder();
|
|
128
|
+
let k = enc.encode(key);
|
|
129
|
+
if (k.length > 64) k = sha256(k);
|
|
130
|
+
const pad = (byte, rest) => {
|
|
131
|
+
const out = new Uint8Array(64 + rest.length);
|
|
132
|
+
for (let i = 0; i < 64; i++) out[i] = (k[i] ?? 0) ^ byte;
|
|
133
|
+
out.set(rest, 64);
|
|
134
|
+
return out;
|
|
135
|
+
};
|
|
136
|
+
const inner = sha256(pad(54, enc.encode(message)));
|
|
137
|
+
return [...sha256(pad(92, inner))].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
138
|
+
}
|
|
139
|
+
function sameString(a, b) {
|
|
140
|
+
if (a.length !== b.length) return false;
|
|
141
|
+
let diff = 0;
|
|
142
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
143
|
+
return diff === 0;
|
|
144
|
+
}
|
|
145
|
+
function verifyWebhook(rawBody, header, secret = runtimeEnv().VIBIZ_WEBHOOK_SECRET, nowSeconds = Math.floor(Date.now() / 1e3)) {
|
|
146
|
+
const bad = (why) => new VibizAppError(400, "invalid_signature", `Webhook refused: ${why}.`);
|
|
147
|
+
if (!secret) {
|
|
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
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (!header) throw bad("no Vibiz-Signature header");
|
|
155
|
+
let t;
|
|
156
|
+
const sigs = [];
|
|
157
|
+
for (const part of header.split(",")) {
|
|
158
|
+
const i = part.indexOf("=");
|
|
159
|
+
const k = part.slice(0, i).trim();
|
|
160
|
+
const v = part.slice(i + 1).trim();
|
|
161
|
+
if (k === "t" && /^\d{1,12}$/.test(v)) t = Number(v);
|
|
162
|
+
else if (k === "v1" && v) sigs.push(v.toLowerCase());
|
|
163
|
+
}
|
|
164
|
+
if (t === void 0 || !sigs.length) throw bad("malformed signature");
|
|
165
|
+
if (Math.abs(nowSeconds - t) > WEBHOOK_TOLERANCE_S) throw bad("too old");
|
|
166
|
+
const expected = hmacSha256Hex(secret, `${t}.${rawBody}`);
|
|
167
|
+
if (!sigs.some((sig) => sameString(sig, expected))) throw bad("signature does not match");
|
|
168
|
+
try {
|
|
169
|
+
return JSON.parse(rawBody);
|
|
170
|
+
} catch {
|
|
171
|
+
throw bad("not JSON");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function userHeader(user) {
|
|
175
|
+
if (/^[\x20-\x7e]*$/.test(user) && !user.startsWith("b64u:")) return user;
|
|
176
|
+
let bin = "";
|
|
177
|
+
for (const b of new TextEncoder().encode(user)) bin += String.fromCharCode(b);
|
|
178
|
+
return `b64u:${btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")}`;
|
|
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
|
+
};
|
|
186
|
+
var VibizApp = class _VibizApp {
|
|
187
|
+
options;
|
|
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;
|
|
193
|
+
constructor(options = {}) {
|
|
194
|
+
this.options = options;
|
|
195
|
+
}
|
|
196
|
+
/** The key, read when first needed, so `loadEnv()` or a late env still works. */
|
|
197
|
+
get key() {
|
|
198
|
+
const key = this.options.key ?? runtimeEnv().VIBIZ_APP_KEY;
|
|
199
|
+
if (!key) {
|
|
200
|
+
throw new VibizAppError(
|
|
201
|
+
401,
|
|
202
|
+
"unauthorized",
|
|
203
|
+
"No app key. Set VIBIZ_APP_KEY (Vibiz puts it in your app\u2019s env), or pass { key }."
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return key;
|
|
207
|
+
}
|
|
208
|
+
get url() {
|
|
209
|
+
return (this.options.url ?? runtimeEnv().VIBIZ_API_URL ?? DEFAULT_URL).replace(/\/$/, "");
|
|
210
|
+
}
|
|
211
|
+
/** A client that attributes spend and events to one of your users. */
|
|
212
|
+
user(id) {
|
|
213
|
+
return new _VibizApp({ ...this.options, user: id });
|
|
214
|
+
}
|
|
215
|
+
async request(path, init = {}) {
|
|
216
|
+
if (typeof globalThis.window !== "undefined" && !this.options.dangerouslyAllowBrowser) {
|
|
217
|
+
throw new VibizAppError(
|
|
218
|
+
400,
|
|
219
|
+
"invalid_request",
|
|
220
|
+
"The app key is a server secret: call Vibiz from your server, not the browser."
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
const res = await (this.options.fetch ?? fetch)(`${this.url}/v1${path}`, {
|
|
224
|
+
method: init.method ?? (init.body === void 0 && !init.raw ? "GET" : "POST"),
|
|
225
|
+
headers: {
|
|
226
|
+
authorization: `Bearer ${this.key}`,
|
|
227
|
+
accept: "application/json",
|
|
228
|
+
...init.body !== void 0 ? { "content-type": "application/json" } : {},
|
|
229
|
+
...init.raw ? { "content-type": init.contentType ?? "application/octet-stream" } : {},
|
|
230
|
+
...this.options.user ? { "x-vibiz-user": userHeader(this.options.user) } : {}
|
|
231
|
+
},
|
|
232
|
+
...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
|
|
233
|
+
...init.raw ? { body: init.raw } : {}
|
|
234
|
+
});
|
|
235
|
+
const text = await res.text();
|
|
236
|
+
let data = null;
|
|
237
|
+
try {
|
|
238
|
+
data = text ? JSON.parse(text) : null;
|
|
239
|
+
} catch {
|
|
240
|
+
data = null;
|
|
241
|
+
}
|
|
242
|
+
if (!res.ok) {
|
|
243
|
+
const e = data?.error;
|
|
244
|
+
throw new VibizAppError(
|
|
245
|
+
res.status,
|
|
246
|
+
e?.code ?? "error",
|
|
247
|
+
e?.message ?? `Vibiz answered ${res.status}.`,
|
|
248
|
+
e?.details
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
return data;
|
|
252
|
+
}
|
|
253
|
+
/** Which project and environment this key is. */
|
|
254
|
+
whoami() {
|
|
255
|
+
return this.request("/app");
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Fetch the app's env from Vibiz and put it into `process.env` (existing
|
|
259
|
+
* variables win unless `override`). For hosts that hold only VIBIZ_APP_KEY:
|
|
260
|
+
* call it once at boot, before anything reads the env. Fetched once per
|
|
261
|
+
* process; `refresh: true` fetches again (after a rotation).
|
|
262
|
+
*/
|
|
263
|
+
async loadEnv(opts = {}) {
|
|
264
|
+
if (!this.loaded || opts.refresh) {
|
|
265
|
+
this.loaded = this.request("/app/env").then((r) => r.env);
|
|
266
|
+
this.loaded.catch(() => {
|
|
267
|
+
this.loaded = null;
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const env = await this.loaded;
|
|
271
|
+
const target = runtimeEnv();
|
|
272
|
+
for (const [k, v] of Object.entries(env)) {
|
|
273
|
+
if (opts.override || target[k] === void 0) target[k] = v;
|
|
274
|
+
}
|
|
275
|
+
return env;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* The AI gateway, OpenAI-compatible: `new OpenAI(vibiz.ai)` or the Vercel AI
|
|
279
|
+
* SDK's `createOpenAI(vibiz.ai)`. Models: `vibiz/fast`, `vibiz/smart`…
|
|
280
|
+
*/
|
|
281
|
+
get ai() {
|
|
282
|
+
const self = this;
|
|
283
|
+
return {
|
|
284
|
+
get baseURL() {
|
|
285
|
+
return `${self.url}/v1/ai`;
|
|
286
|
+
},
|
|
287
|
+
get apiKey() {
|
|
288
|
+
return self.key;
|
|
289
|
+
},
|
|
290
|
+
/** One chat completion, without an SDK. */
|
|
291
|
+
chat: (input) => self.request("/ai/chat/completions", {
|
|
292
|
+
body: {
|
|
293
|
+
model: "vibiz/fast",
|
|
294
|
+
...input,
|
|
295
|
+
...self.options.user ? { user: self.options.user } : {}
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
/** Email from the project's own address (needs the `email` resource). */
|
|
301
|
+
email = {
|
|
302
|
+
send: (input) => this.actions.run("email.send", input)
|
|
303
|
+
};
|
|
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
|
+
*/
|
|
309
|
+
actions = {
|
|
310
|
+
run: (name, input) => this.request(`/app/actions/${encodeURIComponent(name)}`, {
|
|
311
|
+
body: { input }
|
|
312
|
+
})
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* Payments (docs/PAYMENTS.md): Vibiz is the merchant, so this works from the
|
|
316
|
+
* first call, before the owner has verified anything. Refunds and
|
|
317
|
+
* withdrawals are the owner's, in the console.
|
|
318
|
+
*/
|
|
319
|
+
payments = {
|
|
320
|
+
/** A hosted checkout page for a price (one-time or a subscription) or an ad-hoc item. */
|
|
321
|
+
checkout: (input) => this.request(
|
|
322
|
+
"/app/payments/checkout",
|
|
323
|
+
{ body: input }
|
|
324
|
+
),
|
|
325
|
+
/** The user's live subscription (active, trialing or past due), or null. */
|
|
326
|
+
subscription: async (user) => {
|
|
327
|
+
const r = await this.request(
|
|
328
|
+
`/app/payments/subscriptions?user=${encodeURIComponent(user)}&status=active,trialing,past_due`
|
|
329
|
+
);
|
|
330
|
+
return r.data[0] ?? null;
|
|
331
|
+
},
|
|
332
|
+
/** Whether the user has an active (or trialing) subscription, optionally to one price id or lookup key. */
|
|
333
|
+
hasActive: async (user, price) => {
|
|
334
|
+
const r = await this.request(
|
|
335
|
+
`/app/payments/subscriptions?user=${encodeURIComponent(user)}&status=active,trialing`
|
|
336
|
+
);
|
|
337
|
+
if (!price) return r.data.length > 0;
|
|
338
|
+
if (r.data.some((x) => x.price_id === price)) return true;
|
|
339
|
+
const products = await this.payments.products();
|
|
340
|
+
const ids = new Set(
|
|
341
|
+
products.flatMap((p) => p.prices).filter((p) => p.lookup_key === price).map((p) => p.id)
|
|
342
|
+
);
|
|
343
|
+
return r.data.some((x) => x.price_id !== null && ids.has(x.price_id));
|
|
344
|
+
},
|
|
345
|
+
/** A link where the user manages their subscription and card. */
|
|
346
|
+
portal: (user, opts) => this.request("/app/payments/portal", {
|
|
347
|
+
body: { user, return_url: opts.return_url }
|
|
348
|
+
}),
|
|
349
|
+
/** The catalog: products with their prices. */
|
|
350
|
+
products: () => this.request("/app/payments/products").then((r) => r.data),
|
|
351
|
+
/** Payments, newest first (`user` keeps one of yours). */
|
|
352
|
+
payments: (opts = {}) => {
|
|
353
|
+
const q = new URLSearchParams();
|
|
354
|
+
for (const [k, v] of Object.entries(opts)) if (v !== void 0) q.set(k, String(v));
|
|
355
|
+
const qs = q.toString();
|
|
356
|
+
return this.request(
|
|
357
|
+
`/app/payments/payments${qs ? `?${qs}` : ""}`
|
|
358
|
+
);
|
|
359
|
+
},
|
|
360
|
+
/** Cancel a user's subscription (at the end of the period by default). */
|
|
361
|
+
cancel: (subscriptionId, opts = {}) => this.request(
|
|
362
|
+
`/app/payments/subscriptions/${encodeURIComponent(subscriptionId)}/cancel`,
|
|
363
|
+
{ body: opts }
|
|
364
|
+
)
|
|
365
|
+
};
|
|
366
|
+
/**
|
|
367
|
+
* Your connectors (docs/CONNECTORS.md): call an API through Vibiz, which
|
|
368
|
+
* holds the key for this app's environment. The answer's `data` is the
|
|
369
|
+
* API's own content. A dangerous operation needs the owner's permission.
|
|
370
|
+
*/
|
|
371
|
+
connectors = {
|
|
372
|
+
/** The connectors with their enabled operations and parameters. */
|
|
373
|
+
list: () => this.request("/app/connectors").then((r) => r.data),
|
|
374
|
+
call: (name, operation, input = {}) => this.request(`/app/connectors/${encodeURIComponent(name)}/call`, {
|
|
375
|
+
body: { operation, ...input }
|
|
376
|
+
})
|
|
377
|
+
};
|
|
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
|
+
*/
|
|
443
|
+
webhooks = {
|
|
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
|
+
}
|
|
466
|
+
};
|
|
467
|
+
/**
|
|
468
|
+
* A server-side event in the project's analytics, next to the browser's.
|
|
469
|
+
* Never throws: analytics must not break the request that tracks it.
|
|
470
|
+
* Returns whether it was recorded (development keys don't record).
|
|
471
|
+
*/
|
|
472
|
+
async track(name, opts = {}) {
|
|
473
|
+
try {
|
|
474
|
+
const user = opts.user ?? this.options.user;
|
|
475
|
+
const r = await this.request("/app/events", {
|
|
476
|
+
body: {
|
|
477
|
+
name,
|
|
478
|
+
...user ? { user } : {},
|
|
479
|
+
...opts.path ? { path: opts.path } : {},
|
|
480
|
+
...opts.properties ? { properties: opts.properties } : {}
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
return r.recorded;
|
|
484
|
+
} catch (e) {
|
|
485
|
+
globalThis.console?.warn?.(
|
|
486
|
+
`vibiz.track(${name}) failed: ${e instanceof Error ? e.message : String(e)}`
|
|
487
|
+
);
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
var createVibiz = (options) => new VibizApp(options);
|
|
493
|
+
var vibiz = new VibizApp();
|
|
494
|
+
export {
|
|
495
|
+
VibizApp,
|
|
496
|
+
VibizAppError,
|
|
497
|
+
createVibiz,
|
|
498
|
+
verifyWebhook,
|
|
499
|
+
vibiz
|
|
500
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibiz/app",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "One key for your app: env, AI, email, actions and events from Vibiz Stack.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"vibiz",
|
|
7
|
+
"env",
|
|
8
|
+
"ai",
|
|
9
|
+
"email",
|
|
10
|
+
"analytics"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://app.vibiz.app/docs",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/Vibiz-ai/vibiz-stack.git",
|
|
16
|
+
"directory": "packages/app"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/Vibiz-ai/vibiz-stack/issues"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"types": "./dist/index.d.ts"
|
|
40
|
+
}
|