@everystack/cli 0.4.81 → 0.4.82
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 +7 -0
- package/package.json +1 -1
- package/src/cli/authz-compile.ts +0 -0
- package/src/cli/authz-derive.ts +31 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ 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.82] - 2026-09-08
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- four compiler-hardening throws close the A22/F-A10/F-A12 tail
|
|
12
|
+
- column-path check disclosure parity closes the F1 gap
|
|
13
|
+
|
|
7
14
|
## [0.4.81] - 2026-09-08
|
|
8
15
|
|
|
9
16
|
### Added
|
package/package.json
CHANGED
package/src/cli/authz-compile.ts
CHANGED
|
Binary file
|
package/src/cli/authz-derive.ts
CHANGED
|
@@ -364,15 +364,24 @@ function renderColumnAbility(
|
|
|
364
364
|
grantee: string,
|
|
365
365
|
columns: readonly string[],
|
|
366
366
|
policy: PolicyContract,
|
|
367
|
+
notes: string[],
|
|
367
368
|
): string | null {
|
|
368
369
|
const using = policy.using ?? null;
|
|
369
370
|
const owner = parseOwnerPredicate(using);
|
|
370
371
|
const parts: string[] = [];
|
|
371
372
|
const role = policyRoleForGrantee(grantee);
|
|
372
373
|
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
|
|
374
|
+
// A distinct WITH CHECK on a column UPDATE must ride as `check:`, and the compiler REFUSES a
|
|
375
|
+
// role-less column ability that carries any row predicate (F-A12: a column ability has nowhere
|
|
376
|
+
// row-level to put one). So a differing check forces the NAMED-role spelling — `role:
|
|
377
|
+
// 'authenticated'` — which routes to the compiler's named-role column path: that path ANDs the
|
|
378
|
+
// owner back into BOTH halves and honors the check, where the role-less shape would throw.
|
|
379
|
+
const distinctCheck = action === 'update' && policy.check != null && policy.check !== using;
|
|
380
|
+
|
|
381
|
+
// Owner-only uses the model's authenticated default. Every other grantee must be named so the
|
|
382
|
+
// compiler emits the policy TO the live role, not a role-less public ability — and a differing
|
|
383
|
+
// check names it too, to escape the F-A12 refusal above.
|
|
384
|
+
if (!owner || role !== 'authenticated' || distinctCheck) parts.push(`role: '${role}'`);
|
|
376
385
|
if (owner) {
|
|
377
386
|
parts.push(`owner: '${modelFieldKey(owner.column)}'`);
|
|
378
387
|
if (owner.claim !== 'sub') parts.push(`userField: '${owner.claim}'`);
|
|
@@ -382,10 +391,26 @@ function renderColumnAbility(
|
|
|
382
391
|
if (fragment) parts.push(`sql: ${fragment}`);
|
|
383
392
|
}
|
|
384
393
|
|
|
385
|
-
if (
|
|
386
|
-
|
|
394
|
+
if (distinctCheck) {
|
|
395
|
+
// Emit only the guard BEYOND the owner gate. The live WITH CHECK restates the owner term
|
|
396
|
+
// (the compiler's named-role column path ANDs it back in), so carrying the whole predicate
|
|
397
|
+
// would double the owner on recompile and churn the plan. Strip it exactly as the row-level
|
|
398
|
+
// write path does; a check with no owner conjunct rides whole (guard === the check).
|
|
399
|
+
const split = owner ? splitOwnerPredicate(policy.check) : null;
|
|
400
|
+
const guard = split ? split.guard : policy.check;
|
|
401
|
+
const fragment = sqlFragment(guard);
|
|
387
402
|
if (!fragment) return null;
|
|
388
403
|
parts.push(`check: ${fragment}`);
|
|
404
|
+
// Disclosure parity with the row-level write path. When USING is a PURE owner predicate but
|
|
405
|
+
// the live WITH CHECK does NOT restate the owner, the compiler ANDs the owner back into the
|
|
406
|
+
// rendered check — TIGHTENING it (the F-A11 "wider write half" the model cannot express). Say
|
|
407
|
+
// so, rather than let an unexplained conjunct surface in the next db:plan, which an operator
|
|
408
|
+
// might "fix" back toward the wider live shape by hand — the exact spelling the compiler refuses.
|
|
409
|
+
if (split && split.owner == null) {
|
|
410
|
+
notes.push(`policy "${policy.name}" (UPDATE) has a WITH CHECK that does not restate the owner gate in its USING —`);
|
|
411
|
+
notes.push(` it is wider than a column owner ability can express, so the rendered model TIGHTENS it to the owner. Live WITH CHECK:`);
|
|
412
|
+
notes.push(` ${policy.check}`);
|
|
413
|
+
}
|
|
389
414
|
}
|
|
390
415
|
parts.push(`columns: [${columns.map((column) => `'${modelFieldKey(column)}'`).join(', ')}]`);
|
|
391
416
|
return `can('${action}', { ${parts.join(', ')} })`;
|
|
@@ -739,7 +764,7 @@ export function deriveAbilities(
|
|
|
739
764
|
if (!cols.length) continue;
|
|
740
765
|
const action = priv === 'SELECT' ? 'read' : priv === 'UPDATE' ? 'update' : null;
|
|
741
766
|
const policy = action ? columnPolicyFor(contract, grantee, priv) : null;
|
|
742
|
-
const ability = action && policy ? renderColumnAbility(action, grantee, cols, policy) : null;
|
|
767
|
+
const ability = action && policy ? renderColumnAbility(action, grantee, cols, policy, notes) : null;
|
|
743
768
|
if (ability) {
|
|
744
769
|
abilities.push(ability);
|
|
745
770
|
renderedColumnRoles.add(grantee);
|