@classytic/arc 1.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -794
- package/bin/arc.js +91 -52
- package/dist/EventTransport-BD2U0BTc.d.mts +100 -0
- package/dist/EventTransport-BD2U0BTc.d.mts.map +1 -0
- package/dist/HookSystem-BsGV-j2l.mjs +405 -0
- package/dist/HookSystem-BsGV-j2l.mjs.map +1 -0
- package/dist/ResourceRegistry-DsN4KJjV.mjs +250 -0
- package/dist/ResourceRegistry-DsN4KJjV.mjs.map +1 -0
- package/dist/adapters/index.d.mts +5 -0
- package/dist/adapters/index.mjs +3 -0
- package/dist/audit/index.d.mts +82 -0
- package/dist/audit/index.d.mts.map +1 -0
- package/dist/audit/index.mjs +276 -0
- package/dist/audit/index.mjs.map +1 -0
- package/dist/audit/mongodb.d.mts +5 -0
- package/dist/audit/mongodb.mjs +3 -0
- package/dist/audited-C3T5DTUx.mjs +141 -0
- package/dist/audited-C3T5DTUx.mjs.map +1 -0
- package/dist/auth/index.d.mts +189 -0
- package/dist/auth/index.d.mts.map +1 -0
- package/dist/auth/index.mjs +1102 -0
- package/dist/auth/index.mjs.map +1 -0
- package/dist/auth/redis-session.d.mts +44 -0
- package/dist/auth/redis-session.d.mts.map +1 -0
- package/dist/auth/redis-session.mjs +76 -0
- package/dist/auth/redis-session.mjs.map +1 -0
- package/dist/betterAuthOpenApi-BrHKeSAx.mjs +250 -0
- package/dist/betterAuthOpenApi-BrHKeSAx.mjs.map +1 -0
- package/dist/cache/index.d.mts +146 -0
- package/dist/cache/index.d.mts.map +1 -0
- package/dist/cache/index.mjs +92 -0
- package/dist/cache/index.mjs.map +1 -0
- package/dist/caching-Bl28lYsR.mjs +94 -0
- package/dist/caching-Bl28lYsR.mjs.map +1 -0
- package/dist/chunk-C7Uep-_p.mjs +20 -0
- package/dist/circuitBreaker-DeY4FCjs.mjs +1097 -0
- package/dist/circuitBreaker-DeY4FCjs.mjs.map +1 -0
- package/dist/cli/commands/describe.d.mts +19 -0
- package/dist/cli/commands/describe.d.mts.map +1 -0
- package/dist/cli/commands/describe.mjs +239 -0
- package/dist/cli/commands/describe.mjs.map +1 -0
- package/dist/cli/commands/docs.d.mts +14 -0
- package/dist/cli/commands/docs.d.mts.map +1 -0
- package/dist/cli/commands/docs.mjs +53 -0
- package/dist/cli/commands/docs.mjs.map +1 -0
- package/dist/cli/commands/{generate.d.ts → generate.d.mts} +3 -1
- package/dist/cli/commands/generate.d.mts.map +1 -0
- package/dist/cli/commands/generate.mjs +358 -0
- package/dist/cli/commands/generate.mjs.map +1 -0
- package/dist/cli/commands/{init.d.ts → init.d.mts} +12 -8
- package/dist/cli/commands/init.d.mts.map +1 -0
- package/dist/cli/commands/{init.js → init.mjs} +807 -616
- package/dist/cli/commands/init.mjs.map +1 -0
- package/dist/cli/commands/introspect.d.mts +11 -0
- package/dist/cli/commands/introspect.d.mts.map +1 -0
- package/dist/cli/commands/introspect.mjs +76 -0
- package/dist/cli/commands/introspect.mjs.map +1 -0
- package/dist/cli/index.d.mts +17 -0
- package/dist/cli/index.d.mts.map +1 -0
- package/dist/cli/index.mjs +157 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/constants-DdXFXQtN.mjs +85 -0
- package/dist/constants-DdXFXQtN.mjs.map +1 -0
- package/dist/core/index.d.mts +5 -0
- package/dist/core/index.mjs +4 -0
- package/dist/createApp-CUgNqegw.mjs +560 -0
- package/dist/createApp-CUgNqegw.mjs.map +1 -0
- package/dist/defineResource-k0_BDn8v.mjs +2197 -0
- package/dist/defineResource-k0_BDn8v.mjs.map +1 -0
- package/dist/discovery/index.d.mts +47 -0
- package/dist/discovery/index.d.mts.map +1 -0
- package/dist/discovery/index.mjs +110 -0
- package/dist/discovery/index.mjs.map +1 -0
- package/dist/docs/index.d.mts +163 -0
- package/dist/docs/index.d.mts.map +1 -0
- package/dist/docs/index.mjs +73 -0
- package/dist/docs/index.mjs.map +1 -0
- package/dist/elevation-BRy3yFWT.mjs +113 -0
- package/dist/elevation-BRy3yFWT.mjs.map +1 -0
- package/dist/elevation-B_2dRLVP.d.mts +88 -0
- package/dist/elevation-B_2dRLVP.d.mts.map +1 -0
- package/dist/errorHandler-BbcgBmIH.d.mts +73 -0
- package/dist/errorHandler-BbcgBmIH.d.mts.map +1 -0
- package/dist/errorHandler-C1okiriz.mjs +109 -0
- package/dist/errorHandler-C1okiriz.mjs.map +1 -0
- package/dist/errors-B9bZok84.mjs +212 -0
- package/dist/errors-B9bZok84.mjs.map +1 -0
- package/dist/errors-ChKiFz62.d.mts +125 -0
- package/dist/errors-ChKiFz62.d.mts.map +1 -0
- package/dist/eventPlugin-CTrLH3mt.d.mts +125 -0
- package/dist/eventPlugin-CTrLH3mt.d.mts.map +1 -0
- package/dist/eventPlugin-DGR_B2on.mjs +230 -0
- package/dist/eventPlugin-DGR_B2on.mjs.map +1 -0
- package/dist/events/index.d.mts +54 -0
- package/dist/events/index.d.mts.map +1 -0
- package/dist/events/index.mjs +52 -0
- package/dist/events/index.mjs.map +1 -0
- package/dist/events/transports/redis-stream-entry.d.mts +2 -0
- package/dist/events/transports/redis-stream-entry.mjs +178 -0
- package/dist/events/transports/redis-stream-entry.mjs.map +1 -0
- package/dist/events/transports/redis.d.mts +77 -0
- package/dist/events/transports/redis.d.mts.map +1 -0
- package/dist/events/transports/redis.mjs +125 -0
- package/dist/events/transports/redis.mjs.map +1 -0
- package/dist/externalPaths-DlINfKbP.d.mts +51 -0
- package/dist/externalPaths-DlINfKbP.d.mts.map +1 -0
- package/dist/factory/index.d.mts +64 -0
- package/dist/factory/index.d.mts.map +1 -0
- package/dist/factory/index.mjs +3 -0
- package/dist/fastifyAdapter-BkrGrlFi.d.mts +217 -0
- package/dist/fastifyAdapter-BkrGrlFi.d.mts.map +1 -0
- package/dist/fields-DyaDVX4J.d.mts +110 -0
- package/dist/fields-DyaDVX4J.d.mts.map +1 -0
- package/dist/fields-iagOozy0.mjs +115 -0
- package/dist/fields-iagOozy0.mjs.map +1 -0
- package/dist/hooks/index.d.mts +4 -0
- package/dist/hooks/index.mjs +3 -0
- package/dist/idempotency/index.d.mts +97 -0
- package/dist/idempotency/index.d.mts.map +1 -0
- package/dist/idempotency/index.mjs +320 -0
- package/dist/idempotency/index.mjs.map +1 -0
- package/dist/idempotency/mongodb.d.mts +2 -0
- package/dist/idempotency/mongodb.mjs +115 -0
- package/dist/idempotency/mongodb.mjs.map +1 -0
- package/dist/idempotency/redis.d.mts +2 -0
- package/dist/idempotency/redis.mjs +104 -0
- package/dist/idempotency/redis.mjs.map +1 -0
- package/dist/index.d.mts +261 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +105 -0
- package/dist/index.mjs.map +1 -0
- package/dist/integrations/event-gateway.d.mts +47 -0
- package/dist/integrations/event-gateway.d.mts.map +1 -0
- package/dist/integrations/event-gateway.mjs +44 -0
- package/dist/integrations/event-gateway.mjs.map +1 -0
- package/dist/integrations/index.d.mts +5 -0
- package/dist/integrations/index.mjs +1 -0
- package/dist/integrations/jobs.d.mts +104 -0
- package/dist/integrations/jobs.d.mts.map +1 -0
- package/dist/integrations/jobs.mjs +124 -0
- package/dist/integrations/jobs.mjs.map +1 -0
- package/dist/integrations/streamline.d.mts +61 -0
- package/dist/integrations/streamline.d.mts.map +1 -0
- package/dist/integrations/streamline.mjs +126 -0
- package/dist/integrations/streamline.mjs.map +1 -0
- package/dist/integrations/websocket.d.mts +83 -0
- package/dist/integrations/websocket.d.mts.map +1 -0
- package/dist/integrations/websocket.mjs +289 -0
- package/dist/integrations/websocket.mjs.map +1 -0
- package/dist/interface-B01JvPVc.d.mts +78 -0
- package/dist/interface-B01JvPVc.d.mts.map +1 -0
- package/dist/interface-CZe8IkMf.d.mts +55 -0
- package/dist/interface-CZe8IkMf.d.mts.map +1 -0
- package/dist/interface-Ch8HU9uM.d.mts +1098 -0
- package/dist/interface-Ch8HU9uM.d.mts.map +1 -0
- package/dist/introspectionPlugin-rFdO8ZUa.mjs +54 -0
- package/dist/introspectionPlugin-rFdO8ZUa.mjs.map +1 -0
- package/dist/keys-BqNejWup.mjs +43 -0
- package/dist/keys-BqNejWup.mjs.map +1 -0
- package/dist/logger-Df2O2WsW.mjs +79 -0
- package/dist/logger-Df2O2WsW.mjs.map +1 -0
- package/dist/memory-cQgelFOj.mjs +144 -0
- package/dist/memory-cQgelFOj.mjs.map +1 -0
- package/dist/migrations/index.d.mts +157 -0
- package/dist/migrations/index.d.mts.map +1 -0
- package/dist/migrations/index.mjs +261 -0
- package/dist/migrations/index.mjs.map +1 -0
- package/dist/mongodb-BfJVlUJH.mjs +94 -0
- package/dist/mongodb-BfJVlUJH.mjs.map +1 -0
- package/dist/mongodb-CGzRbfAK.d.mts +119 -0
- package/dist/mongodb-CGzRbfAK.d.mts.map +1 -0
- package/dist/mongodb-JN-9JA7K.d.mts +72 -0
- package/dist/mongodb-JN-9JA7K.d.mts.map +1 -0
- package/dist/openapi-G3Cw7XuM.mjs +524 -0
- package/dist/openapi-G3Cw7XuM.mjs.map +1 -0
- package/dist/org/index.d.mts +69 -0
- package/dist/org/index.d.mts.map +1 -0
- package/dist/org/index.mjs +514 -0
- package/dist/org/index.mjs.map +1 -0
- package/dist/org/types.d.mts +83 -0
- package/dist/org/types.d.mts.map +1 -0
- package/dist/org/types.mjs +1 -0
- package/dist/permissions/index.d.mts +279 -0
- package/dist/permissions/index.d.mts.map +1 -0
- package/dist/permissions/index.mjs +579 -0
- package/dist/permissions/index.mjs.map +1 -0
- package/dist/plugins/index.d.mts +173 -0
- package/dist/plugins/index.d.mts.map +1 -0
- package/dist/plugins/index.mjs +523 -0
- package/dist/plugins/index.mjs.map +1 -0
- package/dist/plugins/response-cache.d.mts +88 -0
- package/dist/plugins/response-cache.d.mts.map +1 -0
- package/dist/plugins/response-cache.mjs +284 -0
- package/dist/plugins/response-cache.mjs.map +1 -0
- package/dist/plugins/tracing-entry.d.mts +2 -0
- package/dist/plugins/tracing-entry.mjs +186 -0
- package/dist/plugins/tracing-entry.mjs.map +1 -0
- package/dist/pluralize-CEweyOEm.mjs +87 -0
- package/dist/pluralize-CEweyOEm.mjs.map +1 -0
- package/dist/policies/{index.d.ts → index.d.mts} +204 -169
- package/dist/policies/index.d.mts.map +1 -0
- package/dist/policies/index.mjs +322 -0
- package/dist/policies/index.mjs.map +1 -0
- package/dist/presets/{index.d.ts → index.d.mts} +63 -131
- package/dist/presets/index.d.mts.map +1 -0
- package/dist/presets/index.mjs +144 -0
- package/dist/presets/index.mjs.map +1 -0
- package/dist/presets/multiTenant.d.mts +25 -0
- package/dist/presets/multiTenant.d.mts.map +1 -0
- package/dist/presets/multiTenant.mjs +114 -0
- package/dist/presets/multiTenant.mjs.map +1 -0
- package/dist/presets-BITljm96.mjs +120 -0
- package/dist/presets-BITljm96.mjs.map +1 -0
- package/dist/presets-DzSMwlKj.d.mts +58 -0
- package/dist/presets-DzSMwlKj.d.mts.map +1 -0
- package/dist/prisma-DJbMt3yf.mjs +628 -0
- package/dist/prisma-DJbMt3yf.mjs.map +1 -0
- package/dist/prisma-Dg9GoVdj.d.mts +275 -0
- package/dist/prisma-Dg9GoVdj.d.mts.map +1 -0
- package/dist/queryCachePlugin-7THaI5mt.d.mts +72 -0
- package/dist/queryCachePlugin-7THaI5mt.d.mts.map +1 -0
- package/dist/queryCachePlugin-DMBnp2Q0.mjs +139 -0
- package/dist/queryCachePlugin-DMBnp2Q0.mjs.map +1 -0
- package/dist/redis-D-JAeLtm.d.mts +50 -0
- package/dist/redis-D-JAeLtm.d.mts.map +1 -0
- package/dist/redis-stream-Bdh_vUU8.d.mts +104 -0
- package/dist/redis-stream-Bdh_vUU8.d.mts.map +1 -0
- package/dist/registry/index.d.mts +12 -0
- package/dist/registry/index.d.mts.map +1 -0
- package/dist/registry/index.mjs +4 -0
- package/dist/requestContext-QQD6ROJc.mjs +56 -0
- package/dist/requestContext-QQD6ROJc.mjs.map +1 -0
- package/dist/schemaConverter-BwrmWroW.mjs +99 -0
- package/dist/schemaConverter-BwrmWroW.mjs.map +1 -0
- package/dist/schemas/index.d.mts +64 -0
- package/dist/schemas/index.d.mts.map +1 -0
- package/dist/schemas/index.mjs +83 -0
- package/dist/schemas/index.mjs.map +1 -0
- package/dist/scope/index.d.mts +22 -0
- package/dist/scope/index.d.mts.map +1 -0
- package/dist/scope/index.mjs +66 -0
- package/dist/scope/index.mjs.map +1 -0
- package/dist/sessionManager-jPKLbHE0.d.mts +187 -0
- package/dist/sessionManager-jPKLbHE0.d.mts.map +1 -0
- package/dist/sse-B3c3_yZp.mjs +124 -0
- package/dist/sse-B3c3_yZp.mjs.map +1 -0
- package/dist/testing/index.d.mts +908 -0
- package/dist/testing/index.d.mts.map +1 -0
- package/dist/testing/index.mjs +1977 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/tracing-Cc7vVQPp.d.mts +71 -0
- package/dist/tracing-Cc7vVQPp.d.mts.map +1 -0
- package/dist/typeGuards-DhMNLuvU.mjs +10 -0
- package/dist/typeGuards-DhMNLuvU.mjs.map +1 -0
- package/dist/types/index.d.mts +947 -0
- package/dist/types/index.d.mts.map +1 -0
- package/dist/types/index.mjs +15 -0
- package/dist/types/index.mjs.map +1 -0
- package/dist/types-Beqn1Un7.mjs +39 -0
- package/dist/types-Beqn1Un7.mjs.map +1 -0
- package/dist/types-CIgB7UUl.d.mts +446 -0
- package/dist/types-CIgB7UUl.d.mts.map +1 -0
- package/dist/types-aYB4V7uN.d.mts +87 -0
- package/dist/types-aYB4V7uN.d.mts.map +1 -0
- package/dist/utils/index.d.mts +748 -0
- package/dist/utils/index.d.mts.map +1 -0
- package/dist/utils/index.mjs +6 -0
- package/package.json +194 -68
- package/dist/BaseController-DVAiHxEQ.d.ts +0 -233
- package/dist/adapters/index.d.ts +0 -237
- package/dist/adapters/index.js +0 -668
- package/dist/arcCorePlugin-CsShQdyP.d.ts +0 -273
- package/dist/audit/index.d.ts +0 -195
- package/dist/audit/index.js +0 -319
- package/dist/auth/index.d.ts +0 -47
- package/dist/auth/index.js +0 -174
- package/dist/cli/commands/docs.d.ts +0 -11
- package/dist/cli/commands/docs.js +0 -474
- package/dist/cli/commands/generate.js +0 -334
- package/dist/cli/commands/introspect.d.ts +0 -8
- package/dist/cli/commands/introspect.js +0 -338
- package/dist/cli/index.d.ts +0 -4
- package/dist/cli/index.js +0 -3269
- package/dist/core/index.d.ts +0 -220
- package/dist/core/index.js +0 -2786
- package/dist/createApp-Ce9wl8W9.d.ts +0 -77
- package/dist/docs/index.d.ts +0 -166
- package/dist/docs/index.js +0 -658
- package/dist/errors-8WIxGS_6.d.ts +0 -122
- package/dist/events/index.d.ts +0 -117
- package/dist/events/index.js +0 -89
- package/dist/factory/index.d.ts +0 -38
- package/dist/factory/index.js +0 -1652
- package/dist/hooks/index.d.ts +0 -4
- package/dist/hooks/index.js +0 -199
- package/dist/idempotency/index.d.ts +0 -323
- package/dist/idempotency/index.js +0 -500
- package/dist/index-B4t03KQ0.d.ts +0 -1366
- package/dist/index.d.ts +0 -135
- package/dist/index.js +0 -4756
- package/dist/migrations/index.d.ts +0 -185
- package/dist/migrations/index.js +0 -274
- package/dist/org/index.d.ts +0 -129
- package/dist/org/index.js +0 -220
- package/dist/permissions/index.d.ts +0 -144
- package/dist/permissions/index.js +0 -103
- package/dist/plugins/index.d.ts +0 -46
- package/dist/plugins/index.js +0 -1069
- package/dist/policies/index.js +0 -196
- package/dist/presets/index.js +0 -384
- package/dist/presets/multiTenant.d.ts +0 -39
- package/dist/presets/multiTenant.js +0 -112
- package/dist/registry/index.d.ts +0 -16
- package/dist/registry/index.js +0 -253
- package/dist/testing/index.d.ts +0 -618
- package/dist/testing/index.js +0 -48020
- package/dist/types/index.d.ts +0 -4
- package/dist/types/index.js +0 -8
- package/dist/types-B99TBmFV.d.ts +0 -76
- package/dist/types-BvckRbs2.d.ts +0 -143
- package/dist/utils/index.d.ts +0 -679
- package/dist/utils/index.js +0 -931
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import { a as getTeamId, c as isElevated, l as isMember, n as PUBLIC_SCOPE } from "../types-Beqn1Un7.mjs";
|
|
2
|
+
import { i as resolveEffectiveRoles, n as applyFieldWritePermissions, r as fields, t as applyFieldReadPermissions } from "../fields-iagOozy0.mjs";
|
|
3
|
+
import { t as MemoryCacheStore } from "../memory-cQgelFOj.mjs";
|
|
4
|
+
import { a as presets_exports, c as readOnly, i as ownerWithAdminBypass, n as authenticated, o as publicRead, r as fullPublic, s as publicReadAdminWrite, t as adminOnly } from "../presets-BITljm96.mjs";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
//#region src/permissions/index.ts
|
|
8
|
+
/**
|
|
9
|
+
* Allow public access (no authentication required)
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* permissions: {
|
|
14
|
+
* list: allowPublic(),
|
|
15
|
+
* get: allowPublic(),
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
function allowPublic() {
|
|
20
|
+
const check = () => true;
|
|
21
|
+
check._isPublic = true;
|
|
22
|
+
return check;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Require authentication (any authenticated user)
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* permissions: {
|
|
30
|
+
* create: requireAuth(),
|
|
31
|
+
* update: requireAuth(),
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
function requireAuth() {
|
|
36
|
+
const check = (ctx) => {
|
|
37
|
+
if (!ctx.user) return {
|
|
38
|
+
granted: false,
|
|
39
|
+
reason: "Authentication required"
|
|
40
|
+
};
|
|
41
|
+
return true;
|
|
42
|
+
};
|
|
43
|
+
return check;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Require specific roles
|
|
47
|
+
*
|
|
48
|
+
* @param roles - Required roles (user needs at least one)
|
|
49
|
+
* @param options - Optional bypass roles
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* permissions: {
|
|
54
|
+
* create: requireRoles(['admin', 'editor']),
|
|
55
|
+
* delete: requireRoles(['admin']),
|
|
56
|
+
* }
|
|
57
|
+
*
|
|
58
|
+
* // With bypass roles
|
|
59
|
+
* permissions: {
|
|
60
|
+
* update: requireRoles(['owner'], { bypassRoles: ['admin', 'superadmin'] }),
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
function requireRoles(roles, options) {
|
|
65
|
+
const check = (ctx) => {
|
|
66
|
+
if (!ctx.user) return {
|
|
67
|
+
granted: false,
|
|
68
|
+
reason: "Authentication required"
|
|
69
|
+
};
|
|
70
|
+
const userRoles = ctx.user.roles ?? [];
|
|
71
|
+
if (options?.bypassRoles?.some((r) => userRoles.includes(r))) return true;
|
|
72
|
+
if (roles.some((r) => userRoles.includes(r))) return true;
|
|
73
|
+
return {
|
|
74
|
+
granted: false,
|
|
75
|
+
reason: `Required roles: ${roles.join(", ")}`
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
check._roles = roles;
|
|
79
|
+
return check;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Require resource ownership
|
|
83
|
+
*
|
|
84
|
+
* Returns filters to scope queries to user's owned resources.
|
|
85
|
+
*
|
|
86
|
+
* @param ownerField - Field containing owner ID (default: 'userId')
|
|
87
|
+
* @param options - Optional bypass roles
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```typescript
|
|
91
|
+
* permissions: {
|
|
92
|
+
* update: requireOwnership('userId'),
|
|
93
|
+
* delete: requireOwnership('createdBy', { bypassRoles: ['admin'] }),
|
|
94
|
+
* }
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
function requireOwnership(ownerField = "userId", options) {
|
|
98
|
+
return (ctx) => {
|
|
99
|
+
if (!ctx.user) return {
|
|
100
|
+
granted: false,
|
|
101
|
+
reason: "Authentication required"
|
|
102
|
+
};
|
|
103
|
+
const userRoles = ctx.user.roles ?? [];
|
|
104
|
+
if (options?.bypassRoles?.some((r) => userRoles.includes(r))) return true;
|
|
105
|
+
const userId = ctx.user.id ?? ctx.user._id;
|
|
106
|
+
if (!userId) return {
|
|
107
|
+
granted: false,
|
|
108
|
+
reason: "User identity missing (no id or _id)"
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
granted: true,
|
|
112
|
+
filters: { [ownerField]: userId }
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Combine multiple checks - ALL must pass (AND logic)
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* permissions: {
|
|
122
|
+
* update: allOf(
|
|
123
|
+
* requireAuth(),
|
|
124
|
+
* requireRoles(['editor']),
|
|
125
|
+
* requireOwnership('createdBy')
|
|
126
|
+
* ),
|
|
127
|
+
* }
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
function allOf(...checks) {
|
|
131
|
+
return async (ctx) => {
|
|
132
|
+
let mergedFilters = {};
|
|
133
|
+
for (const check of checks) {
|
|
134
|
+
const result = await check(ctx);
|
|
135
|
+
const normalized = typeof result === "boolean" ? { granted: result } : result;
|
|
136
|
+
if (!normalized.granted) return normalized;
|
|
137
|
+
if (normalized.filters) mergedFilters = {
|
|
138
|
+
...mergedFilters,
|
|
139
|
+
...normalized.filters
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
granted: true,
|
|
144
|
+
filters: Object.keys(mergedFilters).length > 0 ? mergedFilters : void 0
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Combine multiple checks - ANY must pass (OR logic)
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* permissions: {
|
|
154
|
+
* update: anyOf(
|
|
155
|
+
* requireRoles(['admin']),
|
|
156
|
+
* requireOwnership('createdBy')
|
|
157
|
+
* ),
|
|
158
|
+
* }
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
function anyOf(...checks) {
|
|
162
|
+
return async (ctx) => {
|
|
163
|
+
const reasons = [];
|
|
164
|
+
for (const check of checks) {
|
|
165
|
+
const result = await check(ctx);
|
|
166
|
+
const normalized = typeof result === "boolean" ? { granted: result } : result;
|
|
167
|
+
if (normalized.granted) return normalized;
|
|
168
|
+
if (normalized.reason) reasons.push(normalized.reason);
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
granted: false,
|
|
172
|
+
reason: reasons.join("; ")
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Deny all access
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```typescript
|
|
181
|
+
* permissions: {
|
|
182
|
+
* delete: denyAll('Deletion not allowed'),
|
|
183
|
+
* }
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
function denyAll(reason = "Access denied") {
|
|
187
|
+
return () => ({
|
|
188
|
+
granted: false,
|
|
189
|
+
reason
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Dynamic permission based on context
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```typescript
|
|
197
|
+
* permissions: {
|
|
198
|
+
* update: when((ctx) => ctx.data?.status === 'draft'),
|
|
199
|
+
* }
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
function when(condition) {
|
|
203
|
+
return async (ctx) => {
|
|
204
|
+
const result = await condition(ctx);
|
|
205
|
+
return {
|
|
206
|
+
granted: result,
|
|
207
|
+
reason: result ? void 0 : "Condition not met"
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/** Read request.scope safely */
|
|
212
|
+
function getScope(request) {
|
|
213
|
+
return request.scope ?? PUBLIC_SCOPE;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Require membership in the active organization.
|
|
217
|
+
* User must be authenticated AND have an active org (member or elevated scope).
|
|
218
|
+
*
|
|
219
|
+
* Reads `request.scope` set by auth adapters.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```typescript
|
|
223
|
+
* permissions: {
|
|
224
|
+
* list: requireOrgMembership(),
|
|
225
|
+
* get: requireOrgMembership(),
|
|
226
|
+
* }
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
function requireOrgMembership() {
|
|
230
|
+
const check = (ctx) => {
|
|
231
|
+
if (!ctx.user) return {
|
|
232
|
+
granted: false,
|
|
233
|
+
reason: "Authentication required"
|
|
234
|
+
};
|
|
235
|
+
const scope = getScope(ctx.request);
|
|
236
|
+
if (isElevated(scope)) return true;
|
|
237
|
+
if (isMember(scope)) return true;
|
|
238
|
+
return {
|
|
239
|
+
granted: false,
|
|
240
|
+
reason: "Organization membership required"
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
check._orgPermission = "membership";
|
|
244
|
+
return check;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Require specific org-level roles.
|
|
248
|
+
* Reads `request.scope.orgRoles` (set by auth adapters).
|
|
249
|
+
* Elevated scope always passes (platform admin bypass).
|
|
250
|
+
*
|
|
251
|
+
* @param roles - Required org roles (user needs at least one)
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* ```typescript
|
|
255
|
+
* permissions: {
|
|
256
|
+
* create: requireOrgRole('admin', 'owner'),
|
|
257
|
+
* delete: requireOrgRole('owner'),
|
|
258
|
+
* }
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
function requireOrgRole(...args) {
|
|
262
|
+
const roles = Array.isArray(args[0]) ? args[0] : args;
|
|
263
|
+
const check = (ctx) => {
|
|
264
|
+
if (!ctx.user) return {
|
|
265
|
+
granted: false,
|
|
266
|
+
reason: "Authentication required"
|
|
267
|
+
};
|
|
268
|
+
const scope = getScope(ctx.request);
|
|
269
|
+
if (isElevated(scope)) return true;
|
|
270
|
+
if (!isMember(scope)) return {
|
|
271
|
+
granted: false,
|
|
272
|
+
reason: "Organization membership required"
|
|
273
|
+
};
|
|
274
|
+
if (roles.some((r) => scope.orgRoles.includes(r))) return true;
|
|
275
|
+
return {
|
|
276
|
+
granted: false,
|
|
277
|
+
reason: `Required org roles: ${roles.join(", ")}`
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
check._orgRoles = roles;
|
|
281
|
+
return check;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Create a scoped permission system for resource-action patterns.
|
|
285
|
+
* Maps org roles to fine-grained permissions without external API calls.
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```typescript
|
|
289
|
+
* const perms = createOrgPermissions({
|
|
290
|
+
* statements: {
|
|
291
|
+
* product: ['create', 'update', 'delete'],
|
|
292
|
+
* order: ['create', 'approve'],
|
|
293
|
+
* },
|
|
294
|
+
* roles: {
|
|
295
|
+
* owner: { product: ['create', 'update', 'delete'], order: ['create', 'approve'] },
|
|
296
|
+
* admin: { product: ['create', 'update'], order: ['create'] },
|
|
297
|
+
* member: { product: [], order: [] },
|
|
298
|
+
* },
|
|
299
|
+
* });
|
|
300
|
+
*
|
|
301
|
+
* defineResource({
|
|
302
|
+
* permissions: {
|
|
303
|
+
* create: perms.can({ product: ['create'] }),
|
|
304
|
+
* delete: perms.can({ product: ['delete'] }),
|
|
305
|
+
* }
|
|
306
|
+
* });
|
|
307
|
+
* ```
|
|
308
|
+
*/
|
|
309
|
+
function createOrgPermissions(config) {
|
|
310
|
+
const { roles: roleMap } = config;
|
|
311
|
+
function hasPermissions(orgRoles, required) {
|
|
312
|
+
for (const [resource, actions] of Object.entries(required)) for (const action of actions) if (!orgRoles.some((role) => {
|
|
313
|
+
return (roleMap[role]?.[resource])?.includes(action);
|
|
314
|
+
})) return false;
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
can(permissions) {
|
|
319
|
+
return (ctx) => {
|
|
320
|
+
if (!ctx.user) return {
|
|
321
|
+
granted: false,
|
|
322
|
+
reason: "Authentication required"
|
|
323
|
+
};
|
|
324
|
+
const scope = getScope(ctx.request);
|
|
325
|
+
if (isElevated(scope)) return true;
|
|
326
|
+
if (!isMember(scope)) return {
|
|
327
|
+
granted: false,
|
|
328
|
+
reason: "Organization membership required"
|
|
329
|
+
};
|
|
330
|
+
if (hasPermissions(scope.orgRoles, permissions)) return true;
|
|
331
|
+
return {
|
|
332
|
+
granted: false,
|
|
333
|
+
reason: `Missing permissions: ${Object.entries(permissions).map(([r, a]) => `${r}:[${a.join(",")}]`).join(", ")}`
|
|
334
|
+
};
|
|
335
|
+
};
|
|
336
|
+
},
|
|
337
|
+
requireRole(...roles) {
|
|
338
|
+
return requireOrgRole(roles);
|
|
339
|
+
},
|
|
340
|
+
requireMembership() {
|
|
341
|
+
return requireOrgMembership();
|
|
342
|
+
},
|
|
343
|
+
requireTeamMembership() {
|
|
344
|
+
return requireTeamMembership();
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Create a dynamic role-based permission matrix.
|
|
350
|
+
*
|
|
351
|
+
* Use this when role/action mappings are managed outside code
|
|
352
|
+
* (e.g., admin UI matrix, DB-stored ACLs, remote policy service).
|
|
353
|
+
*
|
|
354
|
+
* Supports:
|
|
355
|
+
* - org role union (any assigned org role can grant)
|
|
356
|
+
* - global bypass roles
|
|
357
|
+
* - wildcard resource/action (`*`)
|
|
358
|
+
* - optional in-memory cache
|
|
359
|
+
*/
|
|
360
|
+
function createDynamicPermissionMatrix(config) {
|
|
361
|
+
const logger = config.logger ?? console;
|
|
362
|
+
const legacyTtlMs = config.cache?.ttlMs ?? 0;
|
|
363
|
+
const hasExternalStore = !!config.cacheStore;
|
|
364
|
+
const cacheTtlMs = legacyTtlMs > 0 ? legacyTtlMs : hasExternalStore ? 3e5 : 0;
|
|
365
|
+
const internalStore = !config.cacheStore && cacheTtlMs > 0 ? new MemoryCacheStore({
|
|
366
|
+
defaultTtlMs: cacheTtlMs,
|
|
367
|
+
maxEntries: config.cache?.maxEntries ?? 1e3
|
|
368
|
+
}) : void 0;
|
|
369
|
+
const cacheStore = config.cacheStore ?? internalStore;
|
|
370
|
+
const trackedKeys = /* @__PURE__ */ new Set();
|
|
371
|
+
const nodeId = randomUUID().slice(0, 8);
|
|
372
|
+
const DEFAULT_EVENT_TYPE = "arc.permissions.invalidated";
|
|
373
|
+
let eventBridge = null;
|
|
374
|
+
/** Clear local cache for an org without publishing events (avoids infinite loops). */
|
|
375
|
+
async function localInvalidateByOrg(orgId) {
|
|
376
|
+
if (!cacheStore) return;
|
|
377
|
+
const prefix = `${orgId}::`;
|
|
378
|
+
const toDelete = [];
|
|
379
|
+
for (const key of trackedKeys) if (key.startsWith(prefix)) toDelete.push(key);
|
|
380
|
+
for (const key of toDelete) try {
|
|
381
|
+
await cacheStore.delete(key);
|
|
382
|
+
trackedKeys.delete(key);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
logger.warn(`[DynamicPermissionMatrix] invalidateByOrg delete failed for '${key}': ${error instanceof Error ? error.message : String(error)}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function isActionAllowed(actions, action) {
|
|
388
|
+
if (!actions || actions.length === 0) return false;
|
|
389
|
+
return actions.includes("*") || actions.includes(action);
|
|
390
|
+
}
|
|
391
|
+
function roleAllows(matrix, role, resource, action) {
|
|
392
|
+
const rolePermissions = matrix[role];
|
|
393
|
+
if (!rolePermissions) return false;
|
|
394
|
+
const resourceActions = rolePermissions[resource];
|
|
395
|
+
const wildcardResourceActions = rolePermissions["*"];
|
|
396
|
+
return isActionAllowed(resourceActions, action) || isActionAllowed(wildcardResourceActions, action);
|
|
397
|
+
}
|
|
398
|
+
function buildDefaultCacheKey(ctx, orgId, orgRoles) {
|
|
399
|
+
const userId = String(ctx.user?.id ?? ctx.user?._id ?? "anon");
|
|
400
|
+
const roles = (orgRoles ?? []).slice().sort().join(",");
|
|
401
|
+
return `${orgId ?? "no-org"}::${roles}::${userId}`;
|
|
402
|
+
}
|
|
403
|
+
async function resolveMatrix(ctx, orgId, orgRoles) {
|
|
404
|
+
if (!cacheStore) return config.resolveRolePermissions(ctx);
|
|
405
|
+
const cacheKey = config.cache?.key?.(ctx) ?? buildDefaultCacheKey(ctx, orgId, orgRoles);
|
|
406
|
+
if (!cacheKey) return config.resolveRolePermissions(ctx);
|
|
407
|
+
try {
|
|
408
|
+
const hit = await cacheStore.get(cacheKey);
|
|
409
|
+
if (hit) return hit;
|
|
410
|
+
} catch (error) {
|
|
411
|
+
logger.warn(`[DynamicPermissionMatrix] Cache get failed for '${cacheKey}': ${error instanceof Error ? error.message : String(error)}`);
|
|
412
|
+
}
|
|
413
|
+
const value = await config.resolveRolePermissions(ctx);
|
|
414
|
+
try {
|
|
415
|
+
await cacheStore.set(cacheKey, value, { ttlMs: cacheTtlMs });
|
|
416
|
+
trackedKeys.add(cacheKey);
|
|
417
|
+
const maxTracked = config.cache?.maxEntries ?? 1e4;
|
|
418
|
+
if (trackedKeys.size > maxTracked) {
|
|
419
|
+
const overflow = trackedKeys.size - maxTracked;
|
|
420
|
+
const iter = trackedKeys.values();
|
|
421
|
+
for (let i = 0; i < overflow; i++) {
|
|
422
|
+
const oldest = iter.next().value;
|
|
423
|
+
if (oldest) trackedKeys.delete(oldest);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
} catch (error) {
|
|
427
|
+
logger.warn(`[DynamicPermissionMatrix] Cache set failed for '${cacheKey}': ${error instanceof Error ? error.message : String(error)}`);
|
|
428
|
+
}
|
|
429
|
+
return value;
|
|
430
|
+
}
|
|
431
|
+
function can(required) {
|
|
432
|
+
return async (ctx) => {
|
|
433
|
+
if (!ctx.user) return {
|
|
434
|
+
granted: false,
|
|
435
|
+
reason: "Authentication required"
|
|
436
|
+
};
|
|
437
|
+
const scope = getScope(ctx.request);
|
|
438
|
+
if (isElevated(scope)) return true;
|
|
439
|
+
if (!isMember(scope)) return {
|
|
440
|
+
granted: false,
|
|
441
|
+
reason: "Organization membership required"
|
|
442
|
+
};
|
|
443
|
+
const orgRoles = scope.orgRoles;
|
|
444
|
+
if (orgRoles.length === 0) return {
|
|
445
|
+
granted: false,
|
|
446
|
+
reason: "Not a member of this organization"
|
|
447
|
+
};
|
|
448
|
+
let matrix;
|
|
449
|
+
try {
|
|
450
|
+
matrix = await resolveMatrix(ctx, scope.organizationId, orgRoles);
|
|
451
|
+
} catch (error) {
|
|
452
|
+
return {
|
|
453
|
+
granted: false,
|
|
454
|
+
reason: `Permission matrix resolution failed: ${error instanceof Error ? error.message : String(error)}`
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
for (const [resource, actions] of Object.entries(required)) for (const action of actions) if (!orgRoles.some((role) => roleAllows(matrix, role, resource, action))) return {
|
|
458
|
+
granted: false,
|
|
459
|
+
reason: `Missing permission: ${resource}:${action}`
|
|
460
|
+
};
|
|
461
|
+
return true;
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
can,
|
|
466
|
+
canAction(resource, action) {
|
|
467
|
+
return can({ [resource]: [action] });
|
|
468
|
+
},
|
|
469
|
+
requireRole(...roles) {
|
|
470
|
+
return requireOrgRole(roles);
|
|
471
|
+
},
|
|
472
|
+
requireMembership() {
|
|
473
|
+
return requireOrgMembership();
|
|
474
|
+
},
|
|
475
|
+
requireTeamMembership() {
|
|
476
|
+
return requireTeamMembership();
|
|
477
|
+
},
|
|
478
|
+
async invalidateByOrg(orgId) {
|
|
479
|
+
await localInvalidateByOrg(orgId);
|
|
480
|
+
if (eventBridge) try {
|
|
481
|
+
await eventBridge.publish(eventBridge.eventType, {
|
|
482
|
+
orgId,
|
|
483
|
+
nodeId
|
|
484
|
+
});
|
|
485
|
+
} catch (error) {
|
|
486
|
+
logger.warn(`[DynamicPermissionMatrix] Failed to publish invalidation event for org '${orgId}': ${error instanceof Error ? error.message : String(error)}`);
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
async clearCache() {
|
|
490
|
+
if (!cacheStore) return;
|
|
491
|
+
if (cacheStore.clear) try {
|
|
492
|
+
await cacheStore.clear();
|
|
493
|
+
trackedKeys.clear();
|
|
494
|
+
return;
|
|
495
|
+
} catch (error) {
|
|
496
|
+
logger.warn(`[DynamicPermissionMatrix] cacheStore.clear failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
497
|
+
}
|
|
498
|
+
for (const key of trackedKeys) try {
|
|
499
|
+
await cacheStore.delete(key);
|
|
500
|
+
} catch (error) {
|
|
501
|
+
logger.warn(`[DynamicPermissionMatrix] Cache delete failed for '${key}': ${error instanceof Error ? error.message : String(error)}`);
|
|
502
|
+
}
|
|
503
|
+
trackedKeys.clear();
|
|
504
|
+
},
|
|
505
|
+
async connectEvents(events, options) {
|
|
506
|
+
if (eventBridge) await this.disconnectEvents();
|
|
507
|
+
const eventType = options?.eventType ?? DEFAULT_EVENT_TYPE;
|
|
508
|
+
const unsubscribeFn = await events.subscribe(eventType, async (event) => {
|
|
509
|
+
const payload = event.payload;
|
|
510
|
+
if (!payload?.orgId) return;
|
|
511
|
+
if (payload.nodeId === nodeId) return;
|
|
512
|
+
await localInvalidateByOrg(payload.orgId);
|
|
513
|
+
if (options?.onRemoteInvalidation) try {
|
|
514
|
+
await options.onRemoteInvalidation(payload.orgId);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
logger.warn(`[DynamicPermissionMatrix] onRemoteInvalidation callback failed for org '${payload.orgId}': ${error instanceof Error ? error.message : String(error)}`);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
eventBridge = {
|
|
520
|
+
publish: events.publish,
|
|
521
|
+
unsubscribe: typeof unsubscribeFn === "function" ? unsubscribeFn : null,
|
|
522
|
+
eventType,
|
|
523
|
+
onRemoteInvalidation: options?.onRemoteInvalidation
|
|
524
|
+
};
|
|
525
|
+
},
|
|
526
|
+
async disconnectEvents() {
|
|
527
|
+
if (!eventBridge) return;
|
|
528
|
+
try {
|
|
529
|
+
eventBridge.unsubscribe?.();
|
|
530
|
+
} catch (error) {
|
|
531
|
+
logger.warn(`[DynamicPermissionMatrix] disconnectEvents unsubscribe failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
532
|
+
}
|
|
533
|
+
eventBridge = null;
|
|
534
|
+
},
|
|
535
|
+
get eventsConnected() {
|
|
536
|
+
return eventBridge !== null;
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Require membership in the active team.
|
|
542
|
+
* User must be authenticated, a member of the active org, AND have an active team.
|
|
543
|
+
*
|
|
544
|
+
* Better Auth teams are flat member groups (no team-level roles).
|
|
545
|
+
* Reads `request.scope.teamId` set by the Better Auth adapter.
|
|
546
|
+
*
|
|
547
|
+
* @example
|
|
548
|
+
* ```typescript
|
|
549
|
+
* permissions: {
|
|
550
|
+
* list: requireTeamMembership(),
|
|
551
|
+
* create: requireTeamMembership(),
|
|
552
|
+
* }
|
|
553
|
+
* ```
|
|
554
|
+
*/
|
|
555
|
+
function requireTeamMembership() {
|
|
556
|
+
const check = (ctx) => {
|
|
557
|
+
if (!ctx.user) return {
|
|
558
|
+
granted: false,
|
|
559
|
+
reason: "Authentication required"
|
|
560
|
+
};
|
|
561
|
+
const scope = getScope(ctx.request);
|
|
562
|
+
if (isElevated(scope)) return true;
|
|
563
|
+
if (!isMember(scope)) return {
|
|
564
|
+
granted: false,
|
|
565
|
+
reason: "Organization membership required"
|
|
566
|
+
};
|
|
567
|
+
if (!getTeamId(scope)) return {
|
|
568
|
+
granted: false,
|
|
569
|
+
reason: "No active team"
|
|
570
|
+
};
|
|
571
|
+
return true;
|
|
572
|
+
};
|
|
573
|
+
check._teamPermission = "membership";
|
|
574
|
+
return check;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
//#endregion
|
|
578
|
+
export { adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, authenticated, createDynamicPermissionMatrix, createOrgPermissions, denyAll, fields, fullPublic, ownerWithAdminBypass, presets_exports as permissions, publicRead, publicReadAdminWrite, readOnly, requireAuth, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireTeamMembership, resolveEffectiveRoles, when };
|
|
579
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/permissions/index.ts"],"sourcesContent":["/**\r\n * Permission System\r\n *\r\n * Clean, function-based permission system.\r\n * PermissionCheck is THE ONLY way to define permissions.\r\n *\r\n * @example\r\n * ```typescript\r\n * import { allowPublic, requireAuth, requireRoles } from '@classytic/arc/permissions';\r\n *\r\n * defineResource({\r\n * permissions: {\r\n * list: allowPublic(),\r\n * get: allowPublic(),\r\n * create: requireAuth(),\r\n * update: requireRoles(['admin', 'editor']),\r\n * delete: requireRoles(['admin']),\r\n * }\r\n * });\r\n * ```\r\n */\r\n\r\n// Re-export types\r\nexport type {\r\n PermissionCheck,\r\n PermissionContext,\r\n PermissionResult,\r\n UserBase,\r\n} from \"./types.js\";\r\nimport { randomUUID } from \"node:crypto\";\r\nimport { MemoryCacheStore } from \"../cache/memory.js\";\r\nimport type { CacheLogger, CacheStore } from \"../cache/interface.js\";\r\n\r\nexport interface DynamicPermissionMatrixConfig {\r\n /**\r\n * Resolve role → resource → actions map dynamically (DB/API/config service).\r\n * Called at permission-check time (or cache miss if cache enabled).\r\n */\r\n resolveRolePermissions: (\r\n ctx: PermissionContext,\r\n ) =>\r\n | Record<string, Record<string, readonly string[]>>\r\n | Promise<Record<string, Record<string, readonly string[]>>>;\r\n /**\r\n * Optional cache store adapter.\r\n * Use MemoryCacheStore for single-instance apps or RedisCacheStore for distributed setups.\r\n */\r\n cacheStore?: CacheStore<Record<string, Record<string, readonly string[]>>>;\r\n /** Optional logger for cache/runtime failures (default: console) */\r\n logger?: CacheLogger;\r\n /**\r\n * Legacy convenience in-memory cache config.\r\n * If `cacheStore` is not provided and ttlMs > 0, Arc creates an internal MemoryCacheStore.\r\n */\r\n cache?: {\r\n /** Cache TTL in milliseconds */\r\n ttlMs: number;\r\n /** Optional custom cache key builder */\r\n key?: (ctx: PermissionContext) => string | null | undefined;\r\n /** Hard entry cap for internal memory store (default: 1000) */\r\n maxEntries?: number;\r\n };\r\n}\r\n\r\n/** Minimal publish/subscribe interface for cross-node cache invalidation. */\r\nexport interface PermissionEventBus {\r\n publish: <T>(type: string, payload: T) => Promise<void>;\r\n subscribe: (\r\n pattern: string,\r\n handler: (event: { payload: unknown }) => void | Promise<void>,\r\n ) => Promise<(() => void) | void>;\r\n}\r\n\r\nexport interface ConnectEventsOptions {\r\n /** Called on remote invalidation for app-specific cleanup (e.g., resolver cache) */\r\n onRemoteInvalidation?: (orgId: string) => void | Promise<void>;\r\n /** Custom event type (default: 'arc.permissions.invalidated') */\r\n eventType?: string;\r\n}\r\n\r\nexport interface DynamicPermissionMatrix {\r\n can: (permissions: Record<string, readonly string[]>) => PermissionCheck;\r\n canAction: (resource: string, action: string) => PermissionCheck;\r\n requireRole: (...roles: string[]) => PermissionCheck;\r\n requireMembership: () => PermissionCheck;\r\n requireTeamMembership: () => PermissionCheck;\r\n /** Invalidate cached permissions for a specific organization */\r\n invalidateByOrg: (orgId: string) => Promise<void>;\r\n clearCache: () => Promise<void>;\r\n\r\n /**\r\n * Connect to an event system for cross-node cache invalidation.\r\n *\r\n * Late-binding: call after the event plugin is registered (e.g., in onReady hook).\r\n * Once connected, `invalidateByOrg()` auto-publishes an event, and incoming\r\n * events from other nodes trigger local cache invalidation.\r\n * Echo is suppressed via per-process nodeId matching.\r\n */\r\n connectEvents(\r\n events: PermissionEventBus,\r\n options?: ConnectEventsOptions,\r\n ): Promise<void>;\r\n\r\n /** Disconnect from the event system. Safe to call even if never connected. */\r\n disconnectEvents(): Promise<void>;\r\n\r\n /** Whether events are currently connected. */\r\n readonly eventsConnected: boolean;\r\n}\r\n\r\n// Permission presets — common patterns in one call\r\nimport * as presets from \"./presets.js\";\r\nexport { presets as permissions };\r\nexport {\r\n publicRead,\r\n publicReadAdminWrite,\r\n authenticated,\r\n adminOnly,\r\n ownerWithAdminBypass,\r\n fullPublic,\r\n readOnly,\r\n} from \"./presets.js\";\r\n\r\n// Field-level permissions\r\nexport {\r\n fields,\r\n applyFieldReadPermissions,\r\n applyFieldWritePermissions,\r\n resolveEffectiveRoles,\r\n} from \"./fields.js\";\r\nexport type {\r\n FieldPermission,\r\n FieldPermissionMap,\r\n FieldPermissionType,\r\n} from \"./fields.js\";\r\n\r\nimport type {\r\n PermissionCheck,\r\n PermissionContext,\r\n PermissionResult,\r\n} from \"./types.js\";\r\nimport type { FastifyRequest } from \"fastify\";\r\nimport type { RequestScope } from \"../scope/types.js\";\r\nimport {\r\n isMember,\r\n isElevated,\r\n getOrgId,\r\n getOrgRoles,\r\n getTeamId,\r\n PUBLIC_SCOPE,\r\n} from \"../scope/types.js\";\r\n\r\n// ============================================================================\r\n// Permission Helpers\r\n// ============================================================================\r\n\r\n/**\r\n * Allow public access (no authentication required)\r\n *\r\n * @example\r\n * ```typescript\r\n * permissions: {\r\n * list: allowPublic(),\r\n * get: allowPublic(),\r\n * }\r\n * ```\r\n */\r\nexport function allowPublic(): PermissionCheck {\r\n const check: PermissionCheck = () => true;\r\n // Mark as public for OpenAPI documentation and introspection\r\n check._isPublic = true;\r\n return check;\r\n}\r\n\r\n/**\r\n * Require authentication (any authenticated user)\r\n *\r\n * @example\r\n * ```typescript\r\n * permissions: {\r\n * create: requireAuth(),\r\n * update: requireAuth(),\r\n * }\r\n * ```\r\n */\r\nexport function requireAuth(): PermissionCheck {\r\n const check: PermissionCheck = (ctx) => {\r\n if (!ctx.user) {\r\n return { granted: false, reason: \"Authentication required\" };\r\n }\r\n return true;\r\n };\r\n return check;\r\n}\r\n\r\n/**\r\n * Require specific roles\r\n *\r\n * @param roles - Required roles (user needs at least one)\r\n * @param options - Optional bypass roles\r\n *\r\n * @example\r\n * ```typescript\r\n * permissions: {\r\n * create: requireRoles(['admin', 'editor']),\r\n * delete: requireRoles(['admin']),\r\n * }\r\n *\r\n * // With bypass roles\r\n * permissions: {\r\n * update: requireRoles(['owner'], { bypassRoles: ['admin', 'superadmin'] }),\r\n * }\r\n * ```\r\n */\r\nexport function requireRoles(\r\n roles: readonly string[],\r\n options?: { bypassRoles?: readonly string[] },\r\n): PermissionCheck {\r\n const check: PermissionCheck = (ctx) => {\r\n if (!ctx.user) {\r\n return { granted: false, reason: \"Authentication required\" };\r\n }\r\n\r\n const userRoles = (ctx.user.roles ?? []) as string[];\r\n\r\n // Check bypass roles first\r\n if (options?.bypassRoles?.some((r) => userRoles.includes(r))) {\r\n return true;\r\n }\r\n\r\n // Check required roles (any match)\r\n if (roles.some((r) => userRoles.includes(r))) {\r\n return true;\r\n }\r\n\r\n return {\r\n granted: false,\r\n reason: `Required roles: ${roles.join(\", \")}`,\r\n };\r\n };\r\n check._roles = roles;\r\n return check;\r\n}\r\n\r\n/**\r\n * Require resource ownership\r\n *\r\n * Returns filters to scope queries to user's owned resources.\r\n *\r\n * @param ownerField - Field containing owner ID (default: 'userId')\r\n * @param options - Optional bypass roles\r\n *\r\n * @example\r\n * ```typescript\r\n * permissions: {\r\n * update: requireOwnership('userId'),\r\n * delete: requireOwnership('createdBy', { bypassRoles: ['admin'] }),\r\n * }\r\n * ```\r\n */\r\nexport function requireOwnership<TDoc = any>(\r\n ownerField: Extract<keyof TDoc, string> | string = \"userId\",\r\n options?: { bypassRoles?: readonly string[] },\r\n): PermissionCheck<TDoc> {\r\n return (ctx) => {\r\n if (!ctx.user) {\r\n return { granted: false, reason: \"Authentication required\" };\r\n }\r\n\r\n const userRoles = (ctx.user.roles ?? []) as string[];\r\n\r\n // Check bypass roles\r\n if (options?.bypassRoles?.some((r) => userRoles.includes(r))) {\r\n return true;\r\n }\r\n\r\n // Return filters to scope to owned resources\r\n const userId = ctx.user.id ?? ctx.user._id;\r\n if (!userId) {\r\n return { granted: false, reason: \"User identity missing (no id or _id)\" };\r\n }\r\n return {\r\n granted: true,\r\n filters: { [ownerField]: userId },\r\n };\r\n };\r\n}\r\n\r\n/**\r\n * Combine multiple checks - ALL must pass (AND logic)\r\n *\r\n * @example\r\n * ```typescript\r\n * permissions: {\r\n * update: allOf(\r\n * requireAuth(),\r\n * requireRoles(['editor']),\r\n * requireOwnership('createdBy')\r\n * ),\r\n * }\r\n * ```\r\n */\r\nexport function allOf(...checks: PermissionCheck[]): PermissionCheck {\r\n return async (ctx) => {\r\n let mergedFilters: Record<string, unknown> = {};\r\n\r\n for (const check of checks) {\r\n const result = await check(ctx);\r\n const normalized: PermissionResult =\r\n typeof result === \"boolean\" ? { granted: result } : result;\r\n\r\n if (!normalized.granted) {\r\n return normalized;\r\n }\r\n\r\n // Merge filters\r\n if (normalized.filters) {\r\n mergedFilters = { ...mergedFilters, ...normalized.filters };\r\n }\r\n }\r\n\r\n return {\r\n granted: true,\r\n filters:\r\n Object.keys(mergedFilters).length > 0 ? mergedFilters : undefined,\r\n };\r\n };\r\n}\r\n\r\n/**\r\n * Combine multiple checks - ANY must pass (OR logic)\r\n *\r\n * @example\r\n * ```typescript\r\n * permissions: {\r\n * update: anyOf(\r\n * requireRoles(['admin']),\r\n * requireOwnership('createdBy')\r\n * ),\r\n * }\r\n * ```\r\n */\r\nexport function anyOf(...checks: PermissionCheck[]): PermissionCheck {\r\n return async (ctx) => {\r\n const reasons: string[] = [];\r\n\r\n for (const check of checks) {\r\n const result = await check(ctx);\r\n const normalized: PermissionResult =\r\n typeof result === \"boolean\" ? { granted: result } : result;\r\n\r\n if (normalized.granted) {\r\n return normalized;\r\n }\r\n\r\n if (normalized.reason) {\r\n reasons.push(normalized.reason);\r\n }\r\n }\r\n\r\n return {\r\n granted: false,\r\n reason: reasons.join(\"; \"),\r\n };\r\n };\r\n}\r\n\r\n/**\r\n * Deny all access\r\n *\r\n * @example\r\n * ```typescript\r\n * permissions: {\r\n * delete: denyAll('Deletion not allowed'),\r\n * }\r\n * ```\r\n */\r\nexport function denyAll(reason = \"Access denied\"): PermissionCheck {\r\n return () => ({ granted: false, reason });\r\n}\r\n\r\n/**\r\n * Dynamic permission based on context\r\n *\r\n * @example\r\n * ```typescript\r\n * permissions: {\r\n * update: when((ctx) => ctx.data?.status === 'draft'),\r\n * }\r\n * ```\r\n */\r\nexport function when<TDoc = any>(\r\n condition: (ctx: PermissionContext<TDoc>) => boolean | Promise<boolean>,\r\n): PermissionCheck<TDoc> {\r\n return async (ctx) => {\r\n const result = await condition(ctx);\r\n return {\r\n granted: result,\r\n reason: result ? undefined : \"Condition not met\",\r\n };\r\n };\r\n}\r\n\r\n// ============================================================================\r\n// Organization Permission Helpers\r\n// ============================================================================\r\n\r\n/** Read request.scope safely */\r\nfunction getScope(request: FastifyRequest): RequestScope {\r\n return request.scope ?? PUBLIC_SCOPE;\r\n}\r\n\r\n/**\r\n * Require membership in the active organization.\r\n * User must be authenticated AND have an active org (member or elevated scope).\r\n *\r\n * Reads `request.scope` set by auth adapters.\r\n *\r\n * @example\r\n * ```typescript\r\n * permissions: {\r\n * list: requireOrgMembership(),\r\n * get: requireOrgMembership(),\r\n * }\r\n * ```\r\n */\r\nexport function requireOrgMembership<TDoc = any>(): PermissionCheck<TDoc> {\r\n const check: PermissionCheck<TDoc> = (ctx) => {\r\n if (!ctx.user) {\r\n return { granted: false, reason: \"Authentication required\" };\r\n }\r\n\r\n const scope = getScope(ctx.request);\r\n if (isElevated(scope)) return true;\r\n if (isMember(scope)) return true;\r\n\r\n return { granted: false, reason: \"Organization membership required\" };\r\n };\r\n check._orgPermission = \"membership\";\r\n return check;\r\n}\r\n\r\n/**\r\n * Require specific org-level roles.\r\n * Reads `request.scope.orgRoles` (set by auth adapters).\r\n * Elevated scope always passes (platform admin bypass).\r\n *\r\n * @param roles - Required org roles (user needs at least one)\r\n *\r\n * @example\r\n * ```typescript\r\n * permissions: {\r\n * create: requireOrgRole('admin', 'owner'),\r\n * delete: requireOrgRole('owner'),\r\n * }\r\n * ```\r\n */\r\nexport function requireOrgRole<TDoc = any>(\r\n ...args: string[] | [readonly string[]]\r\n): PermissionCheck<TDoc> {\r\n // Support both: requireOrgRole('admin', 'owner') and requireOrgRole(['admin', 'owner'])\r\n const roles: readonly string[] = Array.isArray(args[0])\r\n ? args[0]\r\n : (args as string[]);\r\n\r\n const check: PermissionCheck<TDoc> = (ctx) => {\r\n if (!ctx.user) {\r\n return { granted: false, reason: \"Authentication required\" };\r\n }\r\n\r\n const scope = getScope(ctx.request);\r\n if (isElevated(scope)) return true;\r\n\r\n if (!isMember(scope)) {\r\n return { granted: false, reason: \"Organization membership required\" };\r\n }\r\n\r\n if (roles.some((r) => scope.orgRoles.includes(r))) {\r\n return true;\r\n }\r\n\r\n return {\r\n granted: false,\r\n reason: `Required org roles: ${roles.join(\", \")}`,\r\n };\r\n };\r\n check._orgRoles = roles;\r\n return check;\r\n}\r\n\r\n/**\r\n * Create a scoped permission system for resource-action patterns.\r\n * Maps org roles to fine-grained permissions without external API calls.\r\n *\r\n * @example\r\n * ```typescript\r\n * const perms = createOrgPermissions({\r\n * statements: {\r\n * product: ['create', 'update', 'delete'],\r\n * order: ['create', 'approve'],\r\n * },\r\n * roles: {\r\n * owner: { product: ['create', 'update', 'delete'], order: ['create', 'approve'] },\r\n * admin: { product: ['create', 'update'], order: ['create'] },\r\n * member: { product: [], order: [] },\r\n * },\r\n * });\r\n *\r\n * defineResource({\r\n * permissions: {\r\n * create: perms.can({ product: ['create'] }),\r\n * delete: perms.can({ product: ['delete'] }),\r\n * }\r\n * });\r\n * ```\r\n */\r\nexport function createOrgPermissions(config: {\r\n statements: Record<string, readonly string[]>;\r\n roles: Record<string, Record<string, readonly string[]>>;\r\n}): {\r\n can: (permissions: Record<string, string[]>) => PermissionCheck;\r\n requireRole: (...roles: string[]) => PermissionCheck;\r\n requireMembership: () => PermissionCheck;\r\n requireTeamMembership: () => PermissionCheck;\r\n} {\r\n const { roles: roleMap } = config;\r\n\r\n function hasPermissions(\r\n orgRoles: string[],\r\n required: Record<string, string[]>,\r\n ): boolean {\r\n // User's effective permissions = union of all their role permissions\r\n for (const [resource, actions] of Object.entries(required)) {\r\n for (const action of actions) {\r\n const granted = orgRoles.some((role) => {\r\n const perms = roleMap[role]?.[resource];\r\n return perms?.includes(action);\r\n });\r\n if (!granted) return false;\r\n }\r\n }\r\n return true;\r\n }\r\n\r\n return {\r\n can(permissions: Record<string, string[]>): PermissionCheck {\r\n return (ctx) => {\r\n if (!ctx.user) {\r\n return { granted: false, reason: \"Authentication required\" };\r\n }\r\n\r\n const scope = getScope(ctx.request);\r\n if (isElevated(scope)) return true;\r\n\r\n if (!isMember(scope)) {\r\n return { granted: false, reason: \"Organization membership required\" };\r\n }\r\n\r\n if (hasPermissions(scope.orgRoles, permissions)) {\r\n return true;\r\n }\r\n\r\n const needed = Object.entries(permissions)\r\n .map(([r, a]) => `${r}:[${a.join(\",\")}]`)\r\n .join(\", \");\r\n return {\r\n granted: false,\r\n reason: `Missing permissions: ${needed}`,\r\n };\r\n };\r\n },\r\n\r\n requireRole(...roles: string[]): PermissionCheck {\r\n return requireOrgRole(roles);\r\n },\r\n\r\n requireMembership(): PermissionCheck {\r\n return requireOrgMembership();\r\n },\r\n\r\n requireTeamMembership(): PermissionCheck {\r\n return requireTeamMembership();\r\n },\r\n };\r\n}\r\n\r\n/**\r\n * Create a dynamic role-based permission matrix.\r\n *\r\n * Use this when role/action mappings are managed outside code\r\n * (e.g., admin UI matrix, DB-stored ACLs, remote policy service).\r\n *\r\n * Supports:\r\n * - org role union (any assigned org role can grant)\r\n * - global bypass roles\r\n * - wildcard resource/action (`*`)\r\n * - optional in-memory cache\r\n */\r\nexport function createDynamicPermissionMatrix(\r\n config: DynamicPermissionMatrixConfig,\r\n): DynamicPermissionMatrix {\r\n const logger = config.logger ?? console;\r\n const legacyTtlMs = config.cache?.ttlMs ?? 0;\r\n const hasExternalStore = !!config.cacheStore;\r\n const cacheTtlMs =\r\n legacyTtlMs > 0 ? legacyTtlMs : hasExternalStore ? 300_000 : 0;\r\n\r\n const internalStore =\r\n !config.cacheStore && cacheTtlMs > 0\r\n ? new MemoryCacheStore<Record<string, Record<string, readonly string[]>>>(\r\n {\r\n defaultTtlMs: cacheTtlMs,\r\n maxEntries: config.cache?.maxEntries ?? 1000,\r\n },\r\n )\r\n : undefined;\r\n\r\n const cacheStore = config.cacheStore ?? internalStore;\r\n const trackedKeys = new Set<string>();\r\n\r\n // ── Cross-node event bridge (late-binding) ───────────────────────\r\n const nodeId = randomUUID().slice(0, 8);\r\n const DEFAULT_EVENT_TYPE = \"arc.permissions.invalidated\";\r\n\r\n interface InternalEventBridge {\r\n publish: <T>(type: string, payload: T) => Promise<void>;\r\n unsubscribe: (() => void) | null;\r\n eventType: string;\r\n onRemoteInvalidation?: (orgId: string) => void | Promise<void>;\r\n }\r\n\r\n let eventBridge: InternalEventBridge | null = null;\r\n\r\n /** Clear local cache for an org without publishing events (avoids infinite loops). */\r\n async function localInvalidateByOrg(orgId: string): Promise<void> {\r\n if (!cacheStore) return;\r\n const prefix = `${orgId}::`;\r\n const toDelete: string[] = [];\r\n for (const key of trackedKeys) {\r\n if (key.startsWith(prefix)) toDelete.push(key);\r\n }\r\n for (const key of toDelete) {\r\n try {\r\n await cacheStore.delete(key);\r\n trackedKeys.delete(key);\r\n } catch (error) {\r\n logger.warn(\r\n `[DynamicPermissionMatrix] invalidateByOrg delete failed for '${key}': ${error instanceof Error ? error.message : String(error)}`,\r\n );\r\n }\r\n }\r\n }\r\n\r\n function isActionAllowed(\r\n actions: readonly string[] | undefined,\r\n action: string,\r\n ): boolean {\r\n if (!actions || actions.length === 0) return false;\r\n return actions.includes(\"*\") || actions.includes(action);\r\n }\r\n\r\n function roleAllows(\r\n matrix: Record<string, Record<string, readonly string[]>>,\r\n role: string,\r\n resource: string,\r\n action: string,\r\n ): boolean {\r\n const rolePermissions = matrix[role];\r\n if (!rolePermissions) return false;\r\n const resourceActions = rolePermissions[resource];\r\n const wildcardResourceActions = rolePermissions[\"*\"];\r\n return (\r\n isActionAllowed(resourceActions, action) ||\r\n isActionAllowed(wildcardResourceActions, action)\r\n );\r\n }\r\n\r\n function buildDefaultCacheKey(\r\n ctx: PermissionContext,\r\n orgId?: string,\r\n orgRoles?: string[],\r\n ): string {\r\n const userId = String(ctx.user?.id ?? ctx.user?._id ?? \"anon\");\r\n const roles = (orgRoles ?? []).slice().sort().join(\",\");\r\n return `${orgId ?? \"no-org\"}::${roles}::${userId}`;\r\n }\r\n\r\n async function resolveMatrix(\r\n ctx: PermissionContext,\r\n orgId?: string,\r\n orgRoles?: string[],\r\n ): Promise<Record<string, Record<string, readonly string[]>>> {\r\n if (!cacheStore) {\r\n return config.resolveRolePermissions(ctx);\r\n }\r\n\r\n const customKey = config.cache?.key?.(ctx);\r\n const cacheKey = customKey ?? buildDefaultCacheKey(ctx, orgId, orgRoles);\r\n\r\n if (!cacheKey) {\r\n return config.resolveRolePermissions(ctx);\r\n }\r\n\r\n try {\r\n const hit = await cacheStore.get(cacheKey);\r\n if (hit) return hit;\r\n } catch (error) {\r\n logger.warn(\r\n `[DynamicPermissionMatrix] Cache get failed for '${cacheKey}': ${error instanceof Error ? error.message : String(error)}`,\r\n );\r\n }\r\n\r\n const value = await config.resolveRolePermissions(ctx);\r\n\r\n try {\r\n await cacheStore.set(cacheKey, value, { ttlMs: cacheTtlMs });\r\n trackedKeys.add(cacheKey);\r\n\r\n // Cap tracked keys to prevent unbounded memory growth\r\n const maxTracked = config.cache?.maxEntries ?? 10_000;\r\n if (trackedKeys.size > maxTracked) {\r\n const overflow = trackedKeys.size - maxTracked;\r\n const iter = trackedKeys.values();\r\n for (let i = 0; i < overflow; i++) {\r\n const oldest = iter.next().value;\r\n if (oldest) trackedKeys.delete(oldest);\r\n }\r\n }\r\n } catch (error) {\r\n logger.warn(\r\n `[DynamicPermissionMatrix] Cache set failed for '${cacheKey}': ${error instanceof Error ? error.message : String(error)}`,\r\n );\r\n }\r\n\r\n return value;\r\n }\r\n\r\n function can(required: Record<string, readonly string[]>): PermissionCheck {\r\n return async (ctx) => {\r\n if (!ctx.user) {\r\n return { granted: false, reason: \"Authentication required\" };\r\n }\r\n\r\n const scope = getScope(ctx.request);\r\n if (isElevated(scope)) return true;\r\n\r\n if (!isMember(scope)) {\r\n return { granted: false, reason: \"Organization membership required\" };\r\n }\r\n\r\n const orgRoles = scope.orgRoles;\r\n if (orgRoles.length === 0) {\r\n return { granted: false, reason: \"Not a member of this organization\" };\r\n }\r\n\r\n let matrix: Record<string, Record<string, readonly string[]>>;\r\n try {\r\n matrix = await resolveMatrix(ctx, scope.organizationId, orgRoles);\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : String(error);\r\n return {\r\n granted: false,\r\n reason: `Permission matrix resolution failed: ${message}`,\r\n };\r\n }\r\n\r\n for (const [resource, actions] of Object.entries(required)) {\r\n for (const action of actions) {\r\n const granted = orgRoles.some((role) =>\r\n roleAllows(matrix, role, resource, action),\r\n );\r\n if (!granted) {\r\n return {\r\n granted: false,\r\n reason: `Missing permission: ${resource}:${action}`,\r\n };\r\n }\r\n }\r\n }\r\n\r\n return true;\r\n };\r\n }\r\n\r\n return {\r\n can,\r\n canAction(resource: string, action: string): PermissionCheck {\r\n return can({ [resource]: [action] });\r\n },\r\n requireRole(...roles: string[]): PermissionCheck {\r\n return requireOrgRole(roles);\r\n },\r\n requireMembership(): PermissionCheck {\r\n return requireOrgMembership();\r\n },\r\n requireTeamMembership(): PermissionCheck {\r\n return requireTeamMembership();\r\n },\r\n async invalidateByOrg(orgId: string): Promise<void> {\r\n await localInvalidateByOrg(orgId);\r\n\r\n // Publish cross-node invalidation event (fail-open)\r\n if (eventBridge) {\r\n try {\r\n await eventBridge.publish(eventBridge.eventType, { orgId, nodeId });\r\n } catch (error) {\r\n logger.warn(\r\n `[DynamicPermissionMatrix] Failed to publish invalidation event for org '${orgId}': ${error instanceof Error ? error.message : String(error)}`,\r\n );\r\n }\r\n }\r\n },\r\n async clearCache(): Promise<void> {\r\n if (!cacheStore) return;\r\n\r\n if (cacheStore.clear) {\r\n try {\r\n await cacheStore.clear();\r\n trackedKeys.clear();\r\n return;\r\n } catch (error) {\r\n logger.warn(\r\n `[DynamicPermissionMatrix] cacheStore.clear failed: ${error instanceof Error ? error.message : String(error)}`,\r\n );\r\n }\r\n }\r\n\r\n // Fallback for stores without clear(): delete known keys for this process.\r\n for (const key of trackedKeys) {\r\n try {\r\n await cacheStore.delete(key);\r\n } catch (error) {\r\n logger.warn(\r\n `[DynamicPermissionMatrix] Cache delete failed for '${key}': ${error instanceof Error ? error.message : String(error)}`,\r\n );\r\n }\r\n }\r\n trackedKeys.clear();\r\n },\r\n\r\n async connectEvents(\r\n events: PermissionEventBus,\r\n options?: ConnectEventsOptions,\r\n ): Promise<void> {\r\n // Disconnect previous connection if any (idempotent reconnect)\r\n if (eventBridge) {\r\n await this.disconnectEvents();\r\n }\r\n\r\n const eventType = options?.eventType ?? DEFAULT_EVENT_TYPE;\r\n\r\n const unsubscribeFn = await events.subscribe(eventType, async (event) => {\r\n const payload = event.payload as\r\n | { orgId?: string; nodeId?: string }\r\n | undefined;\r\n if (!payload?.orgId) return;\r\n\r\n // Echo dedup: skip events published by this node\r\n if (payload.nodeId === nodeId) return;\r\n\r\n // Clear local permission matrix cache (no re-publish)\r\n await localInvalidateByOrg(payload.orgId);\r\n\r\n // App-specific cleanup callback\r\n if (options?.onRemoteInvalidation) {\r\n try {\r\n await options.onRemoteInvalidation(payload.orgId);\r\n } catch (error) {\r\n logger.warn(\r\n `[DynamicPermissionMatrix] onRemoteInvalidation callback failed for org '${payload.orgId}': ${error instanceof Error ? error.message : String(error)}`,\r\n );\r\n }\r\n }\r\n });\r\n\r\n eventBridge = {\r\n publish: events.publish,\r\n unsubscribe: typeof unsubscribeFn === \"function\" ? unsubscribeFn : null,\r\n eventType,\r\n onRemoteInvalidation: options?.onRemoteInvalidation,\r\n };\r\n },\r\n\r\n async disconnectEvents(): Promise<void> {\r\n if (!eventBridge) return;\r\n try {\r\n eventBridge.unsubscribe?.();\r\n } catch (error) {\r\n logger.warn(\r\n `[DynamicPermissionMatrix] disconnectEvents unsubscribe failed: ${error instanceof Error ? error.message : String(error)}`,\r\n );\r\n }\r\n eventBridge = null;\r\n },\r\n\r\n get eventsConnected(): boolean {\r\n return eventBridge !== null;\r\n },\r\n };\r\n}\r\n\r\n// ============================================================================\r\n// Team Permission Helpers\r\n// ============================================================================\r\n\r\n/**\r\n * Require membership in the active team.\r\n * User must be authenticated, a member of the active org, AND have an active team.\r\n *\r\n * Better Auth teams are flat member groups (no team-level roles).\r\n * Reads `request.scope.teamId` set by the Better Auth adapter.\r\n *\r\n * @example\r\n * ```typescript\r\n * permissions: {\r\n * list: requireTeamMembership(),\r\n * create: requireTeamMembership(),\r\n * }\r\n * ```\r\n */\r\nexport function requireTeamMembership<TDoc = any>(): PermissionCheck<TDoc> {\r\n const check: PermissionCheck<TDoc> = (ctx) => {\r\n if (!ctx.user) {\r\n return { granted: false, reason: \"Authentication required\" };\r\n }\r\n\r\n const scope = getScope(ctx.request);\r\n if (isElevated(scope)) return true;\r\n\r\n if (!isMember(scope)) {\r\n return { granted: false, reason: \"Organization membership required\" };\r\n }\r\n\r\n const teamId = getTeamId(scope);\r\n if (!teamId) {\r\n return { granted: false, reason: \"No active team\" };\r\n }\r\n\r\n return true;\r\n };\r\n check._teamPermission = \"membership\";\r\n return check;\r\n}\r\n"],"mappings":"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"}
|