embedded-postgres-node 0.0.0-stage → 0.1.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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +113 -2
  3. package/dist/cjs/binary.d.ts +24 -0
  4. package/dist/cjs/binary.js +168 -0
  5. package/dist/cjs/binary.js.map +1 -0
  6. package/dist/cjs/errors.d.ts +9 -0
  7. package/dist/cjs/errors.js +35 -0
  8. package/dist/cjs/errors.js.map +1 -0
  9. package/dist/cjs/index.d.ts +7 -0
  10. package/dist/cjs/index.js +13 -0
  11. package/dist/cjs/index.js.map +1 -0
  12. package/dist/cjs/lifecycle.d.ts +21 -0
  13. package/dist/cjs/lifecycle.js +209 -0
  14. package/dist/cjs/lifecycle.js.map +1 -0
  15. package/dist/cjs/options.d.ts +46 -0
  16. package/dist/cjs/options.js +79 -0
  17. package/dist/cjs/options.js.map +1 -0
  18. package/dist/cjs/package.json +1 -0
  19. package/dist/cjs/protocol.d.ts +22 -0
  20. package/dist/cjs/protocol.js +70 -0
  21. package/dist/cjs/protocol.js.map +1 -0
  22. package/dist/esm/binary.d.ts +24 -0
  23. package/dist/esm/binary.js +162 -0
  24. package/dist/esm/binary.js.map +1 -0
  25. package/dist/esm/errors.d.ts +9 -0
  26. package/dist/esm/errors.js +29 -0
  27. package/dist/esm/errors.js.map +1 -0
  28. package/dist/esm/index.d.ts +7 -0
  29. package/dist/esm/index.js +4 -0
  30. package/dist/esm/index.js.map +1 -0
  31. package/dist/esm/lifecycle.d.ts +21 -0
  32. package/dist/esm/lifecycle.js +205 -0
  33. package/dist/esm/lifecycle.js.map +1 -0
  34. package/dist/esm/options.d.ts +46 -0
  35. package/dist/esm/options.js +76 -0
  36. package/dist/esm/options.js.map +1 -0
  37. package/dist/esm/protocol.d.ts +22 -0
  38. package/dist/esm/protocol.js +66 -0
  39. package/dist/esm/protocol.js.map +1 -0
  40. package/docs/api.md +73 -0
  41. package/docs/binaries.md +50 -0
  42. package/docs/integration.md +39 -0
  43. package/docs/releasing.md +47 -0
  44. package/docs/validation.md +94 -0
  45. package/examples/jest.config.cjs +1 -0
  46. package/examples/jest.test.cjs +22 -0
  47. package/examples/migrations.mjs +17 -0
  48. package/examples/node-test.mjs +22 -0
  49. package/examples/options.mjs +6 -0
  50. package/examples/scoped.mjs +21 -0
  51. package/examples/vitest.config.mjs +2 -0
  52. package/examples/vitest.test.mjs +21 -0
  53. package/package.json +73 -4
