pgserve 2.4.0 → 2.6.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 (36) hide show
  1. package/README.md +5 -8
  2. package/bin/pgserve-wrapper.cjs +23 -0
  3. package/bin/postgres-server.js +28 -0
  4. package/package.json +2 -1
  5. package/scripts/aggregate-manifest.sh +184 -0
  6. package/scripts/assemble-tarball.sh +191 -0
  7. package/scripts/audit-redaction-lint.js +349 -0
  8. package/scripts/build-binary.sh +213 -0
  9. package/scripts/fetch-postgres-bins.sh +234 -0
  10. package/scripts/postinstall.cjs +102 -18
  11. package/scripts/verify-published-artifacts.sh +211 -0
  12. package/src/audit/audit.js +134 -0
  13. package/src/cli-install.cjs +258 -26
  14. package/src/commands/doctor.js +465 -0
  15. package/src/commands/gc.js +276 -0
  16. package/src/commands/provision.js +396 -0
  17. package/src/commands/trust.js +187 -0
  18. package/src/commands/verify.js +360 -0
  19. package/src/cosign/cache-token.js +328 -0
  20. package/src/cosign/schema.js +97 -0
  21. package/src/cosign/trust-list.js +81 -0
  22. package/src/cosign/trust-store.js +250 -0
  23. package/src/cosign/verify-binary.js +277 -0
  24. package/src/gc/audit-log.js +150 -0
  25. package/src/gc/orphan-detection.js +190 -0
  26. package/src/gc/queries.js +193 -0
  27. package/src/lib/pg-query.js +145 -0
  28. package/src/lib/runtime-json.js +181 -0
  29. package/src/provision/advisory-lock.js +91 -0
  30. package/src/provision/db-naming.js +130 -0
  31. package/src/provision/fingerprint.js +144 -0
  32. package/src/schema/pgserve-meta.js +120 -0
  33. package/src/security/blocked-versions.js +103 -0
  34. package/src/upgrade/index.js +5 -0
  35. package/src/upgrade/steps/binary-cache-flush.js +2 -2
  36. package/src/upgrade/steps/cosign-meta-migration.js +123 -0
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Hardcoded blocklist — pgserve-singleton-no-proxy wish, Group 5.
3
+ *
4
+ * Compile-time list of pgserve versions that `pgserve install` and
5
+ * `pgserve update` MUST refuse, with a clear diagnostic. The trust root
6
+ * is opaque to operators: they cannot edit this file at runtime, only
7
+ * receive an updated list via `pgserve update`.
8
+ *
9
+ * Per SHARED-DESIGN.md §2.5: this is the ONLY revocation surface for v2.4+.
10
+ * No Rekor consultation, no revoked.json sync, no DNS-served blocklist.
11
+ * The list grows when a known-bad version ships and gets pinned here, then
12
+ * an updated pgserve release rolls out via the normal channel.
13
+ *
14
+ * Format: array of { version, reason, advisoryUrl? } records.
15
+ * - version: exact semver string. Range matchers are intentionally
16
+ * unsupported — every blocked version is named explicitly so an
17
+ * auditor reading this file knows exactly what is rejected.
18
+ * - reason: one-line operator-facing explanation (printed on refusal).
19
+ * - advisoryUrl: optional pointer to a CVE/security advisory for the
20
+ * blocked release.
21
+ */
22
+
23
+ /**
24
+ * @typedef {Object} BlockedVersion
25
+ * @property {string} version Exact semver string (no ranges).
26
+ * @property {string} reason Operator-facing diagnostic line.
27
+ * @property {string} [advisoryUrl] Pointer to CVE/security advisory.
28
+ */
29
+
30
+ /** @type {readonly BlockedVersion[]} */
31
+ export const BLOCKED_VERSIONS = Object.freeze([
32
+ // Empty by default. Populate as known-bad versions are identified.
33
+ // Example shape (uncomment + edit when a real block is needed):
34
+ // { version: '2.6.0', reason: 'Postmaster crash on Linux ARM64 — see #999', advisoryUrl: 'https://github.com/namastexlabs/pgserve/security/advisories/GHSA-xxxx' },
35
+ ]);
36
+
37
+ /**
38
+ * Test-only override. The compile-time list is frozen so production code
39
+ * cannot mutate it; tests need a way to inject blocked entries to exercise
40
+ * the throw path. Populated via __addBlockedForTest() and consulted by
41
+ * findBlocked() when non-empty. Cleared via __clearBlockedTestOverridesForTest().
42
+ *
43
+ * @type {BlockedVersion[]}
44
+ */
45
+ const _testOverrides = [];
46
+
47
+ /**
48
+ * Test-only — register an additional blocked entry for the lifetime of a
49
+ * test. Call __clearBlockedTestOverridesForTest() in afterEach to keep
50
+ * tests isolated.
51
+ *
52
+ * @param {BlockedVersion} entry
53
+ */
54
+ export function __addBlockedForTest(entry) {
55
+ if (!entry || typeof entry.version !== 'string' || typeof entry.reason !== 'string') {
56
+ throw new Error('__addBlockedForTest: entry needs { version, reason }');
57
+ }
58
+ _testOverrides.push(entry);
59
+ }
60
+
61
+ /** Test-only — drop all entries registered via __addBlockedForTest. */
62
+ export function __clearBlockedTestOverridesForTest() {
63
+ _testOverrides.length = 0;
64
+ }
65
+
66
+ /**
67
+ * Find the blocklist entry for an exact version string, if any.
68
+ * Considers both the compile-time BLOCKED_VERSIONS list and any test
69
+ * overrides registered via __addBlockedForTest().
70
+ *
71
+ * @param {string} version
72
+ * @returns {BlockedVersion | undefined}
73
+ */
74
+ export function findBlocked(version) {
75
+ if (typeof version !== 'string' || version.length === 0) return undefined;
76
+ const fromOverrides = _testOverrides.find((b) => b.version === version);
77
+ if (fromOverrides) return fromOverrides;
78
+ return BLOCKED_VERSIONS.find((b) => b.version === version);
79
+ }
80
+
81
+ /**
82
+ * Assert that a version is not blocked. Throws an Error with a stable,
83
+ * grep-able prefix (`EBLOCKEDVERSION`) when the version is blocked, so
84
+ * callers (cli-install / upgrade) can detect it and exit with a known code.
85
+ *
86
+ * @param {string} version
87
+ * @throws {Error} when version is blocked
88
+ */
89
+ export function assertNotBlocked(version) {
90
+ const hit = findBlocked(version);
91
+ if (!hit) return;
92
+ const lines = [
93
+ `EBLOCKEDVERSION: pgserve@${version} is blocked.`,
94
+ ` reason: ${hit.reason}`,
95
+ ];
96
+ if (hit.advisoryUrl) lines.push(` advisory: ${hit.advisoryUrl}`);
97
+ lines.push(' remediation: install a different version (run `pgserve update` for the latest).');
98
+ const err = new Error(lines.join('\n'));
99
+ err.code = 'EBLOCKEDVERSION';
100
+ err.version = version;
101
+ err.reason = hit.reason;
102
+ throw err;
103
+ }
@@ -20,11 +20,16 @@ import * as plpgsqlResolve from './steps/plpgsql-resolve.js';
20
20
  import * as envRefresh from './steps/env-refresh.js';
