@everystack/server 0.4.0 → 0.4.1
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 +3 -3
- package/src/db-doctor.ts +8 -3
- package/src/role-chain.ts +13 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everystack/server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
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>",
|
|
@@ -143,8 +143,8 @@
|
|
|
143
143
|
"sst": "4.13.1",
|
|
144
144
|
"ts-jest": "29.4.9",
|
|
145
145
|
"typescript": "5.9.3",
|
|
146
|
-
"@everystack/
|
|
147
|
-
"@everystack/
|
|
146
|
+
"@everystack/cli": "0.4.7",
|
|
147
|
+
"@everystack/auth": "0.4.0"
|
|
148
148
|
},
|
|
149
149
|
"scripts": {
|
|
150
150
|
"test": "jest",
|
package/src/db-doctor.ts
CHANGED
|
@@ -324,8 +324,13 @@ export function buildDoctorReport(probe: DoctorProbe, options: DoctorReportOptio
|
|
|
324
324
|
remediation:
|
|
325
325
|
'Run `everystack db:provision --stage <stage>` — it creates the least-privilege role chain on this '
|
|
326
326
|
+ 'database (idempotent, creates no database) AND writes the DATABASE_URL secret for you, without ever '
|
|
327
|
-
+ 'printing the credential.
|
|
328
|
-
+ '
|
|
327
|
+
+ 'printing the credential. PRECONDITION before flipping the API: the handler must escalate per request '
|
|
328
|
+
+ '(pgSettings + SET ROLE / withRole) — the authenticator holds no privileges of its own (NOINHERIT), so a '
|
|
329
|
+
+ 'handler that never escalates cannot even see the schema and every endpoint fails "relation does not exist". '
|
|
330
|
+
+ 'Then flip in TWO deploys: (1) declare `new sst.Secret("DatabaseUrl")` and link it to the API function while '
|
|
331
|
+
+ 'KEEPING the raw Postgres component linked (an explicit DatabaseUrl already outranks it, and the component is '
|
|
332
|
+
+ 'your instant rollback — unlink the secret to revert); verify with real requests and `everystack db:doctor`; '
|
|
333
|
+
+ '(2) then drop the raw Postgres component from the API link (keep it on ops/worker) and redeploy. '
|
|
329
334
|
+ 'New apps get all of this automatically from the `everystack.Postgres` construct.',
|
|
330
335
|
});
|
|
331
336
|
} else {
|
|
@@ -359,7 +364,7 @@ export function buildDoctorReport(probe: DoctorProbe, options: DoctorReportOptio
|
|
|
359
364
|
status: accessible ? 'fail' : 'pass',
|
|
360
365
|
detail: accessible
|
|
361
366
|
? `the api connection read "${table}" without setting a role — it carries ambient privileges (likely INHERIT + membership in a USING(true) role). A least-privilege role has no grants of its own and must SET ROLE first.`
|
|
362
|
-
: `the api connection cannot read "${table}" without SET ROLE — correct fail-closed behavior
|
|
367
|
+
: `the api connection cannot read "${table}" without SET ROLE — correct fail-closed behavior. NOTE the runtime requirement this creates: the handler MUST escalate per request (pgSettings + SET ROLE / withRole); a handler that never escalates fails every endpoint with "relation ... does not exist".`,
|
|
363
368
|
remediation: accessible
|
|
364
369
|
? `Connect the API as a NOINHERIT role with no table grants of its own (e.g. \`authenticator\`), so memberships only apply via SET ROLE: \`ALTER ROLE <api_role> NOINHERIT;\` and revoke any direct table grants. The handler/SSR then escalate per-request via withRole().`
|
|
365
370
|
: undefined,
|
package/src/role-chain.ts
CHANGED
|
@@ -85,13 +85,18 @@ export interface AdminRoleOptions {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
|
-
* Generate idempotent DDL for the operator (admin) login role. Membership is granted
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
88
|
+
* Generate idempotent DDL for the operator (admin) login role. Membership is granted ONE
|
|
89
|
+
* way — owner → admin — giving the admin ownership rights over every owner-created object
|
|
90
|
+
* (no REASSIGN OWNED). Never the reverse: Postgres forbids circular role membership, and
|
|
91
|
+
* the 0.4.0 reciprocal grant broke the SECOND stage on a shared RDS instance (roles are
|
|
92
|
+
* INSTANCE-level; the first stage's `migrator ∈ owner` membership made the reciprocal a
|
|
93
|
+
* cycle — 'role "migrator" is a member of role "postgres"').
|
|
94
|
+
*
|
|
95
|
+
* Adoption over creation: the grant is skipped when the membership already exists (a
|
|
96
|
+
* prior stage or a manual setup made it) and when granting would FORM a cycle (the owner
|
|
97
|
+
* is already a member of the admin role in some prior topology). A re-provision running
|
|
98
|
+
* AS the admin role (post-split, the ops connection is this role) skips the self-grant
|
|
99
|
+
* Postgres hard-errors on. Does NOT set a password — that's `db:provision`'s job.
|
|
95
100
|
*/
|
|
96
101
|
export function generateAdminRoleSQL(options: AdminRoleOptions = {}): string {
|
|
97
102
|
const adminRole = options.adminRole ?? 'migrator';
|
|
@@ -100,7 +105,7 @@ export function generateAdminRoleSQL(options: AdminRoleOptions = {}): string {
|
|
|
100
105
|
const lines: string[] = [
|
|
101
106
|
`DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${adminRole}') THEN CREATE ROLE ${adminRole} LOGIN INHERIT NOBYPASSRLS; END IF; END $$;`,
|
|
102
107
|
`ALTER ROLE ${adminRole} WITH LOGIN INHERIT NOBYPASSRLS;`,
|
|
103
|
-
`DO $$ BEGIN IF current_user <> '${adminRole}'
|
|
108
|
+
`DO $$ BEGIN IF current_user <> '${adminRole}' AND NOT pg_has_role('${adminRole}', current_user, 'MEMBER') AND NOT pg_has_role(current_user, '${adminRole}', 'MEMBER') THEN EXECUTE format('GRANT %I TO ${adminRole}', current_user); END IF; END $$;`,
|
|
104
109
|
];
|
|
105
110
|
|
|
106
111
|
return lines.join('\n') + '\n';
|