@classytic/arc 2.20.0 → 2.22.0

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 (143) hide show
  1. package/dist/{BaseController-D-MHZ27_.mjs → BaseController-C6FlRWxF.mjs} +2 -2
  2. package/dist/{EventTransport-IEn7Tn-Q.d.mts → EventTransport-QmY7A258.d.mts} +0 -1
  3. package/dist/{QueryCache-TRfXau3W.d.mts → QueryCache-Z1B38koh.d.mts} +0 -1
  4. package/dist/{actionPermissions-C9RMiIpc.mjs → actionPermissions-ze70xXJi.mjs} +1 -1
  5. package/dist/audit/index.d.mts +9 -5
  6. package/dist/auth/audit.d.mts +7 -4
  7. package/dist/auth/audit.mjs +5 -0
  8. package/dist/auth/index.d.mts +35 -14
  9. package/dist/auth/index.mjs +6 -3
  10. package/dist/auth/redis-session.d.mts +1 -2
  11. package/dist/cache/index.d.mts +2 -3
  12. package/dist/cli/commands/describe.d.mts +1 -2
  13. package/dist/cli/commands/docs.mjs +1 -1
  14. package/dist/cli/commands/init.mjs +1 -1
  15. package/dist/cli/index.mjs +1 -1
  16. package/dist/context/index.d.mts +2 -2
  17. package/dist/core/index.d.mts +3 -3
  18. package/dist/core/index.mjs +4 -4
  19. package/dist/{core-CWhyGkiM.mjs → core-b6fgFgnd.mjs} +129 -17
  20. package/dist/{createActionRouter-HlQso0jO.mjs → createActionRouter-h1cEEaEX.mjs} +10 -6
  21. package/dist/{createAggregationRouter-5LzPzgw1.mjs → createAggregationRouter-B1C0kvTL.mjs} +3 -3
  22. package/dist/{createApp-D6uwzCKu.mjs → createApp-Ci7yG5gv.mjs} +390 -192
  23. package/dist/discovery/index.d.mts +0 -1
  24. package/dist/docs/index.d.mts +1 -2
  25. package/dist/docs/index.mjs +1 -1
  26. package/dist/{elevation-xGUBq6p_.d.mts → elevation-C9s5hFP_.d.mts} +0 -1
  27. package/dist/encryption/index.d.mts +2 -144
  28. package/dist/encryption/index.mjs +9 -5
  29. package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-BTzXSwIg.mjs} +24 -3
  30. package/dist/{errorHandler-BtUUDPEa.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -1
  31. package/dist/{errors-DOHkjQlD.d.mts → errors-DHcHVAgk.d.mts} +0 -1
  32. package/dist/{eventPlugin-DbBFd3x_.d.mts → eventPlugin-A6EbthXR.d.mts} +10 -6
  33. package/dist/events/index.d.mts +9 -6
  34. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  35. package/dist/events/transports/redis.d.mts +1 -2
  36. package/dist/factory/index.d.mts +30 -9
  37. package/dist/factory/index.mjs +35 -4
  38. package/dist/{fastify-augmentation-CCQJEDgh.d.mts → fastify-augmentation-9_fuetB4.d.mts} +2 -3
  39. package/dist/fields-D3lhgzhD.d.mts +121 -0
  40. package/dist/health-ScyjIVo5.mjs +202 -0
  41. package/dist/hooks/index.d.mts +1 -1
  42. package/dist/idempotency/index.d.mts +4 -3
  43. package/dist/idempotency/index.mjs +2 -0
  44. package/dist/idempotency/redis.d.mts +1 -1
  45. package/dist/{types-BHMKlMBU.d.mts → index-BEu4wjbL.d.mts} +241 -80
  46. package/dist/{index-DUa8WHz3.d.mts → index-D8MKYGDa.d.mts} +88 -7
  47. package/dist/{index-_--GyzcO.d.mts → index-DbtgCOCj.d.mts} +1324 -1228
  48. package/dist/{index-CdFkIMEd.d.mts → index-ogIHvOjo.d.mts} +36 -7
  49. package/dist/index.d.mts +8 -7
  50. package/dist/index.mjs +6 -6
  51. package/dist/{init-YH_nslQR.mjs → init-DleK3p9f.mjs} +272 -139
  52. package/dist/integrations/event-gateway.d.mts +2 -3
  53. package/dist/integrations/index.d.mts +2 -2
  54. package/dist/integrations/jobs.d.mts +0 -1
  55. package/dist/integrations/mcp/index.d.mts +6 -7
  56. package/dist/integrations/mcp/index.mjs +1 -1
  57. package/dist/integrations/mcp/testing.d.mts +1 -2
  58. package/dist/integrations/mcp/testing.mjs +1 -1
  59. package/dist/integrations/streamline.d.mts +2 -2
  60. package/dist/integrations/webhooks.d.mts +8 -5
  61. package/dist/integrations/webhooks.mjs +9 -5
  62. package/dist/integrations/websocket-pushref-redis.d.mts +1 -2
  63. package/dist/integrations/websocket-redis.d.mts +1 -2
  64. package/dist/integrations/websocket.d.mts +2 -2
  65. package/dist/jsonBody-BlzAoGCe.mjs +43 -0
  66. package/dist/{loadResources-B8UmpITt.mjs → loadResources-DpU8pfjV.mjs} +4 -3
  67. package/dist/logger/index.d.mts +26 -1
  68. package/dist/logger/index.mjs +39 -1
  69. package/dist/{mcpPlugin-CuW6L_uD.mjs → mcpPlugin-ByA2tf6D.mjs} +8 -4
  70. package/dist/middleware/index.d.mts +1 -2
  71. package/dist/{module-JYH9V0I8.d.mts → module-B4u10iOJ.d.mts} +71 -1
  72. package/dist/{openapi-Dq6RbEGK.mjs → openapi-BRpMZD7C.mjs} +3 -3
  73. package/dist/permissions/index.d.mts +4 -3
  74. package/dist/permissions/index.mjs +2 -2
  75. package/dist/{permissions-CgbOLJZG.mjs → permissions-BJMuKMl7.mjs} +33 -1
  76. package/dist/pipeline/index.d.mts +1 -2
  77. package/dist/plugins/index.d.mts +96 -7
  78. package/dist/plugins/index.mjs +86 -268
  79. package/dist/plugins/response-cache.d.mts +5 -3
  80. package/dist/plugins/response-cache.mjs +1 -0
  81. package/dist/plugins/tracing-entry.d.mts +1 -1
  82. package/dist/plugins/tracing-entry.mjs +7 -4
  83. package/dist/presets/filesUpload.d.mts +4 -5
  84. package/dist/presets/filesUpload.mjs +1 -1
  85. package/dist/presets/index.d.mts +1 -2
  86. package/dist/presets/index.mjs +1 -1
  87. package/dist/presets/multiTenant.d.mts +1 -2
  88. package/dist/presets/search.d.mts +2 -3
  89. package/dist/presets/search.mjs +1 -1
  90. package/dist/{presets-DWsYQdbV.mjs → presets-gw0ciIUD.mjs} +1 -1
  91. package/dist/{pushref-registry-DnCqL3aa.d.mts → pushref-registry-BbcnpNTm.d.mts} +4 -2
  92. package/dist/{queryCachePlugin-xzFyKAjT.d.mts → queryCachePlugin-CfYWq-Ak.d.mts} +1 -2
  93. package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
  94. package/dist/{redis-C3W0qbz8.d.mts → redis-UDtLaNj6.d.mts} +0 -1
  95. package/dist/{redis-stream-DsP1vqZB.d.mts → redis-stream-iVR2l7aS.d.mts} +1 -2
  96. package/dist/{registerRoute-DGSREwPw.mjs → registerRoute-xHc42fwB.mjs} +38 -18
  97. package/dist/registry/index.d.mts +2 -2
  98. package/dist/requestId-5BhTVzBv.mjs +113 -0
  99. package/dist/schemaConverter-Ds5reSGs.mjs +168 -0
  100. package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-B-6SPe7g.mjs} +2 -2
  101. package/dist/schemas/index.d.mts +0 -1
  102. package/dist/scim/index.d.mts +0 -1
  103. package/dist/scim/index.mjs +6 -7
  104. package/dist/scope/index.d.mts +2 -3
  105. package/dist/scope/index.mjs +2 -76
  106. package/dist/scope-k15RXfcq.mjs +64 -0
  107. package/dist/{sessionManager-BXG-HGwG.d.mts → sessionManager-BHOM3N_0.d.mts} +10 -6
  108. package/dist/{storage-Co_PQpeQ.d.mts → storage-DLUZ04kK.d.mts} +8 -4
  109. package/dist/testing/index.d.mts +83 -4
  110. package/dist/testing/index.mjs +74 -4
  111. package/dist/testing/storageContract.d.mts +1 -2
  112. package/dist/{tracing-D8jlGXP7.d.mts → tracing-CsGKQJGg.d.mts} +0 -1
  113. package/dist/types/index.d.mts +4 -4
  114. package/dist/types/storage.d.mts +1 -1
  115. package/dist/{types-C9b2HR5W.d.mts → types-Bc42CUyk.d.mts} +1 -2
  116. package/dist/types-Cc91nXuA.d.mts +132 -0
  117. package/dist/{fields-C7ahdGYf.d.mts → types-Csxga7d3.d.mts} +2 -123
  118. package/dist/{types-LNebCoJx.d.mts → types-DH0GupI7.d.mts} +8 -4
  119. package/dist/usage/index.d.mts +142 -0
  120. package/dist/usage/index.mjs +0 -0
  121. package/dist/utils/index.d.mts +220 -144
  122. package/dist/utils/index.mjs +3 -3
  123. package/dist/{utils-ChjYQpjn.mjs → utils-FhJ88TJE.mjs} +129 -39
  124. package/dist/{versioning-D_eBCfNr.d.mts → versioning-DLwklPlG.d.mts} +1 -7
  125. package/dist/{websocket-MlYlrYq0.d.mts → websocket-uuWqM6UY.d.mts} +5 -4
  126. package/llms.txt +39 -0
  127. package/package.json +453 -447
  128. package/skills/arc/SKILL.md +14 -8
  129. package/skills/arc/references/api-reference.md +34 -0
  130. package/skills/arc/references/auth.md +21 -4
  131. package/skills/arc/references/mcp.md +1 -1
  132. package/skills/arc/references/production.md +44 -2
  133. package/skills/arc-code-review/SKILL.md +1 -1
  134. package/skills/arc-code-review/references/anti-patterns.md +71 -18
  135. package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
  136. package/skills/arc-code-review/references/migration-recipes.md +7 -7
  137. package/skills/arc-code-review/references/mongokit-migration.md +4 -3
  138. package/skills/arc-module-publishing/SKILL.md +243 -0
  139. package/skills/arc-module-publishing/references/authoring-modules.md +327 -0
  140. package/skills/arc-module-publishing/references/package-manifest.md +157 -0
  141. package/skills/arc-module-publishing/references/publishing-workflow.md +182 -0
  142. package/dist/preloadResources-CHw3_oA_.mjs +0 -45
  143. package/dist/schemaConverter-U0MnO9vd.mjs +0 -110
