pgserve 2.6.1 → 2.6.5
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/CHANGELOG.md +205 -0
- package/README.md +20 -0
- package/bin/pgserve-wrapper.cjs +27 -2
- package/console/dist/app.js +7 -7
- package/package.json +1 -1
- package/scripts/aggregate-manifest.sh +42 -26
- package/scripts/fetch-postgres-bins.sh +9 -2
- package/scripts/verify-published-artifacts.sh +85 -32
- package/src/admin/admin-bootstrap.js +296 -0
- package/src/cli-config.cjs +37 -0
- package/src/cli-install.cjs +24 -13
- package/src/commands/create-app.js +387 -0
- package/src/commands/doctor.js +65 -0
- package/src/commands/gc.js +16 -1
- package/src/commands/verify.js +94 -4
- package/src/cosign/locked-roots.js +141 -0
- package/src/cosign/trust-list.js +29 -6
- package/src/cosign/verify-binary.js +162 -12
- package/src/gc/audit-log.js +92 -0
- package/src/postgres.js +16 -1
- package/src/schema/autopg-meta.js +120 -0
package/src/postgres.js
CHANGED
|
@@ -1049,8 +1049,23 @@ export class PostgresManager extends EventEmitter {
|
|
|
1049
1049
|
if (!expected) {
|
|
1050
1050
|
this.socketDir = null;
|
|
1051
1051
|
this.databaseDir = null;
|
|
1052
|
+
// B5 (v2.6.3): include the captured postgres stderr/stdout tail
|
|
1053
|
+
// in the WARN so operators tailing pm2 logs see the actual cause
|
|
1054
|
+
// (e.g. "FATAL: could not bind IPv4 address ... Address already
|
|
1055
|
+
// in use", "FATAL: data directory ... has wrong ownership", etc.)
|
|
1056
|
+
// instead of just "subprocess exited unexpectedly". The tail is
|
|
1057
|
+
// capped at 4 KB so a runaway log spam can't bloat the WARN
|
|
1058
|
+
// payload; the bottom of startupOutput is where the fatal
|
|
1059
|
+
// diagnostic typically lands. Empty string when postgres exited
|
|
1060
|
+
// before producing any output (rare; preserved as 'no postgres
|
|
1061
|
+
// output captured' so the field stays present in structured
|
|
1062
|
+
// logs).
|
|
1063
|
+
const STDERR_TAIL_BUDGET = 4096;
|
|
1064
|
+
const tail = startupOutput.length > STDERR_TAIL_BUDGET
|
|
1065
|
+
? startupOutput.slice(-STDERR_TAIL_BUDGET)
|
|
1066
|
+
: startupOutput;
|
|
1052
1067
|
this.logger?.warn(
|
|
1053
|
-
{ code },
|
|
1068
|
+
{ code, postgresStderrTail: tail.trim() || 'no postgres output captured' },
|
|
1054
1069
|
'PostgreSQL subprocess exited unexpectedly — socketDir/databaseDir reset'
|
|
1055
1070
|
);
|
|
1056
1071
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `autopg_meta` table bootstrap.
|
|
3
|
+
*
|
|
4
|
+
* pgserve singleton (v2.6) — `autopg-distribution-cutover-finalize`
|
|
5
|
+
* wish, Group 3 (`pgserve create-app` + manifest LOCK 1).
|
|
6
|
+
*
|
|
7
|
+
* Source-of-truth split (per wish Decision #2 / G3 deliverable 4):
|
|
8
|
+
*
|
|
9
|
+
* `autopg_meta` is the single source of truth for "which apps are
|
|
10
|
+
* registered with this pgserve instance + what cosign trust roots are
|
|
11
|
+
* locked at create-app time." The per-consumer manifest file at
|
|
12
|
+
* `~/.autopg/<sanitized-slug>/manifest.json` (and the sibling
|
|
13
|
+
* `admin.json`) are derived caches; on divergence, the table wins.
|
|
14
|
+
*
|
|
15
|
+
* The `--fix` mutation modes that would regenerate the cache files
|
|
16
|
+
* from the table are NOT implemented in v2.4 read-only V1
|
|
17
|
+
* (src/commands/doctor.js:440-442 prints "--fix tiered modes are not
|
|
18
|
+
* implemented in v2.4"). Until those land, the cache-recovery story is
|
|
19
|
+
* manual: operator deletes the per-consumer dir + re-runs
|
|
20
|
+
* `pgserve create-app <slug>`. The verb is idempotent and preserves
|
|
21
|
+
* the locked_roots already on the row (idempotent re-run touches
|
|
22
|
+
* `last_updated` ONLY).
|
|
23
|
+
*
|
|
24
|
+
* Why a separate table from `pgserve_meta`: different lifecycle.
|
|
25
|
+
* `pgserve_meta` is per-database (provision/gc cohort, fingerprint as
|
|
26
|
+
* PK). `autopg_meta` is per-consumer-app (slug as PK), and an app can
|
|
27
|
+
* exist before any of its DBs do. Splitting the tables keeps each
|
|
28
|
+
* bootstrap genuinely additive + lets the wish Group 4/5 work
|
|
29
|
+
* (per-consumer doctor surface) reach for autopg_meta without
|
|
30
|
+
* crossing into pgserve_meta's invariants.
|
|
31
|
+
*
|
|
32
|
+
* Idempotency: every statement uses `IF NOT EXISTS` (table, indexes).
|
|
33
|
+
* Re-running on an already-bootstrapped database is a no-op.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export const AUTOPG_META_TABLE = 'autopg_meta';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Base columns owned by this module.
|
|
40
|
+
*
|
|
41
|
+
* - slug: PRIMARY KEY — the sanitized consumer slug
|
|
42
|
+
* (sanitizeSlug from src/provision/db-naming.js)
|
|
43
|
+
* - manifest_path: NOT NULL — absolute path to the cache manifest
|
|
44
|
+
* file at ~/.autopg/<slug>/manifest.json
|
|
45
|
+
* - locked_roots: NOT NULL — JSONB array shaped like
|
|
46
|
+
* TRUSTED_IDENTITIES entries, frozen-at-create
|
|
47
|
+
* - created_at: NOT NULL DEFAULT now() — set on insert
|
|
48
|
+
* - last_updated: NOT NULL DEFAULT now() — touched by every
|
|
49
|
+
* create-app re-run; locked_roots stays untouched
|
|
50
|
+
*/
|
|
51
|
+
export const AUTOPG_META_COLUMNS = Object.freeze([
|
|
52
|
+
'slug',
|
|
53
|
+
'manifest_path',
|
|
54
|
+
'locked_roots',
|
|
55
|
+
'created_at',
|
|
56
|
+
'last_updated',
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
// Schema-qualified name. The doctor / verifier read paths probe
|
|
60
|
+
// `to_regclass('public.autopg_meta')`; an unqualified CREATE TABLE
|
|
61
|
+
// could land in a non-public schema if a non-default search_path is
|
|
62
|
+
// configured on the active role, leaving subsequent reads unable to
|
|
63
|
+
// find it. Match the qualification convention pgserve_meta uses.
|
|
64
|
+
const QUALIFIED = `public.${AUTOPG_META_TABLE}`;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Idempotent statements that CREATE the table + supporting indexes.
|
|
68
|
+
* Returned as an array so callers can run each one individually for
|
|
69
|
+
* clear error reporting (mirrors src/schema/pgserve-meta.js shape).
|
|
70
|
+
*/
|
|
71
|
+
export function getBootstrapStatements() {
|
|
72
|
+
return [
|
|
73
|
+
[
|
|
74
|
+
`CREATE TABLE IF NOT EXISTS ${QUALIFIED} (`,
|
|
75
|
+
' slug TEXT PRIMARY KEY,',
|
|
76
|
+
' manifest_path TEXT NOT NULL,',
|
|
77
|
+
' locked_roots JSONB NOT NULL,',
|
|
78
|
+
' created_at TIMESTAMPTZ NOT NULL DEFAULT now(),',
|
|
79
|
+
' last_updated TIMESTAMPTZ NOT NULL DEFAULT now()',
|
|
80
|
+
')',
|
|
81
|
+
].join('\n'),
|
|
82
|
+
`CREATE INDEX IF NOT EXISTS ${AUTOPG_META_TABLE}_last_updated_idx ON ${QUALIFIED} (last_updated)`,
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Single SQL string variant — convenient for embedding the bootstrap in
|
|
88
|
+
* a transaction or pg-init script.
|
|
89
|
+
*/
|
|
90
|
+
export function getBootstrapSQL() {
|
|
91
|
+
return `${getBootstrapStatements().join(';\n\n')};\n`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Apply the bootstrap via a node-postgres-compatible client. The client
|
|
96
|
+
* must expose an async `query(sql)` method (matches both `pg.Client` and
|
|
97
|
+
* `pg.PoolClient`). Returns the list of statements executed.
|
|
98
|
+
*
|
|
99
|
+
* Statements run sequentially so a failure on the index half doesn't
|
|
100
|
+
* masquerade as success after the table half ran.
|
|
101
|
+
*/
|
|
102
|
+
export async function bootstrapAutopgMeta(client) {
|
|
103
|
+
if (!client || typeof client.query !== 'function') {
|
|
104
|
+
throw new TypeError('bootstrapAutopgMeta: client must expose an async query() method');
|
|
105
|
+
}
|
|
106
|
+
const statements = getBootstrapStatements();
|
|
107
|
+
for (const sql of statements) {
|
|
108
|
+
await client.query(sql);
|
|
109
|
+
}
|
|
110
|
+
return statements;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Predicate the doctor / verify read paths can call before deciding
|
|
115
|
+
* whether to query the table. Callers pass the result of
|
|
116
|
+
* `SELECT to_regclass('public.autopg_meta') IS NOT NULL`.
|
|
117
|
+
*/
|
|
118
|
+
export function tableExistsFromRegclass(toRegclassResult) {
|
|
119
|
+
return toRegclassResult === true;
|
|
120
|
+
}
|