@classytic/arc 2.11.2 → 2.11.4

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 (113) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +20 -21
  3. package/bin/arc.js +2 -2
  4. package/dist/{BaseController-JNV08qOT.mjs → BaseController-swXruJ2_.mjs} +2 -2
  5. package/dist/EventTransport-BFQjw9pB.mjs +133 -0
  6. package/dist/{QueryCache-DOBNHBE0.d.mts → QueryCache-D41bfdBB.d.mts} +1 -1
  7. package/dist/{actionPermissions-C8YYU92K.mjs → actionPermissions-sUUKDhtP.mjs} +4 -2
  8. package/dist/adapters/index.d.mts +3 -3
  9. package/dist/adapters/index.mjs +2 -2
  10. package/dist/{adapters-D0tT2Tyo.mjs → adapters-DUUiiimH.mjs} +17 -2
  11. package/dist/audit/index.d.mts +2 -2
  12. package/dist/auth/index.d.mts +4 -4
  13. package/dist/auth/index.mjs +1 -1
  14. package/dist/auth/redis-session.d.mts +1 -1
  15. package/dist/cache/index.d.mts +3 -3
  16. package/dist/cli/commands/docs.mjs +1 -1
  17. package/dist/cli/commands/generate.d.mts +0 -2
  18. package/dist/cli/commands/generate.mjs +16 -16
  19. package/dist/cli/commands/init.mjs +149 -65
  20. package/dist/context/index.mjs +1 -1
  21. package/dist/core/index.d.mts +2 -2
  22. package/dist/core/index.mjs +3 -3
  23. package/dist/{core-DXdSSFW-.mjs → core-CbcQRIch.mjs} +25 -8
  24. package/dist/{createActionRouter-BwaSM0No.mjs → createActionRouter-CIKOcNA7.mjs} +74 -14
  25. package/dist/{createApp-P1d6rjPy.mjs → createApp-C9bRrqlX.mjs} +4 -6
  26. package/dist/defineEvent-D1Ky9M1D.mjs +188 -0
  27. package/dist/docs/index.d.mts +2 -2
  28. package/dist/docs/index.mjs +1 -1
  29. package/dist/{eventPlugin--5HIkdPU.mjs → eventPlugin-Cts2-Tfj.mjs} +9 -135
  30. package/dist/{eventPlugin-CUNjYYRY.d.mts → eventPlugin-DDJoNEPL.d.mts} +34 -7
  31. package/dist/events/index.d.mts +164 -5
  32. package/dist/events/index.mjs +138 -182
  33. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  34. package/dist/events/transports/redis-stream-entry.mjs +204 -31
  35. package/dist/events/transports/redis.d.mts +1 -1
  36. package/dist/factory/index.d.mts +1 -1
  37. package/dist/factory/index.mjs +1 -1
  38. package/dist/{fields-C8Y0XLAu.d.mts → fields-BRjxOAFp.d.mts} +1 -1
  39. package/dist/hooks/index.d.mts +1 -1
  40. package/dist/idempotency/index.d.mts +3 -3
  41. package/dist/idempotency/index.mjs +1 -1
  42. package/dist/idempotency/redis.d.mts +1 -1
  43. package/dist/{index-6u4_Gg6G.d.mts → index-CXXRbnf8.d.mts} +51 -5
  44. package/dist/{index-DdQ3O9Pg.d.mts → index-D9t1KNaB.d.mts} +2 -2
  45. package/dist/{index-BbMrcvGp.d.mts → index-Rg8axYPz.d.mts} +12 -4
  46. package/dist/{index-BdXnTPRj.d.mts → index-m8mOOlFW.d.mts} +3 -3
  47. package/dist/{index-BYCqHCVu.d.mts → index-rHjXmJar.d.mts} +3 -3
  48. package/dist/index.d.mts +7 -7
  49. package/dist/index.mjs +7 -7
  50. package/dist/integrations/event-gateway.d.mts +2 -2
  51. package/dist/integrations/index.d.mts +2 -2
  52. package/dist/integrations/mcp/index.d.mts +2 -2
  53. package/dist/integrations/mcp/index.mjs +1 -1
  54. package/dist/integrations/mcp/testing.d.mts +1 -1
  55. package/dist/integrations/mcp/testing.mjs +1 -1
  56. package/dist/integrations/websocket-redis.d.mts +1 -1
  57. package/dist/integrations/websocket.d.mts +1 -1
  58. package/dist/middleware/index.d.mts +1 -1
  59. package/dist/{openapi-C0L9ar7m.mjs → openapi-D7G1V7ex.mjs} +2 -2
  60. package/dist/org/index.d.mts +2 -2
  61. package/dist/permissions/index.d.mts +2 -2
  62. package/dist/permissions/index.mjs +1 -1
  63. package/dist/{permissions-B4vU9L0Q.mjs → permissions-gd_aUWrR.mjs} +42 -0
  64. package/dist/pipeline/index.d.mts +1 -1
  65. package/dist/plugins/index.d.mts +5 -5
  66. package/dist/plugins/index.mjs +1 -1
  67. package/dist/plugins/tracing-entry.d.mts +1 -1
  68. package/dist/plugins/tracing-entry.mjs +1 -1
  69. package/dist/presets/filesUpload.d.mts +4 -4
  70. package/dist/presets/filesUpload.mjs +1 -1
  71. package/dist/presets/index.d.mts +1 -1
  72. package/dist/presets/index.mjs +1 -1
  73. package/dist/presets/multiTenant.d.mts +1 -1
  74. package/dist/presets/search.d.mts +2 -2
  75. package/dist/presets/search.mjs +1 -1
  76. package/dist/{presets-k604Lj99.mjs → presets-Z7P5w4gF.mjs} +1 -1
  77. package/dist/{queryCachePlugin-BUXBSm4F.d.mts → queryCachePlugin-CqMdLI2-.d.mts} +2 -2
  78. package/dist/{redis-Cm1gnRDf.d.mts → redis-DiMkdHEl.d.mts} +1 -1
  79. package/dist/redis-stream-xTGxB2bm.d.mts +232 -0
  80. package/dist/registry/index.d.mts +1 -1
  81. package/dist/{requestContext-CfRkaxwf.mjs → requestContext-C5XeK3VA.mjs} +15 -0
  82. package/dist/{resourceToTools--okX6QBr.mjs → resourceToTools-CxNmI6xF.mjs} +7 -6
  83. package/dist/{routerShared-DeESFp4a.mjs → routerShared-BqLRb5l7.mjs} +60 -3
  84. package/dist/scope/index.d.mts +2 -2
  85. package/dist/testing/index.d.mts +2 -2
  86. package/dist/testing/index.mjs +1 -1
  87. package/dist/testing/storageContract.d.mts +1 -1
  88. package/dist/types/index.d.mts +4 -4
  89. package/dist/types/storage.d.mts +1 -1
  90. package/dist/{types-9beEMe25.d.mts → types-BQ9TJQNy.d.mts} +1 -1
  91. package/dist/{types-BH7dEGvU.d.mts → types-D7KpfiL1.d.mts} +10 -10
  92. package/dist/utils/index.d.mts +1 -1
  93. package/dist/utils/index.mjs +1 -1
  94. package/dist/{utils-D3Yxnrwr.mjs → utils-CcYTj09l.mjs} +1 -1
  95. package/dist/{versioning-M9lNLhO8.d.mts → versioning-DsglKfM_.d.mts} +1 -1
  96. package/package.json +3 -1
  97. package/skills/arc/SKILL.md +409 -769
  98. package/skills/arc/references/events.md +489 -489
  99. package/dist/redis-stream-CM8TXTix.d.mts +0 -110
  100. /package/dist/{EventTransport-CfVEGaEl.d.mts → EventTransport-CYNUXdCJ.d.mts} +0 -0
  101. /package/dist/{elevation-s5ykdNHr.d.mts → elevation-BQQXZ_VR.d.mts} +0 -0
  102. /package/dist/{errorHandler-Co3lnVmJ.d.mts → errorHandler-DEWmGWPz.d.mts} +0 -0
  103. /package/dist/{externalPaths-Bapitwvd.d.mts → externalPaths-BD5nw6St.d.mts} +0 -0
  104. /package/dist/{interface-CkkWm5uR.d.mts → interface-DfLGcus7.d.mts} +0 -0
  105. /package/dist/{interface-Da0r7Lna.d.mts → interface-beEtJyWM.d.mts} +0 -0
  106. /package/dist/{pluralize-BneOJkpi.mjs → pluralize-CWP6MB39.mjs} +0 -0
  107. /package/dist/{schemaIR-BlG9bY7v.mjs → schemaIR-Dy2p4MxS.mjs} +0 -0
  108. /package/dist/{sessionManager-D-oNWHz3.d.mts → sessionManager-C4Le_UB3.d.mts} +0 -0
  109. /package/dist/{storage-BwGQXUpd.d.mts → storage-Dfzt4VTl.d.mts} +0 -0
  110. /package/dist/{store-helpers-BhrzxvyQ.mjs → store-helpers-Cp4uKC1U.mjs} +0 -0
  111. /package/dist/{tracing-DokiEsuz.d.mts → tracing-QJVprktp.d.mts} +0 -0
  112. /package/dist/{types-tgR4Pt8F.d.mts → types-DDyTPc6y.d.mts} +0 -0
  113. /package/dist/{websocket-CyJ1VIFI.d.mts → websocket-ChC2rqe1.d.mts} +0 -0
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Classytic
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Classytic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,14 +1,19 @@
1
1
  # @classytic/arc
