@everystack/server 0.4.3 → 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.
- package/package.json +2 -2
- package/src/db.ts +79 -45
- 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.
|
|
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>",
|
|
@@ -151,8 +151,8 @@
|
|
|
151
151
|
"sst": "4.13.1",
|
|
152
152
|
"ts-jest": "29.4.9",
|
|
153
153
|
"typescript": "5.9.3",
|
|
154
|
-
"@everystack/cli": "0.4.12",
|
|
155
154
|
"@everystack/auth": "0.4.0",
|
|
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
|
-
*
|
|
16
|
-
*
|
|
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
|
|
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
|
-
|
|
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 (
|
|
36
|
-
const pascal =
|
|
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 (
|
|
39
|
-
const raw =
|
|
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 =
|
|
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 (
|
|
77
|
-
const pascal =
|
|
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 (
|
|
80
|
-
const raw =
|
|
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,17 +109,45 @@ export function getAdminDatabaseUrl(): string | null {
|
|
|
85
109
|
}
|
|
86
110
|
|
|
87
111
|
export function getJwtSecret(): Uint8Array {
|
|
88
|
-
// PascalCase
|
|
89
|
-
const pascal =
|
|
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
|
|
92
|
-
const raw =
|
|
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);
|
|
96
120
|
throw new Error('JWT_SECRET not set — link an sst.Secret or set process.env.JWT_SECRET');
|
|
97
121
|
}
|
|
98
122
|
|
|
123
|
+
/**
|
|
124
|
+
* The ssl option for a connection URL: an explicit `sslmode` query param wins
|
|
125
|
+
* (return no override — postgres.js honors the URL natively), otherwise default
|
|
126
|
+
* to 'require' (RDS). This is what lets ONE handler serve both venues: local
|
|
127
|
+
* Postgres appends `?sslmode=disable` to its URL, deployed stages carry no
|
|
128
|
+
* param and keep TLS. An unparseable URL fails toward TLS and lets the connect
|
|
129
|
+
* report the real error.
|
|
130
|
+
*/
|
|
131
|
+
export function sslOptionForUrl(url: string): 'require' | undefined {
|
|
132
|
+
try {
|
|
133
|
+
return new URL(url).searchParams.has('sslmode') ? undefined : 'require';
|
|
134
|
+
} catch {
|
|
135
|
+
return 'require';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** postgres.js options shared by every connection — ssl derived from the URL. */
|
|
140
|
+
function connectionOptions(url: string, maxConnections?: number): Parameters<typeof postgres>[1] {
|
|
141
|
+
const ssl = sslOptionForUrl(url);
|
|
142
|
+
return {
|
|
143
|
+
max: maxConnections ?? 1,
|
|
144
|
+
idle_timeout: 60,
|
|
145
|
+
connect_timeout: 5,
|
|
146
|
+
max_lifetime: 60 * 5,
|
|
147
|
+
...(ssl ? { ssl } : {}),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
99
151
|
// Lazy singleton DB connection
|
|
100
152
|
let dbInstance: ReturnType<typeof drizzle> | null = null;
|
|
101
153
|
let sqlClient: ReturnType<typeof postgres> | null = null;
|
|
@@ -110,13 +162,7 @@ export function createDb<T extends Record<string, unknown>>(
|
|
|
110
162
|
if (dbInstance) return { db: dbInstance, schema };
|
|
111
163
|
|
|
112
164
|
const url = getDatabaseUrl();
|
|
113
|
-
sqlClient = postgres(url,
|
|
114
|
-
max: options?.maxConnections ?? 1,
|
|
115
|
-
idle_timeout: 60,
|
|
116
|
-
connect_timeout: 5,
|
|
117
|
-
max_lifetime: 60 * 5,
|
|
118
|
-
ssl: 'require',
|
|
119
|
-
});
|
|
165
|
+
sqlClient = postgres(url, connectionOptions(url, options?.maxConnections));
|
|
120
166
|
dbInstance = drizzle(sqlClient, { schema });
|
|
121
167
|
return { db: dbInstance, schema };
|
|
122
168
|
}
|
|
@@ -242,13 +288,7 @@ export function createAdminDb<T extends Record<string, unknown>>(
|
|
|
242
288
|
return createDb(schema, options);
|
|
243
289
|
}
|
|
244
290
|
|
|
245
|
-
adminSqlClient = postgres(adminUrl,
|
|
246
|
-
max: options?.maxConnections ?? 1,
|
|
247
|
-
idle_timeout: 60,
|
|
248
|
-
connect_timeout: 5,
|
|
249
|
-
max_lifetime: 60 * 5,
|
|
250
|
-
ssl: 'require',
|
|
251
|
-
});
|
|
291
|
+
adminSqlClient = postgres(adminUrl, connectionOptions(adminUrl, options?.maxConnections));
|
|
252
292
|
adminDbInstance = drizzle(adminSqlClient, { schema });
|
|
253
293
|
return { db: adminDbInstance, schema };
|
|
254
294
|
}
|
|
@@ -272,11 +312,11 @@ export function getAdminSql(): ReturnType<typeof postgres> {
|
|
|
272
312
|
* different host. Returns null when no replica is configured.
|
|
273
313
|
*/
|
|
274
314
|
export function getReadDatabaseUrl(): string | null {
|
|
275
|
-
// SST Secret — PascalCase (
|
|
276
|
-
const pascal =
|
|
315
|
+
// SST Secret — PascalCase (SST_RESOURCE_ReadDatabaseUrl → { value })
|
|
316
|
+
const pascal = sstLink('ReadDatabaseUrl')?.value as string | undefined;
|
|
277
317
|
if (pascal) return pascal;
|
|
278
|
-
// SST Secret — raw env name (
|
|
279
|
-
const raw =
|
|
318
|
+
// SST Secret — raw env name (SST_RESOURCE_READ_DATABASE_URL → { value })
|
|
319
|
+
const raw = sstLink('READ_DATABASE_URL')?.value as string | undefined;
|
|
280
320
|
if (raw) return raw;
|
|
281
321
|
// process.env fallback
|
|
282
322
|
if (process.env.READ_DATABASE_URL) return process.env.READ_DATABASE_URL;
|
|
@@ -314,13 +354,7 @@ export function createReadDb<T extends Record<string, unknown>>(
|
|
|
314
354
|
const url = getReadDatabaseUrl();
|
|
315
355
|
if (!url) return createDb(schema, options);
|
|
316
356
|
|
|
317
|
-
readSqlClient = postgres(url,
|
|
318
|
-
max: options?.maxConnections ?? 1,
|
|
319
|
-
idle_timeout: 60,
|
|
320
|
-
connect_timeout: 5,
|
|
321
|
-
max_lifetime: 60 * 5,
|
|
322
|
-
ssl: 'require',
|
|
323
|
-
});
|
|
357
|
+
readSqlClient = postgres(url, connectionOptions(url, options?.maxConnections));
|
|
324
358
|
readDbInstance = drizzle(readSqlClient, { schema });
|
|
325
359
|
return { db: readDbInstance, schema };
|
|
326
360
|
}
|
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
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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(
|