@classytic/arc 2.9.1 → 2.10.8

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 (132) hide show
  1. package/README.md +20 -91
  2. package/dist/{BaseController-Vu2yc56T.mjs → BaseController-DVNKvoX4.mjs} +154 -170
  3. package/dist/{ResourceRegistry-Dq3_zBQP.mjs → ResourceRegistry-CcN2LVrc.mjs} +1 -1
  4. package/dist/actionPermissions-TUVR3uiZ.mjs +22 -0
  5. package/dist/adapters/index.d.mts +3 -3
  6. package/dist/adapters/index.mjs +2 -2
  7. package/dist/{adapters-BBqAVvPK.mjs → adapters-BXY4i-hw.mjs} +210 -41
  8. package/dist/audit/index.d.mts +38 -3
  9. package/dist/audit/index.mjs +54 -22
  10. package/dist/auth/index.d.mts +2 -2
  11. package/dist/auth/index.mjs +3 -3
  12. package/dist/cache/index.d.mts +17 -15
  13. package/dist/cache/index.mjs +16 -15
  14. package/dist/{caching-CjybdRwx.mjs → caching-3h93rkJM.mjs} +8 -3
  15. package/dist/cli/commands/describe.mjs +1 -1
  16. package/dist/cli/commands/docs.mjs +2 -2
  17. package/dist/cli/commands/init.mjs +1 -1
  18. package/dist/cli/commands/introspect.mjs +1 -1
  19. package/dist/context/index.d.mts +58 -0
  20. package/dist/context/index.mjs +2 -0
  21. package/dist/core/index.d.mts +2 -2
  22. package/dist/core/index.mjs +3 -4
  23. package/dist/{defineResource-C__jkwvs.mjs → core-3MWJosCH.mjs} +174 -94
  24. package/dist/{createActionRouter-DH1YFL9m.mjs → createActionRouter-C8UUB3Px.mjs} +1 -1
  25. package/dist/{createApp-CBJUJKGP.mjs → createApp-BwnEAO2h.mjs} +53 -19
  26. package/dist/docs/index.d.mts +1 -1
  27. package/dist/docs/index.mjs +2 -2
  28. package/dist/{elevation-DxQ6ACbt.mjs → elevation-Dci0AYLT.mjs} +2 -2
  29. package/dist/errorHandler-2ii4RIYr.d.mts +114 -0
  30. package/dist/{errorHandler-CZDW4EXS.mjs → errorHandler-CSxe7KIM.mjs} +1 -1
  31. package/dist/{eventPlugin-Dl7MoVWH.mjs → eventPlugin-ByU4Cv0e.mjs} +1 -1
  32. package/dist/{eventPlugin-BxvaCIZF.d.mts → eventPlugin-D1ThQ1Pp.d.mts} +1 -1
  33. package/dist/events/index.d.mts +8 -5
  34. package/dist/events/index.mjs +87 -52
  35. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  36. package/dist/events/transports/redis.d.mts +1 -1
  37. package/dist/factory/index.d.mts +1 -1
  38. package/dist/factory/index.mjs +1 -1
  39. package/dist/{types-DZi1aYhm.d.mts → fields-C8Y0XLAu.d.mts} +122 -2
  40. package/dist/hooks/index.d.mts +1 -1
  41. package/dist/idempotency/index.d.mts +5 -2
  42. package/dist/idempotency/index.mjs +46 -37
  43. package/dist/{interface-YrWsmKqE.d.mts → index-BGbpGVyM.d.mts} +2107 -2756
  44. package/dist/{index-CtGKT0lf.d.mts → index-BziRPS4H.d.mts} +81 -7
  45. package/dist/{index-C-xjcA6F.d.mts → index-C_Noptz-.d.mts} +284 -409
  46. package/dist/{index-Cibkchnx.d.mts → index-EqQN6p0W.d.mts} +3 -3
  47. package/dist/index.d.mts +6 -219
  48. package/dist/index.mjs +10 -131
  49. package/dist/integrations/event-gateway.d.mts +1 -1
  50. package/dist/integrations/event-gateway.mjs +1 -1
  51. package/dist/integrations/index.d.mts +1 -1
  52. package/dist/integrations/mcp/index.d.mts +2 -2
  53. package/dist/integrations/mcp/index.mjs +1 -1
  54. package/dist/integrations/mcp/testing.d.mts +1 -1
  55. package/dist/integrations/mcp/testing.mjs +1 -1
  56. package/dist/interface-yhyb_pLY.d.mts +77 -0
  57. package/dist/logger/index.d.mts +81 -0
  58. package/dist/{logger-CDjpjySd.mjs → logger/index.mjs} +1 -6
  59. package/dist/{memory-BFAYkf8H.mjs → memory-DqI-449b.mjs} +23 -8
  60. package/dist/middleware/index.d.mts +109 -0
  61. package/dist/middleware/index.mjs +70 -0
  62. package/dist/multipartBody-CUQGVlM_.mjs +123 -0
  63. package/dist/{openapi-CXuTG1M9.mjs → openapi-DpNpqBmo.mjs} +9 -7
  64. package/dist/org/index.d.mts +2 -2
  65. package/dist/permissions/index.d.mts +3 -4
  66. package/dist/permissions/index.mjs +5 -5
  67. package/dist/{permissions-oNZawnkR.mjs → permissions-wkqRwicB.mjs} +315 -397
  68. package/dist/pipe-CGJxqDGx.mjs +62 -0
  69. package/dist/pipeline/index.d.mts +62 -0
  70. package/dist/pipeline/index.mjs +53 -0
  71. package/dist/plugins/index.d.mts +23 -3
  72. package/dist/plugins/index.mjs +9 -11
  73. package/dist/plugins/response-cache.mjs +1 -1
  74. package/dist/plugins/tracing-entry.mjs +1 -1
  75. package/dist/presets/filesUpload.d.mts +3 -3
  76. package/dist/presets/filesUpload.mjs +255 -1
  77. package/dist/presets/index.d.mts +1 -1
  78. package/dist/presets/index.mjs +2 -2
  79. package/dist/presets/multiTenant.d.mts +1 -1
  80. package/dist/presets/multiTenant.mjs +43 -9
  81. package/dist/presets/search.d.mts +91 -4
  82. package/dist/presets/search.mjs +1 -1
  83. package/dist/{presets-hM4WhNWY.mjs → presets-CrwOvuXI.mjs} +1 -1
  84. package/dist/{queryCachePlugin-DbUVroUG.mjs → queryCachePlugin-ChLNZvFT.mjs} +9 -9
  85. package/dist/{queryCachePlugin-CnTZZTC5.d.mts → queryCachePlugin-Dumka73q.d.mts} +1 -1
  86. package/dist/{queryParser-Cs-6SHQK.mjs → queryParser-NR__Qiju.mjs} +69 -2
  87. package/dist/{redis-stream-Bz-4q96t.d.mts → redis-stream-bkO88VHx.d.mts} +1 -1
  88. package/dist/registry/index.d.mts +1 -1
  89. package/dist/registry/index.mjs +1 -1
  90. package/dist/{requestContext-DYtmNpm5.mjs → requestContext-C38GskNt.mjs} +1 -1
  91. package/dist/{resourceToTools-C3cWymnW.mjs → resourceToTools-BhF3JV5p.mjs} +8 -3
  92. package/dist/scope/index.d.mts +2 -2
  93. package/dist/scope/index.mjs +2 -2
  94. package/dist/{sse-CJpt7LGI.mjs → sse-D8UeDwis.mjs} +1 -1
  95. package/dist/{store-helpers-DFiZl5TL.mjs → store-helpers-DYYUQbQN.mjs} +4 -0
  96. package/dist/testing/index.d.mts +6 -5
  97. package/dist/testing/index.mjs +17 -10
  98. package/dist/types/index.d.mts +5 -5
  99. package/dist/types/index.mjs +1 -31
  100. package/dist/types-CDnTEpga.mjs +27 -0
  101. package/dist/{types-CoSzA-s-.d.mts → types-CVKBssX5.d.mts} +1 -1
  102. package/dist/{types-CunEX4UX.d.mts → types-CVdgPXBW.d.mts} +20 -7
  103. package/dist/utils/index.d.mts +277 -3
  104. package/dist/utils/index.mjs +4 -5
  105. package/dist/{utils-B7FuRr9w.mjs → utils-LMwVidKy.mjs} +303 -2
  106. package/dist/{versioning-Cm8qoFDg.mjs → versioning-B6mimogM.mjs} +3 -5
  107. package/dist/versioning-CeUXHfjw.d.mts +117 -0
  108. package/package.json +31 -18
  109. package/skills/arc/SKILL.md +8 -12
  110. package/skills/arc/references/production.md +0 -41
  111. package/dist/circuitBreaker-CvXkjfrW.d.mts +0 -206
  112. package/dist/circuitBreaker-l18oRgL5.mjs +0 -284
  113. package/dist/core-DNncu0xF.mjs +0 -34
  114. package/dist/dynamic/index.d.mts +0 -93
  115. package/dist/dynamic/index.mjs +0 -122
  116. package/dist/errorHandler-DixGcttC.d.mts +0 -218
  117. package/dist/fields-BC7zcmI9.d.mts +0 -121
  118. package/dist/filesUpload-q8oHt--L.mjs +0 -377
  119. package/dist/interface-DplgQO2e.d.mts +0 -54
  120. package/dist/policies/index.d.mts +0 -425
  121. package/dist/policies/index.mjs +0 -318
  122. package/dist/rpc/index.d.mts +0 -90
  123. package/dist/rpc/index.mjs +0 -248
  124. /package/dist/{EventTransport-CqZ8FyM_.d.mts → EventTransport-CfVEGaEl.d.mts} +0 -0
  125. /package/dist/{applyPermissionResult-bqGpo9ML.mjs → applyPermissionResult-QhV1Pa-g.mjs} +0 -0
  126. /package/dist/{constants-Cxde4rpC.mjs → constants-BhY1OHoH.mjs} +0 -0
  127. /package/dist/{elevation-B6S5csVA.d.mts → elevation-s5ykdNHr.d.mts} +0 -0
  128. /package/dist/{errors-CqWnSqM-.mjs → errors-BqdUDja_.mjs} +0 -0
  129. /package/dist/{fields-CU6FlaDV.mjs → fields-CTMWOUDt.mjs} +0 -0
  130. /package/dist/{keys-qcD-TVJl.mjs → keys-nWQGUTu1.mjs} +0 -0
  131. /package/dist/{types-ZUu_h0jp.mjs → types-D57iXYb8.mjs} +0 -0
  132. /package/dist/{types-BD85MlEK.d.mts → types-tgR4Pt8F.d.mts} +0 -0
