@multiplier-labs/stepflow 0.3.1 → 0.3.4

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 (112) hide show
  1. package/.codekin/reports/code-review/2026-04-11_code-review-daily.md +146 -0
  2. package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +74 -0
  3. package/.codekin/reports/comments/2026-04-04_comment-assessment.md +179 -0
  4. package/.codekin/reports/comments/2026-04-11_comment-assessment.md +161 -0
  5. package/.codekin/reports/comments/2026-04-14_comment-assessment.md +34 -0
  6. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
  7. package/.codekin/reports/complexity/2026-04-11_complexity-report.md +152 -0
  8. package/.codekin/reports/complexity/2026-04-14_complexity-report.md +34 -0
  9. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
  10. package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +93 -0
  11. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
  12. package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +138 -0
  13. package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +121 -0
  14. package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
  15. package/.codekin/reports/security/2026-04-11_security-audit.md +297 -0
  16. package/.github/CODEOWNERS +15 -0
  17. package/.github/dependabot.yml +23 -0
  18. package/.github/workflows/ci.yml +38 -0
  19. package/.github/workflows/publish.yml +17 -5
  20. package/.github/workflows/security.yml +29 -0
  21. package/CHANGELOG.md +34 -0
  22. package/README.md +24 -0
  23. package/SECURITY.md +40 -0
  24. package/dist/{chunk-FQCT3I2A.js → chunk-5XJBAPXF.js} +527 -551
  25. package/dist/chunk-5XJBAPXF.js.map +1 -0
  26. package/dist/{chunk-U5QGN3RT.js → chunk-FMPJBK2N.js} +67 -55
  27. package/dist/chunk-FMPJBK2N.js.map +1 -0
  28. package/dist/{chunk-GXY66V7Q.js → chunk-JPXPB7U5.js} +52 -2
  29. package/dist/chunk-JPXPB7U5.js.map +1 -0
  30. package/dist/{chunk-XCF4PLN7.js → chunk-U3PCVK6O.js} +276 -61
  31. package/dist/chunk-U3PCVK6O.js.map +1 -0
  32. package/dist/events/index.d.ts +60 -7
  33. package/dist/events/index.js +1 -1
  34. package/dist/{index-CPfGsmrp.d.ts → index-Cd0IZ40L.d.ts} +56 -17
  35. package/dist/index.d.ts +15 -23
  36. package/dist/index.js +784 -547
  37. package/dist/index.js.map +1 -1
  38. package/dist/scheduler/index.d.ts +4 -4
  39. package/dist/scheduler/index.js +2 -2
  40. package/dist/storage/index.d.ts +322 -56
  41. package/dist/storage/index.js +4 -2
  42. package/dist/{types-BlwFEGzj.d.ts → types-DSEjiLMj.d.ts} +16 -15
  43. package/dist/{types-DzkEqnQJ.d.ts → types-DZUdWmCc.d.ts} +2 -2
  44. package/dist/{types-BVShq7zT.d.ts → types-K5Gjk3H_.d.ts} +4 -4
  45. package/docs/stepflow-api-reference.md +5 -4
  46. package/package.json +12 -8
  47. package/src/core/engine/concurrency.ts +229 -0
  48. package/src/core/engine/lifecycle.ts +176 -0
  49. package/src/core/engine.test.ts +536 -522
  50. package/src/core/engine.ts +116 -235
  51. package/src/core/index.ts +3 -3
  52. package/src/core/orchestrator/persistence.ts +165 -0
  53. package/src/core/orchestrator/plan-resolution.ts +140 -0
  54. package/src/core/orchestrator/state-transitions.ts +109 -0
  55. package/src/core/orchestrator/step-runner.ts +427 -0
  56. package/src/core/orchestrator.ts +57 -395
  57. package/src/core/types.ts +7 -29
  58. package/src/events/index.ts +4 -4
  59. package/src/events/memory.test.ts +39 -41
  60. package/src/events/memory.ts +7 -10
  61. package/src/events/socketio.test.ts +112 -162
  62. package/src/events/socketio.ts +20 -27
  63. package/src/events/types.ts +16 -19
  64. package/src/events/webhook.test.ts +489 -235
  65. package/src/events/webhook.ts +355 -139
  66. package/src/index.ts +19 -19
  67. package/src/planning/evaluate-condition.test.ts +153 -0
  68. package/src/planning/evaluate-condition.ts +105 -0
  69. package/src/planning/index.ts +6 -3
  70. package/src/planning/planner.ts +45 -152
  71. package/src/planning/planning.test.ts +303 -366
  72. package/src/planning/registry.test.ts +204 -251
  73. package/src/planning/registry.ts +20 -106
  74. package/src/planning/types.ts +23 -27
  75. package/src/scheduler/cron.test.ts +149 -150
  76. package/src/scheduler/cron.ts +58 -69
  77. package/src/scheduler/index.ts +4 -14
  78. package/src/scheduler/postgres-persistence.ts +89 -105
  79. package/src/scheduler/sqlite-persistence.test.ts +96 -96
  80. package/src/scheduler/sqlite-persistence.ts +40 -22
  81. package/src/scheduler/types.ts +4 -9
  82. package/src/storage/index.ts +5 -5
  83. package/src/storage/memory.test.ts +133 -183
  84. package/src/storage/memory.ts +21 -42
  85. package/src/storage/postgres-core.ts +550 -0
  86. package/src/storage/postgres-migration.test.ts +69 -0
  87. package/src/storage/postgres-transaction-pagination.test.ts +107 -0
  88. package/src/storage/postgres.ts +222 -937
  89. package/src/storage/sqlite-core.ts +151 -0
  90. package/src/storage/sqlite.test.ts +330 -281
  91. package/src/storage/sqlite.ts +225 -170
  92. package/src/storage/types.ts +33 -58
  93. package/src/utils/errors.test.ts +78 -80
  94. package/src/utils/errors.ts +27 -57
  95. package/src/utils/id.ts +3 -3
  96. package/src/utils/index.ts +6 -4
  97. package/src/utils/logger.test.ts +52 -58
  98. package/src/utils/logger.ts +4 -4
  99. package/src/utils/pinned-fetch.test.ts +84 -0
  100. package/src/utils/pinned-fetch.ts +118 -0
  101. package/src/utils/postgres-deps.ts +20 -4
  102. package/src/utils/retry.test.ts +47 -67
  103. package/src/utils/retry.ts +9 -9
  104. package/src/utils/safe-json.test.ts +166 -0
  105. package/src/utils/safe-json.ts +107 -0
  106. package/src/utils/webhook-signature.test.ts +111 -0
  107. package/src/utils/webhook-signature.ts +74 -0
  108. package/dist/chunk-FQCT3I2A.js.map +0 -1
  109. package/dist/chunk-GXY66V7Q.js.map +0 -1
  110. package/dist/chunk-U5QGN3RT.js.map +0 -1
  111. package/dist/chunk-XCF4PLN7.js.map +0 -1
  112. package/src/scheduler/postgres-persistence.test.ts +0 -906
