@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/README.md
CHANGED
|
@@ -108,6 +108,17 @@ const productResource = defineResource({
|
|
|
108
108
|
// Plus preset routes: GET /deleted, POST /:id/restore, GET /slug/:slug
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
**Custom primary key?** Use `idField` for resources keyed by UUIDs, slugs, or business identifiers:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
defineResource({
|
|
115
|
+
name: 'job',
|
|
116
|
+
adapter: createMongooseAdapter(JobModel, jobRepository),
|
|
117
|
+
idField: 'jobId', // routes + BaseController lookups + OpenAPI + MCP tools all use this
|
|
118
|
+
});
|
|
119
|
+
// GET /jobs/job-5219f346-a4d → 200 (no ObjectId pattern enforcement)
|
|
120
|
+
```
|
|
121
|
+
|
|
111
122
|
## Authentication
|
|
112
123
|
|
|
113
124
|
Auth uses a discriminated union — pick a `type`:
|
|
@@ -132,6 +143,33 @@ auth: false
|
|
|
132
143
|
|
|
133
144
|
**Decorates:** `app.authenticate`, `app.optionalAuthenticate`, `app.authorize`
|
|
134
145
|
|
|
146
|
+
### Better Auth + Mongoose populate bridge
|
|
147
|
+
|
|
148
|
+
When you back Better Auth with `@better-auth/mongo-adapter`, BA writes through the native `mongodb` driver and never registers anything with Mongoose. Any arc resource that does `Schema({ userId: { ref: 'user' } })` and calls `.populate('userId')` then throws `MissingSchemaError`.
|
|
149
|
+
|
|
150
|
+
Optional helper at a dedicated subpath registers `strict: false` stub Mongoose models for BA's collections so populate works. Lives behind `@classytic/arc/auth/mongoose` so users on Prisma/Drizzle/Kysely never get Mongoose pulled into their bundle.
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import mongoose from 'mongoose';
|
|
154
|
+
import { registerBetterAuthMongooseModels } from '@classytic/arc/auth/mongoose';
|
|
155
|
+
|
|
156
|
+
// Default is core only — every plugin set is opt-in.
|
|
157
|
+
registerBetterAuthMongooseModels(mongoose, {
|
|
158
|
+
plugins: ['organization', 'organization-teams'],
|
|
159
|
+
// For separate @better-auth/* packages:
|
|
160
|
+
extraCollections: ['passkey', 'ssoProvider'],
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Now arc resources can populate BA-owned references:
|
|
164
|
+
const Post = mongoose.model('Post', new mongoose.Schema({
|
|
165
|
+
title: String,
|
|
166
|
+
authorId: { type: String, ref: 'user' },
|
|
167
|
+
}));
|
|
168
|
+
await Post.findOne().populate('authorId');
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Supports `usePlural` (matches `mongodbAdapter({ usePlural: true })`) and `modelOverrides` (for custom `user: { modelName: 'profile' }` configs). Idempotent and de-dupes overlapping plugin sets.
|
|
172
|
+
|
|
135
173
|
### Token Revocation
|
|
136
174
|
|
|
137
175
|
Arc provides the `isRevoked` primitive — you implement the store (Redis, DB, Better Auth):
|
|
@@ -156,7 +194,9 @@ Function-based, composable:
|
|
|
156
194
|
```typescript
|
|
157
195
|
import {
|
|
158
196
|
allowPublic, requireAuth, requireRoles, requireOwnership,
|
|
159
|
-
requireOrgMembership, requireOrgRole,
|
|
197
|
+
requireOrgMembership, requireOrgRole, requireServiceScope,
|
|
198
|
+
requireScopeContext,
|
|
199
|
+
allOf, anyOf, denyAll,
|
|
160
200
|
createDynamicPermissionMatrix,
|
|
161
201
|
} from '@classytic/arc';
|
|
162
202
|
|
|
@@ -166,9 +206,63 @@ permissions: {
|
|
|
166
206
|
create: requireRoles(['admin', 'editor']),
|
|
167
207
|
update: anyOf(requireOwnership('userId'), requireRoles(['admin'])),
|
|
168
208
|
delete: allOf(requireAuth(), requireRoles(['admin'])),
|
|
209
|
+
|
|
210
|
+
// Mixed human + machine routes — accept org admins OR API keys
|
|
211
|
+
bulkImport: anyOf(
|
|
212
|
+
requireOrgRole('admin'), // human path
|
|
213
|
+
requireServiceScope('jobs:bulk-write'), // machine path (OAuth-style)
|
|
214
|
+
),
|
|
215
|
+
|
|
216
|
+
// Multi-level tenancy — branch/project/region scoped routes
|
|
217
|
+
branchAdmin: allOf(requireOrgRole('admin'), requireScopeContext('branchId')),
|
|
218
|
+
euOnly: requireScopeContext('region', 'eu'),
|
|
219
|
+
projectEdit: requireScopeContext({ projectId: 'p-1', region: 'eu' }),
|
|
220
|
+
|
|
221
|
+
// Parent-child org hierarchy (holding → subsidiary → branch, MSP, white-label)
|
|
222
|
+
// Reads scope.ancestorOrgIds (loaded by your auth function from your own org table)
|
|
223
|
+
childOrgAccess: requireOrgInScope((ctx) => ctx.request.params.orgId),
|
|
169
224
|
}
|
|
170
225
|
```
|
|
171
226
|
|
|
227
|
+
`requireRoles()` checks platform roles (`user.role`) AND org roles
|
|
228
|
+
(`scope.orgRoles`) by default — same call works for arc JWT, Better Auth user
|
|
229
|
+
roles, and Better Auth org plugin. `requireOrgMembership()` accepts `member`,
|
|
230
|
+
`service` (API key), and `elevated` scopes; `multiTenantPreset` filters by
|
|
231
|
+
org for all three. For machine identities, `requireServiceScope('jobs:write')`
|
|
232
|
+
mirrors OAuth 2.0 scope strings. For app-defined dimensions beyond org/team
|
|
233
|
+
(branch, project, region, workspace), `requireScopeContext('branchId')`
|
|
234
|
+
reads from `scope.context` populated by your auth function. For parent-child
|
|
235
|
+
org hierarchies (holding → subsidiary, MSP → tenants, white-label),
|
|
236
|
+
`requireOrgInScope((ctx) => ctx.request.params.orgId)` accepts the current
|
|
237
|
+
org or any ancestor in `scope.ancestorOrgIds`.
|
|
238
|
+
|
|
239
|
+
**Multi-level tenant filtering** — the `multiTenantPreset` scales from
|
|
240
|
+
single-org isolation to lockstep filtering across any number of dimensions:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import { multiTenantPreset } from '@classytic/arc/presets';
|
|
244
|
+
|
|
245
|
+
// Single-field (default, backwards compatible)
|
|
246
|
+
multiTenantPreset({ tenantField: 'organizationId' })
|
|
247
|
+
|
|
248
|
+
// Multi-field — org + branch + project, all enforced in lockstep
|
|
249
|
+
multiTenantPreset({
|
|
250
|
+
tenantFields: [
|
|
251
|
+
{ field: 'organizationId', type: 'org' }, // → getOrgId(scope)
|
|
252
|
+
{ field: 'teamId', type: 'team' }, // → getTeamId(scope)
|
|
253
|
+
{ field: 'branchId', contextKey: 'branchId' }, // → scope.context.branchId
|
|
254
|
+
{ field: 'projectId', contextKey: 'projectId' },
|
|
255
|
+
],
|
|
256
|
+
})
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Fail-closed semantics: if any required dimension is missing from the caller's
|
|
260
|
+
scope, list/get/update/delete return 403 with the specific missing field name,
|
|
261
|
+
and create is rejected. Elevated scopes apply whatever resolves and skip the
|
|
262
|
+
rest (cross-context admin bypass). Your auth function populates
|
|
263
|
+
`scope.context` from JWT claims, BA session fields, or request headers — arc
|
|
264
|
+
takes no position on which dimension names you use.
|
|
265
|
+
|
|
172
266
|
**Field-level permissions:**
|
|
173
267
|
|
|
174
268
|
```typescript
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { h as SYSTEM_FIELDS, o as DEFAULT_TENANT_FIELD } from "./constants-Cxde4rpC.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { _ as isElevated, n as PUBLIC_SCOPE, o as getOrgId, v as isMember } from "./types-AOD8fxIw.mjs";
|
|
3
3
|
import { t as buildQueryKey } from "./keys-qcD-TVJl.mjs";
|
|
4
4
|
import { getUserId } from "./types/index.mjs";
|
|
5
5
|
import { i as resolveEffectiveRoles, n as applyFieldWritePermissions } from "./fields-ipsbIRPK.mjs";
|
|
@@ -96,9 +96,12 @@ var AccessControl = class AccessControl {
|
|
|
96
96
|
*/
|
|
97
97
|
async fetchWithAccessControl(id, req, repository, queryOptions) {
|
|
98
98
|
const compoundFilter = this.buildIdFilter(id, req);
|
|
99
|
-
const
|
|
99
|
+
const needsCompoundLookup = Object.keys(compoundFilter).length > 1 || this.idField !== "_id";
|
|
100
100
|
try {
|
|
101
|
-
if (
|
|
101
|
+
if (needsCompoundLookup && typeof repository.getOne === "function") return await repository.getOne(compoundFilter, queryOptions);
|
|
102
|
+
if (this.idField !== "_id") {
|
|
103
|
+
if (typeof repository.getOne !== "function") throw new Error(`Resource with idField="${this.idField}" requires repository.getOne() to look up by custom field. Arc's BaseController cannot fall back to getById() because it would query by _id.`);
|
|
104
|
+
}
|
|
102
105
|
const item = await repository.getById(id, queryOptions);
|
|
103
106
|
if (!item) return null;
|
|
104
107
|
const arcContext = this._meta(req);
|
|
@@ -438,7 +441,7 @@ var BaseController = class {
|
|
|
438
441
|
this.defaultSort = options.defaultSort ?? "-createdAt";
|
|
439
442
|
this.resourceName = options.resourceName;
|
|
440
443
|
this.tenantField = options.tenantField !== void 0 ? options.tenantField : DEFAULT_TENANT_FIELD;
|
|
441
|
-
this.idField = options.idField ?? "_id";
|
|
444
|
+
this.idField = options.idField ?? repository?.idField ?? "_id";
|
|
442
445
|
this._matchesFilter = options.matchesFilter;
|
|
443
446
|
if (options.cache) this._cacheConfig = options.cache;
|
|
444
447
|
if (options.presetFields) this._presetFields = options.presetFields;
|
|
@@ -480,6 +483,29 @@ var BaseController = class {
|
|
|
480
483
|
getHooks(req) {
|
|
481
484
|
return this.meta(req)?.arc?.hooks ?? null;
|
|
482
485
|
}
|
|
486
|
+
/**
|
|
487
|
+
* Resolve the repository primary key for mutation calls (update/delete/restore).
|
|
488
|
+
*
|
|
489
|
+
* When the resource declares a custom `idField` (e.g. `slug`, `jobId`, UUID),
|
|
490
|
+
* the default behavior is to translate the route id → the fetched doc's `_id`
|
|
491
|
+
* because most Mongo repositories key their mutation methods off `_id`.
|
|
492
|
+
*
|
|
493
|
+
* Exception: if the repository itself exposes a matching `idField` property
|
|
494
|
+
* (e.g. MongoKit's `new Repository(Model, [], {}, { idField: 'id' })`), the
|
|
495
|
+
* repository already knows how to look up by that field — so we pass the
|
|
496
|
+
* route id through unchanged and skip the translation.
|
|
497
|
+
*
|
|
498
|
+
* This makes `defineResource({ idField: 'id' })` work end-to-end with repos
|
|
499
|
+
* that natively support custom primary keys, without breaking the slug-style
|
|
500
|
+
* aliasing that Arc 2.6.3 introduced for repos keyed on `_id`.
|
|
501
|
+
*/
|
|
502
|
+
resolveRepoId(id, existing) {
|
|
503
|
+
if (this.idField === "_id") return id;
|
|
504
|
+
if (!existing) return id;
|
|
505
|
+
const repoIdField = this.repository.idField;
|
|
506
|
+
if (repoIdField && repoIdField === this.idField) return id;
|
|
507
|
+
return String(existing["_id"] ?? id);
|
|
508
|
+
}
|
|
483
509
|
/** Resolve cache config for a specific operation, merging per-op overrides */
|
|
484
510
|
resolveCacheConfig(operation) {
|
|
485
511
|
const cfg = this._cacheConfig;
|
|
@@ -719,6 +745,7 @@ var BaseController = class {
|
|
|
719
745
|
details: { code: "OWNERSHIP_DENIED" },
|
|
720
746
|
status: 403
|
|
721
747
|
};
|
|
748
|
+
const repoId = this.resolveRepoId(id, existing);
|
|
722
749
|
const hooks = this.getHooks(req);
|
|
723
750
|
let processedData = data;
|
|
724
751
|
if (hooks && this.resourceName) try {
|
|
@@ -741,7 +768,7 @@ var BaseController = class {
|
|
|
741
768
|
status: 400
|
|
742
769
|
};
|
|
743
770
|
}
|
|
744
|
-
const repoUpdate = async () => this.repository.update(
|
|
771
|
+
const repoUpdate = async () => this.repository.update(repoId, processedData, {
|
|
745
772
|
user,
|
|
746
773
|
context: arcContext
|
|
747
774
|
});
|
|
@@ -797,6 +824,7 @@ var BaseController = class {
|
|
|
797
824
|
details: { code: "OWNERSHIP_DENIED" },
|
|
798
825
|
status: 403
|
|
799
826
|
};
|
|
827
|
+
const repoId = this.resolveRepoId(id, existing);
|
|
800
828
|
const hooks = this.getHooks(req);
|
|
801
829
|
if (hooks && this.resourceName) try {
|
|
802
830
|
await hooks.executeBefore(this.resourceName, "delete", existing, {
|
|
@@ -815,7 +843,7 @@ var BaseController = class {
|
|
|
815
843
|
status: 400
|
|
816
844
|
};
|
|
817
845
|
}
|
|
818
|
-
const repoDelete = async () => this.repository.delete(
|
|
846
|
+
const repoDelete = async () => this.repository.delete(repoId, {
|
|
819
847
|
user,
|
|
820
848
|
context: arcContext
|
|
821
849
|
});
|
|
@@ -910,7 +938,7 @@ var BaseController = class {
|
|
|
910
938
|
error: "ID parameter is required",
|
|
911
939
|
status: 400
|
|
912
940
|
};
|
|
913
|
-
const existing = await this.accessControl.fetchWithAccessControl(id, req, repo);
|
|
941
|
+
const existing = await this.accessControl.fetchWithAccessControl(id, req, repo, { includeDeleted: true });
|
|
914
942
|
if (!existing) return {
|
|
915
943
|
success: false,
|
|
916
944
|
error: "Resource not found",
|
|
@@ -922,7 +950,8 @@ var BaseController = class {
|
|
|
922
950
|
details: { code: "OWNERSHIP_DENIED" },
|
|
923
951
|
status: 403
|
|
924
952
|
};
|
|
925
|
-
const
|
|
953
|
+
const repoId = this.resolveRepoId(id, existing);
|
|
954
|
+
const item = await repo.restore(repoId);
|
|
926
955
|
if (!item) return {
|
|
927
956
|
success: false,
|
|
928
957
|
error: "Resource not found",
|
|
@@ -978,12 +1007,129 @@ var BaseController = class {
|
|
|
978
1007
|
error: "Bulk create requires a non-empty items array",
|
|
979
1008
|
status: 400
|
|
980
1009
|
};
|
|
981
|
-
const
|
|
1010
|
+
const arcContext = this.meta(req);
|
|
1011
|
+
const sanitizedItems = items.map((item) => this.bodySanitizer.sanitize(item ?? {}, "create", req, arcContext));
|
|
1012
|
+
let scopedItems = sanitizedItems;
|
|
1013
|
+
if (this.tenantField) {
|
|
1014
|
+
const scope = arcContext?._scope;
|
|
1015
|
+
if (scope) {
|
|
1016
|
+
if (scope.kind === "public") return {
|
|
1017
|
+
success: false,
|
|
1018
|
+
error: "Organization context required to bulk-create resources",
|
|
1019
|
+
details: { code: "ORG_CONTEXT_REQUIRED" },
|
|
1020
|
+
status: 403
|
|
1021
|
+
};
|
|
1022
|
+
if (!isElevated(scope)) {
|
|
1023
|
+
const orgId = getOrgId(scope);
|
|
1024
|
+
if (!orgId) return {
|
|
1025
|
+
success: false,
|
|
1026
|
+
error: "Organization context required to bulk-create resources",
|
|
1027
|
+
details: { code: "ORG_CONTEXT_REQUIRED" },
|
|
1028
|
+
status: 403
|
|
1029
|
+
};
|
|
1030
|
+
const tenantField = this.tenantField;
|
|
1031
|
+
scopedItems = sanitizedItems.map((item) => ({
|
|
1032
|
+
...item,
|
|
1033
|
+
[tenantField]: orgId
|
|
1034
|
+
}));
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
const created = await repo.createMany(scopedItems);
|
|
1039
|
+
const requested = items.length;
|
|
1040
|
+
const inserted = created.length;
|
|
1041
|
+
const skipped = requested - inserted;
|
|
982
1042
|
return {
|
|
983
1043
|
success: true,
|
|
984
1044
|
data: created,
|
|
985
|
-
status: 201,
|
|
986
|
-
meta: {
|
|
1045
|
+
status: skipped === 0 ? 201 : inserted === 0 ? 422 : 207,
|
|
1046
|
+
meta: {
|
|
1047
|
+
count: inserted,
|
|
1048
|
+
requested,
|
|
1049
|
+
inserted,
|
|
1050
|
+
skipped,
|
|
1051
|
+
...skipped > 0 && {
|
|
1052
|
+
partial: true,
|
|
1053
|
+
reason: inserted === 0 ? "all_invalid" : "some_invalid"
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Build a tenant-scoped filter for bulk update/delete.
|
|
1060
|
+
*
|
|
1061
|
+
* Mirrors `AccessControl.buildIdFilter` semantics for single-doc ops:
|
|
1062
|
+
* - Always merge `_policyFilters` (from permission middleware)
|
|
1063
|
+
* - When `tenantField` is set AND a `member` scope is present, add the
|
|
1064
|
+
* org filter so cross-tenant data can't be touched.
|
|
1065
|
+
* - When the scope is `elevated` (platform admin), no org filter is
|
|
1066
|
+
* applied — admins can bulk-update across orgs intentionally.
|
|
1067
|
+
* - When the scope is `public` on a tenant-scoped resource, deny.
|
|
1068
|
+
* - When NO scope is present at all (e.g., direct controller calls in
|
|
1069
|
+
* unit tests, or app routes without auth middleware), the controller
|
|
1070
|
+
* stays lenient — it's the middleware layer's job to fail-close.
|
|
1071
|
+
* Apps that want fail-close on bulk routes should run the multi-tenant
|
|
1072
|
+
* preset middleware (or equivalent) ahead of these handlers.
|
|
1073
|
+
*
|
|
1074
|
+
* Returns the merged filter, or `null` when access must be denied.
|
|
1075
|
+
*/
|
|
1076
|
+
buildBulkFilter(userFilter, req) {
|
|
1077
|
+
const filter = { ...userFilter };
|
|
1078
|
+
const arcContext = this.meta(req);
|
|
1079
|
+
const policyFilters = arcContext?._policyFilters;
|
|
1080
|
+
if (policyFilters) Object.assign(filter, policyFilters);
|
|
1081
|
+
if (this.tenantField) {
|
|
1082
|
+
const scope = arcContext?._scope;
|
|
1083
|
+
if (!scope) return filter;
|
|
1084
|
+
if (scope.kind === "public") return null;
|
|
1085
|
+
if (isElevated(scope)) return filter;
|
|
1086
|
+
const orgId = getOrgId(scope);
|
|
1087
|
+
if (!orgId) return null;
|
|
1088
|
+
filter[this.tenantField] = orgId;
|
|
1089
|
+
}
|
|
1090
|
+
return filter;
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Sanitize a bulk update data payload through the same write-permission
|
|
1094
|
+
* pipeline as single-doc update(). Handles both shapes:
|
|
1095
|
+
*
|
|
1096
|
+
* - Flat: `{ name: 'x', status: 'y' }`
|
|
1097
|
+
* - Mongo operator: `{ $set: { name: 'x' }, $inc: { views: 1 }, $unset: { tag: '' } }`
|
|
1098
|
+
*
|
|
1099
|
+
* For each operand, runs `bodySanitizer.sanitize('update', ...)` so that
|
|
1100
|
+
* system fields, systemManaged/readonly/immutable rules, AND field-level
|
|
1101
|
+
* write permissions are enforced. Without this, a tenant-scoped user could
|
|
1102
|
+
* pass `{ $set: { organizationId: 'org-b' } }` to move records across orgs.
|
|
1103
|
+
*
|
|
1104
|
+
* Returns the sanitized payload along with the list of stripped fields for
|
|
1105
|
+
* audit/error reporting.
|
|
1106
|
+
*/
|
|
1107
|
+
sanitizeBulkUpdateData(data, req, arcContext) {
|
|
1108
|
+
const stripped = /* @__PURE__ */ new Set();
|
|
1109
|
+
if (!Object.keys(data).some((k) => k.startsWith("$"))) {
|
|
1110
|
+
const before = new Set(Object.keys(data));
|
|
1111
|
+
const sanitized = this.bodySanitizer.sanitize(data, "update", req, arcContext);
|
|
1112
|
+
for (const key of before) if (!(key in sanitized)) stripped.add(key);
|
|
1113
|
+
return {
|
|
1114
|
+
sanitized,
|
|
1115
|
+
stripped: [...stripped]
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
const sanitized = {};
|
|
1119
|
+
for (const [op, operand] of Object.entries(data)) {
|
|
1120
|
+
if (!op.startsWith("$") || operand === null || typeof operand !== "object") {
|
|
1121
|
+
sanitized[op] = operand;
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
const operandObj = operand;
|
|
1125
|
+
const before = new Set(Object.keys(operandObj));
|
|
1126
|
+
const sanitizedOperand = this.bodySanitizer.sanitize(operandObj, "update", req, arcContext);
|
|
1127
|
+
for (const key of before) if (!(key in sanitizedOperand)) stripped.add(key);
|
|
1128
|
+
if (Object.keys(sanitizedOperand).length > 0) sanitized[op] = sanitizedOperand;
|
|
1129
|
+
}
|
|
1130
|
+
return {
|
|
1131
|
+
sanitized,
|
|
1132
|
+
stripped: [...stripped]
|
|
987
1133
|
};
|
|
988
1134
|
}
|
|
989
1135
|
async bulkUpdate(req) {
|
|
@@ -1004,12 +1150,48 @@ var BaseController = class {
|
|
|
1004
1150
|
error: "Bulk update requires non-empty data",
|
|
1005
1151
|
status: 400
|
|
1006
1152
|
};
|
|
1153
|
+
const scopedFilter = this.buildBulkFilter(body.filter, req);
|
|
1154
|
+
if (scopedFilter === null) return {
|
|
1155
|
+
success: false,
|
|
1156
|
+
error: "Organization context required for bulk update",
|
|
1157
|
+
details: { code: "ORG_CONTEXT_REQUIRED" },
|
|
1158
|
+
status: 403
|
|
1159
|
+
};
|
|
1160
|
+
const arcContext = this.meta(req);
|
|
1161
|
+
const { sanitized, stripped } = this.sanitizeBulkUpdateData(body.data, req, arcContext);
|
|
1162
|
+
if (Object.keys(sanitized).length === 0) return {
|
|
1163
|
+
success: false,
|
|
1164
|
+
error: "Bulk update payload contained only protected fields",
|
|
1165
|
+
details: {
|
|
1166
|
+
code: "ALL_FIELDS_STRIPPED",
|
|
1167
|
+
stripped
|
|
1168
|
+
},
|
|
1169
|
+
status: 400
|
|
1170
|
+
};
|
|
1007
1171
|
return {
|
|
1008
1172
|
success: true,
|
|
1009
|
-
data: await repo.updateMany(
|
|
1010
|
-
status: 200
|
|
1173
|
+
data: await repo.updateMany(scopedFilter, sanitized),
|
|
1174
|
+
status: 200,
|
|
1175
|
+
...stripped.length > 0 && { meta: { stripped } }
|
|
1011
1176
|
};
|
|
1012
1177
|
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Bulk delete by `filter` or `ids`.
|
|
1180
|
+
*
|
|
1181
|
+
* Body shape (one of):
|
|
1182
|
+
* - `{ filter: { status: 'archived' } }` — delete by query filter
|
|
1183
|
+
* - `{ ids: ['id1', 'id2', 'id3'] }` — delete specific docs by id
|
|
1184
|
+
*
|
|
1185
|
+
* The `ids` form translates to `{ [idField]: { $in: ids } }` using the
|
|
1186
|
+
* resource's `idField` (so it works with custom PKs like `slug`, `jobId`,
|
|
1187
|
+
* UUID, etc.). Tenant scope and policy filters are merged in either way,
|
|
1188
|
+
* so cross-tenant deletes are blocked at the controller layer.
|
|
1189
|
+
*
|
|
1190
|
+
* Both forms perform a single `repo.deleteMany()` DB call — no per-doc
|
|
1191
|
+
* fetch loop. Per-doc lifecycle hooks (`before:delete`/`after:delete`) do
|
|
1192
|
+
* NOT fire for bulk operations; use the single-doc `delete()` if you need
|
|
1193
|
+
* them, or subscribe to the bulk lifecycle event from the events plugin.
|
|
1194
|
+
*/
|
|
1013
1195
|
async bulkDelete(req) {
|
|
1014
1196
|
const repo = this.repository;
|
|
1015
1197
|
if (!repo.deleteMany) return {
|
|
@@ -1018,14 +1200,30 @@ var BaseController = class {
|
|
|
1018
1200
|
status: 501
|
|
1019
1201
|
};
|
|
1020
1202
|
const body = req.body;
|
|
1021
|
-
|
|
1203
|
+
let userFilter;
|
|
1204
|
+
if (body.ids && body.ids.length > 0) {
|
|
1205
|
+
if (body.filter && Object.keys(body.filter).length > 0) return {
|
|
1206
|
+
success: false,
|
|
1207
|
+
error: "Bulk delete accepts either `ids` or `filter`, not both",
|
|
1208
|
+
status: 400
|
|
1209
|
+
};
|
|
1210
|
+
userFilter = { [this.idField]: { $in: body.ids } };
|
|
1211
|
+
} else if (body.filter && Object.keys(body.filter).length > 0) userFilter = body.filter;
|
|
1212
|
+
else return {
|
|
1022
1213
|
success: false,
|
|
1023
|
-
error: "Bulk delete requires a non-empty filter",
|
|
1214
|
+
error: "Bulk delete requires a non-empty `filter` or `ids` array",
|
|
1024
1215
|
status: 400
|
|
1025
1216
|
};
|
|
1217
|
+
const scopedFilter = this.buildBulkFilter(userFilter, req);
|
|
1218
|
+
if (scopedFilter === null) return {
|
|
1219
|
+
success: false,
|
|
1220
|
+
error: "Organization context required for bulk delete",
|
|
1221
|
+
details: { code: "ORG_CONTEXT_REQUIRED" },
|
|
1222
|
+
status: 403
|
|
1223
|
+
};
|
|
1026
1224
|
return {
|
|
1027
1225
|
success: true,
|
|
1028
|
-
data: await repo.deleteMany(
|
|
1226
|
+
data: await repo.deleteMany(scopedFilter),
|
|
1029
1227
|
status: 200
|
|
1030
1228
|
};
|
|
1031
1229
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { a as PrismaQueryParserOptions, c as MongooseAdapterOptions, i as PrismaQueryParser, l as createMongooseAdapter, n as PrismaAdapterOptions, o as createPrismaAdapter, r as PrismaQueryOptions, s as MongooseAdapter, t as PrismaAdapter } from "../index-
|
|
1
|
+
import { a as RelationMetadata, c as ValidationResult, i as FieldMetadata, o as RepositoryLike, r as DataAdapter, s as SchemaMetadata, t as AdapterFactory } from "../interface-Dwzqt4mn.mjs";
|
|
2
|
+
import { a as PrismaQueryParserOptions, c as MongooseAdapterOptions, i as PrismaQueryParser, l as createMongooseAdapter, n as PrismaAdapterOptions, o as createPrismaAdapter, r as PrismaQueryOptions, s as MongooseAdapter, t as PrismaAdapter } from "../index-StgFaQKD.mjs";
|
|
3
3
|
export { AdapterFactory, DataAdapter, FieldMetadata, MongooseAdapter, MongooseAdapterOptions, PrismaAdapter, PrismaAdapterOptions, PrismaQueryOptions, PrismaQueryParser, PrismaQueryParserOptions, RelationMetadata, RepositoryLike, SchemaMetadata, ValidationResult, createMongooseAdapter, createPrismaAdapter };
|
package/dist/adapters/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as createMongooseAdapter, i as MongooseAdapter, n as PrismaQueryParser, r as createPrismaAdapter, t as PrismaAdapter } from "../adapters-
|
|
1
|
+
import { a as createMongooseAdapter, i as MongooseAdapter, n as PrismaQueryParser, r as createPrismaAdapter, t as PrismaAdapter } from "../adapters-BxGgSHjj.mjs";
|
|
2
2
|
export { MongooseAdapter, PrismaAdapter, PrismaQueryParser, createMongooseAdapter, createPrismaAdapter };
|
|
@@ -71,9 +71,9 @@ var MongooseAdapter = class {
|
|
|
71
71
|
* If a `schemaGenerator` plugin was provided (e.g. MongoKit's buildCrudSchemasFromModel),
|
|
72
72
|
* it is used instead of the built-in basic conversion.
|
|
73
73
|
*/
|
|
74
|
-
generateSchemas(schemaOptions) {
|
|
74
|
+
generateSchemas(schemaOptions, context) {
|
|
75
75
|
try {
|
|
76
|
-
if (this.schemaGenerator) return this.schemaGenerator(this.model, schemaOptions);
|
|
76
|
+
if (this.schemaGenerator) return this.schemaGenerator(this.model, schemaOptions, context);
|
|
77
77
|
const paths = this.model.schema.paths;
|
|
78
78
|
const properties = {};
|
|
79
79
|
const required = [];
|
|
@@ -104,11 +104,13 @@ var MongooseAdapter = class {
|
|
|
104
104
|
createBody: {
|
|
105
105
|
type: "object",
|
|
106
106
|
properties: inputProperties,
|
|
107
|
-
required: inputRequired.length > 0 ? inputRequired : void 0
|
|
107
|
+
required: inputRequired.length > 0 ? inputRequired : void 0,
|
|
108
|
+
additionalProperties: true
|
|
108
109
|
},
|
|
109
110
|
updateBody: {
|
|
110
111
|
type: "object",
|
|
111
|
-
properties: updateProperties
|
|
112
|
+
properties: updateProperties,
|
|
113
|
+
additionalProperties: true
|
|
112
114
|
},
|
|
113
115
|
response: {
|
|
114
116
|
type: "object",
|
|
@@ -187,15 +189,7 @@ var MongooseAdapter = class {
|
|
|
187
189
|
else baseType.items = {};
|
|
188
190
|
break;
|
|
189
191
|
}
|
|
190
|
-
case "Mixed":
|
|
191
|
-
baseType.type = [
|
|
192
|
-
"string",
|
|
193
|
-
"number",
|
|
194
|
-
"boolean",
|
|
195
|
-
"object",
|
|
196
|
-
"array"
|
|
197
|
-
];
|
|
198
|
-
break;
|
|
192
|
+
case "Mixed": break;
|
|
199
193
|
case "Map":
|
|
200
194
|
baseType.type = "object";
|
|
201
195
|
baseType.additionalProperties = true;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region src/permissions/applyPermissionResult.ts
|
|
2
|
+
/**
|
|
3
|
+
* Normalize a permission check return value (`boolean | PermissionResult`)
|
|
4
|
+
* into a concrete `PermissionResult`. This is the only place in Arc that
|
|
5
|
+
* promotes booleans to results — keeps the type narrowing honest everywhere.
|
|
6
|
+
*/
|
|
7
|
+
function normalizePermissionResult(result) {
|
|
8
|
+
if (typeof result === "boolean") return { granted: result };
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Apply a granted `PermissionResult` to a Fastify request — merges row-level
|
|
13
|
+
* filters into `_policyFilters` and conditionally installs the scope.
|
|
14
|
+
*
|
|
15
|
+
* **Scope install rule:** only writes `scope` when the current request scope
|
|
16
|
+
* is absent or `public`. This prevents downgrading an already-authenticated
|
|
17
|
+
* request (e.g. Better Auth set `member`, then a permission check returns a
|
|
18
|
+
* narrower `service` scope — the original `member` wins because it came from
|
|
19
|
+
* a more authoritative source).
|
|
20
|
+
*
|
|
21
|
+
* Safe to call with a non-granted result — it simply no-ops. Callers should
|
|
22
|
+
* still check `result.granted` and send an error response before reaching here,
|
|
23
|
+
* but this function tolerates the misuse defensively.
|
|
24
|
+
*/
|
|
25
|
+
function applyPermissionResult(result, request) {
|
|
26
|
+
if (!result.granted) return;
|
|
27
|
+
if (result.filters) request._policyFilters = {
|
|
28
|
+
...request._policyFilters ?? {},
|
|
29
|
+
...result.filters
|
|
30
|
+
};
|
|
31
|
+
if (result.scope) {
|
|
32
|
+
const current = request.scope;
|
|
33
|
+
if (!current || current.kind === "public") request.scope = result.scope;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { normalizePermissionResult as n, applyPermissionResult as t };
|
package/dist/audit/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as AuditContext, c as AuditStore, i as AuditAction, l as AuditStoreOptions, n as MongoAuditStoreOptions, o as AuditEntry, r as MongoConnection, s as AuditQueryOptions, u as createAuditEntry } from "../mongodb-
|
|
1
|
+
import { a as AuditContext, c as AuditStore, i as AuditAction, l as AuditStoreOptions, n as MongoAuditStoreOptions, o as AuditEntry, r as MongoConnection, s as AuditQueryOptions, u as createAuditEntry } from "../mongodb-DdyYlIXg.mjs";
|
|
2
2
|
import { FastifyPluginAsync } from "fastify";
|
|
3
3
|
|
|
4
4
|
//#region src/audit/auditPlugin.d.ts
|
package/dist/audit/index.mjs
CHANGED
package/dist/audit/mongodb.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as MongoAuditStoreOptions, t as MongoAuditStore } from "../mongodb-
|
|
1
|
+
import { n as MongoAuditStoreOptions, t as MongoAuditStore } from "../mongodb-DdyYlIXg.mjs";
|
|
2
2
|
export { MongoAuditStore, type MongoAuditStoreOptions };
|
package/dist/audit/mongodb.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as MongoAuditStore } from "../mongodb-
|
|
1
|
+
import { t as MongoAuditStore } from "../mongodb-mlgxkYI3.mjs";
|
|
2
2
|
export { MongoAuditStore };
|
package/dist/auth/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as PermissionCheck } from "../types-
|
|
3
|
-
import { t as ExternalOpenApiPaths } from "../externalPaths-
|
|
4
|
-
import { a as SessionManagerOptions, c as createSessionManager, i as SessionData, n as MemorySessionStoreOptions, o as SessionManagerResult, r as SessionCookieOptions, s as SessionStore, t as MemorySessionStore } from "../sessionManager-
|
|
1
|
+
import { g as AuthPluginOptions, h as AuthHelpers } from "../interface-Dwzqt4mn.mjs";
|
|
2
|
+
import { t as PermissionCheck } from "../types-DPsC0taJ.mjs";
|
|
3
|
+
import { t as ExternalOpenApiPaths } from "../externalPaths-Dg7OLsKo.mjs";
|
|
4
|
+
import { a as SessionManagerOptions, c as createSessionManager, i as SessionData, n as MemorySessionStoreOptions, o as SessionManagerResult, r as SessionCookieOptions, s as SessionStore, t as MemorySessionStore } from "../sessionManager-IW4sbIea.mjs";
|
|
5
5
|
import { FastifyPluginAsync, FastifyReply as FastifyReply$1, FastifyRequest as FastifyRequest$1 } from "fastify";
|
|
6
6
|
|
|
7
7
|
//#region src/auth/authPlugin.d.ts
|
package/dist/auth/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as normalizeRoles, t as getUserRoles } from "../types-ZUu_h0jp.mjs";
|
|
2
|
-
import { t as ArcError } from "../errors-
|
|
3
|
-
import {
|
|
4
|
-
import { n as extractBetterAuthOpenApi } from "../betterAuthOpenApi-
|
|
2
|
+
import { t as ArcError } from "../errors-Cg58SLNi.mjs";
|
|
3
|
+
import { h as requireTeamMembership, l as requireOrgMembership, u as requireOrgRole } from "../permissions-CH4cNwJi.mjs";
|
|
4
|
+
import { n as extractBetterAuthOpenApi } from "../betterAuthOpenApi-CCw3YX0g.mjs";
|
|
5
5
|
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
6
6
|
import fp from "fastify-plugin";
|
|
7
7
|
//#region src/auth/authPlugin.ts
|
|
@@ -588,10 +588,11 @@ function createBetterAuthAdapter(options) {
|
|
|
588
588
|
const teamsResponse = await auth.handler(teamsRequest);
|
|
589
589
|
if (teamsResponse.ok) {
|
|
590
590
|
const teamsData = await teamsResponse.json();
|
|
591
|
-
|
|
591
|
+
const teamsList = Array.isArray(teamsData) ? teamsData : teamsData?.teams;
|
|
592
|
+
teams = Array.isArray(teamsList) ? teamsList : [];
|
|
592
593
|
}
|
|
593
594
|
}
|
|
594
|
-
if (teams?.some((t) => t.id === activeTeamId)) scope.teamId = activeTeamId;
|
|
595
|
+
if (teams?.some((t) => normalizeId(t.id) === activeTeamId)) scope.teamId = activeTeamId;
|
|
595
596
|
}
|
|
596
597
|
req.scope = scope;
|
|
597
598
|
}
|
|
@@ -676,7 +677,7 @@ function createBetterAuthAdapter(options) {
|
|
|
676
677
|
if (!fastify.hasDecorator("authenticate")) fastify.decorate("authenticate", authenticate);
|
|
677
678
|
if (!fastify.hasDecorator("optionalAuthenticate")) fastify.decorate("optionalAuthenticate", optionalAuthenticate);
|
|
678
679
|
if (!extractedOpenApi && openapiOpt !== false && auth.api && typeof auth.api === "object") {
|
|
679
|
-
const { extractBetterAuthOpenApi } = await import("../betterAuthOpenApi-
|
|
680
|
+
const { extractBetterAuthOpenApi } = await import("../betterAuthOpenApi-CCw3YX0g.mjs").then((n) => n.t);
|
|
680
681
|
extractedOpenApi = extractBetterAuthOpenApi(auth.api, {
|
|
681
682
|
basePath,
|
|
682
683
|
userFields
|