pgserve 1.1.10 → 2.0.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 (43) hide show
  1. package/.genie/brainstorms/pgserve-v2/DESIGN.md +174 -0
  2. package/.genie/wishes/pgserve-v2/BRIEF-from-genie-pgserve.md +99 -0
  3. package/.genie/wishes/pgserve-v2/WISH.md +442 -0
  4. package/.genie/wishes/release-system-genie-pattern/WISH.md +268 -0
  5. package/.genie/wishes/release-system-genie-pattern/validation.md +205 -0
  6. package/.github/workflows/ci.yml +8 -4
  7. package/.github/workflows/release.yml +233 -111
  8. package/.github/workflows/{build-all-platforms.yml → version.yml} +32 -8
  9. package/AGENTS.md +10 -8
  10. package/CHANGELOG.md +150 -0
  11. package/Makefile +18 -41
  12. package/README.md +186 -1
  13. package/SECURITY.md +109 -0
  14. package/bin/pglite-server.js +253 -1
  15. package/eslint.config.js +2 -0
  16. package/package.json +1 -1
  17. package/src/admin-client.js +171 -0
  18. package/src/audit.js +168 -0
  19. package/src/control-db.js +313 -0
  20. package/src/daemon-control.js +408 -0
  21. package/src/daemon-shared.js +18 -0
  22. package/src/daemon-tcp.js +296 -0
  23. package/src/daemon.js +629 -0
  24. package/src/fingerprint.js +453 -0
  25. package/src/gc.js +351 -0
  26. package/src/index.js +11 -0
  27. package/src/postgres.js +54 -0
  28. package/src/protocol.js +131 -0
  29. package/src/router.js +78 -5
  30. package/src/tenancy.js +75 -0
  31. package/src/tokens.js +102 -0
  32. package/tests/audit.test.js +189 -0
  33. package/tests/control-db.test.js +285 -0
  34. package/tests/daemon-fingerprint-integration.test.js +109 -0
  35. package/tests/daemon-pr24-regression.test.js +201 -0
  36. package/tests/fingerprint.test.js +249 -0
  37. package/tests/fixtures/240-orphan-seed.sql +30 -0
  38. package/tests/multi-tenant.test.js +164 -0
  39. package/tests/orphan-cleanup.test.js +390 -0
  40. package/tests/tcp-listen.test.js +368 -0
  41. package/tests/tenancy.test.js +403 -0
  42. package/.github/release.yml +0 -30
  43. package/scripts/release.cjs +0 -198
@@ -1,138 +1,253 @@
1
- name: Unified Release
1
+ name: Release
2
+
3
+ # Single-branch release pipeline modeled on khal-os/desktop.
4
+ #
5
+ # Two trigger paths into the same workflow:
6
+ #
7
+ # 1. push to main with a hand-bumped package.json (no [skip ci] marker)
8
+ # -> auto-detect path: prepare reads package.json, checks if v${version}
9
+ # tag exists, builds + publishes + creates GitHub Release if not.
10
+ #
11
+ # 2. workflow_dispatch with bump=patch|minor|major
12
+ # -> bump job runs `npm version`, commits with [skip ci], tags, pushes.
13
+ # prepare/build/release continue inline in the same workflow run.
14
+ #
15
+ # Bot-loop guard: bump commits carry [skip ci]. Push of those commits is
16
+ # filtered out by the prepare gate, so the bot's own push does not retrigger.
17
+ #
18
+ # Auth: npm OIDC Trusted Publishing (configured via build-all-platforms.yml).
2
19
 
3
20
  on:
4
- pull_request:
5
- types: [closed]
21
+ push:
6
22
  branches: [main]
7
23
  workflow_dispatch:
8
24
  inputs:
9
- action:
10
- description: 'Release action'
25
+ bump:
26
+ description: "Version bump type"
11
27
  required: true
12
28
  type: choice
13
29
  options:
14
- - bump-rc
15
- - promote
30
+ - patch
31
+ - minor
32
+ - major
16
33
 
17
34
  concurrency:
18
- group: unified-release
35
+ group: release-${{ github.ref }}
19
36
  cancel-in-progress: false
20
37
 
21
38
  permissions:
22
39
  contents: write
