@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,14 +1,15 @@
1
1
  import { p as MUTATION_OPERATIONS } from "../constants-Cxde4rpC.mjs";
2
- import { i as getOrgId } from "../types-BhtYdxZU.mjs";
3
- import { t as requestContext } from "../requestContext-DYtmNpm5.mjs";
4
- import { t as hasEvents } from "../typeGuards-Cj5Rgvlg.mjs";
5
- import { t as HookSystem } from "../HookSystem-COkyWztM.mjs";
6
- import { t as ResourceRegistry } from "../ResourceRegistry-C6ngvOnn.mjs";
7
- import { n as caching_default, t as cachingPlugin } from "../caching-BSXB-Xr7.mjs";
8
- import { t as errorHandlerPlugin } from "../errorHandler-r2595m8T.mjs";
9
- import { n as metrics_default, t as metricsPlugin } from "../metrics-Csh4nsvv.mjs";
10
- import { n as sse_default, t as ssePlugin } from "../sse-BF7GR7IB.mjs";
11
- import { n as versioning_default, t as versioningPlugin } from "../versioning-BzfeHmhj.mjs";
2
+ import { o as getOrgId } from "../types-AOD8fxIw.mjs";
3
+ import { t as requestContext } from "../requestContext-xHIKedG6.mjs";
4
+ import { t as hasEvents } from "../typeGuards-CcFZXgU7.mjs";
5
+ import { t as HookSystem } from "../HookSystem-D7lfx--K.mjs";
6
+ import { t as ResourceRegistry } from "../ResourceRegistry-DsHiG9cL.mjs";
7
+ import { n as caching_default, t as cachingPlugin } from "../caching-5DtLwIqb.mjs";
8
+ import { t as errorHandlerPlugin } from "../errorHandler-CH8wk1eD.mjs";
9
+ import { n as metrics_default, t as metricsPlugin } from "../metrics-Qnvwc-LQ.mjs";
10
+ import { t as replyHelpersPlugin } from "../replyHelpers-uDUIYh7u.mjs";
11
+ import { n as sse_default, t as ssePlugin } from "../sse-6W0hjVS_.mjs";
12
+ import { n as versioning_default, t as versioningPlugin } from "../versioning-CdBbFefk.mjs";
12
13
  import { randomUUID } from "node:crypto";
13
14
  import fp from "fastify-plugin";
14
15
  //#region src/core/arcCorePlugin.ts
@@ -409,4 +410,4 @@ var requestId_default = fp(requestIdPlugin, {
409
410
  fastify: "5.x"
410
411
  });
411
412
  //#endregion
412
- export { arcCorePlugin_default as arcCorePlugin, arcCorePlugin as arcCorePluginFn, caching_default as cachingPlugin, cachingPlugin as cachingPluginFn, createPlugin, errorHandlerPlugin, errorHandlerPlugin as errorHandlerPluginFn, gracefulShutdown_default as gracefulShutdownPlugin, gracefulShutdownPlugin as gracefulShutdownPluginFn, health_default as healthPlugin, healthPlugin as healthPluginFn, metrics_default as metricsPlugin, metricsPlugin as metricsPluginFn, requestId_default as requestIdPlugin, requestIdPlugin as requestIdPluginFn, sse_default as ssePlugin, ssePlugin as ssePluginFn, versioning_default as versioningPlugin, versioningPlugin as versioningPluginFn };
413
+ export { arcCorePlugin_default as arcCorePlugin, arcCorePlugin as arcCorePluginFn, caching_default as cachingPlugin, cachingPlugin as cachingPluginFn, createPlugin, errorHandlerPlugin, errorHandlerPlugin as errorHandlerPluginFn, gracefulShutdown_default as gracefulShutdownPlugin, gracefulShutdownPlugin as gracefulShutdownPluginFn, health_default as healthPlugin, healthPlugin as healthPluginFn, metrics_default as metricsPlugin, metricsPlugin as metricsPluginFn, replyHelpersPlugin, requestId_default as requestIdPlugin, requestIdPlugin as requestIdPluginFn, sse_default as ssePlugin, ssePlugin as ssePluginFn, versioning_default as versioningPlugin, versioningPlugin as versioningPluginFn };
@@ -1,4 +1,4 @@
1
- import { t as hasEvents } from "../typeGuards-Cj5Rgvlg.mjs";
1
+ import { t as hasEvents } from "../typeGuards-CcFZXgU7.mjs";
2
2
  import fp from "fastify-plugin";
3
3
  //#region src/plugins/response-cache.ts
