@omg-dev/server 0.4.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,3278 @@
1
+ import { TriggerScanError, extractTriggers, scanTriggers, writeTriggersManifest } from "./trigger-scan.mjs";
2
+ import path from "node:path";
3
+ import fs from "node:fs";
4
+ import { createAuthMiddleware, createAuthMiddleware as createAuthMiddleware$1 } from "@omg-dev/auth";
5
+ import { AsyncLocalStorage } from "node:async_hooks";
6
+ import { Database } from "bun:sqlite";
7
+ import { Topic } from "@omg-dev/stream/core";
8
+ import { collection, defineSchema, fields, schemaDiff, schemaToSQL } from "@omg-dev/schema";
9
+ import crypto$1 from "node:crypto";
10
+ //#region src/ctx.ts
11
+ const ctxStore = new AsyncLocalStorage();
12
+ const ctx = {
13
+ get userId() {
14
+ return ctxStore.getStore()?.userId ?? null;
15
+ },
16
+ get userEmail() {
17
+ return ctxStore.getStore()?.userEmail;
18
+ },
19
+ get userName() {
20
+ return ctxStore.getStore()?.userName;
21
+ },
22
+ get appId() {
23
+ return ctxStore.getStore()?.appId;
24
+ }
25
+ };
26
+ //#endregion
27
+ //#region src/broker.ts
28
+ const topic = new Topic();
29
+ const adapters = /* @__PURE__ */ new Map();
30
+ const decoder = new TextDecoder();
31
+ /** Extract the JSON payload from an SSE frame `id: N\ndata: <json>\n\n`. */
32
+ function extractData(chunk) {
33
+ const text = decoder.decode(chunk);
34
+ const dataLines = [];
35
+ for (const line of text.split("\n")) if (line.startsWith("data: ")) dataLines.push(line.slice(6));
36
+ return dataLines.join("\n");
37
+ }
38
+ function makeAdapter(client) {
39
+ const adapter = { async write(chunk) {
40
+ if (client.readyState !== 1) {
41
+ adapters.delete(client);
42
+ topic.detach(adapter);
43
+ throw new Error("client not open");
44
+ }
45
+ try {
46
+ client.send(extractData(chunk));
47
+ } catch (err) {
48
+ adapters.delete(client);
49
+ topic.detach(adapter);
50
+ throw err;
51
+ }
52
+ } };
53
+ return adapter;
54
+ }
55
+ function addClient(client) {
56
+ const adapter = makeAdapter(client);
57
+ adapters.set(client, adapter);
58
+ topic.attach(adapter);
59
+ }
60
+ function removeClient(client) {
61
+ const adapter = adapters.get(client);
62
+ if (!adapter) return;
63
+ adapters.delete(client);
64
+ topic.detach(adapter);
65
+ }
66
+ function invalidate(collection) {
67
+ topic.publish({
68
+ type: "invalidate",
69
+ collection
70
+ });
71
+ }
72
+ function clientCount() {
73
+ return topic.size();
74
+ }
75
+ //#endregion
76
+ //#region src/codec.ts
77
+ function encodeValue(type, v) {
78
+ if (v === null || v === void 0) return v;
79
+ if (type === "boolean") return v ? 1 : 0;
80
+ if (typeof type === "object" && "array" in type) return JSON.stringify(v);
81
+ return v;
82
+ }
83
+ function decodeRow(row, fields) {
84
+ const out = { ...row };
85
+ for (const [name, def] of Object.entries(fields)) {
86
+ if (out[name] === null || out[name] === void 0) continue;
87
+ if (def.type === "boolean") out[name] = !!out[name];
88
+ else if (typeof def.type === "object" && "array" in def.type) try {
89
+ out[name] = JSON.parse(out[name]);
90
+ } catch {}
91
+ }
92
+ return out;
93
+ }
94
+ function decodeRows(rows, fields) {
95
+ return rows.map((r) => decodeRow(r, fields));
96
+ }
97
+ //#endregion
98
+ //#region src/predicate.ts
99
+ var PredicateValidationError = class extends Error {
100
+ constructor(message) {
101
+ super(message);
102
+ this.name = "PredicateValidationError";
103
+ }
104
+ };
105
+ const IDENTIFIER_RE$1 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
106
+ const MAX_DEPTH = 32;
107
+ const MAX_CLAUSES = 256;
108
+ function validatePredicate(node) {
109
+ validateAtDepth(node, 0);
110
+ }
111
+ function validateAtDepth(node, depth) {
112
+ if (depth > MAX_DEPTH) throw new PredicateValidationError(`predicate nested too deep (>${MAX_DEPTH})`);
113
+ if (!node || typeof node !== "object" || Array.isArray(node)) throw new PredicateValidationError("predicate must be an object");
114
+ const n = node;
115
+ if (typeof n.op !== "string") throw new PredicateValidationError("predicate missing 'op'");
116
+ switch (n.op) {
117
+ case "and":
118
+ case "or": {
119
+ const m = n;
120
+ if (!Array.isArray(m.clauses)) throw new PredicateValidationError(`'${n.op}' requires 'clauses' array`);
121
+ if (m.clauses.length === 0) throw new PredicateValidationError(`'${n.op}' requires at least one clause`);
122
+ if (m.clauses.length > MAX_CLAUSES) throw new PredicateValidationError(`'${n.op}' has too many clauses (>${MAX_CLAUSES})`);
123
+ for (const c of m.clauses) validateAtDepth(c, depth + 1);
124
+ return;
125
+ }
126
+ case "not":
127
+ validateAtDepth(n.clause, depth + 1);
128
+ return;
129
+ case "eq":
130
+ case "ne": {
131
+ const m = n;
132
+ requireColumn(m.column);
133
+ requireLiteral(m.value);
134
+ return;
135
+ }
136
+ case "gt":
137
+ case "gte":
138
+ case "lt":
139
+ case "lte": {
140
+ const m = n;
141
+ requireColumn(m.column);
142
+ const v = m.value;
143
+ if (v === null || typeof v === "boolean") throw new PredicateValidationError(`'${n.op}' value must be number or string, got ${v === null ? "null" : "boolean"}`);
144
+ if (typeof v !== "number" && typeof v !== "string") throw new PredicateValidationError(`'${n.op}' value must be number or string`);
145
+ if (typeof v === "number" && !Number.isFinite(v)) throw new PredicateValidationError(`'${n.op}' value must be a finite number`);
146
+ return;
147
+ }
148
+ case "in": {
149
+ const m = n;
150
+ requireColumn(m.column);
151
+ if (!Array.isArray(m.values)) throw new PredicateValidationError("'in' requires 'values' array");
152
+ if (m.values.length === 0) throw new PredicateValidationError("'in' values must be non-empty");
153
+ if (m.values.length > MAX_CLAUSES) throw new PredicateValidationError(`'in' has too many values (>${MAX_CLAUSES})`);
154
+ for (const v of m.values) requireLiteral(v);
155
+ return;
156
+ }
157
+ case "like": {
158
+ const m = n;
159
+ requireColumn(m.column);
160
+ if (typeof m.pattern !== "string") throw new PredicateValidationError("'like' requires string 'pattern'");
161
+ return;
162
+ }
163
+ case "isNull":
164
+ case "isNotNull":
165
+ requireColumn(n.column);
166
+ return;
167
+ default: throw new PredicateValidationError(`unknown op: ${String(n.op)}`);
168
+ }
169
+ }
170
+ function requireColumn(c) {
171
+ if (typeof c !== "string" || !IDENTIFIER_RE$1.test(c)) throw new PredicateValidationError(`invalid column: ${String(c)}`);
172
+ }
173
+ function requireLiteral(v) {
174
+ if (v === null) return;
175
+ const t = typeof v;
176
+ if (t !== "string" && t !== "number" && t !== "boolean") throw new PredicateValidationError(`unsupported literal: ${t}`);
177
+ if (t === "number" && !Number.isFinite(v)) throw new PredicateValidationError("non-finite numbers (NaN/Infinity) not allowed");
178
+ }
179
+ function compileToSql(pred) {
180
+ const params = [];
181
+ return {
182
+ sql: emitSql(pred, params),
183
+ params
184
+ };
185
+ }
186
+ function emitSql(p, params) {
187
+ switch (p.op) {
188
+ case "and": return `(${p.clauses.map((c) => emitSql(c, params)).join(" AND ")})`;
189
+ case "or": return `(${p.clauses.map((c) => emitSql(c, params)).join(" OR ")})`;
190
+ case "not": return `(NOT ${emitSql(p.clause, params)})`;
191
+ case "eq":
192
+ params.push(encodeLiteral(p.value));
193
+ return `${p.column} = ?`;
194
+ case "ne":
195
+ params.push(encodeLiteral(p.value));
196
+ return `${p.column} != ?`;
197
+ case "gt":
198
+ params.push(encodeLiteral(p.value));
199
+ return `${p.column} > ?`;
200
+ case "gte":
201
+ params.push(encodeLiteral(p.value));
202
+ return `${p.column} >= ?`;
203
+ case "lt":
204
+ params.push(encodeLiteral(p.value));
205
+ return `${p.column} < ?`;
206
+ case "lte":
207
+ params.push(encodeLiteral(p.value));
208
+ return `${p.column} <= ?`;
209
+ case "in":
210
+ for (const v of p.values) params.push(encodeLiteral(v));
211
+ return `${p.column} IN (${p.values.map(() => "?").join(", ")})`;
212
+ case "like":
213
+ params.push(p.pattern);
214
+ return `${p.column} LIKE ?`;
215
+ case "isNull": return `${p.column} IS NULL`;
216
+ case "isNotNull": return `${p.column} IS NOT NULL`;
217
+ }
218
+ }
219
+ function encodeLiteral(v) {
220
+ if (typeof v === "boolean") return v ? 1 : 0;
221
+ return v;
222
+ }
223
+ /**
224
+ * Compile to a row matcher. The row is the *decoded* shape — same flavour
225
+ * as what auto-crud returns to REST clients (booleans as `true`/`false`,
226
+ * arrays as JS arrays, etc). Subscribers feed decoded rows in.
227
+ */
228
+ function compileToJs(pred) {
229
+ const tri = compileToTri(pred);
230
+ return (row) => tri(row) === true;
231
+ }
232
+ function compileToTri(pred) {
233
+ switch (pred.op) {
234
+ case "and": {
235
+ const cs = pred.clauses.map(compileToTri);
236
+ return (r) => {
237
+ let seenNull = false;
238
+ for (const c of cs) {
239
+ const v = c(r);
240
+ if (v === false) return false;
241
+ if (v === null) seenNull = true;
242
+ }
243
+ return seenNull ? null : true;
244
+ };
245
+ }
246
+ case "or": {
247
+ const cs = pred.clauses.map(compileToTri);
248
+ return (r) => {
249
+ let seenNull = false;
250
+ for (const c of cs) {
251
+ const v = c(r);
252
+ if (v === true) return true;
253
+ if (v === null) seenNull = true;
254
+ }
255
+ return seenNull ? null : false;
256
+ };
257
+ }
258
+ case "not": {
259
+ const c = compileToTri(pred.clause);
260
+ return (r) => {
261
+ const v = c(r);
262
+ if (v === null) return null;
263
+ return !v;
264
+ };
265
+ }
266
+ case "eq": {
267
+ const { column, value } = pred;
268
+ return (r) => {
269
+ const cv = r[column];
270
+ if (cv === null || cv === void 0) return null;
271
+ if (value === null) return null;
272
+ return valuesEqual(cv, value);
273
+ };
274
+ }
275
+ case "ne": {
276
+ const { column, value } = pred;
277
+ return (r) => {
278
+ const cv = r[column];
279
+ if (cv === null || cv === void 0) return null;
280
+ if (value === null) return null;
281
+ return !valuesEqual(cv, value);
282
+ };
283
+ }
284
+ case "gt": {
285
+ const { column, value } = pred;
286
+ return (r) => triCompare(r[column], value, (n) => n > 0);
287
+ }
288
+ case "gte": {
289
+ const { column, value } = pred;
290
+ return (r) => triCompare(r[column], value, (n) => n >= 0);
291
+ }
292
+ case "lt": {
293
+ const { column, value } = pred;
294
+ return (r) => triCompare(r[column], value, (n) => n < 0);
295
+ }
296
+ case "lte": {
297
+ const { column, value } = pred;
298
+ return (r) => triCompare(r[column], value, (n) => n <= 0);
299
+ }
300
+ case "in": {
301
+ const { column, values } = pred;
302
+ return (r) => {
303
+ const cv = r[column];
304
+ if (cv === null || cv === void 0) return null;
305
+ let seenNull = false;
306
+ for (const x of values) {
307
+ if (x === null) {
308
+ seenNull = true;
309
+ continue;
310
+ }
311
+ if (valuesEqual(cv, x)) return true;
312
+ }
313
+ return seenNull ? null : false;
314
+ };
315
+ }
316
+ case "like": {
317
+ const { column, pattern } = pred;
318
+ const re = likeToRegExp(pattern);
319
+ return (r) => {
320
+ const cv = r[column];
321
+ if (cv === null || cv === void 0) return null;
322
+ const s = typeof cv === "string" ? cv : String(cv);
323
+ return re.test(s);
324
+ };
325
+ }
326
+ case "isNull": {
327
+ const { column } = pred;
328
+ return (r) => r[column] === null || r[column] === void 0;
329
+ }
330
+ case "isNotNull": {
331
+ const { column } = pred;
332
+ return (r) => r[column] !== null && r[column] !== void 0;
333
+ }
334
+ }
335
+ }
336
+ function triCompare(a, b, pick) {
337
+ if (a === null || a === void 0) return null;
338
+ const n = orderedCompare(a, b);
339
+ if (Number.isNaN(n)) return null;
340
+ return pick(n);
341
+ }
342
+ /**
343
+ * Equality with SQLite-ish semantics:
344
+ * - NULL is never equal to anything (including NULL) → false. Use isNull
345
+ * if you want NULL matching.
346
+ * - boolean ↔ 0/1 matches (so a decoded `true` matches a stored `1` row
347
+ * if anyone hands us a raw row by accident).
348
+ * - Number/string types compare loosely so an unquoted JSON literal
349
+ * matches the matching DB column.
350
+ */
351
+ function valuesEqual(a, b) {
352
+ if (a === null || a === void 0) return false;
353
+ if (typeof b === "boolean") {
354
+ if (typeof a === "boolean") return a === b;
355
+ if (typeof a === "number") return (b ? 1 : 0) === a;
356
+ return false;
357
+ }
358
+ if (b === null) return false;
359
+ if (typeof a === typeof b) return a === b;
360
+ return String(a) === String(b);
361
+ }
362
+ /**
363
+ * Returns -1 / 0 / +1, or NaN if either side is null/undefined or types
364
+ * don't permit ordering. Callers treat NaN as "predicate false" — same as
365
+ * SQL where any comparison involving NULL yields UNKNOWN.
366
+ */
367
+ function orderedCompare(a, b) {
368
+ if (a === null || a === void 0) return NaN;
369
+ if (typeof b === "number") {
370
+ const av = typeof a === "number" ? a : Number(a);
371
+ if (!Number.isFinite(av)) return NaN;
372
+ return av < b ? -1 : av > b ? 1 : 0;
373
+ }
374
+ const as = typeof a === "string" ? a : String(a);
375
+ return as < b ? -1 : as > b ? 1 : 0;
376
+ }
377
+ /**
378
+ * Translate a SQL LIKE pattern to a JS RegExp. SQLite LIKE is
379
+ * case-insensitive for ASCII by default — we mirror that with the `i` flag.
380
+ * `%` matches any run of characters; `_` matches exactly one. No escape
381
+ * handling yet — adding `ESCAPE '\\'` is a Phase 6 concern.
382
+ */
383
+ function likeToRegExp(pattern) {
384
+ let re = "^";
385
+ for (const ch of pattern) if (ch === "%") re += ".*";
386
+ else if (ch === "_") re += ".";
387
+ else re += ch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
388
+ re += "$";
389
+ return new RegExp(re, "i");
390
+ }
391
+ //#endregion
392
+ //#region src/subscriptions.ts
393
+ const clients = /* @__PURE__ */ new Map();
394
+ const subsByCollection = /* @__PURE__ */ new Map();
395
+ let currentSchema = null;
396
+ function setSubscriptionSchema(schema) {
397
+ currentSchema = schema;
398
+ }
399
+ let seqCounter = 0;
400
+ const ringByCollection = /* @__PURE__ */ new Map();
401
+ let RING_CAP = 256;
402
+ let MAX_SUBS_PER_CLIENT = 64;
403
+ function setMaxSubsPerClient(n) {
404
+ if (!Number.isInteger(n) || n < 1) throw new Error("max subs per client must be a positive integer");
405
+ MAX_SUBS_PER_CLIENT = n;
406
+ }
407
+ /** Test/operator hook: override the per-collection ring cap. */
408
+ function setSubscriptionRingCap(cap) {
409
+ if (!Number.isInteger(cap) || cap < 1) throw new Error("ring cap must be positive integer");
410
+ RING_CAP = cap;
411
+ }
412
+ function nextSeq() {
413
+ seqCounter += 1;
414
+ return seqCounter;
415
+ }
416
+ function pushRing(collection, entry) {
417
+ let ring = ringByCollection.get(collection);
418
+ if (!ring) {
419
+ ring = [];
420
+ ringByCollection.set(collection, ring);
421
+ }
422
+ ring.push(entry);
423
+ if (ring.length > RING_CAP) ring.splice(0, ring.length - RING_CAP);
424
+ }
425
+ function addSubClient(client) {
426
+ if (clients.has(client)) return;
427
+ clients.set(client, /* @__PURE__ */ new Map());
428
+ }
429
+ function removeSubClient(client) {
430
+ const subs = clients.get(client);
431
+ if (!subs) return;
432
+ for (const sub of subs.values()) {
433
+ const set = subsByCollection.get(sub.collection);
434
+ if (set) {
435
+ set.delete(client);
436
+ if (set.size === 0) subsByCollection.delete(sub.collection);
437
+ }
438
+ }
439
+ clients.delete(client);
440
+ }
441
+ function subClientCount() {
442
+ return clients.size;
443
+ }
444
+ function subscriptionCount(collection) {
445
+ if (collection) return subsByCollection.get(collection)?.size ?? 0;
446
+ let n = 0;
447
+ for (const subs of clients.values()) n += subs.size;
448
+ return n;
449
+ }
450
+ async function handleSubMessage(client, raw) {
451
+ let msg;
452
+ try {
453
+ msg = JSON.parse(raw);
454
+ } catch {
455
+ sendError(client, void 0, "bad_json", "message is not valid JSON");
456
+ return;
457
+ }
458
+ if (!msg || typeof msg !== "object" || !("op" in msg)) {
459
+ sendError(client, void 0, "bad_shape", "missing op field");
460
+ return;
461
+ }
462
+ if (msg.op === "sub") {
463
+ if (!msg.subId || typeof msg.subId !== "string") {
464
+ sendError(client, void 0, "bad_subId", "subId required");
465
+ return;
466
+ }
467
+ if (!msg.collection || typeof msg.collection !== "string") {
468
+ sendError(client, msg.subId, "bad_collection", "collection required");
469
+ return;
470
+ }
471
+ const subs = clients.get(client);
472
+ if (!subs) {
473
+ sendError(client, msg.subId, "client_not_registered", "client not registered — call addSubClient first");
474
+ return;
475
+ }
476
+ if (subs.has(msg.subId)) {
477
+ sendError(client, msg.subId, "duplicate_subId", "subId already in use on this client");
478
+ return;
479
+ }
480
+ if (subs.size >= MAX_SUBS_PER_CLIENT) {
481
+ sendError(client, msg.subId, "sub_limit_exceeded", `client has reached the subscription cap (${MAX_SUBS_PER_CLIENT})`);
482
+ return;
483
+ }
484
+ if (!currentSchema || !currentSchema.collections[msg.collection]) {
485
+ sendError(client, msg.subId, "unknown_collection", `no such collection: ${msg.collection}`);
486
+ return;
487
+ }
488
+ if (currentSchema.collections[msg.collection].scope === "user" && !client.ctx.userId) {
489
+ sendError(client, msg.subId, "auth_required", `scoped collection "${msg.collection}" requires an authenticated user`);
490
+ return;
491
+ }
492
+ let predicate = null;
493
+ let predicateJs = null;
494
+ if (msg.where !== void 0) try {
495
+ validatePredicate(msg.where);
496
+ predicate = msg.where;
497
+ predicateJs = compileToJs(msg.where);
498
+ } catch (err) {
499
+ if (err instanceof PredicateValidationError) sendError(client, msg.subId, "bad_predicate", err.message);
500
+ else sendError(client, msg.subId, "bad_predicate", err.message);
501
+ return;
502
+ }
503
+ const sub = {
504
+ subId: msg.subId,
505
+ collection: msg.collection,
506
+ predicate,
507
+ predicateJs
508
+ };
509
+ subs.set(msg.subId, sub);
510
+ let set = subsByCollection.get(msg.collection);
511
+ if (!set) {
512
+ set = /* @__PURE__ */ new Set();
513
+ subsByCollection.set(msg.collection, set);
514
+ }
515
+ set.add(client);
516
+ if (typeof msg.resumeFromSeq === "number" && msg.resumeFromSeq >= 0 && canResumeFrom(msg.collection, msg.resumeFromSeq)) {
517
+ await sendResumeReplay(client, sub, msg.resumeFromSeq);
518
+ return;
519
+ }
520
+ await sendSnapshot(client, sub);
521
+ return;
522
+ }
523
+ if (msg.op === "unsub") {
524
+ const subs = clients.get(client);
525
+ if (!subs) return;
526
+ const sub = subs.get(msg.subId);
527
+ if (!sub) return;
528
+ subs.delete(msg.subId);
529
+ let stillOnCollection = false;
530
+ for (const remaining of subs.values()) if (remaining.collection === sub.collection) {
531
+ stillOnCollection = true;
532
+ break;
533
+ }
534
+ if (!stillOnCollection) {
535
+ const set = subsByCollection.get(sub.collection);
536
+ if (set) {
537
+ set.delete(client);
538
+ if (set.size === 0) subsByCollection.delete(sub.collection);
539
+ }
540
+ }
541
+ return;
542
+ }
543
+ sendError(client, void 0, "unknown_op", `unknown op: ${msg.op}`);
544
+ }
545
+ /**
546
+ * Push a per-row delta to every subscriber of `collection`. Auto-crud
547
+ * invokes this with raw (storage-shape) old/new rows; we decode through
548
+ * the schema once and then run each sub's predicate against the decoded
549
+ * row to compute the membership transition.
550
+ *
551
+ * Pass `oldRow=null` for inserts, `newRow=null` for deletes. For an update
552
+ * pass both; if a sub's predicate flips one direction we emit insert /
553
+ * delete deltas (the row entered or left the read set), if it stays on
554
+ * both sides we emit update.
555
+ *
556
+ * Returns a Promise (same pattern as notifyCollectionChange) so callers
557
+ * can fire-and-forget while tests await for determinism.
558
+ */
559
+ function notifyRowChange(collection, kind, oldRowRaw, newRowRaw) {
560
+ if (!currentSchema) return Promise.resolve();
561
+ const col = currentSchema.collections[collection];
562
+ if (!col) return Promise.resolve();
563
+ const seq = nextSeq();
564
+ pushRing(collection, {
565
+ seq,
566
+ kind,
567
+ oldRowRaw,
568
+ newRowRaw
569
+ });
570
+ const set = subsByCollection.get(collection);
571
+ if (!set || set.size === 0) return Promise.resolve();
572
+ const oldRow = oldRowRaw ? decodeRow(oldRowRaw, col.fields) : null;
573
+ const newRow = newRowRaw ? decodeRow(newRowRaw, col.fields) : null;
574
+ const scoped = col.scope === "user";
575
+ const rowOwner = scoped ? newRowRaw?._owner ?? oldRowRaw?._owner ?? null : null;
576
+ const pending = [];
577
+ const targets = Array.from(set);
578
+ for (const client of targets) {
579
+ if (client.readyState !== 1) {
580
+ removeSubClient(client);
581
+ continue;
582
+ }
583
+ if (scoped) {
584
+ if (!client.ctx.userId || client.ctx.userId !== rowOwner) continue;
585
+ }
586
+ const subs = clients.get(client);
587
+ if (!subs) continue;
588
+ for (const sub of subs.values()) {
589
+ if (sub.collection !== collection) continue;
590
+ pending.push(dispatchDeltaWithSeq(client, sub, kind, oldRow, newRow, seq));
591
+ }
592
+ }
593
+ return Promise.allSettled(pending).then(() => void 0);
594
+ }
595
+ async function dispatchDeltaWithSeq(client, sub, kind, oldRow, newRow, seq) {
596
+ const match = sub.predicateJs;
597
+ const was = oldRow ? match ? match(oldRow) : true : false;
598
+ const now = newRow ? match ? match(newRow) : true : false;
599
+ if (kind === "insert") {
600
+ if (now) {
601
+ emitDelta(client, sub, "insert", newRow, seq);
602
+ return true;
603
+ }
604
+ return false;
605
+ }
606
+ if (kind === "delete") {
607
+ if (was) {
608
+ const id = oldRow.id;
609
+ sendMessage(client, {
610
+ type: "delta",
611
+ subId: sub.subId,
612
+ collection: sub.collection,
613
+ seq,
614
+ op: "delete",
615
+ id
616
+ });
617
+ return true;
618
+ }
619
+ return false;
620
+ }
621
+ if (!was && now) {
622
+ emitDelta(client, sub, "insert", newRow, seq);
623
+ return true;
624
+ }
625
+ if (was && now) {
626
+ emitDelta(client, sub, "update", newRow, seq);
627
+ return true;
628
+ }
629
+ if (was && !now) {
630
+ const id = oldRow.id;
631
+ sendMessage(client, {
632
+ type: "delta",
633
+ subId: sub.subId,
634
+ collection: sub.collection,
635
+ seq,
636
+ op: "delete",
637
+ id
638
+ });
639
+ return true;
640
+ }
641
+ return false;
642
+ }
643
+ function emitDelta(client, sub, op, row, seq) {
644
+ sendMessage(client, {
645
+ type: "delta",
646
+ subId: sub.subId,
647
+ collection: sub.collection,
648
+ seq,
649
+ op,
650
+ row
651
+ });
652
+ }
653
+ /**
654
+ * Coarse fan-out fallback: re-push the entire snapshot to every subscriber
655
+ * of `collection`. Used by user-authored raw-SQL handlers that mutate
656
+ * outside auto-crud — auto-crud itself prefers notifyRowChange. Kept for
657
+ * back-compat; calling this is strictly less efficient than the delta
658
+ * path but always correct.
659
+ */
660
+ function notifyCollectionChange(collection) {
661
+ const set = subsByCollection.get(collection);
662
+ if (!set || set.size === 0) return Promise.resolve();
663
+ const targets = Array.from(set);
664
+ const pending = [];
665
+ for (const client of targets) {
666
+ if (client.readyState !== 1) {
667
+ removeSubClient(client);
668
+ continue;
669
+ }
670
+ const subs = clients.get(client);
671
+ if (!subs) continue;
672
+ for (const sub of subs.values()) {
673
+ if (sub.collection !== collection) continue;
674
+ pending.push(sendSnapshot(client, sub));
675
+ }
676
+ }
677
+ return Promise.allSettled(pending).then(() => void 0);
678
+ }
679
+ async function sendSnapshot(client, sub) {
680
+ const db = getDbInstance();
681
+ if (!db) {
682
+ sendError(client, sub.subId, "db_not_ready", "db not initialized");
683
+ return;
684
+ }
685
+ if (!currentSchema) {
686
+ sendError(client, sub.subId, "no_schema", "schema not registered");
687
+ return;
688
+ }
689
+ const col = currentSchema.collections[sub.collection];
690
+ if (!col) {
691
+ sendError(client, sub.subId, "unknown_collection", `no such collection: ${sub.collection}`);
692
+ return;
693
+ }
694
+ const scoped = col.scope === "user";
695
+ const conditions = [];
696
+ const params = [];
697
+ if (scoped) {
698
+ if (!client.ctx.userId) {
699
+ sendError(client, sub.subId, "auth_required", `scoped collection "${sub.collection}" requires an authenticated user`);
700
+ return;
701
+ }
702
+ conditions.push("_owner = ?");
703
+ params.push(client.ctx.userId);
704
+ }
705
+ if (sub.predicate) {
706
+ if (!predicateColumnsExist(sub.predicate, col.fields)) {
707
+ sendError(client, sub.subId, "bad_predicate", "predicate references unknown column");
708
+ return;
709
+ }
710
+ const compiled = compileToSql(sub.predicate);
711
+ conditions.push(compiled.sql);
712
+ params.push(...compiled.params);
713
+ }
714
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
715
+ try {
716
+ const rows = db.raw().prepare(`SELECT * FROM ${sub.collection} ${where} ORDER BY created_at DESC`).all(...params).map((r) => decodeRow(r, col.fields));
717
+ sendMessage(client, {
718
+ type: "snapshot",
719
+ subId: sub.subId,
720
+ collection: sub.collection,
721
+ seq: seqCounter,
722
+ rows
723
+ });
724
+ } catch (err) {
725
+ sendError(client, sub.subId, "snapshot_failed", err.message ?? String(err));
726
+ }
727
+ }
728
+ function canResumeFrom(collection, fromSeq) {
729
+ const ring = ringByCollection.get(collection);
730
+ if (!ring || ring.length === 0) return fromSeq <= seqCounter;
731
+ const oldest = ring[0];
732
+ if (!oldest) return fromSeq <= seqCounter;
733
+ return fromSeq >= oldest.seq - 1;
734
+ }
735
+ async function sendResumeReplay(client, sub, fromSeq) {
736
+ if (!currentSchema) {
737
+ sendError(client, sub.subId, "no_schema", "schema not registered");
738
+ return;
739
+ }
740
+ const col = currentSchema.collections[sub.collection];
741
+ if (!col) {
742
+ sendError(client, sub.subId, "unknown_collection", `no such collection: ${sub.collection}`);
743
+ return;
744
+ }
745
+ const ring = ringByCollection.get(sub.collection) ?? [];
746
+ let replayed = 0;
747
+ for (const entry of ring) {
748
+ if (entry.seq <= fromSeq) continue;
749
+ if (col.scope === "user") {
750
+ const owner = entry.newRowRaw?._owner ?? entry.oldRowRaw?._owner ?? null;
751
+ if (!client.ctx.userId || client.ctx.userId !== owner) continue;
752
+ }
753
+ const oldRow = entry.oldRowRaw ? decodeRow(entry.oldRowRaw, col.fields) : null;
754
+ const newRow = entry.newRowRaw ? decodeRow(entry.newRowRaw, col.fields) : null;
755
+ if (await dispatchDeltaWithSeq(client, sub, entry.kind, oldRow, newRow, entry.seq)) replayed += 1;
756
+ }
757
+ sendMessage(client, {
758
+ type: "resumed",
759
+ subId: sub.subId,
760
+ collection: sub.collection,
761
+ fromSeq,
762
+ toSeq: seqCounter,
763
+ replayed
764
+ });
765
+ }
766
+ const RESERVED = new Set([
767
+ "id",
768
+ "created_at",
769
+ "updated_at",
770
+ "_owner"
771
+ ]);
772
+ function predicateColumnsExist(p, fields) {
773
+ switch (p.op) {
774
+ case "and":
775
+ case "or": return p.clauses.every((c) => predicateColumnsExist(c, fields));
776
+ case "not": return predicateColumnsExist(p.clause, fields);
777
+ default: return RESERVED.has(p.column) || p.column in fields;
778
+ }
779
+ }
780
+ function sendMessage(client, msg) {
781
+ if (client.readyState !== 1) {
782
+ removeSubClient(client);
783
+ return;
784
+ }
785
+ try {
786
+ client.send(JSON.stringify(msg));
787
+ } catch {
788
+ removeSubClient(client);
789
+ }
790
+ }
791
+ function sendError(client, subId, code, message) {
792
+ sendMessage(client, {
793
+ type: "error",
794
+ subId,
795
+ code,
796
+ message
797
+ });
798
+ }
799
+ //#endregion
800
+ //#region src/db.ts
801
+ const scopedTables = /* @__PURE__ */ new Set();
802
+ function markScoped(table) {
803
+ scopedTables.add(table);
804
+ }
805
+ const RESERVED_COLUMNS = new Set([
806
+ "id",
807
+ "_owner",
808
+ "created_at",
809
+ "updated_at"
810
+ ]);
811
+ const IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
812
+ function stripReserved(data) {
813
+ const out = {};
814
+ for (const [k, v] of Object.entries(data)) if (!RESERVED_COLUMNS.has(k)) out[k] = v;
815
+ return out;
816
+ }
817
+ /**
818
+ * Thrown when a row-level operation on a user-scoped table is attempted with
819
+ * no authenticated user in context. Caught by the apiHandler dispatcher and
820
+ * turned into a 401 — letting the call through (the prior behaviour) silently
821
+ * dropped the `_owner = ?` predicate and exposed every user's rows.
822
+ */
823
+ var VibesAuthRequiredError = class extends Error {
824
+ table;
825
+ constructor(table) {
826
+ super(`[vibes:auth] scoped table "${table}" requires an authenticated user`);
827
+ this.name = "VibesAuthRequiredError";
828
+ this.table = table;
829
+ }
830
+ };
831
+ function openDb(path) {
832
+ const bun = new Database(path, { create: true });
833
+ bun.exec("PRAGMA journal_mode=WAL;");
834
+ function now() {
835
+ return (/* @__PURE__ */ new Date()).toISOString();
836
+ }
837
+ function getOwner() {
838
+ return ctxStore.getStore()?.userId ?? null;
839
+ }
840
+ function isScoped(table) {
841
+ return scopedTables.has(table);
842
+ }
843
+ const db = {
844
+ async getAll(table, opts = {}) {
845
+ const conditions = [];
846
+ const params = [];
847
+ if (isScoped(table)) {
848
+ const owner = getOwner();
849
+ if (!owner) throw new VibesAuthRequiredError(table);
850
+ conditions.push("_owner = ?");
851
+ params.push(owner);
852
+ }
853
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
854
+ let orderClause = "ORDER BY created_at DESC";
855
+ if (opts.orderBy) {
856
+ if (!IDENTIFIER_RE.test(opts.orderBy)) throw new Error(`[vibes] getAll: invalid orderBy column: ${opts.orderBy}`);
857
+ const dir = opts.orderDir === "DESC" ? "DESC" : "ASC";
858
+ orderClause = `ORDER BY ${opts.orderBy} ${dir}`;
859
+ }
860
+ const limit = Number.isInteger(opts.limit) && opts.limit >= 0 ? `LIMIT ${opts.limit}` : "";
861
+ const offset = Number.isInteger(opts.offset) && opts.offset >= 0 ? `OFFSET ${opts.offset}` : "";
862
+ const sql = `SELECT * FROM ${table} ${where} ${orderClause} ${limit} ${offset}`.trim();
863
+ return bun.prepare(sql).all(...params);
864
+ },
865
+ async get(table, id) {
866
+ const conditions = ["id = ?"];
867
+ const params = [id];
868
+ if (isScoped(table)) {
869
+ const owner = getOwner();
870
+ if (!owner) throw new VibesAuthRequiredError(table);
871
+ conditions.push("_owner = ?");
872
+ params.push(owner);
873
+ }
874
+ const sql = `SELECT * FROM ${table} WHERE ${conditions.join(" AND ")} LIMIT 1`;
875
+ return bun.prepare(sql).get(...params) ?? null;
876
+ },
877
+ async insert(table, data) {
878
+ const id = crypto.randomUUID();
879
+ const ts = now();
880
+ const record = {
881
+ ...stripReserved(data),
882
+ id,
883
+ created_at: ts,
884
+ updated_at: ts
885
+ };
886
+ if (isScoped(table)) {
887
+ const owner = getOwner();
888
+ if (!owner) throw new VibesAuthRequiredError(table);
889
+ record._owner = owner;
890
+ }
891
+ const columns = Object.keys(record);
892
+ const placeholders = columns.map(() => "?").join(", ");
893
+ const values = Object.values(record);
894
+ const sql = `INSERT INTO ${table} (${columns.join(", ")}) VALUES (${placeholders})`;
895
+ bun.prepare(sql).run(...values);
896
+ invalidate(table);
897
+ notifyRowChange(table, "insert", null, record);
898
+ return record;
899
+ },
900
+ async update(table, id, data) {
901
+ const existing = await db.get(table, id);
902
+ if (!existing) return null;
903
+ const updates = {
904
+ ...stripReserved(data),
905
+ updated_at: now()
906
+ };
907
+ const setClauses = Object.keys(updates).map((k) => `${k} = ?`).join(", ");
908
+ const values = [...Object.values(updates), id];
909
+ let sql = `UPDATE ${table} SET ${setClauses} WHERE id = ?`;
910
+ if (isScoped(table)) {
911
+ const owner = getOwner();
912
+ if (!owner) throw new VibesAuthRequiredError(table);
913
+ sql += " AND _owner = ?";
914
+ values.push(owner);
915
+ }
916
+ bun.prepare(sql).run(...values);
917
+ const updated = {
918
+ ...existing,
919
+ ...updates
920
+ };
921
+ invalidate(table);
922
+ notifyRowChange(table, "update", existing, updated);
923
+ return updated;
924
+ },
925
+ async delete(table, id) {
926
+ const existing = await db.get(table, id);
927
+ if (!existing) return false;
928
+ const conditions = ["id = ?"];
929
+ const params = [id];
930
+ if (isScoped(table)) {
931
+ const owner = getOwner();
932
+ if (!owner) throw new VibesAuthRequiredError(table);
933
+ conditions.push("_owner = ?");
934
+ params.push(owner);
935
+ }
936
+ const sql = `DELETE FROM ${table} WHERE ${conditions.join(" AND ")}`;
937
+ const deleted = (bun.prepare(sql).run(...params).changes ?? 0) > 0;
938
+ if (deleted) {
939
+ invalidate(table);
940
+ notifyRowChange(table, "delete", existing, null);
941
+ }
942
+ return deleted;
943
+ },
944
+ raw() {
945
+ return bun;
946
+ },
947
+ close() {
948
+ bun.close();
949
+ }
950
+ };
951
+ return db;
952
+ }
953
+ let _dbInstance = null;
954
+ const dbProxy = new Proxy({}, { get(_target, prop) {
955
+ if (!_dbInstance) throw new Error(`[vibes] db not initialized. Call createVibesServer() before using db.`);
956
+ return _dbInstance[prop];
957
+ } });
958
+ function setDbInstance(instance) {
959
+ _dbInstance = instance;
960
+ }
961
+ function getDbInstance() {
962
+ return _dbInstance;
963
+ }
964
+ //#endregion
965
+ //#region src/migrator.ts
966
+ /**
967
+ * Register which tables are user-scoped so db.ts filters by _owner.
968
+ * MUST be called at runtime even when migrations are skipped — otherwise
969
+ * scoped collections silently leak data across users.
970
+ */
971
+ function registerScopes(schema) {
972
+ for (const [tableName, col] of Object.entries(schema.collections)) if (col.scope === "user") markScoped(tableName);
973
+ }
974
+ function migrate(bunDb, schema) {
975
+ console.log("[vibes:migrator] Running migrations...");
976
+ registerScopes(schema);
977
+ const existingTablesResult = bunDb.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all();
978
+ const existingTableNames = new Set(existingTablesResult.map((r) => r.name));
979
+ const oldCollections = {};
980
+ for (const tableName of existingTableNames) {
981
+ const columns = bunDb.prepare(`PRAGMA table_info(${tableName})`).all();
982
+ const userFields = {};
983
+ for (const col of columns) {
984
+ if ([
985
+ "id",
986
+ "created_at",
987
+ "updated_at",
988
+ "_owner"
989
+ ].includes(col.name)) continue;
990
+ userFields[col.name] = { type: "string" };
991
+ }
992
+ oldCollections[tableName] = {
993
+ fields: userFields,
994
+ scope: "global",
995
+ indexes: [],
996
+ uniqueIndexes: []
997
+ };
998
+ }
999
+ const oldSchema = { collections: oldCollections };
1000
+ if (existingTableNames.size === 0) {
1001
+ const sqls = schemaToSQL(schema);
1002
+ for (const sql of sqls) {
1003
+ console.log(`[vibes:migrator] Executing: ${sql.split("\n")[0]}...`);
1004
+ bunDb.exec(sql);
1005
+ }
1006
+ console.log("[vibes:migrator] Initial migration complete.");
1007
+ return;
1008
+ }
1009
+ for (const [tableName, col] of Object.entries(schema.collections)) {
1010
+ if (col.scope !== "user") continue;
1011
+ if (!existingTableNames.has(tableName)) continue;
1012
+ if (bunDb.prepare(`PRAGMA table_info(${tableName})`).all().some((c) => c.name === "_owner")) continue;
1013
+ console.log(`[vibes:migrator] ADD COLUMN ${tableName}._owner (scope→user)`);
1014
+ bunDb.exec(`ALTER TABLE ${tableName} ADD COLUMN _owner TEXT`);
1015
+ }
1016
+ const migrations = schemaDiff(oldSchema, schema);
1017
+ if (migrations.length === 0) {
1018
+ console.log("[vibes:migrator] Schema is up to date.");
1019
+ return;
1020
+ }
1021
+ for (const migration of migrations) switch (migration.type) {
1022
+ case "create_table": {
1023
+ const sqls = schemaToSQL({ collections: { [migration.table]: schema.collections[migration.table] } });
1024
+ for (const sql of sqls) {
1025
+ console.log(`[vibes:migrator] CREATE TABLE ${migration.table}`);
1026
+ bunDb.exec(sql);
1027
+ }
1028
+ break;
1029
+ }
1030
+ case "add_column": {
1031
+ const colType = fieldTypeToSQL(migration.field.type);
1032
+ const sql = `ALTER TABLE ${migration.table} ADD COLUMN ${migration.column} ${colType}`;
1033
+ console.log(`[vibes:migrator] ADD COLUMN ${migration.table}.${migration.column}`);
1034
+ bunDb.exec(sql);
1035
+ break;
1036
+ }
1037
+ case "drop_table":
1038
+ console.warn(`[vibes:migrator] SKIP drop_table ${migration.table} — run manually if intended`);
1039
+ break;
1040
+ case "drop_column":
1041
+ console.warn(`[vibes:migrator] SKIP drop_column ${migration.table}.${migration.column} — run manually if intended`);
1042
+ break;
1043
+ case "add_index": {
1044
+ const indexName = `idx_${migration.table}_${migration.columns.join("_")}`;
1045
+ const sql = `CREATE INDEX IF NOT EXISTS ${indexName} ON ${migration.table} (${migration.columns.join(", ")})`;
1046
+ console.log(`[vibes:migrator] ADD INDEX ${indexName}`);
1047
+ bunDb.exec(sql);
1048
+ break;
1049
+ }
1050
+ case "add_unique_index": {
1051
+ const indexName = `uidx_${migration.table}_${migration.columns.join("_")}`;
1052
+ const sql = `CREATE UNIQUE INDEX IF NOT EXISTS ${indexName} ON ${migration.table} (${migration.columns.join(", ")})`;
1053
+ console.log(`[vibes:migrator] ADD UNIQUE INDEX ${indexName}`);
1054
+ bunDb.exec(sql);
1055
+ break;
1056
+ }
1057
+ }
1058
+ console.log(`[vibes:migrator] Applied ${migrations.length} migration(s).`);
1059
+ }
1060
+ function fieldTypeToSQL(type) {
1061
+ if (type === "string") return "TEXT";
1062
+ if (type === "number") return "REAL";
1063
+ if (type === "boolean") return "INTEGER";
1064
+ if (type === "date") return "TEXT";
1065
+ if (typeof type === "object" && type !== null) return "TEXT";
1066
+ return "TEXT";
1067
+ }
1068
+ //#endregion
1069
+ //#region src/http-error.ts
1070
+ /**
1071
+ * VibesHttpError — an expected, client-facing rejection with an explicit
1072
+ * HTTP status. Handlers (and the infra-API proxy helpers in the
1073
+ * control-plane) throw this instead of a bare Error when the failure is a
1074
+ * known refusal — ownership checks, validation against an upstream API,
1075
+ * upstream unavailability — rather than a bug.
1076
+ *
1077
+ * The dispatcher maps it to its status and, for 4xx, logs a one-liner
1078
+ * instead of a stack-bearing "Handler error in <name>" crash entry. A bare
1079
+ * Error keeps the existing 500-with-crash-log behavior.
1080
+ */
1081
+ var VibesHttpError = class extends Error {
1082
+ status;
1083
+ constructor(status, message) {
1084
+ super(message);
1085
+ this.name = "VibesHttpError";
1086
+ this.status = status;
1087
+ }
1088
+ };
1089
+ //#endregion
1090
+ //#region src/dispatcher.ts
1091
+ function errorResponse(err, where) {
1092
+ if (err instanceof VibesAuthRequiredError) return Response.json({ error: err.message }, { status: 401 });
1093
+ if (err instanceof VibesHttpError && err.status < 500) {
1094
+ console.warn(`[vibes:dispatcher] refused ${where}: ${err.message} (${err.status})`);
1095
+ return Response.json({ error: err.message }, { status: err.status });
1096
+ }
1097
+ console.error(`[vibes:dispatcher] Handler error in ${where}:`, err);
1098
+ const message = err instanceof Error ? err.message : "Internal server error";
1099
+ const status = err instanceof VibesHttpError ? err.status : 500;
1100
+ return Response.json({ error: message }, { status });
1101
+ }
1102
+ function loadRoutes(routesJson) {
1103
+ return routesJson;
1104
+ }
1105
+ function matchPath(pattern, path) {
1106
+ const patternParts = pattern.split("/");
1107
+ const pathParts = path.split("/");
1108
+ if (patternParts.length !== pathParts.length) return null;
1109
+ const params = {};
1110
+ for (let i = 0; i < patternParts.length; i++) {
1111
+ const pp = patternParts[i];
1112
+ const vp = pathParts[i];
1113
+ if (pp.startsWith(":")) params[pp.slice(1)] = decodeURIComponent(vp);
1114
+ else if (pp !== vp) return null;
1115
+ }
1116
+ return params;
1117
+ }
1118
+ const moduleCache = /* @__PURE__ */ new Map();
1119
+ async function loadModule(modulePath) {
1120
+ if (moduleCache.has(modulePath)) return moduleCache.get(modulePath);
1121
+ const mod = await import(modulePath);
1122
+ moduleCache.set(modulePath, mod);
1123
+ return mod;
1124
+ }
1125
+ function clearModuleCache() {
1126
+ moduleCache.clear();
1127
+ }
1128
+ async function handleRequest(req, routes) {
1129
+ const pathname = new URL(req.url).pathname;
1130
+ let matchedRoute = null;
1131
+ let pathParams = {};
1132
+ for (const route of routes) {
1133
+ if (route.method !== req.method) continue;
1134
+ const params = matchPath(route.path, pathname);
1135
+ if (params !== null) {
1136
+ matchedRoute = route;
1137
+ pathParams = params;
1138
+ break;
1139
+ }
1140
+ }
1141
+ if (!matchedRoute) return Response.json({ error: "Not found" }, { status: 404 });
1142
+ if (matchedRoute.inlineHandler) try {
1143
+ return await matchedRoute.inlineHandler(req, pathParams);
1144
+ } catch (err) {
1145
+ return errorResponse(err, `inline route ${matchedRoute.path}`);
1146
+ }
1147
+ const handlerName = matchedRoute.handler;
1148
+ let mod;
1149
+ if (matchedRoute.mod) mod = matchedRoute.mod;
1150
+ else try {
1151
+ mod = await loadModule(matchedRoute.module);
1152
+ } catch (err) {
1153
+ console.error(`[vibes:dispatcher] Failed to load module ${matchedRoute.module}:`, err);
1154
+ return Response.json({ error: "Internal server error" }, { status: 500 });
1155
+ }
1156
+ const handler = mod[handlerName];
1157
+ if (typeof handler !== "function") return Response.json({ error: `Handler "${handlerName}" not found in module` }, { status: 500 });
1158
+ if (matchedRoute.style === "method") try {
1159
+ const result = await handler(req, pathParams);
1160
+ return result instanceof Response ? result : Response.json(result);
1161
+ } catch (err) {
1162
+ return errorResponse(err, handlerName);
1163
+ }
1164
+ let body = void 0;
1165
+ if ([
1166
+ "POST",
1167
+ "PUT",
1168
+ "PATCH"
1169
+ ].includes(req.method)) {
1170
+ if ((req.headers.get("content-type") ?? "").includes("application/json")) try {
1171
+ body = await req.json();
1172
+ } catch {
1173
+ return Response.json({ error: "Invalid JSON body" }, { status: 400 });
1174
+ }
1175
+ }
1176
+ const args = [];
1177
+ if (pathParams.id) args.push(pathParams.id);
1178
+ if (body !== void 0) args.push(body);
1179
+ try {
1180
+ const result = await handler(...args);
1181
+ return result instanceof Response ? result : Response.json(result);
1182
+ } catch (err) {
1183
+ return errorResponse(err, handlerName);
1184
+ }
1185
+ }
1186
+ //#endregion
1187
+ //#region src/auto-crud.ts
1188
+ async function readJsonBody(req) {
1189
+ if (!(req.headers.get("content-type") ?? "").includes("application/json")) return {};
1190
+ const text = await req.text();
1191
+ if (!text.trim()) return {};
1192
+ try {
1193
+ const parsed = JSON.parse(text);
1194
+ return parsed && typeof parsed === "object" ? parsed : {};
1195
+ } catch {
1196
+ throw new Error("Invalid JSON body");
1197
+ }
1198
+ }
1199
+ function jsonError(status, message) {
1200
+ return Response.json({ error: message }, { status });
1201
+ }
1202
+ function getUserId() {
1203
+ return ctxStore.getStore()?.userId ?? null;
1204
+ }
1205
+ function makeListHandler(name, fields, scoped) {
1206
+ return async () => {
1207
+ const db = getDbInstance();
1208
+ if (!db) return jsonError(500, "db not initialized");
1209
+ if (scoped) {
1210
+ const userId = getUserId();
1211
+ if (!userId) return jsonError(401, "Authentication required");
1212
+ const rows = db.raw().prepare(`SELECT * FROM ${name} WHERE _owner = ? ORDER BY created_at DESC`).all(userId);
1213
+ return Response.json(decodeRows(rows, fields));
1214
+ }
1215
+ const rows = db.raw().prepare(`SELECT * FROM ${name} ORDER BY created_at DESC`).all();
1216
+ return Response.json(decodeRows(rows, fields));
1217
+ };
1218
+ }
1219
+ function makeGetHandler(name, fields, scoped) {
1220
+ return async (_req, params) => {
1221
+ const db = getDbInstance();
1222
+ if (!db) return jsonError(500, "db not initialized");
1223
+ if (scoped) {
1224
+ const userId = getUserId();
1225
+ if (!userId) return jsonError(401, "Authentication required");
1226
+ const row = db.raw().prepare(`SELECT * FROM ${name} WHERE id = ? AND _owner = ? LIMIT 1`).get(params.id, userId);
1227
+ if (!row) return jsonError(404, "Not found");
1228
+ return Response.json(decodeRow(row, fields));
1229
+ }
1230
+ const row = db.raw().prepare(`SELECT * FROM ${name} WHERE id = ? LIMIT 1`).get(params.id);
1231
+ if (!row) return jsonError(404, "Not found");
1232
+ return Response.json(decodeRow(row, fields));
1233
+ };
1234
+ }
1235
+ function makeCreateHandler(name, fields, scoped) {
1236
+ return async (req) => {
1237
+ const db = getDbInstance();
1238
+ if (!db) return jsonError(500, "db not initialized");
1239
+ let userId = null;
1240
+ if (scoped) {
1241
+ userId = getUserId();
1242
+ if (!userId) return jsonError(401, "Authentication required");
1243
+ }
1244
+ let body;
1245
+ try {
1246
+ body = await readJsonBody(req);
1247
+ } catch (e) {
1248
+ return jsonError(400, e.message);
1249
+ }
1250
+ const id = crypto.randomUUID();
1251
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
1252
+ const record = {
1253
+ id,
1254
+ created_at: ts,
1255
+ updated_at: ts
1256
+ };
1257
+ if (scoped) record._owner = userId;
1258
+ for (const [k, def] of Object.entries(fields)) if (k in body) record[k] = encodeValue(def.type, body[k]);
1259
+ const cols = Object.keys(record);
1260
+ const placeholders = cols.map(() => "?").join(", ");
1261
+ const values = Object.values(record);
1262
+ db.raw().prepare(`INSERT INTO ${name} (${cols.join(", ")}) VALUES (${placeholders})`).run(...values);
1263
+ invalidate(name);
1264
+ notifyRowChange(name, "insert", null, record);
1265
+ return Response.json(decodeRow(record, fields));
1266
+ };
1267
+ }
1268
+ function makeUpdateHandler(name, fields, scoped) {
1269
+ return async (req, params) => {
1270
+ const db = getDbInstance();
1271
+ if (!db) return jsonError(500, "db not initialized");
1272
+ let userId = null;
1273
+ if (scoped) {
1274
+ userId = getUserId();
1275
+ if (!userId) return jsonError(401, "Authentication required");
1276
+ }
1277
+ let body;
1278
+ try {
1279
+ body = await readJsonBody(req);
1280
+ } catch (e) {
1281
+ return jsonError(400, e.message);
1282
+ }
1283
+ const existing = db.raw().prepare(`SELECT * FROM ${name} WHERE id = ? LIMIT 1`).get(params.id);
1284
+ if (!existing) return jsonError(404, "Not found");
1285
+ if (scoped && existing._owner !== userId) return jsonError(404, "Not found");
1286
+ const updates = {};
1287
+ for (const [k, v] of Object.entries(body)) {
1288
+ if (k === "id" || k === "created_at" || k === "updated_at" || k === "_owner") continue;
1289
+ if (!(k in fields)) return jsonError(400, `Unknown column: ${k}`);
1290
+ updates[k] = encodeValue(fields[k].type, v);
1291
+ }
1292
+ updates.updated_at = (/* @__PURE__ */ new Date()).toISOString();
1293
+ const setClauses = Object.keys(updates).map((k) => `${k} = ?`).join(", ");
1294
+ const values = [...Object.values(updates), params.id];
1295
+ db.raw().prepare(`UPDATE ${name} SET ${setClauses} WHERE id = ?`).run(...values);
1296
+ const merged = {
1297
+ ...existing,
1298
+ ...updates
1299
+ };
1300
+ invalidate(name);
1301
+ notifyRowChange(name, "update", existing, merged);
1302
+ return Response.json(decodeRow(merged, fields));
1303
+ };
1304
+ }
1305
+ function makeRemoveHandler(name, scoped) {
1306
+ return async (_req, params) => {
1307
+ const db = getDbInstance();
1308
+ if (!db) return jsonError(500, "db not initialized");
1309
+ const existing = db.raw().prepare(`SELECT * FROM ${name} WHERE id = ? LIMIT 1`).get(params.id);
1310
+ if (!existing) return jsonError(404, "Not found");
1311
+ if (scoped) {
1312
+ const userId = getUserId();
1313
+ if (!userId) return jsonError(401, "Authentication required");
1314
+ if (existing._owner !== userId) return jsonError(404, "Not found");
1315
+ }
1316
+ db.raw().prepare(`DELETE FROM ${name} WHERE id = ?`).run(params.id);
1317
+ invalidate(name);
1318
+ notifyRowChange(name, "delete", existing, null);
1319
+ return new Response(null, { status: 204 });
1320
+ };
1321
+ }
1322
+ function buildAutoCrudRoutes(schema) {
1323
+ const routes = [];
1324
+ for (const [name, col] of Object.entries(schema.collections)) {
1325
+ const fields = col.fields;
1326
+ const scoped = col.scope === "user";
1327
+ routes.push({
1328
+ method: "GET",
1329
+ path: `/api/${name}`,
1330
+ module: "<auto-crud>",
1331
+ handler: "list",
1332
+ inlineHandler: makeListHandler(name, fields, scoped)
1333
+ });
1334
+ routes.push({
1335
+ method: "GET",
1336
+ path: `/api/${name}/:id`,
1337
+ module: "<auto-crud>",
1338
+ handler: "get",
1339
+ inlineHandler: makeGetHandler(name, fields, scoped)
1340
+ });
1341
+ routes.push({
1342
+ method: "POST",
1343
+ path: `/api/${name}`,
1344
+ module: "<auto-crud>",
1345
+ handler: "create",
1346
+ inlineHandler: makeCreateHandler(name, fields, scoped)
1347
+ });
1348
+ routes.push({
1349
+ method: "PATCH",
1350
+ path: `/api/${name}/:id`,
1351
+ module: "<auto-crud>",
1352
+ handler: "update",
1353
+ inlineHandler: makeUpdateHandler(name, fields, scoped)
1354
+ });
1355
+ routes.push({
1356
+ method: "DELETE",
1357
+ path: `/api/${name}/:id`,
1358
+ module: "<auto-crud>",
1359
+ handler: "remove",
1360
+ inlineHandler: makeRemoveHandler(name, scoped)
1361
+ });
1362
+ }
1363
+ return routes;
1364
+ }
1365
+ //#endregion
1366
+ //#region src/triggers.ts
1367
+ /**
1368
+ * Schedule a function to run on a cron expression (UTC, 5-field syntax).
1369
+ * The schedule must be a literal string — dynamic values will be rejected by
1370
+ * the build-time scanner so the orchestrator can register the trigger.
1371
+ *
1372
+ * Returns the handler unchanged so it can also be invoked directly in tests.
1373
+ */
1374
+ function cron(schedule, handler) {
1375
+ return handler;
1376
+ }
1377
+ /**
1378
+ * Subscribe a function to an event topic. The topic must be a literal string.
1379
+ */
1380
+ function on(topic, handler) {
1381
+ return handler;
1382
+ }
1383
+ /**
1384
+ * Emit an event. Mode-aware: posts to the orchestrator in prod; runs
1385
+ * subscribers in-process in dev.
1386
+ *
1387
+ * Payload is JSON-serialized; total size capped at 64 KB (matched against
1388
+ * the orchestrator-side limit).
1389
+ */
1390
+ async function emit(topic, payload = null) {
1391
+ if (vibesMode$1() === "dev") return emitInProcess(topic, payload);
1392
+ return emitToOrchestrator(topic, payload);
1393
+ }
1394
+ /**
1395
+ * Schedule a one-shot delayed fire of `topic`. Same fan-out semantics as
1396
+ * `emit()` (every `on(topic, …)` subscriber receives one delivery), but
1397
+ * each delivery runs at `atMs` instead of immediately.
1398
+ *
1399
+ * `atMs` is unix milliseconds. Past timestamps fire ASAP. The orchestrator
1400
+ * caps the future horizon at 365 days.
1401
+ *
1402
+ * Returns `{ eventId, subscriberCount, scheduledFor }`. Hold onto `eventId`
1403
+ * if you might need to `cancel(eventId)` before it fires.
1404
+ *
1405
+ * Example:
1406
+ * const { eventId } = await schedule(reminder.dueAt, "reminder.due", { id })
1407
+ * // …later, if user deletes the reminder:
1408
+ * await cancel(eventId)
1409
+ */
1410
+ async function schedule(atMs, topic, payload = null) {
1411
+ if (!Number.isFinite(atMs) || atMs < 0) throw new Error(`schedule("${topic}"): at must be a unix-ms number`);
1412
+ if (!topic || typeof topic !== "string") throw new Error("schedule: topic required");
1413
+ if (vibesMode$1() === "dev") return scheduleInProcess(atMs, topic, payload);
1414
+ return scheduleToOrchestrator(atMs, topic, payload);
1415
+ }
1416
+ /**
1417
+ * Cancel the pending deliveries created by a previous `schedule()`. At-most-
1418
+ * once semantics: anything already claimed or fired is past the cancel
1419
+ * window and is left untouched.
1420
+ *
1421
+ * Returns `{ cancelled }`, the number of pending deliveries dropped. Zero
1422
+ * is a valid response meaning "nothing left to cancel" (already fired, or
1423
+ * never scheduled with this eventId).
1424
+ */
1425
+ async function cancel(eventId) {
1426
+ if (!eventId || typeof eventId !== "string") throw new Error("cancel: eventId required");
1427
+ if (vibesMode$1() === "dev") return cancelInProcess(eventId);
1428
+ return cancelOnOrchestrator(eventId);
1429
+ }
1430
+ const triggerRegistry = /* @__PURE__ */ new Map();
1431
+ const topicSubscribers = /* @__PURE__ */ new Map();
1432
+ const storageHookHandlers = /* @__PURE__ */ new Map();
1433
+ const cronIntervals = /* @__PURE__ */ new Map();
1434
+ let _cronDriver = null;
1435
+ function setCronDriver(driver) {
1436
+ if (driver !== "orchestrator" && driver !== "in-process") throw new Error(`[vibes:triggers] unknown cron driver ${JSON.stringify(driver)} — expected "orchestrator" or "in-process"`);
1437
+ _cronDriver = driver;
1438
+ }
1439
+ function cronDriver() {
1440
+ if (_cronDriver) return _cronDriver;
1441
+ const env = (typeof process !== "undefined" ? process.env?.VIBES_CRON_DRIVER : void 0) ?? "";
1442
+ if (env === "") return "orchestrator";
1443
+ if (env !== "orchestrator" && env !== "in-process") throw new Error(`[vibes:triggers] unknown VIBES_CRON_DRIVER ${JSON.stringify(env)} — expected "orchestrator" or "in-process"`);
1444
+ return env;
1445
+ }
1446
+ /** Clear everything — called on HMR reload. */
1447
+ function clearTriggers() {
1448
+ for (const interval of cronIntervals.values()) clearInterval(interval);
1449
+ cronIntervals.clear();
1450
+ triggerRegistry.clear();
1451
+ topicSubscribers.clear();
1452
+ storageHookHandlers.clear();
1453
+ }
1454
+ /** Load triggers from .vibes/triggers.json (called at createVibesServer boot). */
1455
+ async function loadTriggersFromFile(root) {
1456
+ const triggersPath = path.join(root, ".vibes", "triggers.json");
1457
+ if (!fs.existsSync(triggersPath)) return [];
1458
+ try {
1459
+ const raw = fs.readFileSync(triggersPath, "utf-8");
1460
+ const parsed = JSON.parse(raw);
1461
+ return Array.isArray(parsed) ? parsed : [];
1462
+ } catch (err) {
1463
+ console.error("[vibes:triggers] failed to read triggers.json:", err);
1464
+ return [];
1465
+ }
1466
+ }
1467
+ /**
1468
+ * Populate the registry from a parsed trigger list. Either resolves each
1469
+ * handler via dynamic import (dev) or reads it from the preloaded `mod`
1470
+ * field (prod bundle). Idempotent.
1471
+ */
1472
+ async function registerTriggers(entries) {
1473
+ clearTriggers();
1474
+ for (const e of entries) {
1475
+ let fn;
1476
+ if (e.mod) fn = e.mod[e.exportName];
1477
+ else try {
1478
+ fn = (await import(e.module))[e.exportName];
1479
+ } catch (err) {
1480
+ console.error(`[vibes:triggers] import ${e.module}#${e.exportName} failed:`, err);
1481
+ continue;
1482
+ }
1483
+ if (typeof fn !== "function") {
1484
+ console.error(`[vibes:triggers] ${e.module}#${e.exportName} is not a function (got ${typeof fn}) — skipping`);
1485
+ continue;
1486
+ }
1487
+ triggerRegistry.set(e.handler, {
1488
+ entry: e,
1489
+ fn
1490
+ });
1491
+ if (e.kind === "on") {
1492
+ if (!topicSubscribers.has(e.key)) topicSubscribers.set(e.key, /* @__PURE__ */ new Set());
1493
+ topicSubscribers.get(e.key).add(e.handler);
1494
+ } else if (e.kind === "storage:upload" || e.kind === "storage:delete") {
1495
+ if (!storageHookHandlers.has(e.kind)) storageHookHandlers.set(e.kind, /* @__PURE__ */ new Set());
1496
+ storageHookHandlers.get(e.kind).add(e.handler);
1497
+ }
1498
+ }
1499
+ const inProcessCron = vibesMode$1() === "dev" || cronDriver() === "in-process";
1500
+ if (inProcessCron) scheduleAllCronInProcess();
1501
+ console.log(`[vibes:triggers] registered ${entries.length} trigger(s) — mode=${vibesMode$1()}, cron=${inProcessCron ? "in-process" : "orchestrator"}`);
1502
+ }
1503
+ /** Snapshot of the registry — used by the in-dev Inspect endpoints. */
1504
+ function listTriggers() {
1505
+ return Array.from(triggerRegistry.values()).map(({ entry }) => ({
1506
+ handler: entry.handler,
1507
+ kind: entry.kind,
1508
+ key: entry.key
1509
+ }));
1510
+ }
1511
+ /**
1512
+ * Fan out a storage event to every registered `storage.onUpload` /
1513
+ * `onDelete` handler. Called by the auto-injected /api/_storage/notify
1514
+ * route after a successful PUT/DELETE round-trips back from the browser.
1515
+ *
1516
+ * Handlers run in-process (no orchestrator dispatcher round-trip) — the
1517
+ * notify is best-effort fire-and-forget from the SDK's perspective. A
1518
+ * dropped notify means the hook doesn't fire; the storage object itself
1519
+ * is unaffected.
1520
+ *
1521
+ * Returns the number of handlers invoked. Each handler's failure is
1522
+ * logged but doesn't bubble — one bad handler shouldn't block siblings.
1523
+ */
1524
+ async function dispatchStorageEvent(kind, evt) {
1525
+ const names = storageHookHandlers.get(kind);
1526
+ if (!names || names.size === 0) return 0;
1527
+ let fired = 0;
1528
+ await Promise.all(Array.from(names).map(async (name) => {
1529
+ const resolved = triggerRegistry.get(name);
1530
+ if (!resolved) return;
1531
+ try {
1532
+ const handler = resolved.fn;
1533
+ const payload = kind === "storage:upload" ? {
1534
+ kind: "upload",
1535
+ key: evt.key,
1536
+ size: evt.size ?? 0,
1537
+ contentType: evt.contentType ?? "",
1538
+ userId: evt.userId ?? null
1539
+ } : {
1540
+ kind: "delete",
1541
+ key: evt.key,
1542
+ userId: evt.userId ?? null
1543
+ };
1544
+ await handler({
1545
+ userId: evt.userId ?? null,
1546
+ system: true
1547
+ }, payload);
1548
+ fired++;
1549
+ } catch (err) {
1550
+ console.error(`[vibes:storage] handler ${name} threw:`, err);
1551
+ }
1552
+ }));
1553
+ return fired;
1554
+ }
1555
+ /**
1556
+ * Invoke a registered handler. Returns a Response so the agent can pass
1557
+ * it back to the orchestrator dispatcher loop, which keys retry/done on
1558
+ * the status code.
1559
+ */
1560
+ async function dispatchHandler(body) {
1561
+ const resolved = triggerRegistry.get(body.handler);
1562
+ if (!resolved) return new Response(JSON.stringify({ error: `unknown handler: ${body.handler}` }), {
1563
+ status: 404,
1564
+ headers: { "Content-Type": "application/json" }
1565
+ });
1566
+ let payload = null;
1567
+ if (body.payload && body.payload !== "null") try {
1568
+ payload = JSON.parse(body.payload);
1569
+ } catch (err) {
1570
+ return new Response(JSON.stringify({ error: `payload parse: ${err.message}` }), {
1571
+ status: 400,
1572
+ headers: { "Content-Type": "application/json" }
1573
+ });
1574
+ }
1575
+ try {
1576
+ await ctxStore.run({
1577
+ userId: null,
1578
+ system: true
1579
+ }, async () => {
1580
+ if (resolved.entry.kind === "cron") await resolved.fn({
1581
+ userId: null,
1582
+ system: true
1583
+ });
1584
+ else await resolved.fn({
1585
+ userId: null,
1586
+ system: true
1587
+ }, payload);
1588
+ });
1589
+ return new Response(JSON.stringify({ ok: true }), {
1590
+ status: 200,
1591
+ headers: { "Content-Type": "application/json" }
1592
+ });
1593
+ } catch (err) {
1594
+ const msg = err instanceof Error ? err.message : String(err);
1595
+ console.error(`[vibes:triggers] handler ${body.handler} threw:`, err);
1596
+ return new Response(JSON.stringify({ error: msg }), {
1597
+ status: 500,
1598
+ headers: { "Content-Type": "application/json" }
1599
+ });
1600
+ }
1601
+ }
1602
+ const MAX_EMIT_BYTES = 64 * 1024;
1603
+ const DEV_EMIT_RING = 200;
1604
+ const recentDevEmits = [];
1605
+ const recentDevDeliveries = [];
1606
+ function pushBounded(arr, v, max) {
1607
+ arr.unshift(v);
1608
+ if (arr.length > max) arr.length = max;
1609
+ }
1610
+ function emitInProcess(topic, payload) {
1611
+ if (JSON.stringify(payload ?? null).length > MAX_EMIT_BYTES) throw new Error(`emit("${topic}"): payload exceeds ${MAX_EMIT_BYTES} bytes`);
1612
+ const subs = topicSubscribers.get(topic);
1613
+ const subList = subs ? Array.from(subs) : [];
1614
+ const ev = {
1615
+ id: `evt_dev_${Date.now()}_${Math.floor(Math.random() * 4096).toString(16)}`,
1616
+ topic,
1617
+ payload,
1618
+ subscriberCount: subList.length,
1619
+ createdAt: Date.now()
1620
+ };
1621
+ pushBounded(recentDevEmits, ev, DEV_EMIT_RING);
1622
+ for (const handler of subList) {
1623
+ const resolved = triggerRegistry.get(handler);
1624
+ if (!resolved) continue;
1625
+ const start = Date.now();
1626
+ (async () => {
1627
+ try {
1628
+ await resolved.fn({
1629
+ userId: null,
1630
+ system: true
1631
+ }, payload);
1632
+ pushBounded(recentDevDeliveries, {
1633
+ id: `dlv_dev_${start}_${Math.floor(Math.random() * 4096).toString(16)}`,
1634
+ eventId: ev.id,
1635
+ handler,
1636
+ status: "done",
1637
+ attempts: 1,
1638
+ createdAt: start,
1639
+ runAt: start,
1640
+ doneAt: Date.now()
1641
+ }, DEV_EMIT_RING);
1642
+ } catch (err) {
1643
+ pushBounded(recentDevDeliveries, {
1644
+ id: `dlv_dev_${start}_${Math.floor(Math.random() * 4096).toString(16)}`,
1645
+ eventId: ev.id,
1646
+ handler,
1647
+ status: "failed",
1648
+ attempts: 1,
1649
+ lastError: err instanceof Error ? err.message : String(err),
1650
+ createdAt: start,
1651
+ runAt: start,
1652
+ doneAt: Date.now()
1653
+ }, DEV_EMIT_RING);
1654
+ console.error(`[vibes:triggers] dev emit ${topic} → ${handler} threw:`, err);
1655
+ }
1656
+ })();
1657
+ }
1658
+ return { subscriberCount: subList.length };
1659
+ }
1660
+ async function emitToOrchestrator(topic, payload) {
1661
+ const payloadStr = JSON.stringify(payload ?? null);
1662
+ if (payloadStr.length > MAX_EMIT_BYTES) throw new Error(`emit("${topic}"): payload exceeds ${MAX_EMIT_BYTES} bytes`);
1663
+ const res = await fetch("http://localhost:8080/_emit", {
1664
+ method: "POST",
1665
+ headers: { "Content-Type": "application/json" },
1666
+ body: JSON.stringify({
1667
+ topic,
1668
+ payload: JSON.parse(payloadStr)
1669
+ })
1670
+ });
1671
+ if (!res.ok) {
1672
+ const text = await res.text().catch(() => "");
1673
+ throw new Error(`emit("${topic}") agent ${res.status}: ${text.slice(0, 200)}`);
1674
+ }
1675
+ return { subscriberCount: (await res.json()).subscriberCount ?? 0 };
1676
+ }
1677
+ const pendingScheduledFires = /* @__PURE__ */ new Map();
1678
+ function devId$1(prefix) {
1679
+ return `${prefix}_dev_${Date.now()}_${Math.floor(Math.random() * 4096).toString(16)}`;
1680
+ }
1681
+ function scheduleInProcess(atMs, topic, payload) {
1682
+ if (JSON.stringify(payload ?? null).length > MAX_EMIT_BYTES) throw new Error(`schedule("${topic}"): payload exceeds ${MAX_EMIT_BYTES} bytes`);
1683
+ const subs = topicSubscribers.get(topic);
1684
+ const subList = subs ? Array.from(subs) : [];
1685
+ const eventId = devId$1("evt");
1686
+ const now = Date.now();
1687
+ pushBounded(recentDevEmits, {
1688
+ id: eventId,
1689
+ topic,
1690
+ payload,
1691
+ subscriberCount: subList.length,
1692
+ createdAt: now
1693
+ }, DEV_EMIT_RING);
1694
+ const deliveryIds = [];
1695
+ for (const handler of subList) {
1696
+ const id = devId$1("dlv");
1697
+ deliveryIds.push(id);
1698
+ pushBounded(recentDevDeliveries, {
1699
+ id,
1700
+ eventId,
1701
+ handler,
1702
+ status: "pending",
1703
+ attempts: 0,
1704
+ createdAt: now,
1705
+ runAt: atMs
1706
+ }, DEV_EMIT_RING);
1707
+ }
1708
+ const delay = Math.max(0, atMs - now);
1709
+ const timer = setTimeout(() => {
1710
+ pendingScheduledFires.delete(eventId);
1711
+ for (let i = 0; i < subList.length; i++) {
1712
+ const handler = subList[i];
1713
+ const id = deliveryIds[i];
1714
+ const resolved = triggerRegistry.get(handler);
1715
+ if (!resolved) continue;
1716
+ const startedAt = Date.now();
1717
+ (async () => {
1718
+ try {
1719
+ await resolved.fn({
1720
+ userId: null,
1721
+ system: true
1722
+ }, payload);
1723
+ updateDevDelivery(id, {
1724
+ status: "done",
1725
+ attempts: 1,
1726
+ doneAt: Date.now(),
1727
+ runAt: startedAt
1728
+ });
1729
+ } catch (err) {
1730
+ updateDevDelivery(id, {
1731
+ status: "failed",
1732
+ attempts: 1,
1733
+ doneAt: Date.now(),
1734
+ runAt: startedAt,
1735
+ lastError: err instanceof Error ? err.message : String(err)
1736
+ });
1737
+ console.error(`[vibes:triggers] dev schedule ${topic} → ${handler} threw:`, err);
1738
+ }
1739
+ })();
1740
+ }
1741
+ }, delay);
1742
+ pendingScheduledFires.set(eventId, {
1743
+ timer,
1744
+ topic,
1745
+ payload,
1746
+ fireAt: atMs,
1747
+ deliveryIds
1748
+ });
1749
+ return {
1750
+ eventId,
1751
+ subscriberCount: subList.length,
1752
+ scheduledFor: atMs
1753
+ };
1754
+ }
1755
+ function cancelInProcess(eventId) {
1756
+ const pending = pendingScheduledFires.get(eventId);
1757
+ if (!pending) return { cancelled: 0 };
1758
+ clearTimeout(pending.timer);
1759
+ pendingScheduledFires.delete(eventId);
1760
+ const ids = new Set(pending.deliveryIds);
1761
+ for (let i = recentDevDeliveries.length - 1; i >= 0; i--) if (ids.has(recentDevDeliveries[i].id)) recentDevDeliveries.splice(i, 1);
1762
+ return { cancelled: pending.deliveryIds.length };
1763
+ }
1764
+ function updateDevDelivery(id, patch) {
1765
+ const i = recentDevDeliveries.findIndex((d) => d.id === id);
1766
+ if (i < 0) return;
1767
+ recentDevDeliveries[i] = {
1768
+ ...recentDevDeliveries[i],
1769
+ ...patch
1770
+ };
1771
+ }
1772
+ async function scheduleToOrchestrator(atMs, topic, payload) {
1773
+ const payloadStr = JSON.stringify(payload ?? null);
1774
+ if (payloadStr.length > MAX_EMIT_BYTES) throw new Error(`schedule("${topic}"): payload exceeds ${MAX_EMIT_BYTES} bytes`);
1775
+ const res = await fetch("http://localhost:8080/_schedule", {
1776
+ method: "POST",
1777
+ headers: { "Content-Type": "application/json" },
1778
+ body: JSON.stringify({
1779
+ topic,
1780
+ payload: JSON.parse(payloadStr),
1781
+ runAt: atMs
1782
+ })
1783
+ });
1784
+ if (!res.ok) {
1785
+ const text = await res.text().catch(() => "");
1786
+ throw new Error(`schedule("${topic}") agent ${res.status}: ${text.slice(0, 200)}`);
1787
+ }
1788
+ const body = await res.json();
1789
+ return {
1790
+ eventId: body.eventId ?? "",
1791
+ subscriberCount: body.subscriberCount ?? 0,
1792
+ scheduledFor: body.runAt ?? atMs
1793
+ };
1794
+ }
1795
+ async function cancelOnOrchestrator(eventId) {
1796
+ const url = `http://localhost:8080/_schedule/${encodeURIComponent(eventId)}`;
1797
+ const res = await fetch(url, { method: "DELETE" });
1798
+ if (!res.ok) {
1799
+ const text = await res.text().catch(() => "");
1800
+ throw new Error(`cancel(${eventId}) agent ${res.status}: ${text.slice(0, 200)}`);
1801
+ }
1802
+ return { cancelled: (await res.json()).cancelled ?? 0 };
1803
+ }
1804
+ function scheduleAllCronInProcess() {
1805
+ for (const interval of cronIntervals.values()) clearInterval(interval);
1806
+ cronIntervals.clear();
1807
+ for (const { entry } of triggerRegistry.values()) {
1808
+ if (entry.kind !== "cron") continue;
1809
+ scheduleOneCron(entry.handler, entry.key);
1810
+ }
1811
+ }
1812
+ function scheduleOneCron(handlerName, expr) {
1813
+ const tickFn = async () => {
1814
+ const resolved = triggerRegistry.get(handlerName);
1815
+ if (!resolved) return;
1816
+ const start = Date.now();
1817
+ try {
1818
+ await resolved.fn({
1819
+ userId: null,
1820
+ system: true
1821
+ });
1822
+ pushBounded(recentDevDeliveries, {
1823
+ id: `dlv_dev_${start}_${Math.floor(Math.random() * 4096).toString(16)}`,
1824
+ handler: handlerName,
1825
+ status: "done",
1826
+ attempts: 1,
1827
+ createdAt: start,
1828
+ runAt: start,
1829
+ doneAt: Date.now()
1830
+ }, DEV_EMIT_RING);
1831
+ } catch (err) {
1832
+ pushBounded(recentDevDeliveries, {
1833
+ id: `dlv_dev_${start}_${Math.floor(Math.random() * 4096).toString(16)}`,
1834
+ handler: handlerName,
1835
+ status: "failed",
1836
+ attempts: 1,
1837
+ lastError: err instanceof Error ? err.message : String(err),
1838
+ createdAt: start,
1839
+ runAt: start,
1840
+ doneAt: Date.now()
1841
+ }, DEV_EMIT_RING);
1842
+ console.error(`[vibes:triggers] dev cron ${handlerName} threw:`, err);
1843
+ }
1844
+ };
1845
+ const reschedule = () => {
1846
+ const next = nextCronTime(expr, /* @__PURE__ */ new Date());
1847
+ if (!next) {
1848
+ console.error(`[vibes:triggers] dev cron ${handlerName}: invalid expr ${expr}`);
1849
+ return;
1850
+ }
1851
+ const wait = Math.max(0, next.getTime() - Date.now());
1852
+ const timer = setTimeout(() => {
1853
+ tickFn().finally(reschedule);
1854
+ }, wait);
1855
+ cronIntervals.set(handlerName, timer);
1856
+ };
1857
+ reschedule();
1858
+ }
1859
+ function nextCronTime(expr, after) {
1860
+ try {
1861
+ const fields = expr.trim().split(/\s+/);
1862
+ if (fields.length !== 5) return null;
1863
+ const matchers = fields.map((f, i) => parseField(f, FIELD_BOUNDS[i]));
1864
+ let t = /* @__PURE__ */ new Date(after.getTime() + 6e4 - after.getTime() % 6e4);
1865
+ const limit = t.getTime() + 4 * 365 * 24 * 60 * 60 * 1e3;
1866
+ while (t.getTime() < limit) {
1867
+ if (matchers[0](t.getUTCMinutes()) && matchers[1](t.getUTCHours()) && matchers[2](t.getUTCDate()) && matchers[3](t.getUTCMonth() + 1) && matchers[4](t.getUTCDay())) return t;
1868
+ t = new Date(t.getTime() + 6e4);
1869
+ }
1870
+ return null;
1871
+ } catch {
1872
+ return null;
1873
+ }
1874
+ }
1875
+ const FIELD_BOUNDS = [
1876
+ [0, 59],
1877
+ [0, 23],
1878
+ [1, 31],
1879
+ [1, 12],
1880
+ [0, 6]
1881
+ ];
1882
+ function parseField(field, [lo, hi]) {
1883
+ const allowed = /* @__PURE__ */ new Set();
1884
+ for (const part of field.split(",")) if (part === "*") for (let i = lo; i <= hi; i++) allowed.add(i);
1885
+ else if (part.includes("*/")) {
1886
+ const [, stepStr] = part.split("*/");
1887
+ const step = parseInt(stepStr, 10);
1888
+ if (!Number.isFinite(step) || step <= 0) continue;
1889
+ for (let i = lo; i <= hi; i += step) allowed.add(i);
1890
+ } else if (part.includes("-")) {
1891
+ const [a, b] = part.split("-").map((s) => parseInt(s, 10));
1892
+ if (Number.isFinite(a) && Number.isFinite(b)) for (let i = a; i <= b; i++) allowed.add(i);
1893
+ } else if (part.includes("/")) {
1894
+ const [aStr, bStr] = part.split("/");
1895
+ const a = parseInt(aStr, 10);
1896
+ const b = parseInt(bStr, 10);
1897
+ if (Number.isFinite(a) && Number.isFinite(b) && b > 0) for (let i = a; i <= hi; i += b) allowed.add(i);
1898
+ } else {
1899
+ const n = parseInt(part, 10);
1900
+ if (Number.isFinite(n)) allowed.add(n);
1901
+ }
1902
+ return (v) => allowed.has(v);
1903
+ }
1904
+ let _vibesMode$1 = null;
1905
+ function vibesMode$1() {
1906
+ if (_vibesMode$1) return _vibesMode$1;
1907
+ _vibesMode$1 = ((typeof process !== "undefined" ? process.env?.VIBES_MODE : void 0) ?? "") === "dev" ? "dev" : "prod";
1908
+ return _vibesMode$1;
1909
+ }
1910
+ function devInspectTriggers() {
1911
+ return Array.from(triggerRegistry.values()).map(({ entry }, i) => ({
1912
+ id: `trg_dev_${i}_${entry.handler}`,
1913
+ kind: entry.kind,
1914
+ key: entry.key,
1915
+ handler: entry.handler,
1916
+ version: 0,
1917
+ nextFireAt: 0,
1918
+ createdAt: 0
1919
+ }));
1920
+ }
1921
+ function devInspectDeliveries() {
1922
+ return recentDevDeliveries;
1923
+ }
1924
+ function devInspectEvents() {
1925
+ return recentDevEmits;
1926
+ }
1927
+ //#endregion
1928
+ //#region src/workflows.ts
1929
+ /**
1930
+ * Declare a durable workflow. `name` must be a string literal — the
1931
+ * build-time scanner persists it so the orchestrator can register the
1932
+ * workflow endpoint with the engine. Returns the function unchanged so it
1933
+ * can be invoked directly in tests.
1934
+ */
1935
+ function workflow(name, fn) {
1936
+ return fn;
1937
+ }
1938
+ /**
1939
+ * Start a workflow run by name. Fire-and-forget: resolves with the run id
1940
+ * as soon as the engine has durably accepted the start, not when the run
1941
+ * completes. Payload is JSON-serialized, capped at 64 KB (same as emit()).
1942
+ */
1943
+ async function startWorkflow(name, payload = null, opts = {}) {
1944
+ if (!name || typeof name !== "string") throw new Error("startWorkflow: name required");
1945
+ const payloadStr = JSON.stringify(payload ?? null);
1946
+ if (payloadStr.length > MAX_WORKFLOW_PAYLOAD_BYTES) throw new Error(`startWorkflow("${name}"): payload exceeds ${MAX_WORKFLOW_PAYLOAD_BYTES} bytes`);
1947
+ if (workflowMode() === "dev") return startInProcess(name, payload, opts);
1948
+ return startViaAgent(name, payloadStr, opts);
1949
+ }
1950
+ const workflowRegistry = /* @__PURE__ */ new Map();
1951
+ function clearWorkflows() {
1952
+ workflowRegistry.clear();
1953
+ }
1954
+ /** Load workflow entries from .vibes/workflows.json (dev boot path). */
1955
+ async function loadWorkflowsFromFile(root) {
1956
+ const p = path.join(root, ".vibes", "workflows.json");
1957
+ if (!fs.existsSync(p)) return [];
1958
+ try {
1959
+ const parsed = JSON.parse(fs.readFileSync(p, "utf-8"));
1960
+ return Array.isArray(parsed) ? parsed : [];
1961
+ } catch (err) {
1962
+ console.error("[vibes:workflows] failed to read workflows.json:", err);
1963
+ return [];
1964
+ }
1965
+ }
1966
+ /**
1967
+ * Populate the registry. Resolves each workflow fn from the preloaded `mod`
1968
+ * (prod bundle) or via dynamic import (dev). Idempotent — called at boot
1969
+ * and on HMR reload.
1970
+ */
1971
+ async function registerWorkflows(entries) {
1972
+ clearWorkflows();
1973
+ for (const e of entries) {
1974
+ let fn;
1975
+ if (e.mod) fn = e.mod[e.exportName];
1976
+ else try {
1977
+ fn = (await import(e.module))[e.exportName];
1978
+ } catch (err) {
1979
+ console.error(`[vibes:workflows] import ${e.module}#${e.exportName} failed:`, err);
1980
+ continue;
1981
+ }
1982
+ if (typeof fn !== "function") {
1983
+ console.error(`[vibes:workflows] ${e.module}#${e.exportName} is not a function — skipping`);
1984
+ continue;
1985
+ }
1986
+ if (workflowRegistry.has(e.name)) throw new Error(`[vibes:workflows] duplicate workflow name "${e.name}" (${e.handler} vs ${workflowRegistry.get(e.name).entry.handler})`);
1987
+ workflowRegistry.set(e.name, {
1988
+ entry: e,
1989
+ fn
1990
+ });
1991
+ }
1992
+ if (entries.length > 0) console.log(`[vibes:workflows] registered ${workflowRegistry.size} workflow(s) — mode=${workflowMode()}`);
1993
+ }
1994
+ function listWorkflows() {
1995
+ return Array.from(workflowRegistry.values()).map(({ entry }) => ({
1996
+ name: entry.name,
1997
+ handler: entry.handler
1998
+ }));
1999
+ }
2000
+ const MAX_WORKFLOW_PAYLOAD_BYTES = 64 * 1024;
2001
+ let _mode = null;
2002
+ function workflowMode() {
2003
+ if (_mode) return _mode;
2004
+ _mode = ((typeof process !== "undefined" ? process.env?.VIBES_MODE : void 0) ?? "") === "dev" ? "dev" : "prod";
2005
+ return _mode;
2006
+ }
2007
+ const DEV_RUN_RING = 200;
2008
+ const recentDevRuns = [];
2009
+ const devIdempotency = /* @__PURE__ */ new Map();
2010
+ function pushBoundedRun(run) {
2011
+ recentDevRuns.unshift(run);
2012
+ if (recentDevRuns.length > DEV_RUN_RING) recentDevRuns.length = DEV_RUN_RING;
2013
+ }
2014
+ function devId() {
2015
+ return `run_dev_${Date.now()}_${Math.floor(Math.random() * 4096).toString(16)}`;
2016
+ }
2017
+ function startInProcess(name, payload, opts) {
2018
+ const resolved = workflowRegistry.get(name);
2019
+ if (!resolved) throw new Error(`startWorkflow("${name}"): unknown workflow — declare it as \`export const x = workflow("${name}", async (step, payload) => …)\` in functions/`);
2020
+ if (opts.id) {
2021
+ const existing = devIdempotency.get(opts.id);
2022
+ if (existing) return { runId: existing };
2023
+ }
2024
+ const run = {
2025
+ id: devId(),
2026
+ name,
2027
+ status: "running",
2028
+ payload,
2029
+ steps: [],
2030
+ createdAt: Date.now()
2031
+ };
2032
+ if (opts.id) devIdempotency.set(opts.id, run.id);
2033
+ pushBoundedRun(run);
2034
+ const step = {
2035
+ async run(stepName, fn) {
2036
+ const s = {
2037
+ name: stepName,
2038
+ kind: "run",
2039
+ status: "running",
2040
+ startedAt: Date.now()
2041
+ };
2042
+ run.steps.push(s);
2043
+ try {
2044
+ const out = await fn();
2045
+ Object.assign(s, {
2046
+ status: "done",
2047
+ doneAt: Date.now()
2048
+ });
2049
+ return out;
2050
+ } catch (err) {
2051
+ Object.assign(s, {
2052
+ status: "failed",
2053
+ doneAt: Date.now()
2054
+ });
2055
+ throw err;
2056
+ }
2057
+ },
2058
+ async sleep(stepName, ms) {
2059
+ const s = {
2060
+ name: stepName,
2061
+ kind: "sleep",
2062
+ status: "running",
2063
+ startedAt: Date.now()
2064
+ };
2065
+ run.steps.push(s);
2066
+ await new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
2067
+ Object.assign(s, {
2068
+ status: "done",
2069
+ doneAt: Date.now()
2070
+ });
2071
+ }
2072
+ };
2073
+ ctxStore.run({
2074
+ userId: null,
2075
+ system: true
2076
+ }, async () => {
2077
+ try {
2078
+ const result = await resolved.fn(step, payload);
2079
+ run.status = "done";
2080
+ run.result = result;
2081
+ run.doneAt = Date.now();
2082
+ } catch (err) {
2083
+ run.status = "failed";
2084
+ run.error = err instanceof Error ? err.message : String(err);
2085
+ run.doneAt = Date.now();
2086
+ console.error(`[vibes:workflows] dev run ${name} (${run.id}) failed:`, err);
2087
+ }
2088
+ });
2089
+ return { runId: run.id };
2090
+ }
2091
+ /** Dev Inspect accessor — mirrors devInspectDeliveries() in triggers.ts. */
2092
+ function devInspectWorkflowRuns() {
2093
+ return recentDevRuns;
2094
+ }
2095
+ const DEFAULT_IDENTITY_KEYS = ["publickeyv1_9w1Za94FAEiJ2LzArToAt7mCw6B8UnZYS4EQJkqJ1uZM"];
2096
+ function workflowServiceName(slug) {
2097
+ return `wf_${slug.replace(/-/g, "_")}`;
2098
+ }
2099
+ let restateFetchHandler = null;
2100
+ /**
2101
+ * Build the Restate endpoint fetch handler for the registered workflows.
2102
+ * Called once at createVibesServer boot in prod when the app declares
2103
+ * workflows. Resolves the slug from VIBES_APP_SLUG (injected by the
2104
+ * orchestrator's startDeployServer) — fails loud when missing, because a
2105
+ * silently mis-named service would strand every run.
2106
+ */
2107
+ async function buildWorkflowEndpoint() {
2108
+ if (workflowMode() === "dev") return;
2109
+ if (workflowRegistry.size === 0) return;
2110
+ const slug = process.env.VIBES_APP_SLUG ?? "";
2111
+ if (!slug) throw new Error("[vibes:workflows] VIBES_APP_SLUG not set but this deploy declares workflows — orchestrator must inject it (startDeployServer). Refusing to boot with a mis-named service.");
2112
+ const [{ service }, { createEndpointHandler }] = await Promise.all([import("@restatedev/restate-sdk"), import("@restatedev/restate-sdk/fetch")]);
2113
+ const handlers = {};
2114
+ for (const { entry, fn } of workflowRegistry.values()) handlers[entry.name] = (ctx, payload) => ctxStore.run({
2115
+ userId: null,
2116
+ system: true
2117
+ }, () => fn(makeRestateStepContext(ctx), payload));
2118
+ const keysEnv = process.env.VIBES_WORKFLOW_IDENTITY_KEYS;
2119
+ const identityKeys = keysEnv === "insecure" ? void 0 : keysEnv ? keysEnv.split(",").map((s) => s.trim()).filter(Boolean) : DEFAULT_IDENTITY_KEYS;
2120
+ restateFetchHandler = createEndpointHandler({
2121
+ services: [service({
2122
+ name: workflowServiceName(slug),
2123
+ handlers
2124
+ })],
2125
+ ...identityKeys ? { identityKeys } : {}
2126
+ });
2127
+ console.log(`[vibes:workflows] Restate endpoint ready — service=${workflowServiceName(slug)} workflows=${workflowRegistry.size} identity=${identityKeys ? "verified" : "INSECURE"}`);
2128
+ }
2129
+ function makeRestateStepContext(ctx) {
2130
+ return {
2131
+ run(name, fn) {
2132
+ return ctx.run(name, fn);
2133
+ },
2134
+ sleep(name, ms) {
2135
+ return ctx.sleep(ms, name);
2136
+ }
2137
+ };
2138
+ }
2139
+ /**
2140
+ * Serve a request under /_vibes/workflow/*. Returns null when no endpoint
2141
+ * is mounted (no workflows, or dev mode — dev runs never go through
2142
+ * Restate). index.ts turns null into a 404.
2143
+ */
2144
+ function handleWorkflowRequest(req) {
2145
+ if (!restateFetchHandler) return null;
2146
+ return restateFetchHandler(req);
2147
+ }
2148
+ async function startViaAgent(name, payloadStr, opts) {
2149
+ const res = await fetch("http://localhost:8080/_workflow/start", {
2150
+ method: "POST",
2151
+ headers: { "Content-Type": "application/json" },
2152
+ body: JSON.stringify({
2153
+ workflow: name,
2154
+ payload: JSON.parse(payloadStr),
2155
+ ...opts.id ? { idempotencyKey: opts.id } : {}
2156
+ })
2157
+ });
2158
+ if (!res.ok) {
2159
+ const text = await res.text().catch(() => "");
2160
+ throw new Error(`startWorkflow("${name}") agent ${res.status}: ${text.slice(0, 200)}`);
2161
+ }
2162
+ const body = await res.json();
2163
+ if (!body.runId) throw new Error(`startWorkflow("${name}"): agent returned no runId`);
2164
+ return { runId: body.runId };
2165
+ }
2166
+ //#endregion
2167
+ //#region src/storage.ts
2168
+ function onUpload(handler) {
2169
+ return handler;
2170
+ }
2171
+ function onDelete(handler) {
2172
+ return handler;
2173
+ }
2174
+ let _vibesMode = null;
2175
+ function vibesMode() {
2176
+ if (_vibesMode) return _vibesMode;
2177
+ _vibesMode = ((typeof process !== "undefined" ? process.env?.VIBES_MODE : void 0) ?? "") === "dev" ? "dev" : "prod";
2178
+ return _vibesMode;
2179
+ }
2180
+ const KEY_RE = /^[a-zA-Z0-9._\-/]{1,256}$/;
2181
+ function validateKey(key) {
2182
+ if (!KEY_RE.test(key)) throw new Error(`storage: invalid key ${JSON.stringify(key)}`);
2183
+ if (key.includes("..") || key.startsWith("/") || key.includes("//")) throw new Error(`storage: key cannot contain traversal segments or empty parts`);
2184
+ }
2185
+ function resolveScope(scope, userId) {
2186
+ if ((scope ?? "user") === "user") {
2187
+ const resolved = userId ?? ctx.userId ?? "";
2188
+ if (!resolved) throw new Error(`storage: scope="user" requires an authed user — pass storage.upload(...) from a route that's behind VibesAuthGuard or set scope:"app" explicitly.`);
2189
+ return {
2190
+ scope: "user",
2191
+ userId: resolved
2192
+ };
2193
+ }
2194
+ return {
2195
+ scope: "app",
2196
+ userId: ""
2197
+ };
2198
+ }
2199
+ async function uploadUrl(opts) {
2200
+ validateKey(opts.key);
2201
+ const { scope, userId } = resolveScope(opts.scope, opts.userId);
2202
+ if (vibesMode() === "dev") return devUploadUrl(opts.key, scope, userId, opts.contentType);
2203
+ return prodPresign("put", opts.key, scope, userId, opts.contentType);
2204
+ }
2205
+ async function downloadUrl(key, opts = {}) {
2206
+ validateKey(key);
2207
+ const { scope, userId } = resolveScope(opts.scope, opts.userId);
2208
+ if (vibesMode() === "dev") return devDownloadUrl(key, scope, userId);
2209
+ const res = await prodPresign("get", key, scope, userId);
2210
+ return {
2211
+ url: res.url,
2212
+ method: "GET",
2213
+ key: res.key,
2214
+ expiresAt: res.expiresAt
2215
+ };
2216
+ }
2217
+ async function list(opts = {}) {
2218
+ const { scope, userId } = resolveScope(opts.scope, opts.userId);
2219
+ if (vibesMode() === "dev") return devList(scope, userId, opts.prefix ?? "");
2220
+ return prodList(scope, userId, opts.prefix ?? "", opts.limit ?? 200);
2221
+ }
2222
+ async function del(key, opts = {}) {
2223
+ validateKey(key);
2224
+ const { scope, userId } = resolveScope(opts.scope, opts.userId);
2225
+ if (vibesMode() === "dev") return devDelete(key, scope, userId);
2226
+ return prodDelete(key, scope, userId);
2227
+ }
2228
+ const storage = {
2229
+ uploadUrl,
2230
+ downloadUrl,
2231
+ list,
2232
+ delete: del,
2233
+ onUpload,
2234
+ onDelete
2235
+ };
2236
+ const AGENT_BASE$1 = "http://localhost:8080";
2237
+ async function prodPresign(action, key, scope, userId, contentType) {
2238
+ const res = await fetch(`${AGENT_BASE$1}/_storage/presign`, {
2239
+ method: "POST",
2240
+ headers: { "Content-Type": "application/json" },
2241
+ body: JSON.stringify({
2242
+ action,
2243
+ key,
2244
+ scope,
2245
+ userId,
2246
+ contentType
2247
+ })
2248
+ });
2249
+ if (!res.ok) {
2250
+ const text = await res.text().catch(() => "");
2251
+ throw new Error(`storage.${action === "put" ? "uploadUrl" : "downloadUrl"}(${JSON.stringify(key)}) agent ${res.status}: ${text.slice(0, 200)}`);
2252
+ }
2253
+ const body = await res.json();
2254
+ return {
2255
+ url: body.url,
2256
+ method: action === "put" ? "PUT" : "GET",
2257
+ key: body.key,
2258
+ expiresAt: body.expiresAt,
2259
+ maxBytes: body.maxBytes ?? 25 * 1024 * 1024
2260
+ };
2261
+ }
2262
+ async function prodList(scope, userId, prefix, limit) {
2263
+ const res = await fetch(`${AGENT_BASE$1}/_storage/list`, {
2264
+ method: "POST",
2265
+ headers: { "Content-Type": "application/json" },
2266
+ body: JSON.stringify({
2267
+ scope,
2268
+ userId,
2269
+ prefix,
2270
+ limit
2271
+ })
2272
+ });
2273
+ if (!res.ok) {
2274
+ const text = await res.text().catch(() => "");
2275
+ throw new Error(`storage.list agent ${res.status}: ${text.slice(0, 200)}`);
2276
+ }
2277
+ return (await res.json()).map((o) => ({
2278
+ key: o.key,
2279
+ relPath: o.relPath,
2280
+ size: o.size,
2281
+ lastModified: o.lastModified,
2282
+ contentType: o.contentType
2283
+ }));
2284
+ }
2285
+ async function prodDelete(key, scope, userId) {
2286
+ const res = await fetch(`${AGENT_BASE$1}/_storage/delete`, {
2287
+ method: "POST",
2288
+ headers: { "Content-Type": "application/json" },
2289
+ body: JSON.stringify({
2290
+ key,
2291
+ scope,
2292
+ userId
2293
+ })
2294
+ });
2295
+ if (!res.ok && res.status !== 204) {
2296
+ const text = await res.text().catch(() => "");
2297
+ throw new Error(`storage.delete agent ${res.status}: ${text.slice(0, 200)}`);
2298
+ }
2299
+ }
2300
+ function devRoot() {
2301
+ return path.resolve(process.cwd(), ".vibes", "storage");
2302
+ }
2303
+ function devPath(scope, userId, key) {
2304
+ const base = scope === "user" ? path.join("users", userId) : "app";
2305
+ return path.join(devRoot(), base, key);
2306
+ }
2307
+ function devRelative(scope, userId, key) {
2308
+ return scope === "user" ? `users/${userId}/${key}` : `app/${key}`;
2309
+ }
2310
+ const DEV_TOKEN_SECRET = process.env.VIBES_DEV_STORAGE_SECRET ?? "vibes-dev-storage";
2311
+ function devSignToken(rel, action, expiresAt) {
2312
+ const h = crypto$1.createHmac("sha256", DEV_TOKEN_SECRET);
2313
+ h.update(`${rel}|${action}|${expiresAt}`);
2314
+ return `${expiresAt}.${h.digest("hex").slice(0, 16)}`;
2315
+ }
2316
+ /**
2317
+ * Verify a dev-storage signed token. Exported so the vite-plugin middleware
2318
+ * can call into it without duplicating crypto. (Internal use; not part of the
2319
+ * @omg-dev/server public API.)
2320
+ */
2321
+ function _verifyDevStorageToken(rel, action, token) {
2322
+ const [expStr, sig] = token.split(".");
2323
+ if (!expStr || !sig) return false;
2324
+ const exp = Number(expStr);
2325
+ if (!Number.isFinite(exp) || exp < Date.now()) return false;
2326
+ return devSignToken(rel, action, exp).split(".")[1] === sig;
2327
+ }
2328
+ /**
2329
+ * Write a file into the dev storage tree under the given namespace. Exported
2330
+ * so the vite-plugin middleware can perform the PUT after verifying the
2331
+ * token. Creates parent directories as needed.
2332
+ */
2333
+ function _devStorageWrite(scope, userId, key, data) {
2334
+ const dst = devPath(scope, userId, key);
2335
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
2336
+ fs.writeFileSync(dst, data);
2337
+ }
2338
+ /**
2339
+ * Resolve a dev storage file to its absolute filesystem path so the plugin
2340
+ * middleware can stream the GET. Returns null if the file does not exist.
2341
+ */
2342
+ function _devStorageRead(scope, userId, key) {
2343
+ const src = devPath(scope, userId, key);
2344
+ if (!fs.existsSync(src)) return null;
2345
+ return src;
2346
+ }
2347
+ function devUploadUrl(key, scope, userId, contentType) {
2348
+ const rel = devRelative(scope, userId, key);
2349
+ const expiresAt = Date.now() + 300 * 1e3;
2350
+ return {
2351
+ url: `/_vibes_storage/${rel}?t=${devSignToken(rel, "put", expiresAt)}${contentType ? `&ct=${encodeURIComponent(contentType)}` : ""}`,
2352
+ method: "PUT",
2353
+ key: rel,
2354
+ expiresAt: new Date(expiresAt).toISOString(),
2355
+ maxBytes: 25 * 1024 * 1024
2356
+ };
2357
+ }
2358
+ function devDownloadUrl(key, scope, userId) {
2359
+ const rel = devRelative(scope, userId, key);
2360
+ const expiresAt = Date.now() + 3600 * 1e3;
2361
+ return {
2362
+ url: `/_vibes_storage/${rel}?t=${devSignToken(rel, "get", expiresAt)}`,
2363
+ method: "GET",
2364
+ key: rel,
2365
+ expiresAt: new Date(expiresAt).toISOString()
2366
+ };
2367
+ }
2368
+ function devList(scope, userId, prefix) {
2369
+ const root = devPath(scope, userId, prefix || ".");
2370
+ if (!fs.existsSync(root)) return [];
2371
+ const out = [];
2372
+ function walk(dir) {
2373
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
2374
+ const full = path.join(dir, ent.name);
2375
+ if (ent.isDirectory()) {
2376
+ walk(full);
2377
+ continue;
2378
+ }
2379
+ if (!ent.isFile()) continue;
2380
+ const st = fs.statSync(full);
2381
+ const base = devPath(scope, userId, "");
2382
+ const rel = path.relative(base, full).replaceAll(path.sep, "/");
2383
+ out.push({
2384
+ key: rel,
2385
+ relPath: rel,
2386
+ size: st.size,
2387
+ lastModified: st.mtime.toISOString()
2388
+ });
2389
+ }
2390
+ }
2391
+ walk(root);
2392
+ return out;
2393
+ }
2394
+ function devDelete(key, scope, userId) {
2395
+ const src = devPath(scope, userId, key);
2396
+ if (fs.existsSync(src)) fs.rmSync(src);
2397
+ }
2398
+ //#endregion
2399
+ //#region src/notifications.ts
2400
+ const NOTIFICATIONS_TABLE = "vibesNotifications";
2401
+ const PREFERENCES_TABLE = "vibesNotificationPreferences";
2402
+ const SUBSCRIPTIONS_TABLE = "vibesPushSubscriptions";
2403
+ const DELIVERIES_TABLE = "vibesNotificationDeliveries";
2404
+ const notificationSystemCollectionNames = new Set([
2405
+ NOTIFICATIONS_TABLE,
2406
+ PREFERENCES_TABLE,
2407
+ SUBSCRIPTIONS_TABLE,
2408
+ DELIVERIES_TABLE
2409
+ ]);
2410
+ const notificationSystemSchema = defineSchema({ collections: {
2411
+ [NOTIFICATIONS_TABLE]: collection({ fields: {
2412
+ appId: fields.string(),
2413
+ userId: fields.string(),
2414
+ kind: fields.string(),
2415
+ title: fields.string(),
2416
+ body: fields.string(),
2417
+ url: fields.string(),
2418
+ status: fields.enum([
2419
+ "unread",
2420
+ "read",
2421
+ "archived"
2422
+ ]),
2423
+ priority: fields.enum([
2424
+ "low",
2425
+ "normal",
2426
+ "high"
2427
+ ]),
2428
+ sourceType: fields.string(),
2429
+ sourceId: fields.string(),
2430
+ dedupeKey: fields.string(),
2431
+ dataJson: fields.string(),
2432
+ readAt: fields.number(),
2433
+ createdAt: fields.number()
2434
+ } }).scoped("user").index("_owner", "status", "createdAt").index("_owner", "createdAt").index("appId", "userId", "createdAt"),
2435
+ [PREFERENCES_TABLE]: collection({ fields: {
2436
+ appId: fields.string(),
2437
+ userId: fields.string(),
2438
+ kind: fields.string(),
2439
+ inApp: fields.boolean(),
2440
+ push: fields.boolean(),
2441
+ createdAt: fields.number(),
2442
+ updatedAt: fields.number()
2443
+ } }).scoped("user").index("_owner", "appId", "kind"),
2444
+ [SUBSCRIPTIONS_TABLE]: collection({ fields: {
2445
+ appId: fields.string(),
2446
+ userId: fields.string(),
2447
+ endpoint: fields.string(),
2448
+ p256dh: fields.string(),
2449
+ auth: fields.string(),
2450
+ userAgent: fields.string(),
2451
+ disabledAt: fields.number(),
2452
+ lastSeenAt: fields.number(),
2453
+ createdAt: fields.number(),
2454
+ updatedAt: fields.number()
2455
+ } }).scoped("user").index("_owner", "appId").index("endpoint"),
2456
+ [DELIVERIES_TABLE]: collection({ fields: {
2457
+ notificationId: fields.ref(NOTIFICATIONS_TABLE),
2458
+ appId: fields.string(),
2459
+ userId: fields.string(),
2460
+ channel: fields.enum(["in_app", "push"]),
2461
+ status: fields.enum([
2462
+ "pending",
2463
+ "sent",
2464
+ "failed",
2465
+ "skipped"
2466
+ ]),
2467
+ attempts: fields.number(),
2468
+ error: fields.string(),
2469
+ createdAt: fields.number(),
2470
+ updatedAt: fields.number()
2471
+ } }).scoped("user").index("_owner", "createdAt").index("notificationId", "channel")
2472
+ } });
2473
+ function withNotificationSystemSchema(schema) {
2474
+ if (!schema) return notificationSystemSchema;
2475
+ return { collections: {
2476
+ ...schema.collections,
2477
+ ...notificationSystemSchema.collections
2478
+ } };
2479
+ }
2480
+ function ensureNotificationIndexes(db) {
2481
+ db.raw().exec(`CREATE UNIQUE INDEX IF NOT EXISTS uniq_vibes_notifications_owner_dedupe
2482
+ ON ${NOTIFICATIONS_TABLE} (_owner, appId, dedupeKey)
2483
+ WHERE dedupeKey != ''`);
2484
+ db.raw().exec(`CREATE UNIQUE INDEX IF NOT EXISTS uniq_vibes_push_subscriptions_owner_endpoint
2485
+ ON ${SUBSCRIPTIONS_TABLE} (_owner, appId, endpoint)`);
2486
+ db.raw().exec(`CREATE UNIQUE INDEX IF NOT EXISTS uniq_vibes_notification_prefs_owner_kind
2487
+ ON ${PREFERENCES_TABLE} (_owner, appId, kind)`);
2488
+ }
2489
+ function dbOrThrow() {
2490
+ const db = getDbInstance();
2491
+ if (!db) throw new Error("[vibes:notifications] db not initialized");
2492
+ return db;
2493
+ }
2494
+ function nowIso() {
2495
+ return (/* @__PURE__ */ new Date()).toISOString();
2496
+ }
2497
+ function insertRaw(db, table, data) {
2498
+ const id = typeof data.id === "string" ? data.id : crypto.randomUUID();
2499
+ const iso = nowIso();
2500
+ const record = {
2501
+ ...data,
2502
+ id,
2503
+ created_at: iso,
2504
+ updated_at: iso
2505
+ };
2506
+ const cols = Object.keys(record);
2507
+ const placeholders = cols.map(() => "?").join(", ");
2508
+ db.raw().prepare(`INSERT INTO ${table} (${cols.join(", ")}) VALUES (${placeholders})`).run(...Object.values(record));
2509
+ invalidate(table);
2510
+ notifyRowChange(table, "insert", null, record);
2511
+ return record;
2512
+ }
2513
+ function patchRaw(db, table, id, patch) {
2514
+ const before = db.raw().prepare(`SELECT * FROM ${table} WHERE id = ? LIMIT 1`).get(id);
2515
+ if (!before) return null;
2516
+ const write = {
2517
+ ...patch,
2518
+ updated_at: nowIso()
2519
+ };
2520
+ const cols = Object.keys(write);
2521
+ db.raw().prepare(`UPDATE ${table} SET ${cols.map((c) => `${c} = ?`).join(", ")} WHERE id = ?`).run(...cols.map((c) => write[c]), id);
2522
+ const after = {
2523
+ ...before,
2524
+ ...write
2525
+ };
2526
+ invalidate(table);
2527
+ notifyRowChange(table, "update", before, after);
2528
+ return after;
2529
+ }
2530
+ function getPreference(db, userId, appId, kind) {
2531
+ return db.raw().prepare(`SELECT inApp, push FROM ${PREFERENCES_TABLE}
2532
+ WHERE _owner = ? AND appId = ? AND kind = ?
2533
+ LIMIT 1`).get(userId, appId, kind);
2534
+ }
2535
+ function desiredChannels(db, input, userId, appId) {
2536
+ const pref = getPreference(db, userId, appId, input.kind);
2537
+ return {
2538
+ inApp: input.channels?.inApp ?? (pref ? Boolean(pref.inApp) : true),
2539
+ push: input.channels?.push ?? (pref ? Boolean(pref.push) : true)
2540
+ };
2541
+ }
2542
+ async function configureWebPush() {
2543
+ const publicKey = process.env.VIBES_VAPID_PUBLIC_KEY;
2544
+ const privateKey = process.env.VIBES_VAPID_PRIVATE_KEY;
2545
+ if (!publicKey || !privateKey) return {
2546
+ sender: null,
2547
+ error: "VAPID keys not configured",
2548
+ skipped: true
2549
+ };
2550
+ try {
2551
+ const mod = await new Function("specifier", "return import(specifier)")("web-push");
2552
+ const sender = mod.default ?? mod;
2553
+ sender.setVapidDetails(process.env.VIBES_VAPID_SUBJECT || "mailto:support@omg.dev", publicKey, privateKey);
2554
+ return {
2555
+ sender,
2556
+ error: "",
2557
+ skipped: false
2558
+ };
2559
+ } catch (err) {
2560
+ return {
2561
+ sender: null,
2562
+ error: err instanceof Error ? err.message : String(err),
2563
+ skipped: false
2564
+ };
2565
+ }
2566
+ }
2567
+ async function fanoutPush(db, row) {
2568
+ const subscriptions = db.raw().prepare(`SELECT * FROM ${SUBSCRIPTIONS_TABLE}
2569
+ WHERE _owner = ? AND appId = ? AND disabledAt = 0`).all(row.userId, row.appId);
2570
+ if (subscriptions.length === 0) {
2571
+ insertRaw(db, DELIVERIES_TABLE, {
2572
+ _owner: row.userId,
2573
+ notificationId: row.id,
2574
+ appId: row.appId,
2575
+ userId: row.userId,
2576
+ channel: "push",
2577
+ status: "skipped",
2578
+ attempts: 0,
2579
+ error: "no push subscriptions",
2580
+ createdAt: Date.now(),
2581
+ updatedAt: Date.now()
2582
+ });
2583
+ return;
2584
+ }
2585
+ const webPush = await configureWebPush();
2586
+ for (const sub of subscriptions) {
2587
+ const delivery = insertRaw(db, DELIVERIES_TABLE, {
2588
+ _owner: row.userId,
2589
+ notificationId: row.id,
2590
+ appId: row.appId,
2591
+ userId: row.userId,
2592
+ channel: "push",
2593
+ status: webPush.sender ? "pending" : webPush.skipped ? "skipped" : "failed",
2594
+ attempts: 0,
2595
+ error: webPush.sender ? "" : webPush.error,
2596
+ createdAt: Date.now(),
2597
+ updatedAt: Date.now()
2598
+ });
2599
+ if (!webPush.sender) continue;
2600
+ const pushSub = {
2601
+ endpoint: String(sub.endpoint),
2602
+ keys: {
2603
+ p256dh: String(sub.p256dh),
2604
+ auth: String(sub.auth)
2605
+ }
2606
+ };
2607
+ try {
2608
+ await webPush.sender.sendNotification(pushSub, JSON.stringify({
2609
+ id: row.id,
2610
+ title: row.title,
2611
+ body: row.body,
2612
+ url: row.url,
2613
+ tag: row.dedupeKey || row.id,
2614
+ data: safeJson(row.dataJson)
2615
+ }));
2616
+ patchRaw(db, DELIVERIES_TABLE, String(delivery.id), {
2617
+ status: "sent",
2618
+ attempts: 1,
2619
+ error: "",
2620
+ updatedAt: Date.now()
2621
+ });
2622
+ } catch (err) {
2623
+ const statusCode = typeof err === "object" && err && "statusCode" in err ? Number(err.statusCode) : 0;
2624
+ if (statusCode === 404 || statusCode === 410) patchRaw(db, SUBSCRIPTIONS_TABLE, String(sub.id), {
2625
+ disabledAt: Date.now(),
2626
+ updatedAt: Date.now()
2627
+ });
2628
+ patchRaw(db, DELIVERIES_TABLE, String(delivery.id), {
2629
+ status: "failed",
2630
+ attempts: 1,
2631
+ error: err instanceof Error ? err.message : String(err),
2632
+ updatedAt: Date.now()
2633
+ });
2634
+ }
2635
+ }
2636
+ }
2637
+ function safeJson(raw) {
2638
+ if (!raw) return {};
2639
+ try {
2640
+ return JSON.parse(raw);
2641
+ } catch {
2642
+ return {};
2643
+ }
2644
+ }
2645
+ async function notify(input) {
2646
+ const db = dbOrThrow();
2647
+ const userId = input.userId ?? ctx.userId;
2648
+ if (!userId) return null;
2649
+ const appId = input.appId ?? ctx.appId ?? "app";
2650
+ const channels = desiredChannels(db, input, userId, appId);
2651
+ if (!channels.inApp && !channels.push) return null;
2652
+ const dedupeKey = input.dedupeKey ?? "";
2653
+ if (dedupeKey) {
2654
+ const existing = db.raw().prepare(`SELECT * FROM ${NOTIFICATIONS_TABLE}
2655
+ WHERE _owner = ? AND appId = ? AND dedupeKey = ?
2656
+ LIMIT 1`).get(userId, appId, dedupeKey);
2657
+ if (existing) return existing;
2658
+ }
2659
+ const now = Date.now();
2660
+ let inserted;
2661
+ try {
2662
+ inserted = insertRaw(db, NOTIFICATIONS_TABLE, {
2663
+ _owner: userId,
2664
+ appId,
2665
+ userId,
2666
+ kind: input.kind,
2667
+ title: input.title,
2668
+ body: input.body ?? "",
2669
+ url: input.url ?? "",
2670
+ status: "unread",
2671
+ priority: input.priority ?? "normal",
2672
+ sourceType: input.source?.type ?? "",
2673
+ sourceId: input.source?.id ?? "",
2674
+ dedupeKey,
2675
+ dataJson: input.data ? JSON.stringify(input.data) : "{}",
2676
+ readAt: 0,
2677
+ createdAt: now
2678
+ });
2679
+ } catch (err) {
2680
+ if (dedupeKey && err instanceof Error && /UNIQUE/i.test(err.message)) return db.raw().prepare(`SELECT * FROM ${NOTIFICATIONS_TABLE}
2681
+ WHERE _owner = ? AND appId = ? AND dedupeKey = ?
2682
+ LIMIT 1`).get(userId, appId, dedupeKey);
2683
+ throw err;
2684
+ }
2685
+ if (channels.inApp) insertRaw(db, DELIVERIES_TABLE, {
2686
+ _owner: userId,
2687
+ notificationId: inserted.id,
2688
+ appId,
2689
+ userId,
2690
+ channel: "in_app",
2691
+ status: "sent",
2692
+ attempts: 0,
2693
+ error: "",
2694
+ createdAt: now,
2695
+ updatedAt: now
2696
+ });
2697
+ if (channels.push) fanoutPush(db, inserted);
2698
+ return inserted;
2699
+ }
2700
+ async function readBody(req) {
2701
+ try {
2702
+ const body = await req.json();
2703
+ return body && typeof body === "object" ? body : {};
2704
+ } catch {
2705
+ return {};
2706
+ }
2707
+ }
2708
+ function requireUser() {
2709
+ if (!ctx.userId) return Response.json({ error: "Authentication required" }, { status: 401 });
2710
+ return ctx.userId;
2711
+ }
2712
+ async function notificationsConfigHandler() {
2713
+ return Response.json({ vapidPublicKey: process.env.VIBES_VAPID_PUBLIC_KEY ?? "" });
2714
+ }
2715
+ async function notificationsListHandler(req) {
2716
+ const userId = requireUser();
2717
+ if (userId instanceof Response) return userId;
2718
+ const db = dbOrThrow();
2719
+ const unreadOnly = new URL(req.url).searchParams.get("unread") === "1";
2720
+ const rows = db.raw().prepare(`SELECT * FROM ${NOTIFICATIONS_TABLE}
2721
+ WHERE _owner = ? ${unreadOnly ? "AND status = 'unread'" : ""}
2722
+ ORDER BY createdAt DESC
2723
+ LIMIT 100`).all(userId);
2724
+ return Response.json(rows);
2725
+ }
2726
+ async function notificationsUnreadCountHandler() {
2727
+ const userId = requireUser();
2728
+ if (userId instanceof Response) return userId;
2729
+ const row = dbOrThrow().raw().prepare(`SELECT COUNT(*) AS count FROM ${NOTIFICATIONS_TABLE} WHERE _owner = ? AND status = 'unread'`).get(userId);
2730
+ return Response.json({ count: row?.count ?? 0 });
2731
+ }
2732
+ async function notificationsSubscribeHandler(req) {
2733
+ const userId = requireUser();
2734
+ if (userId instanceof Response) return userId;
2735
+ const body = await readBody(req);
2736
+ const subscription = body.subscription && typeof body.subscription === "object" ? body.subscription : body;
2737
+ const endpoint = typeof subscription.endpoint === "string" ? subscription.endpoint : "";
2738
+ const keys = subscription.keys && typeof subscription.keys === "object" ? subscription.keys : {};
2739
+ const p256dh = typeof keys.p256dh === "string" ? keys.p256dh : "";
2740
+ const auth = typeof keys.auth === "string" ? keys.auth : "";
2741
+ if (!endpoint || !p256dh || !auth) return Response.json({ error: "invalid push subscription" }, { status: 400 });
2742
+ const db = dbOrThrow();
2743
+ const appId = typeof body.appId === "string" && body.appId ? body.appId : ctx.appId ?? "app";
2744
+ const existing = db.raw().prepare(`SELECT id FROM ${SUBSCRIPTIONS_TABLE}
2745
+ WHERE _owner = ? AND appId = ? AND endpoint = ?
2746
+ LIMIT 1`).get(userId, appId, endpoint);
2747
+ const patch = {
2748
+ p256dh,
2749
+ auth,
2750
+ userAgent: req.headers.get("user-agent") ?? "",
2751
+ disabledAt: 0,
2752
+ lastSeenAt: Date.now(),
2753
+ updatedAt: Date.now()
2754
+ };
2755
+ if (existing) {
2756
+ patchRaw(db, SUBSCRIPTIONS_TABLE, existing.id, patch);
2757
+ return Response.json({
2758
+ ok: true,
2759
+ id: existing.id
2760
+ });
2761
+ }
2762
+ const row = insertRaw(db, SUBSCRIPTIONS_TABLE, {
2763
+ _owner: userId,
2764
+ appId,
2765
+ userId,
2766
+ endpoint,
2767
+ ...patch,
2768
+ createdAt: Date.now()
2769
+ });
2770
+ return Response.json({
2771
+ ok: true,
2772
+ id: row.id
2773
+ });
2774
+ }
2775
+ async function notificationsUnsubscribeHandler(req) {
2776
+ const userId = requireUser();
2777
+ if (userId instanceof Response) return userId;
2778
+ const body = await readBody(req);
2779
+ const endpoint = typeof body.endpoint === "string" ? body.endpoint : "";
2780
+ if (!endpoint) return Response.json({ error: "endpoint required" }, { status: 400 });
2781
+ const db = dbOrThrow();
2782
+ const appId = typeof body.appId === "string" && body.appId ? body.appId : ctx.appId ?? "app";
2783
+ const rows = db.raw().prepare(`SELECT id FROM ${SUBSCRIPTIONS_TABLE}
2784
+ WHERE _owner = ? AND appId = ? AND endpoint = ?`).all(userId, appId, endpoint);
2785
+ for (const row of rows) patchRaw(db, SUBSCRIPTIONS_TABLE, row.id, {
2786
+ disabledAt: Date.now(),
2787
+ updatedAt: Date.now()
2788
+ });
2789
+ return Response.json({ ok: true });
2790
+ }
2791
+ async function notificationsReadHandler(req) {
2792
+ const userId = requireUser();
2793
+ if (userId instanceof Response) return userId;
2794
+ const body = await readBody(req);
2795
+ const db = dbOrThrow();
2796
+ const now = Date.now();
2797
+ if (Array.isArray(body.ids)) {
2798
+ for (const id of body.ids) {
2799
+ if (typeof id !== "string") continue;
2800
+ if (db.raw().prepare(`SELECT * FROM ${NOTIFICATIONS_TABLE} WHERE id = ? AND _owner = ? LIMIT 1`).get(id, userId)) patchRaw(db, NOTIFICATIONS_TABLE, id, {
2801
+ status: "read",
2802
+ readAt: now
2803
+ });
2804
+ }
2805
+ return Response.json({ ok: true });
2806
+ }
2807
+ if (body.all === true) {
2808
+ const rows = db.raw().prepare(`SELECT id FROM ${NOTIFICATIONS_TABLE} WHERE _owner = ? AND status = 'unread'`).all(userId);
2809
+ for (const row of rows) patchRaw(db, NOTIFICATIONS_TABLE, row.id, {
2810
+ status: "read",
2811
+ readAt: now
2812
+ });
2813
+ return Response.json({
2814
+ ok: true,
2815
+ count: rows.length
2816
+ });
2817
+ }
2818
+ return Response.json({ error: "ids or all required" }, { status: 400 });
2819
+ }
2820
+ async function notificationsCreateHandler(req) {
2821
+ const userId = requireUser();
2822
+ if (userId instanceof Response) return userId;
2823
+ const body = await readBody(req);
2824
+ const row = await notify({
2825
+ userId,
2826
+ appId: typeof body.appId === "string" ? body.appId : void 0,
2827
+ kind: typeof body.kind === "string" ? body.kind : "app_custom",
2828
+ title: typeof body.title === "string" ? body.title : "Notification",
2829
+ body: typeof body.body === "string" ? body.body : "",
2830
+ url: typeof body.url === "string" ? body.url : "",
2831
+ priority: body.priority === "low" || body.priority === "high" ? body.priority : "normal",
2832
+ source: {
2833
+ type: typeof body.sourceType === "string" ? body.sourceType : "",
2834
+ id: typeof body.sourceId === "string" ? body.sourceId : ""
2835
+ },
2836
+ dedupeKey: typeof body.dedupeKey === "string" ? body.dedupeKey : void 0,
2837
+ data: body.data && typeof body.data === "object" ? body.data : void 0
2838
+ });
2839
+ return Response.json(row);
2840
+ }
2841
+ function notificationServiceWorkerHandler() {
2842
+ return new Response(`self.addEventListener("push", (event) => {
2843
+ let payload = {};
2844
+ try { payload = event.data ? event.data.json() : {}; } catch {}
2845
+ const title = payload.title || "Notification";
2846
+ const options = {
2847
+ body: payload.body || "",
2848
+ tag: payload.tag || payload.id || undefined,
2849
+ data: { url: payload.url || "/", id: payload.id || "" },
2850
+ icon: "/icons/pwa-192x192.png",
2851
+ badge: "/icons/pwa-192x192.png",
2852
+ };
2853
+ event.waitUntil(self.registration.showNotification(title, options));
2854
+ });
2855
+
2856
+ self.addEventListener("notificationclick", (event) => {
2857
+ event.notification.close();
2858
+ const url = (event.notification.data && event.notification.data.url) || "/";
2859
+ event.waitUntil((async () => {
2860
+ const allClients = await clients.matchAll({ type: "window", includeUncontrolled: true });
2861
+ for (const client of allClients) {
2862
+ if ("focus" in client) {
2863
+ client.focus();
2864
+ if ("navigate" in client) return client.navigate(url);
2865
+ return;
2866
+ }
2867
+ }
2868
+ if (clients.openWindow) return clients.openWindow(url);
2869
+ })());
2870
+ });`, { headers: {
2871
+ "content-type": "text/javascript; charset=utf-8",
2872
+ "cache-control": "no-cache",
2873
+ "service-worker-allowed": "/__vibes_push/"
2874
+ } });
2875
+ }
2876
+ //#endregion
2877
+ //#region src/billing.ts
2878
+ const AGENT_BASE = "http://localhost:8080";
2879
+ const MICROS_PER_UNIT = 1e6;
2880
+ /**
2881
+ * Dollars (or whole credit units) → integer micro-units, for the amount
2882
+ * arguments of `track`/`grant`. `usd(5)` → 5_000_000. Re-exported from the
2883
+ * same definition @omg-dev/billing uses so app authors can write `usd(5)`.
2884
+ */
2885
+ function usd(amount) {
2886
+ return Math.round(amount * MICROS_PER_UNIT);
2887
+ }
2888
+ async function agentPost(path, body, label) {
2889
+ const res = await fetch(`${AGENT_BASE}${path}`, {
2890
+ method: "POST",
2891
+ headers: { "Content-Type": "application/json" },
2892
+ body: JSON.stringify(body)
2893
+ });
2894
+ if (!res.ok) {
2895
+ const text = await res.text().catch(() => "");
2896
+ throw new Error(`billing.${label} agent ${res.status}: ${text.slice(0, 200)}`);
2897
+ }
2898
+ return await res.json();
2899
+ }
2900
+ async function agentGet(path, label) {
2901
+ const res = await fetch(`${AGENT_BASE}${path}`, {
2902
+ method: "GET",
2903
+ headers: { Accept: "application/json" }
2904
+ });
2905
+ if (!res.ok) {
2906
+ const text = await res.text().catch(() => "");
2907
+ throw new Error(`billing.${label} agent ${res.status}: ${text.slice(0, 200)}`);
2908
+ }
2909
+ return await res.json();
2910
+ }
2911
+ const billing = {
2912
+ /**
2913
+ * Create (or fetch) the billing customer for one of this app's end-users.
2914
+ * Idempotent on `externalRef` within the app — call it on signup. `plan`
2915
+ * optionally pins the customer onto a declared plan at creation.
2916
+ */
2917
+ async ensureCustomer(externalRef, plan) {
2918
+ return agentPost("/_billing/customers", {
2919
+ externalRef,
2920
+ plan
2921
+ }, "ensureCustomer");
2922
+ },
2923
+ /**
2924
+ * Check whether `externalRef` is entitled to use `feature` right now. Read
2925
+ * the `allow` flag before serving the feature; `reason` explains a denial.
2926
+ */
2927
+ async check(feature, externalRef) {
2928
+ return agentPost("/_billing/check", {
2929
+ feature,
2930
+ externalRef
2931
+ }, "check");
2932
+ },
2933
+ /**
2934
+ * Record `amountMicros` of `feature` usage against `externalRef` after the
2935
+ * feature was served. `idempotencyKey` dedupes retries — reuse the same key
2936
+ * for the same logical unit of work. Returns the new balance.
2937
+ */
2938
+ async track(feature, externalRef, amountMicros, idempotencyKey, reason) {
2939
+ return agentPost("/_billing/track", {
2940
+ feature,
2941
+ externalRef,
2942
+ amountMicros,
2943
+ idempotencyKey,
2944
+ reason
2945
+ }, "track");
2946
+ },
2947
+ /**
2948
+ * Add `amountMicros` of `feature` credit to `externalRef` (a top-up, refund,
2949
+ * or promo grant). `opts.idempotencyKey` dedupes retries; `opts.grantKey`
2950
+ * dedupes a one-time grant (e.g. a signup bonus) across the customer's
2951
+ * lifetime. Returns the new balance.
2952
+ */
2953
+ async grant(externalRef, feature, amountMicros, opts) {
2954
+ return agentPost("/_billing/grant", {
2955
+ externalRef,
2956
+ feature,
2957
+ amountMicros,
2958
+ idempotencyKey: opts.idempotencyKey,
2959
+ grantKey: opts.grantKey,
2960
+ source: opts.source,
2961
+ reason: opts.reason
2962
+ }, "grant");
2963
+ },
2964
+ /**
2965
+ * Read the current `feature` balance for `externalRef` without mutating it.
2966
+ * `exists` is false if the customer was never created.
2967
+ */
2968
+ async balance(externalRef, feature) {
2969
+ return agentGet(`/_billing/balance?${new URLSearchParams({
2970
+ externalRef,
2971
+ feature
2972
+ }).toString()}`, "balance");
2973
+ },
2974
+ /**
2975
+ * Read `externalRef`'s current plan + newest subscription record without
2976
+ * mutating anything. Use it to render entitlement UI — pass `plan` to
2977
+ * <PricingTable currentPlan> or gate paid features. An end-user who never
2978
+ * subscribed reads back the catalog default plan with `subscription: null`.
2979
+ */
2980
+ async subscription(externalRef) {
2981
+ return agentGet(`/_billing/subscription?${new URLSearchParams({ externalRef }).toString()}`, "subscription");
2982
+ },
2983
+ /**
2984
+ * Mint a hosted checkout for one of this app's end-users to buy `plan`, in
2985
+ * the APP OWNER's connected Polar org. Returns the checkout URL to redirect
2986
+ * to. The amount + product are resolved server-side from the active
2987
+ * plan-version (the client cannot set the price). On successful payment the
2988
+ * Polar webhook credits/upgrades `externalRef` automatically.
2989
+ *
2990
+ * export const buyPro: Route = async (ctx) => {
2991
+ * const { checkoutUrl } = await billing.checkout({
2992
+ * plan: "pro",
2993
+ * externalRef: ctx.user.id,
2994
+ * })
2995
+ * return Response.redirect(checkoutUrl, 303)
2996
+ * }
2997
+ */
2998
+ async checkout(opts) {
2999
+ return agentPost("/_billing/checkout", {
3000
+ plan: opts.plan,
3001
+ externalRef: opts.externalRef,
3002
+ successUrl: opts.successUrl,
3003
+ feature: opts.feature,
3004
+ amountMicros: opts.amountMicros
3005
+ }, "checkout");
3006
+ }
3007
+ };
3008
+ //#endregion
3009
+ //#region src/index.ts
3010
+ async function storagePresignHandler(req) {
3011
+ let body;
3012
+ try {
3013
+ body = await req.json();
3014
+ } catch {
3015
+ return Response.json({ error: "invalid JSON body" }, { status: 400 });
3016
+ }
3017
+ const action = body.action ?? "put";
3018
+ if (action !== "put" && action !== "get") return Response.json({ error: `unknown action: ${action}` }, { status: 400 });
3019
+ if (!body.key || typeof body.key !== "string") return Response.json({ error: "key required" }, { status: 400 });
3020
+ const scope = body.scope ?? "user";
3021
+ if (scope !== "user" && scope !== "app") return Response.json({ error: `unknown scope: ${scope}` }, { status: 400 });
3022
+ if (scope === "user" && !ctx.userId) return Response.json({ error: "auth required for scope:user — sign in or pass scope:'app'." }, { status: 401 });
3023
+ try {
3024
+ if (action === "put") {
3025
+ const out = await storage.uploadUrl({
3026
+ key: body.key,
3027
+ contentType: body.contentType,
3028
+ scope
3029
+ });
3030
+ return Response.json(out);
3031
+ }
3032
+ const out = await storage.downloadUrl(body.key, { scope });
3033
+ return Response.json(out);
3034
+ } catch (err) {
3035
+ const msg = err instanceof Error ? err.message : String(err);
3036
+ return Response.json({ error: msg }, { status: 500 });
3037
+ }
3038
+ }
3039
+ async function storageNotifyHandler(req) {
3040
+ let body;
3041
+ try {
3042
+ body = await req.json();
3043
+ } catch {
3044
+ return Response.json({ error: "invalid JSON body" }, { status: 400 });
3045
+ }
3046
+ const action = body.action ?? "upload";
3047
+ if (action !== "upload" && action !== "delete") return Response.json({ error: `unknown action: ${action}` }, { status: 400 });
3048
+ if (!body.key || typeof body.key !== "string") return Response.json({ error: "key required" }, { status: 400 });
3049
+ const scope = body.scope ?? "user";
3050
+ const kind = action === "upload" ? "storage:upload" : "storage:delete";
3051
+ try {
3052
+ const fired = await dispatchStorageEvent(kind, {
3053
+ key: body.key,
3054
+ size: body.size,
3055
+ contentType: body.contentType,
3056
+ userId: ctx.userId ?? null,
3057
+ scope
3058
+ });
3059
+ return Response.json({
3060
+ ok: true,
3061
+ handlersFired: fired
3062
+ });
3063
+ } catch (err) {
3064
+ const msg = err instanceof Error ? err.message : String(err);
3065
+ return Response.json({ error: msg }, { status: 500 });
3066
+ }
3067
+ }
3068
+ async function createVibesServer(opts) {
3069
+ const { root, db: dbPath } = opts;
3070
+ let currentSchema = withNotificationSystemSchema(opts.schema);
3071
+ function loadExplicitRoutes() {
3072
+ if (opts.routes) return loadRoutes(opts.routes);
3073
+ const routesJsonPath = path.join(root, ".vibes", "routes.json");
3074
+ if (!fs.existsSync(routesJsonPath)) return [];
3075
+ return loadRoutes(JSON.parse(fs.readFileSync(routesJsonPath, "utf-8")));
3076
+ }
3077
+ let explicitRoutes = loadExplicitRoutes();
3078
+ let routes = [];
3079
+ let dbInstance = null;
3080
+ function ensureDb() {
3081
+ if (dbInstance) return dbInstance;
3082
+ dbInstance = openDb(path.isAbsolute(dbPath) ? dbPath : path.join(root, dbPath));
3083
+ setDbInstance(dbInstance);
3084
+ return dbInstance;
3085
+ }
3086
+ function hasCollections(s) {
3087
+ return !!s?.collections && Object.keys(s.collections).length > 0;
3088
+ }
3089
+ const storageBuiltinRoutes = [{
3090
+ method: "POST",
3091
+ path: "/api/_storage/presign",
3092
+ module: "@omg-dev/server/storage-presign",
3093
+ handler: "inline",
3094
+ inlineHandler: storagePresignHandler
3095
+ }, {
3096
+ method: "POST",
3097
+ path: "/api/_storage/notify",
3098
+ module: "@omg-dev/server/storage-notify",
3099
+ handler: "inline",
3100
+ inlineHandler: storageNotifyHandler
3101
+ }];
3102
+ const notificationBuiltinRoutes = [
3103
+ {
3104
+ method: "GET",
3105
+ path: "/api/_notifications/config",
3106
+ module: "@omg-dev/server/notifications-config",
3107
+ handler: "inline",
3108
+ inlineHandler: notificationsConfigHandler
3109
+ },
3110
+ {
3111
+ method: "GET",
3112
+ path: "/api/_notifications/list",
3113
+ module: "@omg-dev/server/notifications-list",
3114
+ handler: "inline",
3115
+ inlineHandler: notificationsListHandler
3116
+ },
3117
+ {
3118
+ method: "GET",
3119
+ path: "/api/_notifications/unread-count",
3120
+ module: "@omg-dev/server/notifications-unread-count",
3121
+ handler: "inline",
3122
+ inlineHandler: notificationsUnreadCountHandler
3123
+ },
3124
+ {
3125
+ method: "POST",
3126
+ path: "/api/_notifications/subscribe",
3127
+ module: "@omg-dev/server/notifications-subscribe",
3128
+ handler: "inline",
3129
+ inlineHandler: notificationsSubscribeHandler
3130
+ },
3131
+ {
3132
+ method: "POST",
3133
+ path: "/api/_notifications/unsubscribe",
3134
+ module: "@omg-dev/server/notifications-unsubscribe",
3135
+ handler: "inline",
3136
+ inlineHandler: notificationsUnsubscribeHandler
3137
+ },
3138
+ {
3139
+ method: "POST",
3140
+ path: "/api/_notifications/read",
3141
+ module: "@omg-dev/server/notifications-read",
3142
+ handler: "inline",
3143
+ inlineHandler: notificationsReadHandler
3144
+ },
3145
+ {
3146
+ method: "POST",
3147
+ path: "/api/_notifications/create",
3148
+ module: "@omg-dev/server/notifications-create",
3149
+ handler: "inline",
3150
+ inlineHandler: notificationsCreateHandler
3151
+ }
3152
+ ];
3153
+ function rebuildRoutes() {
3154
+ const explicit = new Set(explicitRoutes.map((r) => `${r.method} ${r.path}`));
3155
+ const builtinKeys = new Set([...storageBuiltinRoutes.map((r) => `${r.method} ${r.path}`), ...notificationBuiltinRoutes.map((r) => `${r.method} ${r.path}`)]);
3156
+ const builtinStorage = storageBuiltinRoutes.filter((r) => !explicit.has(`${r.method} ${r.path}`));
3157
+ const builtinNotifications = notificationBuiltinRoutes.filter((r) => !explicit.has(`${r.method} ${r.path}`));
3158
+ if (hasCollections(currentSchema) && opts.autoCrud !== false) routes = [
3159
+ ...buildAutoCrudRoutes(currentSchema).filter((r) => {
3160
+ if (explicit.has(`${r.method} ${r.path}`) || builtinKeys.has(`${r.method} ${r.path}`)) return false;
3161
+ const collection = r.path.split("/")[2];
3162
+ return !notificationSystemCollectionNames.has(collection);
3163
+ }),
3164
+ ...builtinStorage,
3165
+ ...builtinNotifications,
3166
+ ...explicitRoutes
3167
+ ];
3168
+ else routes = [
3169
+ ...builtinStorage,
3170
+ ...builtinNotifications,
3171
+ ...explicitRoutes
3172
+ ];
3173
+ }
3174
+ if (hasCollections(currentSchema)) {
3175
+ const db = ensureDb();
3176
+ registerScopes(currentSchema);
3177
+ if (opts.migrate !== false) {
3178
+ migrate(db.raw(), currentSchema);
3179
+ ensureNotificationIndexes(db);
3180
+ }
3181
+ }
3182
+ setSubscriptionSchema(currentSchema ?? null);
3183
+ rebuildRoutes();
3184
+ if (opts.cron) setCronDriver(opts.cron);
3185
+ const triggerEntries = opts.triggers && opts.triggers.length > 0 ? opts.triggers : await loadTriggersFromFile(root);
3186
+ if (triggerEntries.length > 0) await registerTriggers(triggerEntries);
3187
+ const workflowEntries = opts.workflows && opts.workflows.length > 0 ? opts.workflows : await loadWorkflowsFromFile(root);
3188
+ if (workflowEntries.length > 0) {
3189
+ await registerWorkflows(workflowEntries);
3190
+ await buildWorkflowEndpoint();
3191
+ }
3192
+ const authMW = typeof opts.auth === "function" ? opts.auth : opts.auth ? createAuthMiddleware$1(opts.auth) : null;
3193
+ const staticDir = opts.staticDir ? path.resolve(root, opts.staticDir) : null;
3194
+ async function serveStatic(req) {
3195
+ if (!staticDir) return null;
3196
+ const url = new URL(req.url);
3197
+ let filePath = path.join(staticDir, url.pathname);
3198
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) return new Response(Bun.file(filePath));
3199
+ const indexPath = path.join(staticDir, "index.html");
3200
+ if (fs.existsSync(indexPath)) return new Response(Bun.file(indexPath));
3201
+ return null;
3202
+ }
3203
+ const instance = {
3204
+ async apiHandler(req) {
3205
+ let authResult = null;
3206
+ if (authMW) try {
3207
+ authResult = await authMW(req);
3208
+ } catch {
3209
+ authResult = null;
3210
+ }
3211
+ return ctxStore.run({
3212
+ userId: authResult?.userId ?? null,
3213
+ userEmail: authResult?.userEmail,
3214
+ userName: authResult?.userName,
3215
+ appId: authResult?.appId
3216
+ }, () => handleRequest(req, routes));
3217
+ },
3218
+ async fetch(req) {
3219
+ const url = new URL(req.url);
3220
+ if (url.pathname === "/_vibes/dispatch" && req.method === "POST") try {
3221
+ return await dispatchHandler(await req.json());
3222
+ } catch (err) {
3223
+ return Response.json({ error: `dispatch: ${err.message}` }, { status: 400 });
3224
+ }
3225
+ if (url.pathname === "/_vibes/inspect/triggers" && req.method === "GET") return Response.json(devInspectTriggers());
3226
+ if (url.pathname === "/_vibes/inspect/deliveries" && req.method === "GET") return Response.json(devInspectDeliveries());
3227
+ if (url.pathname === "/_vibes/inspect/events" && req.method === "GET") return Response.json(devInspectEvents());
3228
+ if (url.pathname === "/_vibes/inspect/workflows" && req.method === "GET") return Response.json(devInspectWorkflowRuns());
3229
+ if (url.pathname.startsWith("/_vibes/workflow")) {
3230
+ const r = handleWorkflowRequest(req);
3231
+ if (r) return r;
3232
+ return Response.json({ error: "no workflow endpoint mounted" }, { status: 404 });
3233
+ }
3234
+ if (url.pathname === "/__vibes_push/sw.js" && req.method === "GET") return notificationServiceWorkerHandler();
3235
+ if (url.pathname.startsWith("/api/")) return instance.apiHandler(req);
3236
+ const staticResponse = await serveStatic(req);
3237
+ if (staticResponse) return staticResponse;
3238
+ return Response.json({ error: "Not found" }, { status: 404 });
3239
+ },
3240
+ migrate(newSchema) {
3241
+ currentSchema = withNotificationSystemSchema(newSchema ?? currentSchema);
3242
+ setSubscriptionSchema(currentSchema ?? null);
3243
+ if (!hasCollections(currentSchema)) {
3244
+ rebuildRoutes();
3245
+ return;
3246
+ }
3247
+ const db = ensureDb();
3248
+ registerScopes(currentSchema);
3249
+ migrate(db.raw(), currentSchema);
3250
+ ensureNotificationIndexes(db);
3251
+ rebuildRoutes();
3252
+ },
3253
+ reloadFunctions() {
3254
+ if (!opts.routes) {
3255
+ explicitRoutes = loadExplicitRoutes();
3256
+ rebuildRoutes();
3257
+ }
3258
+ clearModuleCache();
3259
+ console.log(`[vibes] Function modules reloaded (${explicitRoutes.length} explicit routes).`);
3260
+ },
3261
+ async reloadTriggers() {
3262
+ const entries = await loadTriggersFromFile(root);
3263
+ await registerTriggers(entries);
3264
+ console.log(`[vibes] Triggers reloaded (${entries.length} registered).`);
3265
+ },
3266
+ async reloadWorkflows() {
3267
+ const entries = await loadWorkflowsFromFile(root);
3268
+ await registerWorkflows(entries);
3269
+ console.log(`[vibes] Workflows reloaded (${entries.length} registered).`);
3270
+ },
3271
+ close() {
3272
+ dbInstance?.close();
3273
+ }
3274
+ };
3275
+ return instance;
3276
+ }
3277
+ //#endregion
3278
+ export { PredicateValidationError, TriggerScanError, VibesAuthRequiredError, VibesHttpError, _devStorageRead, _devStorageWrite, _verifyDevStorageToken, addClient, addSubClient, billing, buildAutoCrudRoutes, cancel, clearTriggers, clearWorkflows, clientCount, compileToJs, compileToSql, createAuthMiddleware, createVibesServer, cron, ctx, dbProxy as db, decodeRow, decodeRows, devInspectDeliveries, devInspectEvents, devInspectTriggers, devInspectWorkflowRuns, dispatchHandler, emit, extractTriggers, handleSubMessage, invalidate, listTriggers, listWorkflows, loadTriggersFromFile, loadWorkflowsFromFile, migrate, notificationSystemSchema, notify, notifyCollectionChange, notifyRowChange, on, registerScopes, registerTriggers, registerWorkflows, removeClient, removeSubClient, scanTriggers, schedule, setCronDriver, setMaxSubsPerClient, setSubscriptionRingCap, setSubscriptionSchema, startWorkflow, storage, subClientCount, subscriptionCount, usd, validatePredicate, workflow, workflowServiceName, writeTriggersManifest };