@everystack/model 0.4.27 → 0.4.28
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 +6 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/manage-scope.ts +30 -0
- package/src/model.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ All notable changes to `@everystack/model` are documented here.
|
|
|
4
4
|
|
|
5
5
|
Format based on [Keep a Changelog](https://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
+
## [0.4.28] - 2026-11-05
|
|
8
|
+
|
|
9
|
+
### BREAKING CHANGES
|
|
10
|
+
|
|
11
|
+
- refuse a manage ability whose condition would be dropped
|
|
12
|
+
|
|
7
13
|
## [0.4.27] - 2026-11-04
|
|
8
14
|
|
|
9
15
|
### BREAKING CHANGES
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -53,6 +53,7 @@ export type {
|
|
|
53
53
|
|
|
54
54
|
export { deriveHandlerConfig, deriveForceRlsCarveouts, deriveFunctionSchemas } from './derive';
|
|
55
55
|
export { assertNoLegacySoftDelete } from './legacy-soft-delete';
|
|
56
|
+
export { assertManageScope } from './manage-scope';
|
|
56
57
|
export type { DerivedHandlerConfig, RelationConfig } from './derive';
|
|
57
58
|
|
|
58
59
|
export { ENSURE_DESTROY_LOG_SQL, DESTROY_LOG_MARKER_BODIES, DESTROY_LOG_MARKER_LANGUAGES, DESTROY_LOG_MARKER_SEARCH_PATH } from './destroy-log';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Ability } from './can';
|
|
2
|
+
|
|
3
|
+
const CONDITION_KEYS = ['where', 'sql', 'check'] as const;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A `manage` with no owner and no via compiles to an ALL policy USING true for its role, or, for
|
|
7
|
+
* an authenticated manage on a row-scoped model, to the model's own row scope. Neither path
|
|
8
|
+
* reads the ability's where / sql / check, so `can('manage', { role: 'editor', where })` would
|
|
9
|
+
* let the editor manage every row. Refuse it, whatever the value (`sql\`true\`` included).
|
|
10
|
+
*
|
|
11
|
+
* defineModel calls this, and so does compileTableContract, for a descriptor that never went
|
|
12
|
+
* through defineModel. One message, two callers.
|
|
13
|
+
*/
|
|
14
|
+
export function assertManageScope(table: string, abilities: readonly Ability[] | undefined): void {
|
|
15
|
+
for (const a of abilities ?? []) {
|
|
16
|
+
if (a.action !== 'manage' || a.condition.owner || a.condition.via) continue;
|
|
17
|
+
const keys = CONDITION_KEYS.filter((k) => a.condition[k] !== undefined);
|
|
18
|
+
if (keys.length === 0) continue;
|
|
19
|
+
const key = keys.join(', ');
|
|
20
|
+
const role = a.condition.role ?? 'authenticated';
|
|
21
|
+
throw new Error(
|
|
22
|
+
`model '${table}': can('manage', { role: '${role}', ${key} }) would grant every row: ` +
|
|
23
|
+
`a manage without owner or via ignores ${key}. ` +
|
|
24
|
+
// The resolved role, never a bare placeholder: a copied `can('read', { where })` with no
|
|
25
|
+
// role is a public read, wider than the manage it replaces.
|
|
26
|
+
`Declare the four abilities instead (can('read'|'create'|'update'|'delete', { role: '${role}', ${key} })), ` +
|
|
27
|
+
'or scope it with owner or via.',
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/model.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { lifecycleCascadeHarm } from './dependents';
|
|
|
22
22
|
import type { RawSql } from './raw-sql';
|
|
23
23
|
import { validateTriggersFor, type TriggerSpec, type DependsOnRef } from './derived';
|
|
24
24
|
import { TABLE_PRIVILEGES, type TablePrivilege } from './privileges';
|
|
25
|
+
import { assertManageScope } from './manage-scope';
|
|
25
26
|
|
|
26
27
|
import { assertDeleteHookNames, assertDestroyHooks, type AfterLifecycleHook, type BeforeLifecycleHook, type DestroyHook } from './lifecycle-hooks';
|
|
27
28
|
export type { AfterLifecycleHook, BeforeLifecycleHook, DestroyContext, DestroyHook, HookVeto, LifecycleContext } from './lifecycle-hooks';
|
|
@@ -467,6 +468,7 @@ function assertNoSoftDelete(table: string, def: ModelDefinition): void {
|
|
|
467
468
|
|
|
468
469
|
export function defineModel(table: string, def: ModelDefinition): ModelDescriptor {
|
|
469
470
|
assertNoSoftDelete(table, def);
|
|
471
|
+
assertManageScope(table, def.abilities);
|
|
470
472
|
assertDeleteHookNames(table, def.hooks);
|
|
471
473
|
assertDestroyHooks(table, def.hooks, isLifecycle(def));
|
|
472
474
|
// Refuse first, then inject: the author-declared collisions are checked against what the author wrote.
|