@everystack/cli 0.4.96 → 0.4.97

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,30 @@ 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.97] - 2026-09-26
8
+
9
+ ### Fixed
10
+
11
+ - revoke sessions when users are blocked
12
+ - converge literal and owner policy predicates
13
+ - precreate missing function overloads
14
+
15
+ ### Changed
16
+
17
+ - reuse declared derived during apply
18
+
19
+ ## [0.4.96] - 2026-09-26
20
+
21
+ ### BREAKING CHANGES
22
+
23
+ - defineModel refuses owner or via under a no-session audience
24
+
25
+ ### Fixed
26
+
27
+ - a policy that calls a declared function builds in one run
28
+ - refuse role: 'anon' combined with owner or via
29
+ - db:pull never renders role: 'anon' with owner:
30
+
7
31
  ## [0.4.95] - 2026-09-26
8
32
 
9
33
  ### BREAKING CHANGES
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/cli",
3
- "version": "0.4.96",
3
+ "version": "0.4.97",
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>",
@@ -77,6 +77,16 @@ function ownerPredicate(sqlColumn: string, type: string, claim: string, expr: st
77
77
  return `(${sqlColumn} = (${ce})::${castForType(type)})`;
78
78
  }
79
79
 
80
+ // A hand-written predicate can spell the same inline JWT owner check as `owner:`. Match only
81
+ // that complete shape, then emit the one PostgreSQL form the structured shorthand uses.
82
+ const HAND_WRITTEN_OWNER_RE =
83
+ /^\(?([a-z_][a-z0-9_]*)\s*=\s*\(\(current_setting\('request\.jwt\.claims'(?:::text)?,\s*true\)\)::jsonb\s*->>\s*'([a-z_]+)'(?:::text)?\)::([a-z]+)\)?$/i;
84
+
85
+ function normalizeHandWrittenOwnerPredicate(text: string): string | null {
86
+ const match = HAND_WRITTEN_OWNER_RE.exec(text);
87
+ return match ? ownerPredicate(match[1], match[3], match[2]) : null;
88
+ }
89
+
80
90
  /** The soft-delete guard, in deparser normal form: `(deleted_at IS NULL)`. */
