@omg-dev/server 0.4.28 → 0.4.30

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 CHANGED
@@ -1363,6 +1363,18 @@ function buildAutoCrudRoutes(schema) {
1363
1363
  return routes;
1364
1364
  }
1365
1365
  //#endregion
1366
+ //#region src/agent-url.ts
1367
+ const DEFAULT_AGENT_URL = "http://localhost:8080";
1368
+ /**
1369
+ * Base URL for the in-VM agent.
1370
+ *
1371
+ * Read lazily so local runtimes can inject the emulator URL before loading
1372
+ * user code without coupling the SDK to a particular process bootstrap.
1373
+ */
1374
+ function agentBase() {
1375
+ return process.env.OMG_AGENT_URL || process.env.VIBES_AGENT_URL || DEFAULT_AGENT_URL;
1376
+ }
1377
+ //#endregion
1366
1378
  //#region src/triggers.ts
1367
1379
  /**
1368
1380
  * Schedule a function to run on a cron expression (UTC, 5-field syntax).
@@ -1496,9 +1508,10 @@ async function registerTriggers(entries) {
1496
1508
  storageHookHandlers.get(e.kind).add(e.handler);
1497
1509
  }
1498
1510
  }
1499
- const inProcessCron = vibesMode$1() === "dev" || cronDriver() === "in-process";
1511
+ const inBuild = inBuildMode();
1512
+ const inProcessCron = !inBuild && (vibesMode$1() === "dev" || cronDriver() === "in-process");
1500
1513
  if (inProcessCron) scheduleAllCronInProcess();
1501
- console.log(`[vibes:triggers] registered ${entries.length} trigger(s) — mode=${vibesMode$1()}, cron=${inProcessCron ? "in-process" : "orchestrator"}`);
1514
+ console.log(`[vibes:triggers] registered ${entries.length} trigger(s) — mode=${vibesMode$1()}, cron=${inBuild ? "unarmed (build)" : inProcessCron ? "in-process" : "orchestrator"}`);
1502
1515
  }
1503
1516
  /** Snapshot of the registry — used by the in-dev Inspect endpoints. */
