@everystack/server 0.4.1 → 0.4.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/server",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
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>",
@@ -76,6 +76,10 @@
76
76
  "./pipeline": {
77
77
  "types": "./src/pipeline.ts",
78
78
  "default": "./src/pipeline.ts"
79
+ },
80
+ "./infra": {
81
+ "types": "./src/infra.ts",
82
+ "default": "./src/infra.ts"
79
83
  }
80
84
  },
81
85
  "peerDependencies": {
@@ -87,6 +91,7 @@
87
91
  "@aws-sdk/signature-v4a": ">=3.1048.0",
88
92
  "@everystack/auth": ">=0.1.0",
89
93
  "@everystack/cli": ">=0.1.0",
94
+ "@everystack/pg-tools": ">=0.4.0",
90
95
  "drizzle-orm": "0.41.0",
91
96
  "heic-decode": "2.1.0",
92
97
  "postgres": "3.4.9",
@@ -106,6 +111,9 @@
106
111
  "sst": {
107
112
  "optional": true
108
113
  },
114
+ "@everystack/pg-tools": {
115
+ "optional": true
116
+ },
109
117
  "@everystack/auth": {
110
118
  "optional": true
111
119
  },
@@ -143,8 +151,9 @@
143
151
  "sst": "4.13.1",
144
152
  "ts-jest": "29.4.9",
145
153
  "typescript": "5.9.3",
146
- "@everystack/cli": "0.4.7",
147
- "@everystack/auth": "0.4.0"
154
+ "@everystack/pg-tools": "0.4.0",
155
+ "@everystack/auth": "0.4.0",
156
+ "@everystack/cli": "0.4.8"
148
157
  },
149
158
  "scripts": {
150
159
  "test": "jest",
package/src/db-doctor.ts CHANGED
@@ -87,6 +87,20 @@ export interface DoctorProbe {
87
87
  * an older probe (the check is then skipped). B4.
88
88
  */
89
89
  darkRelations?: DarkRelation[];
90
+ /**
91
+ * pg_dump layer capability: whether @everystack/pg-tools is attached (pg_dump present) and its
92
+ * major can dump this server (client >= server). Drives the one-line "backups" lens — a missing
93
+ * layer blocks db:backup AND the snapshot-first destructive apply. Absent on an older probe (the
94
+ * check is then skipped). See docs/plans/pgdump-layer.md.
95
+ */
96
+ backup?: {
97
+ /** `pg_dump --version` output, or null when the layer is not attached. */
98
+ pgDump: string | null;
99
+ pgDumpMajor: number | null;
100
+ serverMajor: number | null;
101
+ /** pgDumpMajor >= serverMajor — a client can dump an older-or-equal server, never a newer one. */
102
+ compatible: boolean;
103
+ };
90
104
  }
91
105
 
92
106
  /** A function PUBLIC can EXECUTE, and whether it runs as its owner (the dangerous half). */
@@ -544,6 +558,33 @@ export function buildDoctorReport(probe: DoctorProbe, options: DoctorReportOptio
544
558
  }
545
559
  }
546
560
 