81
91
  function softDeleteGuard(sqlColumn: string): string {
82
92
  return `(${sqlColumn} IS NULL)`;
@@ -315,7 +325,9 @@ function rawPredicate(value: unknown, where: string): string | null {
315
325
  // plan a DROP + CREATE that changed nothing. A bare call is the exception: the deparser
316
326
  // prints `has_permission()` with no outer parens, so wrapping it is that same churn.
317
327
  const t = text.trim();
318
- return isBareCall(t) ? t : parenthesizeOnce(t);
328
+ const owner = normalizeHandWrittenOwnerPredicate(t);
329
+ if (owner) return owner;
330
+ return isBareCall(t) || t === 'true' || t === 'false' ? t : parenthesizeOnce(t);
319
331
  }
320
332
 
321
333
  /**
@@ -1056,6 +1056,11 @@ export async function dbApplyCommand(flags: Record<string, string>): Promise<voi
1056
1056
 
1057
1057
  let landed: ApplyPlanResult | undefined;
1058
1058
  let catchUp: CatchUpReport = { ok: true, statements: [] };
1059
+ // Both gates below need the declared derived bundle. Keep its existing lazy timing — the
1060
+ // baseline gate is still the first reader — but never evaluate the models barrel twice.
1061
+ let declaredDbPromise: ReturnType<typeof loadDeclaredDerived> | undefined;
1062
+ const declaredDb = (): ReturnType<typeof loadDeclaredDerived> =>
1063
+ declaredDbPromise ??= loadDeclaredDerived(flags.models);
1059
1064
  try {
1060
1065
  // The edge as hashes — internal bookkeeping, not what a reader came for. Verbose only.
1061
1066
  if (flags.verbose === 'true') info(`plan ${planHash(plan).slice(0, 12)}: ${plan.fromFingerprint.slice(0, 12)} → ${plan.toFingerprint.slice(0, 12)} (${plan.executable} statement(s)${plan.destructive ? `, ${plan.destructive} destructive` : ''})`);
@@ -1110,7 +1115,7 @@ export async function dbApplyCommand(flags: Record<string, string>): Promise<voi
1110
1115
  verifyAuthzBaseline: async ({ contract }) => {
1111
1116
  const stageName = flags.stage || 'local';
1112
1117
  const models = await loadModels(resolveModelsPath(flags.models));
1113
- const governedRoles = (await loadDeclaredDerived(flags.models))?.governedRoles ?? [];
1118
+ const governedRoles = (await declaredDb())?.governedRoles ?? [];
1114
1119
  const declaredAuthz = { tables: models.map((m) => compileTableContract(m, {})), functions: [] };
1115
1120
  const exemptions = ungovernedGrants(contract, governedRoleSet(declaredAuthz, governedRoles));
1116
1121
  const violations = checkAgainstBaseline(await readBaselineFile(), stageName, exemptions);
@@ -1125,8 +1130,8 @@ export async function dbApplyCommand(flags: Record<string, string>): Promise<voi
1125
1130
  // missing ones; --state-only asked for no derived writes, so it is skipped there.
1126
1131
  ...(flags['state-only'] === 'true' ? {} : {
1127
1132
  beforeStateBatch: async (statements: readonly string[]) => {
1128
- const declaredDb = await loadDeclaredDerived(flags.models);
1129
- const created = await createStateReferencedFunctions(runner, session, statements, declaredDb?.objects ?? [], {
1133
+ const declared = await declaredDb();
1134
+ const created = await createStateReferencedFunctions(runner, session, statements, declared?.objects ?? [], {
1130
1135
  actor: process.env.USER ?? null, gitRef: currentGitRef() ?? plan.gitRef,
1131
1136
  });
1132
1137
  if (created.length > 0) info(`created ${created.length} declared function(s) the plan's policies call, before the plan: ${created.join(', ')}`);
@@ -25,6 +25,7 @@ import type { QueryRunner } from './authz-contract';
25
25
  import type { SessionRunner } from './session';
26
26
  import type { SourceObject } from './derived-source';
27
27
  import { executeReconcile } from './commands/db-reconcile';
28
+ import { functionIdentity } from './pg-argtypes';
28
29
 
29
30
  const escapeRe = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
30
31
 
@@ -72,12 +73,17 @@ export async function createStateReferencedFunctions(
72
73
  if (referenced.length === 0) return [];
73
74
 
74
75
  const live = await runner(
75
- `SELECT n.nspname AS schema, p.proname AS name FROM pg_proc p
76
+ `SELECT n.nspname AS schema, p.proname AS name,
77
+ (SELECT COALESCE(string_agg(pg_catalog.format_type(t.oid, NULL), ', ' ORDER BY t.ord), '')
78
+ FROM unnest(p.proargtypes::oid[]) WITH ORDINALITY AS t(oid, ord)) AS args
79
+ FROM pg_proc p
76
80
  JOIN pg_namespace n ON n.oid = p.pronamespace
77
81
  WHERE p.proname IN (${[...new Set(referenced.map((o) => o.name))].map(literal).join(', ')})`,
78
82
  );
79
- const present = new Set(live.map((r: any) => `${r.schema}.${r.name}`));
80
- const missing = referenced.filter((o) => !present.has(`${o.schema}.${o.name}`));
83
+ const present = new Set(live.map((r: any) => functionIdentity(
84
+ String(r.schema), String(r.name), String(r.args).trim() ? String(r.args).split(',').map((arg) => arg.trim()) : [],
85
+ )));
86
+ const missing = referenced.filter((o) => !present.has(o.identity));
81
87
  if (missing.length === 0) return [];
82
88
 
83
89
  const run = await executeReconcile(runner, session, {