@everystack/server 0.3.2 → 0.3.3

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.3.2",
3
+ "version": "0.3.3",
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>",
@@ -74,11 +74,6 @@
74
74
  "default": "./src/media.ts"
75
75
  }
76
76
  },
77
- "scripts": {
78
- "test": "jest",
79
- "build": "tsc --build",
80
- "lint": "tsc --noEmit"
81
- },
82
77
  "peerDependencies": {
83
78
  "esbuild": ">=0.20.0",
84
79
  "@aws-sdk/client-cloudfront-keyvaluestore": ">=3.1053.0",
@@ -134,8 +129,6 @@
134
129
  "@aws-sdk/client-s3": "3.1053.0",
135
130
  "@aws-sdk/lib-storage": "3.1053.0",
136
131
  "@aws-sdk/signature-v4a": "3.1063.0",
137
- "@everystack/auth": "workspace:*",
138
- "@everystack/cli": "workspace:*",
139
132
  "@types/aws-lambda": "8.10.161",
140
133
  "@types/jest": "29.5.14",
141
134
  "@types/node": "22.19.18",
@@ -145,6 +138,13 @@
145
138
  "postgres": "3.4.9",
146
139
  "sst": "4.13.1",
147
140
  "ts-jest": "29.4.9",
148
- "typescript": "5.9.3"
141
+ "typescript": "5.9.3",
142
+ "@everystack/auth": "0.3.1",
143
+ "@everystack/cli": "0.3.27"
144
+ },
145
+ "scripts": {
146
+ "test": "jest",
147
+ "build": "tsc --build",
148
+ "lint": "tsc --noEmit"
149
149
  }
150
- }
150
+ }
package/src/backup-run.ts CHANGED
@@ -132,17 +132,10 @@ export async function listBackups(deps: BackupRunDeps, stage: string): Promise<B
132
132
  * pg_restore throws with its stderr. (We start strict and observe benign-error behavior on the
133
133
  * deploy smoke test; the flags --no-owner/--no-privileges/--no-comments remove the usual offenders.)
134
134
  */
135
- export async function runRestore(deps: BackupRunDeps, id: string): Promise<{ id: string; restored: true }> {
136
- const key = keyForId(id);
137
- if (!key) throw new Error(`Malformed backup id: ${id}`);
138
- const env = { ...process.env, ...pgEnvFromUrl(deps.adminUrl) };
139
- const dbName = dbNameFromUrl(deps.adminUrl);
140
-
141
- const s3 = await getS3(deps.region);
142
- const { GetObjectCommand } = await import('@aws-sdk/client-s3');
143
- const obj: any = await s3.send(new GetObjectCommand({ Bucket: deps.bucket, Key: key }));
144
- if (!obj.Body) throw new Error(`Backup not found: ${id}`);
145
- const body = obj.Body as Readable;
135
+ /** The shared restore pipeline: gzipped custom-format archive stream → gunzip → pg_restore. */
136
+ async function restoreFromStream(adminUrl: string, body: Readable): Promise<void> {
137
+ const env = { ...process.env, ...pgEnvFromUrl(adminUrl) };
138
+ const dbName = dbNameFromUrl(adminUrl);
146
139
 
147
140
  const child = spawn('pg_restore', pgRestoreArgs(dbName), { env, stdio: ['pipe', 'ignore', 'pipe'] });
148
141
  let stderr = '';
@@ -154,8 +147,48 @@ export async function runRestore(deps: BackupRunDeps, id: string): Promise<{ id:
154
147
  });
155
148
 
156
149
  await Promise.all([
157
- pipeline(body, createGunzip(), child.stdin!), // S3 → gunzip → pg_restore stdin
150
+ pipeline(body, createGunzip(), child.stdin!), // source → gunzip → pg_restore stdin
158
151
  childExit,
159
152
  ]);
153
+ }
154
+
155
+ export async function runRestore(deps: BackupRunDeps, id: string): Promise<{ id: string; restored: true }> {
156
+ const key = keyForId(id);
157
+ if (!key) throw new Error(`Malformed backup id: ${id}`);
158
+
159
+ const s3 = await getS3(deps.region);
160
+ const { GetObjectCommand } = await import('@aws-sdk/client-s3');
161
+ const obj: any = await s3.send(new GetObjectCommand({ Bucket: deps.bucket, Key: key }));
162
+ if (!obj.Body) throw new Error(`Backup not found: ${id}`);
163
+
164
+ await restoreFromStream(deps.adminUrl, obj.Body as Readable);
160
165
  return { id, restored: true };
161
166
  }
167
+
168
+ /** The URL mode's precondition — a presigned http(s) download, never a path or bucket ref. */
169
+ export function assertRestoreUrl(url: string): void {
170
+ if (!/^https?:\/\//.test(url)) {
171
+ throw new Error('db:restore url must be an http(s) URL — a presigned backup download (everystack db:backup:download / db:fork mints one).');
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Restore from a presigned URL — the cross-stage fork's hand-off
177
+ * (docs/plans/branch-stages.md). A backup lives in its SOURCE stage's
178
+ * bucket, and this stage's Lambda has no IAM path to it; the operator who
179
+ * can read the source's backups and invoke this action IS the
180
+ * authorization, and the URL expires. Same stream pipeline as the id mode.
181
+ */
182
+ export async function runRestoreFromUrl(
183
+ deps: Pick<BackupRunDeps, 'adminUrl'>,
184
+ url: string,
185
+ ): Promise<{ restored: true }> {
186
+ assertRestoreUrl(url);
187
+ const res = await fetch(url);
188
+ if (!res.ok || !res.body) {
189
+ throw new Error(`fetching the backup failed: HTTP ${res.status} — presigned URLs expire in an hour; mint a fresh one.`);
190
+ }
191
+ const { Readable: NodeReadable } = await import('node:stream');
192
+ await restoreFromStream(deps.adminUrl, NodeReadable.fromWeb(res.body as any));
193
+ return { restored: true };
194
+ }
package/src/plugin.ts CHANGED
@@ -797,16 +797,17 @@ export function dbPlugin(options: DbPluginOptions): Plugin {
797
797
  };
798
798
 
799
799
  actions['db:restore'] = async (payload, _ctx) => {
800
- const { id, confirm } = (payload ?? {}) as { id?: string; confirm?: boolean };
801
- if (!id) return { error: 'db:restore requires { id }' };
800
+ const { id, url, confirm } = (payload ?? {}) as { id?: string; url?: string; confirm?: boolean };
801
+ if (!id && !url) return { error: "db:restore requires { id } (a backup in this stage's bucket) or { url } (a presigned cross-stage hand-off — db:fork mints one)" };
802
+ if (id && url) return { error: 'db:restore takes { id } or { url }, not both' };
802
803
  if (confirm !== true) {
803
804
  return { error: 'db:restore is destructive (drops + replaces the target DB). Pass confirm:true to proceed.' };
804
805
  }
805
806
  const r = await resolveBackupDeps();
806
807
  if ('error' in r) return { error: r.error };
807
- const { runRestore } = await import('./backup-run');
808
+ const { runRestore, runRestoreFromUrl } = await import('./backup-run');
808
809
  try {
809
- return await runRestore(r.deps, id);
810
+ return url ? await runRestoreFromUrl(r.deps, url) : await runRestore(r.deps, id!);
810
811
  } catch (err: any) {
811
812
  return { error: err?.message || String(err) };
812
813
  }