23
- pull-requests: read
24
- id-token: write
40
+ id-token: write # required so the reusable `version.yml` workflow can mint
41
+ # the OIDC token for npm Trusted Publishing — without this,
42
+ # GH rejects the workflow at parse time (startup_failure)
43
+ # because the called job's `id-token: write` permission
44
+ # exceeds what the caller has granted.
25
45
 
26
46
  jobs:
27
- # Gate: Skip bot commits, detect action from PR labels
28
- gate:
29
- name: Release Gate
30
- runs-on: ubuntu-latest
31
- if: |
32
- github.event_name == 'workflow_dispatch' ||
33
- (github.event.pull_request.merged == true &&
34
- (contains(github.event.pull_request.labels.*.name, 'rc') ||
35
- contains(github.event.pull_request.labels.*.name, 'stable')))
36
- outputs:
37
- should_run: ${{ steps.check.outputs.should_run }}
38
- action: ${{ steps.detect.outputs.action }}
39
-
40
- steps:
41
- - name: Check for bot commits
42
- id: check
43
- run: |
44
- # Skip if this is a bot commit (prevents infinite loops)
45
- if [[ "${{ github.actor }}" == "github-actions[bot]" ]]; then
46
- echo "Skipping: bot commit detected"
47
- echo "should_run=false" >> $GITHUB_OUTPUT
48
- else
49
- echo "should_run=true" >> $GITHUB_OUTPUT
50
- fi
51
-
52
- - name: Detect action
53
- id: detect
54
- if: steps.check.outputs.should_run == 'true'
55
- run: |
56
- if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
57
- echo "action=${{ inputs.action }}" >> $GITHUB_OUTPUT
58
- echo "Action from dispatch: ${{ inputs.action }}"
59
- elif [[ "${{ contains(github.event.pull_request.labels.*.name, 'stable') }}" == "true" ]]; then
60
- echo "action=promote" >> $GITHUB_OUTPUT
61
- echo "Action from label: promote (stable)"
62
- elif [[ "${{ contains(github.event.pull_request.labels.*.name, 'rc') }}" == "true" ]]; then
63
- echo "action=bump-rc" >> $GITHUB_OUTPUT
64
- echo "Action from label: bump-rc"
65
- else
66
- echo "No release label found"
67
- echo "action=" >> $GITHUB_OUTPUT
68
- fi
69
-
70
- # Version: Bump version and create tag
71
- version:
72
- name: Bump Version
73
- needs: gate
74
- if: needs.gate.outputs.should_run == 'true' && needs.gate.outputs.action != ''
47
+ # ---------------------------------------------------------------------------
48
+ # Bump (workflow_dispatch only): npm version, commit [skip ci], tag, push.
49
+ # ---------------------------------------------------------------------------
50
+ bump:
51
+ name: Bump version
52
+ if: github.event_name == 'workflow_dispatch'
75
53
  runs-on: ubuntu-latest
54
+ timeout-minutes: 5
76
55
  outputs:
77
56
  version: ${{ steps.bump.outputs.version }}
78
57
  tag: ${{ steps.bump.outputs.tag }}
79
- npm_tag: ${{ steps.bump.outputs.npm_tag }}
80
- is_promote: ${{ steps.bump.outputs.is_promote }}
81
-
82
58
  steps:
83
- - name: Checkout
84
- uses: actions/checkout@v4
59
+ - uses: actions/checkout@v4
85
60
  with:
61
+ ref: main
86
62
  fetch-depth: 0
87
63
  token: ${{ secrets.GITHUB_TOKEN }}
88
64
 
89
- - name: Setup Node.js
90
- uses: actions/setup-node@v4
65
+ - uses: actions/setup-node@v4
91
66
  with:
92
- node-version: '20'
67
+ node-version: "22"
93
68
 
94
- - name: Configure Git
69
+ - name: Configure git
95
70
  run: |
96
71
  git config user.name "github-actions[bot]"
97
72
  git config user.email "github-actions[bot]@users.noreply.github.com"
98
73
 
99
- - name: Run release script
74
+ - name: Bump version
100
75
  id: bump
