@everystack/cli 0.4.66 → 0.4.68

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,893 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@everystack/cli` are documented here.
4
+
5
+ Format based on [Keep a Changelog](https://keepachangelog.com/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Fixed
10
+
11
+ - secrets test expected .js extension stripped by Metro compat
12
+ - async RSA key generation — unblocks the event loop in parallel test runs
13
+
14
+ ## [0.4.66] - 2026-08-15
15
+
16
+ ### Fixed
17
+
18
+ - compile owner-scoped `manage` to write policies + add `internal` flag for functions
19
+
20
+ ## [0.4.65] - 2026-08-15
21
+
22
+ ### Fixed
23
+
24
+ - route rotation DB ops through the ops Lambda (VPC)
25
+
26
+ ## [0.4.64] - 2026-08-15
27
+
28
+ ### Fixed
29
+
30
+ - resolve all closing-gate findings (F1-F8) on Phase B
31
+ - resolve re-review findings N1-N4
32
+
33
+ ## [0.4.63] - 2026-08-15
34
+
35
+ ### Changed
36
+
37
+ - read credentials from SST secret store, not individual SSM params
38
+
39
+ ## [0.4.62] - 2026-08-15
40
+
41
+ ### Added
42
+
43
+ - db:provision --rotate — live credential rotation
44
+
45
+ ## [0.4.61] - 2026-08-14
46
+
47
+ ### Added
48
+
49
+ - add rollingRestart primitive — bump EVERYSTACK_GEN to retire warm containers
50
+ - update calls rollingRestart after the pointer flip
51
+ - update verifies origin serves the new release after restart
52
+ - cache:purge refuses unless origin serves the expected release
53
+ - deploy waits for CloudFront distribution to reach Deployed
54
+
55
+ ### Fixed
56
+
57
+ - drop .js extensions from barrel re-exports so Metro resolves them
58
+ - strip .js extensions from all internal imports for Metro compatibility
59
+ - strip .js from dynamic imports too — Metro traces them the same way
60
+
61
+ ## [0.4.60] - 2026-08-14
62
+
63
+ ### Fixed
64
+
65
+ - the task image no longer computes the fingerprint it is judged by
66
+ - verify the task stamped the fingerprint the operator sent
67
+ - close the closing-gate findings on the backup fingerprint path
68
+ - bind the fixes to checks that fail, and correct the retraction order
69
+
70
+ ## [0.4.59] - 2026-08-13
71
+
72
+ ### Fixed
73
+
74
+ - a failed post-apply re-read is its own phase, not bookkeeping
75
+ - the phase messages tell the truth about the memoir and the COMMIT
76
+ - --only scopes the provenance re-key too
77
+ - a pending provenance re-key is visible; --only must not scope it
78
+ - two guards against orphaning a provenance row
79
+ - drop the plan-shape guard, keep the one at the delete site
80
+ - escalate the guard's dollar tag instead of refusing the object
81
+ - never drop provenance for an object that is still live
82
+ - a function whose signature names a relation waits for it
83
+ - the signature is an ordering edge, and quoted type names count
84
+ - db:sync settles in one pass
85
+ - report the two passes as one operation, honestly
86
+
87
+ ### Documentation
88
+
89
+ - record why a legacy identity the source also claims still migrates
90
+ - the re-key line says it is pending, and the docs decode it
91
+
92
+ ## [0.4.58] - 2026-08-12
93
+
94
+ ### Fixed
95
+
96
+ - reconcile recorded provenance before it migrated it
97
+ - the guard's test could not see an unguarded guard
98
+
99
+ ## [0.4.57] - 2026-08-12
100
+
101
+ ### Fixed
102
+
103
+ - link workspace siblings instead of resolving them from the registry
104
+ - a tree we could not read is not evidence about the database
105
+ - pnpm test was green because it skipped, not because it passed
106
+ - an explicit DATABASE_URL that does not answer is a failure, not a skip
107
+ - the softDelete guard asked about visibility, not about DELETE
108
+ - close the gate findings — the rule was still one grant short
109
+ - db:pull invented a policy for every RLS-off table
110
+
111
+ ## [0.4.56] - 2026-08-10
112
+
113
+ ### Added
114
+
115
+ - compile and apply a function's declared owner
116
+ - the authz contract tells a scoped operator from a superuser
117
+ - db:pull declares a chosen function owner, and the builder refuses one it cannot enact
118
+ - the ownership mechanism is chosen from the builder, not assumed
119
+ - a serial column's sequence inherits its table's grants
120
+ - assert the schema posture, and detect who can plant a shadow object
121
+ - keep the index name the database already has
122
+ - keep the foreign key constraint name too
123
+ - the round-trip oracle, and the three defects it immediately found
124
+ - capture GENERATED ALWAYS AS IDENTITY through the round trip
125
+ - a view's owner is a privilege boundary too — declare it, build it
126
+
127
+ ### Fixed
128
+
129
+ - never mint a REVOKE that ownership explains
130
+ - db:pull reads the search_path, it does not invent one
131
+ - close the two defects the round-trip oracle found
132
+ - a same-name index redefinition drops before it creates
133
+
134
+ ## [0.4.55] - 2026-08-10
135
+
136
+ ### Added
137
+
138
+ - db:build — the models produce a database you keep
139
+
140
+ ### Fixed
141
+
142
+ - the diff path emits declared extensions — fresh builds AND stage applies
143
+ - the generated drizzle artifact typechecks under strict
144
+
145
+ ## [0.4.54] - 2026-08-10
146
+
147
+ ### Fixed
148
+
149
+ - a database and its own restored clone are one state
150
+ - fingerprint v10 — normalizing checks and index predicates MOVES the hash
151
+
152
+ ## [0.4.53] - 2026-08-10
153
+
154
+ ### Fixed
155
+
156
+ - predicate identity survives a PostgreSQL major boundary
157
+ - the schema-USAGE phase diffs against the live nspacl instead of emitting blind
158
+ - a multi-schema checkout's models are never reported unmodeled; --stage labeling discoverable from --database-url pulls
159
+
160
+ ## [0.4.52] - 2026-08-08
161
+
162
+ ### Added
163
+
164
+ - compile rls: false to a grants-only contract — and never downgrade on version skew
165
+ - db:pull transcribes rls: false for the live grants-only shape
166
+ - db:pull names the migration-tool tables it skipped, and what to do
167
+ - db:pull --govern-roles — a foreign role's grants survive migration deletion
168
+
169
+ ### Fixed
170
+
171
+ - the two lint gates learn the rls: false posture — live access named as live
172
+
173
+ ### Documentation
174
+
175
+ - db:check stops overclaiming; db:pull's rls: false line documented
176
+
177
+ ## [0.4.51] - 2026-08-07
178
+
179
+ ### Added
180
+
181
+ - db:check fails a model file the barrel never exported
182
+ - db:pull renders extensions, so the declared state can bootstrap itself
183
+ - db:pull adopts a multi-schema database in one pull
184
+
185
+ ### Fixed
186
+
187
+ - db:plan's adoption class never reached the artifact it explains
188
+ - declared functions exist before the policies that call them
189
+
190
+ ## [0.4.50] - 2026-08-07
191
+
192
+ ### Fixed
193
+
194
+ - a recorded grant is reach, and a foreign grantee is never an ability
195
+
196
+ ## [0.4.49] - 2026-08-06
197
+
198
+ ### BREAKING CHANGES
199
+
200
+ - a grant with no policy is spellable, and no longer renders as an ability
201
+ - --key=value silently changed the TARGET, not just the flag
202
+
203
+ ### Added
204
+
205
+ - db:check flags a CRUD grant declared with no policy
206
+ - read a trigger's SHAPE from the catalog, not from its prose
207
+ - a live EXECUTE grant is a fact, so let a function record one
208
+ - db:pull renders trigger bindings, as standalone descriptors
209
+ - record a relation's inexpressible grants instead of skipping the object
210
+
211
+ ### Fixed
212
+
213
+ - an ability needs a grant AND a covering policy — on every branch
214
+ - a UNION typed the function identity as `name`, truncating ACLs at 63 bytes
215
+ - a trigger function has no caller to declare, so stop demanding one
216
+
217
+ ### Documentation
218
+
219
+ - the trigger API is trigger(), not defineTrigger
220
+
221
+ ## [0.4.48] - 2026-08-04
222
+
223
+ ### Added
224
+
225
+ - declare the SECURITY DEFINER functions, so db:reconcile can see them
226
+ - writtenBy: 'functions' must have functions, checked at composition
227
+
228
+ ## [0.4.47] - 2026-08-04
229
+
230
+ ### Fixed
231
+
232
+ - the direct lane for db:plan and db:refresh, and stop recommending --database-url against a deployed stage
233
+ - db:exec's DML-only guard was blind to four things it advertised catching
234
+
235
+ ## [0.4.46] - 2026-08-04
236
+
237
+ ### Added
238
+
239
+ - policyRoles — the policy's audience, not its grantees
240
+ - field.pgType — a verbatim type is its own identity
241
+ - make column grants first-class
242
+ - db:session — N statements, one connection, one transaction
243
+
244
+ ### Fixed
245
+
246
+ - a replacement is never read as equivalent
247
+ - name what the plan could not classify, and stop crying wolf
248
+ - a redundant default precision is not a type change
249
+ - wire the governed filter into every declared-vs-live comparison
250
+ - a column-scoped UPDATE is not SELECT reach
251
+ - a dead policy leaves the identity, not the record
252
+ - a subsumed policy is not part of the state
253
+ - one definition of a public read, and writtenBy stops guessing
254
+ - the stage lane reads twice, and never calls that a verification
255
+ - route every introspection through one session, and delete the bare SET
256
+
257
+ ## [0.4.45] - 2026-08-02
258
+
259
+ ### BREAKING CHANGES
260
+
261
+ - fingerprint v4 — one equivalence relation, three surfaces
262
+
263
+ ### Added
264
+
265
+ - policy identity by rule — the shared matcher
266
+ - classify every authz statement by why it exists
267
+ - recognise the role split in the shared matcher
268
+ - a fingerprint format change refuses, it does not report drift
269
+ - the model can say REFERENCES, TRIGGER, TRUNCATE
270
+ - the pull says what it did not capture, in the file you review
271
+ - the owner is named, because the contract cannot carry it
272
+
273
+ ### Fixed
274
+
275
+ - a migration tool's journal is not the app's model
276
+ - PUBLIC is one grantee, not two spellings
277
+ - db:pull renders a live TO PUBLIC read as PUBLIC
278
+ - a value that looks like a flag is still a value
279
+
280
+ ## [0.4.44] - 2026-08-02
281
+
282
+ ### Fixed
283
+
284
+ - db:pull --derived-out wrote a derived layer the barrel then excluded
285
+ - a public read and an owner read are two branches, not a contest
286
+ - db:pull dropped the owner branch of a `public OR mine` read
287
+
288
+ ## [0.4.43] - 2026-08-01
289
+
290
+ ### Fixed
291
+
292
+ - a role-scoped read compiled to a grant with no policy, and an omitted WITH CHECK is not a change
293
+ - soft-delete visibility is declared, never inferred from a column name
294
+ - a predicate is parenthesized exactly once, so a pulled policy round-trips
295
+ - a plan's `additive` count was a default, not a finding — and `→ text` was never free
296
+ - db:plan says "do NOT commit" — now it checks whether git agrees
297
+
298
+ ## [0.4.42] - 2026-08-01
299
+
300
+ ### Added
301
+
302
+ - a role the models cannot name keeps its grants, and is named on every run
303
+ - the adoption baseline — a foreign role that was not there at adoption refuses the plan
304
+ - stripping an undeclared grantee is destructive, and db:check verifies the baseline artifact (cli 0.4.41, model 0.4.7)
305
+
306
+ ### Fixed
307
+
308
+ - array-literal quoting, an unvouchable role, a cold build, and a source file grep was skipping
309
+
310
+ ## [0.4.40] - 2026-07-31
311
+
312
+ ### Added
313
+
314
+ - db:authz gets a local venue, and the published bin actually runs
315
+ - the authz vocabulary compiles what it declares
316
+ - db:pull --abilities live — the authz half of the brownfield on-ramp
317
+ - state the operator role once, and two messages that cost an adopter time
318
+ - a function is identified by its name AND its argument types
319
+
320
+ ### Fixed
321
+
322
+ - the red team stops crying wolf — two causes of phantom holes
323
+ - db:fingerprint refuses a barrel it cannot load
324
+ - db:pull read an unnamed multi-word argument as an argument name
325
+ - a killed test run no longer poisons the next one, and a numeric default survives the round trip
326
+
327
+ ### Documentation
328
+
329
+ - scrub a consumer role name from the inheritance docblock
330
+
331
+ ## [0.4.37] - 2026-07-29
332
+
333
+ ### Fixed
334
+
335
+ - db:swap stops fighting its own backup, and its snapshot is now a fact
336
+
337
+ ## [0.4.36] - 2026-07-29
338
+
339
+ ### Added
340
+
341
+ - db:swap pairs the derived layer — zero downtime, no refresh
342
+ - db:swap --dump-build — the instrument for an unreproducible field bug
343
+
344
+ ### Fixed
345
+
346
+ - db:reconcile can rebuild a view backing a function's return type
347
+ - db:swap re-grants schema USAGE — a successful swap dark-sited the app
348
+ - db:swap refuses a PARTIAL derived layer instead of reporting success
349
+ - the paired build sets a search_path — BARE refs could not resolve
350
+ - a paired swap records provenance — the next reconcile was rebuilding everything
351
+ - the schema rewrite must not touch string literals — it renamed JSON keys
352
+ - the mutation-lease refusal names a holder in ANOTHER database
353
+ - "provenance recorded" could print having recorded nothing
354
+ - db:swap will not take its target from DATABASE_URL — --stage was silently overridden
355
+
356
+ ## [0.4.35] - 2026-07-28
357
+
358
+ ### Added
359
+
360
+ - db:backup local upload venue — break-glass local→stage full replace (0.4.33)
361
+ - db:reconcile applies an authz-only change as a regrant, not a matview rebuild (0.4.34)
362
+ - db:swap --rebuild-derived — swap the schema, regenerate the derived layer
363
+ - the restore heartbeat names WHO holds the lock
364
+
365
+ ### Fixed
366
+
367
+ - schema-qualify every derived CREATE so public objects land in public (0.4.34)
368
+ - db:provision writes CAPS secret names only (DATABASE_URL/ADMIN_DATABASE_URL)
369
+ - instrument db:swap's restore leg with a lock-free liveness probe
370
+ - TCP keepalives on every long-running pg binary
371
+ - db:swap refuses instead of silently destroying cross-schema dependents
372
+ - scope the restore heartbeat's COPY counter to our own load
373
+ - walk the swap's dependency gate transitively, not just direct dependents
374
+ - the dependent-drop notice is future tense — it announced drops that had not happened
375
+ - a swap artifact carries base state, not the derived layer
376
+ - the restore heartbeat counts schema growth as progress
377
+
378
+ ## [0.4.32] - 2026-07-23
379
+
380
+ ### Added
381
+
382
+ - db schema-move primitive — movedFrom marker → ALTER TABLE SET SCHEMA
383
+
384
+ ## [0.4.31] - 2026-07-23
385
+
386
+ ### Added
387
+
388
+ - the bring-your-own-database operator path — direct db:provision + optional migrations
389
+ - db:provision keeps master off argv and bakes search_path into the URL — 0.4.21
390
+ - credential-free db:reconcile — the derived-edge ops lane — server 0.4.8 + cli 0.4.22
391
+ - credential-free operator lanes — db:refresh --stage + db:doctor direct venue — server 0.4.9 + cli 0.4.23
392
+ - db:refresh --verify-nonempty — the credential-free dark-panel gate — server 0.4.11 + cli 0.4.25
393
+ - mutation-lease core — the fixed-key advisory session lock
394
+ - db:apply --direct — the credential-free unbounded-clock write lane
395
+ - db:reconcile --direct — the unbounded-clock derived-rebuild lane
396
+ - db:backfill --stage — the missing stage lane for data jobs
397
+ - db:swap --stage --direct — the credential-free hot-swap capstone
398
+ - search_path lives on the role, not the URL (the real fix)
399
+ - db:exec spine — crash-truthful audit + single-transaction invariant
400
+ - db:exec — DML-only by semantic catalog digest, no role or superuser
401
+ - db data-movement Phase 2 — ephemeral Fargate Task lane for pg-binary work
402
+ - db data-movement Phase 3a — db:backup/db:export run in the ephemeral Task lane
403
+ - db data-movement Phase 3b foundation — snapshot-verify gate + shared live fingerprint
404
+ - db data-movement Phase 3b — destructive apply gated on a verified backup
405
+ - db data-movement — restore→Fargate + delete the pg-tools layer
406
+
407
+ ### Fixed
408
+
409
+ - missing optional aws-sdk clients fail with the install remedy, not an IAM red herring
410
+ - update resolves runtimeVersion policies — no more [object Object] release keys
411
+ - db:provision echoes the target database; db:doctor names the no-Ops cause
412
+ - db:reconcile --only <ids> — the recovery exit from a mistaken --rebaseline — server 0.4.10 + cli 0.4.24
413
+ - db:provision preserves connection params (sslmode) when minting the URL — cli 0.4.26
414
+ - --stage discovery carries backupsBucket — db:backup:download --stage works — cli 0.4.27
415
+ - db:swap --direct connects via PG* env, not a search_path URL — libpq rejects it
416
+ - db:swap re-applies authz idempotently — pg_dump carries RLS policies, so CREATE POLICY collided and rolled the swap back
417
+ - db data-movement — Fable build-gate fixes (Task S3 grant + provable fp)
418
+
419
+ ## [0.4.18] - 2026-07-17
420
+
421
+ ### Fixed
422
+
423
+ - db:swap integrity error reports the real failing statement, not an FK guess
424
+ - derived apply creates the schemas its objects live in
425
+
426
+ ## [0.4.17] - 2026-07-17
427
+
428
+ ### Added
429
+
430
+ - field.json() — the Postgres json type, distinct from jsonb
431
+
432
+ ## [0.4.16] - 2026-07-17
433
+
434
+ ### Fixed
435
+
436
+ - --matviews-as-tables renders the MODEL naming idiom (bare name + schema prop)
437
+
438
+ ## [0.4.15] - 2026-07-17
439
+
440
+ ### Added
441
+
442
+ - db:check split-brain gate + db:pull --suggest-keys + the flip doc
443
+
444
+ ## [0.4.14] - 2026-07-17
445
+
446
+ ### Added
447
+
448
+ - db:pull --matviews-as-tables — render matviews as defineMaterializedTable
449
+
450
+ ## [0.4.13] - 2026-07-17
451
+
452
+ ### Added
453
+
454
+ - db:export local venue (--database-url) — the build-locally on-ramp
455
+
456
+ ## [0.4.12] - 2026-07-16
457
+
458
+ ### Added
459
+
460
+ - canonical-sync artifact verbs (gap B)
461
+
462
+ ## [0.4.11] - 2026-07-16
463
+
464
+ ### Fixed
465
+
466
+ - introspect deparsed expressions under a canonical search_path
467
+
468
+ ## [0.4.10] - 2026-07-16
469
+
470
+ ### Fixed
471
+
472
+ - db:pull renders array-column defaults as JS arrays
473
+
474
+ ## [0.4.9] - 2026-07-16
475
+
476
+ ### Fixed
477
+
478
+ - multi-schema derived apply sets search_path so bare body refs resolve
479
+
480
+ ## [0.4.8] - 2026-07-16
481
+
482
+ ### Added
483
+
484
+ - zero-config backup-layer guidance — probe/doctor/apply preflight
485
+
486
+ ## [0.4.7] - 2026-07-16
487
+
488
+ ### Added
489
+
490
+ - db:provision announces what it touched and writes linkable keys — 0.4.5
491
+ - the deploy probe reads a table THROUGH the api function — cli 0.4.6
492
+
493
+ ### Fixed
494
+
495
+ - retire db:migrate from every advertisement; deploy env pinned + visible
496
+ - db:snapshot --stage can finally see databaseInstanceId
497
+
498
+ ## [0.4.4] - 2026-07-14
499
+
500
+ ### Fixed
501
+
502
+ - owner write abilities no longer narrow a public read
503
+ - derived-compile tolerates descriptors without a triggers key
504
+
505
+ ## [0.4.3] - 2026-07-14
506
+
507
+ ### Added
508
+
509
+ - post-deploy probe — deploy fails loudly when a function cannot boot
510
+
511
+ ### Fixed
512
+
513
+ - the observability verbs can see a dead ops plane
514
+
515
+ ## [0.4.2] - 2026-07-10
516
+
517
+ ### Added
518
+
519
+ - the loud undefined gate — a missing import is named at define time, never silently dropped
520
+ - summary-first plans — the shape of the edge prints before the SQL wall
521
+ - db:pull --derived-out <file> — the brownfield splice, first-class
522
+
523
+ ### Fixed
524
+
525
+ - bimodal error advice — a model-compose error is never dressed as an IAM problem
526
+
527
+ ## [0.4.0] - 2026-07-10
528
+
529
+ ### Added
530
+
531
+ - credential-free staged db:apply
532
+ - ingest pipeline framework (Phase 3, Bricks 1-3)
533
+ - surface pipeline last-run in db:sync/db:plan (Phase 3, Brick 4)
534
+ - credential-free pipeline:run --stage (Phase 3, Brick 5)
535
+ - db:pull scaffolds the read model — the explicit on-ramp (Brick A)
536
+ - field.defaultSql() — the raw-default escape hatch (Brick C)
537
+ - the declarative derived layer — descriptor API + rejection rules (Brick B1a)
538
+ - descriptors compile to SourceObject — the reconciler front-end swap (Brick B1b)
539
+ - reconciler edges for the descriptor kinds — triggers + gate-tight escape hatch (Brick B1c)
540
+ - standalone sequences are state — declared, migrated, diffed, fingerprinted (Brick B1d)
541
+ - descriptors go live in the verbs — the B3 wiring (read-model-everywhere)
542
+ - the B2 gates — the read model is enforced, not just expressible (read-model-everywhere)
543
+ - live drift + doctor — what the content hashes can't see (Brick B4, read-model-everywhere)
544
+ - index identity sees everything CREATE INDEX can say (Brick E, read-model-everywhere)
545
+ - db:pull renders the derived layer — the consumer's migration tool (Brick B5, read-model-everywhere)
546
+ - typed reads — declared fields emit the derived layer into the generated schema (Brick B6a, read-model-everywhere)
547
+ - retire db/sql — descriptors are the single home (Brick B7, read-model-everywhere)
548
+ - db:generate --dry-run + the journal remembers schema-out (Brick D, read-model-everywhere)
549
+ - db:reconcile --rebaseline — the defHash-verified migration for source re-renders (red-team blocker, read-model-everywhere)
550
+
551
+ ### Fixed
552
+
553
+ - adoption-path fixes from the red team (read-model-everywhere)
554
+ - reconcile apply hardening — session guard, real CONCURRENTLY fallback (red team, read-model-everywhere)
555
+ - reachability gate mirrors the grant compiler — private models reach their granted roles (red team, read-model-everywhere)
556
+ - the full workspace suite is green — five latent rots, all pre-existing, root-caused
557
+
558
+ ## [0.3.29] - 2026-07-05
559
+
560
+ ### Added
561
+
562
+ - db:check gates force-RLS with no read authz
563
+ - db:reconcile --rebuild for first-contact objects (gap 1b)
564
+
565
+ ### Fixed
566
+
567
+ - deploy busts the stage cache; add --refresh
568
+ - optional-peer AWS clients fail clean
569
+ - db:backup pre-flights the pg_dump layer probe
570
+ - surface the real cause of an ops-handler crash
571
+ - reconcile + direct-connect operator gaps
572
+ - reconcile --apply is atomic across DDL + provenance (gap 7)
573
+ - db:reconcile/apply resolve ADMIN_DATABASE_URL, not the authenticator (gap 3)
574
+
575
+ ## [0.3.28] - 2026-07-04
576
+
577
+ ### Fixed
578
+
579
+ - fingerprint hashes expressions as the diff does, so db:check can pass
580
+
581
+ ## [0.3.27] - 2026-07-04
582
+
583
+ ### Fixed
584
+
585
+ - the gates and generators teach the migration-authority workflow
586
+
587
+ ## [0.3.26] - 2026-07-04
588
+
589
+ ### Added
590
+
591
+ - db:fork — branch stages fork the database, not just the code (branch-stages brick 1+2)
592
+
593
+ ## [0.3.25] - 2026-07-04
594
+
595
+ ### Added
596
+
597
+ - per-branch dev databases from a models-built template (brick 5 remainder)
598
+
599
+ ## [0.3.24] - 2026-07-04
600
+
601
+ ### Added
602
+
603
+ - the backfill lane — one-shot data jobs, own record (brick 8)
604
+
605
+ ## [0.3.23] - 2026-07-04
606
+
607
+ ### Added
608
+
609
+ - losing data should be hard — classification, deprecated(), role-gated destructive (brick 9)
610
+
611
+ ## [0.3.22] - 2026-07-04
612
+
613
+ ### Added
614
+
615
+ - git-descent rule + db:check — the protected stage is a git ref (brick 7)
616
+
617
+ ## [0.3.21] - 2026-07-04
618
+
619
+ ### Added
620
+
621
+ - table-level renamedFrom — rename intent through the diff (brick 6)
622
+
623
+ ## [0.3.20] - 2026-07-04
624
+
625
+ ### Added
626
+
627
+ - db:plan + db:apply — verified edges, fingerprint-pinned at both ends (brick 4)
628
+
629
+ ## [0.3.19] - 2026-07-04
630
+
631
+ ### Added
632
+
633
+ - db:diff — the state edge between two declared states, no database (brick 3)
634
+
635
+ ## [0.3.18] - 2026-07-03
636
+
637
+ ### Added
638
+
639
+ - db:sync — make the database match your checkout (brick 5 core)
640
+
641
+ ## [0.3.17] - 2026-07-03
642
+
643
+ ### Added
644
+
645
+ - derived-source — parse db/sql into derived objects (reconciler brick 1a)
646
+ - derived introspection + reconcile planner (reconciler bricks 1b/1c)
647
+ - db:reconcile — deploy the derived layer (reconciler bricks 1d/1e)
648
+ - db/models is the models home — flagless resolution, legacy fallback
649
+ - db:generate --apply — table changes with no tape (0.3.16)
650
+ - schema fingerprint + db:fingerprint — the base state gets a content address (brick 2)
651
+
652
+ ### Fixed
653
+
654
+ - WITH NO DATA matview creates get a REFRESH — rebuilds arrive populated (0.3.15)
655
+
656
+ ## [0.3.13] - 2026-07-02
657
+
658
+ ### Added
659
+
660
+ - runbook detects instead of asserting — migration-mode tri-state, real scripts, model summary at scale
661
+
662
+ ## [0.3.12] - 2026-07-02
663
+
664
+ ### Added
665
+
666
+ - db:pull and db:generate accept --database-url; cli 0.3.9
667
+ - db:pull per-model files; serial and float round-trip; cli 0.3.10
668
+ - runbook R0 — detectProjectReality, pure local project detection
669
+ - runbook R1 — es:gen/es:slot markers, section templates, idempotent merge
670
+ - runbook R2 — render the app's Models as the runbook data-model chapter
671
+ - runbook R3 — everystack runbook command, --check/--diff/--force, /runbook API, capstone advisory
672
+
673
+ ### Fixed
674
+
675
+ - guard npm publish on workspace-dep packages; cli 0.3.8
676
+ - db:pull field keys are lossless; array columns map; cli 0.3.11
677
+
678
+ ## [0.3.7] - 2026-07-02
679
+
680
+ ### Fixed
681
+
682
+ - honor EVERYSTACK_SURFACE as an explicit surface override
683
+ - pin React to a single 19.2.0 across the monorepo
684
+ - bound the S3 client and list() so a hung call can't drain the pool
685
+
686
+ ## [0.3.6] - 2026-06-30
687
+
688
+ ### Changed
689
+
690
+ - keep @everystack/cli/audit typescript-free
691
+
692
+ ## [0.3.5] - 2026-06-30
693
+
694
+ ### Added
695
+
696
+ - ui-bloat scanner — detect design bloat in a shipped bundle (Brick 5a-A)
697
+ - component-model — the faithful TSX foundation (Brick 5a-B)
698
+ - source-side UI-bloat checks — located, fixable, self-verifying (Brick 5a-C)
699
+ - Check 3 — you rebuilt a ui primitive (Brick 5a-D)
700
+ - Checks 4 & 5 — size/complexity + structural duplication (Brick 5a-E)
701
+ - wire ui:audit + the bundle ui-bloat dimension (Brick 5a-F)
702
+ - screen-aware audit + duplicate-component check (Brick 5b, first piece)
703
+
704
+ ## [0.3.4] - 2026-06-30
705
+
706
+ ### Added
707
+
708
+ - B2 owner-aware red-team probe — catches the IDOR class
709
+ - B2.2 db:authz:owner — Model-driven owner-isolation red-team
710
+ - @everystack/cli/model public API + dogfood the example through it
711
+ - everystack deploy — wrap sst deploy so the consumer never types sst
712
+ - db:snapshot/restore S0 — pure core (key scheme + cross-stage guards)
713
+ - db:snapshot — physical RDS snapshot (control-plane wrapper)
714
+ - db:backup B1 — pg_dump layer + version-gate probe
715
+ - db:backup B3 — backup/list/restore/download commands
716
+ - via: transitive-owner RLS — own a child through its parent
717
+ - compileDrizzleSource — emit the typed drizzle schema from models
718
+ - precise authz grants — derive the tier from abilities, not a uniform constant
719
+ - writtenBy + the FORCE axiom — RLS posture is a model property
720
+ - per-model schema — a model declares the Postgres schema it lives in
721
+ - greenfield generator emits non-public schemas
722
+ - compose storage's dist.blobs into the example — package-owned authz proven
723
+ - own the notification_* tables as Models + array vocabulary
724
+ - column-scoped grants + cross-schema FK / index emission
725
+ - compileModuleMigration + schema USAGE grants + db:generate --init
726
+ - config-free perf/security/SEO audit commands for any deployed app
727
+ - fold security:audit + lighthouse into the audit capstone
728
+ - bundle:audit gains a weight/size dimension (the 180MB gate)
729
+ - bundle:audit weighs the remote bundle and names what is bloating it
730
+
731
+ ### Fixed
732
+
733
+ - db:snapshot error hints only mention IAM on access-denied
734
+ - escape the NUL delimiters in schema-source (text-safe source)
735
+ - db:authz:owner treats a public-read table's cross-owner read as expected
736
+
737
+ ### Changed
738
+
739
+ - db backup/snapshot spec + pure cores (rename snapshot→backup)
740
+ - compose v3 Modules → one clean generated migration
741
+
742
+ ### Documentation
743
+
744
+ - v3 (0.3.0) documentation review — accurate READMEs + guides
745
+ - complete the command reference — db:reset, branches, security:audit
746
+
747
+ ## [0.3.0] - 2026-06-27
748
+
749
+ ### Added
750
+
751
+ - security:audit — the two-surface RLS gate
752
+ - db:doctor — verify the DB is least-privilege + RLS-subject
753
+ - db:provision — the role provisioner + db:doctor remediation
754
+ - break-glass admin db:psql gated on AWS identity
755
+ - authz-contract format + the two new introspection queries
756
+ - db:authz:pull + db:authz:diff, end-to-end on the example app
757
+ - exclude drizzle journal schema + guard the pull/diff workflow
758
+ - the authz red-team — prove enforcement + capture column grants
759
+ - db:authz:test + the db:authz:probe ops action — red-team a deployed stage
760
+ - human-readable authorization review (the CTO can confirm intent)
761
+ - humanize parent-ownership + boolean predicates in the review
762
+ - capture SECDEF owner-bypasses-RLS — the max-blast-radius check
763
+ - SECDEF grant-completeness check in db:authz:test
764
+ - compile Model abilities to the authz contract (v3 Phase 1)
765
+ - compile owner-read + text-owner tables; four app tables round-trip (v3 Phase 1)
766
+ - emitReconcileSql — contract diff to authz SQL (v3 Phase 2)
767
+ - field bridge DDL compiler — fields to CREATE TABLE (v3 data slice)
768
+ - field bridge — CHECK constraints from .validate() Zod refinements
769
+ - compileMigration — Models to one ordered migration (the generator skeleton)
770
+ - whole-DB introspection — the data layer (columns, constraints, FKs)
771
+ - compileTableSchema — data layer's second producer + round-trip
772
+ - diffSchema — the structural schema diff (brownfield generate's missing half)
773
+ - renamedFrom — the opt-in column rename, so the diff preserves data
774
+ - ALTER COLUMN TYPE — real type-change DDL, classified safe/lossy/risky
775
+ - db:generate — the brownfield migration generator, wired end to end
776
+ - db:pull — render field() Models from a live database (the brownfield on-ramp)
777
+ - P0 round-trip identity test against real Postgres
778
+ - P1 — non-destructive by default in db:generate
779
+ - P3 field vocabulary — numeric, varchar, date
780
+ - P3 enum — CREATE TYPE, pg_enum introspection, round-trip
781
+ - P4a FK referential actions + tougher constraint parser
782
+ - P2.1 table-level constraints — composite unique, multi-col check
783
+ - P2.2 indexes — index() builder, round-trip
784
+ - P2.3 db:pull reverses a recognizable CHECK to .validate()
785
+ - P5 apply-time warnings for SET NOT NULL and ADD COLUMN NOT NULL
786
+ - P4b compile/emit/pull for composite foreign keys
787
+ - P6 unify authz into db:generate (one ordered migration)
788
+
789
+ ### Fixed
790
+
791
+ - dedup db:logs:tail so it stops reprinting the boundary batch
792
+ - re-pin search_path on the four core SECDEF auth functions
793
+ - render the authz review legend as a list, not one long line
794
+ - P6 quote reserved table names in emitted authz SQL
795
+
796
+ ## [0.2.37] - 2026-06-17
797
+
798
+ ### Added
799
+
800
+ - rename @everystack/updates → @everystack/cli, add db:migrate/db:seed, remove AdminSecret
801
+ - add interactive console command with Lambda-relay REPL
802
+ - add CloudFront KVS cache versioning with CLI purge and auto-invalidation
803
+ - add StorageAdapter head(), ETag-based SSR reload, SigV4a for KVS
804
+ - configurable per-origin cache headers + image EXIF fix
805
+ - stage-aware resource discovery via AWS APIs
806
+ - EAS-compatible OTA updates — branches, groups, channels, SSR
807
+ - make S3 source of truth, database becomes optional mirror
808
+ - add web publish versioning matching mobile pattern
809
+ - export handleRegisterWeb and UpdatesHandlerOptions from ./handler
810
+ - use X.509 code signing certificates instead of raw RSA keys
811
+ - add db:reset action for dev schema rebuild
812
+ - mobile publish via direct S3 upload + IAM Lambda invoke
813
+ - move hydration fixes into framework, remove auth cache splitting
814
+ - add `everystack diag` command for deployed page freshness diagnostics
815
+ - add SSR hydration debugging tools
816
+ - add browser-only API detection to SSR analyzer
817
+ - add psql wrapper and logs commands for debugging
818
+ - unified querying with S3 fallback for CLI log commands
819
+ - add plugin architecture for composable Lambda handlers
820
+ - extract generic actions into package plugins
821
+ - add --origin flag to logs:tail for image and worker functions
822
+ - add [updates] debug logging to OTA manifest, asset, and route handlers
823
+ - dual-export OTA updates, SSR validation, eas.json env resolution
824
+ - secrets management and auto-injection during update
825
+ - re-add SST secret injection, delegate filtering to app layer
826
+ - content-addressed asset deduplication, fix Metro resolution
827
+ - add secrets import/export for bulk .env operations
828
+ - add deny-by-default env filtering for client bundles
829
+ - add createSecrets helper and transparent naming support
830
+ - sync media assets to S3 during web publish
831
+ - add multiline input and persistent history to console
832
+ - add dot commands and schema introspection
833
+ - add tab completion to console REPL
834
+ - add auth context with login, asUser, and pgSettings
835
+ - add sandbox mode for safe exploration
836
+ - add .login with hidden password, current_user in scope, history redaction
837
+ - add content-hash fingerprinting for media assets
838
+ - log media asset paths and fingerprints during update
839
+ - fail-closed env loader
840
+ - split operator credential from the API connection
841
+ - everystack status — platform health from the terminal
842
+ - migration runner that cannot lie
843
+ - DB-free alerting — artifact-backed evaluation over S3, no Postgres
844
+ - IAM auth for web deploys, replace Sharp layer with SST bundling
845
+
846
+ ### Fixed
847
+
848
+ - normalize cache:purge --path to strip query params and ensure leading slash
849
+ - remove unused @everystack/jobs devDependency
850
+ - include updateId in register-web payload
851
+ - psql Lambda-only queries, composite key notify, logging defaults
852
+ - add AWS KVS + SigV4a as direct dependencies
853
+ - improve error messages and document setup requirements
854
+ - resolve 5 deployment issues from first external testing
855
+ - replace require() with ESM-compatible dynamic imports
856
+ - pin all dependency versions to lockfile-resolved values
857
+ - pgSettings on RPC, publicRpc client auth bypass, logs:tail log group resolution
858
+ - route GET / to manifest handler for expo-updates client
859
+ - propagate --stage to ENVIRONMENT and HOST_URL before config evaluation
860
+ - redirect OTA assets via presigned S3 URL, set publish context
861
+ - surface real error when app.config.js fails to load
862
+ - decouple --stage from ENVIRONMENT, add --environment flag
863
+ - revert @aws-sdk/signature-v4a to 3.1031.0, fix KVS SigV4a signing
864
+ - bypass SigV4a singleton for KVS signing
865
+ - drop GetFunctionConfiguration from log group resolution
866
+ - detect export completion to prevent static pre-rendering hang
867
+ - rewrite secrets for SST Ion, filter EXPO_PUBLIC_* only
868
+ - remove secret auto-injection, skip SSR check for web-only updates
869
+ - clear Metro transform cache on export
870
+ - worker function discovery and log group resolution for SQS subscribers
871
+ - replace infrastructure headers with single X-Server-Debug timestamp
872
+ - version bumps and publish fixes for cli, server, jobs
873
+ - invalidate stale config cache on schema change
874
+ - fall back to outputs.json for custom-named media buckets
875
+ - remove media asset pruning from update command
876
+ - media-sync test uses same minimatch interop as source
877
+ - discover the Router distribution by domain and SST tags
878
+ - use minimatch's named export — the package ships types now
879
+ - logs read S3 directly; invoke self-heals stale Lambda hash
880
+ - harden S3 reader and lock the CLI-reader format contract
881
+
882
+ ### Changed
883
+
884
+ - remove onMutation callback — events replaces it
885
+ - migrate all package schemas to everyschema canonical tables
886
+ - lazy-load AWS SDK imports for faster CLI startup
887
+ - drop type module from all packages, switch Jest to CJS mode
888
+ - consolidate Jest configs into shared base
889
+
890
+ ### Documentation
891
+
892
+ - prepare documentation for open-source release
893
+ - SECURITY DEFINER guidance; feat(cli): auto-export on stale dist/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/cli",
3
- "version": "0.4.66",
3
+ "version": "0.4.68",
4
4
  "description": "CLI and OTA updates for Expo apps on everystack",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "Scalable Technology, Inc. <licensing@scalable.technology>",
@@ -19,7 +19,8 @@
19
19
  "files": [
20
20
  "bin",
21
21
  "src",
22
- "README.md"
22
+ "README.md",
23
+ "CHANGELOG.md"
23
24
  ],
24
25
  "exports": {
25
26
  ".": {
@@ -177,7 +178,7 @@
177
178
  "jest": "29.7.0",
178
179
  "react": "19.2.0",
179
180
  "ts-jest": "29.4.9",
180
- "@everystack/server": "0.4.26"
181
+ "@everystack/server": "0.4.27"
181
182
  },
182
183
  "scripts": {
183
184
  "test": "jest",
@@ -1,8 +1,11 @@
1
+ import { generateKeyPair as cryptoGenerateKeyPair } from 'node:crypto';
2
+ import { promisify } from 'node:util';
1
3
  import fs from 'fs/promises';
2
4
  import path from 'path';
3
5
  import forge from 'node-forge';
4
6
 
5
7
  const { pki, md, random, util } = forge;
8
+ const generateKeyPairAsync = promisify(cryptoGenerateKeyPair);
6
9
 
7
10
  export async function certsCommand(action: 'generate' | 'configure', flags: Record<string, string>): Promise<void> {
8
11
  if (action === 'generate') {
@@ -26,8 +29,16 @@ async function generateCerts(flags: Record<string, string>): Promise<void> {
26
29
  const validityYears = parseInt(flags.years || '10', 10);
27
30
  await fs.mkdir(outputDir, { recursive: true });
28
31
 
29
- // Generate RSA key pair using node-forge
30
- const keyPair = pki.rsa.generateKeyPair(2048);
32
+ // Generate RSA key pair with Node.js crypto (async, native — doesn't block the event loop)
33
+ const { publicKey: pubPem, privateKey: privPem } = await generateKeyPairAsync('rsa', {
34
+ modulusLength: 2048,
35
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
36
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
37
+ });
38
+ const keyPair = {
39
+ publicKey: pki.publicKeyFromPem(pubPem),
40
+ privateKey: pki.privateKeyFromPem(privPem),
41
+ };
31
42
 
32
43
  // Create self-signed X.509 code signing certificate
33
44
  const cert = pki.createCertificate();
@@ -215,27 +215,40 @@ export interface ProvisionSecretPlan {
215
215
  * provisioned by an older CLI is still detected. Pure — the contract is pinned by tests.
216
216
  */
217
217
  export function buildProvisionSecretPlan(args: {
218
- result: { loginRole: string; adminRole?: string; adminVerified?: boolean | null };
219
- authUrl: string;
218
+ result: {
219
+ loginRole: string;
220
+ adminRole?: string;
221
+ adminVerified?: boolean | null;
222
+ /** false = the role already existed as a live login and was NOT re-passworded (additive-only),
223
+ * so its secret must be left untouched. Absent/true = the password was set, write the secret. */
224
+ passwordSet?: boolean;
225
+ adminPasswordSet?: boolean;
226
+ };
227
+ authUrl?: string;
220
228
  adminUrl?: string;
221
229
  existing: Record<string, string>;
222
230
  }): ProvisionSecretPlan {
223
231
  const { result, authUrl, adminUrl, existing } = args;
224
- const updates: Record<string, string> = {
225
- DATABASE_URL: authUrl,
226
- };
232
+ const updates: Record<string, string> = {};
227
233
  const notes: string[] = [];
228
234
  const warnings: string[] = [];
229
235
 
230
- const prevAuthRole = roleOfUrl(existing.DATABASE_URL ?? existing.DatabaseUrl);
231
- notes.push(`DATABASE_URL: ${prevAuthRole ?? 'unset'} → ${result.loginRole}`);
232
- if (prevAuthRole && prevAuthRole !== result.loginRole) {
233
- warnings.push(
234
- `DATABASE_URL was already set (role '${prevAuthRole}') — any function linked to it connects as '${result.loginRole}' after its next cold start. Ensure grants are in place: everystack db:reconcile && everystack db:doctor.`,
235
- );
236
+ if (result.passwordSet !== false && authUrl) {
237
+ updates.DATABASE_URL = authUrl;
238
+ const prevAuthRole = roleOfUrl(existing.DATABASE_URL ?? existing.DatabaseUrl);
239
+ notes.push(`DATABASE_URL: ${prevAuthRole ?? 'unset'} → ${result.loginRole}`);
240
+ if (prevAuthRole && prevAuthRole !== result.loginRole) {
241
+ warnings.push(
242
+ `DATABASE_URL was already set (role '${prevAuthRole}') — any function linked to it connects as '${result.loginRole}' after its next cold start. Ensure grants are in place: everystack db:reconcile && everystack db:doctor.`,
243
+ );
244
+ }
245
+ } else {
246
+ // Additive-only: the authenticator already existed as a live login, so its password was
247
+ // preserved and DATABASE_URL is left as-is. Never rewrite a working credential on a re-provision.
248
+ notes.push(`DATABASE_URL: '${result.loginRole}' already provisioned — password + secret preserved (use --rotate to change it)`);
236
249
  }
237
250
 
238
- if (result.adminRole && adminUrl) {
251
+ if (result.adminRole && adminUrl && result.adminPasswordSet !== false) {
239
252
  updates.ADMIN_DATABASE_URL = adminUrl;
240
253
  const prevAdminRole = roleOfUrl(existing.AdminDatabaseUrl ?? existing.ADMIN_DATABASE_URL);
241
254
  const verified = result.adminVerified === true ? 'login verified' : 'login NOT verified';
@@ -245,6 +258,8 @@ export function buildProvisionSecretPlan(args: {
245
258
  `The '${result.adminRole}' login could not be verified from the ops function — confirm operator connectivity before relying on it: everystack db:doctor.`,
246
259
  );
247
260
  }
261
+ } else if (result.adminRole) {
262
+ notes.push(`ADMIN_DATABASE_URL: '${result.adminRole}' already provisioned — password + secret preserved (use --rotate to change it)`);
248
263
  } else {
249
264
  warnings.push(
250
265
  'No operator login was created — this @everystack/server predates the both-sides split (<0.3.7). Operator tasks stay on the linked Postgres master; upgrade the server pin and re-run db:provision for the full credential split.',
@@ -254,6 +269,37 @@ export function buildProvisionSecretPlan(args: {
254
269
  return { updates, notes, warnings };
255
270
  }
256
271
 
272
+ /**
273
+ * Both login passwords were preserved (an additive-only re-provision). Decide whether that is a
274
+ * true no-op — the secrets backing these roles already exist and name them — or a HALF-DONE stage
275
+ * the operator must recover with --rotate. A role can exist WITH LOGIN yet have no working secret:
276
+ * a prior provision set a fresh password and then failed to write it (the admin-probe abort, or a
277
+ * host it could not resolve). Reporting "already provisioned" there would send the operator away
278
+ * from the only fix, so refuse instead. Pure — pinned by tests.
279
+ */
280
+ export function provisionPreservedIsRecoverable(args: {
281
+ loginRole: string;
282
+ adminRole?: string;
283
+ existing: Record<string, string>;
284
+ }): { ok: true } | { ok: false; reason: string } {
285
+ const { loginRole, adminRole, existing } = args;
286
+ // A rotated stage's secret names the ACTIVE rotation-pair role (e.g. authenticator_a), not the
287
+ // bare role — so accept the base name OR either pair variant as a valid backing credential.
288
+ // Without this, a healthy rotated stage false-refuses on a plain (search_path) re-provision.
289
+ const backsRole = (secretRole: string | undefined | null, base: string): boolean =>
290
+ secretRole === base || secretRole === `${base}_a` || secretRole === `${base}_b`;
291
+ const authOk = backsRole(roleOfUrl(existing.DATABASE_URL ?? existing.DatabaseUrl), loginRole);
292
+ const adminOk = !adminRole || backsRole(roleOfUrl(existing.ADMIN_DATABASE_URL ?? existing.AdminDatabaseUrl), adminRole);
293
+ if (authOk && adminOk) return { ok: true };
294
+ return {
295
+ ok: false,
296
+ reason:
297
+ 'Role(s) already exist but the stage secret does not match them — a prior provision set a '
298
+ + 'password that was never written to the secret store, so no working credential exists. '
299
+ + 'Run `everystack db:provision --rotate` to set a fresh password and write the secret together.',
300
+ };
301
+ }
302
+
257
303
  /** The provisioning core's shape (mirrors @everystack/server/provision — no compile-time dep,
258
304
  * same seam as pipeline-loader). */
259
305
  export interface ServerProvision {
@@ -263,6 +309,7 @@ export interface ServerProvision {
263
309
  execute: (sql: string) => Promise<unknown>;
264
310
  connection?: { host: string; port?: number | string; database: string } | null;
265
311
  connectProbe?: (url: string) => Promise<void>;
312
+ roleHasLogin?: (role: string) => Promise<boolean>;
266
313
  },
267
314
  ): Promise<any>;
268
315
  }
@@ -826,6 +873,12 @@ export async function dbProvisionCommand(flags: Record<string, string>): Promise
826
873
  return await runProvision({ authPassword, adminPassword, searchPath, rotationPairs: !!flags['rotation-pairs'] }, {
827
874
  execute: async (statement) => runner(statement),
828
875
  connection: parseUrlConnection(url),
876
+ // Additive-only: an already-live login role keeps its password (never reset on re-provision).
877
+ roleHasLogin: async (role) => {
878
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(role)) return false;
879
+ const rows = await runner(`SELECT 1 FROM pg_roles WHERE rolname = '${role}' AND rolcanlogin`);
880
+ return Array.isArray(rows) && rows.length > 0;
881
+ },
829
882
  // Same probe as the ops-Lambda venue: a real login as the new role, DDL-capable.
830
883
  connectProbe: async (probeUrl) => {
831
884
  const { default: postgres } = await import('postgres');
@@ -891,7 +944,32 @@ export async function dbProvisionCommand(flags: Record<string, string>): Promise
891
944
  }
892
945
  success(`Role chain ready: ${result.loginRole} (LOGIN, NOINHERIT, no BYPASSRLS) → can SET ROLE to ${result.appRoles.join(', ')}`);
893
946
  if (result.adminRole) {
894
- success(`Operator login ready: ${result.adminRole} (LOGIN, INHERIT owner) — ${result.adminVerified === true ? 'connectivity verified' : 'connectivity NOT verified'}`);
947
+ const adminMsg = result.adminPasswordSet === false
948
+ ? 'existing login preserved (not re-verified — run db:doctor)'
949
+ : (result.adminVerified === true ? 'connectivity verified' : 'connectivity NOT verified');
950
+ success(`Operator login ready: ${result.adminRole} (LOGIN, INHERIT owner) — ${adminMsg}`);
951
+ }
952
+
953
+ // Additive-only: if the login roles already existed as live logins, provision changed no
954
+ // credential — so there is nothing to write and no host to resolve. Never touch the secret
955
+ // store on a no-op re-provision (the footgun that reset a live password then locked the stage out).
956
+ const wroteAuth = result.passwordSet !== false;
957
+ const wroteAdmin = !!result.adminRole && result.adminPasswordSet !== false;
958
+ if (!wroteAuth && !wroteAdmin) {
959
+ // Both roles preserved. Only a true no-op if the secrets backing them exist and name them;
960
+ // a role that exists WITH LOGIN but has no matching secret is a half-done prior provision that
961
+ // plain db:provision cannot recover — refuse with the --rotate remedy rather than report success.
962
+ const recoverable = provisionPreservedIsRecoverable({
963
+ loginRole: result.loginRole,
964
+ adminRole: result.adminRole,
965
+ existing: stageSecrets,
966
+ });
967
+ if (recoverable.ok) {
968
+ success(`Roles already provisioned — passwords and secrets left unchanged. Verify the live credential with \`everystack db:doctor --stage ${flags.stage}\`; run \`everystack db:provision --rotate\` to change credentials.`);
969
+ return;
970
+ }
971
+ fail(recoverable.reason);
972
+ process.exit(1);
895
973
  }
