@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
package/src/cli/authz-compile.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
1129
|
-
const created = await createStateReferencedFunctions(runner, session, statements,
|
|
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
|
|
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) =>
|
|
80
|
-
|
|
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, {
|