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.
- package/README.md +5 -8
- package/bin/pgserve-wrapper.cjs +23 -0
- package/bin/postgres-server.js +28 -0
- package/package.json +2 -1
- package/scripts/aggregate-manifest.sh +184 -0
- package/scripts/assemble-tarball.sh +191 -0
- package/scripts/audit-redaction-lint.js +349 -0
- package/scripts/build-binary.sh +213 -0
- package/scripts/fetch-postgres-bins.sh +234 -0
- package/scripts/postinstall.cjs +102 -18
- package/scripts/verify-published-artifacts.sh +211 -0
- package/src/audit/audit.js +134 -0
- package/src/cli-install.cjs +258 -26
- package/src/commands/doctor.js +465 -0
- package/src/commands/gc.js +276 -0
- package/src/commands/provision.js +396 -0
- package/src/commands/trust.js +187 -0
- package/src/commands/verify.js +360 -0
- package/src/cosign/cache-token.js +328 -0
- package/src/cosign/schema.js +97 -0
- package/src/cosign/trust-list.js +81 -0
- package/src/cosign/trust-store.js +250 -0
- package/src/cosign/verify-binary.js +277 -0
- package/src/gc/audit-log.js +150 -0
- package/src/gc/orphan-detection.js +190 -0
- package/src/gc/queries.js +193 -0
- package/src/lib/pg-query.js +145 -0
- package/src/lib/runtime-json.js +181 -0
- package/src/provision/advisory-lock.js +91 -0
- package/src/provision/db-naming.js +130 -0
- package/src/provision/fingerprint.js +144 -0
- package/src/schema/pgserve-meta.js +120 -0
- package/src/security/blocked-versions.js +103 -0
- package/src/upgrade/index.js +5 -0
- package/src/upgrade/steps/binary-cache-flush.js +2 -2
- package/src/upgrade/steps/cosign-meta-migration.js +123 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured audit emitter for privilege-changing operations.
|
|
3
|
+
*
|
|
4
|
+
* Group 6 of autopg-distribution-cutover. This is the v1 audit surface
|
|
5
|
+
* consumed by Group 5's `create-app` / `list` / `revoke` / `rotate` and the
|
|
6
|
+
* LOCK 1 manifest verifier. Distinct from the legacy `src/audit.js` event
|
|
7
|
+
* stream (DB lifecycle, connection routing): that stream is `event`-keyed
|
|
8
|
+
* and writes to `~/.autopg/audit.log`; this stream is `op`-keyed and writes
|
|
9
|
+
* to `~/.autopg/logs/audit.log` with `schemaVersion: 1`.
|
|
10
|
+
*
|
|
11
|
+
* Records are JSON Lines. Every emit produces exactly one line. The shape
|
|
12
|
+
* is fixed at v1 to give the redaction lint a stable target — adding a new
|
|
13
|
+
* field is a `schemaVersion: 2` migration, not an in-place addition.
|
|
14
|
+
*
|
|
15
|
+
* Threat model the redaction lint guards:
|
|
16
|
+
* - The audit log will leak. Plan for it.
|
|
17
|
+
* - Therefore: no field name may be a secret category, and no value may
|
|
18
|
+
* be sourced from `process.env.*PASSWORD*` (or matching token/secret
|
|
19
|
+
* patterns). The lint enforces this at every call site.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import fs from 'fs';
|
|
23
|
+
import os from 'os';
|
|
24
|
+
import path from 'path';
|
|
25
|
+
|
|
26
|
+
export const AUDIT_SCHEMA_VERSION = 1;
|
|
27
|
+
|
|
28
|
+
export const AUDIT_OPS = Object.freeze({
|
|
29
|
+
CREATE_APP: 'create-app',
|
|
30
|
+
REVOKE: 'revoke',
|
|
31
|
+
ROTATE: 'rotate',
|
|
32
|
+
MANIFEST_VERIFY: 'manifest-verify',
|
|
33
|
+
MANIFEST_VERIFY_BYPASS: 'manifest-verify-bypass',
|
|
34
|
+
ADOPT_EXISTING_DB: 'adopt-existing-db',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const VALID_OPS = new Set(Object.values(AUDIT_OPS));
|
|
38
|
+
|
|
39
|
+
const FILE_MODE = 0o600;
|
|
40
|
+
const DIR_MODE = 0o700;
|
|
41
|
+
|
|
42
|
+
function getConfigDir() {
|
|
43
|
+
return (
|
|
44
|
+
process.env.AUTOPG_CONFIG_DIR ||
|
|
45
|
+
process.env.PGSERVE_CONFIG_DIR ||
|
|
46
|
+
path.join(os.homedir(), '.autopg')
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function defaultLogPath() {
|
|
51
|
+
return path.join(getConfigDir(), 'logs', 'audit.log');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let LOG_PATH = defaultLogPath();
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Override the audit log path. Tests use this to redirect into a scratch
|
|
58
|
+
* dir; the daemon may use it if `AUTOPG_CONFIG_DIR` is set after import.
|
|
59
|
+
*
|
|
60
|
+
* Pass no argument to reset to the default (re-resolves env vars).
|
|
61
|
+
*
|
|
62
|
+
* @param {{logFile?: string}} [cfg]
|
|
63
|
+
*/
|
|
64
|
+
export function configureAuditEmit(cfg = {}) {
|
|
65
|
+
if (cfg.logFile) {
|
|
66
|
+
LOG_PATH = cfg.logFile;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
LOG_PATH = defaultLogPath();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getAuditLogPath() {
|
|
73
|
+
return LOG_PATH;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Emit a single audit record.
|
|
78
|
+
*
|
|
79
|
+
* Required: `op`, `actor`. Optional: `app`, `role`, `manifestSha256`,
|
|
80
|
+
* `sigVerified`, `incidentId`. Unknown fields are passed through verbatim
|
|
81
|
+
* so call sites stay flexible — but the redaction lint validates that the
|
|
82
|
+
* payload never contains secret-shaped names or env-sourced secret values.
|
|
83
|
+
*
|
|
84
|
+
* Record shape on disk (JSON Lines):
|
|
85
|
+
* {"schemaVersion":1,"ts":"<iso>","op":"create-app",...}
|
|
86
|
+
*
|
|
87
|
+
* Returns the written record (mostly for tests; production callers ignore).
|
|
88
|
+
*
|
|
89
|
+
* @param {object} record
|
|
90
|
+
* @param {string} record.op - one of AUDIT_OPS
|
|
91
|
+
* @param {string} [record.actor] - OS user or admin role performing the op
|
|
92
|
+
* @param {string} [record.app] - target app name
|
|
93
|
+
* @param {string} [record.role] - target postgres role
|
|
94
|
+
* @param {string} [record.manifestSha256] - hex sha256 of the verified manifest
|
|
95
|
+
* @param {boolean} [record.sigVerified] - whether the manifest sig verified
|
|
96
|
+
* @param {string} [record.incidentId] - present only when bypass was used
|
|
97
|
+
* @returns {object}
|
|
98
|
+
*/
|
|
99
|
+
export function auditEmit(record) {
|
|
100
|
+
if (!record || typeof record !== 'object') {
|
|
101
|
+
throw new Error('auditEmit: record must be an object');
|
|
102
|
+
}
|
|
103
|
+
if (typeof record.op !== 'string' || !VALID_OPS.has(record.op)) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`auditEmit: unknown op "${record.op}". Allowed: ${[...VALID_OPS].join(', ')}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const out = {
|
|
110
|
+
schemaVersion: AUDIT_SCHEMA_VERSION,
|
|
111
|
+
ts: new Date().toISOString(),
|
|
112
|
+
...record,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
writeJsonLine(out, LOG_PATH);
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function writeJsonLine(record, logFile) {
|
|
120
|
+
const dir = path.dirname(logFile);
|
|
121
|
+
if (!fs.existsSync(dir)) {
|
|
122
|
+
fs.mkdirSync(dir, { recursive: true, mode: DIR_MODE });
|
|
123
|
+
}
|
|
124
|
+
const fd = fs.openSync(logFile, 'a', FILE_MODE);
|
|
125
|
+
try {
|
|
126
|
+
fs.writeSync(fd, JSON.stringify(record) + '\n');
|
|
127
|
+
} finally {
|
|
128
|
+
fs.closeSync(fd);
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
fs.chmodSync(logFile, FILE_MODE);
|
|
132
|
+
} catch { /* best-effort tighten */ }
|
|
133
|
+
}
|
|
134
|
+
|
package/src/cli-install.cjs
CHANGED
|
@@ -34,10 +34,33 @@ const path = require('node:path');
|
|
|
34
34
|
// promise once at module load and await it from async install paths.
|
|
35
35
|
const _adminJsonModuleP = import('./lib/admin-json.js');
|
|
36
36
|
const _socketDirModuleP = import('./lib/socket-dir.js');
|
|
37
|
+
const _runtimeJsonModuleP = import('./lib/runtime-json.js');
|
|
38
|
+
const _blockedVersionsModuleP = import('./security/blocked-versions.js');
|
|
37
39
|
|
|
38
40
|
async function loadCohortModules() {
|
|
39
|
-
const [adminJson, socketDirMod] = await Promise.all([
|
|
40
|
-
|
|
41
|
+
const [adminJson, socketDirMod, runtimeJson, blockedVersions] = await Promise.all([
|
|
42
|
+
_adminJsonModuleP,
|
|
43
|
+
_socketDirModuleP,
|
|
44
|
+
_runtimeJsonModuleP,
|
|
45
|
+
_blockedVersionsModuleP,
|
|
46
|
+
]);
|
|
47
|
+
return { adminJson, socketDirMod, runtimeJson, blockedVersions };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// pgserve singleton (v2.4) — `pgserve-singleton-no-proxy` wish, Group 5.
|
|
51
|
+
//
|
|
52
|
+
// Resolves the running pgserve version from package.json so `assertNotBlocked`
|
|
53
|
+
// can compare against the compile-time BLOCKED_VERSIONS list before any
|
|
54
|
+
// install/update mutation. We intentionally use the package.json shipped
|
|
55
|
+
// with this binary (`require.resolve` from inside cli-install.cjs) rather
|
|
56
|
+
// than the version on the host filesystem — we want to refuse THIS binary
|
|
57
|
+
// running, not a different binary that might happen to live next door.
|
|
58
|
+
function getCurrentVersion() {
|
|
59
|
+
try {
|
|
60
|
+
return require('../package.json').version;
|
|
61
|
+
} catch (_e) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
41
64
|
}
|
|
42
65
|
|
|
43
66
|
// pgserve singleton (v2.4) — `pgserve-singleton-no-proxy` wish, Group 1.
|
|
@@ -165,6 +188,131 @@ function readConfig() {
|
|
|
165
188
|
}
|
|
166
189
|
}
|
|
167
190
|
|
|
191
|
+
// cutover G19: discovery layer used by `autopg port / url / status`.
|
|
192
|
+
//
|
|
193
|
+
// Order of precedence (most-authoritative first):
|
|
194
|
+
// 1. `<socketDir>/runtime.json` — written by the live postmaster at greet
|
|
195
|
+
// time, removed on graceful shutdown. Carries the *current* port + pid
|
|
196
|
+
// for an actually-running daemon.
|
|
197
|
+
// 2. `~/.autopg/admin.json` — supervisor record written at install time.
|
|
198
|
+
// Survives postmaster restarts; doesn't reflect runtime state.
|
|
199
|
+
// 3. `~/.autopg/config.json` — legacy pre-G19 install record. Final
|
|
200
|
+
// fallback so older installs that haven't been re-installed under v2.4
|
|
201
|
+
// still discover cleanly.
|
|
202
|
+
//
|
|
203
|
+
// All readers swallow errors — discovery must never throw on a missing or
|
|
204
|
+
// truncated file. Synchronous on purpose: `dispatch()` for status/url/port
|
|
205
|
+
// is sync and the wrapper handles only `Promise OR number` return types.
|
|
206
|
+
function readRuntimeJsonSync(socketDir) {
|
|
207
|
+
if (typeof socketDir !== 'string' || socketDir.length === 0) return null;
|
|
208
|
+
const file = path.join(socketDir, 'runtime.json');
|
|
209
|
+
let raw;
|
|
210
|
+
try {
|
|
211
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
const parsed = JSON.parse(raw);
|
|
217
|
+
return (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : null;
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function readAdminJsonSync() {
|
|
224
|
+
const file = path.join(getConfigDir(), ADMIN_FILE_NAME);
|
|
225
|
+
let raw;
|
|
226
|
+
try {
|
|
227
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
228
|
+
} catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
const parsed = JSON.parse(raw);
|
|
233
|
+
return (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : null;
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function resolveCanonicalSocketDir() {
|
|
240
|
+
// Mirror src/lib/socket-dir.js#resolveSocketDir — pure function, no fs
|
|
241
|
+
// touch. Inlined here so the sync discovery layer doesn't need a top-
|
|
242
|
+
// level await on the ESM module.
|
|
243
|
+
const xdg = process.env.XDG_RUNTIME_DIR;
|
|
244
|
+
const base = xdg && xdg.length > 0 ? xdg : '/tmp';
|
|
245
|
+
return path.join(base, 'pgserve');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function isLivePid(pid) {
|
|
249
|
+
if (!Number.isInteger(pid) || pid < 1) return false;
|
|
250
|
+
try {
|
|
251
|
+
process.kill(pid, 0);
|
|
252
|
+
return true;
|
|
253
|
+
} catch (err) {
|
|
254
|
+
return err && err.code === 'EPERM';
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Compose a discovery view from runtime.json (preferred), admin.json
|
|
260
|
+
* (fallback), and config.json (legacy fallback). Returns:
|
|
261
|
+
* {
|
|
262
|
+
* runtime: { socketDir, port, pid, autopgPid, schemaVersion } | null,
|
|
263
|
+
* admin: { supervisor, socketDir, port, installedAt, ... } | null,
|
|
264
|
+
* config: { port, dataDir, registeredAt } | null,
|
|
265
|
+
* // composed view — best effort merge for callers that just want
|
|
266
|
+
* // "where do I connect right now?":
|
|
267
|
+
* socketDir: <string|null>,
|
|
268
|
+
* port: <number|null>,
|
|
269
|
+
* liveAutopg: <boolean> // true when runtime.json names a live pid
|
|
270
|
+
* }
|
|
271
|
+
*/
|
|
272
|
+
function readDiscovery() {
|
|
273
|
+
const config = readConfig();
|
|
274
|
+
const admin = readAdminJsonSync();
|
|
275
|
+
// Prefer the socket dir the supervisor recorded at install time — that's
|
|
276
|
+
// the path operators configured. Only fall back to the canonical resolver
|
|
277
|
+
// when the install record is missing (fresh-host case).
|
|
278
|
+
const socketDir = (admin && typeof admin.socketDir === 'string' && admin.socketDir.length > 0)
|
|
279
|
+
? admin.socketDir
|
|
280
|
+
: resolveCanonicalSocketDir();
|
|
281
|
+
const runtime = readRuntimeJsonSync(socketDir);
|
|
282
|
+
|
|
283
|
+
// PR #80 P2 fix: previous logic treated ANY parsed runtime.json as
|
|
284
|
+
// authoritative — a malformed-but-JSON file (no port, no socketDir) would
|
|
285
|
+
// hide later admin.json / config fallbacks because composedPort stayed
|
|
286
|
+
// null while the precedence chain stopped early. Validate that runtime
|
|
287
|
+
// actually carries a usable port + socketDir before treating it as live.
|
|
288
|
+
// Mirrors the admin / config branches' Number.isInteger guard.
|
|
289
|
+
let composedSocketDir = null;
|
|
290
|
+
let composedPort = null;
|
|
291
|
+
const runtimeUsable = runtime
|
|
292
|
+
&& Number.isInteger(runtime.port)
|
|
293
|
+
&& typeof runtime.socketDir === 'string'
|
|
294
|
+
&& runtime.socketDir.length > 0;
|
|
295
|
+
if (runtimeUsable) {
|
|
296
|
+
composedSocketDir = runtime.socketDir;
|
|
297
|
+
composedPort = runtime.port;
|
|
298
|
+
} else if (admin && Number.isInteger(admin.port)) {
|
|
299
|
+
composedSocketDir = admin.socketDir ?? socketDir;
|
|
300
|
+
composedPort = admin.port;
|
|
301
|
+
} else if (config && Number.isInteger(config.port)) {
|
|
302
|
+
composedPort = config.port;
|
|
303
|
+
composedSocketDir = socketDir;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
runtime,
|
|
308
|
+
admin,
|
|
309
|
+
config,
|
|
310
|
+
socketDir: composedSocketDir,
|
|
311
|
+
port: composedPort,
|
|
312
|
+
liveAutopg: !!(runtime && isLivePid(runtime.autopgPid)),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
168
316
|
function writeConfig(config) {
|
|
169
317
|
const dir = getConfigDir();
|
|
170
318
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o755 });
|
|
@@ -545,7 +693,25 @@ function cmdAuthDispatch(args) {
|
|
|
545
693
|
* wrapper before this module is required (avoids re-resolving here).
|
|
546
694
|
*/
|
|
547
695
|
async function cmdInstall(args, ctx) {
|
|
548
|
-
const { adminJson, socketDirMod } = await loadCohortModules();
|
|
696
|
+
const { adminJson, socketDirMod, blockedVersions } = await loadCohortModules();
|
|
697
|
+
|
|
698
|
+
// pgserve singleton (v2.4) — `pgserve-singleton-no-proxy` wish, Group 5.
|
|
699
|
+
// Refuse to install if THIS binary's version appears in the compile-time
|
|
700
|
+
// blocklist. Runs first (before any host-touching work) so the operator
|
|
701
|
+
// sees a clear `EBLOCKEDVERSION:` diagnostic with the locked reason +
|
|
702
|
+
// remediation hint, exit code 4 (distinct from generic install failures).
|
|
703
|
+
const currentVersion = getCurrentVersion();
|
|
704
|
+
if (currentVersion) {
|
|
705
|
+
try {
|
|
706
|
+
blockedVersions.assertNotBlocked(currentVersion);
|
|
707
|
+
} catch (err) {
|
|
708
|
+
if (err.code === 'EBLOCKEDVERSION') {
|
|
709
|
+
process.stderr.write(`${err.message}\n`);
|
|
710
|
+
process.exit(4);
|
|
711
|
+
}
|
|
712
|
+
throw err;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
549
715
|
|
|
550
716
|
// pgserve singleton (v2.4): refuse to install if a different supervisor
|
|
551
717
|
// (Tier B systemd-user / launchd) already owns the host. The cohort
|
|
@@ -704,15 +870,20 @@ function writeSupervisorRecord(adminJson, { supervisor, socketDir, port }) {
|
|
|
704
870
|
/**
|
|
705
871
|
* `pgserve status [--json]`
|
|
706
872
|
*
|
|
707
|
-
* Reports both pm2 state and on-disk
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
873
|
+
* Reports both pm2 state and on-disk discovery (runtime.json → admin.json
|
|
874
|
+
* → config.json fallback chain). Exits 0 with status info regardless of
|
|
875
|
+
* running/stopped — operators script around the JSON output. Non-zero
|
|
876
|
+
* only when nothing was ever installed (no admin.json AND no config.json).
|
|
877
|
+
*
|
|
878
|
+
* Cutover G19: surfaces `runtime` (live socket discovery) and `socketDir`
|
|
879
|
+
* top-level so consumers can pick UDS vs TCP without parsing pm2 jlist.
|
|
711
880
|
*/
|
|
712
881
|
function cmdStatus(args) {
|
|
713
882
|
const json = args.includes('--json');
|
|
714
|
-
const
|
|
715
|
-
|
|
883
|
+
const discovery = readDiscovery();
|
|
884
|
+
const { config, admin, runtime } = discovery;
|
|
885
|
+
|
|
886
|
+
if (!config && !admin) {
|
|
716
887
|
if (json) {
|
|
717
888
|
process.stdout.write(`${JSON.stringify({ installed: false })}\n`);
|
|
718
889
|
} else {
|
|
@@ -720,24 +891,41 @@ function cmdStatus(args) {
|
|
|
720
891
|
}
|
|
721
892
|
return 1;
|
|
722
893
|
}
|
|
894
|
+
|
|
723
895
|
const proc = pm2GetProcess(PM2_PROCESS_NAME);
|
|
724
896
|
const status = proc?.pm2_env?.status ?? 'stopped';
|
|
725
897
|
const pid = proc?.pid ?? null;
|
|
726
898
|
const uptimeMs = proc?.pm2_env?.pm_uptime ? Date.now() - proc.pm2_env.pm_uptime : null;
|
|
727
899
|
const restarts = proc?.pm2_env?.restart_time ?? 0;
|
|
728
900
|
|
|
901
|
+
const port = discovery.port;
|
|
902
|
+
const socketDir = discovery.socketDir;
|
|
903
|
+
const dataDir = config?.dataDir ?? null;
|
|
904
|
+
|
|
729
905
|
const payload = {
|
|
730
906
|
installed: true,
|
|
731
907
|
name: PM2_PROCESS_NAME,
|
|
732
908
|
status,
|
|
733
909
|
pid,
|
|
734
|
-
port
|
|
735
|
-
|
|
910
|
+
port,
|
|
911
|
+
socketDir,
|
|
912
|
+
dataDir,
|
|
736
913
|
logsDir: getLogsDir(),
|
|
737
|
-
url: `postgres://localhost:${
|
|
914
|
+
url: port ? `postgres://localhost:${port}/postgres` : null,
|
|
738
915
|
uptimeMs,
|
|
739
916
|
restarts,
|
|
740
|
-
registeredAt: config
|
|
917
|
+
registeredAt: config?.registeredAt ?? null,
|
|
918
|
+
supervisor: admin?.supervisor ?? null,
|
|
919
|
+
runtime: runtime
|
|
920
|
+
? {
|
|
921
|
+
socketDir: runtime.socketDir,
|
|
922
|
+
port: runtime.port,
|
|
923
|
+
pid: runtime.pid,
|
|
924
|
+
autopgPid: runtime.autopgPid,
|
|
925
|
+
schemaVersion: runtime.schemaVersion,
|
|
926
|
+
live: discovery.liveAutopg,
|
|
927
|
+
}
|
|
928
|
+
: null,
|
|
741
929
|
};
|
|
742
930
|
|
|
743
931
|
if (json) {
|
|
@@ -746,42 +934,53 @@ function cmdStatus(args) {
|
|
|
746
934
|
}
|
|
747
935
|
process.stdout.write(`name ${payload.name}\n`);
|
|
748
936
|
process.stdout.write(`status ${payload.status}${payload.pid ? ` (pid ${payload.pid})` : ''}\n`);
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
937
|
+
if (payload.supervisor) {
|
|
938
|
+
process.stdout.write(`supervisor ${payload.supervisor}\n`);
|
|
939
|
+
}
|
|
940
|
+
if (payload.port != null) process.stdout.write(`port ${payload.port}\n`);
|
|
941
|
+
if (payload.url) process.stdout.write(`url ${payload.url}\n`);
|
|
942
|
+
if (payload.socketDir) process.stdout.write(`socketDir ${payload.socketDir}\n`);
|
|
943
|
+
if (payload.dataDir) process.stdout.write(`dataDir ${payload.dataDir}\n`);
|
|
752
944
|
process.stdout.write(`logsDir ${payload.logsDir}\n`);
|
|
945
|
+
if (payload.runtime) {
|
|
946
|
+
process.stdout.write(`runtime pid=${payload.runtime.pid} autopgPid=${payload.runtime.autopgPid} live=${payload.runtime.live}\n`);
|
|
947
|
+
} else {
|
|
948
|
+
process.stdout.write(`runtime (no runtime.json — postmaster down or never started)\n`);
|
|
949
|
+
}
|
|
753
950
|
if (payload.uptimeMs != null) {
|
|
754
951
|
const sec = Math.floor(payload.uptimeMs / 1000);
|
|
755
952
|
process.stdout.write(`uptime ${sec}s\n`);
|
|
756
953
|
}
|
|
757
954
|
process.stdout.write(`restarts ${payload.restarts}\n`);
|
|
758
|
-
process.stdout.write(`registered ${payload.registeredAt}\n`);
|
|
955
|
+
if (payload.registeredAt) process.stdout.write(`registered ${payload.registeredAt}\n`);
|
|
759
956
|
return 0;
|
|
760
957
|
}
|
|
761
958
|
|
|
762
959
|
/**
|
|
763
960
|
* `pgserve url`
|
|
764
961
|
*
|
|
765
|
-
* Discovery API. Prints the canonical connection string. Downstream
|
|
962
|
+
* Discovery API. Prints the canonical TCP connection string. Downstream
|
|
766
963
|
* installers (genie install, omni install) call this to learn where to
|
|
767
|
-
* connect, instead of hardcoding a port.
|
|
964
|
+
* connect, instead of hardcoding a port. The TCP form is stable across
|
|
965
|
+
* Tier A / Tier B / fingerprint-disabled hosts; UDS callers should
|
|
966
|
+
* resolve `<socketDir>/.s.PGSQL.<port>` from `autopg status --json`.
|
|
768
967
|
*/
|
|
769
968
|
function cmdUrl() {
|
|
770
|
-
const
|
|
771
|
-
if (
|
|
969
|
+
const discovery = readDiscovery();
|
|
970
|
+
if (discovery.port == null) {
|
|
772
971
|
fail('not installed (run: pgserve install)');
|
|
773
972
|
}
|
|
774
|
-
process.stdout.write(`postgres://localhost:${
|
|
973
|
+
process.stdout.write(`postgres://localhost:${discovery.port}/postgres\n`);
|
|
775
974
|
return 0;
|
|
776
975
|
}
|
|
777
976
|
|
|
778
|
-
/** `pgserve port` — print the canonical port. */
|
|
977
|
+
/** `pgserve port` — print the canonical port from runtime.json → admin.json → config.json. */
|
|
779
978
|
function cmdPort() {
|
|
780
|
-
const
|
|
781
|
-
if (
|
|
979
|
+
const discovery = readDiscovery();
|
|
980
|
+
if (discovery.port == null) {
|
|
782
981
|
fail('not installed (run: pgserve install)');
|
|
783
982
|
}
|
|
784
|
-
process.stdout.write(`${
|
|
983
|
+
process.stdout.write(`${discovery.port}\n`);
|
|
785
984
|
return 0;
|
|
786
985
|
}
|
|
787
986
|
|
|
@@ -878,6 +1077,12 @@ function dispatch(subcommand, args, ctx) {
|
|
|
878
1077
|
// dispatcher. dispatch() returns a Promise here; the wrapper
|
|
879
1078
|
// already handles both numeric and Promise returns.
|
|
880
1079
|
return import('./commands/uninstall.js').then((mod) => mod.runUninstall());
|
|
1080
|
+
case 'doctor':
|
|
1081
|
+
// pgserve-singleton-no-proxy Group 3: read-only V1. Reports the
|
|
1082
|
+
// active supervisor + postmaster reachability + admin.json /
|
|
1083
|
+
// runtime.json health. --fix tiered modes deferred to a follow-up
|
|
1084
|
+
// (SHARED-DESIGN §3.2).
|
|
1085
|
+
return import('./commands/doctor.js').then((mod) => mod.runDoctor(args).then((code) => process.exit(code)));
|
|
881
1086
|
case 'status':
|
|
882
1087
|
return cmdStatus(args);
|
|
883
1088
|
case 'url':
|
|
@@ -913,6 +1118,33 @@ function dispatch(subcommand, args, ctx) {
|
|
|
913
1118
|
}
|
|
914
1119
|
case 'auth':
|
|
915
1120
|
return cmdAuthDispatch(args);
|
|
1121
|
+
case 'verify': {
|
|
1122
|
+
// pgserve singleton (v2.4) — `pgserve-singleton-no-proxy` wish, Group 4.
|
|
1123
|
+
// `pgserve verify` is a pure-node command (cosign shellout + HMAC
|
|
1124
|
+
// cache token write); routes through the same async-import pattern
|
|
1125
|
+
// as `uninstall` so the ESM module isn't eagerly loaded.
|
|
1126
|
+
return import('./commands/verify.js').then((mod) => mod.runVerify(args));
|
|
1127
|
+
}
|
|
1128
|
+
case 'trust':
|
|
1129
|
+
// pgserve singleton (v2.4) — wish Group 3, second read-only verb.
|
|
1130
|
+
// `pgserve trust add/list/remove` manages the user-extensible cosign
|
|
1131
|
+
// trust store at ~/.pgserve/trust/identities.json. Pure node.
|
|
1132
|
+
// The wrapper handles the numeric-exit-code case; matches the
|
|
1133
|
+
// verify dispatch style so the wrapper, not the verb, owns
|
|
1134
|
+
// process.exit.
|
|
1135
|
+
return import('./commands/trust.js').then((mod) => mod.runTrust(args));
|
|
1136
|
+
case 'gc':
|
|
1137
|
+
// pgserve singleton (v2.4) — wish Group 3, verb 3. `pgserve gc`
|
|
1138
|
+
// sweeps orphaned databases. Default mode is dry-run; --apply
|
|
1139
|
+
// performs the actual DROP. Composes the orphan classifier +
|
|
1140
|
+
// audit-log writer + psql shellout primitives.
|
|
1141
|
+
return import('./commands/gc.js').then((mod) => mod.runGc(args));
|
|
1142
|
+
case 'provision':
|
|
1143
|
+
// pgserve singleton (v2.4) — wish Group 3, verb 4. Idempotent
|
|
1144
|
+
// CREATE ROLE / DATABASE / GRANT + UPSERT pgserve_meta. Honest
|
|
1145
|
+
// idempotency-driven serialization (see provision.js header for
|
|
1146
|
+
// why no advisory lock). Pure node + psql shellout.
|
|
1147
|
+
return import('./commands/provision.js').then((mod) => mod.runProvision(args));
|
|
916
1148
|
default:
|
|
917
1149
|
throw new Error(`pgserve: dispatch called with unknown subcommand "${subcommand}"`);
|
|
918
1150
|
}
|