101
- run: node scripts/release.cjs --action ${{ needs.gate.outputs.action }}
76
+ run: |
77
+ npm version "${{ inputs.bump }}" --no-git-tag-version
78
+ VERSION=$(node -p "require('./package.json').version")
79
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
80
+ echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
81
+ echo "Bumped to ${VERSION}"
82
+
83
+ - name: Commit, tag, push
84
+ run: |
85
+ VERSION="${{ steps.bump.outputs.version }}"
86
+ TAG="${{ steps.bump.outputs.tag }}"
87
+ git add package.json
88
+ git commit -m "[skip ci] release ${TAG}"
89
+ git tag "${TAG}"
90
+ git push origin HEAD --follow-tags
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Prepare: resolve version, skip if tag already exists, build changelog.
94
+ #
95
+ # Gate handles both event types:
96
+ # - push: bump is skipped; the !cancelled() && !failure() guard lets
97
+ # this job run regardless. The [skip ci] check filters the
98
+ # bot's own bump-commit push so it does not retrigger.
99
+ # - dispatch: bump succeeded; the workflow_dispatch branch of the OR
100
+ # short-circuits the [skip ci] check (the dispatch event
101
+ # itself does not carry the bump's commit message).
102
+ # ---------------------------------------------------------------------------
103
+ prepare:
104
+ name: Prepare release
105
+ needs: bump
106
+ if: |
107
+ !cancelled() && !failure() &&
108
+ (github.event_name == 'workflow_dispatch' ||
109
+ (github.event_name == 'push' &&
110
+ !startsWith(github.event.head_commit.message, '[skip ci]')))
111
+ runs-on: ubuntu-latest
112
+ timeout-minutes: 5
113
+ outputs:
114
+ version: ${{ steps.ver.outputs.version }}
115
+ tag: ${{ steps.ver.outputs.tag }}
116
+ skip: ${{ steps.ver.outputs.skip }}
117
+ changelog: ${{ steps.changelog.outputs.notes }}
118
+ # Surface the resolved checkout-ref so downstream jobs (build, release)
119
+ # can use it. They cannot reference `needs.bump.outputs.*` directly
120
+ # because they only have `needs: prepare` (or [prepare, build]) — not
121
+ # `bump` — in their needs context.
122
+ ref: ${{ needs.bump.outputs.tag || github.sha }}
123
+ steps:
124
+ - uses: actions/checkout@v4
125
+ with:
126
+ # On dispatch, check out the freshly-pushed tag; on push, the
127
+ # triggering SHA already has the bumped package.json.
128
+ # (Prepare cannot reference its own `outputs.ref` here — that's
129
+ # only available to downstream jobs.)
130
+ ref: ${{ needs.bump.outputs.tag || github.sha }}
131
+ fetch-depth: 0
132
+
133
+ - name: Resolve version
134
+ id: ver
135
+ env:
136
+ GH_TOKEN: ${{ github.token }}
137
+ run: |
138
+ VERSION=$(node -p "require('./package.json').version")
139
+ TAG="v${VERSION}"
140
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
141
+ echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
142
+
143
+ if gh release view "${TAG}" > /dev/null 2>&1; then
144
+ echo "Release ${TAG} already exists — skipping"
145
+ echo "skip=true" >> "$GITHUB_OUTPUT"
146
+ else
147
+ echo "skip=false" >> "$GITHUB_OUTPUT"
148
+ fi
149
+
150
+ - name: Find previous release tag
151
+ id: prev
152
+ if: steps.ver.outputs.skip == 'false'
153
+ env:
154
+ GH_TOKEN: ${{ github.token }}
155
+ run: |
156
+ TAG="${{ steps.ver.outputs.tag }}"
157
+ PREV=$(gh release list --limit 50 --json tagName -q '.[].tagName' | grep -v "^${TAG}$" | head -1)
158
+ if [ -n "$PREV" ] && git merge-base --is-ancestor "$PREV" HEAD 2>/dev/null; then
159
+ echo "tag=${PREV}" >> "$GITHUB_OUTPUT"
160
+ echo "Previous tag: ${PREV}"
161
+ else
162
+ echo "tag=" >> "$GITHUB_OUTPUT"
163
+ echo "No previous tag reachable from HEAD"
164
+ fi
165
+
166
+ - name: Generate changelog
167
+ id: changelog
168
+ if: steps.ver.outputs.skip == 'false' && steps.prev.outputs.tag != ''
169
+ run: |
170
+ PREV="${{ steps.prev.outputs.tag }}"
171
+ NOTES=$(git log --oneline "${PREV}..HEAD" --pretty="- %s" | head -50)
172
+ {
173
+ echo "notes<<EOF"
174
+ echo "$NOTES"
175
+ echo "EOF"
176
+ } >> "$GITHUB_OUTPUT"
102
177
 