@@ -1,5 +1,5 @@
1
1
  import { b as isElevated, n as PUBLIC_SCOPE } from "./types-bHblZ9YN.mjs";
2
- import { C as requireAuth, T as requireRoles, y as allowPublic } from "./permissions-CgbOLJZG.mjs";
2
+ import { E as requireRoles, b as allowPublic, w as requireAuth } from "./permissions-BJMuKMl7.mjs";
3
3
  import { multiTenantPreset } from "./presets/multiTenant.mjs";
4
4
  //#region src/presets/ownedByUser.ts
5
5
  /**
@@ -244,8 +244,10 @@ interface PushRefStore {
244
244
  * pre-compute it without re-introducing the very race we're closing.
245
245
  */
246
246
  stageIfInactive(args: {
247
- pushRef: string; /** Raw payload (NOT pre-wrapped). The store assigns seq + wraps atomically. */
248
- payload: unknown; /** Server timestamp (ms) — passed in so tests can pin the clock. */
247
+ pushRef: string;
248
+ /** Raw payload (NOT pre-wrapped). The store assigns seq + wraps atomically. */
249
+ payload: unknown;
250
+ /** Server timestamp (ms) — passed in so tests can pin the clock. */
249
251
  timestamp: number;
250
252
  expiresAt: number;
251
253
  }): Promise<"ok" | "missing" | "active" | "raw">;
