@classytic/arc 2.6.2 → 2.7.1
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 +95 -1
- package/dist/{BaseController-AbbRx3e0.mjs → BaseController-CpMfCXdn.mjs} +214 -16
- package/dist/adapters/index.d.mts +2 -2
- package/dist/adapters/index.mjs +1 -1
- package/dist/{adapters-CTn28N4y.mjs → adapters-BxGgSHjj.mjs} +7 -13
- package/dist/applyPermissionResult-D6GPMsvh.mjs +37 -0
- package/dist/audit/index.d.mts +1 -1
- package/dist/audit/index.mjs +1 -1
- package/dist/audit/mongodb.d.mts +1 -1
- package/dist/audit/mongodb.mjs +1 -1
- package/dist/auth/index.d.mts +4 -4
- package/dist/auth/index.mjs +7 -6
- package/dist/auth/mongoose.d.mts +191 -0
- package/dist/auth/mongoose.mjs +73 -0
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{betterAuthOpenApi-lz0IRbXJ.mjs → betterAuthOpenApi-CCw3YX0g.mjs} +1 -1
- package/dist/cache/index.d.mts +2 -2
- package/dist/cache/index.mjs +2 -2
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +1 -1
- package/dist/cli/commands/init.mjs +7 -5
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -4
- package/dist/{core-C1XCMtqM.mjs → core-BWekSEju.mjs} +41 -13
- package/dist/{createApp-D2w0LdYJ.mjs → createApp-B_nvKNAQ.mjs} +11 -11
- package/dist/{defineResource-Ckxg6HrZ.mjs → defineResource-DZzyl4a4.mjs} +73 -56
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/dynamic/index.d.mts +2 -2
- package/dist/dynamic/index.mjs +2 -2
- package/dist/{elevation-BEdACOLB.mjs → elevation-By_p2lnn.mjs} +1 -1
- package/dist/elevation-Dm-HTBCt.d.mts +23 -0
- package/dist/{errorHandler-Do4vVQ1f.d.mts → errorHandler-COa51ho_.d.mts} +1 -1
- package/dist/{errorHandler-r2595m8T.mjs → errorHandler-DXUttWEO.mjs} +1 -1
- package/dist/{eventPlugin-DW45v4V5.d.mts → eventPlugin-BgLxJkIB.d.mts} +1 -1
- package/dist/{eventPlugin-Ba00swHF.mjs → eventPlugin-DsaNNXzZ.mjs} +1 -1
- package/dist/events/index.d.mts +3 -3
- package/dist/events/index.mjs +1 -1
- 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/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +3 -3
- package/dist/idempotency/mongodb.d.mts +1 -1
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/index-BYpRGXif.d.mts +640 -0
- package/dist/{index-B4uZm82R.d.mts → index-KXM8_JmQ.d.mts} +47 -4
- package/dist/{index-DrCqa3Jq.d.mts → index-StgFaQKD.d.mts} +3 -3
- package/dist/index.d.mts +8 -8
- package/dist/index.mjs +10 -9
- 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-CrN45qz1.d.mts → interface-Dwzqt4mn.d.mts} +204 -18
- package/dist/{mongodb-pMvOlR5_.d.mts → mongodb-Bq90j-Uj.d.mts} +1 -1
- package/dist/{mongodb-kltrBPa1.d.mts → mongodb-DdyYlIXg.d.mts} +1 -1
- package/dist/{openapi-CBmZ6EQN.mjs → openapi-C5UhIeWu.mjs} +1 -1
- package/dist/org/index.d.mts +2 -2
- package/dist/org/index.mjs +1 -1
- package/dist/permissions/index.d.mts +4 -4
- package/dist/permissions/index.mjs +3 -2
- package/dist/{permissions-C8ImI8gC.mjs → permissions-CH4cNwJi.mjs} +358 -64
- package/dist/plugins/index.d.mts +4 -4
- package/dist/plugins/index.mjs +10 -10
- 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/policies/index.d.mts +1 -1
- package/dist/presets/index.d.mts +3 -3
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +53 -3
- package/dist/presets/multiTenant.mjs +89 -47
- package/dist/{presets-BMfdy34e.mjs → presets-BFrGvvjL.mjs} +2 -2
- package/dist/{queryCachePlugin-DcmETvcB.d.mts → queryCachePlugin-Bw8XyJpX.d.mts} +1 -1
- package/dist/{queryCachePlugin-XtFplYO9.mjs → queryCachePlugin-CwTpR04-.mjs} +2 -2
- package/dist/{redis-D0Qc-9EW.d.mts → redis-CyCntzTO.d.mts} +1 -1
- package/dist/{redis-stream-BW9UKLZM.d.mts → redis-stream-We_Ucl9-.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{resourceToTools-DH3c3e-T.mjs → resourceToTools-CkVSSzKg.mjs} +313 -33
- package/dist/rpc/index.d.mts +1 -1
- package/dist/rpc/index.mjs +1 -1
- package/dist/scope/index.d.mts +3 -2
- package/dist/scope/index.mjs +4 -3
- package/dist/{sse-BF7GR7IB.mjs → sse-Bp3dabF1.mjs} +2 -2
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +1 -1
- package/dist/types/index.d.mts +4 -3
- package/dist/types/index.mjs +1 -1
- package/dist/types-AOD8fxIw.mjs +229 -0
- package/dist/types-CNEbix8T.d.mts +286 -0
- package/dist/{types-DurlBP2N.d.mts → types-ClmkMDK1.d.mts} +1 -1
- package/dist/{types-C1Z28coa.d.mts → types-D0qf0Mf4.d.mts} +9 -9
- package/dist/types-DPsC0taJ.d.mts +178 -0
- package/dist/utils/index.d.mts +3 -3
- package/dist/utils/index.mjs +5 -5
- package/package.json +34 -22
- package/skills/arc/SKILL.md +278 -6
- package/skills/arc/references/multi-tenancy.md +208 -0
- package/dist/elevation-C_taLQrM.d.mts +0 -147
- package/dist/index-NGZksqM5.d.mts +0 -398
- package/dist/types-BNUccdcf.d.mts +0 -101
- package/dist/types-BhtYdxZU.mjs +0 -91
- /package/dist/{EventTransport-wc5hSLik.d.mts → EventTransport-CUpRK_Lg.d.mts} +0 -0
- /package/dist/{HookSystem-COkyWztM.mjs → HookSystem-D7lfx--K.mjs} +0 -0
- /package/dist/{ResourceRegistry-C6ngvOnn.mjs → ResourceRegistry-DsHiG9cL.mjs} +0 -0
- /package/dist/{caching-BSXB-Xr7.mjs → caching-5DtLwIqb.mjs} +0 -0
- /package/dist/{circuitBreaker-JP2GdJ4b.d.mts → circuitBreaker-DwxrljLB.d.mts} +0 -0
- /package/dist/{circuitBreaker-BOBOpN2w.mjs → circuitBreaker-l18oRgL5.mjs} +0 -0
- /package/dist/{errors-CcVbl1-T.d.mts → errors-CCSsMpXE.d.mts} +0 -0
- /package/dist/{errors-NoQKsbAT.mjs → errors-Cg58SLNi.mjs} +0 -0
- /package/dist/{externalPaths-DpO-s7r8.d.mts → externalPaths-Dg7OLsKo.d.mts} +0 -0
- /package/dist/{fields-DFwdaWCq.d.mts → fields-CYuLMJPD.d.mts} +0 -0
- /package/dist/{interface-gr-7qo9j.d.mts → interface-B9rHWPxD.d.mts} +0 -0
- /package/dist/{interface-D_BWALyZ.d.mts → interface-CnluRL4_.d.mts} +0 -0
- /package/dist/{logger-Dz3j1ItV.mjs → logger-DLg8-Ueg.mjs} +0 -0
- /package/dist/{memory-BFAYkf8H.mjs → memory-Cp7_cAko.mjs} +0 -0
- /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
- /package/dist/{mongodb-BuQ7fNTg.mjs → mongodb-mlgxkYI3.mjs} +0 -0
- /package/dist/{pluralize-CcT6qF0a.mjs → pluralize-COpOVar8.mjs} +0 -0
- /package/dist/{registry-I-ogLgL9.mjs → registry-B3lRFBWo.mjs} +0 -0
- /package/dist/{requestContext-DYtmNpm5.mjs → requestContext-xHIKedG6.mjs} +0 -0
- /package/dist/{schemaConverter-DjzHpFam.mjs → schemaConverter-0TyONAwM.mjs} +0 -0
- /package/dist/{sessionManager-wbkYj2HL.d.mts → sessionManager-IW4sbIea.d.mts} +0 -0
- /package/dist/{tracing-bz_U4EM1.d.mts → tracing-65B51Dw3.d.mts} +0 -0
- /package/dist/{typeGuards-Cj5Rgvlg.mjs → typeGuards-CcFZXgU7.mjs} +0 -0
- /package/dist/{utils-Dc0WhlIl.mjs → utils-B-l6410F.mjs} +0 -0
- /package/dist/{versioning-BzfeHmhj.mjs → versioning-aUUVziBY.mjs} +0 -0
package/skills/arc/SKILL.md
CHANGED
|
@@ -8,11 +8,11 @@ description: |
|
|
|
8
8
|
Triggers: arc, fastify resource, defineResource, createApp, BaseController, arc preset,
|
|
9
9
|
arc auth, arc events, arc jobs, arc websocket, arc mcp, arc plugin, arc testing, arc cli,
|
|
10
10
|
arc permissions, arc hooks, arc pipeline, arc factory, arc cache, arc QueryCache.
|
|
11
|
-
version: 2.
|
|
11
|
+
version: 2.7.1
|
|
12
12
|
license: MIT
|
|
13
13
|
metadata:
|
|
14
14
|
author: Classytic
|
|
15
|
-
version: "2.
|
|
15
|
+
version: "2.7.1"
|
|
16
16
|
tags:
|
|
17
17
|
- fastify
|
|
18
18
|
- rest-api
|
|
@@ -126,15 +126,57 @@ auth: false
|
|
|
126
126
|
|
|
127
127
|
**Decorates:** `app.authenticate`, `app.optionalAuthenticate`, `app.authorize`
|
|
128
128
|
|
|
129
|
+
### Better Auth + Mongoose populate bridge (`@classytic/arc/auth/mongoose`)
|
|
130
|
+
|
|
131
|
+
When BA uses `@better-auth/mongo-adapter`, it writes via the native `mongodb` driver and never registers Mongoose models. arc resources doing `Schema({ userId: { ref: 'user' } })` then throw `MissingSchemaError` on `.populate()`.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import mongoose from 'mongoose';
|
|
135
|
+
import { registerBetterAuthMongooseModels } from '@classytic/arc/auth/mongoose';
|
|
136
|
+
|
|
137
|
+
// Default: core only (user/session/account/verification). Plugins are opt-in.
|
|
138
|
+
registerBetterAuthMongooseModels(mongoose, {
|
|
139
|
+
plugins: ['organization', 'organization-teams', 'mcp'],
|
|
140
|
+
// For separate @better-auth/* packages (passkey, sso, api-key):
|
|
141
|
+
extraCollections: ['passkey', 'ssoProvider'],
|
|
142
|
+
// Optional:
|
|
143
|
+
usePlural: false, // matches mongodbAdapter({ usePlural })
|
|
144
|
+
modelOverrides: { user: 'profile' }, // for custom user.modelName configs
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Plugin keys** (core BA only — separate packages use `extraCollections`):
|
|
149
|
+
- `organization` → `organization`, `member`, `invitation`
|
|
150
|
+
- `organization-teams` → `team`, `teamMember`
|
|
151
|
+
- `twoFactor` → `twoFactor`
|
|
152
|
+
- `jwt` → `jwks`
|
|
153
|
+
- `oidcProvider` / `oauthProvider` (alias) → `oauthApplication`, `oauthAccessToken`, `oauthConsent`
|
|
154
|
+
- `mcp` → reuses oidcProvider schema (per BA docs)
|
|
155
|
+
- `deviceAuthorization` → `deviceCode`
|
|
156
|
+
|
|
157
|
+
**Field-only plugins** (admin, username, phoneNumber, magicLink, emailOtp, anonymous, bearer, multiSession, siwe, lastLoginMethod, genericOAuth) need NO entry — `strict: false` stubs round-trip extra fields automatically.
|
|
158
|
+
|
|
159
|
+
Lives at a dedicated subpath so non-Mongoose users (Prisma/Drizzle/Kysely) never get Mongoose pulled into their bundle. Idempotent + de-dupes overlapping plugin sets, so `plugins: ['mcp', 'oidcProvider']` won't crash.
|
|
160
|
+
|
|
129
161
|
## Permissions
|
|
130
162
|
|
|
131
|
-
Function-based. A `PermissionCheck` returns `boolean | { granted, reason?, filters? }`:
|
|
163
|
+
Function-based. A `PermissionCheck` returns `boolean | { granted, reason?, filters?, scope? }`:
|
|
132
164
|
|
|
133
165
|
```typescript
|
|
134
166
|
import {
|
|
167
|
+
// Core
|
|
135
168
|
allowPublic, requireAuth, requireRoles, requireOwnership,
|
|
169
|
+
// Org-bound
|
|
136
170
|
requireOrgMembership, requireOrgRole, requireTeamMembership,
|
|
171
|
+
// Service / API key (OAuth-style)
|
|
172
|
+
requireServiceScope,
|
|
173
|
+
// App-defined scope dimensions (branch, project, region, …)
|
|
174
|
+
requireScopeContext,
|
|
175
|
+
// Parent-child org hierarchy
|
|
176
|
+
requireOrgInScope,
|
|
177
|
+
// Combinators
|
|
137
178
|
allOf, anyOf, when, denyAll,
|
|
179
|
+
// Dynamic ACL
|
|
138
180
|
createDynamicPermissionMatrix,
|
|
139
181
|
} from '@classytic/arc';
|
|
140
182
|
|
|
@@ -147,6 +189,173 @@ permissions: {
|
|
|
147
189
|
}
|
|
148
190
|
```
|
|
149
191
|
|
|
192
|
+
**Mixed human + machine routes** — accept both an org admin and an API key:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import { requireServiceScope } from '@classytic/arc';
|
|
196
|
+
|
|
197
|
+
permissions: {
|
|
198
|
+
// Human admins OR API keys with the right OAuth scope
|
|
199
|
+
create: anyOf(
|
|
200
|
+
requireOrgRole('admin'),
|
|
201
|
+
requireServiceScope('jobs:write'),
|
|
202
|
+
),
|
|
203
|
+
|
|
204
|
+
// Org-bound API key with a specific scope (no human path)
|
|
205
|
+
bulkImport: allOf(
|
|
206
|
+
requireOrgMembership(), // accepts member, service, elevated
|
|
207
|
+
requireServiceScope('jobs:bulk-write'), // OAuth-style scope check
|
|
208
|
+
),
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Multi-level tenancy** — for app-defined scope dimensions beyond org/team
|
|
213
|
+
(branch, project, region, workspace, department, …):
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { requireScopeContext } from '@classytic/arc';
|
|
217
|
+
import { multiTenantPreset } from '@classytic/arc/presets';
|
|
218
|
+
|
|
219
|
+
// 1. Populate scope.context in your auth function (from headers, JWT claims,
|
|
220
|
+
// BA session fields — arc takes no position on the source).
|
|
221
|
+
authFn: async (request) => {
|
|
222
|
+
const session = await myAuth.getSession(request);
|
|
223
|
+
request.scope = {
|
|
224
|
+
kind: 'member',
|
|
225
|
+
userId: session.userId,
|
|
226
|
+
userRoles: session.userRoles,
|
|
227
|
+
organizationId: session.orgId,
|
|
228
|
+
orgRoles: session.orgRoles,
|
|
229
|
+
context: {
|
|
230
|
+
branchId: request.headers['x-branch-id'],
|
|
231
|
+
projectId: request.headers['x-project-id'],
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 2. Gate routes by context dimensions
|
|
237
|
+
permissions: {
|
|
238
|
+
branchAdmin: allOf(requireOrgRole('admin'), requireScopeContext('branchId')),
|
|
239
|
+
euOnly: requireScopeContext('region', 'eu'),
|
|
240
|
+
projectEdit: requireScopeContext({ projectId: undefined, region: 'eu' }),
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 3. Auto-filter resource queries across all dimensions in lockstep
|
|
244
|
+
defineResource({
|
|
245
|
+
name: 'job',
|
|
246
|
+
presets: [
|
|
247
|
+
multiTenantPreset({
|
|
248
|
+
tenantFields: [
|
|
249
|
+
{ field: 'organizationId', type: 'org' },
|
|
250
|
+
{ field: 'branchId', contextKey: 'branchId' },
|
|
251
|
+
{ field: 'projectId', contextKey: 'projectId' },
|
|
252
|
+
],
|
|
253
|
+
}),
|
|
254
|
+
],
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Fail-closed: missing dimensions → 403 with the specific missing field name.
|
|
259
|
+
Elevated scopes (platform admins) apply whatever resolves and skip the rest
|
|
260
|
+
(cross-context bypass).
|
|
261
|
+
|
|
262
|
+
**Parent-child org hierarchy** — for holding companies, MSPs managing
|
|
263
|
+
multiple tenants, white-label parent → child accounts. Arc takes no position
|
|
264
|
+
on the source: your auth function loads the chain from your own org table.
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
import { requireOrgInScope } from '@classytic/arc';
|
|
268
|
+
|
|
269
|
+
// 1. Auth function loads ancestorOrgIds from your org table.
|
|
270
|
+
// Order is closest-first (immediate parent → root).
|
|
271
|
+
authFn: async (request) => {
|
|
272
|
+
const session = await myAuth.getSession(request);
|
|
273
|
+
const ancestors = await orgRepo.findAncestors(session.orgId);
|
|
274
|
+
request.scope = {
|
|
275
|
+
kind: 'member',
|
|
276
|
+
userId: session.userId,
|
|
277
|
+
userRoles: session.userRoles,
|
|
278
|
+
organizationId: session.orgId,
|
|
279
|
+
orgRoles: session.orgRoles,
|
|
280
|
+
ancestorOrgIds: ancestors.map(a => a.id), // ['acme-eu', 'acme-holding']
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 2. Gate routes — accepts current org or any ancestor in the chain
|
|
285
|
+
permissions: {
|
|
286
|
+
// GET /orgs/:orgId/jobs — caller can act on any org in their hierarchy
|
|
287
|
+
list: requireOrgInScope((ctx) => ctx.request.params.orgId),
|
|
288
|
+
|
|
289
|
+
// Static target (rare): one route, one specific org
|
|
290
|
+
holdingDashboard: requireOrgInScope('acme-holding'),
|
|
291
|
+
|
|
292
|
+
// Composed: must be admin AND target must be in hierarchy
|
|
293
|
+
childAdmin: allOf(
|
|
294
|
+
requireOrgRole('admin'),
|
|
295
|
+
requireOrgInScope((ctx) => ctx.request.params.orgId),
|
|
296
|
+
),
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**No automatic inheritance** — every check is explicit. `multiTenantPreset`
|
|
301
|
+
does NOT auto-include ancestor data (would be a footgun). Sibling
|
|
302
|
+
subsidiaries naturally don't see each other's data because they aren't in
|
|
303
|
+
each other's chain. Elevated bypass still applies on the permission helper.
|
|
304
|
+
|
|
305
|
+
**Auth source agnostic** — `requireRoles()` checks platform roles
|
|
306
|
+
(`user.role`) AND org roles (`scope.orgRoles`) by default, so it works
|
|
307
|
+
identically with arc JWT, Better Auth user roles, and Better Auth org plugin.
|
|
308
|
+
`requireOrgMembership()` accepts `member`, `service` (API key), and
|
|
309
|
+
`elevated` scopes. `requireOrgRole()` is human-only by design — use
|
|
310
|
+
`anyOf(requireOrgRole(...), requireServiceScope(...))` for mixed routes.
|
|
311
|
+
`scope.context` and `scope.ancestorOrgIds` are populated by your own auth
|
|
312
|
+
function or adapter — arc doesn't bake in any specific dimension or transport.
|
|
313
|
+
|
|
314
|
+
### RequestScope (quick reference)
|
|
315
|
+
|
|
316
|
+
Five kinds, all opt-in. Always read via accessors from `@classytic/arc/scope`,
|
|
317
|
+
never via direct property access.
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
type RequestScope =
|
|
321
|
+
| { kind: 'public' }
|
|
322
|
+
| { kind: 'authenticated'; userId?; userRoles? }
|
|
323
|
+
| { kind: 'member'; userId?; userRoles; organizationId; orgRoles; teamId?; context?; ancestorOrgIds? }
|
|
324
|
+
| { kind: 'service'; clientId; organizationId; scopes?; context?; ancestorOrgIds? }
|
|
325
|
+
| { kind: 'elevated'; userId?; organizationId?; elevatedBy; context?; ancestorOrgIds? };
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
| Kind | Identity | Org context | Set by |
|
|
329
|
+
|---|---|---|---|
|
|
330
|
+
| `public` | none | none | Default for anonymous requests |
|
|
331
|
+
| `authenticated` | userId, userRoles | none | Logged in, no active org |
|
|
332
|
+
| `member` | userId, userRoles | organizationId + orgRoles (+ teamId, context, ancestorOrgIds) | BA org plugin / JWT custom auth |
|
|
333
|
+
| `service` | clientId, scopes | organizationId (required) | API key via `PermissionResult.scope` |
|
|
334
|
+
| `elevated` | userId | organizationId optional | Elevation plugin via `x-arc-scope: platform` header |
|
|
335
|
+
|
|
336
|
+
| Helper | `member` | `service` | `elevated` |
|
|
337
|
+
|---|---|---|---|
|
|
338
|
+
| `requireOrgMembership()` | ✅ | ✅ | ✅ |
|
|
339
|
+
| `requireOrgRole(roles)` | If role matches | ❌ deny w/ guidance | ✅ bypass |
|
|
340
|
+
| `requireServiceScope(scopes)` | ❌ | If scope matches | ✅ bypass |
|
|
341
|
+
| `requireScopeContext(...)` | If keys match | If keys match | ✅ bypass |
|
|
342
|
+
| `requireTeamMembership()` | If `teamId` set | (n/a) | ✅ bypass |
|
|
343
|
+
| `requireOrgInScope(target)` | If target in chain | If target in chain | ✅ bypass |
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
import {
|
|
347
|
+
isMember, isService, isElevated, hasOrgAccess,
|
|
348
|
+
getOrgId, getUserId, getOrgRoles, getServiceScopes,
|
|
349
|
+
getScopeContext, getAncestorOrgIds, isOrgInScope,
|
|
350
|
+
} from '@classytic/arc/scope';
|
|
351
|
+
|
|
352
|
+
if (hasOrgAccess(scope)) // member | service | elevated
|
|
353
|
+
if (isService(scope)) // narrows to API key
|
|
354
|
+
const orgId = getOrgId(scope); // member | service | elevated
|
|
355
|
+
const branch = getScopeContext(scope, 'branchId'); // custom dimension
|
|
356
|
+
isOrgInScope(scope, 'acme-holding'); // pure predicate (no elevated bypass)
|
|
357
|
+
```
|
|
358
|
+
|
|
150
359
|
**Custom permission:**
|
|
151
360
|
|
|
152
361
|
```typescript
|
|
@@ -187,15 +396,34 @@ permissions: { list: acl.canAction('product', 'read') }
|
|
|
187
396
|
| `slugLookup` | GET /slug/:slug | `ISlugLookupController` | `{ slugField }` |
|
|
188
397
|
| `tree` | GET /tree, GET /:parent/children | `ITreeController` | `{ parentField }` |
|
|
189
398
|
| `ownedByUser` | none (middleware) | — | `{ ownerField }` |
|
|
190
|
-
| `multiTenant` | none (middleware) | — | `{ tenantField }` |
|
|
399
|
+
| `multiTenant` | none (middleware) | — | `{ tenantField }` OR `{ tenantFields: TenantFieldSpec[] }` (2.7.1+) |
|
|
191
400
|
| `audited` | none (middleware) | — | — |
|
|
192
401
|
| `bulk` | POST/PATCH/DELETE /bulk | — | `{ operations?, maxCreateItems? }` |
|
|
193
402
|
|
|
194
403
|
```typescript
|
|
404
|
+
// Single-field (default, backwards compatible)
|
|
195
405
|
presets: ['softDelete', { name: 'multiTenant', tenantField: 'organizationId' }]
|
|
406
|
+
|
|
407
|
+
// Multi-field — org + branch + project in lockstep (2.7.1+)
|
|
408
|
+
presets: [
|
|
409
|
+
multiTenantPreset({
|
|
410
|
+
tenantFields: [
|
|
411
|
+
{ field: 'organizationId', type: 'org' }, // → getOrgId(scope)
|
|
412
|
+
{ field: 'teamId', type: 'team' }, // → getTeamId(scope)
|
|
413
|
+
{ field: 'branchId', contextKey: 'branchId' }, // → scope.context.branchId
|
|
414
|
+
{ field: 'projectId', contextKey: 'projectId' },
|
|
415
|
+
],
|
|
416
|
+
}),
|
|
417
|
+
]
|
|
418
|
+
|
|
196
419
|
// Bulk: presets: ['bulk'] or bulkPreset({ operations: ['createMany', 'updateMany'] })
|
|
197
420
|
```
|
|
198
421
|
|
|
422
|
+
`multiTenant` recognizes `member`, `service` (API key), and `elevated`
|
|
423
|
+
scopes uniformly via `hasOrgAccess()`. Multi-field uses fail-closed
|
|
424
|
+
semantics: missing dimensions → 403 with the specific missing field name.
|
|
425
|
+
Elevated scopes apply whatever resolves and skip the rest.
|
|
426
|
+
|
|
199
427
|
### tenantField — When to Use and When to Disable
|
|
200
428
|
|
|
201
429
|
Arc defaults `tenantField` to `'organizationId'` on BaseController. This silently adds `{ organizationId: scope.organizationId }` to every query when the user has an org context. Correct for per-org resources, wrong for company-wide resources.
|
|
@@ -220,6 +448,31 @@ When to use `tenantField: false`:
|
|
|
220
448
|
- Cross-org reports or analytics
|
|
221
449
|
- Single-tenant apps where org scoping isn't needed
|
|
222
450
|
|
|
451
|
+
### idField — Custom Primary Key
|
|
452
|
+
|
|
453
|
+
Default is `'_id'`. Override for resources keyed by a business identifier (UUID, slug, `ORD-2026-0001`, `job-5219f346-a4d`, etc.).
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
defineResource({
|
|
457
|
+
name: 'job',
|
|
458
|
+
adapter: createMongooseAdapter(JobModel, jobRepository),
|
|
459
|
+
idField: 'jobId', // ← one line
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// GET /jobs/job-5219f346-a4d → controller runs { jobId: 'job-5219f346-a4d' }
|
|
463
|
+
// GET /jobs/<uuid> → accepted (no ObjectId pattern enforcement)
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Changes all three layers:
|
|
467
|
+
- **Fastify AJV** — strips any ObjectId pattern from `params.id` so custom formats aren't pre-rejected
|
|
468
|
+
- **BaseController** — `get`/`update`/`delete` query by `{ [idField]: id }` (merged with tenant + policy filters)
|
|
469
|
+
- **OpenAPI docs** — `spec.paths['/jobs/{id}']` emits a plain string `id` with description
|
|
470
|
+
- **MCP tools** — auto-generated CRUD tools use `idField` transparently
|
|
471
|
+
|
|
472
|
+
URL path segment stays `:id` (not `:jobId`) — clients send the ID value, Arc maps it server-side. User-provided `openApiSchemas.params` still overrides everything.
|
|
473
|
+
|
|
474
|
+
For custom adapters, honor the new `AdapterSchemaContext` passed to `generateSchemas(options, context?)` to emit the right `params.id` pattern from the start. Legacy adapters still work — Arc's safety net strips mismatched ObjectId patterns automatically.
|
|
475
|
+
|
|
223
476
|
## QueryCache
|
|
224
477
|
|
|
225
478
|
TanStack Query-inspired server cache with stale-while-revalidate and auto-invalidation on mutations.
|
|
@@ -635,6 +888,10 @@ import { defineResource, BaseController, allowPublic } from '@classytic/arc';
|
|
|
635
888
|
import { createApp } from '@classytic/arc/factory';
|
|
636
889
|
import { MemoryCacheStore, RedisCacheStore, QueryCache } from '@classytic/arc/cache';
|
|
637
890
|
import { createBetterAuthAdapter, extractBetterAuthOpenApi } from '@classytic/arc/auth';
|
|
891
|
+
// 2.7.1+: optional Mongoose stub-models bridge for `populate()` against
|
|
892
|
+
// Better Auth collections — only loaded if you import it (subpath gate
|
|
893
|
+
// keeps Mongoose out of Prisma/Drizzle/Kysely bundles).
|
|
894
|
+
import { registerBetterAuthMongooseModels } from '@classytic/arc/auth/mongoose';
|
|
638
895
|
import type { ExternalOpenApiPaths } from '@classytic/arc/docs';
|
|
639
896
|
import { eventPlugin } from '@classytic/arc/events';
|
|
640
897
|
import { RedisEventTransport } from '@classytic/arc/events/redis';
|
|
@@ -651,7 +908,21 @@ import { createTestApp } from '@classytic/arc/testing';
|
|
|
651
908
|
import { Type, ArcListResponse } from '@classytic/arc/schemas';
|
|
652
909
|
import { createStateMachine, CircuitBreaker, withCompensation, defineCompensation } from '@classytic/arc/utils';
|
|
653
910
|
import { defineMigration } from '@classytic/arc/migrations';
|
|
654
|
-
|
|
911
|
+
// Scope accessors — full surface as of 2.7.1
|
|
912
|
+
import {
|
|
913
|
+
// Type guards
|
|
914
|
+
isMember, isService, isElevated, isAuthenticated, hasOrgAccess,
|
|
915
|
+
// Identity / org accessors
|
|
916
|
+
getUserId, getUserRoles, getOrgId, getOrgRoles, getTeamId, getClientId,
|
|
917
|
+
// Service scopes (OAuth-style strings on API keys)
|
|
918
|
+
getServiceScopes,
|
|
919
|
+
// App-defined scope dimensions (branch, project, region, …)
|
|
920
|
+
getScopeContext, getScopeContextMap,
|
|
921
|
+
// Parent-child org hierarchy
|
|
922
|
+
getAncestorOrgIds, isOrgInScope,
|
|
923
|
+
// Generic request-side helper
|
|
924
|
+
getRequestScope,
|
|
925
|
+
} from '@classytic/arc/scope';
|
|
655
926
|
import { createTenantKeyGenerator } from '@classytic/arc/scope';
|
|
656
927
|
import { createRoleHierarchy } from '@classytic/arc/permissions';
|
|
657
928
|
import { createServiceClient } from '@classytic/arc/rpc';
|
|
@@ -659,7 +930,7 @@ import { metricsPlugin, versioningPlugin } from '@classytic/arc/plugins';
|
|
|
659
930
|
import { webhookPlugin } from '@classytic/arc/integrations/webhooks';
|
|
660
931
|
import { mcpPlugin, createMcpServer, defineTool, definePrompt, fieldRulesToZod, resourceToTools } from '@classytic/arc/mcp';
|
|
661
932
|
import { EventOutbox, MemoryOutboxStore } from '@classytic/arc/events';
|
|
662
|
-
import { bulkPreset } from '@classytic/arc/presets';
|
|
933
|
+
import { bulkPreset, multiTenantPreset, type TenantFieldSpec } from '@classytic/arc/presets';
|
|
663
934
|
```
|
|
664
935
|
|
|
665
936
|
## References (Progressive Disclosure)
|
|
@@ -668,5 +939,6 @@ import { bulkPreset } from '@classytic/arc/presets';
|
|
|
668
939
|
- **[events](references/events.md)** — Domain events, transports, retry, outbox pattern, auto-emission
|
|
669
940
|
- **[integrations](references/integrations.md)** — BullMQ jobs, WebSocket, EventGateway, Streamline, Webhooks
|
|
670
941
|
- **[mcp](references/mcp.md)** — MCP tools for AI agents, auto-generation from resources, custom tools, Better Auth OAuth 2.1
|
|
942
|
+
- **[multi-tenancy](references/multi-tenancy.md)** — Scope ladder, `tenantField` read sites, `PermissionResult.scope`, API key auth without a separate auth plugin
|
|
671
943
|
- **[production](references/production.md)** — Health, audit, idempotency, tracing, metrics, versioning, SSE, QueryCache, bulk ops, saga, RPC schema versioning, tenant rate limiting
|
|
672
944
|
- **[testing](references/testing.md)** — Test app, mocks, data factories, in-memory MongoDB
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Multi-Tenancy Playbook
|
|
3
|
+
description: Where `tenantField` takes effect, how it composes with `_policyFilters`, and how to install scope from custom auth (API keys, service accounts, gateway headers)
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Multi-Tenancy Playbook
|
|
7
|
+
|
|
8
|
+
Arc's multi-tenancy has exactly **one source of truth**: `request.scope`. This document shows where it's read, how filters compose, and how to wire custom authentication strategies (API keys, service accounts) without fighting the framework.
|
|
9
|
+
|
|
10
|
+
## Mental model
|
|
11
|
+
|
|
12
|
+
Every request goes through this ladder — if any step is missed, tenant isolation silently breaks:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
1. Authenticator populates request.scope (member | service | elevated | ...)
|
|
16
|
+
↓
|
|
17
|
+
2. Permission check validates access (allowPublic | requireAuth | requireApiKey | ...)
|
|
18
|
+
↓ may ALSO set request.scope via PermissionResult.scope
|
|
19
|
+
3. fastifyAdapter snapshots scope into metadata._scope
|
|
20
|
+
↓
|
|
21
|
+
4. QueryResolver / AccessControl read metadata._scope
|
|
22
|
+
↓
|
|
23
|
+
5. If tenantField is set AND getOrgId(scope) returns an orgId,
|
|
24
|
+
the org filter is ALWAYS prepended to the query
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## The 5 scope kinds
|
|
28
|
+
|
|
29
|
+
| Kind | Use case | Has `userId` | Has `organizationId` | Helper |
|
|
30
|
+
|-----------------|-------------------------------------|:------------:|:--------------------:|--------------------------|
|
|
31
|
+
| `public` | No authentication | ❌ | ❌ | `PUBLIC_SCOPE` |
|
|
32
|
+
| `authenticated` | Logged-in user, no org context | ✅ | ❌ | `getUserId()` |
|
|
33
|
+
| `member` | User in an org with specific roles | ✅ | ✅ | `isMember()` + `getOrgId()` |
|
|
34
|
+
| `service` | Machine-to-machine (API keys, bots) | ❌ (`clientId` instead) | ✅ | `isService()` + `getClientId()` |
|
|
35
|
+
| `elevated` | Platform admin, explicit elevation | ✅ | Optional | `isElevated()` |
|
|
36
|
+
|
|
37
|
+
**Why `service` is distinct from `member`:** a service account is not a human. It has no user record, no global roles, and should be auditable as a machine actor. Faking it as a `member` with `userId: client._id` pollutes audit logs, confuses rate-limit keys, and makes future per-service-scope authorization (OAuth scopes) harder.
|
|
38
|
+
|
|
39
|
+
## Where `tenantField` is read
|
|
40
|
+
|
|
41
|
+
Every resource defines `tenantField` (default: `'organizationId'`, or `false` to disable). It is read in **exactly three places** and ALL of them derive the org ID from `metadata._scope` via `getOrgId()`:
|
|
42
|
+
|
|
43
|
+
| File | Method | Purpose |
|
|
44
|
+
|--------------------------------|-------------------------|------------------------------------------------|
|
|
45
|
+
| `src/core/QueryResolver.ts` | `resolve()` | `list` — prepends `{ [tenantField]: orgId }` to the filter |
|
|
46
|
+
| `src/core/AccessControl.ts` | `buildIdFilter()` | `get`, `update`, `delete` by ID — compound filter so cross-tenant IDs 404 |
|
|
47
|
+
| `src/core/BaseController.ts` | `buildBulkFilter()` | `bulkUpdate`, `bulkDelete` — narrows bulk ops to the caller's org |
|
|
48
|
+
|
|
49
|
+
**Everything else** (`BodySanitizer`, `MongoKit QueryParser`, presets) already composes cleanly with these three read points. If `getOrgId(scope)` returns the right value, you have tenant isolation — end of story.
|
|
50
|
+
|
|
51
|
+
## Filter composition
|
|
52
|
+
|
|
53
|
+
`_policyFilters` and `tenantField` are different and both apply:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// The final filter passed to the database is:
|
|
57
|
+
{
|
|
58
|
+
...parsedUserFilters, // from the incoming query string
|
|
59
|
+
..._policyFilters, // from permission checks (ownership, project scoping, etc.)
|
|
60
|
+
[tenantField]: orgId, // from metadata._scope (if tenantField is set and scope has orgId)
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Priority rules:**
|
|
65
|
+
- `_policyFilters` overrides user-supplied filters (the user can't bypass them)
|
|
66
|
+
- `tenantField` only gets injected if `_policyFilters` does not already set it (so a more specific policy filter wins)
|
|
67
|
+
- `elevated` scope with no `organizationId` means the filter is skipped entirely (admin sees everything)
|
|
68
|
+
|
|
69
|
+
## Four ways to install scope
|
|
70
|
+
|
|
71
|
+
### 1. Better Auth (recommended for human users)
|
|
72
|
+
|
|
73
|
+
The `betterAuth` adapter reads the session cookie and installs `kind: 'member'` automatically. Nothing to wire.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
await createApp({
|
|
77
|
+
auth: getAuth(), // Better Auth instance
|
|
78
|
+
// ... scope is set by the adapter, zero code
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 2. Custom Fastify authenticator (JWT, headers, whatever)
|
|
83
|
+
|
|
84
|
+
Arc's `authPlugin.ts` auto-derives scope from `request.user` after your authenticator returns. If your user has `organizationId`, you get a `member` scope for free:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
await createApp({
|
|
88
|
+
auth: {
|
|
89
|
+
type: 'authenticator',
|
|
90
|
+
authenticate: async (request) => {
|
|
91
|
+
const user = await verifyJwt(request.headers.authorization);
|
|
92
|
+
return user; // Must have `organizationId` for member scope
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 3. `PermissionResult.scope` (NEW — best for API keys, service accounts, custom headers)
|
|
99
|
+
|
|
100
|
+
Return the scope directly from your permission check. This is the cleanest integration point for machine-to-machine auth — no separate auth plugin, no scope-derivation-from-user magic. It's explicit, typed, and auditable.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
// src/permissions/apiKey.ts
|
|
104
|
+
import type { PermissionCheck } from '@classytic/arc/permissions';
|
|
105
|
+
import { ClientModel } from '../models/Client.js';
|
|
106
|
+
|
|
107
|
+
export function requireApiKey(): PermissionCheck {
|
|
108
|
+
return async ({ request }) => {
|
|
109
|
+
const apiKey = request.headers['x-api-key'] as string | undefined;
|
|
110
|
+
if (!apiKey) return { granted: false, reason: 'Missing API key' };
|
|
111
|
+
|
|
112
|
+
const client = await ClientModel.findOne({ apiKey });
|
|
113
|
+
if (!client) return { granted: false, reason: 'Invalid API key' };
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
granted: true,
|
|
117
|
+
scope: {
|
|
118
|
+
kind: 'service',
|
|
119
|
+
clientId: String(client._id),
|
|
120
|
+
organizationId: String(client.companyId),
|
|
121
|
+
scopes: client.allowedScopes, // optional OAuth-style scope strings
|
|
122
|
+
},
|
|
123
|
+
// Optional additional row-level narrowing
|
|
124
|
+
filters: client.projectId ? { projectId: client.projectId } : undefined,
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Then wire it into the resource:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
defineResource({
|
|
134
|
+
name: 'job',
|
|
135
|
+
adapter: createMongooseAdapter({ model: Job, repository: jobRepo }),
|
|
136
|
+
controller: new BaseController(jobRepo, { tenantField: 'companyId' }),
|
|
137
|
+
permissions: {
|
|
138
|
+
list: requireApiKey(),
|
|
139
|
+
get: requireApiKey(),
|
|
140
|
+
create: requireApiKey(),
|
|
141
|
+
update: requireApiKey(),
|
|
142
|
+
delete: requireApiKey(),
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**That's the whole setup.** No auth plugin, no scope-resolution hook, no separate tenant-filter middleware. One file, one function, full tenant isolation — verified against cross-tenant `get`, `list`, `update`, and `delete` in [tests/e2e/permission-scope-wire.test.ts](../../../tests/e2e/permission-scope-wire.test.ts).
|
|
148
|
+
|
|
149
|
+
**Override safety:** `PermissionResult.scope` only writes to `request.scope` when the current scope is still `public`. An already-authenticated request (from Better Auth, JWT, etc.) is never downgraded or overwritten.
|
|
150
|
+
|
|
151
|
+
### 4. Custom Fastify hook (advanced)
|
|
152
|
+
|
|
153
|
+
For exotic cases (mTLS, downstream RPC calls), write an `onRequest` hook that sets `request.scope` directly. Arc reads from `request.scope` — it doesn't care how you got there.
|
|
154
|
+
|
|
155
|
+
## Decision table
|
|
156
|
+
|
|
157
|
+
| Your auth source | Where to set scope |
|
|
158
|
+
|-----------------------------------|------------------------------------------------------|
|
|
159
|
+
| Better Auth session cookie | Nothing — `betterAuth` adapter handles it |
|
|
160
|
+
| JWT with `organizationId` claim | Return it from `authenticate()` — auth plugin derives scope |
|
|
161
|
+
| API key → DB lookup | **`PermissionResult.scope`** ← the clean path |
|
|
162
|
+
| Gateway-injected headers | `PermissionResult.scope` or a custom `onRequest` hook |
|
|
163
|
+
| Service account OAuth token | `PermissionResult.scope` with `kind: 'service'` |
|
|
164
|
+
| Platform admin elevation | `elevationPlugin` — sets `kind: 'elevated'` |
|
|
165
|
+
|
|
166
|
+
## What happens when scope is missing
|
|
167
|
+
|
|
168
|
+
If `tenantField` is set but `getOrgId(scope)` returns `undefined`:
|
|
169
|
+
|
|
170
|
+
- **`list` / `get` / `update` / `delete`** — the filter is NOT narrowed. A caller with `public` scope would see ALL rows across ALL tenants. **This is why you must always pair `tenantField` with a permission check that installs a scope with `organizationId`.**
|
|
171
|
+
- **`bulkUpdate` / `bulkDelete`** — `buildBulkFilter()` returns `null` and the bulk operation is rejected. This is a safety net, but don't rely on it for `list`.
|
|
172
|
+
|
|
173
|
+
Arc does NOT auto-derive scope from `request.user.organizationId` — that's a footgun (silent tenant leak if you set the user but forget the scope). If you want this behavior, either use the JWT/custom authenticator path (which does derive automatically) or return `scope` explicitly from your permission check.
|
|
174
|
+
|
|
175
|
+
## Gotchas
|
|
176
|
+
|
|
177
|
+
1. **`tenantField` is per-resource, not per-app.** Different resources can use different tenant field names (`companyId`, `workspaceId`, `tenantId`). The org ID always comes from `getOrgId(scope)`.
|
|
178
|
+
2. **`elevated` with no `organizationId` bypasses tenant filtering.** This is intentional (admin sees everything), but it means you can't use `kind: 'elevated'` for normal per-org access.
|
|
179
|
+
3. **`systemManaged` is your seatbelt for create/update.** Mark tenant fields (`companyId`, `organizationId`) as `systemManaged` in `fieldRules` so `BodySanitizer` strips any client-supplied value. The tenant field is then injected from the scope at write time — not from the request body.
|
|
180
|
+
4. **Rate-limit keys respect all 5 scope kinds.** The built-in `createTenantKeyGenerator` uses `organizationId` for member/service/elevated and falls back to `userId`/IP for authenticated/public.
|
|
181
|
+
|
|
182
|
+
## Helpers reference
|
|
183
|
+
|
|
184
|
+
All exported from `@classytic/arc/scope`:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import {
|
|
188
|
+
// Type guards
|
|
189
|
+
isMember, isService, isElevated, isAuthenticated, hasOrgAccess,
|
|
190
|
+
|
|
191
|
+
// Accessors (return undefined when not applicable — no throws)
|
|
192
|
+
getUserId, // authenticated | member | elevated
|
|
193
|
+
getClientId, // service only
|
|
194
|
+
getOrgId, // member | service | elevated
|
|
195
|
+
getUserRoles, // authenticated | member
|
|
196
|
+
getOrgRoles, // member only
|
|
197
|
+
getServiceScopes, // service only (OAuth-style scope strings)
|
|
198
|
+
getTeamId, // member with teamId set
|
|
199
|
+
|
|
200
|
+
// Canonical request extractor
|
|
201
|
+
getOrgContext,
|
|
202
|
+
|
|
203
|
+
// Constants
|
|
204
|
+
PUBLIC_SCOPE, AUTHENTICATED_SCOPE,
|
|
205
|
+
|
|
206
|
+
type RequestScope,
|
|
207
|
+
} from '@classytic/arc/scope';
|
|
208
|
+
```
|