103
- - name: Push changes
178
+ # Echo the resolved outputs so downstream skip/no-skip decisions are
179
+ # debuggable from the run log without re-running with step debug.
180
+ - name: Debug resolved outputs
104
181
  run: |
105
- git push origin HEAD:${{ github.ref_name }}
106
- git push origin --tags
182
+ echo "version=${{ steps.ver.outputs.version }}"
183
+ echo "tag=${{ steps.ver.outputs.tag }}"
184
+ echo "skip=${{ steps.ver.outputs.skip }}"
185
+ echo "prev=${{ steps.prev.outputs.tag }}"
107
186
 
108
- # Build & Publish: Always build and publish (npm_tag comes from version job)
187
+ # ---------------------------------------------------------------------------
188
+ # Build & Publish: matrix build of platform binaries + npm publish via OIDC.
189
+ #
190
+ # The reusable workflow filename is `version.yml` because npm Trusted
191
+ # Publisher is configured against that exact path. Renaming requires
192
+ # updating the npmjs.com Trusted Publisher entry first.
193
+ #
194
+ # The `if:` uses `always() && needs.prepare.result == 'success' &&
195
+ # needs.prepare.outputs.skip != 'true'`. This is the bulletproof pattern
196
+ # for reusable-workflow callers when any upstream job in the `needs:`
197
+ # chain was SKIPPED. With the simpler `needs.prepare.outputs.skip != 'true'`
198
+ # alone, GH Actions silently evaluated the gate as false even though the
199
+ # debug step in `prepare` proved the output was actually `'false'` — a
200
+ # known GH Actions quirk: when a job's transitive `needs:` chain includes
201
+ # a skipped job (here, `bump` is skipped on push events), the reusable-
202
+ # workflow caller's expression evaluator treats `needs.<job>.outputs.<x>`
203
+ # as null/missing regardless of the actual value.
204
+ #
205
+ # `always()` opts out of the implicit success() filter; the explicit
206
+ # `result == 'success'` reinstates it correctly; the outputs check then
207
+ # works as intended.
208
+ # ---------------------------------------------------------------------------
109
209
  build:
110
210
  name: Build & Publish
111
- needs: version
112
- uses: ./.github/workflows/build-all-platforms.yml
211
+ needs: prepare
212
+ if: |
213
+ always() &&
214
+ needs.prepare.result == 'success' &&
215
+ needs.prepare.outputs.skip != 'true'
216
+ uses: ./.github/workflows/version.yml
113
217
  with:
114
- version: ${{ needs.version.outputs.version }}
115
- npm_tag: ${{ needs.version.outputs.npm_tag }}
116
- ref: ${{ needs.version.outputs.tag }}
218
+ version: ${{ needs.prepare.outputs.version }}
219
+ npm_tag: latest
220
+ # Use prepare.outputs.ref (which resolves to the bump-job tag on
221
+ # dispatch, or `github.sha` on push). Build cannot reference
222
+ # `needs.bump.*` directly — only `prepare` is in its `needs:` chain.
223
+ # On the push path nobody creates the tag before this runs; the
224
+ # SHA-based checkout works because the merge commit already has the
225
+ # bumped package.json. The release job creates the tag at the end
226
+ # via `gh release create`.
227
+ ref: ${{ needs.prepare.outputs.ref }}
117
228
  secrets: inherit
118
229
 
119
- # GitHub Release: Create release with changelog
120
- github-release:
230
+ # ---------------------------------------------------------------------------
231
+ # Release: download artifacts, create GitHub Release with cliff-free notes.
232
+ # ---------------------------------------------------------------------------
233
+ release:
121
234
  name: Create GitHub Release
122
- needs: [version, build]
123
- if: always() && needs.version.result == 'success' && needs.build.result == 'success'
235
+ needs: [prepare, build]
236
+ if: |
237
+ always() &&
238
+ needs.prepare.result == 'success' &&
239
+ needs.build.result == 'success' &&
240
+ needs.prepare.outputs.skip != 'true'
124
241
  runs-on: ubuntu-latest