@@ -0,0 +1,62 @@
1
+ import { r as ForbiddenError } from "./errors-BqdUDja_.mjs";
2
+ //#region src/pipeline/pipe.ts
3
+ /**
4
+ * Compose pipeline steps into an ordered array.
5
+ * Accepts guards, transforms, and interceptors in any order.
6
+ */
7
+ function pipe(...steps) {
8
+ return steps;
9
+ }
10
+ /**
11
+ * Check if a step applies to the given operation.
12
+ */
13
+ function appliesTo(step, operation) {
14
+ if (!step.operations || step.operations.length === 0) return true;
15
+ return step.operations.includes(operation);
16
+ }
17
+ /**
18
+ * Execute a pipeline against a request context.
19
+ *
20
+ * This is the core runtime that createCrudRouter uses to execute pipelines.
21
+ * External usage is not needed — this is wired automatically when `pipe` is set.
22
+ *
23
+ * @param steps - Pipeline steps to execute
24
+ * @param ctx - The pipeline context (extends IRequestContext)
25
+ * @param handler - The actual controller method to call
26
+ * @param operation - The CRUD operation name
27
+ * @returns The controller response (possibly modified by interceptors)
28
+ */
29
+ async function executePipeline(steps, ctx, handler, operation) {
30
+ const guards = [];
31
+ const transforms = [];
32
+ const interceptors = [];
33
+ for (const step of steps) {
34
+ if (!appliesTo(step, operation)) continue;
35
+ switch (step._type) {
36
+ case "guard":
37
+ guards.push(step);
38
+ break;
39
+ case "transform":
40
+ transforms.push(step);
41
+ break;
42
+ case "interceptor":
43
+ interceptors.push(step);
44
+ break;
45
+ }
46
+ }
47
+ for (const g of guards) if (!await g.handler(ctx)) throw new ForbiddenError(`Guard '${g.name}' denied access`);
48
+ let currentCtx = ctx;
49
+ for (const t of transforms) {
50
+ const result = await t.handler(currentCtx);
51
+ if (result) currentCtx = result;
52
+ }
53
+ let chain = () => handler(currentCtx);
54
+ for (let i = interceptors.length - 1; i >= 0; i--) {
55
+ const interceptor = interceptors[i];
56
+ const next = chain;
57
+ chain = () => interceptor.handler(currentCtx, next);
58
+ }
59
+ return chain();
60
+ }
61
+ //#endregion
62
+ export { pipe as n, executePipeline as t };
@@ -0,0 +1,62 @@
1
+ import { Bt as PipelineContext, Ft as Guard, Ht as Transform, It as Interceptor, Lt as NextFunction, Mt as IControllerResponse, Rt as OperationFilter, Vt as PipelineStep, zt as PipelineConfig } from "../index-BGbpGVyM.mjs";
2
+
3
+ //#region src/pipeline/guard.d.ts
4
+ interface GuardOptions {
5
+ operations?: OperationFilter;
6
+ handler: (ctx: PipelineContext) => boolean | Promise<boolean>;
7
+ }
8
+ /**
9
+ * Create a named guard.
10
+ *
11
+ * @param name - Guard name (for debugging/introspection)
12
+ * @param handlerOrOptions - Handler function or options object
13
+ */
14
+ declare function guard(name: string, handlerOrOptions: ((ctx: PipelineContext) => boolean | Promise<boolean>) | GuardOptions): Guard;
15
+ //#endregion
16
+ //#region src/pipeline/intercept.d.ts
17
+ interface InterceptOptions {
18
+ operations?: OperationFilter;
19
+ handler: (ctx: PipelineContext, next: NextFunction) => Promise<IControllerResponse<unknown>>;
20
+ }
21
+ /**
22
+ * Create a named interceptor.
23
+ *
24
+ * @param name - Interceptor name (for debugging/introspection)
25
+ * @param handlerOrOptions - Handler function or options object
26
+ */
27
+ declare function intercept(name: string, handlerOrOptions: ((ctx: PipelineContext, next: NextFunction) => Promise<IControllerResponse<unknown>>) | InterceptOptions): Interceptor;
28
+ //#endregion
29
+ //#region src/pipeline/pipe.d.ts
30
+ /**
31
+ * Compose pipeline steps into an ordered array.
32
+ * Accepts guards, transforms, and interceptors in any order.
33
+ */
34
+ declare function pipe(...steps: PipelineStep[]): PipelineStep[];
35
+ /**
36
+ * Execute a pipeline against a request context.
37
+ *
38
+ * This is the core runtime that createCrudRouter uses to execute pipelines.
39
+ * External usage is not needed — this is wired automatically when `pipe` is set.
40
+ *
41
+ * @param steps - Pipeline steps to execute
42
+ * @param ctx - The pipeline context (extends IRequestContext)
43
+ * @param handler - The actual controller method to call
44
+ * @param operation - The CRUD operation name
45
+ * @returns The controller response (possibly modified by interceptors)
46
+ */
47
+ declare function executePipeline(steps: PipelineStep[], ctx: PipelineContext, handler: (ctx: PipelineContext) => Promise<IControllerResponse<unknown>>, operation: string): Promise<IControllerResponse<unknown>>;
48
+ //#endregion
49
+ //#region src/pipeline/transform.d.ts
50
+ interface TransformOptions {
51
+ operations?: OperationFilter;
52
+ handler: (ctx: PipelineContext) => PipelineContext | undefined | Promise<PipelineContext | undefined>;
53
+ }
54
+ /**
55
+ * Create a named transform.
56
+ *
57
+ * @param name - Transform name (for debugging/introspection)
58
+ * @param handlerOrOptions - Handler function or options object
59
+ */
60
+ declare function transform(name: string, handlerOrOptions: ((ctx: PipelineContext) => PipelineContext | undefined | Promise<PipelineContext | undefined>) | TransformOptions): Transform;
61
+ //#endregion
62
+ export { type Guard, type Interceptor, type NextFunction, type OperationFilter, type PipelineConfig, type PipelineContext, type PipelineStep, type Transform, executePipeline, guard, intercept, pipe, transform };
@@ -0,0 +1,53 @@
1
+ import { n as pipe, t as executePipeline } from "../pipe-CGJxqDGx.mjs";
2
+ //#region src/pipeline/guard.ts
3
+ /**
4
+ * Create a named guard.
5
+ *
6
+ * @param name - Guard name (for debugging/introspection)
7
+ * @param handlerOrOptions - Handler function or options object
8
+ */
9
+ function guard(name, handlerOrOptions) {
10
+ const opts = typeof handlerOrOptions === "function" ? { handler: handlerOrOptions } : handlerOrOptions;
11
+ return {
12
+ _type: "guard",
13
+ name,
14
+ operations: opts.operations,
15
+ handler: opts.handler
16
+ };
17
+ }
18
+ //#endregion
19
+ //#region src/pipeline/intercept.ts
20
+ /**
21
+ * Create a named interceptor.
22
+ *
23
+ * @param name - Interceptor name (for debugging/introspection)
24
+ * @param handlerOrOptions - Handler function or options object
25
+ */
26
+ function intercept(name, handlerOrOptions) {
27
+ const opts = typeof handlerOrOptions === "function" ? { handler: handlerOrOptions } : handlerOrOptions;
28
+ return {
29
+ _type: "interceptor",
30
+ name,
31
+ operations: opts.operations,
32
+ handler: opts.handler
33
+ };
34
+ }
35
+ //#endregion
36
+ //#region src/pipeline/transform.ts
37
+ /**
38
+ * Create a named transform.
39
+ *
40
+ * @param name - Transform name (for debugging/introspection)
41
+ * @param handlerOrOptions - Handler function or options object
42
+ */
43
+ function transform(name, handlerOrOptions) {
44
+ const opts = typeof handlerOrOptions === "function" ? { handler: handlerOrOptions } : handlerOrOptions;
45
+ return {
46
+ _type: "transform",
47
+ name,
48
+ operations: opts.operations,
49
+ handler: opts.handler
50
+ };
51
+ }
52
+ //#endregion
53
+ export { executePipeline, guard, intercept, pipe, transform };
@@ -1,6 +1,7 @@
1
- import { $ as PresetHook, G as MiddlewareConfig, Gt as ResourceRegistry, Tn as HookSystem, _t as RouteSchemaOptions, mt as RouteDefinition, p as AnyRecord } from "../interface-YrWsmKqE.mjs";
1
+ import { Dt as RouteSchemaOptions, J as HookSystem, Yt as AnyRecord, gt as PresetHook, pt as MiddlewareConfig, wt as RouteDefinition, z as ResourceRegistry } from "../index-BGbpGVyM.mjs";
2
2
  import { t as ExternalOpenApiPaths } from "../externalPaths-Bapitwvd.mjs";
