@classytic/arc 2.11.3 → 2.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +27 -18
  2. package/dist/{BaseController-swXruJ2_.mjs → BaseController-DX_T-bDB.mjs} +388 -423
  3. package/dist/EventTransport-CT_52aWU.d.mts +34 -0
  4. package/dist/EventTransport-DLWoUMHy.mjs +103 -0
  5. package/dist/{QueryCache-DOBNHBE0.d.mts → QueryCache-D41bfdBB.d.mts} +1 -1
  6. package/dist/{ResourceRegistry-DkAeAuTX.mjs → ResourceRegistry-CTERg_2x.mjs} +139 -66
  7. package/dist/audit/index.d.mts +2 -2
  8. package/dist/audit/index.mjs +1 -1
  9. package/dist/auth/audit.d.mts +199 -0
  10. package/dist/auth/audit.mjs +288 -0
  11. package/dist/auth/index.d.mts +5 -5
  12. package/dist/auth/index.mjs +117 -191
  13. package/dist/auth/redis-session.d.mts +1 -1
  14. package/dist/{betterAuthOpenApi-DwxtK3uG.mjs → betterAuthOpenApi--M_i87dQ.mjs} +1 -1
  15. package/dist/buildHandler-olo-gt94.mjs +610 -0
  16. package/dist/cache/index.d.mts +3 -3
  17. package/dist/cache/index.mjs +3 -3
  18. package/dist/cli/commands/describe.d.mts +89 -13
  19. package/dist/cli/commands/describe.mjs +56 -2
  20. package/dist/cli/commands/docs.mjs +2 -2
  21. package/dist/cli/commands/generate.mjs +147 -48
  22. package/dist/cli/commands/init.d.mts +13 -0
  23. package/dist/cli/commands/init.mjs +237 -112
  24. package/dist/cli/commands/introspect.mjs +8 -1
  25. package/dist/context/index.mjs +1 -1
  26. package/dist/core/index.d.mts +3 -3
  27. package/dist/core/index.mjs +5 -5
  28. package/dist/core-D72ia0EH.mjs +1399 -0
  29. package/dist/{createActionRouter-u3ql2EDo.mjs → createActionRouter-CEvzKcy8.mjs} +7 -20
  30. package/dist/createAggregationRouter-CyecOxnO.mjs +114 -0
  31. package/dist/{createApp-BFxtdKy6.mjs → createApp-XX2-N0Yd.mjs} +31 -27
  32. package/dist/defineEvent-D5h7EvAx.mjs +188 -0
  33. package/dist/docs/index.d.mts +2 -2
  34. package/dist/docs/index.mjs +2 -2
  35. package/dist/{elevation-DOFoxoDs.mjs → elevation-DgoeTyfX.mjs} +1 -1
  36. package/dist/errorHandler-Bk-AGhkU.mjs +174 -0
  37. package/dist/errorHandler-DFr45ZG4.d.mts +45 -0
  38. package/dist/errors-j4aJm1Wg.mjs +184 -0
  39. package/dist/{eventPlugin-KrFIQ097.mjs → eventPlugin-CaKTYkYM.mjs} +35 -137
  40. package/dist/{eventPlugin-CUNjYYRY.d.mts → eventPlugin-qXpqTebY.d.mts} +57 -7
  41. package/dist/events/index.d.mts +164 -5
  42. package/dist/events/index.mjs +133 -209
  43. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  44. package/dist/events/transports/redis-stream-entry.mjs +204 -31
  45. package/dist/events/transports/redis.d.mts +1 -1
  46. package/dist/factory/index.d.mts +2 -2
  47. package/dist/factory/index.mjs +2 -2
  48. package/dist/{fields-C8Y0XLAu.d.mts → fields-COhcH3fk.d.mts} +23 -2
  49. package/dist/hooks/index.d.mts +1 -1
  50. package/dist/hooks/index.mjs +1 -1
  51. package/dist/idempotency/index.d.mts +3 -3
  52. package/dist/idempotency/index.mjs +1 -20
  53. package/dist/idempotency/redis.d.mts +1 -1
  54. package/dist/idempotency/redis.mjs +1 -1
  55. package/dist/{index-BYCqHCVu.d.mts → index-BTqLEvhu.d.mts} +164 -4
  56. package/dist/{index-6u4_Gg6G.d.mts → index-BtW7qYwa.d.mts} +661 -281
  57. package/dist/{index-BdXnTPRj.d.mts → index-Ds61mrJE.d.mts} +50 -4
  58. package/dist/{index-DdQ3O9Pg.d.mts → index-Dz5IKsrE.d.mts} +360 -219
  59. package/dist/index.d.mts +6 -7
  60. package/dist/index.mjs +9 -10
  61. package/dist/integrations/event-gateway.d.mts +2 -2
  62. package/dist/integrations/event-gateway.mjs +1 -1
  63. package/dist/integrations/index.d.mts +2 -2
  64. package/dist/integrations/mcp/index.d.mts +2 -2
  65. package/dist/integrations/mcp/index.mjs +1 -1
  66. package/dist/integrations/mcp/testing.d.mts +1 -1
  67. package/dist/integrations/mcp/testing.mjs +1 -1
  68. package/dist/integrations/streamline.d.mts +60 -11
  69. package/dist/integrations/streamline.mjs +75 -85
  70. package/dist/integrations/websocket-redis.d.mts +1 -1
  71. package/dist/integrations/websocket.d.mts +1 -1
  72. package/dist/integrations/websocket.mjs +2 -8
  73. package/dist/middleware/index.d.mts +1 -1
  74. package/dist/middleware/index.mjs +2 -2
  75. package/dist/migrations/index.d.mts +23 -3
  76. package/dist/migrations/index.mjs +0 -7
  77. package/dist/{multipartBody-CvTR1Un6.mjs → multipartBody-BOvVSVCD.mjs} +11 -8
  78. package/dist/{openapi-BGUn7Ki1.mjs → openapi-CiOMVW1p.mjs} +143 -13
  79. package/dist/org/index.d.mts +2 -2
  80. package/dist/org/index.mjs +1 -1
  81. package/dist/permissions/index.d.mts +3 -3
  82. package/dist/permissions/index.mjs +3 -3
  83. package/dist/{permissions-gd_aUWrR.mjs → permissions-ohQyv50e.mjs} +404 -176
  84. package/dist/{pipe-DVoIheVC.mjs → pipe-Zr0KXjQe.mjs} +1 -1
  85. package/dist/pipeline/index.d.mts +1 -1
  86. package/dist/pipeline/index.mjs +1 -1
  87. package/dist/plugins/index.d.mts +18 -33
  88. package/dist/plugins/index.mjs +33 -13
  89. package/dist/plugins/response-cache.mjs +1 -1
  90. package/dist/plugins/tracing-entry.d.mts +1 -1
  91. package/dist/plugins/tracing-entry.mjs +1 -1
  92. package/dist/presets/filesUpload.d.mts +5 -5
  93. package/dist/presets/filesUpload.mjs +6 -9
  94. package/dist/presets/index.d.mts +1 -1
  95. package/dist/presets/index.mjs +1 -1
  96. package/dist/presets/multiTenant.d.mts +1 -1
  97. package/dist/presets/multiTenant.mjs +2 -2
  98. package/dist/presets/search.d.mts +2 -2
  99. package/dist/presets/search.mjs +6 -8
  100. package/dist/{presets-Z7P5w4gF.mjs → presets-BbkjdPeH.mjs} +6 -28
  101. package/dist/{queryCachePlugin-BUXBSm4F.d.mts → queryCachePlugin-CqMdLI2-.d.mts} +2 -2
  102. package/dist/{queryCachePlugin-Bq6bO6vc.mjs → queryCachePlugin-m1XsgAIJ.mjs} +3 -3
  103. package/dist/{redis-Cm1gnRDf.d.mts → redis-DiMkdHEl.d.mts} +1 -1
  104. package/dist/redis-stream-D6HzR1Z_.d.mts +232 -0
  105. package/dist/registry/index.d.mts +1 -1
  106. package/dist/registry/index.mjs +2 -2
  107. package/dist/{replyHelpers-ByllIXXV.mjs → replyHelpers-CK-FNO8E.mjs} +3 -21
  108. package/dist/{resourceToTools-ByZpgjeH.mjs → resourceToTools-C5coh64w.mjs} +224 -71
  109. package/dist/{routerShared-BqLRb5l7.mjs → routerShared-D6_fEGHh.mjs} +40 -36
  110. package/dist/{schemaIR-BlG9bY7v.mjs → schemaIR-7Vl611Qs.mjs} +1 -1
  111. package/dist/schemas/index.d.mts +100 -30
  112. package/dist/schemas/index.mjs +86 -29
  113. package/dist/scim/index.d.mts +264 -0
  114. package/dist/scim/index.mjs +963 -0
  115. package/dist/scope/index.d.mts +3 -3
  116. package/dist/scope/index.mjs +4 -4
  117. package/dist/{sse-V7aXc3bW.mjs → sse-Bz-5ZeTt.mjs} +1 -1
  118. package/dist/{store-helpers-BhrzxvyQ.mjs → store-helpers-BkIN9-vu.mjs} +1 -1
  119. package/dist/testing/index.d.mts +2 -8
  120. package/dist/testing/index.mjs +16 -24
  121. package/dist/testing/storageContract.d.mts +1 -1
  122. package/dist/types/index.d.mts +4 -4
  123. package/dist/types/storage.d.mts +1 -1
  124. package/dist/{types-BH7dEGvU.d.mts → types-BvqwCCSx.d.mts} +77 -29
  125. package/dist/{types-tgR4Pt8F.d.mts → types-CTYvcwHe.d.mts} +195 -1
  126. package/dist/{types-AOD8fxIw.mjs → types-C_s5moIu.mjs} +117 -1
  127. package/dist/{types-9beEMe25.d.mts → types-DQHFc8PM.d.mts} +1 -1
  128. package/dist/utils/index.d.mts +2 -2
  129. package/dist/utils/index.mjs +5 -5
  130. package/dist/{utils-CcYTj09l.mjs → utils-_h9B3c57.mjs} +1269 -1334
  131. package/dist/{versioning-M9lNLhO8.d.mts → versioning-DTTvc80y.d.mts} +1 -1
  132. package/package.json +24 -34
  133. package/skills/arc/SKILL.md +521 -785
  134. package/skills/arc/references/agent-auth.md +238 -0
  135. package/skills/arc/references/api-reference.md +187 -0
  136. package/skills/arc/references/auth.md +354 -7
  137. package/skills/arc/references/enterprise-auth.md +94 -0
  138. package/skills/arc/references/events.md +8 -6
  139. package/skills/arc/references/mcp.md +2 -2
  140. package/skills/arc/references/multi-tenancy.md +11 -2
  141. package/skills/arc/references/production.md +10 -9
  142. package/skills/arc/references/scim.md +247 -0
  143. package/skills/arc/references/testing.md +1 -1
  144. package/skills/arc-code-review/SKILL.md +141 -0
  145. package/skills/arc-code-review/references/anti-patterns.md +911 -0
  146. package/skills/arc-code-review/references/arc-cheatsheet.md +380 -0
  147. package/skills/arc-code-review/references/migration-recipes.md +700 -0
  148. package/skills/arc-code-review/references/mongokit-migration.md +386 -0
  149. package/skills/arc-code-review/references/scaffolding.md +230 -0
  150. package/skills/arc-code-review/references/severity.md +127 -0
  151. package/dist/EventTransport-CfVEGaEl.d.mts +0 -293
  152. package/dist/adapters/index.d.mts +0 -3
  153. package/dist/adapters/index.mjs +0 -2
  154. package/dist/adapters-D0tT2Tyo.mjs +0 -949
  155. package/dist/auth/mongoose.d.mts +0 -191
  156. package/dist/auth/mongoose.mjs +0 -73
  157. package/dist/core-DnUsRpuX.mjs +0 -1049
  158. package/dist/errorHandler-BQm8ZxTK.mjs +0 -173
  159. package/dist/errorHandler-Co3lnVmJ.d.mts +0 -114
  160. package/dist/errors-D5c-5BJL.mjs +0 -232
  161. package/dist/index-BbMrcvGp.d.mts +0 -362
  162. package/dist/redis-stream-CM8TXTix.d.mts +0 -110
  163. /package/dist/{HookSystem-CGsMd6oK.mjs → HookSystem-Iiebom92.mjs} +0 -0
  164. /package/dist/{actionPermissions-sUUKDhtP.mjs → actionPermissions-CyUkQu6O.mjs} +0 -0
  165. /package/dist/{caching-CheW3m-S.mjs → caching-SM8gghN6.mjs} +0 -0
  166. /package/dist/{constants-BhY1OHoH.mjs → constants-Cxde4rpC.mjs} +0 -0
  167. /package/dist/{elevation-s5ykdNHr.d.mts → elevation-BXOWoGCF.d.mts} +0 -0
  168. /package/dist/{externalPaths-Bapitwvd.d.mts → externalPaths-BD5nw6St.d.mts} +0 -0
  169. /package/dist/{interface-CkkWm5uR.d.mts → interface-DfLGcus7.d.mts} +0 -0
  170. /package/dist/{interface-Da0r7Lna.d.mts → interface-beEtJyWM.d.mts} +0 -0
  171. /package/dist/{keys-CARyUjiR.mjs → keys-CGcCbNyu.mjs} +0 -0
  172. /package/dist/{loadResources-CPpkyKfM.mjs → loadResources-DBMQg_Aj.mjs} +0 -0
  173. /package/dist/{memory-DikHSvWa.mjs → memory-UBydS5ku.mjs} +0 -0
  174. /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
  175. /package/dist/{pluralize-BneOJkpi.mjs → pluralize-DQgqgifU.mjs} +0 -0
  176. /package/dist/{registry-D63ee7fl.mjs → registry-I-ogLgL9.mjs} +0 -0
  177. /package/dist/{requestContext-C5XeK3VA.mjs → requestContext-SSaaTgW8.mjs} +0 -0
  178. /package/dist/{schemaConverter-B0oKLuqI.mjs → schemaConverter-De34B1ZG.mjs} +0 -0
  179. /package/dist/{sessionManager-D-oNWHz3.d.mts → sessionManager-C4Le_UB3.d.mts} +0 -0
  180. /package/dist/{storage-BwGQXUpd.d.mts → storage-Dfzt4VTl.d.mts} +0 -0
  181. /package/dist/{tracing-DokiEsuz.d.mts → tracing-QJVprktp.d.mts} +0 -0
  182. /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-BzkXkvVv.mjs} +0 -0
  183. /package/dist/{types-DV9WDfeg.mjs → types-D57iXYb8.mjs} +0 -0
  184. /package/dist/{versioning-CGPjkqAg.mjs → versioning-BUrT5aP4.mjs} +0 -0
  185. /package/dist/{websocket-CyJ1VIFI.d.mts → websocket-ChC2rqe1.d.mts} +0 -0
