@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.
Files changed (135) hide show
  1. package/README.md +95 -1
  2. package/dist/{BaseController-AbbRx3e0.mjs → BaseController-CpMfCXdn.mjs} +214 -16
  3. package/dist/adapters/index.d.mts +2 -2
  4. package/dist/adapters/index.mjs +1 -1
  5. package/dist/{adapters-CTn28N4y.mjs → adapters-BxGgSHjj.mjs} +7 -13
  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-B_nvKNAQ.mjs} +11 -11
  27. package/dist/{defineResource-Ckxg6HrZ.mjs → defineResource-DZzyl4a4.mjs} +73 -56
  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-Dm-HTBCt.d.mts +23 -0
  34. package/dist/{errorHandler-Do4vVQ1f.d.mts → errorHandler-COa51ho_.d.mts} +1 -1
  35. package/dist/{errorHandler-r2595m8T.mjs → errorHandler-DXUttWEO.mjs} +1 -1
  36. package/dist/{eventPlugin-DW45v4V5.d.mts → eventPlugin-BgLxJkIB.d.mts} +1 -1
  37. package/dist/{eventPlugin-Ba00swHF.mjs → eventPlugin-DsaNNXzZ.mjs} +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-BYpRGXif.d.mts +640 -0
  50. package/dist/{index-B4uZm82R.d.mts → index-KXM8_JmQ.d.mts} +47 -4
  51. package/dist/{index-DrCqa3Jq.d.mts → index-StgFaQKD.d.mts} +3 -3
  52. package/dist/index.d.mts +8 -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 +1 -1
  59. package/dist/integrations/mcp/testing.d.mts +1 -1
  60. package/dist/integrations/mcp/testing.mjs +1 -1
  61. package/dist/{interface-CrN45qz1.d.mts → interface-Dwzqt4mn.d.mts} +204 -18
  62. package/dist/{mongodb-pMvOlR5_.d.mts → mongodb-Bq90j-Uj.d.mts} +1 -1
  63. package/dist/{mongodb-kltrBPa1.d.mts → mongodb-DdyYlIXg.d.mts} +1 -1
  64. package/dist/{openapi-CBmZ6EQN.mjs → openapi-C5UhIeWu.mjs} +1 -1
  65. package/dist/org/index.d.mts +2 -2
  66. package/dist/org/index.mjs +1 -1
  67. package/dist/permissions/index.d.mts +4 -4
  68. package/dist/permissions/index.mjs +3 -2
  69. package/dist/{permissions-C8ImI8gC.mjs → permissions-CH4cNwJi.mjs} +358 -64
  70. package/dist/plugins/index.d.mts +4 -4
  71. package/dist/plugins/index.mjs +10 -10
  72. package/dist/plugins/response-cache.mjs +1 -1
  73. package/dist/plugins/tracing-entry.d.mts +1 -1
  74. package/dist/plugins/tracing-entry.mjs +1 -1
  75. package/dist/policies/index.d.mts +1 -1
  76. package/dist/presets/index.d.mts +3 -3
  77. package/dist/presets/index.mjs +1 -1
  78. package/dist/presets/multiTenant.d.mts +53 -3
  79. package/dist/presets/multiTenant.mjs +89 -47
  80. package/dist/{presets-BMfdy34e.mjs → presets-BFrGvvjL.mjs} +2 -2
  81. package/dist/{queryCachePlugin-DcmETvcB.d.mts → queryCachePlugin-Bw8XyJpX.d.mts} +1 -1
  82. package/dist/{queryCachePlugin-XtFplYO9.mjs → queryCachePlugin-CwTpR04-.mjs} +2 -2
  83. package/dist/{redis-D0Qc-9EW.d.mts → redis-CyCntzTO.d.mts} +1 -1
  84. package/dist/{redis-stream-BW9UKLZM.d.mts → redis-stream-We_Ucl9-.d.mts} +1 -1
  85. package/dist/registry/index.d.mts +1 -1
  86. package/dist/registry/index.mjs +2 -2
  87. package/dist/{resourceToTools-DH3c3e-T.mjs → resourceToTools-CkVSSzKg.mjs} +313 -33
  88. package/dist/rpc/index.d.mts +1 -1
  89. package/dist/rpc/index.mjs +1 -1
  90. package/dist/scope/index.d.mts +3 -2
  91. package/dist/scope/index.mjs +4 -3
  92. package/dist/{sse-BF7GR7IB.mjs → sse-Bp3dabF1.mjs} +2 -2
  93. package/dist/testing/index.d.mts +2 -2
  94. package/dist/testing/index.mjs +1 -1
  95. package/dist/types/index.d.mts +4 -3
  96. package/dist/types/index.mjs +1 -1
  97. package/dist/types-AOD8fxIw.mjs +229 -0
  98. package/dist/types-CNEbix8T.d.mts +286 -0
  99. package/dist/{types-DurlBP2N.d.mts → types-ClmkMDK1.d.mts} +1 -1
  100. package/dist/{types-C1Z28coa.d.mts → types-D0qf0Mf4.d.mts} +9 -9
  101. package/dist/types-DPsC0taJ.d.mts +178 -0
  102. package/dist/utils/index.d.mts +3 -3
  103. package/dist/utils/index.mjs +5 -5
  104. package/package.json +34 -22
  105. package/skills/arc/SKILL.md +278 -6
  106. package/skills/arc/references/multi-tenancy.md +208 -0
  107. package/dist/elevation-C_taLQrM.d.mts +0 -147
  108. package/dist/index-NGZksqM5.d.mts +0 -398
  109. package/dist/types-BNUccdcf.d.mts +0 -101
  110. package/dist/types-BhtYdxZU.mjs +0 -91
  111. /package/dist/{EventTransport-wc5hSLik.d.mts → EventTransport-CUpRK_Lg.d.mts} +0 -0
  112. /package/dist/{HookSystem-COkyWztM.mjs → HookSystem-D7lfx--K.mjs} +0 -0
  113. /package/dist/{ResourceRegistry-C6ngvOnn.mjs → ResourceRegistry-DsHiG9cL.mjs} +0 -0
  114. /package/dist/{caching-BSXB-Xr7.mjs → caching-5DtLwIqb.mjs} +0 -0
  115. /package/dist/{circuitBreaker-JP2GdJ4b.d.mts → circuitBreaker-DwxrljLB.d.mts} +0 -0
  116. /package/dist/{circuitBreaker-BOBOpN2w.mjs → circuitBreaker-l18oRgL5.mjs} +0 -0
  117. /package/dist/{errors-CcVbl1-T.d.mts → errors-CCSsMpXE.d.mts} +0 -0
  118. /package/dist/{errors-NoQKsbAT.mjs → errors-Cg58SLNi.mjs} +0 -0
  119. /package/dist/{externalPaths-DpO-s7r8.d.mts → externalPaths-Dg7OLsKo.d.mts} +0 -0
  120. /package/dist/{fields-DFwdaWCq.d.mts → fields-CYuLMJPD.d.mts} +0 -0
  121. /package/dist/{interface-gr-7qo9j.d.mts → interface-B9rHWPxD.d.mts} +0 -0
  122. /package/dist/{interface-D_BWALyZ.d.mts → interface-CnluRL4_.d.mts} +0 -0
  123. /package/dist/{logger-Dz3j1ItV.mjs → logger-DLg8-Ueg.mjs} +0 -0
  124. /package/dist/{memory-BFAYkf8H.mjs → memory-Cp7_cAko.mjs} +0 -0
  125. /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
  126. /package/dist/{mongodb-BuQ7fNTg.mjs → mongodb-mlgxkYI3.mjs} +0 -0
  127. /package/dist/{pluralize-CcT6qF0a.mjs → pluralize-COpOVar8.mjs} +0 -0
  128. /package/dist/{registry-I-ogLgL9.mjs → registry-B3lRFBWo.mjs} +0 -0
  129. /package/dist/{requestContext-DYtmNpm5.mjs → requestContext-xHIKedG6.mjs} +0 -0
  130. /package/dist/{schemaConverter-DjzHpFam.mjs → schemaConverter-0TyONAwM.mjs} +0 -0
  131. /package/dist/{sessionManager-wbkYj2HL.d.mts → sessionManager-IW4sbIea.d.mts} +0 -0
  132. /package/dist/{tracing-bz_U4EM1.d.mts → tracing-65B51Dw3.d.mts} +0 -0
  133. /package/dist/{typeGuards-Cj5Rgvlg.mjs → typeGuards-CcFZXgU7.mjs} +0 -0
  134. /package/dist/{utils-Dc0WhlIl.mjs → utils-B-l6410F.mjs} +0 -0
  135. /package/dist/{versioning-BzfeHmhj.mjs → versioning-aUUVziBY.mjs} +0 -0
