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,104 +0,0 @@
1
- /**
2
- * Multi-Tenant Router Demo
3
- *
4
- * Shows how to use the new multi-tenant architecture
5
- */
6
-
7
- import { startMultiTenantServer } from '../src/index.js';
8
- import pg from 'pg';
9
-
10
- const { Client } = pg;
11
-
12
- async function demo() {
13
- console.log('🚀 Starting multi-tenant router demo...\n');
14
-
15
- // Start multi-tenant router
16
- const router = await startMultiTenantServer({
17
- port: 15432,
18
- baseDir: './demo-data',
19
- logLevel: 'info'
20
- });
21
-
22
- console.log('\n📊 Initial stats:');
23
- console.log(JSON.stringify(router.getStats(), null, 2));
24
-
25
- // Connect to database "user123" (auto-created)
26
- console.log('\n📥 Connecting to database: user123');
27
- const client1 = new Client({
28
- host: '127.0.0.1',
29
- port: 15432,
30
- database: 'user123'
31
- });
32
-
33
- await client1.connect();
34
- console.log('✅ Connected to user123');
35
-
36
- // Create table and insert data
37
- await client1.query('CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)');
38
- await client1.query("INSERT INTO users (name) VALUES ('Alice'), ('Bob')");
39
-
40
- const result1 = await client1.query('SELECT * FROM users');
41
- console.log('📋 user123 data:', result1.rows);
42
-
43
- await client1.end();
44
- console.log('🔌 Disconnected from user123');
45
-
46
- // Connect to database "app456" (auto-created)
47
- console.log('\n📥 Connecting to database: app456');
48
- const client2 = new Client({
49
- host: '127.0.0.1',
50
- port: 15432,
51
- database: 'app456'
52
- });
53
-
54
- await client2.connect();
55
- console.log('✅ Connected to app456');
56
-
57
- // Different schema in different database
58
- await client2.query('CREATE TABLE posts (id SERIAL PRIMARY KEY, title TEXT)');
59
- await client2.query("INSERT INTO posts (title) VALUES ('Hello World'), ('Multi-tenant magic')");
60
-
61
- const result2 = await client2.query('SELECT * FROM posts');
62
- console.log('📋 app456 data:', result2.rows);
63
-
64
- await client2.end();
65
- console.log('🔌 Disconnected from app456');
66
-
67
- // Show final stats
68
- console.log('\n📊 Final stats:');
69
- console.log(JSON.stringify(router.getStats(), null, 2));
70
-
71
- console.log('\n📋 All databases:');
72
- console.log(JSON.stringify(router.listDatabases(), null, 2));
73
-
74
- // Reconnect to user123 to verify data persists
75
- console.log('\n🔄 Reconnecting to user123 to verify data persists...');
76
- const client1Again = new Client({
77
- host: '127.0.0.1',
78
- port: 15432,
79
- database: 'user123'
80
- });
81
-
82
- await client1Again.connect();
83
- const persistedData = await client1Again.query('SELECT * FROM users');
84
- console.log('✅ Persisted data in user123:', persistedData.rows);
85
-
86
- await client1Again.end();
87
-
88
- // Stop router
89
- console.log('\n🛑 Stopping router...');
90
- await router.stop();
91
-
92
- console.log('\n✅ Demo complete!');
93
- console.log('\n🎯 Key achievements:');
94
- console.log(' • Single port (15432) handled multiple databases');
95
- console.log(' • Auto-provisioned user123 and app456');
96
- console.log(' • Data isolated between databases');
97
- console.log(' • Data persisted across reconnections');
98
- console.log(' • Zero configuration required!');
99
- }
100
-
101
- demo().catch((error) => {
102
- console.error('❌ Demo failed:', error);
103
- process.exit(1);
104
- });
package/install.sh DELETED
@@ -1,123 +0,0 @@
1
- #!/usr/bin/env bash
2
- # ============================================================================
3
- # pgserve — Canonical PostgreSQL backbone installer
4
- #
5
- # Bootstraps a single shared pgserve instance under pm2 supervision. Used as
6
- # a prerequisite by `omni/install.sh` and `genie/install.sh` so every
7
- # automagik service on a host points at the same Postgres.
8
- #
9
- # Usage:
10
- # curl -fsSL https://raw.githubusercontent.com/namastexlabs/pgserve/main/install.sh | bash
11
- #
12
- # With pinned version:
13
- # PGSERVE_VERSION=^2.1.1 curl -fsSL .../install.sh | bash
14
- #
15
- # Local checkout:
16
- # bash install.sh
17
- #
18
- # Idempotent — re-running is a no-op success when pgserve is already
19
- # registered under pm2 with a healthy entry.
20
- # ============================================================================
21
- set -euo pipefail
22
-
23
- PGSERVE_VERSION="${PGSERVE_VERSION:-^2.1.0}"
24
-
25
- # Colors (no-op when stdout isn't a tty)
26
- if [[ -t 1 ]]; then
27
- RED='\033[0;31m'
28
- GREEN='\033[0;32m'
29
- YELLOW='\033[1;33m'
30
- BLUE='\033[0;34m'
31
- CYAN='\033[0;36m'
32
- BOLD='\033[1m'
33
- NC='\033[0m'
34
- else
35
- RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' NC=''
36
- fi
37
-
38
- info() { printf "${BLUE}ℹ${NC} %s\n" "$*"; }
39
- ok() { printf "${GREEN}✓${NC} %s\n" "$*"; }
40
- warn() { printf "${YELLOW}⚠${NC} %s\n" "$*"; }
41
- fail() { printf "${RED}✗${NC} %s\n" "$*" >&2; exit 1; }
42
- step() { printf "\n${BOLD}${CYAN}▸ %s${NC}\n" "$*"; }
43
-
44
- has_cmd() { command -v "$1" >/dev/null 2>&1; }
45
-
46
- # ============================================================================
47
- # Prerequisites: bun + pm2
48
- # ============================================================================
49
-
50
- ensure_bun() {
51
- if has_cmd bun; then
52
- ok "bun $(bun --version 2>/dev/null || echo '?')"
53
- return 0
54
- fi
55
- info "Installing bun (https://bun.sh)..."
56
- curl -fsSL https://bun.sh/install | bash >/dev/null 2>&1 || fail "bun install failed — see https://bun.sh"
57
- # Make bun available to the rest of this script without requiring a re-login.
58
- export PATH="$HOME/.bun/bin:$PATH"
59
- has_cmd bun || fail "bun installed but not on PATH — restart your shell and re-run."
60
- ok "bun $(bun --version)"
61
- }
62
-
63
- ensure_pm2() {
64
- if has_cmd pm2; then
65
- ok "pm2 $(pm2 --version 2>/dev/null || echo '?')"
66
- return 0
67
- fi
68
- info "Installing pm2 (process supervisor)..."
69
- bun add -g pm2 >/dev/null 2>&1 || fail "pm2 install failed — try: bun add -g pm2"
70
- has_cmd pm2 || fail "pm2 installed but not on PATH — restart your shell and re-run."
71
- ok "pm2 installed"
72
- }
73
-
74
- # ============================================================================
75
- # pgserve binary + pm2 registration
76
- # ============================================================================
77
-
78
- ensure_pgserve_binary() {
79
- # Probe via `pgserve port` (real subcommand). `pgserve --version` doesn't
80
- # exist in 2.1.x — using it would false-negative and trigger a redundant
81
- # reinstall every time install.sh runs.
82
- if has_cmd pgserve && pgserve port >/dev/null 2>&1; then
83
- ok "pgserve binary present (port $(pgserve port 2>/dev/null))"
84
- return 0
85
- fi
86
- info "Installing pgserve@${PGSERVE_VERSION} globally..."
87
- bun add -g "pgserve@${PGSERVE_VERSION}" >/dev/null 2>&1 \
88
- || fail "pgserve install failed — try: bun add -g pgserve@${PGSERVE_VERSION}"
89
- has_cmd pgserve || fail "pgserve installed but not on PATH — restart your shell and re-run."
90
- ok "pgserve $(pgserve port 2>/dev/null || echo '?')"
91
- }
92
-
93
- register_pgserve_pm2() {
94
- info "Registering pgserve under pm2 (idempotent)..."
95
- # `pgserve install` prints its own success/already-installed line and exits
96
- # 0 in both cases. We pipe stderr through so any pm2 errors surface to the
97
- # operator (the pm2-6.x --min-uptime breakage we hit on 2026-04-30 was
98
- # invisible because stderr was being captured).
99
- pgserve install || fail "pgserve install failed — see ~/.pgserve/logs/pgserve-error.log"
100
- }
101
-
102
- # ============================================================================
103
- # Main
104
- # ============================================================================
105
-
106
- main() {
107
- step "Installing canonical pgserve"
108
- ensure_bun
109
- ensure_pm2
110
- ensure_pgserve_binary
111
- register_pgserve_pm2
112
-
113
- echo ""
114
- ok "Canonical pgserve ready"
115
- info "URL: $(pgserve url 2>/dev/null || echo '<run: pgserve url>')"
116
- info "Port: $(pgserve port 2>/dev/null || echo '?')"
117
- info "Logs: ~/.pgserve/logs/"
118
- echo ""
119
- info "Other automagik services on this host (omni, genie, ...) will share this pgserve."
120
- echo ""
121
- }
122
-
123
- main "$@"
package/knip.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "$schema": "https://unpkg.com/knip@5/schema.json",
3
- "entry": ["src/index.js", "bin/postgres-server.js", "bin/pgserve-wrapper.cjs"],
4
- "project": ["src/**/*.js", "bin/**/*.js", "bin/**/*.cjs"],
5
- "ignore": ["tests/**", "helpers/**", "scripts/**"],
6
- "ignoreBinaries": ["scripts/test-npx.sh", "scripts/test-bun-self-heal.sh", "make"],
7
- "ignoreDependencies": ["bun"],
8
- "ignoreExportsUsedInFile": true
9
- }
@@ -1,189 +0,0 @@
1
- /**
2
- * Tests for src/audit.js — JSONL writer with rotation + syslog target.
3
- *
4
- * Tests use temp dirs under /tmp; nothing touches the user's real
5
- * `~/.pgserve/audit.log`. The syslog test stubs `logger` via PATH so we
6
- * don't depend on (or pollute) the host's syslog daemon.
7
- */
8
-
9
- import { test, expect, beforeEach, afterEach } from 'bun:test';
10
- import fs from 'fs';
11
- import os from 'os';
12
- import path from 'path';
13
- import {
14
- audit,
15
- configureAudit,
16
- readAuditTarget,
17
- AUDIT_EVENTS,
18
- _internals,
19
- } from '../src/audit.js';
20
-
21
- let scratchDir;
22
-
23
- beforeEach(() => {
24
- scratchDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgserve-audit-test-'));
25
- configureAudit({
26
- logFile: path.join(scratchDir, 'audit.log'),
27
- target: 'file',
28
- });
29
- });
30
-
31
- afterEach(() => {
32
- try {
33
- fs.rmSync(scratchDir, { recursive: true, force: true });
34
- } catch { /* noop */ }
35
- });
36
-
37
- test('audit() appends a JSON line per event', () => {
38
- audit(AUDIT_EVENTS.DB_CREATED, { fingerprint: 'abc123def456', db: 'app_demo_abc123def456' });
39
- audit(AUDIT_EVENTS.CONNECTION_ROUTED, { fingerprint: 'abc123def456', peer_pid: 1234 });
40
-
41
- const logFile = path.join(scratchDir, 'audit.log');
42
- const lines = fs.readFileSync(logFile, 'utf8').trim().split('\n');
43
- expect(lines.length).toBe(2);
44
- const r1 = JSON.parse(lines[0]);
45
- expect(r1.event).toBe('db_created');
46
- expect(r1.fingerprint).toBe('abc123def456');
47
- expect(typeof r1.ts).toBe('string');
48
- expect(new Date(r1.ts).toString()).not.toBe('Invalid Date');
49
-
50
- const r2 = JSON.parse(lines[1]);
51
- expect(r2.event).toBe('connection_routed');
52
- expect(r2.peer_pid).toBe(1234);
53
- });
54
-
55
- test('audit() refuses unknown events', () => {
56
- expect(() => audit('definitely_not_a_real_event', {})).toThrow(/unknown event/);
57
- });
58
-
59
- test('audit() creates the parent directory if missing', () => {
60
- const nested = path.join(scratchDir, 'nested', 'sub', 'audit.log');
61
- audit(AUDIT_EVENTS.DB_CREATED, { fingerprint: 'a'.repeat(12) }, { logFile: nested });
62
- expect(fs.existsSync(nested)).toBe(true);
63
- });
64
-
65
- test('all v2.0 event names are exported (incl. Group 6 tcp_*)', () => {
66
- expect(Object.values(AUDIT_EVENTS).sort()).toEqual([
67
- 'connection_denied_fingerprint_mismatch',
68
- 'connection_routed',
69
- 'db_created',
70
- 'db_persist_honored',
71
- 'db_reaped_liveness',
72
- 'db_reaped_ttl',
73
- 'enforcement_kill_switch_used',
74
- 'tcp_token_denied',
75
- 'tcp_token_issued',
76
- 'tcp_token_used',
77
- ]);
78
- });
79
-
80
- test('rotation kicks in once existing file crosses 50 MB', () => {
81
- const logFile = path.join(scratchDir, 'audit.log');
82
- // Use a sparse file to simulate a 50 MB log without writing 50 MB.
83
- const fd = fs.openSync(logFile, 'w');
84
- fs.ftruncateSync(fd, _internals.ROTATE_THRESHOLD_BYTES);
85
- fs.closeSync(fd);
86
-
87
- audit(AUDIT_EVENTS.DB_CREATED, { fingerprint: 'r'.repeat(12) });
88
-
89
- // Original file rotated to .1, fresh file holds the new line.
90
- expect(fs.existsSync(`${logFile}.1`)).toBe(true);
91
- const fresh = fs.readFileSync(logFile, 'utf8');
92
- expect(fresh.trim().split('\n').length).toBe(1);
93
- expect(JSON.parse(fresh.trim()).event).toBe('db_created');
94
-
95
- // The rotated file is the original 50 MB sparse file.
96
- expect(fs.statSync(`${logFile}.1`).size).toBe(_internals.ROTATE_THRESHOLD_BYTES);
97
- });
98
-
99
- test('rotation cascades up to KEEP files and drops the eldest', () => {
100
- const logFile = path.join(scratchDir, 'audit.log');
101
- // Pre-populate audit.log.1 ... audit.log.5 with distinct markers.
102
- for (let i = 1; i <= _internals.ROTATE_KEEP; i++) {
103
- fs.writeFileSync(`${logFile}.${i}`, `slot-${i}\n`);
104
- }
105
- // And the live audit.log just under threshold.
106
- const fd = fs.openSync(logFile, 'w');
107
- fs.ftruncateSync(fd, _internals.ROTATE_THRESHOLD_BYTES);
108
- fs.closeSync(fd);
109
-
110
- audit(AUDIT_EVENTS.DB_CREATED, { fingerprint: 'q'.repeat(12) });
111
-
112
- // .5 (was "slot-5") dropped; .4 → .5; .3 → .4; .2 → .3; .1 → .2; live → .1.
113
- expect(fs.readFileSync(`${logFile}.5`, 'utf8').trim()).toBe('slot-4');
114
- expect(fs.readFileSync(`${logFile}.4`, 'utf8').trim()).toBe('slot-3');
115
- expect(fs.readFileSync(`${logFile}.3`, 'utf8').trim()).toBe('slot-2');
116
- expect(fs.readFileSync(`${logFile}.2`, 'utf8').trim()).toBe('slot-1');
117
- expect(fs.statSync(`${logFile}.1`).size).toBe(_internals.ROTATE_THRESHOLD_BYTES);
118
- });
119
-
120
- test('audit({target:"syslog"}) spawns logger -t pgserve-audit', async () => {
121
- // Stub `logger` by prepending a temp shim to PATH.
122
- const shimDir = path.join(scratchDir, 'shim');
123
- fs.mkdirSync(shimDir, { recursive: true });
124
- const marker = path.join(scratchDir, 'logger-calls.txt');
125
- const shimPath = path.join(shimDir, 'logger');
126
- fs.writeFileSync(
127
- shimPath,
128
- `#!/usr/bin/env bash
129
- # Capture argv to a marker file so the test can verify the spawn.
130
- printf '%s\\n' "$*" >> "${marker}"
131
- `,
132
- { mode: 0o755 },
133
- );
134
-
135
- const oldPath = process.env.PATH;
136
- process.env.PATH = `${shimDir}:${oldPath}`;
137
- try {
138
- audit(
139
- AUDIT_EVENTS.CONNECTION_ROUTED,
140
- { fingerprint: 's'.repeat(12) },
141
- { target: 'syslog' },
142
- );
143
- // logger is spawned async; poll briefly for the marker.
144
- const deadline = Date.now() + 2000;
145
- while (!fs.existsSync(marker) && Date.now() < deadline) {
146
- await new Promise(r => setTimeout(r, 25));
147
- }
148
- expect(fs.existsSync(marker)).toBe(true);
149
- const contents = fs.readFileSync(marker, 'utf8');
150
- expect(contents).toContain('-t pgserve-audit');
151
- expect(contents).toContain('"event":"connection_routed"');
152
- } finally {
153
- process.env.PATH = oldPath;
154
- }
155
- });
156
-
157
- test('audit({target:"syslog"}) swallows missing logger binary', () => {
158
- // Point PATH at an empty dir → `logger` cannot be found → no throw.
159
- const empty = path.join(scratchDir, 'empty');
160
- fs.mkdirSync(empty);
161
- const oldPath = process.env.PATH;
162
- process.env.PATH = empty;
163
- try {
164
- expect(() =>
165
- audit(
166
- AUDIT_EVENTS.CONNECTION_ROUTED,
167
- { fingerprint: 'z'.repeat(12) },
168
- { target: 'syslog' },
169
- ),
170
- ).not.toThrow();
171
- } finally {
172
- process.env.PATH = oldPath;
173
- }
174
- });
175
-
176
- test('readAuditTarget reads pgserve.audit.target from package.json', () => {
177
- const pkgFile = path.join(scratchDir, 'package.json');
178
- fs.writeFileSync(
179
- pkgFile,
180
- JSON.stringify({ name: 'demo', pgserve: { audit: { target: 'syslog' } } }),
181
- );
182
- expect(readAuditTarget(pkgFile)).toBe('syslog');
183
-
184
- fs.writeFileSync(pkgFile, JSON.stringify({ name: 'demo' }));
185
- expect(readAuditTarget(pkgFile)).toBe('file');
186
-
187
- // Missing file → file (default).
188
- expect(readAuditTarget(path.join(scratchDir, 'missing.json'))).toBe('file');
189
- });
@@ -1,167 +0,0 @@
1
- /**
2
- * Backpressure / Large Message Regression Tests
3
- *
4
- * Reproduces the deadlock from issue #14: TCP proxy drops bytes when
5
- * socket buffers are full, causing PostgreSQL to wait forever for the
6
- * remainder of a truncated wire protocol message.
7
- */
8
-
9
- import { startMultiTenantServer } from '../src/index.js';
10
- import pg from 'pg';
11
- import { test, expect } from 'bun:test';
12
- import fs from 'fs';
13
-
14
- const { Client } = pg;
15
-
16
- const TEST_PORT = 15433;
17
- const testDataDir = './test-data-backpressure';
18
-
19
- function cleanup() {
20
- if (fs.existsSync(testDataDir)) {
21
- fs.rmSync(testDataDir, { recursive: true, force: true });
22
- }
23
- }
24
-
25
- /** Create a connected pg.Client */
26
- async function connect(dbName) {
27
- const client = new Client({
28
- host: '127.0.0.1',
29
- port: TEST_PORT,
30
- database: dbName,
31
- user: 'postgres',
32
- password: 'postgres',
33
- });
34
- await client.connect();
35
- return client;
36
- }
37
-
38
- test('Large INSERT (~360KB payload) does not deadlock', async () => {
39
- cleanup();
40
- const router = await startMultiTenantServer({
41
- port: TEST_PORT,
42
- baseDir: testDataDir,
43
- logLevel: 'warn',
44
- });
45
-
46
- let client;
47
- try {
48
- client = await connect('bp_insert');
49
- await client.query('CREATE TABLE big (id SERIAL PRIMARY KEY, payload TEXT)');
50
-
51
- // ~360KB of text — exceeds typical socket buffer size
52
- const bigPayload = 'x'.repeat(360_000);
53
- await client.query('INSERT INTO big (payload) VALUES ($1)', [bigPayload]);
54
-
55
- const res = await client.query('SELECT length(payload) AS len FROM big');
56
- expect(Number(res.rows[0].len)).toBe(360000);
57
- } finally {
58
- if (client) await client.end().catch(() => {});
59
- await router.stop();
60
- cleanup();
61
- }
62
- }, 30_000);
63
-
64
- test('Large SELECT result (500KB+) does not deadlock', async () => {
65
- cleanup();
66
- const router = await startMultiTenantServer({
67
- port: TEST_PORT,
68
- baseDir: testDataDir,
69
- logLevel: 'warn',
70
- });
71
-
72
- let client;
73
- try {
74
- client = await connect('bp_select');
75
- await client.query('CREATE TABLE chunks (id SERIAL PRIMARY KEY, data TEXT)');
76
-
77
- // Insert many rows that sum to >500KB
78
- const chunkSize = 10_000;
79
- const numChunks = 60; // 60 * 10KB = 600KB total
80
- const chunk = 'y'.repeat(chunkSize);
81
-
82
- for (let i = 0; i < numChunks; i++) {
83
- await client.query('INSERT INTO chunks (data) VALUES ($1)', [chunk]);
84
- }
85
-
86
- // Fetch all rows in a single result set (PG→Client backpressure)
87
- const res = await client.query('SELECT * FROM chunks');
88
- expect(res.rows.length).toBe(numChunks);
89
- expect(res.rows[0].data.length).toBe(chunkSize);
90
- } finally {
91
- if (client) await client.end().catch(() => {});
92
- await router.stop();
93
- cleanup();
94
- }
95
- }, 30_000);
96
-
97
- test('Large single query with multi-value INSERT (500KB+)', async () => {
98
- cleanup();
99
- const router = await startMultiTenantServer({
100
- port: TEST_PORT,
101
- baseDir: testDataDir,
102
- logLevel: 'warn',
103
- });
104
-
105
- let client;
106
- try {
107
- client = await connect('bp_multivalue');
108
- await client.query('CREATE TABLE items (id INT, val TEXT)');
109
-
110
- // Build a single INSERT with many value tuples to produce a large wire message
111
- const rowCount = 500;
112
- const rowValue = 'z'.repeat(1_000); // 1KB per row → ~500KB total
113
- const values = [];
114
- const params = [];
115
- for (let i = 0; i < rowCount; i++) {
116
- values.push(`($${i * 2 + 1}, $${i * 2 + 2})`);
117
- params.push(i, rowValue);
118
- }
119
-
120
- const sql = `INSERT INTO items (id, val) VALUES ${values.join(', ')}`;
121
- await client.query(sql, params);
122
-
123
- const res = await client.query('SELECT count(*)::int AS cnt FROM items');
124
- expect(res.rows[0].cnt).toBe(rowCount);
125
- } finally {
126
- if (client) await client.end().catch(() => {});
127
- await router.stop();
128
- cleanup();
129
- }
130
- }, 30_000);
131
-
132
- test('Concurrent large operations (5 clients x 300KB)', async () => {
133
- cleanup();
134
- const router = await startMultiTenantServer({
135
- port: TEST_PORT,
136
- baseDir: testDataDir,
137
- logLevel: 'warn',
138
- });
139
-
140
- try {
141
- const numClients = 5;
142
- const payloadSize = 300_000;
143
- const payload = 'c'.repeat(payloadSize);
144
-
145
- // Run all clients concurrently
146
- const results = await Promise.all(
147
- Array.from({ length: numClients }, async (_, i) => {
148
- const dbName = `bp_concurrent_${i}`;
149
- const client = await connect(dbName);
150
- await client.query('CREATE TABLE stress (id SERIAL PRIMARY KEY, data TEXT)');
151
- await client.query('INSERT INTO stress (data) VALUES ($1)', [payload]);
152
-
153
- const res = await client.query('SELECT length(data) AS len FROM stress');
154
- await client.end();
155
- return parseInt(res.rows[0].len, 10);
156
- })
157
- );
158
-
159
- // All clients should have successfully stored the full payload
160
- for (const len of results) {
161
- expect(len).toBe(payloadSize);
162
- }
163
- } finally {
164
- await router.stop();
165
- cleanup();
166
- }
167
- }, 60_000);