@@ -0,0 +1,174 @@
1
+ import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
+ import { m as statusToArcCode, p as isArcError } from "./errors-j4aJm1Wg.mjs";
3
+ import { isHttpError, toErrorContract } from "@classytic/repo-core/errors";
4
+ import fp from "fastify-plugin";
5
+ //#region src/plugins/errorHandler.ts
6
+ var errorHandler_exports = /* @__PURE__ */ __exportAll({
7
+ defaultIsDuplicateKeyError: () => defaultIsDuplicateKeyError,
8
+ errorHandlerPlugin: () => errorHandlerPlugin
9
+ });
10
+ /**
11
+ * Default duplicate-key detector. Strict driver-code matching only — never
12
+ * message strings (false positives mask real WriteConflict / NotWritable
13
+ * errors as 409s). Long-tail drivers compose: see jsdoc on
14
+ * {@link ErrorHandlerOptions.isDuplicateKeyError}.
15
+ */
16
+ function defaultIsDuplicateKeyError(err) {
17
+ if (!err || typeof err !== "object") return false;
18
+ const e = err;
19
+ if (e.code === 11e3 || e.codeName === "DuplicateKey") return true;
20
+ if (e.code === "P2002") return true;
21
+ if (e.code === "23505") return true;
22
+ if (e.code === "ER_DUP_ENTRY" || e.errno === 1062) return true;
23
+ if (e.code === "SQLITE_CONSTRAINT_UNIQUE" || e.code === "SQLITE_CONSTRAINT_PRIMARYKEY") return true;
24
+ return false;
25
+ }
26
+ function extractDuplicateFields(err) {
27
+ if (!err || typeof err !== "object") return null;
28
+ const e = err;
29
+ if (e.keyValue && typeof e.keyValue === "object") return Object.keys(e.keyValue);
30
+ if (e.meta?.target) {
31
+ if (Array.isArray(e.meta.target)) return e.meta.target.map(String);
32
+ if (typeof e.meta.target === "string") return [e.meta.target];
33
+ }
34
+ if (typeof e.constraint === "string") return [e.constraint];
35
+ return null;
36
+ }
37
+ /** Map Fastify schema-validation errors → canonical `ErrorDetail[]`. */
38
+ function fastifyValidationDetails(error) {
39
+ return (error.validation ?? []).map((v) => {
40
+ const missingProperty = v.params?.missingProperty;
41
+ const path = v.instancePath?.replace(/^\//, "") || (typeof missingProperty === "string" ? missingProperty : void 0);
42
+ return {
43
+ ...path ? { path } : {},
44
+ code: v.keyword ?? "invalid",
45
+ message: v.message || "Invalid value"
46
+ };
47
+ });
48
+ }
49
+ /** Map Mongoose `ValidationError.errors` → canonical `ErrorDetail[]`. */
50
+ function mongooseValidationDetails(errors) {
51
+ return Object.entries(errors).map(([field, e]) => ({
52
+ path: e.path || field,
53
+ code: "validation_error",
54
+ message: e.message
55
+ }));
56
+ }
57
+ async function errorHandlerPluginFn(fastify, options = {}) {
58
+ const isProduction = process.env.NODE_ENV === "production";
59
+ const { includeStack = !isProduction, onError, errorMap = {}, errorMappers = [], isDuplicateKeyError = defaultIsDuplicateKeyError } = options;
60
+ fastify.setErrorHandler(async (error, request, reply) => {
61
+ if (onError) try {
62
+ await onError(error, request);
63
+ } catch (callbackError) {
64
+ request.log.error({ err: callbackError }, "Error in onError callback");
65
+ }
66
+ const correlationId = request.id;
67
+ const contract = classify(error, {
68
+ errorMappers,
69
+ errorMap,
70
+ isDuplicateKeyError
71
+ });
72
+ const meta = { ...contract.meta ?? {} };
73
+ if (includeStack && error.stack) meta.stack = error.stack;
74
+ const wire = {
75
+ ...contract,
76
+ ...correlationId ? { correlationId } : {},
77
+ ...Object.keys(meta).length > 0 ? { meta } : {}
78
+ };
79
+ const status = wire.status ?? 500;
80
+ if (status >= 500) request.log.error({
81
+ err: error,
82
+ status
83
+ }, "Server error");
84
+ else if (status >= 400) request.log.warn({
85
+ err: error,
86
+ status
87
+ }, "Client error");
88
+ if (isArcError(error) && error.cause) request.log.error({ cause: error.cause }, "Error cause chain");
89
+ return reply.status(status).send(wire);
90
+ });
91
+ }
92
+ /**
93
+ * Single-pass error → `ErrorContract` classifier. Dispatch order is fixed:
94
+ *
95
+ * 1. Class-based mappers (`instanceof`) — user-registered domain errors
96
+ * 2. `ArcError` / any `HttpError`-shaped throw — flows through `toErrorContract`
97
+ * 3. Fastify schema-validation errors — `error.validation` array
98
+ * 4. Fastify-style errors with a numeric `statusCode`
99
+ * 5. Name-keyed `errorMap` entries
100
+ * 6. Mongoose `ValidationError` / `CastError`
101
+ * 7. Driver-specific duplicate-key classifier
102
+ * 8. Fallback: `arc.internal_error` 500
103
+ */
104
+ function classify(error, ctx) {
105
+ for (const mapper of ctx.errorMappers) if (error instanceof mapper.type) {
106
+ const mapped = mapper.toResponse(error);
107
+ return {
108
+ code: mapped.code ?? statusToArcCode(mapped.status),
109
+ message: mapped.message ?? error.message,
110
+ status: mapped.status,
111
+ ...mapped.details ? { details: mapped.details } : {},
112
+ ...mapped.meta ? { meta: mapped.meta } : {}
113
+ };
114
+ }
115
+ if (isArcError(error) || isHttpError(error)) return toErrorContract(error);
116
+ const fastifyErr = error;
117
+ if (Array.isArray(fastifyErr.validation)) return {
118
+ code: "arc.validation_error",
119
+ message: "Validation failed",
120
+ status: 400,
121
+ details: fastifyValidationDetails(fastifyErr)
122
+ };
123
+ if (typeof fastifyErr.statusCode === "number") return {
124
+ code: statusToArcCode(fastifyErr.statusCode),
125
+ message: error.message || "Error",
126
+ status: fastifyErr.statusCode
127
+ };
128
+ if (error.name && ctx.errorMap[error.name]) {
129
+ const m = ctx.errorMap[error.name];
130
+ return {
131
+ code: m.code,
132
+ message: m.message ?? error.message,
133
+ status: m.statusCode
134
+ };
135
+ }
136
+ if (error.name === "ValidationError" && "errors" in error) {
137
+ const errs = error.errors;
138
+ return {
139
+ code: "arc.validation_error",
140
+ message: error.message || "Validation failed",
141
+ status: 400,
142
+ details: mongooseValidationDetails(errs)
143
+ };
144
+ }
145
+ if (error.name === "CastError") return {
146
+ code: "arc.invalid_id",
147
+ message: "Invalid identifier format",
148
+ status: 400
149
+ };
150
+ if (ctx.isDuplicateKeyError(error)) {
151
+ const fields = extractDuplicateFields(error);
152
+ return {
153
+ code: "arc.conflict",
154
+ message: "Resource already exists",
155
+ status: 409,
156
+ ...fields && fields.length > 0 ? { details: fields.map((f) => ({
157
+ path: f,
158
+ code: "duplicate_key",
159
+ message: `Duplicate value for "${f}"`
160
+ })) } : {}
161
+ };
162
+ }
163
+ return {
164
+ code: "arc.internal_error",
165
+ message: error.message || "Internal Server Error",
166
+ status: 500
167
+ };
168
+ }
169
+ const errorHandlerPlugin = fp(errorHandlerPluginFn, {
170
+ name: "arc-error-handler",
171
+ fastify: "5.x"
172
+ });
173
+ //#endregion
174
+ export { errorHandlerPlugin as n, errorHandler_exports as r, defaultIsDuplicateKeyError as t };
@@ -0,0 +1,45 @@
1
+ import { ErrorDetail } from "@classytic/repo-core/errors";
2
+ import { FastifyInstance, FastifyRequest } from "fastify";
3
+
4
+ //#region src/plugins/errorHandler.d.ts
5
+ /**
6
+ * Class-based error mapper — `instanceof` check converts a thrown class
7
+ * to a partial `ErrorContract`. Highest-priority dispatch in the handler.
8
+ */
9
+ interface ErrorMapper<T extends Error = Error> {
10
+ type: abstract new (...args: any[]) => T;
11
+ toResponse: (error: T) => {
12
+ status: number;
13
+ code?: string;
14
+ message?: string;
15
+ details?: ReadonlyArray<ErrorDetail>;
16
+ meta?: Record<string, unknown>;
17
+ };
18
+ }
19
+ interface ErrorHandlerOptions {
20
+ /** Include `meta.stack` on the wire (defaults to `NODE_ENV !== 'production'`). */
21
+ includeStack?: boolean;
22
+ /** Custom callback fired for every error — log to Sentry / Datadog / etc. */
23
+ onError?: (error: Error, request: FastifyRequest) => void | Promise<void>;
24
+ /** Map by `error.name` string. Lower priority than `errorMappers`. */
25
+ errorMap?: Record<string, {
26
+ statusCode: number;
27
+ code: string;
28
+ message?: string;
29
+ }>;
30
+ /** Map by `instanceof`. Highest priority — checked first. */
31
+ errorMappers?: ErrorMapper[];
32
+ /** Driver-aware duplicate-key classifier. Override to add long-tail drivers. */
33
+ isDuplicateKeyError?: (err: unknown) => boolean;
34
+ }
35
+ /**
36
+ * Default duplicate-key detector. Strict driver-code matching only — never
37
+ * message strings (false positives mask real WriteConflict / NotWritable
38
+ * errors as 409s). Long-tail drivers compose: see jsdoc on
39
+ * {@link ErrorHandlerOptions.isDuplicateKeyError}.
40
+ */
41
+ declare function defaultIsDuplicateKeyError(err: unknown): boolean;
42
+ declare function errorHandlerPluginFn(fastify: FastifyInstance, options?: ErrorHandlerOptions): Promise<void>;
43
+ declare const errorHandlerPlugin: typeof errorHandlerPluginFn;
44
+ //#endregion
45
+ export { errorHandlerPlugin as i, ErrorMapper as n, defaultIsDuplicateKeyError as r, ErrorHandlerOptions as t };
@@ -0,0 +1,184 @@
1
+ //#region src/utils/errors.ts
2
+ /**
3
+ * Base Arc Error. Implements the canonical `HttpError` contract — `status`
4
+ * mirrors `statusCode` and `meta` mirrors `details`, so consumers reading
5
+ * either name see the same value without adapter glue.
6
+ */
7
+ var ArcError = class extends Error {
8
+ name = "ArcError";
9
+ code;
10
+ statusCode;
11
+ details;
12
+ cause;
13
+ constructor(message, options = {}) {
14
+ super(message, options.cause ? { cause: options.cause } : void 0);
15
+ this.code = options.code ?? "arc.error";
16
+ this.statusCode = options.statusCode ?? 500;
17
+ this.details = options.details;
18
+ this.cause = options.cause;
19
+ if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
20
+ }
21
+ /** `HttpError.status` mirror — repo-core's `toErrorContract` reads this. */
22
+ get status() {
23
+ return this.statusCode;
24
+ }
25
+ /** `HttpError.meta` mirror — `details` under the canonical name. */
26
+ get meta() {
27
+ return this.details;
28
+ }
29
+ };
30
+ var NotFoundError = class extends ArcError {
31
+ constructor(resource, identifier) {
32
+ const message = identifier ? `${resource} with identifier '${identifier}' not found` : `${resource} not found`;
33
+ super(message, {
34
+ code: "arc.not_found",
35
+ statusCode: 404,
36
+ details: {
37
+ resource,
38
+ ...identifier ? { identifier } : {}
39
+ }
40
+ });
41
+ this.name = "NotFoundError";
42
+ }
43
+ };
44
+ var ValidationError = class extends ArcError {
45
+ errors;
46
+ constructor(message, errors = []) {
47
+ super(message, {
48
+ code: "arc.validation_error",
49
+ statusCode: 400,
50
+ details: { errors }
51
+ });
52
+ this.name = "ValidationError";
53
+ this.errors = errors;
54
+ }
55
+ };
56
+ var UnauthorizedError = class extends ArcError {
57
+ constructor(message = "Authentication required") {
58
+ super(message, {
59
+ code: "arc.unauthorized",
60
+ statusCode: 401
61
+ });
62
+ this.name = "UnauthorizedError";
63
+ }
64
+ };
65
+ var ForbiddenError = class extends ArcError {
66
+ constructor(message = "Access denied") {
67
+ super(message, {
68
+ code: "arc.forbidden",
69
+ statusCode: 403
70
+ });
71
+ this.name = "ForbiddenError";
72
+ }
73
+ };
74
+ var ConflictError = class extends ArcError {
75
+ constructor(message, field) {
76
+ super(message, {
77
+ code: "arc.conflict",
78
+ statusCode: 409,
79
+ ...field ? { details: { field } } : {}
80
+ });
81
+ this.name = "ConflictError";
82
+ }
83
+ };
84
+ var OrgRequiredError = class extends ArcError {
85
+ organizations;
86
+ constructor(message, organizations) {
87
+ super(message, {
88
+ code: "arc.org.selection_required",
89
+ statusCode: 403,
90
+ ...organizations ? { details: { organizations } } : {}
91
+ });
92
+ this.name = "OrgRequiredError";
93
+ this.organizations = organizations;
94
+ }
95
+ };
96
+ var OrgAccessDeniedError = class extends ArcError {
97
+ constructor(orgId) {
98
+ super("Organization access denied", {
99
+ code: "arc.org.access_denied",
100
+ statusCode: 403,
101
+ ...orgId ? { details: { organizationId: orgId } } : {}
102
+ });
103
+ this.name = "OrgAccessDeniedError";
104
+ }
105
+ };
106
+ var RateLimitError = class extends ArcError {
107
+ retryAfter;
108
+ constructor(message = "Too many requests", retryAfter) {
109
+ super(message, {
110
+ code: "arc.rate_limited",
111
+ statusCode: 429,
112
+ ...retryAfter ? { details: { retryAfter } } : {}
113
+ });
114
+ this.name = "RateLimitError";
115
+ this.retryAfter = retryAfter;
116
+ }
117
+ };
118
+ var ServiceUnavailableError = class extends ArcError {
119
+ constructor(message = "Service temporarily unavailable") {
120
+ super(message, {
121
+ code: "arc.service_unavailable",
122
+ statusCode: 503
123
+ });
124
+ this.name = "ServiceUnavailableError";
125
+ }
126
+ };
127
+ /**
128
+ * Status-code → canonical `arc.*` code mapping. Used by {@link createError}
129
+ * and the global error handler when no explicit code is supplied.
130
+ */
131
+ const STATUS_CODE_MAP = {
132
+ 400: "arc.bad_request",
133
+ 401: "arc.unauthorized",
134
+ 403: "arc.forbidden",
135
+ 404: "arc.not_found",
136
+ 409: "arc.conflict",
137
+ 422: "arc.unprocessable_entity",
138
+ 429: "arc.rate_limited",
139
+ 500: "arc.internal_error",
140
+ 502: "arc.bad_gateway",
141
+ 503: "arc.service_unavailable",
142
+ 504: "arc.gateway_timeout"
143
+ };
144
+ /** Status → canonical arc code, falling back to `arc.error`. */
145
+ function statusToArcCode(status) {
146
+ return STATUS_CODE_MAP[status] ?? "arc.error";
147
+ }
148
+ /**
149
+ * Quick `ArcError` constructor when the bundled subclasses don't fit.
150
+ *
151
+ * If `details.code` is a string, it's lifted to the top-level `error.code`
152
+ * (the canonical wire slot for business signals — `'ORG_CONTEXT_REQUIRED'`,
153
+ * `'ALL_FIELDS_STRIPPED'`, etc). The same code stays in `details` so
154
+ * in-process consumers reading `error.details.code` still work. Without
155
+ * this lift, business codes get buried in `details` and the wire envelope
156
+ * carries only the status-derived `arc.forbidden` / `arc.bad_request`
157
+ * fallback — `repo-core`'s `toErrorContract` doesn't surface free-form
158
+ * `details` objects (its `details[]` array is reserved for validation /
159
+ * duplicate-key items).
160
+ */
161
+ function createError(statusCode, message, details) {
162
+ return new ArcError(message, {
163
+ code: (typeof details?.code === "string" ? details.code : void 0) ?? statusToArcCode(statusCode),
164
+ statusCode,
165
+ ...details ? { details } : {}
166
+ });
167
+ }
168
+ /**
169
+ * Domain-error escape hatch. Use a hierarchical code that scopes the error
170
+ * to your package (`'commerce.cart.locked'`, `'payment.gateway.timeout'`).
171
+ */
172
+ function createDomainError(code, message, statusCode = 400, details) {
173
+ return new ArcError(message, {
174
+ code,
175
+ statusCode,
176
+ ...details ? { details } : {}
177
+ });
178
+ }
179
+ /** Type guard. */
180
+ function isArcError(error) {
181
+ return error instanceof ArcError;
182
+ }
183
+ //#endregion
184
+ export { OrgAccessDeniedError as a, ServiceUnavailableError as c, createDomainError as d, createError as f, NotFoundError as i, UnauthorizedError as l, statusToArcCode as m, ConflictError as n, OrgRequiredError as o, isArcError as p, ForbiddenError as r, RateLimitError as s, ArcError as t, ValidationError as u };
@@ -1,144 +1,20 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
- import { t as requestContext } from "./requestContext-C5XeK3VA.mjs";
2
+ import { arcLog } from "./logger/index.mjs";
3
+ import { d as createDomainError } from "./errors-j4aJm1Wg.mjs";
4
+ import { t as requestContext } from "./requestContext-SSaaTgW8.mjs";
5
+ import { n as createEvent, t as MemoryEventTransport } from "./EventTransport-DLWoUMHy.mjs";
3
6
  import fp from "fastify-plugin";
4
- //#region src/events/EventTransport.ts
5
- /**
6
- * In-memory event transport (default)
7
- * Events are delivered synchronously within the process.
8
- * Not suitable for multi-instance deployments.
9
- */
10
- var MemoryEventTransport = class {
11
- name = "memory";
12
- handlers = /* @__PURE__ */ new Map();
13
- logger;
14
- constructor(options) {
15
- this.logger = options?.logger ?? console;
16
- }
17
- async publish(event) {
18
- const exactHandlers = this.handlers.get(event.type) ?? /* @__PURE__ */ new Set();
19
- const wildcardHandlers = this.handlers.get("*") ?? /* @__PURE__ */ new Set();
20
- const patternHandlers = /* @__PURE__ */ new Set();
21
- for (const [pattern, handlers] of this.handlers.entries()) if (pattern.endsWith(".*")) {
22
- const prefix = pattern.slice(0, -2);
23
- if (event.type.startsWith(`${prefix}.`)) for (const h of handlers) patternHandlers.add(h);
24
- }
25
- const allHandlers = new Set([
26
- ...exactHandlers,
27
- ...wildcardHandlers,
28
- ...patternHandlers
29
- ]);
30
- for (const handler of allHandlers) try {
31
- await handler(event);
32
- } catch (err) {
33
- this.logger.error(`[EventTransport] Handler error for ${event.type}:`, err);
34
- }
35
- }
36
- /**
37
- * Reference `publishMany` implementation — delegates to `publish()` in order.
38
- *
39
- * Production transports (Kafka, Redis pipeline, SQS batch) should override
40
- * this with a single batched network call. Memory transport has nothing to
41
- * batch, so we just loop — the loop still returns a proper result map so
42
- * `EventOutbox.relay` can exercise the batched code path in tests.
43
- */
44
- async publishMany(events) {
45
- const results = /* @__PURE__ */ new Map();
46
- for (const event of events) try {
47
- await this.publish(event);
48
- results.set(event.meta.id, null);
49
- } catch (err) {
50
- results.set(event.meta.id, err instanceof Error ? err : new Error(String(err)));
51
- }
52
- return results;
53
- }
54
- async subscribe(pattern, handler) {
55
- if (!this.handlers.has(pattern)) this.handlers.set(pattern, /* @__PURE__ */ new Set());
56
- this.handlers.get(pattern)?.add(handler);
57
- return () => {
58
- const set = this.handlers.get(pattern);
59
- if (set) {
60
- set.delete(handler);
61
- if (set.size === 0) this.handlers.delete(pattern);
62
- }
63
- };
64
- }
65
- async close() {
66
- this.handlers.clear();
67
- }
68
- };
69
- /**
70
- * Create a domain event with auto-generated metadata.
71
- *
72
- * `id` and `timestamp` are filled in; everything else is caller-controlled.
73
- * Set `schemaVersion` explicitly for any event type you plan to evolve.
74
- */
75
- function createEvent(type, payload, meta) {
76
- return {
77
- type,
78
- payload,
79
- meta: {
80
- id: crypto.randomUUID(),
81
- timestamp: /* @__PURE__ */ new Date(),
82
- ...meta
83
- }
84
- };
85
- }
86
- /**
87
- * Create a child event that chains causation from a parent event.
88
- *
89
- * Rules:
90
- * - `causationId` is set to the parent's `id` (direct cause)
91
- * - `correlationId` is inherited from the parent if set, else falls back
92
- * to the parent's `id` (root correlation)
93
- * - `userId` / `organizationId` are inherited when not overridden so the
94
- * whole chain stays scoped to the originating principal/tenant
95
- *
96
- * Caller-supplied `meta` wins over inherited fields — pass `{ userId: newActor }`
97
- * to override when a subsystem acts on behalf of a different principal.
98
- *
99
- * @example
100
- * ```typescript
101
- * const orderPlaced = createEvent('order.placed', { orderId: 'o1' }, {
102
- * correlationId: req.id, userId: user.id,
103
- * });
104
- * await events.publish(orderPlaced);
105
- *
106
- * // Downstream handler emits a child event:
107
- * const reserved = createChildEvent(orderPlaced, 'inventory.reserved', {
108
- * orderId: 'o1', skus: ['sku-1', 'sku-2'],
109
- * });
110
- * // reserved.meta.causationId === orderPlaced.meta.id
111
- * // reserved.meta.correlationId === orderPlaced.meta.correlationId
112
- * // reserved.meta.userId === user.id (inherited)
113
- * ```
114
- */
115
- function createChildEvent(parent, type, payload, meta) {
116
- const inherited = {
117
- correlationId: parent.meta.correlationId ?? parent.meta.id,
118
- causationId: parent.meta.id
119
- };
120
- if (parent.meta.userId !== void 0) inherited.userId = parent.meta.userId;
121
- if (parent.meta.organizationId !== void 0) inherited.organizationId = parent.meta.organizationId;
122
- if (parent.meta.source !== void 0) inherited.source = parent.meta.source;
123
- if (parent.meta.idempotencyKey !== void 0) inherited.idempotencyKey = parent.meta.idempotencyKey;
124
- return {
125
- type,
126
- payload,
127
- meta: {
128
- id: crypto.randomUUID(),
129
- timestamp: /* @__PURE__ */ new Date(),
130
- ...inherited,
131
- ...meta
132
- }
133
- };
134
- }
135
- //#endregion
136
7
  //#region src/events/retry.ts
137
8
  /**
138
9
  * Wrap an event handler with retry logic and dead letter support.
139
10
  *
140
11
  * On failure, retries with exponential backoff (with jitter).
141
12
  * After all retries exhausted, calls `onDead` callback if provided.
13
+ *
14
+ * Generic in the payload type `T` so composing with `wrapWithSchema<T>` /
15
+ * `subscribeWithSchema<T>` doesn't force a cast at the boundary — the inner
16
+ * `handler: EventHandler<T>` flows through to the returned wrapper. Defaults
17
+ * to `unknown` for raw `subscribe(pattern, withRetry(...))` call sites.
142
18
  */
143
19
  function withRetry(handler, options = {}) {
144
20
  const { maxRetries = 3, backoffMs = 1e3, maxBackoffMs = 3e4, jitter = .1, transport, onDead, name, logger = console } = options;
@@ -235,8 +111,18 @@ var eventPlugin_exports = /* @__PURE__ */ __exportAll({
235
111
  eventPlugin: () => eventPlugin
236
112
  });
237
113
  const eventPlugin = async (fastify, opts = {}) => {
238
- const { transport = new MemoryEventTransport(), logEvents = false, failOpen = true, retry: retryOpts, deadLetterQueue: dlqOpts, wal, onPublish, onPublishError, registry, validateMode: rawValidateMode } = opts;
114
+ const { transport = new MemoryEventTransport(), logEvents = false, failOpen = true, retry: retryOpts, deadLetterQueue: dlqOpts, wal, onPublish, onPublishError, registry, validateMode: rawValidateMode, warnOnDuplicate: rawWarnOnDuplicate } = opts;
239
115
  const validateMode = rawValidateMode ?? (registry ? "warn" : "off");
116
+ const warnOnDuplicate = rawWarnOnDuplicate ?? process.env.NODE_ENV !== "production";
117
+ const DUP_WINDOW_MS = 5e3;
118
+ const recentPublishes = warnOnDuplicate ? /* @__PURE__ */ new Map() : /* @__PURE__ */ new Map();
119
+ const evictExpiredPublishes = (now) => {
120
+ if (recentPublishes.size === 0) return;
121
+ for (const [key, timestamp] of recentPublishes) {
122
+ if (now - timestamp <= DUP_WINDOW_MS) break;
123
+ recentPublishes.delete(key);
124
+ }
125
+ };
240
126
  fastify.decorate("events", {
241
127
  publish: async (type, payload, meta) => {
242
128
  if (!type || typeof type !== "string") throw new Error("[Arc Events] Event type must be a non-empty string");
@@ -247,16 +133,28 @@ const eventPlugin = async (fastify, opts = {}) => {
247
133
  ...store?.requestId && !meta?.correlationId ? { correlationId: store.requestId } : {},
248
134
  ...meta
249
135
  });
136
+ if (warnOnDuplicate && event.meta.correlationId) {
137
+ const now = Date.now();
138
+ evictExpiredPublishes(now);
139
+ const dupKey = `${type}::${event.meta.correlationId}`;
140
+ const previous = recentPublishes.get(dupKey);
141
+ if (previous !== void 0 && now - previous <= DUP_WINDOW_MS) arcLog("events").warn(`Duplicate publish detected: event type "${type}" published twice within ${DUP_WINDOW_MS}ms with correlationId "${event.meta.correlationId}". Subscribers will fire twice for the same logical event. Common cause: a domain service holds both a publisher and a notification helper that also publishes to the same bus — pick one. Set \`arcPlugins: { events: { warnOnDuplicate: false } }\` to silence.`);
142
+ recentPublishes.delete(dupKey);
143
+ recentPublishes.set(dupKey, now);
144
+ }
250
145
  if (logEvents) fastify.log?.info?.({
251
146
  eventType: type,
252
147
  eventId: event.meta.id,
253
148
  correlationId: event.meta.correlationId
254
149
  }, "Publishing event");
255
150
  if (registry && validateMode !== "off") {
256
- const result = registry.validate(type, payload);
151
+ const result = registry.validate(type, payload, event.meta.schemaVersion);
257
152
  if (!result.valid) {
258
153
  const msg = `[Arc Events] Event '${type}' payload validation failed: ${result.errors?.join("; ")}`;
259
- if (validateMode === "reject") throw new Error(msg);
154
+ if (validateMode === "reject") throw createDomainError("arc.event.validation_error", msg, 400, {
155
+ event: type,
156
+ errors: result.errors
157
+ });
260
158
  fastify.log?.warn?.(msg);
261
159
  }
262
160
  }
@@ -321,4 +219,4 @@ var eventPlugin_default = fp(eventPlugin, {
321
219
  fastify: "5.x"
322
220
  });
323
221
  //#endregion
324
- export { MemoryEventTransport as a, withRetry as i, eventPlugin_exports as n, createChildEvent as o, createDeadLetterPublisher as r, createEvent as s, eventPlugin as t };
222
+ export { withRetry as i, eventPlugin_exports as n, createDeadLetterPublisher as r, eventPlugin as t };