3
- import { _ as CachingRule, a as VersioningOptions, c as MetricEntry, d as _default$4, f as metricsPlugin, g as CachingOptions, h as ssePlugin, i as errorHandlerPlugin, l as MetricsCollector, m as _default$6, n as ErrorMapper, o as _default$7, p as SSEOptions, r as defaultIsDuplicateKeyError, s as versioningPlugin, t as ErrorHandlerOptions, u as MetricsOptions, v as _default$1, y as cachingPlugin } from "../errorHandler-DixGcttC.mjs";
3
+ import { a as MetricsCollector, c as metricsPlugin, d as ssePlugin, f as CachingOptions, h as cachingPlugin, i as MetricEntry, l as SSEOptions, m as _default$1, n as _default$7, o as MetricsOptions, p as CachingRule, r as versioningPlugin, s as _default$4, t as VersioningOptions, u as _default$6 } from "../versioning-CeUXHfjw.mjs";
4
+ import { i as errorHandlerPlugin, n as ErrorMapper, r as defaultIsDuplicateKeyError, t as ErrorHandlerOptions } from "../errorHandler-2ii4RIYr.mjs";
4
5
  import { t as TracingOptions } from "../tracing-xqXzWeaf.mjs";
5
6
  import { FastifyInstance, FastifyPluginAsync } from "fastify";
