pgserve 2.1.3 → 2.2.1

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 (235) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +105 -1
  3. package/bin/autopg-wrapper.cjs +16 -0
  4. package/bin/pgserve-wrapper.cjs +32 -6
  5. package/bin/postgres-server.js +56 -0
  6. package/console/README.md +131 -0
  7. package/console/api.js +173 -0
  8. package/console/app.jsx +483 -0
  9. package/console/colors_and_type.css +227 -0
  10. package/console/components.jsx +167 -0
  11. package/console/console.css +1666 -0
  12. package/console/data.jsx +350 -0
  13. package/console/index.html +31 -0
  14. package/console/screens/databases.jsx +5 -0
  15. package/console/screens/health.jsx +5 -0
  16. package/console/screens/ingress.jsx +5 -0
  17. package/console/screens/optimizer.jsx +5 -0
  18. package/console/screens/rlm-sim.jsx +5 -0
  19. package/console/screens/rlm-trace.jsx +5 -0
  20. package/console/screens/security.jsx +5 -0
  21. package/console/screens/settings.jsx +611 -0
  22. package/console/screens/sql.jsx +5 -0
  23. package/console/screens/sync.jsx +5 -0
  24. package/console/screens/tables.jsx +5 -0
  25. package/console/tweaks-panel.jsx +425 -0
  26. package/package.json +14 -2
  27. package/scripts/postinstall.cjs +60 -0
  28. package/src/cli-config.cjs +310 -0
  29. package/src/cli-install.cjs +112 -11
  30. package/src/cli-restart.cjs +228 -0
  31. package/src/cli-ui.cjs +580 -0
  32. package/src/cluster.js +43 -38
  33. package/src/postgres.js +141 -19
  34. package/src/settings-loader.cjs +235 -0
  35. package/src/settings-migrate.cjs +212 -0
  36. package/src/settings-pg-args.cjs +146 -0
  37. package/src/settings-schema.cjs +422 -0
  38. package/src/settings-validator.cjs +416 -0
  39. package/src/settings-writer.cjs +288 -0
  40. package/src/upgrade/index.js +65 -0
  41. package/src/upgrade/runner.js +23 -0
  42. package/src/upgrade/steps/binary-cache-flush.js +67 -0
  43. package/src/upgrade/steps/consumer-signal.js +40 -0
  44. package/src/upgrade/steps/env-refresh.js +89 -0
  45. package/src/upgrade/steps/health-validate.js +53 -0
  46. package/src/upgrade/steps/plpgsql-resolve.js +66 -0
  47. package/src/upgrade/steps/port-reconcile.js +52 -0
  48. package/.claude/context/windows-debug.md +0 -119
  49. package/.genie/AGENTS.md +0 -15
  50. package/.genie/agents/README.md +0 -110
  51. package/.genie/agents/analyze.md +0 -176
  52. package/.genie/agents/forge.md +0 -290
  53. package/.genie/agents/garbage-cleaner.md +0 -324
  54. package/.genie/agents/garbage-collector.md +0 -596
  55. package/.genie/agents/github-issue-gc.md +0 -618
  56. package/.genie/agents/review.md +0 -380
  57. package/.genie/agents/semantic-analyzer/find-duplicates.md +0 -90
  58. package/.genie/agents/semantic-analyzer/find-orphans.md +0 -99
  59. package/.genie/agents/semantic-analyzer.md +0 -101
  60. package/.genie/agents/update.md +0 -182
  61. package/.genie/agents/wish.md +0 -357
  62. package/.genie/brainstorms/pgserve-v2/DESIGN.md +0 -174
  63. package/.genie/code/AGENTS.md +0 -694
  64. package/.genie/code/agents/audit/risk.md +0 -173
  65. package/.genie/code/agents/audit/security.md +0 -189
  66. package/.genie/code/agents/audit.md +0 -145
  67. package/.genie/code/agents/challenge.md +0 -230
  68. package/.genie/code/agents/change-reviewer.md +0 -295
  69. package/.genie/code/agents/code-garbage-collector.md +0 -425
  70. package/.genie/code/agents/code-quality.md +0 -410
  71. package/.genie/code/agents/commit-suggester.md +0 -255
  72. package/.genie/code/agents/commit.md +0 -124
  73. package/.genie/code/agents/consensus.md +0 -204
  74. package/.genie/code/agents/daily-standup.md +0 -722
  75. package/.genie/code/agents/docgen.md +0 -48
  76. package/.genie/code/agents/explore.md +0 -79
  77. package/.genie/code/agents/fix.md +0 -100
  78. package/.genie/code/agents/git/commit-advisory.md +0 -219
  79. package/.genie/code/agents/git/workflows/issue.md +0 -244
  80. package/.genie/code/agents/git/workflows/pr.md +0 -179
  81. package/.genie/code/agents/git/workflows/release.md +0 -460
  82. package/.genie/code/agents/git/workflows/report.md +0 -342
  83. package/.genie/code/agents/git.md +0 -432
  84. package/.genie/code/agents/implementor.md +0 -161
  85. package/.genie/code/agents/install.md +0 -515
  86. package/.genie/code/agents/issue-creator.md +0 -344
  87. package/.genie/code/agents/polish.md +0 -116
  88. package/.genie/code/agents/qa.md +0 -653
  89. package/.genie/code/agents/refactor.md +0 -294
  90. package/.genie/code/agents/release.md +0 -1129
  91. package/.genie/code/agents/roadmap.md +0 -885
  92. package/.genie/code/agents/tests.md +0 -557
  93. package/.genie/code/agents/tracer.md +0 -50
  94. package/.genie/code/agents/update/upstream-update.md +0 -85
  95. package/.genie/code/agents/update/versions/generic-update.md +0 -305
  96. package/.genie/code/agents/vibe.md +0 -1317
  97. package/.genie/code/spells/agent-configuration.md +0 -58
  98. package/.genie/code/spells/automated-rc-publishing.md +0 -106
  99. package/.genie/code/spells/branch-tracker-guidance.md +0 -28
  100. package/.genie/code/spells/debug.md +0 -320
  101. package/.genie/code/spells/emoji-naming-convention.md +0 -303
  102. package/.genie/code/spells/evidence-storage.md +0 -26
  103. package/.genie/code/spells/file-naming-rules.md +0 -35
  104. package/.genie/code/spells/forge-code-blueprints.md +0 -195
  105. package/.genie/code/spells/genie-integration.md +0 -153
  106. package/.genie/code/spells/publishing-protocol.md +0 -61
  107. package/.genie/code/spells/team-consultation-protocol.md +0 -284
  108. package/.genie/code/spells/tool-requirements.md +0 -20
  109. package/.genie/code/spells/triad-maintenance-protocol.md +0 -154
  110. package/.genie/code/teams/tech-council/council.md +0 -328
  111. package/.genie/code/teams/tech-council/jt.md +0 -352
  112. package/.genie/code/teams/tech-council/nayr.md +0 -305
  113. package/.genie/code/teams/tech-council/oettam.md +0 -375
  114. package/.genie/neurons/README.md +0 -193
  115. package/.genie/neurons/forge.md +0 -106
  116. package/.genie/neurons/genie.md +0 -63
  117. package/.genie/neurons/review.md +0 -106
  118. package/.genie/neurons/wish.md +0 -104
  119. package/.genie/product/README.md +0 -20
  120. package/.genie/product/cli-automation.md +0 -359
  121. package/.genie/product/environment.md +0 -60
  122. package/.genie/product/mission.md +0 -60
  123. package/.genie/product/roadmap.md +0 -44
  124. package/.genie/product/tech-stack.md +0 -34
  125. package/.genie/product/templates/context-template.md +0 -218
  126. package/.genie/product/templates/qa-done-report-template.md +0 -68
  127. package/.genie/product/templates/review-report-template.md +0 -89
  128. package/.genie/product/templates/wish-template.md +0 -120
  129. package/.genie/scripts/helpers/analyze-commit.js +0 -195
  130. package/.genie/scripts/helpers/bullet-counter.js +0 -194
  131. package/.genie/scripts/helpers/bullet-find.js +0 -289
  132. package/.genie/scripts/helpers/bullet-id.js +0 -244
  133. package/.genie/scripts/helpers/check-secrets.js +0 -237
  134. package/.genie/scripts/helpers/count-tokens.js +0 -200
  135. package/.genie/scripts/helpers/create-frontmatter.js +0 -456
  136. package/.genie/scripts/helpers/detect-markers.js +0 -293
  137. package/.genie/scripts/helpers/detect-todos.js +0 -267
  138. package/.genie/scripts/helpers/detect-unlabeled-blocks.js +0 -135
  139. package/.genie/scripts/helpers/embeddings.js +0 -344
  140. package/.genie/scripts/helpers/find-empty-sections.js +0 -158
  141. package/.genie/scripts/helpers/index.js +0 -319
  142. package/.genie/scripts/helpers/validate-frontmatter.js +0 -578
  143. package/.genie/scripts/helpers/validate-links.js +0 -207
  144. package/.genie/scripts/helpers/validate-paths.js +0 -373
  145. package/.genie/spells/README.md +0 -9
  146. package/.genie/spells/ace-protocol.md +0 -118
  147. package/.genie/spells/ask-one-at-a-time.md +0 -175
  148. package/.genie/spells/backup-analyzer.md +0 -542
  149. package/.genie/spells/blocker.md +0 -12
  150. package/.genie/spells/break-things-move-fast.md +0 -56
  151. package/.genie/spells/context-candidates.md +0 -72
  152. package/.genie/spells/context-critic.md +0 -51
  153. package/.genie/spells/defer-to-expertise.md +0 -278
  154. package/.genie/spells/delegate-dont-do.md +0 -292
  155. package/.genie/spells/error-investigation-protocol.md +0 -328
  156. package/.genie/spells/evidence-based-completion.md +0 -273
  157. package/.genie/spells/experiment.md +0 -65
  158. package/.genie/spells/file-creation-protocol.md +0 -229
  159. package/.genie/spells/forge-integration.md +0 -281
  160. package/.genie/spells/forge-orchestration.md +0 -514
  161. package/.genie/spells/gather-context.md +0 -18
  162. package/.genie/spells/global-health-check.md +0 -34
  163. package/.genie/spells/global-noop-roundtrip.md +0 -25
  164. package/.genie/spells/install-genie.md +0 -1232
  165. package/.genie/spells/install.md +0 -82
  166. package/.genie/spells/investigate-before-commit.md +0 -112
  167. package/.genie/spells/know-yourself.md +0 -288
  168. package/.genie/spells/learn.md +0 -828
  169. package/.genie/spells/mcp-diagnostic-protocol.md +0 -246
  170. package/.genie/spells/mcp-first.md +0 -124
  171. package/.genie/spells/multi-step-execution.md +0 -67
  172. package/.genie/spells/orchestration-boundary-protocol.md +0 -256
  173. package/.genie/spells/orchestrator-not-implementor.md +0 -189
  174. package/.genie/spells/prompt.md +0 -746
  175. package/.genie/spells/reflect.md +0 -404
  176. package/.genie/spells/routing-decision-matrix.md +0 -368
  177. package/.genie/spells/run-in-parallel.md +0 -12
  178. package/.genie/spells/session-state-updater-example.md +0 -196
  179. package/.genie/spells/session-state-updater.md +0 -220
  180. package/.genie/spells/track-long-running-tasks.md +0 -133
  181. package/.genie/spells/troubleshoot-infrastructure.md +0 -176
  182. package/.genie/spells/upgrade-genie.md +0 -415
  183. package/.genie/spells/url-presentation-protocol.md +0 -301
  184. package/.genie/spells/wish-initiation.md +0 -158
  185. package/.genie/spells/wish-issue-linkage.md +0 -410
  186. package/.genie/spells/wish-lifecycle.md +0 -100
  187. package/.genie/state/provider-status.json +0 -3
  188. package/.genie/state/version.json +0 -16
  189. package/.genie/wishes/canonical-pgserve-pm2-supervision/WISH.md +0 -290
  190. package/.genie/wishes/pgserve-v2/BRIEF-from-genie-pgserve.md +0 -99
  191. package/.genie/wishes/pgserve-v2/WISH.md +0 -442
  192. package/.genie/wishes/release-system-genie-pattern/WISH.md +0 -268
  193. package/.genie/wishes/release-system-genie-pattern/validation.md +0 -205
  194. package/.gitguardian.yaml +0 -29
  195. package/.gitguardianignore +0 -16
  196. package/.github/workflows/ci.yml +0 -122
  197. package/.github/workflows/release.yml +0 -289
  198. package/.github/workflows/version.yml +0 -228
  199. package/.husky/pre-commit +0 -2
  200. package/AGENTS.md +0 -433
  201. package/CLAUDE.md +0 -1
  202. package/Makefile +0 -285
  203. package/assets/icon.ico +0 -0
  204. package/bun.lock +0 -435
  205. package/bunfig.toml +0 -28
  206. package/ecosystem.config.cjs +0 -23
  207. package/eslint.config.js +0 -63
  208. package/examples/multi-tenant-demo.js +0 -104
  209. package/install.sh +0 -123
  210. package/knip.json +0 -9
  211. package/tests/audit.test.js +0 -189
  212. package/tests/backpressure.test.js +0 -167
  213. package/tests/benchmarks/runner.js +0 -1197
  214. package/tests/benchmarks/vector-generator.js +0 -368
  215. package/tests/cli-install.test.js +0 -322
  216. package/tests/control-db.test.js +0 -285
  217. package/tests/daemon-args.test.js +0 -86
  218. package/tests/daemon-control.test.js +0 -171
  219. package/tests/daemon-fingerprint-integration.test.js +0 -111
  220. package/tests/daemon-pr24-regression.test.js +0 -198
  221. package/tests/fingerprint.test.js +0 -263
  222. package/tests/fixtures/240-orphan-seed.sql +0 -30
  223. package/tests/multi-tenant.test.js +0 -374
  224. package/tests/orphan-cleanup.test.js +0 -390
  225. package/tests/pg-version-regex.test.js +0 -129
  226. package/tests/quick-bench.js +0 -135
  227. package/tests/router-handshake-retry.test.js +0 -119
  228. package/tests/router-handshake-watchdog.test.js +0 -110
  229. package/tests/sdk.test.js +0 -71
  230. package/tests/stale-postmaster-pid.test.js +0 -85
  231. package/tests/stress-test.js +0 -439
  232. package/tests/sync-perf-test.js +0 -150
  233. package/tests/tcp-listen.test.js +0 -368
  234. package/tests/tenancy.test.js +0 -403
  235. package/tests/wrapper-supervision.test.js +0 -107
