@everystack/server 0.4.4 → 0.4.5

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.
Files changed (3) hide show
  1. package/package.json +2 -2
  2. package/src/db.ts +48 -24
  3. package/src/plugin.ts +10 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/server",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
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>",
@@ -152,7 +152,7 @@
152
152
  "ts-jest": "29.4.9",
153
153
  "typescript": "5.9.3",
154
154
  "@everystack/auth": "0.4.0",
155
- "@everystack/cli": "0.4.18",
155
+ "@everystack/cli": "0.4.19",
156
156
  "@everystack/pg-tools": "0.4.0"
157
157
  },
158
158
  "scripts": {
package/src/db.ts CHANGED
@@ -7,19 +7,26 @@
7
7
  * transparently. Works with createSecrets() or manual sst.Secret declarations.
8
8
  */
9
9
 
10
- import { Resource } from 'sst';
11
10
  import { drizzle } from 'drizzle-orm/postgres-js';
12
11
  import postgres from 'postgres';
13
12
 
14
13
  /**
15
- * Safely read a Resource property. Returns undefined if the Resource
16
- * or property doesn't exist (avoids runtime throw from SST's proxy).
14
+ * Read an SST-linked resource WITHOUT importing the `sst` package. SST injects
15
+ * every link as `process.env.SST_RESOURCE_<Name>` (a JSON blob) — SST's own
16
+ * Resource proxy just parses that env var (see sst/dist/resource/shared.js:
17
+ * `raw[name] = JSON.parse(process.env['SST_RESOURCE_' + name])`). Reading it
18
+ * directly means this module never imports `sst`, which is a deploy-time
19
+ * construct: Metro/esbuild cannot bundle it, and SST's own build-time
20
+ * version-check plugin mis-reads a node_module's `sst` import and fails the
21
+ * build. So the API/SSR helpers stayed unbundleable in the exact runtime half
22
+ * the framework targets. Returns undefined when the link is absent — every
23
+ * getter then falls through to process.env, unchanged.
17
24
  */
18
- function tryResource<T>(fn: () => T): T | undefined {
25
+ function sstLink<T = Record<string, any>>(name: string): T | undefined {
26
+ const raw = process.env[`SST_RESOURCE_${name}`];
27
+ if (!raw) return undefined;
19
28
  try {
20
- const val = fn();
21
- // SST's Resource proxy may return undefined for missing keys
22
- return val === undefined ? undefined : val;
29
+ return JSON.parse(raw) as T;
23
30
  } catch {
24
31
  return undefined;
25
32
  }
@@ -32,11 +39,11 @@ export function getDatabaseUrl(): string {
32
39
  // resolved first, setting DATABASE_URL to a least-privilege role would be silently
33
40
  // ignored. See docs/plans/credential-split-rls-everywhere.md.
34
41
 
35
- // SST Secret — PascalCase (Resource.DatabaseUrl.value)
36
- const pascal = tryResource(() => (Resource as any).DatabaseUrl?.value as string);
42
+ // SST Secret — PascalCase (SST_RESOURCE_DatabaseUrl → { value })
43
+ const pascal = sstLink('DatabaseUrl')?.value as string | undefined;
37
44
  if (pascal) return pascal;
38
- // SST Secret — raw env name (Resource.DATABASE_URL.value)
39
- const raw = tryResource(() => (Resource as any).DATABASE_URL?.value as string);
45
+ // SST Secret — raw env name (SST_RESOURCE_DATABASE_URL → { value })
46
+ const raw = sstLink('DATABASE_URL')?.value as string | undefined;
40
47
  if (raw) return raw;
41
48
  // process.env fallback
42
49
  if (process.env.DATABASE_URL) return process.env.DATABASE_URL;
@@ -54,8 +61,25 @@ export function getDatabaseUrl(): string {
54
61
  * no dedicated ADMIN_DATABASE_URL is provisioned — kept separate from getDatabaseUrl() so the
55
62
  * operator path never accidentally resolves the least-privilege DATABASE_URL secret.
56
63
  */
64
+ export interface DatabaseComponentInfo {
65
+ username: string;
66
+ password: string;
67
+ host: string;
68
+ port: number;
69
+ database: string;
70
+ }
71
+
72
+ /**
73
+ * The linked SST Postgres component's connection fields, or null when none is linked.
74
+ * The single reader of the `Database` link — callers that only need host/port/database
75
+ * (db:psql, provision probe) use this instead of importing `sst`.
76
+ */
77
+ export function getDatabaseComponentInfo(): DatabaseComponentInfo | null {
78
+ return sstLink<DatabaseComponentInfo>('Database') ?? null;
79
+ }
80
+
57
81
  export function getMasterDatabaseUrl(): string | null {
58
- const db = tryResource(() => Resource.Database);
82
+ const db = getDatabaseComponentInfo();
59
83
  if (db?.host) {
60
84
  return `postgresql://${db.username}:${db.password}@${db.host}:${db.port}/${db.database}`;
61
85
  }
@@ -73,11 +97,11 @@ export function getMasterDatabaseUrl(): string | null {
73
97
  * Returns null when no admin URL is configured (single-credential setup).
74
98
  */
75
99
  export function getAdminDatabaseUrl(): string | null {
76
- // SST Secret — PascalCase (Resource.AdminDatabaseUrl.value)
77
- const pascal = tryResource(() => (Resource as any).AdminDatabaseUrl?.value as string);
100
+ // SST Secret — PascalCase (SST_RESOURCE_AdminDatabaseUrl → { value })
101
+ const pascal = sstLink('AdminDatabaseUrl')?.value as string | undefined;
78
102
  if (pascal) return pascal;
79
- // SST Secret — raw env name (Resource.ADMIN_DATABASE_URL.value)
80
- const raw = tryResource(() => (Resource as any).ADMIN_DATABASE_URL?.value as string);
103
+ // SST Secret — raw env name (SST_RESOURCE_ADMIN_DATABASE_URL → { value })
104
+ const raw = sstLink('ADMIN_DATABASE_URL')?.value as string | undefined;
81
105
  if (raw) return raw;
82
106
  // process.env fallback
83
107
  if (process.env.ADMIN_DATABASE_URL) return process.env.ADMIN_DATABASE_URL;
@@ -85,11 +109,11 @@ export function getAdminDatabaseUrl(): string | null {
85
109
  }
86
110
 
87
111
  export function getJwtSecret(): Uint8Array {
88
- // PascalCase Resource (Resource.JwtSecret.value)
89
- const pascal = tryResource(() => (Resource as any).JwtSecret?.value as string);
112
+ // PascalCase Secret (SST_RESOURCE_JwtSecret → { value })
113
+ const pascal = sstLink('JwtSecret')?.value as string | undefined;
90
114
  if (pascal) return new TextEncoder().encode(pascal);
91
- // Raw env name Resource (Resource.JWT_SECRET.value)
92
- const raw = tryResource(() => (Resource as any).JWT_SECRET?.value as string);
115
+ // Raw env name Secret (SST_RESOURCE_JWT_SECRET → { value })
116
+ const raw = sstLink('JWT_SECRET')?.value as string | undefined;
93
117
  if (raw) return new TextEncoder().encode(raw);
94
118
  // process.env fallback
95
119
  if (process.env.JWT_SECRET) return new TextEncoder().encode(process.env.JWT_SECRET);
@@ -288,11 +312,11 @@ export function getAdminSql(): ReturnType<typeof postgres> {
288
312
  * different host. Returns null when no replica is configured.
289
313
  */
290
314
  export function getReadDatabaseUrl(): string | null {
291
- // SST Secret — PascalCase (Resource.ReadDatabaseUrl.value)
292
- const pascal = tryResource(() => (Resource as any).ReadDatabaseUrl?.value as string);
315
+ // SST Secret — PascalCase (SST_RESOURCE_ReadDatabaseUrl → { value })
316
+ const pascal = sstLink('ReadDatabaseUrl')?.value as string | undefined;
293
317
  if (pascal) return pascal;
294
- // SST Secret — raw env name (Resource.READ_DATABASE_URL.value)
295
- const raw = tryResource(() => (Resource as any).READ_DATABASE_URL?.value as string);
318
+ // SST Secret — raw env name (SST_RESOURCE_READ_DATABASE_URL → { value })
319
+ const raw = sstLink('READ_DATABASE_URL')?.value as string | undefined;
296
320
  if (raw) return raw;
297
321
  // process.env fallback
298
322
  if (process.env.READ_DATABASE_URL) return process.env.READ_DATABASE_URL;
package/src/plugin.ts CHANGED
@@ -42,8 +42,8 @@ export interface PluginContext {
42
42
  verifyToken: (token: string) => Promise<Record<string, unknown> | null>;
43
43
  /** Runtime environment */
44
44
  environment: string;
45
- /** Publish a background job */
46
- publishJob: (type: string, payload: unknown, options?: { schedulable?: boolean; runAt?: Date }) => Promise<string>;
45
+ /** Publish a background job. Optional — a lean app with no @everystack/jobs never sets it. */
46
+ publishJob?: (type: string, payload: unknown, options?: { schedulable?: boolean; runAt?: Date }) => Promise<string>;
47
47
  /** Cancel a scheduled job */
48
48
  cancelJob?: (jobId: string) => Promise<void>;
49
49
  /** Reschedule a pending job */
@@ -596,18 +596,12 @@ export function dbPlugin(options: DbPluginOptions): Plugin {
596
596
  if (options.connectionInfo) {
597
597
  return await options.connectionInfo();
598
598
  }
599
- try {
600
- const { Resource } = await import('sst');
601
- return {
602
- host: (Resource as any).Database.host,
603
- port: (Resource as any).Database.port,
604
- database: (Resource as any).Database.database,
605
- username: (Resource as any).Database.username,
606
- password: (Resource as any).Database.password,
607
- };
608
- } catch {
599
+ const { getDatabaseComponentInfo } = await import('./db');
600
+ const db = getDatabaseComponentInfo();
601
+ if (!db) {
609
602
  return { error: 'No connectionInfo callback provided and SST Resource.Database not available' };
610
603
  }
604
+ return { host: db.host, port: db.port, database: db.database, username: db.username, password: db.password };
611
605
  };
612
606
 
613
607
  // --- db:query ---
@@ -778,13 +772,10 @@ export function dbPlugin(options: DbPluginOptions): Plugin {
778
772
  connection = { host: info.host, port: info.port, database: info.database };
779
773
  }
780
774
  } else {
781
- try {
782
- const { Resource } = await import('sst');
783
- const db = (Resource as any).Database;
784
- if (db?.host) connection = { host: db.host, port: db.port, database: db.database };
785
- } catch {
786
- // No linked component and no callback — the admin login lands unverified.
787
- }
775
+ const { getDatabaseComponentInfo } = await import('./db');
776
+ const db = getDatabaseComponentInfo();
777
+ // No linked component and no callback — the admin login lands unverified.
778
+ if (db?.host) connection = { host: db.host, port: db.port, database: db.database };
788
779
  }
789
780
 
790
781
  return await runProvision(