21
21
  import * as consumerSignal from './steps/consumer-signal.js';
22
22
  import * as healthValidate from './steps/health-validate.js';
23
+ import * as cosignMetaMigration from './steps/cosign-meta-migration.js';
23
24
 
24
25
  export const STEPS = [
25
26
  { name: 'port-reconcile', impl: portReconcile },
26
27
  { name: 'binary-cache-flush', impl: binaryCacheFlush },
27
28
  { name: 'plpgsql-resolve', impl: plpgsqlResolve },
29
+ // pgserve singleton (v2.4) — `pgserve-singleton-no-proxy` wish, Group 4.
30
+ // Adds the additive `verified_*` columns to `pgserve_meta`. Runs after
31
+ // plpgsql-resolve so the extension is available; idempotent per-DB.
32
+ { name: 'cosign-meta-migration', impl: cosignMetaMigration },
28
33
  { name: 'env-refresh', impl: envRefresh },
29
34
  { name: 'consumer-signal', impl: consumerSignal },
30
35
  { name: 'health-validate', impl: healthValidate },
@@ -57,8 +57,8 @@ export async function execute({ log, warn }) {
57
57
 
58
58
  if (!downloadFn) {
59
59
  warn(`binary needs refresh (pinned=${pinned}, cached=${marker || 'missing'}) but autopg postgres module not exposing download API`);
60
- warn(`operator action: rerun \`bun install -g @automagik/autopg@latest\``);
61
- return { status: 'FAIL', detail: 'binary refresh needs autopg npm reinstall' };
60
+ warn(`operator action: rerun \`curl -fsSL https://raw.githubusercontent.com/namastexlabs/pgserve/main/install.sh | bash\` to refresh from GitHub Releases`);
61
+ return { status: 'FAIL', detail: 'binary refresh needs install.sh rerun (no npm dependency)' };
62
62
  }
63
63
  log(`re-downloading PG ${pinned} into ${cacheDir}`);
64
64
  await downloadFn({ version: pinned, targetDir: cacheDir });
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Step — pgserve_meta cosign columns (additive).
3
+ *
4
+ * pgserve singleton (v2.4) — `pgserve-singleton-no-proxy` wish, Group 4.
5
+ *
6
+ * Adds `verified_at`, `verified_identity`, `verified_tier` to every
7
+ * `pgserve_meta` table the upgrade step finds. The schema delta is
8
+ * additive (Decision P4) — pre-cosign rows continue to work, columns are
9
+ * NULL until Group 3's `pgserve provision` writes them.
10
+ *
11
+ * Runs idempotently: `ADD COLUMN IF NOT EXISTS` plus a guarded DO-block
12
+ * for the CHECK constraint. Re-running on an already-migrated host is a
13
+ * no-op. If `pgserve_meta` does not exist (fresh install before G3 has
14
+ * provisioned anything) the step is a SKIP.
15
+ */
16
+
17
+ import { spawnSync } from 'node:child_process';
18
+
19
+ import { getMigrationStatements } from '../../cosign/schema.js';
20
+
21
+ export const name = 'cosign-meta-migration';
22
+ const CANONICAL_PORT = 5432;
23
+ const SYSTEM_DBS = new Set(['template0', 'template1']);
24
+
25
+ // PR #79 fix: previous implementation used execSync with a template string +
26
+ // JSON.stringify(sql). The migration SQL contains `DO $$ ... $$` blocks; bash
27
+ // expands `$$` to its PID, corrupting the SQL before psql sees it. Switch to
28
+ // spawnSync (shell:false) with the SQL fed through stdin — no shell parsing,
29
+ // no expansion, no injection surface.
30
+ function pgQuery({ db, sql, captureStdout = false, port = CANONICAL_PORT }) {
31
+ const env = { ...process.env, PGPASSWORD: process.env.PGPASSWORD || 'postgres' };
32
+ const result = spawnSync(
33
+ 'psql',
34
+ ['-h', '127.0.0.1', '-p', String(port), '-U', 'postgres', '-d', db, '-At', '-f', '-'],
35
+ { env, input: sql, stdio: ['pipe', 'pipe', 'pipe'] }
36
+ );
37
+ if (result.status !== 0) {
38
+ const stderr = (result.stderr || Buffer.from('')).toString();
39
+ const err = new Error(`psql exited ${result.status}: ${stderr.trim()}`);
40
+ err.status = result.status;
41
+ err.stderr = stderr;
42
+ throw err;
43
+ }
44
+ const stdout = (result.stdout || Buffer.from('')).toString();
45
+ return captureStdout ? stdout.trim() : stdout;
46
+ }
47
+
48
+ function listUserDbs() {
49
+ const out = pgQuery({
50
+ db: 'postgres',
51
+ sql: "SELECT datname FROM pg_database WHERE NOT datistemplate ORDER BY datname",
52
+ captureStdout: true,
53
+ });
54
+ return out ? out.split('\n').filter(Boolean).filter((d) => !SYSTEM_DBS.has(d)) : [];
55
+ }
56
+
57
+ function pgserveMetaExists(db) {
58
+ const out = pgQuery({
59
+ db,
60
+ sql: "SELECT to_regclass('public.pgserve_meta') IS NOT NULL",
61
+ captureStdout: true,
62
+ });
63
+ return out === 't' || out === 'true';
64
+ }
65
+
66
+ export async function plan() {
67
+ let dbs;
68
+ try {
69
+ dbs = listUserDbs();
70
+ } catch (err) {
71
+ return `cannot enumerate DBs: ${err.message}`;
72
+ }
73
+ if (dbs.length === 0) return 'no user DBs — skip';
74
+ const targets = [];
75
+ for (const db of dbs) {
76
+ try {
77
+ if (pgserveMetaExists(db)) targets.push(db);
78
+ } catch {
79
+ // Skip silently — DB might be unreachable, listed but not connectable.
80
+ }
81
+ }
82
+ if (targets.length === 0) return 'no DB hosts pgserve_meta yet — skip';
83
+ return `would apply additive cosign columns to pgserve_meta in: ${targets.join(', ')}`;
84
+ }
85
+
86
+ export async function execute({ log, warn }) {
87
+ let dbs;
88
+ try {
89
+ dbs = listUserDbs();
90
+ } catch (err) {
91
+ return { status: 'FAIL', detail: `cannot enumerate DBs: ${err.message}` };
92
+ }
93
+ if (dbs.length === 0) return { status: 'SKIP', detail: 'no user DBs to migrate' };
94
+
95
+ const statements = getMigrationStatements();
96
+ let migrated = 0;
97
+ let skipped = 0;
98
+ for (const db of dbs) {
99
+ let exists;
100
+ try {
101
+ exists = pgserveMetaExists(db);
102
+ } catch (err) {
103
+ warn(`[cosign-meta-migration] ${db}: cannot probe pgserve_meta — ${err.message}`);
104
+ skipped++;
105
+ continue;
106
+ }
107
+ if (!exists) {
108
+ skipped++;
109
+ continue;
110
+ }
111
+ try {
112
+ for (const sql of statements) {
113
+ pgQuery({ db, sql });
114
+ }
115
+ log(`[cosign-meta-migration] ${db}: applied ${statements.length} idempotent statement(s)`);
116
+ migrated++;
117
+ } catch (err) {
118
+ warn(`[cosign-meta-migration] ${db}: failed — ${err.message}`);
119
+ skipped++;
120
+ }
121
+ }
122
+ return { status: 'OK', detail: `migrated ${migrated} DB(s), skipped ${skipped}` };
123
+ }