@@ -1,7 +1,6 @@
1
1
  import { r as CacheStore } from "./interface-C1_WLTMK.mjs";
2
- import { i as QueryCache } from "./QueryCache-TRfXau3W.mjs";
2
+ import { i as QueryCache } from "./QueryCache-Z1B38koh.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
-
5
4
  //#region src/cache/queryCachePlugin.d.ts
6
5
  interface QueryCachePluginOptions {
7
6
  /** CacheStore instance. Default: MemoryCacheStore with default options. */
@@ -0,0 +1,14 @@
1
+ //#region src/scope/rateLimitKey.ts
2
+ function createTenantKeyGenerator(opts) {
3
+ if (opts?.strategy) return opts.strategy;
4
+ return (ctx) => {
5
+ const scope = ctx.scope;
6
+ if (!scope || scope.kind === "public") return ctx.ip;
7
+ if (scope.kind === "member") return scope.organizationId;
8
+ if (scope.kind === "service") return scope.organizationId;
9
+ if (scope.kind === "elevated") return scope.organizationId ?? scope.userId ?? ctx.ip;
10
+ return scope.userId ?? ctx.ip;
11
+ };
12
+ }
13
+ //#endregion
14
+ export { createTenantKeyGenerator as t };
@@ -1,5 +1,4 @@
1
1
  import { n as IdempotencyResult, r as IdempotencyStore } from "./interface-Uk8ONJv7.mjs";
2
-
3
2
  //#region src/idempotency/stores/redis.d.ts
4
3
  interface RedisClient {
5
4
  get(key: string): Promise<string | null>;
@@ -1,5 +1,4 @@
1
- import { a as EventTransport, i as EventLogger, n as DomainEvent, r as EventHandler } from "./EventTransport-IEn7Tn-Q.mjs";
2
-
1
+ import { a as EventTransport, i as EventLogger, n as DomainEvent, r as EventHandler } from "./EventTransport-QmY7A258.mjs";
3
2
  //#region src/events/transports/redis-stream.d.ts
4
3
  interface RedisStreamLike {
5
4
  xadd(key: string, id: string, ...fieldValues: string[]): Promise<string | null>;
@@ -1,6 +1,6 @@
1
1
  import { f as createError } from "./errors-DkO8Nk6-.mjs";
2
2
  import { n as requestContext } from "./requestContext-BZKOr13H.mjs";
3
- import { M as evaluateAndApplyPermission } from "./permissions-CgbOLJZG.mjs";
3
+ import { N as evaluateAndApplyPermission } from "./permissions-BJMuKMl7.mjs";
4
4
  //#region src/core/middlewares/arcDecorator.ts
5
5
  /**
6
6
  * Build the `arcDecorator` preHandler for a resource.
@@ -81,24 +81,44 @@ function buildAuthMiddlewareForPermissions(fastify, permissions) {
81
81
  //#endregion
82
82
  //#region src/core/middlewares/chain.ts
83
83
  /**
84
- * Compose preHandler[] in the canonical order. Every null/undefined entry is
85
- * dropped. Keeps CRUD and Action routers from accidentally ordering the same
86
- * ingredients differently (regression risk: cache before auth → user-scoped
87
- * cache keys leak across users).
84
+ * Compose the route-level `onRequest` + `preHandler` arrays in the canonical
85
+ * order. Every null/undefined entry is dropped. Keeps the three routers from
86
+ * accidentally ordering the same ingredients differently (regression risk:
87
+ * cache before auth → user-scoped cache keys leak across users; auth after
88
+ * body parse → anonymous schema probing).
88
89
  *
89
- * Canonical order:
90
- * preAuth → arcDecorator → authMw → permissionMw → pluginMw → routeGuards → customMws
90
+ * onRequest constraints (Fastify lifecycle): `request.body` is undefined —
91
+ * everything placed there must be header/cookie/param/query-driven. arc's
92
+ * `authenticate` / `optionalAuthenticate` qualify by contract (Bearer
93
+ * header, session cookie, api-key header); `preAuth` handlers exist
94
+ * precisely to promote query tokens into headers (SSE `?token=`); the arc
95
+ * decorator stamps static route metadata.
91
96
  */
92
- function buildPreHandlerChain(parts) {
93
- return [
94
- ...parts.preAuth ?? [],
95
- parts.arcDecorator,
96
- parts.authMw ?? null,
97
- parts.permissionMw ?? null,
98
- parts.pluginMw ?? null,
99
- ...parts.routeGuards ?? [],
100
- ...parts.customMws ?? []
101
- ].filter(Boolean);
97
+ function buildRouteHooks(parts) {
98
+ return {
99
+ onRequest: [
100
+ ...parts.preAuth ?? [],
101
+ parts.arcDecorator,
102
+ parts.authMw ?? null
103
+ ].filter(Boolean),
104
+ preHandler: [
105
+ parts.permissionMw ?? null,
106
+ parts.pluginMw ?? null,
107
+ ...parts.routeGuards ?? [],
108
+ ...parts.customMws ?? []
109
+ ].filter(Boolean)
110
+ };
111
+ }
112
+ /**
113
+ * Spread helper — turns `RouteHookChains` into the two Fastify route-option
114
+ * fields, omitting empty arrays so routes without middleware stay
115
+ * option-free (Fastify skips absent hooks faster than empty arrays).
116
+ */
117
+ function routeHookOptions(hooks) {
118
+ return {
119
+ ...hooks.onRequest.length > 0 ? { onRequest: hooks.onRequest } : {},
120
+ ...hooks.preHandler.length > 0 ? { preHandler: hooks.preHandler } : {}
121
+ };
102
122
  }
103
123
  /**
104
124
  * `RouteDefinition.preHandler` accepts two shapes:
@@ -329,4 +349,4 @@ function tryRegisterRoute(fastify, opts, context) {
329
349
  }
330
350
  }
331
351
  //#endregion
332
- export { selectPluginMw as a, buildPreHandlerChain as c, buildAuthMiddlewareForPermissions as d, buildArcDecorator as f, resolveRouterPluginMw as i, resolveRoutePreHandlers as l, buildRateLimitConfig as n, buildActionPermissionMw as o, buildRouteConfig as r, buildCrudPermissionMw as s, tryRegisterRoute as t, buildAuthMiddleware as u };
352
+ export { selectPluginMw as a, buildRouteHooks as c, buildAuthMiddleware as d, buildAuthMiddlewareForPermissions as f, resolveRouterPluginMw as i, resolveRoutePreHandlers as l, buildRateLimitConfig as n, buildActionPermissionMw as o, buildArcDecorator as p, buildRouteConfig as r, buildCrudPermissionMw as s, tryRegisterRoute as t, routeHookOptions as u };
@@ -1,7 +1,7 @@
1
- import { J as CrudRouteKey, R as RegisterOptions, V as ResourceDefinition, at as ResolvedTenantPurge, k as IntrospectionPluginOptions, p as RegistryEntry, z as ResourceRegistry } from "../index-_--GyzcO.mjs";
1
+ import { K as ResolvedTenantPurge, R as RegisterOptions, V as ResourceDefinition, X as CrudRouteKey, k as IntrospectionPluginOptions, p as RegistryEntry, z as ResourceRegistry } from "../index-DbtgCOCj.mjs";
2
+ import "../fastify-augmentation-9_fuetB4.mjs";
2
3
  import { FastifyPluginAsync } from "fastify";
3
4
  import { TenantPurgeProgress, TenantPurgeStrategy } from "@classytic/repo-core/repository";
4
-
5
5
  //#region src/registry/assertNoTenantData.d.ts
6
6
  interface AssertNoTenantDataOptions {
7
7
  readonly organizationId: string;
@@ -0,0 +1,113 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fp from "fastify-plugin";
3
+ //#region src/plugins/requestId.ts
4
+ /**
5
+ * Request ID Plugin
6
+ *
7
+ * Propagates request IDs for distributed tracing.
8
+ * - Accepts incoming x-request-id header (sanitized)
9
+ * - Generates UUID if not provided
10
+ * - Echoes the id on the response header + W3C trace-context propagation
11
+ *
12
+ * ID RESOLUTION LIVES AT THE SERVER LEVEL, NOT HERE. Fastify binds
13
+ * `request.log = logger.child({ reqId: request.id })` at request
14
+ * construction — before any hook runs — so an id assigned inside a hook
15
+ * never reaches the logs. `createApp` therefore wires
16
+ * {@link createRequestIdGenerator} into Fastify's `genReqId` option, and
17
+ * this plugin simply adopts `request.id`. Standalone registration on a
18
+ * bare Fastify instance falls back to the legacy in-hook resolution
19
+ * (header echo / UUID), with the documented limitation that
20
+ * `request.log`'s `reqId` binding predates the overwrite — pass
21
+ * `genReqId: createRequestIdGenerator()` to `Fastify()` to fix that.
22
+ *
23
+ * @example
24
+ * import { requestIdPlugin, createRequestIdGenerator } from '@classytic/arc';
25
+ *
26
+ * const fastify = Fastify({ genReqId: createRequestIdGenerator() });
27
+ * await fastify.register(requestIdPlugin);
28
+ *
29
+ * fastify.get('/', async (request) => {
30
+ * request.log.info('traced'); // carries the same reqId as the header
31
+ * return { id: request.id };
32
+ * });
33
+ */
34
+ /**
35
+ * Sanitize an incoming request-id header value: max 128 chars, alphanumeric
36
+ * plus dashes/underscores/dots/colons only. Returns `undefined` for anything
37
+ * else (crafted values could pollute logs or response headers). Array values
38
+ * (repeated headers) are rejected outright — a client sending multiple
39
+ * request-id headers is not tracing in good faith.
40
+ */
41
+ function sanitizeRequestId(value) {
42
+ if (typeof value !== "string") return void 0;
43
+ const trimmed = value.trim();
44
+ return trimmed.length > 0 && trimmed.length <= 128 && /^[\w.:-]+$/.test(trimmed) ? trimmed : void 0;
45
+ }
46
+ /**
47
+ * Build a Fastify `genReqId` function: adopt a sanitized incoming request-id
48
+ * header, or generate one. Wire this into the `Fastify()` constructor so
49
+ * `request.id`, `request.log`'s `reqId` binding, and the echoed response
50
+ * header all agree — `createApp` does this automatically.
51
+ */
52
+ function createRequestIdGenerator(opts = {}) {
53
+ const { header = "x-request-id", generator = randomUUID } = opts;
54
+ const headerName = header.toLowerCase();
55
+ return (req) => sanitizeRequestId(req.headers[headerName]) ?? generator();
56
+ }
57
+ /** Fastify's default request ids are `req-<base36 counter>`. */
58
+ const FASTIFY_DEFAULT_ID = /^req-[0-9a-z]+$/;
59
+ /** W3C traceparent format: `version-traceid-parentid-flags` */
60
+ const TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i;
61
+ /** W3C spec: version ff is reserved and invalid; trace/parent IDs must not be all zeros */
62
+ const TRACEID_ZEROS = "0".repeat(32);
63
+ const PARENTID_ZEROS = "0".repeat(16);
64
+ /** tracestate: printable ASCII, <= 512 bytes */
65
+ const TRACESTATE_MAX = 512;
66
+ function isValidTraceparent(tp) {
67
+ const m = TRACEPARENT_RE.exec(tp);
68
+ if (!m) return false;
69
+ const version = m[1] ?? "";
70
+ const traceId = m[2] ?? "";
71
+ const parentId = m[3] ?? "";
72
+ if (version.toLowerCase() === "ff") return false;
73
+ if (traceId.toLowerCase() === TRACEID_ZEROS) return false;
74
+ if (parentId.toLowerCase() === PARENTID_ZEROS) return false;
75
+ return true;
76
+ }
77
+ const requestIdPlugin = async (fastify, opts = {}) => {
78
+ const { header = "x-request-id", generator = randomUUID, setResponseHeader = true, propagateTraceContext = true } = opts;
79
+ if (!fastify.hasRequestDecorator("requestId")) fastify.decorateRequest("requestId", "");
80
+ if (!fastify.hasRequestDecorator("traceContext")) fastify.decorateRequest("traceContext", void 0);
81
+ fastify.addHook("onRequest", async (request, reply) => {
82
+ const incoming = sanitizeRequestId(request.headers[header]);
83
+ let requestId = String(request.id);
84
+ if (incoming && incoming !== requestId) requestId = incoming;
85
+ else if (!incoming && FASTIFY_DEFAULT_ID.test(requestId)) requestId = generator();
86
+ if (requestId !== request.id) request.id = requestId;
87
+ request.requestId = requestId;
88
+ if (setResponseHeader) reply.header(header, requestId);
89
+ if (propagateTraceContext) {
90
+ const rawTraceparent = request.headers["traceparent"];
91
+ const rawTracestate = request.headers["tracestate"];
92
+ if (typeof rawTraceparent === "string") {
93
+ const tp = rawTraceparent.trim();
94
+ if (isValidTraceparent(tp)) {
95
+ const ts = typeof rawTracestate === "string" && rawTracestate.length > 0 && rawTracestate.length <= TRACESTATE_MAX ? rawTracestate : void 0;
96
+ request.traceContext = {
97
+ traceparent: tp,
98
+ ...ts ? { tracestate: ts } : {}
99
+ };
100
+ reply.header("traceparent", tp);
101
+ if (ts) reply.header("tracestate", ts);
102
+ }
103
+ }
104
+ }
105
+ });
106
+ fastify.log?.debug?.("Request ID plugin registered");
107
+ };
108
+ var requestId_default = fp(requestIdPlugin, {
109
+ name: "arc-request-id",
110
+ fastify: "5.x"
111
+ });
112
+ //#endregion
113
+ export { sanitizeRequestId as i, requestIdPlugin as n, requestId_default as r, createRequestIdGenerator as t };
@@ -0,0 +1,168 @@
1
+ import { arcLog } from "./logger/index.mjs";
2
+ //#region src/utils/schemaConverter.ts
3
+ /**
4
+ * Schema Converter — Detect-First, Convert-Only-When-Needed
5
+ *
6
+ * Converts Zod v4 schemas to JSON Schema using Zod's native `z.toJSONSchema()`.
7
+ * Plain JSON Schema objects pass through with zero overhead.
8
+ *
9
+ * Zod is an **optional** peer dependency — loaded lazily at module init;
10
+ * `ensureSchemaConverter()` awaits readiness (arc's resource plugin does this
11
+ * before converting). When a Zod schema shows up and can't be converted
12
+ * (zod missing, zod v3, conversion throw), VALIDATION paths fail fast at
13
+ * boot with an actionable error; DOC paths warn and degrade — see
14
+ * {@link UnconvertibleMode}. Hosts that never pass Zod pay nothing.
15
+ *
16
+ * ## Targets
17
+ *
18
+ * Zod v4's `toJSONSchema` supports multiple output targets; arc picks per consumer:
19
+ *
20
+ * - **`draft-7`** (default) — for Fastify route schemas. Fastify v5 bundles AJV 8
21
+ * configured for draft-07, which uses **numeric** `exclusiveMinimum`/`exclusiveMaximum`.
22
+ * The `openapi-3.0` target emits the **boolean** form inherited from draft-04
23
+ * (`exclusiveMinimum: true` alongside `minimum`), which AJV rejects at route
24
+ * registration with `schema is invalid: data/properties/X/exclusiveMinimum must be number`.
25
+ * Using `draft-7` fixes `.positive() / .negative() / .gt() / .lt()` out of the box.
26
+ * - **`openapi-3.0`** — for OpenAPI doc generation (arc emits OpenAPI 3.0.3). Keeps
27
+ * the boolean exclusive form that 3.0 tooling expects.
28
+ */
29
+ const log = arcLog("schema");
30
+ /** Default target for Fastify-consumed schemas (matches Fastify v5's default AJV draft). */
31
+ const DEFAULT_FASTIFY_TARGET = "draft-7";
32
+ /** Default target for OpenAPI document generation (matches arc's emitted OpenAPI version). */
33
+ const DEFAULT_OPENAPI_TARGET = "openapi-3.0";
34
+ let _toJSONSchema = null;
35
+ const zodReady = import("zod").then(({ z }) => {
36
+ if (typeof z?.toJSONSchema === "function") _toJSONSchema = (schema, opts) => z.toJSONSchema(schema, opts);
37
+ }).catch(() => {});
38
+ /**
39
+ * Await Zod-converter readiness. Arc's resource plugin awaits this once
40
+ * before any schema conversion, closing the (theoretical) race between the
41
+ * lazy `import("zod")` above and boot-time route registration. Resolves
42
+ * immediately when zod isn't installed — absence is handled per-schema.
43
+ */
44
+ function ensureSchemaConverter() {
45
+ return zodReady;
46
+ }
47
+ /**
48
+ * Check if an object is already a plain JSON Schema.
49
+ * Returns true if it has JSON Schema markers (`type`, `properties`, `$ref`,
50
+ * `allOf`, `anyOf`, `oneOf`, `items`, `enum`) and does NOT have Zod markers.
51
+ */
52
+ function isJsonSchema(input) {
53
+ if (input === null || typeof input !== "object") return false;
54
+ const obj = input;
55
+ if ("_def" in obj || "_zod" in obj) return false;
56
+ return "type" in obj || "properties" in obj || "$ref" in obj || "allOf" in obj || "anyOf" in obj || "oneOf" in obj || "items" in obj || "enum" in obj;
57
+ }
58
+ /**
59
+ * Check if an object is a Zod schema (has `_zod` marker from Zod v4).
60
+ */
61
+ function isZodSchema(input) {
62
+ return input !== null && typeof input === "object" && "_zod" in input;
63
+ }
64
+ /**
65
+ * Zod v3 schemas carry `_def` but NOT v4's `_zod` marker. They can't be
66
+ * converted (`z.toJSONSchema()` is v4-only) — detect them so the failure is
67
+ * an actionable "upgrade to zod v4" instead of a cryptic AJV boot error
68
+ * three layers away.
69
+ */
70
+ function isZodV3Schema(input) {
71
+ if (input === null || typeof input !== "object") return false;
72
+ const obj = input;
73
+ return "_def" in obj && !("_zod" in obj);
74
+ }
75
+ function unconvertible(mode, message, cause) {
76
+ if (mode === "throw") throw new Error(`[arc/schema] ${message}`, cause ? { cause } : void 0);
77
+ log.warn(`${message} — degrading to permissive { type: 'object' } (validation disabled for this slot)`);
78
+ return { type: "object" };
79
+ }
80
+ /**
81
+ * Convert any schema input to JSON Schema.
82
+ *
83
+ * Detection order:
84
+ * 1. `null`/`undefined` → `undefined`
85
+ * 2. Already JSON Schema → pass through as-is (zero overhead)
86
+ * 3. Zod v4 schema → `z.toJSONSchema(schema, { target })`
87
+ * 4. Zod v3 schema → unconvertible (actionable "upgrade to v4" error/warn)
88
+ * 5. Unrecognized object → return as-is (treat as opaque schema)
89
+ *
90
+ * @param input Schema (Zod, plain JSON Schema, or opaque object)
91
+ * @param target Output target — defaults to `draft-7` for Fastify compatibility.
92
+ * Pass `openapi-3.0`/`openapi-3.1` for OpenAPI document generation.
93
+ * @param mode Failure policy for unconvertible Zod schemas — see
94
+ * {@link UnconvertibleMode}. Defaults to `'warn'`.
95
+ */
96
+ function toJsonSchema(input, target = DEFAULT_FASTIFY_TARGET, mode = "warn") {
97
+ if (input == null) return void 0;
98
+ if (typeof input !== "object") return void 0;
99
+ if (isJsonSchema(input)) return input;
100
+ if (isZodSchema(input)) {
101
+ if (!_toJSONSchema) return unconvertible(mode, "A Zod schema was passed but zod v4 is not installed (or not yet loaded). Install zod >=4 — arc converts via zod's native z.toJSONSchema().");
102
+ try {
103
+ const converted = _toJSONSchema(input, { target });
104
+ if ("$schema" in converted) delete converted.$schema;
105
+ return converted;
106
+ } catch (cause) {
107
+ return unconvertible(mode, `z.toJSONSchema() failed for a Zod schema (target: ${target}): ${cause instanceof Error ? cause.message : String(cause)}. Zod-only features with no JSON Schema equivalent (.refine/.transform/custom checks) cannot express wire validation — enforce those in the handler or a pipeline step.`, cause);
108
+ }
109
+ }
110
+ if (isZodV3Schema(input)) return unconvertible(mode, "A Zod v3 schema was passed (has `_def` but no v4 `_zod` marker). arc requires zod >=4 for schema conversion — upgrade zod, or pass plain JSON Schema.");
111
+ return input;
112
+ }
113
+ /**
114
+ * Convert all schema fields in an OpenApiSchemas object.
115
+ * JSON Schema values pass through unchanged. Only Zod schemas are converted.
116
+ *
117
+ * Defaults to the `openapi-3.0` target since this function feeds OpenAPI doc
118
+ * generation, not Fastify route validation.
119
+ */
120
+ function convertOpenApiSchemas(schemas, target = DEFAULT_OPENAPI_TARGET) {
121
+ const result = {};
122
+ const schemaFields = [
123
+ "entity",
124
+ "createBody",
125
+ "updateBody",
126
+ "params",
127
+ "listQuery",
128
+ "response"
129
+ ];
130
+ for (const field of schemaFields) {
131
+ const value = schemas[field];
132
+ if (value !== void 0) result[field] = toJsonSchema(value, target) ?? value;
133
+ }
134
+ for (const [key, value] of Object.entries(schemas)) if (!schemaFields.includes(key)) result[key] = value;
135
+ return result;
136
+ }
137
+ /**
138
+ * Convert schema values in a Fastify route schema record.
139
+ *
140
+ * Handles `body`, `querystring`, `params`, `headers` (top-level conversion)
141
+ * and `response` (iterates by status code — each value converted individually).
142
+ *
143
+ * JSON Schema values pass through unchanged. Only Zod schemas are converted.
144
+ *
145
+ * Used for both custom routes and customSchemas (CRUD overrides).
146
+ *
147
+ * Defaults to `draft-7` so Fastify v5's bundled AJV 8 accepts the output.
148
+ * Pass `openapi-3.0` (or `openapi-3.1`) when generating OpenAPI documents.
149
+ */
150
+ function convertRouteSchema(schema, target = DEFAULT_FASTIFY_TARGET) {
151
+ const result = { ...schema };
152
+ const mode = target === DEFAULT_FASTIFY_TARGET ? "throw" : "warn";
153
+ for (const field of [
154
+ "body",
155
+ "querystring",
156
+ "params",
157
+ "headers"
158
+ ]) if (result[field] !== void 0) result[field] = toJsonSchema(result[field], target, mode) ?? result[field];
159
+ if (result.response !== void 0 && typeof result.response === "object" && result.response !== null) {
160
+ const responseObj = result.response;
161
+ const convertedResponse = {};
162
+ for (const [statusCode, responseSchema] of Object.entries(responseObj)) convertedResponse[statusCode] = toJsonSchema(responseSchema, target, mode) ?? responseSchema;
163
+ result.response = convertedResponse;
164
+ }
165
+ return result;
166
+ }
167
+ //#endregion
168
+ export { isZodSchema as a, isJsonSchema as i, convertRouteSchema as n, toJsonSchema as o, ensureSchemaConverter as r, convertOpenApiSchemas as t };
@@ -1,4 +1,4 @@
1
- import { a as toJsonSchema } from "./schemaConverter-U0MnO9vd.mjs";
1
+ import { o as toJsonSchema } from "./schemaConverter-Ds5reSGs.mjs";
2
2
  //#region src/core/schemaIR.ts
3
3
  /**
4
4
  * Schema IR — one canonical representation, two adapters.
@@ -49,7 +49,7 @@ function normalizeSchemaIR(raw) {
49
49
  properties: {},
50
50
  required: []
51
51
  };
52
- const converted = toJsonSchema(raw);
52
+ const converted = toJsonSchema(raw, "draft-7", "throw");
53
53
  if (!converted || typeof converted !== "object" || converted.type !== "object" && !("properties" in converted)) return {
54
54
  properties: {},
55
55
  required: []
@@ -1,7 +1,6 @@
1
1
  import { ErrorContract, ErrorDetail } from "@classytic/repo-core/errors";
2
2
  import { FastifyPluginAsyncTypebox, FastifyPluginCallbackTypebox, TypeBoxTypeProvider, TypeBoxValidatorCompiler } from "@fastify/type-provider-typebox";
3
3
  import { Static, TObject, TSchema, TSchema as TSchema$1, Type } from "@sinclair/typebox";
4
-
5
4
  //#region src/schemas/index.d.ts
6
5
  /**
7
6
  * Paginated list response — full union of every wire shape `toCanonicalList`
@@ -1,6 +1,5 @@
1
1
  import { FastifyPluginAsync, FastifyRequest } from "fastify";
2
2
  import { RepositoryLike } from "@classytic/repo-core/adapter";
3
-
4
3
  //#region src/scim/mapping.d.ts
5
4
  /**
6
5
  * SCIM 2.0 ↔ arc resource mapping
@@ -1,3 +1,4 @@
1
+ import { t as parseJsonBody } from "../jsonBody-BlzAoGCe.mjs";
1
2
  import fp from "fastify-plugin";
2
3
  //#region src/scim/errors.ts
3
4
  var ScimError = class extends Error {
@@ -93,18 +94,16 @@ function asRecord(doc) {
93
94
  /**
94
95
  * Register the `application/scim+json` Fastify content-type parser. Idempotent
95
96
  * — second registration in the same scope is a no-op. Empty bodies (DELETE,
96
- * GET) yield `undefined` rather than crashing on `JSON.parse("")`.
97
+ * GET) yield `undefined` rather than crashing on `JSON.parse("")`. Parsing
98
+ * flows through `parseJsonBody`, so malformed SCIM payloads surface as 400
99
+ * `arc.bad_request` (not 500) and prototype-poisoning bodies are rejected —
100
+ * same contract as arc's main `application/json` parser.
97
101
  */
98
102
  function ensureScimContentTypeParser(fastify) {
99
103
  if (fastify.hasContentTypeParser("application/scim+json")) return;
100
104
  fastify.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_req, body, done) => {
101
- const raw = body;
102
- if (!raw || raw.length === 0) {
103
- done(null, void 0);
104
- return;
105
- }
106
105
  try {
107
- done(null, JSON.parse(raw));
106
+ done(null, parseJsonBody(body, "Invalid SCIM JSON payload"));
108
107
  } catch (err) {
109
108
  done(err, void 0);
110
109
  }
@@ -1,7 +1,6 @@
1
- import { C as isMember, D as requireOrgId, E as requireClientId, O as requireTeamId, S as isElevated, T as isService, _ as getTenantFromRequest, a as getAncestorOrgIds, b as hasOrgAccess, c as getMandate, d as getOrgRoles, f as getRequestScope, g as getTeamId, h as getServiceScopes, i as RequestScope, k as requireUserId, l as getOrgContext, m as getScopeContextMap, n as Mandate, o as getClientId, p as getScopeContext, r as PUBLIC_SCOPE, s as getDPoPJkt, t as AUTHENTICATED_SCOPE, u as getOrgId, v as getUserId, w as isOrgInScope, x as isAuthenticated, y as getUserRoles } from "../types-LNebCoJx.mjs";
2
- import { i as elevationPlugin, n as ElevationOptions, r as _default, t as ElevationEvent } from "../elevation-xGUBq6p_.mjs";
1
+ import { C as isMember, D as requireOrgId, E as requireClientId, O as requireTeamId, S as isElevated, T as isService, _ as getTenantFromRequest, a as getAncestorOrgIds, b as hasOrgAccess, c as getMandate, d as getOrgRoles, f as getRequestScope, g as getTeamId, h as getServiceScopes, i as RequestScope, k as requireUserId, l as getOrgContext, m as getScopeContextMap, n as Mandate, o as getClientId, p as getScopeContext, r as PUBLIC_SCOPE, s as getDPoPJkt, t as AUTHENTICATED_SCOPE, u as getOrgId, v as getUserId, w as isOrgInScope, x as isAuthenticated, y as getUserRoles } from "../types-DH0GupI7.mjs";
2
+ import { i as elevationPlugin, n as ElevationOptions, r as _default, t as ElevationEvent } from "../elevation-C9s5hFP_.mjs";
3
3
  import { FastifyReply, FastifyRequest } from "fastify";
4
-
5
4
  //#region src/scope/rateLimitKey.d.ts
6
5
  interface RateLimitKeyContext {
7
6
  ip: string;
@@ -1,79 +1,5 @@
1
1
  import { C as isService, D as requireUserId, E as requireTeamId, S as isOrgInScope, T as requireOrgId, _ as getUserRoles, a as getDPoPJkt, b as isElevated, c as getOrgId, d as getScopeContext, f as getScopeContextMap, g as getUserId, h as getTenantFromRequest, i as getClientId, l as getOrgRoles, m as getTeamId, n as PUBLIC_SCOPE, o as getMandate, p as getServiceScopes, r as getAncestorOrgIds, s as getOrgContext, t as AUTHENTICATED_SCOPE, u as getRequestScope, v as hasOrgAccess, w as requireClientId, x as isMember, y as isAuthenticated } from "../types-bHblZ9YN.mjs";
2
- import { n as normalizeRoles } from "../types-XTApa8b6.mjs";
3
2
  import { n as elevation_default, t as elevationPlugin } from "../elevation-COpYKt2h.mjs";
4
- //#region src/scope/rateLimitKey.ts
5
- function createTenantKeyGenerator(opts) {
6
- if (opts?.strategy) return opts.strategy;
7
- return (ctx) => {
8
- const scope = ctx.scope;
9
- if (!scope || scope.kind === "public") return ctx.ip;
10
- if (scope.kind === "member") return scope.organizationId;
11
- if (scope.kind === "service") return scope.organizationId;
12
- if (scope.kind === "elevated") return scope.organizationId ?? scope.userId ?? ctx.ip;
13
- return scope.userId ?? ctx.ip;
14
- };
15
- }
16
- //#endregion
17
- //#region src/scope/resolveOrgFromHeader.ts
18
- /**
19
- * Create a preHandler hook that resolves org scope from a header.
20
- * Must run AFTER authentication so `request.user` is populated.
21
- * If the header is present and user is a member, sets `request.scope` to `member`.
22
- * If the header is absent, scope stays as-is (typically `authenticated`).
23
- */
24
- function resolveOrgFromHeader(options) {
25
- const { header = "x-organization-id", resolveMembership } = options;
26
- return async (request, reply) => {
27
- const orgId = request.headers[header];
28
- if (!orgId) return;
29
- const scope = request.scope;
30
- if (!scope || scope.kind === "public") {
31
- reply.code(401).send({
32
- success: false,
33
- error: "Unauthorized",
34
- message: "Authentication required for organization access",
35
- code: "ORG_AUTH_REQUIRED"
36
- });
37
- return;
38
- }
39
- if (scope.kind === "elevated") return;
40
- const user = request.user;
41
- if (!user) {
42
- reply.code(401).send({
43
- success: false,
44
- error: "Unauthorized",
45
- message: "Authentication required for organization access",
46
- code: "ORG_AUTH_REQUIRED"
47
- });
48
- return;
49
- }
50
- const userId = String(user.id ?? user._id ?? "");
51
- if (!userId) {
52
- reply.code(401).send({
53
- success: false,
54
- error: "Unauthorized",
55
- message: "User identity required for organization access"
56
- });
57
- return;
58
- }
59
- const membership = await resolveMembership(userId, orgId);
60
- if (!membership) {
61
- reply.code(403).send({
62
- success: false,
63
- error: "Forbidden",
64
- message: "Not a member of this organization",
65
- code: "ORG_ACCESS_DENIED"
66
- });
67
- return;
68
- }
69
- request.scope = {
70
- kind: "member",
71
- userId: userId || void 0,
72
- userRoles: normalizeRoles(user.role),
73
- organizationId: orgId,
74
- orgRoles: membership.roles
75
- };
76
- };
77
- }
78
- //#endregion
3
+ import { t as createTenantKeyGenerator } from "../rateLimitKey-BoWRH3e7.mjs";
4
+ import { t as resolveOrgFromHeader } from "../scope-k15RXfcq.mjs";
79
5
  export { AUTHENTICATED_SCOPE, PUBLIC_SCOPE, createTenantKeyGenerator, elevation_default as elevationPlugin, elevationPlugin as elevationPluginFn, getAncestorOrgIds, getClientId, getDPoPJkt, getMandate, getOrgContext, getOrgId, getOrgRoles, getRequestScope, getScopeContext, getScopeContextMap, getServiceScopes, getTeamId, getTenantFromRequest, getUserId, getUserRoles, hasOrgAccess, isAuthenticated, isElevated, isMember, isOrgInScope, isService, requireClientId, requireOrgId, requireTeamId, requireUserId, resolveOrgFromHeader };
@@ -0,0 +1,64 @@
1
+ import { n as normalizeRoles } from "./types-XTApa8b6.mjs";
2
+ //#region src/scope/resolveOrgFromHeader.ts
3
+ /**
4
+ * Create a preHandler hook that resolves org scope from a header.
5
+ * Must run AFTER authentication so `request.user` is populated.
6
+ * If the header is present and user is a member, sets `request.scope` to `member`.
7
+ * If the header is absent, scope stays as-is (typically `authenticated`).
8
+ */
9
+ function resolveOrgFromHeader(options) {
10
+ const { header = "x-organization-id", resolveMembership } = options;
11
+ return async (request, reply) => {
12
+ const orgId = request.headers[header];
13
+ if (!orgId) return;
14
+ const scope = request.scope;
15
+ if (!scope || scope.kind === "public") {
16
+ reply.code(401).send({
17
+ success: false,
18
+ error: "Unauthorized",
19
+ message: "Authentication required for organization access",
20
+ code: "ORG_AUTH_REQUIRED"
21
+ });
22
+ return;
23
+ }
24
+ if (scope.kind === "elevated") return;
25
+ const user = request.user;
26
+ if (!user) {
27
+ reply.code(401).send({
28
+ success: false,
29
+ error: "Unauthorized",
30
+ message: "Authentication required for organization access",
31
+ code: "ORG_AUTH_REQUIRED"
32
+ });
33
+ return;
34
+ }
35
+ const userId = String(user.id ?? user._id ?? "");
36
+ if (!userId) {
37
+ reply.code(401).send({
38
+ success: false,
39
+ error: "Unauthorized",
40
+ message: "User identity required for organization access"
41
+ });
42
+ return;
43
+ }
44
+ const membership = await resolveMembership(userId, orgId);
45
+ if (!membership) {
46
+ reply.code(403).send({
47
+ success: false,
48
+ error: "Forbidden",
49
+ message: "Not a member of this organization",
50
+ code: "ORG_ACCESS_DENIED"
51
+ });
52
+ return;
53
+ }
54
+ request.scope = {
55
+ kind: "member",
56
+ userId: userId || void 0,
57
+ userRoles: normalizeRoles(user.role),
58
+ organizationId: orgId,
59
+ orgRoles: membership.roles
60
+ };
61
+ };
62
+ }
63
+ //#endregion
64
+ export { resolveOrgFromHeader as t };