@zhama/tego-plugin-sdk 0.0.0-stage → 1.36.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 (98) hide show
  1. package/LICENSE +172 -0
  2. package/README.md +846 -2
  3. package/access-catalog.schema.json +304 -0
  4. package/access-fixtures/resolve.json +1359 -0
  5. package/bin/tego-plugin-pack.mjs +2 -0
  6. package/bin/tego-plugin.mjs +3 -0
  7. package/dist/access-drizzle.cjs +1 -0
  8. package/dist/access-drizzle.d.cts +726 -0
  9. package/dist/access-drizzle.d.ts +726 -0
  10. package/dist/access-drizzle.js +1 -0
  11. package/dist/access-hono.cjs +3 -0
  12. package/dist/access-hono.d.cts +108 -0
  13. package/dist/access-hono.d.ts +108 -0
  14. package/dist/access-hono.js +3 -0
  15. package/dist/access-implications.cjs +1 -0
  16. package/dist/access-implications.d.cts +25 -0
  17. package/dist/access-implications.d.ts +25 -0
  18. package/dist/access-implications.js +1 -0
  19. package/dist/access.cjs +26 -0
  20. package/dist/access.d.cts +884 -0
  21. package/dist/access.d.ts +884 -0
  22. package/dist/access.js +26 -0
  23. package/dist/catalog-CSSbrfEf.d.cts +361 -0
  24. package/dist/catalog-CSSbrfEf.d.ts +361 -0
  25. package/dist/chunk-3KUOBGJN.js +19 -0
  26. package/dist/chunk-3WA3M347.cjs +1 -0
  27. package/dist/chunk-5CU33JVY.js +1 -0
  28. package/dist/chunk-7K37PVGV.js +5 -0
  29. package/dist/chunk-7XHF3YSK.js +1 -0
  30. package/dist/chunk-AJY7ZHXK.cjs +3 -0
  31. package/dist/chunk-BN6TGHSB.cjs +1 -0
  32. package/dist/chunk-BTHXQWHT.js +1 -0
  33. package/dist/chunk-C2OCOPZQ.cjs +1 -0
  34. package/dist/chunk-IMAVYQ4U.cjs +4 -0
  35. package/dist/chunk-OP64Y4IN.cjs +1 -0
  36. package/dist/chunk-QKSPPMVN.js +1 -0
  37. package/dist/chunk-ROFUQLOZ.cjs +1 -0
  38. package/dist/chunk-SNUCO2MK.js +1 -0
  39. package/dist/chunk-THMWRUAT.cjs +19 -0
  40. package/dist/chunk-VMCGDDGP.js +3 -0
  41. package/dist/chunk-W7C357A6.cjs +5 -0
  42. package/dist/chunk-WLTASJF6.js +4 -0
  43. package/dist/egress.cjs +7 -0
  44. package/dist/egress.d.cts +9 -0
  45. package/dist/egress.d.ts +9 -0
  46. package/dist/egress.js +7 -0
  47. package/dist/frontend.cjs +2 -0
  48. package/dist/frontend.d.cts +353 -0
  49. package/dist/frontend.d.ts +353 -0
  50. package/dist/frontend.js +2 -0
  51. package/dist/gateway-signature.cjs +1 -0
  52. package/dist/gateway-signature.d.cts +112 -0
  53. package/dist/gateway-signature.d.ts +112 -0
  54. package/dist/gateway-signature.js +1 -0
  55. package/dist/index.cjs +1 -0
  56. package/dist/index.d.cts +528 -0
  57. package/dist/index.d.ts +528 -0
  58. package/dist/index.js +1 -0
  59. package/dist/manifest-types-DQARCQJt.d.cts +627 -0
  60. package/dist/manifest-types-DQARCQJt.d.ts +627 -0
  61. package/dist/manifest.cjs +1 -0
  62. package/dist/manifest.d.cts +2824 -0
  63. package/dist/manifest.d.ts +2824 -0
  64. package/dist/manifest.js +1 -0
  65. package/dist/mcp-identity.cjs +2 -0
  66. package/dist/mcp-identity.d.cts +106 -0
  67. package/dist/mcp-identity.d.ts +106 -0
  68. package/dist/mcp-identity.js +2 -0
  69. package/dist/pack.cjs +1 -0
  70. package/dist/pack.d.cts +169 -0
  71. package/dist/pack.d.ts +169 -0
  72. package/dist/pack.js +1 -0
  73. package/dist/publish.cjs +1 -0
  74. package/dist/publish.d.cts +96 -0
  75. package/dist/publish.d.ts +96 -0
  76. package/dist/publish.js +1 -0
  77. package/dist/scaffold.cjs +523 -0
  78. package/dist/scaffold.d.cts +182 -0
  79. package/dist/scaffold.d.ts +182 -0
  80. package/dist/scaffold.js +523 -0
  81. package/dist/server-runtime.cjs +1 -0
  82. package/dist/server-runtime.d.cts +63 -0
  83. package/dist/server-runtime.d.ts +63 -0
  84. package/dist/server-runtime.js +1 -0
  85. package/dist/server.cjs +1 -0
  86. package/dist/server.d.cts +1808 -0
  87. package/dist/server.d.ts +1808 -0
  88. package/dist/server.js +1 -0
  89. package/dist/testing.cjs +2 -0
  90. package/dist/testing.d.cts +92 -0
  91. package/dist/testing.d.ts +92 -0
  92. package/dist/testing.js +2 -0
  93. package/dist/vite.cjs +1 -0
  94. package/dist/vite.d.cts +133 -0
  95. package/dist/vite.d.ts +133 -0
  96. package/dist/vite.js +1 -0
  97. package/package.json +160 -4
  98. package/sql/access.sql +73 -0
