@everystack/server 0.4.27 → 0.4.28

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 CHANGED
@@ -4,6 +4,23 @@ All notable changes to `@everystack/server` are documented here.
4
4
 
5
5
  Format based on [Keep a Changelog](https://keepachangelog.com/).
6
6
 
7
+ ## [0.4.28] - 2026-09-02
8
+
9
+ ### Added
10
+
11
+ - wire proveDescent into db:apply — ledger read shares the lock session
12
+ - retire --force-descent for the git-native escape + a proven-lineage-lost attestation
13
+ - the stage lane's fast-forward rule is structurally non-droppable (q1-fail-closed)
14
+ - version the db:apply wire contract, both directions (q1-contract-version)
15
+
16
+ ### Fixed
17
+
18
+ - split-brain belt + correct the version-skew remedy (closing gate)
19
+
20
+ ### Changed
21
+
22
+ - delete the orphaned recompile-descent helpers, rewire the illusion of coverage (F1)
23
+
7
24
  ## [0.4.26] - 2026-08-15
8
25
 
9
26
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/server",
3
- "version": "0.4.27",
3
+ "version": "0.4.28",
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>",
@@ -187,7 +187,7 @@
187
187
  "sst": "4.13.1",
188
188
  "ts-jest": "29.4.9",
189
189
  "typescript": "5.9.3",
190
- "@everystack/cli": "0.4.67",
190
+ "@everystack/cli": "0.4.70",
191
191
  "@everystack/auth": "0.4.4"
192
192
  },