561
+ // Backups lens — the pg_dump layer gates db:backup AND the snapshot-first destructive apply. A
562
+ // missing layer isn't a security hole (warn, not fail), but it silently blocks the ceremony.
563
+ if (probe.backup) {
564
+ const b = probe.backup;
565
+ if (b.pgDump && b.compatible) {
566
+ checks.push({
567
+ name: 'Logical backups available (pg_dump layer)',
568
+ status: 'pass',
569
+ detail: `pg_dump major ${b.pgDumpMajor} attached and compatible with server major ${b.serverMajor} — db:backup and the snapshot-first destructive apply are ready`,
570
+ });
571
+ } else if (b.pgDump && !b.compatible) {
572
+ checks.push({
573
+ name: 'Logical backups available (pg_dump layer)',
574
+ status: 'warn',
575
+ detail: `pg_dump major ${b.pgDumpMajor} is older than server major ${b.serverMajor} — it will refuse to dump (no bypass exists)`,
576
+ remediation: 'Upgrade @everystack/pg-tools to advance the client ceiling to >= the server major, then redeploy.',
577
+ });
578
+ } else {
579
+ checks.push({
580
+ name: 'Logical backups available (pg_dump layer)',
581
+ status: 'warn',
582
+ detail: 'pg_dump layer not attached — db:backup and the snapshot-first destructive apply are unavailable',
583
+ remediation: 'Install @everystack/pg-tools and redeploy (pgDumpLayer attaches it automatically), or pass a bring-your-own layerArn in sst.config.ts.',
584
+ });
585
+ }
586
+ }
587
+
547
588
  return {
548
589
  api,
549
590
  admin,
package/src/infra.ts ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @everystack/server/infra — deploy-time helpers for a consumer's `sst.config.ts`.
3
+ *
4
+ * `pgDumpLayer()` attaches the PostgreSQL client-tools Lambda layer that `db:backup`/`db:restore`
5
+ * (and the snapshot-first apply ceremony) need. It replaces the old `PGDUMP_LAYER_ARN` env-var line
6
+ * with a zero-config, declarative resource: the framework ships the binaries (@everystack/pg-tools),
7
+ * and this publishes them as a content-addressed LayerVersion into the CONSUMER's own account.
8
+ *
9
+ * Declarative, not imperative: it returns a Pulumi-managed resource's arn (or an escape-hatch
10
+ * string), so the SST/Pulumi engine owns the layer's lifecycle — no SDK side effects at config-eval,
11
+ * no untracked state, clean teardown. Same artifact sha => same layer name => redeploys are no-ops.
12
+ */
13
+
14
+ import { manifest, layerZipPath } from '@everystack/pg-tools';
15
+
16
+ /**
17
+ * SST's ambient config globals, injected so this package typechecks standalone (it must not depend on
18
+ * SST's per-app generated types). In `sst.config.ts` you already have both — pass `{ aws, $util }`.
19
+ * Typed loosely on purpose; the shapes are Pulumi's and not worth vendoring here.
20
+ */
21
+ export interface PgDumpLayerContext {
22
+ /** SST's global `aws` (@pulumi/aws) — for `aws.lambda.LayerVersion`. */
23
+ aws: any;
24
+ /** SST's global `$util` (@pulumi/pulumi) — for `$util.asset.FileArchive`. */
25
+ $util: any;
26
+ }
27
+
28
+ export interface PgDumpLayerOptions {
29
+ /**
30
+ * The stage's PostgreSQL server major. The shipped client must be >= this (pg_dump/pg_restore are
31
+ * backward compatible; a client older than the server is refused). Read it from the engine you declare.
32
+ */
33
+ pgMajor: number;
34
+ /**
35
+ * Escape hatch: attach this exact layer ARN instead of the managed publish. For consumers who build
36
+ * their own (e.g. x86_64, an exotic major) via tools/pgdump-layer. Outranks everything else.
37
+ */
38
+ layerArn?: string;
39
+ }
40
+
41
+ let warnedEnv = false;
42
+
43
+ /**
44
+ * Resolve and attach the pg_dump layer. Returns the ARN to spread into a Function's `layers: []`.
45
+ *
46
+ * Precedence: explicit `layerArn` > `PGDUMP_LAYER_ARN` (deprecated alias, warns once) > the managed
47
+ * content-addressed LayerVersion published into this account.
48
+ *
49
+ * @example
50
+ * import { pgDumpLayer } from '@everystack/server/infra';
51
+ * const ops = new sst.aws.Function('Ops', {
52
+ * layers: [pgDumpLayer({ aws, $util }, 'PgDump', { pgMajor: 16 })],
53
+ * // ...
54
+ * });
55
+ */
56
+ export function pgDumpLayer(
57
+ ctx: PgDumpLayerContext,
58
+ name: string,
59
+ opts: PgDumpLayerOptions,
60
+ ): any /* Input<string> — the layer arn; `any` at the Pulumi interop boundary so it spreads into layers:[] */ {
61
+ // 1) explicit escape hatch wins.
62
+ if (opts.layerArn) return opts.layerArn;
63
+
64
+ // 2) deprecated env alias — still honored for one minor line, but warned.
65
+ const envArn = process.env.PGDUMP_LAYER_ARN;
66
+ if (envArn) {
67
+ if (!warnedEnv) {
68
+ warnedEnv = true;
69
+ console.warn(
70
+ '[everystack] PGDUMP_LAYER_ARN is deprecated and will be removed in the next minor. ' +
71
+ 'Install @everystack/pg-tools and drop the env var (the layer is published automatically), ' +
72
+ 'or pass pgDumpLayer(ctx, name, { layerArn }) for a bring-your-own layer.',
73
+ );
74
+ }
75
+ return envArn;
76
+ }
77
+
78
+ // 3) the managed publish: a content-addressed LayerVersion in the consumer's own account.
79
+ if (manifest.major < opts.pgMajor) {
80
+ throw new Error(
81
+ `@everystack/pg-tools ships pg${manifest.major} client tools, but this stage's server is ` +
82
+ `pg${opts.pgMajor}. pg_dump/pg_restore must be >= the server. Upgrade @everystack/pg-tools ` +
83
+ `to advance the client ceiling, or pass a bring-your-own layerArn.`,
84
+ );
85
+ }
86
+ const sha8 = manifest.sha256.slice(0, 8);
87
+ const layer = new ctx.aws.lambda.LayerVersion(name, {
88
+ // content-addressed: same artifact => same name => Pulumi treats redeploys as no-ops.
89
+ layerName: `everystack-pgdump${manifest.major}-${sha8}`,
90
+ code: new ctx.$util.asset.FileArchive(layerZipPath()),
91
+ compatibleRuntimes: ['nodejs20.x'],
92
+ compatibleArchitectures: [manifest.arch],
93
+ description: `everystack pg_dump/pg_restore — pg${manifest.postgresql}, sha ${sha8}`,
94
+ });
95
+ return layer.arn;
96
+ }
package/src/plugin.ts CHANGED
@@ -725,8 +725,34 @@ export function dbPlugin(options: DbPluginOptions): Plugin {
725
725
  }