@@ -0,0 +1 @@
1
+ import{sql as t}from"drizzle-orm";import{pgTable as i,text as n,timestamp as a,integer as r,jsonb as d,boolean as _,uniqueIndex as c,index as o,check as s,foreignKey as u}from"drizzle-orm/pg-core";var l=i("tego_access_roles",{id:n("id").primaryKey(),tenantId:n("tenant_id").notNull(),key:n("key"),builtin:_("builtin").notNull(),domainType:n("domain_type"),domainId:n("domain_id"),name:n("name").notNull(),description:n("description").notNull(),grants:d("grants").$type().notNull(),catalogVersion:r("catalog_version").notNull(),createdAt:a("created_at",{withTimezone:!0}).notNull().defaultNow(),updatedAt:a("updated_at",{withTimezone:!0}).notNull().defaultNow(),updatedBy:n("updated_by")},e=>[c("tego_access_roles_builtin_key_unique").on(e.tenantId,e.key).where(t`${e.builtin}`),o("tego_access_roles_tenant_idx").on(e.tenantId),s("tego_access_roles_builtin_shape",t`(${e.builtin} AND ${e.key} IS NOT NULL AND ${e.domainType} IS NULL AND ${e.domainId} IS NULL) OR (NOT ${e.builtin} AND ${e.key} IS NULL)`),s("tego_access_roles_domain_pair",t`(${e.domainType} IS NULL) = (${e.domainId} IS NULL)`)]),p=i("tego_access_assignments",{id:n("id").primaryKey(),tenantId:n("tenant_id").notNull(),roleId:n("role_id").notNull(),principalKind:n("principal_kind").notNull(),principalId:n("principal_id").notNull(),includeDescendants:_("include_descendants").notNull(),unitRoles:n("unit_roles").array(),domainType:n("domain_type"),domainId:n("domain_id"),createdBy:n("created_by").notNull(),createdAt:a("created_at",{withTimezone:!0}).notNull().defaultNow()},e=>[u({name:"tego_access_assignments_role_fk",columns:[e.roleId],foreignColumns:[l.id]}).onDelete("cascade"),c("tego_access_assignments_unique").on(e.tenantId,e.roleId,e.principalKind,e.principalId,t`coalesce(${e.domainType}, '')`,t`coalesce(${e.domainId}, '')`),o("tego_access_assignments_principal_idx").on(e.tenantId,e.principalKind,e.principalId),o("tego_access_assignments_role_idx").on(e.roleId),s("tego_access_assignments_domain_pair",t`(${e.domainType} IS NULL) = (${e.domainId} IS NULL)`)]),m=i("tego_access_settings",{tenantId:n("tenant_id").primaryKey(),unassignedRoleId:n("unassigned_role_id"),version:r("version").notNull(),epoch:n("epoch").notNull(),updatedAt:a("updated_at",{withTimezone:!0}).notNull().defaultNow(),updatedBy:n("updated_by")},e=>[u({name:"tego_access_settings_unassigned_role_fk",columns:[e.unassignedRoleId],foreignColumns:[l.id]}).onDelete("restrict")]),g=i("tego_access_audit",{id:n("id").primaryKey(),tenantId:n("tenant_id").notNull(),actorId:n("actor_id"),action:n("action").notNull(),targetType:n("target_type").notNull(),targetId:n("target_id").notNull(),before:d("before"),after:d("after"),at:a("at",{withTimezone:!0}).notNull().defaultNow()},e=>[o("tego_access_audit_tenant_at_idx").on(e.tenantId,e.at)]);export{p as tegoAccessAssignments,g as tegoAccessAudit,l as tegoAccessRoles,m as tegoAccessSettings};
@@ -0,0 +1,3 @@
1
+ "use strict";var b=Object.defineProperty;var d=(o,a)=>b(o,"name",{value:a,configurable:!0});var hono=require("hono"),constants=require("hono/utils/constants"),handler=require("hono/utils/handler"),zod=require("zod"),access=require("./access.cjs"),POLICY=Symbol.for("tego.access.policy");function mark(o,a){return Object.assign(o,{[POLICY]:a})}d(mark,"mark");function policyOf(o){for(let a=o;typeof a=="function";){const l=a[POLICY];if(l)return l;a=a[constants.COMPOSED_HANDLER]}}d(policyOf,"policyOf");function defaultDenied(o,a){return o instanceof access.AccessAdminError?a.json({error:{code:o.code,message:o.message}},o.status):o.status===404?a.json({error:{code:"not_found",message:"Not found"}},404):a.json({error:{code:"permission_required",message:o.message,permission:o.decision.permission,reason:o.decision.reason}},403)}d(defaultDenied,"defaultDenied");function domainFromQuery(o){const a=o.req.query("domainType"),l=o.req.query("domainId");if(!a&&!l)return null;if(!a||!l)throw new access.AccessAdminError(400,"invalid_domain","Pass both domainType and domainId, or neither");return{type:a,id:l}}d(domainFromQuery,"domainFromQuery");function accessHono(o){const a=new WeakMap,l=d(n=>{let t=a.get(n);return t||(t={},a.set(n,t)),t},"stateOf"),q=o.onDenied??defaultDenied,{session:m}=o,w=d(async(n,t,r)=>{try{await r()}catch(i){if(i instanceof access.AccessDeniedError)return q(i,n);if(i instanceof access.AccessAdminError)return defaultDenied(i,n);throw i}await t()},"guard"),u={public(){return mark(async(n,t)=>t(),{kind:"public"})},authenticated(){return mark(async(n,t)=>t(),{kind:"authenticated"})},permission(n,t={}){return mark((r,i)=>w(r,i,async()=>{const h=await t.domain?.(r)??null;l(r).decision=await m(r).assert(n,void 0,{domain:h})}),{kind:"permission",permission:n})},resource(n,t,r={}){return mark((i,h)=>w(i,h,async()=>{const e=m(i),s=await e.authorize(n);if(!s.allowed&&!(r.domain&&await e.holdsAnywhere(n)))throw new access.AccessDeniedError(s,403);const c=await t(i);if(c===null)throw new access.AccessDeniedError({...s,allowed:!1,reason:"scope_mismatch"},404);const y=await r.domain?.(i,c)??null,A=await e.authorize(n,c,{domain:y});if(!A.allowed)throw new access.AccessDeniedError(A,r.conceal?404:403);const j=l(i);j.decision=A,j.resource=c}),{kind:"resource",permission:n})}};return{policy:u,session:m,resourceOf:d(n=>{const t=a.get(n);if(!t||!("resource"in t))throw new Error("No resource policy ran for this route");return t.resource},"resourceOf"),decisionOf:d(n=>a.get(n)?.decision,"decisionOf"),routes:d(()=>{const n=new hono.Hono,t=mark(async(e,s)=>s(),{kind:"access-admin"}),r=d(e=>async s=>{try{return await e(s,new access.AccessAdmin(m(s)))}catch(c){if(c instanceof zod.ZodError)return defaultDenied(new access.AccessAdminError(400,"invalid_input",c.issues.map(y=>`${y.path.join(".")}: ${y.message}`).join("; ")),s);if(c instanceof access.AccessAdminError||c instanceof access.AccessDeniedError)return defaultDenied(c,s);throw c}},"run"),i=d(async e=>{try{return await e.req.json()}catch{throw new access.AccessAdminError(400,"invalid_json","The body is not JSON")}},"json"),h=d(e=>e===void 0?void 0:/^[0-9]+$/.test(e)?Number(e):Number.NaN,"intOrUndefined");return n.get("/access/me",u.authenticated(),r(async e=>{const s=domainFromQuery(e);return e.json(await m(e).snapshot({domain:s}))})),n.get("/access/catalog",t,r(async(e,s)=>e.json(await s.catalogView(domainFromQuery(e))))),n.get("/access/roles",t,r(async(e,s)=>e.json({roles:await s.listRoles(domainFromQuery(e))}))),n.post("/access/roles",t,r(async(e,s)=>e.json({role:await s.createRole(access.createRoleInputSchema.parse(await i(e)))},201))),n.put("/access/roles/:id",t,r(async(e,s)=>e.json({role:await s.updateRole(e.req.param("id"),access.updateRoleInputSchema.parse(await i(e)))}))),n.delete("/access/roles/:id",t,r(async(e,s)=>(await s.deleteRole(e.req.param("id")),e.body(null,204)))),n.post("/access/roles/:id/clone",t,r(async(e,s)=>e.json({role:await s.cloneRole(e.req.param("id"),access.cloneRoleInputSchema.parse(await i(e)))},201))),n.get("/access/assignments",t,r(async(e,s)=>{const c=access.assignmentQuerySchema.parse({roleId:e.req.query("roleId"),principalKind:e.req.query("principalKind"),principalId:e.req.query("principalId")});return e.json({assignments:await s.listAssignments(domainFromQuery(e),c)})})),n.post("/access/assignments",t,r(async(e,s)=>e.json({assignment:await s.createAssignment(access.createAssignmentInputSchema.parse(await i(e)))},201))),n.delete("/access/assignments/:id",t,r(async(e,s)=>(await s.deleteAssignment(e.req.param("id")),e.body(null,204)))),n.get("/access/principals",t,r(async(e,s)=>{const c=e.req.query("ids"),y=access.principalQuerySchema.parse({kind:e.req.query("kind"),search:e.req.query("search")||void 0,ids:c?c.split(",").filter(Boolean):void 0,offset:h(e.req.query("offset")),limit:h(e.req.query("limit"))});return e.json(await s.principals(domainFromQuery(e),y))})),n.get("/access/explain/:userId",t,r(async(e,s)=>e.json(await s.explain(e.req.param("userId"),domainFromQuery(e))))),n.get("/access/settings",t,r(async(e,s)=>e.json(await s.settings()))),n.put("/access/settings",t,r(async(e,s)=>e.json(await s.updateSettings(access.updateSettingsInputSchema.parse(await i(e)))))),n.get("/access/audit",t,r(async(e,s)=>e.json(await s.audit(access.auditQuerySchema.parse({offset:h(e.req.query("offset")),limit:h(e.req.query("limit"))}))))),n},"routes")}}d(accessHono,"accessHono");function assertRouteCoverage(o,a={}){const l=new Map;for(const u of o.routes){const p=`${u.method} ${u.path}`;let f=l.get(p);f||(f={method:u.method,path:u.path,entries:0,policies:[],firstHandler:null},l.set(p,f));const g=f.entries++,n=policyOf(u.handler);n?f.policies.push({policy:n,at:g}):f.firstHandler===null&&!handler.isMiddleware(handler.findTargetHandler(u.handler))&&(f.firstHandler=g)}const q=new Set(a.publicRoutes??[]),m=[],w=[];for(const[u,p]of l){if(p.firstHandler===null){if(p.method==="ALL")continue;m.push(`${u}: no handler`);continue}if(p.policies.length!==1){m.push(`${u}: ${p.policies.length===0?"no policy":`${p.policies.length} policies`}`);continue}const[{policy:f,at:g}]=p.policies;if(g>p.firstHandler){m.push(`${u}: the policy comes after the handler`);continue}f.kind==="public"&&!q.has(u)&&m.push(`${u}: policy.public() on a route the manifest does not declare public`),w.push({method:p.method,path:p.path,policy:f})}if(m.length>0)throw new Error(`Routes without exactly one access policy:
2
+ ${m.join(`
3
+ `)}`);return w}d(assertRouteCoverage,"assertRouteCoverage"),exports.accessHono=accessHono,exports.assertRouteCoverage=assertRouteCoverage,exports.policyOf=policyOf;
@@ -0,0 +1,108 @@
1
+ import { Context, MiddlewareHandler, Hono } from 'hono';
2
+ import { RouterRoute } from 'hono/types';
3
+ import { AccessSession, AccessDeniedError, AccessDomain, Decision } from './access.cjs';
4
+
5
+ /**
6
+ * The enforcement point for Hono backends: route policies, the `/access`
7
+ * admin API, and the coverage check that keeps every route under a policy.
8
+ *
9
+ * Every route declares exactly one policy. `assertRouteCoverage(app)` in a
10
+ * test makes that a build failure rather than a review comment, so a route
11
+ * added without a check is caught before it ships — the default is deny by
12
+ * construction, not by remembering.
13
+ */
14
+
15
+ type PolicyKind = 'public' | 'authenticated' | 'permission' | 'resource' | 'access-admin';
16
+ interface PolicyDescriptor {
17
+ kind: PolicyKind;
18
+ permission?: string;
19
+ }
20
+ /**
21
+ * The policy a route handler carries.
22
+ *
23
+ * `app.route()` wraps each handler of a sub-app that has its own error
24
+ * handler, keeping the original under `COMPOSED_HANDLER`; the marker is on the
25
+ * original, so the wrapping is followed.
26
+ */
27
+ declare function policyOf(handler: unknown): PolicyDescriptor | undefined;
28
+ interface AccessHonoOptions<P extends string> {
29
+ /**
30
+ * The caller's session for this request, from `engine.forRequest`. The
31
+ * plugin creates it once per request — typically where it builds its Hono
32
+ * env — so its policies and its handlers share one session and its cache.
33
+ */
34
+ session(c: Context): AccessSession<P>;
35
+ /**
36
+ * Maps a policy denial on the plugin's own routes to its error response.
37
+ * The `/access` routes always answer `{ error: { code, message } }`: that
38
+ * shape is the contract `@tego/plugin-access-ui` and the Python SDK share.
39
+ */
40
+ onDenied?(error: AccessDeniedError, c: Context): Response;
41
+ }
42
+ interface PermissionPolicyOptions {
43
+ /** The domain to resolve in; global when omitted. */
44
+ domain?: (c: Context) => AccessDomain | null | Promise<AccessDomain | null>;
45
+ }
46
+ interface ResourcePolicyOptions<R> {
47
+ /**
48
+ * 404 rather than 403 for a resource outside the caller's scope, or in a
49
+ * domain where they do not hold the permission.
50
+ */
51
+ conceal?: boolean;
52
+ domain?: (c: Context, resource: R) => AccessDomain | null | Promise<AccessDomain | null>;
53
+ }
54
+ declare function accessHono<P extends string>(options: AccessHonoOptions<P>): {
55
+ policy: {
56
+ /** An anonymous route the manifest declares in `publicRoutes`. */
57
+ public(): MiddlewareHandler;
58
+ /** Any signed-in caller; for routes that authorize inside the handler. */
59
+ authenticated(): MiddlewareHandler;
60
+ /** The permission must be held, at any scope. */
61
+ permission(permission: P, opts?: PermissionPolicyOptions): MiddlewareHandler;
62
+ /**
63
+ * The permission, and the resource `loader` finds, within scope.
64
+ *
65
+ * In this order: the functional check first (403 — reveals nothing about
66
+ * any record), then the lookup (404 when missing), then the check on the
67
+ * resource in its domain. A caller who cannot use the feature therefore
68
+ * learns nothing about which ids exist.
69
+ *
70
+ * With `domain`, the functional check passes when the permission is held
71
+ * in any domain, since which one the resource is in is known only once it
72
+ * is loaded. Under `conceal`, a resource in a domain where the permission
73
+ * is not held is then a 404 like one outside the held scope; otherwise
74
+ * either is a 403.
75
+ */
76
+ resource<R>(permission: P, loader: (c: Context) => Promise<R | null>, opts?: ResourcePolicyOptions<R>): MiddlewareHandler;
77
+ };
78
+ session: (c: Context) => AccessSession<P>;
79
+ resourceOf: <R>(c: Context) => R;
80
+ decisionOf: (c: Context) => Decision | undefined;
81
+ routes: () => Hono;
82
+ };
83
+ interface RouteCoverageOptions {
84
+ /** The manifest's `publicRoutes`, e.g. `POST /public/trainings/:token/sign`. */
85
+ publicRoutes?: readonly string[];
86
+ }
87
+ interface RoutePolicy {
88
+ method: string;
89
+ path: string;
90
+ policy: PolicyDescriptor;
91
+ }
92
+ /**
93
+ * Every route's policy, or an error listing the routes without exactly one.
94
+ *
95
+ * Reads `app.routes`, where Hono lists each middleware and handler of a route
96
+ * in registration order with its function reference — so the markers the
97
+ * policies carry are visible without running a request. Middleware and
98
+ * handlers are told apart the way Hono's `isMiddleware` does, by whether the
99
+ * function takes `next`: `ALL` entries with no handler are `app.use`
100
+ * middleware and not routes, while an `app.all` handler is a route like any
101
+ * other. The policy must come before the route's first handler, which
102
+ * answers without running anything after it.
103
+ */
104
+ declare function assertRouteCoverage(app: {
105
+ readonly routes: readonly RouterRoute[];
106
+ }, options?: RouteCoverageOptions): RoutePolicy[];
107
+
108
+ export { type AccessHonoOptions, type PermissionPolicyOptions, type PolicyDescriptor, type PolicyKind, type ResourcePolicyOptions, type RouteCoverageOptions, type RoutePolicy, accessHono, assertRouteCoverage, policyOf };
@@ -0,0 +1,108 @@
1
+ import { Context, MiddlewareHandler, Hono } from 'hono';
2
+ import { RouterRoute } from 'hono/types';
3
+ import { AccessSession, AccessDeniedError, AccessDomain, Decision } from './access.js';
4
+
5
+ /**
6
+ * The enforcement point for Hono backends: route policies, the `/access`
7
+ * admin API, and the coverage check that keeps every route under a policy.
8
+ *
9
+ * Every route declares exactly one policy. `assertRouteCoverage(app)` in a
10
+ * test makes that a build failure rather than a review comment, so a route
11
+ * added without a check is caught before it ships — the default is deny by
12
+ * construction, not by remembering.
13
+ */
14
+
15
+ type PolicyKind = 'public' | 'authenticated' | 'permission' | 'resource' | 'access-admin';
16
+ interface PolicyDescriptor {
17
+ kind: PolicyKind;
18
+ permission?: string;
19
+ }
20
+ /**
21
+ * The policy a route handler carries.
22
+ *
23
+ * `app.route()` wraps each handler of a sub-app that has its own error
24
+ * handler, keeping the original under `COMPOSED_HANDLER`; the marker is on the
25
+ * original, so the wrapping is followed.
26
+ */
27
+ declare function policyOf(handler: unknown): PolicyDescriptor | undefined;
28
+ interface AccessHonoOptions<P extends string> {
29
+ /**
30
+ * The caller's session for this request, from `engine.forRequest`. The
31
+ * plugin creates it once per request — typically where it builds its Hono
32
+ * env — so its policies and its handlers share one session and its cache.
33
+ */
34
+ session(c: Context): AccessSession<P>;
35
+ /**
36
+ * Maps a policy denial on the plugin's own routes to its error response.
37
+ * The `/access` routes always answer `{ error: { code, message } }`: that
38
+ * shape is the contract `@tego/plugin-access-ui` and the Python SDK share.
39
+ */
40
+ onDenied?(error: AccessDeniedError, c: Context): Response;
41
+ }
42
+ interface PermissionPolicyOptions {
43
+ /** The domain to resolve in; global when omitted. */
44
+ domain?: (c: Context) => AccessDomain | null | Promise<AccessDomain | null>;
45
+ }
46
+ interface ResourcePolicyOptions<R> {
47
+ /**
48
+ * 404 rather than 403 for a resource outside the caller's scope, or in a
49
+ * domain where they do not hold the permission.
50
+ */
51
+ conceal?: boolean;
52
+ domain?: (c: Context, resource: R) => AccessDomain | null | Promise<AccessDomain | null>;
53
+ }
54
+ declare function accessHono<P extends string>(options: AccessHonoOptions<P>): {
55
+ policy: {
56
+ /** An anonymous route the manifest declares in `publicRoutes`. */
57
+ public(): MiddlewareHandler;
58
+ /** Any signed-in caller; for routes that authorize inside the handler. */
59
+ authenticated(): MiddlewareHandler;
60
+ /** The permission must be held, at any scope. */
61
+ permission(permission: P, opts?: PermissionPolicyOptions): MiddlewareHandler;
62
+ /**
63
+ * The permission, and the resource `loader` finds, within scope.
64
+ *
65
+ * In this order: the functional check first (403 — reveals nothing about
66
+ * any record), then the lookup (404 when missing), then the check on the
67
+ * resource in its domain. A caller who cannot use the feature therefore
68
+ * learns nothing about which ids exist.
69
+ *
70
+ * With `domain`, the functional check passes when the permission is held
71
+ * in any domain, since which one the resource is in is known only once it
72
+ * is loaded. Under `conceal`, a resource in a domain where the permission
73
+ * is not held is then a 404 like one outside the held scope; otherwise
74
+ * either is a 403.
75
+ */
76
+ resource<R>(permission: P, loader: (c: Context) => Promise<R | null>, opts?: ResourcePolicyOptions<R>): MiddlewareHandler;
77
+ };
78
+ session: (c: Context) => AccessSession<P>;
79
+ resourceOf: <R>(c: Context) => R;
80
+ decisionOf: (c: Context) => Decision | undefined;
81
+ routes: () => Hono;
82
+ };
83
+ interface RouteCoverageOptions {
84
+ /** The manifest's `publicRoutes`, e.g. `POST /public/trainings/:token/sign`. */
85
+ publicRoutes?: readonly string[];
86
+ }
87
+ interface RoutePolicy {
88
+ method: string;
89
+ path: string;
90
+ policy: PolicyDescriptor;
91
+ }
92
+ /**
93
+ * Every route's policy, or an error listing the routes without exactly one.
94
+ *
95
+ * Reads `app.routes`, where Hono lists each middleware and handler of a route
96
+ * in registration order with its function reference — so the markers the
97
+ * policies carry are visible without running a request. Middleware and
98
+ * handlers are told apart the way Hono's `isMiddleware` does, by whether the
99
+ * function takes `next`: `ALL` entries with no handler are `app.use`
100
+ * middleware and not routes, while an `app.all` handler is a route like any
101
+ * other. The policy must come before the route's first handler, which
102
+ * answers without running anything after it.
103
+ */
104
+ declare function assertRouteCoverage(app: {
105
+ readonly routes: readonly RouterRoute[];
106
+ }, options?: RouteCoverageOptions): RoutePolicy[];
107
+
108
+ export { type AccessHonoOptions, type PermissionPolicyOptions, type PolicyDescriptor, type PolicyKind, type ResourcePolicyOptions, type RouteCoverageOptions, type RoutePolicy, accessHono, assertRouteCoverage, policyOf };
@@ -0,0 +1,3 @@
1
+ var _=Object.defineProperty;var l=(i,o)=>_(i,"name",{value:o,configurable:!0});import{Hono as v}from"hono";import{COMPOSED_HANDLER as H}from"hono/utils/constants";import{isMiddleware as N,findTargetHandler as E}from"hono/utils/handler";import{ZodError as D}from"zod";import{AccessAdminError as w,AccessAdmin as x,AccessDeniedError as b,createRoleInputSchema as L,updateRoleInputSchema as M,cloneRoleInputSchema as P,assignmentQuerySchema as Q,createAssignmentInputSchema as T,principalQuerySchema as C,updateSettingsInputSchema as z,auditQuerySchema as K}from"./access.js";var O=Symbol.for("tego.access.policy");function I(i,o){return Object.assign(i,{[O]:o})}l(I,"mark");function $(i){for(let o=i;typeof o=="function";){const d=o[O];if(d)return d;o=o[H]}}l($,"policyOf");function S(i,o){return i instanceof w?o.json({error:{code:i.code,message:i.message}},i.status):i.status===404?o.json({error:{code:"not_found",message:"Not found"}},404):o.json({error:{code:"permission_required",message:i.message,permission:i.decision.permission,reason:i.decision.reason}},403)}l(S,"defaultDenied");function g(i){const o=i.req.query("domainType"),d=i.req.query("domainId");if(!o&&!d)return null;if(!o||!d)throw new w(400,"invalid_domain","Pass both domainType and domainId, or neither");return{type:o,id:d}}l(g,"domainFromQuery");function B(i){const o=new WeakMap,d=l(s=>{let t=o.get(s);return t||(t={},o.set(s,t)),t},"stateOf"),k=i.onDenied??S,{session:m}=i,q=l(async(s,t,a)=>{try{await a()}catch(r){if(r instanceof b)return k(r,s);if(r instanceof w)return S(r,s);throw r}await t()},"guard"),u={public(){return I(async(s,t)=>t(),{kind:"public"})},authenticated(){return I(async(s,t)=>t(),{kind:"authenticated"})},permission(s,t={}){return I((a,r)=>q(a,r,async()=>{const y=await t.domain?.(a)??null;d(a).decision=await m(a).assert(s,void 0,{domain:y})}),{kind:"permission",permission:s})},resource(s,t,a={}){return I((r,y)=>q(r,y,async()=>{const e=m(r),n=await e.authorize(s);if(!n.allowed&&!(a.domain&&await e.holdsAnywhere(s)))throw new b(n,403);const c=await t(r);if(c===null)throw new b({...n,allowed:!1,reason:"scope_mismatch"},404);const h=await a.domain?.(r,c)??null,R=await e.authorize(s,c,{domain:h});if(!R.allowed)throw new b(R,a.conceal?404:403);const A=d(r);A.decision=R,A.resource=c}),{kind:"resource",permission:s})}};return{policy:u,session:m,resourceOf:l(s=>{const t=o.get(s);if(!t||!("resource"in t))throw new Error("No resource policy ran for this route");return t.resource},"resourceOf"),decisionOf:l(s=>o.get(s)?.decision,"decisionOf"),routes:l(()=>{const s=new v,t=I(async(e,n)=>n(),{kind:"access-admin"}),a=l(e=>async n=>{try{return await e(n,new x(m(n)))}catch(c){if(c instanceof D)return S(new w(400,"invalid_input",c.issues.map(h=>`${h.path.join(".")}: ${h.message}`).join("; ")),n);if(c instanceof w||c instanceof b)return S(c,n);throw c}},"run"),r=l(async e=>{try{return await e.req.json()}catch{throw new w(400,"invalid_json","The body is not JSON")}},"json"),y=l(e=>e===void 0?void 0:/^[0-9]+$/.test(e)?Number(e):Number.NaN,"intOrUndefined");return s.get("/access/me",u.authenticated(),a(async e=>{const n=g(e);return e.json(await m(e).snapshot({domain:n}))})),s.get("/access/catalog",t,a(async(e,n)=>e.json(await n.catalogView(g(e))))),s.get("/access/roles",t,a(async(e,n)=>e.json({roles:await n.listRoles(g(e))}))),s.post("/access/roles",t,a(async(e,n)=>e.json({role:await n.createRole(L.parse(await r(e)))},201))),s.put("/access/roles/:id",t,a(async(e,n)=>e.json({role:await n.updateRole(e.req.param("id"),M.parse(await r(e)))}))),s.delete("/access/roles/:id",t,a(async(e,n)=>(await n.deleteRole(e.req.param("id")),e.body(null,204)))),s.post("/access/roles/:id/clone",t,a(async(e,n)=>e.json({role:await n.cloneRole(e.req.param("id"),P.parse(await r(e)))},201))),s.get("/access/assignments",t,a(async(e,n)=>{const c=Q.parse({roleId:e.req.query("roleId"),principalKind:e.req.query("principalKind"),principalId:e.req.query("principalId")});return e.json({assignments:await n.listAssignments(g(e),c)})})),s.post("/access/assignments",t,a(async(e,n)=>e.json({assignment:await n.createAssignment(T.parse(await r(e)))},201))),s.delete("/access/assignments/:id",t,a(async(e,n)=>(await n.deleteAssignment(e.req.param("id")),e.body(null,204)))),s.get("/access/principals",t,a(async(e,n)=>{const c=e.req.query("ids"),h=C.parse({kind:e.req.query("kind"),search:e.req.query("search")||void 0,ids:c?c.split(",").filter(Boolean):void 0,offset:y(e.req.query("offset")),limit:y(e.req.query("limit"))});return e.json(await n.principals(g(e),h))})),s.get("/access/explain/:userId",t,a(async(e,n)=>e.json(await n.explain(e.req.param("userId"),g(e))))),s.get("/access/settings",t,a(async(e,n)=>e.json(await n.settings()))),s.put("/access/settings",t,a(async(e,n)=>e.json(await n.updateSettings(z.parse(await r(e)))))),s.get("/access/audit",t,a(async(e,n)=>e.json(await n.audit(K.parse({offset:y(e.req.query("offset")),limit:y(e.req.query("limit"))}))))),s},"routes")}}l(B,"accessHono");function F(i,o={}){const d=new Map;for(const u of i.routes){const p=`${u.method} ${u.path}`;let f=d.get(p);f||(f={method:u.method,path:u.path,entries:0,policies:[],firstHandler:null},d.set(p,f));const j=f.entries++,s=$(u.handler);s?f.policies.push({policy:s,at:j}):f.firstHandler===null&&!N(E(u.handler))&&(f.firstHandler=j)}const k=new Set(o.publicRoutes??[]),m=[],q=[];for(const[u,p]of d){if(p.firstHandler===null){if(p.method==="ALL")continue;m.push(`${u}: no handler`);continue}if(p.policies.length!==1){m.push(`${u}: ${p.policies.length===0?"no policy":`${p.policies.length} policies`}`);continue}const[{policy:f,at:j}]=p.policies;if(j>p.firstHandler){m.push(`${u}: the policy comes after the handler`);continue}f.kind==="public"&&!k.has(u)&&m.push(`${u}: policy.public() on a route the manifest does not declare public`),q.push({method:p.method,path:p.path,policy:f})}if(m.length>0)throw new Error(`Routes without exactly one access policy:
2
+ ${m.join(`
3
+ `)}`);return q}l(F,"assertRouteCoverage");export{B as accessHono,F as assertRouteCoverage,$ as policyOf};
@@ -0,0 +1 @@
1
+ "use strict";var n=Object.defineProperty;var e=(r,t)=>n(r,"name",{value:t,configurable:!0});var chunkOP64Y4IN_cjs=require("./chunk-OP64Y4IN.cjs");Object.defineProperty(exports,"expandImplications",{enumerable:!0,get:e(function(){return chunkOP64Y4IN_cjs.expandImplications},"get")}),Object.defineProperty(exports,"mergeGrant",{enumerable:!0,get:e(function(){return chunkOP64Y4IN_cjs.mergeGrant},"get")}),Object.defineProperty(exports,"scopeRank",{enumerable:!0,get:e(function(){return chunkOP64Y4IN_cjs.scopeRank},"get")});
@@ -0,0 +1,25 @@
1
+ import { e as CatalogPermission, G as Grants, a as GrantValue } from './catalog-CSSbrfEf.cjs';
2
+ import 'zod';
3
+
4
+ /**
5
+ * What a set of grants amounts to once each permission's `implies` is
6
+ * followed.
7
+ *
8
+ * Kept free of runtime imports so a plugin's frontend can draw a role the way
9
+ * the engine resolves it (`@tego/plugin-sdk/access/implications`) without
10
+ * bundling the catalog schema.
11
+ */
12
+
13
+ /**
14
+ * Adds what each granted permission implies, carrying its scope.
15
+ *
16
+ * Reaches a fixpoint, so a chain of implications resolves however it is
17
+ * ordered; an implied grant never narrows one held directly.
18
+ */
19
+ declare function expandImplications(permissions: Record<string, CatalogPermission>, grants: Grants): Grants;
20
+ /** The wider of two grants of one permission. */
21
+ declare function mergeGrant(permission: CatalogPermission, current: GrantValue | undefined, next: GrantValue): GrantValue;
22
+ /** Position on the permission's ladder; -1 for a scope it does not offer. */
23
+ declare function scopeRank(permission: CatalogPermission, scope: GrantValue): number;
24
+
25
+ export { expandImplications, mergeGrant, scopeRank };
@@ -0,0 +1,25 @@
1
+ import { e as CatalogPermission, G as Grants, a as GrantValue } from './catalog-CSSbrfEf.js';
2
+ import 'zod';
3
+
4
+ /**
5
+ * What a set of grants amounts to once each permission's `implies` is
6
+ * followed.
7
+ *
8
+ * Kept free of runtime imports so a plugin's frontend can draw a role the way
9
+ * the engine resolves it (`@tego/plugin-sdk/access/implications`) without
10
+ * bundling the catalog schema.
11
+ */
12
+
13
+ /**
14
+ * Adds what each granted permission implies, carrying its scope.
15
+ *
16
+ * Reaches a fixpoint, so a chain of implications resolves however it is
17
+ * ordered; an implied grant never narrows one held directly.
18
+ */
19
+ declare function expandImplications(permissions: Record<string, CatalogPermission>, grants: Grants): Grants;
20
+ /** The wider of two grants of one permission. */
21
+ declare function mergeGrant(permission: CatalogPermission, current: GrantValue | undefined, next: GrantValue): GrantValue;
22
+ /** Position on the permission's ladder; -1 for a scope it does not offer. */
23
+ declare function scopeRank(permission: CatalogPermission, scope: GrantValue): number;
24
+
25
+ export { expandImplications, mergeGrant, scopeRank };
@@ -0,0 +1 @@
1
+ export{expandImplications,mergeGrant,scopeRank}from"./chunk-SNUCO2MK.js";
@@ -0,0 +1,26 @@
1
+ "use strict";var b=Object.defineProperty;var r=(e,t)=>b(e,"name",{value:t,configurable:!0});var chunkAJY7ZHXK_cjs=require("./chunk-AJY7ZHXK.cjs"),chunkOP64Y4IN_cjs=require("./chunk-OP64Y4IN.cjs"),zod=require("zod"),zodToJsonSchema=require("zod-to-json-schema");function subjectFromContext(e){return{tenantId:e.tenantId,userId:e.userId,hostRoles:e.roles,...e.avatarId?{agent:{avatarId:e.avatarId,delegation:e.delegation?.actions??[]}}:{}}}r(subjectFromContext,"subjectFromContext");var EffectiveGrants=class{static{r(this,"EffectiveGrants")}constructor(e,t,n,s,a,i,o){this.catalog=e,this.subject=t,this.domain=n,this.held=s,this.withheld=a,this.sources=i,this.assignedDomains=o}catalog;subject;domain;held;withheld;sources;assignedDomains;value(e){return this.held.get(e)?.value}can(e){return this.held.has(e)}scope(e){const t=this.held.get(e)?.value;return typeof t=="string"?t:null}atLeast(e,t){const n=this.catalog.permissions[e],s=this.held.get(e)?.value;if(!n?.scopes||typeof s!="string")return!1;const a=n.scopes.indexOf(t);return a>=0&&n.scopes.indexOf(s)>=a}via(e){return this.held.get(e)?.via??this.withheld.get(e)?.via??[]}denial(e){return this.held.has(e)?null:this.withheld.get(e)?.reason??"not_granted"}permissions(){return Object.fromEntries([...this.held].map(([e,t])=>[e,t.value]))}managesThroughHostRole(){return this.held.get(chunkAJY7ZHXK_cjs.RESERVED_PERMISSIONS.manage)?.via.some(e=>e.source==="hostRole")??!1}decide(e){const t=this.held.get(e);return t?{allowed:!0,permission:e,scope:typeof t.value=="string"?t.value:null,reason:"granted",via:t.via}:{allowed:!1,permission:e,scope:null,reason:this.denial(e),via:this.via(e)}}};async function decideResource(e,t,n,s){const a=e.decide(t);if(!a.allowed||n===void 0||a.scope===null)return a;const i=e.catalog.permissions[t].scopes;for(let o=i.indexOf(a.scope);o>=0;o--){const d=i[o];if(await s(d))return{...a,matchedScope:d}}return{...a,allowed:!1,reason:"scope_mismatch"}}r(decideResource,"decideResource");function sameDomain(e,t){return e===null||t===null?e===t:e.type===t.type&&e.id===t.id}r(sameDomain,"sameDomain");function assignmentInDomain(e,t){return e.domain===null||sameDomain(e.domain,t)}r(assignmentInDomain,"assignmentInDomain");function assignmentMatches(e,t,n){const{principal:s}=e;return s.kind==="user"?s.id===t.userId:n.some(a=>a.kind===s.kind&&(a.id===s.id||s.includeDescendants&&a.ancestors.includes(s.id))&&(s.unitRoles===null||a.role!==void 0&&s.unitRoles.includes(a.role)))}r(assignmentMatches,"assignmentMatches");function validGrants(e,t){const n={};for(const[s,a]of Object.entries(t)){const i=e.permissions[s];i&&chunkAJY7ZHXK_cjs.grantProblem(i,a)===null&&(n[s]=a)}return n}r(validGrants,"validGrants");function narrowInherited(e,t,n){const s=chunkOP64Y4IN_cjs.expandImplications(e.permissions,validGrants(e,n)),a={};for(const[i,o]of Object.entries(s)){const d=t[i];if(d===void 0)continue;const l=e.permissions[i];a[i]=l.scopes!==null&&chunkOP64Y4IN_cjs.scopeRank(l,o)>chunkOP64Y4IN_cjs.scopeRank(l,d)?d:o}return a}r(narrowInherited,"narrowInherited");function unassignedGrants(e,t){const{[chunkAJY7ZHXK_cjs.RESERVED_PERMISSIONS.manage]:n,...s}=validGrants(e,t);return chunkOP64Y4IN_cjs.expandImplications(e.permissions,s)}r(unassignedGrants,"unassignedGrants");function resolveGrants(e){const{catalog:t,subject:n,roles:s,domain:a}=e,i=[],o=e.assignments.filter(c=>assignmentMatches(c,n,e.memberships)&&s.has(c.roleId)),d=[];for(const c of o)c.domain&&!d.some(g=>sameDomain(g,c.domain))&&d.push(c.domain);const l=o.filter(c=>assignmentInDomain(c,a));for(const c of l){const g=s.get(c.roleId);i.push({roleId:g.id,roleKey:g.key,source:"assignment",principal:{kind:c.principal.kind,id:c.principal.id},domain:c.domain})}const m=e.inheritance?.ancestors??[],p=new Map;for(const c of o){if(c.domain===null||!m.some(I=>sameDomain(I,c.domain)))continue;const g=s.get(c.roleId),h=chunkOP64Y4IN_cjs.expandImplications(t.permissions,validGrants(t,g.grants)),y=narrowInherited(t,h,e.inheritance.inherit(h));if(Object.keys(y).length===0)continue;const _={roleId:g.id,roleKey:g.key,source:"assignment",principal:{kind:c.principal.kind,id:c.principal.id},domain:c.domain,inherited:!0};i.push(_),p.set(_,y)}const w=new Map;for(const c of s.values())c.builtin&&c.key&&w.set(c.key,c);for(const c of n.hostRoles)for(const g of t.hostRoleBindings[c]??[]){const h=w.get(g);h&&i.push({roleId:h.id,roleKey:h.key,source:"hostRole",hostRole:c})}if(l.length===0&&e.unassignedRoleId){const c=s.get(e.unassignedRoleId);c&&i.push({roleId:c.id,roleKey:c.key,source:"unassigned"})}const f=new Map;for(const c of i){const g=s.get(c.roleId),h=p.get(c)??(c.source==="unassigned"?unassignedGrants(t,g.grants):chunkOP64Y4IN_cjs.expandImplications(t.permissions,validGrants(t,g.grants)));for(const[y,_]of Object.entries(h)){const I=t.permissions[y],E=f.get(y),R=chunkOP64Y4IN_cjs.mergeGrant(I,E?.value,_);E?(E.value=R,E.via.push(c)):f.set(y,{value:R,via:[c]})}}const u=new Map;if(n.agent){const c=n.agent.delegation;for(const[g,h]of f){const y=t.permissions[g].agent;y.allowed?y.requires.every(_=>c.includes(_))||u.set(g,{reason:"delegation_missing",via:h.via}):u.set(g,{reason:"agent_forbidden",via:h.via})}for(const g of u.keys())f.delete(g)}return new EffectiveGrants(t,n,a,f,u,i,d)}r(resolveGrants,"resolveGrants");var identifierPattern=/^[a-z_][a-z0-9_]*$/,IDENT=Symbol("tego.access.ident"),FRAGMENT=Symbol("tego.access.fragment");function ident(e){if(!identifierPattern.test(e))throw new Error(`"${e}" is not a plain lower-case SQL identifier (^[a-z_][a-z0-9_]*$)`);return{[IDENT]:e}}r(ident,"ident");function isIdentifier(e){return typeof e=="object"&&e!==null&&IDENT in e}r(isIdentifier,"isIdentifier");function isFragment(e){return typeof e=="object"&&e!==null&&FRAGMENT in e}r(isFragment,"isFragment");function fragment(e,t){return{[FRAGMENT]:!0,strings:e,values:t}}r(fragment,"fragment");function sql(e,...t){const n=[e[0]],s=[],a=r(i=>{n[n.length-1]+=i},"append");return t.forEach((i,o)=>{isIdentifier(i)?a(i[IDENT]):isFragment(i)?(a(i.strings[0]),i.values.forEach((d,l)=>{s.push(d),n.push(i.strings[l+1])})):(s.push(i),n.push("")),a(e[o+1])}),fragment(n,s)}r(sql,"sql");var TRUE_FRAGMENT=fragment(["TRUE"],[]),FALSE_FRAGMENT=fragment(["FALSE"],[]);function orFragments(e){const t=e.filter(s=>s!==FALSE_FRAGMENT);if(t.includes(TRUE_FRAGMENT))return TRUE_FRAGMENT;if(t.length===0)return FALSE_FRAGMENT;if(t.length===1)return t[0];let n=sql`(${t[0]})`;for(const s of t.slice(1))n=sql`${n} OR (${s})`;return sql`(${n})`}r(orFragments,"orFragments");function renderFragment(e,t=0){let n=e.strings[0];return e.values.forEach((s,a)=>{n+=`$${t+a+1}${e.strings[a+1]}`}),{text:n,params:[...e.values]}}r(renderFragment,"renderFragment");function bindFragment(e,t){const n=renderFragment(t,e.length);return{text:n.text,params:[...e,...n.params]}}r(bindFragment,"bindFragment");function refColumn(e,t){const n=e.columns?.[t];if(!n)throw new Error(`This scope filters on "${t}": pass it in ref.columns, e.g. { ${t}: 'created_by' }`);return ident(n)}r(refColumn,"refColumn");var allScope={matches:r(()=>!0,"matches"),filter:r(()=>TRUE_FRAGMENT,"filter")},ownScope={matches:r((e,t)=>t.ownerId===e.userId,"matches"),filter:r((e,t)=>sql`${ident(t.alias)}.${refColumn(t,"ownerId")} = ${e.userId}`,"filter")},BUILTIN_SCOPES={all:allScope,own:ownScope},AccessDeniedError=class extends Error{static{r(this,"AccessDeniedError")}constructor(e,t){super(t===404?"Not found":`Permission "${e.permission}" is required (${e.reason})`),this.decision=e,this.status=t,this.name="AccessDeniedError"}decision;status},AccessAdminError=class extends Error{static{r(this,"AccessAdminError")}constructor(e,t,n){super(n),this.status=e,this.code=t,this.name="AccessAdminError"}status;code},iso=r(e=>e instanceof Date?e.toISOString():new Date(e).toISOString(),"iso");function domainOf(e,t){return e!==null&&t!==null?{type:e,id:t}:null}r(domainOf,"domainOf");function toRole(e){return{id:e.id,key:e.key,builtin:e.builtin,domain:domainOf(e.domain_type,e.domain_id),name:e.name,description:e.description,grants:e.grants,catalogVersion:e.catalog_version,createdAt:iso(e.created_at),updatedAt:iso(e.updated_at),updatedBy:e.updated_by}}r(toRole,"toRole");function toAssignment(e){return{id:e.id,roleId:e.role_id,principal:{kind:e.principal_kind,id:e.principal_id,includeDescendants:e.include_descendants,unitRoles:e.unit_roles},domain:domainOf(e.domain_type,e.domain_id),createdBy:e.created_by,createdAt:iso(e.created_at)}}r(toAssignment,"toAssignment");var ROLE_COLUMNS="id, key, builtin, domain_type, domain_id, name, description, grants, catalog_version, created_at, updated_at, updated_by",ASSIGNMENT_COLUMNS="id, role_id, principal_kind, principal_id, include_descendants, unit_roles, domain_type, domain_id, created_by, created_at";async function readSettings(e,t){const[n]=await e.query("SELECT unassigned_role_id, version, epoch, updated_at, updated_by FROM tego_access_settings WHERE tenant_id = $1",[t]);return n?{unassignedRoleId:n.unassigned_role_id,version:n.version,epoch:n.epoch,updatedAt:iso(n.updated_at),updatedBy:n.updated_by}:null}r(readSettings,"readSettings");async function listRoles(e,t){return(await e.query(`SELECT ${ROLE_COLUMNS} FROM tego_access_roles WHERE tenant_id = $1 ORDER BY builtin DESC, created_at, id`,[t])).map(toRole)}r(listRoles,"listRoles");async function findRole(e,t,n){const[s]=await e.query(`SELECT ${ROLE_COLUMNS} FROM tego_access_roles WHERE tenant_id = $1 AND id = $2`,[t,n]);return s?toRole(s):null}r(findRole,"findRole");async function principalKindsInUse(e,t){return(await e.query("SELECT DISTINCT principal_kind FROM tego_access_assignments WHERE tenant_id = $1",[t])).map(s=>s.principal_kind)}r(principalKindsInUse,"principalKindsInUse");async function assignmentsFor(e,t,n){return n.length===0?[]:(await e.query(`SELECT ${ASSIGNMENT_COLUMNS} FROM tego_access_assignments
2
+ WHERE tenant_id = $1
3
+ AND (principal_kind, principal_id) IN (
4
+ SELECT * FROM unnest($2::text[], $3::text[])
5
+ )`,[t,n.map(a=>a.kind),n.map(a=>a.id)])).map(toAssignment)}r(assignmentsFor,"assignmentsFor");async function listAssignments(e,t,n){const s=["tenant_id = $1"],a=[t],i=r(d=>(a.push(d),`$${a.length}`),"param");return n.roleId&&s.push(`role_id = ${i(n.roleId)}`),n.principal&&s.push(`principal_kind = ${i(n.principal.kind)} AND principal_id = ${i(n.principal.id)}`),n.domain===null?s.push("domain_type IS NULL AND domain_id IS NULL"):n.domain&&s.push(`domain_type = ${i(n.domain.type)} AND domain_id = ${i(n.domain.id)}`),(await e.query(`SELECT ${ASSIGNMENT_COLUMNS} FROM tego_access_assignments WHERE ${s.join(" AND ")} ORDER BY created_at, id`,a)).map(toAssignment)}r(listAssignments,"listAssignments");async function findAssignment(e,t,n){const[s]=await e.query(`SELECT ${ASSIGNMENT_COLUMNS} FROM tego_access_assignments WHERE tenant_id = $1 AND id = $2`,[t,n]);return s?toAssignment(s):null}r(findAssignment,"findAssignment");async function countAssignments(e,t,n){const[s]=await e.query("SELECT count(*)::text AS count FROM tego_access_assignments WHERE tenant_id = $1 AND role_id = $2",[t,n]);return Number(s?.count??0)}r(countAssignments,"countAssignments");async function assignmentCounts(e,t){const n=await e.query("SELECT role_id, count(*)::text AS count FROM tego_access_assignments WHERE tenant_id = $1 GROUP BY role_id",[t]);return new Map(n.map(s=>[s.role_id,Number(s.count)]))}r(assignmentCounts,"assignmentCounts");async function lockAndBump(e,t,n){const[s]=await e.query(`UPDATE tego_access_settings
6
+ SET version = version + 1, updated_at = now(), updated_by = $2
7
+ WHERE tenant_id = $1
8
+ RETURNING version`,[t,n]);if(!s)throw new Error("Access settings are missing: call access.reconcile(host) from the backend start hook");return s.version}r(lockAndBump,"lockAndBump");async function insertRole(e,t,n){const[s]=await e.query(`INSERT INTO tego_access_roles
9
+ (id, tenant_id, key, builtin, domain_type, domain_id, name, description, grants, catalog_version, updated_by)
10
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11)
11
+ RETURNING ${ROLE_COLUMNS}`,[crypto.randomUUID(),t,n.key,n.builtin,n.domain?.type??null,n.domain?.id??null,n.name,n.description,JSON.stringify(n.grants),n.catalogVersion,n.updatedBy]);return toRole(s)}r(insertRole,"insertRole");async function updateRole(e,t,n,s){const[a]=await e.query(`UPDATE tego_access_roles
12
+ SET name = $3, description = $4, grants = $5::jsonb, catalog_version = $6,
13
+ updated_by = $7, updated_at = now()
14
+ WHERE tenant_id = $1 AND id = $2
15
+ RETURNING ${ROLE_COLUMNS}`,[t,n,s.name,s.description,JSON.stringify(s.grants),s.catalogVersion,s.updatedBy]);return toRole(a)}r(updateRole,"updateRole");async function deleteRole(e,t,n){await e.query("DELETE FROM tego_access_roles WHERE tenant_id = $1 AND id = $2",[t,n])}r(deleteRole,"deleteRole");async function insertAssignment(e,t,n){const[s]=await e.query(`INSERT INTO tego_access_assignments
16
+ (id, tenant_id, role_id, principal_kind, principal_id, include_descendants, unit_roles, domain_type, domain_id, created_by)
17
+ VALUES ($1, $2, $3, $4, $5, $6, $7::text[], $8, $9, $10)
18
+ ON CONFLICT DO NOTHING
19
+ RETURNING ${ASSIGNMENT_COLUMNS}`,[crypto.randomUUID(),t,n.roleId,n.principal.kind,n.principal.id,n.principal.includeDescendants,n.principal.unitRoles,n.domain?.type??null,n.domain?.id??null,n.createdBy]);return s?toAssignment(s):null}r(insertAssignment,"insertAssignment");async function deleteAssignment(e,t,n){await e.query("DELETE FROM tego_access_assignments WHERE tenant_id = $1 AND id = $2",[t,n])}r(deleteAssignment,"deleteAssignment");async function writeSettings(e,t,n){await e.query("UPDATE tego_access_settings SET unassigned_role_id = $2 WHERE tenant_id = $1",[t,n])}r(writeSettings,"writeSettings");async function appendAudit(e,t,n){await e.query(`INSERT INTO tego_access_audit (id, tenant_id, actor_id, action, target_type, target_id, before, after)
20
+ VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8::jsonb)`,[crypto.randomUUID(),t,n.actorId,n.action,n.targetType,n.targetId,n.before==null?null:JSON.stringify(n.before),n.after==null?null:JSON.stringify(n.after)])}r(appendAudit,"appendAudit");async function listAudit(e,t,n){const s=await e.query(`SELECT id, actor_id, action, target_type, target_id, before, after, at
21
+ FROM tego_access_audit WHERE tenant_id = $1
22
+ ORDER BY at DESC, id DESC OFFSET $2 LIMIT $3`,[t,n.offset,n.limit+1]);return{items:s.slice(0,n.limit).map(a=>({id:a.id,actorId:a.actor_id,action:a.action,targetType:a.target_type,targetId:a.target_id,before:a.before,after:a.after,at:iso(a.at)})),hasMore:s.length>n.limit}}r(listAudit,"listAudit");var Lru=class{static{r(this,"Lru")}constructor(e){this.max=e}max;map=new Map;get(e){const t=this.map.get(e);return t!==void 0&&(this.map.delete(e),this.map.set(e,t)),t}set(e,t){this.map.delete(e),this.map.set(e,t),this.map.size>this.max&&this.map.delete(this.map.keys().next().value)}},directoryRequired=r(()=>new Error('Access reads people and units from the host directory: declare "directory.read" in the manifest'),"directoryRequired"),userPrincipalSource={kind:"user",memberships:r(async()=>[],"memberships"),list:r(async(e,t)=>{if(!e.directory)throw directoryRequired();const n=await e.directory.list({kind:"users",...t});return{items:n.items.map(s=>({id:s.id,displayName:s.displayName,active:s.active})),hasMore:n.hasMore}},"list")},unitPrincipalSource={kind:"unit",memberships:r(async(e,t)=>{if(!t.directory)throw directoryRequired();return(await t.directory.identity(e.userId)).memberships.map(s=>({kind:"unit",id:s.unitId,ancestors:s.ancestors,role:s.role}))},"memberships"),list:r(async(e,t)=>{if(!e.directory)throw directoryRequired();const n=await e.directory.list({kind:"units",...t});return{items:n.items.map(s=>({id:s.id,displayName:s.displayName,parentId:s.parentId??null,active:s.active})),hasMore:n.hasMore}},"list")};function domainKey(e){return e?`${e.type}:${e.id}`:""}r(domainKey,"domainKey");var AccessEngine=class{static{r(this,"AccessEngine")}catalog;evaluators;principalSources;domainSources;hooks;tenantCache=new Lru(32);assignmentCache;constructor(e){this.catalog=e.catalog;const t=new Map(Object.entries({...BUILTIN_SCOPES,...e.evaluators}));for(const a of Object.keys(this.catalog.scopes))if(!t.has(a))throw new Error(`Scope "${a}" has no evaluator: pass one in "evaluators"`);this.evaluators=t;const n=new Map([["user",userPrincipalSource],["unit",unitPrincipalSource]]);for(const a of e.principals??[])n.set(a.kind,a);for(const a of this.catalog.assignment.principals)if(!n.has(a))throw new Error(`Principal kind "${a}" has no source: pass one in "principals"`);this.principalSources=n;const s=new Map(Object.entries(e.domains??{}));for(const[a,i]of Object.entries(this.catalog.domains)){const o=s.get(a);if(!o)throw new Error(`Domain type "${a}" has no source: pass one in "domains"`);if(i.hierarchical&&(!o.ancestors||!o.inherit))throw new Error(`Domain type "${a}" is hierarchical: its source needs "ancestors" and "inherit"`)}this.domainSources=s,this.hooks=e.hooks??{},this.assignmentCache=new Lru(e.cacheSize??2e3)}forRequest(e,t){if(e.tenantId!==t.tenantId)throw new Error("The request names a tenant this install does not serve");return new AccessSession(this,subjectFromContext(e),t)}forSubject(e,t){if(e.tenantId!==t.tenantId)throw new Error("The subject names a tenant this install does not serve");return new AccessSession(this,e,t)}async subjectFor(e,t){if(!t.directory)throw directoryRequired();const n=await t.directory.identity(e);return n.active?{tenantId:t.tenantId,userId:e,hostRoles:n.roleNames}:null}async tenantState(e){const t=e.tenantId,n=await readSettings(e.db,t);if(!n)throw new Error("Access is not set up for this tenant: call access.reconcile(host) from the backend start hook");const s=`${t}:${n.epoch}:${n.version}`,a=this.tenantCache.get(s);if(a)return{...a,settings:n};const[i,o]=await Promise.all([listRoles(e.db,t),principalKindsInUse(e.db,t)]),d={settings:n,roles:new Map(i.map(l=>[l.id,l])),kinds:o};return this.tenantCache.set(s,d),d}async assignments(e,t,n){const s=`${e.tenantId}:${t.epoch}:${t.version}:${n.map(o=>`${o.kind}/${o.id}`).sort().join(",")}`,a=this.assignmentCache.get(s);if(a)return a;const i=await assignmentsFor(e.db,e.tenantId,n);return this.assignmentCache.set(s,i),i}async reconcile(e){const t=e.tenantId;return e.db.transaction(async n=>{await n.query("SELECT pg_advisory_xact_lock(hashtext($1))",[`tego_access:${t}`]);const a=(await n.query(`INSERT INTO tego_access_settings (tenant_id, unassigned_role_id, version, epoch)
23
+ VALUES ($1, NULL, 0, $2) ON CONFLICT (tenant_id) DO NOTHING RETURNING tenant_id`,[t,crypto.randomUUID()])).length>0;await n.query("SELECT 1 FROM tego_access_settings WHERE tenant_id = $1 FOR UPDATE",[t]);const i=await listRoles(n,t),o=await readSettings(n,t),d=new Map(i.filter(u=>u.builtin).map(u=>[u.key,u])),l={created:[],updated:[],retired:[],staleGrants:[]},m=(()=>{let u=!1;return async()=>{u||(u=!0,await lockAndBump(n,t,null))}})(),p=new Map;for(const u of Object.values(this.catalog.roles)){const c=d.get(u.key);if(!c){await m();const g=await insertRole(n,t,{key:u.key,builtin:!0,domain:null,name:u.key,description:"",grants:u.grants,catalogVersion:this.catalog.version,updatedBy:null});p.set(u.key,g.id),l.created.push(u.key),await appendAudit(n,t,{actorId:null,action:"role.create",targetType:"role",targetId:g.id,before:null,after:{key:g.key,grants:g.grants}});continue}p.set(u.key,c.id),(c.catalogVersion!==this.catalog.version||!sameGrants(c.grants,u.grants))&&(await m(),await updateRole(n,t,c.id,{name:u.key,description:"",grants:u.grants,catalogVersion:this.catalog.version,updatedBy:null}),l.updated.push(u.key),await appendAudit(n,t,{actorId:null,action:"role.update",targetType:"role",targetId:c.id,before:{grants:c.grants},after:{grants:u.grants}}))}const w=this.catalog.defaults.unassigned?p.get(this.catalog.defaults.unassigned):null;for(const[u,c]of d)this.catalog.roles[u]||(await m(),o.unassignedRoleId===c.id&&await writeSettings(n,t,w),await deleteRole(n,t,c.id),l.retired.push(u),await appendAudit(n,t,{actorId:null,action:"role.delete",targetType:"role",targetId:c.id,before:{key:u,grants:c.grants},after:null}),await this.hooks.onChange?.(n,{type:"role.deleted",role:c},{tenantId:t,host:e}));a&&(await m(),await writeSettings(n,t,w));for(const u of i)if(!u.builtin)for(const[c,g]of Object.entries(u.grants)){const h=this.catalog.permissions[c];(!h||(h.scopes===null?g!==!0:typeof g!="string"||!h.scopes.includes(g)))&&l.staleGrants.push({roleId:u.id,permission:c})}return l.created.length+l.updated.length+l.retired.length>0&&await this.hooks.onChange?.(n,{type:"catalog.reconciled",report:l},{tenantId:t,host:e}),l.staleGrants.length>0&&e.log.warn("Custom roles hold grants the access catalog no longer has",{staleGrants:l.staleGrants}),l})}};function sameGrants(e,t){const n=Object.keys(e);return n.length===Object.keys(t).length&&n.every(s=>e[s]===t[s])}r(sameGrants,"sameGrants");var AccessSession=class{static{r(this,"AccessSession")}constructor(e,t,n){this.engine=e,this.subject=t,this.host=n}engine;subject;host;state;memberships;grantsByDomain=new Map;tenant(){return this.state??=this.engine.tenantState(this.host),this.state}async loadMemberships(e){return this.memberships??=(async()=>(await Promise.all(e.filter(n=>n!=="user").map(n=>{const s=this.engine.principalSources.get(n);return s?s.memberships(this.subject,this.host):(this.host.log.warn("An assignment names a principal kind with no source",{kind:n}),[])}))).flat())(),this.memberships}async inheritance(e){if(!e||!this.engine.catalog.domains[e.type]?.hierarchical)return null;const t=this.engine.domainSources.get(e.type);return{ancestors:(await t.ancestors(e.id,this.host)).map(s=>({type:e.type,id:s})),inherit:r(s=>t.inherit(s),"inherit")}}grants(e=null){const t=domainKey(e);let n=this.grantsByDomain.get(t);return n||(n=(async()=>{const s=await this.tenant(),a=await this.loadMemberships(s.kinds),i=[{kind:"user",id:this.subject.userId}];for(const l of a){i.push({kind:l.kind,id:l.id});for(const m of l.ancestors)i.push({kind:l.kind,id:m})}const[o,d]=await Promise.all([this.engine.assignments(this.host,s.settings,i),this.inheritance(e)]);return resolveGrants({catalog:this.engine.catalog,subject:this.subject,roles:s.roles,assignments:o,memberships:a,unassignedRoleId:s.settings.unassignedRoleId,domain:e,...d?{inheritance:d}:{}})})(),this.grantsByDomain.set(t,n)),n}async can(e,t={}){return(await this.grants(t.domain)).can(e)}async holdsAnywhere(e){const t=await this.grants();if(t.can(e))return!0;for(const n of t.assignedDomains)if((await this.grants(n)).can(e))return!0;return!1}async scope(e,t={}){return(await this.grants(t.domain)).scope(e)}async authorize(e,t,n={}){const s=await this.grants(n.domain);return decideResource(s,e,t,a=>this.engine.evaluators.get(a).matches(this.subject,t,{host:this.host,permission:e}))}async assert(e,t,n={}){const s=await this.authorize(e,t,n);if(!s.allowed)throw new AccessDeniedError(s,s.reason==="scope_mismatch"&&n.conceal?404:403);return s}async filter(e,t,n={}){ident(t.alias);const s=await this.grants(n.domain);if(!s.can(e))return FALSE_FRAGMENT;const a=s.scope(e);if(a===null)return TRUE_FRAGMENT;const i=this.engine.catalog.permissions[e].scopes;return orFragments(i.slice(0,i.indexOf(a)+1).map(o=>this.engine.evaluators.get(o).filter(this.subject,t)))}async snapshot(e={}){const t=await this.grants(e.domain),n=await this.tenant(),s=this.engine.catalog,a=[];for(const o of t.sources){if(a.some(l=>l.id===o.roleId&&l.source===o.source))continue;const d=n.roles.get(o.roleId);a.push({id:d.id,key:d.key,builtin:d.builtin,name:d.name,nameKey:d.key?s.roles[d.key]?.nameKey??null:null,source:o.source,...o.hostRole?{hostRole:o.hostRole}:{}})}const i={};for(const o of t.assignedDomains)(i[o.type]??=[]).push(o.id);return{userId:this.subject.userId,agent:this.subject.agent!==void 0,catalogVersion:s.version,domain:e.domain??null,permissions:t.permissions(),menus:Object.fromEntries(Object.entries(s.menus).map(([o,d])=>[o,d.some(l=>t.can(l))])),roles:a,domains:i}}async manages(e={}){return(await this.grants(e.domain)).can(chunkAJY7ZHXK_cjs.RESERVED_PERMISSIONS.manage)}};function createAccess(e){return new AccessEngine(e)}r(createAccess,"createAccess");var namePattern=/^[a-z][a-z0-9_]*$/,UNIT_ROLES=["owner","admin","member","viewer"],domainInputSchema=zod.z.object({type:zod.z.string().regex(namePattern),id:zod.z.string().min(1).max(200)}).strict(),grantsInputSchema=zod.z.record(zod.z.string(),zod.z.union([zod.z.literal(!0),zod.z.string().regex(namePattern)])),createRoleInputSchema=zod.z.object({name:zod.z.string().trim().min(1).max(80),description:zod.z.string().trim().max(500).default(""),grants:grantsInputSchema,domain:domainInputSchema.nullable().default(null)}).strict(),updateRoleInputSchema=zod.z.object({name:zod.z.string().trim().min(1).max(80).optional(),description:zod.z.string().trim().max(500).optional(),grants:grantsInputSchema.optional()}).strict(),cloneRoleInputSchema=zod.z.object({name:zod.z.string().trim().min(1).max(80),domain:domainInputSchema.nullable().optional()}).strict(),createAssignmentInputSchema=zod.z.object({roleId:zod.z.string().min(1),principal:zod.z.object({kind:zod.z.string().regex(namePattern),id:zod.z.string().min(1).max(200),includeDescendants:zod.z.boolean().default(!1),unitRoles:zod.z.array(zod.z.enum(UNIT_ROLES)).min(1).nullable().default(null)}).strict(),domain:domainInputSchema.nullable().default(null)}).strict(),assignmentQuerySchema=zod.z.object({roleId:zod.z.string().min(1).optional(),principalKind:zod.z.string().regex(namePattern).optional(),principalId:zod.z.string().min(1).optional()}).strict(),principalQuerySchema=zod.z.object({kind:zod.z.string().regex(namePattern),search:zod.z.string().max(200).optional(),ids:zod.z.array(zod.z.string().min(1)).min(1).max(100).optional(),offset:zod.z.number().int().min(0).default(0),limit:zod.z.number().int().min(1).max(100).default(50)}).strict(),updateSettingsInputSchema=zod.z.object({unassignedRoleId:zod.z.string().min(1).nullable()}).strict(),auditQuerySchema=zod.z.object({offset:zod.z.number().int().min(0).default(0),limit:zod.z.number().int().min(1).max(200).default(50)}).strict(),forbidden=r((e,t)=>new AccessAdminError(403,e,t),"forbidden"),badRequest=r((e,t)=>new AccessAdminError(400,e,t),"badRequest"),conflict=r((e,t)=>new AccessAdminError(409,e,t),"conflict"),notFound=r(e=>new AccessAdminError(404,"not_found",`${e} not found`),"notFound");function sameDomain2(e,t){return e===null||t===null?e===t:e.type===t.type&&e.id===t.id}r(sameDomain2,"sameDomain2");function uncoveredGrants(e,t,n){const s=chunkOP64Y4IN_cjs.expandImplications(e.permissions,validGrants(e,n)),a=[];for(const[i,o]of Object.entries(s)){const d=e.permissions[i],l=t.value(i);(l===void 0||d.scopes!==null&&chunkOP64Y4IN_cjs.scopeRank(d,l)<chunkOP64Y4IN_cjs.scopeRank(d,o))&&a.push(i)}return a}r(uncoveredGrants,"uncoveredGrants");var AccessAdmin=class{static{r(this,"AccessAdmin")}constructor(e){this.session=e}session;get engine(){return this.session.engine}get catalog(){return this.session.engine.catalog}get host(){return this.session.host}get tenantId(){return this.session.subject.tenantId}get actorId(){return this.session.subject.userId}async manager(e){const t=await this.session.grants(e);if(!t.can(chunkAJY7ZHXK_cjs.RESERVED_PERMISSIONS.manage))throw forbidden("permission_required",`Permission "${chunkAJY7ZHXK_cjs.RESERVED_PERMISSIONS.manage}" is required`);return t}assertCovers(e,t){if(e.managesThroughHostRole())return;const n=uncoveredGrants(this.catalog,e,t);if(n.length>0)throw forbidden("escalation_denied",`You can only grant what you hold yourself. Missing: ${n.join(", ")}`)}async assertDomain(e){if(!e)return;if(!this.catalog.domains[e.type])throw badRequest("unknown_domain",`Unknown domain type "${e.type}"`);if(!(await this.engine.domainSources.get(e.type).describe([e.id],this.host)).has(e.id))throw badRequest("domain_not_found",`${e.type} "${e.id}" does not exist`)}validateGrants(e,t={}){const n=[];for(const[s,a]of Object.entries(e)){const i=this.catalog.permissions[s];if(!i){n.push(`${s}: unknown permission`);continue}i.deprecated&&t[s]===void 0&&n.push(`${s}: deprecated, cannot be newly granted`);const o=chunkAJY7ZHXK_cjs.grantProblem(i,a);o&&n.push(`${s}: ${o}`)}if(n.length>0)throw badRequest("invalid_grants",n.join("; "));return e}async write(e){return this.host.db.transaction(async t=>(await lockAndBump(t,this.tenantId,this.actorId),e(t,n=>this.engine.hooks.onChange?.(t,n,{tenantId:this.tenantId,host:this.host})??Promise.resolve(),n=>appendAudit(t,this.tenantId,{actorId:this.actorId,...n}))))}async role(e){const t=await findRole(this.host.db,this.tenantId,e);if(!t)throw notFound("Role");return t}view(e,t,n){const s=e.key?this.catalog.roles[e.key]:void 0,a=validGrants(this.catalog,e.grants);return{id:e.id,key:e.key,builtin:e.builtin,domain:e.domain,name:e.name,nameKey:s?.nameKey??null,description:e.description,descriptionKey:s?.descriptionKey??null,grants:a,assignableBy:s?.assignableBy??"access-manager",exemptFromRoleLimit:s?.exemptFromRoleLimit??!1,assignmentCount:t.get(e.id)??0,isDefault:e.id===n,staleGrants:Object.keys(e.grants).filter(i=>!(i in a)),updatedAt:e.updatedAt,updatedBy:e.updatedBy}}async catalogView(e){return await this.manager(e),this.catalog}async listRoles(e){await this.manager(e);const t=await this.session.tenant(),n=await assignmentCounts(this.host.db,this.tenantId);return[...t.roles.values()].filter(s=>s.domain===null||sameDomain2(s.domain,e)).map(s=>this.view(s,n,t.settings.unassignedRoleId))}async createRole(e){const t=await this.manager(e.domain);await this.assertDomain(e.domain);const n=this.validateGrants(e.grants);return this.assertCovers(t,n),this.write(async(s,a,i)=>{await this.assertNameFree(s,e.name,e.domain,null);const o=await insertRole(s,this.tenantId,{key:null,builtin:!1,domain:e.domain,name:e.name,description:e.description,grants:n,catalogVersion:this.catalog.version,updatedBy:this.actorId});return await i({action:"role.create",targetType:"role",targetId:o.id,before:null,after:{name:o.name,domain:o.domain,grants:o.grants}}),await a({type:"role.created",role:o}),this.view(o,new Map,null)})}async assertNameFree(e,t,n,s){if((await e.query(`SELECT id FROM tego_access_roles
24
+ WHERE tenant_id = $1 AND NOT builtin AND lower(name) = lower($2)
25
+ AND domain_type IS NOT DISTINCT FROM $3 AND domain_id IS NOT DISTINCT FROM $4
26
+ AND id IS DISTINCT FROM $5`,[this.tenantId,t,n?.type??null,n?.id??null,s])).length>0)throw conflict("role_name_taken",`A role named "${t}" already exists`)}async updateRole(e,t){const n=await this.role(e);if(n.builtin)throw badRequest("builtin_locked","Built-in roles follow the plugin catalog; clone one to change it");const s=await this.manager(n.domain),a=t.grants?this.validateGrants(t.grants,n.grants):validGrants(this.catalog,n.grants);return this.assertCovers(s,validGrants(this.catalog,n.grants)),this.assertCovers(s,a),this.write(async(i,o,d)=>{const[l]=await i.query("SELECT unassigned_role_id FROM tego_access_settings WHERE tenant_id = $1",[this.tenantId]);l?.unassigned_role_id===e&&this.assertDefaultMayHold(a);const m=t.name??n.name;t.name&&await this.assertNameFree(i,m,n.domain,e);const p=await updateRole(i,this.tenantId,e,{name:m,description:t.description??n.description,grants:a,catalogVersion:this.catalog.version,updatedBy:this.actorId});await d({action:"role.update",targetType:"role",targetId:e,before:{name:n.name,description:n.description,grants:n.grants},after:{name:p.name,description:p.description,grants:p.grants}}),await o({type:"role.updated",role:p,before:n});const w=await assignmentCounts(i,this.tenantId);return this.view(p,w,l?.unassigned_role_id??null)})}async deleteRole(e){const t=await this.role(e);if(t.builtin)throw badRequest("builtin_locked","Built-in roles cannot be deleted");const n=await this.manager(t.domain);this.assertCovers(n,t.grants),await this.write(async(s,a,i)=>{if(await countAssignments(s,this.tenantId,e)>0)throw conflict("role_in_use","Remove the role from everyone holding it first");const[o]=await s.query("SELECT unassigned_role_id FROM tego_access_settings WHERE tenant_id = $1",[this.tenantId]);if(o?.unassigned_role_id===e)throw conflict("role_is_default","This role is what unassigned users get; choose another default first");await deleteRole(s,this.tenantId,e),await i({action:"role.delete",targetType:"role",targetId:e,before:{name:t.name,domain:t.domain,grants:t.grants},after:null}),await a({type:"role.deleted",role:t})})}async cloneRole(e,t){const n=await this.role(e),s=t.domain===void 0?n.domain:t.domain;if(n.domain&&!sameDomain2(n.domain,s))throw badRequest("domain_mismatch","A role of one domain can only be cloned within that domain");return this.createRole({name:t.name,description:n.description,grants:validGrants(this.catalog,n.grants),domain:s})}async principalNames(e){const t=new Map;for(const s of e)t.has(s.kind)||t.set(s.kind,new Set),t.get(s.kind).add(s.id);const n=new Map;return await Promise.all([...t].map(async([s,a])=>{const i=this.engine.principalSources.get(s);if(!i)return;const o=[...a];for(let d=0;d<o.length;d+=100){const l=o.slice(d,d+100),m=await i.list(this.host,{ids:l,offset:0,limit:l.length});for(const p of m.items)n.set(`${s}/${p.id}`,p.displayName)}})),n}async domainNames(e){const t=new Map,n=new Map;for(const s of e)n.has(s.type)||n.set(s.type,new Set),n.get(s.type).add(s.id);return await Promise.all([...n].map(async([s,a])=>{const i=this.engine.domainSources.get(s);if(i)for(const[o,d]of await i.describe([...a],this.host))t.set(`${s}:${o}`,d)})),t}async listAssignments(e,t){await this.manager(e);const n=await listAssignments(this.host.db,this.tenantId,{...t.roleId?{roleId:t.roleId}:{},...t.principalKind&&t.principalId?{principal:{kind:t.principalKind,id:t.principalId}}:{},domain:e}),[s,a]=await Promise.all([this.principalNames(n.map(i=>i.principal)),this.domainNames(n.flatMap(i=>i.domain?[i.domain]:[]))]);return n.map(i=>({...i,principalName:s.get(`${i.principal.kind}/${i.principal.id}`)??null,domainName:i.domain?a.get(`${i.domain.type}:${i.domain.id}`)??null:null}))}assertDefaultMayHold(e){if(chunkAJY7ZHXK_cjs.RESERVED_PERMISSIONS.manage in chunkOP64Y4IN_cjs.expandImplications(this.catalog.permissions,validGrants(this.catalog,e)))throw badRequest("default_role_manages","The default role cannot manage access: every user would")}assertMayAssign(e,t){if((t.key?this.catalog.roles[t.key]:void 0)?.assignableBy==="host-admin"&&!e.managesThroughHostRole())throw forbidden("host_admin_required","Only a host administrator can assign or remove this role");this.assertCovers(e,t.grants)}async createAssignment(e){const t=await this.role(e.roleId),{principal:n,domain:s}=e;if(t.domain&&!sameDomain2(t.domain,s))throw badRequest("domain_mismatch","This role belongs to one domain and can only be assigned there");const a=await this.manager(s);if(await this.assertDomain(s),!this.catalog.assignment.principals.includes(n.kind))throw badRequest("principal_kind_not_allowed",`Roles cannot be assigned to "${n.kind}" in this plugin`);if(n.kind==="user"&&(n.includeDescendants||n.unitRoles))throw badRequest("invalid_principal","includeDescendants and unitRoles apply to units and groups only");if(n.kind!=="unit"&&n.unitRoles)throw badRequest("invalid_principal","unitRoles apply to host units only");this.assertMayAssign(a,t);const o=(await this.principalNames([n])).get(`${n.kind}/${n.id}`);if(o===void 0)throw badRequest("principal_not_found",`${n.kind} "${n.id}" does not exist`);const d=t.key?this.catalog.roles[t.key]:void 0,l=this.catalog.assignment.maxRolesPerPrincipal[s?s.type:chunkAJY7ZHXK_cjs.GLOBAL_DOMAIN];return this.write(async(m,p,w)=>{if(n.kind==="user"&&l!==void 0&&!d?.exemptFromRoleLimit){const c=await listAssignments(m,this.tenantId,{principal:{kind:"user",id:n.id},domain:s}),g=await this.session.tenant();if(c.filter(y=>{const _=g.roles.get(y.roleId);return!(_?.key?this.catalog.roles[_.key]:void 0)?.exemptFromRoleLimit}).length>=l)throw conflict("role_limit_reached",`A user can hold at most ${l} role(s) here; remove the current one first`)}const f=await insertAssignment(m,this.tenantId,{roleId:t.id,principal:n,domain:s,createdBy:this.actorId});if(!f)throw conflict("already_assigned","This role is already assigned to them here");await w({action:"assignment.create",targetType:"assignment",targetId:f.id,before:null,after:{roleId:t.id,principal:n,domain:s}}),await p({type:"assignment.created",assignment:f});const u=await this.domainNames(s?[s]:[]);return{...f,principalName:o,domainName:s?u.get(`${s.type}:${s.id}`)??null:null}})}async deleteAssignment(e){const t=await findAssignment(this.host.db,this.tenantId,e);if(!t)throw notFound("Assignment");const n=await this.role(t.roleId),s=await this.manager(t.domain);this.assertMayAssign(s,n),await this.write(async(a,i,o)=>{await deleteAssignment(a,this.tenantId,e),await o({action:"assignment.delete",targetType:"assignment",targetId:e,before:{roleId:t.roleId,principal:t.principal,domain:t.domain},after:null}),await i({type:"assignment.deleted",assignment:t})})}async principals(e,t){if(await this.manager(e),!this.catalog.assignment.principals.includes(t.kind))throw badRequest("principal_kind_not_allowed",`Roles cannot be assigned to "${t.kind}" in this plugin`);return this.engine.principalSources.get(t.kind).list(this.host,{...t.search?{search:t.search}:{},...t.ids?{ids:t.ids}:{},offset:t.offset,limit:t.limit})}async explain(e,t){await this.manager(t);const n=await this.engine.subjectFor(e,this.host);if(!n)throw notFound("Active user");const s=await this.engine.forSubject(n,this.host).grants(t),a=await this.session.tenant();return{userId:e,domain:t,roles:s.sources.map(i=>{const o=a.roles.get(i.roleId);return{...i,name:o.name,nameKey:o.key?this.catalog.roles[o.key]?.nameKey??null:null}}),permissions:Object.keys(this.catalog.permissions).map(i=>{const{allowed:o,scope:d,reason:l,via:m}=s.decide(i);return{permission:i,allowed:o,scope:d,reason:l,via:m}})}}async settings(){await this.manager(null);const{unassignedRoleId:e,version:t,updatedAt:n,updatedBy:s}=(await this.session.tenant()).settings;return{unassignedRoleId:e,version:t,updatedAt:n,updatedBy:s}}async updateSettings(e){const t=await this.manager(null),n=(await this.session.tenant()).settings;if(n.unassignedRoleId!==null){const s=await findRole(this.host.db,this.tenantId,n.unassignedRoleId);s&&this.assertMayAssign(t,s)}if(e.unassignedRoleId!==null){const s=await this.role(e.unassignedRoleId);if(s.domain)throw badRequest("domain_role","The default role must be a global role");this.assertMayAssign(t,s)}return this.write(async(s,a,i)=>{if(e.unassignedRoleId!==null){const d=await findRole(s,this.tenantId,e.unassignedRoleId);if(!d)throw notFound("Role");this.assertDefaultMayHold(d.grants)}await writeSettings(s,this.tenantId,e.unassignedRoleId),await i({action:"settings.update",targetType:"settings",targetId:this.tenantId,before:{unassignedRoleId:n.unassignedRoleId},after:{unassignedRoleId:e.unassignedRoleId}}),await a({type:"settings.updated",before:{unassignedRoleId:n.unassignedRoleId},after:{unassignedRoleId:e.unassignedRoleId}});const[o]=await s.query("SELECT version, updated_at, updated_by FROM tego_access_settings WHERE tenant_id = $1",[this.tenantId]);return{unassignedRoleId:e.unassignedRoleId,version:o.version,updatedAt:new Date(o.updated_at).toISOString(),updatedBy:o.updated_by}})}async audit(e){if(!(await this.session.grants(null)).can(chunkAJY7ZHXK_cjs.RESERVED_PERMISSIONS.audit))throw forbidden("permission_required",`Permission "${chunkAJY7ZHXK_cjs.RESERVED_PERMISSIONS.audit}" is required`);const n=await listAudit(this.host.db,this.tenantId,e),s=await this.principalNames(n.items.flatMap(a=>a.actorId?[{kind:"user",id:a.actorId}]:[]));return{...n,items:n.items.map(a=>({...a,actorName:a.actorId?s.get(`user/${a.actorId}`)??null:null}))}}};function accessCatalogJsonSchema(){return zodToJsonSchema.zodToJsonSchema(chunkAJY7ZHXK_cjs.accessCatalogSchema,{name:"TegoAccessCatalog",$refStrategy:"none"})}r(accessCatalogJsonSchema,"accessCatalogJsonSchema"),Object.defineProperty(exports,"AccessCatalogError",{enumerable:!0,get:r(function(){return chunkAJY7ZHXK_cjs.AccessCatalogError},"get")}),Object.defineProperty(exports,"BUILTIN_PRINCIPAL_KINDS",{enumerable:!0,get:r(function(){return chunkAJY7ZHXK_cjs.BUILTIN_PRINCIPAL_KINDS},"get")}),Object.defineProperty(exports,"DELEGATION_ACTIONS",{enumerable:!0,get:r(function(){return chunkAJY7ZHXK_cjs.DELEGATION_ACTIONS},"get")}),Object.defineProperty(exports,"GLOBAL_DOMAIN",{enumerable:!0,get:r(function(){return chunkAJY7ZHXK_cjs.GLOBAL_DOMAIN},"get")}),Object.defineProperty(exports,"HOST_ADMIN_ROLE",{enumerable:!0,get:r(function(){return chunkAJY7ZHXK_cjs.HOST_ADMIN_ROLE},"get")}),Object.defineProperty(exports,"RESERVED_GROUP",{enumerable:!0,get:r(function(){return chunkAJY7ZHXK_cjs.RESERVED_GROUP},"get")}),Object.defineProperty(exports,"RESERVED_PERMISSIONS",{enumerable:!0,get:r(function(){return chunkAJY7ZHXK_cjs.RESERVED_PERMISSIONS},"get")}),Object.defineProperty(exports,"accessCatalogFromJson",{enumerable:!0,get:r(function(){return chunkAJY7ZHXK_cjs.accessCatalogFromJson},"get")}),Object.defineProperty(exports,"accessCatalogSchema",{enumerable:!0,get:r(function(){return chunkAJY7ZHXK_cjs.accessCatalogSchema},"get")}),Object.defineProperty(exports,"defineAccess",{enumerable:!0,get:r(function(){return chunkAJY7ZHXK_cjs.defineAccess},"get")}),Object.defineProperty(exports,"grantProblem",{enumerable:!0,get:r(function(){return chunkAJY7ZHXK_cjs.grantProblem},"get")}),Object.defineProperty(exports,"validateAccessCatalog",{enumerable:!0,get:r(function(){return chunkAJY7ZHXK_cjs.validateAccessCatalog},"get")}),Object.defineProperty(exports,"expandImplications",{enumerable:!0,get:r(function(){return chunkOP64Y4IN_cjs.expandImplications},"get")}),Object.defineProperty(exports,"mergeGrant",{enumerable:!0,get:r(function(){return chunkOP64Y4IN_cjs.mergeGrant},"get")}),Object.defineProperty(exports,"scopeRank",{enumerable:!0,get:r(function(){return chunkOP64Y4IN_cjs.scopeRank},"get")}),exports.AccessAdmin=AccessAdmin,exports.AccessAdminError=AccessAdminError,exports.AccessDeniedError=AccessDeniedError,exports.AccessEngine=AccessEngine,exports.AccessSession=AccessSession,exports.BUILTIN_SCOPES=BUILTIN_SCOPES,exports.EffectiveGrants=EffectiveGrants,exports.FALSE_FRAGMENT=FALSE_FRAGMENT,exports.TRUE_FRAGMENT=TRUE_FRAGMENT,exports.accessCatalogJsonSchema=accessCatalogJsonSchema,exports.allScope=allScope,exports.assignmentInDomain=assignmentInDomain,exports.assignmentMatches=assignmentMatches,exports.assignmentQuerySchema=assignmentQuerySchema,exports.auditQuerySchema=auditQuerySchema,exports.bindFragment=bindFragment,exports.cloneRoleInputSchema=cloneRoleInputSchema,exports.createAccess=createAccess,exports.createAssignmentInputSchema=createAssignmentInputSchema,exports.createRoleInputSchema=createRoleInputSchema,exports.decideResource=decideResource,exports.domainInputSchema=domainInputSchema,exports.domainKey=domainKey,exports.ident=ident,exports.orFragments=orFragments,exports.ownScope=ownScope,exports.principalQuerySchema=principalQuerySchema,exports.refColumn=refColumn,exports.renderFragment=renderFragment,exports.resolveGrants=resolveGrants,exports.sql=sql,exports.subjectFromContext=subjectFromContext,exports.uncoveredGrants=uncoveredGrants,exports.unitPrincipalSource=unitPrincipalSource,exports.updateRoleInputSchema=updateRoleInputSchema,exports.updateSettingsInputSchema=updateSettingsInputSchema,exports.userPrincipalSource=userPrincipalSource,exports.validGrants=validGrants;