unfazed 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +321 -0
  2. package/dist/client.d.ts +1 -1
  3. package/dist/client.js +1 -1
  4. package/dist/client.js.map +1 -1
  5. package/dist/errors.d.ts +25 -0
  6. package/dist/errors.js +25 -0
  7. package/dist/errors.js.map +1 -1
  8. package/dist/index.d.ts +21 -0
  9. package/dist/index.js +22 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/remote/app.d.ts +18 -0
  12. package/dist/remote/app.js +76 -0
  13. package/dist/remote/app.js.map +1 -0
  14. package/dist/remote/documents.d.ts +175 -0
  15. package/dist/remote/documents.js +122 -0
  16. package/dist/remote/documents.js.map +1 -0
  17. package/dist/remote/values.d.ts +67 -0
  18. package/dist/remote/values.js +156 -0
  19. package/dist/remote/values.js.map +1 -0
  20. package/dist/remote/world.d.ts +50 -0
  21. package/dist/remote/world.js +612 -0
  22. package/dist/remote/world.js.map +1 -0
  23. package/dist/workflow/config.d.ts +23 -0
  24. package/dist/workflow/config.js +48 -0
  25. package/dist/workflow/config.js.map +1 -0
  26. package/dist/workflow/errors.d.ts +33 -0
  27. package/dist/workflow/errors.js +16 -0
  28. package/dist/workflow/errors.js.map +1 -0
  29. package/dist/workflow/index.d.ts +30 -0
  30. package/dist/workflow/index.js +23 -0
  31. package/dist/workflow/index.js.map +1 -0
  32. package/dist/workflow/inspect-render.d.ts +93 -0
  33. package/dist/workflow/inspect-render.js +302 -0
  34. package/dist/workflow/inspect-render.js.map +1 -0
  35. package/dist/workflow/inspect.d.ts +52 -0
  36. package/dist/workflow/inspect.js +879 -0
  37. package/dist/workflow/inspect.js.map +1 -0
  38. package/dist/workflow/migration-files.d.ts +13 -0
  39. package/dist/workflow/migration-files.js +52 -0
  40. package/dist/workflow/migration-files.js.map +1 -0
  41. package/dist/workflow/migration-types.d.ts +19 -0
  42. package/dist/workflow/migration-types.js +8 -0
  43. package/dist/workflow/migration-types.js.map +1 -0
  44. package/dist/workflow/migrations.d.ts +12 -0
  45. package/dist/workflow/migrations.generated.d.ts +2 -0
  46. package/dist/workflow/migrations.generated.js +34 -0
  47. package/dist/workflow/migrations.generated.js.map +1 -0
  48. package/dist/workflow/migrations.js +94 -0
  49. package/dist/workflow/migrations.js.map +1 -0
  50. package/dist/workflow/nitro-flow-guard.d.ts +4 -0
  51. package/dist/workflow/nitro-flow-guard.js +19 -0
  52. package/dist/workflow/nitro-flow-guard.js.map +1 -0
  53. package/dist/workflow/nitro-inspect.d.ts +3 -0
  54. package/dist/workflow/nitro-inspect.js +19 -0
  55. package/dist/workflow/nitro-inspect.js.map +1 -0
  56. package/dist/workflow/nitro-plugin.d.ts +1 -0
  57. package/dist/workflow/nitro-plugin.js +13 -0
  58. package/dist/workflow/nitro-plugin.js.map +1 -0
  59. package/dist/workflow/nitro.d.ts +70 -0
  60. package/dist/workflow/nitro.js +213 -0
  61. package/dist/workflow/nitro.js.map +1 -0
  62. package/dist/workflow/public-url.d.ts +27 -0
  63. package/dist/workflow/public-url.js +45 -0
  64. package/dist/workflow/public-url.js.map +1 -0
  65. package/dist/workflow/queue.d.ts +128 -0
  66. package/dist/workflow/queue.js +375 -0
  67. package/dist/workflow/queue.js.map +1 -0
  68. package/dist/workflow/state.d.ts +25 -0
  69. package/dist/workflow/state.js +15 -0
  70. package/dist/workflow/state.js.map +1 -0
  71. package/dist/workflow/transport.d.ts +12 -0
  72. package/dist/workflow/transport.js +29 -0
  73. package/dist/workflow/transport.js.map +1 -0
  74. package/dist/workflow/world.d.ts +123 -0
  75. package/dist/workflow/world.js +375 -0
  76. package/dist/workflow/world.js.map +1 -0
  77. package/package.json +66 -5
