@classytic/arc 2.9.1 → 2.10.8
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 +20 -91
- package/dist/{BaseController-Vu2yc56T.mjs → BaseController-DVNKvoX4.mjs} +154 -170
- package/dist/{ResourceRegistry-Dq3_zBQP.mjs → ResourceRegistry-CcN2LVrc.mjs} +1 -1
- package/dist/actionPermissions-TUVR3uiZ.mjs +22 -0
- package/dist/adapters/index.d.mts +3 -3
- package/dist/adapters/index.mjs +2 -2
- package/dist/{adapters-BBqAVvPK.mjs → adapters-BXY4i-hw.mjs} +210 -41
- package/dist/audit/index.d.mts +38 -3
- package/dist/audit/index.mjs +54 -22
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.mjs +3 -3
- package/dist/cache/index.d.mts +17 -15
- package/dist/cache/index.mjs +16 -15
- package/dist/{caching-CjybdRwx.mjs → caching-3h93rkJM.mjs} +8 -3
- package/dist/cli/commands/describe.mjs +1 -1
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/context/index.d.mts +58 -0
- package/dist/context/index.mjs +2 -0
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +3 -4
- package/dist/{defineResource-C__jkwvs.mjs → core-3MWJosCH.mjs} +174 -94
- package/dist/{createActionRouter-DH1YFL9m.mjs → createActionRouter-C8UUB3Px.mjs} +1 -1
- package/dist/{createApp-CBJUJKGP.mjs → createApp-BwnEAO2h.mjs} +53 -19
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-DxQ6ACbt.mjs → elevation-Dci0AYLT.mjs} +2 -2
- package/dist/errorHandler-2ii4RIYr.d.mts +114 -0
- package/dist/{errorHandler-CZDW4EXS.mjs → errorHandler-CSxe7KIM.mjs} +1 -1
- package/dist/{eventPlugin-Dl7MoVWH.mjs → eventPlugin-ByU4Cv0e.mjs} +1 -1
- package/dist/{eventPlugin-BxvaCIZF.d.mts → eventPlugin-D1ThQ1Pp.d.mts} +1 -1
- package/dist/events/index.d.mts +8 -5
- package/dist/events/index.mjs +87 -52
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +1 -1
- package/dist/{types-DZi1aYhm.d.mts → fields-C8Y0XLAu.d.mts} +122 -2
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +5 -2
- package/dist/idempotency/index.mjs +46 -37
- package/dist/{interface-YrWsmKqE.d.mts → index-BGbpGVyM.d.mts} +2107 -2756
- package/dist/{index-CtGKT0lf.d.mts → index-BziRPS4H.d.mts} +81 -7
- package/dist/{index-C-xjcA6F.d.mts → index-C_Noptz-.d.mts} +284 -409
- package/dist/{index-Cibkchnx.d.mts → index-EqQN6p0W.d.mts} +3 -3
- package/dist/index.d.mts +6 -219
- package/dist/index.mjs +10 -131
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/interface-yhyb_pLY.d.mts +77 -0
- package/dist/logger/index.d.mts +81 -0
- package/dist/{logger-CDjpjySd.mjs → logger/index.mjs} +1 -6
- package/dist/{memory-BFAYkf8H.mjs → memory-DqI-449b.mjs} +23 -8
- package/dist/middleware/index.d.mts +109 -0
- package/dist/middleware/index.mjs +70 -0
- package/dist/multipartBody-CUQGVlM_.mjs +123 -0
- package/dist/{openapi-CXuTG1M9.mjs → openapi-DpNpqBmo.mjs} +9 -7
- package/dist/org/index.d.mts +2 -2
- package/dist/permissions/index.d.mts +3 -4
- package/dist/permissions/index.mjs +5 -5
- package/dist/{permissions-oNZawnkR.mjs → permissions-wkqRwicB.mjs} +315 -397
- package/dist/pipe-CGJxqDGx.mjs +62 -0
- package/dist/pipeline/index.d.mts +62 -0
- package/dist/pipeline/index.mjs +53 -0
- package/dist/plugins/index.d.mts +23 -3
- package/dist/plugins/index.mjs +9 -11
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +3 -3
- package/dist/presets/filesUpload.mjs +255 -1
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +2 -2
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +43 -9
- package/dist/presets/search.d.mts +91 -4
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-hM4WhNWY.mjs → presets-CrwOvuXI.mjs} +1 -1
- package/dist/{queryCachePlugin-DbUVroUG.mjs → queryCachePlugin-ChLNZvFT.mjs} +9 -9
- package/dist/{queryCachePlugin-CnTZZTC5.d.mts → queryCachePlugin-Dumka73q.d.mts} +1 -1
- package/dist/{queryParser-Cs-6SHQK.mjs → queryParser-NR__Qiju.mjs} +69 -2
- package/dist/{redis-stream-Bz-4q96t.d.mts → redis-stream-bkO88VHx.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +1 -1
- package/dist/{requestContext-DYtmNpm5.mjs → requestContext-C38GskNt.mjs} +1 -1
- package/dist/{resourceToTools-C3cWymnW.mjs → resourceToTools-BhF3JV5p.mjs} +8 -3
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +2 -2
- package/dist/{sse-CJpt7LGI.mjs → sse-D8UeDwis.mjs} +1 -1
- package/dist/{store-helpers-DFiZl5TL.mjs → store-helpers-DYYUQbQN.mjs} +4 -0
- package/dist/testing/index.d.mts +6 -5
- package/dist/testing/index.mjs +17 -10
- package/dist/types/index.d.mts +5 -5
- package/dist/types/index.mjs +1 -31
- package/dist/types-CDnTEpga.mjs +27 -0
- package/dist/{types-CoSzA-s-.d.mts → types-CVKBssX5.d.mts} +1 -1
- package/dist/{types-CunEX4UX.d.mts → types-CVdgPXBW.d.mts} +20 -7
- package/dist/utils/index.d.mts +277 -3
- package/dist/utils/index.mjs +4 -5
- package/dist/{utils-B7FuRr9w.mjs → utils-LMwVidKy.mjs} +303 -2
- package/dist/{versioning-Cm8qoFDg.mjs → versioning-B6mimogM.mjs} +3 -5
- package/dist/versioning-CeUXHfjw.d.mts +117 -0
- package/package.json +31 -18
- package/skills/arc/SKILL.md +8 -12
- package/skills/arc/references/production.md +0 -41
- package/dist/circuitBreaker-CvXkjfrW.d.mts +0 -206
- package/dist/circuitBreaker-l18oRgL5.mjs +0 -284
- package/dist/core-DNncu0xF.mjs +0 -34
- package/dist/dynamic/index.d.mts +0 -93
- package/dist/dynamic/index.mjs +0 -122
- package/dist/errorHandler-DixGcttC.d.mts +0 -218
- package/dist/fields-BC7zcmI9.d.mts +0 -121
- package/dist/filesUpload-q8oHt--L.mjs +0 -377
- package/dist/interface-DplgQO2e.d.mts +0 -54
- package/dist/policies/index.d.mts +0 -425
- package/dist/policies/index.mjs +0 -318
- package/dist/rpc/index.d.mts +0 -90
- package/dist/rpc/index.mjs +0 -248
- /package/dist/{EventTransport-CqZ8FyM_.d.mts → EventTransport-CfVEGaEl.d.mts} +0 -0
- /package/dist/{applyPermissionResult-bqGpo9ML.mjs → applyPermissionResult-QhV1Pa-g.mjs} +0 -0
- /package/dist/{constants-Cxde4rpC.mjs → constants-BhY1OHoH.mjs} +0 -0
- /package/dist/{elevation-B6S5csVA.d.mts → elevation-s5ykdNHr.d.mts} +0 -0
- /package/dist/{errors-CqWnSqM-.mjs → errors-BqdUDja_.mjs} +0 -0
- /package/dist/{fields-CU6FlaDV.mjs → fields-CTMWOUDt.mjs} +0 -0
- /package/dist/{keys-qcD-TVJl.mjs → keys-nWQGUTu1.mjs} +0 -0
- /package/dist/{types-ZUu_h0jp.mjs → types-D57iXYb8.mjs} +0 -0
- /package/dist/{types-BD85MlEK.d.mts → types-tgR4Pt8F.d.mts} +0 -0
|
@@ -1,209 +1,23 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
|
|
2
2
|
import { _ as isElevated, b as isService, c as getRequestScope, d as getServiceScopes, f as getTeamId, h as hasOrgAccess, l as getScopeContext, p as getUserId, u as getScopeContextMap, v as isMember, y as isOrgInScope } from "./types-AOD8fxIw.mjs";
|
|
3
|
-
import { t as getUserRoles } from "./types-
|
|
4
|
-
import { t as MemoryCacheStore } from "./memory-
|
|
3
|
+
import { t as getUserRoles } from "./types-D57iXYb8.mjs";
|
|
4
|
+
import { t as MemoryCacheStore } from "./memory-DqI-449b.mjs";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
-
//#region src/permissions/
|
|
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
|
|
6
|
+
//#region src/permissions/core.ts
|
|
182
7
|
/**
|
|
183
8
|
* Normalize a `string | [readonly string[]]` rest-args tuple into a single
|
|
184
9
|
* `readonly string[]`. Lets a permission helper accept BOTH variadic and
|
|
185
|
-
* array call shapes from
|
|
186
|
-
* re-implementing the same ternary.
|
|
10
|
+
* array call shapes from one overload signature.
|
|
187
11
|
*
|
|
188
12
|
* Used by `requireOrgRole`, `requireServiceScope`, etc. **Not** used by
|
|
189
13
|
* `requireRoles` — that helper has a richer overload signature with an
|
|
190
14
|
* options object and stays on its own normalization path.
|
|
191
|
-
*
|
|
192
|
-
* @example
|
|
193
|
-
* ```typescript
|
|
194
|
-
* function requireFoo(...args: string[] | [readonly string[]]) {
|
|
195
|
-
* const items = normalizeVariadicOrArray(args);
|
|
196
|
-
* // items is always readonly string[]
|
|
197
|
-
* }
|
|
198
|
-
* requireFoo('a', 'b', 'c');
|
|
199
|
-
* requireFoo(['a', 'b', 'c']);
|
|
200
|
-
* ```
|
|
201
15
|
*/
|
|
202
16
|
function normalizeVariadicOrArray(args) {
|
|
203
17
|
return Array.isArray(args[0]) ? args[0] : args;
|
|
204
18
|
}
|
|
205
19
|
/**
|
|
206
|
-
* Allow public access (no authentication required)
|
|
20
|
+
* Allow public access (no authentication required).
|
|
207
21
|
*
|
|
208
22
|
* @example
|
|
209
23
|
* ```typescript
|
|
@@ -219,7 +33,7 @@ function allowPublic() {
|
|
|
219
33
|
return check;
|
|
220
34
|
}
|
|
221
35
|
/**
|
|
222
|
-
* Require authentication (any authenticated user)
|
|
36
|
+
* Require authentication (any authenticated user).
|
|
223
37
|
*
|
|
224
38
|
* @example
|
|
225
39
|
* ```typescript
|
|
@@ -276,27 +90,9 @@ function requireRoles(rolesOrFirst, optionsOrSecond, ...rest) {
|
|
|
276
90
|
return check;
|
|
277
91
|
}
|
|
278
92
|
/**
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
* means `roles('admin')` and `requireRoles('admin')` are now functionally
|
|
283
|
-
* identical. This helper is preserved for backwards compatibility and for
|
|
284
|
-
* call sites that prefer the shorter `roles()` name.
|
|
285
|
-
*
|
|
286
|
-
* **For new code, prefer `requireRoles()`** — it's the canonical name and
|
|
287
|
-
* matches the rest of the `requireXxx()` family (`requireAuth`, `requireOwnership`,
|
|
288
|
-
* `requireOrgRole`, etc.).
|
|
289
|
-
*
|
|
290
|
-
* For platform-only checks: `requireRoles(['admin'], { includeOrgRoles: false })`
|
|
291
|
-
* For org-only checks: `requireOrgRole('admin')`
|
|
292
|
-
*
|
|
293
|
-
* @example
|
|
294
|
-
* ```typescript
|
|
295
|
-
* // These are identical:
|
|
296
|
-
* roles('admin', 'editor')
|
|
297
|
-
* requireRoles('admin', 'editor')
|
|
298
|
-
* requireRoles(['admin', 'editor'])
|
|
299
|
-
* ```
|
|
93
|
+
* Short-form alias of `requireRoles()`. Identical behavior — checks both
|
|
94
|
+
* platform roles AND org roles. Prefer `requireRoles` for new code; this
|
|
95
|
+
* exists for call sites that want a terser name.
|
|
300
96
|
*/
|
|
301
97
|
function roles(...args) {
|
|
302
98
|
const roleList = normalizeVariadicOrArray(args);
|
|
@@ -319,12 +115,8 @@ function roles(...args) {
|
|
|
319
115
|
return check;
|
|
320
116
|
}
|
|
321
117
|
/**
|
|
322
|
-
* Require resource ownership
|
|
323
|
-
*
|
|
324
|
-
* Returns filters to scope queries to user's owned resources.
|
|
325
|
-
*
|
|
326
|
-
* @param ownerField - Field containing owner ID (default: 'userId')
|
|
327
|
-
* @param options - Optional bypass roles
|
|
118
|
+
* Require resource ownership. Returns filters to scope queries to the
|
|
119
|
+
* caller's owned resources.
|
|
328
120
|
*
|
|
329
121
|
* @example
|
|
330
122
|
* ```typescript
|
|
@@ -354,33 +146,18 @@ function requireOwnership(ownerField = "userId", options) {
|
|
|
354
146
|
};
|
|
355
147
|
}
|
|
356
148
|
/**
|
|
357
|
-
* Combine multiple checks
|
|
149
|
+
* Combine multiple checks — ALL must pass (AND logic).
|
|
358
150
|
*
|
|
359
151
|
* Each child runs against the **accumulated** state of previous children:
|
|
360
|
-
* - `filters` from earlier children
|
|
361
|
-
*
|
|
362
|
-
* - `scope` from earlier children is installed on the request before the
|
|
363
|
-
* next child runs (so e.g. `requireOrgMembership` after `requireApiKey`
|
|
364
|
-
* sees the service scope from the API key check)
|
|
152
|
+
* - `filters` from earlier children merge into the next child's `_policyFilters`
|
|
153
|
+
* - `scope` from earlier children installs on the request before the next child runs
|
|
365
154
|
*
|
|
366
|
-
* The final
|
|
367
|
-
* the merged `scope`, so the outer middleware's `applyPermissionResult` call
|
|
368
|
-
* sees the same end-state.
|
|
155
|
+
* The final result carries both merged `filters` and merged `scope`.
|
|
369
156
|
*
|
|
370
157
|
* @example
|
|
371
158
|
* ```typescript
|
|
372
|
-
* // CRUD permissions composed across roles + ownership
|
|
373
|
-
* permissions: {
|
|
374
|
-
* update: allOf(
|
|
375
|
-
* requireAuth(),
|
|
376
|
-
* requireRoles(['editor']),
|
|
377
|
-
* requireOwnership('createdBy')
|
|
378
|
-
* ),
|
|
379
|
-
* }
|
|
380
|
-
*
|
|
381
|
-
* // Custom auth + org membership — first check installs the scope,
|
|
382
|
-
* // second check reads it.
|
|
383
159
|
* permissions: {
|
|
160
|
+
* update: allOf(requireAuth(), requireRoles(['editor']), requireOwnership('createdBy')),
|
|
384
161
|
* list: allOf(requireApiKey(), requireOrgMembership()),
|
|
385
162
|
* }
|
|
386
163
|
* ```
|
|
@@ -432,15 +209,12 @@ function allOf(...checks) {
|
|
|
432
209
|
};
|
|
433
210
|
}
|
|
434
211
|
/**
|
|
435
|
-
* Combine multiple checks
|
|
212
|
+
* Combine multiple checks — ANY must pass (OR logic).
|
|
436
213
|
*
|
|
437
214
|
* @example
|
|
438
215
|
* ```typescript
|
|
439
216
|
* permissions: {
|
|
440
|
-
* update: anyOf(
|
|
441
|
-
* requireRoles(['admin']),
|
|
442
|
-
* requireOwnership('createdBy')
|
|
443
|
-
* ),
|
|
217
|
+
* update: anyOf(requireRoles(['admin']), requireOwnership('createdBy')),
|
|
444
218
|
* }
|
|
445
219
|
* ```
|
|
446
220
|
*/
|
|
@@ -460,15 +234,38 @@ function anyOf(...checks) {
|
|
|
460
234
|
};
|
|
461
235
|
}
|
|
462
236
|
/**
|
|
463
|
-
*
|
|
237
|
+
* Invert a permission check. Grants when the wrapped check denies, denies
|
|
238
|
+
* when the wrapped check grants. Useful for "block if X" patterns —
|
|
239
|
+
* e.g. `not(requireRoles(['guest']))` to deny guest access.
|
|
240
|
+
*
|
|
241
|
+
* NOTE: filters and scope from the wrapped check are intentionally
|
|
242
|
+
* discarded — an inverted check has no row-level meaning.
|
|
464
243
|
*
|
|
465
244
|
* @example
|
|
466
245
|
* ```typescript
|
|
467
246
|
* permissions: {
|
|
468
|
-
*
|
|
247
|
+
* internalApi: not(requireRoles(['external'])),
|
|
248
|
+
* adminUI: allOf(requireAuth(), not(requireRoles(['readonly']))),
|
|
469
249
|
* }
|
|
470
250
|
* ```
|
|
471
251
|
*/
|
|
252
|
+
function not(check, reason = "Access denied") {
|
|
253
|
+
return async (ctx) => {
|
|
254
|
+
const result = await check(ctx);
|
|
255
|
+
return (typeof result === "boolean" ? { granted: result } : result).granted ? {
|
|
256
|
+
granted: false,
|
|
257
|
+
reason
|
|
258
|
+
} : true;
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Deny all access.
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ```typescript
|
|
266
|
+
* permissions: { delete: denyAll('Deletion not allowed') }
|
|
267
|
+
* ```
|
|
268
|
+
*/
|
|
472
269
|
function denyAll(reason = "Access denied") {
|
|
473
270
|
return () => ({
|
|
474
271
|
granted: false,
|
|
@@ -476,7 +273,7 @@ function denyAll(reason = "Access denied") {
|
|
|
476
273
|
});
|
|
477
274
|
}
|
|
478
275
|
/**
|
|
479
|
-
* Dynamic permission based on
|
|
276
|
+
* Dynamic permission based on a condition function.
|
|
480
277
|
*
|
|
481
278
|
* @example
|
|
482
279
|
* ```typescript
|
|
@@ -494,26 +291,28 @@ function when(condition) {
|
|
|
494
291
|
};
|
|
495
292
|
};
|
|
496
293
|
}
|
|
294
|
+
//#endregion
|
|
295
|
+
//#region src/permissions/scope.ts
|
|
497
296
|
/**
|
|
498
|
-
*
|
|
499
|
-
* carries org context: `member` (human user with org membership), `service`
|
|
500
|
-
* (API key bound to an org), or `elevated` (platform admin). Denies for
|
|
501
|
-
* `public` and `authenticated` scopes (no org context).
|
|
297
|
+
* Permission Scope — checks bound to RequestScope.
|
|
502
298
|
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
299
|
+
* All read `request.scope` populated by an auth adapter (Better Auth bridge,
|
|
300
|
+
* JWT custom auth, or an upstream permission check returning
|
|
301
|
+
* `PermissionResult.scope`).
|
|
302
|
+
*/
|
|
303
|
+
/**
|
|
304
|
+
* Require an org-bound caller. Grants for `member`, `service`, and
|
|
305
|
+
* `elevated` scopes (anything with org context). Denies `public` and
|
|
306
|
+
* `authenticated` (no org context).
|
|
506
307
|
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
308
|
+
* Canonical "is the caller acting inside an org" check. Usual partner for
|
|
309
|
+
* `multiTenantPreset` — if a route filters by tenant, you almost always
|
|
310
|
+
* want this gate too.
|
|
509
311
|
*
|
|
510
312
|
* @example
|
|
511
313
|
* ```typescript
|
|
512
314
|
* permissions: {
|
|
513
315
|
* list: requireOrgMembership(),
|
|
514
|
-
* get: requireOrgMembership(),
|
|
515
|
-
*
|
|
516
|
-
* // Composed with an OAuth-style scope check for API-key callers
|
|
517
316
|
* create: allOf(requireOrgMembership(), requireServiceScope('jobs:write')),
|
|
518
317
|
* }
|
|
519
318
|
* ```
|
|
@@ -534,28 +333,15 @@ function requireOrgMembership() {
|
|
|
534
333
|
return check;
|
|
535
334
|
}
|
|
536
335
|
/**
|
|
537
|
-
* Require specific org-level roles.
|
|
538
|
-
* Reads `request.scope.orgRoles` (set by auth adapters).
|
|
336
|
+
* Require specific org-level roles. Reads `request.scope.orgRoles`.
|
|
539
337
|
* Elevated scope always passes (platform admin bypass).
|
|
540
338
|
*
|
|
541
339
|
* **Service scopes (API keys) always fail this check** — services don't
|
|
542
|
-
* carry user-style org roles, only OAuth-style `scopes` strings. For
|
|
543
|
-
* that should accept BOTH human admins AND API keys, compose
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
* create: anyOf(
|
|
548
|
-
* requireOrgRole('admin'), // human path
|
|
549
|
-
* requireServiceScope('jobs:write'), // machine path
|
|
550
|
-
* ),
|
|
551
|
-
* }
|
|
552
|
-
* ```
|
|
553
|
-
*
|
|
554
|
-
* This separation is intentional — implicit "API key bypasses role checks"
|
|
555
|
-
* is the kind of footgun that ships data breaches. Services must opt into
|
|
556
|
-
* specific scopes the same way OAuth clients do.
|
|
557
|
-
*
|
|
558
|
-
* @param roles - Required org roles (user needs at least one)
|
|
340
|
+
* carry user-style org roles, only OAuth-style `scopes` strings. For
|
|
341
|
+
* routes that should accept BOTH human admins AND API keys, compose with
|
|
342
|
+
* `anyOf(requireOrgRole(...), requireServiceScope(...))`. The implicit
|
|
343
|
+
* "API key bypasses role check" path is intentionally NOT supported —
|
|
344
|
+
* it's the kind of footgun that ships data breaches.
|
|
559
345
|
*
|
|
560
346
|
* @example
|
|
561
347
|
* ```typescript
|
|
@@ -593,44 +379,24 @@ function requireOrgRole(...args) {
|
|
|
593
379
|
}
|
|
594
380
|
/**
|
|
595
381
|
* Require specific OAuth-style scope strings on a service (API key) identity.
|
|
596
|
-
*
|
|
597
|
-
* Reads `request.scope.scopes` — only populated when the scope kind is
|
|
598
|
-
* `service`. Mirrors how OAuth 2.0 / Better Auth's apiKey plugin / API
|
|
599
|
-
* gateways express machine permissions: a comma- or array-encoded list of
|
|
600
|
-
* scope strings like `'jobs:read'`, `'jobs:write'`, `'memories:*'`.
|
|
382
|
+
* Reads `request.scope.scopes` (only present when `scope.kind === 'service'`).
|
|
601
383
|
*
|
|
602
384
|
* **Pass behavior:**
|
|
603
|
-
* - `service` scope where `scopes` contains ANY
|
|
604
|
-
* - `elevated` scope
|
|
385
|
+
* - `service` scope where `scopes` contains ANY required string → grant
|
|
386
|
+
* - `elevated` scope → grant
|
|
605
387
|
* - Anything else → deny with a clear reason
|
|
606
388
|
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
* ```typescript
|
|
611
|
-
* permissions: {
|
|
612
|
-
* create: anyOf(
|
|
613
|
-
* requireOrgRole('admin'),
|
|
614
|
-
* requireServiceScope('jobs:write'),
|
|
615
|
-
* ),
|
|
616
|
-
* }
|
|
617
|
-
* ```
|
|
618
|
-
*
|
|
619
|
-
* @param scopes - Required scope strings (caller needs at least one)
|
|
389
|
+
* Does **not** grant for `member` scopes — humans go through `requireOrgRole`.
|
|
390
|
+
* For routes that should accept both, compose with `anyOf`.
|
|
620
391
|
*
|
|
621
392
|
* @example
|
|
622
393
|
* ```typescript
|
|
623
|
-
* // Variadic
|
|
624
394
|
* requireServiceScope('jobs:write')
|
|
625
395
|
* requireServiceScope('jobs:read', 'jobs:write')
|
|
626
|
-
*
|
|
627
|
-
* // Array
|
|
628
396
|
* requireServiceScope(['jobs:read', 'jobs:write'])
|
|
629
397
|
*
|
|
630
|
-
* // Composed with org membership for org-scoped API keys
|
|
631
398
|
* permissions: {
|
|
632
399
|
* list: allOf(requireOrgMembership(), requireServiceScope('jobs:read')),
|
|
633
|
-
* create: allOf(requireOrgMembership(), requireServiceScope('jobs:write')),
|
|
634
400
|
* }
|
|
635
401
|
* ```
|
|
636
402
|
*/
|
|
@@ -655,46 +421,26 @@ function requireServiceScope(...args) {
|
|
|
655
421
|
return check;
|
|
656
422
|
}
|
|
657
423
|
/**
|
|
658
|
-
* Require app-defined scope context dimensions (branch, project,
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
*
|
|
662
|
-
* available on `member`, `service`, and `elevated` scope kinds). Arc takes
|
|
663
|
-
* no position on what dimensions you use — you set them, you check them.
|
|
424
|
+
* Require app-defined scope context dimensions (branch, project, region,
|
|
425
|
+
* workspace, …) on the request. Arc takes no position on what dimensions
|
|
426
|
+
* you use — your auth function populates `scope.context`, your routes
|
|
427
|
+
* gate on it.
|
|
664
428
|
*
|
|
665
429
|
* **Three call shapes:**
|
|
666
|
-
*
|
|
667
430
|
* ```typescript
|
|
668
|
-
* //
|
|
669
|
-
* requireScopeContext('branchId')
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
* requireScopeContext('branchId', 'eng-paris')
|
|
673
|
-
*
|
|
674
|
-
* // 3. Multi-key (object form, AND semantics) — every key must match
|
|
675
|
-
* requireScopeContext({ branchId: 'eng-paris', projectId: 'p-123' })
|
|
676
|
-
* requireScopeContext({ region: 'eu', branchId: undefined }) // 'undefined' = presence-only for that key
|
|
431
|
+
* requireScopeContext('branchId') // presence only
|
|
432
|
+
* requireScopeContext('branchId', 'eng-paris') // value match
|
|
433
|
+
* requireScopeContext({ branchId: 'eng-paris', projectId: 'p-1' }) // multi-key (AND)
|
|
434
|
+
* requireScopeContext({ region: 'eu', branchId: undefined }) // mixed
|
|
677
435
|
* ```
|
|
678
436
|
*
|
|
679
|
-
* **Pass behavior:**
|
|
680
|
-
*
|
|
681
|
-
* - `elevated` scope (platform admin) → grant unconditionally (cross-context bypass)
|
|
682
|
-
* - Any required key missing or mismatched → deny with a clear reason
|
|
683
|
-
* - Scope kind without context support (`public`, `authenticated`) → deny
|
|
684
|
-
*
|
|
685
|
-
* Pairs with `multiTenantPreset({ tenantFields: [...] })` for row-level
|
|
686
|
-
* filtering on the same dimensions.
|
|
437
|
+
* **Pass behavior:** all required keys present (and matching when
|
|
438
|
+
* specified) → grant. `elevated` scope grants unconditionally.
|
|
687
439
|
*
|
|
688
440
|
* @example
|
|
689
441
|
* ```typescript
|
|
690
442
|
* permissions: {
|
|
691
|
-
* // Branch-scoped CRUD — caller must have branchId in their scope context
|
|
692
443
|
* list: allOf(requireOrgMembership(), requireScopeContext('branchId')),
|
|
693
|
-
*
|
|
694
|
-
* // Project admin — caller must have BOTH project context AND admin role
|
|
695
|
-
* delete: allOf(requireOrgRole('admin'), requireScopeContext('projectId')),
|
|
696
|
-
*
|
|
697
|
-
* // Region-locked endpoint
|
|
698
444
|
* euOnly: requireScopeContext('region', 'eu'),
|
|
699
445
|
* }
|
|
700
446
|
* ```
|
|
@@ -734,34 +480,20 @@ function requireScopeContext(keyOrMap, value) {
|
|
|
734
480
|
* Require that the caller's scope grants access to a target organization
|
|
735
481
|
* — either the current org or one of its ancestors (`scope.ancestorOrgIds`).
|
|
736
482
|
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
* access to via the chain". Arc takes no position on the source of the
|
|
741
|
-
* chain — your auth function loads `ancestorOrgIds` from your own data
|
|
742
|
-
* model. There's no automatic inheritance: every route opts in explicitly.
|
|
483
|
+
* For parent-child organization hierarchies (holding → subsidiary → branch,
|
|
484
|
+
* MSP → tenant, white-label parent → child). Auth function pre-loads
|
|
485
|
+
* `ancestorOrgIds`; routes opt in explicitly. No automatic inheritance.
|
|
743
486
|
*
|
|
744
487
|
* **Two call shapes:**
|
|
745
|
-
*
|
|
746
488
|
* ```typescript
|
|
747
|
-
* //
|
|
748
|
-
* requireOrgInScope(
|
|
749
|
-
*
|
|
750
|
-
* // Dynamic target — extracted from request params/body/headers per call
|
|
751
|
-
* requireOrgInScope((ctx) => ctx.request.params.orgId)
|
|
752
|
-
* requireOrgInScope((ctx) => ctx.request.body?.organizationId)
|
|
489
|
+
* requireOrgInScope('acme-holding') // static
|
|
490
|
+
* requireOrgInScope((ctx) => ctx.request.params.orgId) // dynamic
|
|
753
491
|
* ```
|
|
754
492
|
*
|
|
755
|
-
*
|
|
756
|
-
* - Target equals `scope.organizationId` → grant
|
|
757
|
-
* - Target appears in `scope.ancestorOrgIds` → grant
|
|
758
|
-
* - `elevated` scope → grant unconditionally (cross-org admin bypass)
|
|
759
|
-
* - Target is undefined (extractor returned nothing) → deny with reason
|
|
760
|
-
* - Anything else → deny with target name in reason
|
|
493
|
+
* `elevated` scope grants unconditionally (cross-org bypass).
|
|
761
494
|
*
|
|
762
495
|
* @example
|
|
763
496
|
* ```typescript
|
|
764
|
-
* // /orgs/:orgId/jobs — caller can act on any org in their hierarchy chain
|
|
765
497
|
* permissions: {
|
|
766
498
|
* list: requireOrgInScope((ctx) => ctx.request.params.orgId),
|
|
767
499
|
* create: allOf(
|
|
@@ -791,8 +523,56 @@ function requireOrgInScope(target) {
|
|
|
791
523
|
return check;
|
|
792
524
|
}
|
|
793
525
|
/**
|
|
794
|
-
*
|
|
795
|
-
*
|
|
526
|
+
* Require membership in the active team. User must be authenticated, a
|
|
527
|
+
* member of the active org, AND have an active team. Better Auth teams
|
|
528
|
+
* are flat member groups (no team-level roles). Reads `request.scope.teamId`.
|
|
529
|
+
*
|
|
530
|
+
* @example
|
|
531
|
+
* ```typescript
|
|
532
|
+
* permissions: {
|
|
533
|
+
* list: requireTeamMembership(),
|
|
534
|
+
* create: requireTeamMembership(),
|
|
535
|
+
* }
|
|
536
|
+
* ```
|
|
537
|
+
*/
|
|
538
|
+
function requireTeamMembership() {
|
|
539
|
+
const check = (ctx) => {
|
|
540
|
+
if (!ctx.user) return {
|
|
541
|
+
granted: false,
|
|
542
|
+
reason: "Authentication required"
|
|
543
|
+
};
|
|
544
|
+
const scope = getRequestScope(ctx.request);
|
|
545
|
+
if (isElevated(scope)) return true;
|
|
546
|
+
if (!isMember(scope)) return {
|
|
547
|
+
granted: false,
|
|
548
|
+
reason: "Organization membership required"
|
|
549
|
+
};
|
|
550
|
+
if (!getTeamId(scope)) return {
|
|
551
|
+
granted: false,
|
|
552
|
+
reason: "No active team"
|
|
553
|
+
};
|
|
554
|
+
return true;
|
|
555
|
+
};
|
|
556
|
+
check._teamPermission = "membership";
|
|
557
|
+
return check;
|
|
558
|
+
}
|
|
559
|
+
//#endregion
|
|
560
|
+
//#region src/permissions/dynamic.ts
|
|
561
|
+
/**
|
|
562
|
+
* Permission Matrices — role × resource × action mapping.
|
|
563
|
+
*
|
|
564
|
+
* Two flavors:
|
|
565
|
+
* - `createOrgPermissions` — static, compile-time-known matrix
|
|
566
|
+
* - `createDynamicPermissionMatrix` — runtime-resolved, with optional
|
|
567
|
+
* cache + cross-node event invalidation
|
|
568
|
+
*
|
|
569
|
+
* Both produce `PermissionCheck` instances that compose with the rest of
|
|
570
|
+
* the permission system.
|
|
571
|
+
*/
|
|
572
|
+
/**
|
|
573
|
+
* Create a static role × resource × action permission system. Compile-time
|
|
574
|
+
* matrix — use when role mappings are known at build time and don't change
|
|
575
|
+
* per-deployment.
|
|
796
576
|
*
|
|
797
577
|
* @example
|
|
798
578
|
* ```typescript
|
|
@@ -856,24 +636,24 @@ function createOrgPermissions(config) {
|
|
|
856
636
|
};
|
|
857
637
|
}
|
|
858
638
|
/**
|
|
859
|
-
* Create a dynamic role-based permission matrix.
|
|
860
|
-
*
|
|
861
|
-
*
|
|
862
|
-
* (e.g., admin UI matrix, DB-stored ACLs, remote policy service).
|
|
639
|
+
* Create a dynamic role-based permission matrix. Use when role/action
|
|
640
|
+
* mappings are managed outside code (admin UI, DB-stored ACLs, remote
|
|
641
|
+
* policy service).
|
|
863
642
|
*
|
|
864
643
|
* Supports:
|
|
865
|
-
* -
|
|
866
|
-
* -
|
|
867
|
-
* -
|
|
868
|
-
* -
|
|
644
|
+
* - Org role union (any assigned org role can grant)
|
|
645
|
+
* - Global bypass roles
|
|
646
|
+
* - Wildcard resource/action (`*`)
|
|
647
|
+
* - Optional in-memory or distributed cache
|
|
648
|
+
* - Cross-node invalidation via the event bus
|
|
869
649
|
*/
|
|
870
650
|
function createDynamicPermissionMatrix(config) {
|
|
871
651
|
const logger = config.logger ?? console;
|
|
872
|
-
const
|
|
652
|
+
const configuredTtlSeconds = config.cache?.ttlSeconds ?? 0;
|
|
873
653
|
const hasExternalStore = !!config.cacheStore;
|
|
874
|
-
const
|
|
875
|
-
const internalStore = !config.cacheStore &&
|
|
876
|
-
|
|
654
|
+
const cacheTtlSeconds = configuredTtlSeconds > 0 ? configuredTtlSeconds : hasExternalStore ? 300 : 0;
|
|
655
|
+
const internalStore = !config.cacheStore && cacheTtlSeconds > 0 ? new MemoryCacheStore({
|
|
656
|
+
defaultTtlSeconds: cacheTtlSeconds,
|
|
877
657
|
maxEntries: config.cache?.maxEntries ?? 1e3
|
|
878
658
|
}) : void 0;
|
|
879
659
|
const cacheStore = config.cacheStore ?? internalStore;
|
|
@@ -881,7 +661,6 @@ function createDynamicPermissionMatrix(config) {
|
|
|
881
661
|
const nodeId = randomUUID().slice(0, 8);
|
|
882
662
|
const DEFAULT_EVENT_TYPE = "arc.permissions.invalidated";
|
|
883
663
|
let eventBridge = null;
|
|
884
|
-
/** Clear local cache for an org without publishing events (avoids infinite loops). */
|
|
885
664
|
async function localInvalidateByOrg(orgId) {
|
|
886
665
|
if (!cacheStore) return;
|
|
887
666
|
const prefix = `${orgId}::`;
|
|
@@ -922,7 +701,7 @@ function createDynamicPermissionMatrix(config) {
|
|
|
922
701
|
}
|
|
923
702
|
const value = await config.resolveRolePermissions(ctx);
|
|
924
703
|
try {
|
|
925
|
-
await cacheStore.set(cacheKey, value,
|
|
704
|
+
await cacheStore.set(cacheKey, value, cacheTtlSeconds);
|
|
926
705
|
trackedKeys.add(cacheKey);
|
|
927
706
|
const maxTracked = config.cache?.maxEntries ?? 1e4;
|
|
928
707
|
if (trackedKeys.size > maxTracked) {
|
|
@@ -1047,41 +826,180 @@ function createDynamicPermissionMatrix(config) {
|
|
|
1047
826
|
}
|
|
1048
827
|
};
|
|
1049
828
|
}
|
|
829
|
+
//#endregion
|
|
830
|
+
//#region src/permissions/roleHierarchy.ts
|
|
1050
831
|
/**
|
|
1051
|
-
*
|
|
1052
|
-
* User must be authenticated, a member of the active org, AND have an active team.
|
|
832
|
+
* Create a role hierarchy from a parent → children map.
|
|
1053
833
|
*
|
|
1054
|
-
*
|
|
1055
|
-
*
|
|
834
|
+
* Each key is a parent role, each value is the array of roles it inherits.
|
|
835
|
+
* Inheritance is transitive: if A → B and B → C, then A expands to [A, B, C].
|
|
836
|
+
* Circular references are handled safely (visited set).
|
|
837
|
+
*/
|
|
838
|
+
function createRoleHierarchy(map) {
|
|
839
|
+
const cache = /* @__PURE__ */ new Map();
|
|
840
|
+
function resolveRole(role, visited) {
|
|
841
|
+
if (visited.has(role)) return [];
|
|
842
|
+
visited.add(role);
|
|
843
|
+
const cached = cache.get(role);
|
|
844
|
+
if (cached) return cached;
|
|
845
|
+
const children = map[role];
|
|
846
|
+
if (!children || children.length === 0) {
|
|
847
|
+
cache.set(role, [role]);
|
|
848
|
+
return [role];
|
|
849
|
+
}
|
|
850
|
+
const result = [role];
|
|
851
|
+
for (const child of children) result.push(...resolveRole(child, visited));
|
|
852
|
+
const deduped = [...new Set(result)];
|
|
853
|
+
cache.set(role, deduped);
|
|
854
|
+
return deduped;
|
|
855
|
+
}
|
|
856
|
+
return {
|
|
857
|
+
expand(roles) {
|
|
858
|
+
if (roles.length === 0) return [];
|
|
859
|
+
const all = /* @__PURE__ */ new Set();
|
|
860
|
+
for (const role of roles) for (const expanded of resolveRole(role, /* @__PURE__ */ new Set())) all.add(expanded);
|
|
861
|
+
return [...all];
|
|
862
|
+
},
|
|
863
|
+
includes(userRoles, requiredRole) {
|
|
864
|
+
if (userRoles.length === 0) return false;
|
|
865
|
+
return this.expand(userRoles).includes(requiredRole);
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
//#endregion
|
|
870
|
+
//#region src/permissions/presets.ts
|
|
871
|
+
/**
|
|
872
|
+
* Permission Presets — Common permission patterns in one call.
|
|
873
|
+
*
|
|
874
|
+
* Reduces 5 lines of permission declarations to 1.
|
|
875
|
+
* Each preset returns a ResourcePermissions object that can be
|
|
876
|
+
* spread or overridden per-operation.
|
|
1056
877
|
*
|
|
1057
878
|
* @example
|
|
1058
879
|
* ```typescript
|
|
1059
|
-
* permissions
|
|
1060
|
-
*
|
|
1061
|
-
*
|
|
1062
|
-
* }
|
|
880
|
+
* import { permissions } from '@classytic/arc';
|
|
881
|
+
*
|
|
882
|
+
* // Public read, authenticated write
|
|
883
|
+
* defineResource({ name: 'product', permissions: permissions.publicRead() });
|
|
884
|
+
*
|
|
885
|
+
* // Override specific operations
|
|
886
|
+
* defineResource({
|
|
887
|
+
* name: 'product',
|
|
888
|
+
* permissions: permissions.publicRead({ delete: requireRoles(['superadmin']) }),
|
|
889
|
+
* });
|
|
1063
890
|
* ```
|
|
1064
891
|
*/
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
892
|
+
var presets_exports = /* @__PURE__ */ __exportAll({
|
|
893
|
+
adminOnly: () => adminOnly,
|
|
894
|
+
authenticated: () => authenticated,
|
|
895
|
+
fullPublic: () => fullPublic,
|
|
896
|
+
ownerWithAdminBypass: () => ownerWithAdminBypass,
|
|
897
|
+
publicRead: () => publicRead,
|
|
898
|
+
publicReadAdminWrite: () => publicReadAdminWrite,
|
|
899
|
+
readOnly: () => readOnly
|
|
900
|
+
});
|
|
901
|
+
/**
|
|
902
|
+
* Merge a base preset with user overrides.
|
|
903
|
+
* Overrides replace individual operations — undefined values don't clear them.
|
|
904
|
+
*/
|
|
905
|
+
function withOverrides(base, overrides) {
|
|
906
|
+
if (!overrides) return base;
|
|
907
|
+
const filtered = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== void 0));
|
|
908
|
+
return {
|
|
909
|
+
...base,
|
|
910
|
+
...filtered
|
|
1082
911
|
};
|
|
1083
|
-
|
|
1084
|
-
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Public read, authenticated write.
|
|
915
|
+
* list + get = allowPublic(), create + update + delete = requireAuth()
|
|
916
|
+
*/
|
|
917
|
+
function publicRead(overrides) {
|
|
918
|
+
return withOverrides({
|
|
919
|
+
list: allowPublic(),
|
|
920
|
+
get: allowPublic(),
|
|
921
|
+
create: requireAuth(),
|
|
922
|
+
update: requireAuth(),
|
|
923
|
+
delete: requireAuth()
|
|
924
|
+
}, overrides);
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Public read, admin write.
|
|
928
|
+
* list + get = allowPublic(), create + update + delete = requireRoles(['admin'])
|
|
929
|
+
*/
|
|
930
|
+
function publicReadAdminWrite(roles = ["admin"], overrides) {
|
|
931
|
+
return withOverrides({
|
|
932
|
+
list: allowPublic(),
|
|
933
|
+
get: allowPublic(),
|
|
934
|
+
create: requireRoles(roles),
|
|
935
|
+
update: requireRoles(roles),
|
|
936
|
+
delete: requireRoles(roles)
|
|
937
|
+
}, overrides);
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* All operations require authentication.
|
|
941
|
+
*/
|
|
942
|
+
function authenticated(overrides) {
|
|
943
|
+
return withOverrides({
|
|
944
|
+
list: requireAuth(),
|
|
945
|
+
get: requireAuth(),
|
|
946
|
+
create: requireAuth(),
|
|
947
|
+
update: requireAuth(),
|
|
948
|
+
delete: requireAuth()
|
|
949
|
+
}, overrides);
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* All operations require specific roles.
|
|
953
|
+
* @param roles - Required roles (user needs at least one). Default: ['admin']
|
|
954
|
+
*/
|
|
955
|
+
function adminOnly(roles = ["admin"], overrides) {
|
|
956
|
+
return withOverrides({
|
|
957
|
+
list: requireRoles(roles),
|
|
958
|
+
get: requireRoles(roles),
|
|
959
|
+
create: requireRoles(roles),
|
|
960
|
+
update: requireRoles(roles),
|
|
961
|
+
delete: requireRoles(roles)
|
|
962
|
+
}, overrides);
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Owner-scoped with admin bypass.
|
|
966
|
+
* list = auth (scoped to owner), get = auth, create = auth,
|
|
967
|
+
* update + delete = ownership check with admin bypass.
|
|
968
|
+
*
|
|
969
|
+
* @param ownerField - Field containing owner ID (default: 'userId')
|
|
970
|
+
* @param bypassRoles - Roles that bypass ownership check (default: ['admin'])
|
|
971
|
+
*/
|
|
972
|
+
function ownerWithAdminBypass(ownerField = "userId", bypassRoles = ["admin"], overrides) {
|
|
973
|
+
return withOverrides({
|
|
974
|
+
list: requireAuth(),
|
|
975
|
+
get: requireAuth(),
|
|
976
|
+
create: requireAuth(),
|
|
977
|
+
update: anyOf(requireRoles(bypassRoles), requireOwnership(ownerField)),
|
|
978
|
+
delete: anyOf(requireRoles(bypassRoles), requireOwnership(ownerField))
|
|
979
|
+
}, overrides);
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Full public access — no auth required for any operation.
|
|
983
|
+
* Use sparingly (dev/testing, truly public APIs).
|
|
984
|
+
*/
|
|
985
|
+
function fullPublic(overrides) {
|
|
986
|
+
return withOverrides({
|
|
987
|
+
list: allowPublic(),
|
|
988
|
+
get: allowPublic(),
|
|
989
|
+
create: allowPublic(),
|
|
990
|
+
update: allowPublic(),
|
|
991
|
+
delete: allowPublic()
|
|
992
|
+
}, overrides);
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Read-only: list + get authenticated, write operations denied.
|
|
996
|
+
* Useful for computed/derived resources.
|
|
997
|
+
*/
|
|
998
|
+
function readOnly(overrides) {
|
|
999
|
+
return withOverrides({
|
|
1000
|
+
list: requireAuth(),
|
|
1001
|
+
get: requireAuth()
|
|
1002
|
+
}, overrides);
|
|
1085
1003
|
}
|
|
1086
1004
|
//#endregion
|
|
1087
|
-
export {
|
|
1005
|
+
export { requireAuth as C, when as D, roles as E, not as S, requireRoles as T, requireTeamMembership as _, presets_exports as a, anyOf as b, readOnly as c, createOrgPermissions as d, requireOrgInScope as f, requireServiceScope as g, requireScopeContext as h, ownerWithAdminBypass as i, createRoleHierarchy as l, requireOrgRole as m, authenticated as n, publicRead as o, requireOrgMembership as p, fullPublic as r, publicReadAdminWrite as s, adminOnly as t, createDynamicPermissionMatrix as u, allOf as v, requireOwnership as w, denyAll as x, allowPublic as y };
|