6
7
  import * as _$node_stream0 from "node:stream";
@@ -34,7 +35,26 @@ interface ArcCore {
34
35
  }
35
36
  declare module "fastify" {
36
37
  interface FastifyInstance {
37
- arc: ArcCore;
38
+ /**
39
+ * Arc core decorator. Optional because:
40
+ *
41
+ * 1. Consumers who import any `@classytic/arc/*` subpath get this module
42
+ * augmentation merged into every `FastifyInstance` they see — even
43
+ * instances on apps that never register {@link arcCorePlugin}. Making
44
+ * it required would force those apps to assert or guard every
45
+ * property access.
46
+ * 2. Hosts that extend `FastifyInstance` to narrow `arc` to their own
47
+ * type (`interface X extends FastifyInstance { arc?: MyArc }`) were
48
+ * previously blocked because non-optional `arc: ArcCore` conflicts
49
+ * with the re-declaration. An optional field lets hosts re-declare
50
+ * with a compatible subtype without fighting TS.
51
+ *
52
+ * Inside Arc's own code, any call site that runs *after* `arcCorePlugin`
53
+ * has registered treats this as non-null — see the call sites in
54
+ * `factory/registerAuth.ts` and `factory/createApp.ts` which assert
55
+ * with `fastify.arc!` or narrow explicitly.
56
+ */
57
+ arc?: ArcCore;
38
58
  }
39
59
  }
