@voyantjs/hono 0.28.1 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.d.ts CHANGED
@@ -1,7 +1,35 @@
1
1
  import { Hono } from "hono";
2
2
  import type { VoyantAppConfig, VoyantBindings, VoyantVariables } from "./types.js";
3
+ /**
4
+ * App handle returned alongside the Hono instance. Carries `ready()` for
5
+ * headless / sibling-process deployments that need to fire the lazy
6
+ * bootstrap before the first HTTP request — workflow runtimes (Mode 2's
7
+ * sibling-process pattern, tests) call this so the time wheel and
8
+ * manifest registration kick off without traffic.
9
+ *
10
+ * Returned via the augmented Hono instance: `app.ready` is attached
11
+ * directly so the existing call sites (which destructure / pass `app`
12
+ * around) keep working without a wrapper.
13
+ */
14
+ export interface VoyantAppExtensions<TBindings = unknown> {
15
+ /**
16
+ * Resolves once the lazy bootstrap completes. Idempotent — multiple
17
+ * calls share the same promise. Use from tests + Mode 2 sibling
18
+ * processes where no request will arrive to trigger boot. See
19
+ * architecture doc §18 + §18.1.
20
+ *
21
+ * Accepts the runtime bindings that the bootstrap should run with. For
22
+ * binding-dependent configs (e.g. Mode 1 / Cloudflare, where the driver
23
+ * factory reads DO + KV bindings off `env`) callers MUST pass the real
24
+ * bindings; otherwise the memoized bootstrap promise locks in a driver
25
+ * built from `{}` and every later request reuses that broken instance.
26
+ * Mode 2 / InMemory drivers ignore bindings, so the no-arg form is safe
27
+ * there (defaults to `{}` for back-compat with tests).
28
+ */
29
+ ready(bindings?: TBindings): Promise<void>;
30
+ }
3
31
  export declare function createApp<TBindings extends VoyantBindings>(config: VoyantAppConfig<TBindings>): Hono<{
4
32
  Bindings: TBindings;
5
33
  Variables: VoyantVariables;
6
- }>;
34
+ }> & VoyantAppExtensions<TBindings>;
7
35
  //# sourceMappingURL=app.d.ts.map
package/dist/app.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAS3B,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAoBlF,wBAAgB,SAAS,CAAC,SAAS,SAAS,cAAc,EACxD,MAAM,EAAE,eAAe,CAAC,SAAS,CAAC,GACjC,IAAI,CAAC;IAAE,QAAQ,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,eAAe,CAAA;CAAE,CAAC,CAiJ3D"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAS3B,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAoBlF;;;;;;;;;;GAUG;AACH,MAAM,WAAW,mBAAmB,CAAC,SAAS,GAAG,OAAO;IACtD;;;;;;;;;;;;;OAaG;IACH,KAAK,CAAC,QAAQ,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3C;AAED,wBAAgB,SAAS,CAAC,SAAS,SAAS,cAAc,EACxD,MAAM,EAAE,eAAe,CAAC,SAAS,CAAC,GACjC,IAAI,CAAC;IAAE,QAAQ,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,eAAe,CAAA;CAAE,CAAC,GAAG,mBAAmB,CAAC,SAAS,CAAC,CAuP5F"}
package/dist/app.js CHANGED
@@ -1,4 +1,5 @@
1
- import { createContainer, createEventBus, createQueryRunner } from "@voyantjs/core";
1
+ import { createContainer, createEventBus, createQueryRunner, } from "@voyantjs/core";
2
+ import { buildManifest } from "@voyantjs/workflows/events";
2
3
  import { Hono } from "hono";
3
4
  import { requireAuth } from "./middleware/auth.js";
4
5
  import { cors } from "./middleware/cors.js";