4
4
  /**
@@ -1,2 +1,2 @@
1
- import { a as traced, i as isTracingAvailable, n as _default, r as createSpan, t as TracingOptions } from "../tracing-bz_U4EM1.mjs";
1
+ import { a as traced, i as isTracingAvailable, n as _default, r as createSpan, t as TracingOptions } from "../tracing-DEqdGkr-.mjs";
2
2
  export { type TracingOptions, createSpan, isTracingAvailable, traced, _default as tracingPlugin };
@@ -44,7 +44,7 @@ try {
44
44
  function createTracerProvider(options) {
45
45
  if (!isAvailable) return null;
46
46
  const { serviceName = "@classytic/arc", serviceVersion, exporterUrl = "http://localhost:4318/v1/traces" } = options;
47
- const resolvedVersion = serviceVersion ?? "2.6.3";
47
+ const resolvedVersion = serviceVersion ?? "2.7.3";
48
48
  const exporter = new OTLPTraceExporter({ url: exporterUrl });
49
49
  const provider = new NodeTracerProvider({ resource: { attributes: {
50
50
  "service.name": serviceName,
@@ -1,4 +1,4 @@
1
- import { t as PermissionCheck } from "../types-BNUccdcf.mjs";
1
+ import { t as PermissionCheck } from "../types-B4BNthET.mjs";
2
2
  import { FastifyReply, FastifyRequest } from "fastify";
3
3
 
4
4
  //#region src/policies/PolicyInterface.d.ts
@@ -1,5 +1,5 @@
1
- import { Gt as PaginatedResult, It as IControllerResponse, Lt as IRequestContext, Z as PresetResult, ot as ResourceConfig, u as AnyRecord } from "../interface-DYH8AXGe.mjs";
2
- import { MultiTenantOptions, multiTenantPreset } from "./multiTenant.mjs";
1
+ import { Gt as PaginatedResult, It as IControllerResponse, Lt as IRequestContext, Z as PresetResult, ot as ResourceConfig, u as AnyRecord } from "../interface-B91alUzq.mjs";
2
+ import { MultiTenantOptions, TenantFieldSpec, multiTenantPreset } from "./multiTenant.mjs";
3
3
 
4
4
  //#region src/presets/ownedByUser.d.ts
5
5
  interface OwnedByUserOptions {
@@ -271,4 +271,4 @@ type PresetInput = string | PresetResult | {
271
271
  */
272
272
  declare function applyPresets<TDoc = AnyRecord>(config: ResourceConfig<TDoc>, presets?: PresetInput[]): ResourceConfig<TDoc>;
273
273
  //#endregion
274
- export { type AuditedPresetOptions, type BulkOperation, type BulkPresetOptions, type IAuditedPreset, type IMultiTenantPreset, type IOwnedByUserPreset, type IPresetController, type ISlugLookupController, type ISoftDeleteController, type ITreeController, type MultiTenantOptions, type OwnedByUserOptions, type SlugLookupOptions, type TreeOptions, applyPresets, auditedPreset, bulkPreset, flexibleMultiTenantPreset, getAvailablePresets, getPreset, multiTenantPreset, ownedByUserPreset, registerPreset, slugLookupPreset, softDeletePreset, treePreset };
274
+ export { type AuditedPresetOptions, type BulkOperation, type BulkPresetOptions, type IAuditedPreset, type IMultiTenantPreset, type IOwnedByUserPreset, type IPresetController, type ISlugLookupController, type ISoftDeleteController, type ITreeController, type MultiTenantOptions, type OwnedByUserOptions, type SlugLookupOptions, type TenantFieldSpec, type TreeOptions, applyPresets, auditedPreset, bulkPreset, flexibleMultiTenantPreset, getAvailablePresets, getPreset, multiTenantPreset, ownedByUserPreset, registerPreset, slugLookupPreset, softDeletePreset, treePreset };
@@ -1,3 +1,3 @@
1
1
  import { multiTenantPreset } from "./multiTenant.mjs";
2
- import { a as registerPreset, c as auditedPreset, d as ownedByUserPreset, i as getPreset, l as softDeletePreset, n as flexibleMultiTenantPreset, o as treePreset, r as getAvailablePresets, s as bulkPreset, t as applyPresets, u as slugLookupPreset } from "../presets-BMfdy34e.mjs";
2
+ import { a as registerPreset, c as auditedPreset, d as ownedByUserPreset, i as getPreset, l as softDeletePreset, n as flexibleMultiTenantPreset, o as treePreset, r as getAvailablePresets, s as bulkPreset, t as applyPresets, u as slugLookupPreset } from "../presets-BFrGvvjL.mjs";
3
3
  export { applyPresets, auditedPreset, bulkPreset, flexibleMultiTenantPreset, getAvailablePresets, getPreset, multiTenantPreset, ownedByUserPreset, registerPreset, slugLookupPreset, softDeletePreset, treePreset };