40
60
  declare const arcCorePlugin: FastifyPluginAsync<ArcCorePluginOptions>;
@@ -1,15 +1,15 @@
1
- import { p as MUTATION_OPERATIONS } from "../constants-Cxde4rpC.mjs";
1
+ import { p as MUTATION_OPERATIONS } from "../constants-BhY1OHoH.mjs";
2
2
  import { o as getOrgId } from "../types-AOD8fxIw.mjs";
3
- import { t as requestContext } from "../requestContext-DYtmNpm5.mjs";
3
+ import { t as requestContext } from "../requestContext-C38GskNt.mjs";
4
4
  import { t as hasEvents } from "../typeGuards-Cj5Rgvlg.mjs";
5
5
  import { t as HookSystem } from "../HookSystem-BjFu7zf1.mjs";
6
- import { t as ResourceRegistry } from "../ResourceRegistry-Dq3_zBQP.mjs";
7
- import { n as caching_default, t as cachingPlugin } from "../caching-CjybdRwx.mjs";
8
- import { n as errorHandlerPlugin, t as defaultIsDuplicateKeyError } from "../errorHandler-CZDW4EXS.mjs";
6
+ import { t as ResourceRegistry } from "../ResourceRegistry-CcN2LVrc.mjs";
7
+ import { n as caching_default, t as cachingPlugin } from "../caching-3h93rkJM.mjs";
8
+ import { n as errorHandlerPlugin, t as defaultIsDuplicateKeyError } from "../errorHandler-CSxe7KIM.mjs";
9
9
  import { n as metrics_default, t as metricsPlugin } from "../metrics-TuOmguhi.mjs";
10
10
  import { t as replyHelpersPlugin } from "../replyHelpers-BLojtuvR.mjs";
11
- import { n as sse_default, t as ssePlugin } from "../sse-CJpt7LGI.mjs";
12
- import { n as versioning_default, t as versioningPlugin } from "../versioning-Cm8qoFDg.mjs";
11
+ import { n as sse_default, t as ssePlugin } from "../sse-D8UeDwis.mjs";
12
+ import { n as versioning_default, t as versioningPlugin } from "../versioning-B6mimogM.mjs";
13
13
  import { randomUUID } from "node:crypto";
14
14
  import fp from "fastify-plugin";
15
15
  //#region src/core/arcCorePlugin.ts