2
2
 
3
- Database-agnostic resource framework for Fastify. One `defineResource()` call → REST + auth + permissions + events + caching + OpenAPI + MCP tools — without boilerplate.
3
+ Database-agnostic resource framework for Fastify. One `defineResource()` call → REST + auth + permissions + events + caching + OpenAPI + MCP tools.
4
4
 
5
- **v2.11** · Fastify 5+ · Node.js 22+ · ESM only
5
+ Fastify 5+ · Node.js 22+ · ESM only
6
6
 
7
7
  ```bash
8
8
  npm install @classytic/arc fastify
9
- npm install @classytic/mongokit mongoose # MongoDB (most common)
9
+
10
+ # Security defaults createApp() loads (each opt-out via `cors: false` etc.)
11
+ npm install @fastify/cors @fastify/helmet @fastify/rate-limit @fastify/under-pressure @fastify/sensible
12
+
13
+ # Storage adapter — pick one
14
+ npm install @classytic/mongokit mongoose # MongoDB
10
15
  # OR @classytic/sqlitekit drizzle-orm better-sqlite3 (sqlite)
11
- # OR bring your own: implement RepositoryLike from @classytic/repo-core
16
+ # OR implement RepositoryLike from @classytic/repo-core
12
17
  ```
13
18
 