@@ -1,77 +1,71 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import { ConsoleLogger, SilentLogger, createScopedLogger } from "./logger";
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { ConsoleLogger, SilentLogger, createScopedLogger } from './logger';
3
3
 
4
- describe("ConsoleLogger", () => {
5
- it("should log debug messages with prefix", () => {
6
- const spy = vi.spyOn(console, "debug").mockImplementation(() => {});
7
- const logger = new ConsoleLogger("[test]", "debug");
4
+ describe('ConsoleLogger', () => {
5
+ it('should log debug messages with prefix', () => {
6
+ const spy = vi.spyOn(console, 'debug').mockImplementation(() => {});
7
+ const logger = new ConsoleLogger('[test]', 'debug');
8
8
 
9
- logger.debug("hello", { extra: true });
9
+ logger.debug('hello', { extra: true });
10
10
 
11
- expect(spy).toHaveBeenCalledWith("[test] [DEBUG]", "hello", {
12
- extra: true,
13
- });
11
+ expect(spy).toHaveBeenCalledWith('[test] [DEBUG]', 'hello', { extra: true });
14
12
  spy.mockRestore();
15
13
  });
16
14
 
17
- it("should log info messages with prefix", () => {
18
- const spy = vi.spyOn(console, "info").mockImplementation(() => {});
19
- const logger = new ConsoleLogger("[test]");
15
+ it('should log info messages with prefix', () => {
16
+ const spy = vi.spyOn(console, 'info').mockImplementation(() => {});
17
+ const logger = new ConsoleLogger('[test]');
20
18
 
21
- logger.info("info msg");
19
+ logger.info('info msg');
22
20
 
23
- expect(spy).toHaveBeenCalledWith("[test] [INFO]", "info msg");
21
+ expect(spy).toHaveBeenCalledWith('[test] [INFO]', 'info msg');
24
22
  spy.mockRestore();
25
23
  });
26
24
 
27
- it("should log warn messages with prefix", () => {
28
- const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
29
- const logger = new ConsoleLogger("[test]");
25
+ it('should log warn messages with prefix', () => {
26
+ const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
27
+ const logger = new ConsoleLogger('[test]');
30
28
 
31
- logger.warn("warning");
29
+ logger.warn('warning');
32
30
 
33
- expect(spy).toHaveBeenCalledWith("[test] [WARN]", "warning");
31
+ expect(spy).toHaveBeenCalledWith('[test] [WARN]', 'warning');
34
32
  spy.mockRestore();
35
33
  });
36
34
 
37
- it("should log error messages with prefix", () => {
38
- const spy = vi.spyOn(console, "error").mockImplementation(() => {});
39
- const logger = new ConsoleLogger("[test]");
35
+ it('should log error messages with prefix', () => {
36
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
37
+ const logger = new ConsoleLogger('[test]');
40
38
 
41
- logger.error("error msg", new Error("boom"));
39
+ logger.error('error msg', new Error('boom'));
42
40
 
43
- expect(spy).toHaveBeenCalledWith(
44
- "[test] [ERROR]",
45
- "error msg",
46
- expect.any(Error),
47
- );
41
+ expect(spy).toHaveBeenCalledWith('[test] [ERROR]', 'error msg', expect.any(Error));
48
42
  spy.mockRestore();
49
43
  });
50
44
 
51
- it("should use default prefix when none provided", () => {
52
- const spy = vi.spyOn(console, "info").mockImplementation(() => {});
45
+ it('should use default prefix when none provided', () => {
46
+ const spy = vi.spyOn(console, 'info').mockImplementation(() => {});
53
47
  const logger = new ConsoleLogger();
54
48
 
55
- logger.info("test");
49
+ logger.info('test');
56
50
 
57
- expect(spy).toHaveBeenCalledWith("[workflow] [INFO]", "test");
51
+ expect(spy).toHaveBeenCalledWith('[workflow] [INFO]', 'test');
58
52
  spy.mockRestore();
59
53
  });
60
54
  });
61
55
 
62
- describe("SilentLogger", () => {
63
- it("should not throw on any log level", () => {
56
+ describe('SilentLogger', () => {
57
+ it('should not throw on any log level', () => {
64
58
  const logger = new SilentLogger();
65
59
 
66
- expect(() => logger.debug("test")).not.toThrow();
67
- expect(() => logger.info("test")).not.toThrow();
68
- expect(() => logger.warn("test")).not.toThrow();
69
- expect(() => logger.error("test")).not.toThrow();
60
+ expect(() => logger.debug('test')).not.toThrow();
61
+ expect(() => logger.info('test')).not.toThrow();
62
+ expect(() => logger.warn('test')).not.toThrow();
63
+ expect(() => logger.error('test')).not.toThrow();
70
64
  });
71
65
  });
72
66
 
73
- describe("createScopedLogger", () => {
74
- it("should prefix messages with run ID", () => {
67
+ describe('createScopedLogger', () => {
68
+ it('should prefix messages with run ID', () => {
75
69
  const calls: string[] = [];
76
70
  const baseLogger = {
77
71
  debug: (msg: string) => calls.push(`debug:${msg}`),
@@ -80,22 +74,22 @@ describe("createScopedLogger", () => {
80
74
  error: (msg: string) => calls.push(`error:${msg}`),
81
75
  };
82
76
 
83
- const scoped = createScopedLogger(baseLogger, "run-123");
77
+ const scoped = createScopedLogger(baseLogger, 'run-123');
84
78
 
85
- scoped.debug("d");
86
- scoped.info("i");
87
- scoped.warn("w");
88
- scoped.error("e");
79
+ scoped.debug('d');
80
+ scoped.info('i');
81
+ scoped.warn('w');
82
+ scoped.error('e');
89
83
 
90
84
  expect(calls).toEqual([
91
- "debug:[run:run-123] d",
92
- "info:[run:run-123] i",
93
- "warn:[run:run-123] w",
94
- "error:[run:run-123] e",
85
+ 'debug:[run:run-123] d',
86
+ 'info:[run:run-123] i',
87
+ 'warn:[run:run-123] w',
88
+ 'error:[run:run-123] e',
95
89
  ]);
96
90
  });
97
91
 
98
- it("should include step key when provided", () => {
92
+ it('should include step key when provided', () => {
99
93
  const calls: string[] = [];
100
94
  const baseLogger = {
101
95
  debug: (msg: string) => calls.push(msg),
@@ -104,13 +98,13 @@ describe("createScopedLogger", () => {
104
98
  error: (msg: string) => calls.push(msg),
105
99
  };
106
100
 
107
- const scoped = createScopedLogger(baseLogger, "run-1", "step-a");
108
- scoped.info("hello");
101
+ const scoped = createScopedLogger(baseLogger, 'run-1', 'step-a');
102
+ scoped.info('hello');
109
103
 
110
- expect(calls[0]).toBe("[run:run-1][step:step-a] hello");
104
+ expect(calls[0]).toBe('[run:run-1][step:step-a] hello');
111
105
  });
112
106
 
113
- it("should forward extra arguments", () => {
107
+ it('should forward extra arguments', () => {
114
108
  const captured: unknown[][] = [];
115
109
  const baseLogger = {
116
110
  debug: (...args: unknown[]) => captured.push(args),
@@ -119,9 +113,9 @@ describe("createScopedLogger", () => {
119
113
  error: (...args: unknown[]) => captured.push(args),
120
114
  };
121
115
 
122
- const scoped = createScopedLogger(baseLogger, "r1");
123
- scoped.info("msg", { data: 1 }, 42);
116
+ const scoped = createScopedLogger(baseLogger, 'r1');
117
+ scoped.info('msg', { data: 1 }, 42);
124
118
 
125
- expect(captured[0]).toEqual(["[run:r1] msg", { data: 1 }, 42]);
119
+ expect(captured[0]).toEqual(['[run:r1] msg', { data: 1 }, 42]);
126
120
  });
127
121
  });
@@ -2,10 +2,10 @@
2
2
  * Logger utilities for the workflow engine.
3
3
  */
4
4
 
5
- import type { Logger, WorkflowError } from "../core/types";
5
+ import type { Logger, WorkflowError } from '../core/types';
6
6
 
7
7
  /** Log level for ConsoleLogger. Levels are ordered: debug < info < warn < error. */
8
- export type LogLevel = "debug" | "info" | "warn" | "error";
8
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
9
9
 
10
10
  const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
11
11
  debug: 0,
@@ -23,7 +23,7 @@ export class ConsoleLogger implements Logger {
23
23
  private prefix: string;
24
24
  private minLevel: number;
25
25
 
26
- constructor(prefix = "[workflow]", level: LogLevel = "info") {
26
+ constructor(prefix = '[workflow]', level: LogLevel = 'info') {
27
27
  this.prefix = prefix;
28
28
  this.minLevel = LOG_LEVEL_PRIORITY[level];
29
29
  }
@@ -79,7 +79,7 @@ export function sanitizeErrorForStorage(error: WorkflowError): WorkflowError {
79
79
  export function createScopedLogger(
80
80
  logger: Logger,
81
81
  runId: string,
82
- stepKey?: string,
82
+ stepKey?: string
83
83
  ): Logger {
84
84
  const prefix = stepKey ? `[run:${runId}][step:${stepKey}]` : `[run:${runId}]`;
85
85
 
@@ -0,0 +1,84 @@
1
+ /**
2
+ * pinnedFetch is the linchpin of the M1 (DNS rebinding TOCTOU) fix: by
3
+ * passing a `lookup` function that returns a caller-supplied IP, the TCP
4
+ * connection cannot be re-resolved to a different address. We test it
5
+ * against a real loopback HTTP server because ESM forbids spying on the
6
+ * `http` module's exports and we want to exercise the real request path
7
+ * end-to-end.
8
+ */
9
+
10
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
11
+ import * as http from 'node:http';
12
+ import { pinnedFetch } from './pinned-fetch';
13
+
14
+ describe('pinnedFetch', () => {
15
+ let server: http.Server;
16
+ let port: number;
17
+ let lastRequest: { hostHeader?: string; method?: string; path?: string; body: string } = {
18
+ body: '',
19
+ };
20
+
21
+ beforeAll(async () => {
22
+ server = http.createServer((req, res) => {
23
+ const chunks: Buffer[] = [];
24
+ req.on('data', (c: Buffer) => chunks.push(c));
25
+ req.on('end', () => {
26
+ lastRequest = {
27
+ hostHeader: req.headers.host,
28
+ method: req.method,
29
+ path: req.url,
30
+ body: Buffer.concat(chunks).toString('utf8'),
31
+ };
32
+ res.writeHead(204, 'No Content');
33
+ res.end();
34
+ });
35
+ });
36
+ await new Promise<void>((resolve) => {
37
+ server.listen(0, '127.0.0.1', () => resolve());
38
+ });
39
+ port = (server.address() as { port: number }).port;
40
+ });
41
+
42
+ afterAll(async () => {
43
+ await new Promise<void>((resolve) => server.close(() => resolve()));
44
+ });
45
+
46
+ it('connects to the pinned IP even when the URL hostname is unrelated', async () => {
47
+ // The URL hostname is one that would never resolve to 127.0.0.1 via DNS.
48
+ // pinnedFetch must ignore the URL's would-be DNS answer and connect to
49
+ // the pinned address. If pinning is broken, the connection will fail
50
+ // or hit a different host.
51
+ const response = await pinnedFetch(
52
+ `http://rebind.invalid:${port}/hook`,
53
+ { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{"ok":1}' },
54
+ '127.0.0.1',
55
+ 4,
56
+ );
57
+
58
+ expect(response.ok).toBe(true);
59
+ expect(response.status).toBe(204);
60
+ expect(lastRequest.method).toBe('POST');
61
+ expect(lastRequest.path).toBe('/hook');
62
+ expect(lastRequest.body).toBe('{"ok":1}');
63
+ });
64
+
65
+ it('preserves the original hostname in the Host header (vhost / TLS SNI)', async () => {
66
+ await pinnedFetch(
67
+ `http://example.test:${port}/`,
68
+ { method: 'GET' },
69
+ '127.0.0.1',
70
+ 4,
71
+ );
72
+
73
+ // The Host header must reflect the URL hostname, not the pinned IP, so
74
+ // that virtual-host routing and TLS certificate validation work.
75
+ expect(lastRequest.hostHeader).toContain('example.test');
76
+ });
77
+
78
+ it('rejects when the connection cannot be established', async () => {
79
+ // Pin to a non-listening port on loopback.
80
+ await expect(
81
+ pinnedFetch(`http://example.test:1/`, { method: 'GET' }, '127.0.0.1', 4),
82
+ ).rejects.toThrow();
83
+ });
84
+ });
@@ -0,0 +1,118 @@
1
+ /**
2
+ * DNS-pinned fetch helper.
3
+ *
4
+ * Performs an HTTP(S) request that connects to a caller-supplied IP address
5
+ * instead of re-resolving the URL hostname. Used by the webhook transport to
6
+ * close the SSRF DNS-rebinding TOCTOU window: the hostname is resolved
7
+ * exactly once (by the caller), the resolved IP is validated, and the
8
+ * resolved IP is then pinned for the connection so the request cannot be
9
+ * rerouted by a hostile DNS server.
10
+ *
11
+ * The original hostname is preserved for TLS SNI / certificate verification
12
+ * and for the HTTP `Host` header.
13
+ *
14
+ * Audit: 2026-04-27 finding M1 — DNS TOCTOU between SSRF validation and
15
+ * connect.
16
+ */
17
+
18
+ import * as http from 'node:http';
19
+ import * as https from 'node:https';
20
+ import type { LookupAddress, LookupOptions } from 'node:dns';
21
+
22
+ /** Minimal fetch-compatible Response surface used by the webhook transport. */
23
+ export interface PinnedFetchResponse {
24
+ ok: boolean;
25
+ status: number;
26
+ statusText: string;
27
+ }
28
+
29
+ /** Subset of `RequestInit` accepted by `pinnedFetch`. */
30
+ export interface PinnedFetchInit {
31
+ method?: string;
32
+ headers?: Record<string, string>;
33
+ body?: string;
34
+ signal?: AbortSignal;
35
+ }
36
+
37
+ /**
38
+ * Make an HTTP(S) request that connects to `address` instead of resolving
39
+ * `url`'s hostname. Returns a minimal `Response`-shaped object compatible
40
+ * with the webhook transport's status / ok checks.
41
+ *
42
+ * @throws Error on transport failure or abort
43
+ */
44
+ export function pinnedFetch(
45
+ url: string,
46
+ init: PinnedFetchInit,
47
+ address: string,
48
+ family: 4 | 6 = 4,
49
+ ): Promise<PinnedFetchResponse> {
50
+ const parsed = new URL(url);
51
+ const isHttps = parsed.protocol === 'https:';
52
+ const lib = isHttps ? https : http;
53
+ const port = parsed.port ? Number(parsed.port) : isHttps ? 443 : 80;
54
+
55
+ // `lookup` is called by the agent's socket-create path. By returning the
56
+ // pre-validated address here we guarantee the TCP connection target is the
57
+ // IP whose safety the caller already verified. Node may invoke this with
58
+ // `{ all: true }` (expects an array result) or single-address mode
59
+ // (expects positional address/family) — handle both for cross-version safety.
60
+ const lookup = (
61
+ _hostname: string,
62
+ options: LookupOptions,
63
+ cb: (err: NodeJS.ErrnoException | null, addr: string | LookupAddress[], fam?: number) => void,
64
+ ): void => {
65
+ if (options && options.all === true) {
66
+ cb(null, [{ address, family }]);
67
+ } else {
68
+ cb(null, address, family);
69
+ }
70
+ };
71
+
72
+ return new Promise<PinnedFetchResponse>((resolve, reject) => {
73
+ const req = lib.request(
74
+ {
75
+ hostname: parsed.hostname,
76
+ port,
77
+ path: `${parsed.pathname || '/'}${parsed.search}`,
78
+ method: init.method ?? 'GET',
79
+ headers: init.headers,
80
+ lookup,
81
+ servername: parsed.hostname,
82
+ },
83
+ (res) => {
84
+ // Drain the body — we only inspect status, but leaving the socket
85
+ // half-read would leak it.
86
+ res.resume();
87
+ res.on('end', () => {
88
+ const status = res.statusCode ?? 0;
89
+ resolve({
90
+ ok: status >= 200 && status < 300,
91
+ status,
92
+ statusText: res.statusMessage ?? '',
93
+ });
94
+ });
95
+ res.on('error', reject);
96
+ },
97
+ );
98
+
99
+ req.on('error', reject);
100
+
101
+ if (init.signal) {
102
+ if (init.signal.aborted) {
103
+ req.destroy(new Error('Request aborted'));
104
+ } else {
105
+ init.signal.addEventListener(
106
+ 'abort',
107
+ () => req.destroy(new Error('Request aborted')),
108
+ { once: true },
109
+ );
110
+ }
111
+ }
112
+
113
+ if (init.body !== undefined) {
114
+ req.write(init.body);
115
+ }
116
+ req.end();
117
+ });
118
+ }
@@ -37,21 +37,21 @@ export async function loadPostgresDeps(): Promise<PostgresDeps> {
37
37
  let pgModule: any;
38
38
 
39
39
  try {
40
- const kyselyMod = await import("kysely");
40
+ const kyselyMod = await import('kysely');
41
41
  Kysely = kyselyMod.Kysely;
42
42
  PostgresDialect = kyselyMod.PostgresDialect;
43
43
  sql = kyselyMod.sql;
44
44
  } catch {
45
45
  throw new Error(
46
- 'PostgreSQL support requires the "kysely" package. Install it with: npm install kysely',
46
+ 'PostgreSQL support requires the "kysely" package. Install it with: npm install kysely'
47
47
  );
48
48
  }
49
49
  try {
50
- const pg = await import("pg");
50
+ const pg = await import('pg');
51
51
  pgModule = pg.default ?? pg;
52
52
  } catch {
53
53
  throw new Error(
54
- 'PostgreSQL support requires the "pg" package. Install it with: npm install pg',
54
+ 'PostgreSQL support requires the "pg" package. Install it with: npm install pg'
55
55
  );
56
56
  }
57
57
 
@@ -61,3 +61,19 @@ export async function loadPostgresDeps(): Promise<PostgresDeps> {
61
61
 
62
62
  return loadingPromise;
63
63
  }
64
+
65
+ /**
66
+ * Synchronously access the cached PostgreSQL deps. Throws if
67
+ * `loadPostgresDeps()` has not yet completed. Used by code paths that run
68
+ * after `PostgresStorageAdapter.initialize()` and need direct access to
69
+ * `sql` / `Kysely` without re-awaiting.
70
+ */
71
+ export function requirePostgresDeps(): PostgresDeps {
72
+ if (!cached) {
73
+ throw new Error(
74
+ 'PostgreSQL dependencies have not been loaded yet. ' +
75
+ 'Call loadPostgresDeps() (or PostgresStorageAdapter.initialize()) first.'
76
+ );
77
+ }
78
+ return cached;
79
+ }