896
974
 
897
975
  // Resolve host/port/db (not secret) to build the connection strings in memory.
@@ -928,8 +1006,8 @@ export async function dbProvisionCommand(flags: Record<string, string>): Promise
928
1006
  const mint = (role: string, password: string): string =>
929
1007
  preserveConnParams(`postgresql://${role}:${password}@${conn.host}:${conn.port ?? 5432}/${conn.database}`, sourceUrl);
930
1008
 
931
- const authUrl = mint(result.loginRole, authPassword);
932
- const adminUrl = result.adminRole ? mint(result.adminRole, adminPassword) : undefined;
1009
+ const authUrl = wroteAuth ? mint(result.loginRole, authPassword) : undefined;
1010
+ const adminUrl = wroteAdmin && result.adminRole ? mint(result.adminRole, adminPassword) : undefined;
933
1011
 
934
1012
  // Write the secrets directly to the SST secret store — never display them. One blob
935
1013
  // write updates every key together, so there is no window where the API side is the
@@ -24,13 +24,33 @@
24
24
  * for n ≥ 1. PostgreSQL defines an array-to-array cast element-wise (parse_coerce), so
25
25
  * the two expressions denote the same value for every input; wrapping an element in
26
26
  * parentheses is parse-neutral. The empty array is EXCLUDED: `ARRAY[]` without a cast has