@@ -0,0 +1,94 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { drizzle } from "drizzle-orm/node-postgres";
5
+ import { migrate } from "drizzle-orm/node-postgres/migrator";
6
+ import { MIGRATIONS_SCHEMA, MIGRATIONS_TABLE } from "./migration-types.js";
7
+ /**
8
+ * `pg_advisory_lock` key pair ("unfz", "wfmg"). Two VMs of one app migrating
9
+ * the same database at once (a redeploy's overlap on a fresh database) would
10
+ * otherwise race on `CREATE SCHEMA` and fail one of them.
11
+ */
12
+ const LOCK = [0x756e667a, 0x77666d67];
13
+ const LOCK_WAIT_MS = 120_000;
14
+ /**
15
+ * Applies the migrations with drizzle's own migrator, exactly as the upstream
16
+ * `bootstrap` CLI does, from a temporary folder (the migrator reads files).
17
+ * Idempotent: applied migrations are skipped by the migrator's own journal.
18
+ *
19
+ * Runs on every cold start, so the common case, a database that already has
20
+ * the newest migration, costs one query and touches neither the lock nor the
21
+ * file system.
22
+ */
23
+ export async function applyMigrations(pool, migrations) {
24
+ if (await isCurrent(pool, migrations))
25
+ return;
26
+ const dir = await mkdtemp(join(tmpdir(), "unfazed-workflow-migrations-"));
27
+ try {
28
+ await mkdir(join(dir, "meta"));
29
+ await writeFile(join(dir, "meta", "_journal.json"), migrations.journal);
30
+ for (const [name, sql] of Object.entries(migrations.files)) {
31
+ await writeFile(join(dir, name), sql);
32
+ }
33
+ const client = await pool.connect();
34
+ let failed = false;
35
+ try {
36
+ await acquireLock(client);
37
+ try {
38
+ await migrate(drizzle(client), {
39
+ migrationsFolder: dir,
40
+ migrationsTable: MIGRATIONS_TABLE,
41
+ migrationsSchema: MIGRATIONS_SCHEMA,
42
+ });
43
+ }
44
+ finally {
45
+ await client.query("SELECT pg_advisory_unlock($1, $2)", LOCK);
46
+ }
47
+ }
48
+ catch (error) {
49
+ failed = true;
50
+ throw error;
51
+ }
52
+ finally {
53
+ // A session that failed mid-migration may still hold the lock or an
54
+ // aborted transaction: drop it rather than return it to the pool.
55
+ client.release(failed);
56
+ }
57
+ }
58
+ finally {
59
+ await rm(dir, { recursive: true, force: true });
60
+ }
61
+ }
62
+ /**
63
+ * Whether the newest migration is already applied. drizzle applies every
64
+ * migration whose journal `when` is newer than the newest `created_at` it
65
+ * recorded, so "recorded >= newest `when`" means it would apply nothing.
66
+ */
67
+ async function isCurrent(pool, migrations) {
68
+ const { entries } = JSON.parse(migrations.journal);
69
+ const newest = Math.max(...entries.map((entry) => entry.when));
70
+ try {
71
+ const { rows } = await pool.query(`SELECT max(created_at)::text AS last FROM "${MIGRATIONS_SCHEMA}"."${MIGRATIONS_TABLE}"`);
72
+ const last = rows[0]?.last;
73
+ return last !== null && last !== undefined && Number(last) >= newest;
74
+ }
75
+ catch (error) {
76
+ if (error.code === "42P01" || error.code === "3F000") {
77
+ return false; // no bookkeeping table (or schema) yet: a fresh database
78
+ }
79
+ throw error;
80
+ }
81
+ }
82
+ async function acquireLock(client) {
83
+ const deadline = Date.now() + LOCK_WAIT_MS;
84
+ for (;;) {
85
+ const { rows } = await client.query("SELECT pg_try_advisory_lock($1, $2) AS locked", LOCK);
86
+ if (rows[0]?.locked)
87
+ return;
88
+ if (Date.now() > deadline) {
89
+ throw new Error(`another process has held the workflow migration lock for ${LOCK_WAIT_MS / 1000} s; is a migration stuck?`);
90
+ }
91
+ await new Promise((resolve) => setTimeout(resolve, 200));
92
+ }
93
+ }
94
+ //# sourceMappingURL=migrations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/workflow/migrations.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdir, mkdtemp, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { drizzle } from \"drizzle-orm/node-postgres\";\nimport { migrate } from \"drizzle-orm/node-postgres/migrator\";\nimport type { Pool, PoolClient } from \"pg\";\nimport { MIGRATIONS_SCHEMA, MIGRATIONS_TABLE, type WorkflowMigrations } from \"./migration-types.js\";\n\n/**\n * `pg_advisory_lock` key pair (\"unfz\", \"wfmg\"). Two VMs of one app migrating\n * the same database at once (a redeploy's overlap on a fresh database) would\n * otherwise race on `CREATE SCHEMA` and fail one of them.\n */\nconst LOCK: [number, number] = [0x756e667a, 0x77666d67];\nconst LOCK_WAIT_MS = 120_000;\n\n/**\n * Applies the migrations with drizzle's own migrator, exactly as the upstream\n * `bootstrap` CLI does, from a temporary folder (the migrator reads files).\n * Idempotent: applied migrations are skipped by the migrator's own journal.\n *\n * Runs on every cold start, so the common case, a database that already has\n * the newest migration, costs one query and touches neither the lock nor the\n * file system.\n */\nexport async function applyMigrations(pool: Pool, migrations: WorkflowMigrations): Promise<void> {\n if (await isCurrent(pool, migrations)) return;\n const dir = await mkdtemp(join(tmpdir(), \"unfazed-workflow-migrations-\"));\n try {\n await mkdir(join(dir, \"meta\"));\n await writeFile(join(dir, \"meta\", \"_journal.json\"), migrations.journal);\n for (const [name, sql] of Object.entries(migrations.files)) {\n await writeFile(join(dir, name), sql);\n }\n const client = await pool.connect();\n let failed = false;\n try {\n await acquireLock(client);\n try {\n await migrate(drizzle(client), {\n migrationsFolder: dir,\n migrationsTable: MIGRATIONS_TABLE,\n migrationsSchema: MIGRATIONS_SCHEMA,\n });\n } finally {\n await client.query(\"SELECT pg_advisory_unlock($1, $2)\", LOCK);\n }\n } catch (error) {\n failed = true;\n throw error;\n } finally {\n // A session that failed mid-migration may still hold the lock or an\n // aborted transaction: drop it rather than return it to the pool.\n client.release(failed);\n }\n } finally {\n await rm(dir, { recursive: true, force: true });\n }\n}\n\n/**\n * Whether the newest migration is already applied. drizzle applies every\n * migration whose journal `when` is newer than the newest `created_at` it\n * recorded, so \"recorded >= newest `when`\" means it would apply nothing.\n */\nasync function isCurrent(pool: Pool, migrations: WorkflowMigrations): Promise<boolean> {\n const { entries } = JSON.parse(migrations.journal) as { entries: { when: number }[] };\n const newest = Math.max(...entries.map((entry) => entry.when));\n try {\n const { rows } = await pool.query<{ last: string | null }>(\n `SELECT max(created_at)::text AS last FROM \"${MIGRATIONS_SCHEMA}\".\"${MIGRATIONS_TABLE}\"`,\n );\n const last = rows[0]?.last;\n return last !== null && last !== undefined && Number(last) >= newest;\n } catch (error) {\n if ((error as { code?: string }).code === \"42P01\" || (error as { code?: string }).code === \"3F000\") {\n return false; // no bookkeeping table (or schema) yet: a fresh database\n }\n throw error;\n }\n}\n\nasync function acquireLock(client: PoolClient): Promise<void> {\n const deadline = Date.now() + LOCK_WAIT_MS;\n for (;;) {\n const { rows } = await client.query<{ locked: boolean }>(\n \"SELECT pg_try_advisory_lock($1, $2) AS locked\",\n LOCK,\n );\n if (rows[0]?.locked) return;\n if (Date.now() > deadline) {\n throw new Error(\n `another process has held the workflow migration lock for ${LOCK_WAIT_MS / 1000} s; is a migration stuck?`,\n );\n }\n await new Promise((resolve) => setTimeout(resolve, 200));\n }\n}\n"]}
@@ -0,0 +1,4 @@
1
+ export default function unfazedFlowGuard(event: {
2
+ req: Request;
3
+ url?: URL;
4
+ }): Response | undefined;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Nitro middleware on the Workflow SDK's flow route: a request without the
3
+ * platform's queue token gets 401 before the SDK sees it. The World's queue
4
+ * handler checks the same token; this also covers the SDK's unauthenticated
5
+ * `?__health` answer, so the route serves the internet nothing but the
6
+ * refusal. Public ingress strips the header, so only the platform can send it.
7
+ */
8
+ import { DELIVERY_HEADERS, verifyQueueToken } from "./queue.js";
9
+ const FLOW_ROUTE = "/.well-known/workflow/v1/flow";
10
+ export default function unfazedFlowGuard(event) {
11
+ const pathname = (event.url ?? new URL(event.req.url)).pathname;
12
+ if (pathname !== FLOW_ROUTE)
13
+ return undefined;
14
+ const expected = process.env.UNFZ_STATE_TOKEN?.trim() || undefined;
15
+ if (verifyQueueToken(expected, event.req.headers.get(DELIVERY_HEADERS.token)))
16
+ return undefined;
17
+ return Response.json({ error: "unauthorized", message: "missing or invalid queue token" }, { status: 401 });
18
+ }
19
+ //# sourceMappingURL=nitro-flow-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nitro-flow-guard.js","sourceRoot":"","sources":["../../src/workflow/nitro-flow-guard.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEhE,MAAM,UAAU,GAAG,+BAA+B,CAAC;AAEnD,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,KAAkC;IACzE,MAAM,QAAQ,GAAG,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC;IAChE,IAAI,QAAQ,KAAK,UAAU;QAAE,OAAO,SAAS,CAAC;IAC9C,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,IAAI,EAAE,IAAI,SAAS,CAAC;IACnE,IAAI,gBAAgB,CAAC,QAAQ,EAAE,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IAChG,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,gCAAgC,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;AAC9G,CAAC","sourcesContent":["/**\n * Nitro middleware on the Workflow SDK's flow route: a request without the\n * platform's queue token gets 401 before the SDK sees it. The World's queue\n * handler checks the same token; this also covers the SDK's unauthenticated\n * `?__health` answer, so the route serves the internet nothing but the\n * refusal. Public ingress strips the header, so only the platform can send it.\n */\nimport { DELIVERY_HEADERS, verifyQueueToken } from \"./queue.js\";\n\nconst FLOW_ROUTE = \"/.well-known/workflow/v1/flow\";\n\nexport default function unfazedFlowGuard(event: { req: Request; url?: URL }): Response | undefined {\n const pathname = (event.url ?? new URL(event.req.url)).pathname;\n if (pathname !== FLOW_ROUTE) return undefined;\n const expected = process.env.UNFZ_STATE_TOKEN?.trim() || undefined;\n if (verifyQueueToken(expected, event.req.headers.get(DELIVERY_HEADERS.token))) return undefined;\n return Response.json({ error: \"unauthorized\", message: \"missing or invalid queue token\" }, { status: 401 });\n}\n"]}
@@ -0,0 +1,3 @@
1
+ export default function unfazedInspect(event: {
2
+ req: Request;
3
+ }): Promise<Response>;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The Nitro handler `unfazed/workflow/nitro` mounts on
3
+ * `/.well-known/workflow/v1/unfazed/inspect/v1/**`: the workflow inspection
4
+ * route, reading through the World the server plugin installed.
5
+ */
6
+ import info from "#unfazed/workflow-inspect-info";
7
+ import { getRun } from "workflow/api";
8
+ import { getWorld } from "workflow/runtime";
9
+ import { createInspectHandler } from "./inspect.js";
10
+ const handler = createInspectHandler({
11
+ getWorld,
12
+ getRun,
13
+ workflows: info?.workflows ?? [],
14
+ ...(info?.workflow ? { workflowVersion: info.workflow } : {}),
15
+ });
16
+ export default function unfazedInspect(event) {
17
+ return handler(event.req);
18
+ }
19
+ //# sourceMappingURL=nitro-inspect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nitro-inspect.js","sourceRoot":"","sources":["../../src/workflow/nitro-inspect.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,IAAI,MAAM,gCAAgC,CAAC;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEpD,MAAM,OAAO,GAAG,oBAAoB,CAAC;IACnC,QAAQ;IACR,MAAM;IACN,SAAS,EAAE,IAAI,EAAE,SAAS,IAAI,EAAE;IAChC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;CAC9D,CAAC,CAAC;AAEH,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,KAAuB;IAC5D,OAAO,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAC5B,CAAC","sourcesContent":["/**\n * The Nitro handler `unfazed/workflow/nitro` mounts on\n * `/.well-known/workflow/v1/unfazed/inspect/v1/**`: the workflow inspection\n * route, reading through the World the server plugin installed.\n */\nimport info from \"#unfazed/workflow-inspect-info\";\nimport { getRun } from \"workflow/api\";\nimport { getWorld } from \"workflow/runtime\";\nimport { createInspectHandler } from \"./inspect.js\";\n\nconst handler = createInspectHandler({\n getWorld,\n getRun,\n workflows: info?.workflows ?? [],\n ...(info?.workflow ? { workflowVersion: info.workflow } : {}),\n});\n\nexport default function unfazedInspect(event: { req: Request }): Promise<Response> {\n return handler(event.req);\n}\n"]}
@@ -0,0 +1 @@
1
+ export default function unfazedWorkflowPlugin(): void;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The Nitro server plugin `unfazed/workflow/nitro` registers. Nitro runs
3
+ * plugins synchronously while the app initialises, so the World is installed
4
+ * before the first request can reach a workflow route, and the runtime never
5
+ * falls back to its Local World on a cold start.
6
+ */
7
+ import migrations from "#unfazed/workflow-migrations";
8
+ import { setWorld } from "workflow/runtime";
9
+ import { createWorld } from "./world.js";
10
+ export default function unfazedWorkflowPlugin() {
11
+ setWorld(createWorld(migrations ? { migrations } : {}));
12
+ }
13
+ //# sourceMappingURL=nitro-plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nitro-plugin.js","sourceRoot":"","sources":["../../src/workflow/nitro-plugin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,UAAU,MAAM,8BAA8B,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,CAAC,OAAO,UAAU,qBAAqB;IAC3C,QAAQ,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC1D,CAAC","sourcesContent":["/**\n * The Nitro server plugin `unfazed/workflow/nitro` registers. Nitro runs\n * plugins synchronously while the app initialises, so the World is installed\n * before the first request can reach a workflow route, and the runtime never\n * falls back to its Local World on a cold start.\n */\nimport migrations from \"#unfazed/workflow-migrations\";\nimport { setWorld } from \"workflow/runtime\";\nimport { createWorld } from \"./world.js\";\n\nexport default function unfazedWorkflowPlugin(): void {\n setWorld(createWorld(migrations ? { migrations } : {}));\n}\n"]}
@@ -0,0 +1,70 @@
1
+ /** Virtual module the plugin reads the app's migrations from. */
2
+ export declare const MIGRATIONS_MODULE_ID = "#unfazed/workflow-migrations";
3
+ /** The Workflow SDK's flow route, which the guard protects. */
4
+ export declare const FLOW_ROUTE = "/.well-known/workflow/v1/flow";
5
+ /** Virtual module the inspection route reads the build's workflow list and SDK version from. */
6
+ export declare const INSPECT_INFO_MODULE_ID = "#unfazed/workflow-inspect-info";
7
+ /** `INSPECT_BASE_PATH` (inspect.ts), repeated so loading this module at build time loads no SDK code. */
8
+ export declare const INSPECT_ROUTE = "/.well-known/workflow/v1/unfazed/inspect/v1";
9
+ /** The slice of Nitro v3's `Nitro` this module reads and writes. */
10
+ interface NitroLike {
11
+ options: {
12
+ dev: boolean;
13
+ preset: string;
14
+ rootDir: string;
15
+ buildDir: string;
16
+ noExternals?: boolean | unknown[];
17
+ plugins: string[];
18
+ handlers: {
19
+ route?: string;
20
+ handler: string;
21
+ middleware?: boolean;
22
+ }[];
23
+ virtual: Record<string, string | (() => string | Promise<string>)>;
24
+ entry?: string;
25
+ serveStatic?: boolean | string;
26
+ alias: Record<string, string>;
27
+ exportConditions?: string[];
28
+ commands?: Record<string, string | undefined>;
29
+ _config?: {
30
+ preset?: string;
31
+ };
32
+ _c12?: {
33
+ layers?: {
34
+ config?: {
35
+ preset?: string;
36
+ } | null;
37
+ configFile?: string;
38
+ }[];
39
+ };
40
+ };
41
+ logger?: {
42
+ info(message: string): void;
43
+ warn(message: string): void;
44
+ };
45
+ }
46
+ declare const unfazedWorkflowNitro: {
47
+ name: string;
48
+ setup(input: {
49
+ options: object;
50
+ }): Promise<void>;
51
+ };
52
+ export default unfazedWorkflowNitro;
53
+ /** What the inspection route's `info` reports about this build. */
54
+ export interface InspectBuildInfo {
55
+ /** The app's Workflow SDK version, if it could be read. */
56
+ workflow: string | null;
57
+ /** The workflow machine names in the build's manifest. */
58
+ workflows: string[];
59
+ }
60
+ /**
61
+ * The app's `workflow` version and the workflows in the manifest
62
+ * `workflow/nitro` writes to `<buildDir>/workflow/manifest.json`
63
+ * (`workflows: { [file]: { [name]: { workflowId } } }`).
64
+ */
65
+ export declare function readInspectInfo(rootDir: string, buildDir: string): Promise<InspectBuildInfo>;
66
+ /** A preset the build chose explicitly, and where, or undefined when Nitro picked it. */
67
+ export declare function explicitPreset(options: Pick<NitroLike["options"], "_config" | "_c12">, env?: Record<string, string | undefined>): {
68
+ preset: string;
69
+ from: string;
70
+ } | undefined;
@@ -0,0 +1,213 @@
1
+ /**
2
+ * `unfazed/workflow/nitro` — a Nitro (v3) module that installs the unfazed
3
+ * World in production builds.
4
+ *
5
+ * ```ts
6
+ * // nitro.config.ts
7
+ * export default defineConfig({
8
+ * preset: "standard",
9
+ * modules: ["workflow/nitro", "unfazed/workflow/nitro"],
10
+ * routes: { "/**": "./src/app.ts" },
11
+ * });
12
+ * ```
13
+ *
14
+ * In a production build it:
15
+ * - registers a server plugin that calls `setWorld(createWorld())` while the
16
+ * app initialises, before the first request (the World is never left to
17
+ * the runtime's `WORKFLOW_TARGET_WORLD` lookup, which needs node_modules);
18
+ * - embeds the SQL migrations of the app's own `@workflow/world-postgres`,
19
+ * so the schema always matches the storage code that is bundled;
20
+ * - guards `/.well-known/workflow/v1/flow`: anything without the platform's
21
+ * queue token gets 401 before the Workflow SDK sees it (including its
22
+ * unauthenticated `?__health` probe);
23
+ * - mounts the workflow inspection route the unfazed dashboard and MCP read
24
+ * runs through (`/.well-known/workflow/v1/unfazed/inspect/v1/*`, the same
25
+ * queue token, see `createInspectHandler`), with the build's workflow list
26
+ * and Workflow SDK version;
27
+ * - sets `noExternals: true`, because a service VM has no node_modules;
28
+ * - builds Nitro's `standard` output (a bundle that default-exports
29
+ * `{ fetch }`, which the unfazed runtime serves) whenever no preset was
30
+ * chosen, whatever Nitro detected on the build machine: `node-server`
31
+ * under Node, `bun` under Bun (the unfazed build VM), `deno-server` under
32
+ * Deno. A preset chosen explicitly (`preset` in nitro.config.ts or
33
+ * .nitrorc, `NITRO_PRESET`, `SERVER_PRESET`) other than `standard` fails
34
+ * the build with a message saying so: those bundles start a server of
35
+ * their own, which unfazed cannot serve properly. Static files are served
36
+ * only when `serveStatic: "inline"` embeds them in the bundle.
37
+ *
38
+ * `nitro dev` is left untouched, so local development keeps Workflow SDK's
39
+ * Local World.
40
+ */
41
+ import { createRequire } from "node:module";
42
+ import { join } from "node:path";
43
+ import { fileURLToPath } from "node:url";
44
+ import { readFile } from "node:fs/promises";
45
+ import { readPackageVersion, readWorldPostgresMigrations } from "./migration-files.js";
46
+ /** The aliases of Nitro's `standard` preset (nitro 3.0.260903-beta). */
47
+ const STANDARD_ALIASES = {
48
+ srvx: "srvx/generic",
49
+ "srvx/bun": "srvx/bun",
50
+ "srvx/deno": "srvx/deno",
51
+ "srvx/node": "srvx/node",
52
+ "srvx/generic": "srvx/generic",
53
+ "srvx/tracing": "srvx/tracing",
54
+ "srvx/body-limit": "srvx/body-limit",
55
+ };
56
+ /** Virtual module the plugin reads the app's migrations from. */
57
+ export const MIGRATIONS_MODULE_ID = "#unfazed/workflow-migrations";
58
+ /** The Workflow SDK's flow route, which the guard protects. */
59
+ export const FLOW_ROUTE = "/.well-known/workflow/v1/flow";
60
+ /** Virtual module the inspection route reads the build's workflow list and SDK version from. */
61
+ export const INSPECT_INFO_MODULE_ID = "#unfazed/workflow-inspect-info";
62
+ /** `INSPECT_BASE_PATH` (inspect.ts), repeated so loading this module at build time loads no SDK code. */
63
+ export const INSPECT_ROUTE = "/.well-known/workflow/v1/unfazed/inspect/v1";
64
+ const here = (file) => fileURLToPath(new URL(file, import.meta.url));
65
+ const unfazedWorkflowNitro = {
66
+ name: "unfazed/workflow/nitro",
67
+ async setup(input) {
68
+ const nitro = input;
69
+ const { options } = nitro;
70
+ const log = nitro.logger ?? console;
71
+ if (options.dev) {
72
+ log.info("unfazed/workflow: `nitro dev` keeps Workflow SDK's Local World; production builds use the unfazed World.");
73
+ return;
74
+ }
75
+ options.noExternals = true;
76
+ await applyStandardOutput(nitro, log);
77
+ const migrations = await readAppMigrations(options.rootDir);
78
+ if (!migrations) {
79
+ log.warn("unfazed/workflow: cannot read the app's @workflow/world-postgres migrations; " +
80
+ "using the copy embedded in unfazed. Install @workflow/world-postgres in the app.");
81
+ }
82
+ options.virtual[MIGRATIONS_MODULE_ID] = `export default ${JSON.stringify(migrations ?? null)};`;
83
+ options.plugins.push(here("./nitro-plugin.js"));
84
+ options.handlers.push({ route: FLOW_ROUTE, middleware: true, handler: here("./nitro-flow-guard.js") });
85
+ // Rendered when the bundle is built, after `workflow/nitro` has written the
86
+ // build's manifest (its `build:before` hook).
87
+ const { rootDir, buildDir } = options;
88
+ options.virtual[INSPECT_INFO_MODULE_ID] = async () => `export default ${JSON.stringify(await readInspectInfo(rootDir, buildDir))};`;
89
+ options.handlers.push({ route: `${INSPECT_ROUTE}/**`, handler: here("./nitro-inspect.js") });
90
+ },
91
+ };
92
+ export default unfazedWorkflowNitro;
93
+ /**
94
+ * The app's `workflow` version and the workflows in the manifest
95
+ * `workflow/nitro` writes to `<buildDir>/workflow/manifest.json`
96
+ * (`workflows: { [file]: { [name]: { workflowId } } }`).
97
+ */
98
+ export async function readInspectInfo(rootDir, buildDir) {
99
+ let workflow = null;
100
+ try {
101
+ const entry = createRequire(join(rootDir, "package.json")).resolve("workflow");
102
+ workflow = (await readPackageVersion(entry, "workflow")) ?? null;
103
+ }
104
+ catch {
105
+ // not resolvable from the app: reported as unknown
106
+ }
107
+ const workflows = new Set();
108
+ try {
109
+ const manifest = JSON.parse(await readFile(join(buildDir, "workflow", "manifest.json"), "utf8"));
110
+ for (const entries of Object.values(manifest.workflows ?? {})) {
111
+ for (const entry of Object.values(entries ?? {})) {
112
+ if (typeof entry?.workflowId === "string")
113
+ workflows.add(entry.workflowId);
114
+ }
115
+ }
116
+ }
117
+ catch {
118
+ // no manifest: an empty list
119
+ }
120
+ return { workflow, workflows: [...workflows].sort() };
121
+ }
122
+ async function readAppMigrations(rootDir) {
123
+ try {
124
+ const entry = createRequire(join(rootDir, "package.json")).resolve("@workflow/world-postgres");
125
+ return await readWorldPostgresMigrations(entry);
126
+ }
127
+ catch {
128
+ return undefined;
129
+ }
130
+ }
131
+ /**
132
+ * The presets Nitro picks by itself from the build machine's runtime when it
133
+ * detects no hosting provider, and the export condition each adds.
134
+ */
135
+ const RUNTIME_DEFAULT_PRESETS = {
136
+ "node-server": undefined,
137
+ bun: "bun",
138
+ "deno-server": "deno",
139
+ };
140
+ /** A preset the build chose explicitly, and where, or undefined when Nitro picked it. */
141
+ export function explicitPreset(options, env = process.env) {
142
+ if (env.NITRO_PRESET)
143
+ return { preset: env.NITRO_PRESET, from: "NITRO_PRESET" };
144
+ if (env.SERVER_PRESET)
145
+ return { preset: env.SERVER_PRESET, from: "SERVER_PRESET" };
146
+ if (options._config?.preset)
147
+ return { preset: options._config.preset, from: "the options Nitro was created with" };
148
+ // Only a config file's layer counts (nitro.config.*, .nitrorc): the first
149
+ // layer carries the preset Nitro detected itself.
150
+ const layer = options._c12?.layers?.find((l) => l?.configFile && l.config?.preset);
151
+ if (layer?.config?.preset)
152
+ return { preset: layer.config.preset, from: layer.configFile };
153
+ return undefined;
154
+ }
155
+ /**
156
+ * Nitro resolves its preset before modules run, so a module cannot switch
157
+ * presets; it can give the build what the `standard` preset sets. Against
158
+ * the presets Nitro picks by itself (`node-server`, `bun`, `deno-server`)
159
+ * that is: the entry (export `{ fetch }` instead of starting a server: under
160
+ * the `bun` preset the bundle would serve the port itself, with Bun's 10 s
161
+ * idle timeout cutting every quiet stream), not serving static files from
162
+ * disk, no runtime export condition, and the aliases that give h3 srvx's
163
+ * runtime-neutral build (under `node-server`'s `node` condition h3 answers
164
+ * with srvx's Node `Response` stand-in, which `Bun.serve` refuses).
165
+ * `serveStatic: "inline"` stays: it embeds the public assets in the bundle
166
+ * and reads nothing from disk, which is how a service VM (the bundle and
167
+ * nothing next to it) can serve a UI.
168
+ *
169
+ * A preset chosen explicitly, or a hosting provider's preset Nitro detected,
170
+ * fails the build: its output is not something unfazed can serve.
171
+ */
172
+ async function applyStandardOutput(nitro, log) {
173
+ const { options } = nitro;
174
+ const explicit = explicitPreset(options);
175
+ if (options.preset === "standard")
176
+ return;
177
+ const fix = 'Remove the preset so this module builds "standard", or set preset: "standard" in nitro.config.ts.';
178
+ if (explicit) {
179
+ throw new Error(`unfazed/workflow/nitro: this build uses Nitro preset "${options.preset}" (chosen in ${explicit.from}). ` +
180
+ 'unfazed serves a bundle that default-exports { fetch }, which is Nitro\'s "standard" preset; ' +
181
+ `"${options.preset}" builds something else (a bundle that starts its own server, whose idle timeout ` +
182
+ `cuts workflow streams). ${fix}`);
183
+ }
184
+ if (!(options.preset in RUNTIME_DEFAULT_PRESETS)) {
185
+ throw new Error(`unfazed/workflow/nitro: Nitro chose preset "${options.preset}" for this build environment, ` +
186
+ 'and unfazed serves only Nitro\'s "standard" output (a bundle that default-exports { fetch }). ' +
187
+ 'Set preset: "standard" in nitro.config.ts.');
188
+ }
189
+ let standardEntry;
190
+ try {
191
+ const { presetsDir } = (await import("nitro/meta"));
192
+ standardEntry = join(presetsDir, "standard", "runtime", "server");
193
+ }
194
+ catch (error) {
195
+ throw new Error(`unfazed/workflow/nitro: cannot locate Nitro's "standard" preset (${error instanceof Error ? error.message : String(error)}). ` +
196
+ 'Set preset: "standard" in nitro.config.ts.');
197
+ }
198
+ const detected = options.preset;
199
+ options.entry = standardEntry;
200
+ if (options.serveStatic !== "inline")
201
+ options.serveStatic = false;
202
+ const condition = RUNTIME_DEFAULT_PRESETS[detected];
203
+ if (condition && options.exportConditions) {
204
+ options.exportConditions = options.exportConditions.filter((c) => c !== condition);
205
+ }
206
+ // As the standard preset sets them; the subpaths map to themselves so the
207
+ // bare `srvx` alias does not rewrite them.
208
+ options.alias = { ...options.alias, ...STANDARD_ALIASES };
209
+ options.commands = { ...options.commands, preview: "npx srvx --prod ./" };
210
+ options.preset = "standard";
211
+ log.info(`unfazed/workflow: building Nitro's "standard" output (Nitro detected "${detected}"; no preset was chosen).`);
212
+ }
213
+ //# sourceMappingURL=nitro.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nitro.js","sourceRoot":"","sources":["../../src/workflow/nitro.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `unfazed/workflow/nitro` — a Nitro (v3) module that installs the unfazed\n * World in production builds.\n *\n * ```ts\n * // nitro.config.ts\n * export default defineConfig({\n * preset: \"standard\",\n * modules: [\"workflow/nitro\", \"unfazed/workflow/nitro\"],\n * routes: { \"/**\": \"./src/app.ts\" },\n * });\n * ```\n *\n * In a production build it:\n * - registers a server plugin that calls `setWorld(createWorld())` while the\n * app initialises, before the first request (the World is never left to\n * the runtime's `WORKFLOW_TARGET_WORLD` lookup, which needs node_modules);\n * - embeds the SQL migrations of the app's own `@workflow/world-postgres`,\n * so the schema always matches the storage code that is bundled;\n * - guards `/.well-known/workflow/v1/flow`: anything without the platform's\n * queue token gets 401 before the Workflow SDK sees it (including its\n * unauthenticated `?__health` probe);\n * - mounts the workflow inspection route the unfazed dashboard and MCP read\n * runs through (`/.well-known/workflow/v1/unfazed/inspect/v1/*`, the same\n * queue token, see `createInspectHandler`), with the build's workflow list\n * and Workflow SDK version;\n * - sets `noExternals: true`, because a service VM has no node_modules;\n * - builds Nitro's `standard` output (a bundle that default-exports\n * `{ fetch }`, which the unfazed runtime serves) whenever no preset was\n * chosen, whatever Nitro detected on the build machine: `node-server`\n * under Node, `bun` under Bun (the unfazed build VM), `deno-server` under\n * Deno. A preset chosen explicitly (`preset` in nitro.config.ts or\n * .nitrorc, `NITRO_PRESET`, `SERVER_PRESET`) other than `standard` fails\n * the build with a message saying so: those bundles start a server of\n * their own, which unfazed cannot serve properly. Static files are served\n * only when `serveStatic: \"inline\"` embeds them in the bundle.\n *\n * `nitro dev` is left untouched, so local development keeps Workflow SDK's\n * Local World.\n */\nimport { createRequire } from \"node:module\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { readFile } from \"node:fs/promises\";\nimport { readPackageVersion, readWorldPostgresMigrations } from \"./migration-files.js\";\n\n/** The aliases of Nitro's `standard` preset (nitro 3.0.260903-beta). */\nconst STANDARD_ALIASES: Record<string, string> = {\n srvx: \"srvx/generic\",\n \"srvx/bun\": \"srvx/bun\",\n \"srvx/deno\": \"srvx/deno\",\n \"srvx/node\": \"srvx/node\",\n \"srvx/generic\": \"srvx/generic\",\n \"srvx/tracing\": \"srvx/tracing\",\n \"srvx/body-limit\": \"srvx/body-limit\",\n};\n\n/** Virtual module the plugin reads the app's migrations from. */\nexport const MIGRATIONS_MODULE_ID = \"#unfazed/workflow-migrations\";\n/** The Workflow SDK's flow route, which the guard protects. */\nexport const FLOW_ROUTE = \"/.well-known/workflow/v1/flow\";\n/** Virtual module the inspection route reads the build's workflow list and SDK version from. */\nexport const INSPECT_INFO_MODULE_ID = \"#unfazed/workflow-inspect-info\";\n/** `INSPECT_BASE_PATH` (inspect.ts), repeated so loading this module at build time loads no SDK code. */\nexport const INSPECT_ROUTE = \"/.well-known/workflow/v1/unfazed/inspect/v1\";\n\n/** The slice of Nitro v3's `Nitro` this module reads and writes. */\ninterface NitroLike {\n options: {\n dev: boolean;\n preset: string;\n rootDir: string;\n buildDir: string;\n noExternals?: boolean | unknown[];\n plugins: string[];\n handlers: { route?: string; handler: string; middleware?: boolean }[];\n virtual: Record<string, string | (() => string | Promise<string>)>;\n entry?: string;\n serveStatic?: boolean | string;\n alias: Record<string, string>;\n exportConditions?: string[];\n commands?: Record<string, string | undefined>;\n _config?: { preset?: string };\n _c12?: { layers?: { config?: { preset?: string } | null; configFile?: string }[] };\n };\n logger?: { info(message: string): void; warn(message: string): void };\n}\n\nconst here = (file: string) => fileURLToPath(new URL(file, import.meta.url));\n\nconst unfazedWorkflowNitro = {\n name: \"unfazed/workflow/nitro\",\n async setup(input: { options: object }): Promise<void> {\n const nitro = input as unknown as NitroLike;\n const { options } = nitro;\n const log = nitro.logger ?? console;\n if (options.dev) {\n log.info(\"unfazed/workflow: `nitro dev` keeps Workflow SDK's Local World; production builds use the unfazed World.\");\n return;\n }\n\n options.noExternals = true;\n await applyStandardOutput(nitro, log);\n\n const migrations = await readAppMigrations(options.rootDir);\n if (!migrations) {\n log.warn(\n \"unfazed/workflow: cannot read the app's @workflow/world-postgres migrations; \" +\n \"using the copy embedded in unfazed. Install @workflow/world-postgres in the app.\",\n );\n }\n options.virtual[MIGRATIONS_MODULE_ID] = `export default ${JSON.stringify(migrations ?? null)};`;\n options.plugins.push(here(\"./nitro-plugin.js\"));\n options.handlers.push({ route: FLOW_ROUTE, middleware: true, handler: here(\"./nitro-flow-guard.js\") });\n\n // Rendered when the bundle is built, after `workflow/nitro` has written the\n // build's manifest (its `build:before` hook).\n const { rootDir, buildDir } = options;\n options.virtual[INSPECT_INFO_MODULE_ID] = async () =>\n `export default ${JSON.stringify(await readInspectInfo(rootDir, buildDir))};`;\n options.handlers.push({ route: `${INSPECT_ROUTE}/**`, handler: here(\"./nitro-inspect.js\") });\n },\n};\n\nexport default unfazedWorkflowNitro;\n\n/** What the inspection route's `info` reports about this build. */\nexport interface InspectBuildInfo {\n /** The app's Workflow SDK version, if it could be read. */\n workflow: string | null;\n /** The workflow machine names in the build's manifest. */\n workflows: string[];\n}\n\n/**\n * The app's `workflow` version and the workflows in the manifest\n * `workflow/nitro` writes to `<buildDir>/workflow/manifest.json`\n * (`workflows: { [file]: { [name]: { workflowId } } }`).\n */\nexport async function readInspectInfo(rootDir: string, buildDir: string): Promise<InspectBuildInfo> {\n let workflow: string | null = null;\n try {\n const entry = createRequire(join(rootDir, \"package.json\")).resolve(\"workflow\");\n workflow = (await readPackageVersion(entry, \"workflow\")) ?? null;\n } catch {\n // not resolvable from the app: reported as unknown\n }\n const workflows = new Set<string>();\n try {\n const manifest = JSON.parse(await readFile(join(buildDir, \"workflow\", \"manifest.json\"), \"utf8\")) as {\n workflows?: Record<string, Record<string, { workflowId?: unknown }>>;\n };\n for (const entries of Object.values(manifest.workflows ?? {})) {\n for (const entry of Object.values(entries ?? {})) {\n if (typeof entry?.workflowId === \"string\") workflows.add(entry.workflowId);\n }\n }\n } catch {\n // no manifest: an empty list\n }\n return { workflow, workflows: [...workflows].sort() };\n}\n\nasync function readAppMigrations(rootDir: string) {\n try {\n const entry = createRequire(join(rootDir, \"package.json\")).resolve(\"@workflow/world-postgres\");\n return await readWorldPostgresMigrations(entry);\n } catch {\n return undefined;\n }\n}\n\n/**\n * The presets Nitro picks by itself from the build machine's runtime when it\n * detects no hosting provider, and the export condition each adds.\n */\nconst RUNTIME_DEFAULT_PRESETS: Record<string, string | undefined> = {\n \"node-server\": undefined,\n bun: \"bun\",\n \"deno-server\": \"deno\",\n};\n\n/** A preset the build chose explicitly, and where, or undefined when Nitro picked it. */\nexport function explicitPreset(\n options: Pick<NitroLike[\"options\"], \"_config\" | \"_c12\">,\n env: Record<string, string | undefined> = process.env,\n): { preset: string; from: string } | undefined {\n if (env.NITRO_PRESET) return { preset: env.NITRO_PRESET, from: \"NITRO_PRESET\" };\n if (env.SERVER_PRESET) return { preset: env.SERVER_PRESET, from: \"SERVER_PRESET\" };\n if (options._config?.preset) return { preset: options._config.preset, from: \"the options Nitro was created with\" };\n // Only a config file's layer counts (nitro.config.*, .nitrorc): the first\n // layer carries the preset Nitro detected itself.\n const layer = options._c12?.layers?.find((l) => l?.configFile && l.config?.preset);\n if (layer?.config?.preset) return { preset: layer.config.preset, from: layer.configFile! };\n return undefined;\n}\n\n/**\n * Nitro resolves its preset before modules run, so a module cannot switch\n * presets; it can give the build what the `standard` preset sets. Against\n * the presets Nitro picks by itself (`node-server`, `bun`, `deno-server`)\n * that is: the entry (export `{ fetch }` instead of starting a server: under\n * the `bun` preset the bundle would serve the port itself, with Bun's 10 s\n * idle timeout cutting every quiet stream), not serving static files from\n * disk, no runtime export condition, and the aliases that give h3 srvx's\n * runtime-neutral build (under `node-server`'s `node` condition h3 answers\n * with srvx's Node `Response` stand-in, which `Bun.serve` refuses).\n * `serveStatic: \"inline\"` stays: it embeds the public assets in the bundle\n * and reads nothing from disk, which is how a service VM (the bundle and\n * nothing next to it) can serve a UI.\n *\n * A preset chosen explicitly, or a hosting provider's preset Nitro detected,\n * fails the build: its output is not something unfazed can serve.\n */\nasync function applyStandardOutput(nitro: NitroLike, log: Pick<Console, \"info\">): Promise<void> {\n const { options } = nitro;\n const explicit = explicitPreset(options);\n if (options.preset === \"standard\") return;\n const fix = 'Remove the preset so this module builds \"standard\", or set preset: \"standard\" in nitro.config.ts.';\n if (explicit) {\n throw new Error(\n `unfazed/workflow/nitro: this build uses Nitro preset \"${options.preset}\" (chosen in ${explicit.from}). ` +\n 'unfazed serves a bundle that default-exports { fetch }, which is Nitro\\'s \"standard\" preset; ' +\n `\"${options.preset}\" builds something else (a bundle that starts its own server, whose idle timeout ` +\n `cuts workflow streams). ${fix}`,\n );\n }\n if (!(options.preset in RUNTIME_DEFAULT_PRESETS)) {\n throw new Error(\n `unfazed/workflow/nitro: Nitro chose preset \"${options.preset}\" for this build environment, ` +\n 'and unfazed serves only Nitro\\'s \"standard\" output (a bundle that default-exports { fetch }). ' +\n 'Set preset: \"standard\" in nitro.config.ts.',\n );\n }\n let standardEntry: string;\n try {\n const { presetsDir } = (await import(\"nitro/meta\")) as { presetsDir: string };\n standardEntry = join(presetsDir, \"standard\", \"runtime\", \"server\");\n } catch (error) {\n throw new Error(\n `unfazed/workflow/nitro: cannot locate Nitro's \"standard\" preset (${error instanceof Error ? error.message : String(error)}). ` +\n 'Set preset: \"standard\" in nitro.config.ts.',\n );\n }\n const detected = options.preset;\n options.entry = standardEntry;\n if (options.serveStatic !== \"inline\") options.serveStatic = false;\n const condition = RUNTIME_DEFAULT_PRESETS[detected];\n if (condition && options.exportConditions) {\n options.exportConditions = options.exportConditions.filter((c) => c !== condition);\n }\n // As the standard preset sets them; the subpaths map to themselves so the\n // bare `srvx` alias does not rewrite them.\n options.alias = { ...options.alias, ...STANDARD_ALIASES };\n options.commands = { ...options.commands, preview: \"npx srvx --prod ./\" };\n options.preset = \"standard\";\n log.info(`unfazed/workflow: building Nitro's \"standard\" output (Nitro detected \"${detected}\"; no preset was chosen).`);\n}\n"]}
@@ -0,0 +1,27 @@
1
+ import { type WorkflowEnv } from "./config.js";
2
+ /** What {@link applyPublicUrl} did, and why. */
3
+ export type PublicUrlResult = {
4
+ applied: true;
5
+ host: string;
6
+ } | {
7
+ applied: false;
8
+ reason: "vercel_url_set" | "no_public_url" | "path_form" | "not_https" | "invalid";
9
+ };
10
+ /**
11
+ * Points Workflow SDK at the app's public address, so `createWebhook().url`
12
+ * and `getWorkflowMetadata().url` are right with no app code.
13
+ *
14
+ * The SDK (5.x) builds its base URL as `https://${VERCEL_URL}` when
15
+ * `VERCEL_URL` is set, else `http://localhost:<port>`, and offers no other
16
+ * setting. So when `VERCEL_URL` is unset and the public URL is a bare https
17
+ * host (a custom domain or `https://<alias>.unfz.to`), this sets
18
+ * `VERCEL_URL` to that host in this process's environment. Inside Workflow SDK
19
+ * that variable decides only this base URL (and skips probing for the local
20
+ * port, which the base URL then no longer needs).
21
+ *
22
+ * A path-form public URL (`https://app.unfazed.dev/<alias>`) cannot be
23
+ * expressed that way: the SDK's base path is fixed at build time and the Nitro
24
+ * integration sets none. Such apps build webhook URLs from `UNFZ_PUBLIC_URL`
25
+ * themselves (see the README). An explicit `VERCEL_URL` is never touched.
26
+ */
27
+ export declare function applyPublicUrl(publicUrl: string | undefined, target?: WorkflowEnv): PublicUrlResult;
@@ -0,0 +1,45 @@
1
+ import { envValue } from "./config.js";
2
+ import { logOnce } from "./state.js";
3
+ /**
4
+ * Points Workflow SDK at the app's public address, so `createWebhook().url`
5
+ * and `getWorkflowMetadata().url` are right with no app code.
6
+ *
7
+ * The SDK (5.x) builds its base URL as `https://${VERCEL_URL}` when
8
+ * `VERCEL_URL` is set, else `http://localhost:<port>`, and offers no other
9
+ * setting. So when `VERCEL_URL` is unset and the public URL is a bare https
10
+ * host (a custom domain or `https://<alias>.unfz.to`), this sets
11
+ * `VERCEL_URL` to that host in this process's environment. Inside Workflow SDK
12
+ * that variable decides only this base URL (and skips probing for the local
13
+ * port, which the base URL then no longer needs).
14
+ *
15
+ * A path-form public URL (`https://app.unfazed.dev/<alias>`) cannot be
16
+ * expressed that way: the SDK's base path is fixed at build time and the Nitro
17
+ * integration sets none. Such apps build webhook URLs from `UNFZ_PUBLIC_URL`
18
+ * themselves (see the README). An explicit `VERCEL_URL` is never touched.
19
+ */
20
+ export function applyPublicUrl(publicUrl, target = process.env) {
21
+ if (envValue(target, "VERCEL_URL"))
22
+ return { applied: false, reason: "vercel_url_set" };
23
+ if (!publicUrl || publicUrl.trim() === "")
24
+ return { applied: false, reason: "no_public_url" };
25
+ let url;
26
+ try {
27
+ url = new URL(publicUrl);
28
+ }
29
+ catch {
30
+ return { applied: false, reason: "invalid" };
31
+ }
32
+ if (url.username || url.password)
33
+ return { applied: false, reason: "invalid" };
34
+ // The SDK always prefixes `https://`.
35
+ if (url.protocol !== "https:")
36
+ return { applied: false, reason: "not_https" };
37
+ if (url.pathname.replace(/\/+$/, "") !== "" || url.search || url.hash) {
38
+ logOnce(`path-form:${publicUrl}`, () => console.warn(`[unfazed/workflow] UNFZ_PUBLIC_URL (${publicUrl}) has a path, which Workflow SDK cannot put into ` +
39
+ "createWebhook().url; build webhook URLs from UNFZ_PUBLIC_URL and the webhook token instead."));
40
+ return { applied: false, reason: "path_form" };
41
+ }
42
+ target.VERCEL_URL = url.host;
43
+ return { applied: true, host: url.host };
44
+ }
45
+ //# sourceMappingURL=public-url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-url.js","sourceRoot":"","sources":["../../src/workflow/public-url.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAoB,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAUrC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,cAAc,CAAC,SAA6B,EAAE,SAAsB,OAAO,CAAC,GAAG;IAC7F,IAAI,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;IACxF,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IAC9F,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAC/C,CAAC;IACD,IAAI,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,QAAQ;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAC/E,sCAAsC;IACtC,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IAC9E,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,EAAE,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;QACtE,OAAO,CAAC,aAAa,SAAS,EAAE,EAAE,GAAG,EAAE,CACrC,OAAO,CAAC,IAAI,CACV,uCAAuC,SAAS,mDAAmD;YACjG,6FAA6F,CAChG,CACF,CAAC;QACF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IACjD,CAAC;IACD,MAAM,CAAC,UAAU,GAAG,GAAG,CAAC,IAAI,CAAC;IAC7B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;AAC3C,CAAC","sourcesContent":["import { envValue, type WorkflowEnv } from \"./config.js\";\nimport { logOnce } from \"./state.js\";\n\n/** What {@link applyPublicUrl} did, and why. */\nexport type PublicUrlResult =\n | { applied: true; host: string }\n | {\n applied: false;\n reason: \"vercel_url_set\" | \"no_public_url\" | \"path_form\" | \"not_https\" | \"invalid\";\n };\n\n/**\n * Points Workflow SDK at the app's public address, so `createWebhook().url`\n * and `getWorkflowMetadata().url` are right with no app code.\n *\n * The SDK (5.x) builds its base URL as `https://${VERCEL_URL}` when\n * `VERCEL_URL` is set, else `http://localhost:<port>`, and offers no other\n * setting. So when `VERCEL_URL` is unset and the public URL is a bare https\n * host (a custom domain or `https://<alias>.unfz.to`), this sets\n * `VERCEL_URL` to that host in this process's environment. Inside Workflow SDK\n * that variable decides only this base URL (and skips probing for the local\n * port, which the base URL then no longer needs).\n *\n * A path-form public URL (`https://app.unfazed.dev/<alias>`) cannot be\n * expressed that way: the SDK's base path is fixed at build time and the Nitro\n * integration sets none. Such apps build webhook URLs from `UNFZ_PUBLIC_URL`\n * themselves (see the README). An explicit `VERCEL_URL` is never touched.\n */\nexport function applyPublicUrl(publicUrl: string | undefined, target: WorkflowEnv = process.env): PublicUrlResult {\n if (envValue(target, \"VERCEL_URL\")) return { applied: false, reason: \"vercel_url_set\" };\n if (!publicUrl || publicUrl.trim() === \"\") return { applied: false, reason: \"no_public_url\" };\n let url: URL;\n try {\n url = new URL(publicUrl);\n } catch {\n return { applied: false, reason: \"invalid\" };\n }\n if (url.username || url.password) return { applied: false, reason: \"invalid\" };\n // The SDK always prefixes `https://`.\n if (url.protocol !== \"https:\") return { applied: false, reason: \"not_https\" };\n if (url.pathname.replace(/\\/+$/, \"\") !== \"\" || url.search || url.hash) {\n logOnce(`path-form:${publicUrl}`, () =>\n console.warn(\n `[unfazed/workflow] UNFZ_PUBLIC_URL (${publicUrl}) has a path, which Workflow SDK cannot put into ` +\n \"createWebhook().url; build webhook URLs from UNFZ_PUBLIC_URL and the webhook token instead.\",\n ),\n );\n return { applied: false, reason: \"path_form\" };\n }\n target.VERCEL_URL = url.host;\n return { applied: true, host: url.host };\n}\n"]}