@everystack/model 0.4.19 → 0.4.20
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 +1 -1
- package/src/can.ts +17 -0
- package/src/index.ts +1 -1
- package/src/model.ts +17 -1
package/package.json
CHANGED
package/src/can.ts
CHANGED
|
@@ -138,6 +138,23 @@ export function isPublicReadAbility(ability: Ability): boolean {
|
|
|
138
138
|
&& (!ability.condition.role || ability.condition.role === 'anon');
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* The no-session audience a condition names, spelled as declared — or null.
|
|
143
|
+
*
|
|
144
|
+
* `role: 'anon'` IS the caller with no session; `policyRoles: 'public'` admits that caller. An
|
|
145
|
+
* `owner`/`via` scope compares the row to the caller's JWT claim, which neither can supply, so
|
|
146
|
+
* the pair never does what it says: under anon it matches no row, and under PUBLIC the compiler
|
|
147
|
+
* drops the open audience and emits the signed-in owner policy. ONE definition, because two
|
|
148
|
+
* surfaces enforce it: `defineModel` refuses the pair and `db:pull` never renders it.
|
|
149
|
+
*/
|
|
150
|
+
export function sessionlessAudience(
|
|
151
|
+
condition: Pick<AbilityCondition, 'role' | 'policyRoles'>,
|
|
152
|
+
): "role: 'anon'" | "policyRoles: 'public'" | null {
|
|
153
|
+
if (condition.role === 'anon') return "role: 'anon'";
|
|
154
|
+
if (condition.policyRoles === 'public') return "policyRoles: 'public'";
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
141
158
|
/**
|
|
142
159
|
* Does this ability hand out DELETE?
|
|
143
160
|
*
|
package/src/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type { TableConstraint } from './constraints';
|
|
|
20
20
|
export { sql } from './raw-sql';
|
|
21
21
|
export type { RawSql } from './raw-sql';
|
|
22
22
|
|
|
23
|
-
export { can, isColumnAbility, isPublicReadAbility, isDeleteAbility, needsSoftDeleteDeclaration, privilegesGrantDelete } from './can';
|
|
23
|
+
export { can, isColumnAbility, isPublicReadAbility, isDeleteAbility, needsSoftDeleteDeclaration, privilegesGrantDelete, sessionlessAudience } from './can';
|
|
24
24
|
export type { SoftDeleteFacts } from './can';
|
|
25
25
|
export type { Ability, AbilityCondition, Action } from './can';
|
|
26
26
|
|
package/src/model.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import type { FieldBuilder } from './field';
|
|
15
15
|
import type { Ability } from './can';
|
|
16
|
-
import { isPublicReadAbility, isDeleteAbility, needsSoftDeleteDeclaration, privilegesGrantDelete } from './can';
|
|
16
|
+
import { isPublicReadAbility, isDeleteAbility, needsSoftDeleteDeclaration, privilegesGrantDelete, sessionlessAudience } from './can';
|
|
17
17
|
import type { Relation } from './relations';
|
|
18
18
|
import type { TableConstraint } from './constraints';
|
|
19
19
|
import type { RawSql } from './raw-sql';
|
|
@@ -317,6 +317,22 @@ export function defineModel(table: string, def: ModelDefinition): ModelDescripto
|
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
// `owner`/`via` compare a row column to the caller's JWT claim; a no-session audience has
|
|
321
|
+
// none. Truthiness matches the compiler, which treats `owner: ''` as no row scope.
|
|
322
|
+
const rowScope = a.condition.owner ? 'owner' : a.condition.via ? 'via' : null;
|
|
323
|
+
const audience = sessionlessAudience(a.condition);
|
|
324
|
+
if (rowScope && audience) {
|
|
325
|
+
const outcome = audience === "role: 'anon'"
|
|
326
|
+
? 'can never match a row: anon has no session'
|
|
327
|
+
: 'is narrower than declared: PUBLIC admits callers with no session, so the open audience is dropped';
|
|
328
|
+
throw new Error(
|
|
329
|
+
`model '${table}': can('${a.action}', { ${audience}, ${rowScope}: '${a.condition[rowScope]}' }) ${outcome}. ` +
|
|
330
|
+
`${rowScope} compares the row to the signed-in user's claim. ` +
|
|
331
|
+
`Either drop ${audience} (the ability then applies to signed-in callers, their own rows), ` +
|
|
332
|
+
`or drop ${rowScope} and scope the anonymous read with columns or where.`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
320
336
|
// `policyRoles` decouples the policy's AUDIENCE from its GRANTEES, and it is deliberately
|
|
321
337
|
// amputated to one open set. Anything else would be a second spelling of the `role:` axis
|
|
322
338
|
// and could name a role no grant backs — a policy that reads as access which does not
|