@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 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.92",
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.32"
185
+ "@everystack/server": "0.4.33"
186
186
  },
187
187
  "scripts": {
188
188
  "test": "jest",
@@ -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 and every schema the batch will create an object of theirs in. Empty
277
- * `schemas` means the batch only GRANTs/REVOKEs as that owner: it must exist and be assumable,
278
- * but it creates nothing, so it needs CREATE nowhere. */
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
- if (r.owner && !byOwner.has(r.owner)) byOwner.set(r.owner, new Set());
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
- return [...byOwner.entries()]
305
- .map(([owner, schemas]) => ({ owner, schemas: [...schemas].sort() }))
306
- .sort((a, b) => a.owner.localeCompare(b.owner));
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
- // A grant-only owner still needs its row (does it exist, can we assume it): one with no schema.
319
- const pairs = requirements.flatMap((r) => (r.schemas.length ? r.schemas : [null])
320
- .map((s) => `(${escapeLiteral(r.owner)}, ${s === null ? 'NULL::text' : escapeLiteral(s)})`));
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 ${pairs.join(', ')})
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
- /** Null for a grant-only owner — nothing is created, so there is no schema to check. */
356
- schema_name: string | null;
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
- * Three ways a declared owner fails, all named at once with the exact statement that fixes each:
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
- if (row.schema_name == null) continue;
415
- if (!truthy(row.schema_exists)) noCreate.push({ owner, schema: String(row.schema_name), exists: false });
416
- else if (!truthy(row.owner_can_create)) noCreate.push({ owner, schema: String(row.schema_name), exists: true });
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\`: it ENACTS these grants (and ADOPTS any`,
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