@@ -40,11 +41,85 @@ export function createApp(config) {
40
41
  for (const sub of expanded?.subscribers ?? []) {
41
42
  eventBus.subscribe(sub.event, sub.handler);
42
43
  }
44
+ // ---- Workflow runtime wiring (synchronous setup; manifest registration
45
+ // + EventBus forwarder run inside the lazy bootstrap below) ----
46
+ //
47
+ // We collect `workflows` + `eventFilters` from every module and plugin
48
+ // here so the failure mode for "duplicate workflow id across modules"
49
+ // surfaces at construction time (per architecture doc §18, the workflow
50
+ // runtime is fail-closed).
51
+ const collectedWorkflows = [];
52
+ const collectedFilters = [];
53
+ for (const mod of allModules) {
54
+ if (mod.module.workflows)
55
+ collectedWorkflows.push(...mod.module.workflows);
56
+ if (mod.module.eventFilters)
57
+ collectedFilters.push(...mod.module.eventFilters);
58
+ }
59
+ for (const plugin of config.plugins ?? []) {
60
+ if (plugin.workflows)
61
+ collectedWorkflows.push(...plugin.workflows);
62
+ if (plugin.eventFilters)
63
+ collectedFilters.push(...plugin.eventFilters);
64
+ }
65
+ // Validate duplicate workflow ids across modules + plugins. Same id from
66
+ // re-imports (HMR / shared bundles) is fine because identity is by id —
67
+ // we only flag genuinely-different definitions sharing an id.
68
+ if (config.workflows && collectedWorkflows.length > 0) {
69
+ const seen = new Map();
70
+ for (const wf of collectedWorkflows) {
71
+ const existing = seen.get(wf.id);
72
+ if (existing && existing !== wf) {
73
+ throw new Error(`[voyant] duplicate workflow id "${wf.id}" registered by multiple modules/plugins. ` +
74
+ `Workflow ids must be unique across the app — use a module-scoped prefix ` +
75
+ `(e.g. "${wf.id.includes(".") ? wf.id : `<module>.${wf.id}`}").`);
76
+ }
77
+ seen.set(wf.id, wf);
78
+ }
79
+ }
80
+ // Workflow driver construction is **deferred** to the lazy bootstrap
81
+ // path so CF-edge users (whose driver options come from `env.*`
82
+ // bindings only available at request time) can pass a function-of-
83
+ // bindings shape — `(env) => createCloudflareEdgeDriver({...})` —
84
+ // rather than constructing at module-load time. Mode 2 / InMemory
85
+ // users pass a direct factory; the framework adapts both shapes.
86
+ // See reviewer feedback P2.1 + architecture doc §6.3.
87
+ // biome-ignore lint/suspicious/noExplicitAny: WorkflowDriver shape varies across driver implementations
88
+ let workflowDriver;
43
89
  let bootstrapPromise = null;
44
90
  function ensureRuntimeBootstrapped(bindings) {
45
91
  if (!bootstrapPromise) {
46
92
  bootstrapPromise = (async () => {
47
93
  const ctx = { bindings, container, eventBus };
94
+ // ---- Workflow runtime FIRST — fail-closed manifest registration
95
+ // and EventBus forwarder must be in place before any module
96
+ // bootstrap can emit. Otherwise a `module.bootstrap` that
97
+ // emits an event during its own bootstrap would route through
98
+ // a bus with no workflow forwarder yet, silently losing the
99
+ // event. Per architecture doc §21.22 + reviewer feedback P2.3.
100
+ if (config.workflows) {
101
+ // `driver` is always a function-of-bindings (per
102
+ // VoyantWorkflowsConfig — see types.ts + reviewer feedback P2.1).
103
+ // Mode 2 / InMemory users wrap with `() => createXxxDriver({...})`.
104
+ // CF-edge users use `(env) => createCloudflareEdgeDriver({ env.* })`.
105
+ // We invoke with bindings, then the resulting DriverFactory
106
+ // with framework deps.
107
+ const factoryDeps = {
108
+ services: containerToServiceResolver(container),
109
+ logger: makeFrameworkLogger(config.logger),
110
+ };
111
+ const factory = config.workflows.driver(bindings);
112
+ workflowDriver = factory(factoryDeps);
113
+ await wireWorkflowRuntime({
114
+ modules: allModules.map((m) => m.module),
115
+ collectedWorkflows,
116
+ collectedFilters,
117
+ driver: workflowDriver,
118
+ environment: config.workflows.environment ?? "development",
119
+ projectId: config.workflows.projectId ?? "default",
120
+ eventBus,
121
+ });
122
+ }
48
123
  // Run each bootstrap in isolation — a single failing plugin/module/extension
49
124
  // must not poison the cached promise and kill the whole app's request pipeline.
50
125
  const runIsolated = async (label, fn) => {
@@ -80,7 +155,18 @@ export function createApp(config) {
80
155
  if (query) {
81
156
  c.set("query", query);
82
157
  }
158
+ // Bootstrap (fires once, idempotent) — resolves the workflow driver
159
+ // with c.env-supplied bindings on the first request, so deferred
160
+ // driver construction sees real runtime bindings (reviewer P2.1).
83
161
  await ensureRuntimeBootstrapped(c.env);
162
+ if (workflowDriver) {
163
+ // Surfaced on `c.var.workflowDriver` so HTTP route handlers can
164
+ // call `driver.trigger(...)` directly without re-resolving from
165
+ // the container. Also used by the optional HTTP ingest adapter
166
+ // (`mountHttpIngestAdapter` from `@voyantjs/workflows/http-ingest`).
167
+ ;
168
+ c.set("workflowDriver", workflowDriver);
169
+ }
84
170
  return next();
85
171
  });
86
172
  // Request ID header
@@ -134,5 +220,137 @@ export function createApp(config) {
134
220
  if (config.additionalRoutes) {
135
221
  config.additionalRoutes(app);
136
222
  }
137
- return app;
223
+ // Attach `ready()` directly to the Hono instance. Fires the lazy
224
+ // bootstrap with the supplied bindings (or `{}` for back-compat with
225
+ // Mode 2 / InMemory drivers that ignore them). Production code never
226
+ // calls `ready()` — the first request triggers the same boot via
227
+ // `ensureRuntimeBootstrapped(c.env)`. Tests + Mode 2 sibling processes
228
+ // use this so the time wheel + manifest registration happen without
229
+ // traffic; CF-edge users that want eager boot must pass the real `env`
230
+ // (otherwise the memoized bootstrap promise locks in a driver built
231
+ // from `{}` and every later request reuses that broken instance).
232
+ const augmented = app;
233
+ augmented.ready = (bindings) => ensureRuntimeBootstrapped(bindings ?? {});
234
+ return augmented;
235
+ }
236
+ /**
237
+ * Build the manifest, register it with the driver, and install a single
238
+ * EventBus subscriber per unique eventType seen across the manifest's
239
+ * filters — that subscriber forwards into `driver.ingestEvent(...)`.
240
+ *
241
+ * The forwarder stamps `metadata.eventId` with a fresh content-derived
242
+ * id when missing, so the framework's idempotency derivation
243
+ * (`${filterId}:${eventId}`) gives stable run-dedup across retries.
244
+ *
245
+ * Failure modes are fail-closed: a manifest registration that throws
246
+ * rejects the bootstrap promise, and the next request sees a 503 with
247
+ * the registration error in the body.
248
+ */
249
+ async function wireWorkflowRuntime(args) {
250
+ // The descriptors collected from modules + plugins use core's structural
251
+ // types (`{ id, eventType }` only — see `EventFilterDescriptor` in core);
252
+ // the manifest builder needs the runtime shape with `.manifest` populated.
253
+ // Validate before casting so a contract-violating plugin fails loudly here
254
+ // instead of crashing on `entry.manifest.id` deep inside the sort.
255
+ const filterEntries = [];
256
+ for (const entry of args.collectedFilters) {
257
+ const candidate = entry;
258
+ if (!candidate.manifest || typeof candidate.manifest.id !== "string") {
259
+ throw new Error(`[voyant] event filter "${entry.id}" (event "${entry.eventType}") is missing the runtime ` +
260
+ `\`manifest\` field. Filters must be produced via \`trigger.on(eventName, { ... })\` from ` +
261
+ `@voyantjs/workflows — the public EventFilterDescriptor is the structural minimum, but ` +
262
+ `createApp() needs the manifest payload to register with the driver.`);
263
+ }
264
+ filterEntries.push(candidate);
265
+ }
266
+ const manifest = await buildManifest({
267
+ projectId: args.projectId,
268
+ environment: args.environment,
269
+ workflows: args.collectedWorkflows.map((w) => ({ id: w.id })),
270
+ eventFilters: filterEntries,
271
+ });
272
+ await args.driver.registerManifest({
273
+ environment: args.environment,
274
+ manifest,
275
+ });
276
+ // Install one EventBus subscriber per unique eventType. Each subscriber
277
+ // forwards the envelope through `driver.ingestEvent(...)`, which
278
+ // routes through the same predicate/mapper machinery the HTTP
279
+ // ingest path uses (see architecture doc §15).
280
+ const eventTypes = new Set(filterEntries.map((f) => f.eventType));
281
+ for (const eventType of eventTypes) {
282
+ args.eventBus.subscribe(eventType, async (envelope) => {
283
+ const stamped = ensureMetadataEventId(envelope);
284
+ try {
285
+ await args.driver.ingestEvent({
286
+ environment: args.environment,
287
+ envelope: stamped,
288
+ });
289
+ }
290
+ catch (err) {
291
+ // Subscribers are observers per the EventBus contract — a misbehaving
292
+ // driver / network glitch must not break the emitter. The driver's
293
+ // own error reporting (logger calls + counter increments) surfaces
294
+ // the failure for ops; we swallow here to preserve the bus contract.
295
+ const message = err instanceof Error ? err.message : String(err);
296
+ console.error(`[voyant] workflow forwarder for "${eventType}" failed: ${message}`);
297
+ }
298
+ });
299
+ }
300
+ }
301
+ /**
302
+ * Adapt the framework's `ModuleContainer` (which has `register`) to a
303
+ * read-only `ServiceResolver` view (`resolve` + `has` only). This is
304
+ * what driver factories receive in their `services` dep.
305
+ */
306
+ function containerToServiceResolver(container) {
307
+ return {
308
+ resolve(name) {
309
+ return container.resolve(name);
310
+ },
311
+ has(name) {
312
+ return container.has(name);
313
+ },
314
+ };
315
+ }
316
+ /**
317
+ * Adapt the framework's optional `LoggerProvider` to the structural
318
+ * `DriverLogger` shape `(level, msg, data?) => void`. When no logger
319
+ * is configured, the adapter routes through `console`.
320
+ */
321
+ function makeFrameworkLogger(loggerProvider) {
322
+ void loggerProvider;
323
+ return (level, msg, data) => {
324
+ const fn = level === "error"
325
+ ? console.error
326
+ : level === "warn"
327
+ ? console.warn
328
+ : level === "debug"
329
+ ? console.debug
330
+ : console.log;
331
+ if (data !== undefined)
332
+ fn(`[voyant] ${msg}`, data);
333
+ else
334
+ fn(`[voyant] ${msg}`);
335
+ };
336
+ }
337
+ function ensureMetadataEventId(envelope) {
338
+ const metadata = envelope.metadata;
339
+ if (metadata !== undefined &&
340
+ metadata !== null &&
341
+ typeof metadata === "object" &&
342
+ typeof metadata.eventId === "string" &&
343
+ (metadata.eventId.length ?? 0) > 0) {
344
+ return envelope;
345
+ }
346
+ const eventId = `evt_${Date.now().toString(36)}_${Math.floor(Math.random() * 1_000_000)
347
+ .toString(36)
348
+ .padStart(4, "0")}`;
349
+ return {
350
+ ...envelope,
351
+ metadata: {
352
+ ...(metadata ?? {}),
353
+ eventId,
354
+ },
355
+ };
138
356
  }
@@ -1,5 +1,5 @@
1
1
  import type { MiddlewareHandler } from "hono";
2
- import type { DbFactory, VoyantAuthIntegration, VoyantBindings, VoyantVariables } from "../types.js";
2
+ import { type DbFactory, type VoyantAuthIntegration, type VoyantBindings, type VoyantVariables } from "../types.js";
3
3
  export declare function requireAuth<TBindings extends VoyantBindings>(dbFactory: DbFactory<TBindings>, opts?: {
4
4
  publicPaths?: string[];
5
5
  auth?: VoyantAuthIntegration<TBindings>;
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAI7C,OAAO,KAAK,EAAE,SAAS,EAAE,qBAAqB,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAsCpG,wBAAgB,WAAW,CAAC,SAAS,SAAS,cAAc,EAC1D,SAAS,EAAE,SAAS,CAAC,SAAS,CAAC,EAC/B,IAAI,CAAC,EAAE;IACL,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,IAAI,CAAC,EAAE,qBAAqB,CAAC,SAAS,CAAC,CAAA;CACxC,GACA,iBAAiB,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,EAAE,eAAe,CAAA;CAC3B,CAAC,CA4ID"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAI7C,OAAO,EACL,KAAK,SAAS,EAEd,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAA;AAsCpB,wBAAgB,WAAW,CAAC,SAAS,SAAS,cAAc,EAC1D,SAAS,EAAE,SAAS,CAAC,SAAS,CAAC,EAC/B,IAAI,CAAC,EAAE;IACL,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,IAAI,CAAC,EAAE,qBAAqB,CAAC,SAAS,CAAC,CAAA;CACxC,GACA,iBAAiB,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,EAAE,eAAe,CAAA;CAC3B,CAAC,CAqJD"}
@@ -2,6 +2,7 @@ import { apikeyTable } from "@voyantjs/db/schema/iam";
2
2
  import { and, eq } from "drizzle-orm";
3
3
  import { sha256Base64Url } from "../auth/crypto.js";
4
4
  import { extractBearerToken, verifySession } from "../auth/session-jwt.js";
5
+ import { resolveDbFactoryResult, } from "../types.js";
5
6
  function permissionsToScopes(permissions) {
6
7
  if (!permissions)
7
8
  return [];
@@ -72,8 +73,8 @@ export function requireAuth(dbFactory, opts) {
72
73
  }
73
74
  // Strategy 2: Core-owned API key support (voy_ prefixed)
74
75
  if (token?.startsWith(API_KEY_PREFIX)) {
76
+ const { db, dispose } = resolveDbFactoryResult(dbFactory(c.env));
75
77
  try {
76
- const db = dbFactory(c.env);
77
78
  const keyHash = await sha256Base64Url(token);
78
79
  const [row] = await db
79
80
  .select()
@@ -128,18 +129,31 @@ export function requireAuth(dbFactory, opts) {
128
129
  catch {
129
130
  // fall through to next strategy
130
131
  }
132
+ finally {
133
+ // Schedule pool teardown AFTER the queries above settle. waitUntil
134
+ // keeps the worker alive for the close handshake.
135
+ if (dispose)
136
+ c.executionCtx.waitUntil?.(dispose());
137
+ }
131
138
  }
132
139
  // Strategy 3: App-provided auth resolution (cookies, provider tokens, etc.)
133
140
  if (opts?.auth?.resolve) {
134
- const resolved = await opts.auth.resolve({
135
- request: c.req.raw,
136
- env: c.env,
137
- db: dbFactory(c.env),
138
- ctx: c.executionCtx,
139
- });
140
- if (resolved?.userId) {
141
- applyAuthContext(c, resolved);
142
- return next();
141
+ const { db: resolveDb, dispose: resolveDispose } = resolveDbFactoryResult(dbFactory(c.env));
142
+ try {
143
+ const resolved = await opts.auth.resolve({
144
+ request: c.req.raw,
145
+ env: c.env,
146
+ db: resolveDb,
147
+ ctx: c.executionCtx,
148
+ });
149
+ if (resolved?.userId) {
150
+ applyAuthContext(c, resolved);
151
+ return next();
152
+ }
153
+ }
154
+ finally {
155
+ if (resolveDispose)
156
+ c.executionCtx.waitUntil?.(resolveDispose());
143
157
  }
144
158
  }
145
159
  // Strategy 4: Generic session-claims bearer token support
@@ -1,5 +1,19 @@
1
1
  import type { MiddlewareHandler } from "hono";
2
- import type { DbFactory, VoyantBindings, VoyantDb } from "../types.js";
2
+ import { type DbFactory, type VoyantBindings, type VoyantDb } from "../types.js";
3
+ /**
4
+ * Resolves the per-request db client and stores it on Hono context.
5
+ *
6
+ * If the factory returns a {@link DisposableDb} (e.g. a
7
+ * `dbFromEnvForApp` that owns a per-request Neon WebSocket Pool), the
8
+ * middleware schedules `dispose()` via `c.executionCtx.waitUntil` so
9
+ * the Pool closes cleanly after the response is sent. Without the
10
+ * scheduled dispose every request would leak its Pool until isolate
11
+ * teardown, which at scale exhausts Neon's connection budget.
12
+ *
13
+ * Factories that return a plain {@link VoyantDb} (e.g. a long-lived
14
+ * postgres-js client cached at the module level) are wired up as
15
+ * before with no cleanup hook.
16
+ */
3
17
  export declare function db<TBindings extends VoyantBindings>(factory: DbFactory<TBindings>): MiddlewareHandler<{
4
18
  Bindings: TBindings;
5
19
  Variables: {
@@ -1 +1 @@
1
- {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/middleware/db.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAE7C,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAEtE,wBAAgB,EAAE,CAAC,SAAS,SAAS,cAAc,EACjD,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,GAC5B,iBAAiB,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,EAAE;QAAE,EAAE,EAAE,QAAQ,CAAA;KAAE,CAAA;CAC5B,CAAC,CAKD"}
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/middleware/db.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAE7C,OAAO,EAAE,KAAK,SAAS,EAAkB,KAAK,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAA;AAYhG;;;;;;;;;;;;;GAaG;AACH,wBAAgB,EAAE,CAAC,SAAS,SAAS,cAAc,EACjD,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,GAC5B,iBAAiB,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,EAAE;QAAE,EAAE,EAAE,QAAQ,CAAA;KAAE,CAAA;CAC5B,CAAC,CAuBD"}
@@ -1,6 +1,41 @@
1
+ import { isDisposableDb } from "../types.js";
2
+ /**
3
+ * Resolves the per-request db client and stores it on Hono context.
4
+ *
5
+ * If the factory returns a {@link DisposableDb} (e.g. a
6
+ * `dbFromEnvForApp` that owns a per-request Neon WebSocket Pool), the
7
+ * middleware schedules `dispose()` via `c.executionCtx.waitUntil` so
8
+ * the Pool closes cleanly after the response is sent. Without the
9
+ * scheduled dispose every request would leak its Pool until isolate
10
+ * teardown, which at scale exhausts Neon's connection budget.
11
+ *
12
+ * Factories that return a plain {@link VoyantDb} (e.g. a long-lived
13
+ * postgres-js client cached at the module level) are wired up as
14
+ * before with no cleanup hook.
15
+ */
1
16
  export function db(factory) {
2
17
  return async (c, next) => {
3
- c.set("db", factory(c.env));
18
+ const result = factory(c.env);
19
+ if (isDisposableDb(result)) {
20
+ c.set("db", result.db);
21
+ try {
22
+ await next();
23
+ }
24
+ finally {
25
+ // `executionCtx` is undefined in unit-test contexts where Hono
26
+ // is invoked directly without a Workers runtime — fall back to
27
+ // an inline await so cleanup still runs.
28
+ const ctx = c.executionCtx;
29
+ if (ctx && typeof ctx.waitUntil === "function") {
30
+ ctx.waitUntil(result.dispose());
31
+ }
32
+ else {
33
+ await result.dispose();
34
+ }
35
+ }
36
+ return;
37
+ }
38
+ c.set("db", result);
4
39
  await next();
5
40
  };
6
41
  }
@@ -1 +1 @@
1
- {"version":3,"file":"error-boundary.d.ts","sourceRoot":"","sources":["../../src/middleware/error-boundary.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAYtD,eAAO,MAAM,SAAS,EAAE,iBAKvB,CAAA;AAID,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,QAAQ,CA+CjE;AAED,eAAO,MAAM,aAAa,EAAE,iBAM3B,CAAA"}
1
+ {"version":3,"file":"error-boundary.d.ts","sourceRoot":"","sources":["../../src/middleware/error-boundary.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAYtD,eAAO,MAAM,SAAS,EAAE,iBAKvB,CAAA;AAID,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,QAAQ,CAiDjE;AAED,eAAO,MAAM,aAAa,EAAE,iBAM3B,CAAA"}
@@ -42,6 +42,8 @@ export function handleApiError(err, c) {
42
42
  method: c.req.method,
43
43
  headers,
44
44
  err: err instanceof Error ? err.message : String(err),
45
+ cause: err instanceof Error && err.cause ? String(err.cause) : undefined,
46
+ stack: err instanceof Error ? err.stack : undefined,
45
47
  });
46
48
  }
47
49
  catch {
@@ -1,5 +1,5 @@
1
1
  import type { MiddlewareHandler } from "hono";
2
- import type { DbFactory, VoyantBindings, VoyantVariables } from "../types.js";
2
+ import { type DbFactory, type VoyantBindings, type VoyantVariables } from "../types.js";
3
3
  /**
4
4
  * Twenty-four hours, in milliseconds. Default TTL for stored idempotency
5
5
  * keys. Tunable per middleware instance.
@@ -63,6 +63,10 @@ export declare function idempotencyKey<TBindings extends VoyantBindings = Voyant
63
63
  }>;
64
64
  /**
65
65
  * Sweep expired idempotency rows. Call from a daily cron.
66
+ *
67
+ * If `dbFactory` returns a `DisposableDb` (e.g. a per-call Neon
68
+ * WebSocket Pool), the sweep awaits `dispose()` before returning so
69
+ * the connection closes cleanly inside the cron handler.
66
70
  */
67
71
  export declare function purgeExpiredIdempotencyKeys<TBindings extends VoyantBindings>(dbFactory: DbFactory<TBindings>, env: TBindings): Promise<{
68
72
  removed: number;
@@ -1 +1 @@
1
- {"version":3,"file":"idempotency-key.d.ts","sourceRoot":"","sources":["../../src/middleware/idempotency-key.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAE7C,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAE7E;;;GAGG;AACH,eAAO,MAAM,0BAA0B,QAAsB,CAAA;AAmB7D,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAClE;AAED,UAAU,sBAAsB;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAC9B;AAED,OAAO,QAAQ,MAAM,CAAC;IACpB,UAAU,kBAAmB,SAAQ,sBAAsB;KAAG;CAC/D;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAAC,SAAS,SAAS,cAAc,GAAG,cAAc,EAC9E,OAAO,GAAE,qBAA0B,GAClC,iBAAiB,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,EAAE,eAAe,CAAA;CAC3B,CAAC,CAkID;AAiBD;;GAEG;AACH,wBAAsB,2BAA2B,CAAC,SAAS,SAAS,cAAc,EAChF,SAAS,EAAE,SAAS,CAAC,SAAS,CAAC,EAC/B,GAAG,EAAE,SAAS,GACb,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAO9B"}
1
+ {"version":3,"file":"idempotency-key.d.ts","sourceRoot":"","sources":["../../src/middleware/idempotency-key.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAE7C,OAAO,EACL,KAAK,SAAS,EAEd,KAAK,cAAc,EAEnB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAA;AAEpB;;;GAGG;AACH,eAAO,MAAM,0BAA0B,QAAsB,CAAA;AAmB7D,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAClE;AAED,UAAU,sBAAsB;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAC9B;AAED,OAAO,QAAQ,MAAM,CAAC;IACpB,UAAU,kBAAmB,SAAQ,sBAAsB;KAAG;CAC/D;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAAC,SAAS,SAAS,cAAc,GAAG,cAAc,EAC9E,OAAO,GAAE,qBAA0B,GAClC,iBAAiB,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,EAAE,eAAe,CAAA;CAC3B,CAAC,CAoID;AAiBD;;;;;;GAMG;AACH,wBAAsB,2BAA2B,CAAC,SAAS,SAAS,cAAc,EAChF,SAAS,EAAE,SAAS,CAAC,SAAS,CAAC,EAC/B,GAAG,EAAE,SAAS,GACb,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAa9B"}
@@ -1,5 +1,6 @@
1
1
  import { infraIdempotencyKeysTable } from "@voyantjs/db/schema/infra";
2
2
  import { and, eq, lt } from "drizzle-orm";
3
+ import { isDisposableDb, } from "../types.js";
3
4
  /**
4
5
  * Twenty-four hours, in milliseconds. Default TTL for stored idempotency
5
6
  * keys. Tunable per middleware instance.
@@ -58,6 +59,8 @@ export function idempotencyKey(options = {}) {
58
59
  const scope = options.scope ?? `${c.req.method} ${new URL(c.req.url).pathname}`;
59
60
  const rawBody = await c.req.text();
60
61
  const bodyHash = await sha256Hex(rawBody);
62
+ // The `db` middleware always unwraps a `DisposableDb` to a plain
63
+ // `VoyantDb` before storing on context, so this cast is safe.
61
64
  const db = c.get("db");
62
65
  if (!db) {
63
66
  throw new Error("idempotencyKey middleware requires `db` on the request context. Mount `db()` (or `createApp`) before this middleware.");
@@ -168,12 +171,24 @@ function pickStringField(body, keys) {
168
171
  }
169
172
  /**
170
173
  * Sweep expired idempotency rows. Call from a daily cron.
174
+ *
175
+ * If `dbFactory` returns a `DisposableDb` (e.g. a per-call Neon
176
+ * WebSocket Pool), the sweep awaits `dispose()` before returning so
177
+ * the connection closes cleanly inside the cron handler.
171
178
  */
172
179
  export async function purgeExpiredIdempotencyKeys(dbFactory, env) {
173
- const db = dbFactory(env);
174
- const result = await db
175
- .delete(infraIdempotencyKeysTable)
176
- .where(lt(infraIdempotencyKeysTable.expiresAt, new Date()))
177
- .returning();
178
- return { removed: result.length };
180
+ const result = dbFactory(env);
181
+ const db = isDisposableDb(result) ? result.db : result;
182
+ const dispose = isDisposableDb(result) ? result.dispose : undefined;
183
+ try {
184
+ const rows = await db
185
+ .delete(infraIdempotencyKeysTable)
186
+ .where(lt(infraIdempotencyKeysTable.expiresAt, new Date()))
187
+ .returning();
188
+ return { removed: rows.length };
189
+ }
190
+ finally {
191
+ if (dispose)
192
+ await dispose();
193
+ }
179
194
  }
@@ -1,5 +1,5 @@
1
1
  import type { MiddlewareHandler } from "hono";
2
- import type { DbFactory, VoyantAuthIntegration, VoyantBindings, VoyantVariables } from "../types.js";
2
+ import { type DbFactory, type VoyantAuthIntegration, type VoyantBindings, type VoyantVariables } from "../types.js";
3
3
  export declare function requirePermission<TBindings extends VoyantBindings>(dbFactory: DbFactory<TBindings>, resource: string, action: string, opts?: {
4
4
  auth?: VoyantAuthIntegration<TBindings>;
5
5
  }): MiddlewareHandler<{
@@ -1 +1 @@
1
- {"version":3,"file":"require-permission.d.ts","sourceRoot":"","sources":["../../src/middleware/require-permission.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAG7C,OAAO,KAAK,EAAE,SAAS,EAAE,qBAAqB,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAepG,wBAAgB,iBAAiB,CAAC,SAAS,SAAS,cAAc,EAChE,SAAS,EAAE,SAAS,CAAC,SAAS,CAAC,EAC/B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE;IACL,IAAI,CAAC,EAAE,qBAAqB,CAAC,SAAS,CAAC,CAAA;CACxC,GACA,iBAAiB,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,EAAE,eAAe,CAAA;CAC3B,CAAC,CAkDD"}
1
+ {"version":3,"file":"require-permission.d.ts","sourceRoot":"","sources":["../../src/middleware/require-permission.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAG7C,OAAO,EACL,KAAK,SAAS,EAEd,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EAEnB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAA;AAqBpB,wBAAgB,iBAAiB,CAAC,SAAS,SAAS,cAAc,EAChE,SAAS,EAAE,SAAS,CAAC,SAAS,CAAC,EAC/B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE;IACL,IAAI,CAAC,EAAE,qBAAqB,CAAC,SAAS,CAAC,CAAA;CACxC,GACA,iBAAiB,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,EAAE,eAAe,CAAA;CAC3B,CAAC,CAiED"}
@@ -1,4 +1,5 @@
1
1
  import { requireUserId } from "../auth/require-user.js";
2
+ import { isDisposableDb, } from "../types.js";
2
3
  import { ForbiddenApiError, UnauthorizedApiError } from "../validation.js";
3
4
  function hasScope(scopes, permission) {
4
5
  if (!scopes || scopes.length === 0)
@@ -30,26 +31,42 @@ export function requirePermission(dbFactory, resource, action, opts) {
30
31
  if (!opts?.auth?.hasPermission) {
31
32
  return c.json({ error: "No auth permission checker configured" }, 500);
32
33
  }
33
- const allowed = await opts.auth.hasPermission({
34
- request: c.req.raw,
35
- env: c.env,
36
- db: dbFactory(c.env),
37
- ctx: c.executionCtx,
38
- auth: {
39
- userId,
40
- actor,
41
- sessionId: c.get("sessionId"),
42
- organizationId: c.get("organizationId"),
43
- callerType: c.get("callerType"),
44
- scopes,
45
- isInternalRequest: c.get("isInternalRequest"),
46
- apiKeyId: c.get("apiKeyId"),
47
- },
48
- permission,
49
- });
50
- if (!allowed) {
51
- throw new ForbiddenApiError();
34
+ const factoryResult = dbFactory(c.env);
35
+ const db = isDisposableDb(factoryResult) ? factoryResult.db : factoryResult;
36
+ const dispose = isDisposableDb(factoryResult) ? factoryResult.dispose : undefined;
37
+ try {
38
+ const allowed = await opts.auth.hasPermission({
39
+ request: c.req.raw,
40
+ env: c.env,
41
+ db,
42
+ ctx: c.executionCtx,
43
+ auth: {
44
+ userId,
45
+ actor,
46
+ sessionId: c.get("sessionId"),
47
+ organizationId: c.get("organizationId"),
48
+ callerType: c.get("callerType"),
49
+ scopes,
50
+ isInternalRequest: c.get("isInternalRequest"),
51
+ apiKeyId: c.get("apiKeyId"),
52
+ },
53
+ permission,
54
+ });
55
+ if (!allowed) {
56
+ throw new ForbiddenApiError();
57
+ }
58
+ return next();
59
+ }
60
+ finally {
61
+ if (dispose) {
62
+ const ctx = c.executionCtx;
63
+ if (ctx && typeof ctx.waitUntil === "function") {
64
+ ctx.waitUntil(dispose());
65
+ }
66
+ else {
67
+ await dispose();
68
+ }
69
+ }
52
70
  }
53
- return next();
54
71
  };
55
72
  }
package/dist/plugin.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { BootstrapHandler, LinkDefinition, Subscriber } from "@voyantjs/core";
1
+ import type { BootstrapHandler, EventFilterDescriptor, LinkDefinition, Subscriber, WorkflowDescriptor } from "@voyantjs/core";
2
2
  import type { HonoExtension, HonoModule } from "./module.js";
3
3
  /**
4
4
  * Hono-flavoured bundle contribution surface.
@@ -27,6 +27,17 @@ export interface HonoBundle {
27
27
  subscribers?: Subscriber[];
28
28
  /** Link definitions contributed by the plugin. */
29
29
  links?: LinkDefinition[];
30
+ /**
31
+ * Workflows contributed by the plugin. Mirrors the `Plugin.workflows`
32
+ * field in `@voyantjs/core` — collected at `createApp()` boot and
33
+ * registered with the configured workflow driver.
34
+ */
35
+ workflows?: readonly WorkflowDescriptor[];
36
+ /**
37
+ * Event filters contributed by the plugin. Mirrors
38
+ * `Plugin.eventFilters` in `@voyantjs/core`.
39
+ */
40
+ eventFilters?: readonly EventFilterDescriptor[];
30
41
  }
31
42
  /** @deprecated Prefer {@link HonoBundle}. */
32
43
  export type HonoPlugin = HonoBundle;
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAElF,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE5D;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,UAAU;IACzB,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,qEAAqE;IACrE,SAAS,CAAC,EAAE,gBAAgB,CAAA;IAC5B,gEAAgE;IAChE,OAAO,CAAC,EAAE,UAAU,EAAE,CAAA;IACtB,sEAAsE;IACtE,UAAU,CAAC,EAAE,aAAa,EAAE,CAAA;IAC5B,wEAAwE;IACxE,WAAW,CAAC,EAAE,UAAU,EAAE,CAAA;IAC1B,kDAAkD;IAClD,KAAK,CAAC,EAAE,cAAc,EAAE,CAAA;CACzB;AAED,6CAA6C;AAC7C,MAAM,MAAM,UAAU,GAAG,UAAU,CAAA;AAEnC;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,UAAU,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAEnE;AAED,mDAAmD;AACnD,eAAO,MAAM,gBAAgB,yBAAmB,CAAA;AAEhD,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,UAAU,EAAE,CAAA;IACrB,UAAU,EAAE,aAAa,EAAE,CAAA;IAC3B,WAAW,EAAE,UAAU,EAAE,CAAA;IACzB,KAAK,EAAE,cAAc,EAAE,CAAA;CACxB;AAED,sDAAsD;AACtD,MAAM,MAAM,mBAAmB,GAAG,mBAAmB,CAAA;AAErD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,mBAAmB,CAoBzF;AAED,oDAAoD;AACpD,eAAO,MAAM,iBAAiB,0BAAoB,CAAA"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,qBAAqB,EACrB,cAAc,EACd,UAAU,EACV,kBAAkB,EACnB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE5D;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,UAAU;IACzB,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,qEAAqE;IACrE,SAAS,CAAC,EAAE,gBAAgB,CAAA;IAC5B,gEAAgE;IAChE,OAAO,CAAC,EAAE,UAAU,EAAE,CAAA;IACtB,sEAAsE;IACtE,UAAU,CAAC,EAAE,aAAa,EAAE,CAAA;IAC5B,wEAAwE;IACxE,WAAW,CAAC,EAAE,UAAU,EAAE,CAAA;IAC1B,kDAAkD;IAClD,KAAK,CAAC,EAAE,cAAc,EAAE,CAAA;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAA;IACzC;;;OAGG;IACH,YAAY,CAAC,EAAE,SAAS,qBAAqB,EAAE,CAAA;CAChD;AAED,6CAA6C;AAC7C,MAAM,MAAM,UAAU,GAAG,UAAU,CAAA;AAEnC;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,UAAU,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAEnE;AAED,mDAAmD;AACnD,eAAO,MAAM,gBAAgB,yBAAmB,CAAA;AAEhD,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,UAAU,EAAE,CAAA;IACrB,UAAU,EAAE,aAAa,EAAE,CAAA;IAC3B,WAAW,EAAE,UAAU,EAAE,CAAA;IACzB,KAAK,EAAE,cAAc,EAAE,CAAA;CACxB;AAED,sDAAsD;AACtD,MAAM,MAAM,mBAAmB,GAAG,mBAAmB,CAAA;AAErD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,mBAAmB,CAoBzF;AAED,oDAAoD;AACpD,eAAO,MAAM,iBAAiB,0BAAoB,CAAA"}
package/dist/types.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Actor, VoyantVariables as CoreVoyantVariables, EventBus, LinkService, ModuleContainer, QueryGraphContext, QueryRunner, VoyantAuthContext, VoyantPermission } from "@voyantjs/core";
2
2
  import type { KVStore } from "@voyantjs/utils/cache";
3
3
  import type { NeonHttpDatabase } from "drizzle-orm/neon-http";
4
+ import type { NeonDatabase as NeonWsDatabase } from "drizzle-orm/neon-serverless";
4
5
  import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
5
6
  import type { Hono } from "hono";
6
7
  import type { HonoExtension, HonoModule } from "./module.js";
@@ -21,7 +22,7 @@ export interface VoyantBindings {
21
22
  RATE_LIMIT?: KVStore;
22
23
  CACHE?: KVStore;
23
24
  }
24
- export type VoyantDb = PostgresJsDatabase | NeonHttpDatabase;
25
+ export type VoyantDb = PostgresJsDatabase | NeonHttpDatabase | NeonWsDatabase;
25
26
  export type VoyantQueryRuntime = QueryRunner;
26
27
  export type VoyantVariables = CoreVoyantVariables & {
27
28
  db: VoyantDb;
@@ -33,7 +34,30 @@ export type VoyantVariables = CoreVoyantVariables & {
33
34
  /** Shared cross-module query runtime, when the app wires one in. */
34
35
  query?: VoyantQueryRuntime;
35
36
  };
36
- export type DbFactory<TBindings extends VoyantBindings = VoyantBindings> = (env: TBindings) => VoyantDb;
37
+ /**
38
+ * Per-request handle returned by a {@link DbFactory} that owns its own
39
+ * Pool / connection: a drizzle client plus a `dispose()` the db
40
+ * middleware schedules via `c.executionCtx.waitUntil` after the
41
+ * response is sent. Used by templates that build a Neon WebSocket
42
+ * Pool per request (see e.g. `dbFromEnvForApp` in template
43
+ * `src/api/lib/db.ts`) — without `dispose()`, the Pool stays open
44
+ * until the Workers isolate is reclaimed.
45
+ */
46
+ export interface DisposableDb {
47
+ db: VoyantDb;
48
+ dispose: () => Promise<void>;
49
+ }
50
+ export type DbFactory<TBindings extends VoyantBindings = VoyantBindings> = (env: TBindings) => VoyantDb | DisposableDb;
51
+ export declare function isDisposableDb(value: VoyantDb | DisposableDb): value is DisposableDb;
52
+ /**
53
+ * Normalize a {@link DbFactory} return value to `{ db, dispose? }` so
54
+ * call sites don't repeat the `isDisposableDb` shape check. `dispose`
55
+ * is `undefined` for plain `VoyantDb` factories.
56
+ */
57
+ export declare function resolveDbFactoryResult(value: VoyantDb | DisposableDb): {
58
+ db: VoyantDb;
59
+ dispose?: () => Promise<void>;
60
+ };
37
61
  /**
38
62
  * The shape returned by a custom `auth.resolve` integration. Both `userId`
39
63
  * and `actor` are required: `requireActor` is fail-closed, so a resolver
@@ -90,6 +114,76 @@ export interface VoyantAppConfig<TBindings extends VoyantBindings = VoyantBindin
90
114
  auth?: VoyantAuthIntegration<TBindings>;
91
115
  publicPaths?: string[];
92
116
  logger?: LoggerProvider;
117
+ /**
118
+ * Workflow runtime configuration. When set, `createApp()` collects
119
+ * `module.workflows` + `module.eventFilters` (plus the same fields
120
+ * from plugins), invokes `workflows.driver` with framework deps, and
121
+ * — inside the lazy bootstrap path — registers the manifest with the
122
+ * driver and installs an EventBus forwarder that routes emitted
123
+ * events to `driver.ingestEvent(...)`.
124
+ *
125
+ * See `docs/architecture/workflows-runtime-architecture.md` §6, §18.
126
+ */
127
+ workflows?: VoyantWorkflowsConfig;
93
128
  additionalRoutes?: (app: Hono<any>) => void;
94
129
  }
130
+ /**
131
+ * Workflow runtime configuration block. The driver is resolved at boot
132
+ * time (inside the lazy bootstrap path), after framework deps and —
133
+ * crucially — after runtime bindings are available.
134
+ *
135
+ * `driver` is **always** a function-of-bindings: `(env) => DriverFactory`.
136
+ * This unambiguous shape works for all deployment modes:
137
+ *
138
+ * **Mode 2 / InMemory** — wrap your direct factory:
139
+ *
140
+ * workflows: {
141
+ * driver: () => createNodeStandaloneDriver({ db }),
142
+ * }
143
+ *
144
+ * **Mode 1 (CF edge)** — pull options off `env`:
145
+ *
146
+ * workflows: {
147
+ * driver: (env) => createCloudflareEdgeDriver({
148
+ * orchestratorNamespace: env.WORKFLOW_RUN_DO,
149
+ * manifestKv: env.WORKFLOW_MANIFESTS,
150
+ * tenantScript: "tenant-bundle",
151
+ * }),
152
+ * }
153
+ *
154
+ * The single shape avoids ambiguous "is this a factory or a
155
+ * factory-of-factories?" heuristics. See architecture doc §6.3 +
156
+ * reviewer feedback P2.1.
157
+ */
158
+ export interface VoyantWorkflowsConfig<TBindings = unknown> {
159
+ /**
160
+ * Function-of-bindings that returns a `DriverFactory`. Resolved
161
+ * lazily with `c.env` once bindings are available, then invoked
162
+ * with `{ services, logger }` to produce the driver.
163
+ */
164
+ driver: (bindings: TBindings) => WorkflowDriverFactoryShape;
165
+ /**
166
+ * Environment the manifest registers under. Defaults to `"development"`.
167
+ * Workflow filters are environment-scoped (production manifests don't
168
+ * see preview events and vice versa) per architecture doc §21.10.
169
+ */
170
+ environment?: "production" | "preview" | "development";
171
+ /**
172
+ * Project / tenant identifier baked into the manifest. Single-tenant
173
+ * runtimes leave this unset (defaults to `"default"`). Multi-tenant
174
+ * deployments override per-app via voyant-cloud's wrapper layer.
175
+ */
176
+ projectId?: string;
177
+ }
178
+ /**
179
+ * Structural shape of a `DriverFactory` from `@voyantjs/workflows/driver`.
180
+ * The SDK package's concrete `DriverFactory` satisfies this via TS
181
+ * structural compat (architecture doc §21.19).
182
+ */
183
+ type WorkflowDriverFactoryShape = (deps: {
184
+ services: any;
185
+ logger: any;
186
+ now?: () => number;
187
+ }) => any;
188
+ export {};
95
189
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,KAAK,EACL,eAAe,IAAI,mBAAmB,EACtC,QAAQ,EACR,WAAW,EACX,eAAe,EACf,iBAAiB,EACjB,WAAW,EACX,iBAAiB,EACjB,gBAAgB,EACjB,MAAM,gBAAgB,CAAA;AACvB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAEhC,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7C,MAAM,WAAW,sBAAsB;IACrC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAA;IAC/C,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAA;CACpC;AAED,MAAM,WAAW,cAAc;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,MAAM,MAAM,QAAQ,GAAG,kBAAkB,GAAG,gBAAgB,CAAA;AAC5D,MAAM,MAAM,kBAAkB,GAAG,WAAW,CAAA;AAE5C,MAAM,MAAM,eAAe,GAAG,mBAAmB,GAAG;IAClD,EAAE,EAAE,QAAQ,CAAA;IACZ,oEAAoE;IACpE,SAAS,EAAE,eAAe,CAAA;IAC1B,QAAQ,EAAE,QAAQ,CAAA;IAClB,mEAAmE;IACnE,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,oEAAoE;IACpE,KAAK,CAAC,EAAE,kBAAkB,CAAA;CAC3B,CAAA;AAED,MAAM,MAAM,SAAS,CAAC,SAAS,SAAS,cAAc,GAAG,cAAc,IAAI,CACzE,GAAG,EAAE,SAAS,KACX,QAAQ,CAAA;AAEb;;;;;GAKG;AACH,MAAM,MAAM,wBAAwB,GAAG,IAAI,CAAC,iBAAiB,EAAE,OAAO,CAAC,GAAG;IACxE,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,KAAK,CAAA;CACb,CAAA;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAAA;CAC3B;AAED,MAAM,WAAW,qBAAqB,CAAC,SAAS,SAAS,cAAc,GAAG,cAAc;IACtF,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,EAAE,SAAS,CAAA;IACd,EAAE,EAAE,QAAQ,CAAA;IACZ,GAAG,CAAC,EAAE,sBAAsB,CAAA;CAC7B;AAED,MAAM,WAAW,wBAAwB,CAAC,SAAS,SAAS,cAAc,GAAG,cAAc,CACzF,SAAQ,qBAAqB,CAAC,SAAS,CAAC;IACxC,UAAU,EAAE,gBAAgB,CAAA;IAC5B,IAAI,EAAE,wBAAwB,CAAA;CAC/B;AAED,MAAM,WAAW,qBAAqB,CAAC,SAAS,SAAS,cAAc,GAAG,cAAc;IACtF,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,KAAK;QAC5B,KAAK,EAAE,CACL,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,SAAS,EACd,GAAG,CAAC,EAAE,sBAAsB,KACzB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;KAClC,CAAA;IACD;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,CACR,IAAI,EAAE,qBAAqB,CAAC,SAAS,CAAC,KACnC,OAAO,CAAC,wBAAwB,GAAG,IAAI,CAAC,GAAG,wBAAwB,GAAG,IAAI,CAAA;IAC/E,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,wBAAwB,CAAC,SAAS,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAA;CAC1F;AAED,MAAM,WAAW,eAAe,CAAC,SAAS,SAAS,cAAc,GAAG,cAAc;IAChF,EAAE,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;IACxB,OAAO,CAAC,EAAE,UAAU,EAAE,CAAA;IACtB,UAAU,CAAC,EAAE,aAAa,EAAE,CAAA;IAC5B,OAAO,CAAC,EAAE,UAAU,EAAE,CAAA;IACtB,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,KAAK,CAAC,EAAE,iBAAiB,GAAG,kBAAkB,CAAA;IAC9C,IAAI,CAAC,EAAE,qBAAqB,CAAC,SAAS,CAAC,CAAA;IACvC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,MAAM,CAAC,EAAE,cAAc,CAAA;IAEvB,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,IAAI,CAAA;CAC5C"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js CHANGED
@@ -1 +1,12 @@
1
- export {};
1
+ export function isDisposableDb(value) {
2
+ return (typeof value.dispose === "function" &&
3
+ value.db !== undefined);
4
+ }
5
+ /**
6
+ * Normalize a {@link DbFactory} return value to `{ db, dispose? }` so
7
+ * call sites don't repeat the `isDisposableDb` shape check. `dispose`
8
+ * is `undefined` for plain `VoyantDb` factories.
9
+ */
10
+ export function resolveDbFactoryResult(value) {
11
+ return isDisposableDb(value) ? value : { db: value };
12
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/hono",
3
- "version": "0.28.1",
3
+ "version": "0.29.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -94,16 +94,18 @@
94
94
  "drizzle-orm": "^0.45.2",
95
95
  "hono": "^4.12.10",
96
96
  "zod": "^4.3.6",
97
- "@voyantjs/core": "0.28.1",
98
- "@voyantjs/db": "0.28.1",
99
- "@voyantjs/types": "0.28.1",
100
- "@voyantjs/utils": "0.28.1"
97
+ "@voyantjs/db": "0.29.0",
98
+ "@voyantjs/core": "0.29.0",
99
+ "@voyantjs/types": "0.29.0",
100
+ "@voyantjs/utils": "0.29.0",
101
+ "@voyantjs/workflows": "0.29.0"
101
102
  },
102
103
  "devDependencies": {
103
104
  "@cloudflare/workers-types": "^4.20260426.1",
104
105
  "typescript": "^6.0.2",
105
106
  "vitest": "^4.1.2",
106
- "@voyantjs/voyant-typescript-config": "0.1.0"
107
+ "@voyantjs/voyant-typescript-config": "0.1.0",
108
+ "@voyantjs/workflows-orchestrator": "0.29.0"
107
109
  },
108
110
  "files": [
109
111
  "dist"