@classytic/arc 2.9.1 → 2.10.3
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 +19 -90
- package/dist/{BaseController-Vu2yc56T.mjs → BaseController-CbKKIflT.mjs} +8 -44
- package/dist/{ResourceRegistry-Dq3_zBQP.mjs → ResourceRegistry-BPd6NQDm.mjs} +1 -1
- 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 +41 -7
- package/dist/auth/index.d.mts +4 -4
- package/dist/auth/index.mjs +5 -5
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/cache/index.d.mts +17 -15
- package/dist/cache/index.mjs +15 -14
- package/dist/{caching-CjybdRwx.mjs → caching-CBpK_SCM.mjs} +8 -3
- package/dist/cli/commands/describe.mjs +1 -1
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +3 -4
- package/dist/{defineResource-C__jkwvs.mjs → core-CcR01lup.mjs} +44 -12
- package/dist/{createActionRouter-DH1YFL9m.mjs → createActionRouter-Bp_5c_2b.mjs} +1 -1
- package/dist/{createApp-CBJUJKGP.mjs → createApp-BuvPma24.mjs} +14 -14
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-DxQ6ACbt.mjs → elevation-C7hgL_aI.mjs} +2 -2
- package/dist/{errorHandler-CZDW4EXS.mjs → errorHandler-Bb49BvPD.mjs} +1 -1
- package/dist/{errorHandler-DixGcttC.d.mts → errorHandler-DRQ3EqfL.d.mts} +1 -1
- package/dist/{eventPlugin-BxvaCIZF.d.mts → eventPlugin-CxWgpd6K.d.mts} +1 -1
- package/dist/{eventPlugin-Dl7MoVWH.mjs → eventPlugin-DCUjuiQT.mjs} +1 -1
- package/dist/events/index.d.mts +8 -5
- package/dist/events/index.mjs +34 -17
- 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 +2 -2
- package/dist/{types-DZi1aYhm.d.mts → fields-Lo1VUDpt.d.mts} +121 -1
- package/dist/{filesUpload-q8oHt--L.mjs → filesUpload-t21LS-py.mjs} +2 -2
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +7 -4
- package/dist/idempotency/index.mjs +9 -11
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-Cibkchnx.d.mts → index-8qw4y6ff.d.mts} +2 -2
- package/dist/{index-C-xjcA6F.d.mts → index-ChIw3776.d.mts} +283 -408
- package/dist/{interface-YrWsmKqE.d.mts → index-Cl0uoKd5.d.mts} +1885 -2741
- package/dist/{index-CtGKT0lf.d.mts → index-DStwgFUK.d.mts} +81 -7
- package/dist/index.d.mts +7 -8
- package/dist/index.mjs +11 -12
- 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-D218ikEo.d.mts +77 -0
- package/dist/{memory-BFAYkf8H.mjs → memory-B5Amv9A1.mjs} +23 -8
- package/dist/{openapi-CXuTG1M9.mjs → openapi-B5F8AddX.mjs} +2 -2
- 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-Dk6mshja.mjs} +315 -397
- package/dist/plugins/index.d.mts +4 -4
- package/dist/plugins/index.mjs +12 -14
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +3 -3
- package/dist/presets/filesUpload.mjs +1 -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 +1 -1
- package/dist/presets/search.d.mts +91 -4
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-hM4WhNWY.mjs → presets-fLJVXdVn.mjs} +1 -1
- package/dist/{queryCachePlugin-CnTZZTC5.d.mts → queryCachePlugin-BKbWjgDG.d.mts} +1 -1
- package/dist/{queryCachePlugin-DbUVroUG.mjs → queryCachePlugin-DQCEfJis.mjs} +8 -8
- package/dist/{queryParser-Cs-6SHQK.mjs → queryParser-DBqBB6AC.mjs} +1 -1
- package/dist/{redis-MXLp1oOf.d.mts → redis-DqyeggCa.d.mts} +1 -1
- package/dist/{redis-stream-Bz-4q96t.d.mts → redis-stream-CakIQmwR.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{resourceToTools-C3cWymnW.mjs → resourceToTools-BElv3xPT.mjs} +3 -3
- package/dist/scope/index.d.mts +1 -1
- package/dist/scope/index.mjs +2 -2
- package/dist/{sse-CJpt7LGI.mjs → sse-yBCgOLGu.mjs} +1 -1
- package/dist/testing/index.d.mts +6 -5
- package/dist/testing/index.mjs +8 -10
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/index.mjs +1 -31
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-CoSzA-s-.d.mts → types-Btdda02s.d.mts} +1 -1
- package/dist/{types-CunEX4UX.d.mts → types-Co8k3NyS.d.mts} +9 -9
- package/dist/types-Csi3FLfq.mjs +27 -0
- package/dist/utils/index.d.mts +207 -3
- package/dist/utils/index.mjs +3 -4
- package/dist/{utils-B7FuRr9w.mjs → utils-B2fNOD_i.mjs} +285 -2
- package/dist/{versioning-Cm8qoFDg.mjs → versioning-C2U_bLY0.mjs} +3 -5
- package/package.json +15 -18
- package/skills/arc/SKILL.md +7 -11
- 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/fields-BC7zcmI9.d.mts +0 -121
- 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-CUw5NNWe.d.mts} +0 -0
- /package/dist/{HookSystem-BjFu7zf1.mjs → HookSystem-BNYKnrXF.mjs} +0 -0
- /package/dist/{applyPermissionResult-bqGpo9ML.mjs → applyPermissionResult-QhV1Pa-g.mjs} +0 -0
- /package/dist/{betterAuthOpenApi--rdY15Ld.mjs → betterAuthOpenApi-BBRVhjQN.mjs} +0 -0
- /package/dist/{constants-Cxde4rpC.mjs → constants-BhY1OHoH.mjs} +0 -0
- /package/dist/{elevation-B6S5csVA.d.mts → elevation-C5SwtkAn.d.mts} +0 -0
- /package/dist/{errors-BI8kEKsO.d.mts → errors-CCSsMpXE.d.mts} +0 -0
- /package/dist/{errors-CqWnSqM-.mjs → errors-D5c-5BJL.mjs} +0 -0
- /package/dist/{externalPaths-Bapitwvd.d.mts → externalPaths-BQ8QijNH.d.mts} +0 -0
- /package/dist/{fields-CU6FlaDV.mjs → fields-bxkeltzz.mjs} +0 -0
- /package/dist/{interface-B-pe8fhj.d.mts → interface-CSbZdv_3.d.mts} +0 -0
- /package/dist/{loadResources-Bksk8ydA.mjs → loadResources-BAzJItAJ.mjs} +0 -0
- /package/dist/{logger-CDjpjySd.mjs → logger-DLg8-Ueg.mjs} +0 -0
- /package/dist/{metrics-TuOmguhi.mjs → metrics-DuhiSEZI.mjs} +0 -0
- /package/dist/{pluralize-CWP6MB39.mjs → pluralize-A0tWEl1K.mjs} +0 -0
- /package/dist/{registry-B0Wl7uVV.mjs → registry-B3lRFBWo.mjs} +0 -0
- /package/dist/{replyHelpers-BLojtuvR.mjs → replyHelpers-CXtJDAZ0.mjs} +0 -0
- /package/dist/{requestContext-DYtmNpm5.mjs → requestContext-xHIKedG6.mjs} +0 -0
- /package/dist/{sessionManager-D-oNWHz3.d.mts → sessionManager-BkzVU8h2.d.mts} +0 -0
- /package/dist/{storage-BwGQXUpd.d.mts → storage-CVk_SEn2.d.mts} +0 -0
- /package/dist/{store-helpers-DFiZl5TL.mjs → store-helpers-ZCSMJJAX.mjs} +0 -0
- /package/dist/{tracing-xqXzWeaf.d.mts → tracing-65B51Dw3.d.mts} +0 -0
- /package/dist/{types-ZUu_h0jp.mjs → types-DV9WDfeg.mjs} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { r as RequestScope } from "./types-BD85MlEK.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { c as PermissionCheck, l as PermissionContext, u as PermissionResult } from "./fields-Lo1VUDpt.mjs";
|
|
3
|
+
import { r as CacheStore, t as CacheLogger } from "./interface-D218ikEo.mjs";
|
|
4
4
|
import { FastifyRequest } from "fastify";
|
|
5
5
|
|
|
6
6
|
//#region src/permissions/applyPermissionResult.d.ts
|
|
@@ -36,161 +36,9 @@ type RequestSink = FastifyRequest & {
|
|
|
36
36
|
*/
|
|
37
37
|
declare function applyPermissionResult(result: PermissionResult, request: RequestSink): void;
|
|
38
38
|
//#endregion
|
|
39
|
-
//#region src/permissions/
|
|
40
|
-
/**
|
|
41
|
-
* Role Hierarchy — Composable RBAC Inheritance
|
|
42
|
-
*
|
|
43
|
-
* Expands roles based on an inheritance map. Apply at scope-building time
|
|
44
|
-
* so that requireRoles() works with the already-expanded list.
|
|
45
|
-
*
|
|
46
|
-
* @example
|
|
47
|
-
* ```typescript
|
|
48
|
-
* import { createRoleHierarchy } from '@classytic/arc/permissions';
|
|
49
|
-
*
|
|
50
|
-
* const hierarchy = createRoleHierarchy({
|
|
51
|
-
* superadmin: ['admin'],
|
|
52
|
-
* admin: ['branch_manager'],
|
|
53
|
-
* branch_manager: ['member'],
|
|
54
|
-
* });
|
|
55
|
-
*
|
|
56
|
-
* // When building scope:
|
|
57
|
-
* const expandedRoles = hierarchy.expand(user.roles);
|
|
58
|
-
* // ['superadmin'] → ['superadmin', 'admin', 'branch_manager', 'member']
|
|
59
|
-
*
|
|
60
|
-
* // Check inclusion:
|
|
61
|
-
* hierarchy.includes(['admin'], 'branch_manager'); // true (admin inherits branch_manager)
|
|
62
|
-
* hierarchy.includes(['member'], 'admin'); // false (child doesn't inherit parent)
|
|
63
|
-
* ```
|
|
64
|
-
*/
|
|
65
|
-
interface RoleHierarchy {
|
|
66
|
-
/** Expand roles to include all inherited (child) roles. Deduplicated. */
|
|
67
|
-
expand(roles: readonly string[]): string[];
|
|
68
|
-
/** Check if any of the user's roles (expanded) include the required role. */
|
|
69
|
-
includes(userRoles: readonly string[], requiredRole: string): boolean;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Create a role hierarchy from a parent → children map.
|
|
73
|
-
*
|
|
74
|
-
* Each key is a parent role, each value is the array of roles it inherits.
|
|
75
|
-
* Inheritance is transitive: if A → B and B → C, then A expands to [A, B, C].
|
|
76
|
-
* Circular references are handled safely (visited set).
|
|
77
|
-
*/
|
|
78
|
-
declare function createRoleHierarchy(map: Record<string, readonly string[]>): RoleHierarchy;
|
|
79
|
-
declare namespace presets_d_exports {
|
|
80
|
-
export { adminOnly, authenticated, fullPublic, ownerWithAdminBypass, publicRead, publicReadAdminWrite, readOnly };
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* ResourcePermissions shape — matches the type in types/index.ts
|
|
84
|
-
*/
|
|
85
|
-
interface ResourcePermissions<TDoc = any> {
|
|
86
|
-
list?: PermissionCheck<TDoc>;
|
|
87
|
-
get?: PermissionCheck<TDoc>;
|
|
88
|
-
create?: PermissionCheck<TDoc>;
|
|
89
|
-
update?: PermissionCheck<TDoc>;
|
|
90
|
-
delete?: PermissionCheck<TDoc>;
|
|
91
|
-
}
|
|
92
|
-
type PermissionOverrides<TDoc = any> = Partial<ResourcePermissions<TDoc>>;
|
|
93
|
-
/**
|
|
94
|
-
* Public read, authenticated write.
|
|
95
|
-
* list + get = allowPublic(), create + update + delete = requireAuth()
|
|
96
|
-
*/
|
|
97
|
-
declare function publicRead<TDoc = any>(overrides?: PermissionOverrides<TDoc>): ResourcePermissions<TDoc>;
|
|
98
|
-
/**
|
|
99
|
-
* Public read, admin write.
|
|
100
|
-
* list + get = allowPublic(), create + update + delete = requireRoles(['admin'])
|
|
101
|
-
*/
|
|
102
|
-
declare function publicReadAdminWrite<TDoc = any>(roles?: readonly string[], overrides?: PermissionOverrides<TDoc>): ResourcePermissions<TDoc>;
|
|
103
|
-
/**
|
|
104
|
-
* All operations require authentication.
|
|
105
|
-
*/
|
|
106
|
-
declare function authenticated<TDoc = any>(overrides?: PermissionOverrides<TDoc>): ResourcePermissions<TDoc>;
|
|
107
|
-
/**
|
|
108
|
-
* All operations require specific roles.
|
|
109
|
-
* @param roles - Required roles (user needs at least one). Default: ['admin']
|
|
110
|
-
*/
|
|
111
|
-
declare function adminOnly<TDoc = any>(roles?: readonly string[], overrides?: PermissionOverrides<TDoc>): ResourcePermissions<TDoc>;
|
|
112
|
-
/**
|
|
113
|
-
* Owner-scoped with admin bypass.
|
|
114
|
-
* list = auth (scoped to owner), get = auth, create = auth,
|
|
115
|
-
* update + delete = ownership check with admin bypass.
|
|
116
|
-
*
|
|
117
|
-
* @param ownerField - Field containing owner ID (default: 'userId')
|
|
118
|
-
* @param bypassRoles - Roles that bypass ownership check (default: ['admin'])
|
|
119
|
-
*/
|
|
120
|
-
declare function ownerWithAdminBypass<TDoc = any>(ownerField?: Extract<keyof TDoc, string> | string, bypassRoles?: readonly string[], overrides?: PermissionOverrides<TDoc>): ResourcePermissions<TDoc>;
|
|
121
|
-
/**
|
|
122
|
-
* Full public access — no auth required for any operation.
|
|
123
|
-
* Use sparingly (dev/testing, truly public APIs).
|
|
124
|
-
*/
|
|
125
|
-
declare function fullPublic<TDoc = any>(overrides?: PermissionOverrides<TDoc>): ResourcePermissions<TDoc>;
|
|
126
|
-
/**
|
|
127
|
-
* Read-only: list + get authenticated, write operations denied.
|
|
128
|
-
* Useful for computed/derived resources.
|
|
129
|
-
*/
|
|
130
|
-
declare function readOnly<TDoc = any>(overrides?: PermissionOverrides<TDoc>): ResourcePermissions<TDoc>;
|
|
131
|
-
//#endregion
|
|
132
|
-
//#region src/permissions/index.d.ts
|
|
133
|
-
interface DynamicPermissionMatrixConfig {
|
|
134
|
-
/**
|
|
135
|
-
* Resolve role → resource → actions map dynamically (DB/API/config service).
|
|
136
|
-
* Called at permission-check time (or cache miss if cache enabled).
|
|
137
|
-
*/
|
|
138
|
-
resolveRolePermissions: (ctx: PermissionContext) => Record<string, Record<string, readonly string[]>> | Promise<Record<string, Record<string, readonly string[]>>>;
|
|
139
|
-
/**
|
|
140
|
-
* Optional cache store adapter.
|
|
141
|
-
* Use MemoryCacheStore for single-instance apps or RedisCacheStore for distributed setups.
|
|
142
|
-
*/
|
|
143
|
-
cacheStore?: CacheStore<Record<string, Record<string, readonly string[]>>>;
|
|
144
|
-
/** Optional logger for cache/runtime failures (default: console) */
|
|
145
|
-
logger?: CacheLogger;
|
|
146
|
-
/**
|
|
147
|
-
* Legacy convenience in-memory cache config.
|
|
148
|
-
* If `cacheStore` is not provided and ttlMs > 0, Arc creates an internal MemoryCacheStore.
|
|
149
|
-
*/
|
|
150
|
-
cache?: {
|
|
151
|
-
/** Cache TTL in milliseconds */ttlMs: number; /** Optional custom cache key builder */
|
|
152
|
-
key?: (ctx: PermissionContext) => string | null | undefined; /** Hard entry cap for internal memory store (default: 1000) */
|
|
153
|
-
maxEntries?: number;
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
/** Minimal publish/subscribe interface for cross-node cache invalidation. */
|
|
157
|
-
interface PermissionEventBus {
|
|
158
|
-
publish: <T>(type: string, payload: T) => Promise<void>;
|
|
159
|
-
subscribe: (pattern: string, handler: (event: {
|
|
160
|
-
payload: unknown;
|
|
161
|
-
}) => void | Promise<void>) => Promise<(() => void) | undefined>;
|
|
162
|
-
}
|
|
163
|
-
interface ConnectEventsOptions {
|
|
164
|
-
/** Called on remote invalidation for app-specific cleanup (e.g., resolver cache) */
|
|
165
|
-
onRemoteInvalidation?: (orgId: string) => void | Promise<void>;
|
|
166
|
-
/** Custom event type (default: 'arc.permissions.invalidated') */
|
|
167
|
-
eventType?: string;
|
|
168
|
-
}
|
|
169
|
-
interface DynamicPermissionMatrix {
|
|
170
|
-
can: (permissions: Record<string, readonly string[]>) => PermissionCheck;
|
|
171
|
-
canAction: (resource: string, action: string) => PermissionCheck;
|
|
172
|
-
requireRole: (...roles: string[]) => PermissionCheck;
|
|
173
|
-
requireMembership: () => PermissionCheck;
|
|
174
|
-
requireTeamMembership: () => PermissionCheck;
|
|
175
|
-
/** Invalidate cached permissions for a specific organization */
|
|
176
|
-
invalidateByOrg: (orgId: string) => Promise<void>;
|
|
177
|
-
clearCache: () => Promise<void>;
|
|
178
|
-
/**
|
|
179
|
-
* Connect to an event system for cross-node cache invalidation.
|
|
180
|
-
*
|
|
181
|
-
* Late-binding: call after the event plugin is registered (e.g., in onReady hook).
|
|
182
|
-
* Once connected, `invalidateByOrg()` auto-publishes an event, and incoming
|
|
183
|
-
* events from other nodes trigger local cache invalidation.
|
|
184
|
-
* Echo is suppressed via per-process nodeId matching.
|
|
185
|
-
*/
|
|
186
|
-
connectEvents(events: PermissionEventBus, options?: ConnectEventsOptions): Promise<void>;
|
|
187
|
-
/** Disconnect from the event system. Safe to call even if never connected. */
|
|
188
|
-
disconnectEvents(): Promise<void>;
|
|
189
|
-
/** Whether events are currently connected. */
|
|
190
|
-
readonly eventsConnected: boolean;
|
|
191
|
-
}
|
|
39
|
+
//#region src/permissions/core.d.ts
|
|
192
40
|
/**
|
|
193
|
-
* Allow public access (no authentication required)
|
|
41
|
+
* Allow public access (no authentication required).
|
|
194
42
|
*
|
|
195
43
|
* @example
|
|
196
44
|
* ```typescript
|
|
@@ -202,7 +50,7 @@ interface DynamicPermissionMatrix {
|
|
|
202
50
|
*/
|
|
203
51
|
declare function allowPublic(): PermissionCheck;
|
|
204
52
|
/**
|
|
205
|
-
* Require authentication (any authenticated user)
|
|
53
|
+
* Require authentication (any authenticated user).
|
|
206
54
|
*
|
|
207
55
|
* @example
|
|
208
56
|
* ```typescript
|
|
@@ -213,48 +61,23 @@ declare function allowPublic(): PermissionCheck;
|
|
|
213
61
|
* ```
|
|
214
62
|
*/
|
|
215
63
|
declare function requireAuth(): PermissionCheck;
|
|
216
|
-
/**
|
|
217
|
-
* Require specific roles
|
|
218
|
-
*
|
|
219
|
-
* @param roles - Required roles (user needs at least one)
|
|
220
|
-
* @param options - Optional bypass roles
|
|
221
|
-
*
|
|
222
|
-
* @example
|
|
223
|
-
* ```typescript
|
|
224
|
-
* permissions: {
|
|
225
|
-
* create: requireRoles(['admin', 'editor']),
|
|
226
|
-
* delete: requireRoles(['admin']),
|
|
227
|
-
* }
|
|
228
|
-
*
|
|
229
|
-
* // With bypass roles
|
|
230
|
-
* permissions: {
|
|
231
|
-
* update: requireRoles(['owner'], { bypassRoles: ['admin', 'superadmin'] }),
|
|
232
|
-
* }
|
|
233
|
-
* ```
|
|
234
|
-
*/
|
|
235
64
|
/**
|
|
236
65
|
* Require one of the specified roles. Checks BOTH platform roles
|
|
237
66
|
* (`user.role`) AND organization roles (`scope.orgRoles`) by default —
|
|
238
67
|
* passing in either layer grants access. Elevated scope always passes.
|
|
239
68
|
*
|
|
240
69
|
* Accepts EITHER variadic strings OR a single readonly array — both forms
|
|
241
|
-
* produce identical behavior.
|
|
70
|
+
* produce identical behavior.
|
|
242
71
|
*
|
|
243
72
|
* @example
|
|
244
73
|
* ```typescript
|
|
245
|
-
* requireRoles('admin')
|
|
246
|
-
* requireRoles('admin', 'editor')
|
|
247
|
-
* requireRoles(['admin', 'editor'])
|
|
248
|
-
* requireRoles(['admin'], { bypassRoles: ['superadmin'] })
|
|
249
|
-
* requireRoles(['admin'], { includeOrgRoles: false })
|
|
74
|
+
* requireRoles('admin')
|
|
75
|
+
* requireRoles('admin', 'editor')
|
|
76
|
+
* requireRoles(['admin', 'editor'])
|
|
77
|
+
* requireRoles(['admin'], { bypassRoles: ['superadmin'] })
|
|
78
|
+
* requireRoles(['admin'], { includeOrgRoles: false }) // platform-only
|
|
250
79
|
* ```
|
|
251
80
|
*
|
|
252
|
-
* **2.7.1 BREAKING CHANGE:** `includeOrgRoles` now defaults to `true`. The
|
|
253
|
-
* old default (`false`, platform-only) was a footgun for the common case of
|
|
254
|
-
* Better Auth's organization plugin where roles like 'admin' are assigned at
|
|
255
|
-
* the org level. To restore the old behavior explicitly, pass
|
|
256
|
-
* `{ includeOrgRoles: false }`.
|
|
257
|
-
*
|
|
258
81
|
* For org-only role checks, prefer `requireOrgRole('admin')`.
|
|
259
82
|
*/
|
|
260
83
|
declare function requireRoles(role: string, ...rest: string[]): PermissionCheck;
|
|
@@ -262,45 +85,19 @@ declare function requireRoles(roles: readonly string[], options?: {
|
|
|
262
85
|
bypassRoles?: readonly string[];
|
|
263
86
|
/**
|
|
264
87
|
* Also check org membership roles (`scope.orgRoles`) when in org context.
|
|
265
|
-
* Default: `true`
|
|
266
|
-
*
|
|
267
|
-
* Set to `false` to restore the pre-2.7.1 behavior of checking only
|
|
268
|
-
* platform roles (`user.role`). For org-only role checks, prefer
|
|
269
|
-
* `requireOrgRole('admin')` instead.
|
|
88
|
+
* Default: `true`. Set to `false` to check only platform roles.
|
|
270
89
|
*/
|
|
271
90
|
includeOrgRoles?: boolean;
|
|
272
91
|
}): PermissionCheck;
|
|
273
92
|
/**
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
* means `roles('admin')` and `requireRoles('admin')` are now functionally
|
|
278
|
-
* identical. This helper is preserved for backwards compatibility and for
|
|
279
|
-
* call sites that prefer the shorter `roles()` name.
|
|
280
|
-
*
|
|
281
|
-
* **For new code, prefer `requireRoles()`** — it's the canonical name and
|
|
282
|
-
* matches the rest of the `requireXxx()` family (`requireAuth`, `requireOwnership`,
|
|
283
|
-
* `requireOrgRole`, etc.).
|
|
284
|
-
*
|
|
285
|
-
* For platform-only checks: `requireRoles(['admin'], { includeOrgRoles: false })`
|
|
286
|
-
* For org-only checks: `requireOrgRole('admin')`
|
|
287
|
-
*
|
|
288
|
-
* @example
|
|
289
|
-
* ```typescript
|
|
290
|
-
* // These are identical:
|
|
291
|
-
* roles('admin', 'editor')
|
|
292
|
-
* requireRoles('admin', 'editor')
|
|
293
|
-
* requireRoles(['admin', 'editor'])
|
|
294
|
-
* ```
|
|
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.
|
|
295
96
|
*/
|
|
296
97
|
declare function roles(...args: string[] | [readonly string[]]): PermissionCheck;
|
|
297
98
|
/**
|
|
298
|
-
* Require resource ownership
|
|
299
|
-
*
|
|
300
|
-
* Returns filters to scope queries to user's owned resources.
|
|
301
|
-
*
|
|
302
|
-
* @param ownerField - Field containing owner ID (default: 'userId')
|
|
303
|
-
* @param options - Optional bypass roles
|
|
99
|
+
* Require resource ownership. Returns filters to scope queries to the
|
|
100
|
+
* caller's owned resources.
|
|
304
101
|
*
|
|
305
102
|
* @example
|
|
306
103
|
* ```typescript
|
|
@@ -314,65 +111,62 @@ declare function requireOwnership<TDoc = Record<string, unknown>>(ownerField?: E
|
|
|
314
111
|
bypassRoles?: readonly string[];
|
|
315
112
|
}): PermissionCheck<TDoc>;
|
|
316
113
|
/**
|
|
317
|
-
* Combine multiple checks
|
|
114
|
+
* Combine multiple checks — ALL must pass (AND logic).
|
|
318
115
|
*
|
|
319
116
|
* Each child runs against the **accumulated** state of previous children:
|
|
320
|
-
* - `filters` from earlier children
|
|
321
|
-
*
|
|
322
|
-
* - `scope` from earlier children is installed on the request before the
|
|
323
|
-
* next child runs (so e.g. `requireOrgMembership` after `requireApiKey`
|
|
324
|
-
* sees the service scope from the API key check)
|
|
117
|
+
* - `filters` from earlier children merge into the next child's `_policyFilters`
|
|
118
|
+
* - `scope` from earlier children installs on the request before the next child runs
|
|
325
119
|
*
|
|
326
|
-
* The final
|
|
327
|
-
* the merged `scope`, so the outer middleware's `applyPermissionResult` call
|
|
328
|
-
* sees the same end-state.
|
|
120
|
+
* The final result carries both merged `filters` and merged `scope`.
|
|
329
121
|
*
|
|
330
122
|
* @example
|
|
331
123
|
* ```typescript
|
|
332
|
-
* // CRUD permissions composed across roles + ownership
|
|
333
|
-
* permissions: {
|
|
334
|
-
* update: allOf(
|
|
335
|
-
* requireAuth(),
|
|
336
|
-
* requireRoles(['editor']),
|
|
337
|
-
* requireOwnership('createdBy')
|
|
338
|
-
* ),
|
|
339
|
-
* }
|
|
340
|
-
*
|
|
341
|
-
* // Custom auth + org membership — first check installs the scope,
|
|
342
|
-
* // second check reads it.
|
|
343
124
|
* permissions: {
|
|
125
|
+
* update: allOf(requireAuth(), requireRoles(['editor']), requireOwnership('createdBy')),
|
|
344
126
|
* list: allOf(requireApiKey(), requireOrgMembership()),
|
|
345
127
|
* }
|
|
346
128
|
* ```
|
|
347
129
|
*/
|
|
348
130
|
declare function allOf(...checks: PermissionCheck[]): PermissionCheck;
|
|
349
131
|
/**
|
|
350
|
-
* Combine multiple checks
|
|
132
|
+
* Combine multiple checks — ANY must pass (OR logic).
|
|
351
133
|
*
|
|
352
134
|
* @example
|
|
353
135
|
* ```typescript
|
|
354
136
|
* permissions: {
|
|
355
|
-
* update: anyOf(
|
|
356
|
-
* requireRoles(['admin']),
|
|
357
|
-
* requireOwnership('createdBy')
|
|
358
|
-
* ),
|
|
137
|
+
* update: anyOf(requireRoles(['admin']), requireOwnership('createdBy')),
|
|
359
138
|
* }
|
|
360
139
|
* ```
|
|
361
140
|
*/
|
|
362
141
|
declare function anyOf(...checks: PermissionCheck[]): PermissionCheck;
|
|
363
142
|
/**
|
|
364
|
-
*
|
|
143
|
+
* Invert a permission check. Grants when the wrapped check denies, denies
|
|
144
|
+
* when the wrapped check grants. Useful for "block if X" patterns —
|
|
145
|
+
* e.g. `not(requireRoles(['guest']))` to deny guest access.
|
|
146
|
+
*
|
|
147
|
+
* NOTE: filters and scope from the wrapped check are intentionally
|
|
148
|
+
* discarded — an inverted check has no row-level meaning.
|
|
365
149
|
*
|
|
366
150
|
* @example
|
|
367
151
|
* ```typescript
|
|
368
152
|
* permissions: {
|
|
369
|
-
*
|
|
153
|
+
* internalApi: not(requireRoles(['external'])),
|
|
154
|
+
* adminUI: allOf(requireAuth(), not(requireRoles(['readonly']))),
|
|
370
155
|
* }
|
|
371
156
|
* ```
|
|
372
157
|
*/
|
|
158
|
+
declare function not(check: PermissionCheck, reason?: string): PermissionCheck;
|
|
159
|
+
/**
|
|
160
|
+
* Deny all access.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```typescript
|
|
164
|
+
* permissions: { delete: denyAll('Deletion not allowed') }
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
373
167
|
declare function denyAll(reason?: string): PermissionCheck;
|
|
374
168
|
/**
|
|
375
|
-
* Dynamic permission based on
|
|
169
|
+
* Dynamic permission based on a condition function.
|
|
376
170
|
*
|
|
377
171
|
* @example
|
|
378
172
|
* ```typescript
|
|
@@ -382,54 +176,187 @@ declare function denyAll(reason?: string): PermissionCheck;
|
|
|
382
176
|
* ```
|
|
383
177
|
*/
|
|
384
178
|
declare function when<TDoc = Record<string, unknown>>(condition: (ctx: PermissionContext<TDoc>) => boolean | Promise<boolean>): PermissionCheck<TDoc>;
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/permissions/dynamic.d.ts
|
|
181
|
+
interface DynamicPermissionMatrixConfig {
|
|
182
|
+
/**
|
|
183
|
+
* Resolve role → resource → actions map dynamically (DB / API / config service).
|
|
184
|
+
* Called at permission-check time (or cache miss when cache enabled).
|
|
185
|
+
*/
|
|
186
|
+
resolveRolePermissions: (ctx: PermissionContext) => Record<string, Record<string, readonly string[]>> | Promise<Record<string, Record<string, readonly string[]>>>;
|
|
187
|
+
/**
|
|
188
|
+
* Optional cache store adapter. Use MemoryCacheStore for single-instance
|
|
189
|
+
* apps, RedisCacheStore for distributed setups.
|
|
190
|
+
*/
|
|
191
|
+
cacheStore?: CacheStore<Record<string, Record<string, readonly string[]>>>;
|
|
192
|
+
/** Optional logger for cache/runtime failures (default: console). */
|
|
193
|
+
logger?: CacheLogger;
|
|
194
|
+
/**
|
|
195
|
+
* Convenience in-memory cache config. If `cacheStore` is not provided
|
|
196
|
+
* and `ttlSeconds > 0`, Arc creates an internal MemoryCacheStore.
|
|
197
|
+
*/
|
|
198
|
+
cache?: {
|
|
199
|
+
/** Cache TTL in seconds */ttlSeconds: number; /** Optional custom cache key builder */
|
|
200
|
+
key?: (ctx: PermissionContext) => string | null | undefined; /** Hard entry cap for internal memory store (default: 1000) */
|
|
201
|
+
maxEntries?: number;
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/** Minimal publish/subscribe interface for cross-node cache invalidation. */
|
|
205
|
+
interface PermissionEventBus {
|
|
206
|
+
publish: <T>(type: string, payload: T) => Promise<void>;
|
|
207
|
+
subscribe: (pattern: string, handler: (event: {
|
|
208
|
+
payload: unknown;
|
|
209
|
+
}) => void | Promise<void>) => Promise<(() => void) | undefined>;
|
|
210
|
+
}
|
|
211
|
+
interface ConnectEventsOptions {
|
|
212
|
+
/** Called on remote invalidation for app-specific cleanup (e.g. resolver cache). */
|
|
213
|
+
onRemoteInvalidation?: (orgId: string) => void | Promise<void>;
|
|
214
|
+
/** Custom event type (default: 'arc.permissions.invalidated'). */
|
|
215
|
+
eventType?: string;
|
|
216
|
+
}
|
|
217
|
+
interface DynamicPermissionMatrix {
|
|
218
|
+
can: (permissions: Record<string, readonly string[]>) => PermissionCheck;
|
|
219
|
+
canAction: (resource: string, action: string) => PermissionCheck;
|
|
220
|
+
requireRole: (...roles: string[]) => PermissionCheck;
|
|
221
|
+
requireMembership: () => PermissionCheck;
|
|
222
|
+
requireTeamMembership: () => PermissionCheck;
|
|
223
|
+
/** Invalidate cached permissions for a specific organization. */
|
|
224
|
+
invalidateByOrg: (orgId: string) => Promise<void>;
|
|
225
|
+
clearCache: () => Promise<void>;
|
|
226
|
+
/**
|
|
227
|
+
* Connect to an event system for cross-node cache invalidation.
|
|
228
|
+
*
|
|
229
|
+
* Late-binding: call after the event plugin is registered (e.g. in an
|
|
230
|
+
* `onReady` hook). Once connected, `invalidateByOrg()` auto-publishes an
|
|
231
|
+
* event, and incoming events from other nodes trigger local cache
|
|
232
|
+
* invalidation. Echo is suppressed via per-process nodeId matching.
|
|
233
|
+
*/
|
|
234
|
+
connectEvents(events: PermissionEventBus, options?: ConnectEventsOptions): Promise<void>;
|
|
235
|
+
/** Disconnect from the event system. Safe to call even if never connected. */
|
|
236
|
+
disconnectEvents(): Promise<void>;
|
|
237
|
+
/** Whether events are currently connected. */
|
|
238
|
+
readonly eventsConnected: boolean;
|
|
239
|
+
}
|
|
385
240
|
/**
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
* `public` and `authenticated` scopes (no org context).
|
|
241
|
+
* Create a static role × resource × action permission system. Compile-time
|
|
242
|
+
* matrix — use when role mappings are known at build time and don't change
|
|
243
|
+
* per-deployment.
|
|
390
244
|
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```typescript
|
|
247
|
+
* const perms = createOrgPermissions({
|
|
248
|
+
* statements: {
|
|
249
|
+
* product: ['create', 'update', 'delete'],
|
|
250
|
+
* order: ['create', 'approve'],
|
|
251
|
+
* },
|
|
252
|
+
* roles: {
|
|
253
|
+
* owner: { product: ['create', 'update', 'delete'], order: ['create', 'approve'] },
|
|
254
|
+
* admin: { product: ['create', 'update'], order: ['create'] },
|
|
255
|
+
* member: { product: [], order: [] },
|
|
256
|
+
* },
|
|
257
|
+
* });
|
|
394
258
|
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
259
|
+
* defineResource({
|
|
260
|
+
* permissions: {
|
|
261
|
+
* create: perms.can({ product: ['create'] }),
|
|
262
|
+
* delete: perms.can({ product: ['delete'] }),
|
|
263
|
+
* }
|
|
264
|
+
* });
|
|
265
|
+
* ```
|
|
266
|
+
*/
|
|
267
|
+
declare function createOrgPermissions(config: {
|
|
268
|
+
statements: Record<string, readonly string[]>;
|
|
269
|
+
roles: Record<string, Record<string, readonly string[]>>;
|
|
270
|
+
}): {
|
|
271
|
+
can: (permissions: Record<string, string[]>) => PermissionCheck;
|
|
272
|
+
requireRole: (...roles: string[]) => PermissionCheck;
|
|
273
|
+
requireMembership: () => PermissionCheck;
|
|
274
|
+
requireTeamMembership: () => PermissionCheck;
|
|
275
|
+
};
|
|
276
|
+
/**
|
|
277
|
+
* Create a dynamic role-based permission matrix. Use when role/action
|
|
278
|
+
* mappings are managed outside code (admin UI, DB-stored ACLs, remote
|
|
279
|
+
* policy service).
|
|
280
|
+
*
|
|
281
|
+
* Supports:
|
|
282
|
+
* - Org role union (any assigned org role can grant)
|
|
283
|
+
* - Global bypass roles
|
|
284
|
+
* - Wildcard resource/action (`*`)
|
|
285
|
+
* - Optional in-memory or distributed cache
|
|
286
|
+
* - Cross-node invalidation via the event bus
|
|
287
|
+
*/
|
|
288
|
+
declare function createDynamicPermissionMatrix(config: DynamicPermissionMatrixConfig): DynamicPermissionMatrix;
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/permissions/roleHierarchy.d.ts
|
|
291
|
+
/**
|
|
292
|
+
* Role Hierarchy — Composable RBAC Inheritance
|
|
293
|
+
*
|
|
294
|
+
* Expands roles based on an inheritance map. Apply at scope-building time
|
|
295
|
+
* so that requireRoles() works with the already-expanded list.
|
|
397
296
|
*
|
|
398
297
|
* @example
|
|
399
298
|
* ```typescript
|
|
400
|
-
*
|
|
401
|
-
* list: requireOrgMembership(),
|
|
402
|
-
* get: requireOrgMembership(),
|
|
299
|
+
* import { createRoleHierarchy } from '@classytic/arc/permissions';
|
|
403
300
|
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
301
|
+
* const hierarchy = createRoleHierarchy({
|
|
302
|
+
* superadmin: ['admin'],
|
|
303
|
+
* admin: ['branch_manager'],
|
|
304
|
+
* branch_manager: ['member'],
|
|
305
|
+
* });
|
|
306
|
+
*
|
|
307
|
+
* // When building scope:
|
|
308
|
+
* const expandedRoles = hierarchy.expand(user.roles);
|
|
309
|
+
* // ['superadmin'] → ['superadmin', 'admin', 'branch_manager', 'member']
|
|
310
|
+
*
|
|
311
|
+
* // Check inclusion:
|
|
312
|
+
* hierarchy.includes(['admin'], 'branch_manager'); // true (admin inherits branch_manager)
|
|
313
|
+
* hierarchy.includes(['member'], 'admin'); // false (child doesn't inherit parent)
|
|
407
314
|
* ```
|
|
408
315
|
*/
|
|
409
|
-
|
|
316
|
+
interface RoleHierarchy {
|
|
317
|
+
/** Expand roles to include all inherited (child) roles. Deduplicated. */
|
|
318
|
+
expand(roles: readonly string[]): string[];
|
|
319
|
+
/** Check if any of the user's roles (expanded) include the required role. */
|
|
320
|
+
includes(userRoles: readonly string[], requiredRole: string): boolean;
|
|
321
|
+
}
|
|
410
322
|
/**
|
|
411
|
-
*
|
|
412
|
-
* Reads `request.scope.orgRoles` (set by auth adapters).
|
|
413
|
-
* Elevated scope always passes (platform admin bypass).
|
|
323
|
+
* Create a role hierarchy from a parent → children map.
|
|
414
324
|
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
325
|
+
* Each key is a parent role, each value is the array of roles it inherits.
|
|
326
|
+
* Inheritance is transitive: if A → B and B → C, then A expands to [A, B, C].
|
|
327
|
+
* Circular references are handled safely (visited set).
|
|
328
|
+
*/
|
|
329
|
+
declare function createRoleHierarchy(map: Record<string, readonly string[]>): RoleHierarchy;
|
|
330
|
+
//#endregion
|
|
331
|
+
//#region src/permissions/scope.d.ts
|
|
332
|
+
/**
|
|
333
|
+
* Require an org-bound caller. Grants for `member`, `service`, and
|
|
334
|
+
* `elevated` scopes (anything with org context). Denies `public` and
|
|
335
|
+
* `authenticated` (no org context).
|
|
418
336
|
*
|
|
337
|
+
* Canonical "is the caller acting inside an org" check. Usual partner for
|
|
338
|
+
* `multiTenantPreset` — if a route filters by tenant, you almost always
|
|
339
|
+
* want this gate too.
|
|
340
|
+
*
|
|
341
|
+
* @example
|
|
419
342
|
* ```typescript
|
|
420
343
|
* permissions: {
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
* requireServiceScope('jobs:write'), // machine path
|
|
424
|
-
* ),
|
|
344
|
+
* list: requireOrgMembership(),
|
|
345
|
+
* create: allOf(requireOrgMembership(), requireServiceScope('jobs:write')),
|
|
425
346
|
* }
|
|
426
347
|
* ```
|
|
348
|
+
*/
|
|
349
|
+
declare function requireOrgMembership<TDoc = Record<string, unknown>>(): PermissionCheck<TDoc>;
|
|
350
|
+
/**
|
|
351
|
+
* Require specific org-level roles. Reads `request.scope.orgRoles`.
|
|
352
|
+
* Elevated scope always passes (platform admin bypass).
|
|
427
353
|
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
354
|
+
* **Service scopes (API keys) always fail this check** — services don't
|
|
355
|
+
* carry user-style org roles, only OAuth-style `scopes` strings. For
|
|
356
|
+
* routes that should accept BOTH human admins AND API keys, compose with
|
|
357
|
+
* `anyOf(requireOrgRole(...), requireServiceScope(...))`. The implicit
|
|
358
|
+
* "API key bypasses role check" path is intentionally NOT supported —
|
|
359
|
+
* it's the kind of footgun that ships data breaches.
|
|
433
360
|
*
|
|
434
361
|
* @example
|
|
435
362
|
* ```typescript
|
|
@@ -442,89 +369,49 @@ declare function requireOrgMembership<TDoc = Record<string, unknown>>(): Permiss
|
|
|
442
369
|
declare function requireOrgRole<TDoc = Record<string, unknown>>(...args: string[] | [readonly string[]]): PermissionCheck<TDoc>;
|
|
443
370
|
/**
|
|
444
371
|
* Require specific OAuth-style scope strings on a service (API key) identity.
|
|
445
|
-
*
|
|
446
|
-
* Reads `request.scope.scopes` — only populated when the scope kind is
|
|
447
|
-
* `service`. Mirrors how OAuth 2.0 / Better Auth's apiKey plugin / API
|
|
448
|
-
* gateways express machine permissions: a comma- or array-encoded list of
|
|
449
|
-
* scope strings like `'jobs:read'`, `'jobs:write'`, `'memories:*'`.
|
|
372
|
+
* Reads `request.scope.scopes` (only present when `scope.kind === 'service'`).
|
|
450
373
|
*
|
|
451
374
|
* **Pass behavior:**
|
|
452
|
-
* - `service` scope where `scopes` contains ANY
|
|
453
|
-
* - `elevated` scope
|
|
375
|
+
* - `service` scope where `scopes` contains ANY required string → grant
|
|
376
|
+
* - `elevated` scope → grant
|
|
454
377
|
* - Anything else → deny with a clear reason
|
|
455
378
|
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
* ```typescript
|
|
460
|
-
* permissions: {
|
|
461
|
-
* create: anyOf(
|
|
462
|
-
* requireOrgRole('admin'),
|
|
463
|
-
* requireServiceScope('jobs:write'),
|
|
464
|
-
* ),
|
|
465
|
-
* }
|
|
466
|
-
* ```
|
|
467
|
-
*
|
|
468
|
-
* @param scopes - Required scope strings (caller needs at least one)
|
|
379
|
+
* Does **not** grant for `member` scopes — humans go through `requireOrgRole`.
|
|
380
|
+
* For routes that should accept both, compose with `anyOf`.
|
|
469
381
|
*
|
|
470
382
|
* @example
|
|
471
383
|
* ```typescript
|
|
472
|
-
* // Variadic
|
|
473
384
|
* requireServiceScope('jobs:write')
|
|
474
385
|
* requireServiceScope('jobs:read', 'jobs:write')
|
|
475
|
-
*
|
|
476
|
-
* // Array
|
|
477
386
|
* requireServiceScope(['jobs:read', 'jobs:write'])
|
|
478
387
|
*
|
|
479
|
-
* // Composed with org membership for org-scoped API keys
|
|
480
388
|
* permissions: {
|
|
481
389
|
* list: allOf(requireOrgMembership(), requireServiceScope('jobs:read')),
|
|
482
|
-
* create: allOf(requireOrgMembership(), requireServiceScope('jobs:write')),
|
|
483
390
|
* }
|
|
484
391
|
* ```
|
|
485
392
|
*/
|
|
486
393
|
declare function requireServiceScope<TDoc = Record<string, unknown>>(...args: string[] | [readonly string[]]): PermissionCheck<TDoc>;
|
|
487
394
|
/**
|
|
488
|
-
* Require app-defined scope context dimensions (branch, project,
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
* available on `member`, `service`, and `elevated` scope kinds). Arc takes
|
|
493
|
-
* no position on what dimensions you use — you set them, you check them.
|
|
395
|
+
* Require app-defined scope context dimensions (branch, project, region,
|
|
396
|
+
* workspace, …) on the request. Arc takes no position on what dimensions
|
|
397
|
+
* you use — your auth function populates `scope.context`, your routes
|
|
398
|
+
* gate on it.
|
|
494
399
|
*
|
|
495
400
|
* **Three call shapes:**
|
|
496
|
-
*
|
|
497
401
|
* ```typescript
|
|
498
|
-
* //
|
|
499
|
-
* requireScopeContext('branchId')
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
* requireScopeContext('branchId', 'eng-paris')
|
|
503
|
-
*
|
|
504
|
-
* // 3. Multi-key (object form, AND semantics) — every key must match
|
|
505
|
-
* requireScopeContext({ branchId: 'eng-paris', projectId: 'p-123' })
|
|
506
|
-
* requireScopeContext({ region: 'eu', branchId: undefined }) // 'undefined' = presence-only for that key
|
|
402
|
+
* requireScopeContext('branchId') // presence only
|
|
403
|
+
* requireScopeContext('branchId', 'eng-paris') // value match
|
|
404
|
+
* requireScopeContext({ branchId: 'eng-paris', projectId: 'p-1' }) // multi-key (AND)
|
|
405
|
+
* requireScopeContext({ region: 'eu', branchId: undefined }) // mixed
|
|
507
406
|
* ```
|
|
508
407
|
*
|
|
509
|
-
* **Pass behavior:**
|
|
510
|
-
*
|
|
511
|
-
* - `elevated` scope (platform admin) → grant unconditionally (cross-context bypass)
|
|
512
|
-
* - Any required key missing or mismatched → deny with a clear reason
|
|
513
|
-
* - Scope kind without context support (`public`, `authenticated`) → deny
|
|
514
|
-
*
|
|
515
|
-
* Pairs with `multiTenantPreset({ tenantFields: [...] })` for row-level
|
|
516
|
-
* filtering on the same dimensions.
|
|
408
|
+
* **Pass behavior:** all required keys present (and matching when
|
|
409
|
+
* specified) → grant. `elevated` scope grants unconditionally.
|
|
517
410
|
*
|
|
518
411
|
* @example
|
|
519
412
|
* ```typescript
|
|
520
413
|
* permissions: {
|
|
521
|
-
* // Branch-scoped CRUD — caller must have branchId in their scope context
|
|
522
414
|
* list: allOf(requireOrgMembership(), requireScopeContext('branchId')),
|
|
523
|
-
*
|
|
524
|
-
* // Project admin — caller must have BOTH project context AND admin role
|
|
525
|
-
* delete: allOf(requireOrgRole('admin'), requireScopeContext('projectId')),
|
|
526
|
-
*
|
|
527
|
-
* // Region-locked endpoint
|
|
528
415
|
* euOnly: requireScopeContext('region', 'eu'),
|
|
529
416
|
* }
|
|
530
417
|
* ```
|
|
@@ -534,34 +421,20 @@ declare function requireScopeContext<TDoc = Record<string, unknown>>(keyOrMap: s
|
|
|
534
421
|
* Require that the caller's scope grants access to a target organization
|
|
535
422
|
* — either the current org or one of its ancestors (`scope.ancestorOrgIds`).
|
|
536
423
|
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
* access to via the chain". Arc takes no position on the source of the
|
|
541
|
-
* chain — your auth function loads `ancestorOrgIds` from your own data
|
|
542
|
-
* model. There's no automatic inheritance: every route opts in explicitly.
|
|
424
|
+
* For parent-child organization hierarchies (holding → subsidiary → branch,
|
|
425
|
+
* MSP → tenant, white-label parent → child). Auth function pre-loads
|
|
426
|
+
* `ancestorOrgIds`; routes opt in explicitly. No automatic inheritance.
|
|
543
427
|
*
|
|
544
428
|
* **Two call shapes:**
|
|
545
|
-
*
|
|
546
429
|
* ```typescript
|
|
547
|
-
* //
|
|
548
|
-
* requireOrgInScope(
|
|
549
|
-
*
|
|
550
|
-
* // Dynamic target — extracted from request params/body/headers per call
|
|
551
|
-
* requireOrgInScope((ctx) => ctx.request.params.orgId)
|
|
552
|
-
* requireOrgInScope((ctx) => ctx.request.body?.organizationId)
|
|
430
|
+
* requireOrgInScope('acme-holding') // static
|
|
431
|
+
* requireOrgInScope((ctx) => ctx.request.params.orgId) // dynamic
|
|
553
432
|
* ```
|
|
554
433
|
*
|
|
555
|
-
*
|
|
556
|
-
* - Target equals `scope.organizationId` → grant
|
|
557
|
-
* - Target appears in `scope.ancestorOrgIds` → grant
|
|
558
|
-
* - `elevated` scope → grant unconditionally (cross-org admin bypass)
|
|
559
|
-
* - Target is undefined (extractor returned nothing) → deny with reason
|
|
560
|
-
* - Anything else → deny with target name in reason
|
|
434
|
+
* `elevated` scope grants unconditionally (cross-org bypass).
|
|
561
435
|
*
|
|
562
436
|
* @example
|
|
563
437
|
* ```typescript
|
|
564
|
-
* // /orgs/:orgId/jobs — caller can act on any org in their hierarchy chain
|
|
565
438
|
* permissions: {
|
|
566
439
|
* list: requireOrgInScope((ctx) => ctx.request.params.orgId),
|
|
567
440
|
* create: allOf(
|
|
@@ -573,59 +446,9 @@ declare function requireScopeContext<TDoc = Record<string, unknown>>(keyOrMap: s
|
|
|
573
446
|
*/
|
|
574
447
|
declare function requireOrgInScope<TDoc = Record<string, unknown>>(target: string | ((ctx: PermissionContext<TDoc>) => string | undefined)): PermissionCheck<TDoc>;
|
|
575
448
|
/**
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
* @example
|
|
580
|
-
* ```typescript
|
|
581
|
-
* const perms = createOrgPermissions({
|
|
582
|
-
* statements: {
|
|
583
|
-
* product: ['create', 'update', 'delete'],
|
|
584
|
-
* order: ['create', 'approve'],
|
|
585
|
-
* },
|
|
586
|
-
* roles: {
|
|
587
|
-
* owner: { product: ['create', 'update', 'delete'], order: ['create', 'approve'] },
|
|
588
|
-
* admin: { product: ['create', 'update'], order: ['create'] },
|
|
589
|
-
* member: { product: [], order: [] },
|
|
590
|
-
* },
|
|
591
|
-
* });
|
|
592
|
-
*
|
|
593
|
-
* defineResource({
|
|
594
|
-
* permissions: {
|
|
595
|
-
* create: perms.can({ product: ['create'] }),
|
|
596
|
-
* delete: perms.can({ product: ['delete'] }),
|
|
597
|
-
* }
|
|
598
|
-
* });
|
|
599
|
-
* ```
|
|
600
|
-
*/
|
|
601
|
-
declare function createOrgPermissions(config: {
|
|
602
|
-
statements: Record<string, readonly string[]>;
|
|
603
|
-
roles: Record<string, Record<string, readonly string[]>>;
|
|
604
|
-
}): {
|
|
605
|
-
can: (permissions: Record<string, string[]>) => PermissionCheck;
|
|
606
|
-
requireRole: (...roles: string[]) => PermissionCheck;
|
|
607
|
-
requireMembership: () => PermissionCheck;
|
|
608
|
-
requireTeamMembership: () => PermissionCheck;
|
|
609
|
-
};
|
|
610
|
-
/**
|
|
611
|
-
* Create a dynamic role-based permission matrix.
|
|
612
|
-
*
|
|
613
|
-
* Use this when role/action mappings are managed outside code
|
|
614
|
-
* (e.g., admin UI matrix, DB-stored ACLs, remote policy service).
|
|
615
|
-
*
|
|
616
|
-
* Supports:
|
|
617
|
-
* - org role union (any assigned org role can grant)
|
|
618
|
-
* - global bypass roles
|
|
619
|
-
* - wildcard resource/action (`*`)
|
|
620
|
-
* - optional in-memory cache
|
|
621
|
-
*/
|
|
622
|
-
declare function createDynamicPermissionMatrix(config: DynamicPermissionMatrixConfig): DynamicPermissionMatrix;
|
|
623
|
-
/**
|
|
624
|
-
* Require membership in the active team.
|
|
625
|
-
* User must be authenticated, a member of the active org, AND have an active team.
|
|
626
|
-
*
|
|
627
|
-
* Better Auth teams are flat member groups (no team-level roles).
|
|
628
|
-
* Reads `request.scope.teamId` set by the Better Auth adapter.
|
|
449
|
+
* Require membership in the active team. User must be authenticated, a
|
|
450
|
+
* member of the active org, AND have an active team. Better Auth teams
|
|
451
|
+
* are flat member groups (no team-level roles). Reads `request.scope.teamId`.
|
|
629
452
|
*
|
|
630
453
|
* @example
|
|
631
454
|
* ```typescript
|
|
@@ -636,5 +459,57 @@ declare function createDynamicPermissionMatrix(config: DynamicPermissionMatrixCo
|
|
|
636
459
|
* ```
|
|
637
460
|
*/
|
|
638
461
|
declare function requireTeamMembership<TDoc = Record<string, unknown>>(): PermissionCheck<TDoc>;
|
|
462
|
+
declare namespace presets_d_exports {
|
|
463
|
+
export { adminOnly, authenticated, fullPublic, ownerWithAdminBypass, publicRead, publicReadAdminWrite, readOnly };
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* ResourcePermissions shape — matches the type in types/index.ts
|
|
467
|
+
*/
|
|
468
|
+
interface ResourcePermissions<TDoc = any> {
|
|
469
|
+
list?: PermissionCheck<TDoc>;
|
|
470
|
+
get?: PermissionCheck<TDoc>;
|
|
471
|
+
create?: PermissionCheck<TDoc>;
|
|
472
|
+
update?: PermissionCheck<TDoc>;
|
|
473
|
+
delete?: PermissionCheck<TDoc>;
|
|
474
|
+
}
|
|
475
|
+
type PermissionOverrides<TDoc = any> = Partial<ResourcePermissions<TDoc>>;
|
|
476
|
+
/**
|
|
477
|
+
* Public read, authenticated write.
|
|
478
|
+
* list + get = allowPublic(), create + update + delete = requireAuth()
|
|
479
|
+
*/
|
|
480
|
+
declare function publicRead<TDoc = any>(overrides?: PermissionOverrides<TDoc>): ResourcePermissions<TDoc>;
|
|
481
|
+
/**
|
|
482
|
+
* Public read, admin write.
|
|
483
|
+
* list + get = allowPublic(), create + update + delete = requireRoles(['admin'])
|
|
484
|
+
*/
|
|
485
|
+
declare function publicReadAdminWrite<TDoc = any>(roles?: readonly string[], overrides?: PermissionOverrides<TDoc>): ResourcePermissions<TDoc>;
|
|
486
|
+
/**
|
|
487
|
+
* All operations require authentication.
|
|
488
|
+
*/
|
|
489
|
+
declare function authenticated<TDoc = any>(overrides?: PermissionOverrides<TDoc>): ResourcePermissions<TDoc>;
|
|
490
|
+
/**
|
|
491
|
+
* All operations require specific roles.
|
|
492
|
+
* @param roles - Required roles (user needs at least one). Default: ['admin']
|
|
493
|
+
*/
|
|
494
|
+
declare function adminOnly<TDoc = any>(roles?: readonly string[], overrides?: PermissionOverrides<TDoc>): ResourcePermissions<TDoc>;
|
|
495
|
+
/**
|
|
496
|
+
* Owner-scoped with admin bypass.
|
|
497
|
+
* list = auth (scoped to owner), get = auth, create = auth,
|
|
498
|
+
* update + delete = ownership check with admin bypass.
|
|
499
|
+
*
|
|
500
|
+
* @param ownerField - Field containing owner ID (default: 'userId')
|
|
501
|
+
* @param bypassRoles - Roles that bypass ownership check (default: ['admin'])
|
|
502
|
+
*/
|
|
503
|
+
declare function ownerWithAdminBypass<TDoc = any>(ownerField?: Extract<keyof TDoc, string> | string, bypassRoles?: readonly string[], overrides?: PermissionOverrides<TDoc>): ResourcePermissions<TDoc>;
|
|
504
|
+
/**
|
|
505
|
+
* Full public access — no auth required for any operation.
|
|
506
|
+
* Use sparingly (dev/testing, truly public APIs).
|
|
507
|
+
*/
|
|
508
|
+
declare function fullPublic<TDoc = any>(overrides?: PermissionOverrides<TDoc>): ResourcePermissions<TDoc>;
|
|
509
|
+
/**
|
|
510
|
+
* Read-only: list + get authenticated, write operations denied.
|
|
511
|
+
* Useful for computed/derived resources.
|
|
512
|
+
*/
|
|
513
|
+
declare function readOnly<TDoc = any>(overrides?: PermissionOverrides<TDoc>): ResourcePermissions<TDoc>;
|
|
639
514
|
//#endregion
|
|
640
|
-
export {
|
|
515
|
+
export { requireRoles as A, allOf as C, not as D, denyAll as E, when as M, applyPermissionResult as N, requireAuth as O, normalizePermissionResult as P, createOrgPermissions as S, anyOf as T, ConnectEventsOptions as _, presets_d_exports as a, PermissionEventBus as b, readOnly as c, requireOrgRole as d, requireScopeContext as f, createRoleHierarchy as g, RoleHierarchy as h, ownerWithAdminBypass as i, roles as j, requireOwnership as k, requireOrgInScope as l, requireTeamMembership as m, authenticated as n, publicRead as o, requireServiceScope as p, fullPublic as r, publicReadAdminWrite as s, adminOnly as t, requireOrgMembership as u, DynamicPermissionMatrix as v, allowPublic as w, createDynamicPermissionMatrix as x, DynamicPermissionMatrixConfig as y };
|