@classytic/arc 2.4.3 → 2.6.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 (82) hide show
  1. package/README.md +57 -6
  2. package/dist/{BaseController-CkM5dUh_.mjs → BaseController-AbbRx3e0.mjs} +5 -2
  3. package/dist/{ResourceRegistry-DeCIFlix.mjs → ResourceRegistry-C6ngvOnn.mjs} +1 -0
  4. package/dist/adapters/index.d.mts +2 -2
  5. package/dist/adapters/index.mjs +1 -1
  6. package/dist/{adapters-DTC4Ug66.mjs → adapters-CTn28N4y.mjs} +72 -11
  7. package/dist/audit/index.d.mts +32 -6
  8. package/dist/audit/index.mjs +32 -4
  9. package/dist/audit/mongodb.d.mts +1 -1
  10. package/dist/auth/index.d.mts +1 -1
  11. package/dist/auth/index.mjs +2 -2
  12. package/dist/cli/commands/docs.mjs +1 -1
  13. package/dist/cli/commands/init.mjs +12 -9
  14. package/dist/cli/commands/introspect.mjs +1 -1
  15. package/dist/core/index.d.mts +2 -2
  16. package/dist/core/index.mjs +2 -2
  17. package/dist/{createApp-CBgVaFyh.mjs → createApp-D2w0LdYJ.mjs} +431 -290
  18. package/dist/{defineResource-B22gcNvn.mjs → defineResource-Ckxg6HrZ.mjs} +125 -22
  19. package/dist/discovery/index.mjs +1 -1
  20. package/dist/docs/index.d.mts +1 -1
  21. package/dist/dynamic/index.d.mts +1 -1
  22. package/dist/dynamic/index.mjs +2 -2
  23. package/dist/{elevation-Ca_yveIO.d.mts → elevation-C_taLQrM.d.mts} +27 -1
  24. package/dist/{errorHandler-DMbGdzBG.mjs → errorHandler-r2595m8T.mjs} +1 -1
  25. package/dist/{errors-CPpvPHT0.d.mts → errors-CcVbl1-T.d.mts} +17 -1
  26. package/dist/{errors-rxhfP7Hf.mjs → errors-NoQKsbAT.mjs} +23 -1
  27. package/dist/{eventPlugin-iGrSEmwJ.d.mts → eventPlugin-DW45v4V5.d.mts} +30 -2
  28. package/dist/events/index.d.mts +2 -2
  29. package/dist/events/index.mjs +40 -10
  30. package/dist/factory/index.d.mts +44 -23
  31. package/dist/factory/index.mjs +152 -2
  32. package/dist/hooks/index.d.mts +1 -1
  33. package/dist/idempotency/index.d.mts +3 -3
  34. package/dist/idempotency/mongodb.d.mts +1 -1
  35. package/dist/idempotency/redis.d.mts +1 -1
  36. package/dist/{index-BL8CaQih.d.mts → index-B4uZm82R.d.mts} +2 -2
  37. package/dist/{index-yhxyjqNb.d.mts → index-DrCqa3Jq.d.mts} +4 -8
  38. package/dist/{index-Diqcm14c.d.mts → index-NGZksqM5.d.mts} +30 -1
  39. package/dist/index.d.mts +6 -6
  40. package/dist/index.mjs +8 -7
  41. package/dist/integrations/event-gateway.mjs +1 -1
  42. package/dist/integrations/index.d.mts +1 -1
  43. package/dist/integrations/mcp/index.d.mts +4 -2
  44. package/dist/integrations/mcp/index.mjs +1 -1
  45. package/dist/integrations/mcp/testing.d.mts +1 -1
  46. package/dist/integrations/mcp/testing.mjs +1 -1
  47. package/dist/{interface-DGmPxakH.d.mts → interface-CrN45qz1.d.mts} +229 -13
  48. package/dist/{mongodb-CUpYfxfD.d.mts → mongodb-kltrBPa1.d.mts} +10 -0
  49. package/dist/{mongodb-bga9AbkD.d.mts → mongodb-pMvOlR5_.d.mts} +1 -1
  50. package/dist/org/index.d.mts +1 -1
  51. package/dist/org/index.mjs +1 -1
  52. package/dist/permissions/index.d.mts +2 -2
  53. package/dist/permissions/index.mjs +2 -2
  54. package/dist/{permissions-Jk5x3sxz.mjs → permissions-C8ImI8gC.mjs} +44 -2
  55. package/dist/plugins/index.d.mts +1 -1
  56. package/dist/plugins/index.mjs +4 -4
  57. package/dist/plugins/tracing-entry.mjs +1 -1
  58. package/dist/presets/index.d.mts +1 -1
  59. package/dist/presets/index.mjs +1 -1
  60. package/dist/presets/multiTenant.d.mts +1 -1
  61. package/dist/presets/multiTenant.mjs +1 -1
  62. package/dist/{presets-OMPaHMTY.mjs → presets-BMfdy34e.mjs} +2 -2
  63. package/dist/{redis-CQ5YxMC5.d.mts → redis-D0Qc-9EW.d.mts} +1 -1
  64. package/dist/registry/index.d.mts +1 -1
  65. package/dist/registry/index.mjs +1 -1
  66. package/dist/{resourceToTools-PMFE8HIv.mjs → resourceToTools-DH3c3e-T.mjs} +81 -7
  67. package/dist/scope/index.d.mts +2 -2
  68. package/dist/scope/index.mjs +2 -2
  69. package/dist/{sse-BkViJPlT.mjs → sse-BF7GR7IB.mjs} +1 -1
  70. package/dist/testing/index.d.mts +26 -3
  71. package/dist/testing/index.mjs +46 -2
  72. package/dist/types/index.d.mts +3 -3
  73. package/dist/types/index.mjs +23 -2
  74. package/dist/{types-C6TQjtdi.mjs → types-BhtYdxZU.mjs} +26 -1
  75. package/dist/{types-Dt0-AI6E.d.mts → types-C1Z28coa.d.mts} +195 -6
  76. package/dist/{types-BJmgxNbF.d.mts → types-DurlBP2N.d.mts} +1 -1
  77. package/dist/utils/index.d.mts +2 -2
  78. package/dist/utils/index.mjs +1 -1
  79. package/package.json +6 -5
  80. package/skills/arc/SKILL.md +151 -4
  81. package/skills/arc/references/mcp.md +160 -2
  82. /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
