@everystack/server 0.4.26 → 0.4.27

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 ADDED
@@ -0,0 +1,283 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@everystack/server` are documented here.
4
+
5
+ Format based on [Keep a Changelog](https://keepachangelog.com/).
6
+
7
+ ## [0.4.26] - 2026-08-15
8
+
9
+ ### Fixed
10
+
11
+ - route rotation DB ops through the ops Lambda (VPC)
12
+
13
+ ## [0.4.25] - 2026-08-15
14
+
15
+ ### Fixed
16
+
17
+ - resolve all closing-gate findings (F1-F8) on Phase B
18
+ - resolve re-review findings N1-N4
19
+
20
+ ## [0.4.24] - 2026-08-15
21
+
22
+ ### Added
23
+
24
+ - auto-call initCredentials on Lambda cold start
25
+
26
+ ### Changed
27
+
28
+ - read credentials from SST secret store, not individual SSM params
29
+
30
+ ## [0.4.23] - 2026-08-15
31
+
32
+ ### Added
33
+
34
+ - runtime SSM credential resolution for live rotation
35
+ - A/B rotation pair provisioning for credential rotation
36
+ - A/B rotation logic — detect, provision, retire, drain
37
+
38
+ ## [0.4.22] - 2026-08-14
39
+
40
+ ### Added
41
+
42
+ - emit x-everystack-release/runtime/created headers from SSR
43
+
44
+ ### Fixed
45
+
46
+ - clear Metro module registry on SSR bundle swap
47
+
48
+ ## [0.4.21] - 2026-08-14
49
+
50
+ ### Fixed
51
+
52
+ - the destructive-apply gate refuses an image-computed fingerprint
53
+ - close the closing-gate findings on the backup fingerprint path
54
+ - bind the fixes to checks that fail, and correct the retraction order
55
+
56
+ ### Documentation
57
+
58
+ - the operator credential is the ops channel only
59
+ - db:provision cannot split a local developer's credentials
60
+
61
+ ## [0.4.20] - 2026-08-12
62
+
63
+ ### Fixed
64
+
65
+ - the example app compiled its tests with type checking off
66
+
67
+ ## [0.4.19] - 2026-08-05
68
+
69
+ ### BREAKING CHANGES
70
+
71
+ - seed and db:reset are declared, not inferred from a stage name
72
+
73
+ ### Added
74
+
75
+ - a cheat gate for stage-name-as-trust-boundary, and the sweep that found two more
76
+
77
+ ## [0.4.18] - 2026-08-05
78
+
79
+ ### Fixed
80
+
81
+ - a 500 returns a request id, never the error text
82
+
83
+ ## [0.4.17] - 2026-08-04
84
+
85
+ ### Fixed
86
+
87
+ - db:doctor printed two remediations that break what they protect
88
+
89
+ ## [0.4.16] - 2026-08-04
90
+
91
+ ### Added
92
+
93
+ - db:session — N statements, one connection, one transaction
94
+
95
+ ### Fixed
96
+
97
+ - route every introspection through one session, and delete the bare SET
98
+
99
+ ## [0.4.15] - 2026-07-28
100
+
101
+ ### Added
102
+
103
+ - expose WAF/caching as an MCP resource — fix the 403-every-PostgREST trap
104
+
105
+ ### Fixed
106
+
107
+ - TCP keepalives on every long-running pg binary
108
+
109
+ ## [0.4.14] - 2026-07-23
110
+
111
+ ### Added
112
+
113
+ - the bring-your-own-database operator path — direct db:provision + optional migrations
114
+ - sstResource() — read any SST link without importing sst
115
+ - credential-free db:reconcile — the derived-edge ops lane — server 0.4.8 + cli 0.4.22
116
+ - credential-free operator lanes — db:refresh --stage + db:doctor direct venue — server 0.4.9 + cli 0.4.23
117
+ - db:refresh --verify-nonempty — the credential-free dark-panel gate — server 0.4.11 + cli 0.4.25
118
+ - db:operator-url ops action — the --direct/db:swap --stage foundation
119
+ - db:apply --direct — the credential-free unbounded-clock write lane
120
+ - db:backfill --stage — the missing stage lane for data jobs
121
+ - search_path lives on the role, not the URL (the real fix)
122
+ - db:exec — DML-only by semantic catalog digest, no role or superuser
123
+ - db data-movement Phase 2 — ephemeral Fargate Task lane for pg-binary work
124
+ - db data-movement Phase 3a — db:backup/db:export run in the ephemeral Task lane
125
+ - db data-movement Phase 3b foundation — snapshot-verify gate + shared live fingerprint
126
+ - db data-movement Phase 3b — destructive apply gated on a verified backup
127
+ - db data-movement — restore→Fargate + delete the pg-tools layer
128
+
129
+ ### Fixed
130
+
131
+ - drop the `sst` import — read SST links from process.env, unbreak every bundle — 0.4.5
132
+ - resolve SST links delivered via the encrypted key file — 0.4.7
133
+ - db:reconcile --only <ids> — the recovery exit from a mistaken --rebaseline — server 0.4.10 + cli 0.4.24
134
+ - opaque the expo-router loader-data blob so crawlers can't harvest /index URLs
135
+ - db data-movement — Fable build-gate fixes (Task S3 grant + provable fp)
136
+ - full backup excludes the everystack ops schema (restore --clean choked on it)
137
+
138
+ ## [0.4.4] - 2026-07-17
139
+
140
+ ### Fixed
141
+
142
+ - createDb honors an explicit sslmode in the connection URL
143
+
144
+ ## [0.4.3] - 2026-07-16
145
+
146
+ ### Added
147
+
148
+ - canonical-sync artifact verbs (gap B)
149
+
150
+ ## [0.4.2] - 2026-07-16
151
+
152
+ ### Added
153
+
154
+ - pgDumpLayer() declarative layer resource; deprecate PGDUMP_LAYER_ARN
155
+ - zero-config backup-layer guidance — probe/doctor/apply preflight
156
+
157
+ ### Fixed
158
+
159
+ - the flip guidance can no longer dark-site an API — rides server 0.4.0
160
+ - admin role grants are cycle-safe and adoptive — 0.4.1
161
+
162
+ ## [0.4.0] - 2026-07-15
163
+
164
+ ### Added
165
+
166
+ - credential-free staged db:apply
167
+ - ingest pipeline framework (Phase 3, Bricks 1-3)
168
+ - surface pipeline last-run in db:sync/db:plan (Phase 3, Brick 4)
169
+ - credential-free pipeline:run --stage (Phase 3, Brick 5)
170
+ - WAF rate limiting answers 429 + Retry-After, not the default 403
171
+ - live drift + doctor — what the content hashes can't see (Brick B4, read-model-everywhere)
172
+ - built-in _health action; the seed module loads lazily — 0.3.6
173
+ - db:provision provisions BOTH sides of the credential split — 0.3.7
174
+ - forward-port afterRpc + read-replica routing from the 0.2.x line
175
+
176
+ ### Fixed
177
+
178
+ - optional-peer AWS clients fail clean
179
+
180
+ ## [0.3.3] - 2026-07-04
181
+
182
+ ### Added
183
+
184
+ - IAM auth for web deploys, replace Sharp layer with SST bundling
185
+ - rename @everystack/updates → @everystack/cli, add db:migrate/db:seed, remove AdminSecret
186
+ - add StorageAdapter head(), ETag-based SSR reload, SigV4a for KVS
187
+ - add mode: postgres — pgcrypto auth via postgres.js
188
+ - configurable per-origin cache headers + image EXIF fix
189
+ - consolidate drizzle migrations + logging schema updates
190
+ - add afterExtract hook to WebHandlerOptions
191
+ - EAS-compatible OTA updates — branches, groups, channels, SSR
192
+ - add web publish versioning matching mobile pattern
193
+ - add X-Bundle-Key response header to SSR for debugging
194
+ - add optional logSink for structured request logging
195
+ - cdn auth helper — verify JWTs at the edge (no Lambda hop)
196
+ - composeViewerRequest + Feature interface + minifier
197
+ - cookie-based SSR authentication
198
+ - compact CFF verifier + dead code stripping for 10KB budget
199
+ - move hydration fixes into framework, remove auth cache splitting
200
+ - add `everystack diag` command for deployed page freshness diagnostics
201
+ - add plugin architecture for composable Lambda handlers
202
+ - extract ssrPlugin into @everystack/server/plugin
203
+ - extract generic actions into package plugins
204
+ - add auth plugin with layered security and client credentials
205
+ - replace publicPaths with requireAuth pattern
206
+ - add publicMethods to authFeature for anonymous reads at edge
207
+ - add SQS adapter for progressive job dispatch
208
+ - S3 origin cache for processed images
209
+ - HEIC decode fallback via heic-decode, passthrough for unsupported formats
210
+ - content-addressed asset deduplication, fix Metro resolution
211
+ - add EventBridge Scheduler support for cancel/reschedule
212
+ - add createSecrets helper and transparent naming support
213
+ - expose ctx, notifications, and rpc in console eval scope
214
+ - add ActiveRecord-style model layer for console
215
+ - add dot commands and schema introspection
216
+ - add auth context with login, asUser, and pgSettings
217
+ - add sandbox mode for safe exploration
218
+ - add .login with hidden password, current_user in scope, history redaction
219
+ - add content-hash fingerprinting for media assets
220
+ - split operator credential from the API connection
221
+ - add cache-state + timing observability headers to image handler
222
+ - edge request id as traceId + shared timing headers
223
+ - migration runner that cannot lie
224
+ - fail-closed bundle-boundary guard (@everystack/server/testing)
225
+ - web bundle guard (assertWebBundleClean)
226
+ - withRole/roleFor + explicit DATABASE_URL precedence
227
+ - db:doctor — verify the DB is least-privilege + RLS-subject
228
+ - db:provision — the role provisioner + db:doctor remediation
229
+ - least-priv client auth (invoker+RLS) + db:doctor auth.users carve-out
230
+ - make db:doctor FORCE-RLS carve-outs app-configurable
231
+ - ship jest-preset so consumers can import @everystack/server/testing
232
+ - db:authz:test + the db:authz:probe ops action — red-team a deployed stage
233
+ - db:doctor enforces "app-reachable tables require RLS"
234
+ - db:backup B1 — pg_dump layer + version-gate probe
235
+ - db:backup B2 — pg_dump/pg_restore ops actions, streamed to S3
236
+ - db:doctor functions teeth + derive every FORCE carveout
237
+ - db:fork — branch stages fork the database, not just the code (branch-stages brick 1+2)
238
+
239
+ ### Fixed
240
+
241
+ - replace crossfilter2 with plain JS, fix ESM test failures
242
+ - drop immutable from image default — use consistent 60s browser TTL
243
+ - require crypto module in CFF verifiers, strip routeSite
244
+ - skip edge auth for non-API paths when tableFromPath is enabled
245
+ - psql Lambda-only queries, composite key notify, logging defaults
246
+ - include stubs directory in published package
247
+ - resolve 5 deployment issues from first external testing
248
+ - resolve 7 high-priority security findings from red team audit
249
+ - fix sharp mock and S3Client mock for ESM test compatibility
250
+ - resolve auth test failures and logging build error
251
+ - pin all dependency versions to lockfile-resolved values
252
+ - strip _a auth flag from CFF query params
253
+ - upscale source to fill box when fit=cover, passthrough non-image files
254
+ - handle HEIC decode failures, oversized passthrough, uncached errors
255
+ - replace infrastructure headers with single X-Server-Debug timestamp
256
+ - version bumps and publish fixes for cli, server, jobs
257
+ - enforce RLS by default and handle unstringifiable results
258
+ - ops Lambda boots the full app so the console keeps rpc/jobs
259
+ - short edge TTL for SSR HTML so it refreshes within 60s
260
+ - never public-cache cookie-authenticated SSR responses
261
+ - release-unique SSR build dir so warm Lambdas pick up new releases
262
+ - operator connection falls back to master, never the authenticator
263
+ - correct jest-preset usage example in doc comment
264
+ - jest-preset self-pins @everystack/server/testing (cold-cache TS2307)
265
+ - detect HEIC by magic bytes, not Sharp
266
+
267
+ ### Changed
268
+
269
+ - extract server infra into @expo-stack/server + @expo-stack/auth packages
270
+ - migrate all package schemas to everyschema canonical tables
271
+ - consolidate authorization to PostgreSQL as single source of truth
272
+ - extract generic code to packages, remove dead code
273
+ - drop type module from all packages, switch Jest to CJS mode
274
+ - consolidate Jest configs into shared base
275
+ - fold ops handler into createPluginLambdaHandler({ http: false })
276
+
277
+ ### Documentation
278
+
279
+ - rewrite documentation around V1→V2→V3 progressive complexity model
280
+ - prepare documentation for open-source release
281
+ - close remaining security punchlist items (L6, M3)
282
+ - warn that jest-preset owns `transform` (the TS2307 footgun)
283
+ - v3 (0.3.0) documentation review — accurate READMEs + guides
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/server",
3
- "version": "0.4.26",
3
+ "version": "0.4.27",
4
4
  "description": "Server runtime primitives for Lambda — event adapters, routing, SSR, image processing",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "Scalable Technology, Inc. <licensing@scalable.technology>",
@@ -20,7 +20,8 @@
20
20
  "src",
21
21
  "stubs",
22
22
  "jest-preset.js",
23
- "README.md"
23
+ "README.md",
24
+ "CHANGELOG.md"
24
25
  ],
25
26
  "exports": {
26
27
  ".": {
@@ -186,8 +187,8 @@
186
187
  "sst": "4.13.1",
187
188
  "ts-jest": "29.4.9",
188
189
  "typescript": "5.9.3",
189
- "@everystack/cli": "0.4.65",
190
- "@everystack/auth": "0.4.3"
190
+ "@everystack/cli": "0.4.67",
191
+ "@everystack/auth": "0.4.4"
191
192
  },
192
193
  "scripts": {
193
194
  "test": "jest",
package/src/plugin.ts CHANGED
@@ -800,9 +800,9 @@ export function dbPlugin(options: DbPluginOptions): Plugin {
800
800
  // --- db:provision — BOTH sides of the credential split, in the safe order ---
801
801
  // Runs as the operator and creates ONLY roles (no database, no tables, no schema), so
802
802
  // it is safe on an existing database: an app that brings its own DATABASE_URL can run
803
- // this to adopt the least-privilege split without re-provisioning anything. The
804
- // auto-provisioning construct invokes it at deploy time with a generated password.
805
- // With adminPassword, also creates the operator login (migrator) and VERIFIES its
803
+ // this to adopt the least-privilege split without re-provisioning anything. It is
804
+ // ADDITIVE-ONLY: an already-live login role keeps its password (only db:provision --rotate
805
+ // changes an existing one). With adminPassword, also creates the operator login (migrator) and VERIFIES its
806
806
  // real connectivity BEFORE the authenticator password is set — the ordering that
807
807
  // keeps a BYO-database app from sawing off its own ops plane (see provision.ts).
808
808
  actions['db:provision'] = async (payload, _ctx) => {
@@ -829,6 +829,15 @@ export function dbPlugin(options: DbPluginOptions): Plugin {
829
829
  {
830
830
  execute: async (statement) => opsDb.execute(sql.raw(statement)),
831
831
  connection,
832
+ // Additive-only: an already-live login role keeps its password (never reset on re-provision).
833
+ roleHasLogin: async (role) => {
834
+ const res: any = await opsDb.execute(sql`SELECT 1 FROM pg_roles WHERE rolname = ${role} AND rolcanlogin`);
835
+ if (Array.isArray(res)) return res.length > 0;
836
+ if (Array.isArray(res?.rows)) return res.rows.length > 0;
837
+ // Unrecognized driver shape — PRESERVE the credential. Skipping a password is recoverable
838
+ // (the CLI half-done guard catches it); resetting a live one on a false-negative is the lockout.
839
+ return true;
840
+ },
832
841
  connectProbe: async (url) => {
833
842
  const { default: postgres } = await import('postgres');
834
843
  // 'prefer' handles both TLS-required RDS and plain local Postgres.
package/src/provision.ts CHANGED
@@ -48,6 +48,16 @@ export interface ProvisionDeps {
48
48
  connection?: { host: string; port?: number | string; database: string } | null;
49
49
  /** Real login attempt as the admin role — resolves on success, throws on failure. */
50
50
  connectProbe?: (url: string) => Promise<void>;
51
+ /**
52
+ * Does this role ALREADY exist with LOGIN? When it does, provision is ADDITIVE-ONLY:
53
+ * it preserves the live password instead of resetting it. A plain `db:provision` used to
54
+ * `ALTER ROLE … WITH LOGIN PASSWORD` unconditionally, so re-running it on a stage whose
55
+ * secrets already matched rewrote the live password to a fresh value — and if the CLI
56
+ * then failed to write the secret, the stage was locked out (needed the RDS master to
57
+ * recover). Only `--rotate` changes an existing password. Absent → treated as
58
+ * not-existing (the role is being created now, so its password IS set).
59
+ */
60
+ roleHasLogin?: (role: string) => Promise<boolean>;
51
61
  }
52
62
 
53
63
  export interface ProvisionResult {
@@ -89,9 +99,16 @@ export async function runProvision(
89
99
  return { error: err?.message || String(err) };
90
100
  }
91
101
 
102
+ // Existence check BEFORE the role chain runs — the chain CREATEs the roles, so after it
103
+ // every role "exists". A role that already had LOGIN *before* this run is a live credential
104
+ // we must not reset (additive-only); one created now gets its password set.
105
+ const login = loginRole ?? 'authenticator';
106
+ const admin = adminRole ?? 'migrator';
107
+ const authExisted = !!authPassword && !!deps.roleHasLogin && (await deps.roleHasLogin(login));
108
+ const adminExisted = !!adminPassword && !!deps.roleHasLogin && (await deps.roleHasLogin(admin));
109
+
92
110
  await deps.execute(roleSql);
93
111
 
94
- const login = loginRole ?? 'authenticator';
95
112
  const result: ProvisionResult = {
96
113
  provisioned: true,
97
114
  loginRole: login,
@@ -107,35 +124,43 @@ export async function runProvision(
107
124
 
108
125
  // Admin side FIRST — created and verified before the authenticator swap can happen.
109
126
  if (adminPassword && adminSql) {
110
- const admin = adminRole ?? 'migrator';
111
127
  await deps.execute(adminSql);
112
- await deps.execute(`ALTER ROLE ${admin} WITH LOGIN PASSWORD '${adminPassword}';`);
128
+ result.adminRole = admin;
113
129
  // The operator role gets the same role-default search_path — this is the role db:swap/apply/
114
130
  // reconcile --direct connect as, so a clean URL (no ?search_path=) still resolves bare refs.
131
+ // Safe + idempotent even when adopting an existing role.
115
132
  const adminSearchPathSql = searchPath ? generateSearchPathSQL(admin, searchPath) : null;
116
133
  if (adminSearchPathSql) await deps.execute(adminSearchPathSql);
117
- result.adminRole = admin;
118
- result.adminPasswordSet = true;
119
-
120
- if (deps.connection && deps.connectProbe) {
121
- const { host, port, database } = deps.connection;
122
- const url = `postgresql://${admin}:${adminPassword}@${host}:${port ?? 5432}/${database}`;
123
- try {
124
- await deps.connectProbe(url);
125
- result.adminVerified = true;
126
- } catch (err: any) {
127
- // The whole point of the ordering: a broken admin login must abort the swap,
128
- // or a BYO-database app loses its only privileged path.
129
- return {
130
- error:
131
- `admin login verification failed for role '${admin}': ${err?.message || String(err)} — ` +
132
- 'the authenticator password was NOT set and DATABASE_URL must not be rewritten.',
133
- adminRole: admin,
134
- adminVerified: false,
135
- };
136
- }
134
+
135
+ if (adminExisted) {
136
+ // Additive-only: the operator login already exists with a password the app is using.
137
+ // Do NOT reset it, and do NOT probe with a password we never set (--rotate changes it).
138
+ result.adminPasswordSet = false;
139
+ result.adminVerified = null; // unchanged — verify via db:doctor
137
140
  } else {
138
- result.adminVerified = null; // no way to probe — caller should verify via db:doctor
141
+ await deps.execute(`ALTER ROLE ${admin} WITH LOGIN PASSWORD '${adminPassword}';`);
142
+ result.adminPasswordSet = true;
143
+
144
+ if (deps.connection && deps.connectProbe) {
145
+ const { host, port, database } = deps.connection;
146
+ const url = `postgresql://${admin}:${adminPassword}@${host}:${port ?? 5432}/${database}`;
147
+ try {
148
+ await deps.connectProbe(url);
149
+ result.adminVerified = true;
150
+ } catch (err: any) {
151
+ // The whole point of the ordering: a broken admin login must abort the swap,
152
+ // or a BYO-database app loses its only privileged path.
153
+ return {
154
+ error:
155
+ `admin login verification failed for role '${admin}': ${err?.message || String(err)} — ` +
156
+ 'the authenticator password was NOT set and DATABASE_URL must not be rewritten.',
157
+ adminRole: admin,
158
+ adminVerified: false,
159
+ };
160
+ }
161
+ } else {
162
+ result.adminVerified = null; // no way to probe — caller should verify via db:doctor
163
+ }
139
164
  }
140
165
  }
141
166
 
@@ -154,7 +179,6 @@ export async function runProvision(
154
179
  }
155
180
 
156
181
  if (adminSql) {
157
- const admin = adminRole ?? 'migrator';
158
182
  // Admin pairs need owner membership to run DDL (F4 fix)
159
183
  const adminPairSql = generateRotationPairSQL(admin, 'INHERIT NOBYPASSRLS');
160
184
  await deps.execute(adminPairSql);
@@ -176,7 +200,9 @@ export async function runProvision(
176
200
  }
177
201
 
178
202
  // Authenticator LAST — only reached when the admin side (if requested) held up.
179
- if (authPassword) {
203
+ // Additive-only: skip the password when the authenticator already exists as a live login,
204
+ // so a re-provision never rewrites the credential the app is currently using.
205
+ if (authPassword && !authExisted) {
180
206
  await deps.execute(`ALTER ROLE ${login} WITH LOGIN PASSWORD '${authPassword}';`);
181
207
  result.passwordSet = true;
182
208
  }