@entifix/authz 0.5.0 → 0.7.0
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/dist/policy/can.d.ts +3 -3
- package/dist/policy/can.js.map +1 -1
- package/dist/policy/service-crossing-policy.d.ts +1 -2
- package/dist/policy/service-crossing-policy.d.ts.map +1 -1
- package/dist/policy/service-crossing-policy.js.map +1 -1
- package/dist/values/guarded-command.d.ts +2 -2
- package/dist/values/guarded-command.js.map +1 -1
- package/dist/values/guarded-nav.d.ts +4 -4
- package/dist/values/guarded-nav.js.map +1 -1
- package/dist/values/permission.d.ts +2 -2
- package/dist/values/permission.js.map +1 -1
- package/dist/values/role.d.ts +53 -21
- package/dist/values/role.d.ts.map +1 -1
- package/dist/values/role.js +49 -24
- package/dist/values/role.js.map +1 -1
- package/dist/values/screen-address.d.ts +3 -5
- package/dist/values/screen-address.d.ts.map +1 -1
- package/dist/values/screen-address.js +1 -1
- package/dist/values/screen-address.js.map +1 -1
- package/dist/values/screen-type.d.ts +14 -13
- package/dist/values/screen-type.d.ts.map +1 -1
- package/dist/values/screen-type.js +14 -13
- package/dist/values/screen-type.js.map +1 -1
- package/package.json +2 -2
package/dist/policy/can.d.ts
CHANGED
|
@@ -8,9 +8,9 @@ import { type Permission } from '../values/permission.js';
|
|
|
8
8
|
* `string` rather than a closed `Role` union for the same reason: a host with
|
|
9
9
|
* roles of its own must be able to name them.
|
|
10
10
|
*
|
|
11
|
-
* A host keeps the strictness it wants on its own side —
|
|
12
|
-
* as `Record<Role, readonly Permission[]
|
|
13
|
-
* error where it is written.
|
|
11
|
+
* A host keeps the strictness it wants on its own side — declaring the table
|
|
12
|
+
* as `Record<Role, readonly Permission[]>` over its own role ladder makes a
|
|
13
|
+
* typo there a compile error where it is written.
|
|
14
14
|
*/
|
|
15
15
|
export type GrantTable = Readonly<Record<string, readonly Permission[]>>;
|
|
16
16
|
/**
|
package/dist/policy/can.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/policy/can.ts"],"sourcesContent":["import { type Permission, permissionMatches } from '../values/permission.js';\n\n/**\n * Which permissions each role grants.\n *\n * ⚠️ **Supplied by the host, never declared here.** The table that says a `user`\n * may ring up a counter sale is a statement about one product, and a framework\n * that ships one has decided its adopters' business for them. The key is a bare\n * `string` rather than a closed `Role` union for the same reason: a host with\n * roles of its own must be able to name them.\n *\n * A host keeps the strictness it wants on its own side —
|
|
1
|
+
{"version":3,"sources":["../../src/policy/can.ts"],"sourcesContent":["import { type Permission, permissionMatches } from '../values/permission.js';\n\n/**\n * Which permissions each role grants.\n *\n * ⚠️ **Supplied by the host, never declared here.** The table that says a `user`\n * may ring up a counter sale is a statement about one product, and a framework\n * that ships one has decided its adopters' business for them. The key is a bare\n * `string` rather than a closed `Role` union for the same reason: a host with\n * roles of its own must be able to name them.\n *\n * A host keeps the strictness it wants on its own side — declaring the table\n * as `Record<Role, readonly Permission[]>` over its own role ladder makes a\n * typo there a compile error where it is written.\n */\nexport type GrantTable = Readonly<Record<string, readonly Permission[]>>;\n\n/**\n * Expand a principal's roles into the permissions they grant. A role the table\n * does not name contributes nothing — an unknown string in a token can never\n * widen access.\n */\nexport const permissionsOf = (\n grants: GrantTable,\n roles: readonly string[],\n): readonly Permission[] => roles.flatMap(role => grants[role] ?? []);\n\n/**\n * The pure authorization check, shared by every layer: the service guard, the\n * server-rendered nav filter, and the browser. No Effect, no IO — so it runs\n * unchanged in Node, in a Next edge middleware, and in a React component.\n */\nexport const can = (\n grants: GrantTable,\n roles: readonly string[],\n required: Permission,\n): boolean =>\n permissionsOf(grants, roles).some(granted =>\n permissionMatches(granted, required),\n );\n"],"names":["permissionMatches","permissionsOf","grants","roles","flatMap","role","can","required","some","granted"],"mappings":"AAAA,SAA0BA,iBAAiB,QAAQ,0BAA0B;AAiB7E;;;;CAIC,GACD,OAAO,MAAMC,gBAAgB,CAC3BC,QACAC,QAC0BA,MAAMC,OAAO,CAACC,CAAAA;YAAQH;gBAAAA,eAAAA,MAAM,CAACG,KAAK,YAAZH,eAAgB,EAAE;OAAE;AAEtE;;;;CAIC,GACD,OAAO,MAAMI,MAAM,CACjBJ,QACAC,OACAI,WAEAN,cAAcC,QAAQC,OAAOK,IAAI,CAACC,CAAAA,UAChCT,kBAAkBS,SAASF,WAC3B"}
|
|
@@ -8,8 +8,7 @@ import { type Permission } from '../values/permission.js';
|
|
|
8
8
|
* looking a *role string* up in the grant table, so a crossing grant living
|
|
9
9
|
* there would be inherited by any access token carrying the matching string —
|
|
10
10
|
* turning a session claim into a cross-organization write. Two tables, two
|
|
11
|
-
* lookups, and nothing a session guard calls can reach this one
|
|
12
|
-
* ([ADR 0023](../../../../../../docs/adr/0023-service-to-service-tenant-crossing.md)).
|
|
11
|
+
* lookups, and nothing a session guard calls can reach this one.
|
|
13
12
|
*/
|
|
14
13
|
export interface ServiceCrossingPolicy {
|
|
15
14
|
allows(required: Permission): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-crossing-policy.d.ts","sourceRoot":"","sources":["../../src/policy/service-crossing-policy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEjC,OAAO,EAAE,KAAK,UAAU,EAAqB,MAAM,yBAAyB,CAAC;AAE7E
|
|
1
|
+
{"version":3,"file":"service-crossing-policy.d.ts","sourceRoot":"","sources":["../../src/policy/service-crossing-policy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEjC,OAAO,EAAE,KAAK,UAAU,EAAqB,MAAM,yBAAyB,CAAC;AAE7E;;;;;;;;;GASG;AACH,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,QAAQ,EAAE,UAAU,GAAG,OAAO,CAAC;CACvC;;AAED,qFAAqF;AACrF,qBAAa,wBAAyB,SAAQ,6BAGf;CAAG;AAElC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,+BAA+B,GAC1C,SAAS,SAAS,UAAU,EAAE,KAC7B,qBAGD,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,EAAE,qBAEhC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/policy/service-crossing-policy.ts"],"sourcesContent":["import { Context } from 'effect';\n\nimport { type Permission, permissionMatches } from '../values/permission.js';\n\n/**\n * What a **service** may do when it crosses into another party's tenant storage.\n *\n * ⚠️ **A second port rather than a second method on {@link PolicyDecision}, and\n * the separation is the security property.** `permissionsOf` expands grants by\n * looking a *role string* up in the grant table, so a crossing grant living\n * there would be inherited by any access token carrying the matching string —\n * turning a session claim into a cross-organization write. Two tables, two\n * lookups, and nothing a session guard calls can reach this one
|
|
1
|
+
{"version":3,"sources":["../../src/policy/service-crossing-policy.ts"],"sourcesContent":["import { Context } from 'effect';\n\nimport { type Permission, permissionMatches } from '../values/permission.js';\n\n/**\n * What a **service** may do when it crosses into another party's tenant storage.\n *\n * ⚠️ **A second port rather than a second method on {@link PolicyDecision}, and\n * the separation is the security property.** `permissionsOf` expands grants by\n * looking a *role string* up in the grant table, so a crossing grant living\n * there would be inherited by any access token carrying the matching string —\n * turning a session claim into a cross-organization write. Two tables, two\n * lookups, and nothing a session guard calls can reach this one.\n */\nexport interface ServiceCrossingPolicy {\n allows(required: Permission): boolean;\n}\n\n/** DI tag the composition root binds to a concrete {@link ServiceCrossingPolicy}. */\nexport class ServiceCrossingPolicyTag extends Context.Service<\n ServiceCrossingPolicyTag,\n ServiceCrossingPolicy\n>()('ServiceCrossingPolicyTag') {}\n\n/**\n * The allowlist implementation: a host hands over the whole of what its fleet\n * may do, in one list.\n *\n * The comparison goes through `permissionMatches` rather than `includes`, so a\n * required permission is matched by the same rule everywhere in the system;\n * what differs is only which list is consulted.\n *\n * ⚠️ **A host must not put a wildcard in this list.** A `*` segment would make\n * fleet membership itself the capability, which is the distinction the\n * token/permission split exists to draw: the token proves the caller is the\n * fleet, the list says what the fleet may do.\n */\nexport const makeStaticServiceCrossingPolicy = (\n allowed: readonly Permission[],\n): ServiceCrossingPolicy => ({\n allows: required =>\n allowed.some(granted => permissionMatches(granted, required)),\n});\n\n/**\n * Refuses every crossing.\n *\n * The right default for an application that has none — an example app, a single\n * service, anything with no saga reaching across a tenant boundary. Supplied\n * explicitly rather than left as a missing Layer, because a missing Layer is a\n * compile error at the composition root and \"we do no crossings\" deserves to be\n * sayable.\n */\nexport const NoServiceCrossings: ServiceCrossingPolicy = {\n allows: () => false,\n};\n"],"names":["Context","permissionMatches","ServiceCrossingPolicyTag","Service","makeStaticServiceCrossingPolicy","allowed","allows","required","some","granted","NoServiceCrossings"],"mappings":"AAAA,SAASA,OAAO,QAAQ,SAAS;AAEjC,SAA0BC,iBAAiB,QAAQ,0BAA0B;AAgB7E,mFAAmF,GACnF,OAAO,MAAMC,iCAAiCF,QAAQG,OAAO,GAGzD;AAA6B;AAEjC;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,kCAAkC,CAC7CC,UAC2B,CAAA;QAC3BC,QAAQC,CAAAA,WACNF,QAAQG,IAAI,CAACC,CAAAA,UAAWR,kBAAkBQ,SAASF;IACvD,CAAA,EAAG;AAEH;;;;;;;;CAQC,GACD,OAAO,MAAMG,qBAA4C;IACvDJ,QAAQ,IAAM;AAChB,EAAE"}
|
|
@@ -44,8 +44,8 @@ export interface GuardedCommand {
|
|
|
44
44
|
/** Omit to show the command to every signed-in user. */
|
|
45
45
|
permission?: Permission;
|
|
46
46
|
/**
|
|
47
|
-
* Also gate on the acting organization's provisioning —
|
|
48
|
-
*
|
|
47
|
+
* Also gate on the acting organization's provisioning — the second ceiling,
|
|
48
|
+
* beside the role — using the domain segment of its own {@link permission}, with all
|
|
49
49
|
* the reasoning {@link GuardedNavItem.entitled} carries. Setting it without a
|
|
50
50
|
* permission is the same declaration error, and fails the same way.
|
|
51
51
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/values/guarded-command.ts"],"sourcesContent":["import type { Permission } from './permission.js';\n\n/**\n * A deterministic command plus the permission it requires, if any.\n *\n * The sibling of {@link GuardedNavItem}, and here for the identical reason: this\n * package is the only vocabulary a shell and the host application both reach,\n * so a framework shell can contribute commands to whichever host\n * mounts it without either of them importing the other.\n *\n * It is a *separate* type rather than a nav item with a flag because the two\n * answer different questions. A nav item is a place you can be; a command is\n * something you can do, and it is the thing that has keywords, a page and no\n * sidebar row. Folding them would put `keywordsKey` on every destination and\n * `type` on every command.\n *\n * `labelKey`/`keywordsKey` are **namespace-qualified catalog keys**, not copy —\n * whoever renders the palette does not own the wording, and spelling the\n * namespace out is also what makes an `app:` key visible to the lint rule that\n * keeps those inside `apps/`.\n *\n * Filtering on `permission`/`entitled` is **presentation only**, exactly as it\n * is for navigation: the route behind the command is what refuses the request.\n */\nexport interface GuardedCommand {\n /** Stable id, unique across every contributing shell. Also the recency key. */\n key: string;\n labelKey: string;\n /**\n * A catalog key whose value is a **comma-separated** list of extra terms this\n * command answers to — how a Spanish command still matches the English word\n * someone typed. `parseKeywords` in `@entifix/core` owns the split.\n */\n keywordsKey?: string;\n /** Locale-free, like every other internal href; the renderer prefixes it. */\n href: string;\n /**\n * Which palette page this command sits on. Omitted means the root.\n *\n * Depth is a stack of pages rather than a nested prefix, so a group of related\n * commands — every \"new record\" route, say — is one root entry that descends\n * instead of N entries competing with the destinations beside them.\n */\n page?: string;\n /** Omit to show the command to every signed-in user. */\n permission?: Permission;\n /**\n * Also gate on the acting organization's provisioning —
|
|
1
|
+
{"version":3,"sources":["../../src/values/guarded-command.ts"],"sourcesContent":["import type { Permission } from './permission.js';\n\n/**\n * A deterministic command plus the permission it requires, if any.\n *\n * The sibling of {@link GuardedNavItem}, and here for the identical reason: this\n * package is the only vocabulary a shell and the host application both reach,\n * so a framework shell can contribute commands to whichever host\n * mounts it without either of them importing the other.\n *\n * It is a *separate* type rather than a nav item with a flag because the two\n * answer different questions. A nav item is a place you can be; a command is\n * something you can do, and it is the thing that has keywords, a page and no\n * sidebar row. Folding them would put `keywordsKey` on every destination and\n * `type` on every command.\n *\n * `labelKey`/`keywordsKey` are **namespace-qualified catalog keys**, not copy —\n * whoever renders the palette does not own the wording, and spelling the\n * namespace out is also what makes an `app:` key visible to the lint rule that\n * keeps those inside `apps/`.\n *\n * Filtering on `permission`/`entitled` is **presentation only**, exactly as it\n * is for navigation: the route behind the command is what refuses the request.\n */\nexport interface GuardedCommand {\n /** Stable id, unique across every contributing shell. Also the recency key. */\n key: string;\n labelKey: string;\n /**\n * A catalog key whose value is a **comma-separated** list of extra terms this\n * command answers to — how a Spanish command still matches the English word\n * someone typed. `parseKeywords` in `@entifix/core` owns the split.\n */\n keywordsKey?: string;\n /** Locale-free, like every other internal href; the renderer prefixes it. */\n href: string;\n /**\n * Which palette page this command sits on. Omitted means the root.\n *\n * Depth is a stack of pages rather than a nested prefix, so a group of related\n * commands — every \"new record\" route, say — is one root entry that descends\n * instead of N entries competing with the destinations beside them.\n */\n page?: string;\n /** Omit to show the command to every signed-in user. */\n permission?: Permission;\n /**\n * Also gate on the acting organization's provisioning — the second ceiling,\n * beside the role — using the domain segment of its own {@link permission}, with all\n * the reasoning {@link GuardedNavItem.entitled} carries. Setting it without a\n * permission is the same declaration error, and fails the same way.\n */\n entitled?: boolean;\n}\n\n/**\n * The page every \"create a record\" command descends into.\n *\n * A shared constant rather than a literal in each shell: the domain shells\n * declare commands onto this page and the palette synthesizes the root entry\n * that opens it, so the two halves have to agree and there is no test that\n * would notice a typo — the page would simply be unreachable, with every\n * command still declared and still granted.\n */\nexport const NEW_COMMAND_PAGE = 'new';\n"],"names":["NEW_COMMAND_PAGE"],"mappings":"AAuDA;;;;;;;;CAQC,GACD,OAAO,MAAMA,mBAAmB,MAAM"}
|
|
@@ -32,8 +32,8 @@ export interface GuardedNavItem {
|
|
|
32
32
|
/** Omit to show the item to every signed-in user. */
|
|
33
33
|
permission?: Permission;
|
|
34
34
|
/**
|
|
35
|
-
* Also gate this item on the acting organization's provisioning —
|
|
36
|
-
*
|
|
35
|
+
* Also gate this item on the acting organization's provisioning — the second
|
|
36
|
+
* ceiling, beside the role — using the domain segment of its own {@link permission}.
|
|
37
37
|
*
|
|
38
38
|
* A boolean rather than a domain name, because the domain is already written
|
|
39
39
|
* one line up: a second string here could disagree with the permission the
|
|
@@ -43,7 +43,7 @@ export interface GuardedNavItem {
|
|
|
43
43
|
* **Omitted is the right answer for most items.** An organization is not
|
|
44
44
|
* "provisioned for" `catalog-reference`, `config` or `authn` — those are
|
|
45
45
|
* operator-owned, and `catalog-reference` is explicitly never grantable
|
|
46
|
-
|
|
46
|
+
*. Setting this on one of them would hide the platform's own
|
|
47
47
|
* vocabulary from every vendor.
|
|
48
48
|
*
|
|
49
49
|
* Setting it without a `permission` is a declaration error and throws: there
|
|
@@ -61,7 +61,7 @@ export interface GuardedNavSection {
|
|
|
61
61
|
items: GuardedNavItem[];
|
|
62
62
|
/**
|
|
63
63
|
* What shape these screens are — the top tier of the sidebar, above the
|
|
64
|
-
* domain
|
|
64
|
+
* domain.
|
|
65
65
|
*
|
|
66
66
|
* Every contributing shell declares its own, because this interface is the
|
|
67
67
|
* only thing a shell and the host application both reach: adding the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/values/guarded-nav.ts"],"sourcesContent":["import type { Permission } from './permission.js';\nimport type { ScreenType } from './screen-type.js';\n\n/**\n * A navigation destination plus the permission it requires, if any.\n *\n * This is the permission-annotated half of navigation, which is why it lives with\n * the authorization vocabulary rather than in a shell: an entry and the route\n * behind it must be expressed in the same terms `requirePermission` enforces, and\n * `can` — the filter every layer applies — is declared right next to it.\n *\n * It is also the one package both a shell and the host application depend on.\n * That matters because a shell contributes its own nav\n * fragment to whichever host mounts it, and shells may not depend on each other.\n *\n * `label`/`title` are **namespace-qualified catalog keys**, not copy. Whoever\n * renders the list does not necessarily own its wording, so the namespace travels\n * with the key — and spelling it out is also what makes an `app:` key visible to\n * the lint rule that keeps those keys inside `apps/`.\n *\n * Filtering on these is **presentation only**. Hiding an entry protects nothing;\n * the service is what refuses the request.\n */\nexport interface GuardedNavItem {\n label: string;\n href: string;\n icon?: string;\n /**\n * When set, the item also offers \"Open in workspace\" — a link to\n * `/workspace?tab=<workspace>`.\n */\n workspace?: string;\n /** Omit to show the item to every signed-in user. */\n permission?: Permission;\n /**\n * Also gate this item on the acting organization's provisioning —
|
|
1
|
+
{"version":3,"sources":["../../src/values/guarded-nav.ts"],"sourcesContent":["import type { Permission } from './permission.js';\nimport type { ScreenType } from './screen-type.js';\n\n/**\n * A navigation destination plus the permission it requires, if any.\n *\n * This is the permission-annotated half of navigation, which is why it lives with\n * the authorization vocabulary rather than in a shell: an entry and the route\n * behind it must be expressed in the same terms `requirePermission` enforces, and\n * `can` — the filter every layer applies — is declared right next to it.\n *\n * It is also the one package both a shell and the host application depend on.\n * That matters because a shell contributes its own nav\n * fragment to whichever host mounts it, and shells may not depend on each other.\n *\n * `label`/`title` are **namespace-qualified catalog keys**, not copy. Whoever\n * renders the list does not necessarily own its wording, so the namespace travels\n * with the key — and spelling it out is also what makes an `app:` key visible to\n * the lint rule that keeps those keys inside `apps/`.\n *\n * Filtering on these is **presentation only**. Hiding an entry protects nothing;\n * the service is what refuses the request.\n */\nexport interface GuardedNavItem {\n label: string;\n href: string;\n icon?: string;\n /**\n * When set, the item also offers \"Open in workspace\" — a link to\n * `/workspace?tab=<workspace>`.\n */\n workspace?: string;\n /** Omit to show the item to every signed-in user. */\n permission?: Permission;\n /**\n * Also gate this item on the acting organization's provisioning — the second\n * ceiling, beside the role — using the domain segment of its own {@link permission}.\n *\n * A boolean rather than a domain name, because the domain is already written\n * one line up: a second string here could disagree with the permission the\n * route behind it actually checks, which is the drift this whole interface\n * exists to prevent.\n *\n * **Omitted is the right answer for most items.** An organization is not\n * \"provisioned for\" `catalog-reference`, `config` or `authn` — those are\n * operator-owned, and `catalog-reference` is explicitly never grantable\n *. Setting this on one of them would hide the platform's own\n * vocabulary from every vendor.\n *\n * Setting it without a `permission` is a declaration error and throws: there\n * would be no domain to read, and both possible defaults — always shown,\n * always hidden — read as a bug somewhere else entirely.\n *\n * A session with **no** organization ignores this flag: the ceiling is a\n * property of an organization, so an operator is outside it rather than\n * refused by it.\n */\n entitled?: boolean;\n}\n\nexport interface GuardedNavSection {\n title?: string;\n items: GuardedNavItem[];\n /**\n * What shape these screens are — the top tier of the sidebar, above the\n * domain.\n *\n * Every contributing shell declares its own, because this interface is the\n * only thing a shell and the host application both reach: adding the\n * tier is not an edit in the host.\n *\n * **Optional, and narrowly so.** A section with no type is not a fifth\n * category, it is a section that is not a screen group at all — the account\n * surface is the one case, and it carries no permission for the same reason.\n * A second untyped section appearing without a written reason means the\n * taxonomy is missing a case.\n */\n type?: ScreenType;\n}\n"],"names":[],"mappings":"AA4DA,WAkBC"}
|
|
@@ -16,8 +16,8 @@ export type Action = EntityAction;
|
|
|
16
16
|
* Per-entity and open, rather than one global union every domain appends to:
|
|
17
17
|
* that would make an unrelated domain's verb assignable everywhere and grow
|
|
18
18
|
* without bound. The check that a verb exists is therefore at runtime, in
|
|
19
|
-
* {@link permissionForUseCase}, and in
|
|
20
|
-
*
|
|
19
|
+
* {@link permissionForUseCase}, and in whatever source scan a host's register
|
|
20
|
+
* runs.
|
|
21
21
|
*/
|
|
22
22
|
export type UseCaseVerb = string;
|
|
23
23
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/values/permission.ts"],"sourcesContent":["import {\n describeEntityUseCases,\n EntifixBuildError,\n type Entity,\n ENTITY_ACTIONS,\n type EntityAction,\n type EntityConstructor,\n extractMetaEntity,\n extractMetaUseCaseBinding,\n type UseCaseConstructor,\n} from '@entifix/core';\n\n/**\n * What a caller wants to do with a resource.\n *\n * Defined in `@entifix/core` and aliased here, not duplicated: a UI\n * reads the same triple off a served `EntityMetadataDocument` to decide whether\n * to render Save and Delete, and a T3 package may not import this one, which\n * is T4. Aliasing keeps one definition and leaves every call site below\n * unchanged.\n */\nexport const Actions = ENTITY_ACTIONS;\nexport type Action = EntityAction;\n\n/**\n * A verb declared by `@useCase()` — `publish`, `revoke-sessions`, `approve`.\n *\n * Per-entity and open, rather than one global union every domain appends to:\n * that would make an unrelated domain's verb assignable everywhere and grow\n * without bound. The check that a verb exists is therefore at runtime, in\n * {@link permissionForUseCase}, and in
|
|
1
|
+
{"version":3,"sources":["../../src/values/permission.ts"],"sourcesContent":["import {\n describeEntityUseCases,\n EntifixBuildError,\n type Entity,\n ENTITY_ACTIONS,\n type EntityAction,\n type EntityConstructor,\n extractMetaEntity,\n extractMetaUseCaseBinding,\n type UseCaseConstructor,\n} from '@entifix/core';\n\n/**\n * What a caller wants to do with a resource.\n *\n * Defined in `@entifix/core` and aliased here, not duplicated: a UI\n * reads the same triple off a served `EntityMetadataDocument` to decide whether\n * to render Save and Delete, and a T3 package may not import this one, which\n * is T4. Aliasing keeps one definition and leaves every call site below\n * unchanged.\n */\nexport const Actions = ENTITY_ACTIONS;\nexport type Action = EntityAction;\n\n/**\n * A verb declared by `@useCase()` — `publish`, `revoke-sessions`, `approve`.\n *\n * Per-entity and open, rather than one global union every domain appends to:\n * that would make an unrelated domain's verb assignable everywhere and grow\n * without bound. The check that a verb exists is therefore at runtime, in\n * {@link permissionForUseCase}, and in whatever source scan a host's register\n * runs.\n */\nexport type UseCaseVerb = string;\n\n/**\n * The third segment of a permission: the CRUD triple, or a use-case verb.\n *\n * `Action` stays the narrow three-value union, and every place that guards CRUD\n * keeps using it. This wider alias exists only where a segment has already been\n * parsed off a string and could legitimately be either.\n */\nexport type PermissionAction = Action | UseCaseVerb;\n\n/**\n * A permission is three colon-separated segments — `<domain>:<entityKey>:<action>`\n * — so it can be derived from an entity's own `@entity({ domain, key })`\n * metadata rather than maintained as a parallel list. `*` in any segment of a\n * **granted** permission is a wildcard (see {@link permissionMatches}); a\n * **required** permission is always concrete.\n */\nexport type Permission = `${string}:${string}:${string}`;\n\n/** The `<domain>:<entityKey>` half of a permission — what is being acted on. */\nexport type Resource = `${string}:${string}`;\n\nconst SEGMENT_COUNT = 3;\nconst WILDCARD = '*';\n\n/** Build a permission from its parts. `*` is a legal action on a grant. */\nexport const permissionFor = (\n domain: string,\n entityKey: string,\n action: PermissionAction,\n): Permission => `${domain}:${entityKey}:${action}`;\n\n/** Join a resource and an action into the permission that guards it. */\nexport const permissionOf = (\n resource: Resource,\n action: PermissionAction,\n): Permission => `${resource}:${action}`;\n\n/**\n * Derive the permission guarding an entity from the entity itself. This is the\n * point of the `<domain>:<key>:<action>` shape: making a new entity guardable\n * needs no new vocabulary, only its existing `@entity()` metadata.\n */\nexport const permissionForEntity = <T extends Entity>(\n entityConstructor: EntityConstructor<T>,\n action: Action,\n): Permission => {\n const meta = extractMetaEntity(entityConstructor);\n if (meta.domain === undefined || meta.key === undefined) {\n throw new EntifixBuildError(\n `Entity ${entityConstructor.name} needs both a domain and a key to derive a permission`,\n );\n }\n return permissionFor(meta.domain, meta.key, action);\n};\n\n/**\n * Derive the permission guarding a use case.\n *\n * The **one-argument** form is what call sites use: a `@useCase()` class knows\n * both the entity it acts on and its own verb, so the verb string is written\n * once — in the decorator — and every guard imports the derived constant instead\n * of retyping it. The **two-argument** form is for a caller that holds only the\n * entity, which is the `$metadata` route walking an entity's descriptors.\n *\n * Both check that the entity really declares the verb, so a typo throws at\n * module load rather than silently denying every request. That check is at\n * runtime because compile-time narrowing does not survive the decorator\n * boundary: `Symbol.metadata` erases types and `extractMetaEntity` hands back an\n * untyped `MetaEntity`. Nothing is lost that existed — `parsePermission` looks\n * like it validates the action segment and does not.\n */\nexport function permissionForUseCase(useCase: UseCaseConstructor): Permission;\nexport function permissionForUseCase<T extends Entity>(\n entityConstructor: EntityConstructor<T>,\n key: UseCaseVerb,\n): Permission;\nexport function permissionForUseCase(\n target: UseCaseConstructor | EntityConstructor<Entity>,\n key?: UseCaseVerb,\n): Permission {\n const binding =\n key === undefined\n ? extractMetaUseCaseBinding(target as UseCaseConstructor)\n : { entity: target as EntityConstructor<Entity>, key };\n\n const meta = extractMetaEntity(binding.entity);\n if (meta.domain === undefined || meta.key === undefined) {\n throw new EntifixBuildError(\n `Entity ${binding.entity.name} needs both a domain and a key to derive a permission`,\n );\n }\n const declares = describeEntityUseCases(binding.entity).some(\n useCase => useCase.key === binding.key,\n );\n if (!declares) {\n throw new EntifixBuildError(\n `Entity ${binding.entity.name} does not declare the use case \"${binding.key}\"`,\n );\n }\n return permissionFor(meta.domain, meta.key, binding.key);\n}\n\n/**\n * Split a permission back into the resource + action a policy decision reads.\n * Throws on anything that is not exactly three segments, so a typo in a guard\n * fails at the call site instead of silently denying every request.\n */\nexport const parsePermission = (\n permission: Permission,\n): { resource: Resource; action: PermissionAction } => {\n const segments = permission.split(':');\n if (segments.length !== SEGMENT_COUNT) {\n throw new EntifixBuildError(\n `Malformed permission \"${permission}\" — expected <domain>:<entityKey>:<action>`,\n );\n }\n // The cast is only the arity the length check above already established. It\n // deliberately does not narrow the action segment: that used to say `Action`,\n // which read as a typo check it never performed, and is now false as well —\n // a use-case verb is a legal third segment.\n const [domain, entityKey, action] = segments as [\n string,\n string,\n PermissionAction,\n ];\n return { resource: `${domain}:${entityKey}`, action };\n};\n\n/**\n * Does a granted permission cover a required one? Compared segment by segment,\n * with `*` on the granted side matching anything. `super-admin`'s `*:*:*` thus\n * covers everything, and `product-configuration-management:*:read` covers every\n * entity in that domain without naming them.\n */\nexport const permissionMatches = (\n granted: Permission,\n required: Permission,\n): boolean => {\n const grantedSegments = granted.split(':');\n const requiredSegments = required.split(':');\n if (\n grantedSegments.length !== SEGMENT_COUNT ||\n requiredSegments.length !== SEGMENT_COUNT\n ) {\n return false;\n }\n return grantedSegments.every(\n (segment, index) =>\n segment === WILDCARD || segment === requiredSegments[index],\n );\n};\n"],"names":["describeEntityUseCases","EntifixBuildError","ENTITY_ACTIONS","extractMetaEntity","extractMetaUseCaseBinding","Actions","SEGMENT_COUNT","WILDCARD","permissionFor","domain","entityKey","action","permissionOf","resource","permissionForEntity","entityConstructor","meta","undefined","key","name","permissionForUseCase","target","binding","entity","declares","some","useCase","parsePermission","permission","segments","split","length","permissionMatches","granted","required","grantedSegments","requiredSegments","every","segment","index"],"mappings":"AAAA,SACEA,sBAAsB,EACtBC,iBAAiB,EAEjBC,cAAc,EAGdC,iBAAiB,EACjBC,yBAAyB,QAEpB,gBAAgB;AAEvB;;;;;;;;CAQC,GACD,OAAO,MAAMC,UAAUH,eAAe;AAmCtC,MAAMI,gBAAgB;AACtB,MAAMC,WAAW;AAEjB,yEAAyE,GACzE,OAAO,MAAMC,gBAAgB,CAC3BC,QACAC,WACAC,SACe,GAAGF,OAAO,CAAC,EAAEC,UAAU,CAAC,EAAEC,QAAQ,CAAC;AAEpD,sEAAsE,GACtE,OAAO,MAAMC,eAAe,CAC1BC,UACAF,SACe,GAAGE,SAAS,CAAC,EAAEF,QAAQ,CAAC;AAEzC;;;;CAIC,GACD,OAAO,MAAMG,sBAAsB,CACjCC,mBACAJ;IAEA,MAAMK,OAAOb,kBAAkBY;IAC/B,IAAIC,KAAKP,MAAM,KAAKQ,aAAaD,KAAKE,GAAG,KAAKD,WAAW;QACvD,MAAM,IAAIhB,kBACR,CAAC,OAAO,EAAEc,kBAAkBI,IAAI,CAAC,qDAAqD,CAAC;IAE3F;IACA,OAAOX,cAAcQ,KAAKP,MAAM,EAAEO,KAAKE,GAAG,EAAEP;AAC9C,EAAE;AAuBF,OAAO,SAASS,qBACdC,MAAsD,EACtDH,GAAiB;IAEjB,MAAMI,UACJJ,QAAQD,YACJb,0BAA0BiB,UAC1B;QAAEE,QAAQF;QAAqCH;IAAI;IAEzD,MAAMF,OAAOb,kBAAkBmB,QAAQC,MAAM;IAC7C,IAAIP,KAAKP,MAAM,KAAKQ,aAAaD,KAAKE,GAAG,KAAKD,WAAW;QACvD,MAAM,IAAIhB,kBACR,CAAC,OAAO,EAAEqB,QAAQC,MAAM,CAACJ,IAAI,CAAC,qDAAqD,CAAC;IAExF;IACA,MAAMK,WAAWxB,uBAAuBsB,QAAQC,MAAM,EAAEE,IAAI,CAC1DC,CAAAA,UAAWA,QAAQR,GAAG,KAAKI,QAAQJ,GAAG;IAExC,IAAI,CAACM,UAAU;QACb,MAAM,IAAIvB,kBACR,CAAC,OAAO,EAAEqB,QAAQC,MAAM,CAACJ,IAAI,CAAC,gCAAgC,EAAEG,QAAQJ,GAAG,CAAC,CAAC,CAAC;IAElF;IACA,OAAOV,cAAcQ,KAAKP,MAAM,EAAEO,KAAKE,GAAG,EAAEI,QAAQJ,GAAG;AACzD;AAEA;;;;CAIC,GACD,OAAO,MAAMS,kBAAkB,CAC7BC;IAEA,MAAMC,WAAWD,WAAWE,KAAK,CAAC;IAClC,IAAID,SAASE,MAAM,KAAKzB,eAAe;QACrC,MAAM,IAAIL,kBACR,CAAC,sBAAsB,EAAE2B,WAAW,0CAA0C,CAAC;IAEnF;IACA,4EAA4E;IAC5E,8EAA8E;IAC9E,4EAA4E;IAC5E,4CAA4C;IAC5C,MAAM,CAACnB,QAAQC,WAAWC,OAAO,GAAGkB;IAKpC,OAAO;QAAEhB,UAAU,GAAGJ,OAAO,CAAC,EAAEC,WAAW;QAAEC;IAAO;AACtD,EAAE;AAEF;;;;;CAKC,GACD,OAAO,MAAMqB,oBAAoB,CAC/BC,SACAC;IAEA,MAAMC,kBAAkBF,QAAQH,KAAK,CAAC;IACtC,MAAMM,mBAAmBF,SAASJ,KAAK,CAAC;IACxC,IACEK,gBAAgBJ,MAAM,KAAKzB,iBAC3B8B,iBAAiBL,MAAM,KAAKzB,eAC5B;QACA,OAAO;IACT;IACA,OAAO6B,gBAAgBE,KAAK,CAC1B,CAACC,SAASC,QACRD,YAAY/B,YAAY+B,YAAYF,gBAAgB,CAACG,MAAM;AAEjE,EAAE"}
|
package/dist/values/role.d.ts
CHANGED
|
@@ -1,26 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* A host's role ladder: its roles in rank order, and the assignment rule bound
|
|
3
|
+
* to them ([ADR 0007](../../../../../docs/adr/0007-the-host-supplies-the-role-ladder.md)).
|
|
4
|
+
*
|
|
5
|
+
* The rungs are the host's — which roles exist is one product's hierarchy, and
|
|
6
|
+
* a framework that declared them would have decided it for every adopter. The
|
|
7
|
+
* rule over them is the framework's: a principal may assign a role at or below
|
|
8
|
+
* its own highest rank, and a principal carrying no recognised role can assign
|
|
9
|
+
* nothing. That rule is why a ladder exists at all; a host that wants another
|
|
10
|
+
* one wants a policy, which `GrantTable` and `can` already are.
|
|
11
|
+
*
|
|
12
|
+
* The ladder is the one attribute that rides in the access token, and an
|
|
13
|
+
* ordered tier is what makes the escalation rule expressible without a policy
|
|
14
|
+
* language.
|
|
6
15
|
*/
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
export interface RoleLadder<TRole extends string> {
|
|
17
|
+
/** Every role, from the lowest rung to the highest. Order is rank. */
|
|
18
|
+
readonly roles: readonly TRole[];
|
|
19
|
+
/** What a self-registered account starts as; never assignable by the caller. */
|
|
20
|
+
readonly defaultRole: TRole;
|
|
21
|
+
/** Seniority. Higher wins; used only for the assignment rule, never for grants. */
|
|
22
|
+
readonly rank: Readonly<Record<TRole, number>>;
|
|
23
|
+
/** Narrow an unknown value — a request body, a stored document — to a role. */
|
|
24
|
+
isRole(value: unknown): value is TRole;
|
|
25
|
+
/**
|
|
26
|
+
* The highest rank among the roles a principal carries, or `-1` when it
|
|
27
|
+
* carries none, or only unrecognised ones — which can never satisfy an
|
|
28
|
+
* assignment.
|
|
29
|
+
*/
|
|
30
|
+
highestRank(roles: readonly string[]): number;
|
|
31
|
+
/**
|
|
32
|
+
* May an actor create or promote someone to `target`? At or below the
|
|
33
|
+
* actor's own rank, so the second rung mints its own rung and the first but
|
|
34
|
+
* never the third, and a caller with no roles mints nothing.
|
|
35
|
+
*/
|
|
36
|
+
canAssignRole(actorRoles: readonly string[], target: TRole): boolean;
|
|
37
|
+
}
|
|
38
|
+
/** The role union a ladder was defined over. */
|
|
39
|
+
export type RoleOf<TLadder> = TLadder extends RoleLadder<infer TRole> ? TRole : never;
|
|
40
|
+
export interface RoleLadderOptions<TRole extends string> {
|
|
41
|
+
/** Defaults to the lowest rung. */
|
|
42
|
+
readonly defaultRole?: TRole;
|
|
43
|
+
}
|
|
15
44
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
45
|
+
* Define a ladder, once, in whichever package a host keeps its grant table:
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* export const ladder = defineRoleLadder(['user', 'admin', 'owner'] as const);
|
|
49
|
+
* export type Role = RoleOf<typeof ladder>;
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* ⚠️ **An empty ladder and a repeated rung throw here**, at definition rather
|
|
53
|
+
* than at the first assignment: both make rank meaningless, and both would
|
|
54
|
+
* otherwise fail in silence — an empty ladder refusing every assignment, a
|
|
55
|
+
* repeated rung ranking itself by whichever copy was read last.
|
|
18
56
|
*/
|
|
19
|
-
export declare const
|
|
20
|
-
/**
|
|
21
|
-
* May an actor create or promote someone to `target`? At or below the actor's
|
|
22
|
-
* own tier — so an admin mints admins and users but never a super-admin, and an
|
|
23
|
-
* unauthenticated signup (no roles) can mint nothing.
|
|
24
|
-
*/
|
|
25
|
-
export declare const canAssignRole: (actorRoles: readonly string[], target: Role) => boolean;
|
|
57
|
+
export declare const defineRoleLadder: <const TRoles extends readonly [string, ...string[]]>(roles: TRoles, options?: RoleLadderOptions<TRoles[number]>) => RoleLadder<TRoles[number]>;
|
|
26
58
|
//# sourceMappingURL=role.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role.d.ts","sourceRoot":"","sources":["../../src/values/role.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"role.d.ts","sourceRoot":"","sources":["../../src/values/role.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,UAAU,CAAC,KAAK,SAAS,MAAM;IAC9C,sEAAsE;IACtE,QAAQ,CAAC,KAAK,EAAE,SAAS,KAAK,EAAE,CAAC;IACjC,gFAAgF;IAChF,QAAQ,CAAC,WAAW,EAAE,KAAK,CAAC;IAC5B,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/C,+EAA+E;IAC/E,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,KAAK,CAAC;IACvC;;;;OAIG;IACH,WAAW,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC;IAC9C;;;;OAIG;IACH,aAAa,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,EAAE,KAAK,GAAG,OAAO,CAAC;CACtE;AAED,gDAAgD;AAChD,MAAM,MAAM,MAAM,CAAC,OAAO,IACxB,OAAO,SAAS,UAAU,CAAC,MAAM,KAAK,CAAC,GAAG,KAAK,GAAG,KAAK,CAAC;AAE1D,MAAM,WAAW,iBAAiB,CAAC,KAAK,SAAS,MAAM;IACrD,mCAAmC;IACnC,QAAQ,CAAC,WAAW,CAAC,EAAE,KAAK,CAAC;CAC9B;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,GAC3B,KAAK,CAAC,MAAM,SAAS,SAAS,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,EAEnD,OAAO,MAAM,EACb,UAAS,iBAAiB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAM,KAC9C,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAmD3B,CAAC"}
|
package/dist/values/role.js
CHANGED
|
@@ -1,28 +1,53 @@
|
|
|
1
|
+
import { EntifixLogicError } from '@entifix/core';
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
* Define a ladder, once, in whichever package a host keeps its grant table:
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* export const ladder = defineRoleLadder(['user', 'admin', 'owner'] as const);
|
|
7
|
+
* export type Role = RoleOf<typeof ladder>;
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ **An empty ladder and a repeated rung throw here**, at definition rather
|
|
11
|
+
* than at the first assignment: both make rank meaningless, and both would
|
|
12
|
+
* otherwise fail in silence — an empty ladder refusing every assignment, a
|
|
13
|
+
* repeated rung ranking itself by whichever copy was read last.
|
|
14
|
+
*/ export const defineRoleLadder = (roles, options = {})=>{
|
|
15
|
+
var _options_defaultRole;
|
|
16
|
+
if (roles.length === 0) {
|
|
17
|
+
throw new EntifixLogicError('A role ladder needs at least one rung.', undefined, {
|
|
18
|
+
roles
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const repeated = roles.filter((role, index)=>roles.indexOf(role) !== index);
|
|
22
|
+
if (repeated.length > 0) {
|
|
23
|
+
throw new EntifixLogicError(`A role ladder names each rung once; repeated: ${[
|
|
24
|
+
...new Set(repeated)
|
|
25
|
+
].join(', ')}.`, undefined, {
|
|
26
|
+
roles,
|
|
27
|
+
repeated
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const defaultRole = (_options_defaultRole = options.defaultRole) != null ? _options_defaultRole : roles[0];
|
|
31
|
+
if (!roles.includes(defaultRole)) {
|
|
32
|
+
throw new EntifixLogicError(`The default role "${defaultRole}" is not a rung of the ladder.`, undefined, {
|
|
33
|
+
roles,
|
|
34
|
+
defaultRole
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const rank = Object.fromEntries(roles.map((role, index)=>[
|
|
38
|
+
role,
|
|
39
|
+
index
|
|
40
|
+
]));
|
|
41
|
+
const isRole = (value)=>typeof value === 'string' && Object.hasOwn(rank, value);
|
|
42
|
+
const highestRank = (carried)=>carried.reduce((highest, role)=>isRole(role) ? Math.max(highest, rank[role]) : highest, -1);
|
|
43
|
+
return {
|
|
44
|
+
roles,
|
|
45
|
+
defaultRole,
|
|
46
|
+
rank,
|
|
47
|
+
isRole,
|
|
48
|
+
highestRank,
|
|
49
|
+
canAssignRole: (actorRoles, target)=>highestRank(actorRoles) >= rank[target]
|
|
50
|
+
};
|
|
16
51
|
};
|
|
17
|
-
/** Narrow an unknown value (a request body, a Mongo document) to a {@link Role}. */ export const isRole = (value)=>typeof value === 'string' && Roles.includes(value);
|
|
18
|
-
/**
|
|
19
|
-
* The highest rank among the roles a principal carries, or `-1` when it carries
|
|
20
|
-
* none (or only unrecognised ones) — which can never satisfy an assignment.
|
|
21
|
-
*/ export const highestRank = (roles)=>roles.reduce((rank, role)=>isRole(role) ? Math.max(rank, ROLE_RANK[role]) : rank, -1);
|
|
22
|
-
/**
|
|
23
|
-
* May an actor create or promote someone to `target`? At or below the actor's
|
|
24
|
-
* own tier — so an admin mints admins and users but never a super-admin, and an
|
|
25
|
-
* unauthenticated signup (no roles) can mint nothing.
|
|
26
|
-
*/ export const canAssignRole = (actorRoles, target)=>highestRank(actorRoles) >= ROLE_RANK[target];
|
|
27
52
|
|
|
28
53
|
//# sourceMappingURL=role.js.map
|
package/dist/values/role.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/values/role.ts"],"sourcesContent":["/**\n *
|
|
1
|
+
{"version":3,"sources":["../../src/values/role.ts"],"sourcesContent":["import { EntifixLogicError } from '@entifix/core';\n\n/**\n * A host's role ladder: its roles in rank order, and the assignment rule bound\n * to them ([ADR 0007](../../../../../docs/adr/0007-the-host-supplies-the-role-ladder.md)).\n *\n * The rungs are the host's — which roles exist is one product's hierarchy, and\n * a framework that declared them would have decided it for every adopter. The\n * rule over them is the framework's: a principal may assign a role at or below\n * its own highest rank, and a principal carrying no recognised role can assign\n * nothing. That rule is why a ladder exists at all; a host that wants another\n * one wants a policy, which `GrantTable` and `can` already are.\n *\n * The ladder is the one attribute that rides in the access token, and an\n * ordered tier is what makes the escalation rule expressible without a policy\n * language.\n */\nexport interface RoleLadder<TRole extends string> {\n /** Every role, from the lowest rung to the highest. Order is rank. */\n readonly roles: readonly TRole[];\n /** What a self-registered account starts as; never assignable by the caller. */\n readonly defaultRole: TRole;\n /** Seniority. Higher wins; used only for the assignment rule, never for grants. */\n readonly rank: Readonly<Record<TRole, number>>;\n /** Narrow an unknown value — a request body, a stored document — to a role. */\n isRole(value: unknown): value is TRole;\n /**\n * The highest rank among the roles a principal carries, or `-1` when it\n * carries none, or only unrecognised ones — which can never satisfy an\n * assignment.\n */\n highestRank(roles: readonly string[]): number;\n /**\n * May an actor create or promote someone to `target`? At or below the\n * actor's own rank, so the second rung mints its own rung and the first but\n * never the third, and a caller with no roles mints nothing.\n */\n canAssignRole(actorRoles: readonly string[], target: TRole): boolean;\n}\n\n/** The role union a ladder was defined over. */\nexport type RoleOf<TLadder> =\n TLadder extends RoleLadder<infer TRole> ? TRole : never;\n\nexport interface RoleLadderOptions<TRole extends string> {\n /** Defaults to the lowest rung. */\n readonly defaultRole?: TRole;\n}\n\n/**\n * Define a ladder, once, in whichever package a host keeps its grant table:\n *\n * ```ts\n * export const ladder = defineRoleLadder(['user', 'admin', 'owner'] as const);\n * export type Role = RoleOf<typeof ladder>;\n * ```\n *\n * ⚠️ **An empty ladder and a repeated rung throw here**, at definition rather\n * than at the first assignment: both make rank meaningless, and both would\n * otherwise fail in silence — an empty ladder refusing every assignment, a\n * repeated rung ranking itself by whichever copy was read last.\n */\nexport const defineRoleLadder = <\n const TRoles extends readonly [string, ...string[]],\n>(\n roles: TRoles,\n options: RoleLadderOptions<TRoles[number]> = {},\n): RoleLadder<TRoles[number]> => {\n type TRole = TRoles[number];\n\n if ((roles as readonly string[]).length === 0) {\n throw new EntifixLogicError(\n 'A role ladder needs at least one rung.',\n undefined,\n { roles },\n );\n }\n const repeated = roles.filter((role, index) => roles.indexOf(role) !== index);\n if (repeated.length > 0) {\n throw new EntifixLogicError(\n `A role ladder names each rung once; repeated: ${[...new Set(repeated)].join(', ')}.`,\n undefined,\n { roles, repeated },\n );\n }\n\n const defaultRole = options.defaultRole ?? (roles[0] as TRole);\n if (!(roles as readonly string[]).includes(defaultRole)) {\n throw new EntifixLogicError(\n `The default role \"${defaultRole}\" is not a rung of the ladder.`,\n undefined,\n { roles, defaultRole },\n );\n }\n\n const rank = Object.fromEntries(\n roles.map((role, index) => [role, index]),\n ) as Record<TRole, number>;\n\n const isRole = (value: unknown): value is TRole =>\n typeof value === 'string' && Object.hasOwn(rank, value);\n\n const highestRank = (carried: readonly string[]): number =>\n carried.reduce(\n (highest, role) =>\n isRole(role) ? Math.max(highest, rank[role]) : highest,\n -1,\n );\n\n return {\n roles,\n defaultRole,\n rank,\n isRole,\n highestRank,\n canAssignRole: (actorRoles, target) =>\n highestRank(actorRoles) >= rank[target],\n };\n};\n"],"names":["EntifixLogicError","defineRoleLadder","roles","options","length","undefined","repeated","filter","role","index","indexOf","Set","join","defaultRole","includes","rank","Object","fromEntries","map","isRole","value","hasOwn","highestRank","carried","reduce","highest","Math","max","canAssignRole","actorRoles","target"],"mappings":"AAAA,SAASA,iBAAiB,QAAQ,gBAAgB;AAiDlD;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,mBAAmB,CAG9BC,OACAC,UAA6C,CAAC,CAAC;QAoB3BA;IAhBpB,IAAI,AAACD,MAA4BE,MAAM,KAAK,GAAG;QAC7C,MAAM,IAAIJ,kBACR,0CACAK,WACA;YAAEH;QAAM;IAEZ;IACA,MAAMI,WAAWJ,MAAMK,MAAM,CAAC,CAACC,MAAMC,QAAUP,MAAMQ,OAAO,CAACF,UAAUC;IACvE,IAAIH,SAASF,MAAM,GAAG,GAAG;QACvB,MAAM,IAAIJ,kBACR,CAAC,8CAA8C,EAAE;eAAI,IAAIW,IAAIL;SAAU,CAACM,IAAI,CAAC,MAAM,CAAC,CAAC,EACrFP,WACA;YAAEH;YAAOI;QAAS;IAEtB;IAEA,MAAMO,eAAcV,uBAAAA,QAAQU,WAAW,YAAnBV,uBAAwBD,KAAK,CAAC,EAAE;IACpD,IAAI,CAAC,AAACA,MAA4BY,QAAQ,CAACD,cAAc;QACvD,MAAM,IAAIb,kBACR,CAAC,kBAAkB,EAAEa,YAAY,8BAA8B,CAAC,EAChER,WACA;YAAEH;YAAOW;QAAY;IAEzB;IAEA,MAAME,OAAOC,OAAOC,WAAW,CAC7Bf,MAAMgB,GAAG,CAAC,CAACV,MAAMC,QAAU;YAACD;YAAMC;SAAM;IAG1C,MAAMU,SAAS,CAACC,QACd,OAAOA,UAAU,YAAYJ,OAAOK,MAAM,CAACN,MAAMK;IAEnD,MAAME,cAAc,CAACC,UACnBA,QAAQC,MAAM,CACZ,CAACC,SAASjB,OACRW,OAAOX,QAAQkB,KAAKC,GAAG,CAACF,SAASV,IAAI,CAACP,KAAK,IAAIiB,SACjD,CAAC;IAGL,OAAO;QACLvB;QACAW;QACAE;QACAI;QACAG;QACAM,eAAe,CAACC,YAAYC,SAC1BR,YAAYO,eAAed,IAAI,CAACe,OAAO;IAC3C;AACF,EAAE"}
|
|
@@ -3,8 +3,7 @@ import type { ScreenType } from './screen-type.js';
|
|
|
3
3
|
* Where a screen lives, as data — a {@link ScreenType}, the entity or screen key
|
|
4
4
|
* within it, and optionally the record being viewed.
|
|
5
5
|
*
|
|
6
|
-
* This is the parsed form of a workspace tab address
|
|
7
|
-
* ([ADR 0042](../../../../../../docs/adr/0042-the-workspace-address-is-the-taxonomy-serialized.md)).
|
|
6
|
+
* This is the parsed form of a workspace tab address.
|
|
8
7
|
*/
|
|
9
8
|
export interface ScreenAddress {
|
|
10
9
|
readonly type: ScreenType;
|
|
@@ -17,8 +16,7 @@ export interface ScreenAddress {
|
|
|
17
16
|
*
|
|
18
17
|
* One segment for both, rather than a member per type, because the grammar is
|
|
19
18
|
* what a `TabKind.match` parses and a second optional segment would make every
|
|
20
|
-
* kind's parser handle a shape no kind produces
|
|
21
|
-
* ([ADR 0045](../../../../../../docs/adr/0045-the-wizard-a-step-graph-and-a-submit-that-hands-off.md)).
|
|
19
|
+
* kind's parser handle a shape no kind produces.
|
|
22
20
|
*/
|
|
23
21
|
readonly id?: string;
|
|
24
22
|
}
|
|
@@ -36,7 +34,7 @@ export interface ScreenAddress {
|
|
|
36
34
|
*
|
|
37
35
|
* There is no address for a section carrying no {@link ScreenType}. That is not
|
|
38
36
|
* an omission: the account surface is deliberately outside the taxonomy
|
|
39
|
-
|
|
37
|
+
*, and taking a `ScreenType` here is what makes "the account cannot
|
|
40
38
|
* be a tab" a fact the compiler enforces rather than a rule somebody remembers.
|
|
41
39
|
*/
|
|
42
40
|
export declare function screenAddress({ type, key, id }: ScreenAddress): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screen-address.d.ts","sourceRoot":"","sources":["../../src/values/screen-address.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD
|
|
1
|
+
{"version":3,"file":"screen-address.d.ts","sourceRoot":"","sources":["../../src/values/screen-address.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,gFAAgF;IAChF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,aAAa,GAAG,MAAM,CAEtE;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,GACd;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAQrC"}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* There is no address for a section carrying no {@link ScreenType}. That is not
|
|
14
14
|
* an omission: the account surface is deliberately outside the taxonomy
|
|
15
|
-
|
|
15
|
+
*, and taking a `ScreenType` here is what makes "the account cannot
|
|
16
16
|
* be a tab" a fact the compiler enforces rather than a rule somebody remembers.
|
|
17
17
|
*/ export function screenAddress({ type, key, id }) {
|
|
18
18
|
return id === undefined ? `${type}:${key}` : `${type}:${key}:${id}`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/values/screen-address.ts"],"sourcesContent":["import type { ScreenType } from './screen-type.js';\n\n/**\n * Where a screen lives, as data — a {@link ScreenType}, the entity or screen key\n * within it, and optionally the record being viewed.\n *\n * This is the parsed form of a workspace tab address
|
|
1
|
+
{"version":3,"sources":["../../src/values/screen-address.ts"],"sourcesContent":["import type { ScreenType } from './screen-type.js';\n\n/**\n * Where a screen lives, as data — a {@link ScreenType}, the entity or screen key\n * within it, and optionally the record being viewed.\n *\n * This is the parsed form of a workspace tab address.\n */\nexport interface ScreenAddress {\n readonly type: ScreenType;\n /** The entity key (`@entity({ key })`) or, for a hand-built screen, its own. */\n readonly key: string;\n /**\n * The position **within** the screen, and what that is depends on the type:\n * a record for `master` (absent for the list of them), a step for `wizard`\n * (absent for its first step).\n *\n * One segment for both, rather than a member per type, because the grammar is\n * what a `TabKind.match` parses and a second optional segment would make every\n * kind's parser handle a shape no kind produces.\n */\n readonly id?: string;\n}\n\n/**\n * Serialize a {@link ScreenAddress} — `<type>:<key>` for a list, and\n * `<type>:<key>:<id>` for one record.\n *\n * The address is the taxonomy serialized, which is why this lives beside\n * {@link ScreenType} rather than in the workspace that consumes it: it was three\n * grammars (`catalog:` for a list, `entity:` for a record, `system:` for a\n * hand-built screen) spelled out at five call sites that had to agree with each\n * other by hand. Two of those were the same string built twice — the tab address\n * and the autosaved draft's key — so a drift between them silently detached a\n * tab from its own draft.\n *\n * There is no address for a section carrying no {@link ScreenType}. That is not\n * an omission: the account surface is deliberately outside the taxonomy\n *, and taking a `ScreenType` here is what makes \"the account cannot\n * be a tab\" a fact the compiler enforces rather than a rule somebody remembers.\n */\nexport function screenAddress({ type, key, id }: ScreenAddress): string {\n return id === undefined ? `${type}:${key}` : `${type}:${key}:${id}`;\n}\n\n/**\n * The inverse of everything {@link screenAddress} writes after `<type>:` — which\n * is exactly what a `TabKind.match` receives, because the registry has already\n * split the kind off the front.\n *\n * Returns `null` rather than a partial result for anything malformed, so a\n * caller cannot accidentally treat an empty key or an empty id as present. A\n * `'brand:'` that parsed as `{ key: 'brand', id: '' }` would resolve to the\n * record editor for a record with no id.\n */\nexport function parseScreenPayload(\n payload: string,\n): { key: string; id?: string } | null {\n const separator = payload.indexOf(':');\n if (separator === -1) {\n return payload === '' ? null : { key: payload };\n }\n const key = payload.slice(0, separator);\n const id = payload.slice(separator + 1);\n return key === '' || id === '' ? null : { key, id };\n}\n"],"names":["screenAddress","type","key","id","undefined","parseScreenPayload","payload","separator","indexOf","slice"],"mappings":"AAwBA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASA,cAAc,EAAEC,IAAI,EAAEC,GAAG,EAAEC,EAAE,EAAiB;IAC5D,OAAOA,OAAOC,YAAY,GAAGH,KAAK,CAAC,EAAEC,KAAK,GAAG,GAAGD,KAAK,CAAC,EAAEC,IAAI,CAAC,EAAEC,IAAI;AACrE;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASE,mBACdC,OAAe;IAEf,MAAMC,YAAYD,QAAQE,OAAO,CAAC;IAClC,IAAID,cAAc,CAAC,GAAG;QACpB,OAAOD,YAAY,KAAK,OAAO;YAAEJ,KAAKI;QAAQ;IAChD;IACA,MAAMJ,MAAMI,QAAQG,KAAK,CAAC,GAAGF;IAC7B,MAAMJ,KAAKG,QAAQG,KAAK,CAACF,YAAY;IACrC,OAAOL,QAAQ,MAAMC,OAAO,KAAK,OAAO;QAAED;QAAKC;IAAG;AACpD"}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What shape a back-office screen takes — the
|
|
3
|
-
* [ADR
|
|
2
|
+
* What shape a back-office screen takes — the framework's back-office model,
|
|
3
|
+
* [ADR 0007](../../../../../../docs/adr/0007-the-host-supplies-the-role-ladder.md).
|
|
4
4
|
*
|
|
5
|
-
* | Value |
|
|
6
|
-
* | ----------- |
|
|
7
|
-
* | `master` |
|
|
8
|
-
* | `operation` |
|
|
9
|
-
* | `wizard` |
|
|
10
|
-
* | `report` |
|
|
5
|
+
* | Value | The record came from | Lifecycle | Verbs |
|
|
6
|
+
* | ----------- | ---------------------------- | --------- | ------------ |
|
|
7
|
+
* | `master` | a person authored it | none | CRUD |
|
|
8
|
+
* | `operation` | a process produced it | yes | domain verbs |
|
|
9
|
+
* | `wizard` | a guided, multi-step entry | ends | next/finish |
|
|
10
|
+
* | `report` | nothing — it is an aggregate | n/a | read only |
|
|
11
11
|
*
|
|
12
|
+
* The rendered names are the host's, through {@link SCREEN_TYPE_LABEL_KEYS}.
|
|
12
13
|
* The order is the sidebar's top tier, which is the **type** rather than the
|
|
13
|
-
* domain: four fixed entries against
|
|
14
|
-
*
|
|
14
|
+
* domain: four fixed entries against however many domains a host has, so
|
|
15
|
+
* "where does this screen go" has one answer whoever owns it. The daily operator's speed is
|
|
15
16
|
* the command palette's job, not the sidebar's.
|
|
16
17
|
*
|
|
17
18
|
* The identifiers are English because every other identifier here is, but two of
|
|
@@ -21,9 +22,9 @@
|
|
|
21
22
|
* consequence is that the code word and the screen word differ for three of the
|
|
22
23
|
* four, and {@link SCREEN_TYPE_LABEL_KEYS} is the one place that reconciles them.
|
|
23
24
|
*
|
|
24
|
-
* "Publish" is an **action on a `master` screen**, not a fifth value.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* "Publish" is an **action on a `master` screen**, not a fifth value. Every
|
|
26
|
+
* entity can declare verbs, so treating a verb as grounds for promotion to
|
|
27
|
+
* `operation` would promote all of them.
|
|
27
28
|
*/
|
|
28
29
|
export declare const ScreenTypes: readonly ["master", "operation", "wizard", "report"];
|
|
29
30
|
export type ScreenType = (typeof ScreenTypes)[number];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screen-type.d.ts","sourceRoot":"","sources":["../../src/values/screen-type.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"screen-type.d.ts","sourceRoot":"","sources":["../../src/values/screen-type.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,WAAW,sDAAuD,CAAC;AAChF,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtD;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAK7D,CAAC;AAEF,8FAA8F;AAC9F,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,UAEH,CAAC;AAErD;;;GAGG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,UAAU,GAAG,SAAS,KAAG,MACO,CAAC"}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What shape a back-office screen takes — the
|
|
3
|
-
* [ADR
|
|
2
|
+
* What shape a back-office screen takes — the framework's back-office model,
|
|
3
|
+
* [ADR 0007](../../../../../../docs/adr/0007-the-host-supplies-the-role-ladder.md).
|
|
4
4
|
*
|
|
5
|
-
* | Value |
|
|
6
|
-
* | ----------- |
|
|
7
|
-
* | `master` |
|
|
8
|
-
* | `operation` |
|
|
9
|
-
* | `wizard` |
|
|
10
|
-
* | `report` |
|
|
5
|
+
* | Value | The record came from | Lifecycle | Verbs |
|
|
6
|
+
* | ----------- | ---------------------------- | --------- | ------------ |
|
|
7
|
+
* | `master` | a person authored it | none | CRUD |
|
|
8
|
+
* | `operation` | a process produced it | yes | domain verbs |
|
|
9
|
+
* | `wizard` | a guided, multi-step entry | ends | next/finish |
|
|
10
|
+
* | `report` | nothing — it is an aggregate | n/a | read only |
|
|
11
11
|
*
|
|
12
|
+
* The rendered names are the host's, through {@link SCREEN_TYPE_LABEL_KEYS}.
|
|
12
13
|
* The order is the sidebar's top tier, which is the **type** rather than the
|
|
13
|
-
* domain: four fixed entries against
|
|
14
|
-
*
|
|
14
|
+
* domain: four fixed entries against however many domains a host has, so
|
|
15
|
+
* "where does this screen go" has one answer whoever owns it. The daily operator's speed is
|
|
15
16
|
* the command palette's job, not the sidebar's.
|
|
16
17
|
*
|
|
17
18
|
* The identifiers are English because every other identifier here is, but two of
|
|
@@ -21,9 +22,9 @@
|
|
|
21
22
|
* consequence is that the code word and the screen word differ for three of the
|
|
22
23
|
* four, and {@link SCREEN_TYPE_LABEL_KEYS} is the one place that reconciles them.
|
|
23
24
|
*
|
|
24
|
-
* "Publish" is an **action on a `master` screen**, not a fifth value.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* "Publish" is an **action on a `master` screen**, not a fifth value. Every
|
|
26
|
+
* entity can declare verbs, so treating a verb as grounds for promotion to
|
|
27
|
+
* `operation` would promote all of them.
|
|
27
28
|
*/ export const ScreenTypes = [
|
|
28
29
|
'master',
|
|
29
30
|
'operation',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/values/screen-type.ts"],"sourcesContent":["/**\n * What shape a back-office screen takes — the
|
|
1
|
+
{"version":3,"sources":["../../src/values/screen-type.ts"],"sourcesContent":["/**\n * What shape a back-office screen takes — the framework's back-office model,\n * [ADR 0007](../../../../../../docs/adr/0007-the-host-supplies-the-role-ladder.md).\n *\n * | Value | The record came from | Lifecycle | Verbs |\n * | ----------- | ---------------------------- | --------- | ------------ |\n * | `master` | a person authored it | none | CRUD |\n * | `operation` | a process produced it | yes | domain verbs |\n * | `wizard` | a guided, multi-step entry | ends | next/finish |\n * | `report` | nothing — it is an aggregate | n/a | read only |\n *\n * The rendered names are the host's, through {@link SCREEN_TYPE_LABEL_KEYS}.\n * The order is the sidebar's top tier, which is the **type** rather than the\n * domain: four fixed entries against however many domains a host has, so\n * \"where does this screen go\" has one answer whoever owns it. The daily operator's speed is\n * the command palette's job, not the sidebar's.\n *\n * The identifiers are English because every other identifier here is, but two of\n * the obvious words were unusable — `query` collides with RSQL, TanStack Query\n * and `filterable` metadata, and `assistant` reads as an AI agent — so the enum\n * takes `report` and `wizard`, which are already what this repo calls those. The\n * consequence is that the code word and the screen word differ for three of the\n * four, and {@link SCREEN_TYPE_LABEL_KEYS} is the one place that reconciles them.\n *\n * \"Publish\" is an **action on a `master` screen**, not a fifth value. Every\n * entity can declare verbs, so treating a verb as grounds for promotion to\n * `operation` would promote all of them.\n */\nexport const ScreenTypes = ['master', 'operation', 'wizard', 'report'] as const;\nexport type ScreenType = (typeof ScreenTypes)[number];\n\n/**\n * The shipped Spanish names, as catalog keys.\n *\n * `shell:`-namespaced copy in a `business:policy` package is unusual and\n * deliberate: the four names are one vocabulary, and a second declaration site\n * for them is the drift `nav.ts` was already merged once to stop. Only the\n * `app:` namespace is restricted by location, so this is legal as well as\n * intended.\n */\nexport const SCREEN_TYPE_LABEL_KEYS: Record<ScreenType, string> = {\n master: 'shell:nav.screenType.master',\n operation: 'shell:nav.screenType.operation',\n wizard: 'shell:nav.screenType.wizard',\n report: 'shell:nav.screenType.report',\n};\n\n/** Narrow an unknown value (a stored tab address, a request body) to a {@link ScreenType}. */\nexport const isScreenType = (value: unknown): value is ScreenType =>\n typeof value === 'string' &&\n (ScreenTypes as readonly string[]).includes(value);\n\n/**\n * Sort key for the top tier. A section with no type sorts last — the account\n * surface is not a screen group, so it belongs below the ones that are.\n */\nexport const screenTypeRank = (type: ScreenType | undefined): number =>\n type === undefined ? ScreenTypes.length : ScreenTypes.indexOf(type);\n"],"names":["ScreenTypes","SCREEN_TYPE_LABEL_KEYS","master","operation","wizard","report","isScreenType","value","includes","screenTypeRank","type","undefined","length","indexOf"],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BC,GACD,OAAO,MAAMA,cAAc;IAAC;IAAU;IAAa;IAAU;CAAS,CAAU;AAGhF;;;;;;;;CAQC,GACD,OAAO,MAAMC,yBAAqD;IAChEC,QAAQ;IACRC,WAAW;IACXC,QAAQ;IACRC,QAAQ;AACV,EAAE;AAEF,4FAA4F,GAC5F,OAAO,MAAMC,eAAe,CAACC,QAC3B,OAAOA,UAAU,YACjB,AAACP,YAAkCQ,QAAQ,CAACD,OAAO;AAErD;;;CAGC,GACD,OAAO,MAAME,iBAAiB,CAACC,OAC7BA,SAASC,YAAYX,YAAYY,MAAM,GAAGZ,YAAYa,OAAO,CAACH,MAAM"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entifix/authz",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Authorization vocabulary: permissions derived from entities, roles, a grant table, and a PolicyDecision port.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@swc/helpers": "0.5.23",
|
|
54
|
-
"@entifix/core": "0.
|
|
54
|
+
"@entifix/core": "0.7.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"effect": "^4.0.5"
|