@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 CHANGED
@@ -1,3 +1,23 @@
1
- # Temporary Holding Version
1
+ # @vibiz/app
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
@@ -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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }