@classytic/arc 2.3.0 → 2.4.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 +187 -18
- package/bin/arc.js +11 -3
- package/dist/BaseController-CkM5dUh_.mjs +1031 -0
- package/dist/{EventTransport-BkUDYZEb.d.mts → EventTransport-wc5hSLik.d.mts} +1 -1
- package/dist/{HookSystem-BsGV-j2l.mjs → HookSystem-COkyWztM.mjs} +2 -3
- package/dist/{ResourceRegistry-7Ic20ZMw.mjs → ResourceRegistry-DeCIFlix.mjs} +8 -5
- package/dist/adapters/index.d.mts +3 -5
- package/dist/adapters/index.mjs +2 -3
- package/dist/{prisma-DJbMt3yf.mjs → adapters-DTC4Ug66.mjs} +45 -12
- package/dist/audit/index.d.mts +4 -7
- package/dist/audit/index.mjs +2 -29
- package/dist/audit/mongodb.d.mts +1 -4
- package/dist/audit/mongodb.mjs +2 -3
- package/dist/auth/index.d.mts +7 -9
- package/dist/auth/index.mjs +65 -63
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/auth/redis-session.mjs +1 -2
- package/dist/{betterAuthOpenApi-DjWDddNc.mjs → betterAuthOpenApi-lz0IRbXJ.mjs} +4 -6
- package/dist/cache/index.d.mts +23 -23
- package/dist/cache/index.mjs +4 -6
- package/dist/{caching-GSDJcA6-.mjs → caching-BSXB-Xr7.mjs} +2 -24
- package/dist/chunk-BpYLSNr0.mjs +14 -0
- package/dist/circuitBreaker-BOBOpN2w.mjs +284 -0
- package/dist/circuitBreaker-JP2GdJ4b.d.mts +206 -0
- package/dist/cli/commands/describe.mjs +24 -7
- package/dist/cli/commands/docs.mjs +6 -7
- package/dist/cli/commands/doctor.d.mts +10 -0
- package/dist/cli/commands/doctor.mjs +156 -0
- package/dist/cli/commands/generate.mjs +66 -17
- package/dist/cli/commands/init.mjs +315 -45
- package/dist/cli/commands/introspect.mjs +2 -4
- package/dist/cli/index.d.mts +1 -10
- package/dist/cli/index.mjs +4 -153
- package/dist/{constants-DdXFXQtN.mjs → constants-Cxde4rpC.mjs} +1 -2
- package/dist/core/index.d.mts +3 -5
- package/dist/core/index.mjs +5 -4
- package/dist/core-C1XCMtqM.mjs +185 -0
- package/dist/{createApp-CgKOPhA4.mjs → createApp-ByWNRsZj.mjs} +64 -35
- package/dist/{defineResource-DWbpJYtm.mjs → defineResource-D9aY5Cy6.mjs} +108 -1157
- package/dist/discovery/index.mjs +37 -5
- package/dist/docs/index.d.mts +6 -9
- package/dist/docs/index.mjs +3 -21
- package/dist/dynamic/index.d.mts +93 -0
- package/dist/dynamic/index.mjs +122 -0
- package/dist/{elevation-DSTbVvYj.mjs → elevation-BEdACOLB.mjs} +5 -36
- package/dist/{elevation-DGo5shaX.d.mts → elevation-Ca_yveIO.d.mts} +41 -7
- package/dist/{errorHandler-C3GY3_ow.mjs → errorHandler--zp54tGc.mjs} +3 -5
- package/dist/errorHandler-Do4vVQ1f.d.mts +139 -0
- package/dist/{errors-DBANPbGr.mjs → errors-rxhfP7Hf.mjs} +1 -2
- package/dist/{eventPlugin-BEOvaDqo.mjs → eventPlugin-Ba00swHF.mjs} +25 -27
- package/dist/{eventPlugin-H6wDDjGO.d.mts → eventPlugin-iGrSEmwJ.d.mts} +105 -5
- package/dist/events/index.d.mts +72 -7
- package/dist/events/index.mjs +216 -4
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +19 -7
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/events/transports/redis.mjs +3 -4
- package/dist/factory/index.d.mts +23 -9
- package/dist/factory/index.mjs +48 -3
- package/dist/{fields-Bi_AVKSo.d.mts → fields-DFwdaWCq.d.mts} +1 -1
- package/dist/{fields-CTd_CrKr.mjs → fields-ipsbIRPK.mjs} +1 -2
- package/dist/hooks/index.d.mts +1 -3
- package/dist/hooks/index.mjs +2 -3
- package/dist/idempotency/index.d.mts +5 -5
- package/dist/idempotency/index.mjs +3 -7
- package/dist/idempotency/mongodb.d.mts +1 -1
- package/dist/idempotency/mongodb.mjs +4 -5
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +2 -5
- package/dist/{fastifyAdapter-6b_eRDBw.d.mts → index-BL8CaQih.d.mts} +56 -57
- package/dist/index-Diqcm14c.d.mts +369 -0
- package/dist/{prisma-Dy5S5F5i.d.mts → index-yhxyjqNb.d.mts} +4 -5
- package/dist/index.d.mts +100 -105
- package/dist/index.mjs +85 -58
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +8 -4
- package/dist/integrations/index.d.mts +4 -2
- package/dist/integrations/index.mjs +1 -1
- package/dist/integrations/jobs.d.mts +2 -2
- package/dist/integrations/jobs.mjs +63 -14
- package/dist/integrations/mcp/index.d.mts +219 -0
- package/dist/integrations/mcp/index.mjs +572 -0
- package/dist/integrations/mcp/testing.d.mts +53 -0
- package/dist/integrations/mcp/testing.mjs +104 -0
- package/dist/integrations/streamline.mjs +39 -19
- package/dist/integrations/webhooks.d.mts +56 -0
- package/dist/integrations/webhooks.mjs +139 -0
- package/dist/integrations/websocket-redis.d.mts +46 -0
- package/dist/integrations/websocket-redis.mjs +50 -0
- package/dist/integrations/websocket.d.mts +68 -2
- package/dist/integrations/websocket.mjs +96 -13
- package/dist/{interface-CSNjltAc.d.mts → interface-B4awm1RJ.d.mts} +2 -2
- package/dist/interface-DGmPxakH.d.mts +2213 -0
- package/dist/{keys-DhqDRxv3.mjs → keys-qcD-TVJl.mjs} +3 -4
- package/dist/{logger-ByrvQWZO.mjs → logger-Dz3j1ItV.mjs} +2 -4
- package/dist/{memory-B2v7KrCB.mjs → memory-Cb_7iy9e.mjs} +2 -4
- package/dist/metrics-Csh4nsvv.mjs +224 -0
- package/dist/migrations/index.d.mts +113 -44
- package/dist/migrations/index.mjs +84 -102
- package/dist/{mongodb-DNKEExbf.mjs → mongodb-BuQ7fNTg.mjs} +1 -4
- package/dist/{mongodb-ClykrfGo.d.mts → mongodb-CUpYfxfD.d.mts} +2 -3
- package/dist/{mongodb-Dg8O_gvd.d.mts → mongodb-bga9AbkD.d.mts} +2 -2
- package/dist/{openapi-9nB_kiuR.mjs → openapi-CBmZ6EQN.mjs} +4 -21
- package/dist/org/index.d.mts +12 -14
- package/dist/org/index.mjs +92 -119
- package/dist/org/types.d.mts +2 -2
- package/dist/org/types.mjs +1 -1
- package/dist/permissions/index.d.mts +4 -278
- package/dist/permissions/index.mjs +4 -579
- package/dist/permissions-CA5zg0yK.mjs +751 -0
- package/dist/plugins/index.d.mts +104 -107
- package/dist/plugins/index.mjs +203 -313
- package/dist/plugins/response-cache.mjs +4 -69
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +24 -11
- package/dist/{pluralize-CM-jZg7p.mjs → pluralize-CcT6qF0a.mjs} +12 -13
- package/dist/policies/index.d.mts +2 -2
- package/dist/policies/index.mjs +80 -83
- package/dist/presets/index.d.mts +26 -19
- package/dist/presets/index.mjs +2 -142
- package/dist/presets/multiTenant.d.mts +1 -4
- package/dist/presets/multiTenant.mjs +4 -6
- package/dist/presets-C9QXJV1u.mjs +422 -0
- package/dist/{queryCachePlugin-B6R0d4av.mjs → queryCachePlugin-ClosZdNS.mjs} +6 -27
- package/dist/{queryCachePlugin-Q6SYuHZ6.d.mts → queryCachePlugin-DcmETvcB.d.mts} +3 -3
- package/dist/queryParser-CgCtsjti.mjs +352 -0
- package/dist/{redis-UwjEp8Ea.d.mts → redis-CQ5YxMC5.d.mts} +2 -2
- package/dist/{redis-stream-CBg0upHI.d.mts → redis-stream-BW9UKLZM.d.mts} +9 -2
- package/dist/registry/index.d.mts +1 -4
- package/dist/registry/index.mjs +3 -4
- package/dist/{introspectionPlugin-B3JkrjwU.mjs → registry-I-ogLgL9.mjs} +1 -8
- package/dist/{requestContext-xi6OKBL-.mjs → requestContext-DYtmNpm5.mjs} +1 -3
- package/dist/resourceToTools-PMFE8HIv.mjs +533 -0
- package/dist/rpc/index.d.mts +90 -0
- package/dist/rpc/index.mjs +248 -0
- package/dist/{schemaConverter-Dtg0Kt9T.mjs → schemaConverter-DjzHpFam.mjs} +1 -2
- package/dist/schemas/index.d.mts +30 -30
- package/dist/schemas/index.mjs +2 -4
- package/dist/scope/index.d.mts +13 -2
- package/dist/scope/index.mjs +18 -5
- package/dist/{sessionManager-D_iEHjQl.d.mts → sessionManager-wbkYj2HL.d.mts} +2 -2
- package/dist/{sse-DkqQ1uxb.mjs → sse-BkViJPlT.mjs} +4 -25
- package/dist/testing/index.d.mts +551 -567
- package/dist/testing/index.mjs +1744 -1799
- package/dist/{tracing-8CEbhF0w.d.mts → tracing-bz_U4EM1.d.mts} +6 -1
- package/dist/{typeGuards-DwxA1t_L.mjs → typeGuards-Cj5Rgvlg.mjs} +1 -2
- package/dist/types/index.d.mts +4 -946
- package/dist/types/index.mjs +2 -4
- package/dist/types-BJmgxNbF.d.mts +275 -0
- package/dist/{types-RLkFVgaw.d.mts → types-BNUccdcf.d.mts} +2 -2
- package/dist/{types-Beqn1Un7.mjs → types-C6TQjtdi.mjs} +30 -2
- package/dist/{types-tKwaViYB.d.mts → types-Dt0-AI6E.d.mts} +68 -27
- package/dist/{types-DelU6kln.mjs → types-ZUu_h0jp.mjs} +1 -2
- package/dist/utils/index.d.mts +254 -351
- package/dist/utils/index.mjs +7 -6
- package/dist/utils-Dc0WhlIl.mjs +594 -0
- package/dist/versioning-BzfeHmhj.mjs +37 -0
- package/package.json +44 -10
- package/skills/arc/SKILL.md +518 -0
- package/skills/arc/references/auth.md +250 -0
- package/skills/arc/references/events.md +272 -0
- package/skills/arc/references/integrations.md +385 -0
- package/skills/arc/references/mcp.md +431 -0
- package/skills/arc/references/production.md +610 -0
- package/skills/arc/references/testing.md +183 -0
- package/dist/audited-CGdLiSlE.mjs +0 -140
- package/dist/chunk-C7Uep-_p.mjs +0 -20
- package/dist/circuitBreaker-CSS2VvL6.mjs +0 -1109
- package/dist/errorHandler-CW3OOeYq.d.mts +0 -72
- package/dist/interface-BtdYtQUA.d.mts +0 -1114
- package/dist/presets-BTeYbw7h.d.mts +0 -57
- package/dist/presets-CeFtfDR8.mjs +0 -119
- /package/dist/{errors-DAWRdiYP.d.mts → errors-CPpvPHT0.d.mts} +0 -0
- /package/dist/{externalPaths-SyPF2tgK.d.mts → externalPaths-DpO-s7r8.d.mts} +0 -0
- /package/dist/{interface-DTbsvIWe.d.mts → interface-D_BWALyZ.d.mts} +0 -0
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
|
|
2
|
+
import { a as getTeamId, d as isMember, n as PUBLIC_SCOPE, o as getUserId, u as isElevated } from "./types-C6TQjtdi.mjs";
|
|
3
|
+
import { t as getUserRoles } from "./types-ZUu_h0jp.mjs";
|
|
4
|
+
import { t as MemoryCacheStore } from "./memory-Cb_7iy9e.mjs";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
//#region src/permissions/roleHierarchy.ts
|
|
7
|
+
/**
|
|
8
|
+
* Create a role hierarchy from a parent → children map.
|
|
9
|
+
*
|
|
10
|
+
* Each key is a parent role, each value is the array of roles it inherits.
|
|
11
|
+
* Inheritance is transitive: if A → B and B → C, then A expands to [A, B, C].
|
|
12
|
+
* Circular references are handled safely (visited set).
|
|
13
|
+
*/
|
|
14
|
+
function createRoleHierarchy(map) {
|
|
15
|
+
const cache = /* @__PURE__ */ new Map();
|
|
16
|
+
function resolveRole(role, visited) {
|
|
17
|
+
if (visited.has(role)) return [];
|
|
18
|
+
visited.add(role);
|
|
19
|
+
const cached = cache.get(role);
|
|
20
|
+
if (cached) return cached;
|
|
21
|
+
const children = map[role];
|
|
22
|
+
if (!children || children.length === 0) {
|
|
23
|
+
cache.set(role, [role]);
|
|
24
|
+
return [role];
|
|
25
|
+
}
|
|
26
|
+
const result = [role];
|
|
27
|
+
for (const child of children) result.push(...resolveRole(child, visited));
|
|
28
|
+
const deduped = [...new Set(result)];
|
|
29
|
+
cache.set(role, deduped);
|
|
30
|
+
return deduped;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
expand(roles) {
|
|
34
|
+
if (roles.length === 0) return [];
|
|
35
|
+
const all = /* @__PURE__ */ new Set();
|
|
36
|
+
for (const role of roles) for (const expanded of resolveRole(role, /* @__PURE__ */ new Set())) all.add(expanded);
|
|
37
|
+
return [...all];
|
|
38
|
+
},
|
|
39
|
+
includes(userRoles, requiredRole) {
|
|
40
|
+
if (userRoles.length === 0) return false;
|
|
41
|
+
return this.expand(userRoles).includes(requiredRole);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/permissions/presets.ts
|
|
47
|
+
/**
|
|
48
|
+
* Permission Presets — Common permission patterns in one call.
|
|
49
|
+
*
|
|
50
|
+
* Reduces 5 lines of permission declarations to 1.
|
|
51
|
+
* Each preset returns a ResourcePermissions object that can be
|
|
52
|
+
* spread or overridden per-operation.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* import { permissions } from '@classytic/arc';
|
|
57
|
+
*
|
|
58
|
+
* // Public read, authenticated write
|
|
59
|
+
* defineResource({ name: 'product', permissions: permissions.publicRead() });
|
|
60
|
+
*
|
|
61
|
+
* // Override specific operations
|
|
62
|
+
* defineResource({
|
|
63
|
+
* name: 'product',
|
|
64
|
+
* permissions: permissions.publicRead({ delete: requireRoles(['superadmin']) }),
|
|
65
|
+
* });
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
var presets_exports = /* @__PURE__ */ __exportAll({
|
|
69
|
+
adminOnly: () => adminOnly,
|
|
70
|
+
authenticated: () => authenticated,
|
|
71
|
+
fullPublic: () => fullPublic,
|
|
72
|
+
ownerWithAdminBypass: () => ownerWithAdminBypass,
|
|
73
|
+
publicRead: () => publicRead,
|
|
74
|
+
publicReadAdminWrite: () => publicReadAdminWrite,
|
|
75
|
+
readOnly: () => readOnly
|
|
76
|
+
});
|
|
77
|
+
/**
|
|
78
|
+
* Merge a base preset with user overrides.
|
|
79
|
+
* Overrides replace individual operations — undefined values don't clear them.
|
|
80
|
+
*/
|
|
81
|
+
function withOverrides(base, overrides) {
|
|
82
|
+
if (!overrides) return base;
|
|
83
|
+
const filtered = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== void 0));
|
|
84
|
+
return {
|
|
85
|
+
...base,
|
|
86
|
+
...filtered
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Public read, authenticated write.
|
|
91
|
+
* list + get = allowPublic(), create + update + delete = requireAuth()
|
|
92
|
+
*/
|
|
93
|
+
function publicRead(overrides) {
|
|
94
|
+
return withOverrides({
|
|
95
|
+
list: allowPublic(),
|
|
96
|
+
get: allowPublic(),
|
|
97
|
+
create: requireAuth(),
|
|
98
|
+
update: requireAuth(),
|
|
99
|
+
delete: requireAuth()
|
|
100
|
+
}, overrides);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Public read, admin write.
|
|
104
|
+
* list + get = allowPublic(), create + update + delete = requireRoles(['admin'])
|
|
105
|
+
*/
|
|
106
|
+
function publicReadAdminWrite(roles = ["admin"], overrides) {
|
|
107
|
+
return withOverrides({
|
|
108
|
+
list: allowPublic(),
|
|
109
|
+
get: allowPublic(),
|
|
110
|
+
create: requireRoles(roles),
|
|
111
|
+
update: requireRoles(roles),
|
|
112
|
+
delete: requireRoles(roles)
|
|
113
|
+
}, overrides);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* All operations require authentication.
|
|
117
|
+
*/
|
|
118
|
+
function authenticated(overrides) {
|
|
119
|
+
return withOverrides({
|
|
120
|
+
list: requireAuth(),
|
|
121
|
+
get: requireAuth(),
|
|
122
|
+
create: requireAuth(),
|
|
123
|
+
update: requireAuth(),
|
|
124
|
+
delete: requireAuth()
|
|
125
|
+
}, overrides);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* All operations require specific roles.
|
|
129
|
+
* @param roles - Required roles (user needs at least one). Default: ['admin']
|
|
130
|
+
*/
|
|
131
|
+
function adminOnly(roles = ["admin"], overrides) {
|
|
132
|
+
return withOverrides({
|
|
133
|
+
list: requireRoles(roles),
|
|
134
|
+
get: requireRoles(roles),
|
|
135
|
+
create: requireRoles(roles),
|
|
136
|
+
update: requireRoles(roles),
|
|
137
|
+
delete: requireRoles(roles)
|
|
138
|
+
}, overrides);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Owner-scoped with admin bypass.
|
|
142
|
+
* list = auth (scoped to owner), get = auth, create = auth,
|
|
143
|
+
* update + delete = ownership check with admin bypass.
|
|
144
|
+
*
|
|
145
|
+
* @param ownerField - Field containing owner ID (default: 'userId')
|
|
146
|
+
* @param bypassRoles - Roles that bypass ownership check (default: ['admin'])
|
|
147
|
+
*/
|
|
148
|
+
function ownerWithAdminBypass(ownerField = "userId", bypassRoles = ["admin"], overrides) {
|
|
149
|
+
return withOverrides({
|
|
150
|
+
list: requireAuth(),
|
|
151
|
+
get: requireAuth(),
|
|
152
|
+
create: requireAuth(),
|
|
153
|
+
update: anyOf(requireRoles(bypassRoles), requireOwnership(ownerField)),
|
|
154
|
+
delete: anyOf(requireRoles(bypassRoles), requireOwnership(ownerField))
|
|
155
|
+
}, overrides);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Full public access — no auth required for any operation.
|
|
159
|
+
* Use sparingly (dev/testing, truly public APIs).
|
|
160
|
+
*/
|
|
161
|
+
function fullPublic(overrides) {
|
|
162
|
+
return withOverrides({
|
|
163
|
+
list: allowPublic(),
|
|
164
|
+
get: allowPublic(),
|
|
165
|
+
create: allowPublic(),
|
|
166
|
+
update: allowPublic(),
|
|
167
|
+
delete: allowPublic()
|
|
168
|
+
}, overrides);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Read-only: list + get authenticated, write operations denied.
|
|
172
|
+
* Useful for computed/derived resources.
|
|
173
|
+
*/
|
|
174
|
+
function readOnly(overrides) {
|
|
175
|
+
return withOverrides({
|
|
176
|
+
list: requireAuth(),
|
|
177
|
+
get: requireAuth()
|
|
178
|
+
}, overrides);
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/permissions/index.ts
|
|
182
|
+
/**
|
|
183
|
+
* Allow public access (no authentication required)
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```typescript
|
|
187
|
+
* permissions: {
|
|
188
|
+
* list: allowPublic(),
|
|
189
|
+
* get: allowPublic(),
|
|
190
|
+
* }
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
function allowPublic() {
|
|
194
|
+
const check = () => true;
|
|
195
|
+
check._isPublic = true;
|
|
196
|
+
return check;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Require authentication (any authenticated user)
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```typescript
|
|
203
|
+
* permissions: {
|
|
204
|
+
* create: requireAuth(),
|
|
205
|
+
* update: requireAuth(),
|
|
206
|
+
* }
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
function requireAuth() {
|
|
210
|
+
const check = (ctx) => {
|
|
211
|
+
if (!ctx.user) return {
|
|
212
|
+
granted: false,
|
|
213
|
+
reason: "Authentication required"
|
|
214
|
+
};
|
|
215
|
+
return true;
|
|
216
|
+
};
|
|
217
|
+
return check;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Require specific roles
|
|
221
|
+
*
|
|
222
|
+
* @param roles - Required roles (user needs at least one)
|
|
223
|
+
* @param options - Optional bypass roles
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```typescript
|
|
227
|
+
* permissions: {
|
|
228
|
+
* create: requireRoles(['admin', 'editor']),
|
|
229
|
+
* delete: requireRoles(['admin']),
|
|
230
|
+
* }
|
|
231
|
+
*
|
|
232
|
+
* // With bypass roles
|
|
233
|
+
* permissions: {
|
|
234
|
+
* update: requireRoles(['owner'], { bypassRoles: ['admin', 'superadmin'] }),
|
|
235
|
+
* }
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
function requireRoles(roles, options) {
|
|
239
|
+
const check = (ctx) => {
|
|
240
|
+
if (!ctx.user) return {
|
|
241
|
+
granted: false,
|
|
242
|
+
reason: "Authentication required"
|
|
243
|
+
};
|
|
244
|
+
const userRoles = getUserRoles(ctx.user);
|
|
245
|
+
if (options?.bypassRoles?.some((r) => userRoles.includes(r))) return true;
|
|
246
|
+
if (roles.some((r) => userRoles.includes(r))) return true;
|
|
247
|
+
return {
|
|
248
|
+
granted: false,
|
|
249
|
+
reason: `Required roles: ${roles.join(", ")}`
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
check._roles = roles;
|
|
253
|
+
return check;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Require resource ownership
|
|
257
|
+
*
|
|
258
|
+
* Returns filters to scope queries to user's owned resources.
|
|
259
|
+
*
|
|
260
|
+
* @param ownerField - Field containing owner ID (default: 'userId')
|
|
261
|
+
* @param options - Optional bypass roles
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```typescript
|
|
265
|
+
* permissions: {
|
|
266
|
+
* update: requireOwnership('userId'),
|
|
267
|
+
* delete: requireOwnership('createdBy', { bypassRoles: ['admin'] }),
|
|
268
|
+
* }
|
|
269
|
+
* ```
|
|
270
|
+
*/
|
|
271
|
+
function requireOwnership(ownerField = "userId", options) {
|
|
272
|
+
return (ctx) => {
|
|
273
|
+
if (!ctx.user) return {
|
|
274
|
+
granted: false,
|
|
275
|
+
reason: "Authentication required"
|
|
276
|
+
};
|
|
277
|
+
const userRoles = getUserRoles(ctx.user);
|
|
278
|
+
if (options?.bypassRoles?.some((r) => userRoles.includes(r))) return true;
|
|
279
|
+
const userId = getUserId(getScope(ctx.request)) ?? ctx.user.id ?? ctx.user._id;
|
|
280
|
+
if (!userId) return {
|
|
281
|
+
granted: false,
|
|
282
|
+
reason: "User identity missing (no id or _id)"
|
|
283
|
+
};
|
|
284
|
+
return {
|
|
285
|
+
granted: true,
|
|
286
|
+
filters: { [ownerField]: userId }
|
|
287
|
+
};
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Combine multiple checks - ALL must pass (AND logic)
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```typescript
|
|
295
|
+
* permissions: {
|
|
296
|
+
* update: allOf(
|
|
297
|
+
* requireAuth(),
|
|
298
|
+
* requireRoles(['editor']),
|
|
299
|
+
* requireOwnership('createdBy')
|
|
300
|
+
* ),
|
|
301
|
+
* }
|
|
302
|
+
* ```
|
|
303
|
+
*/
|
|
304
|
+
function allOf(...checks) {
|
|
305
|
+
return async (ctx) => {
|
|
306
|
+
let mergedFilters = {};
|
|
307
|
+
for (const check of checks) {
|
|
308
|
+
const result = await check(ctx);
|
|
309
|
+
const normalized = typeof result === "boolean" ? { granted: result } : result;
|
|
310
|
+
if (!normalized.granted) return normalized;
|
|
311
|
+
if (normalized.filters) mergedFilters = {
|
|
312
|
+
...mergedFilters,
|
|
313
|
+
...normalized.filters
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
granted: true,
|
|
318
|
+
filters: Object.keys(mergedFilters).length > 0 ? mergedFilters : void 0
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Combine multiple checks - ANY must pass (OR logic)
|
|
324
|
+
*
|
|
325
|
+
* @example
|
|
326
|
+
* ```typescript
|
|
327
|
+
* permissions: {
|
|
328
|
+
* update: anyOf(
|
|
329
|
+
* requireRoles(['admin']),
|
|
330
|
+
* requireOwnership('createdBy')
|
|
331
|
+
* ),
|
|
332
|
+
* }
|
|
333
|
+
* ```
|
|
334
|
+
*/
|
|
335
|
+
function anyOf(...checks) {
|
|
336
|
+
return async (ctx) => {
|
|
337
|
+
const reasons = [];
|
|
338
|
+
for (const check of checks) {
|
|
339
|
+
const result = await check(ctx);
|
|
340
|
+
const normalized = typeof result === "boolean" ? { granted: result } : result;
|
|
341
|
+
if (normalized.granted) return normalized;
|
|
342
|
+
if (normalized.reason) reasons.push(normalized.reason);
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
granted: false,
|
|
346
|
+
reason: reasons.join("; ")
|
|
347
|
+
};
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Deny all access
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* ```typescript
|
|
355
|
+
* permissions: {
|
|
356
|
+
* delete: denyAll('Deletion not allowed'),
|
|
357
|
+
* }
|
|
358
|
+
* ```
|
|
359
|
+
*/
|
|
360
|
+
function denyAll(reason = "Access denied") {
|
|
361
|
+
return () => ({
|
|
362
|
+
granted: false,
|
|
363
|
+
reason
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Dynamic permission based on context
|
|
368
|
+
*
|
|
369
|
+
* @example
|
|
370
|
+
* ```typescript
|
|
371
|
+
* permissions: {
|
|
372
|
+
* update: when((ctx) => ctx.data?.status === 'draft'),
|
|
373
|
+
* }
|
|
374
|
+
* ```
|
|
375
|
+
*/
|
|
376
|
+
function when(condition) {
|
|
377
|
+
return async (ctx) => {
|
|
378
|
+
const result = await condition(ctx);
|
|
379
|
+
return {
|
|
380
|
+
granted: result,
|
|
381
|
+
reason: result ? void 0 : "Condition not met"
|
|
382
|
+
};
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/** Read request.scope safely */
|
|
386
|
+
function getScope(request) {
|
|
387
|
+
return request.scope ?? PUBLIC_SCOPE;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Require membership in the active organization.
|
|
391
|
+
* User must be authenticated AND have an active org (member or elevated scope).
|
|
392
|
+
*
|
|
393
|
+
* Reads `request.scope` set by auth adapters.
|
|
394
|
+
*
|
|
395
|
+
* @example
|
|
396
|
+
* ```typescript
|
|
397
|
+
* permissions: {
|
|
398
|
+
* list: requireOrgMembership(),
|
|
399
|
+
* get: requireOrgMembership(),
|
|
400
|
+
* }
|
|
401
|
+
* ```
|
|
402
|
+
*/
|
|
403
|
+
function requireOrgMembership() {
|
|
404
|
+
const check = (ctx) => {
|
|
405
|
+
if (!ctx.user) return {
|
|
406
|
+
granted: false,
|
|
407
|
+
reason: "Authentication required"
|
|
408
|
+
};
|
|
409
|
+
const scope = getScope(ctx.request);
|
|
410
|
+
if (isElevated(scope)) return true;
|
|
411
|
+
if (isMember(scope)) return true;
|
|
412
|
+
return {
|
|
413
|
+
granted: false,
|
|
414
|
+
reason: "Organization membership required"
|
|
415
|
+
};
|
|
416
|
+
};
|
|
417
|
+
check._orgPermission = "membership";
|
|
418
|
+
return check;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Require specific org-level roles.
|
|
422
|
+
* Reads `request.scope.orgRoles` (set by auth adapters).
|
|
423
|
+
* Elevated scope always passes (platform admin bypass).
|
|
424
|
+
*
|
|
425
|
+
* @param roles - Required org roles (user needs at least one)
|
|
426
|
+
*
|
|
427
|
+
* @example
|
|
428
|
+
* ```typescript
|
|
429
|
+
* permissions: {
|
|
430
|
+
* create: requireOrgRole('admin', 'owner'),
|
|
431
|
+
* delete: requireOrgRole('owner'),
|
|
432
|
+
* }
|
|
433
|
+
* ```
|
|
434
|
+
*/
|
|
435
|
+
function requireOrgRole(...args) {
|
|
436
|
+
const roles = Array.isArray(args[0]) ? args[0] : args;
|
|
437
|
+
const check = (ctx) => {
|
|
438
|
+
if (!ctx.user) return {
|
|
439
|
+
granted: false,
|
|
440
|
+
reason: "Authentication required"
|
|
441
|
+
};
|
|
442
|
+
const scope = getScope(ctx.request);
|
|
443
|
+
if (isElevated(scope)) return true;
|
|
444
|
+
if (!isMember(scope)) return {
|
|
445
|
+
granted: false,
|
|
446
|
+
reason: "Organization membership required"
|
|
447
|
+
};
|
|
448
|
+
if (roles.some((r) => scope.orgRoles.includes(r))) return true;
|
|
449
|
+
return {
|
|
450
|
+
granted: false,
|
|
451
|
+
reason: `Required org roles: ${roles.join(", ")}`
|
|
452
|
+
};
|
|
453
|
+
};
|
|
454
|
+
check._orgRoles = roles;
|
|
455
|
+
return check;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Create a scoped permission system for resource-action patterns.
|
|
459
|
+
* Maps org roles to fine-grained permissions without external API calls.
|
|
460
|
+
*
|
|
461
|
+
* @example
|
|
462
|
+
* ```typescript
|
|
463
|
+
* const perms = createOrgPermissions({
|
|
464
|
+
* statements: {
|
|
465
|
+
* product: ['create', 'update', 'delete'],
|
|
466
|
+
* order: ['create', 'approve'],
|
|
467
|
+
* },
|
|
468
|
+
* roles: {
|
|
469
|
+
* owner: { product: ['create', 'update', 'delete'], order: ['create', 'approve'] },
|
|
470
|
+
* admin: { product: ['create', 'update'], order: ['create'] },
|
|
471
|
+
* member: { product: [], order: [] },
|
|
472
|
+
* },
|
|
473
|
+
* });
|
|
474
|
+
*
|
|
475
|
+
* defineResource({
|
|
476
|
+
* permissions: {
|
|
477
|
+
* create: perms.can({ product: ['create'] }),
|
|
478
|
+
* delete: perms.can({ product: ['delete'] }),
|
|
479
|
+
* }
|
|
480
|
+
* });
|
|
481
|
+
* ```
|
|
482
|
+
*/
|
|
483
|
+
function createOrgPermissions(config) {
|
|
484
|
+
const { roles: roleMap } = config;
|
|
485
|
+
function hasPermissions(orgRoles, required) {
|
|
486
|
+
for (const [resource, actions] of Object.entries(required)) for (const action of actions) if (!orgRoles.some((role) => {
|
|
487
|
+
return (roleMap[role]?.[resource])?.includes(action);
|
|
488
|
+
})) return false;
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
can(permissions) {
|
|
493
|
+
return (ctx) => {
|
|
494
|
+
if (!ctx.user) return {
|
|
495
|
+
granted: false,
|
|
496
|
+
reason: "Authentication required"
|
|
497
|
+
};
|
|
498
|
+
const scope = getScope(ctx.request);
|
|
499
|
+
if (isElevated(scope)) return true;
|
|
500
|
+
if (!isMember(scope)) return {
|
|
501
|
+
granted: false,
|
|
502
|
+
reason: "Organization membership required"
|
|
503
|
+
};
|
|
504
|
+
if (hasPermissions(scope.orgRoles, permissions)) return true;
|
|
505
|
+
return {
|
|
506
|
+
granted: false,
|
|
507
|
+
reason: `Missing permissions: ${Object.entries(permissions).map(([r, a]) => `${r}:[${a.join(",")}]`).join(", ")}`
|
|
508
|
+
};
|
|
509
|
+
};
|
|
510
|
+
},
|
|
511
|
+
requireRole(...roles) {
|
|
512
|
+
return requireOrgRole(roles);
|
|
513
|
+
},
|
|
514
|
+
requireMembership() {
|
|
515
|
+
return requireOrgMembership();
|
|
516
|
+
},
|
|
517
|
+
requireTeamMembership() {
|
|
518
|
+
return requireTeamMembership();
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Create a dynamic role-based permission matrix.
|
|
524
|
+
*
|
|
525
|
+
* Use this when role/action mappings are managed outside code
|
|
526
|
+
* (e.g., admin UI matrix, DB-stored ACLs, remote policy service).
|
|
527
|
+
*
|
|
528
|
+
* Supports:
|
|
529
|
+
* - org role union (any assigned org role can grant)
|
|
530
|
+
* - global bypass roles
|
|
531
|
+
* - wildcard resource/action (`*`)
|
|
532
|
+
* - optional in-memory cache
|
|
533
|
+
*/
|
|
534
|
+
function createDynamicPermissionMatrix(config) {
|
|
535
|
+
const logger = config.logger ?? console;
|
|
536
|
+
const legacyTtlMs = config.cache?.ttlMs ?? 0;
|
|
537
|
+
const hasExternalStore = !!config.cacheStore;
|
|
538
|
+
const cacheTtlMs = legacyTtlMs > 0 ? legacyTtlMs : hasExternalStore ? 3e5 : 0;
|
|
539
|
+
const internalStore = !config.cacheStore && cacheTtlMs > 0 ? new MemoryCacheStore({
|
|
540
|
+
defaultTtlMs: cacheTtlMs,
|
|
541
|
+
maxEntries: config.cache?.maxEntries ?? 1e3
|
|
542
|
+
}) : void 0;
|
|
543
|
+
const cacheStore = config.cacheStore ?? internalStore;
|
|
544
|
+
const trackedKeys = /* @__PURE__ */ new Set();
|
|
545
|
+
const nodeId = randomUUID().slice(0, 8);
|
|
546
|
+
const DEFAULT_EVENT_TYPE = "arc.permissions.invalidated";
|
|
547
|
+
let eventBridge = null;
|
|
548
|
+
/** Clear local cache for an org without publishing events (avoids infinite loops). */
|
|
549
|
+
async function localInvalidateByOrg(orgId) {
|
|
550
|
+
if (!cacheStore) return;
|
|
551
|
+
const prefix = `${orgId}::`;
|
|
552
|
+
const toDelete = [];
|
|
553
|
+
for (const key of trackedKeys) if (key.startsWith(prefix)) toDelete.push(key);
|
|
554
|
+
for (const key of toDelete) try {
|
|
555
|
+
await cacheStore.delete(key);
|
|
556
|
+
trackedKeys.delete(key);
|
|
557
|
+
} catch (error) {
|
|
558
|
+
logger.warn(`[DynamicPermissionMatrix] invalidateByOrg delete failed for '${key}': ${error instanceof Error ? error.message : String(error)}`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function isActionAllowed(actions, action) {
|
|
562
|
+
if (!actions || actions.length === 0) return false;
|
|
563
|
+
return actions.includes("*") || actions.includes(action);
|
|
564
|
+
}
|
|
565
|
+
function roleAllows(matrix, role, resource, action) {
|
|
566
|
+
const rolePermissions = matrix[role];
|
|
567
|
+
if (!rolePermissions) return false;
|
|
568
|
+
const resourceActions = rolePermissions[resource];
|
|
569
|
+
const wildcardResourceActions = rolePermissions["*"];
|
|
570
|
+
return isActionAllowed(resourceActions, action) || isActionAllowed(wildcardResourceActions, action);
|
|
571
|
+
}
|
|
572
|
+
function buildDefaultCacheKey(ctx, orgId, orgRoles) {
|
|
573
|
+
const userId = String(ctx.user?.id ?? ctx.user?._id ?? "anon");
|
|
574
|
+
const roles = (orgRoles ?? []).slice().sort().join(",");
|
|
575
|
+
return `${orgId ?? "no-org"}::${roles}::${userId}`;
|
|
576
|
+
}
|
|
577
|
+
async function resolveMatrix(ctx, orgId, orgRoles) {
|
|
578
|
+
if (!cacheStore) return config.resolveRolePermissions(ctx);
|
|
579
|
+
const cacheKey = config.cache?.key?.(ctx) ?? buildDefaultCacheKey(ctx, orgId, orgRoles);
|
|
580
|
+
if (!cacheKey) return config.resolveRolePermissions(ctx);
|
|
581
|
+
try {
|
|
582
|
+
const hit = await cacheStore.get(cacheKey);
|
|
583
|
+
if (hit) return hit;
|
|
584
|
+
} catch (error) {
|
|
585
|
+
logger.warn(`[DynamicPermissionMatrix] Cache get failed for '${cacheKey}': ${error instanceof Error ? error.message : String(error)}`);
|
|
586
|
+
}
|
|
587
|
+
const value = await config.resolveRolePermissions(ctx);
|
|
588
|
+
try {
|
|
589
|
+
await cacheStore.set(cacheKey, value, { ttlMs: cacheTtlMs });
|
|
590
|
+
trackedKeys.add(cacheKey);
|
|
591
|
+
const maxTracked = config.cache?.maxEntries ?? 1e4;
|
|
592
|
+
if (trackedKeys.size > maxTracked) {
|
|
593
|
+
const overflow = trackedKeys.size - maxTracked;
|
|
594
|
+
const iter = trackedKeys.values();
|
|
595
|
+
for (let i = 0; i < overflow; i++) {
|
|
596
|
+
const oldest = iter.next().value;
|
|
597
|
+
if (oldest) trackedKeys.delete(oldest);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
} catch (error) {
|
|
601
|
+
logger.warn(`[DynamicPermissionMatrix] Cache set failed for '${cacheKey}': ${error instanceof Error ? error.message : String(error)}`);
|
|
602
|
+
}
|
|
603
|
+
return value;
|
|
604
|
+
}
|
|
605
|
+
function can(required) {
|
|
606
|
+
return async (ctx) => {
|
|
607
|
+
if (!ctx.user) return {
|
|
608
|
+
granted: false,
|
|
609
|
+
reason: "Authentication required"
|
|
610
|
+
};
|
|
611
|
+
const scope = getScope(ctx.request);
|
|
612
|
+
if (isElevated(scope)) return true;
|
|
613
|
+
if (!isMember(scope)) return {
|
|
614
|
+
granted: false,
|
|
615
|
+
reason: "Organization membership required"
|
|
616
|
+
};
|
|
617
|
+
const orgRoles = scope.orgRoles;
|
|
618
|
+
if (orgRoles.length === 0) return {
|
|
619
|
+
granted: false,
|
|
620
|
+
reason: "Not a member of this organization"
|
|
621
|
+
};
|
|
622
|
+
let matrix;
|
|
623
|
+
try {
|
|
624
|
+
matrix = await resolveMatrix(ctx, scope.organizationId, orgRoles);
|
|
625
|
+
} catch (error) {
|
|
626
|
+
return {
|
|
627
|
+
granted: false,
|
|
628
|
+
reason: `Permission matrix resolution failed: ${error instanceof Error ? error.message : String(error)}`
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
for (const [resource, actions] of Object.entries(required)) for (const action of actions) if (!orgRoles.some((role) => roleAllows(matrix, role, resource, action))) return {
|
|
632
|
+
granted: false,
|
|
633
|
+
reason: `Missing permission: ${resource}:${action}`
|
|
634
|
+
};
|
|
635
|
+
return true;
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
can,
|
|
640
|
+
canAction(resource, action) {
|
|
641
|
+
return can({ [resource]: [action] });
|
|
642
|
+
},
|
|
643
|
+
requireRole(...roles) {
|
|
644
|
+
return requireOrgRole(roles);
|
|
645
|
+
},
|
|
646
|
+
requireMembership() {
|
|
647
|
+
return requireOrgMembership();
|
|
648
|
+
},
|
|
649
|
+
requireTeamMembership() {
|
|
650
|
+
return requireTeamMembership();
|
|
651
|
+
},
|
|
652
|
+
async invalidateByOrg(orgId) {
|
|
653
|
+
await localInvalidateByOrg(orgId);
|
|
654
|
+
if (eventBridge) try {
|
|
655
|
+
await eventBridge.publish(eventBridge.eventType, {
|
|
656
|
+
orgId,
|
|
657
|
+
nodeId
|
|
658
|
+
});
|
|
659
|
+
} catch (error) {
|
|
660
|
+
logger.warn(`[DynamicPermissionMatrix] Failed to publish invalidation event for org '${orgId}': ${error instanceof Error ? error.message : String(error)}`);
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
async clearCache() {
|
|
664
|
+
if (!cacheStore) return;
|
|
665
|
+
if (cacheStore.clear) try {
|
|
666
|
+
await cacheStore.clear();
|
|
667
|
+
trackedKeys.clear();
|
|
668
|
+
return;
|
|
669
|
+
} catch (error) {
|
|
670
|
+
logger.warn(`[DynamicPermissionMatrix] cacheStore.clear failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
671
|
+
}
|
|
672
|
+
for (const key of trackedKeys) try {
|
|
673
|
+
await cacheStore.delete(key);
|
|
674
|
+
} catch (error) {
|
|
675
|
+
logger.warn(`[DynamicPermissionMatrix] Cache delete failed for '${key}': ${error instanceof Error ? error.message : String(error)}`);
|
|
676
|
+
}
|
|
677
|
+
trackedKeys.clear();
|
|
678
|
+
},
|
|
679
|
+
async connectEvents(events, options) {
|
|
680
|
+
if (eventBridge) await this.disconnectEvents();
|
|
681
|
+
const eventType = options?.eventType ?? DEFAULT_EVENT_TYPE;
|
|
682
|
+
const unsubscribeFn = await events.subscribe(eventType, async (event) => {
|
|
683
|
+
const payload = event.payload;
|
|
684
|
+
if (!payload?.orgId) return;
|
|
685
|
+
if (payload.nodeId === nodeId) return;
|
|
686
|
+
await localInvalidateByOrg(payload.orgId);
|
|
687
|
+
if (options?.onRemoteInvalidation) try {
|
|
688
|
+
await options.onRemoteInvalidation(payload.orgId);
|
|
689
|
+
} catch (error) {
|
|
690
|
+
logger.warn(`[DynamicPermissionMatrix] onRemoteInvalidation callback failed for org '${payload.orgId}': ${error instanceof Error ? error.message : String(error)}`);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
eventBridge = {
|
|
694
|
+
publish: events.publish,
|
|
695
|
+
unsubscribe: typeof unsubscribeFn === "function" ? unsubscribeFn : null,
|
|
696
|
+
eventType,
|
|
697
|
+
onRemoteInvalidation: options?.onRemoteInvalidation
|
|
698
|
+
};
|
|
699
|
+
},
|
|
700
|
+
async disconnectEvents() {
|
|
701
|
+
if (!eventBridge) return;
|
|
702
|
+
try {
|
|
703
|
+
eventBridge.unsubscribe?.();
|
|
704
|
+
} catch (error) {
|
|
705
|
+
logger.warn(`[DynamicPermissionMatrix] disconnectEvents unsubscribe failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
706
|
+
}
|
|
707
|
+
eventBridge = null;
|
|
708
|
+
},
|
|
709
|
+
get eventsConnected() {
|
|
710
|
+
return eventBridge !== null;
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Require membership in the active team.
|
|
716
|
+
* User must be authenticated, a member of the active org, AND have an active team.
|
|
717
|
+
*
|
|
718
|
+
* Better Auth teams are flat member groups (no team-level roles).
|
|
719
|
+
* Reads `request.scope.teamId` set by the Better Auth adapter.
|
|
720
|
+
*
|
|
721
|
+
* @example
|
|
722
|
+
* ```typescript
|
|
723
|
+
* permissions: {
|
|
724
|
+
* list: requireTeamMembership(),
|
|
725
|
+
* create: requireTeamMembership(),
|
|
726
|
+
* }
|
|
727
|
+
* ```
|
|
728
|
+
*/
|
|
729
|
+
function requireTeamMembership() {
|
|
730
|
+
const check = (ctx) => {
|
|
731
|
+
if (!ctx.user) return {
|
|
732
|
+
granted: false,
|
|
733
|
+
reason: "Authentication required"
|
|
734
|
+
};
|
|
735
|
+
const scope = getScope(ctx.request);
|
|
736
|
+
if (isElevated(scope)) return true;
|
|
737
|
+
if (!isMember(scope)) return {
|
|
738
|
+
granted: false,
|
|
739
|
+
reason: "Organization membership required"
|
|
740
|
+
};
|
|
741
|
+
if (!getTeamId(scope)) return {
|
|
742
|
+
granted: false,
|
|
743
|
+
reason: "No active team"
|
|
744
|
+
};
|
|
745
|
+
return true;
|
|
746
|
+
};
|
|
747
|
+
check._teamPermission = "membership";
|
|
748
|
+
return check;
|
|
749
|
+
}
|
|
750
|
+
//#endregion
|
|
751
|
+
export { createRoleHierarchy as S, ownerWithAdminBypass as _, createOrgPermissions as a, publicReadAdminWrite as b, requireOrgMembership as c, requireRoles as d, requireTeamMembership as f, fullPublic as g, authenticated as h, createDynamicPermissionMatrix as i, requireOrgRole as l, adminOnly as m, allowPublic as n, denyAll as o, when as p, anyOf as r, requireAuth as s, allOf as t, requireOwnership as u, presets_exports as v, readOnly as x, publicRead as y };
|