27
- * no type, so its cast is load-bearing and stays.
27
+ * no type, so its cast is load-bearing and stays. (The measured CROSS-VERSION case: two PG
28
+ * majors const-fold the same DDL differently.)
29
+ *
30
+ * LITERAL-TEXT-CAST STRIP: `'lit'::text == 'lit'`, but ONLY the bare `text` cast applied
31
+ * directly to a single-quoted string literal. This is a CROSS-PRODUCER class, present on
32
+ * every version: PostgreSQL coerces an unknown-type string literal compared to a text
33
+ * column and RECORDS `'lit'::text` in the stored tree, while the author's model source
34
+ * writes the un-cast `'lit'`. The two producers' texts never converge without this rule, so
35
+ * a `text_col = 'lit'` partial index / CHECK / policy re-diffs DROP+CREATE forever and the
36
+ * fingerprint gate cannot MATCH. Safe because `text` takes no typmod and its input function
37
+ * is the identity on the literal's bytes — the cast cannot change the value. The rule is
38
+ * `text`-ONLY for exactly that reason: `bpchar`/`character(n)` blank-pad or blank-trim,
39
+ * `varchar(n)`/`"char"` truncate — those casts CHANGE the value, so stripping one would
40
+ * collapse two opposite-truth predicates to equal text (a false match, the disaster). The
41
+ * array-type cast `::text[]` is likewise excluded — it carries the element type. See the
42
+ * `char(2)` false-match test for why the family is not widened.
28
43
  *