@@ -0,0 +1,162 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { createReadStream } from 'node:fs';
3
+ import { access, chmod, mkdir, open, rename, rm } from 'node:fs/promises';
4
+ import { constants } from 'node:fs';
5
+ import { homedir } from 'node:os';
6
+ import { join, resolve } from 'node:path';
7
+ import { PostgresError, positiveTimeout } from './errors.js';
8
+ /** Reviewed upstream release; every platform asset was checked against checksums.txt. */
9
+ export const DEFAULT_CLI_RELEASE = Object.freeze({
10
+ version: 'v2.0.0-alpha.1',
11
+ checksums: Object.freeze({
12
+ 'linux-amd64': '69ef1bd6ca8ec00acf74fe90a5b6204657caa833934aada36591000b90daa7c6',
13
+ 'linux-arm64': '9bb6f2febcf3ce92b678c25aa1e06ae2e0a46a728b6da3ef31709aed2a2493a5',
14
+ 'darwin-amd64': '0f28163e7f346f444d7b3e790205df22e4e92d88acd2de15bd650fa45b114ffb',
15
+ 'darwin-arm64': '97866b33b4a59a5f60a807b55593af162d65f4e4f903696f2c1f638c01de19c0',
16
+ 'windows-amd64': '787e744b5c1c8ae88f77a043d0e3400a220e07e9ce4d03a55b9073a97f4cee6e',
17
+ 'windows-arm64': '07ee6b62c24c0e1a61a3523cf6331560b8a0f64ab780bb5ee6240541a568364d',
18
+ }),
19
+ });
20
+ export function cliPlatform(platform = process.platform, arch = process.arch) {
21
+ const os = platform === 'win32' ? 'windows' : platform;
22
+ const cpu = arch === 'x64' ? 'amd64' : arch;
23
+ if (!['linux', 'darwin', 'windows'].includes(os) || !['amd64', 'arm64'].includes(cpu)) {
24
+ throw new PostgresError('BINARY', 'CLI supports Linux, macOS and Windows on x64 and arm64');
25
+ }
26
+ return `${os}-${cpu}`;
27
+ }
28
+ export function cliAsset(target) {
29
+ return `embedded-postgres_${target.replace('-', '_')}${target.startsWith('windows-') ? '.exe' : ''}`;
30
+ }
31
+ async function digest(path) {
32
+ try {
33
+ const hash = createHash('sha256');
34
+ for await (const chunk of createReadStream(path))
35
+ hash.update(chunk);
36
+ return hash.digest('hex');
37
+ }
38
+ catch (error) {
39
+ if (error.code === 'ENOENT')
40
+ return undefined;
41
+ throw error;
42
+ }
43
+ }
44
+ const maximumBytes = 128 * 1024 * 1024;
45
+ function validateUrl(url, allowQuery = false) {
46
+ if (url.username || url.password || (!allowQuery && url.search) || url.hash || !(url.protocol === 'https:' || (url.protocol === 'http:' && ['127.0.0.1', '[::1]', 'localhost'].includes(url.hostname)))) {
47
+ throw new PostgresError('CONFIG', 'CLI downloads require HTTPS (HTTP is allowed only for loopback mirrors), without credentials, query or fragment');
48
+ }
49
+ }
50
+ async function download(url, signal) {
51
+ for (let redirects = 0; redirects <= 5; redirects++) {
52
+ validateUrl(url, true); // GitHub's asset CDN uses signed query parameters.
53
+ const response = await fetch(url, { signal, redirect: 'manual' });
54
+ if (![301, 302, 303, 307, 308].includes(response.status))
55
+ return response;
56
+ await response.body?.cancel();
57
+ const location = response.headers.get('location');
58
+ if (!location)
59
+ throw new PostgresError('BINARY', 'CLI download redirect has no location');
60
+ url = new URL(location, url);
61
+ }
62
+ throw new PostgresError('BINARY', 'CLI download has too many redirects');
63
+ }
64
+ /** Explicit source, then EMBEDDED_POSTGRES_CLI, then the package's pinned release. */
65
+ export async function resolveCli(source, signal) {
66
+ if (signal?.aborted)
67
+ throw new PostgresError('ABORTED', 'CLI resolution aborted');
68
+ source ??= process.env.EMBEDDED_POSTGRES_CLI ? { path: process.env.EMBEDDED_POSTGRES_CLI } : {};
69
+ if ('path' in source) {
70
+ if (!source.path || source.path.includes('\0'))
71
+ throw new PostgresError('CONFIG', 'cli.path must name an executable');
72
+ const path = resolve(source.path);
73
+ try {
74
+ await access(path, constants.X_OK);
75
+ }
76
+ catch {
77
+ throw new PostgresError('BINARY', 'Local CLI is missing or not executable');
78
+ }
79
+ return path;
80
+ }
81
+ const release = source.release ?? DEFAULT_CLI_RELEASE;
82
+ if (!/^v2\.\d+\.\d+(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$/.test(release.version))
83
+ throw new PostgresError('CONFIG', 'CLI release must be an exact v2 tag');
84
+ const target = cliPlatform();
85
+ const expected = release.checksums[target]?.toLowerCase();
86
+ if (!expected || !/^[a-f0-9]{64}$/.test(expected))
87
+ throw new PostgresError('CONFIG', 'A trusted SHA-256 checksum is required for this CLI platform');
88
+ const timeout = positiveTimeout(source.downloadTimeoutMs ?? 120_000, 'downloadTimeoutMs');
89
+ let base;
90
+ try {
91
+ base = new URL(release.baseUrl ?? 'https://github.com/fergusstrange/embedded-postgres/releases/download/');
92
+ }
93
+ catch {
94
+ throw new PostgresError('CONFIG', 'Invalid CLI release base URL');
95
+ }
96
+ validateUrl(base);
97
+ base.pathname = base.pathname.replace(/\/?$/, '/');
98
+ const asset = cliAsset(target);
99
+ const directory = join(resolve(source.cacheDir ?? join(homedir(), '.cache', 'embedded-postgres-node')), release.version, expected);
100
+ const destination = join(directory, asset);
101
+ let temporary;
102
+ try {
103
+ if (await digest(destination) === expected) {
104
+ await chmod(destination, 0o700);
105
+ return destination;
106
+ }
107
+ if (source.offline)
108
+ throw new PostgresError('BINARY', 'Verified CLI is unavailable in the offline cache');
109
+ await mkdir(directory, { recursive: true, mode: 0o700 });
110
+ temporary = join(directory, `.${asset}.${randomUUID()}.tmp`);
111
+ const downloadSignal = AbortSignal.any([AbortSignal.timeout(timeout), ...(signal ? [signal] : [])]);
112
+ const response = await download(new URL(`${release.version}/${asset}`, base), downloadSignal);
113
+ if (!response.ok || !response.body) {
114
+ await response.body?.cancel();
115
+ throw new PostgresError('BINARY', `CLI download failed (HTTP ${response.status}); verify that the pinned release has been published`);
116
+ }
117
+ if (Number(response.headers.get('content-length')) > maximumBytes) {
118
+ await response.body.cancel();
119
+ throw new PostgresError('BINARY', 'CLI download exceeds 128 MiB');
120
+ }
121
+ const file = await open(temporary, 'wx', 0o700);
122
+ const hash = createHash('sha256');
123
+ let size = 0;
124
+ try {
125
+ for await (const chunk of response.body) {
126
+ size += chunk.byteLength;
127
+ if (size > maximumBytes)
128
+ throw new PostgresError('BINARY', 'CLI download exceeds 128 MiB');
129
+ hash.update(chunk);
130
+ await file.writeFile(chunk);
131
+ }
132
+ }
133
+ finally {
134
+ await file.close();
135
+ }
136
+ if (hash.digest('hex') !== expected)
137
+ throw new PostgresError('BINARY', 'CLI checksum mismatch; downloaded binary was not installed');
138
+ // Atomic replacement; concurrent callers install identical verified bytes.
139
+ try {
140
+ await rename(temporary, destination);
141
+ }
142
+ catch (error) {
143
+ if (await digest(destination) !== expected)
144
+ throw error;
145
+ }
146
+ return destination;
147
+ }
148
+ catch (error) {
149
+ if (signal?.aborted)
150
+ throw new PostgresError('ABORTED', 'CLI resolution aborted');
151
+ if (error instanceof PostgresError)
152
+ throw error;
153
+ if (error.name === 'TimeoutError')
154
+ throw new PostgresError('TIMEOUT', 'CLI download deadline exceeded');
155
+ throw new PostgresError('BINARY', 'Unable to resolve CLI binary; check network and cache permissions');
156
+ }
157
+ finally {
158
+ if (temporary)
159
+ await rm(temporary, { force: true });
160
+ }
161
+ }
162
+ //# sourceMappingURL=binary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binary.js","sourceRoot":"","sources":["../../src/binary.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomUUID } from 'node:crypto';\nimport { createReadStream } from 'node:fs';\nimport { access, chmod, mkdir, open, rename, rm } from 'node:fs/promises';\nimport { constants } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join, resolve } from 'node:path';\nimport { PostgresError, positiveTimeout } from './errors.js';\n\nexport type CliPlatform = 'linux-amd64' | 'linux-arm64' | 'darwin-amd64' | 'darwin-arm64' | 'windows-amd64' | 'windows-arm64';\nexport interface CliRelease {\n /** Exact published v2 tag. There is deliberately no implicit latest release. */\n version: string;\n /** Trusted SHA-256 pins, copied from reviewed upstream release checksums.txt. */\n checksums: Readonly<Partial<Record<CliPlatform, string>>>;\n /** Directory containing VERSION/ASSET, defaulting to upstream GitHub releases. */\n baseUrl?: string;\n}\n\n/** Reviewed upstream release; every platform asset was checked against checksums.txt. */\nexport const DEFAULT_CLI_RELEASE: Readonly<CliRelease> = Object.freeze({\n version: 'v2.0.0-alpha.1',\n checksums: Object.freeze({\n 'linux-amd64': '69ef1bd6ca8ec00acf74fe90a5b6204657caa833934aada36591000b90daa7c6',\n 'linux-arm64': '9bb6f2febcf3ce92b678c25aa1e06ae2e0a46a728b6da3ef31709aed2a2493a5',\n 'darwin-amd64': '0f28163e7f346f444d7b3e790205df22e4e92d88acd2de15bd650fa45b114ffb',\n 'darwin-arm64': '97866b33b4a59a5f60a807b55593af162d65f4e4f903696f2c1f638c01de19c0',\n 'windows-amd64': '787e744b5c1c8ae88f77a043d0e3400a220e07e9ce4d03a55b9073a97f4cee6e',\n 'windows-arm64': '07ee6b62c24c0e1a61a3523cf6331560b8a0f64ab780bb5ee6240541a568364d',\n }),\n});\n\nexport type CliSource = { path: string } | {\n /** Omit to use this package's reviewed DEFAULT_CLI_RELEASE. */\n release?: CliRelease;\n cacheDir?: string;\n offline?: boolean;\n downloadTimeoutMs?: number;\n};\n\nexport function cliPlatform(platform: string = process.platform, arch: string = process.arch): CliPlatform {\n const os = platform === 'win32' ? 'windows' : platform;\n const cpu = arch === 'x64' ? 'amd64' : arch;\n if (!['linux', 'darwin', 'windows'].includes(os) || !['amd64', 'arm64'].includes(cpu)) {\n throw new PostgresError('BINARY', 'CLI supports Linux, macOS and Windows on x64 and arm64');\n }\n return `${os}-${cpu}` as CliPlatform;\n}\n\nexport function cliAsset(target: CliPlatform): string {\n return `embedded-postgres_${target.replace('-', '_')}${target.startsWith('windows-') ? '.exe' : ''}`;\n}\n\nasync function digest(path: string): Promise<string | undefined> {\n try {\n const hash = createHash('sha256');\n for await (const chunk of createReadStream(path)) hash.update(chunk);\n return hash.digest('hex');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;\n throw error;\n }\n}\n\nconst maximumBytes = 128 * 1024 * 1024;\nfunction validateUrl(url: URL, allowQuery = false): void {\n if (url.username || url.password || (!allowQuery && url.search) || url.hash || !(url.protocol === 'https:' || (url.protocol === 'http:' && ['127.0.0.1', '[::1]', 'localhost'].includes(url.hostname)))) {\n throw new PostgresError('CONFIG', 'CLI downloads require HTTPS (HTTP is allowed only for loopback mirrors), without credentials, query or fragment');\n }\n}\n\nasync function download(url: URL, signal: AbortSignal): Promise<Response> {\n for (let redirects = 0; redirects <= 5; redirects++) {\n validateUrl(url, true); // GitHub's asset CDN uses signed query parameters.\n const response = await fetch(url, { signal, redirect: 'manual' });\n if (![301, 302, 303, 307, 308].includes(response.status)) return response;\n await response.body?.cancel();\n const location = response.headers.get('location');\n if (!location) throw new PostgresError('BINARY', 'CLI download redirect has no location');\n url = new URL(location, url);\n }\n throw new PostgresError('BINARY', 'CLI download has too many redirects');\n}\n\n/** Explicit source, then EMBEDDED_POSTGRES_CLI, then the package's pinned release. */\nexport async function resolveCli(source?: CliSource, signal?: AbortSignal): Promise<string> {\n if (signal?.aborted) throw new PostgresError('ABORTED', 'CLI resolution aborted');\n source ??= process.env.EMBEDDED_POSTGRES_CLI ? { path: process.env.EMBEDDED_POSTGRES_CLI } : {};\n if ('path' in source) {\n if (!source.path || source.path.includes('\\0')) throw new PostgresError('CONFIG', 'cli.path must name an executable');\n const path = resolve(source.path);\n try { await access(path, constants.X_OK); } catch { throw new PostgresError('BINARY', 'Local CLI is missing or not executable'); }\n return path;\n }\n const release = source.release ?? DEFAULT_CLI_RELEASE;\n if (!/^v2\\.\\d+\\.\\d+(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$/.test(release.version)) throw new PostgresError('CONFIG', 'CLI release must be an exact v2 tag');\n const target = cliPlatform();\n const expected = release.checksums[target]?.toLowerCase();\n if (!expected || !/^[a-f0-9]{64}$/.test(expected)) throw new PostgresError('CONFIG', 'A trusted SHA-256 checksum is required for this CLI platform');\n const timeout = positiveTimeout(source.downloadTimeoutMs ?? 120_000, 'downloadTimeoutMs');\n let base: URL;\n try { base = new URL(release.baseUrl ?? 'https://github.com/fergusstrange/embedded-postgres/releases/download/'); } catch { throw new PostgresError('CONFIG', 'Invalid CLI release base URL'); }\n validateUrl(base);\n base.pathname = base.pathname.replace(/\\/?$/, '/');\n const asset = cliAsset(target);\n const directory = join(resolve(source.cacheDir ?? join(homedir(), '.cache', 'embedded-postgres-node')), release.version, expected);\n const destination = join(directory, asset);\n let temporary: string | undefined;\n try {\n if (await digest(destination) === expected) {\n await chmod(destination, 0o700);\n return destination;\n }\n if (source.offline) throw new PostgresError('BINARY', 'Verified CLI is unavailable in the offline cache');\n await mkdir(directory, { recursive: true, mode: 0o700 });\n temporary = join(directory, `.${asset}.${randomUUID()}.tmp`);\n const downloadSignal = AbortSignal.any([AbortSignal.timeout(timeout), ...(signal ? [signal] : [])]);\n const response = await download(new URL(`${release.version}/${asset}`, base), downloadSignal);\n if (!response.ok || !response.body) {\n await response.body?.cancel();\n throw new PostgresError('BINARY', `CLI download failed (HTTP ${response.status}); verify that the pinned release has been published`);\n }\n if (Number(response.headers.get('content-length')) > maximumBytes) {\n await response.body.cancel();\n throw new PostgresError('BINARY', 'CLI download exceeds 128 MiB');\n }\n const file = await open(temporary, 'wx', 0o700);\n const hash = createHash('sha256');\n let size = 0;\n try {\n for await (const chunk of response.body) {\n size += chunk.byteLength;\n if (size > maximumBytes) throw new PostgresError('BINARY', 'CLI download exceeds 128 MiB');\n hash.update(chunk);\n await file.writeFile(chunk);\n }\n } finally { await file.close(); }\n if (hash.digest('hex') !== expected) throw new PostgresError('BINARY', 'CLI checksum mismatch; downloaded binary was not installed');\n // Atomic replacement; concurrent callers install identical verified bytes.\n try { await rename(temporary, destination); } catch (error) {\n if (await digest(destination) !== expected) throw error;\n }\n return destination;\n } catch (error) {\n if (signal?.aborted) throw new PostgresError('ABORTED', 'CLI resolution aborted');\n if (error instanceof PostgresError) throw error;\n if ((error as Error).name === 'TimeoutError') throw new PostgresError('TIMEOUT', 'CLI download deadline exceeded');\n throw new PostgresError('BINARY', 'Unable to resolve CLI binary; check network and cache permissions');\n } finally {\n if (temporary) await rm(temporary, { force: true });\n }\n}\n"]}
@@ -0,0 +1,9 @@
1
+ export type ErrorCode = 'CONFIG' | 'BINARY' | 'SPAWN' | 'PROTOCOL' | 'STARTUP' | 'TIMEOUT' | 'ABORTED' | 'EXIT' | 'SHUTDOWN';
2
+ /** Diagnostics deliberately omit command arguments, raw protocol and error causes. */
3
+ export declare class PostgresError extends Error {
4
+ readonly code: ErrorCode;
5
+ readonly stderr: string;
6
+ constructor(code: ErrorCode, message: string, stderr?: string);
7
+ }
8
+ export declare function redact(text: string, secrets: readonly string[]): string;
9
+ export declare function positiveTimeout(value: number, name: string): number;
@@ -0,0 +1,29 @@
1
+ /** Diagnostics deliberately omit command arguments, raw protocol and error causes. */
2
+ export class PostgresError extends Error {
3
+ code;
4
+ stderr;
5
+ constructor(code, message, stderr = '') {
6
+ super(message);
7
+ this.name = 'PostgresError';
8
+ this.code = code;
9
+ this.stderr = stderr;
10
+ }
11
+ }
12
+ export function redact(text, secrets) {
13
+ // Redact complete URLs, including Unix-socket URLs and query parameters.
14
+ let result = text.replace(/postgres(?:ql)?:\/\/[^\s"'<>]+/gi, '[redacted connection URL]');
15
+ for (const secret of secrets) {
16
+ if (secret) {
17
+ result = result.split(secret).join('[redacted]');
18
+ result = result.split(encodeURIComponent(secret)).join('[redacted]');
19
+ }
20
+ }
21
+ return result;
22
+ }
23
+ export function positiveTimeout(value, name) {
24
+ if (!Number.isSafeInteger(value) || value < 1 || value > 2_147_483_647) {
25
+ throw new PostgresError('CONFIG', `${name} must be an integer between 1 and 2147483647 milliseconds`);
26
+ }
27
+ return value;
28
+ }
29
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"AAEA,sFAAsF;AACtF,MAAM,OAAO,aAAc,SAAQ,KAAK;IAC7B,IAAI,CAAY;IAChB,MAAM,CAAS;IACxB,YAAY,IAAe,EAAE,OAAe,EAAE,MAAM,GAAG,EAAE;QACvD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED,MAAM,UAAU,MAAM,CAAC,IAAY,EAAE,OAA0B;IAC7D,yEAAyE;IACzE,IAAI,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,kCAAkC,EAAE,2BAA2B,CAAC,CAAC;IAC3F,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YACjD,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACvE,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAa,EAAE,IAAY;IACzD,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,aAAa,EAAE,CAAC;QACvE,MAAM,IAAI,aAAa,CAAC,QAAQ,EAAE,GAAG,IAAI,2DAA2D,CAAC,CAAC;IACxG,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["export type ErrorCode = 'CONFIG' | 'BINARY' | 'SPAWN' | 'PROTOCOL' | 'STARTUP' | 'TIMEOUT' | 'ABORTED' | 'EXIT' | 'SHUTDOWN';\n\n/** Diagnostics deliberately omit command arguments, raw protocol and error causes. */\nexport class PostgresError extends Error {\n readonly code: ErrorCode;\n readonly stderr: string;\n constructor(code: ErrorCode, message: string, stderr = '') {\n super(message);\n this.name = 'PostgresError';\n this.code = code;\n this.stderr = stderr;\n }\n}\n\nexport function redact(text: string, secrets: readonly string[]): string {\n // Redact complete URLs, including Unix-socket URLs and query parameters.\n let result = text.replace(/postgres(?:ql)?:\\/\\/[^\\s\"'<>]+/gi, '[redacted connection URL]');\n for (const secret of secrets) {\n if (secret) {\n result = result.split(secret).join('[redacted]');\n result = result.split(encodeURIComponent(secret)).join('[redacted]');\n }\n }\n return result;\n}\n\nexport function positiveTimeout(value: number, name: string): number {\n if (!Number.isSafeInteger(value) || value < 1 || value > 2_147_483_647) {\n throw new PostgresError('CONFIG', `${name} must be an integer between 1 and 2147483647 milliseconds`);\n }\n return value;\n}\n"]}
@@ -0,0 +1,7 @@
1
+ export { startPostgres, withPostgres } from './lifecycle.js';
2
+ export type { PostgresInstance, ExitResult } from './lifecycle.js';
3
+ export type { PostgresOptions } from './options.js';
4
+ export { resolveCli, cliPlatform, DEFAULT_CLI_RELEASE } from './binary.js';
5
+ export type { CliSource, CliRelease, CliPlatform } from './binary.js';
6
+ export { PostgresError } from './errors.js';
7
+ export type { ErrorCode } from './errors.js';
@@ -0,0 +1,4 @@
1
+ export { startPostgres, withPostgres } from './lifecycle.js';
2
+ export { resolveCli, cliPlatform, DEFAULT_CLI_RELEASE } from './binary.js';
3
+ export { PostgresError } from './errors.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAG7D,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAE3E,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC","sourcesContent":["export { startPostgres, withPostgres } from './lifecycle.js';\nexport type { PostgresInstance, ExitResult } from './lifecycle.js';\nexport type { PostgresOptions } from './options.js';\nexport { resolveCli, cliPlatform, DEFAULT_CLI_RELEASE } from './binary.js';\nexport type { CliSource, CliRelease, CliPlatform } from './binary.js';\nexport { PostgresError } from './errors.js';\nexport type { ErrorCode } from './errors.js';\n"]}
@@ -0,0 +1,21 @@
1
+ import { PostgresError } from './errors.js';
2
+ import type { PostgresOptions } from './options.js';
3
+ export interface ExitResult {
4
+ readonly exitCode: number | null;
5
+ readonly signal: NodeJS.Signals | null;
6
+ readonly stopped: boolean;
7
+ readonly error?: PostgresError;
8
+ }
9
+ export interface PostgresInstance extends AsyncDisposable {
10
+ /** A credential: do not log or snapshot it. */
11
+ readonly connectionUrl: string;
12
+ readonly port: number;
13
+ /** Resolves on termination; never rejects, so ignoring it cannot cause an unhandled rejection. */
14
+ readonly closed: Promise<ExitResult>;
15
+ /** Closes stdin and waits for stopped and process exit. Repeated calls share one result. */
16
+ stop(): Promise<void>;
17
+ }
18
+ /** Start a database whose lifetime is owned by the caller (or its AbortSignal). */
19
+ export declare function startPostgres(options?: PostgresOptions): Promise<PostgresInstance>;
20
+ /** User-owned migrations/pools belong inside fn; close pools before fn returns. */
21
+ export declare function withPostgres<T>(options: PostgresOptions, fn: (postgres: PostgresInstance) => Promise<T>): Promise<T>;
@@ -0,0 +1,205 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { inspect } from 'node:util';
3
+ import { resolveCli } from './binary.js';
4
+ import { PostgresError, redact } from './errors.js';
5
+ import { prepareOptions } from './options.js';
6
+ import { ProtocolParser } from './protocol.js';
7
+ /** Start a database whose lifetime is owned by the caller (or its AbortSignal). */
8
+ export async function startPostgres(options = {}) {
9
+ const prepared = prepareOptions(options);
10
+ const deadline = new AbortController();
11
+ const lifetime = AbortSignal.any([deadline.signal, ...(options.signal ? [options.signal] : [])]);
12
+ const startupTimer = setTimeout(() => deadline.abort(), prepared.startupDeadlineMs);
13
+ let executable;
14
+ try {
15
+ executable = await resolveCli(options.cli, lifetime);
16
+ if (lifetime.aborted)
17
+ throw new PostgresError('ABORTED', 'PostgreSQL startup aborted');
18
+ }
19
+ catch (error) {
20
+ clearTimeout(startupTimer);
21
+ if (deadline.signal.aborted)
22
+ throw new PostgresError('TIMEOUT', 'PostgreSQL startup deadline exceeded during CLI resolution');
23
+ throw error;
24
+ }
25
+ return new Promise((resolve, reject) => {
26
+ const child = spawn(executable, prepared.args, {
27
+ stdio: ['pipe', 'pipe', 'pipe'], env: prepared.env, windowsHide: true, shell: false,
28
+ // Escape Node's Windows kill-on-parent-exit job so the CLI can observe
29
+ // stdin EOF and clean up. Keep the child referenced and its pipes owned.
30
+ detached: process.platform === 'win32',
31
+ });
32
+ let ready = false;
33
+ let stopped = false;
34
+ let stopping = false;
35
+ let finished = false;
36
+ let stderr = '';
37
+ let discardStderrLine = false;
38
+ let failure;
39
+ let brokenProtocol = false;
40
+ let shutdownTimer;
41
+ let reapTimer;
42
+ const secrets = [prepared.password];
43
+ let resolveClosed;
44
+ const closed = new Promise(resolve => { resolveClosed = resolve; });
45
+ let stopPromise;
46
+ const finish = (exitCode, signal) => {
47
+ if (finished)
48
+ return;
49
+ finished = true;
50
+ clearTimeout(startupTimer);
51
+ clearTimeout(shutdownTimer);
52
+ clearTimeout(reapTimer);
53
+ lifetime.removeEventListener('abort', abort);
54
+ if (!failure && (!ready || !stopped || exitCode !== 0)) {
55
+ failure = new PostgresError('EXIT', `CLI exited ${ready ? 'after' : 'before'} readiness without clean shutdown (code ${exitCode}, signal ${signal})`);
56
+ }
57
+ const error = failure ? new PostgresError(failure.code, redact(failure.message, secrets), redact(stderr, secrets)) : undefined;
58
+ const result = Object.freeze({ exitCode, signal, stopped, ...(error ? { error } : {}) });
59
+ resolveClosed(result);
60
+ if (!ready)
61
+ reject(error ?? new PostgresError('EXIT', 'CLI exited before readiness'));
62
+ };
63
+ const requestStop = () => {
64
+ if (stopping || finished)
65
+ return;
66
+ stopping = true;
67
+ clearTimeout(startupTimer);
68
+ child.stdin.end();
69
+ shutdownTimer = setTimeout(() => {
70
+ failure = new PostgresError('SHUTDOWN', `${failure ? failure.message + '; ' : ''}CLI shutdown deadline exceeded; forced owner termination, PostgreSQL cleanup cannot be confirmed`);
71
+ child.kill('SIGKILL');
72
+ // A descendant holding stdio open must not make teardown wait forever.
73
+ reapTimer = setTimeout(() => {
74
+ child.stdin.destroy();
75
+ child.stdout.destroy();
76
+ child.stderr.destroy();
77
+ child.unref();
78
+ finish(child.exitCode, child.signalCode);
79
+ }, 2_000);
80
+ }, prepared.shutdownDeadlineMs);
81
+ };
82
+ const fail = (error) => { failure ??= error; requestStop(); };
83
+ const abort = () => fail(new PostgresError(deadline.signal.aborted ? 'TIMEOUT' : 'ABORTED', deadline.signal.aborted ? 'PostgreSQL startup deadline exceeded' : 'PostgreSQL lifetime aborted'));
84
+ const stop = () => {
85
+ requestStop();
86
+ stopPromise ??= closed.then(result => { if (result.error)
87
+ throw result.error; });
88
+ return stopPromise;
89
+ };
90
+ const parser = new ProtocolParser(event => {
91
+ if (event.event === 'error') {
92
+ fail(new PostgresError(ready ? 'EXIT' : 'STARTUP', event.error));
93
+ }
94
+ else if (event.event === 'stopped') {
95
+ if (stopped || !ready) {
96
+ fail(new PostgresError('PROTOCOL', 'Unexpected CLI stopped event'));
97
+ return;
98
+ }
99
+ stopped = true;
100
+ if (!stopping)
101
+ fail(new PostgresError('EXIT', 'CLI stopped unexpectedly'));
102
+ }
103
+ else {
104
+ if (ready || stopped) {
105
+ fail(new PostgresError('PROTOCOL', 'Duplicate or out-of-order CLI ready event'));
106
+ return;
107
+ }
108
+ secrets.push(event.connection_url, new URL(event.connection_url).password);
109
+ if (stopping)
110
+ return;
111
+ ready = true;
112
+ clearTimeout(startupTimer);
113
+ const instance = {
114
+ get connectionUrl() { return event.connection_url; },
115
+ port: event.port, closed, stop,
116
+ [Symbol.asyncDispose]: stop,
117
+ };
118
+ // Common inspection/serialization should not accidentally print credentials.
119
+ Object.defineProperties(instance, {
120
+ connectionUrl: { enumerable: false },
121
+ toJSON: { value: () => ({ port: event.port }) },
122
+ [inspect.custom]: { value: () => `PostgresInstance { port: ${event.port} }` },
123
+ });
124
+ resolve(Object.freeze(instance));
125
+ }
126
+ });
127
+ child.stdout.setEncoding('utf8');
128
+ child.stderr.setEncoding('utf8');
129
+ child.stdout.on('data', (chunk) => {
130
+ if (brokenProtocol)
131
+ return;
132
+ try {
133
+ parser.push(chunk);
134
+ }
135
+ catch (error) {
136
+ brokenProtocol = true;
137
+ fail(error);
138
+ }
139
+ });
140
+ child.stdout.on('end', () => {
141
+ if (brokenProtocol)
142
+ return;
143
+ try {
144
+ parser.finish();
145
+ }
146
+ catch (error) {
147
+ brokenProtocol = true;
148
+ fail(error);
149
+ }
150
+ // Protocol EOF while the owner lives must also release our lifetime pipe.
151
+ if (!stopped)
152
+ requestStop();
153
+ });
154
+ child.stderr.on('data', (chunk) => {
155
+ if (discardStderrLine) {
156
+ const newline = chunk.indexOf('\n');
157
+ if (newline === -1)
158
+ return;
159
+ chunk = chunk.slice(newline + 1);
160
+ discardStderrLine = false;
161
+ }
162
+ stderr += chunk;
163
+ if (stderr.length > 64 * 1024) {
164
+ stderr = stderr.slice(-64 * 1024);
165
+ // Discard a truncated line entirely, including its future chunks.
166
+ const newline = stderr.indexOf('\n');
167
+ discardStderrLine = newline === -1;
168
+ stderr = discardStderrLine ? '' : stderr.slice(newline + 1);
169
+ }
170
+ });
171
+ child.stdout.on('error', () => fail(new PostgresError('PROTOCOL', 'Unable to read CLI protocol pipe')));
172
+ child.stderr.on('error', () => fail(new PostgresError('EXIT', 'Unable to read CLI diagnostic pipe')));
173
+ child.stdin.on('error', () => fail(new PostgresError('EXIT', 'CLI stdin pipe closed unexpectedly')));
174
+ child.on('error', () => fail(new PostgresError('SPAWN', 'Unable to spawn CLI executable')));
175
+ child.on('close', finish);
176
+ lifetime.addEventListener('abort', abort, { once: true });
177
+ if (lifetime.aborted)
178
+ abort();
179
+ });
180
+ }
181
+ /** User-owned migrations/pools belong inside fn; close pools before fn returns. */
182
+ export async function withPostgres(options, fn) {
183
+ const postgres = await startPostgres(options);
184
+ let failed = false;
185
+ let callbackError;
186
+ try {
187
+ return await fn(postgres);
188
+ }
189
+ catch (error) {
190
+ failed = true;
191
+ callbackError = error;
192
+ throw error;
193
+ }
194
+ finally {
195
+ try {
196
+ await postgres.stop();
197
+ }
198
+ catch (cleanupError) {
199
+ if (failed)
200
+ throw new AggregateError([callbackError, cleanupError], 'PostgreSQL callback and cleanup both failed');
201
+ throw cleanupError;
202
+ }
203
+ }
204
+ }
205
+ //# sourceMappingURL=lifecycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../../src/lifecycle.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport { inspect } from 'node:util';\nimport { resolveCli } from './binary.js';\nimport { PostgresError, redact } from './errors.js';\nimport { prepareOptions } from './options.js';\nimport type { PostgresOptions } from './options.js';\nimport { ProtocolParser } from './protocol.js';\n\nexport interface ExitResult {\n readonly exitCode: number | null;\n readonly signal: NodeJS.Signals | null;\n readonly stopped: boolean;\n readonly error?: PostgresError;\n}\n\nexport interface PostgresInstance extends AsyncDisposable {\n /** A credential: do not log or snapshot it. */\n readonly connectionUrl: string;\n readonly port: number;\n /** Resolves on termination; never rejects, so ignoring it cannot cause an unhandled rejection. */\n readonly closed: Promise<ExitResult>;\n /** Closes stdin and waits for stopped and process exit. Repeated calls share one result. */\n stop(): Promise<void>;\n}\n\n/** Start a database whose lifetime is owned by the caller (or its AbortSignal). */\nexport async function startPostgres(options: PostgresOptions = {}): Promise<PostgresInstance> {\n const prepared = prepareOptions(options);\n const deadline = new AbortController();\n const lifetime = AbortSignal.any([deadline.signal, ...(options.signal ? [options.signal] : [])]);\n const startupTimer = setTimeout(() => deadline.abort(), prepared.startupDeadlineMs);\n let executable: string;\n try {\n executable = await resolveCli(options.cli, lifetime);\n if (lifetime.aborted) throw new PostgresError('ABORTED', 'PostgreSQL startup aborted');\n } catch (error) {\n clearTimeout(startupTimer);\n if (deadline.signal.aborted) throw new PostgresError('TIMEOUT', 'PostgreSQL startup deadline exceeded during CLI resolution');\n throw error;\n }\n\n return new Promise<PostgresInstance>((resolve, reject) => {\n const child = spawn(executable, prepared.args, {\n stdio: ['pipe', 'pipe', 'pipe'], env: prepared.env, windowsHide: true, shell: false,\n // Escape Node's Windows kill-on-parent-exit job so the CLI can observe\n // stdin EOF and clean up. Keep the child referenced and its pipes owned.\n detached: process.platform === 'win32',\n });\n let ready = false;\n let stopped = false;\n let stopping = false;\n let finished = false;\n let stderr = '';\n let discardStderrLine = false;\n let failure: PostgresError | undefined;\n let brokenProtocol = false;\n let shutdownTimer: NodeJS.Timeout | undefined;\n let reapTimer: NodeJS.Timeout | undefined;\n const secrets = [prepared.password];\n let resolveClosed!: (result: ExitResult) => void;\n const closed = new Promise<ExitResult>(resolve => { resolveClosed = resolve; });\n let stopPromise: Promise<void> | undefined;\n\n const finish = (exitCode: number | null, signal: NodeJS.Signals | null) => {\n if (finished) return;\n finished = true;\n clearTimeout(startupTimer);\n clearTimeout(shutdownTimer);\n clearTimeout(reapTimer);\n lifetime.removeEventListener('abort', abort);\n if (!failure && (!ready || !stopped || exitCode !== 0)) {\n failure = new PostgresError('EXIT', `CLI exited ${ready ? 'after' : 'before'} readiness without clean shutdown (code ${exitCode}, signal ${signal})`);\n }\n const error = failure ? new PostgresError(failure.code, redact(failure.message, secrets), redact(stderr, secrets)) : undefined;\n const result: ExitResult = Object.freeze({ exitCode, signal, stopped, ...(error ? { error } : {}) });\n resolveClosed(result);\n if (!ready) reject(error ?? new PostgresError('EXIT', 'CLI exited before readiness'));\n };\n const requestStop = () => {\n if (stopping || finished) return;\n stopping = true;\n clearTimeout(startupTimer);\n child.stdin.end();\n shutdownTimer = setTimeout(() => {\n failure = new PostgresError('SHUTDOWN', `${failure ? failure.message + '; ' : ''}CLI shutdown deadline exceeded; forced owner termination, PostgreSQL cleanup cannot be confirmed`);\n child.kill('SIGKILL');\n // A descendant holding stdio open must not make teardown wait forever.\n reapTimer = setTimeout(() => {\n child.stdin.destroy();\n child.stdout.destroy();\n child.stderr.destroy();\n child.unref();\n finish(child.exitCode, child.signalCode);\n }, 2_000);\n }, prepared.shutdownDeadlineMs);\n };\n const fail = (error: PostgresError) => { failure ??= error; requestStop(); };\n const abort = () => fail(new PostgresError(deadline.signal.aborted ? 'TIMEOUT' : 'ABORTED', deadline.signal.aborted ? 'PostgreSQL startup deadline exceeded' : 'PostgreSQL lifetime aborted'));\n const stop = () => {\n requestStop();\n stopPromise ??= closed.then(result => { if (result.error) throw result.error; });\n return stopPromise;\n };\n const parser = new ProtocolParser(event => {\n if (event.event === 'error') {\n fail(new PostgresError(ready ? 'EXIT' : 'STARTUP', event.error));\n } else if (event.event === 'stopped') {\n if (stopped || !ready) { fail(new PostgresError('PROTOCOL', 'Unexpected CLI stopped event')); return; }\n stopped = true;\n if (!stopping) fail(new PostgresError('EXIT', 'CLI stopped unexpectedly'));\n } else {\n if (ready || stopped) { fail(new PostgresError('PROTOCOL', 'Duplicate or out-of-order CLI ready event')); return; }\n secrets.push(event.connection_url, new URL(event.connection_url).password);\n if (stopping) return;\n ready = true;\n clearTimeout(startupTimer);\n const instance: PostgresInstance = {\n get connectionUrl() { return event.connection_url; },\n port: event.port, closed, stop,\n [Symbol.asyncDispose]: stop,\n };\n // Common inspection/serialization should not accidentally print credentials.\n Object.defineProperties(instance, {\n connectionUrl: { enumerable: false },\n toJSON: { value: () => ({ port: event.port }) },\n [inspect.custom]: { value: () => `PostgresInstance { port: ${event.port} }` },\n });\n resolve(Object.freeze(instance));\n }\n });\n child.stdout.setEncoding('utf8');\n child.stderr.setEncoding('utf8');\n child.stdout.on('data', (chunk: string) => {\n if (brokenProtocol) return;\n try { parser.push(chunk); } catch (error) { brokenProtocol = true; fail(error as PostgresError); }\n });\n child.stdout.on('end', () => {\n if (brokenProtocol) return;\n try { parser.finish(); } catch (error) { brokenProtocol = true; fail(error as PostgresError); }\n // Protocol EOF while the owner lives must also release our lifetime pipe.\n if (!stopped) requestStop();\n });\n child.stderr.on('data', (chunk: string) => {\n if (discardStderrLine) {\n const newline = chunk.indexOf('\\n');\n if (newline === -1) return;\n chunk = chunk.slice(newline + 1);\n discardStderrLine = false;\n }\n stderr += chunk;\n if (stderr.length > 64 * 1024) {\n stderr = stderr.slice(-64 * 1024);\n // Discard a truncated line entirely, including its future chunks.\n const newline = stderr.indexOf('\\n');\n discardStderrLine = newline === -1;\n stderr = discardStderrLine ? '' : stderr.slice(newline + 1);\n }\n });\n child.stdout.on('error', () => fail(new PostgresError('PROTOCOL', 'Unable to read CLI protocol pipe')));\n child.stderr.on('error', () => fail(new PostgresError('EXIT', 'Unable to read CLI diagnostic pipe')));\n child.stdin.on('error', () => fail(new PostgresError('EXIT', 'CLI stdin pipe closed unexpectedly')));\n child.on('error', () => fail(new PostgresError('SPAWN', 'Unable to spawn CLI executable')));\n child.on('close', finish);\n lifetime.addEventListener('abort', abort, { once: true });\n if (lifetime.aborted) abort();\n });\n}\n\n/** User-owned migrations/pools belong inside fn; close pools before fn returns. */\nexport async function withPostgres<T>(options: PostgresOptions, fn: (postgres: PostgresInstance) => Promise<T>): Promise<T> {\n const postgres = await startPostgres(options);\n let failed = false;\n let callbackError: unknown;\n try { return await fn(postgres); } catch (error) { failed = true; callbackError = error; throw error; }\n finally {\n try { await postgres.stop(); } catch (cleanupError) {\n if (failed) throw new AggregateError([callbackError, cleanupError], 'PostgreSQL callback and cleanup both failed');\n throw cleanupError;\n }\n }\n}\n"]}
@@ -0,0 +1,46 @@
1
+ import type { CliSource } from './binary.js';
2
+ export interface PostgresOptions {
3
+ /** Explicit executable/release; otherwise EMBEDDED_POSTGRES_CLI or the package's pinned release. */
4
+ cli?: CliSource;
5
+ postgresVersion?: string;
6
+ database?: string;
7
+ username?: string;
8
+ password?: string;
9
+ /** Zero (the default) lets PostgreSQL select an available port. */
10
+ port?: number;
11
+ cacheDir?: string;
12
+ binaries?: string;
13
+ storage?: {
14
+ type: 'disposable';
15
+ workDir?: string;
16
+ } | {
17
+ type: 'persistent';
18
+ dataDir: string;
19
+ workDir?: string;
20
+ };
21
+ runAs?: {
22
+ uid: number;
23
+ gid: number;
24
+ };
25
+ parameters?: Readonly<Record<string, string>>;
26
+ startTimeoutMs?: number;
27
+ stopTimeoutMs?: number;
28
+ /** Outer startup deadline, including executable resolution. Defaults to startTimeoutMs + 30s. */
29
+ startupDeadlineMs?: number;
30
+ /** Must exceed stopTimeoutMs + 2s. Defaults to stopTimeoutMs + 5s. */
31
+ shutdownDeadlineMs?: number;
32
+ /** Lifetime cancellation: abort before or after readiness requests cleanup. */
33
+ signal?: AbortSignal;
34
+ /** Additional child environment. EP_* keys are reserved. */
35
+ env?: Readonly<Record<string, string>>;
36
+ /** CLI flag names without --. Core, credential, config and lifecycle flags are reserved. */
37
+ cliOptions?: Readonly<Record<string, string | number | boolean | readonly string[]>>;
38
+ }
39
+ export interface PreparedOptions {
40
+ args: string[];
41
+ env: NodeJS.ProcessEnv;
42
+ password: string;
43
+ startupDeadlineMs: number;
44
+ shutdownDeadlineMs: number;
45
+ }
46
+ export declare function prepareOptions(options: PostgresOptions): PreparedOptions;