14
19
  ---
@@ -18,7 +23,7 @@ npm install @classytic/mongokit mongoose # MongoDB (most common)
18
23
  | | |
19
24
  |---|---|
20
25
  | **One call, full REST** | `defineResource({ name, adapter, presets, permissions })` → `GET /`, `GET /:id`, `POST /`, `PATCH /:id`, `DELETE /:id` + custom routes + actions |
21
- | **DB-agnostic** | Mongoose, Prisma, Drizzle, or any `RepositoryLike` impl. Swap backends without rewriting routes. |
26
+ | **DB-agnostic** | Mongoose, Drizzle/sqlitekit, or any `RepositoryLike` impl swap backends without rewriting routes. (Prisma adapter is experimental: implemented, no integration tests yet.) |
22
27
  | **Multi-tenant by default** | Tenant-field auto-injected, scope-aware queries, per-org cache keys, elevation events. |
23
28
  | **Tree-shakable subpaths** | `@classytic/arc/auth`, `/events`, `/cache`, `/mcp`, `/integrations/jobs` — pay only for what you import. |
24
29
  | **MCP tools, free** | Resources auto-generate Model Context Protocol tools for AI agents. Same permissions, same field rules. |
@@ -33,12 +38,19 @@ import { createApp, loadResources } from '@classytic/arc/factory';
33
38
 
34
39
  await mongoose.connect(process.env.DB_URI);
35
40
 
41
+ // Fail fast on missing CORS env — silent `undefined` here drops to surprising
42
+ // browser defaults. Browser apps: declare an explicit allowlist (below).
43
+ // Server-to-server / API-key services: `cors: { origin: '*', credentials: false }`
44
+ // or `cors: false` to disable entirely (CORS is a browser-only concern).
45
+ const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS;
46
+ if (!ALLOWED_ORIGINS) throw new Error('ALLOWED_ORIGINS env is required');
47
+
36
48
  const app = await createApp({
37
49
  preset: 'production',
38
50
  resourcePrefix: '/api/v1',
39
51
  resources: await loadResources(import.meta.url), // auto-discover *.resource.ts
40
52
  auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
41
- cors: { origin: process.env.ALLOWED_ORIGINS?.split(',') },
53
+ cors: { origin: ALLOWED_ORIGINS.split(','), credentials: true },
42
54
  });
43
55
 
44
56
  await app.listen({ port: 8040, host: '0.0.0.0' });
@@ -60,7 +72,7 @@ resources: async () => {
60
72
  },