@@ -66,20 +66,36 @@ const productionPreset = {
66
66
  }
67
67
  };
68
68
  /**
69
+ * Try to detect if `pino-pretty` is installed (devDep). Returns the transport
70
+ * config if available, or falls back to plain JSON logging. This prevents the
71
+ * common "pino-pretty not found" crash in production when someone uses the
72
+ * development preset by mistake (or via NODE_ENV-based preset selection).
73
+ */
74
+ function devLoggerConfig() {
75
+ try {
76
+ const req = eval("require") ?? null;
77
+ if (req?.resolve) {
78
+ req.resolve("pino-pretty");
79
+ return {
80
+ level: "debug",
81
+ transport: {
82
+ target: "pino-pretty",
83
+ options: {
84
+ colorize: true,
85
+ translateTime: "SYS:HH:MM:ss",
86
+ ignore: "pid,hostname"
87
+ }
88
+ }
89
+ };
90
+ }
91
+ } catch {}
92
+ return { level: "debug" };
93
+ }
94
+ /**
69
95
  * Development preset - relaxed security, verbose logging
70
96
  */
71
97
  const developmentPreset = {
72
- logger: {
73
- level: "debug",
74
- transport: {
75
- target: "pino-pretty",
76
- options: {
77
- colorize: true,
78
- translateTime: "SYS:HH:MM:ss",
79
- ignore: "pid,hostname"
80
- }
81
- }
82
- },
98
+ logger: devLoggerConfig(),
83
99
  trustProxy: true,
84
100
  helmet: { contentSecurityPolicy: false },
85
101
  cors: {
@@ -120,6 +136,7 @@ const testingPreset = {
120
136
  cors: false,
121
137
  rateLimit: false,
122
138
  underPressure: false,
139
+ arcPlugins: { gracefulShutdown: false },
123
140
  sensible: true,
124
141
  multipart: { limits: {
125
142
  fileSize: 1024 * 1024,
@@ -181,49 +198,263 @@ function getPreset(name) {
181
198
  }
182
199
  }
183
200
  //#endregion
184
- //#region src/factory/createApp.ts
201
+ //#region src/factory/registerArcPlugins.ts
185
202
  /**
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);
203
+ * Register Arc core plugin and event system.
204
+ * Returns loaded plugin modules for registerArcPlugins (avoids duplicate dynamic import).
205
+ */
206
+ async function registerArcCore(fastify, config, trackPlugin) {
207
+ const { arcCorePlugin, requestIdPlugin, healthPlugin, gracefulShutdownPlugin } = await import("./plugins/index.mjs");
208
+ await fastify.register(arcCorePlugin, { emitEvents: config.arcPlugins?.emitEvents !== false });
209
+ trackPlugin("arc-core");
210
+ if (config.arcPlugins?.events !== false) {
211
+ const { default: eventPlugin } = await import("./eventPlugin-Ba00swHF.mjs").then((n) => n.n);
212
+ const eventOpts = typeof config.arcPlugins?.events === "object" ? config.arcPlugins.events : {};
213
+ await fastify.register(eventPlugin, {
214
+ ...eventOpts,
215
+ transport: config.stores?.events
216
+ });
217
+ trackPlugin("arc-events", eventOpts);
218
+ fastify.log.debug(`Arc events plugin enabled (transport: ${fastify.events.transportName})`);
219
+ }
220
+ return {
221
+ requestIdPlugin,
222
+ healthPlugin,
223
+ gracefulShutdownPlugin
224
+ };
225
+ }
226
+ /**
227
+ * Register opt-in Arc plugins (requestId, health, gracefulShutdown,
228
+ * caching, queryCache, SSE, metrics, versioning).
214
229
  *
215
- * const orderResource = defineResource({
216
- * adapter: createMongooseAdapter({ model: OrderModel, repository: orderRepo }),
217
- * });
230
+ * @param modules - Plugin modules loaded by registerArcCore (avoids re-importing)
231
+ */
232
+ async function registerArcPlugins(fastify, config, trackPlugin, modules) {
233
+ const { requestIdPlugin, healthPlugin, gracefulShutdownPlugin } = modules;
234
+ if (config.arcPlugins?.requestId !== false) {
235
+ await fastify.register(requestIdPlugin);
236
+ trackPlugin("arc-request-id");
237
+ }
238
+ if (config.arcPlugins?.health !== false) {
239
+ await fastify.register(healthPlugin);
240
+ trackPlugin("arc-health");
241
+ }
242
+ if (config.arcPlugins?.gracefulShutdown !== false) {
243
+ await fastify.register(gracefulShutdownPlugin);
244
+ trackPlugin("arc-graceful-shutdown");
245
+ }
246
+ if (config.arcPlugins?.caching) {
247
+ const { default: cachingPlugin } = await import("./caching-BSXB-Xr7.mjs").then((n) => n.r);
248
+ const opts = config.arcPlugins.caching === true ? {} : config.arcPlugins.caching;
249
+ await fastify.register(cachingPlugin, opts);
250
+ trackPlugin("arc-caching", opts);
251
+ }
252
+ if (config.arcPlugins?.queryCache) {
253
+ const { queryCachePlugin } = await import("./queryCachePlugin-XtFplYO9.mjs").then((n) => n.n);
254
+ const opts = config.arcPlugins.queryCache === true ? {} : config.arcPlugins.queryCache;
255
+ const store = config.stores?.queryCache ?? new (await (import("./memory-BFAYkf8H.mjs").then((n) => n.n))).MemoryCacheStore();
256
+ await fastify.register(queryCachePlugin, {
257
+ store,
258
+ ...opts
259
+ });
260
+ trackPlugin("arc-query-cache", opts);
261
+ }
262
+ if (config.arcPlugins?.sse) if (config.arcPlugins?.events === false) fastify.log.warn("SSE plugin requires events plugin (arcPlugins.events). SSE disabled.");
263
+ else {
264
+ const { default: ssePlugin } = await import("./sse-BF7GR7IB.mjs").then((n) => n.r);
265
+ const opts = config.arcPlugins.sse === true ? {} : config.arcPlugins.sse;
266
+ await fastify.register(ssePlugin, opts);
267
+ trackPlugin("arc-sse", opts);
268
+ }
269
+ if (config.arcPlugins?.metrics) {
270
+ const { default: metricsPlugin } = await import("./metrics-Csh4nsvv.mjs").then((n) => n.r);
271
+ const opts = config.arcPlugins.metrics === true ? {} : config.arcPlugins.metrics;
272
+ await fastify.register(metricsPlugin, opts);
273
+ trackPlugin("arc-metrics", opts);
274
+ }
275
+ if (config.arcPlugins?.versioning) {
276
+ const { default: versioningPlugin } = await import("./versioning-BzfeHmhj.mjs").then((n) => n.r);
277
+ await fastify.register(versioningPlugin, config.arcPlugins.versioning);
278
+ trackPlugin("arc-versioning", config.arcPlugins.versioning);
279
+ }
280
+ }
281
+ //#endregion
282
+ //#region src/factory/registerAuth.ts
283
+ /**
284
+ * Decorate request.scope with PUBLIC_SCOPE default.
285
+ * Every request starts as public; auth hooks upgrade it.
286
+ */
287
+ function decorateRequestScope(fastify) {
288
+ fastify.decorateRequest("scope", null);
289
+ fastify.addHook("onRequest", async (request) => {
290
+ if (!request.scope) request.scope = PUBLIC_SCOPE;
291
+ });
292
+ }
293
+ /**
294
+ * Register the configured auth strategy (JWT, Better Auth, Custom, or Authenticator).
295
+ */
296
+ async function registerAuth(fastify, config, trackPlugin) {
297
+ const authConfig = config.auth;
298
+ if (authConfig === false || !authConfig) {
299
+ fastify.log.debug("Authentication disabled");
300
+ return;
301
+ }
302
+ switch (authConfig.type) {
303
+ case "betterAuth": {
304
+ const { plugin, openapi } = authConfig.betterAuth;
305
+ await fastify.register(plugin);
306
+ trackPlugin("auth-better-auth");
307
+ if (openapi && !fastify.arc.externalOpenApiPaths.includes(openapi)) fastify.arc.externalOpenApiPaths.push(openapi);
308
+ fastify.log.debug("Better Auth authentication enabled");
309
+ break;
310
+ }
311
+ case "custom":
312
+ await fastify.register(authConfig.plugin);
313
+ trackPlugin("auth-custom");
314
+ fastify.log.debug("Custom authentication plugin enabled");
315
+ break;
316
+ case "authenticator": {
317
+ const { authenticate, optionalAuthenticate } = authConfig;
318
+ fastify.decorate("authenticate", async (request, reply) => {
319
+ await authenticate(request, reply);
320
+ });
321
+ if (!fastify.hasDecorator("optionalAuthenticate")) if (optionalAuthenticate) fastify.decorate("optionalAuthenticate", async (request, reply) => {
322
+ await optionalAuthenticate(request, reply);
323
+ });
324
+ else fastify.decorate("optionalAuthenticate", createOptionalAuthenticate(authenticate));
325
+ trackPlugin("auth-authenticator");
326
+ fastify.log.debug("Custom authenticator enabled");
327
+ break;
328
+ }
329
+ case "jwt": {
330
+ const { authPlugin } = await import("./auth/index.mjs");
331
+ const { type: _, ...arcAuthOpts } = authConfig;
332
+ await fastify.register(authPlugin, arcAuthOpts);
333
+ trackPlugin("auth-jwt");
334
+ fastify.log.debug("Arc authentication plugin enabled");
335
+ break;
336
+ }
337
+ }
338
+ }
339
+ /**
340
+ * Register elevation plugin (opt-in, runs after auth).
341
+ */
342
+ async function registerElevation(fastify, config, trackPlugin) {
343
+ if (!config.elevation) return;
344
+ const { elevationPlugin } = await import("./elevation-BEdACOLB.mjs").then((n) => n.r);
345
+ await fastify.register(elevationPlugin, config.elevation);
346
+ trackPlugin("arc-elevation", config.elevation);
347
+ fastify.log.debug("Elevation plugin enabled");
348
+ }
349
+ /**
350
+ * Register error handler plugin (opt-out).
351
+ */
352
+ async function registerErrorHandler(fastify, config, trackPlugin) {
353
+ if (config.errorHandler === false) return;
354
+ const { errorHandlerPlugin } = await import("./errorHandler-r2595m8T.mjs").then((n) => n.n);
355
+ const errorOpts = typeof config.errorHandler === "object" ? config.errorHandler : { includeStack: config.preset !== "production" };
356
+ await fastify.register(errorHandlerPlugin, errorOpts);
357
+ trackPlugin("arc-error-handler", errorOpts);
358
+ fastify.log.debug("Arc error handler enabled");
359
+ }
360
+ /**
361
+ * Create an optionalAuthenticate that wraps the main authenticate function.
362
+ * Intercepts 401/403 responses so unauthenticated requests proceed as public.
218
363
  *
219
- * const analyticsResource = defineResource({
220
- * adapter: createMongooseAdapter({ model: AnalyticsModel, repository: analyticsRepo }),
221
- * });
364
+ * Uses a try/catch approach first; falls back to reply proxy only when
365
+ * the authenticator calls reply.code(401).send() instead of throwing.
222
366
  */
223
- var createApp_exports = /* @__PURE__ */ __exportAll({
224
- ArcFactory: () => ArcFactory,
225
- createApp: () => createApp
226
- });
367
+ function createOptionalAuthenticate(authenticate) {
368
+ return async (request, reply) => {
369
+ let intercepted = false;
370
+ const proxyReply = new Proxy(reply, { get(target, prop) {
371
+ if (prop === "code") return (statusCode) => {
372
+ if (statusCode === 401 || statusCode === 403) {
373
+ intercepted = true;
374
+ return new Proxy(target, { get(_t, p) {
375
+ if (p === "send" || p === "type" || p === "header" || p === "headers") return () => proxyReply;
376
+ return Reflect.get(target, p, target);
377
+ } });
378
+ }
379
+ return target.code(statusCode);
380
+ };
381
+ if (prop === "send" && intercepted) return () => proxyReply;
382
+ if (prop === "sent") return intercepted ? false : target.sent;
383
+ return Reflect.get(target, prop, target);
384
+ } });
385
+ try {
386
+ await authenticate(request, proxyReply);
387
+ } catch {}
388
+ };
389
+ }
390
+ //#endregion
391
+ //#region src/factory/registerResources.ts
392
+ /** Register a single resource with descriptive error on failure. */
393
+ async function registerOne(parent, resource) {
394
+ const name = resource.name ?? "unknown";
395
+ try {
396
+ await parent.register(resource.toPlugin());
397
+ } catch (err) {
398
+ const msg = err instanceof Error ? err.message : String(err);
399
+ parent.log.error(`Failed to register resource "${name}": ${msg}`);
400
+ throw new Error(`Resource "${name}" failed to register: ${msg}. Check the resource definition, adapter, and permissions.`);
401
+ }
402
+ }
403
+ /**
404
+ * Execute the full resource lifecycle:
405
+ * 1. plugins() — infra (DB, docs, webhooks)
406
+ * 2. bootstrap[] — domain init (singletons, event handlers)
407
+ * 3. resources[] — auto-discovered routes (split by prefix)
408
+ * 4. afterResources() — post-registration wiring
409
+ * 5. onReady/onClose — lifecycle hooks
410
+ */
411
+ async function registerResources(fastify, config) {
412
+ if (config.plugins) {
413
+ await config.plugins(fastify);
414
+ fastify.log.debug("Custom plugins registered");
415
+ }
416
+ if (config.bootstrap?.length) {
417
+ for (const init of config.bootstrap) await init(fastify);
418
+ fastify.log.debug(`${config.bootstrap.length} bootstrap function(s) executed`);
419
+ }
420
+ if (config.resources?.length) {
421
+ const seen = /* @__PURE__ */ new Set();
422
+ for (const resource of config.resources) if (resource.name) {
423
+ 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.`);
424
+ seen.add(resource.name);
425
+ }
426
+ const prefixed = [];
427
+ const root = [];
428
+ for (const resource of config.resources) if (resource.skipGlobalPrefix) root.push(resource);
429
+ else prefixed.push(resource);
430
+ for (const resource of root) await registerOne(fastify, resource);
431
+ if (prefixed.length) if (config.resourcePrefix) await fastify.register(async (scoped) => {
432
+ for (const resource of prefixed) await registerOne(scoped, resource);
433
+ }, { prefix: config.resourcePrefix });
434
+ else for (const resource of prefixed) await registerOne(fastify, resource);
435
+ const names = config.resources.map((r) => r.name ?? "?").join(", ");
436
+ const prefix = config.resourcePrefix ? ` (prefix: ${config.resourcePrefix})` : "";
437
+ fastify.log.info(`${config.resources.length} resource(s) registered${prefix}: ${names}`);
438
+ }
439
+ if (config.afterResources) {
440
+ await config.afterResources(fastify);
441
+ fastify.log.debug("afterResources hook executed");
442
+ }
443
+ if (config.onReady) {
444
+ const onReady = config.onReady;
445
+ fastify.addHook("onReady", async () => {
446
+ await onReady(fastify);
447
+ });
448
+ }
449
+ if (config.onClose) {
450
+ const onClose = config.onClose;
451
+ fastify.addHook("onClose", async () => {
452
+ await onClose(fastify);
453
+ });
454
+ }
455
+ }
456
+ //#endregion
457
+ //#region src/factory/registerSecurity.ts
227
458
  const PLUGIN_REGISTRY = {
228
459
  cors: {
229
460
  package: "@fastify/cors",
@@ -256,6 +487,7 @@ const PLUGIN_REGISTRY = {
256
487
  optional: true
257
488
  }
258
489
  };
490
+ /** Load a plugin from the registry with helpful error messages. */
259
491
  async function loadPlugin(name, logger) {
260
492
  const entry = PLUGIN_REGISTRY[name];
261
493
  if (!entry) throw new Error(`Unknown plugin: ${name}`);
@@ -273,81 +505,10 @@ async function loadPlugin(name, logger) {
273
505
  }
274
506
  }
275
507
  /**
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
508
+ * Register security plugins (Helmet, CORS, Rate Limiting).
509
+ * All enabled by default — set to `false` to opt out.
289
510
  */
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
- });
511
+ async function registerSecurityPlugins(fastify, config) {
351
512
  if (config.helmet !== false) {
352
513
  const helmet = await loadPlugin("helmet");
353
514
  await fastify.register(helmet, config.helmet ?? {});
@@ -369,19 +530,22 @@ async function createApp(options) {
369
530
  };
370
531
  await fastify.register(rateLimit, rateLimitOpts);
371
532
  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({ ... }) }");
533
+ 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");
534
+ 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
535
  }
377
536
  fastify.log.debug("Rate limiting enabled");
378
537
  } else fastify.log.warn("Rate limiting disabled");
538
+ }
539
+ /**
540
+ * Register performance and utility plugins (Under Pressure, Sensible, Multipart, Raw Body).
541
+ */
542
+ async function registerUtilityPlugins(fastify, config) {
379
543
  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
544
  if (config.underPressure !== false) {
381
545
  const underPressure = await loadPlugin("underPressure");
382
546
  await fastify.register(underPressure, config.underPressure ?? { exposeStatusRoute: true });
383
547
  fastify.log.debug("Health monitoring (under-pressure) enabled");
384
- } else fastify.log.debug("Health monitoring disabled");
548
+ }
385
549
  if (config.sensible !== false) {
386
550
  const sensible = await loadPlugin("sensible");
387
551
  await fastify.register(sensible);
@@ -414,9 +578,135 @@ async function createApp(options) {
414
578
  fastify.log.debug("Raw body parsing enabled");
415
579
  }
416
580
  }
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 */
581
+ }
582
+ //#endregion
583
+ //#region src/factory/createApp.ts
584
+ /**
585
+ * ArcFactory - Production-ready Fastify application factory
586
+ *
587
+ * Enforces security best practices by making plugins opt-out instead of opt-in.
588
+ * A developer must explicitly disable security features rather than forget to enable them.
589
+ *
590
+ * Note: Arc is database-agnostic. Connect your database separately and provide
591
+ * adapters when defining resources. This allows multiple databases, custom
592
+ * connection pooling, and full control over your data layer.
593
+ *
594
+ * @example
595
+ * // 1. Connect your database(s) separately
596
+ * import mongoose from 'mongoose';
597
+ * await mongoose.connect(process.env.MONGO_URI);
598
+ *
599
+ * // 2. Create Arc app with resources
600
+ * const app = await createApp({
601
+ * preset: 'production',
602
+ * auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
603
+ * cors: { origin: ['https://example.com'] },
604
+ * resources: [productResource, orderResource],
605
+ * });
606
+ *
607
+ * @example
608
+ * // Multiple databases example
609
+ * const primaryDb = await mongoose.connect(process.env.PRIMARY_DB);
610
+ * const analyticsDb = mongoose.createConnection(process.env.ANALYTICS_DB);
611
+ *
612
+ * const orderResource = defineResource({
613
+ * adapter: createMongooseAdapter({ model: OrderModel, repository: orderRepo }),
614
+ * });
615
+ *
616
+ * const analyticsResource = defineResource({
617
+ * adapter: createMongooseAdapter({ model: AnalyticsModel, repository: analyticsRepo }),
618
+ * });
619
+ */
620
+ var createApp_exports = /* @__PURE__ */ __exportAll({
621
+ ArcFactory: () => ArcFactory,
622
+ createApp: () => createApp
623
+ });
624
+ const MEMORY_STORE_NAMES = new Set(["memory", "memory-cache"]);
625
+ function validateAuthOptions(options) {
626
+ const authConfig = options.auth;
627
+ if (authConfig === false || !authConfig) return;
628
+ 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 } }");
629
+ }
630
+ function validateDistributedRuntime(options) {
631
+ const deferredWarnings = [];
632
+ if (options.runtime !== "distributed") return deferredWarnings;
633
+ const missing = [];
634
+ const events = options.stores?.events;
635
+ if (!events || MEMORY_STORE_NAMES.has(events.name)) missing.push("events transport");
636
+ if (options.arcPlugins?.caching) {
637
+ const cache = options.stores?.cache;
638
+ if (!cache || MEMORY_STORE_NAMES.has(cache.name)) missing.push("cache store");
639
+ }
640
+ const idempotency = options.stores?.idempotency;
641
+ if (idempotency && MEMORY_STORE_NAMES.has(idempotency.name)) missing.push("idempotency store (memory-backed in distributed mode)");
642
+ 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.");
643
+ if (options.arcPlugins?.queryCache) {
644
+ const qc = options.stores?.queryCache;
645
+ if (!qc || MEMORY_STORE_NAMES.has(qc.name)) missing.push("queryCache store");
646
+ }
647
+ 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.`);
648
+ return deferredWarnings;
649
+ }
650
+ /**
651
+ * Create a production-ready Fastify application with Arc framework.
652
+ *
653
+ * Boot order:
654
+ * ```
655
+ * 0. Logger, validation, preset merge
656
+ * 1. Create Fastify instance
657
+ * 2. Security plugins (Helmet, CORS, Rate Limit) — opt-out
658
+ * 3. Utility plugins (Under Pressure, Sensible, Multipart, Raw Body)
659
+ * 4. Arc core (fastify.arc, events)
660
+ * 5. Arc plugins (requestId, health, caching, SSE, metrics, versioning)
661
+ * 6. Auth (scope decoration, auth strategy, elevation, error handler)
662
+ * 7. plugins() — user infra (DB, docs, webhooks)
663
+ * 8. bootstrap[] — domain init (singletons, event handlers)
664
+ * 9. resources[] — auto-discovered routes (prefix + skipGlobalPrefix)
665
+ * 10. afterResources() — post-registration wiring
666
+ * 11. onReady/onClose — lifecycle hooks
667
+ * ```
668
+ */
669
+ async function createApp(options) {
670
+ if (options.debug !== void 0 && options.debug !== false) {
671
+ const { configureArcLogger } = await import("./logger-Dz3j1ItV.mjs").then((n) => n.r);
672
+ configureArcLogger({ debug: options.debug });
673
+ }
674
+ validateAuthOptions(options);
675
+ const deferredWarnings = validateDistributedRuntime(options);
676
+ const config = {
677
+ ...options.preset ? getPreset(options.preset) : {},
678
+ ...options
679
+ };
680
+ const fastify = Fastify({
681
+ logger: config.logger ?? true,
682
+ trustProxy: config.trustProxy ?? false,
683
+ routerOptions: { querystringParser: (str) => qs.parse(str) },
684
+ ajv: { customOptions: {
685
+ coerceTypes: true,
686
+ useDefaults: true,
687
+ removeAdditional: false,
688
+ keywords: ["example", ...config.ajv?.keywords ?? []]
689
+ } }
690
+ });
691
+ for (const warning of deferredWarnings) fastify.log.warn(warning);
692
+ if (config.typeProvider === "typebox") try {
693
+ const { TypeBoxValidatorCompiler } = await import("@fastify/type-provider-typebox");
694
+ fastify.setValidatorCompiler(TypeBoxValidatorCompiler);
695
+ } catch {
696
+ fastify.log.warn("typeProvider: \"typebox\" requested but @fastify/type-provider-typebox is not installed.");
697
+ }
698
+ const sjp = await import("secure-json-parse");
699
+ fastify.removeContentTypeParser("application/json");
700
+ fastify.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
701
+ if (!body || body.length === 0) return done(null, void 0);
702
+ try {
703
+ done(null, sjp.parse(body));
704
+ } catch (err) {
705
+ done(err);
706
+ }
707
+ });
708
+ await registerSecurityPlugins(fastify, config);
709
+ await registerUtilityPlugins(fastify, config);
420
710
  const trackPlugin = (name, opts) => {
421
711
  fastify.arc.plugins.set(name, {
422
712
  name,
@@ -424,162 +714,13 @@ async function createApp(options) {
424
714
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
425
715
  });
426
716
  };
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-XtFplYO9.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-BFAYkf8H.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-DMbGdzBG.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";
717
+ await registerArcPlugins(fastify, config, trackPlugin, await registerArcCore(fastify, config, trackPlugin));
718
+ decorateRequestScope(fastify);
719
+ await registerAuth(fastify, config, trackPlugin);
720
+ await registerElevation(fastify, config, trackPlugin);
721
+ await registerErrorHandler(fastify, config, trackPlugin);
722
+ await registerResources(fastify, config);
723
+ const authMode = config.auth === false ? "none" : config.auth ? config.auth.type : "none";
583
724
  fastify.log.info({
584
725
  preset: config.preset ?? "custom",
585
726
  runtime: config.runtime ?? "memory",
@@ -614,4 +755,4 @@ const ArcFactory = {
614
755
  }
615
756
  };
616
757
  //#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 };
758
+ 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 };