@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/cli",
3
- "version": "0.4.81",
3
+ "version": "0.4.82",
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>",
Binary file
@@ -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
- // Owner-only uses the model's authenticated default. Every other grantee must be named so
374
- // the compiler emits the policy TO the live role, rather than a role-less public ability.
375
- if (!owner || role !== 'authenticated') parts.push(`role: '${role}'`);
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 (action === 'update' && policy.check && policy.check !== using) {
386
- const fragment = sqlFragment(policy.check);
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);