@classytic/arc 2.6.3 → 2.7.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.
Files changed (143) hide show
  1. package/README.md +98 -3
  2. package/dist/{BaseController-DzRtluEF.mjs → BaseController-CpMfCXdn.mjs} +134 -16
  3. package/dist/adapters/index.d.mts +2 -2
  4. package/dist/adapters/index.mjs +1 -1
  5. package/dist/{adapters-gM-WYjNe.mjs → adapters-BxGgSHjj.mjs} +1 -9
  6. package/dist/applyPermissionResult-D6GPMsvh.mjs +37 -0
  7. package/dist/audit/index.d.mts +1 -1
  8. package/dist/audit/index.mjs +1 -1
  9. package/dist/audit/mongodb.d.mts +1 -1
  10. package/dist/audit/mongodb.mjs +1 -1
  11. package/dist/auth/index.d.mts +4 -4
  12. package/dist/auth/index.mjs +7 -6
  13. package/dist/auth/mongoose.d.mts +191 -0
  14. package/dist/auth/mongoose.mjs +73 -0
  15. package/dist/auth/redis-session.d.mts +1 -1
  16. package/dist/{betterAuthOpenApi-lz0IRbXJ.mjs → betterAuthOpenApi-CCw3YX0g.mjs} +1 -1
  17. package/dist/cache/index.d.mts +2 -2
  18. package/dist/cache/index.mjs +2 -2
  19. package/dist/cli/commands/docs.mjs +2 -2
  20. package/dist/cli/commands/generate.mjs +1 -1
  21. package/dist/cli/commands/init.mjs +7 -5
  22. package/dist/cli/commands/introspect.mjs +1 -1
  23. package/dist/core/index.d.mts +3 -3
  24. package/dist/core/index.mjs +4 -4
  25. package/dist/{core-C1XCMtqM.mjs → core-BWekSEju.mjs} +41 -13
  26. package/dist/{createApp-D2w0LdYJ.mjs → createApp-D7e77m8C.mjs} +25 -14
  27. package/dist/{defineResource-wWMBB4GP.mjs → defineResource-DZzyl4a4.mjs} +42 -37
  28. package/dist/docs/index.d.mts +2 -2
  29. package/dist/docs/index.mjs +1 -1
  30. package/dist/dynamic/index.d.mts +2 -2
  31. package/dist/dynamic/index.mjs +2 -2
  32. package/dist/{elevation-BEdACOLB.mjs → elevation-By_p2lnn.mjs} +1 -1
  33. package/dist/elevation-D7WK0RXq.d.mts +23 -0
  34. package/dist/{errorHandler-r2595m8T.mjs → errorHandler-CH8wk1eD.mjs} +17 -2
  35. package/dist/{errorHandler-Do4vVQ1f.d.mts → errorHandler-pCpEtNd7.d.mts} +46 -2
  36. package/dist/{eventPlugin-Ba00swHF.mjs → eventPlugin-B6U_nCFU.mjs} +4 -3
  37. package/dist/{eventPlugin-DW45v4V5.d.mts → eventPlugin-CdvUoUna.d.mts} +1 -1
  38. package/dist/events/index.d.mts +3 -3
  39. package/dist/events/index.mjs +1 -1
  40. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  41. package/dist/events/transports/redis.d.mts +1 -1
  42. package/dist/factory/index.d.mts +1 -1
  43. package/dist/factory/index.mjs +1 -1
  44. package/dist/hooks/index.d.mts +1 -1
  45. package/dist/hooks/index.mjs +1 -1
  46. package/dist/idempotency/index.d.mts +3 -3
  47. package/dist/idempotency/mongodb.d.mts +1 -1
  48. package/dist/idempotency/redis.d.mts +1 -1
  49. package/dist/index-B0extFr4.d.mts +640 -0
  50. package/dist/{index-gz6iuzCp.d.mts → index-BjShrzoj.d.mts} +47 -4
  51. package/dist/{index-CHeJa4Zd.d.mts → index-C9eYNjGR.d.mts} +1 -1
  52. package/dist/index.d.mts +9 -8
  53. package/dist/index.mjs +10 -9
  54. package/dist/integrations/event-gateway.d.mts +1 -1
  55. package/dist/integrations/event-gateway.mjs +1 -1
  56. package/dist/integrations/index.d.mts +1 -1
  57. package/dist/integrations/mcp/index.d.mts +2 -2
  58. package/dist/integrations/mcp/index.mjs +8 -5
  59. package/dist/integrations/mcp/testing.d.mts +1 -1
  60. package/dist/integrations/mcp/testing.mjs +1 -1
  61. package/dist/integrations/webhooks.d.mts +58 -1
  62. package/dist/integrations/webhooks.mjs +78 -7
  63. package/dist/integrations/websocket.d.mts +7 -1
  64. package/dist/integrations/websocket.mjs +7 -1
  65. package/dist/{interface-DYH8AXGe.d.mts → interface-B91alUzq.d.mts} +151 -15
  66. package/dist/{mongodb-pMvOlR5_.d.mts → mongodb-B7zupyck.d.mts} +1 -1
  67. package/dist/{mongodb-kltrBPa1.d.mts → mongodb-Cgu9F1Nd.d.mts} +1 -1
  68. package/dist/{openapi-CBmZ6EQN.mjs → openapi-BBSTVcMm.mjs} +1 -1
  69. package/dist/org/index.d.mts +2 -2
  70. package/dist/org/index.mjs +1 -1
  71. package/dist/permissions/index.d.mts +4 -4
  72. package/dist/permissions/index.mjs +3 -2
  73. package/dist/{permissions-C8ImI8gC.mjs → permissions-CH4cNwJi.mjs} +358 -64
  74. package/dist/plugins/index.d.mts +52 -5
  75. package/dist/plugins/index.mjs +12 -11
  76. package/dist/plugins/response-cache.mjs +1 -1
  77. package/dist/plugins/tracing-entry.d.mts +1 -1
  78. package/dist/plugins/tracing-entry.mjs +1 -1
  79. package/dist/policies/index.d.mts +1 -1
  80. package/dist/presets/index.d.mts +3 -3
  81. package/dist/presets/index.mjs +1 -1
  82. package/dist/presets/multiTenant.d.mts +53 -3
  83. package/dist/presets/multiTenant.mjs +89 -47
  84. package/dist/{presets-BMfdy34e.mjs → presets-BFrGvvjL.mjs} +2 -2
  85. package/dist/{queryCachePlugin-DcmETvcB.d.mts → queryCachePlugin-Ckl71mkc.d.mts} +1 -1
  86. package/dist/{queryCachePlugin-XtFplYO9.mjs → queryCachePlugin-CwTpR04-.mjs} +2 -2
  87. package/dist/{redis-D0Qc-9EW.d.mts → redis-3TQxm2VZ.d.mts} +1 -1
  88. package/dist/{redis-stream-BW9UKLZM.d.mts → redis-stream-Dag5LFa9.d.mts} +1 -1
  89. package/dist/registry/index.d.mts +1 -1
  90. package/dist/registry/index.mjs +2 -2
  91. package/dist/replyHelpers-uDUIYh7u.mjs +40 -0
  92. package/dist/{resourceToTools-nCJWnG1r.mjs → resourceToTools-BJkoQoUP.mjs} +74 -25
  93. package/dist/rpc/index.d.mts +1 -1
  94. package/dist/rpc/index.mjs +1 -1
  95. package/dist/scope/index.d.mts +3 -2
  96. package/dist/scope/index.mjs +4 -3
  97. package/dist/{sse-BF7GR7IB.mjs → sse-6W0hjVS_.mjs} +2 -2
  98. package/dist/testing/index.d.mts +2 -2
  99. package/dist/testing/index.mjs +1 -1
  100. package/dist/types/index.d.mts +4 -3
  101. package/dist/types/index.mjs +1 -1
  102. package/dist/types--D3vvfdt.d.mts +286 -0
  103. package/dist/{types-By-5mIfn.d.mts → types-2FlNl0mL.d.mts} +44 -9
  104. package/dist/types-AOD8fxIw.mjs +229 -0
  105. package/dist/types-B4BNthET.d.mts +178 -0
  106. package/dist/{types-B4_TDdPe.d.mts → types-C5g2oRC7.d.mts} +18 -2
  107. package/dist/utils/index.d.mts +3 -3
  108. package/dist/utils/index.mjs +5 -5
  109. package/package.json +21 -6
  110. package/skills/arc/SKILL.md +314 -6
  111. package/skills/arc/references/integrations.md +32 -7
  112. package/skills/arc/references/mcp.md +31 -7
  113. package/skills/arc/references/multi-tenancy.md +208 -0
  114. package/skills/arc/references/production.md +69 -0
  115. package/dist/elevation-C_taLQrM.d.mts +0 -147
  116. package/dist/index-NGZksqM5.d.mts +0 -398
  117. package/dist/types-BNUccdcf.d.mts +0 -101
  118. package/dist/types-BhtYdxZU.mjs +0 -91
  119. /package/dist/{EventTransport-wc5hSLik.d.mts → EventTransport-C4VheKeC.d.mts} +0 -0
  120. /package/dist/{HookSystem-COkyWztM.mjs → HookSystem-D7lfx--K.mjs} +0 -0
  121. /package/dist/{ResourceRegistry-C6ngvOnn.mjs → ResourceRegistry-DsHiG9cL.mjs} +0 -0
  122. /package/dist/{caching-BSXB-Xr7.mjs → caching-5DtLwIqb.mjs} +0 -0
  123. /package/dist/{circuitBreaker-JP2GdJ4b.d.mts → circuitBreaker-BBPDt-J_.d.mts} +0 -0
  124. /package/dist/{circuitBreaker-BOBOpN2w.mjs → circuitBreaker-l18oRgL5.mjs} +0 -0
  125. /package/dist/{errors-CcVbl1-T.d.mts → errors-BS6lZvWy.d.mts} +0 -0
  126. /package/dist/{errors-NoQKsbAT.mjs → errors-Cg58SLNi.mjs} +0 -0
  127. /package/dist/{externalPaths-DpO-s7r8.d.mts → externalPaths-iba7jD3d.d.mts} +0 -0
  128. /package/dist/{fields-DFwdaWCq.d.mts → fields-D4nMDqnK.d.mts} +0 -0
  129. /package/dist/{interface-D_BWALyZ.d.mts → interface-CG7oRZjX.d.mts} +0 -0
  130. /package/dist/{interface-gr-7qo9j.d.mts → interface-CSbZdv_3.d.mts} +0 -0
  131. /package/dist/{logger-Dz3j1ItV.mjs → logger-DLg8-Ueg.mjs} +0 -0
  132. /package/dist/{memory-BFAYkf8H.mjs → memory-Cp7_cAko.mjs} +0 -0
  133. /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
  134. /package/dist/{mongodb-BuQ7fNTg.mjs → mongodb-B7X7P1P8.mjs} +0 -0
  135. /package/dist/{pluralize-CcT6qF0a.mjs → pluralize-Dckfq6US.mjs} +0 -0
  136. /package/dist/{registry-I-ogLgL9.mjs → registry-B3lRFBWo.mjs} +0 -0
  137. /package/dist/{requestContext-DYtmNpm5.mjs → requestContext-xHIKedG6.mjs} +0 -0
  138. /package/dist/{schemaConverter-DjzHpFam.mjs → schemaConverter-0TyONAwM.mjs} +0 -0
  139. /package/dist/{sessionManager-wbkYj2HL.d.mts → sessionManager-CEo9jwPI.d.mts} +0 -0
  140. /package/dist/{tracing-bz_U4EM1.d.mts → tracing-DEqdGkr-.d.mts} +0 -0
  141. /package/dist/{typeGuards-Cj5Rgvlg.mjs → typeGuards-CcFZXgU7.mjs} +0 -0
  142. /package/dist/{utils-Dc0WhlIl.mjs → utils-B-l6410F.mjs} +0 -0
  143. /package/dist/{versioning-BzfeHmhj.mjs → versioning-CdBbFefk.mjs} +0 -0
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
- import { d as isElevated, f as isMember, n as PUBLIC_SCOPE, o as getTeamId, s as getUserId } from "./types-BhtYdxZU.mjs";
2
+ import { _ as isElevated, b as isService, c as getRequestScope, d as getServiceScopes, f as getTeamId, h as hasOrgAccess, l as getScopeContext, p as getUserId, u as getScopeContextMap, v as isMember, y as isOrgInScope } from "./types-AOD8fxIw.mjs";
3
3
  import { t as getUserRoles } from "./types-ZUu_h0jp.mjs";