125
- permissions:
126
- contents: write
127
-
242
+ timeout-minutes: 10
128
243
  steps:
129
- - name: Checkout
130
- uses: actions/checkout@v4
244
+ - uses: actions/checkout@v4
131
245
  with:
132
- ref: ${{ needs.version.outputs.tag }}
246
+ # Same as the build job: prefer the bump job's tag (dispatch
247
+ # path) but fall back to the SHA (push path, no tag exists yet).
248
+ ref: ${{ needs.prepare.outputs.ref }}
133
249
 
134
- - name: Download artifacts
135
- if: needs.build.result == 'success'
250
+ - name: Download binaries
136
251
  uses: actions/download-artifact@v4
137
252
  with:
138
253
  path: dist/
@@ -140,28 +255,35 @@ jobs:
140
255
  merge-multiple: true
141
256
 
142
257
  - name: List artifacts
258
+ run: ls -la dist/
259
+
260
+ - name: Create release
261
+ env:
262
+ GH_TOKEN: ${{ github.token }}
263
+ RELEASE_NOTES: ${{ needs.prepare.outputs.changelog }}
143
264
  run: |
144
- if [ -d "dist" ]; then
145
- echo "Release artifacts:"
146
- ls -la dist/
147
- else
148
- echo "No artifacts (promote release)"
265
+ TAG="${{ needs.prepare.outputs.tag }}"
266
+ VERSION="${{ needs.prepare.outputs.version }}"
267
+
268
+ if [ -z "$RELEASE_NOTES" ]; then
269
+ RELEASE_NOTES="Release ${TAG}"
149
270
  fi
150
271
 