61
73
  ```
62
74
 
63
- `loadResources({ context })` (2.11.1+) threads engine handles into resources whose default export is `(ctx) => defineResource(...)`. No parallel factory files, no `exclude: [...]` bookkeeping.
75
+ `loadResources({ context })` threads engine handles into resources whose default export is `(ctx) => defineResource(...)`. No parallel factory files, no `exclude: [...]` bookkeeping.
64
76
 
65
77
  ---
66
78
 
@@ -199,7 +211,7 @@ const ctx = await createTestApp({
199
211
  authMode: 'jwt',
200
212
  connectMongoose: true, // in-memory Mongo + Mongoose connect
201
213
  });
202
- ctx.auth.register('admin', { user: { id: '1', roles: ['admin'] }, orgId: 'org-1' });
214
+ ctx.auth.register('admin', { user: { id: '1', role: 'admin' }, orgId: 'org-1' });
203
215
 
204
216
  const res = await ctx.app.inject({
205
217
  method: 'POST',
@@ -229,19 +241,6 @@ arc doctor # diagnose env
229
241
 
230
242
  ---
231
243
 
232
- ## Highlights from recent releases
233
-
234
- | Version | Headline |
235
- |---|---|
236
- | **2.11.2** | `RouteSchemaOptions['query']` types `allowedPopulate` + `allowedLookups` |
237
- | **2.11.1** | `loadResources({ context })` + factory exports; `ActionDefinition.schema` widened to `unknown`; `silent` removed in favor of `arcLog` fallback |
238
- | **2.11.0** | `BaseController` mixin split, testing surface rewrite (`createTestApp`, `TestAuthProvider`, `expectArc`), action-router parity, async resources factory |
239
- | **2.10** | Permissions split, `RepositoryLike` plugs into outbox/audit/idempotency, plugin onSend race fix |
240
-
241
- Full history: [`/changelog/v2.md`](changelog/v2.md).
242
-
243
- ---
244
-
245
244
  ## Documentation
246
245
 
247
246
  - **Skill** for AI agents: `npx skills add classytic/arc` — wires arc into Claude Code / agentic flows.
package/bin/arc.js CHANGED
@@ -334,7 +334,7 @@ GLOBAL OPTIONS
334
334
 
335
335
  INIT OPTIONS
336
336
  --mongokit Use MongoKit adapter (default, recommended)
337
- --custom Use custom adapter (empty template)
337
+ --custom Use custom / Drizzle-ready adapter template
338
338
  --better-auth Use Better Auth (default, recommended)
339
339
  --jwt Use Arc built-in JWT auth
340
340
  --multi-tenant, --multi Multi-tenant mode (adds org scoping)
@@ -346,7 +346,7 @@ INIT OPTIONS
346
346
  --skip-install Skip npm install after scaffolding
347
347
 
348
348
  GENERATE SUBCOMMANDS
349
- resource, r Generate full resource (model, repo, controller, schemas, resource)
349
+ resource, r Generate resource-first scaffold (model, repo, resource, test)
350
350
  controller, c Generate controller only
351
351
  model, m Generate model only
352
352
  repository, repo Generate repository only
@@ -1,9 +1,9 @@
1
1
  import { h as SYSTEM_FIELDS, o as DEFAULT_TENANT_FIELD } from "./constants-BhY1OHoH.mjs";
2
2
  import { arcLog } from "./logger/index.mjs";
3
3
  import { _ as isElevated, n as PUBLIC_SCOPE, o as getOrgId, v as isMember } from "./types-AOD8fxIw.mjs";
4
- import { M as simpleEqualityMatcher, j as getUserId, k as ArcQueryParser } from "./utils-D3Yxnrwr.mjs";
4
+ import { M as simpleEqualityMatcher, j as getUserId, k as ArcQueryParser } from "./utils-CcYTj09l.mjs";
5
5
  import { t as buildQueryKey } from "./keys-CARyUjiR.mjs";
6
- import { M as applyFieldWritePermissions, P as resolveEffectiveRoles } from "./permissions-B4vU9L0Q.mjs";
6
+ import { M as applyFieldWritePermissions, P as resolveEffectiveRoles } from "./permissions-gd_aUWrR.mjs";
7
7
  import { t as getUserRoles } from "./types-DV9WDfeg.mjs";
8
8
  import { r as ForbiddenError } from "./errors-D5c-5BJL.mjs";
9
9
  //#region src/core/AccessControl.ts
@@ -0,0 +1,133 @@
1
+ //#region src/events/EventTransport.ts
2
+ /**
3
+ * In-memory event transport (default)
4
+ * Events are delivered synchronously within the process.
5
+ * Not suitable for multi-instance deployments.
6
+ */
7
+ var MemoryEventTransport = class {
8
+ name = "memory";
9
+ handlers = /* @__PURE__ */ new Map();
10
+ logger;
11
+ constructor(options) {
12
+ this.logger = options?.logger ?? console;
13
+ }
14
+ async publish(event) {
15
+ const exactHandlers = this.handlers.get(event.type) ?? /* @__PURE__ */ new Set();
16
+ const wildcardHandlers = this.handlers.get("*") ?? /* @__PURE__ */ new Set();
17
+ const patternHandlers = /* @__PURE__ */ new Set();
18
+ for (const [pattern, handlers] of this.handlers.entries()) if (pattern.endsWith(".*")) {
19
+ const prefix = pattern.slice(0, -2);
20
+ if (event.type.startsWith(`${prefix}.`)) for (const h of handlers) patternHandlers.add(h);
21
+ }
22
+ const allHandlers = new Set([
23
+ ...exactHandlers,
24
+ ...wildcardHandlers,
25
+ ...patternHandlers
26
+ ]);
27
+ for (const handler of allHandlers) try {
28
+ await handler(event);
29
+ } catch (err) {
30
+ this.logger.error(`[EventTransport] Handler error for ${event.type}:`, err);
31
+ }
32
+ }
33
+ /**
34
+ * Reference `publishMany` implementation — delegates to `publish()` in order.
35
+ *
36
+ * Production transports (Kafka, Redis pipeline, SQS batch) should override
37
+ * this with a single batched network call. Memory transport has nothing to
38
+ * batch, so we just loop — the loop still returns a proper result map so
39
+ * `EventOutbox.relay` can exercise the batched code path in tests.
40
+ */
41
+ async publishMany(events) {
42
+ const results = /* @__PURE__ */ new Map();
43
+ for (const event of events) try {
44
+ await this.publish(event);
45
+ results.set(event.meta.id, null);
46
+ } catch (err) {
47
+ results.set(event.meta.id, err instanceof Error ? err : new Error(String(err)));
48
+ }
49
+ return results;
50
+ }
51
+ async subscribe(pattern, handler) {
52
+ if (!this.handlers.has(pattern)) this.handlers.set(pattern, /* @__PURE__ */ new Set());
53
+ this.handlers.get(pattern)?.add(handler);
54
+ return () => {
55
+ const set = this.handlers.get(pattern);
56
+ if (set) {
57
+ set.delete(handler);
58
+ if (set.size === 0) this.handlers.delete(pattern);
59
+ }
60
+ };
61
+ }
62
+ async close() {
63
+ this.handlers.clear();
64
+ }
65
+ };
66
+ /**
67
+ * Create a domain event with auto-generated metadata.
68
+ *
69
+ * `id` and `timestamp` are filled in; everything else is caller-controlled.
70
+ * Set `schemaVersion` explicitly for any event type you plan to evolve.
71
+ */
72
+ function createEvent(type, payload, meta) {
73
+ return {
74
+ type,
75
+ payload,
76
+ meta: {
77
+ id: crypto.randomUUID(),
78
+ timestamp: /* @__PURE__ */ new Date(),
79
+ ...meta
80
+ }
81
+ };
82
+ }
83
+ /**
84
+ * Create a child event that chains causation from a parent event.
85
+ *
86
+ * Rules:
87
+ * - `causationId` is set to the parent's `id` (direct cause)
88
+ * - `correlationId` is inherited from the parent if set, else falls back
89
+ * to the parent's `id` (root correlation)
90
+ * - `userId` / `organizationId` are inherited when not overridden so the
91
+ * whole chain stays scoped to the originating principal/tenant
92
+ *
93
+ * Caller-supplied `meta` wins over inherited fields — pass `{ userId: newActor }`
94
+ * to override when a subsystem acts on behalf of a different principal.
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * const orderPlaced = createEvent('order.placed', { orderId: 'o1' }, {
99
+ * correlationId: req.id, userId: user.id,
100
+ * });
101
+ * await events.publish(orderPlaced);
102
+ *
103
+ * // Downstream handler emits a child event:
104
+ * const reserved = createChildEvent(orderPlaced, 'inventory.reserved', {
105
+ * orderId: 'o1', skus: ['sku-1', 'sku-2'],
106
+ * });
107
+ * // reserved.meta.causationId === orderPlaced.meta.id
108
+ * // reserved.meta.correlationId === orderPlaced.meta.correlationId
109
+ * // reserved.meta.userId === user.id (inherited)
110
+ * ```
111
+ */
112
+ function createChildEvent(parent, type, payload, meta) {
113
+ const inherited = {
114
+ correlationId: parent.meta.correlationId ?? parent.meta.id,
115
+ causationId: parent.meta.id
116
+ };
117
+ if (parent.meta.userId !== void 0) inherited.userId = parent.meta.userId;
118
+ if (parent.meta.organizationId !== void 0) inherited.organizationId = parent.meta.organizationId;
119
+ if (parent.meta.source !== void 0) inherited.source = parent.meta.source;
120
+ if (parent.meta.idempotencyKey !== void 0) inherited.idempotencyKey = parent.meta.idempotencyKey;
121
+ return {
122
+ type,
123
+ payload,
124
+ meta: {
125
+ id: crypto.randomUUID(),
126
+ timestamp: /* @__PURE__ */ new Date(),
127
+ ...inherited,
128
+ ...meta
129
+ }
130
+ };
131
+ }
132
+ //#endregion
133
+ export { createChildEvent as n, createEvent as r, MemoryEventTransport as t };
@@ -1,4 +1,4 @@
1
- import { r as CacheStore } from "./interface-Da0r7Lna.mjs";
1
+ import { r as CacheStore } from "./interface-beEtJyWM.mjs";
2
2
 
