@entifix/authz 0.1.0-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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +4 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/policy/can.d.ts +28 -0
  7. package/dist/policy/can.d.ts.map +1 -0
  8. package/dist/policy/can.js +16 -0
  9. package/dist/policy/can.js.map +1 -0
  10. package/dist/policy/index.d.ts +4 -0
  11. package/dist/policy/index.d.ts.map +1 -0
  12. package/dist/policy/index.js +5 -0
  13. package/dist/policy/index.js.map +1 -0
  14. package/dist/policy/policy-decision.d.ts +48 -0
  15. package/dist/policy/policy-decision.d.ts.map +1 -0
  16. package/dist/policy/policy-decision.js +21 -0
  17. package/dist/policy/policy-decision.js.map +1 -0
  18. package/dist/policy/service-crossing-policy.d.ts +46 -0
  19. package/dist/policy/service-crossing-policy.d.ts.map +1 -0
  20. package/dist/policy/service-crossing-policy.js +32 -0
  21. package/dist/policy/service-crossing-policy.js.map +1 -0
  22. package/dist/values/guarded-command.d.ts +64 -0
  23. package/dist/values/guarded-command.d.ts.map +1 -0
  24. package/dist/values/guarded-command.js +11 -0
  25. package/dist/values/guarded-command.js.map +1 -0
  26. package/dist/values/guarded-nav.d.ts +78 -0
  27. package/dist/values/guarded-nav.d.ts.map +1 -0
  28. package/dist/values/guarded-nav.js +3 -0
  29. package/dist/values/guarded-nav.js.map +1 -0
  30. package/dist/values/index.d.ts +7 -0
  31. package/dist/values/index.d.ts.map +1 -0
  32. package/dist/values/index.js +8 -0
  33. package/dist/values/index.js.map +1 -0
  34. package/dist/values/permission.d.ts +85 -0
  35. package/dist/values/permission.d.ts.map +1 -0
  36. package/dist/values/permission.js +74 -0
  37. package/dist/values/permission.js.map +1 -0
  38. package/dist/values/role.d.ts +26 -0
  39. package/dist/values/role.d.ts.map +1 -0
  40. package/dist/values/role.js +28 -0
  41. package/dist/values/role.js.map +1 -0
  42. package/dist/values/screen-address.d.ts +57 -0
  43. package/dist/values/screen-address.d.ts.map +1 -0
  44. package/dist/values/screen-address.js +44 -0
  45. package/dist/values/screen-address.js.map +1 -0
  46. package/dist/values/screen-type.d.ts +47 -0
  47. package/dist/values/screen-type.d.ts.map +1 -0
  48. package/dist/values/screen-type.js +53 -0
  49. package/dist/values/screen-type.js.map +1 -0
  50. package/package.json +65 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 r10c Technologies
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ export * from './policy/index.js';
2
+ export * from './values/index.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './policy/index.js';
2
+ export * from './values/index.js';
3
+
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './policy/index.js';\nexport * from './values/index.js';\n"],"names":[],"mappings":"AAAA,cAAc,oBAAoB;AAClC,cAAc,oBAAoB"}
@@ -0,0 +1,28 @@
1
+ import { type Permission } from '../values/permission.js';
2
+ /**
3
+ * Which permissions each role grants.
4
+ *
5
+ * ⚠️ **Supplied by the host, never declared here.** The table that says a `user`
6
+ * may ring up a counter sale is a statement about one product, and a framework
7
+ * that ships one has decided its adopters' business for them. The key is a bare
8
+ * `string` rather than a closed `Role` union for the same reason: a host with
9
+ * roles of its own must be able to name them.
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.
14
+ */
15
+ export type GrantTable = Readonly<Record<string, readonly Permission[]>>;
16
+ /**
17
+ * Expand a principal's roles into the permissions they grant. A role the table
18
+ * does not name contributes nothing — an unknown string in a token can never
19
+ * widen access.
20
+ */
21
+ export declare const permissionsOf: (grants: GrantTable, roles: readonly string[]) => readonly Permission[];
22
+ /**
23
+ * The pure authorization check, shared by every layer: the service guard, the
24
+ * server-rendered nav filter, and the browser. No Effect, no IO — so it runs
25
+ * unchanged in Node, in a Next edge middleware, and in a React component.
26
+ */
27
+ export declare const can: (grants: GrantTable, roles: readonly string[], required: Permission) => boolean;
28
+ //# sourceMappingURL=can.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"can.d.ts","sourceRoot":"","sources":["../../src/policy/can.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,UAAU,EAAqB,MAAM,yBAAyB,CAAC;AAE7E;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,CAAC,CAAC,CAAC;AAEzE;;;;GAIG;AACH,eAAO,MAAM,aAAa,GACxB,QAAQ,UAAU,EAClB,OAAO,SAAS,MAAM,EAAE,KACvB,SAAS,UAAU,EAA+C,CAAC;AAEtE;;;;GAIG;AACH,eAAO,MAAM,GAAG,GACd,QAAQ,UAAU,EAClB,OAAO,SAAS,MAAM,EAAE,EACxB,UAAU,UAAU,KACnB,OAGA,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { permissionMatches } from '../values/permission.js';
2
+ /**
3
+ * Expand a principal's roles into the permissions they grant. A role the table
4
+ * does not name contributes nothing — an unknown string in a token can never
5
+ * widen access.
6
+ */ export const permissionsOf = (grants, roles)=>roles.flatMap((role)=>{
7
+ var _grants_role;
8
+ return (_grants_role = grants[role]) != null ? _grants_role : [];
9
+ });
10
+ /**
11
+ * The pure authorization check, shared by every layer: the service guard, the
12
+ * server-rendered nav filter, and the browser. No Effect, no IO — so it runs
13
+ * unchanged in Node, in a Next edge middleware, and in a React component.
14
+ */ export const can = (grants, roles, required)=>permissionsOf(grants, roles).some((granted)=>permissionMatches(granted, required));
15
+
16
+ //# sourceMappingURL=can.js.map
@@ -0,0 +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"}
@@ -0,0 +1,4 @@
1
+ export * from './can.js';
2
+ export * from './policy-decision.js';
3
+ export * from './service-crossing-policy.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/policy/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,sBAAsB,CAAC;AACrC,cAAc,8BAA8B,CAAC"}
@@ -0,0 +1,5 @@
1
+ export * from './can.js';
2
+ export * from './policy-decision.js';
3
+ export * from './service-crossing-policy.js';
4
+
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/policy/index.ts"],"sourcesContent":["export * from './can.js';\nexport * from './policy-decision.js';\nexport * from './service-crossing-policy.js';\n"],"names":[],"mappings":"AAAA,cAAc,WAAW;AACzB,cAAc,uBAAuB;AACrC,cAAc,+BAA+B"}
@@ -0,0 +1,48 @@
1
+ import { Context } from 'effect';
2
+ import { type PermissionAction, type Resource } from '../values/permission.js';
3
+ import { type GrantTable } from './can.js';
4
+ /** The subject half of a policy request — a `Principal` structurally satisfies it. */
5
+ export interface PolicySubject {
6
+ readonly roles: readonly string[];
7
+ readonly attributes?: Readonly<Record<string, unknown>>;
8
+ }
9
+ /**
10
+ * What a caller wants to do, in attribute-based terms. `resource`, `attributes`
11
+ * and `context` are the ABAC seam: the v1 implementation reads only
12
+ * `subject.roles` and `resource`/`action`, but the shape is already the one a
13
+ * rule engine needs, so adopting one never touches a call site.
14
+ */
15
+ export interface PolicyRequest {
16
+ readonly subject: PolicySubject;
17
+ /** `<domain>:<entityKey>` — what is being acted on. */
18
+ readonly resource: Resource;
19
+ readonly action: PermissionAction;
20
+ /** Request-time facts a future policy may read (tenant, time, ownership). */
21
+ readonly context?: Readonly<Record<string, unknown>>;
22
+ }
23
+ /**
24
+ * The authorization port. Synchronous on purpose: a decision must be cheap
25
+ * enough to take on every request, and the v1 answer is a table lookup.
26
+ */
27
+ export interface PolicyDecision {
28
+ decide(request: PolicyRequest): boolean;
29
+ }
30
+ declare const PolicyDecisionTag_base: Context.TagClass<PolicyDecisionTag, "PolicyDecisionTag", PolicyDecision>;
31
+ /** DI tag the composition root binds to a concrete {@link PolicyDecision}. */
32
+ export declare class PolicyDecisionTag extends PolicyDecisionTag_base {
33
+ }
34
+ /**
35
+ * The v1 implementation: role-derived grants from a table the host supplies,
36
+ * ignoring resource attributes and context. Swapping in an attribute-aware
37
+ * engine is a change of the `Layer` that provides {@link PolicyDecisionTag},
38
+ * nothing more.
39
+ *
40
+ * ⚠️ **The table is an argument now, and that is the whole seam.** It used to be
41
+ * imported, which meant this framework shipped the sentence "a `user` may ring
42
+ * up a counter sale" to everyone who installed it. Passing it in costs one
43
+ * argument at each composition root and is what lets two applications with
44
+ * different roles share a policy engine.
45
+ */
46
+ export declare const makeStaticPolicyDecision: (grants: GrantTable) => PolicyDecision;
47
+ export {};
48
+ //# sourceMappingURL=policy-decision.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-decision.d.ts","sourceRoot":"","sources":["../../src/policy/policy-decision.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEjC,OAAO,EACL,KAAK,gBAAgB,EAErB,KAAK,QAAQ,EACd,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAO,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAEhD,sFAAsF;AACtF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACzD;AAED;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACtD;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC;CACzC;;AAED,8EAA8E;AAC9E,qBAAa,iBAAkB,SAAQ,sBAGpC;CAAG;AAEN;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,wBAAwB,GACnC,QAAQ,UAAU,KACjB,cAGD,CAAC"}
@@ -0,0 +1,21 @@
1
+ import { Context } from 'effect';
2
+ import { permissionOf } from '../values/permission.js';
3
+ import { can } from './can.js';
4
+ /** DI tag the composition root binds to a concrete {@link PolicyDecision}. */ export class PolicyDecisionTag extends Context.Tag('PolicyDecisionTag')() {
5
+ }
6
+ /**
7
+ * The v1 implementation: role-derived grants from a table the host supplies,
8
+ * ignoring resource attributes and context. Swapping in an attribute-aware
9
+ * engine is a change of the `Layer` that provides {@link PolicyDecisionTag},
10
+ * nothing more.
11
+ *
12
+ * ⚠️ **The table is an argument now, and that is the whole seam.** It used to be
13
+ * imported, which meant this framework shipped the sentence "a `user` may ring
14
+ * up a counter sale" to everyone who installed it. Passing it in costs one
15
+ * argument at each composition root and is what lets two applications with
16
+ * different roles share a policy engine.
17
+ */ export const makeStaticPolicyDecision = (grants)=>({
18
+ decide: ({ subject, resource, action })=>can(grants, subject.roles, permissionOf(resource, action))
19
+ });
20
+
21
+ //# sourceMappingURL=policy-decision.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/policy/policy-decision.ts"],"sourcesContent":["import { Context } from 'effect';\n\nimport {\n type PermissionAction,\n permissionOf,\n type Resource,\n} from '../values/permission.js';\nimport { can, type GrantTable } from './can.js';\n\n/** The subject half of a policy request — a `Principal` structurally satisfies it. */\nexport interface PolicySubject {\n readonly roles: readonly string[];\n readonly attributes?: Readonly<Record<string, unknown>>;\n}\n\n/**\n * What a caller wants to do, in attribute-based terms. `resource`, `attributes`\n * and `context` are the ABAC seam: the v1 implementation reads only\n * `subject.roles` and `resource`/`action`, but the shape is already the one a\n * rule engine needs, so adopting one never touches a call site.\n */\nexport interface PolicyRequest {\n readonly subject: PolicySubject;\n /** `<domain>:<entityKey>` — what is being acted on. */\n readonly resource: Resource;\n readonly action: PermissionAction;\n /** Request-time facts a future policy may read (tenant, time, ownership). */\n readonly context?: Readonly<Record<string, unknown>>;\n}\n\n/**\n * The authorization port. Synchronous on purpose: a decision must be cheap\n * enough to take on every request, and the v1 answer is a table lookup.\n */\nexport interface PolicyDecision {\n decide(request: PolicyRequest): boolean;\n}\n\n/** DI tag the composition root binds to a concrete {@link PolicyDecision}. */\nexport class PolicyDecisionTag extends Context.Tag('PolicyDecisionTag')<\n PolicyDecisionTag,\n PolicyDecision\n>() {}\n\n/**\n * The v1 implementation: role-derived grants from a table the host supplies,\n * ignoring resource attributes and context. Swapping in an attribute-aware\n * engine is a change of the `Layer` that provides {@link PolicyDecisionTag},\n * nothing more.\n *\n * ⚠️ **The table is an argument now, and that is the whole seam.** It used to be\n * imported, which meant this framework shipped the sentence \"a `user` may ring\n * up a counter sale\" to everyone who installed it. Passing it in costs one\n * argument at each composition root and is what lets two applications with\n * different roles share a policy engine.\n */\nexport const makeStaticPolicyDecision = (\n grants: GrantTable,\n): PolicyDecision => ({\n decide: ({ subject, resource, action }) =>\n can(grants, subject.roles, permissionOf(resource, action)),\n});\n"],"names":["Context","permissionOf","can","PolicyDecisionTag","Tag","makeStaticPolicyDecision","grants","decide","subject","resource","action","roles"],"mappings":"AAAA,SAASA,OAAO,QAAQ,SAAS;AAEjC,SAEEC,YAAY,QAEP,0BAA0B;AACjC,SAASC,GAAG,QAAyB,WAAW;AA+BhD,4EAA4E,GAC5E,OAAO,MAAMC,0BAA0BH,QAAQI,GAAG,CAAC;AAG9C;AAEL;;;;;;;;;;;CAWC,GACD,OAAO,MAAMC,2BAA2B,CACtCC,SACoB,CAAA;QACpBC,QAAQ,CAAC,EAAEC,OAAO,EAAEC,QAAQ,EAAEC,MAAM,EAAE,GACpCR,IAAII,QAAQE,QAAQG,KAAK,EAAEV,aAAaQ,UAAUC;IACtD,CAAA,EAAG"}
@@ -0,0 +1,46 @@
1
+ import { Context } from 'effect';
2
+ import { type Permission } from '../values/permission.js';
3
+ /**
4
+ * What a **service** may do when it crosses into another party's tenant storage.
5
+ *
6
+ * ⚠️ **A second port rather than a second method on {@link PolicyDecision}, and
7
+ * the separation is the security property.** `permissionsOf` expands grants by
8
+ * looking a *role string* up in the grant table, so a crossing grant living
9
+ * there would be inherited by any access token carrying the matching string —
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)).
13
+ */
14
+ export interface ServiceCrossingPolicy {
15
+ allows(required: Permission): boolean;
16
+ }
17
+ declare const ServiceCrossingPolicyTag_base: Context.TagClass<ServiceCrossingPolicyTag, "ServiceCrossingPolicyTag", ServiceCrossingPolicy>;
18
+ /** DI tag the composition root binds to a concrete {@link ServiceCrossingPolicy}. */
19
+ export declare class ServiceCrossingPolicyTag extends ServiceCrossingPolicyTag_base {
20
+ }
21
+ /**
22
+ * The allowlist implementation: a host hands over the whole of what its fleet
23
+ * may do, in one list.
24
+ *
25
+ * The comparison goes through `permissionMatches` rather than `includes`, so a
26
+ * required permission is matched by the same rule everywhere in the system;
27
+ * what differs is only which list is consulted.
28
+ *
29
+ * ⚠️ **A host must not put a wildcard in this list.** A `*` segment would make
30
+ * fleet membership itself the capability, which is the distinction the
31
+ * token/permission split exists to draw: the token proves the caller is the
32
+ * fleet, the list says what the fleet may do.
33
+ */
34
+ export declare const makeStaticServiceCrossingPolicy: (allowed: readonly Permission[]) => ServiceCrossingPolicy;
35
+ /**
36
+ * Refuses every crossing.
37
+ *
38
+ * The right default for an application that has none — an example app, a single
39
+ * service, anything with no saga reaching across a tenant boundary. Supplied
40
+ * explicitly rather than left as a missing Layer, because a missing Layer is a
41
+ * compile error at the composition root and "we do no crossings" deserves to be
42
+ * sayable.
43
+ */
44
+ export declare const NoServiceCrossings: ServiceCrossingPolicy;
45
+ export {};
46
+ //# sourceMappingURL=service-crossing-policy.d.ts.map
@@ -0,0 +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,6BAEM;CAAG;AAEvD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,+BAA+B,GAC1C,SAAS,SAAS,UAAU,EAAE,KAC7B,qBAGD,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,EAAE,qBAEhC,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { Context } from 'effect';
2
+ import { permissionMatches } from '../values/permission.js';
3
+ /** DI tag the composition root binds to a concrete {@link ServiceCrossingPolicy}. */ export class ServiceCrossingPolicyTag extends Context.Tag('ServiceCrossingPolicyTag')() {
4
+ }
5
+ /**
6
+ * The allowlist implementation: a host hands over the whole of what its fleet
7
+ * may do, in one list.
8
+ *
9
+ * The comparison goes through `permissionMatches` rather than `includes`, so a
10
+ * required permission is matched by the same rule everywhere in the system;
11
+ * what differs is only which list is consulted.
12
+ *
13
+ * ⚠️ **A host must not put a wildcard in this list.** A `*` segment would make
14
+ * fleet membership itself the capability, which is the distinction the
15
+ * token/permission split exists to draw: the token proves the caller is the
16
+ * fleet, the list says what the fleet may do.
17
+ */ export const makeStaticServiceCrossingPolicy = (allowed)=>({
18
+ allows: (required)=>allowed.some((granted)=>permissionMatches(granted, required))
19
+ });
20
+ /**
21
+ * Refuses every crossing.
22
+ *
23
+ * The right default for an application that has none — an example app, a single
24
+ * service, anything with no saga reaching across a tenant boundary. Supplied
25
+ * explicitly rather than left as a missing Layer, because a missing Layer is a
26
+ * compile error at the composition root and "we do no crossings" deserves to be
27
+ * sayable.
28
+ */ export const NoServiceCrossings = {
29
+ allows: ()=>false
30
+ };
31
+
32
+ //# sourceMappingURL=service-crossing-policy.js.map
@@ -0,0 +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.Tag(\n 'ServiceCrossingPolicyTag',\n)<ServiceCrossingPolicyTag, ServiceCrossingPolicy>() {}\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","Tag","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,GAAG,CACvD;AACoD;AAEtD;;;;;;;;;;;;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"}
@@ -0,0 +1,64 @@
1
+ import type { Permission } from './permission.js';
2
+ /**
3
+ * A deterministic command plus the permission it requires, if any.
4
+ *
5
+ * The sibling of {@link GuardedNavItem}, and here for the identical reason: this
6
+ * package is the only vocabulary a `layer:shell` package and a `layer:app` both
7
+ * reach, so a `scope:shared` shell can contribute commands to whichever host
8
+ * mounts it without either of them importing the other.
9
+ *
10
+ * It is a *separate* type rather than a nav item with a flag because the two
11
+ * answer different questions. A nav item is a place you can be; a command is
12
+ * something you can do, and it is the thing that has keywords, a page and no
13
+ * sidebar row. Folding them would put `keywordsKey` on every destination and
14
+ * `type` on every command.
15
+ *
16
+ * `labelKey`/`keywordsKey` are **namespace-qualified catalog keys**, not copy —
17
+ * whoever renders the palette does not own the wording, and spelling the
18
+ * namespace out is also what makes an `app:` key visible to the lint rule that
19
+ * keeps those inside `apps/`.
20
+ *
21
+ * Filtering on `permission`/`entitled` is **presentation only**, exactly as it
22
+ * is for navigation: the route behind the command is what refuses the request.
23
+ */
24
+ export interface GuardedCommand {
25
+ /** Stable id, unique across every contributing shell. Also the recency key. */
26
+ key: string;
27
+ labelKey: string;
28
+ /**
29
+ * A catalog key whose value is a **comma-separated** list of extra terms this
30
+ * command answers to — how a Spanish command still matches the English word
31
+ * someone typed. `parseKeywords` in `@entifix/core` owns the split.
32
+ */
33
+ keywordsKey?: string;
34
+ /** Locale-free, like every other internal href; the renderer prefixes it. */
35
+ href: string;
36
+ /**
37
+ * Which palette page this command sits on. Omitted means the root.
38
+ *
39
+ * Depth is a stack of pages rather than a nested prefix, so a group of related
40
+ * commands — every "new record" route, say — is one root entry that descends
41
+ * instead of N entries competing with the destinations beside them.
42
+ */
43
+ page?: string;
44
+ /** Omit to show the command to every signed-in user. */
45
+ permission?: Permission;
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
49
+ * the reasoning {@link GuardedNavItem.entitled} carries. Setting it without a
50
+ * permission is the same declaration error, and fails the same way.
51
+ */
52
+ entitled?: boolean;
53
+ }
54
+ /**
55
+ * The page every "create a record" command descends into.
56
+ *
57
+ * A shared constant rather than a literal in each shell: the domain shells
58
+ * declare commands onto this page and the palette synthesizes the root entry
59
+ * that opens it, so the two halves have to agree and there is no test that
60
+ * would notice a typo — the page would simply be unreachable, with every
61
+ * command still declared and still granted.
62
+ */
63
+ export declare const NEW_COMMAND_PAGE = "new";
64
+ //# sourceMappingURL=guarded-command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guarded-command.d.ts","sourceRoot":"","sources":["../../src/values/guarded-command.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,cAAc;IAC7B,+EAA+E;IAC/E,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,QAAQ,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The page every "create a record" command descends into.
3
+ *
4
+ * A shared constant rather than a literal in each shell: the domain shells
5
+ * declare commands onto this page and the palette synthesizes the root entry
6
+ * that opens it, so the two halves have to agree and there is no test that
7
+ * would notice a typo — the page would simply be unreachable, with every
8
+ * command still declared and still granted.
9
+ */ export const NEW_COMMAND_PAGE = 'new';
10
+
11
+ //# sourceMappingURL=guarded-command.js.map
@@ -0,0 +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 `layer:shell` package and a `layer:app` both\n * reach, so a `scope:shared` 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"}
@@ -0,0 +1,78 @@
1
+ import type { Permission } from './permission.js';
2
+ import type { ScreenType } from './screen-type.js';
3
+ /**
4
+ * A navigation destination plus the permission it requires, if any.
5
+ *
6
+ * This is the permission-annotated half of navigation, which is why it lives with
7
+ * the authorization vocabulary rather than in a shell: an entry and the route
8
+ * behind it must be expressed in the same terms `requirePermission` enforces, and
9
+ * `can` — the filter every layer applies — is declared right next to it.
10
+ *
11
+ * It is also the only layer both a `layer:shell` package and a `layer:app` can
12
+ * depend on. That matters because a `scope:shared` shell contributes its own nav
13
+ * fragment to whichever host mounts it, and shells may not depend on each other.
14
+ *
15
+ * `label`/`title` are **namespace-qualified catalog keys**, not copy. Whoever
16
+ * renders the list does not necessarily own its wording, so the namespace travels
17
+ * with the key — and spelling it out is also what makes an `app:` key visible to
18
+ * the lint rule that keeps those keys inside `apps/`.
19
+ *
20
+ * Filtering on these is **presentation only**. Hiding an entry protects nothing;
21
+ * the service is what refuses the request.
22
+ */
23
+ export interface GuardedNavItem {
24
+ label: string;
25
+ href: string;
26
+ icon?: string;
27
+ /**
28
+ * When set, the item also offers "Open in workspace" — a link to
29
+ * `/workspace?tab=<workspace>`.
30
+ */
31
+ workspace?: string;
32
+ /** Omit to show the item to every signed-in user. */
33
+ permission?: Permission;
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}.
37
+ *
38
+ * A boolean rather than a domain name, because the domain is already written
39
+ * one line up: a second string here could disagree with the permission the
40
+ * route behind it actually checks, which is the drift this whole interface
41
+ * exists to prevent.
42
+ *
43
+ * **Omitted is the right answer for most items.** An organization is not
44
+ * "provisioned for" `catalog-reference`, `config` or `authn` — those are
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
47
+ * vocabulary from every vendor.
48
+ *
49
+ * Setting it without a `permission` is a declaration error and throws: there
50
+ * would be no domain to read, and both possible defaults — always shown,
51
+ * always hidden — read as a bug somewhere else entirely.
52
+ *
53
+ * A session with **no** organization ignores this flag: the ceiling is a
54
+ * property of an organization, so an operator is outside it rather than
55
+ * refused by it.
56
+ */
57
+ entitled?: boolean;
58
+ }
59
+ export interface GuardedNavSection {
60
+ title?: string;
61
+ items: GuardedNavItem[];
62
+ /**
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)).
65
+ *
66
+ * Every contributing shell declares its own, because this interface is the
67
+ * only thing a `layer:shell` package and a `layer:app` both reach: adding the
68
+ * tier is not an edit in the host.
69
+ *
70
+ * **Optional, and narrowly so.** A section with no type is not a fifth
71
+ * category, it is a section that is not a screen group at all — the account
72
+ * surface is the one case, and it carries no permission for the same reason.
73
+ * A second untyped section appearing without a written reason means the
74
+ * taxonomy is missing a case.
75
+ */
76
+ type?: ScreenType;
77
+ }
78
+ //# sourceMappingURL=guarded-nav.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guarded-nav.d.ts","sourceRoot":"","sources":["../../src/values/guarded-nav.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB;;;;;;;;;;;;;OAaG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB"}
@@ -0,0 +1,3 @@
1
+ export { };
2
+
3
+ //# sourceMappingURL=guarded-nav.js.map
@@ -0,0 +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 only layer both a `layer:shell` package and a `layer:app` can\n * depend on. That matters because a `scope:shared` 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 `layer:shell` package and a `layer:app` 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"}
@@ -0,0 +1,7 @@
1
+ export * from './guarded-command.js';
2
+ export * from './guarded-nav.js';
3
+ export * from './permission.js';
4
+ export * from './role.js';
5
+ export * from './screen-address.js';
6
+ export * from './screen-type.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/values/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC"}
@@ -0,0 +1,8 @@
1
+ export * from './guarded-command.js';
2
+ export * from './guarded-nav.js';
3
+ export * from './permission.js';
4
+ export * from './role.js';
5
+ export * from './screen-address.js';
6
+ export * from './screen-type.js';
7
+
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/values/index.ts"],"sourcesContent":["export * from './guarded-command.js';\nexport * from './guarded-nav.js';\nexport * from './permission.js';\nexport * from './role.js';\nexport * from './screen-address.js';\nexport * from './screen-type.js';\n"],"names":[],"mappings":"AAAA,cAAc,uBAAuB;AACrC,cAAc,mBAAmB;AACjC,cAAc,kBAAkB;AAChC,cAAc,YAAY;AAC1B,cAAc,sBAAsB;AACpC,cAAc,mBAAmB"}
@@ -0,0 +1,85 @@
1
+ import { type Entity, type EntityAction, type EntityConstructor, type UseCaseConstructor } from '@entifix/core';
2
+ /**
3
+ * What a caller wants to do with a resource.
4
+ *
5
+ * Defined in `@entifix/core` and aliased here, not duplicated: a UI
6
+ * reads the same triple off a served `EntityMetadataDocument` to decide whether
7
+ * to render Save and Delete, and `entifix:react` may not import the business
8
+ * layer. Aliasing keeps one definition and leaves every call site below
9
+ * unchanged.
10
+ */
11
+ export declare const Actions: readonly ["read", "write", "delete"];
12
+ export type Action = EntityAction;
13
+ /**
14
+ * A verb declared by `@useCase()` — `publish`, `revoke-sessions`, `approve`.
15
+ *
16
+ * Per-entity and open, rather than one global union every domain appends to:
17
+ * that would make an unrelated domain's verb assignable everywhere and grow
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)).
21
+ */
22
+ export type UseCaseVerb = string;
23
+ /**
24
+ * The third segment of a permission: the CRUD triple, or a use-case verb.
25
+ *
26
+ * `Action` stays the narrow three-value union, and every place that guards CRUD
27
+ * keeps using it. This wider alias exists only where a segment has already been
28
+ * parsed off a string and could legitimately be either.
29
+ */
30
+ export type PermissionAction = Action | UseCaseVerb;
31
+ /**
32
+ * A permission is three colon-separated segments — `<domain>:<entityKey>:<action>`
33
+ * — so it can be derived from an entity's own `@entity({ domain, key })`
34
+ * metadata rather than maintained as a parallel list. `*` in any segment of a
35
+ * **granted** permission is a wildcard (see {@link permissionMatches}); a
36
+ * **required** permission is always concrete.
37
+ */
38
+ export type Permission = `${string}:${string}:${string}`;
39
+ /** The `<domain>:<entityKey>` half of a permission — what is being acted on. */
40
+ export type Resource = `${string}:${string}`;
41
+ /** Build a permission from its parts. `*` is a legal action on a grant. */
42
+ export declare const permissionFor: (domain: string, entityKey: string, action: PermissionAction) => Permission;
43
+ /** Join a resource and an action into the permission that guards it. */
44
+ export declare const permissionOf: (resource: Resource, action: PermissionAction) => Permission;
45
+ /**
46
+ * Derive the permission guarding an entity from the entity itself. This is the
47
+ * point of the `<domain>:<key>:<action>` shape: making a new entity guardable
48
+ * needs no new vocabulary, only its existing `@entity()` metadata.
49
+ */
50
+ export declare const permissionForEntity: <T extends Entity>(entityConstructor: EntityConstructor<T>, action: Action) => Permission;
51
+ /**
52
+ * Derive the permission guarding a use case.
53
+ *
54
+ * The **one-argument** form is what call sites use: a `@useCase()` class knows
55
+ * both the entity it acts on and its own verb, so the verb string is written
56
+ * once — in the decorator — and every guard imports the derived constant instead
57
+ * of retyping it. The **two-argument** form is for a caller that holds only the
58
+ * entity, which is the `$metadata` route walking an entity's descriptors.
59
+ *
60
+ * Both check that the entity really declares the verb, so a typo throws at
61
+ * module load rather than silently denying every request. That check is at
62
+ * runtime because compile-time narrowing does not survive the decorator
63
+ * boundary: `Symbol.metadata` erases types and `extractMetaEntity` hands back an
64
+ * untyped `MetaEntity`. Nothing is lost that existed — `parsePermission` looks
65
+ * like it validates the action segment and does not.
66
+ */
67
+ export declare function permissionForUseCase(useCase: UseCaseConstructor): Permission;
68
+ export declare function permissionForUseCase<T extends Entity>(entityConstructor: EntityConstructor<T>, key: UseCaseVerb): Permission;
69
+ /**
70
+ * Split a permission back into the resource + action a policy decision reads.
71
+ * Throws on anything that is not exactly three segments, so a typo in a guard
72
+ * fails at the call site instead of silently denying every request.
73
+ */
74
+ export declare const parsePermission: (permission: Permission) => {
75
+ resource: Resource;
76
+ action: PermissionAction;
77
+ };
78
+ /**
79
+ * Does a granted permission cover a required one? Compared segment by segment,
80
+ * with `*` on the granted side matching anything. `super-admin`'s `*:*:*` thus
81
+ * covers everything, and `product-configuration-management:*:read` covers every
82
+ * entity in that domain without naming them.
83
+ */
84
+ export declare const permissionMatches: (granted: Permission, required: Permission) => boolean;
85
+ //# sourceMappingURL=permission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission.d.ts","sourceRoot":"","sources":["../../src/values/permission.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,MAAM,EAEX,KAAK,YAAY,EACjB,KAAK,iBAAiB,EAGtB,KAAK,kBAAkB,EACxB,MAAM,eAAe,CAAC;AAEvB;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,sCAAiB,CAAC;AACtC,MAAM,MAAM,MAAM,GAAG,YAAY,CAAC;AAElC;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC;AAEjC;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,WAAW,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,GAAG,MAAM,IAAI,MAAM,IAAI,MAAM,EAAE,CAAC;AAEzD,gFAAgF;AAChF,MAAM,MAAM,QAAQ,GAAG,GAAG,MAAM,IAAI,MAAM,EAAE,CAAC;AAK7C,2EAA2E;AAC3E,eAAO,MAAM,aAAa,GACxB,QAAQ,MAAM,EACd,WAAW,MAAM,EACjB,QAAQ,gBAAgB,KACvB,UAAgD,CAAC;AAEpD,wEAAwE;AACxE,eAAO,MAAM,YAAY,GACvB,UAAU,QAAQ,EAClB,QAAQ,gBAAgB,KACvB,UAAqC,CAAC;AAEzC;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAAI,CAAC,SAAS,MAAM,EAClD,mBAAmB,iBAAiB,CAAC,CAAC,CAAC,EACvC,QAAQ,MAAM,KACb,UAQF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,kBAAkB,GAAG,UAAU,CAAC;AAC9E,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,MAAM,EACnD,iBAAiB,EAAE,iBAAiB,CAAC,CAAC,CAAC,EACvC,GAAG,EAAE,WAAW,GACf,UAAU,CAAC;AA2Bd;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAC1B,YAAY,UAAU,KACrB;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAA;CAiBhD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAC5B,SAAS,UAAU,EACnB,UAAU,UAAU,KACnB,OAaF,CAAC"}
@@ -0,0 +1,74 @@
1
+ import { describeEntityUseCases, EntifixBuildError, ENTITY_ACTIONS, extractMetaEntity, extractMetaUseCaseBinding } from '@entifix/core';
2
+ /**
3
+ * What a caller wants to do with a resource.
4
+ *
5
+ * Defined in `@entifix/core` and aliased here, not duplicated: a UI
6
+ * reads the same triple off a served `EntityMetadataDocument` to decide whether
7
+ * to render Save and Delete, and `entifix:react` may not import the business
8
+ * layer. Aliasing keeps one definition and leaves every call site below
9
+ * unchanged.
10
+ */ export const Actions = ENTITY_ACTIONS;
11
+ const SEGMENT_COUNT = 3;
12
+ const WILDCARD = '*';
13
+ /** Build a permission from its parts. `*` is a legal action on a grant. */ export const permissionFor = (domain, entityKey, action)=>`${domain}:${entityKey}:${action}`;
14
+ /** Join a resource and an action into the permission that guards it. */ export const permissionOf = (resource, action)=>`${resource}:${action}`;
15
+ /**
16
+ * Derive the permission guarding an entity from the entity itself. This is the
17
+ * point of the `<domain>:<key>:<action>` shape: making a new entity guardable
18
+ * needs no new vocabulary, only its existing `@entity()` metadata.
19
+ */ export const permissionForEntity = (entityConstructor, action)=>{
20
+ const meta = extractMetaEntity(entityConstructor);
21
+ if (meta.domain === undefined || meta.key === undefined) {
22
+ throw new EntifixBuildError(`Entity ${entityConstructor.name} needs both a domain and a key to derive a permission`);
23
+ }
24
+ return permissionFor(meta.domain, meta.key, action);
25
+ };
26
+ export function permissionForUseCase(target, key) {
27
+ const binding = key === undefined ? extractMetaUseCaseBinding(target) : {
28
+ entity: target,
29
+ key
30
+ };
31
+ const meta = extractMetaEntity(binding.entity);
32
+ if (meta.domain === undefined || meta.key === undefined) {
33
+ throw new EntifixBuildError(`Entity ${binding.entity.name} needs both a domain and a key to derive a permission`);
34
+ }
35
+ const declares = describeEntityUseCases(binding.entity).some((useCase)=>useCase.key === binding.key);
36
+ if (!declares) {
37
+ throw new EntifixBuildError(`Entity ${binding.entity.name} does not declare the use case "${binding.key}"`);
38
+ }
39
+ return permissionFor(meta.domain, meta.key, binding.key);
40
+ }
41
+ /**
42
+ * Split a permission back into the resource + action a policy decision reads.
43
+ * Throws on anything that is not exactly three segments, so a typo in a guard
44
+ * fails at the call site instead of silently denying every request.
45
+ */ export const parsePermission = (permission)=>{
46
+ const segments = permission.split(':');
47
+ if (segments.length !== SEGMENT_COUNT) {
48
+ throw new EntifixBuildError(`Malformed permission "${permission}" — expected <domain>:<entityKey>:<action>`);
49
+ }
50
+ // The cast is only the arity the length check above already established. It
51
+ // deliberately does not narrow the action segment: that used to say `Action`,
52
+ // which read as a typo check it never performed, and is now false as well —
53
+ // a use-case verb is a legal third segment.
54
+ const [domain, entityKey, action] = segments;
55
+ return {
56
+ resource: `${domain}:${entityKey}`,
57
+ action
58
+ };
59
+ };
60
+ /**
61
+ * Does a granted permission cover a required one? Compared segment by segment,
62
+ * with `*` on the granted side matching anything. `super-admin`'s `*:*:*` thus
63
+ * covers everything, and `product-configuration-management:*:read` covers every
64
+ * entity in that domain without naming them.
65
+ */ export const permissionMatches = (granted, required)=>{
66
+ const grantedSegments = granted.split(':');
67
+ const requiredSegments = required.split(':');
68
+ if (grantedSegments.length !== SEGMENT_COUNT || requiredSegments.length !== SEGMENT_COUNT) {
69
+ return false;
70
+ }
71
+ return grantedSegments.every((segment, index)=>segment === WILDCARD || segment === requiredSegments[index]);
72
+ };
73
+
74
+ //# sourceMappingURL=permission.js.map
@@ -0,0 +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 `entifix:react` may not import the business\n * layer. 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"}
@@ -0,0 +1,26 @@
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.
6
+ */
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;
15
+ /**
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.
18
+ */
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;
26
+ //# sourceMappingURL=role.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,28 @@
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.
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
16
+ };
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
+
28
+ //# sourceMappingURL=role.js.map
@@ -0,0 +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"}
@@ -0,0 +1,57 @@
1
+ import type { ScreenType } from './screen-type.js';
2
+ /**
3
+ * Where a screen lives, as data — a {@link ScreenType}, the entity or screen key
4
+ * within it, and optionally the record being viewed.
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)).
8
+ */
9
+ export interface ScreenAddress {
10
+ readonly type: ScreenType;
11
+ /** The entity key (`@entity({ key })`) or, for a hand-built screen, its own. */
12
+ readonly key: string;
13
+ /**
14
+ * The position **within** the screen, and what that is depends on the type:
15
+ * a record for `master` (absent for the list of them), a step for `wizard`
16
+ * (absent for its first step).
17
+ *
18
+ * One segment for both, rather than a member per type, because the grammar is
19
+ * 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)).
22
+ */
23
+ readonly id?: string;
24
+ }
25
+ /**
26
+ * Serialize a {@link ScreenAddress} — `<type>:<key>` for a list, and
27
+ * `<type>:<key>:<id>` for one record.
28
+ *
29
+ * The address is the taxonomy serialized, which is why this lives beside
30
+ * {@link ScreenType} rather than in the workspace that consumes it: it was three
31
+ * grammars (`catalog:` for a list, `entity:` for a record, `system:` for a
32
+ * hand-built screen) spelled out at five call sites that had to agree with each
33
+ * other by hand. Two of those were the same string built twice — the tab address
34
+ * and the autosaved draft's key — so a drift between them silently detached a
35
+ * tab from its own draft.
36
+ *
37
+ * There is no address for a section carrying no {@link ScreenType}. That is not
38
+ * an omission: the account surface is deliberately outside the taxonomy
39
+ * (ADR 0033), and taking a `ScreenType` here is what makes "the account cannot
40
+ * be a tab" a fact the compiler enforces rather than a rule somebody remembers.
41
+ */
42
+ export declare function screenAddress({ type, key, id }: ScreenAddress): string;
43
+ /**
44
+ * The inverse of everything {@link screenAddress} writes after `<type>:` — which
45
+ * is exactly what a `TabKind.match` receives, because the registry has already
46
+ * split the kind off the front.
47
+ *
48
+ * Returns `null` rather than a partial result for anything malformed, so a
49
+ * caller cannot accidentally treat an empty key or an empty id as present. A
50
+ * `'brand:'` that parsed as `{ key: 'brand', id: '' }` would resolve to the
51
+ * record editor for a record with no id.
52
+ */
53
+ export declare function parseScreenPayload(payload: string): {
54
+ key: string;
55
+ id?: string;
56
+ } | null;
57
+ //# sourceMappingURL=screen-address.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Serialize a {@link ScreenAddress} — `<type>:<key>` for a list, and
3
+ * `<type>:<key>:<id>` for one record.
4
+ *
5
+ * The address is the taxonomy serialized, which is why this lives beside
6
+ * {@link ScreenType} rather than in the workspace that consumes it: it was three
7
+ * grammars (`catalog:` for a list, `entity:` for a record, `system:` for a
8
+ * hand-built screen) spelled out at five call sites that had to agree with each
9
+ * other by hand. Two of those were the same string built twice — the tab address
10
+ * and the autosaved draft's key — so a drift between them silently detached a
11
+ * tab from its own draft.
12
+ *
13
+ * There is no address for a section carrying no {@link ScreenType}. That is not
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
16
+ * be a tab" a fact the compiler enforces rather than a rule somebody remembers.
17
+ */ export function screenAddress({ type, key, id }) {
18
+ return id === undefined ? `${type}:${key}` : `${type}:${key}:${id}`;
19
+ }
20
+ /**
21
+ * The inverse of everything {@link screenAddress} writes after `<type>:` — which
22
+ * is exactly what a `TabKind.match` receives, because the registry has already
23
+ * split the kind off the front.
24
+ *
25
+ * Returns `null` rather than a partial result for anything malformed, so a
26
+ * caller cannot accidentally treat an empty key or an empty id as present. A
27
+ * `'brand:'` that parsed as `{ key: 'brand', id: '' }` would resolve to the
28
+ * record editor for a record with no id.
29
+ */ export function parseScreenPayload(payload) {
30
+ const separator = payload.indexOf(':');
31
+ if (separator === -1) {
32
+ return payload === '' ? null : {
33
+ key: payload
34
+ };
35
+ }
36
+ const key = payload.slice(0, separator);
37
+ const id = payload.slice(separator + 1);
38
+ return key === '' || id === '' ? null : {
39
+ key,
40
+ id
41
+ };
42
+ }
43
+
44
+ //# sourceMappingURL=screen-address.js.map
@@ -0,0 +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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * What shape a back-office screen takes — the taxonomy of
3
+ * [ADR 0033](../../../../../../docs/adr/0033-the-screen-taxonomy.md).
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 |
11
+ *
12
+ * 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
15
+ * the command palette's job, not the sidebar's.
16
+ *
17
+ * The identifiers are English because every other identifier here is, but two of
18
+ * the obvious words were unusable — `query` collides with RSQL, TanStack Query
19
+ * and `filterable` metadata, and `assistant` reads as an AI agent — so the enum
20
+ * takes `report` and `wizard`, which are already what this repo calls those. The
21
+ * consequence is that the code word and the screen word differ for three of the
22
+ * four, and {@link SCREEN_TYPE_LABEL_KEYS} is the one place that reconciles them.
23
+ *
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.
27
+ */
28
+ export declare const ScreenTypes: readonly ["master", "operation", "wizard", "report"];
29
+ export type ScreenType = (typeof ScreenTypes)[number];
30
+ /**
31
+ * The shipped Spanish names, as catalog keys.
32
+ *
33
+ * `shell:`-namespaced copy in a `business:policy` package is unusual and
34
+ * deliberate: the four names are one vocabulary, and a second declaration site
35
+ * for them is the drift `nav.ts` was already merged once to stop. Only the
36
+ * `app:` namespace is restricted by location, so this is legal as well as
37
+ * intended.
38
+ */
39
+ export declare const SCREEN_TYPE_LABEL_KEYS: Record<ScreenType, string>;
40
+ /** Narrow an unknown value (a stored tab address, a request body) to a {@link ScreenType}. */
41
+ export declare const isScreenType: (value: unknown) => value is ScreenType;
42
+ /**
43
+ * Sort key for the top tier. A section with no type sorts last — the account
44
+ * surface is not a screen group, so it belongs below the ones that are.
45
+ */
46
+ export declare const screenTypeRank: (type: ScreenType | undefined) => number;
47
+ //# sourceMappingURL=screen-type.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * What shape a back-office screen takes — the taxonomy of
3
+ * [ADR 0033](../../../../../../docs/adr/0033-the-screen-taxonomy.md).
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 |
11
+ *
12
+ * 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
15
+ * the command palette's job, not the sidebar's.
16
+ *
17
+ * The identifiers are English because every other identifier here is, but two of
18
+ * the obvious words were unusable — `query` collides with RSQL, TanStack Query
19
+ * and `filterable` metadata, and `assistant` reads as an AI agent — so the enum
20
+ * takes `report` and `wizard`, which are already what this repo calls those. The
21
+ * consequence is that the code word and the screen word differ for three of the
22
+ * four, and {@link SCREEN_TYPE_LABEL_KEYS} is the one place that reconciles them.
23
+ *
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.
27
+ */ export const ScreenTypes = [
28
+ 'master',
29
+ 'operation',
30
+ 'wizard',
31
+ 'report'
32
+ ];
33
+ /**
34
+ * The shipped Spanish names, as catalog keys.
35
+ *
36
+ * `shell:`-namespaced copy in a `business:policy` package is unusual and
37
+ * deliberate: the four names are one vocabulary, and a second declaration site
38
+ * for them is the drift `nav.ts` was already merged once to stop. Only the
39
+ * `app:` namespace is restricted by location, so this is legal as well as
40
+ * intended.
41
+ */ export const SCREEN_TYPE_LABEL_KEYS = {
42
+ master: 'shell:nav.screenType.master',
43
+ operation: 'shell:nav.screenType.operation',
44
+ wizard: 'shell:nav.screenType.wizard',
45
+ report: 'shell:nav.screenType.report'
46
+ };
47
+ /** Narrow an unknown value (a stored tab address, a request body) to a {@link ScreenType}. */ export const isScreenType = (value)=>typeof value === 'string' && ScreenTypes.includes(value);
48
+ /**
49
+ * Sort key for the top tier. A section with no type sorts last — the account
50
+ * surface is not a screen group, so it belongs below the ones that are.
51
+ */ export const screenTypeRank = (type)=>type === undefined ? ScreenTypes.length : ScreenTypes.indexOf(type);
52
+
53
+ //# sourceMappingURL=screen-type.js.map
@@ -0,0 +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"}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@entifix/authz",
3
+ "version": "0.1.0-0",
4
+ "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/r10c-technologies/entifix.git",
8
+ "directory": "packages/ts/authz"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "!**/*.tsbuildinfo",
16
+ "LICENSE"
17
+ ],
18
+ "type": "module",
19
+ "main": "./dist/index.js",
20
+ "module": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ "./package.json": "./package.json",
24
+ ".": {
25
+ "@entifix/source": "./src/index.ts",
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js",
28
+ "default": "./dist/index.js"
29
+ }
30
+ },
31
+ "nx": {
32
+ "tags": [
33
+ "layer:entifix",
34
+ "scope:shared",
35
+ "entifix:contract",
36
+ "tier:4"
37
+ ],
38
+ "sourceRoot": "packages/ts/authz/src",
39
+ "targets": {
40
+ "build": {
41
+ "executor": "@nx/js:swc",
42
+ "outputs": [
43
+ "{options.outputPath}"
44
+ ],
45
+ "options": {
46
+ "outputPath": "packages/ts/authz/dist",
47
+ "main": "packages/ts/authz/src/index.ts",
48
+ "tsConfig": "packages/ts/authz/tsconfig.lib.json",
49
+ "skipTypeCheck": true,
50
+ "stripLeadingPaths": true
51
+ }
52
+ }
53
+ }
54
+ },
55
+ "dependencies": {
56
+ "@swc/helpers": "0.5.23",
57
+ "@entifix/core": "0.1.0-0"
58
+ },
59
+ "peerDependencies": {
60
+ "effect": ">=3.22 <4"
61
+ },
62
+ "devDependencies": {
63
+ "effect": "3.22.1"
64
+ }
65
+ }