151
- - name: Create GitHub Release
152
- uses: softprops/action-gh-release@v2
153
- with:
154
- tag_name: ${{ needs.version.outputs.tag }}
155
- name: ${{ needs.version.outputs.tag }}
156
- generate_release_notes: true
157
- body: |
158
- ## Install
159
-
160
- ```bash
161
- npm install pgserve@${{ needs.version.outputs.npm_tag }}
162
- bunx pgserve@${{ needs.version.outputs.npm_tag }}
163
- ```
164
- prerelease: ${{ contains(needs.version.outputs.version, '-rc.') }}
165
- files: |
272
+ {
273
+ echo "$RELEASE_NOTES"
274
+ echo ""
275
+ echo "## Install"
276
+ echo ""
277
+ echo '```bash'
278
+ echo "npm install pgserve@${VERSION}"
279
+ echo "bunx pgserve@${VERSION}"
280
+ echo '```'
281
+ } > /tmp/release-notes.md
282
+
283
+ # The tag already exists (created by bump job on dispatch, or by the
284
+ # human commit on push). gh release create resolves --target via the
285
+ # tag automatically when omitted.
286
+ gh release create "${TAG}" \
287
+ --title "${TAG}" \
288
+ --notes-file /tmp/release-notes.md \
166
289
  dist/*
167
- fail_on_unmatched_files: false
@@ -65,7 +65,7 @@ jobs:
65
65
  - name: Setup Bun
66
66
  uses: oven-sh/setup-bun@v2
67
67
  with:
68
- bun-version: latest
68
+ bun-version: 1.3.11
69
69
 
70
70
  - name: Install dependencies
71
71
  run: bun install
@@ -112,7 +112,12 @@ jobs:
112
112
  needs: build
113
113
  runs-on: ubuntu-latest
114
114
  if: inputs.version != ''
115
- environment: npm-publish
115
+ # Note: `environment: npm-publish` was removed because the npmjs.com
116
+ # Trusted Publisher entry for `pgserve` does not declare an environment
117
+ # name. With the env gate present here, the OIDC token's environment
118
+ # claim did not match the registry's expectation and `npm publish`
119
+ # returned 404. Re-add this line if/when the Trusted Publisher entry
120
+ # has its Environment Name field set to `npm-publish`.
116
121
  permissions:
117
122
  id-token: write
118
123
  contents: read
@@ -123,20 +128,34 @@ jobs:
123
128
  with:
124
129
  ref: ${{ inputs.ref || github.ref }}
125
130
 
131
+ # Node 24 ships npm 11.5+ which has built-in OIDC trusted-publisher
132
+ # support. Avoids the `npm install -g npm@latest` self-upgrade bug
133
+ # (Arborist clobbering its own promise-retry dep mid-install) that
134
+ # broke rlmx's OIDC publish on Node 22.
126
135
  - name: Setup Node.js
127
136
  uses: actions/setup-node@v4
128
137
  with:
129
- node-version: '20'
138
+ node-version: '24'
130
139
  registry-url: 'https://registry.npmjs.org'
131
140
 
132
141
  - name: Setup Bun
133
142
  uses: oven-sh/setup-bun@v2
134
143
  with:
135
- bun-version: latest
144
+ bun-version: 1.3.11
136
145
 
137
146
  - name: Install dependencies
138
147
  run: bun install
139
148
 
149
+ - name: Verify npm version supports OIDC trusted publishing
150
+ run: |
151
+ NPM_VERSION=$(npm --version)
152
+ echo "npm version: ${NPM_VERSION}"
153
+ MAJOR=$(echo "${NPM_VERSION}" | cut -d. -f1)
154
+ if [ "${MAJOR}" -lt 11 ]; then
155
+ echo "::error::npm ${NPM_VERSION} too old — OIDC requires >= 11.5.1. Bump node-version above."
156
+ exit 1
157
+ fi
158
+
140
159
  - name: Download all artifacts
141
160
  uses: actions/download-artifact@v4
142
161
  with:
@@ -179,13 +198,18 @@ jobs:
179
198
  echo "published=false" >> $GITHUB_OUTPUT
180
199
  fi
181
200
 
182
- - name: Publish to npm
201
+ - name: Publish to npm via OIDC
183
202
  if: steps.check.outputs.published == 'false'
203
+ env:
204
+ HUSKY: "0"
205
+ # npm auto-enables provenance in any CI env with `id-token: write`,
206
+ # regardless of the --provenance CLI flag. On some runners the
207
+ # server-side sigstore check fails with 422; disable explicitly.
208
+ # OIDC token exchange still happens.
209
+ NPM_CONFIG_PROVENANCE: "false"
184
210
  run: |
185
- echo "Publishing version ${{ inputs.version }} with tag ${{ inputs.npm_tag }}"
211
+ echo "Publishing version ${{ inputs.version }} with tag ${{ inputs.npm_tag }} via OIDC"
186
212
  npm publish --access public --tag ${{ inputs.npm_tag }}
187
- env:
188
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
189
213
 
190
214
  - name: Verify publish
191
215
  if: steps.check.outputs.published == 'false'
package/AGENTS.md CHANGED
@@ -190,16 +190,18 @@ Before editing ANY implementation file, Base Genie must check:
190
190
 
191
191
  **Protocol:** `@.genie/spells/orchestration-boundary-protocol.md`
192
192
 
193
- **Release Workflow Protocol:**
194
- - Never manually trigger `workflow_dispatch` for releases
195
- - Never manually bump version in package.json
196
- - ✅ Always use PR with `rc` or `stable` label - release.yml auto-triggers on merge
197
- - Version bumping is automated by scripts/release.cjs
198
-
199
- **Documented Violations:**
193
+ **Release Workflow Protocol (push-to-main, single tier):**
194
+ - Manual path: bump locally with `npm version patch|minor|major`, commit, PR to main. Merge → `release.yml` fires automatically.
195
+ - Bot path: `gh workflow run release.yml -f bump=patch` (or `minor`/`major`). Bot bumps, tags, builds binaries, publishes to npm via OIDC.
196
+ - ✅ Skip: any commit message starting with `[skip ci]` is filtered by the prepare gate.
197
+ - No `rc`/`stable` PR labels (legacy — removed). No `scripts/release.cjs` (deleted).
198
+ - ❌ Don't edit `package.json` `version` directly on `main` outside the `npm version` flow above.
199
+ - ⚙️ npm publish runs from `.github/workflows/version.yml` (the file npmjs.com Trusted Publisher is bound to).
200
+
201
+ **Documented Violations (history):**
200
202
  - Bug #168, task b51db539, 2025-10-21 (duplicate implementation)
201
203
  - 2025-10-26 (claimed release implementation steps without investigating automation)
202
- - 2025-12-08 (manually set version to 1.1.0 + triggered workflow_dispatch → version jumped to 1.1.1-rc.1)
204
+ - 2025-12-08 (manually set version to 1.1.0 + triggered workflow_dispatch → version jumped to 1.1.1-rc.1; this class of error is no longer possible — the bump path goes through `gh workflow run`, not direct package.json edits)
203
205
 
204
206
  ### 4. Task State Optimization - Live State, Not Documentation 🔴 CRITICAL
205
207
  **Rule:** Task state is ephemeral runtime data, not permanent documentation
package/CHANGELOG.md ADDED
@@ -0,0 +1,150 @@
1
+ # Changelog
2
+
3
+ All notable changes to `pgserve` are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres
5
+ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## 2.0.0 — Unreleased
8
+
9
+ > The release date will replace "Unreleased" when the v2.0.0 release workflow
10
+ > fires. The CHANGELOG is committed ahead of the release trigger so consumers
11
+ > can review the migration plan before the artifact lands on npm.
12
+
13
+ ### Pin guidance (read this first)
14
+
15
+ Existing v1 consumers should pin `pgserve@^1.x` in their `package.json` until
16
+ they have completed the migration described below. v2 changes the default
17
+ transport (Unix socket, no TCP), the identity model (kernel-rooted
18
+ fingerprint), the database layout (one DB per fingerprint), and the daemon
19
+ process model (singleton). A blind upgrade will break v1 connection strings.
20
+
21
+ ```jsonc
22
+ // package.json — keep v1 until you migrate
23
+ {
24
+ "dependencies": {
25
+ "pgserve": "^1.2.0"
26
+ }
27
+ }
28
+ ```
29
+
30
+ ### Breaking changes
31
+
32
+ - **TCP is no longer the default.** v1 bound `127.0.0.1:8432` for every
33
+ consumer. v2 binds a Unix control socket at
34
+ `${XDG_RUNTIME_DIR:-/tmp}/pgserve/control.sock` (mode `0600`, dir mode
35
+ `0700`) plus a `.s.PGSQL.5432` symlink so libpq clients connect with no
36
+ host/port/user/password. To keep a TCP listener, opt in explicitly with
37
+ `--listen <port>` (see "Compat TCP via --listen" in the README).
38
+ - **Fingerprint enforcement is default-ON.** Each connecting peer is
39
+ identified via `SO_PEERCRED` + the resolved `package.json` `name`,
40
+ collapsed to a 12-hex fingerprint. The daemon refuses to route a peer
41
+ into a database that does not match its fingerprint with SQLSTATE
42
+ `28P01 invalid_authorization — database fingerprint mismatch`. The
43
+ emergency kill switch is `PGSERVE_DISABLE_FINGERPRINT_ENFORCEMENT=1`
44
+ (deprecated; the daemon emits a stderr warning at boot when the env var
45
+ is observed).
46
+ - **Database-per-fingerprint isolation.** v1 served arbitrary database
47
+ names freely. v2 auto-creates `app_<sanitized-name>_<12hex>` for each
48
+ unique fingerprint on first connect; cross-fingerprint reads are denied.
49
+ `psql -l` will show one row per consumer rather than the shared pool
50
+ v1 produced. Monorepo rule: the root `package.json` `name` wins for all
51
+ packages under it.
52
+ - **Singleton daemon via control socket.** v1 spun up a server per
53
+ invocation, leaving consumers to coordinate ports themselves. v2
54
+ enforces one daemon per host: a second `pgserve daemon` exits with
55
+ `already running, pid N`. Run it under PM2 or systemd (snippets in the
56
+ README) — there is no PM-managed multi-process mode anymore.
57
+ - **GC sweep emits `db_reaped_ttl` and `db_reaped_liveness` audit events.**
58
+ Default lifecycle is now ephemeral: a database whose `liveness_pid` is
59
+ dead AND whose `last_connection_at` is older than 24h is dropped on the
60
+ next sweep (boot, hourly, sampled on-connect). To opt out, add
61
+ `pgserve.persist: true` to the consumer's `package.json` — flagged
62
+ databases are never reaped.
63
+
64
+ ### Migration guide
65
+
66
+ 1. **Connection strings** — drop credentials and the port; switch to the
67
+ socket form.
68
+
69
+ ```diff
70
+ - postgres://user:pass@localhost:5432/db
71
+ + postgres:///db?host=${XDG_RUNTIME_DIR:-/tmp}/pgserve
72
+ ```
73
+
74
+ Equivalently, for `psql`:
75
+
76
+ ```bash
77
+ psql -h "${XDG_RUNTIME_DIR:-/tmp}/pgserve" -d myapp
78
+ ```
79
+
80
+ 2. **Long-lived apps** — anything whose data needs to outlive a 24h idle
81
+ window (genie state stores, dashboards, anything with state worth
82
+ keeping) must declare persistence in its `package.json`:
83
+
84
+ ```jsonc
85
+ {
86
+ "name": "my-long-lived-app",
87
+ "pgserve": { "persist": true }
88
+ }
89
+ ```
90
+
91
+ Without this flag, the GC sweep will reap the database after the TTL
92
+ plus liveness check passes.
93
+
94
+ 3. **Need TCP?** Opt in with `--listen` and use issued tokens. TCP peers
95
+ cannot use `SO_PEERCRED`, so they must authenticate at connect time.
96
+
97
+ ```bash
98
+ pgserve daemon --listen :5432
99
+
100
+ # Issue a bearer token for a known fingerprint (printed once):
101
+ pgserve daemon issue-token --fingerprint <12hex>
102
+
103
+ # TCP clients pass the token via libpq application_name as
104
+ # ?fingerprint=<hex>&token=<bearer>
105
+ # Revoke when done:
106
+ pgserve daemon revoke-token <token-id>
107
+ ```
108
+
109
+ Without `--listen`, no TCP port is bound — verify with
110
+ `ss -tlnp | grep -v pgserve` returning no pgserve rows.
111
+
112
+ 4. **Kill switch (emergency only).** If the fingerprint enforcement
113
+ denies a connection you cannot otherwise unblock, set
114
+ `PGSERVE_DISABLE_FINGERPRINT_ENFORCEMENT=1` for the daemon. The
115
+ bypassed connection emits an `enforcement_kill_switch_used` audit
116
+ event; the daemon logs a deprecation warning at boot whenever the
117
+ variable is observed. The kill switch will be removed in a future
118
+ major; treat it as a debugging tool, not a production setting.
119
+
120
+ ### New features (group references map to wish execution groups)
121
+
122
+ - **Group 4 — Database-per-fingerprint + enforcement + kill switch.**
123
+ Auto-create `app_<name>_<12hex>` on first connect, deny
124
+ cross-fingerprint reads with SQLSTATE `28P01`, audit event
125
+ `connection_denied_fingerprint_mismatch`. Sanitizer collapses
126
+ non-`[a-z0-9]` runs to `_`, lowercases, truncates to 30 chars to keep
127
+ the resulting DB name ≤ 63 chars.
128
+ - **Group 5 — Lifecycle + persist flag + GC sweep.** Three-layer
129
+ lifecycle: liveness (peer pid alive), 24h TTL since last connection,
130
+ and `pgserve.persist: true` override. Sweep runs at daemon boot,
131
+ hourly, and sampled on-connect at 1/N where N = max(1, dbCount/10).
132
+ Reaped databases emit `db_reaped_ttl` or `db_reaped_liveness` audit
133
+ events; the on-connect sweep does not block accept latency past 50 ms
134
+ P99.
135
+ - **Group 6 — `--listen` opt-in TCP + token auth.** Daemon CLI accepts
136
+ `--listen [host:]port` (repeatable). Tokens issued via
137
+ `pgserve daemon issue-token --fingerprint <hex>`, hashed at rest into
138
+ `pgserve_meta.allowed_tokens`, verified with constant-time compare.
139
+ New audit events: `tcp_token_issued`, `tcp_token_used`,
140
+ `tcp_token_denied`. Without `--listen`, no TCP port is bound.
141
+
142
+ ### Compatibility
143
+
144
+ - Node.js >= 18 (unchanged).
145
+ - Linux x64, macOS ARM64/x64, Windows x64. Windows uses named pipes for
146
+ the control socket; PM2/systemd snippets are Linux-first.
147
+ - `--ram` (Linux/WSL2 `/dev/shm`), `--pgvector`, `--sync-to`, and the
148
+ rest of the v1 runtime flags continue to work unchanged.
149
+
150
+ ---