@entifix/authz 0.5.0 → 0.6.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.
@@ -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 — r10c declares the table
12
- * as `Record<Role, readonly Permission[]>`, so a typo there is still a compile
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
  /**
@@ -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 — r10c declares the table\n * as `Record<Role, readonly Permission[]>`, so a typo there is still a compile\n * 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"}
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;;;;;;;;;;GAUG;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
+ {"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\n * ([ADR 0023](../../../../../../docs/adr/0023-service-to-service-tenant-crossing.md)).\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;AAiB7E,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"}
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 — ADR 0007's second
48
- * ceiling — using the domain segment of its own {@link permission}, with all
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 — ADR 0007's second\n * ceiling — 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"}
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 — ADR 0007's
36
- * second ceiling — using the domain segment of its own {@link permission}.
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
- * (ADR 0022). Setting this on one of them would hide the platform's own
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 ([ADR 0033](../../../../../../docs/adr/0033-the-screen-taxonomy.md)).
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 — ADR 0007's\n * second ceiling — 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 * (ADR 0022). 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 ([ADR 0033](../../../../../../docs/adr/0033-the-screen-taxonomy.md)).\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"}
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 the source scan `@r10c/slices` runs
20
- * ([ADR 0026](../../../../../docs/adr/0026-the-use-case-descriptor-and-served-entity-metadata.md)).
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 the source scan `@r10c/slices` runs\n * ([ADR 0026](../../../../../docs/adr/0026-the-use-case-descriptor-and-served-entity-metadata.md)).\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"}
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"}
@@ -1,26 +1,58 @@
1
1
  /**
2
- * The role aspect attached to every user. Deliberately a small, ordered set
3
- * rather than free-form strings: it is the one attribute that rides in the
4
- * access token, and an ordered tier is what makes the escalation rule
5
- * ("assign at or below your own tier") expressible without a policy language.
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 declare const Roles: readonly ["user", "admin", "super-admin"];
8
- export type Role = (typeof Roles)[number];
9
- /** Default for a self-registered account; never assignable by the caller. */
10
- export declare const DEFAULT_ROLE: Role;
11
- /** Seniority. Higher wins; only used for the assignment rule, not for grants. */
12
- export declare const ROLE_RANK: Record<Role, number>;
13
- /** Narrow an unknown value (a request body, a Mongo document) to a {@link Role}. */
14
- export declare const isRole: (value: unknown) => value is Role;
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
- * The highest rank among the roles a principal carries, or `-1` when it carries
17
- * none (or only unrecognised ones) — which can never satisfy an assignment.
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 highestRank: (roles: readonly string[]) => number;
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":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,KAAK,2CAA4C,CAAC;AAC/D,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1C,6EAA6E;AAC7E,eAAO,MAAM,YAAY,EAAE,IAAa,CAAC;AAEzC,iFAAiF;AACjF,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAI1C,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,MAAM,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,IAC0B,CAAC;AAE5E;;;GAGG;AACH,eAAO,MAAM,WAAW,GAAI,OAAO,SAAS,MAAM,EAAE,KAAG,MAIpD,CAAC;AAEJ;;;;GAIG;AACH,eAAO,MAAM,aAAa,GACxB,YAAY,SAAS,MAAM,EAAE,EAC7B,QAAQ,IAAI,KACX,OAAuD,CAAC"}
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"}
@@ -1,28 +1,53 @@
1
+ import { EntifixLogicError } from '@entifix/core';
1
2
  /**
2
- * The role aspect attached to every user. Deliberately a small, ordered set
3
- * rather than free-form strings: it is the one attribute that rides in the
4
- * access token, and an ordered tier is what makes the escalation rule
5
- * ("assign at or below your own tier") expressible without a policy language.
6
- */ export const Roles = [
7
- 'user',
8
- 'admin',
9
- 'super-admin'
10
- ];
11
- /** Default for a self-registered account; never assignable by the caller. */ export const DEFAULT_ROLE = 'user';
12
- /** Seniority. Higher wins; only used for the assignment rule, not for grants. */ export const ROLE_RANK = {
13
- user: 0,
14
- admin: 1,
15
- 'super-admin': 2
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
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/values/role.ts"],"sourcesContent":["/**\n * The role aspect attached to every user. Deliberately a small, ordered set\n * rather than free-form strings: it is the one attribute that rides in the\n * access token, and an ordered tier is what makes the escalation rule\n * (\"assign at or below your own tier\") expressible without a policy language.\n */\nexport const Roles = ['user', 'admin', 'super-admin'] as const;\nexport type Role = (typeof Roles)[number];\n\n/** Default for a self-registered account; never assignable by the caller. */\nexport const DEFAULT_ROLE: Role = 'user';\n\n/** Seniority. Higher wins; only used for the assignment rule, not for grants. */\nexport const ROLE_RANK: Record<Role, number> = {\n user: 0,\n admin: 1,\n 'super-admin': 2,\n};\n\n/** Narrow an unknown value (a request body, a Mongo document) to a {@link Role}. */\nexport const isRole = (value: unknown): value is Role =>\n typeof value === 'string' && (Roles as readonly string[]).includes(value);\n\n/**\n * The highest rank among the roles a principal carries, or `-1` when it carries\n * none (or only unrecognised ones) — which can never satisfy an assignment.\n */\nexport const highestRank = (roles: readonly string[]): number =>\n roles.reduce(\n (rank, role) => (isRole(role) ? Math.max(rank, ROLE_RANK[role]) : rank),\n -1,\n );\n\n/**\n * May an actor create or promote someone to `target`? At or below the actor's\n * own tier — so an admin mints admins and users but never a super-admin, and an\n * unauthenticated signup (no roles) can mint nothing.\n */\nexport const canAssignRole = (\n actorRoles: readonly string[],\n target: Role,\n): boolean => highestRank(actorRoles) >= ROLE_RANK[target];\n"],"names":["Roles","DEFAULT_ROLE","ROLE_RANK","user","admin","isRole","value","includes","highestRank","roles","reduce","rank","role","Math","max","canAssignRole","actorRoles","target"],"mappings":"AAAA;;;;;CAKC,GACD,OAAO,MAAMA,QAAQ;IAAC;IAAQ;IAAS;CAAc,CAAU;AAG/D,2EAA2E,GAC3E,OAAO,MAAMC,eAAqB,OAAO;AAEzC,+EAA+E,GAC/E,OAAO,MAAMC,YAAkC;IAC7CC,MAAM;IACNC,OAAO;IACP,eAAe;AACjB,EAAE;AAEF,kFAAkF,GAClF,OAAO,MAAMC,SAAS,CAACC,QACrB,OAAOA,UAAU,YAAY,AAACN,MAA4BO,QAAQ,CAACD,OAAO;AAE5E;;;CAGC,GACD,OAAO,MAAME,cAAc,CAACC,QAC1BA,MAAMC,MAAM,CACV,CAACC,MAAMC,OAAUP,OAAOO,QAAQC,KAAKC,GAAG,CAACH,MAAMT,SAAS,CAACU,KAAK,IAAID,MAClE,CAAC,GACD;AAEJ;;;;CAIC,GACD,OAAO,MAAMI,gBAAgB,CAC3BC,YACAC,SACYT,YAAYQ,eAAed,SAAS,CAACe,OAAO,CAAC"}
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
- * (ADR 0033), and taking a `ScreenType` here is what makes "the account cannot
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;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,gFAAgF;IAChF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;;;;;;OASG;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"}
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
- * (ADR 0033), and taking a `ScreenType` here is what makes "the account cannot
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\n * ([ADR 0042](../../../../../../docs/adr/0042-the-workspace-address-is-the-taxonomy-serialized.md)).\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 * ([ADR 0045](../../../../../../docs/adr/0045-the-wizard-a-step-graph-and-a-submit-that-hands-off.md)).\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 * (ADR 0033), 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":"AA0BA;;;;;;;;;;;;;;;;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
+ {"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 taxonomy of
3
- * [ADR 0033](../../../../../../docs/adr/0033-the-screen-taxonomy.md).
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 | Rendered | The record came from | Lifecycle | Verbs |
6
- * | ----------- | ---------- | -------------------- | --------- | ------------ |
7
- * | `master` | Definiciones | *you* authored it | none | CRUD |
8
- * | `operation` | Operaciones | a *process* made it| yes | domain verbs |
9
- * | `wizard` | Asistentes | guided, multi-step | ends | next/finish |
10
- * | `report` | Consultas | nothing — aggregate| n/a | read only |
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 eleven domains, so "where do I add a
14
- * courier" has one answer whoever owns couriers. The daily operator's speed is
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. Since
25
- * ADR 0026 gave every entity a way to declare verbs, treating a verb as grounds
26
- * for promotion to `operation` would promote all of them.
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;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;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
+ {"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 taxonomy of
3
- * [ADR 0033](../../../../../../docs/adr/0033-the-screen-taxonomy.md).
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 | Rendered | The record came from | Lifecycle | Verbs |
6
- * | ----------- | ---------- | -------------------- | --------- | ------------ |
7
- * | `master` | Definiciones | *you* authored it | none | CRUD |
8
- * | `operation` | Operaciones | a *process* made it| yes | domain verbs |
9
- * | `wizard` | Asistentes | guided, multi-step | ends | next/finish |
10
- * | `report` | Consultas | nothing — aggregate| n/a | read only |
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 eleven domains, so "where do I add a
14
- * courier" has one answer whoever owns couriers. The daily operator's speed is
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. Since
25
- * ADR 0026 gave every entity a way to declare verbs, treating a verb as grounds
26
- * for promotion to `operation` would promote all of them.
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 taxonomy of\n * [ADR 0033](../../../../../../docs/adr/0033-the-screen-taxonomy.md).\n *\n * | Value | Rendered | The record came from | Lifecycle | Verbs |\n * | ----------- | ---------- | -------------------- | --------- | ------------ |\n * | `master` | Definiciones | *you* authored it | none | CRUD |\n * | `operation` | Operaciones | a *process* made it| yes | domain verbs |\n * | `wizard` | Asistentes | guided, multi-step | ends | next/finish |\n * | `report` | Consultas | nothing — aggregate| n/a | read only |\n *\n * The order is the sidebar's top tier, which is the **type** rather than the\n * domain: four fixed entries against eleven domains, so \"where do I add a\n * courier\" has one answer whoever owns couriers. 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. Since\n * ADR 0026 gave every entity a way to declare verbs, treating a verb as grounds\n * for promotion to `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;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,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"}
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.5.0",
3
+ "version": "0.6.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.5.0"
54
+ "@entifix/core": "0.6.0"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "effect": "^4.0.5"