@classytic/arc 2.4.2 → 2.6.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 (104) hide show
  1. package/README.md +22 -6
  2. package/dist/{BaseController-CkM5dUh_.mjs → BaseController-AbbRx3e0.mjs} +5 -2
  3. package/dist/adapters/index.d.mts +2 -2
  4. package/dist/adapters/index.mjs +1 -1
  5. package/dist/{adapters-DTC4Ug66.mjs → adapters-CTn28N4y.mjs} +72 -11
  6. package/dist/audit/index.d.mts +1 -1
  7. package/dist/audit/index.mjs +11 -1
  8. package/dist/audit/mongodb.d.mts +1 -1
  9. package/dist/auth/index.d.mts +1 -1
  10. package/dist/auth/index.mjs +2 -2
  11. package/dist/cache/index.mjs +2 -2
  12. package/dist/cli/commands/describe.d.mts +1 -1
  13. package/dist/cli/commands/describe.mjs +1 -1
  14. package/dist/cli/commands/generate.d.mts +1 -1
  15. package/dist/cli/commands/generate.mjs +1 -1
  16. package/dist/cli/commands/init.d.mts +1 -1
  17. package/dist/cli/commands/init.mjs +13 -10
  18. package/dist/cli/commands/introspect.d.mts +1 -1
  19. package/dist/cli/commands/introspect.mjs +1 -1
  20. package/dist/cli/index.d.mts +4 -4
  21. package/dist/cli/index.mjs +4 -4
  22. package/dist/core/index.d.mts +2 -2
  23. package/dist/core/index.mjs +2 -2
  24. package/dist/{createApp-ByWNRsZj.mjs → createApp-Bol7DLUf.mjs} +404 -279
  25. package/dist/{defineResource-D9aY5Cy6.mjs → defineResource-bVKHjQzE.mjs} +116 -19
  26. package/dist/discovery/index.d.mts +1 -1
  27. package/dist/discovery/index.mjs +2 -2
  28. package/dist/docs/index.d.mts +1 -1
  29. package/dist/dynamic/index.d.mts +1 -1
  30. package/dist/dynamic/index.mjs +2 -2
  31. package/dist/{elevation-Ca_yveIO.d.mts → elevation-C_taLQrM.d.mts} +27 -1
  32. package/dist/{errorHandler--zp54tGc.mjs → errorHandler-r2595m8T.mjs} +5 -5
  33. package/dist/{errors-CPpvPHT0.d.mts → errors-CcVbl1-T.d.mts} +17 -1
  34. package/dist/{errors-rxhfP7Hf.mjs → errors-NoQKsbAT.mjs} +23 -1
  35. package/dist/{eventPlugin-iGrSEmwJ.d.mts → eventPlugin-DW45v4V5.d.mts} +30 -2
  36. package/dist/events/index.d.mts +2 -2
  37. package/dist/events/index.mjs +40 -10
  38. package/dist/events/transports/redis.d.mts +1 -1
  39. package/dist/events/transports/redis.mjs +1 -1
  40. package/dist/factory/index.d.mts +44 -23
  41. package/dist/factory/index.mjs +136 -2
  42. package/dist/hooks/index.d.mts +1 -1
  43. package/dist/idempotency/index.d.mts +3 -3
  44. package/dist/idempotency/mongodb.d.mts +1 -1
  45. package/dist/idempotency/redis.d.mts +1 -1
  46. package/dist/{index-yhxyjqNb.d.mts → index-BIsZ_su5.d.mts} +4 -8
  47. package/dist/{index-BL8CaQih.d.mts → index-Cb3gtbg7.d.mts} +2 -2
  48. package/dist/{index-Diqcm14c.d.mts → index-NGZksqM5.d.mts} +30 -1
  49. package/dist/index.d.mts +6 -6
  50. package/dist/index.mjs +8 -7
  51. package/dist/integrations/event-gateway.d.mts +1 -1
  52. package/dist/integrations/event-gateway.mjs +2 -2
  53. package/dist/integrations/index.d.mts +1 -1
  54. package/dist/integrations/jobs.d.mts +1 -1
  55. package/dist/integrations/jobs.mjs +1 -1
  56. package/dist/integrations/mcp/index.d.mts +4 -2
  57. package/dist/integrations/mcp/index.mjs +1 -1
  58. package/dist/integrations/mcp/testing.d.mts +1 -1
  59. package/dist/integrations/mcp/testing.mjs +1 -1
  60. package/dist/integrations/streamline.d.mts +1 -1
  61. package/dist/integrations/streamline.mjs +1 -1
  62. package/dist/integrations/websocket-redis.d.mts +1 -1
  63. package/dist/integrations/websocket-redis.mjs +1 -1
  64. package/dist/integrations/websocket.d.mts +1 -1
  65. package/dist/integrations/websocket.mjs +1 -1
  66. package/dist/{interface-DGmPxakH.d.mts → interface-DDW43OmS.d.mts} +194 -13
  67. package/dist/{memory-Cb_7iy9e.mjs → memory-BFAYkf8H.mjs} +1 -4
  68. package/dist/{mongodb-CUpYfxfD.d.mts → mongodb-kltrBPa1.d.mts} +10 -0
  69. package/dist/{mongodb-bga9AbkD.d.mts → mongodb-pMvOlR5_.d.mts} +1 -1
  70. package/dist/org/index.d.mts +1 -1
  71. package/dist/org/index.mjs +1 -1
  72. package/dist/permissions/index.d.mts +2 -2
  73. package/dist/permissions/index.mjs +2 -2
  74. package/dist/{permissions-CA5zg0yK.mjs → permissions-C8ImI8gC.mjs} +45 -3
  75. package/dist/plugins/index.d.mts +1 -1
  76. package/dist/plugins/index.mjs +3 -3
  77. package/dist/plugins/response-cache.d.mts +1 -1
  78. package/dist/plugins/response-cache.mjs +1 -1
  79. package/dist/plugins/tracing-entry.mjs +1 -1
  80. package/dist/presets/index.d.mts +2 -2
  81. package/dist/presets/index.mjs +2 -2
  82. package/dist/presets/multiTenant.d.mts +2 -2
  83. package/dist/presets/multiTenant.mjs +2 -2
  84. package/dist/{presets-C9QXJV1u.mjs → presets-BMfdy34e.mjs} +3 -3
  85. package/dist/{queryCachePlugin-ClosZdNS.mjs → queryCachePlugin-XtFplYO9.mjs} +1 -1
  86. package/dist/{redis-CQ5YxMC5.d.mts → redis-D0Qc-9EW.d.mts} +1 -1
  87. package/dist/registry/index.d.mts +1 -1
  88. package/dist/{resourceToTools-PMFE8HIv.mjs → resourceToTools-DH3c3e-T.mjs} +81 -7
  89. package/dist/scope/index.d.mts +2 -2
  90. package/dist/scope/index.mjs +2 -2
  91. package/dist/{sse-BkViJPlT.mjs → sse-BF7GR7IB.mjs} +1 -1
  92. package/dist/testing/index.d.mts +2 -2
  93. package/dist/testing/index.mjs +1 -1
  94. package/dist/types/index.d.mts +3 -3
  95. package/dist/types/index.mjs +23 -2
  96. package/dist/{types-C6TQjtdi.mjs → types-BhtYdxZU.mjs} +26 -1
  97. package/dist/{types-Dt0-AI6E.d.mts → types-D5hJ-k_3.d.mts} +189 -6
  98. package/dist/{types-BJmgxNbF.d.mts → types-D5rjsS_i.d.mts} +1 -1
  99. package/dist/utils/index.d.mts +2 -2
  100. package/dist/utils/index.mjs +1 -1
  101. package/package.json +7 -5
  102. package/skills/arc/SKILL.md +115 -8
  103. package/skills/arc/references/mcp.md +160 -2
  104. /package/dist/{interface-B4awm1RJ.d.mts → interface-gr-7qo9j.d.mts} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