@@ -1,403 +0,0 @@
1
- /**
2
- * Group 4 — database-per-fingerprint + enforcement + kill switch.
3
- *
4
- * Boots a real pgserve daemon with isolated control socket + audit log,
5
- * stubs SO_PEERCRED to return synthetic creds, and overrides the
6
- * fingerprint-derivation cwd per-accept so a single test process can
7
- * masquerade as several different "projects" connecting to the daemon.
8
- *
9
- * Coverage (mirrors WISH §Group 4 acceptance bullets):
10
- * 1. Two peers with different fingerprints get different DBs
11
- * 2. Same peer reconnecting reaches its existing DB
12
- * 3. Cross-fingerprint connection denied with SQLSTATE 28P01
13
- * 4. Kill-switch env: cross-fingerprint succeeds + audit event emitted
14
- * 5. Sanitizer: name "@scope/foo bar" → "_scope_foo_bar"
15
- *
16
- * Plus unit tests on `sanitizeName` and `resolveTenantDatabaseName` and a
17
- * boot-time deprecation warning check.
18
- */
19
-
20
- import {
21
- describe,
22
- test,
23
- expect,
24
- beforeAll,
25
- afterAll,
26
- beforeEach,
27
- afterEach,
28
- } from 'bun:test';
29
- import fs from 'fs';
30
- import os from 'os';
31
- import path from 'path';
32
- import pg from 'pg';
33
-
34
- import {
35
- PgserveDaemon,
36
- resolveControlSocketPath,
37
- resolvePidLockPath,
38
- resolveLibpqCompatPath,
39
- } from '../src/daemon.js';
40
- import { _setPeerCredImpl, initFingerprintFfi } from '../src/fingerprint.js';
41
- import { configureAudit, AUDIT_EVENTS } from '../src/audit.js';
42
- import {
43
- sanitizeName,
44
- resolveTenantDatabaseName,
45
- KILL_SWITCH_ENV,
46
- } from '../src/tenancy.js';
47
- import { createLogger } from '../src/logger.js';
48
-
49
- const { Client } = pg;
50
-
51
- // ---------------------------------------------------------------------------
52
- // Pure-function unit tests
53
- // ---------------------------------------------------------------------------
54
-
55
- describe('sanitizeName', () => {
56
- test('collapses non-[a-z0-9] runs to a single underscore', () => {
57
- expect(sanitizeName('hello-world')).toBe('hello_world');
58
- expect(sanitizeName('hello---world')).toBe('hello_world');
59
- expect(sanitizeName('a..b..c')).toBe('a_b_c');
60
- });
61
-
62
- test('lowercases', () => {
63
- expect(sanitizeName('UPPER-CASE')).toBe('upper_case');
64
- expect(sanitizeName('MixedCase')).toBe('mixedcase');
65
- });
66
-
67
- test('preserves alphanumerics', () => {
68
- expect(sanitizeName('foo123')).toBe('foo123');
69
- expect(sanitizeName('1to1')).toBe('1to1');
70
- });
71
-
72
- test('truncates to 30 chars', () => {
73
- const long = 'a'.repeat(50);
74
- expect(sanitizeName(long).length).toBe(30);
75
- });
76
-
77
- test('handles the wish-spec example', () => {
78
- expect(sanitizeName('@scope/foo bar')).toBe('_scope_foo_bar');
79
- });
80
-
81
- test('falls back to "anon" for empty or pure-non-alphanumeric input', () => {
82
- expect(sanitizeName('')).toBe('anon');
83
- expect(sanitizeName(null)).toBe('anon');
84
- expect(sanitizeName(undefined)).toBe('anon');
85
- expect(sanitizeName('@@@')).toBe('anon');
86
- });
87
- });
88
-
89
- describe('resolveTenantDatabaseName', () => {
90
- test('builds canonical app_<sanitized>_<fingerprint>', () => {
91
- expect(resolveTenantDatabaseName({ name: 'demo', fingerprint: 'abcdef012345' }))
92
- .toBe('app_demo_abcdef012345');
93
- });
94
-
95
- test('applies sanitization', () => {
96
- expect(resolveTenantDatabaseName({ name: '@scope/foo bar', fingerprint: 'abcdef012345' }))
97
- .toBe('app__scope_foo_bar_abcdef012345');
98
- });
99
-
100
- test('rejects malformed fingerprints', () => {
101
- expect(() => resolveTenantDatabaseName({ name: 'x', fingerprint: 'TOO-SHORT' }))
102
- .toThrow(/12 hex chars/);
103
- expect(() => resolveTenantDatabaseName({ name: 'x', fingerprint: 'GHIJKL012345' }))
104
- .toThrow(/12 hex chars/);
105
- });
106
-
107
- test('result fits in PG identifier limit (≤63 chars)', () => {
108
- const longName = 'a'.repeat(80);
109
- const ident = resolveTenantDatabaseName({ name: longName, fingerprint: 'abcdef012345' });
110
- expect(ident.length).toBeLessThanOrEqual(63);
111
- });
112
- });
113
-
114
- // ---------------------------------------------------------------------------
115
- // Daemon integration tests
116
- //
117
- // One daemon shared across the integration suite — PG startup is slow and
118
- // the tests are independent at the pgserve_meta level (each clears its
119
- // state). Per-accept fingerprint behaviour is driven by an override queue
120
- // the test pushes into before each connect.
121
- // ---------------------------------------------------------------------------
122
-
123
- describe('daemon tenancy enforcement', () => {
124
- let daemon;
125
- let scratch;
126
- let controlSocketDir;
127
- let auditFile;
128
- let overridesQueue;
129
- let savedAuditDefaults;
130
-
131
- beforeAll(async () => {
132
- await initFingerprintFfi();
133
- // Stub peer creds: every accept on the test daemon's control socket
134
- // appears to come from this process. The real creds matter only for
135
- // uid (used in fingerprint hashing); pid is ignored once we override
136
- // cwd via `_fingerprintAcceptOpts`.
137
- _setPeerCredImpl(() => ({
138
- pid: process.pid,
139
- uid: process.getuid(),
140
- gid: process.getgid(),
141
- }));
142
-
143
- scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'pgserve-tenancy-test-'));
144
- controlSocketDir = path.join(scratch, 'sock');
145
- fs.mkdirSync(controlSocketDir, { recursive: true });
146
- auditFile = path.join(scratch, 'audit.log');
147
-
148
- // Save the audit module's mutable globals so we can restore them after
149
- // the suite (other tests rely on the defaults).
150
- savedAuditDefaults = {
151
- logFile: path.join(os.homedir(), '.pgserve', 'audit.log'),
152
- target: process.env.PGSERVE_AUDIT_TARGET || 'file',
153
- };
154
-
155
- overridesQueue = [];
156
-
157
- daemon = new PgserveDaemon({
158
- controlSocketDir,
159
- controlSocketPath: resolveControlSocketPath(controlSocketDir),
160
- pidLockPath: resolvePidLockPath(controlSocketDir),
161
- libpqCompatPath: resolveLibpqCompatPath(controlSocketDir, 5432),
162
- auditLogFile: auditFile,
163
- auditTarget: 'file',
164
- pgPort: 16700,
165
- logger: createLogger({ level: process.env.LOG_LEVEL || 'warn' }),
166
- _fingerprintAcceptOpts: () => overridesQueue.shift() || {},
167
- });
168
- await daemon.start();
169
- });
170
-
171
- afterAll(async () => {
172
- try { await daemon.stop(); } catch { /* swallow */ }
173
- _setPeerCredImpl(null);
174
- if (savedAuditDefaults) configureAudit(savedAuditDefaults);
175
- try { fs.rmSync(scratch, { recursive: true, force: true }); } catch { /* swallow */ }
176
- });
177
-
178
- beforeEach(async () => {
179
- overridesQueue.length = 0;
180
- // Clear pgserve_meta and drop any user DBs from prior tests so each
181
- // test starts from a clean slate.
182
- if (daemon._adminClient) {
183
- try { await daemon._adminClient.query('TRUNCATE pgserve_meta'); } catch { /* schema not yet created in odd cases */ }
184
- const r = await daemon._adminClient.query(`
185
- SELECT datname FROM pg_database
186
- WHERE datname LIKE 'app_%' AND datistemplate = false
187
- `);
188
- for (const row of r.rows) {
189
- try { await daemon._adminClient.query(`DROP DATABASE "${row.datname}"`); } catch { /* swallow */ }
190
- }
191
- }
192
- // Reset audit log so each test reads only its own events.
193
- try { fs.writeFileSync(auditFile, '', { mode: 0o600 }); } catch { /* swallow */ }
194
- daemon.enforcementDisabled = false;
195
- });
196
-
197
- function makeProject(name, dirName = name) {
198
- const dir = path.join(scratch, dirName);
199
- fs.mkdirSync(dir, { recursive: true });
200
- fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name }));
201
- return dir;
202
- }
203
-
204
- function pushOverride(projDir, scriptArgv1 = 'index.js') {
205
- overridesQueue.push({
206
- cwdOverride: projDir,
207
- cmdlineOverride: ['bun', scriptArgv1],
208
- });
209
- }
210
-
211
- async function makeClient({ database, expectError = false } = {}) {
212
- const client = new Client({
213
- host: controlSocketDir,
214
- port: 5432,
215
- database: database || 'postgres',
216
- user: 'postgres',
217
- password: 'postgres',
218
- });
219
- // pg.Client's end() can hang after a FATAL connect failure (it tries
220
- // to send a Terminate message on a closed socket), so on the deny path
221
- // we swallow connect's rejection and return immediately. The TCP
222
- // socket is already FIN'd by the daemon.
223
- if (expectError) {
224
- // Suppress unhandled-error events on the underlying socket.
225
- client.on('error', () => { /* swallow */ });
226
- let err;
227
- try { await client.connect(); } catch (e) { err = e; }
228
- return { error: err };
229
- }
230
- await client.connect();
231
- return { client };
232
- }
233
-
234
- function readAudit() {
235
- if (!fs.existsSync(auditFile)) return [];
236
- return fs.readFileSync(auditFile, 'utf8')
237
- .split('\n')
238
- .filter(Boolean)
239
- .map((l) => JSON.parse(l));
240
- }
241
-
242
- test('two peers with different fingerprints get different DBs', async () => {
243
- const projA = makeProject('proj-a');
244
- const projB = makeProject('proj-b');
245
-
246
- pushOverride(projA);
247
- const { client: ca } = await makeClient();
248
- const ra = await ca.query('SELECT current_database() AS db');
249
- await ca.end();
250
-
251
- pushOverride(projB);
252
- const { client: cb } = await makeClient();
253
- const rb = await cb.query('SELECT current_database() AS db');
254
- await cb.end();
255
-
256
- expect(ra.rows[0].db).toMatch(/^app_proj_a_[0-9a-f]{12}$/);
257
- expect(rb.rows[0].db).toMatch(/^app_proj_b_[0-9a-f]{12}$/);
258
- expect(ra.rows[0].db).not.toBe(rb.rows[0].db);
259
-
260
- const events = readAudit();
261
- const created = events.filter((e) => e.event === AUDIT_EVENTS.DB_CREATED);
262
- expect(created.length).toBe(2);
263
- expect(created.map((e) => e.database).sort()).toEqual(
264
- [ra.rows[0].db, rb.rows[0].db].sort(),
265
- );
266
- });
267
-
268
- test('same peer reconnecting reaches its existing DB (no second db_created)', async () => {
269
- const projA = makeProject('reconnect-app');
270
-
271
- pushOverride(projA);
272
- const { client: c1 } = await makeClient();
273
- const r1 = await c1.query('SELECT current_database() AS db');
274
- await c1.end();
275
-
276
- pushOverride(projA);
277
- const { client: c2 } = await makeClient();
278
- const r2 = await c2.query('SELECT current_database() AS db');
279
- await c2.end();
280
-
281
- expect(r2.rows[0].db).toBe(r1.rows[0].db);
282
-
283
- const created = readAudit().filter((e) => e.event === AUDIT_EVENTS.DB_CREATED);
284
- expect(created.length).toBe(1);
285
- });
286
-
287
- test('cross-fingerprint connection denied with SQLSTATE 28P01', async () => {
288
- const projA = makeProject('tenant-a');
289
- const projB = makeProject('tenant-b');
290
-
291
- // Provision tenant A's DB first.
292
- pushOverride(projA);
293
- const { client: ca } = await makeClient();
294
- const ra = await ca.query('SELECT current_database() AS db');
295
- await ca.end();
296
- const tenantADb = ra.rows[0].db;
297
-
298
- // Now have tenant B try to connect explicitly into tenant A's DB.
299
- pushOverride(projB);
300
- const { error } = await makeClient({ database: tenantADb, expectError: true });
301
-
302
- expect(error).toBeDefined();
303
- expect(error.code).toBe('28P01');
304
-
305
- const denied = readAudit().filter(
306
- (e) => e.event === AUDIT_EVENTS.CONNECTION_DENIED_FINGERPRINT_MISMATCH,
307
- );
308
- expect(denied.length).toBe(1);
309
- expect(denied[0].requested_database).toBe(tenantADb);
310
- expect(denied[0].owned_database).toMatch(/^app_tenant_b_[0-9a-f]{12}$/);
311
- });
312
-
313
- test('kill-switch env: cross-fingerprint succeeds and emits audit event', async () => {
314
- const projA = makeProject('killswitch-a');
315
- const projB = makeProject('killswitch-b');
316
-
317
- // Provision tenant A.
318
- pushOverride(projA);
319
- const { client: ca } = await makeClient();
320
- const ra = await ca.query('SELECT current_database() AS db');
321
- await ca.end();
322
- const tenantADb = ra.rows[0].db;
323
-
324
- // Flip the live kill-switch flag on the daemon (the env var is read
325
- // once at construction; this is the test seam for the same effect).
326
- daemon.enforcementDisabled = true;
327
-
328
- pushOverride(projB);
329
- const { client: cb } = await makeClient({ database: tenantADb });
330
- const rb = await cb.query('SELECT current_database() AS db');
331
- await cb.end();
332
-
333
- // Bypass succeeded: tenant B's session reached tenant A's DB.
334
- expect(rb.rows[0].db).toBe(tenantADb);
335
-
336
- const events = readAudit();
337
- const bypass = events.filter(
338
- (e) => e.event === AUDIT_EVENTS.ENFORCEMENT_KILL_SWITCH_USED,
339
- );
340
- expect(bypass.length).toBe(1);
341
- expect(bypass[0].owned_database).toMatch(/^app_killswitch_b_[0-9a-f]{12}$/);
342
- expect(bypass[0].requested_database).toBe(tenantADb);
343
-
344
- // No deny event should fire while the kill switch is active.
345
- const denied = events.filter(
346
- (e) => e.event === AUDIT_EVENTS.CONNECTION_DENIED_FINGERPRINT_MISMATCH,
347
- );
348
- expect(denied.length).toBe(0);
349
- });
350
-
351
- test('sanitizer: name "@scope/foo bar" produces app__scope_foo_bar_<hex>', async () => {
352
- const projScoped = makeProject('@scope/foo bar', 'scoped-pkg');
353
-
354
- pushOverride(projScoped);
355
- const { client } = await makeClient();
356
- const r = await client.query('SELECT current_database() AS db');
357
- await client.end();
358
-
359
- expect(r.rows[0].db).toMatch(/^app__scope_foo_bar_[0-9a-f]{12}$/);
360
- });
361
- });
362
-
363
- // ---------------------------------------------------------------------------
364
- // Boot-time deprecation warning
365
- // ---------------------------------------------------------------------------
366
-
367
- describe('boot deprecation warning when kill switch is set', () => {
368
- test('writes a deprecation message to stderr at start()', async () => {
369
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgserve-killswitch-boot-'));
370
- const controlDir = path.join(dir, 'sock');
371
- fs.mkdirSync(controlDir, { recursive: true });
372
-
373
- const captured = [];
374
- const origWrite = process.stderr.write.bind(process.stderr);
375
- process.stderr.write = (chunk, ...rest) => {
376
- captured.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'));
377
- return origWrite(chunk, ...rest);
378
- };
379
-
380
- let d;
381
- try {
382
- d = new PgserveDaemon({
383
- controlSocketDir: controlDir,
384
- controlSocketPath: resolveControlSocketPath(controlDir),
385
- pidLockPath: resolvePidLockPath(controlDir),
386
- libpqCompatPath: resolveLibpqCompatPath(controlDir, 5432),
387
- pgPort: 16780,
388
- enforcementDisabled: true,
389
- logger: createLogger({ level: 'warn' }),
390
- });
391
- await d.start();
392
-
393
- const merged = captured.join('');
394
- expect(merged).toContain(KILL_SWITCH_ENV);
395
- expect(merged).toContain('DISABLED');
396
- expect(merged).toContain('deprecated');
397
- } finally {
398
- process.stderr.write = origWrite;
399
- try { if (d) await d.stop(); } catch { /* swallow */ }
400
- try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* swallow */ }
401
- }
402
- });
403
- });
@@ -1,107 +0,0 @@
1
- /**
2
- * Wrapper supervision: postgres backend death surfaces to the wrapper
3
- *
4
- * Verifies that:
5
- * 1. PostgresManager extends EventEmitter and emits `backendExited` when
6
- * the postgres child exits.
7
- * 2. `expected: true` is reported when the exit was initiated by stop().
8
- * 3. `expected: false` is reported when the child was killed externally
9
- * (the case the wrapper needs to react to per pgserve#45).
10
- * 4. PgserveDaemon re-emits `backendDiedUnexpectedly` only for unexpected
11
- * exits, not for clean stop().
12
- *
13
- * Tests use the real Bun.spawn'd postgres binary via PostgresManager because
14
- * the supervision contract is end-to-end — a unit test with a mocked process
15
- * would prove only that the JS plumbing fires.
16
- */
17
-
18
- import { PostgresManager } from '../src/postgres.js';
19
- import { PgserveDaemon } from '../src/daemon.js';
20
- import { EventEmitter } from 'events';
21
- import { test, expect } from 'bun:test';
22
- import fs from 'fs';
23
- import path from 'path';
24
- import os from 'os';
25
-
26
- function quietLogger() {
27
- return {
28
- info: () => {}, warn: () => {}, error: () => {}, debug: () => {},
29
- child: () => quietLogger(),
30
- };
31
- }
32
-
33
- test('PostgresManager extends EventEmitter', () => {
34
- const mgr = new PostgresManager({});
35
- expect(mgr).toBeInstanceOf(EventEmitter);
36
- });
37
-
38
- test('PostgresManager emits backendExited with expected=true after stop()', async () => {
39
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgserve-supv-stop-'));
40
- const mgr = new PostgresManager({ dataDir: dir, logger: quietLogger() });
41
- let event = null;
42
- mgr.on('backendExited', (info) => { event = info; });
43
- await mgr.start();
44
- await mgr.stop();
45
- // Give event loop a tick to flush exited.then handler if not already drained
46
- await new Promise((r) => setTimeout(r, 50));
47
- expect(event).not.toBeNull();
48
- expect(event.expected).toBe(true);
49
- fs.rmSync(dir, { recursive: true, force: true });
50
- }, 60000);
51
-
52
- // External-SIGKILL integration coverage runs on Linux only. On macOS,
53
- // Bun.spawn'd postgres reliably refuses to surface its `exited` promise
54
- // within the test deadline when killed by SIGKILL — Bun's posix_spawn
55
- // path on darwin holds parent reaping until grandchildren reap, which
56
- // postgres never does fast enough for a deterministic test. The
57
- // `expected=false` branch is still covered cross-platform by the
58
- // daemon-level re-emit test below, which feeds a synthetic
59
- // `backendExited` payload through a fake EventEmitter and bypasses the
60
- // OS signal-handling variability entirely.
61
- const linuxOnly = process.platform === 'linux' ? test : test.skip;
62
- linuxOnly('PostgresManager emits backendExited with expected=false on external SIGKILL (linux)', async () => {
63
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgserve-supv-kill-'));
64
- const mgr = new PostgresManager({ dataDir: dir, logger: quietLogger() });
65
- let event = null;
66
- mgr.on('backendExited', (info) => { event = info; });
67
- await mgr.start();
68
- const childPid = mgr.process?.pid;
69
- expect(childPid).toBeGreaterThan(0);
70
- // External kill — _stopping stays false, so the handler must mark unexpected
71
- process.kill(childPid, 'SIGKILL');
72
- // Wait for the exit handler to fire (max 3s)
73
- for (let i = 0; i < 60 && event === null; i++) {
74
- await new Promise((r) => setTimeout(r, 50));
75
- }
76
- expect(event).not.toBeNull();
77
- expect(event.expected).toBe(false);
78
- // Cleanup: paths were nulled by the unexpected-exit branch, so stop() is a no-op
79
- await mgr.stop().catch(() => {});
80
- fs.rmSync(dir, { recursive: true, force: true });
81
- }, 60000);
82
-
83
- test('PgserveDaemon re-emits backendDiedUnexpectedly only on unexpected exit', () => {
84
- // Pure plumbing test — synthesize PgserveDaemon and a fake pgManager that
85
- // is just an EventEmitter; verify the wiring.
86
- const fakePgManager = new EventEmitter();
87
- // PgserveDaemon constructor needs a baseDir; passing a tmp dir avoids
88
- // touching real config.
89
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgserve-supv-daemon-'));
90
- const daemon = new PgserveDaemon({
91
- baseDir: dir,
92
- logger: quietLogger(),
93
- pgManager: fakePgManager,
94
- enforcementDisabled: true,
95
- });
96
- const events = [];
97
- daemon.on('backendDiedUnexpectedly', (info) => events.push(info));
98
-
99
- fakePgManager.emit('backendExited', { code: 0, expected: true });
100
- expect(events).toHaveLength(0); // clean stop — no re-emit
101
-
102
- fakePgManager.emit('backendExited', { code: 137, expected: false });
103
- expect(events).toHaveLength(1);
104
- expect(events[0]).toEqual({ code: 137, expected: false });
105
-
106
- fs.rmSync(dir, { recursive: true, force: true });
107
- });