@@ -1,9 +1,59 @@
1
- import { S as CrudRouteKey, Z as PresetResult } from "../interface-DYH8AXGe.mjs";
1
+ import { S as CrudRouteKey, Z as PresetResult } from "../interface-B91alUzq.mjs";
2
2
 
3
3
  //#region src/presets/multiTenant.d.ts
4
+ /**
5
+ * One tenant dimension for multi-field filtering. Discriminated by source:
6
+ * - `type: 'org'` → reads `getOrgId(scope)`
7
+ * - `type: 'team'` → reads `getTeamId(scope)`
8
+ * - `contextKey` → reads `getScopeContext(scope, contextKey)` (any custom dimension)
9
+ */
10
+ type TenantFieldSpec = {
11
+ field: string;
12
+ type: "org";
13
+ } | {
14
+ field: string;
15
+ type: "team";
16
+ } | {
17
+ field: string;
18
+ contextKey: string;
19
+ };
4
20
  interface MultiTenantOptions {
5
- /** Field name in database (default: 'organizationId') */
21
+ /**
22
+ * Single-field form: name of the database field to filter by.
23
+ * Reads `getOrgId(scope)` as the value source.
24
+ *
25
+ * Mutually exclusive with `tenantFields` — pass one or the other, not both.
26
+ *
27
+ * @default 'organizationId'
28
+ */
6
29
  tenantField?: string;
30
+ /**
31
+ * Multi-field form (2.7.1+): list of tenant dimensions to filter by in
32
+ * lockstep. Use this when a resource is scoped by more than just
33
+ * organization — e.g. organization + branch, organization + project,
34
+ * or organization + team + workspace.
35
+ *
36
+ * Each entry is a discriminated `TenantFieldSpec` declaring where the
37
+ * value comes from. Use `type: 'org'` / `type: 'team'` for built-in scope
38
+ * fields, or `contextKey: '...'` to read from `scope.context` (set by
39
+ * your auth function).
40
+ *
41
+ * Fail-closed: if any required dimension is missing, the request is
42
+ * rejected. Elevated scopes apply whatever resolves and skip the rest.
43
+ *
44
+ * Mutually exclusive with `tenantField`.
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * multiTenantPreset({
49
+ * tenantFields: [
50
+ * { field: 'organizationId', type: 'org' },
51
+ * { field: 'branchId', contextKey: 'branchId' },
52
+ * ],
53
+ * })
54
+ * ```
55
+ */
56
+ tenantFields?: readonly TenantFieldSpec[];
7
57
  /**
8
58
  * Routes that allow public access (no auth required)
9
59
  * When a route is in this array:
@@ -18,4 +68,4 @@ interface MultiTenantOptions {
18
68
  }
19
69
  declare function multiTenantPreset(options?: MultiTenantOptions): PresetResult;
20
70
  //#endregion
21
- export { MultiTenantOptions, multiTenantPreset };
71
+ export { MultiTenantOptions, TenantFieldSpec, multiTenantPreset };
@@ -1,31 +1,59 @@
1
- import { o as DEFAULT_TENANT_FIELD } from "../constants-Cxde4rpC.mjs";
2
- import { d as isElevated, f as isMember, i as getOrgId, n as PUBLIC_SCOPE } from "../types-BhtYdxZU.mjs";
1
+ import "../constants-Cxde4rpC.mjs";
2
+ import { _ as isElevated, c as getRequestScope, f as getTeamId, h as hasOrgAccess, l as getScopeContext, o as getOrgId } from "../types-AOD8fxIw.mjs";
3
3
  //#region src/presets/multiTenant.ts
4
- /** Read request.scope safely */
5
- function getScope(request) {
6
- return request.scope ?? PUBLIC_SCOPE;
4
+ /**
5
+ * Resolve a single TenantFieldSpec against the current scope.
6
+ * Returns `undefined` if the source value isn't present on the scope.
7
+ */
8
+ function resolveSpec(scope, spec) {
9
+ if ("contextKey" in spec) return getScopeContext(scope, spec.contextKey);
10
+ if (spec.type === "org") return getOrgId(scope);
11
+ if (spec.type === "team") return getTeamId(scope);
12
+ }
13
+ /** Resolve every spec — returns the partial map of fields that have a value. */
14
+ function resolveAll(scope, specs) {
15
+ const resolved = {};
16
+ const missing = [];
17
+ for (const spec of specs) {
18
+ const value = resolveSpec(scope, spec);
19
+ if (value !== void 0) resolved[spec.field] = value;
20
+ else missing.push(spec.field);
21
+ }
22
+ return {
23
+ resolved,
24
+ missing
25
+ };
7
26
  }
8
27
  /**
9
- * Create tenant filter middleware
10
- * Adds tenant filter to query for list/get operations.
11
- * Reads `request.scope` for org context and elevation bypass.
28
+ * Create tenant filter middleware (strict).
29
+ * Walks the configured tenant fields and applies all of them in lockstep.
30
+ * Fails closed if any non-elevated caller is missing a required dimension.
12
31
  */
13
- function createTenantFilter(tenantField) {
32
+ function createTenantFilter(specs) {
14
33
  return async (request, reply) => {
15
- const scope = getScope(request);
34
+ const scope = getRequestScope(request);
16
35
  if (isElevated(scope)) {
17
- const orgId = getOrgId(scope);
18
- if (orgId) request._policyFilters = {
36
+ const { resolved } = resolveAll(scope, specs);
37
+ if (Object.keys(resolved).length > 0) request._policyFilters = {
19
38
  ...request._policyFilters ?? {},
20
- [tenantField]: orgId
39
+ ...resolved
21
40
  };
22
41
  return;
23
42
  }
24
- if (isMember(scope)) {
25
- request._policyFilters = {
26
- ...request._policyFilters ?? {},
27
- [tenantField]: scope.organizationId
28
- };
43
+ if (hasOrgAccess(scope)) {
44
+ const { resolved, missing } = resolveAll(scope, specs);
45
+ if (missing.length === 0) {
46
+ request._policyFilters = {
47
+ ...request._policyFilters ?? {},
48
+ ...resolved
49
+ };
50
+ return;
51
+ }
52
+ reply.code(403).send({
53
+ success: false,
54
+ error: "Forbidden",
55
+ message: `Tenant context incomplete — missing: ${missing.join(", ")}`
56
+ });
29
57
  return;
30
58
  }
31
59
  if (scope.kind === "public") {
@@ -44,57 +72,71 @@ function createTenantFilter(tenantField) {
44
72
  };
45
73
  }
46
74
  /**
47
- * Create flexible tenant filter middleware
48
- * For routes in allowPublic: only filter when org context is present
49
- * No org context = allow through (public data)
50
- * Org context present = require auth and apply filter
75
+ * Create flexible tenant filter middleware (allowPublic).
76
+ * Same policy as the strict variant for authenticated callers, but
77
+ * allows public/unauthenticated requests through without filtering.
51
78
  */
52
- function createFlexibleTenantFilter(tenantField) {
53
- return async (request, _reply) => {
54
- const scope = getScope(request);
79
+ function createFlexibleTenantFilter(specs) {
80
+ return async (request, reply) => {
81
+ const scope = getRequestScope(request);
55
82
  if (isElevated(scope)) {
56
- const orgId = getOrgId(scope);
57
- if (orgId) request._policyFilters = {
83
+ const { resolved } = resolveAll(scope, specs);
84
+ if (Object.keys(resolved).length > 0) request._policyFilters = {
58
85
  ...request._policyFilters ?? {},
59
- [tenantField]: orgId
86
+ ...resolved
60
87
  };
61
88
  return;
62
89
  }
63
- if (isMember(scope)) {
64
- request._policyFilters = {
65
- ...request._policyFilters ?? {},
66
- [tenantField]: scope.organizationId
67
- };
90
+ if (hasOrgAccess(scope)) {
91
+ const { resolved, missing } = resolveAll(scope, specs);
92
+ if (missing.length === 0) {
93
+ request._policyFilters = {
94
+ ...request._policyFilters ?? {},
95
+ ...resolved
96
+ };
97
+ return;
98
+ }
99
+ reply.code(403).send({
100
+ success: false,
101
+ error: "Forbidden",
102
+ message: `Tenant context incomplete — missing: ${missing.join(", ")}`
103
+ });
68
104
  return;
69
105
  }
70
106
  };
71
107
  }
72
108
  /**
73
- * Create tenant injection middleware
74
- * Injects tenant ID into request body on create.
75
- * Reads `request.scope` for org context.
109
+ * Create tenant injection middleware.
110
+ * Walks the configured tenant fields and writes each into the request body.
111
+ * Fails closed if any required dimension is missing for non-elevated callers.
76
112
  */
77
- function createTenantInjection(tenantField) {
113
+ function createTenantInjection(specs) {
78
114
  return async (request, reply) => {
79
- const scope = getScope(request);
80
- const orgId = getOrgId(scope);
81
- if (isElevated(scope) && !orgId) return;
82
- if (!orgId) {
115
+ const scope = getRequestScope(request);
116
+ if (isElevated(scope) && !getOrgId(scope)) return;
117
+ const { resolved, missing } = resolveAll(scope, specs);
118
+ if (missing.length > 0) {
83
119
  reply.code(403).send({
84
120
  success: false,
85
121
  error: "Forbidden",
86
- message: "Organization context required to create resources"
122
+ message: `Tenant context incomplete missing: ${missing.join(", ")}`
87
123
  });
88
124
  return;
89
125
  }
90
- if (request.body) request.body[tenantField] = orgId;
126
+ if (request.body) Object.assign(request.body, resolved);
91
127
  };
92
128
  }
93
129
  function multiTenantPreset(options = {}) {
94
- const { tenantField = DEFAULT_TENANT_FIELD, allowPublic = [] } = options;
95
- const strictTenantFilter = createTenantFilter(tenantField);
96
- const flexibleTenantFilter = createFlexibleTenantFilter(tenantField);
97
- const tenantInjection = createTenantInjection(tenantField);
130
+ const { tenantField, tenantFields, allowPublic = [] } = options;
131
+ if (tenantField !== void 0 && tenantFields !== void 0) throw new Error("multiTenantPreset: pass either `tenantField` (single-field) or `tenantFields` (multi-field), not both");
132
+ const specs = tenantFields ?? [{
133
+ field: tenantField ?? "organizationId",
134
+ type: "org"
135
+ }];
136
+ if (specs.length === 0) throw new Error("multiTenantPreset: `tenantFields` must contain at least one entry");
137
+ const strictTenantFilter = createTenantFilter(specs);
138
+ const flexibleTenantFilter = createFlexibleTenantFilter(specs);
139
+ const tenantInjection = createTenantInjection(specs);
98
140
  const getFilter = (route) => allowPublic.includes(route) ? flexibleTenantFilter : strictTenantFilter;
99
141
  return {
100
142
  name: "multiTenant",
@@ -1,6 +1,6 @@
1
- import { d as isElevated, n as PUBLIC_SCOPE } from "./types-BhtYdxZU.mjs";
1
+ import { _ as isElevated, n as PUBLIC_SCOPE } from "./types-AOD8fxIw.mjs";
2
2
  import { multiTenantPreset } from "./presets/multiTenant.mjs";
3
- import { d as requireRoles, n as allowPublic, s as requireAuth } from "./permissions-C8ImI8gC.mjs";
3
+ import { f as requireRoles, n as allowPublic, s as requireAuth } from "./permissions-CH4cNwJi.mjs";
4
4
  //#region src/presets/ownedByUser.ts
5
5
  /**
6
6
  * Create ownership check middleware.
@@ -1,4 +1,4 @@
1
- import { i as CacheStore } from "./interface-D_BWALyZ.mjs";
1
+ import { i as CacheStore } from "./interface-CG7oRZjX.mjs";
2
2
  import { FastifyPluginAsync } from "fastify";
3
3
 
4
4
  //#region src/cache/QueryCache.d.ts
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
2
  import { i as versionKey, r as tagVersionKey } from "./keys-qcD-TVJl.mjs";
3
- import { t as hasEvents } from "./typeGuards-Cj5Rgvlg.mjs";
4
- import { t as MemoryCacheStore } from "./memory-BFAYkf8H.mjs";
3
+ import { t as hasEvents } from "./typeGuards-CcFZXgU7.mjs";
4
+ import { t as MemoryCacheStore } from "./memory-Cp7_cAko.mjs";
5
5
  import fp from "fastify-plugin";
6
6
  //#region src/cache/QueryCache.ts
7
7
  var QueryCache = class {
@@ -1,4 +1,4 @@
1
- import { n as IdempotencyResult, r as IdempotencyStore } from "./interface-gr-7qo9j.mjs";
1
+ import { n as IdempotencyResult, r as IdempotencyStore } from "./interface-CSbZdv_3.mjs";
2
2
 
3
3
  //#region src/idempotency/stores/redis.d.ts
4
4
  interface RedisClient {
@@ -1,4 +1,4 @@
1
- import { i as EventTransport, n as EventHandler, r as EventLogger, t as DomainEvent } from "./EventTransport-wc5hSLik.mjs";
1
+ import { i as EventTransport, n as EventHandler, r as EventLogger, t as DomainEvent } from "./EventTransport-C4VheKeC.mjs";
2
2
 
3
3
  //#region src/events/transports/redis-stream.d.ts
4
4
  interface RedisStreamLike {
@@ -1,4 +1,4 @@
1
- import { Bt as ResourceRegistry, R as IntrospectionPluginOptions, zt as RegisterOptions } from "../interface-DYH8AXGe.mjs";
1
+ import { Bt as ResourceRegistry, R as IntrospectionPluginOptions, zt as RegisterOptions } from "../interface-B91alUzq.mjs";
2
2
  import { FastifyPluginAsync } from "fastify";
3
3
 
4
4
  //#region src/registry/introspectionPlugin.d.ts
@@ -1,3 +1,3 @@
1
- import { n as introspectionPlugin_default, t as introspectionPlugin } from "../registry-I-ogLgL9.mjs";
2
- import { t as ResourceRegistry } from "../ResourceRegistry-C6ngvOnn.mjs";
1
+ import { n as introspectionPlugin_default, t as introspectionPlugin } from "../registry-B3lRFBWo.mjs";
2
+ import { t as ResourceRegistry } from "../ResourceRegistry-DsHiG9cL.mjs";
3
3
  export { ResourceRegistry, introspectionPlugin_default as introspectionPlugin, introspectionPlugin as introspectionPluginFn };
@@ -0,0 +1,40 @@
1
+ import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
+ import fp from "fastify-plugin";
3
+ //#region src/plugins/replyHelpers.ts
4
+ var replyHelpers_exports = /* @__PURE__ */ __exportAll({ replyHelpersPlugin: () => replyHelpersPlugin });
5
+ async function replyHelpersPluginFn(fastify) {
6
+ fastify.decorateReply("ok", function(data, statusCode = 200) {
7
+ return this.code(statusCode).send({
8
+ success: true,
9
+ data
10
+ });
11
+ });
12
+ fastify.decorateReply("fail", function(error, statusCode = 400) {
13
+ if (Array.isArray(error)) return this.code(statusCode).send({
14
+ success: false,
15
+ errors: error
16
+ });
17
+ return this.code(statusCode).send({
18
+ success: false,
19
+ error
20
+ });
21
+ });
22
+ fastify.decorateReply("paginated", function(result) {
23
+ return this.code(200).send({
24
+ success: true,
25
+ ...result
26
+ });
27
+ });
28
+ fastify.decorateReply("stream", function(source, options) {
29
+ this.code(options.statusCode ?? 200);
30
+ this.header("content-type", options.contentType);
31
+ if (options.filename) this.header("content-disposition", `attachment; filename="${options.filename}"`);
32
+ return this.send(source);
33
+ });
34
+ }
35
+ const replyHelpersPlugin = fp(replyHelpersPluginFn, {
36
+ name: "arc-reply-helpers",
37
+ fastify: "5.x"
38
+ });
39
+ //#endregion
40
+ export { replyHelpers_exports as n, replyHelpersPlugin as t };
@@ -1,5 +1,6 @@
1
- import { t as BaseController } from "./BaseController-DzRtluEF.mjs";
2
- import { t as pluralize } from "./pluralize-CcT6qF0a.mjs";
1
+ import { t as BaseController } from "./BaseController-CpMfCXdn.mjs";
2
+ import { n as normalizePermissionResult } from "./applyPermissionResult-D6GPMsvh.mjs";
3
+ import { t as pluralize } from "./pluralize-Dckfq6US.mjs";
3
4
  import { z } from "zod";
4
5
  //#region src/integrations/mcp/createMcpServer.ts
5
6
  /**
@@ -88,7 +89,9 @@ const PAGINATION_SHAPE = {
88
89
  page: z.number().int().min(1).optional().describe("Page number (1-based)"),
89
90
  limit: z.number().int().min(1).max(100).optional().describe("Items per page (max 100)"),
90
91
  sort: z.string().optional().describe("Sort field, prefix with - for descending"),
91
- search: z.string().optional().describe("Full-text search query")
92
+ search: z.string().optional().describe("Full-text search query"),
93
+ select: z.string().optional().describe("Comma-separated field list to project (e.g. 'name,price'). Prefix with '-' to exclude (e.g. '-description')."),
94
+ populate: z.string().optional().describe("Comma-separated relation paths to hydrate (e.g. 'supplier,category'). Follows Mongoose populate syntax when the adapter is MongoKit.")
92
95
  };
93
96
  /**
94
97
  * Convert Arc fieldRules to a flat Zod shape.
@@ -179,6 +182,7 @@ function buildListShape(fieldRules, options) {
179
182
  if (allHidden.has(name)) continue;
180
183
  const rule = fieldRules[name];
181
184
  if (!rule) continue;
185
+ if (rule.hidden || rule.systemManaged) continue;
182
186
  const filterField = buildFieldSchema(rule);
183
187
  shape[name] = filterField.optional();
184
188
  if (allowedOperators?.length) {
@@ -212,11 +216,19 @@ function buildListShape(fieldRules, options) {
212
216
  * | create | {} | {} | all input fields |
213
217
  * | update | { id } | {} | input minus id |
214
218
  * | delete | { id } | {} | undefined |
219
+ *
220
+ * **scopeOverride** — when a permission check (e.g. `requireApiKey()`) returns
221
+ * `PermissionResult.scope`, the MCP tool handler must install it on the request
222
+ * context the same way CRUD/action routes do. This parameter follows the exact
223
+ * same non-downgrade rule as `applyPermissionResult`: it overrides only when
224
+ * the session-derived scope is `public` (i.e. MCP called with `auth: false`).
225
+ * An authenticated session scope is never overwritten.
215
226
  */
216
- function buildRequestContext(input, auth, operation, policyFilters) {
217
- const scope = buildScope(auth);
227
+ function buildRequestContext(input, auth, operation, policyFilters, scopeOverride) {
228
+ const sessionScope = buildScope(auth);
229
+ const scope = scopeOverride && sessionScope.kind === "public" ? scopeOverride : sessionScope;
218
230
  const base = {
219
- user: auth ? {
231
+ user: auth?.userId ? {
220
232
  id: auth.userId,
221
233
  _id: auth.userId,
222
234
  ...auth
@@ -264,7 +276,30 @@ function buildRequestContext(input, auth, operation, policyFilters) {
264
276
  };
265
277
  }
266
278
  }
267
- /** Convert MCP operator keys (`price_gt`) to MongoKit bracket notation (`price[gt]`). */
279
+ /**
280
+ * Convert MCP operator keys (`price_gt`, `location_withinRadius`) to the
281
+ * nested object shape MongoKit's QueryParser expects (`{ price: { gt: ... } }`,
282
+ * `{ location: { withinRadius: ... } }`).
283
+ *
284
+ * **Comparison operators** (price_gt, age_lte, …): coerce filter values via
285
+ * the parser's coercion path.
286
+ *
287
+ * **Set operators** (status_in, role_nin, …): MongoKit accepts both
288
+ * comma-separated strings and arrays.
289
+ *
290
+ * **Existence** (deletedAt_exists): coerced to boolean by the parser.
291
+ *
292
+ * **Geo operators** (location_near, location_withinRadius, location_geoWithin,
293
+ * location_nearSphere): MongoKit 3.5.5+ — values are coordinate strings the
294
+ * parser's geo primitive handles. Without these in the allowlist, MCP agents
295
+ * couldn't pass geo filters at all and Arc would silently leak unfiltered docs.
296
+ *
297
+ * Keep this set in sync with MongoKit's QueryParser operators map (search for
298
+ * `private readonly operators` in QueryParser.ts) plus the geo operators
299
+ * recognized by `isGeoOperator` in primitives/geo.ts. We deliberately list
300
+ * them explicitly here rather than asking the parser at runtime — Arc must
301
+ * not import MongoKit internals just to know what an operator looks like.
302
+ */
268
303
  const OPERATOR_SUFFIXES = new Set([
269
304
  "eq",
270
305
  "ne",
@@ -274,7 +309,16 @@ const OPERATOR_SUFFIXES = new Set([
274
309
  "lte",
275
310
  "in",
276
311
  "nin",
277
- "exists"
312
+ "exists",
313
+ "size",
314
+ "type",
315
+ "like",
316
+ "contains",
317
+ "regex",
318
+ "near",
319
+ "nearSphere",
320
+ "withinRadius",
321
+ "geoWithin"
278
322
  ]);
279
323
  function expandOperatorKeys(input) {
280
324
  const out = {};
@@ -300,17 +344,23 @@ function expandOperatorKeys(input) {
300
344
  }
301
345
  function buildScope(auth) {
302
346
  if (!auth) return { kind: "public" };
347
+ if (auth.clientId && auth.organizationId) return {
348
+ kind: "service",
349
+ clientId: auth.clientId,
350
+ organizationId: auth.organizationId,
351
+ scopes: auth.scopes
352
+ };
303
353
  if (auth.organizationId) return {
304
354
  kind: "member",
305
355
  userId: auth.userId,
306
- userRoles: [],
356
+ userRoles: auth.roles ?? [],
307
357
  organizationId: auth.organizationId,
308
- orgRoles: []
358
+ orgRoles: auth.orgRoles ?? []
309
359
  };
310
360
  return {
311
361
  kind: "authenticated",
312
362
  userId: auth.userId,
313
- userRoles: []
363
+ userRoles: auth.roles ?? []
314
364
  };
315
365
  }
316
366
  //#endregion
@@ -648,15 +698,15 @@ function createHandler(op, controller, resourceName, permissions) {
648
698
  }],
649
699
  isError: true
650
700
  };
651
- const policyFilters = await evaluatePermission(permissions?.[op], ctx.session, resourceName, op, input);
652
- if (policyFilters === false) return {
701
+ const permResult = await evaluatePermission(permissions?.[op], ctx.session, resourceName, op, input);
702
+ if (permResult && !permResult.granted) return {
653
703
  content: [{
654
704
  type: "text",
655
- text: `Permission denied: ${op} on ${resourceName}`
705
+ text: `Permission denied: ${op} on ${resourceName}${permResult.reason ? ` — ${permResult.reason}` : ""}`
656
706
  }],
657
707
  isError: true
658
708
  };
659
- return toCallToolResult(await method(buildRequestContext(input, ctx.session, op, policyFilters || void 0)));
709
+ return toCallToolResult(await method(buildRequestContext(input, ctx.session, op, permResult?.filters, permResult?.scope)));
660
710
  } catch (err) {
661
711
  const msg = err instanceof Error ? err.message : String(err);
662
712
  ctx.log("error", `${resourceName}.${op}: ${msg}`).catch(() => {});
@@ -698,10 +748,13 @@ function createAdditionalRouteHandler(route, controller, hasId) {
698
748
  /**
699
749
  * Evaluate a resource's permission check in MCP context.
700
750
  *
701
- * Returns:
702
- * - `false` if permission denied
703
- * - `Record<string, unknown>` if granted with filters (ownership patterns)
704
- * - `null` if granted without filters (or no permission check defined)
751
+ * Returns the full normalized `PermissionResult` so the caller can honor
752
+ * ALL side-effects (filters + scope) consistently with CRUD/action routes.
753
+ * Returns `null` when no permission is defined (= allow, no side effects).
754
+ *
755
+ * Promoting booleans to `PermissionResult` via the shared `normalizePermissionResult`
756
+ * helper keeps the contract aligned with the rest of Arc — there is a single
757
+ * normalization path for every call site.
705
758
  */
706
759
  async function evaluatePermission(check, session, resource, action, input) {
707
760
  if (!check) return null;
@@ -710,7 +763,7 @@ async function evaluatePermission(check, session, resource, action, input) {
710
763
  _id: session.userId,
711
764
  ...session
712
765
  } : null;
713
- const result = await check({
766
+ return normalizePermissionResult(await check({
714
767
  user,
715
768
  request: {
716
769
  user,
@@ -724,11 +777,7 @@ async function evaluatePermission(check, session, resource, action, input) {
724
777
  resourceId: typeof input.id === "string" ? input.id : void 0,
725
778
  params: {},
726
779
  data: input
727
- });
728
- if (typeof result === "boolean") return result ? null : false;
729
- const permResult = result;
730
- if (!permResult.granted) return false;
731
- return permResult.filters ?? null;
780
+ }));
732
781
  }
733
782
  /**
734
783
  * Derive a fieldRules-shaped object from the adapter's auto-generated body
@@ -1,4 +1,4 @@
1
- import { r as CircuitBreakerOptions } from "../circuitBreaker-JP2GdJ4b.mjs";
1
+ import { r as CircuitBreakerOptions } from "../circuitBreaker-BBPDt-J_.mjs";
2
2
 
3
3
  //#region src/rpc/serviceClient.d.ts
4
4
  interface RetryConfig {
@@ -1,4 +1,4 @@
1
- import { t as CircuitBreaker } from "../circuitBreaker-BOBOpN2w.mjs";
1
+ import { t as CircuitBreaker } from "../circuitBreaker-l18oRgL5.mjs";
2
2
  //#region src/rpc/serviceClient.ts
3
3
  /**
4
4
  * Service Client — Resource-Oriented RPC