@@ -393,15 +393,13 @@ var health_default = fp(healthPlugin, {
393
393
  const requestIdPlugin = async (fastify, opts = {}) => {
394
394
  const { header = "x-request-id", generator = randomUUID, setResponseHeader = true } = opts;
395
395
  if (!fastify.hasRequestDecorator("requestId")) fastify.decorateRequest("requestId", "");
396
- fastify.addHook("onRequest", async (request) => {
396
+ fastify.addHook("onRequest", async (request, reply) => {
397
397
  const incomingId = request.headers[header];
398
398
  const sanitized = typeof incomingId === "string" ? incomingId.trim() : "";
399
399
  const requestId = sanitized.length > 0 && sanitized.length <= 128 && /^[\w.:-]+$/.test(sanitized) ? sanitized : generator();
400
400
  request.id = requestId;
401
401
  request.requestId = requestId;
402
- });
403
- if (setResponseHeader) fastify.addHook("onSend", async (request, reply) => {
404
- reply.header(header, request.requestId);
402
+ if (setResponseHeader) reply.header(header, requestId);
405
403
  });
406
404
  fastify.log?.debug?.("Request ID plugin registered");
407
405
  };
@@ -145,7 +145,7 @@ const responseCachePluginImpl = async (fastify, opts = {}) => {
145
145
  request.__arcCacheTTL = 0;
146
146
  reply.code(entry.statusCode).send(entry.body);
147
147
  };
148
- fastify.addHook("onSend", async (request, reply, payload) => {
148
+ fastify.addHook("preSerialization", async (request, reply, payload) => {
149
149
  const ttl = request.__arcCacheTTL;
150
150
  if (!ttl || ttl <= 0) return payload;
151
151
  if (request.method !== "GET" && request.method !== "HEAD") return payload;
@@ -44,7 +44,7 @@ try {
44
44
  function createTracerProvider(options) {
45
45
  if (!isAvailable) return null;
46
46
  const { serviceName = "@classytic/arc", serviceVersion, exporterUrl = "http://localhost:4318/v1/traces" } = options;
47
- const resolvedVersion = serviceVersion ?? "2.9.1";
47
+ const resolvedVersion = serviceVersion ?? "2.10.8";
48
48
  const exporter = new OTLPTraceExporter({ url: exporterUrl });
49
49
  const provider = new NodeTracerProvider({ resource: { attributes: {
50
50
  "service.name": serviceName,
@@ -1,6 +1,6 @@
1
- import { r as RequestScope } from "../types-BD85MlEK.mjs";
2
- import { et as PresetResult } from "../interface-YrWsmKqE.mjs";
3
- import { t as PermissionCheck } from "../types-DZi1aYhm.mjs";
1
+ import { _t as PresetResult } from "../index-BGbpGVyM.mjs";
2
+ import { r as RequestScope } from "../types-tgR4Pt8F.mjs";
3
+ import { c as PermissionCheck } from "../fields-C8Y0XLAu.mjs";
4
4
  import { a as StorageReadResult, i as StorageReadRange, n as StorageContext, o as StorageUploadInput, r as StorageFile, t as Storage } from "../storage-BwGQXUpd.mjs";
5
5
 
6
6
  //#region src/presets/filesUpload.d.ts
@@ -1,2 +1,256 @@
1
- import { t as filesUploadPreset } from "../filesUpload-q8oHt--L.mjs";
1
+ import { o as getOrgId, p as getUserId } from "../types-AOD8fxIw.mjs";
2
+ import { i as NotFoundError, u as ValidationError } from "../errors-BqdUDja_.mjs";
3
+ import { C as requireAuth, y as allowPublic } from "../permissions-wkqRwicB.mjs";
4
+ import { t as multipartBody } from "../multipartBody-CUQGVlM_.mjs";
5
+ //#region src/presets/filesUpload.ts
6
+ const DEFAULT_FIELD_NAME = "file";
7
+ const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
8
+ function defaultContextFrom(scope) {
9
+ if (!scope) return {};
10
+ const userId = getUserId(scope);
11
+ const organizationId = getOrgId(scope);
12
+ const ctx = {};
13
+ if (userId !== void 0) ctx.userId = userId;
14
+ if (organizationId !== void 0) ctx.organizationId = organizationId;
15
+ return ctx;
16
+ }
17
+ function buildStorageContext(request, contextFrom) {
18
+ const scope = request.scope;
19
+ return {
20
+ scope: contextFrom(scope),
21
+ requestId: request.id
22
+ };
23
+ }
24
+ /**
25
+ * Parse a single-range `Range: bytes=start-end` header.
26
+ *
27
+ * Returns `undefined` when the header is missing or unparseable. Only
28
+ * satisfiable single ranges are supported — multi-range requests fall through
29
+ * to the full-object response (per RFC 7233 §4.1 a server MAY ignore ranges).
30
+ */
31
+ function parseRangeHeader(header, totalSize) {
32
+ if (!header || !header.startsWith("bytes=")) return void 0;
33
+ const spec = header.slice(6).split(",")[0]?.trim();
34
+ if (!spec) return void 0;
35
+ const dashIndex = spec.indexOf("-");
36
+ if (dashIndex === -1) return void 0;
37
+ const startRaw = spec.slice(0, dashIndex);
38
+ const endRaw = spec.slice(dashIndex + 1);
39
+ if (startRaw === "") {
40
+ if (totalSize === void 0) return void 0;
41
+ const suffix = Number(endRaw);
42
+ if (!Number.isFinite(suffix) || suffix <= 0) return void 0;
43
+ return {
44
+ start: Math.max(0, totalSize - suffix),
45
+ end: totalSize - 1
46
+ };
47
+ }
48
+ const start = Number(startRaw);
49
+ if (!Number.isFinite(start) || start < 0) return void 0;
50
+ if (endRaw === "") {
51
+ if (totalSize === void 0) return void 0;
52
+ return {
53
+ start,
54
+ end: totalSize - 1
55
+ };
56
+ }
57
+ const end = Number(endRaw);
58
+ if (!Number.isFinite(end) || end < start) return void 0;
59
+ if (totalSize !== void 0 && end >= totalSize) return {
60
+ start,
61
+ end: totalSize - 1
62
+ };
63
+ return {
64
+ start,
65
+ end
66
+ };
67
+ }
68
+ /**
69
+ * Strict policy — rejects filenames that could escape a storage root or
70
+ * confuse a filesystem. Safe default for disk/S3 adapters that compose
71
+ * `${prefix}/${filename}` or `path.join(root, filename)`.
72
+ */
73
+ function strictFilenamePolicy(filename) {
74
+ if (filename.length === 0) throw new ValidationError("Upload filename is empty");
75
+ if (filename.length > 255) throw new ValidationError("Upload filename exceeds 255 characters");
76
+ if (filename.includes("\0")) throw new ValidationError("Upload filename contains a NUL byte");
77
+ if (filename.includes("/") || filename.includes("\\")) throw new ValidationError("Upload filename contains a path separator");
78
+ if (filename === "." || filename === "..") throw new ValidationError("Upload filename is a path traversal component");
79
+ return filename;
80
+ }
81
+ /** Resolve the user-supplied policy option into a concrete validator. */
82
+ function resolveFilenamePolicy(policy) {
83
+ if (policy === void 0 || policy === true) return strictFilenamePolicy;
84
+ if (policy === false || policy === "*") return (f) => f;
85
+ if (typeof policy === "function") return (filename) => {
86
+ const result = policy(filename);
87
+ if (result === false) throw new ValidationError(`Upload filename rejected: ${filename}`);
88
+ if (typeof result === "string") return result;
89
+ return filename;
90
+ };
91
+ return strictFilenamePolicy;
92
+ }
93
+ function makeUploadHandler(deps) {
94
+ return async function uploadHandler(request, reply) {
95
+ const file = (request.body?._files)?.[deps.fieldName];
96
+ if (!file) throw new ValidationError(`Missing file field '${deps.fieldName}' in multipart body`);
97
+ const filename = deps.applyFilenamePolicy(file.filename);
98
+ const ctx = buildStorageContext(request, deps.contextFrom);
99
+ const result = await deps.storage.upload({
100
+ buffer: file.buffer,
101
+ filename,
102
+ mimeType: file.mimetype,
103
+ size: file.size
104
+ }, ctx);
105
+ return reply.code(201).send({
106
+ success: true,
107
+ data: toResponseFile(result)
108
+ });
109
+ };
110
+ }
111
+ function toResponseFile(file) {
112
+ const payload = {
113
+ id: file.id,
114
+ url: file.url,
115
+ pathname: file.pathname,
116
+ contentType: file.contentType,
117
+ bytes: file.bytes
118
+ };
119
+ if (file.metadata !== void 0) payload.metadata = file.metadata;
120
+ return payload;
121
+ }
122
+ function makeReadHandler(deps) {
123
+ return async function readHandler(request, reply) {
124
+ const { id } = request.params;
125
+ const ctx = buildStorageContext(request, deps.contextFrom);
126
+ reply.header("Accept-Ranges", "bytes");
127
+ const rangeHeader = request.headers.range;
128
+ let result;
129
+ try {
130
+ const parsed = rangeHeader ? parseRangeHeader(rangeHeader, void 0) : void 0;
131
+ result = await deps.storage.read(id, ctx, parsed);
132
+ } catch (err) {
133
+ throw toNotFound(err, "File", id);
134
+ }
135
+ if (result.kind === "buffer") return sendBuffer(reply, result, rangeHeader);
136
+ return sendStream(reply, result, rangeHeader);
137
+ };
138
+ }
139
+ function sendBuffer(reply, result, rangeHeader) {
140
+ reply.type(result.contentType);
141
+ const total = result.totalBytes ?? result.buffer.length;
142
+ if (result.range) {
143
+ const { start, end } = result.range;
144
+ reply.code(206);
145
+ reply.header("Content-Range", `bytes ${start}-${end}/${total}`);
146
+ reply.header("Content-Length", String(result.buffer.length));
147
+ return reply.send(result.buffer);
148
+ }
149
+ if (rangeHeader) {
150
+ const parsed = parseRangeHeader(rangeHeader, total);
151
+ if (parsed) {
152
+ const slice = result.buffer.subarray(parsed.start, parsed.end + 1);
153
+ reply.code(206);
154
+ reply.header("Content-Range", `bytes ${parsed.start}-${parsed.end}/${total}`);
155
+ reply.header("Content-Length", String(slice.length));
156
+ return reply.send(slice);
157
+ }
158
+ }
159
+ reply.header("Content-Length", String(result.buffer.length));
160
+ return reply.send(result.buffer);
161
+ }
162
+ function sendStream(reply, result, rangeHeader) {
163
+ reply.type(result.contentType);
164
+ if (result.range && result.bytes !== void 0) {
165
+ const { start, end } = result.range;
166
+ reply.code(206);
167
+ reply.header("Content-Range", `bytes ${start}-${end}/${result.bytes}`);
168
+ reply.header("Content-Length", String(end - start + 1));
169
+ } else if (result.bytes !== void 0) {
170
+ reply.header("Content-Length", String(result.bytes));
171
+ if (rangeHeader) reply.request.log.debug({ url: reply.request.url }, "filesUploadPreset: adapter returned unsliced stream for a range request — sending full object");
172
+ }
173
+ return reply.send(result.stream);
174
+ }
175
+ function makeDeleteHandler(deps) {
176
+ return async function deleteHandler(request, reply) {
177
+ const { id } = request.params;
178
+ const ctx = buildStorageContext(request, deps.contextFrom);
179
+ if (!await deps.storage.delete(id, ctx)) throw new NotFoundError("File", id);
180
+ return reply.code(204).send();
181
+ };
182
+ }
183
+ function toNotFound(err, resource, id) {
184
+ if (err instanceof NotFoundError) return err;
185
+ const maybe = err;
186
+ if (maybe?.statusCode === 404 || maybe?.code === "NOT_FOUND") return new NotFoundError(resource, id);
187
+ if (typeof maybe?.message === "string" && /not\s*found/i.test(maybe.message)) return new NotFoundError(resource, id);
188
+ return err;
189
+ }
190
+ /**
191
+ * Create a files-upload preset bound to a `Storage` adapter.
192
+ *
193
+ * The preset uses `raw: true` routes so binary responses bypass arc's JSON
194
+ * envelope. Upload still returns the standard `{ success: true, data }`
195
+ * envelope manually because the response is structured metadata, not bytes.
196
+ */
197
+ function filesUploadPreset(options) {
198
+ if (!options?.storage) throw new Error("filesUploadPreset: `storage` is required");
199
+ const deps = {
200
+ storage: options.storage,
201
+ fieldName: options.fieldName ?? DEFAULT_FIELD_NAME,
202
+ contextFrom: options.contextFrom ?? defaultContextFrom,
203
+ applyFilenamePolicy: resolveFilenamePolicy(options.sanitizeFilename)
204
+ };
205
+ const maxFileSize = options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
206
+ const allowedMimeTypes = options.allowedMimeTypes;
207
+ const includeRoutes = {
208
+ upload: options.includeRoutes?.upload ?? true,
209
+ read: options.includeRoutes?.read ?? true,
210
+ delete: options.includeRoutes?.delete ?? true
211
+ };
212
+ return {
213
+ name: "filesUpload",
214
+ routes: (permissions) => {
215
+ const routes = [];
216
+ if (includeRoutes.upload) routes.push({
217
+ method: "POST",
218
+ path: "/upload",
219
+ operation: "filesUpload.upload",
220
+ summary: "Upload a file",
221
+ description: "Accepts a multipart/form-data request and persists the bytes via the configured Storage adapter.",
222
+ permissions: options.permissions?.upload ?? permissions.create ?? requireAuth(),
223
+ preHandler: [multipartBody({
224
+ maxFileSize,
225
+ allowedMimeTypes,
226
+ requiredFields: [deps.fieldName]
227
+ })],
228
+ raw: true,
229
+ handler: makeUploadHandler(deps)
230
+ });
231
+ if (includeRoutes.read) routes.push({
232
+ method: "GET",
233
+ path: "/:id",
234
+ operation: "filesUpload.read",
235
+ summary: "Download a file",
236
+ description: "Streams the stored bytes. Supports single-range `Range: bytes=start-end`.",
237
+ permissions: options.permissions?.read ?? permissions.get ?? allowPublic(),
238
+ raw: true,
239
+ handler: makeReadHandler(deps),
240
+ mcp: false
241
+ });
242
+ if (includeRoutes.delete) routes.push({
243
+ method: "DELETE",
244
+ path: "/:id",
245
+ operation: "filesUpload.delete",
246
+ summary: "Delete a file",
247
+ permissions: options.permissions?.delete ?? permissions.delete ?? requireAuth(),
248
+ raw: true,
249
+ handler: makeDeleteHandler(deps)
250
+ });
251
+ return routes;
252
+ }
253
+ };
254
+ }
255
+ //#endregion
2
256
  export { filesUploadPreset };
@@ -1,4 +1,4 @@
1
- import { Ht as IRequestContext, Vt as IControllerResponse, et as PresetResult, lt as ResourceConfig, on as PaginationResult, p as AnyRecord } from "../interface-YrWsmKqE.mjs";
1
+ import { Mt as IControllerResponse, Nt as IRequestContext, Yt as AnyRecord, _t as PresetResult, bt as ResourceConfig, d as PaginationResult } from "../index-BGbpGVyM.mjs";
2
2
  import { FilesUploadPresetOptions, FilesUploadPresetPermissions, FilesUploadPresetRoutes, filesUploadPreset } from "./filesUpload.mjs";
3
3
  import { MultiTenantOptions, TenantFieldSpec, multiTenantPreset } from "./multiTenant.mjs";
4
4
  import { SearchHandler, SearchPresetOptions, SearchRouteConfig, searchPreset } from "./search.mjs";
@@ -1,5 +1,5 @@
1
1
  import { multiTenantPreset } from "./multiTenant.mjs";
2
- import { a as registerPreset, c as auditedPreset, d as ownedByUserPreset, i as getPreset, l as softDeletePreset, n as flexibleMultiTenantPreset, o as treePreset, r as getAvailablePresets, s as bulkPreset, t as applyPresets, u as slugLookupPreset } from "../presets-hM4WhNWY.mjs";
3
- import { t as filesUploadPreset } from "../filesUpload-q8oHt--L.mjs";
2
+ import { a as registerPreset, c as auditedPreset, d as ownedByUserPreset, i as getPreset, l as softDeletePreset, n as flexibleMultiTenantPreset, o as treePreset, r as getAvailablePresets, s as bulkPreset, t as applyPresets, u as slugLookupPreset } from "../presets-CrwOvuXI.mjs";
3
+ import { filesUploadPreset } from "./filesUpload.mjs";
4
4
  import { searchPreset } from "./search.mjs";
5
5
  export { applyPresets, auditedPreset, bulkPreset, filesUploadPreset, flexibleMultiTenantPreset, getAvailablePresets, getPreset, multiTenantPreset, ownedByUserPreset, registerPreset, searchPreset, slugLookupPreset, softDeletePreset, treePreset };
@@ -1,4 +1,4 @@
1
- import { T as CrudRouteKey, et as PresetResult } from "../interface-YrWsmKqE.mjs";
1
+ import { _t as PresetResult, lt as CrudRouteKey } from "../index-BGbpGVyM.mjs";
2
2
 
3
3
  //#region src/presets/multiTenant.d.ts
4
4
  /**