29
44
  * Anything the scanner does not positively recognize is left byte-for-byte unchanged — an
30
45
  * unrecognized spelling is a MISS (drop + create, the pre-existing behavior), never a guess.
31
- * Escalation is named, not implied: the SECOND cross-version variant found in the field is
46
+ * Escalation is named, not implied: the SECOND cross-VERSION variant found in the field is
32
47
  * the trigger to adopt libpg_query and move identity onto normalized ASTs (Ty, 2026-08-09) —
33
- * a growing rule table over raw text is where scanning stops being defensible.
48
+ * a growing rule table over raw text is where scanning stops being defensible. One correction
49
+ * to that trigger (2026-08-24): libpg_query closes cross-VERSION SYNTAX drift (whitespace,
50
+ * parens, keyword case), NOT cross-PRODUCER COERCION drift — a raw parse tree carries no type
51
+ * resolution, so `'lit'` and `'lit'::text` still differ structurally without a catalog, which
52
+ * the CI-pure `db:diff` lane does not have. That class earns a provable text rewrite here,
53
+ * verified end-to-end by `db:check`'s from-scratch build on a real database.
34
54
  *
35
55
  * Applied at the PRODUCERS (live introspection's `predicate()`, the compiler's policy
36
56
  * assembly), so every downstream comparison — the identity matcher, the canonical hash, the