1504
1517
  function listTriggers() {
@@ -1660,7 +1673,8 @@ function emitInProcess(topic, payload) {
1660
1673
  async function emitToOrchestrator(topic, payload) {
1661
1674
  const payloadStr = JSON.stringify(payload ?? null);
1662
1675
  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", {
1676
+ const url = `${agentBase()}/_emit`;
1677
+ const res = await fetch(url, {
1664
1678
  method: "POST",
1665
1679
  headers: { "Content-Type": "application/json" },
1666
1680
  body: JSON.stringify({
@@ -1772,7 +1786,8 @@ function updateDevDelivery(id, patch) {
1772
1786
  async function scheduleToOrchestrator(atMs, topic, payload) {
1773
1787
  const payloadStr = JSON.stringify(payload ?? null);
1774
1788
  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", {
1789
+ const url = `${agentBase()}/_schedule`;
1790
+ const res = await fetch(url, {
1776
1791
  method: "POST",
1777
1792
  headers: { "Content-Type": "application/json" },
1778
1793
  body: JSON.stringify({
@@ -1793,7 +1808,7 @@ async function scheduleToOrchestrator(atMs, topic, payload) {
1793
1808
  };
1794
1809
  }
1795
1810
  async function cancelOnOrchestrator(eventId) {
1796
- const url = `http://localhost:8080/_schedule/${encodeURIComponent(eventId)}`;
1811
+ const url = `${agentBase()}/_schedule/${encodeURIComponent(eventId)}`;
1797
1812
  const res = await fetch(url, { method: "DELETE" });
1798
1813
  if (!res.ok) {
1799
1814
  const text = await res.text().catch(() => "");
@@ -1907,6 +1922,9 @@ function vibesMode$1() {
1907
1922
  _vibesMode$1 = ((typeof process !== "undefined" ? process.env?.VIBES_MODE : void 0) ?? "") === "dev" ? "dev" : "prod";
1908
1923
  return _vibesMode$1;
1909
1924
  }
1925
+ function inBuildMode() {
1926
+ return (typeof process !== "undefined" ? process.env?.VIBES_BUILD : void 0) === "1";
1927
+ }
1910
1928
  function devInspectTriggers() {
1911
1929
  return Array.from(triggerRegistry.values()).map(({ entry }, i) => ({
1912
1930
  id: `trg_dev_${i}_${entry.handler}`,
@@ -2146,7 +2164,7 @@ function handleWorkflowRequest(req) {
2146
2164
  return restateFetchHandler(req);
2147
2165
  }
2148
2166
  async function startViaAgent(name, payloadStr, opts) {
2149
- const res = await fetch("http://localhost:8080/_workflow/start", {
2167
+ const res = await fetch(`${agentBase()}/_workflow/start`, {
2150
2168
  method: "POST",
2151
2169
  headers: { "Content-Type": "application/json" },
2152
2170
  body: JSON.stringify({
@@ -2233,9 +2251,8 @@ const storage = {
2233
2251
  onUpload,
2234
2252
  onDelete
2235
2253
  };
2236
- const AGENT_BASE$1 = "http://localhost:8080";
2237
2254
  async function prodPresign(action, key, scope, userId, contentType) {
2238
- const res = await fetch(`${AGENT_BASE$1}/_storage/presign`, {
2255
+ const res = await fetch(`${agentBase()}/_storage/presign`, {
2239
2256
  method: "POST",
2240
2257
  headers: { "Content-Type": "application/json" },
2241
2258
  body: JSON.stringify({
@@ -2260,7 +2277,7 @@ async function prodPresign(action, key, scope, userId, contentType) {
2260
2277
  };
2261
2278
  }
2262
2279
  async function prodList(scope, userId, prefix, limit) {
2263
- const res = await fetch(`${AGENT_BASE$1}/_storage/list`, {
2280
+ const res = await fetch(`${agentBase()}/_storage/list`, {
2264
2281
  method: "POST",
2265
2282
  headers: { "Content-Type": "application/json" },
2266
2283
  body: JSON.stringify({
@@ -2283,7 +2300,7 @@ async function prodList(scope, userId, prefix, limit) {
2283
2300
  }));
2284
2301
  }
2285
2302
  async function prodDelete(key, scope, userId) {
2286
- const res = await fetch(`${AGENT_BASE$1}/_storage/delete`, {
2303
+ const res = await fetch(`${agentBase()}/_storage/delete`, {
2287
2304
  method: "POST",
2288
2305
  headers: { "Content-Type": "application/json" },
2289
2306
  body: JSON.stringify({
@@ -2875,7 +2892,6 @@ self.addEventListener("notificationclick", (event) => {
2875
2892
  }
2876
2893
  //#endregion
2877
2894
  //#region src/billing.ts
2878
- const AGENT_BASE = "http://localhost:8080";
2879
2895
  const MICROS_PER_UNIT = 1e6;
2880
2896
  /**
2881
2897
  * Dollars (or whole credit units) → integer micro-units, for the amount
@@ -2886,7 +2902,7 @@ function usd(amount) {
2886
2902
  return Math.round(amount * MICROS_PER_UNIT);
2887
2903
  }
2888
2904
  async function agentPost(path, body, label) {
2889
- const res = await fetch(`${AGENT_BASE}${path}`, {
2905
+ const res = await fetch(`${agentBase()}${path}`, {
2890
2906
  method: "POST",
2891
2907
  headers: { "Content-Type": "application/json" },
2892
2908
  body: JSON.stringify(body)
@@ -2898,7 +2914,7 @@ async function agentPost(path, body, label) {
2898
2914
  return await res.json();
2899
2915
  }
2900
2916
  async function agentGet(path, label) {
2901
- const res = await fetch(`${AGENT_BASE}${path}`, {
2917
+ const res = await fetch(`${agentBase()}${path}`, {
2902
2918
  method: "GET",
2903
2919
  headers: { Accept: "application/json" }
2904
2920
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omg-dev/server",
3
- "version": "0.4.28",
3
+ "version": "0.4.30",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -14,14 +14,19 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@restatedev/restate-sdk": "1.14.5",
17
- "@omg-dev/auth": "0.4.28",
18
- "@omg-dev/schema": "0.4.28",
19
- "@omg-dev/stream": "0.4.28",
17
+ "@omg-dev/auth": "0.4.30",
18
+ "@omg-dev/schema": "0.4.30",
19
+ "@omg-dev/stream": "0.4.30",
20
20
  "web-push": "^3.6.7"
21
21
  },
22
22
  "scripts": {
23
23
  "build": "vp pack src/index.ts src/trigger-scan.ts --no-dts",
24
- "test": "vp test run"
24
+ "test": "vp test run",
25
+ "typecheck": "tsc6 --noEmit --allowImportingTsExtensions --types bun"
26
+ },
27
+ "devDependencies": {
28
+ "@types/bun": "^1.0.0",
29
+ "typescript": "npm:@typescript/typescript6@6.0.2"
25
30
  },
26
31
  "license": "MIT",
27
32
  "repository": {
@@ -0,0 +1,36 @@
1
+ import { afterEach, describe, expect, it } from "vitest"
2
+ import { agentBase } from "./agent-url.ts"
3
+
4
+ const originalOmgAgentUrl = process.env.OMG_AGENT_URL
5
+ const originalVibesAgentUrl = process.env.VIBES_AGENT_URL
6
+
7
+ afterEach(() => {
8
+ if (originalOmgAgentUrl === undefined) delete process.env.OMG_AGENT_URL
9
+ else process.env.OMG_AGENT_URL = originalOmgAgentUrl
10
+
11
+ if (originalVibesAgentUrl === undefined) delete process.env.VIBES_AGENT_URL
12
+ else process.env.VIBES_AGENT_URL = originalVibesAgentUrl
13
+ })
14
+
15
+ describe("agentBase", () => {
16
+ it("preserves the in-VM default when no override is set", () => {
17
+ delete process.env.OMG_AGENT_URL
18
+ delete process.env.VIBES_AGENT_URL
19
+
20
+ expect(agentBase()).toBe("http://localhost:8080")
21
+ })
22
+
23
+ it("uses VIBES_AGENT_URL as the compatibility override", () => {
24
+ delete process.env.OMG_AGENT_URL
25
+ process.env.VIBES_AGENT_URL = "http://127.0.0.1:4100"
26
+
27
+ expect(agentBase()).toBe("http://127.0.0.1:4100")
28
+ })
29
+
30
+ it("prefers OMG_AGENT_URL when both overrides are set", () => {
31
+ process.env.OMG_AGENT_URL = "http://127.0.0.1:4200"
32
+ process.env.VIBES_AGENT_URL = "http://127.0.0.1:4100"
33
+
34
+ expect(agentBase()).toBe("http://127.0.0.1:4200")
35
+ })
36
+ })
@@ -0,0 +1,11 @@
1
+ const DEFAULT_AGENT_URL = "http://localhost:8080"
2
+
3
+ /**
4
+ * Base URL for the in-VM agent.
5
+ *
6
+ * Read lazily so local runtimes can inject the emulator URL before loading
7
+ * user code without coupling the SDK to a particular process bootstrap.
8
+ */
9
+ export function agentBase(): string {
10
+ return process.env.OMG_AGENT_URL || process.env.VIBES_AGENT_URL || DEFAULT_AGENT_URL
11
+ }
package/src/billing.ts CHANGED
@@ -34,7 +34,8 @@
34
34
  // return Response.json(result)
35
35
  // }
36
36
 
37
- const AGENT_BASE = "http://localhost:8080"
37
+ import { agentBase } from "./agent-url.ts"
38
+
38
39
  const MICROS_PER_UNIT = 1_000_000
39
40
 
40
41
  /**
@@ -140,7 +141,7 @@ export interface GrantOptions {
140
141
  // out, throw on non-2xx with a truncated body for diagnosability.
141
142
 
142
143
  async function agentPost<T>(path: string, body: unknown, label: string): Promise<T> {
143
- const res = await fetch(`${AGENT_BASE}${path}`, {
144
+ const res = await fetch(`${agentBase()}${path}`, {
144
145
  method: "POST",
145
146
  headers: { "Content-Type": "application/json" },
146
147
  body: JSON.stringify(body),
@@ -153,7 +154,7 @@ async function agentPost<T>(path: string, body: unknown, label: string): Promise
153
154
  }
154
155
 
155
156
  async function agentGet<T>(path: string, label: string): Promise<T> {
156
- const res = await fetch(`${AGENT_BASE}${path}`, {
157
+ const res = await fetch(`${agentBase()}${path}`, {
157
158
  method: "GET",
158
159
  headers: { Accept: "application/json" },
159
160
  })
package/src/storage.ts CHANGED
@@ -31,6 +31,7 @@ import path from "node:path"
31
31
  import fs from "node:fs"
32
32
  import crypto from "node:crypto"
33
33
  import { ctxStore, ctx } from "./ctx.ts"
34
+ import { agentBase } from "./agent-url.ts"
34
35
 
35
36
  // ── Types ────────────────────────────────────────────────────────────────────
36
37
 
@@ -192,8 +193,6 @@ export const storage = {
192
193
 
193
194
  // ── prod-mode: HTTP to the agent ─────────────────────────────────────────────
194
195
 
195
- const AGENT_BASE = "http://localhost:8080"
196
-
197
196
  async function prodPresign(
198
197
  action: "put" | "get",
199
198
  key: string,
@@ -201,7 +200,7 @@ async function prodPresign(
201
200
  userId: string,
202
201
  contentType?: string,
203
202
  ): Promise<PresignedUploadResult> {
204
- const res = await fetch(`${AGENT_BASE}/_storage/presign`, {
203
+ const res = await fetch(`${agentBase()}/_storage/presign`, {
205
204
  method: "POST",
206
205
  headers: { "Content-Type": "application/json" },
207
206
  body: JSON.stringify({ action, key, scope, userId, contentType }),
@@ -221,7 +220,7 @@ async function prodPresign(
221
220
  }
222
221
 
223
222
  async function prodList(scope: StorageScope, userId: string, prefix: string, limit: number): Promise<StorageItem[]> {
224
- const res = await fetch(`${AGENT_BASE}/_storage/list`, {
223
+ const res = await fetch(`${agentBase()}/_storage/list`, {
225
224
  method: "POST",
226
225
  headers: { "Content-Type": "application/json" },
227
226
  body: JSON.stringify({ scope, userId, prefix, limit }),
@@ -247,7 +246,7 @@ async function prodList(scope: StorageScope, userId: string, prefix: string, lim
247
246
  }
248
247
 
249
248
  async function prodDelete(key: string, scope: StorageScope, userId: string): Promise<void> {
250
- const res = await fetch(`${AGENT_BASE}/_storage/delete`, {
249
+ const res = await fetch(`${agentBase()}/_storage/delete`, {
251
250
  method: "POST",
252
251
  headers: { "Content-Type": "application/json" },
253
252
  body: JSON.stringify({ key, scope, userId }),
@@ -0,0 +1,80 @@
1
+ // Regression test for the vibes-build hang (confirmed: `[vibes-build] done.`
2
+ // prints, process never exits when the project declares cron()/on()).
3
+ //
4
+ // Root cause: registerTriggers() arms an in-process setTimeout chain for
5
+ // every cron() handler whenever vibesMode()==="dev" or cronDriver()===
6
+ // "in-process" — including when createVibesServer() is constructed by the
7
+ // vibes vite-plugin's configureServer hook, which prerenderApp() (part of
8
+ // `vibes-build`) triggers indirectly via a nested Vite dev server. Those
9
+ // timers keep the event loop alive forever with no caller left to clear
10
+ // them, since a build process has no request loop to eventually close it.
11
+ //
12
+ // This can only be observed as a live-process behavior (does the process
13
+ // exit on its own?), not through a return-value assertion on
14
+ // registerTriggers() — so this test spawns a real Bun subprocess and checks
15
+ // whether it terminates within a bounded window.
16
+
17
+ import { describe, expect, test } from "bun:test"
18
+ import fs from "node:fs"
19
+ import os from "node:os"
20
+ import path from "node:path"
21
+
22
+ const triggersModulePath = path.join(import.meta.dir, "..", "triggers.ts")
23
+
24
+ function writeRegisterScript(): string {
25
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "vibes-build-mode-cron-"))
26
+ const file = path.join(dir, "register.ts")
27
+ fs.writeFileSync(
28
+ file,
29
+ [
30
+ `import { registerTriggers } from ${JSON.stringify(triggersModulePath)}`,
31
+ "await registerTriggers([",
32
+ ' { handler: "trg.tick", kind: "cron", key: "* * * * *", module: "/dev/null", exportName: "tick", mod: { tick: async () => {} } },',
33
+ ' { handler: "trg.onEvt", kind: "on", key: "some.topic", module: "/dev/null", exportName: "onEvt", mod: { onEvt: async () => {} } },',
34
+ "])",
35
+ 'console.log("REGISTERED")',
36
+ "",
37
+ ].join("\n"),
38
+ )
39
+ return file
40
+ }
41
+
42
+ // Runs `script` in a fresh Bun subprocess with `env` merged over the current
43
+ // process env. Resolves `{ exited: true }` if the process exits within
44
+ // `timeoutMs` on its own, or `{ exited: false }` (after force-killing it) if
45
+ // it's still alive at the deadline — the exact symptom of the original bug.
46
+ async function runAndWaitForExit(
47
+ script: string,
48
+ env: Record<string, string>,
49
+ timeoutMs: number,
50
+ ): Promise<{ exited: boolean }> {
51
+ const proc = Bun.spawn(["bun", "run", script], {
52
+ env: { ...process.env, ...env },
53
+ stdout: "pipe",
54
+ stderr: "pipe",
55
+ })
56
+ let timedOut = false
57
+ const timer = setTimeout(() => {
58
+ timedOut = true
59
+ proc.kill()
60
+ }, timeoutMs)
61
+ await proc.exited
62
+ clearTimeout(timer)
63
+ return { exited: !timedOut }
64
+ }
65
+
66
+ describe("registerTriggers build-mode cron gating", () => {
67
+ test("VIBES_BUILD=1: a cron()+on() project terminates on its own", async () => {
68
+ const script = writeRegisterScript()
69
+ const { exited } = await runAndWaitForExit(script, { VIBES_BUILD: "1", VIBES_MODE: "dev" }, 8000)
70
+ expect(exited).toBe(true)
71
+ }, 15000)
72
+
73
+ test("sanity: without VIBES_BUILD, dev-mode cron is still armed (stays alive)", async () => {
74
+ // Mirrors the fixed case above to prove the guard is scoped to build
75
+ // mode only — normal `bun dev` cron scheduling must be unaffected.
76
+ const script = writeRegisterScript()
77
+ const { exited } = await runAndWaitForExit(script, { VIBES_MODE: "dev" }, 2000)
78
+ expect(exited).toBe(false)
79
+ }, 15000)
80
+ })
package/src/triggers.ts CHANGED
@@ -25,6 +25,7 @@
25
25
  import path from "node:path"
26
26
  import fs from "node:fs"
27
27
  import { ctxStore, type VibesCtx } from "./ctx.ts"
28
+ import { agentBase } from "./agent-url.ts"
28
29
 
29
30
  // ── Public API ───────────────────────────────────────────────────────────────
30
31
 
@@ -242,12 +243,20 @@ export async function registerTriggers(entries: TriggerEntry[]): Promise<void> {
242
243
  }
243
244
  }
244
245
  void registeredFirstTime // suppress unused — kept for future HMR diff
245
- const inProcessCron = vibesMode() === "dev" || cronDriver() === "in-process"
246
+ // vibes-build sets VIBES_BUILD=1 for its whole process, including the
247
+ // nested Vite dev server it spins up for prerender SSG. A build is not a
248
+ // runtime — nothing should ever arm a live setTimeout chain there, or the
249
+ // process hangs forever after writing its artifacts (confirmed hang: cron
250
+ // schedulers keep the event loop open with no caller left to clear them).
251
+ const inBuild = inBuildMode()
252
+ const inProcessCron = !inBuild && (vibesMode() === "dev" || cronDriver() === "in-process")
246
253
  if (inProcessCron) {
247
254
  scheduleAllCronInProcess()
248
255
  }
249
256
  console.log(
250
- `[vibes:triggers] registered ${entries.length} trigger(s) — mode=${vibesMode()}, cron=${inProcessCron ? "in-process" : "orchestrator"}`,
257
+ `[vibes:triggers] registered ${entries.length} trigger(s) — mode=${vibesMode()}, cron=${
258
+ inBuild ? "unarmed (build)" : inProcessCron ? "in-process" : "orchestrator"
259
+ }`,
251
260
  )
252
261
  }
253
262
 
@@ -472,7 +481,7 @@ async function emitToOrchestrator(topic: string, payload: unknown): Promise<{ su
472
481
  if (payloadStr.length > MAX_EMIT_BYTES) {
473
482
  throw new Error(`emit("${topic}"): payload exceeds ${MAX_EMIT_BYTES} bytes`)
474
483
  }
475
- const url = "http://localhost:8080/_emit"
484
+ const url = `${agentBase()}/_emit`
476
485
  const res = await fetch(url, {
477
486
  method: "POST",
478
487
  headers: { "Content-Type": "application/json" },
@@ -629,7 +638,7 @@ async function scheduleToOrchestrator(
629
638
  if (payloadStr.length > MAX_EMIT_BYTES) {
630
639
  throw new Error(`schedule("${topic}"): payload exceeds ${MAX_EMIT_BYTES} bytes`)
631
640
  }
632
- const url = "http://localhost:8080/_schedule"
641
+ const url = `${agentBase()}/_schedule`
633
642
  const res = await fetch(url, {
634
643
  method: "POST",
635
644
  headers: { "Content-Type": "application/json" },
@@ -656,7 +665,7 @@ async function scheduleToOrchestrator(
656
665
  }
657
666
 
658
667
  async function cancelOnOrchestrator(eventId: string): Promise<{ cancelled: number }> {
659
- const url = `http://localhost:8080/_schedule/${encodeURIComponent(eventId)}`
668
+ const url = `${agentBase()}/_schedule/${encodeURIComponent(eventId)}`
660
669
  const res = await fetch(url, { method: "DELETE" })
661
670
  if (!res.ok) {
662
671
  const text = await res.text().catch(() => "")
@@ -810,6 +819,15 @@ function vibesMode(): "dev" | "prod" {
810
819
  return _vibesMode
811
820
  }
812
821
 
822
+ // Set for the whole lifetime of `vibes-build` (packages/vite-plugin/src/build.ts).
823
+ // A build process has no caller left to clear timers once it "finishes" —
824
+ // so cron must never be armed in-process while this is set, however
825
+ // createVibesServer() got invoked (directly, or via prerender's nested dev
826
+ // server).
827
+ function inBuildMode(): boolean {
828
+ return (typeof process !== "undefined" ? process.env?.VIBES_BUILD : undefined) === "1"
829
+ }
830
+
813
831
  // ── Dev Inspect data accessors ───────────────────────────────────────────────
814
832
  //
815
833
  // In dev mode, the dashboard's Inspect tabs read the in-memory rings
package/src/workflows.ts CHANGED
@@ -38,6 +38,7 @@
38
38
  import path from "node:path"
39
39
  import fs from "node:fs"
40
40
  import { ctxStore, type VibesCtx } from "./ctx.ts"
41
+ import { agentBase } from "./agent-url.ts"
41
42
 
42
43
  // ── Public API ───────────────────────────────────────────────────────────────
43
44
 
@@ -417,7 +418,7 @@ async function startViaAgent(
417
418
  payloadStr: string,
418
419
  opts: StartWorkflowOptions,
419
420
  ): Promise<{ runId: string }> {
420
- const res = await fetch("http://localhost:8080/_workflow/start", {
421
+ const res = await fetch(`${agentBase()}/_workflow/start`, {
421
422
  method: "POST",
422
423
  headers: { "Content-Type": "application/json" },
423
424
  body: JSON.stringify({