@@ -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.2";
47
+ const resolvedVersion = serviceVersion ?? "2.7.1";
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-DPsC0taJ.mjs";
2
2
  import { FastifyReply, FastifyRequest } from "fastify";
3
3
 
4
4
  //#region src/policies/PolicyInterface.d.ts
@@ -1,5 +1,5 @@
1
- import { Ft as IControllerResponse, It as IRequestContext, Wt as PaginatedResult, X as PresetResult, at as ResourceConfig, l as AnyRecord } from "../interface-CrN45qz1.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-Dwzqt4mn.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 { X as PresetResult, x as CrudRouteKey } from "../interface-CrN45qz1.mjs";
1
+ import { S as CrudRouteKey, Z as PresetResult } from "../interface-Dwzqt4mn.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-CnluRL4_.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-B9rHWPxD.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-CUpRK_Lg.mjs";
2
2
 
3
3
  //#region src/events/transports/redis-stream.d.ts
4
4
  interface RedisStreamLike {
@@ -1,4 +1,4 @@
1
- import { L as IntrospectionPluginOptions, Rt as RegisterOptions, zt as ResourceRegistry } from "../interface-CrN45qz1.mjs";
1
+ import { Bt as ResourceRegistry, R as IntrospectionPluginOptions, zt as RegisterOptions } from "../interface-Dwzqt4mn.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 };