@@ -121,6 +141,53 @@ function distributeOnce(s: string): string | null {
121
141
  return null;
122
142
  }
123
143
 
144
+ /**
145
+ * A bare `text` cast token — `::text` or `::pg_catalog.text` — from just past a literal's
146
+ * closing quote. Whitespace around `::` is tolerated (pg emits none; an author might). The
147
+ * trailing `\b` rejects `::textfoo`; the caller additionally rejects a following `[` so the
148
+ * array-type cast `::text[]` is never stripped.
149
+ */
150
+ const LITERAL_TEXT_CAST = /^\s*::\s*(?:pg_catalog\.)?text\b/;
151
+
152
+ /**
153
+ * Strip a bare `text` cast applied directly to a single-quoted string literal, wherever one
154
+ * appears. One left-to-right pass is complete: removing a cast token cannot create a new
155
+ * `'literal'::text` pair to its left, and it never forms an `(ARRAY[…])::T[]` for the other
156
+ * rule. Quoted regions are skipped so a `::text` inside a literal is content, not structure.
157
+ */
158
+ function stripLiteralTextCast(s: string): string {
159
+ let out = '';
160
+ let i = 0;
161
+ while (i < s.length) {
162
+ const c = s[i];
163
+ if (c === "'") {
164
+ const close = skipQuoted(s, i);
165
+ if (close < 0) return out + s.slice(i); // unterminated literal — leave the rest alone
166
+ out += s.slice(i, close + 1); // the literal itself, verbatim
167
+ i = close + 1;
168
+ // Drop a bare text cast on the literal, and any stacked `::text::text`, so the result
169
+ // is a true fixpoint. `::text[]` is the array-type cast (element type, load-bearing),
170
+ // not a scalar text cast — the look-ahead skips whitespace so `::text []` is kept too.
171
+ let m = LITERAL_TEXT_CAST.exec(s.slice(i));
172
+ while (m && !/^\s*\[/.test(s.slice(i + m[0].length))) {
173
+ i += m[0].length; // drop the cast token
174
+ m = LITERAL_TEXT_CAST.exec(s.slice(i));
175
+ }
176
+ continue;
177
+ }
178
+ if (c === '"') {
179
+ const close = skipQuoted(s, i);
180
+ if (close < 0) return out + s.slice(i);
181
+ out += s.slice(i, close + 1); // a quoted identifier — never a string literal, skip whole
182
+ i = close + 1;
183
+ continue;
184
+ }
185
+ out += c;
186
+ i++;
187
+ }
188
+ return out;
189
+ }
190
+
124
191
  /**
125
192
  * Normalize one deparsed expression. Idempotent; unrecognized text returns unchanged.
126
193
  * Fixpoint-bounded: nesting deeper than the cap returns its progress, which is still
@@ -130,8 +197,8 @@ export function normalizeDeparsedExpr(text: string): string {
130
197
  let s = text;
131
198
  for (let n = 0; n < 64; n++) {
132
199
  const next = distributeOnce(s);
133
- if (next === null) return s;
200
+ if (next === null) break;
134
201
  s = next;
135
202
  }
136
- return s;
203
+ return stripLiteralTextCast(s);
137
204
  }