@classytic/arc 2.3.0 → 2.4.2

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 (175) hide show
  1. package/README.md +187 -18
  2. package/bin/arc.js +11 -3
  3. package/dist/BaseController-CkM5dUh_.mjs +1031 -0
  4. package/dist/{EventTransport-BkUDYZEb.d.mts → EventTransport-wc5hSLik.d.mts} +1 -1
  5. package/dist/{HookSystem-BsGV-j2l.mjs → HookSystem-COkyWztM.mjs} +2 -3
  6. package/dist/{ResourceRegistry-7Ic20ZMw.mjs → ResourceRegistry-DeCIFlix.mjs} +8 -5
  7. package/dist/adapters/index.d.mts +3 -5
  8. package/dist/adapters/index.mjs +2 -3
  9. package/dist/{prisma-DJbMt3yf.mjs → adapters-DTC4Ug66.mjs} +45 -12
  10. package/dist/audit/index.d.mts +4 -7
  11. package/dist/audit/index.mjs +2 -29
  12. package/dist/audit/mongodb.d.mts +1 -4
  13. package/dist/audit/mongodb.mjs +2 -3
  14. package/dist/auth/index.d.mts +7 -9
  15. package/dist/auth/index.mjs +65 -63
  16. package/dist/auth/redis-session.d.mts +1 -1
  17. package/dist/auth/redis-session.mjs +1 -2
  18. package/dist/{betterAuthOpenApi-DjWDddNc.mjs → betterAuthOpenApi-lz0IRbXJ.mjs} +4 -6
  19. package/dist/cache/index.d.mts +23 -23
  20. package/dist/cache/index.mjs +4 -6
  21. package/dist/{caching-GSDJcA6-.mjs → caching-BSXB-Xr7.mjs} +2 -24
  22. package/dist/chunk-BpYLSNr0.mjs +14 -0
  23. package/dist/circuitBreaker-BOBOpN2w.mjs +284 -0
  24. package/dist/circuitBreaker-JP2GdJ4b.d.mts +206 -0
  25. package/dist/cli/commands/describe.mjs +24 -7
  26. package/dist/cli/commands/docs.mjs +6 -7
  27. package/dist/cli/commands/doctor.d.mts +10 -0
  28. package/dist/cli/commands/doctor.mjs +156 -0
  29. package/dist/cli/commands/generate.mjs +66 -17
  30. package/dist/cli/commands/init.mjs +315 -45
  31. package/dist/cli/commands/introspect.mjs +2 -4
  32. package/dist/cli/index.d.mts +1 -10
  33. package/dist/cli/index.mjs +4 -153
  34. package/dist/{constants-DdXFXQtN.mjs → constants-Cxde4rpC.mjs} +1 -2
  35. package/dist/core/index.d.mts +3 -5
  36. package/dist/core/index.mjs +5 -4
  37. package/dist/core-C1XCMtqM.mjs +185 -0
  38. package/dist/{createApp-CgKOPhA4.mjs → createApp-ByWNRsZj.mjs} +64 -35
  39. package/dist/{defineResource-DWbpJYtm.mjs → defineResource-D9aY5Cy6.mjs} +108 -1157
  40. package/dist/discovery/index.mjs +37 -5
  41. package/dist/docs/index.d.mts +6 -9
  42. package/dist/docs/index.mjs +3 -21
  43. package/dist/dynamic/index.d.mts +93 -0
  44. package/dist/dynamic/index.mjs +122 -0
  45. package/dist/{elevation-DSTbVvYj.mjs → elevation-BEdACOLB.mjs} +5 -36
  46. package/dist/{elevation-DGo5shaX.d.mts → elevation-Ca_yveIO.d.mts} +41 -7
  47. package/dist/{errorHandler-C3GY3_ow.mjs → errorHandler--zp54tGc.mjs} +3 -5
  48. package/dist/errorHandler-Do4vVQ1f.d.mts +139 -0
  49. package/dist/{errors-DBANPbGr.mjs → errors-rxhfP7Hf.mjs} +1 -2
  50. package/dist/{eventPlugin-BEOvaDqo.mjs → eventPlugin-Ba00swHF.mjs} +25 -27
  51. package/dist/{eventPlugin-H6wDDjGO.d.mts → eventPlugin-iGrSEmwJ.d.mts} +105 -5
  52. package/dist/events/index.d.mts +72 -7
  53. package/dist/events/index.mjs +216 -4
  54. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  55. package/dist/events/transports/redis-stream-entry.mjs +19 -7
  56. package/dist/events/transports/redis.d.mts +1 -1
  57. package/dist/events/transports/redis.mjs +3 -4
  58. package/dist/factory/index.d.mts +23 -9
  59. package/dist/factory/index.mjs +48 -3
  60. package/dist/{fields-Bi_AVKSo.d.mts → fields-DFwdaWCq.d.mts} +1 -1
  61. package/dist/{fields-CTd_CrKr.mjs → fields-ipsbIRPK.mjs} +1 -2
  62. package/dist/hooks/index.d.mts +1 -3
  63. package/dist/hooks/index.mjs +2 -3
  64. package/dist/idempotency/index.d.mts +5 -5
  65. package/dist/idempotency/index.mjs +3 -7
  66. package/dist/idempotency/mongodb.d.mts +1 -1
  67. package/dist/idempotency/mongodb.mjs +4 -5
  68. package/dist/idempotency/redis.d.mts +1 -1
  69. package/dist/idempotency/redis.mjs +2 -5
  70. package/dist/{fastifyAdapter-6b_eRDBw.d.mts → index-BL8CaQih.d.mts} +56 -57
  71. package/dist/index-Diqcm14c.d.mts +369 -0
  72. package/dist/{prisma-Dy5S5F5i.d.mts → index-yhxyjqNb.d.mts} +4 -5
  73. package/dist/index.d.mts +100 -105
  74. package/dist/index.mjs +85 -58
  75. package/dist/integrations/event-gateway.d.mts +1 -1
  76. package/dist/integrations/event-gateway.mjs +8 -4
  77. package/dist/integrations/index.d.mts +4 -2
  78. package/dist/integrations/index.mjs +1 -1
  79. package/dist/integrations/jobs.d.mts +2 -2
  80. package/dist/integrations/jobs.mjs +63 -14
  81. package/dist/integrations/mcp/index.d.mts +219 -0
  82. package/dist/integrations/mcp/index.mjs +572 -0
  83. package/dist/integrations/mcp/testing.d.mts +53 -0
  84. package/dist/integrations/mcp/testing.mjs +104 -0
  85. package/dist/integrations/streamline.mjs +39 -19
  86. package/dist/integrations/webhooks.d.mts +56 -0
  87. package/dist/integrations/webhooks.mjs +139 -0
  88. package/dist/integrations/websocket-redis.d.mts +46 -0
  89. package/dist/integrations/websocket-redis.mjs +50 -0
  90. package/dist/integrations/websocket.d.mts +68 -2
  91. package/dist/integrations/websocket.mjs +96 -13
  92. package/dist/{interface-CSNjltAc.d.mts → interface-B4awm1RJ.d.mts} +2 -2
  93. package/dist/interface-DGmPxakH.d.mts +2213 -0
  94. package/dist/{keys-DhqDRxv3.mjs → keys-qcD-TVJl.mjs} +3 -4
  95. package/dist/{logger-ByrvQWZO.mjs → logger-Dz3j1ItV.mjs} +2 -4
  96. package/dist/{memory-B2v7KrCB.mjs → memory-Cb_7iy9e.mjs} +2 -4
  97. package/dist/metrics-Csh4nsvv.mjs +224 -0
  98. package/dist/migrations/index.d.mts +113 -44
  99. package/dist/migrations/index.mjs +84 -102
  100. package/dist/{mongodb-DNKEExbf.mjs → mongodb-BuQ7fNTg.mjs} +1 -4
  101. package/dist/{mongodb-ClykrfGo.d.mts → mongodb-CUpYfxfD.d.mts} +2 -3
  102. package/dist/{mongodb-Dg8O_gvd.d.mts → mongodb-bga9AbkD.d.mts} +2 -2
  103. package/dist/{openapi-9nB_kiuR.mjs → openapi-CBmZ6EQN.mjs} +4 -21
  104. package/dist/org/index.d.mts +12 -14
  105. package/dist/org/index.mjs +92 -119
  106. package/dist/org/types.d.mts +2 -2
  107. package/dist/org/types.mjs +1 -1
  108. package/dist/permissions/index.d.mts +4 -278
  109. package/dist/permissions/index.mjs +4 -579
  110. package/dist/permissions-CA5zg0yK.mjs +751 -0
  111. package/dist/plugins/index.d.mts +104 -107
  112. package/dist/plugins/index.mjs +203 -313
  113. package/dist/plugins/response-cache.mjs +4 -69
  114. package/dist/plugins/tracing-entry.d.mts +1 -1
  115. package/dist/plugins/tracing-entry.mjs +24 -11
  116. package/dist/{pluralize-CM-jZg7p.mjs → pluralize-CcT6qF0a.mjs} +12 -13
  117. package/dist/policies/index.d.mts +2 -2
  118. package/dist/policies/index.mjs +80 -83
  119. package/dist/presets/index.d.mts +26 -19
  120. package/dist/presets/index.mjs +2 -142
  121. package/dist/presets/multiTenant.d.mts +1 -4
  122. package/dist/presets/multiTenant.mjs +4 -6
  123. package/dist/presets-C9QXJV1u.mjs +422 -0
  124. package/dist/{queryCachePlugin-B6R0d4av.mjs → queryCachePlugin-ClosZdNS.mjs} +6 -27
  125. package/dist/{queryCachePlugin-Q6SYuHZ6.d.mts → queryCachePlugin-DcmETvcB.d.mts} +3 -3
  126. package/dist/queryParser-CgCtsjti.mjs +352 -0
  127. package/dist/{redis-UwjEp8Ea.d.mts → redis-CQ5YxMC5.d.mts} +2 -2
  128. package/dist/{redis-stream-CBg0upHI.d.mts → redis-stream-BW9UKLZM.d.mts} +9 -2
  129. package/dist/registry/index.d.mts +1 -4
  130. package/dist/registry/index.mjs +3 -4
  131. package/dist/{introspectionPlugin-B3JkrjwU.mjs → registry-I-ogLgL9.mjs} +1 -8
  132. package/dist/{requestContext-xi6OKBL-.mjs → requestContext-DYtmNpm5.mjs} +1 -3
  133. package/dist/resourceToTools-PMFE8HIv.mjs +533 -0
  134. package/dist/rpc/index.d.mts +90 -0
  135. package/dist/rpc/index.mjs +248 -0
  136. package/dist/{schemaConverter-Dtg0Kt9T.mjs → schemaConverter-DjzHpFam.mjs} +1 -2
  137. package/dist/schemas/index.d.mts +30 -30
  138. package/dist/schemas/index.mjs +2 -4
  139. package/dist/scope/index.d.mts +13 -2
  140. package/dist/scope/index.mjs +18 -5
  141. package/dist/{sessionManager-D_iEHjQl.d.mts → sessionManager-wbkYj2HL.d.mts} +2 -2
  142. package/dist/{sse-DkqQ1uxb.mjs → sse-BkViJPlT.mjs} +4 -25
  143. package/dist/testing/index.d.mts +551 -567
  144. package/dist/testing/index.mjs +1744 -1799
  145. package/dist/{tracing-8CEbhF0w.d.mts → tracing-bz_U4EM1.d.mts} +6 -1
  146. package/dist/{typeGuards-DwxA1t_L.mjs → typeGuards-Cj5Rgvlg.mjs} +1 -2
  147. package/dist/types/index.d.mts +4 -946
  148. package/dist/types/index.mjs +2 -4
  149. package/dist/types-BJmgxNbF.d.mts +275 -0
  150. package/dist/{types-RLkFVgaw.d.mts → types-BNUccdcf.d.mts} +2 -2
  151. package/dist/{types-Beqn1Un7.mjs → types-C6TQjtdi.mjs} +30 -2
  152. package/dist/{types-tKwaViYB.d.mts → types-Dt0-AI6E.d.mts} +68 -27
  153. package/dist/{types-DelU6kln.mjs → types-ZUu_h0jp.mjs} +1 -2
  154. package/dist/utils/index.d.mts +254 -351
  155. package/dist/utils/index.mjs +7 -6
  156. package/dist/utils-Dc0WhlIl.mjs +594 -0
  157. package/dist/versioning-BzfeHmhj.mjs +37 -0
  158. package/package.json +44 -10
  159. package/skills/arc/SKILL.md +518 -0
  160. package/skills/arc/references/auth.md +250 -0
  161. package/skills/arc/references/events.md +272 -0
  162. package/skills/arc/references/integrations.md +385 -0
  163. package/skills/arc/references/mcp.md +431 -0
  164. package/skills/arc/references/production.md +610 -0
  165. package/skills/arc/references/testing.md +183 -0
  166. package/dist/audited-CGdLiSlE.mjs +0 -140
  167. package/dist/chunk-C7Uep-_p.mjs +0 -20
  168. package/dist/circuitBreaker-CSS2VvL6.mjs +0 -1109
  169. package/dist/errorHandler-CW3OOeYq.d.mts +0 -72
  170. package/dist/interface-BtdYtQUA.d.mts +0 -1114
  171. package/dist/presets-BTeYbw7h.d.mts +0 -57
  172. package/dist/presets-CeFtfDR8.mjs +0 -119
  173. /package/dist/{errors-DAWRdiYP.d.mts → errors-CPpvPHT0.d.mts} +0 -0
  174. /package/dist/{externalPaths-SyPF2tgK.d.mts → externalPaths-DpO-s7r8.d.mts} +0 -0
  175. /package/dist/{interface-DTbsvIWe.d.mts → interface-D_BWALyZ.d.mts} +0 -0
@@ -0,0 +1,610 @@
1
+ # Arc Production Features
2
+
3
+ Health checks, audit trail, idempotency, tracing, SSE, caching, graceful shutdown.
4
+
5
+ ## Health Plugin
6
+
7
+ Kubernetes-ready liveness/readiness probes:
8
+
9
+ ```typescript
10
+ import { healthPlugin } from '@classytic/arc/plugins';
11
+
12
+ await fastify.register(healthPlugin, {
13
+ prefix: '/_health',
14
+ version: '1.0.0',
15
+ checks: [
16
+ { name: 'mongodb', check: () => mongoose.connection.readyState === 1, critical: true },
17
+ { name: 'redis', check: async () => await redis.ping() === 'PONG', timeout: 3000 },
18
+ ],
19
+ });
20
+
21
+ // GET /_health/live → { status: 'ok', timestamp, version }
22
+ // GET /_health/ready → { status: 'ok', checks: { mongodb: { status: 'ok', latency: 5 } } }
23
+ // Returns 503 if critical check fails
24
+ ```
25
+
26
+ ## Request ID Plugin
27
+
28
+ ```typescript
29
+ import { requestIdPlugin } from '@classytic/arc/plugins';
30
+
31
+ await fastify.register(requestIdPlugin, {
32
+ header: 'x-request-id',
33
+ setResponseHeader: true,
34
+ generator: () => crypto.randomUUID(),
35
+ });
36
+ ```
37
+
38
+ ## Graceful Shutdown Plugin
39
+
40
+ ```typescript
41
+ import { gracefulShutdownPlugin } from '@classytic/arc/plugins';
42
+
43
+ await fastify.register(gracefulShutdownPlugin, {
44
+ timeout: 30000,
45
+ signals: ['SIGTERM', 'SIGINT'],
46
+ logEvents: true,
47
+ onShutdown: async () => {
48
+ await mongoose.disconnect();
49
+ await redis.quit();
50
+ },
51
+ });
52
+
53
+ // Sequence: receive signal → stop accepting connections → wait for in-flight
54
+ // → run onShutdown → close Fastify → exit process
55
+ ```
56
+
57
+ ## Audit Plugin
58
+
59
+ Change tracking with pluggable storage:
60
+
61
+ ```typescript
62
+ import { auditPlugin } from '@classytic/arc/audit';
63
+
64
+ // Development
65
+ await fastify.register(auditPlugin, { enabled: true, stores: ['memory'] });
66
+
67
+ // Production
68
+ await fastify.register(auditPlugin, {
69
+ enabled: true,
70
+ stores: ['mongodb'],
71
+ mongoConnection: mongoose.connection,
72
+ mongoCollection: 'audit_logs',
73
+ ttlDays: 90, // Auto-cleanup via TTL index
74
+ });
75
+
76
+ // Usage
77
+ await fastify.audit.create('product', product._id, product, request.auditContext);
78
+ await fastify.audit.update('product', id, beforeDoc, afterDoc, request.auditContext);
79
+ await fastify.audit.delete('product', id, deletedDoc, request.auditContext);
80
+ await fastify.audit.custom('product', id, 'price_changed', { oldPrice: 100, newPrice: 150 }, ctx);
81
+
82
+ // Query
83
+ const entries = await fastify.audit.query({
84
+ resource: 'product', documentId: 'prod-123',
85
+ action: 'update', from: new Date('2024-01-01'), limit: 100,
86
+ });
87
+ ```
88
+
89
+ **Audit entry:**
90
+
91
+ ```typescript
92
+ interface AuditEntry {
93
+ id: string; timestamp: Date;
94
+ action: 'create' | 'update' | 'delete' | 'restore' | 'custom';
95
+ resource: string; documentId: string;
96
+ before?: Record<string, unknown>;
97
+ after?: Record<string, unknown>;
98
+ metadata?: Record<string, unknown>;
99
+ context: { user?; organizationId?; requestId?; ipAddress?; userAgent?; };
100
+ }
101
+ ```
102
+
103
+ **Custom store:** Implement `AuditStore` interface (`log`, `query`, `close`).
104
+
105
+ ## Idempotency Plugin
106
+
107
+ Exactly-once semantics for mutating operations:
108
+
109
+ ```typescript
110
+ import { idempotencyPlugin } from '@classytic/arc/idempotency';
111
+
112
+ await fastify.register(idempotencyPlugin, {
113
+ enabled: true,
114
+ headerName: 'idempotency-key', // Default
115
+ ttlMs: 86400000, // 24 hours
116
+ lockTimeoutMs: 30000,
117
+ methods: ['POST', 'PUT', 'PATCH'],
118
+ include: [/\/orders/], // Only these routes
119
+ exclude: [/\/health/],
120
+ retryAfterSeconds: 1,
121
+ });
122
+ ```
123
+
124
+ **Client:**
125
+
126
+ ```typescript
127
+ fetch('/api/orders', {
128
+ method: 'POST',
129
+ headers: { 'Idempotency-Key': 'order-abc123-uuid' },
130
+ body: JSON.stringify({ items: [...] }),
131
+ });
132
+ // First request: processes, caches response
133
+ // Retry: returns cached response + x-idempotency-replayed: true
134
+ // Concurrent: returns 409 Conflict + Retry-After: 1
135
+ ```
136
+
137
+ **Storage backends:**
138
+
139
+ ```typescript
140
+ // Memory (default, dev)
141
+ import { MemoryIdempotencyStore } from '@classytic/arc/idempotency';
142
+
143
+ // Redis (production, multi-instance)
144
+ import { RedisIdempotencyStore } from '@classytic/arc/idempotency/redis';
145
+ store: new RedisIdempotencyStore({ client: redis, prefix: 'idem:', ttlMs: 86400000 })
146
+
147
+ // MongoDB (production, no Redis)
148
+ import { MongoIdempotencyStore } from '@classytic/arc/idempotency/mongodb';
149
+ store: new MongoIdempotencyStore({ connection: mongoose.connection, collection: 'arc_idempotency', createIndex: true })
150
+ ```
151
+
152
+ **IdempotencyStore interface:**
153
+
154
+ ```typescript
155
+ interface IdempotencyStore {
156
+ readonly name: string;
157
+ get(key: string): Promise<IdempotencyResult | undefined>;
158
+ set(key: string, result): Promise<void>;
159
+ tryLock(key: string, requestId: string, ttlMs: number): Promise<boolean>;
160
+ unlock(key: string, requestId: string): Promise<void>;
161
+ isLocked(key: string): Promise<boolean>;
162
+ delete(key: string): Promise<void>;
163
+ close?(): Promise<void>;
164
+ }
165
+ ```
166
+
167
+ ## OpenTelemetry Tracing
168
+
169
+ ```typescript
170
+ import { tracingPlugin } from '@classytic/arc/plugins/tracing';
171
+
172
+ await fastify.register(tracingPlugin, {
173
+ serviceName: 'my-api',
174
+ exporterUrl: 'http://localhost:4318/v1/traces',
175
+ sampleRate: 0.1, // Trace 10% of requests
176
+ });
177
+
178
+ // Custom spans
179
+ import { createSpan } from '@classytic/arc/plugins/tracing';
180
+
181
+ return createSpan(req, 'processPayment', async (span) => {
182
+ span.setAttribute('orderId', order._id);
183
+ return await processPayment(order);
184
+ });
185
+ ```
186
+
187
+ ## QueryCache (Server Cache)
188
+
189
+ TanStack Query-inspired server cache with stale-while-revalidate and auto-invalidation on mutations.
190
+
191
+ ```typescript
192
+ // Enable globally
193
+ const app = await createApp({
194
+ arcPlugins: { queryCache: true }, // Memory store, zero config
195
+ });
196
+
197
+ // Per-resource config
198
+ defineResource({
199
+ name: 'product',
200
+ cache: {
201
+ staleTime: 30, // seconds fresh (no revalidation)
202
+ gcTime: 300, // seconds stale data kept (SWR window)
203
+ tags: ['catalog'], // cross-resource grouping
204
+ invalidateOn: { 'category.*': ['catalog'] }, // event → tag invalidation
205
+ },
206
+ });
207
+ ```
208
+
209
+ **How it works:**
210
+ - `GET` → cached with `x-cache: HIT | STALE | MISS` header
211
+ - `POST/PATCH/DELETE` → auto-bumps resource version, invalidating cached queries
212
+ - Cross-resource: category mutation bumps `catalog` tag → products cache invalidated
213
+ - Multi-tenant safe: cache keys scoped by userId + orgId
214
+
215
+ **Runtime modes:**
216
+
217
+ | Mode | Store | Config |
218
+ |------|-------|--------|
219
+ | `memory` (default) | `MemoryCacheStore` (50 MiB budget) | Zero config |
220
+ | `distributed` | `RedisCacheStore` | `stores: { queryCache: new RedisCacheStore({ client: redis }) }` |
221
+
222
+ ## Response Cache Plugin
223
+
224
+ ```typescript
225
+ import { responseCachePlugin } from '@classytic/arc/plugins/response-cache';
226
+
227
+ await fastify.register(responseCachePlugin, {
228
+ // ETag generation + Cache-Control headers per route
229
+ });
230
+ ```
231
+
232
+ **Note:** When QueryCache is active for a resource, response-cache is automatically skipped for that resource's GET routes.
233
+
234
+ ## SSE Plugin (Server-Sent Events)
235
+
236
+ Bridges Arc domain events to SSE streams. Requires `eventPlugin` (auto-registered by factory).
237
+
238
+ ```typescript
239
+ // Via factory (recommended)
240
+ const app = await createApp({
241
+ arcPlugins: {
242
+ sse: {
243
+ path: '/events/stream', // SSE endpoint (default: '/events/stream')
244
+ requireAuth: true, // Fail-closed auth (default: true)
245
+ patterns: ['order.*', 'product.*'], // Event patterns to stream (default: ['*'])
246
+ orgScoped: false, // Filter events by org from request.scope (default: false)
247
+ heartbeat: 30000, // Heartbeat interval in ms (default: 30000)
248
+ },
249
+ },
250
+ });
251
+
252
+ // Manual registration
253
+ import { ssePlugin } from '@classytic/arc/plugins';
254
+ await fastify.register(ssePlugin, { requireAuth: true, orgScoped: true });
255
+ ```
256
+
257
+ **Fail-closed auth:** When `requireAuth: true` (default), throws at registration if `fastify.authenticate` is missing — prevents exposing SSE without auth.
258
+
259
+ **Org-scoped:** When `orgScoped: true`, events with `organizationId` are only sent to clients whose `request.scope` matches. Prevents cross-tenant leakage.
260
+
261
+ ## Error Handler Plugin
262
+
263
+ ```typescript
264
+ import { errorHandlerPlugin } from '@classytic/arc/plugins';
265
+
266
+ // Global error handling — catches all errors, logs, formats response
267
+ // Auto-registered by createApp()
268
+ ```
269
+
270
+ ## Migrations
271
+
272
+ ```typescript
273
+ import { defineMigration, MigrationRunner } from '@classytic/arc/migrations';
274
+
275
+ const v2 = defineMigration({
276
+ version: 2,
277
+ resource: 'product',
278
+ up: async (db) => {
279
+ await db.collection('products').updateMany({}, { $rename: { oldField: 'newField' } });
280
+ },
281
+ down: async (db) => {
282
+ await db.collection('products').updateMany({}, { $rename: { newField: 'oldField' } });
283
+ },
284
+ });
285
+
286
+ const runner = new MigrationRunner(mongoose.connection.db);
287
+ await runner.up([v2]);
288
+ ```
289
+
290
+ ## Policies
291
+
292
+ Query-level authorization — modify queries based on user:
293
+
294
+ ```typescript
295
+ import { createAccessControlPolicy } from '@classytic/arc/policies';
296
+
297
+ const editorPolicy = createAccessControlPolicy({
298
+ statements: [
299
+ { resource: 'product', action: ['create', 'update'] },
300
+ { resource: 'order', action: ['read'] },
301
+ ],
302
+ });
303
+
304
+ defineResource({
305
+ permissions: {
306
+ create: editorPolicy,
307
+ update: editorPolicy,
308
+ },
309
+ });
310
+ ```
311
+
312
+ ## OpenAPI & External Paths
313
+
314
+ Arc auto-generates OpenAPI 3.0 specs from resource definitions. External integrations (auth adapters, custom routes) inject their paths via `ExternalOpenApiPaths`.
315
+
316
+ ```typescript
317
+ import type { ExternalOpenApiPaths } from '@classytic/arc/docs';
318
+
319
+ const externalPaths: ExternalOpenApiPaths = {
320
+ paths: { '/api/auth/sign-in': { post: { summary: 'Sign in', ... } } },
321
+ schemas: { User: { type: 'object', properties: { ... } } },
322
+ securitySchemes: {
323
+ cookieAuth: { type: 'apiKey', in: 'cookie', name: 'session_token' },
324
+ },
325
+ tags: [{ name: 'Authentication' }],
326
+ // Declare additional security alternatives for Arc resource paths
327
+ resourceSecurity: [{ apiKeyAuth: [], orgHeader: [] }],
328
+ };
329
+ ```
330
+
331
+ **`resourceSecurity`** — declarative registration of auth alternatives for resource paths:
332
+ - Each array item is **OR**'d with `bearerAuth` (the default)
333
+ - Keys within the same object are **AND**'d (all required together)
334
+ - Example: `[{ apiKeyAuth: [], orgHeader: [] }]` → "bearer OR (api-key AND org-header)"
335
+
336
+ Arc's Better Auth adapter (`extractBetterAuthOpenApi`) auto-populates `resourceSecurity` when the `apiKey()` plugin is detected — no manual configuration needed.
337
+
338
+ **Security scheme definitions:**
339
+
340
+ | Scheme | Type | Source | Always present? |
341
+ |--------|------|--------|-----------------|
342
+ | `bearerAuth` | HTTP Bearer | Arc core | Yes |
343
+ | `orgHeader` | API Key (`x-organization-id`) | Arc core | Yes (multi-tenant) |
344
+ | `cookieAuth` | API Key (cookie) | Better Auth adapter | When Better Auth active |
345
+ | `apiKeyAuth` | API Key (`x-api-key`) | Better Auth adapter | When `apiKey()` plugin active |
346
+
347
+ ## Deployment
348
+
349
+ Arc requires Node.js APIs (`node:crypto`, `AsyncLocalStorage`). Use `toFetchHandler()` for serverless/edge.
350
+
351
+ | Environment | Preset | Handler | Notes |
352
+ |-------------|--------|---------|-------|
353
+ | Docker/K8s | `production` | `app.listen()` | Full production |
354
+ | Google Cloud Run | `production` | `app.listen()` | Set min-instances > 0 for WebSocket |
355
+ | Railway/Render/Fly.io | `production` | `app.listen()` | Works with zero config |
356
+ | AWS Lambda | `edge` | `toFetchHandler()` | Node.js runtime |
357
+ | Vercel Serverless | `edge` | `toFetchHandler()` | Node.js runtime |
358
+ | Cloudflare Workers | `edge` | `toFetchHandler()` | Enable `nodejs_compat` in wrangler.toml |
359
+
360
+ **Edge handler:**
361
+ ```typescript
362
+ import { createApp, toFetchHandler } from '@classytic/arc/factory';
363
+ const app = await createApp({ preset: 'edge', auth: { type: 'jwt', jwt: { secret } } });
364
+ export default { fetch: toFetchHandler(app) }; // Cloudflare Workers / any fetch-based runtime
365
+ ```
366
+
367
+ **Production checklist:**
368
+
369
+ ```typescript
370
+ // Validate env vars at startup
371
+ if (!process.env.JWT_SECRET || process.env.JWT_SECRET.length < 32) throw new Error('...');
372
+
373
+ const app = await createApp({
374
+ preset: 'production',
375
+ auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
376
+ cors: { origin: process.env.ALLOWED_ORIGINS?.split(',') ?? [], credentials: true },
377
+ rateLimit: { max: 100, timeWindow: '1 minute' },
378
+ arcPlugins: { queryCache: true },
379
+ });
380
+
381
+ process.on('SIGTERM', () => app.close());
382
+ process.on('SIGINT', () => app.close());
383
+ ```
384
+
385
+ ## Distributed Runtime
386
+
387
+ `runtime: 'distributed'` only validates stores you actually enable:
388
+
389
+ - `stores.events` — always required
390
+ - `stores.cache` — only when `arcPlugins.caching` enabled
391
+ - `stores.queryCache` — only when `arcPlugins.queryCache` enabled
392
+ - `stores.idempotency` — never validated (per-resource opt-in)
393
+
394
+ ## Under-Pressure
395
+
396
+ Production preset: `maxEventLoopDelay: 3000` (avoids false 503s on Render/Railway/Fly.io). Override: `underPressure: { maxEventLoopDelay: 500 }`. Disable: `underPressure: false`.
397
+
398
+ ## CORS
399
+
400
+ Production warns (not throws) when origin missing. `origin: '*'` from env is allowed. Smart CORS auto-converts `credentials: true` + `origin: '*'` to `origin: true`.
401
+
402
+ ## Metrics Plugin
403
+
404
+ Prometheus-compatible metrics endpoint — zero external dependencies:
405
+
406
+ ```typescript
407
+ import { metricsPlugin } from '@classytic/arc/plugins';
408
+
409
+ await fastify.register(metricsPlugin, {
410
+ path: '/_metrics', // default
411
+ prefix: 'arc', // metric name prefix (default: 'arc')
412
+ onCollect: (metrics) => pushToOTLP(metrics), // optional OTLP push
413
+ });
414
+
415
+ // GET /_metrics → Prometheus text format
416
+ ```
417
+
418
+ **Built-in counters**: `arc_http_requests_total`, `arc_http_request_duration_seconds`, `arc_crud_operations_total`, `arc_cache_hits_total`, `arc_cache_misses_total`, `arc_events_published_total`, `arc_events_consumed_total`, `arc_circuit_breaker_state`.
419
+
420
+ **Programmatic access**: `fastify.metrics.recordOperation(resource, op, status, durationMs)`, `.recordCacheHit(resource)`, `.recordEventPublish(type)`, `.reset()`.
421
+
422
+ ## API Versioning Plugin
423
+
424
+ Header-based or URL prefix-based versioning with deprecation warnings:
425
+
426
+ ```typescript
427
+ import { versioningPlugin } from '@classytic/arc/plugins';
428
+
429
+ // Header-based: clients send Accept-Version: 2
430
+ await fastify.register(versioningPlugin, {
431
+ type: 'header',
432
+ deprecated: ['1'],
433
+ sunset: '2025-12-01',
434
+ });
435
+
436
+ // Prefix-based: /v2/products
437
+ await fastify.register(versioningPlugin, { type: 'prefix' });
438
+ ```
439
+
440
+ **Headers set**: `x-api-version` on every response. `Deprecation: true` + `Sunset` for deprecated versions. Access via `request.apiVersion`.
441
+
442
+ ## Per-Tenant Rate Limiting
443
+
444
+ Scope-aware rate limit key generator — isolates rate limits by org, user, or IP:
445
+
446
+ ```typescript
447
+ import { createTenantKeyGenerator } from '@classytic/arc/scope';
448
+
449
+ const app = await createApp({
450
+ rateLimit: {
451
+ max: 100,
452
+ timeWindow: '1 minute',
453
+ keyGenerator: createTenantKeyGenerator(),
454
+ },
455
+ });
456
+ ```
457
+
458
+ **Key resolution**: `member` → `organizationId`, `authenticated` → `userId`, `elevated` → `organizationId ?? userId`, `public` → IP. Custom strategy: `createTenantKeyGenerator({ strategy: (ctx) => ctx.ip })`.
459
+
460
+ ## Event Outbox
461
+
462
+ Transactional outbox pattern — at-least-once delivery even if transport is down:
463
+
464
+ ```typescript
465
+ import { EventOutbox, MemoryOutboxStore } from '@classytic/arc/events';
466
+
467
+ const outbox = new EventOutbox({
468
+ store: new MemoryOutboxStore(), // or MongoOutboxStore for production
469
+ transport: redisTransport,
470
+ });
471
+
472
+ // In business logic (same DB transaction)
473
+ await outbox.store(event);
474
+
475
+ // Relay cron (runs every few seconds)
476
+ const relayed = await outbox.relay(); // publishes pending → transport
477
+ ```
478
+
479
+ **OutboxStore interface**: `save(event)`, `getPending(limit)`, `acknowledge(eventId)`.
480
+
481
+ ## RPC Service Client — Schema Versioning
482
+
483
+ The service client supports a `schemaVersion` option for contract compatibility between services:
484
+
485
+ ```typescript
486
+ import { createServiceClient } from '@classytic/arc/rpc';
487
+
488
+ const catalog = createServiceClient({
489
+ baseUrl: 'http://catalog:3000',
490
+ schemaVersion: '1.2.0', // sent as x-arc-schema-version header
491
+ correlationId: () => request.id,
492
+ retry: { maxRetries: 2 },
493
+ });
494
+
495
+ const products = await catalog.resource('product').list();
496
+ ```
497
+
498
+ Receiving services can check `request.headers['x-arc-schema-version']` to detect version mismatches.
499
+
500
+ ## Bulk Operations Preset
501
+
502
+ Adds bulk CRUD routes — repository must provide `createMany`, `updateMany`, `deleteMany`:
503
+
504
+ ```typescript
505
+ defineResource({
506
+ name: 'product',
507
+ adapter: createMongooseAdapter({ model, repository }),
508
+ presets: ['bulk'], // adds POST/PATCH/DELETE /{resource}/bulk
509
+ });
510
+
511
+ // Or with options
512
+ presets: [bulkPreset({ operations: ['createMany', 'updateMany'], maxCreateItems: 500 })]
513
+ ```
514
+
515
+ **Routes**: `POST /bulk` (body: `{ items }`) → `createMany`, `PATCH /bulk` (body: `{ filter, data }`) → `updateMany`, `DELETE /bulk` (body: `{ filter }`) → `deleteMany`. Permissions inherit from `create`/`update`/`delete`.
516
+
517
+ ## Compensating Transaction
518
+
519
+ In-process rollback primitive — runs steps in order, compensates in reverse on failure.
520
+ For distributed sagas across services, use Temporal, Inngest, or Streamline.
521
+
522
+ ```typescript
523
+ import { withCompensation } from '@classytic/arc/utils';
524
+
525
+ const result = await withCompensation('place-order', [
526
+ {
527
+ name: 'reserve-inventory',
528
+ execute: async (ctx) => {
529
+ const res = await inventoryService.reserve(ctx.items);
530
+ ctx.reservationId = res.id;
531
+ return res;
532
+ },
533
+ compensate: async (_ctx, result) => {
534
+ await inventoryService.release(result.id);
535
+ },
536
+ },
537
+ {
538
+ name: 'charge-payment',
539
+ execute: async (ctx) => await paymentService.charge(ctx.total),
540
+ compensate: async (_ctx, result) => await paymentService.refund(result.chargeId),
541
+ },
542
+ {
543
+ name: 'send-confirmation',
544
+ execute: async (ctx) => await emailService.send(ctx.email),
545
+ // No compensate — emails can't be unsent
546
+ },
547
+ ], { items: cart.items, total: cart.total, email: user.email });
548
+
549
+ if (!result.success) {
550
+ console.error(`Saga failed at ${result.failedStep}: ${result.error}`);
551
+ // Compensation already ran for completed steps
552
+ }
553
+ ```
554
+
555
+ **`defineCompensation()`** — reusable definition:
556
+
557
+ ```typescript
558
+ const placeOrder = defineCompensation('place-order', steps);
559
+ await placeOrder.execute({ items, total, email });
560
+ ```
561
+
562
+ **Fire-and-forget steps** — don't block, don't compensate, errors swallowed:
563
+
564
+ ```typescript
565
+ await withCompensation('checkout', [
566
+ { name: 'save-order', execute: saveOrder, compensate: cancelOrder },
567
+ { name: 'send-email', execute: sendEmail, fireAndForget: true }, // non-blocking
568
+ { name: 'charge', execute: chargeCard, compensate: refundCard },
569
+ ]);
570
+ // 'charge' runs immediately after 'save-order' — doesn't wait for email
571
+ ```
572
+
573
+ **Lifecycle hooks** — wire to Arc events, logging, or metrics:
574
+
575
+ ```typescript
576
+ await withCompensation('checkout', steps, { orderId }, {
577
+ onStepComplete: (stepName, result) => {
578
+ fastify.events.publish(`checkout.${stepName}.completed`, result);
579
+ },
580
+ onStepFailed: (stepName, error) => {
581
+ fastify.events.publish(`checkout.${stepName}.failed`, { error: error.message });
582
+ },
583
+ onCompensate: (stepName) => {
584
+ fastify.log.warn(`Compensated: ${stepName}`);
585
+ },
586
+ });
587
+ ```
588
+
589
+ **In an additionalRoute with Arc auth:**
590
+
591
+ ```typescript
592
+ defineResource({
593
+ name: 'order',
594
+ additionalRoutes: [{
595
+ method: 'POST',
596
+ path: '/:id/checkout',
597
+ permissions: requireAuth(),
598
+ wrapHandler: false,
599
+ handler: async (request, reply) => {
600
+ const result = await withCompensation('checkout', steps, { orderId: request.params.id });
601
+ if (!result.success) return reply.code(422).send({ error: result.error });
602
+ return reply.send({ success: true, data: result.results });
603
+ },
604
+ }],
605
+ });
606
+ ```
607
+
608
+ **Compensation errors**: collected in `result.compensationErrors[]` without stopping rollback. **Context**: mutable `Record<string, unknown>` shared across all steps.
609
+
610
+ **Scope**: In-process primitive. Process crash = no compensation. For durable distributed workflows, use Temporal, Inngest, or `@classytic/streamline`.