- import { n as PUBLIC_SCOPE } from "./types-C6TQjtdi.mjs";
2
+ import { n as PUBLIC_SCOPE } from "./types-BhtYdxZU.mjs";
3
3
  import Fastify from "fastify";
4
4
  import qs from "qs";
5
5
  //#region src/factory/presets.ts
@@ -120,6 +120,7 @@ const testingPreset = {
120
120
  cors: false,
121
121
  rateLimit: false,
122
122
  underPressure: false,
123
+ arcPlugins: { gracefulShutdown: false },
123
124
  sensible: true,
124
125
  multipart: { limits: {
125
126
  fileSize: 1024 * 1024,
@@ -181,49 +182,263 @@ function getPreset(name) {
181
182
  }
182
183
  }
183
184
  //#endregion
184
- //#region src/factory/createApp.ts
185
+ //#region src/factory/registerArcPlugins.ts
185
186
  /**
186
- * ArcFactory - Production-ready Fastify application factory
187
- *
188
- * Enforces security best practices by making plugins opt-out instead of opt-in.
189
- * A developer must explicitly disable security features rather than forget to enable them.
190
- *
191
- * Note: Arc is database-agnostic. Connect your database separately and provide
192
- * adapters when defining resources. This allows multiple databases, custom
193
- * connection pooling, and full control over your data layer.
194
- *
195
- * @example
196
- * // 1. Connect your database(s) separately
197
- * import mongoose from 'mongoose';
198
- * await mongoose.connect(process.env.MONGO_URI);
199
- *
200
- * // 2. Create Arc app (no database config needed)
201
- * const app = await createApp({
202
- * preset: 'production',
203
- * auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
204
- * cors: { origin: ['https://example.com'] },
205
- * });
206
- *
207
- * // 3. Register resources with your adapters
208
- * await app.register(productResource.toPlugin());
209
- *
210
- * @example
211
- * // Multiple databases example
212
- * const primaryDb = await mongoose.connect(process.env.PRIMARY_DB);
213
- * const analyticsDb = mongoose.createConnection(process.env.ANALYTICS_DB);
187
+ * Register Arc core plugin and event system.
188
+ * Returns loaded plugin modules for registerArcPlugins (avoids duplicate dynamic import).
189
+ */
190
+ async function registerArcCore(fastify, config, trackPlugin) {
191
+ const { arcCorePlugin, requestIdPlugin, healthPlugin, gracefulShutdownPlugin } = await import("./plugins/index.mjs");
192
+ await fastify.register(arcCorePlugin, { emitEvents: config.arcPlugins?.emitEvents !== false });
193
+ trackPlugin("arc-core");
194
+ if (config.arcPlugins?.events !== false) {
195
+ const { default: eventPlugin } = await import("./eventPlugin-Ba00swHF.mjs").then((n) => n.n);
196
+ const eventOpts = typeof config.arcPlugins?.events === "object" ? config.arcPlugins.events : {};
197
+ await fastify.register(eventPlugin, {
198
+ ...eventOpts,
199
+ transport: config.stores?.events
200
+ });
201
+ trackPlugin("arc-events", eventOpts);
202
+ fastify.log.debug(`Arc events plugin enabled (transport: ${fastify.events.transportName})`);
203
+ }
204
+ return {
205
+ requestIdPlugin,
206
+ healthPlugin,
207
+ gracefulShutdownPlugin
208
+ };
209
+ }
210
+ /**
211
+ * Register opt-in Arc plugins (requestId, health, gracefulShutdown,
212
+ * caching, queryCache, SSE, metrics, versioning).
214
213
  *
215
- * const orderResource = defineResource({
216
- * adapter: createMongooseAdapter({ model: OrderModel, repository: orderRepo }),
217
- * });
214
+ * @param modules - Plugin modules loaded by registerArcCore (avoids re-importing)
215
+ */
216
+ async function registerArcPlugins(fastify, config, trackPlugin, modules) {
217
+ const { requestIdPlugin, healthPlugin, gracefulShutdownPlugin } = modules;
218
+ if (config.arcPlugins?.requestId !== false) {
219
+ await fastify.register(requestIdPlugin);
220
+ trackPlugin("arc-request-id");
221
+ }
222
+ if (config.arcPlugins?.health !== false) {
223
+ await fastify.register(healthPlugin);
224
+ trackPlugin("arc-health");
225
+ }
226
+ if (config.arcPlugins?.gracefulShutdown !== false) {
227
+ await fastify.register(gracefulShutdownPlugin);
228
+ trackPlugin("arc-graceful-shutdown");
229
+ }
230
+ if (config.arcPlugins?.caching) {
231
+ const { default: cachingPlugin } = await import("./caching-BSXB-Xr7.mjs").then((n) => n.r);
232
+ const opts = config.arcPlugins.caching === true ? {} : config.arcPlugins.caching;
233
+ await fastify.register(cachingPlugin, opts);
234
+ trackPlugin("arc-caching", opts);
235
+ }
236
+ if (config.arcPlugins?.queryCache) {
237
+ const { queryCachePlugin } = await import("./queryCachePlugin-XtFplYO9.mjs").then((n) => n.n);
238
+ const opts = config.arcPlugins.queryCache === true ? {} : config.arcPlugins.queryCache;
239
+ const store = config.stores?.queryCache ?? new (await (import("./memory-BFAYkf8H.mjs").then((n) => n.n))).MemoryCacheStore();
240
+ await fastify.register(queryCachePlugin, {
241
+ store,
242
+ ...opts
243
+ });
244
+ trackPlugin("arc-query-cache", opts);
245
+ }
246
+ if (config.arcPlugins?.sse) if (config.arcPlugins?.events === false) fastify.log.warn("SSE plugin requires events plugin (arcPlugins.events). SSE disabled.");
247
+ else {
248
+ const { default: ssePlugin } = await import("./sse-BF7GR7IB.mjs").then((n) => n.r);
249
+ const opts = config.arcPlugins.sse === true ? {} : config.arcPlugins.sse;
250
+ await fastify.register(ssePlugin, opts);
251
+ trackPlugin("arc-sse", opts);
252
+ }
253
+ if (config.arcPlugins?.metrics) {
254
+ const { default: metricsPlugin } = await import("./metrics-Csh4nsvv.mjs").then((n) => n.r);
255
+ const opts = config.arcPlugins.metrics === true ? {} : config.arcPlugins.metrics;
256
+ await fastify.register(metricsPlugin, opts);
257
+ trackPlugin("arc-metrics", opts);
258
+ }
259
+ if (config.arcPlugins?.versioning) {
260
+ const { default: versioningPlugin } = await import("./versioning-BzfeHmhj.mjs").then((n) => n.r);
261
+ await fastify.register(versioningPlugin, config.arcPlugins.versioning);
262
+ trackPlugin("arc-versioning", config.arcPlugins.versioning);
263
+ }
264
+ }
265
+ //#endregion
266
+ //#region src/factory/registerAuth.ts
267
+ /**
268
+ * Decorate request.scope with PUBLIC_SCOPE default.
269
+ * Every request starts as public; auth hooks upgrade it.
270
+ */
271
+ function decorateRequestScope(fastify) {
272
+ fastify.decorateRequest("scope", null);
273
+ fastify.addHook("onRequest", async (request) => {
274
+ if (!request.scope) request.scope = PUBLIC_SCOPE;
275
+ });
276
+ }
277
+ /**
278
+ * Register the configured auth strategy (JWT, Better Auth, Custom, or Authenticator).
279
+ */
280
+ async function registerAuth(fastify, config, trackPlugin) {
281
+ const authConfig = config.auth;
282
+ if (authConfig === false || !authConfig) {
283
+ fastify.log.debug("Authentication disabled");
284
+ return;
285
+ }
286
+ switch (authConfig.type) {
287
+ case "betterAuth": {
288
+ const { plugin, openapi } = authConfig.betterAuth;
289
+ await fastify.register(plugin);
290
+ trackPlugin("auth-better-auth");
291
+ if (openapi && !fastify.arc.externalOpenApiPaths.includes(openapi)) fastify.arc.externalOpenApiPaths.push(openapi);
292
+ fastify.log.debug("Better Auth authentication enabled");
293
+ break;
294
+ }
295
+ case "custom":
296
+ await fastify.register(authConfig.plugin);
297
+ trackPlugin("auth-custom");
298
+ fastify.log.debug("Custom authentication plugin enabled");
299
+ break;
300
+ case "authenticator": {
301
+ const { authenticate, optionalAuthenticate } = authConfig;
302
+ fastify.decorate("authenticate", async (request, reply) => {
303
+ await authenticate(request, reply);
304
+ });
305
+ if (!fastify.hasDecorator("optionalAuthenticate")) if (optionalAuthenticate) fastify.decorate("optionalAuthenticate", async (request, reply) => {
306
+ await optionalAuthenticate(request, reply);
307
+ });
308
+ else fastify.decorate("optionalAuthenticate", createOptionalAuthenticate(authenticate));
309
+ trackPlugin("auth-authenticator");
310
+ fastify.log.debug("Custom authenticator enabled");
311
+ break;
312
+ }
313
+ case "jwt": {
314
+ const { authPlugin } = await import("./auth/index.mjs");
315
+ const { type: _, ...arcAuthOpts } = authConfig;
316
+ await fastify.register(authPlugin, arcAuthOpts);
317
+ trackPlugin("auth-jwt");
318
+ fastify.log.debug("Arc authentication plugin enabled");
319
+ break;
320
+ }
321
+ }
322
+ }
323
+ /**
324
+ * Register elevation plugin (opt-in, runs after auth).
325
+ */
326
+ async function registerElevation(fastify, config, trackPlugin) {
327
+ if (!config.elevation) return;
328
+ const { elevationPlugin } = await import("./elevation-BEdACOLB.mjs").then((n) => n.r);
329
+ await fastify.register(elevationPlugin, config.elevation);
330
+ trackPlugin("arc-elevation", config.elevation);
331
+ fastify.log.debug("Elevation plugin enabled");
332
+ }
333
+ /**
334
+ * Register error handler plugin (opt-out).
335
+ */
336
+ async function registerErrorHandler(fastify, config, trackPlugin) {
337
+ if (config.errorHandler === false) return;
338
+ const { errorHandlerPlugin } = await import("./errorHandler-r2595m8T.mjs").then((n) => n.n);
339
+ const errorOpts = typeof config.errorHandler === "object" ? config.errorHandler : { includeStack: config.preset !== "production" };
340
+ await fastify.register(errorHandlerPlugin, errorOpts);
341
+ trackPlugin("arc-error-handler", errorOpts);
342
+ fastify.log.debug("Arc error handler enabled");
343
+ }
344
+ /**
345
+ * Create an optionalAuthenticate that wraps the main authenticate function.
346
+ * Intercepts 401/403 responses so unauthenticated requests proceed as public.
218
347
  *
219
- * const analyticsResource = defineResource({
220
- * adapter: createMongooseAdapter({ model: AnalyticsModel, repository: analyticsRepo }),
221
- * });
348
+ * Uses a try/catch approach first; falls back to reply proxy only when
349
+ * the authenticator calls reply.code(401).send() instead of throwing.
222
350
  */
223
- var createApp_exports = /* @__PURE__ */ __exportAll({
224
- ArcFactory: () => ArcFactory,
225
- createApp: () => createApp
226
- });
351
+ function createOptionalAuthenticate(authenticate) {
352
+ return async (request, reply) => {
353
+ let intercepted = false;
354
+ const proxyReply = new Proxy(reply, { get(target, prop) {
355
+ if (prop === "code") return (statusCode) => {
356
+ if (statusCode === 401 || statusCode === 403) {
357
+ intercepted = true;
358
+ return new Proxy(target, { get(_t, p) {
359
+ if (p === "send" || p === "type" || p === "header" || p === "headers") return () => proxyReply;
360
+ return Reflect.get(target, p, target);
361
+ } });
362
+ }
363
+ return target.code(statusCode);
364
+ };
365
+ if (prop === "send" && intercepted) return () => proxyReply;
366
+ if (prop === "sent") return intercepted ? false : target.sent;
367
+ return Reflect.get(target, prop, target);
368
+ } });
369
+ try {
370
+ await authenticate(request, proxyReply);
371
+ } catch {}
372
+ };
373
+ }
374
+ //#endregion
375
+ //#region src/factory/registerResources.ts
376
+ /** Register a single resource with descriptive error on failure. */
377
+ async function registerOne(parent, resource) {
378
+ const name = resource.name ?? "unknown";
379
+ try {
380
+ await parent.register(resource.toPlugin());
381
+ } catch (err) {
382
+ const msg = err instanceof Error ? err.message : String(err);
383
+ parent.log.error(`Failed to register resource "${name}": ${msg}`);
384
+ throw new Error(`Resource "${name}" failed to register: ${msg}. Check the resource definition, adapter, and permissions.`);
385
+ }
386
+ }
387
+ /**
388
+ * Execute the full resource lifecycle:
389
+ * 1. plugins() — infra (DB, docs, webhooks)
390
+ * 2. bootstrap[] — domain init (singletons, event handlers)
391
+ * 3. resources[] — auto-discovered routes (split by prefix)
392
+ * 4. afterResources() — post-registration wiring
393
+ * 5. onReady/onClose — lifecycle hooks
394
+ */
395
+ async function registerResources(fastify, config) {
396
+ if (config.plugins) {
397
+ await config.plugins(fastify);
398
+ fastify.log.debug("Custom plugins registered");
399
+ }
400
+ if (config.bootstrap?.length) {
401
+ for (const init of config.bootstrap) await init(fastify);
402
+ fastify.log.debug(`${config.bootstrap.length} bootstrap function(s) executed`);
403
+ }
404
+ if (config.resources?.length) {
405
+ const seen = /* @__PURE__ */ new Set();
406
+ for (const resource of config.resources) if (resource.name) {
407
+ if (seen.has(resource.name)) fastify.log.warn(`Duplicate resource name "${resource.name}" detected. This will cause route conflicts. Check your resources array and loadResources() output.`);
408
+ seen.add(resource.name);
409
+ }
410
+ const prefixed = [];
411
+ const root = [];
412
+ for (const resource of config.resources) if (resource.skipGlobalPrefix) root.push(resource);
413
+ else prefixed.push(resource);
414
+ for (const resource of root) await registerOne(fastify, resource);
415
+ if (prefixed.length) if (config.resourcePrefix) await fastify.register(async (scoped) => {
416
+ for (const resource of prefixed) await registerOne(scoped, resource);
417
+ }, { prefix: config.resourcePrefix });
418
+ else for (const resource of prefixed) await registerOne(fastify, resource);
419
+ const names = config.resources.map((r) => r.name ?? "?").join(", ");
420
+ const prefix = config.resourcePrefix ? ` (prefix: ${config.resourcePrefix})` : "";
421
+ fastify.log.info(`${config.resources.length} resource(s) registered${prefix}: ${names}`);
422
+ }
423
+ if (config.afterResources) {
424
+ await config.afterResources(fastify);
425
+ fastify.log.debug("afterResources hook executed");
426
+ }
427
+ if (config.onReady) {
428
+ const onReady = config.onReady;
429
+ fastify.addHook("onReady", async () => {
430
+ await onReady(fastify);
431
+ });
432
+ }
433
+ if (config.onClose) {
434
+ const onClose = config.onClose;
435
+ fastify.addHook("onClose", async () => {
436
+ await onClose(fastify);
437
+ });
438
+ }
439
+ }
440
+ //#endregion
441
+ //#region src/factory/registerSecurity.ts
227
442
  const PLUGIN_REGISTRY = {
228
443
  cors: {
229
444
  package: "@fastify/cors",
@@ -256,6 +471,7 @@ const PLUGIN_REGISTRY = {
256
471
  optional: true
257
472
  }
258
473
  };
474
+ /** Load a plugin from the registry with helpful error messages. */
259
475
  async function loadPlugin(name, logger) {
260
476
  const entry = PLUGIN_REGISTRY[name];
261
477
  if (!entry) throw new Error(`Unknown plugin: ${name}`);
@@ -273,81 +489,10 @@ async function loadPlugin(name, logger) {
273
489
  }
274
490
  }
275
491
  /**
276
- * Create a production-ready Fastify application with Arc framework
277
- *
278
- * Security plugins are enabled by default (opt-out):
279
- * - helmet (security headers)
280
- * - cors (cross-origin requests)
281
- * - rateLimit (DDoS protection)
282
- * - underPressure (health monitoring)
283
- *
284
- * Note: Compression is not included due to known Fastify 5 issues.
285
- * Use a reverse proxy (Nginx, Caddy) or CDN for compression.
286
- *
287
- * @param options - Application configuration
288
- * @returns Configured Fastify instance
492
+ * Register security plugins (Helmet, CORS, Rate Limiting).
493
+ * All enabled by default — set to `false` to opt out.
289
494
  */
290
- async function createApp(options) {
291
- if (options.debug !== void 0 && options.debug !== false) {
292
- const { configureArcLogger } = await import("./logger-Dz3j1ItV.mjs").then((n) => n.r);
293
- configureArcLogger({ debug: options.debug });
294
- }
295
- const authConfig = options.auth;
296
- const isAuthDisabled = authConfig === false;
297
- if (!isAuthDisabled && authConfig && authConfig.type === "jwt") {
298
- if (!authConfig.jwt?.secret && !authConfig.authenticate) throw new Error("createApp: JWT secret required when Arc auth is enabled.\nProvide auth.jwt.secret, auth.authenticate, or set auth: false to disable.\nExample: auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } }");
299
- }
300
- const deferredWarnings = [];
301
- if (options.runtime === "distributed") {
302
- const MEMORY_NAMES = new Set(["memory", "memory-cache"]);
303
- const missing = [];
304
- const eventsTransport = options.stores?.events;
305
- if (!eventsTransport || MEMORY_NAMES.has(eventsTransport.name)) missing.push("events transport");
306
- if (options.arcPlugins?.caching) {
307
- const cacheStore = options.stores?.cache;
308
- if (!cacheStore || MEMORY_NAMES.has(cacheStore.name)) missing.push("cache store");
309
- }
310
- const idempotencyStore = options.stores?.idempotency;
311
- if (idempotencyStore && MEMORY_NAMES.has(idempotencyStore.name)) missing.push("idempotency store (memory-backed in distributed mode)");
312
- else if (!idempotencyStore) deferredWarnings.push("runtime: 'distributed' — no idempotency store configured. Write-path deduplication will be instance-local. If resources use the idempotency plugin, provide stores.idempotency with a Redis/MongoDB store.");
313
- if (options.arcPlugins?.queryCache) {
314
- const qcStore = options.stores?.queryCache;
315
- if (!qcStore || MEMORY_NAMES.has(qcStore.name)) missing.push("queryCache store");
316
- }
317
- if (missing.length > 0) throw new Error(`[Arc] runtime: 'distributed' requires Redis/durable adapters.\nMissing: ${missing.join(", ")}.\nProvide Redis-backed stores or use runtime: 'memory' for development.`);
318
- }
319
- const config = {
320
- ...options.preset ? getPreset(options.preset) : {},
321
- ...options
322
- };
323
- const fastify = Fastify({
324
- logger: config.logger ?? true,
325
- trustProxy: config.trustProxy ?? false,
326
- routerOptions: { querystringParser: (str) => qs.parse(str) },
327
- ajv: { customOptions: {
328
- coerceTypes: true,
329
- useDefaults: true,
330
- removeAdditional: false,
331
- keywords: ["example", ...config.ajv?.keywords ?? []]
332
- } }
333
- });
334
- for (const warning of deferredWarnings) fastify.log.warn(warning);
335
- if (config.typeProvider === "typebox") try {
336
- const { TypeBoxValidatorCompiler } = await import("@fastify/type-provider-typebox");
337
- fastify.setValidatorCompiler(TypeBoxValidatorCompiler);
338
- fastify.log.debug("TypeBox type provider enabled");
339
- } catch {
340
- fastify.log.warn("typeProvider: \"typebox\" requested but @fastify/type-provider-typebox is not installed. Install it with: npm install @sinclair/typebox @fastify/type-provider-typebox");
341
- }
342
- fastify.removeContentTypeParser("application/json");
343
- fastify.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
344
- if (!body || body.length === 0) return done(null, void 0);
345
- try {
346
- done(null, JSON.parse(body));
347
- } catch (err) {
348
- done(err);
349
- }
350
- });
495
+ async function registerSecurityPlugins(fastify, config) {
351
496
  if (config.helmet !== false) {
352
497
  const helmet = await loadPlugin("helmet");
353
498
  await fastify.register(helmet, config.helmet ?? {});
@@ -369,19 +514,22 @@ async function createApp(options) {
369
514
  };
370
515
  await fastify.register(rateLimit, rateLimitOpts);
371
516
  if (!(typeof rateLimitOpts === "object" && "store" in rateLimitOpts)) {
372
- if (config.runtime === "distributed") {
373
- fastify.log.error("Rate limiting is using in-memory store in distributed mode. Each instance tracks limits independently — this breaks rate limiting. Configure a Redis store: rateLimit: { store: new RedisStore({ ... }) }");
374
- throw new Error("[Arc] runtime: 'distributed' with rate limiting requires a shared store.\nProvide rateLimit: { store: new RedisStore({ ... }) } or disable rate limiting: rateLimit: false");
375
- } else if (config.preset === "production") fastify.log.warn("Rate limiting is using in-memory store. In multi-instance deployments, each instance tracks limits independently. Configure a Redis store for distributed rate limiting: rateLimit: { store: new RedisStore({ ... }) }");
517
+ if (config.runtime === "distributed") throw new Error("[Arc] runtime: 'distributed' with rate limiting requires a shared store.\nProvide rateLimit: { store: new RedisStore({ ... }) } or disable rate limiting: rateLimit: false");
518
+ else if (config.preset === "production") fastify.log.warn("Rate limiting is using in-memory store. In multi-instance deployments, each instance tracks limits independently. Configure a Redis store for distributed rate limiting.");
376
519
  }
377
520
  fastify.log.debug("Rate limiting enabled");
378
521
  } else fastify.log.warn("Rate limiting disabled");
522
+ }
523
+ /**
524
+ * Register performance and utility plugins (Under Pressure, Sensible, Multipart, Raw Body).
525
+ */
526
+ async function registerUtilityPlugins(fastify, config) {
379
527
  if (config.preset === "production") fastify.log.warn("Response compression is not enabled (Fastify 5 stream issues). Use a reverse proxy (Nginx, Caddy, Cloudflare) for gzip/brotli in production.");
380
528
  if (config.underPressure !== false) {
381
529
  const underPressure = await loadPlugin("underPressure");
382
530
  await fastify.register(underPressure, config.underPressure ?? { exposeStatusRoute: true });
383
531
  fastify.log.debug("Health monitoring (under-pressure) enabled");
384
- } else fastify.log.debug("Health monitoring disabled");
532
+ }
385
533
  if (config.sensible !== false) {
386
534
  const sensible = await loadPlugin("sensible");
387
535
  await fastify.register(sensible);
@@ -414,9 +562,135 @@ async function createApp(options) {
414
562
  fastify.log.debug("Raw body parsing enabled");
415
563
  }
416
564
  }
417
- const { arcCorePlugin, requestIdPlugin, healthPlugin, gracefulShutdownPlugin } = await import("./plugins/index.mjs");
418
- await fastify.register(arcCorePlugin, { emitEvents: config.arcPlugins?.emitEvents !== false });
419
- /** Track a plugin in the Arc plugin registry */
565
+ }
566
+ //#endregion
567
+ //#region src/factory/createApp.ts
568
+ /**
569
+ * ArcFactory - Production-ready Fastify application factory
570
+ *
571
+ * Enforces security best practices by making plugins opt-out instead of opt-in.
572
+ * A developer must explicitly disable security features rather than forget to enable them.
573
+ *
574
+ * Note: Arc is database-agnostic. Connect your database separately and provide
575
+ * adapters when defining resources. This allows multiple databases, custom
576
+ * connection pooling, and full control over your data layer.
577
+ *
578
+ * @example
579
+ * // 1. Connect your database(s) separately
580
+ * import mongoose from 'mongoose';
581
+ * await mongoose.connect(process.env.MONGO_URI);
582
+ *
583
+ * // 2. Create Arc app with resources
584
+ * const app = await createApp({
585
+ * preset: 'production',
586
+ * auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
587
+ * cors: { origin: ['https://example.com'] },
588
+ * resources: [productResource, orderResource],
589
+ * });
590
+ *
591
+ * @example
592
+ * // Multiple databases example
593
+ * const primaryDb = await mongoose.connect(process.env.PRIMARY_DB);
594
+ * const analyticsDb = mongoose.createConnection(process.env.ANALYTICS_DB);
595
+ *
596
+ * const orderResource = defineResource({
597
+ * adapter: createMongooseAdapter({ model: OrderModel, repository: orderRepo }),
598
+ * });
599
+ *
600
+ * const analyticsResource = defineResource({
601
+ * adapter: createMongooseAdapter({ model: AnalyticsModel, repository: analyticsRepo }),
602
+ * });
603
+ */
604
+ var createApp_exports = /* @__PURE__ */ __exportAll({
605
+ ArcFactory: () => ArcFactory,
606
+ createApp: () => createApp
607
+ });
608
+ const MEMORY_STORE_NAMES = new Set(["memory", "memory-cache"]);
609
+ function validateAuthOptions(options) {
610
+ const authConfig = options.auth;
611
+ if (authConfig === false || !authConfig) return;
612
+ if (authConfig.type === "jwt" && !authConfig.jwt?.secret && !authConfig.authenticate) throw new Error("createApp: JWT secret required when Arc auth is enabled.\nProvide auth.jwt.secret, auth.authenticate, or set auth: false to disable.\nExample: auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } }");
613
+ }
614
+ function validateDistributedRuntime(options) {
615
+ const deferredWarnings = [];
616
+ if (options.runtime !== "distributed") return deferredWarnings;
617
+ const missing = [];
618
+ const events = options.stores?.events;
619
+ if (!events || MEMORY_STORE_NAMES.has(events.name)) missing.push("events transport");
620
+ if (options.arcPlugins?.caching) {
621
+ const cache = options.stores?.cache;
622
+ if (!cache || MEMORY_STORE_NAMES.has(cache.name)) missing.push("cache store");
623
+ }
624
+ const idempotency = options.stores?.idempotency;
625
+ if (idempotency && MEMORY_STORE_NAMES.has(idempotency.name)) missing.push("idempotency store (memory-backed in distributed mode)");
626
+ else if (!idempotency) deferredWarnings.push("runtime: 'distributed' — no idempotency store configured. Write-path deduplication will be instance-local. If resources use the idempotency plugin, provide stores.idempotency with a Redis/MongoDB store.");
627
+ if (options.arcPlugins?.queryCache) {
628
+ const qc = options.stores?.queryCache;
629
+ if (!qc || MEMORY_STORE_NAMES.has(qc.name)) missing.push("queryCache store");
630
+ }
631
+ if (missing.length > 0) throw new Error(`[Arc] runtime: 'distributed' requires Redis/durable adapters.\nMissing: ${missing.join(", ")}.\nProvide Redis-backed stores or use runtime: 'memory' for development.`);
632
+ return deferredWarnings;
633
+ }
634
+ /**
635
+ * Create a production-ready Fastify application with Arc framework.
636
+ *
637
+ * Boot order:
638
+ * ```
639
+ * 0. Logger, validation, preset merge
640
+ * 1. Create Fastify instance
641
+ * 2. Security plugins (Helmet, CORS, Rate Limit) — opt-out
642
+ * 3. Utility plugins (Under Pressure, Sensible, Multipart, Raw Body)
643
+ * 4. Arc core (fastify.arc, events)
644
+ * 5. Arc plugins (requestId, health, caching, SSE, metrics, versioning)
645
+ * 6. Auth (scope decoration, auth strategy, elevation, error handler)
646
+ * 7. plugins() — user infra (DB, docs, webhooks)
647
+ * 8. bootstrap[] — domain init (singletons, event handlers)
648
+ * 9. resources[] — auto-discovered routes (prefix + skipGlobalPrefix)
649
+ * 10. afterResources() — post-registration wiring
650
+ * 11. onReady/onClose — lifecycle hooks
651
+ * ```
652
+ */
653
+ async function createApp(options) {
654
+ if (options.debug !== void 0 && options.debug !== false) {
655
+ const { configureArcLogger } = await import("./logger-Dz3j1ItV.mjs").then((n) => n.r);
656
+ configureArcLogger({ debug: options.debug });
657
+ }
658
+ validateAuthOptions(options);
659
+ const deferredWarnings = validateDistributedRuntime(options);
660
+ const config = {
661
+ ...options.preset ? getPreset(options.preset) : {},
662
+ ...options
663
+ };
664
+ const fastify = Fastify({
665
+ logger: config.logger ?? true,
666
+ trustProxy: config.trustProxy ?? false,
667
+ routerOptions: { querystringParser: (str) => qs.parse(str) },
668
+ ajv: { customOptions: {
669
+ coerceTypes: true,
670
+ useDefaults: true,
671
+ removeAdditional: false,
672
+ keywords: ["example", ...config.ajv?.keywords ?? []]
673
+ } }
674
+ });
675
+ for (const warning of deferredWarnings) fastify.log.warn(warning);
676
+ if (config.typeProvider === "typebox") try {
677
+ const { TypeBoxValidatorCompiler } = await import("@fastify/type-provider-typebox");
678
+ fastify.setValidatorCompiler(TypeBoxValidatorCompiler);
679
+ } catch {
680
+ fastify.log.warn("typeProvider: \"typebox\" requested but @fastify/type-provider-typebox is not installed.");
681
+ }
682
+ const sjp = await import("secure-json-parse");
683
+ fastify.removeContentTypeParser("application/json");
684
+ fastify.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
685
+ if (!body || body.length === 0) return done(null, void 0);
686
+ try {
687
+ done(null, sjp.parse(body));
688
+ } catch (err) {
689
+ done(err);
690
+ }
691
+ });
692
+ await registerSecurityPlugins(fastify, config);
693
+ await registerUtilityPlugins(fastify, config);
420
694
  const trackPlugin = (name, opts) => {
421
695
  fastify.arc.plugins.set(name, {
422
696
  name,
@@ -424,162 +698,13 @@ async function createApp(options) {
424
698
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
425
699
  });
426
700
  };
427
- trackPlugin("arc-core");
428
- if (config.arcPlugins?.events !== false) {
429
- const { default: eventPlugin } = await import("./eventPlugin-Ba00swHF.mjs").then((n) => n.n);
430
- const eventOpts = typeof config.arcPlugins?.events === "object" ? config.arcPlugins.events : {};
431
- await fastify.register(eventPlugin, {
432
- ...eventOpts,
433
- transport: options.stores?.events
434
- });
435
- trackPlugin("arc-events", eventOpts);
436
- fastify.log.debug(`Arc events plugin enabled (transport: ${fastify.events.transportName})`);
437
- }
438
- if (config.arcPlugins?.requestId !== false) {
439
- await fastify.register(requestIdPlugin);
440
- trackPlugin("arc-request-id");
441
- fastify.log.debug("Arc requestId plugin enabled");
442
- }
443
- if (config.arcPlugins?.health !== false) {
444
- await fastify.register(healthPlugin);
445
- trackPlugin("arc-health");
446
- fastify.log.debug("Arc health plugin enabled");
447
- }
448
- if (config.arcPlugins?.gracefulShutdown !== false) {
449
- await fastify.register(gracefulShutdownPlugin);
450
- trackPlugin("arc-graceful-shutdown");
451
- fastify.log.debug("Arc gracefulShutdown plugin enabled");
452
- }
453
- if (config.arcPlugins?.caching) {
454
- const { default: cachingPlugin } = await import("./caching-BSXB-Xr7.mjs").then((n) => n.r);
455
- const cachingOpts = config.arcPlugins.caching === true ? {} : config.arcPlugins.caching;
456
- await fastify.register(cachingPlugin, cachingOpts);
457
- trackPlugin("arc-caching", cachingOpts);
458
- fastify.log.debug("Arc caching plugin enabled");
459
- }
460
- if (config.arcPlugins?.queryCache) {
461
- const { queryCachePlugin } = await import("./queryCachePlugin-ClosZdNS.mjs").then((n) => n.n);
462
- const qcOpts = config.arcPlugins.queryCache === true ? {} : config.arcPlugins.queryCache;
463
- const store = options.stores?.queryCache ?? new (await (import("./memory-Cb_7iy9e.mjs").then((n) => n.n))).MemoryCacheStore();
464
- await fastify.register(queryCachePlugin, {
465
- store,
466
- ...qcOpts
467
- });
468
- trackPlugin("arc-query-cache", qcOpts);
469
- fastify.log.debug("Arc queryCache plugin enabled");
470
- }
471
- if (config.arcPlugins?.sse) if (config.arcPlugins?.events === false) fastify.log.warn("SSE plugin requires events plugin (arcPlugins.events). SSE disabled.");
472
- else {
473
- const { default: ssePlugin } = await import("./sse-BkViJPlT.mjs").then((n) => n.r);
474
- const sseOpts = config.arcPlugins.sse === true ? {} : config.arcPlugins.sse;
475
- await fastify.register(ssePlugin, sseOpts);
476
- trackPlugin("arc-sse", sseOpts);
477
- fastify.log.debug("Arc SSE plugin enabled");
478
- }
479
- if (config.arcPlugins?.metrics) {
480
- const { default: metricsPlugin } = await import("./metrics-Csh4nsvv.mjs").then((n) => n.r);
481
- const metricsOpts = config.arcPlugins.metrics === true ? {} : config.arcPlugins.metrics;
482
- await fastify.register(metricsPlugin, metricsOpts);
483
- trackPlugin("arc-metrics", metricsOpts);
484
- fastify.log.debug("Arc metrics plugin enabled");
485
- }
486
- if (config.arcPlugins?.versioning) {
487
- const { default: versioningPlugin } = await import("./versioning-BzfeHmhj.mjs").then((n) => n.r);
488
- await fastify.register(versioningPlugin, config.arcPlugins.versioning);
489
- trackPlugin("arc-versioning", config.arcPlugins.versioning);
490
- fastify.log.debug("Arc versioning plugin enabled");
491
- }
492
- fastify.decorateRequest("scope", null);
493
- fastify.addHook("onRequest", async (request) => {
494
- if (!request.scope) request.scope = PUBLIC_SCOPE;
495
- });
496
- if (isAuthDisabled) fastify.log.debug("Authentication disabled");
497
- else if (authConfig) switch (authConfig.type) {
498
- case "betterAuth": {
499
- const { plugin, openapi } = authConfig.betterAuth;
500
- await fastify.register(plugin);
501
- trackPlugin("auth-better-auth");
502
- if (openapi && !fastify.arc.externalOpenApiPaths.includes(openapi)) fastify.arc.externalOpenApiPaths.push(openapi);
503
- fastify.log.debug("Better Auth authentication enabled");
504
- break;
505
- }
506
- case "custom":
507
- await fastify.register(authConfig.plugin);
508
- trackPlugin("auth-custom");
509
- fastify.log.debug("Custom authentication plugin enabled");
510
- break;
511
- case "authenticator": {
512
- const { authenticate, optionalAuthenticate } = authConfig;
513
- fastify.decorate("authenticate", async (request, reply) => {
514
- await authenticate(request, reply);
515
- });
516
- if (!fastify.hasDecorator("optionalAuthenticate")) if (optionalAuthenticate) fastify.decorate("optionalAuthenticate", async (request, reply) => {
517
- await optionalAuthenticate(request, reply);
518
- });
519
- else fastify.decorate("optionalAuthenticate", async (request, reply) => {
520
- let intercepted = false;
521
- const proxyReply = new Proxy(reply, { get(target, prop) {
522
- if (prop === "code") return (statusCode) => {
523
- if (statusCode === 401 || statusCode === 403) {
524
- intercepted = true;
525
- return new Proxy(target, { get(_t, p) {
526
- if (p === "send" || p === "type" || p === "header" || p === "headers") return () => proxyReply;
527
- return Reflect.get(target, p, target);
528
- } });
529
- }
530
- return target.code(statusCode);
531
- };
532
- if (prop === "send" && intercepted) return () => proxyReply;
533
- if (prop === "sent") return intercepted ? false : target.sent;
534
- return Reflect.get(target, prop, target);
535
- } });
536
- try {
537
- await authenticate(request, proxyReply);
538
- } catch {}
539
- });
540
- trackPlugin("auth-authenticator");
541
- fastify.log.debug("Custom authenticator enabled");
542
- break;
543
- }
544
- case "jwt": {
545
- const { authPlugin } = await import("./auth/index.mjs");
546
- const { type: _, ...arcAuthOpts } = authConfig;
547
- await fastify.register(authPlugin, arcAuthOpts);
548
- trackPlugin("auth-jwt");
549
- fastify.log.debug("Arc authentication plugin enabled");
550
- break;
551
- }
552
- }
553
- if (config.elevation) {
554
- const { elevationPlugin } = await import("./elevation-BEdACOLB.mjs").then((n) => n.r);
555
- await fastify.register(elevationPlugin, config.elevation);
556
- trackPlugin("arc-elevation", config.elevation);
557
- fastify.log.debug("Elevation plugin enabled");
558
- }
559
- if (config.errorHandler !== false) {
560
- const { errorHandlerPlugin } = await import("./errorHandler--zp54tGc.mjs").then((n) => n.n);
561
- const errorOpts = typeof config.errorHandler === "object" ? config.errorHandler : { includeStack: config.preset !== "production" };
562
- await fastify.register(errorHandlerPlugin, errorOpts);
563
- trackPlugin("arc-error-handler", errorOpts);
564
- fastify.log.debug("Arc error handler enabled");
565
- }
566
- if (config.plugins) {
567
- await config.plugins(fastify);
568
- fastify.log.debug("Custom plugins registered");
569
- }
570
- if (config.onReady) {
571
- const onReady = config.onReady;
572
- fastify.addHook("onReady", async () => {
573
- await onReady(fastify);
574
- });
575
- }
576
- if (config.onClose) {
577
- const onClose = config.onClose;
578
- fastify.addHook("onClose", async () => {
579
- await onClose(fastify);
580
- });
581
- }
582
- const authMode = isAuthDisabled ? "none" : authConfig ? authConfig.type : "none";
701
+ await registerArcPlugins(fastify, config, trackPlugin, await registerArcCore(fastify, config, trackPlugin));
702
+ decorateRequestScope(fastify);
703
+ await registerAuth(fastify, config, trackPlugin);
704
+ await registerElevation(fastify, config, trackPlugin);
705
+ await registerErrorHandler(fastify, config, trackPlugin);
706
+ await registerResources(fastify, config);
707
+ const authMode = config.auth === false ? "none" : config.auth ? config.auth.type : "none";
583
708
  fastify.log.info({
584
709
  preset: config.preset ?? "custom",
585
710
  runtime: config.runtime ?? "memory",
@@ -614,4 +739,4 @@ const ArcFactory = {
614
739
  }
615
740
  };
616
741
  //#endregion
617
- export { getPreset as a, developmentPreset as i, createApp as n, productionPreset as o, createApp_exports as r, testingPreset as s, ArcFactory as t };
742
+ export { edgePreset as a, testingPreset as c, developmentPreset as i, createApp as n, getPreset as o, createApp_exports as r, productionPreset as s, ArcFactory as t };