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