3
3
  //#region src/cache/QueryCache.d.ts
4
4
  /** Metadata wrapper stored in CacheStore */
@@ -5,8 +5,10 @@
5
5
  *
6
6
  * Callers decide what "no gate" means:
7
7
  * - HTTP: boot-time throw in `normalizeActionsToRouterConfig`.
8
- * - MCP: treated as allow (legacy) — but the HTTP fallback now fills the
9
- * gap when `permissions.update` is set, so the MCP hole closes too.
8
+ * - MCP: tool-generation throw in `resourceToTools` (mirrors HTTP the
9
+ * two surfaces fail closed identically so MCP can't expose an
10
+ * unauthenticated mutating tool when the HTTP plugin lifecycle hasn't
11
+ * run).
10
12
  * - OpenAPI: docs advertise the endpoint as unauthenticated.
11
13
  */
12
14
  function resolveActionPermission(input) {
@@ -1,3 +1,3 @@
1
- import { An as RelationMetadata, En as AdapterFactory, Mn as SchemaMetadata, Nn as ValidationResult, On as DataAdapter, jn as RepositoryLike, kn as FieldMetadata } from "../index-6u4_Gg6G.mjs";
2
- import { a as PrismaQueryParserOptions, c as MongooseAdapterOptions, d as DrizzleAdapterOptions, f as createDrizzleAdapter, i as PrismaQueryParser, l as createMongooseAdapter, n as PrismaAdapterOptions, o as createPrismaAdapter, r as PrismaQueryOptions, s as MongooseAdapter, t as PrismaAdapter, u as DrizzleAdapter } from "../index-BbMrcvGp.mjs";
3
- export { AdapterFactory, DataAdapter, DrizzleAdapter, DrizzleAdapterOptions, FieldMetadata, MongooseAdapter, MongooseAdapterOptions, PrismaAdapter, PrismaAdapterOptions, PrismaQueryOptions, PrismaQueryParser, PrismaQueryParserOptions, RelationMetadata, RepositoryLike, SchemaMetadata, ValidationResult, createDrizzleAdapter, createMongooseAdapter, createPrismaAdapter };
1
+ import { An as FieldMetadata, Dn as AdapterRepositoryInput, En as AdapterFactory, Fn as asRepositoryLike, Mn as RepositoryLike, Nn as SchemaMetadata, Pn as ValidationResult, jn as RelationMetadata, kn as DataAdapter } from "../index-CXXRbnf8.mjs";
2
+ import { a as PrismaQueryParserOptions, c as MongooseAdapterOptions, d as DrizzleAdapterOptions, f as createDrizzleAdapter, i as PrismaQueryParser, l as createMongooseAdapter, n as PrismaAdapterOptions, o as createPrismaAdapter, r as PrismaQueryOptions, s as MongooseAdapter, t as PrismaAdapter, u as DrizzleAdapter } from "../index-Rg8axYPz.mjs";
3
+ export { AdapterFactory, AdapterRepositoryInput, DataAdapter, DrizzleAdapter, DrizzleAdapterOptions, FieldMetadata, MongooseAdapter, MongooseAdapterOptions, PrismaAdapter, PrismaAdapterOptions, PrismaQueryOptions, PrismaQueryParser, PrismaQueryParserOptions, RelationMetadata, RepositoryLike, SchemaMetadata, ValidationResult, asRepositoryLike, createDrizzleAdapter, createMongooseAdapter, createPrismaAdapter };
@@ -1,2 +1,2 @@
1
- import { a as createMongooseAdapter, i as MongooseAdapter, n as PrismaQueryParser, o as DrizzleAdapter, r as createPrismaAdapter, s as createDrizzleAdapter, t as PrismaAdapter } from "../adapters-D0tT2Tyo.mjs";
2
- export { DrizzleAdapter, MongooseAdapter, PrismaAdapter, PrismaQueryParser, createDrizzleAdapter, createMongooseAdapter, createPrismaAdapter };
1
+ import { a as createMongooseAdapter, c as createDrizzleAdapter, i as MongooseAdapter, n as PrismaQueryParser, o as asRepositoryLike, r as createPrismaAdapter, s as DrizzleAdapter, t as PrismaAdapter } from "../adapters-DUUiiimH.mjs";
2
+ export { DrizzleAdapter, MongooseAdapter, PrismaAdapter, PrismaQueryParser, asRepositoryLike, createDrizzleAdapter, createMongooseAdapter, createPrismaAdapter };
@@ -264,6 +264,21 @@ function createDrizzleAdapter(options) {
264
264
  return new DrizzleAdapter(options);
265
265
  }
266
266
  //#endregion
267
+ //#region src/adapters/interface.ts
268
+ /**
269
+ * Widen a permissive `AdapterRepositoryInput<TDoc>` to arc's strict
270
+ * `RepositoryLike<TDoc>` view. Single-source escape hatch for the
271
+ * filter-IR drift documented on `AdapterRepositoryInput`.
272
+ *
273
+ * Arc internals (audit / outbox / idempotency, BaseController) still see
274
+ * the IR-aware `RepositoryLike`; only the call paths arc exercises are
275
+ * shared between the two views, and those use the narrower
276
+ * `Record<string, unknown>` filter shape both sides agree on.
277
+ */
278
+ function asRepositoryLike(input) {
279
+ return input;
280
+ }
281
+ //#endregion
267
282
  //#region src/adapters/mongoose.ts
268
283
  /**
269
284
  * Mongoose data adapter with proper type safety
@@ -280,7 +295,7 @@ var MongooseAdapter = class {
280
295
  if (!isMongooseModel(options.model)) throw new TypeError("MongooseAdapter: Invalid model. Expected Mongoose Model instance.\nUsage: createMongooseAdapter({ model: YourModel, repository: yourRepo })");
281
296
  if (!isRepository(options.repository)) throw new TypeError("MongooseAdapter: Invalid repository. Expected StandardRepo instance.\nUsage: createMongooseAdapter({ model: YourModel, repository: yourRepo })");
282
297
  this.model = options.model;
283
- this.repository = options.repository;
298
+ this.repository = asRepositoryLike(options.repository);
284
299
  this.schemaGenerator = options.schemaGenerator;
285
300
  this.name = `MongooseAdapter<${options.model.modelName}>`;
286
301
  }
@@ -946,4 +961,4 @@ function createPrismaAdapter(options) {
946
961
  return new PrismaAdapter(options);
947
962
  }
948
963
  //#endregion
949
- export { createMongooseAdapter as a, MongooseAdapter as i, PrismaQueryParser as n, DrizzleAdapter as o, createPrismaAdapter as r, createDrizzleAdapter as s, PrismaAdapter as t };
964
+ export { createMongooseAdapter as a, createDrizzleAdapter as c, MongooseAdapter as i, PrismaQueryParser as n, asRepositoryLike as o, createPrismaAdapter as r, DrizzleAdapter as s, PrismaAdapter as t };
@@ -1,5 +1,5 @@
1
- import { jn as RepositoryLike } from "../index-6u4_Gg6G.mjs";
2
- import { d as UserBase } from "../fields-C8Y0XLAu.mjs";
1
+ import { Mn as RepositoryLike } from "../index-CXXRbnf8.mjs";
2
+ import { d as UserBase } from "../fields-BRjxOAFp.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
 
5
5
  //#region src/audit/stores/interface.d.ts
@@ -1,7 +1,7 @@
1
- import { Ot as AuthHelpers, kt as AuthPluginOptions } from "../index-6u4_Gg6G.mjs";
2
- import { c as PermissionCheck } from "../fields-C8Y0XLAu.mjs";
3
- import { t as ExternalOpenApiPaths } from "../externalPaths-Bapitwvd.mjs";
4
- import { a as SessionManagerOptions, c as createSessionManager, i as SessionData, n as MemorySessionStoreOptions, o as SessionManagerResult, r as SessionCookieOptions, s as SessionStore, t as MemorySessionStore } from "../sessionManager-D-oNWHz3.mjs";
1
+ import { Ot as AuthHelpers, kt as AuthPluginOptions } from "../index-CXXRbnf8.mjs";
2
+ import { c as PermissionCheck } from "../fields-BRjxOAFp.mjs";
3
+ import { t as ExternalOpenApiPaths } from "../externalPaths-BD5nw6St.mjs";
4
+ import { a as SessionManagerOptions, c as createSessionManager, i as SessionData, n as MemorySessionStoreOptions, o as SessionManagerResult, r as SessionCookieOptions, s as SessionStore, t as MemorySessionStore } from "../sessionManager-C4Le_UB3.mjs";
5
5
  import { FastifyPluginAsync, FastifyReply as FastifyReply$1, FastifyRequest } from "fastify";
6
6
 
7
7
  //#region src/auth/authPlugin.d.ts
@@ -1,4 +1,4 @@
1
- import { _ as requireTeamMembership, m as requireOrgRole, p as requireOrgMembership } from "../permissions-B4vU9L0Q.mjs";
1
+ import { _ as requireTeamMembership, m as requireOrgRole, p as requireOrgMembership } from "../permissions-gd_aUWrR.mjs";
2
2
  import { n as normalizeRoles, t as getUserRoles } from "../types-DV9WDfeg.mjs";
3
3
  import { t as ArcError } from "../errors-D5c-5BJL.mjs";
4
4
  import { n as extractBetterAuthOpenApi } from "../betterAuthOpenApi-DwxtK3uG.mjs";
@@ -1,4 +1,4 @@
1
- import { i as SessionData, s as SessionStore } from "../sessionManager-D-oNWHz3.mjs";
1
+ import { i as SessionData, s as SessionStore } from "../sessionManager-C4Le_UB3.mjs";
2
2
 
3
3
  //#region src/auth/redis-session.d.ts
4
4
  /** Minimal Redis client interface — compatible with ioredis */
@@ -1,6 +1,6 @@
1
- import { n as CacheStats, r as CacheStore, t as CacheLogger } from "../interface-Da0r7Lna.mjs";
2
- import { a as QueryCacheConfig, i as QueryCache, n as CacheResult, r as CacheStatus, t as CacheEnvelope } from "../QueryCache-DOBNHBE0.mjs";
3
- import { i as queryCachePlugin, n as QueryCacheDefaults, r as QueryCachePluginOptions, t as CrossResourceRule } from "../queryCachePlugin-BUXBSm4F.mjs";
1
+ import { n as CacheStats, r as CacheStore, t as CacheLogger } from "../interface-beEtJyWM.mjs";
2
+ import { a as QueryCacheConfig, i as QueryCache, n as CacheResult, r as CacheStatus, t as CacheEnvelope } from "../QueryCache-D41bfdBB.mjs";
3
+ import { i as queryCachePlugin, n as QueryCacheDefaults, r as QueryCachePluginOptions, t as CrossResourceRule } from "../queryCachePlugin-CqMdLI2-.mjs";
4
4
 
5
5
  //#region src/cache/keys.d.ts
6
6
  /**
@@ -1,5 +1,5 @@
1
1
  import { t as ResourceRegistry } from "../../ResourceRegistry-DkAeAuTX.mjs";
2
- import { t as buildOpenApiSpec } from "../../openapi-C0L9ar7m.mjs";
2
+ import { t as buildOpenApiSpec } from "../../openapi-D7G1V7ex.mjs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { mkdirSync, writeFileSync } from "node:fs";
@@ -6,8 +6,6 @@
6
6
  * - src/resources/product/product.model.ts
7
7
  * - src/resources/product/product.repository.ts
8
8
  * - src/resources/product/product.resource.ts
9
- * - src/resources/product/product.controller.ts
10
- * - src/resources/product/product.schemas.ts
11
9
  *
12
10
  * Handles kebab-case names: `arc g r org-profile` generates:
13
11
  * - Class names: OrgProfile, OrgProfileRepository
@@ -1,4 +1,4 @@
1
- import { t as pluralize } from "../../pluralize-BneOJkpi.mjs";
1
+ import { t as pluralize } from "../../pluralize-CWP6MB39.mjs";
2
2
  import { join } from "node:path";
3
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  //#region src/cli/commands/generate.ts
@@ -9,8 +9,6 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
9
  * - src/resources/product/product.model.ts
10
10
  * - src/resources/product/product.repository.ts
11
11
  * - src/resources/product/product.resource.ts
12
- * - src/resources/product/product.controller.ts
13
- * - src/resources/product/product.schemas.ts
14
12
  *
15
13
  * Handles kebab-case names: `arc g r org-profile` generates:
16
14
  * - Class names: OrgProfile, OrgProfileRepository
@@ -58,7 +56,7 @@ ${ts ? `
58
56
  export interface I${name} {
59
57
  name: string;
60
58
  description?: string;
61
- isActive: boolean;
59
+ ${isMultiTenant ? "organizationId: string;\n " : ""}isActive: boolean;
62
60
  }
63
61
 
64
62
  export type ${name}Document = HydratedDocument<I${name}>;
@@ -67,14 +65,14 @@ const ${camel}Schema = new Schema${ts ? `<I${name}>` : ""}(
67
65
  {
68
66
  name: { type: String, required: true, trim: true },
69
67
  description: { type: String, trim: true },
70
- isActive: { type: Boolean, default: true },
68
+ ${isMultiTenant ? "organizationId: { type: String, required: true, index: true },\n " : ""}isActive: { type: Boolean, default: true },
71
69
  },
72
70
  { timestamps: true }
73
71
  );
74
72
 
75
73
  // Indexes
76
74
  ${camel}Schema.index({ name: 1 });
77
- ${camel}Schema.index({ isActive: 1 });
75
+ ${isMultiTenant ? `${camel}Schema.index({ organizationId: 1, isActive: 1 });\n` : ""}${camel}Schema.index({ isActive: 1 });
78
76
 
79
77
  const ${name} = mongoose.models.${name} || mongoose.model('${name}', ${camel}Schema);
80
78
  export default ${name};
@@ -193,20 +191,21 @@ export default crudSchemas;
193
191
  */
194
192
 
195
193
  import { defineResource, createMongooseAdapter } from '@classytic/arc';
196
- import { requireAuth, requireRoles } from '@classytic/arc/permissions';
194
+ import { allOf, requireOrgMembership, requireRoles } from '@classytic/arc/permissions';
195
+ import { multiTenantPreset } from '@classytic/arc/presets';
197
196
  import ${name}${ts ? `, { type I${name} }` : ""} from './${fileName}.model.js';
198
197
  import ${camel}Repository from './${fileName}.repository.js';${queryParserImport}
199
198
 
200
199
  const ${camel}Resource = defineResource${ts ? `<I${name}>` : ""}({
201
200
  name: '${fileName}',
202
201
  adapter: createMongooseAdapter({ model: ${name}, repository: ${camel}Repository }),${queryParserConfig}
203
- presets: ['softDelete'],
202
+ presets: ['softDelete', multiTenantPreset({ tenantField: 'organizationId' })],
204
203
  permissions: {
205
- list: requireAuth(),
206
- get: requireAuth(),
207
- create: requireRoles(['admin']),
208
- update: requireRoles(['admin']),
209
- delete: requireRoles(['admin']),
204
+ list: requireOrgMembership(),
205
+ get: requireOrgMembership(),
206
+ create: allOf(requireOrgMembership(), requireRoles(['admin'])),
207
+ update: allOf(requireOrgMembership(), requireRoles(['admin'])),
208
+ delete: allOf(requireOrgMembership(), requireRoles(['admin'])),
210
209
  },
211
210
  });
212
211
 
@@ -367,7 +366,7 @@ async function generate(type, args) {
367
366
  }
368
367
  }
369
368
  /**
370
- * Generate a full resource
369
+ * Generate a resource-first scaffold
371
370
  */
372
371
  async function generateResource(name, lowerName, resourcePath, templates, ext, includeMcp = false) {
373
372
  console.log(`\nGenerating resource: ${name}...\n`);
@@ -417,8 +416,9 @@ Next steps:
417
416
  src/resources/${lowerName}/${lowerName}.model.${ext}
418
417
 
419
418
  3. Adjust permissions in ${lowerName}.resource.${ext}:
420
- ${isMultiTenant ? ` - requireAuth() any authenticated user
421
- - requireRoles(['admin']) specific platform roles` : ` - requireAuth() → any authenticated user
419
+ ${isMultiTenant ? ` - requireOrgMembership() member of the current organization
420
+ - multiTenantPreset() injects and filters organizationId
421
+ - requireRoles(['admin']) → admin writes inside the org scope` : ` - requireAuth() → any authenticated user
422
422
  - requireRoles(['admin']) → specific platform roles`}
423
423
 
424
424
  4. Run tests: