@everystack/cli 0.4.65 → 0.4.66
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/package.json +2 -2
- package/src/cli/authz-compile.ts +21 -22
- package/src/cli/derived-lint.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everystack/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.66",
|
|
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>",
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"structured-headers": "1.0.1",
|
|
127
127
|
"tsx": "4.21.0",
|
|
128
128
|
"typescript": "5.9.3",
|
|
129
|
-
"@everystack/model": "0.4.
|
|
129
|
+
"@everystack/model": "0.4.17"
|
|
130
130
|
},
|
|
131
131
|
"peerDependencies": {
|
|
132
132
|
"@everystack/server": ">=0.4.0",
|
package/src/cli/authz-compile.ts
CHANGED
|
@@ -331,54 +331,53 @@ export function compileTableContract(model: ModelDescriptor, opts: CompileOption
|
|
|
331
331
|
const isRoleRead = (a: Ability): boolean =>
|
|
332
332
|
a.action === 'read' && Boolean(a.condition.role) && !isColumnAbility(a);
|
|
333
333
|
|
|
334
|
-
//
|
|
335
|
-
//
|
|
334
|
+
// `manage` + owner/via desugars into four verb abilities for policy emission.
|
|
335
|
+
// Role-scoped `manage` (the admin bypass) passes through — it matches `hasAdminManage`
|
|
336
|
+
// directly. Grants handle `manage` natively via `verbsFor`, so this expansion is
|
|
337
|
+
// policy-compiler-local: `compileGrants` still reads `model.abilities`.
|
|
338
|
+
const abilities: readonly Ability[] = model.abilities.flatMap((a) => {
|
|
339
|
+
if (a.action !== 'manage' || !rowScoped(a)) return [a];
|
|
340
|
+
const c = a.condition;
|
|
341
|
+
return [
|
|
342
|
+
{ action: 'read' as const, condition: c },
|
|
343
|
+
{ action: 'create' as const, condition: c },
|
|
344
|
+
{ action: 'update' as const, condition: c },
|
|
345
|
+
{ action: 'delete' as const, condition: c },
|
|
346
|
+
];
|
|
347
|
+
});
|
|
348
|
+
|
|
336
349
|
const predFor = (action: Ability['action']): string | null => {
|
|
337
|
-
for (const a of
|
|
350
|
+
for (const a of abilities) {
|
|
338
351
|
if (a.action !== action || isRoleRead(a) || isColumnAbility(a)) continue;
|
|
339
352
|
const p = rawPredicate(a.condition.sql ?? a.condition.where, `${table}: can('${action}')`);
|
|
340
353
|
if (p) return p;
|
|
341
354
|
}
|
|
342
355
|
return null;
|
|
343
356
|
};
|
|
344
|
-
/**
|
|
345
|
-
* The predicate on the PUBLIC read — a read ability with neither a role nor a row scope.
|
|
346
|
-
*
|
|
347
|
-
* Read separately from {@link ownerReadPred} because the two are different BRANCHES of
|
|
348
|
-
* the same authenticated policy. Taking both from one ordered lookup made the anon
|
|
349
|
-
* policy depend on the order the abilities happened to be declared in: put the owner
|
|
350
|
-
* read first and `anon` inherited the owner branch's guard as its whole public rule.
|
|
351
|
-
*/
|
|
352
357
|
const publicReadPred = (): string | null => {
|
|
353
|
-
for (const a of
|
|
358
|
+
for (const a of abilities) {
|
|
354
359
|
if (a.action !== 'read' || a.condition.role || rowScoped(a)) continue;
|
|
355
360
|
const p = rawPredicate(a.condition.sql ?? a.condition.where, `${table}: can('read')`);
|
|
356
361
|
if (p) return p;
|
|
357
362
|
}
|
|
358
363
|
return null;
|
|
359
364
|
};
|
|
360
|
-
|
|
361
|
-
/** The predicate the OWNER read narrows ITSELF by — `can('read', { owner, sql })`. */
|
|
362
365
|
const ownerReadPred = (): string | null => {
|
|
363
|
-
for (const a of
|
|
366
|
+
for (const a of abilities) {
|
|
364
367
|
if (a.action !== 'read' || !rowScoped(a) || isColumnAbility(a)) continue;
|
|
365
368
|
const p = rawPredicate(a.condition.sql ?? a.condition.where, `${table}: can('read', { owner })`);
|
|
366
369
|
if (p) return p;
|
|
367
370
|
}
|
|
368
371
|
return null;
|
|
369
372
|
};
|
|
370
|
-
|
|
371
|
-
/** The WRITE half, when the author declared one distinct from the read half. */
|
|
372
373
|
const checkFor = (action: Ability['action']): string | null => {
|
|
373
|
-
for (const a of
|
|
374
|
+
for (const a of abilities) {
|
|
374
375
|
if (a.action !== action) continue;
|
|
375
376
|
const p = rawPredicate(a.condition.check, `${table}: can('${action}', { check })`);
|
|
376
377
|
if (p) return p;
|
|
377
378
|
}
|
|
378
379
|
return null;
|
|
379
380
|
};
|
|
380
|
-
|
|
381
|
-
const abilities = model.abilities;
|
|
382
381
|
const hasAdminManage = abilities.some((a) => a.action === 'manage' && a.condition.role === 'admin');
|
|
383
382
|
const hasPublicRead = abilities.some((a) => a.action === 'read' && !a.condition.role && !rowScoped(a));
|
|
384
383
|
// The audience is open when the public read says so. Read off the SAME abilities that make
|
|
@@ -569,8 +568,8 @@ export function compileTableContract(model: ModelDescriptor, opts: CompileOption
|
|
|
569
568
|
// key, and undefined must mean ENABLED — the pre-flag behavior — never a silent
|
|
570
569
|
// security downgrade via version skew.
|
|
571
570
|
rls: { enabled: model.rls !== false, forced: model.rls !== false && model.writtenBy === 'app' },
|
|
572
|
-
grants: compileGrants(abilities, model.privileges),
|
|
573
|
-
...(compileColumnGrants(abilities) ? { columnGrants: compileColumnGrants(abilities) } : {}),
|
|
571
|
+
grants: compileGrants(model.abilities, model.privileges),
|
|
572
|
+
...(compileColumnGrants(model.abilities) ? { columnGrants: compileColumnGrants(model.abilities) } : {}),
|
|
574
573
|
policies,
|
|
575
574
|
};
|
|
576
575
|
}
|
package/src/cli/derived-lint.ts
CHANGED
|
@@ -189,6 +189,7 @@ export function findSecdefExecuteGaps(derived: readonly DerivedDescriptor[]): De
|
|
|
189
189
|
for (const d of derived) {
|
|
190
190
|
if (d.kind !== 'function' || d.security !== 'definer' || d.abilities.length > 0) continue;
|
|
191
191
|
if (d.returns === 'trigger') continue;
|
|
192
|
+
if (d.internal) continue;
|
|
192
193
|
if (Object.keys(d.privileges ?? {}).length > 0) continue;
|
|
193
194
|
const identity = identityOf(d);
|
|
194
195
|
gaps.push({
|