4
- import { t as MemoryCacheStore } from "./memory-BFAYkf8H.mjs";
4
+ import { t as MemoryCacheStore } from "./memory-Cp7_cAko.mjs";
5
5
  import { randomUUID } from "node:crypto";
6
6
  //#region src/permissions/roleHierarchy.ts
7
7
  /**
@@ -180,6 +180,29 @@ function readOnly(overrides) {
180
180
  //#endregion
181
181
  //#region src/permissions/index.ts
182
182
  /**
183
+ * Normalize a `string | [readonly string[]]` rest-args tuple into a single
184
+ * `readonly string[]`. Lets a permission helper accept BOTH variadic and
185
+ * array call shapes from the same overload signature without each helper
186
+ * re-implementing the same ternary.
187
+ *
188
+ * Used by `requireOrgRole`, `requireServiceScope`, etc. **Not** used by
189
+ * `requireRoles` — that helper has a richer overload signature with an
190
+ * options object and stays on its own normalization path.
191
+ *
192
+ * @example
193
+ * ```typescript
194
+ * function requireFoo(...args: string[] | [readonly string[]]) {
195
+ * const items = normalizeVariadicOrArray(args);
196
+ * // items is always readonly string[]
197
+ * }
198
+ * requireFoo('a', 'b', 'c');
199
+ * requireFoo(['a', 'b', 'c']);
200
+ * ```
201
+ */
202
+ function normalizeVariadicOrArray(args) {
203
+ return Array.isArray(args[0]) ? args[0] : args;
204
+ }
205
+ /**
183
206
  * Allow public access (no authentication required)
184
207
  *
185
208
  * @example
@@ -216,26 +239,21 @@ function requireAuth() {
216
239
  };
217
240
  return check;
218
241
  }
219
- /**
220
- * Require specific roles
221
- *
222
- * @param roles - Required roles (user needs at least one)
223
- * @param options - Optional bypass roles
224
- *
225
- * @example
226
- * ```typescript
227
- * permissions: {
228
- * create: requireRoles(['admin', 'editor']),
229
- * delete: requireRoles(['admin']),
230
- * }
231
- *
232
- * // With bypass roles
233
- * permissions: {
234
- * update: requireRoles(['owner'], { bypassRoles: ['admin', 'superadmin'] }),
235
- * }
236
- * ```
237
- */
238
- function requireRoles(roles, options) {
242
+ function requireRoles(rolesOrFirst, optionsOrSecond, ...rest) {
243
+ let roles;
244
+ let options;
245
+ if (typeof rolesOrFirst === "string") {
246
+ roles = [
247
+ rolesOrFirst,
248
+ ...typeof optionsOrSecond === "string" ? [optionsOrSecond] : [],
249
+ ...rest
250
+ ];
251
+ options = void 0;
252
+ } else {
253
+ roles = rolesOrFirst;
254
+ options = optionsOrSecond && typeof optionsOrSecond === "object" ? optionsOrSecond : void 0;
255
+ }
256
+ const includeOrgRoles = options?.includeOrgRoles ?? true;
239
257
  const check = (ctx) => {
240
258
  if (!ctx.user) return {
241
259
  granted: false,
@@ -244,8 +262,9 @@ function requireRoles(roles, options) {
244
262
  const userRoles = getUserRoles(ctx.user);
245
263
  if (options?.bypassRoles?.some((r) => userRoles.includes(r))) return true;
246
264
  if (roles.some((r) => userRoles.includes(r))) return true;
247
- if (options?.includeOrgRoles) {
248
- const scope = getScope(ctx.request);
265
+ if (includeOrgRoles) {
266
+ const scope = getRequestScope(ctx.request);
267
+ if (isElevated(scope)) return true;
249
268
  if (isMember(scope) && roles.some((r) => scope.orgRoles.includes(r))) return true;
250
269
  }
251
270
  return {
@@ -257,25 +276,30 @@ function requireRoles(roles, options) {
257
276
  return check;
258
277
  }
259
278
  /**
260
- * Unified role check — checks both platform roles AND org roles.
279
+ * **Alias of `requireRoles()`** — checks both platform roles AND org roles.
261
280
  *
262
- * This is the recommended helper for Better Auth organization plugin users.
263
- * It checks `user.role` (platform) first, then `scope.orgRoles` (org membership).
264
- * Elevated scope always passes.
281
+ * Since 2.7.1, `requireRoles()` defaults to `includeOrgRoles: true`, which
282
+ * means `roles('admin')` and `requireRoles('admin')` are now functionally
283
+ * identical. This helper is preserved for backwards compatibility and for
284
+ * call sites that prefer the shorter `roles()` name.
265
285
  *
266
- * For platform-only checks: use `requireRoles(['admin'])`
267
- * For org-only checks: use `requireOrgRole('admin')`
286
+ * **For new code, prefer `requireRoles()`** — it's the canonical name and
287
+ * matches the rest of the `requireXxx()` family (`requireAuth`, `requireOwnership`,
288
+ * `requireOrgRole`, etc.).
289
+ *
290
+ * For platform-only checks: `requireRoles(['admin'], { includeOrgRoles: false })`
291
+ * For org-only checks: `requireOrgRole('admin')`
268
292
  *
269
293
  * @example
270
294
  * ```typescript
271
- * permissions: {
272
- * create: roles('admin', 'editor'), // passes if user has role at either level
273
- * delete: roles('admin'),
274
- * }
295
+ * // These are identical:
296
+ * roles('admin', 'editor')
297
+ * requireRoles('admin', 'editor')
298
+ * requireRoles(['admin', 'editor'])
275
299
  * ```
276
300
  */
277
301
  function roles(...args) {
278
- const roleList = Array.isArray(args[0]) ? args[0] : args;
302
+ const roleList = normalizeVariadicOrArray(args);
279
303
  const check = (ctx) => {
280
304
  if (!ctx.user) return {
281
305
  granted: false,
@@ -283,7 +307,7 @@ function roles(...args) {
283
307
  };
284
308
  const userRoles = getUserRoles(ctx.user);
285
309
  if (roleList.some((r) => userRoles.includes(r))) return true;
286
- const scope = getScope(ctx.request);
310
+ const scope = getRequestScope(ctx.request);
287
311
  if (isElevated(scope)) return true;
288
312
  if (isMember(scope) && roleList.some((r) => scope.orgRoles.includes(r))) return true;
289
313
  return {
@@ -318,7 +342,7 @@ function requireOwnership(ownerField = "userId", options) {
318
342
  };
319
343
  const userRoles = getUserRoles(ctx.user);
320
344
  if (options?.bypassRoles?.some((r) => userRoles.includes(r))) return true;
321
- const userId = getUserId(getScope(ctx.request)) ?? ctx.user.id ?? ctx.user._id;
345
+ const userId = getUserId(getRequestScope(ctx.request)) ?? ctx.user.id ?? ctx.user._id;
322
346
  if (!userId) return {
323
347
  granted: false,
324
348
  reason: "User identity missing (no id or _id)"
@@ -330,10 +354,22 @@ function requireOwnership(ownerField = "userId", options) {
330
354
  };
331
355
  }
332
356
  /**
333
- * Combine multiple checks - ALL must pass (AND logic)
357
+ * Combine multiple checks - ALL must pass (AND logic).
358
+ *
359
+ * Each child runs against the **accumulated** state of previous children:
360
+ * - `filters` from earlier children are merged into the next child's
361
+ * `_policyFilters` (so e.g. `requireOwnership` sees row-level scoping)
362
+ * - `scope` from earlier children is installed on the request before the
363
+ * next child runs (so e.g. `requireOrgMembership` after `requireApiKey`
364
+ * sees the service scope from the API key check)
365
+ *
366
+ * The final returned `PermissionResult` carries both the merged `filters` AND
367
+ * the merged `scope`, so the outer middleware's `applyPermissionResult` call
368
+ * sees the same end-state.
334
369
  *
335
370
  * @example
336
371
  * ```typescript
372
+ * // CRUD permissions composed across roles + ownership
337
373
  * permissions: {
338
374
  * update: allOf(
339
375
  * requireAuth(),
@@ -341,23 +377,57 @@ function requireOwnership(ownerField = "userId", options) {
341
377
  * requireOwnership('createdBy')
342
378
  * ),
343
379
  * }
380
+ *
381
+ * // Custom auth + org membership — first check installs the scope,
382
+ * // second check reads it.
383
+ * permissions: {
384
+ * list: allOf(requireApiKey(), requireOrgMembership()),
385
+ * }
344
386
  * ```
345
387
  */
346
388
  function allOf(...checks) {
347
389
  return async (ctx) => {
348
390
  let mergedFilters = {};
349
- for (const check of checks) {
350
- const result = await check(ctx);
351
- const normalized = typeof result === "boolean" ? { granted: result } : result;
352
- if (!normalized.granted) return normalized;
353
- if (normalized.filters) mergedFilters = {
354
- ...mergedFilters,
355
- ...normalized.filters
356
- };
391
+ let installedScope;
392
+ const sink = ctx.request;
393
+ const originalFilters = sink._policyFilters;
394
+ const originalScope = sink.scope;
395
+ try {
396
+ for (const check of checks) {
397
+ const result = await check(ctx);
398
+ const normalized = typeof result === "boolean" ? { granted: result } : result;
399
+ if (!normalized.granted) {
400
+ sink._policyFilters = originalFilters;
401
+ sink.scope = originalScope;
402
+ return normalized;
403
+ }
404
+ if (normalized.filters) {
405
+ mergedFilters = {
406
+ ...mergedFilters,
407
+ ...normalized.filters
408
+ };
409
+ sink._policyFilters = {
410
+ ...sink._policyFilters ?? {},
411
+ ...normalized.filters
412
+ };
413
+ }
414
+ if (normalized.scope) {
415
+ const current = sink.scope;
416
+ if (!current || current.kind === "public") {
417
+ sink.scope = normalized.scope;
418
+ installedScope = normalized.scope;
419
+ } else if (!installedScope) installedScope = normalized.scope;
420
+ }
421
+ }
422
+ } catch (err) {
423
+ sink._policyFilters = originalFilters;
424
+ sink.scope = originalScope;
425
+ throw err;
357
426
  }
358
427
  return {
359
428
  granted: true,
360
- filters: Object.keys(mergedFilters).length > 0 ? mergedFilters : void 0
429
+ filters: Object.keys(mergedFilters).length > 0 ? mergedFilters : void 0,
430
+ scope: installedScope
361
431
  };
362
432
  };
363
433
  }
@@ -424,33 +494,37 @@ function when(condition) {
424
494
  };
425
495
  };
426
496
  }
427
- /** Read request.scope safely */
428
- function getScope(request) {
429
- return request.scope ?? PUBLIC_SCOPE;
430
- }
431
497
  /**
432
- * Require membership in the active organization.
433
- * User must be authenticated AND have an active org (member or elevated scope).
498
+ * Require an org-bound caller. Grants access for any scope kind that
499
+ * carries org context: `member` (human user with org membership), `service`
500
+ * (API key bound to an org), or `elevated` (platform admin). Denies for
501
+ * `public` and `authenticated` scopes (no org context).
434
502
  *
435
- * Reads `request.scope` set by auth adapters.
503
+ * This is the canonical "is the caller acting inside an org" check, and the
504
+ * usual partner for `multiTenantPreset` — if a route is multi-tenant
505
+ * filtered, you almost always want this gate too.
506
+ *
507
+ * Reads `request.scope` set by auth adapters or by upstream permission
508
+ * checks via `PermissionResult.scope` (e.g. a custom `requireApiKey()`).
436
509
  *
437
510
  * @example
438
511
  * ```typescript
439
512
  * permissions: {
440
513
  * list: requireOrgMembership(),
441
514
  * get: requireOrgMembership(),
515
+ *
516
+ * // Composed with an OAuth-style scope check for API-key callers
517
+ * create: allOf(requireOrgMembership(), requireServiceScope('jobs:write')),
442
518
  * }
443
519
  * ```
444
520
  */
445
521
  function requireOrgMembership() {
446
522
  const check = (ctx) => {
523
+ if (hasOrgAccess(getRequestScope(ctx.request))) return true;
447
524
  if (!ctx.user) return {
448
525
  granted: false,
449
526
  reason: "Authentication required"
450
527
  };
451
- const scope = getScope(ctx.request);
452
- if (isElevated(scope)) return true;
453
- if (isMember(scope)) return true;
454
528
  return {
455
529
  granted: false,
456
530
  reason: "Organization membership required"
@@ -464,6 +538,23 @@ function requireOrgMembership() {
464
538
  * Reads `request.scope.orgRoles` (set by auth adapters).
465
539
  * Elevated scope always passes (platform admin bypass).
466
540
  *
541
+ * **Service scopes (API keys) always fail this check** — services don't
542
+ * carry user-style org roles, only OAuth-style `scopes` strings. For routes
543
+ * that should accept BOTH human admins AND API keys, compose explicitly:
544
+ *
545
+ * ```typescript
546
+ * permissions: {
547
+ * create: anyOf(
548
+ * requireOrgRole('admin'), // human path
549
+ * requireServiceScope('jobs:write'), // machine path
550
+ * ),
551
+ * }
552
+ * ```
553
+ *
554
+ * This separation is intentional — implicit "API key bypasses role checks"
555
+ * is the kind of footgun that ships data breaches. Services must opt into
556
+ * specific scopes the same way OAuth clients do.
557
+ *
467
558
  * @param roles - Required org roles (user needs at least one)
468
559
  *
469
560
  * @example
@@ -475,14 +566,18 @@ function requireOrgMembership() {
475
566
  * ```
476
567
  */
477
568
  function requireOrgRole(...args) {
478
- const roles = Array.isArray(args[0]) ? args[0] : args;
569
+ const roles = normalizeVariadicOrArray(args);
479
570
  const check = (ctx) => {
571
+ const scope = getRequestScope(ctx.request);
572
+ if (isElevated(scope)) return true;
573
+ if (isService(scope)) return {
574
+ granted: false,
575
+ reason: "Service scopes (API keys) cannot satisfy requireOrgRole. Use requireServiceScope(...) for machine identities, or compose with anyOf(requireOrgRole(...), requireServiceScope(...)) to accept both."
576
+ };
480
577
  if (!ctx.user) return {
481
578
  granted: false,
482
579
  reason: "Authentication required"
483
580
  };
484
- const scope = getScope(ctx.request);
485
- if (isElevated(scope)) return true;
486
581
  if (!isMember(scope)) return {
487
582
  granted: false,
488
583
  reason: "Organization membership required"
@@ -497,6 +592,205 @@ function requireOrgRole(...args) {
497
592
  return check;
498
593
  }
499
594
  /**
595
+ * Require specific OAuth-style scope strings on a service (API key) identity.
596
+ *
597
+ * Reads `request.scope.scopes` — only populated when the scope kind is
598
+ * `service`. Mirrors how OAuth 2.0 / Better Auth's apiKey plugin / API
599
+ * gateways express machine permissions: a comma- or array-encoded list of
600
+ * scope strings like `'jobs:read'`, `'jobs:write'`, `'memories:*'`.
601
+ *
602
+ * **Pass behavior:**
603
+ * - `service` scope where `scopes` contains ANY of the required strings → grant
604
+ * - `elevated` scope (platform admin) → grant
605
+ * - Anything else → deny with a clear reason
606
+ *
607
+ * Notably this does **not** grant for `member` scopes — humans go through
608
+ * `requireOrgRole`. For routes that should accept both, compose with `anyOf`:
609
+ *
610
+ * ```typescript
611
+ * permissions: {
612
+ * create: anyOf(
613
+ * requireOrgRole('admin'),
614
+ * requireServiceScope('jobs:write'),
615
+ * ),
616
+ * }
617
+ * ```
618
+ *
619
+ * @param scopes - Required scope strings (caller needs at least one)
620
+ *
621
+ * @example
622
+ * ```typescript
623
+ * // Variadic
624
+ * requireServiceScope('jobs:write')
625
+ * requireServiceScope('jobs:read', 'jobs:write')
626
+ *
627
+ * // Array
628
+ * requireServiceScope(['jobs:read', 'jobs:write'])
629
+ *
630
+ * // Composed with org membership for org-scoped API keys
631
+ * permissions: {
632
+ * list: allOf(requireOrgMembership(), requireServiceScope('jobs:read')),
633
+ * create: allOf(requireOrgMembership(), requireServiceScope('jobs:write')),
634
+ * }
635
+ * ```
636
+ */
637
+ function requireServiceScope(...args) {
638
+ const required = normalizeVariadicOrArray(args);
639
+ if (required.length === 0) throw new Error("requireServiceScope() requires at least one scope string (e.g. requireServiceScope('jobs:write'))");
640
+ const check = (ctx) => {
641
+ const scope = getRequestScope(ctx.request);
642
+ if (isElevated(scope)) return true;
643
+ if (!isService(scope)) return {
644
+ granted: false,
645
+ reason: "Service identity required (API key). For human users, use requireOrgRole(...) or compose with anyOf(requireOrgRole(...), requireServiceScope(...))."
646
+ };
647
+ const granted = getServiceScopes(scope);
648
+ if (required.some((r) => granted.includes(r))) return true;
649
+ return {
650
+ granted: false,
651
+ reason: `Required service scopes: ${required.join(", ")} (granted: ${granted.length > 0 ? granted.join(", ") : "none"})`
652
+ };
653
+ };
654
+ check._serviceScopes = required;
655
+ return check;
656
+ }
657
+ /**
658
+ * Require app-defined scope context dimensions (branch, project, department,
659
+ * region, workspace, etc.) on the current request.
660
+ *
661
+ * Reads `request.scope.context` (a `Readonly<Record<string, string>>` slot
662
+ * available on `member`, `service`, and `elevated` scope kinds). Arc takes
663
+ * no position on what dimensions you use — you set them, you check them.
664
+ *
665
+ * **Three call shapes:**
666
+ *
667
+ * ```typescript
668
+ * // 1. Presence check — key must exist on scope.context
669
+ * requireScopeContext('branchId')
670
+ *
671
+ * // 2. Value match — key must equal a specific string
672
+ * requireScopeContext('branchId', 'eng-paris')
673
+ *
674
+ * // 3. Multi-key (object form, AND semantics) — every key must match
675
+ * requireScopeContext({ branchId: 'eng-paris', projectId: 'p-123' })
676
+ * requireScopeContext({ region: 'eu', branchId: undefined }) // 'undefined' = presence-only for that key
677
+ * ```
678
+ *
679
+ * **Pass behavior:**
680
+ * - All required keys present (and matching values when specified) → grant
681
+ * - `elevated` scope (platform admin) → grant unconditionally (cross-context bypass)
682
+ * - Any required key missing or mismatched → deny with a clear reason
683
+ * - Scope kind without context support (`public`, `authenticated`) → deny
684
+ *
685
+ * Pairs with `multiTenantPreset({ tenantFields: [...] })` for row-level
686
+ * filtering on the same dimensions.
687
+ *
688
+ * @example
689
+ * ```typescript
690
+ * permissions: {
691
+ * // Branch-scoped CRUD — caller must have branchId in their scope context
692
+ * list: allOf(requireOrgMembership(), requireScopeContext('branchId')),
693
+ *
694
+ * // Project admin — caller must have BOTH project context AND admin role
695
+ * delete: allOf(requireOrgRole('admin'), requireScopeContext('projectId')),
696
+ *
697
+ * // Region-locked endpoint
698
+ * euOnly: requireScopeContext('region', 'eu'),
699
+ * }
700
+ * ```
701
+ */
702
+ function requireScopeContext(keyOrMap, value) {
703
+ let required;
704
+ if (typeof keyOrMap === "string") required = { [keyOrMap]: value };
705
+ else if (keyOrMap && typeof keyOrMap === "object") required = keyOrMap;
706
+ else throw new Error("requireScopeContext() requires a key (string), key+value, or { key: value } map");
707
+ const requiredKeys = Object.keys(required);
708
+ if (requiredKeys.length === 0) throw new Error("requireScopeContext() requires at least one key (e.g. requireScopeContext('branchId'))");
709
+ const check = (ctx) => {
710
+ const scope = getRequestScope(ctx.request);
711
+ if (isElevated(scope)) return true;
712
+ if (!getScopeContextMap(scope)) return {
713
+ granted: false,
714
+ reason: "Scope context required (member, service, or elevated scope). Populate request.scope.context in your auth function."
715
+ };
716
+ for (const key of requiredKeys) {
717
+ const expected = required[key];
718
+ const actual = getScopeContext(scope, key);
719
+ if (actual === void 0) return {
720
+ granted: false,
721
+ reason: `Required scope context key "${key}" is missing`
722
+ };
723
+ if (expected !== void 0 && actual !== expected) return {
724
+ granted: false,
725
+ reason: `Required scope context "${key}" must equal "${expected}" (got "${actual}")`
726
+ };
727
+ }
728
+ return true;
729
+ };
730
+ check._scopeContext = required;
731
+ return check;
732
+ }
733
+ /**
734
+ * Require that the caller's scope grants access to a target organization
735
+ * — either the current org or one of its ancestors (`scope.ancestorOrgIds`).
736
+ *
737
+ * Designed for parent-child organization hierarchies (holding company →
738
+ * subsidiary → branch, MSP → managed tenants, white-label parent → child
739
+ * accounts) where some routes need to accept "this org OR any org I have
740
+ * access to via the chain". Arc takes no position on the source of the
741
+ * chain — your auth function loads `ancestorOrgIds` from your own data
742
+ * model. There's no automatic inheritance: every route opts in explicitly.
743
+ *
744
+ * **Two call shapes:**
745
+ *
746
+ * ```typescript
747
+ * // Static target — rare, used when one route only ever acts on one org
748
+ * requireOrgInScope('acme-holding')
749
+ *
750
+ * // Dynamic target — extracted from request params/body/headers per call
751
+ * requireOrgInScope((ctx) => ctx.request.params.orgId)
752
+ * requireOrgInScope((ctx) => ctx.request.body?.organizationId)
753
+ * ```
754
+ *
755
+ * **Pass behavior:**
756
+ * - Target equals `scope.organizationId` → grant
757
+ * - Target appears in `scope.ancestorOrgIds` → grant
758
+ * - `elevated` scope → grant unconditionally (cross-org admin bypass)
759
+ * - Target is undefined (extractor returned nothing) → deny with reason
760
+ * - Anything else → deny with target name in reason
761
+ *
762
+ * @example
763
+ * ```typescript
764
+ * // /orgs/:orgId/jobs — caller can act on any org in their hierarchy chain
765
+ * permissions: {
766
+ * list: requireOrgInScope((ctx) => ctx.request.params.orgId),
767
+ * create: allOf(
768
+ * requireOrgInScope((ctx) => ctx.request.body?.organizationId),
769
+ * requireOrgRole('admin'),
770
+ * ),
771
+ * }
772
+ * ```
773
+ */
774
+ function requireOrgInScope(target) {
775
+ if (target === void 0 || target === null) throw new Error("requireOrgInScope() requires a target org id (string) or an extractor function");
776
+ const check = (ctx) => {
777
+ const scope = getRequestScope(ctx.request);
778
+ if (isElevated(scope)) return true;
779
+ const targetOrgId = typeof target === "function" ? target(ctx) : target;
780
+ if (!targetOrgId) return {
781
+ granted: false,
782
+ reason: "requireOrgInScope: target org id could not be resolved from the request"
783
+ };
784
+ if (isOrgInScope(scope, targetOrgId)) return true;
785
+ return {
786
+ granted: false,
787
+ reason: `Target organization "${targetOrgId}" is not in the caller's org hierarchy`
788
+ };
789
+ };
790
+ check._orgInScopeTarget = target;
791
+ return check;
792
+ }
793
+ /**
500
794
  * Create a scoped permission system for resource-action patterns.
501
795
  * Maps org roles to fine-grained permissions without external API calls.
502
796
  *
@@ -537,7 +831,7 @@ function createOrgPermissions(config) {
537
831
  granted: false,
538
832
  reason: "Authentication required"
539
833
  };
540
- const scope = getScope(ctx.request);
834
+ const scope = getRequestScope(ctx.request);
541
835
  if (isElevated(scope)) return true;
542
836
  if (!isMember(scope)) return {
543
837
  granted: false,
@@ -650,7 +944,7 @@ function createDynamicPermissionMatrix(config) {
650
944
  granted: false,
651
945
  reason: "Authentication required"
652
946
  };
653
- const scope = getScope(ctx.request);
947
+ const scope = getRequestScope(ctx.request);
654
948
  if (isElevated(scope)) return true;
655
949
  if (!isMember(scope)) return {
656
950
  granted: false,
@@ -774,7 +1068,7 @@ function requireTeamMembership() {
774
1068
  granted: false,
775
1069
  reason: "Authentication required"
776
1070
  };
777
- const scope = getScope(ctx.request);
1071
+ const scope = getRequestScope(ctx.request);
778
1072
  if (isElevated(scope)) return true;
779
1073
  if (!isMember(scope)) return {
780
1074
  granted: false,
@@ -790,4 +1084,4 @@ function requireTeamMembership() {
790
1084
  return check;
791
1085
  }
792
1086
  //#endregion
793
- export { createRoleHierarchy as C, readOnly as S, fullPublic as _, createOrgPermissions as a, publicRead as b, requireOrgMembership as c, requireRoles as d, requireTeamMembership as f, authenticated as g, adminOnly as h, createDynamicPermissionMatrix as i, requireOrgRole as l, when as m, allowPublic as n, denyAll as o, roles as p, anyOf as r, requireAuth as s, allOf as t, requireOwnership as u, ownerWithAdminBypass as v, publicReadAdminWrite as x, presets_exports as y };
1087
+ export { publicRead as C, createRoleHierarchy as E, presets_exports as S, readOnly as T, when as _, createOrgPermissions as a, fullPublic as b, requireOrgInScope as c, requireOwnership as d, requireRoles as f, roles as g, requireTeamMembership as h, createDynamicPermissionMatrix as i, requireOrgMembership as l, requireServiceScope as m, allowPublic as n, denyAll as o, requireScopeContext as p, anyOf as r, requireAuth as s, allOf as t, requireOrgRole as u, adminOnly as v, publicReadAdminWrite as w, ownerWithAdminBypass as x, authenticated as y };
@@ -1,8 +1,9 @@
1
- import { Bt as ResourceRegistry, H as MiddlewareConfig, X as PresetHook, cn as HookSystem, ft as RouteSchemaOptions, l as AdditionalRoute, u as AnyRecord } from "../interface-DYH8AXGe.mjs";
2
- import { t as ExternalOpenApiPaths } from "../externalPaths-DpO-s7r8.mjs";
3
- import { _ as cachingPlugin, a as versioningPlugin, c as MetricsOptions, d as SSEOptions, f as _default$6, g as _default$1, h as CachingRule, i as _default$7, l as _default$4, m as CachingOptions, n as errorHandlerPlugin, o as MetricEntry, p as ssePlugin, r as VersioningOptions, s as MetricsCollector, t as ErrorHandlerOptions, u as metricsPlugin } from "../errorHandler-Do4vVQ1f.mjs";
4
- import { t as TracingOptions } from "../tracing-bz_U4EM1.mjs";
1
+ import { Bt as ResourceRegistry, H as MiddlewareConfig, X as PresetHook, cn as HookSystem, ft as RouteSchemaOptions, l as AdditionalRoute, u as AnyRecord } from "../interface-B91alUzq.mjs";
2
+ import { t as ExternalOpenApiPaths } from "../externalPaths-iba7jD3d.mjs";
3
+ import { _ as cachingPlugin, a as versioningPlugin, c as MetricsOptions, d as SSEOptions, f as _default$6, g as _default$1, h as CachingRule, i as _default$7, l as _default$4, m as CachingOptions, n as errorHandlerPlugin, o as MetricEntry, p as ssePlugin, r as VersioningOptions, s as MetricsCollector, t as ErrorHandlerOptions, u as metricsPlugin } from "../errorHandler-pCpEtNd7.mjs";
4
+ import { t as TracingOptions } from "../tracing-DEqdGkr-.mjs";
5
5
  import { FastifyInstance, FastifyPluginAsync } from "fastify";
6
+ import * as _$node_stream0 from "node:stream";
6
7
 
7
8
  //#region src/core/arcCorePlugin.d.ts
8
9
  interface ArcCorePluginOptions {
@@ -148,6 +149,52 @@ interface HealthOptions {
148
149
  declare const healthPlugin: FastifyPluginAsync<HealthOptions>;
149
150
  declare const _default$3: FastifyPluginAsync<HealthOptions>;
150
151
  //#endregion
152
+ //#region src/plugins/replyHelpers.d.ts
153
+ declare module "fastify" {
154
+ interface FastifyReply {
155
+ /** Send a success response with data */
156
+ ok<T>(data: T, statusCode?: number): FastifyReply;
157
+ /** Send an error response */
158
+ fail(error: string | string[], statusCode?: number): FastifyReply;
159
+ /** Send a paginated list response */
160
+ paginated<T>(result: {
161
+ docs: T[];
162
+ total: number;
163
+ page: number;
164
+ limit: number;
165
+ [key: string]: unknown;
166
+ }): FastifyReply;
167
+ /**
168
+ * Stream a readable source as a file download or raw stream.
169
+ *
170
+ * @example
171
+ * ```typescript
172
+ * // CSV export
173
+ * return reply.stream(csvReadableStream, {
174
+ * contentType: 'text/csv',
175
+ * filename: 'export.csv',
176
+ * });
177
+ *
178
+ * // PDF download
179
+ * return reply.stream(pdfBuffer, {
180
+ * contentType: 'application/pdf',
181
+ * filename: 'report.pdf',
182
+ * });
183
+ *
184
+ * // Raw stream (no Content-Disposition)
185
+ * return reply.stream(dataStream, { contentType: 'application/octet-stream' });
186
+ * ```
187
+ */
188
+ stream(source: _$node_stream0.Readable | Buffer | AsyncIterable<unknown>, options: {
189
+ contentType: string;
190
+ filename?: string;
191
+ statusCode?: number;
192
+ }): FastifyReply;
193
+ }
194
+ }
195
+ declare function replyHelpersPluginFn(fastify: FastifyInstance): Promise<void>;
196
+ declare const replyHelpersPlugin: typeof replyHelpersPluginFn;
197
+ //#endregion
151
198
  //#region src/plugins/requestId.d.ts
152
199
  interface RequestIdOptions {
153
200
  /** Header name to read/write request ID (default: 'x-request-id') */
@@ -166,4 +213,4 @@ declare module "fastify" {
166
213
  declare const requestIdPlugin: FastifyPluginAsync<RequestIdOptions>;
167
214
  declare const _default$5: FastifyPluginAsync<RequestIdOptions>;
168
215
  //#endregion
169
- export { type ArcCore, type ArcCorePluginOptions, type ArcPlugin, type CachingOptions, type CachingRule, type CreatePluginDefinition, type ErrorHandlerOptions, type GracefulShutdownOptions, type HealthCheck, type HealthOptions, type MetricEntry, type MetricsCollector, type MetricsOptions, type PluginMeta, type PluginResourceResult, type RequestIdOptions, type SSEOptions, type TracingOptions, type VersioningOptions, _default as arcCorePlugin, arcCorePlugin as arcCorePluginFn, _default$1 as cachingPlugin, cachingPlugin as cachingPluginFn, createPlugin, errorHandlerPlugin, errorHandlerPlugin as errorHandlerPluginFn, _default$2 as gracefulShutdownPlugin, gracefulShutdownPlugin as gracefulShutdownPluginFn, _default$3 as healthPlugin, healthPlugin as healthPluginFn, _default$4 as metricsPlugin, metricsPlugin as metricsPluginFn, _default$5 as requestIdPlugin, requestIdPlugin as requestIdPluginFn, _default$6 as ssePlugin, ssePlugin as ssePluginFn, _default$7 as versioningPlugin, versioningPlugin as versioningPluginFn };
216
+ export { type ArcCore, type ArcCorePluginOptions, type ArcPlugin, type CachingOptions, type CachingRule, type CreatePluginDefinition, type ErrorHandlerOptions, type GracefulShutdownOptions, type HealthCheck, type HealthOptions, type MetricEntry, type MetricsCollector, type MetricsOptions, type PluginMeta, type PluginResourceResult, type RequestIdOptions, type SSEOptions, type TracingOptions, type VersioningOptions, _default as arcCorePlugin, arcCorePlugin as arcCorePluginFn, _default$1 as cachingPlugin, cachingPlugin as cachingPluginFn, createPlugin, errorHandlerPlugin, errorHandlerPlugin as errorHandlerPluginFn, _default$2 as gracefulShutdownPlugin, gracefulShutdownPlugin as gracefulShutdownPluginFn, _default$3 as healthPlugin, healthPlugin as healthPluginFn, _default$4 as metricsPlugin, metricsPlugin as metricsPluginFn, replyHelpersPlugin, _default$5 as requestIdPlugin, requestIdPlugin as requestIdPluginFn, _default$6 as ssePlugin, ssePlugin as ssePluginFn, _default$7 as versioningPlugin, versioningPlugin as versioningPluginFn };