726
726
  }
727
727
 
728
+ // Backups lens — is the pg_dump layer attached AND version-compatible? Reuse the
729
+ // db:backup:probe internals; a missing layer leaves pgDump null (the lens warns), never throws.
730
+ let backup: import('./db-doctor').DoctorProbe['backup'];
731
+ try {
732
+ const { pgClientVersion, parsePgDumpMajor, serverMajorFromNum, isDumpCompatible } = await import('./backup');
733
+ let pgDump: string | null = null;
734
+ let pgDumpMajor: number | null = null;
735
+ try {
736
+ pgDump = await pgClientVersion('pg_dump');
737
+ pgDumpMajor = parsePgDumpMajor(pgDump);
738
+ } catch {
739
+ // layer absent — pgDump stays null, the lens reports "not attached"
740
+ }
741
+ const svnRow = rowsOf(await opsDb.execute(sql.raw('SHOW server_version_num')))[0];
742
+ const svn = Number(svnRow?.server_version_num);
743
+ const serverMajor = Number.isFinite(svn) ? serverMajorFromNum(svn) : null;
744
+ backup = {
745
+ pgDump,
746
+ pgDumpMajor,
747
+ serverMajor,
748
+ compatible: pgDumpMajor != null && Number.isFinite(svn) ? isDumpCompatible(pgDumpMajor, svn) : false,
749
+ };
750
+ } catch {
751
+ // leave backup undefined → the lens is skipped, exactly like the other optional probes
752
+ }
753
+
728
754
  return buildDoctorReport(
729
- { api, admin, forceRls, appGrantedRls, apiAmbientRead, secdefFunctions, publicExecutableFunctions, darkRelations },
755
+ { api, admin, forceRls, appGrantedRls, apiAmbientRead, secdefFunctions, publicExecutableFunctions, darkRelations, backup },
730
756
  { forceRlsCarveouts: options.forceRlsCarveouts, functionSchemas: options.functionSchemas },
731
757
  );
732
758
  };
@@ -794,7 +820,7 @@ export function dbPlugin(options: DbPluginOptions): Plugin {
794
820
  try {
795
821
  pgDump = await pgClientVersion('pg_dump');
796
822
  } catch (err: any) {
797
- return { error: `pg_dump not available — is the pg_dump layer attached to this function? (${err?.message || String(err)})` };
823
+ return { error: `pg_dump not available — the pg_dump layer is not attached. Install @everystack/pg-tools and redeploy (pgDumpLayer() attaches it automatically), or pass a bring-your-own layerArn in sst.config.ts. (${err?.message || String(err)})` };
798
824
  }
799
825
  const pgDumpMajor = parsePgDumpMajor(pgDump);
800
826