@everystack/cli 0.4.92 → 0.4.93
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/CHANGELOG.md +51 -0
- package/package.json +2 -2
- package/src/cli/derived-apply.ts +56 -20
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,57 @@ All notable changes to `@everystack/cli` are documented here.
|
|
|
4
4
|
|
|
5
5
|
Format based on [Keep a Changelog](https://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
+
## [0.4.93] - 2026-09-24
|
|
8
|
+
|
|
9
|
+
### BREAKING CHANGES
|
|
10
|
+
|
|
11
|
+
- a declared owner needs USAGE on its schema; db:apply refuses without it, db:provision (server 0.4.33) grants it
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- name USAGE with CREATE for a missing schema; review follow-ups
|
|
16
|
+
|
|
17
|
+
## [0.4.92] - 2026-09-24
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- grant drift on an owned object runs as its owner
|
|
22
|
+
- an apply fails unless the grants it set really converged
|
|
23
|
+
- settle a built object's grants before checking convergence
|
|
24
|
+
- settle statements are recorded and named; pin settle-as-owner
|
|
25
|
+
|
|
26
|
+
### Documentation
|
|
27
|
+
|
|
28
|
+
- a declared owner needs USAGE on its schema to grant there
|
|
29
|
+
|
|
30
|
+
## [0.4.91] - 2026-09-24
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- one apply revokes a grantee dropped from a replaced function
|
|
35
|
+
- a lowercase public grant is PUBLIC in the function grant diff
|
|
36
|
+
|
|
37
|
+
## [0.4.90] - 2026-09-23
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- db:plan warns when the derived layer is out of sync
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- db:apply reports skipped brownfield derived objects by default
|
|
46
|
+
|
|
47
|
+
## [0.4.89] - 2026-09-22
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- outputsDescribeStage matches SST's truncated app-name prefix
|
|
52
|
+
- discoverFunction/discoverCfFunction/discoverBuckets match SST's truncated app-name prefix
|
|
53
|
+
- warn once when cloudfront:ListTagsForResource is denied during distribution discovery
|
|
54
|
+
- db:provision --rotate probes siteUrl on a no-CDN_URL stage
|
|
55
|
+
- update refuses a missing HOST_URL with or without --stage
|
|
56
|
+
- db:reconcile/db:refresh --only respects parens in function identities
|
|
57
|
+
|
|
7
58
|
## [0.4.88] - 2026-09-21
|
|
8
59
|
|
|
9
60
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everystack/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.93",
|
|
4
4
|
"description": "CLI and OTA updates for Expo apps on everystack",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"author": "Scalable Technology, Inc. <licensing@scalable.technology>",
|
|
@@ -182,7 +182,7 @@
|
|
|
182
182
|
"react": "19.2.0",
|
|
183
183
|
"ts-jest": "29.4.9",
|
|
184
184
|
"zod": "3.25.67",
|
|
185
|
-
"@everystack/server": "0.4.
|
|
185
|
+
"@everystack/server": "0.4.33"
|
|
186
186
|
},
|
|
187
187
|
"scripts": {
|
|
188
188
|
"test": "jest",
|
package/src/cli/derived-apply.ts
CHANGED
|
@@ -273,12 +273,13 @@ export function renderEnsureObjectSchemas(objects: SourceObject[]): string[] {
|
|
|
273
273
|
// Declared-owner preflight — the refusal that beats a raw Postgres error.
|
|
274
274
|
// ---------------------------------------------------------------------------
|
|
275
275
|
|
|
276
|
-
/** One declared owner
|
|
277
|
-
*
|
|
278
|
-
*
|
|
276
|
+
/** One declared owner, every schema the batch will create an object of theirs in (`schemas`: the
|
|
277
|
+
* owner needs CREATE and USAGE there), and every other schema it only GRANTs/REVOKEs in
|
|
278
|
+
* (`grantSchemas`: USAGE alone — a GRANT names `schema.object`, and that lookup needs USAGE). */
|
|
279
279
|
export interface OwnerRequirement {
|
|
280
280
|
owner: string;
|
|
281
281
|
schemas: string[];
|
|
282
|
+
grantSchemas?: string[];
|
|
282
283
|
}
|
|
283
284
|
|
|
284
285
|
/**
|
|
@@ -298,12 +299,18 @@ export function ownerRequirements(plan: ReconcilePlan, source: SourceObject[]):
|
|
|
298
299
|
if (!obj?.owner) continue;
|
|
299
300
|
(byOwner.get(obj.owner) ?? byOwner.set(obj.owner, new Set()).get(obj.owner)!).add(obj.schema);
|
|
300
301
|
}
|
|
302
|
+
const grantOnly = new Map<string, Set<string>>();
|
|
301
303
|
for (const r of plan.regrants) {
|
|
302
|
-
|
|
304
|
+
const schema = srcById.get(r.identity)?.schema;
|
|
305
|
+
if (!r.owner || !schema) continue;
|
|
306
|
+
(grantOnly.get(r.owner) ?? grantOnly.set(r.owner, new Set()).get(r.owner)!).add(schema);
|
|
303
307
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
.
|
|
308
|
+
const owners = new Set([...byOwner.keys(), ...grantOnly.keys()]);
|
|
309
|
+
return [...owners].sort().map((owner) => {
|
|
310
|
+
const schemas = [...(byOwner.get(owner) ?? [])].sort();
|
|
311
|
+
const grantSchemas = [...(grantOnly.get(owner) ?? [])].filter((s) => !schemas.includes(s)).sort();
|
|
312
|
+
return { owner, schemas, ...(grantSchemas.length ? { grantSchemas } : {}) };
|
|
313
|
+
});
|
|
307
314
|
}
|
|
308
315
|
|
|
309
316
|
/**
|
|
@@ -315,11 +322,13 @@ export function ownerRequirements(plan: ReconcilePlan, source: SourceObject[]):
|
|
|
315
322
|
* and the privilege only evaluated when it is really there.
|
|
316
323
|
*/
|
|
317
324
|
export function ownerPreflightSql(requirements: OwnerRequirement[]): string {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
.map((s) => `(${escapeLiteral(r.owner)}, ${
|
|
325
|
+
const rowsFor = (r: OwnerRequirement) => [
|
|
326
|
+
...r.schemas.map((s) => `(${escapeLiteral(r.owner)}, ${escapeLiteral(s)}, true)`),
|
|
327
|
+
...(r.grantSchemas ?? []).map((s) => `(${escapeLiteral(r.owner)}, ${escapeLiteral(s)}, false)`),
|
|
328
|
+
];
|
|
329
|
+
const triples = requirements.flatMap(rowsFor);
|
|
321
330
|
return `
|
|
322
|
-
WITH declared(owner_name, schema_name) AS (VALUES ${
|
|
331
|
+
WITH declared(owner_name, schema_name, needs_create) AS (VALUES ${triples.join(', ')})
|
|
323
332
|
SELECT
|
|
324
333
|
CURRENT_USER AS builder,
|
|
325
334
|
-- Which mechanism the applier may use. A SUPERUSER (RDS: an rds_superuser member) can hand an
|
|
@@ -335,12 +344,15 @@ SELECT
|
|
|
335
344
|
) AS builder_is_superuser,
|
|
336
345
|
d.owner_name,
|
|
337
346
|
d.schema_name,
|
|
347
|
+
d.needs_create,
|
|
338
348
|
(r.oid IS NOT NULL) AS role_exists,
|
|
339
349
|
(n.oid IS NOT NULL) AS schema_exists,
|
|
340
350
|
r.rolsuper AS owner_is_superuser,
|
|
341
351
|
CASE WHEN r.oid IS NULL THEN NULL ELSE pg_has_role(CURRENT_USER, r.oid, 'SET') END AS can_set_role,
|
|
342
352
|
CASE WHEN r.oid IS NULL OR n.oid IS NULL THEN NULL
|
|
343
|
-
ELSE has_schema_privilege(r.oid, n.oid, 'CREATE') END AS owner_can_create
|
|
353
|
+
ELSE has_schema_privilege(r.oid, n.oid, 'CREATE') END AS owner_can_create,
|
|
354
|
+
CASE WHEN r.oid IS NULL OR n.oid IS NULL THEN NULL
|
|
355
|
+
ELSE has_schema_privilege(r.oid, n.oid, 'USAGE') END AS owner_can_use
|
|
344
356
|
FROM declared d
|
|
345
357
|
LEFT JOIN pg_roles r ON r.rolname = d.owner_name
|
|
346
358
|
LEFT JOIN pg_namespace n ON n.nspname = d.schema_name
|
|
@@ -352,13 +364,15 @@ export interface OwnerPreflightRow {
|
|
|
352
364
|
builder?: unknown;
|
|
353
365
|
builder_is_superuser?: unknown;
|
|
354
366
|
owner_name: string;
|
|
355
|
-
|
|
356
|
-
|
|
367
|
+
schema_name: string;
|
|
368
|
+
/** False for a schema the owner only GRANTs/REVOKEs in — USAGE is checked, CREATE is not. */
|
|
369
|
+
needs_create?: unknown;
|
|
357
370
|
role_exists?: unknown;
|
|
358
371
|
schema_exists?: unknown;
|
|
359
372
|
owner_is_superuser?: unknown;
|
|
360
373
|
can_set_role?: unknown;
|
|
361
374
|
owner_can_create?: unknown;
|
|
375
|
+
owner_can_use?: unknown;
|
|
362
376
|
}
|
|
363
377
|
|
|
364
378
|
const truthy = (v: unknown): boolean => v === true || v === 't' || v === 'true';
|
|
@@ -377,7 +391,7 @@ export function builderOwnerMode(rows: OwnerPreflightRow[]): OwnerApplyMode {
|
|
|
377
391
|
/**
|
|
378
392
|
* The refusal text, or null when every declared owner is usable.
|
|
379
393
|
*
|
|
380
|
-
*
|
|
394
|
+
* Four ways a declared owner fails, all named at once with the exact statement that fixes each:
|
|
381
395
|
*
|
|
382
396
|
* 1. THE ROLE DOES NOT EXIST. The build refuses and does NOT create it. A role is
|
|
383
397
|
* cluster-scoped and a security principal, and a principal invented by a tool cannot know
|
|
@@ -389,6 +403,9 @@ export function builderOwnerMode(rows: OwnerPreflightRow[]): OwnerApplyMode {
|
|
|
389
403
|
* 3. THE OWNER CANNOT CREATE IN THE SCHEMA. Same mechanism — the CREATE happens AS the owner,
|
|
390
404
|
* so the owner needs CREATE on the schema the object lives in. A schema that does not exist
|
|
391
405
|
* yet counts: this build creates it as the BUILDER, and the owner will not hold CREATE on it.
|
|
406
|
+
* 4. THE OWNER CANNOT USE THE SCHEMA. Every GRANT/REVOKE it runs on its objects (attachments,
|
|
407
|
+
* a replace's revokes, regrants, the settle pass) names `schema.object`, and resolving that
|
|
408
|
+
* needs USAGE — checked for schemas it creates in AND schemas it only grants in.
|
|
392
409
|
*/
|
|
393
410
|
export function ownerPreflightRefusal(rows: OwnerPreflightRow[]): string | null {
|
|
394
411
|
const builder = rows.find((r) => r.builder != null)?.builder;
|
|
@@ -396,6 +413,7 @@ export function ownerPreflightRefusal(rows: OwnerPreflightRow[]): string | null
|
|
|
396
413
|
const missing = new Set<string>();
|
|
397
414
|
const unassumable = new Set<string>();
|
|
398
415
|
const noCreate: Array<{ owner: string; schema: string; exists: boolean }> = [];
|
|
416
|
+
const noUsage: Array<{ owner: string; schema: string }> = [];
|
|
399
417
|
|
|
400
418
|
// A superuser builder creates as itself and ALTERs the owner, so it needs neither membership in
|
|
401
419
|
// the owner nor CREATE on the owner's behalf. Refusing on either would be a false refusal that
|
|
@@ -411,16 +429,28 @@ export function ownerPreflightRefusal(rows: OwnerPreflightRow[]): string | null
|
|
|
411
429
|
// A superuser owner holds CREATE everywhere, including in a schema this build is about to
|
|
412
430
|
// make — there is nothing to refuse.
|
|
413
431
|
if (truthy(row.owner_is_superuser)) continue;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
432
|
+
const schema = String(row.schema_name);
|
|
433
|
+
const needsCreate = truthy(row.needs_create);
|
|
434
|
+
if (!truthy(row.schema_exists)) {
|
|
435
|
+
// Named whole, in one round: the owner will need both once the builder creates the schema.
|
|
436
|
+
if (needsCreate) {
|
|
437
|
+
noCreate.push({ owner, schema, exists: false });
|
|
438
|
+
noUsage.push({ owner, schema });
|
|
439
|
+
}
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (needsCreate && !truthy(row.owner_can_create)) noCreate.push({ owner, schema, exists: true });
|
|
443
|
+
// Every GRANT/REVOKE the owner runs names `schema.object`, and resolving it needs USAGE. An
|
|
444
|
+
// owned function always has one (its REVOKE ALL … FROM PUBLIC), so this was a mid-batch
|
|
445
|
+
// `permission denied for schema` before it was a refusal.
|
|
446
|
+
if (!truthy(row.owner_can_use)) noUsage.push({ owner, schema });
|
|
417
447
|
}
|
|
418
448
|
|
|
419
|
-
if (!missing.size && !unassumable.size && !noCreate.length) return null;
|
|
449
|
+
if (!missing.size && !unassumable.size && !noCreate.length && !noUsage.length) return null;
|
|
420
450
|
|
|
421
451
|
const lines: string[] = [
|
|
422
452
|
`declared owner(s) cannot be enacted by '${builderName}' — refused before any DDL, so nothing is half-applied.`,
|
|
423
|
-
`The one-command fix is \`everystack db:provision
|
|
453
|
+
`The one-command fix is \`everystack db:provision\` (USAGE needs @everystack/server >= 0.4.33 on the stage): it ENACTS these grants (and ADOPTS any`,
|
|
424
454
|
`pre-existing objects owned by the master) from the declared model, so you never hand-write them.`,
|
|
425
455
|
`The exact statements, if you must run them by hand:`,
|
|
426
456
|
];
|
|
@@ -443,6 +473,12 @@ export function ownerPreflightRefusal(rows: OwnerPreflightRow[]): string | null
|
|
|
443
473
|
` GRANT CREATE ON SCHEMA ${schema} TO ${owner};`,
|
|
444
474
|
);
|
|
445
475
|
}
|
|
476
|
+
for (const { owner, schema } of noUsage) {
|
|
477
|
+
lines.push(
|
|
478
|
+
` NO USAGE ON SCHEMA '${owner}' cannot use schema "${schema}" — its GRANT/REVOKE on the objects there name the schema:`,
|
|
479
|
+
` GRANT USAGE ON SCHEMA ${schema} TO ${owner};`,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
446
482
|
return lines.join('\n');
|
|
447
483
|
}
|
|
448
484
|
|