193
193
  "scripts": {
package/src/plugin.ts CHANGED
@@ -12,6 +12,18 @@ import type {
12
12
  APIGatewayProxyEventV2,
13
13
  APIGatewayProxyStructuredResultV2,
14
14
  } from 'aws-lambda';
15
+ import type { ApplyPlanOptions } from '@everystack/cli/apply';
16
+
17
+ /**
18
+ * The stage lane's apply options, with `checkDescent` made REQUIRED. The fast-forward rule is the
19
+ * one gate that crosses the wire on every stage apply (additive included); typing it non-optional
20
+ * here forbids the conditional spread that once let a missing descent verdict silently skip it.
21
+ * verifyAuthority/verifySnapshot stay optional — they are destructive-only, and the stage lane
22
+ * refuses destructive plans outright, so they never arise on this lane.
23
+ */
24
+ type StageApplyOptions = ApplyPlanOptions & {
25
+ checkDescent: NonNullable<ApplyPlanOptions['checkDescent']>;
26
+ };
15
27
  import {
16
28
  createRouter,
17
29
  eventToRequest,
@@ -191,11 +203,17 @@ export function createPluginLambdaHandler(
191
203
  // the action lookup (it must answer even when initialization itself is the failure) and
192
204
  // reserved: an app action named `_health` is never reached.
193
205
  if (event._action === '_health') {
206
+ // q1-contract-version direction B: advertise the db:apply wire-contract version so a NEWER
207
+ // CLI detects an OLDER deployed stage BEFORE it dispatches a write (prevention, not a
208
+ // post-write alarm). Stamped on BOTH branches, so a stage whose DB init is failing still
209
+ // tells the CLI its contract — an absent field means the deployed server predates the
210
+ // handshake. Old servers simply omit it; the CLI reads absent as "redeploy the stage".
211
+ const { APPLY_CONTRACT_VERSION } = await import('@everystack/cli/apply');
194
212
  try {
195
213
  const { actions } = await ensureInitialized();
196
- return { ok: true, mode: httpEnabled ? 'http' : 'ops', actions: Object.keys(actions).sort() };
214
+ return { ok: true, mode: httpEnabled ? 'http' : 'ops', actions: Object.keys(actions).sort(), applyContract: APPLY_CONTRACT_VERSION };
197
215
  } catch (error) {
198
- return { ok: false, error: String(error) };
216
+ return { ok: false, error: String(error), applyContract: APPLY_CONTRACT_VERSION };
199
217
  }
200
218
  }
201
219
  const { ctx, actions } = await ensureInitialized();
@@ -958,12 +976,21 @@ export function dbPlugin(options: DbPluginOptions): Plugin {
958
976
  // direct --database-url path runs, only here the connection never leaves the
959
977
  // Lambda. A destructive plan snapshots first (db:backup), in order.
960
978
  actions['db:apply'] = async (payload, ctx) => {
961
- const { plan, actor, gitRef, descentVerdict, authorityVerdict, stage, snapshotRef } =
979
+ const { plan, contractVersion, actor, gitRef, dirty, descentVerdict, descentAttested, authorityVerdict, stage, snapshotRef } =
962
980
  (payload ?? {}) as {
963
981
  plan?: any;
982
+ /** The CLI's db:apply wire-contract version. Checked for EXACT equality against this
983
+ * server's APPLY_CONTRACT_VERSION — a skew refuses before any write (direction A). */
984
+ contractVersion?: number;
964
985
  actor?: string | null;
965
986
  gitRef?: string | null;
987
+ /** The CLI applied from a dirty worktree — recorded on the schema_log witness so a
988
+ * later descent ignores this row's git_ref. */
989
+ dirty?: boolean;
966
990
  descentVerdict?: { ok: boolean; reason?: string };
991
+ /** When the CLI honoured a --attest-lineage-lost waiver, the note to record in the
992
+ * schema_log outcome — the durable audit of the deliberate descent waiver. */
993
+ descentAttested?: string;
967
994
  authorityVerdict?: { ok: boolean; reason?: string };
968
995
  stage?: string;
969
996
  /** An explicit backup id to verify; absent → the gate auto-resolves the stage's newest. */
@@ -972,10 +999,29 @@ export function dbPlugin(options: DbPluginOptions): Plugin {
972
999
  if (!plan || typeof plan !== 'object' || !Array.isArray(plan.statements)) {
973
1000
  return { error: 'db:apply requires { plan } — a reviewed edge minted by db:plan' };
974
1001
  }
975
- const { executeApplyPlan, PLAN_VERSION } = await import('@everystack/cli/apply');
1002
+ const { executeApplyPlan, PLAN_VERSION, APPLY_CONTRACT_VERSION } = await import('@everystack/cli/apply');
976
1003
  if (plan.v !== PLAN_VERSION) {
977
1004
  return { error: `plan version ${plan.v} is not this server's ${PLAN_VERSION} — re-mint with a matching db:plan` };
978
1005
  }
1006
+ // q1-contract-version direction A: the db:apply WIRE CONTRACT (payload keys + the gates this
1007
+ // server runs from them) is versioned apart from the plan. An exact-equality mismatch — an
1008
+ // older CLI that declares none, or a bumped one — refuses before any write, so a renamed or
1009
+ // dropped payload key can never ride through as a silently skipped gate. Redeploying the
1010
+ // stage's ops Lambda ships the matching cli it bundles, so the versions realign together.
1011
+ if (contractVersion !== APPLY_CONTRACT_VERSION) {
1012
+ // This fires only for an OLDER CLI: a current CLI catches the skew at the _health handshake and
1013
+ // never dispatches. So the remedy leads with upgrading the CLI, not redeploying the (current) stage.
1014
+ return { error: `this db:apply payload declares apply-contract v${contractVersion ?? '(none)'} but this stage's ops Lambda enforces v${APPLY_CONTRACT_VERSION} — your everystack CLI is older than the deployed stage. Upgrade the CLI to match (or, if the stage was rolled back, redeploy it); nothing was applied.` };
1015
+ }
1016
+ // q1-fail-closed: descent (the fast-forward rule) is the one gate that crosses the wire on
1017
+ // EVERY stage apply, additive included. It is NOT waivable by omission. A payload with no
1018
+ // well-formed descent verdict is an older CLI or a malformed client — refuse before any write,
1019
+ // never silently skip the rule. (The conditional-spread that let a missing verdict skip
1020
+ // checkDescent was the INFO-1 bug; StageApplyOptions now forbids dropping the hook, and this
1021
+ // guards the evidence that feeds it.)
1022
+ if (!descentVerdict || typeof descentVerdict.ok !== 'boolean') {
1023
+ return { error: 'this db:apply payload carries no descent verdict — the fast-forward rule cannot be waived by omission. Upgrade the everystack CLI (an older CLI omits it), or this is a client bug.' };
1024
+ }
979
1025
 
980
1026
  const { sql } = await import('drizzle-orm');
981
1027
  const runner = async (query: string): Promise<any[]> => {
@@ -995,14 +1041,17 @@ export function dbPlugin(options: DbPluginOptions): Plugin {
995
1041
  const SNAPSHOT_FRESHNESS_MS = 60 * 60_000;
996
1042
  let snapshotId: string | undefined;
997
1043
  try {
998
- const result = await executeApplyPlan(runner, session, plan, {
1044
+ // checkDescent is REQUIRED by StageApplyOptions — no conditional spread can drop it. The
1045
+ // verdict is guaranteed well-formed by the guard above; the hook enforces + records it here,
1046
+ // in order, atomic with the lock. Authority/snapshot stay conditional (destructive-only, and
1047
+ // the stage lane refuses destructive outright, so they never light up on this lane).
1048
+ const applyOptions: StageApplyOptions = {
999
1049
  actor: actor ?? null,
1000
1050
  gitRef: gitRef ?? null,
1001
- // Descent/authority are decided by the CLI (checkout + caller identity)
1002
- // and enforced + recorded here, in order, atomic with the lock.
1003
- ...(descentVerdict
1004
- ? { verifyDescent: async () => (descentVerdict.ok ? { ok: true as const } : { ok: false as const, reason: descentVerdict.reason ?? 'descent refused' }) }
1005
- : {}),
1051
+ dirty: dirty === true,
1052
+ checkDescent: async () => (descentVerdict.ok ? { ok: true as const } : { ok: false as const, reason: descentVerdict.reason ?? 'descent refused' }),
1053
+ // A honoured --attest-lineage-lost waiver is recorded in the schema_log outcome.
1054
+ ...(descentAttested ? { descentNote: () => descentAttested } : {}),
1006
1055
  ...(destructive && authorityVerdict
1007
1056
  ? { verifyAuthority: async () => (authorityVerdict.ok ? { ok: true as const } : { ok: false as const, reason: authorityVerdict.reason ?? 'authority refused' }) }
1008
1057
  : {}),
@@ -1030,8 +1079,11 @@ export function dbPlugin(options: DbPluginOptions): Plugin {
1030
1079
  },
1031
1080
  }
1032
1081
  : {}),
1033
- });
1034
- return snapshotId ? { ...result, snapshotId } : result;
1082
+ };
1083
+ const result = await executeApplyPlan(runner, session, plan, applyOptions);
1084
+ // Echo the contract version the write ran under (SHOULD, legibility — the safety mechanism is
1085
+ // the pre-transaction refusal above, not this field).
1086
+ return { ...result, contractVersion: APPLY_CONTRACT_VERSION, ...(snapshotId ? { snapshotId } : {}) };
1035
1087
  } catch (err: any) {
1036
1088
  return { error: err?.message || String(err) };
1037
1089
  }