@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 +17 -0
- package/package.json +2 -2
- package/src/plugin.ts +64 -12
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
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
|
-
|
|
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
|
}
|