@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.
- package/README.md +27 -18
- package/dist/{BaseController-swXruJ2_.mjs → BaseController-DX_T-bDB.mjs} +388 -423
- package/dist/EventTransport-CT_52aWU.d.mts +34 -0
- package/dist/EventTransport-DLWoUMHy.mjs +103 -0
- package/dist/{QueryCache-DOBNHBE0.d.mts → QueryCache-D41bfdBB.d.mts} +1 -1
- package/dist/{ResourceRegistry-DkAeAuTX.mjs → ResourceRegistry-CTERg_2x.mjs} +139 -66
- package/dist/audit/index.d.mts +2 -2
- package/dist/audit/index.mjs +1 -1
- package/dist/auth/audit.d.mts +199 -0
- package/dist/auth/audit.mjs +288 -0
- package/dist/auth/index.d.mts +5 -5
- package/dist/auth/index.mjs +117 -191
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{betterAuthOpenApi-DwxtK3uG.mjs → betterAuthOpenApi--M_i87dQ.mjs} +1 -1
- package/dist/buildHandler-olo-gt94.mjs +610 -0
- package/dist/cache/index.d.mts +3 -3
- package/dist/cache/index.mjs +3 -3
- package/dist/cli/commands/describe.d.mts +89 -13
- package/dist/cli/commands/describe.mjs +56 -2
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +147 -48
- package/dist/cli/commands/init.d.mts +13 -0
- package/dist/cli/commands/init.mjs +237 -112
- package/dist/cli/commands/introspect.mjs +8 -1
- package/dist/context/index.mjs +1 -1
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +5 -5
- package/dist/core-D72ia0EH.mjs +1399 -0
- package/dist/{createActionRouter-u3ql2EDo.mjs → createActionRouter-CEvzKcy8.mjs} +7 -20
- package/dist/createAggregationRouter-CyecOxnO.mjs +114 -0
- package/dist/{createApp-BFxtdKy6.mjs → createApp-XX2-N0Yd.mjs} +31 -27
- package/dist/defineEvent-D5h7EvAx.mjs +188 -0
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-DOFoxoDs.mjs → elevation-DgoeTyfX.mjs} +1 -1
- package/dist/errorHandler-Bk-AGhkU.mjs +174 -0
- package/dist/errorHandler-DFr45ZG4.d.mts +45 -0
- package/dist/errors-j4aJm1Wg.mjs +184 -0
- package/dist/{eventPlugin-KrFIQ097.mjs → eventPlugin-CaKTYkYM.mjs} +35 -137
- package/dist/{eventPlugin-CUNjYYRY.d.mts → eventPlugin-qXpqTebY.d.mts} +57 -7
- package/dist/events/index.d.mts +164 -5
- package/dist/events/index.mjs +133 -209
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +204 -31
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +2 -2
- package/dist/factory/index.mjs +2 -2
- package/dist/{fields-C8Y0XLAu.d.mts → fields-COhcH3fk.d.mts} +23 -2
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +3 -3
- package/dist/idempotency/index.mjs +1 -20
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +1 -1
- package/dist/{index-BYCqHCVu.d.mts → index-BTqLEvhu.d.mts} +164 -4
- package/dist/{index-6u4_Gg6G.d.mts → index-BtW7qYwa.d.mts} +661 -281
- package/dist/{index-BdXnTPRj.d.mts → index-Ds61mrJE.d.mts} +50 -4
- package/dist/{index-DdQ3O9Pg.d.mts → index-Dz5IKsrE.d.mts} +360 -219
- package/dist/index.d.mts +6 -7
- package/dist/index.mjs +9 -10
- package/dist/integrations/event-gateway.d.mts +2 -2
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/streamline.d.mts +60 -11
- package/dist/integrations/streamline.mjs +75 -85
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +2 -8
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +2 -2
- package/dist/migrations/index.d.mts +23 -3
- package/dist/migrations/index.mjs +0 -7
- package/dist/{multipartBody-CvTR1Un6.mjs → multipartBody-BOvVSVCD.mjs} +11 -8
- package/dist/{openapi-BGUn7Ki1.mjs → openapi-CiOMVW1p.mjs} +143 -13
- package/dist/org/index.d.mts +2 -2
- package/dist/org/index.mjs +1 -1
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +3 -3
- package/dist/{permissions-gd_aUWrR.mjs → permissions-ohQyv50e.mjs} +404 -176
- package/dist/{pipe-DVoIheVC.mjs → pipe-Zr0KXjQe.mjs} +1 -1
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +1 -1
- package/dist/plugins/index.d.mts +18 -33
- package/dist/plugins/index.mjs +33 -13
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +5 -5
- package/dist/presets/filesUpload.mjs +6 -9
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +2 -2
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +6 -8
- package/dist/{presets-Z7P5w4gF.mjs → presets-BbkjdPeH.mjs} +6 -28
- package/dist/{queryCachePlugin-BUXBSm4F.d.mts → queryCachePlugin-CqMdLI2-.d.mts} +2 -2
- package/dist/{queryCachePlugin-Bq6bO6vc.mjs → queryCachePlugin-m1XsgAIJ.mjs} +3 -3
- package/dist/{redis-Cm1gnRDf.d.mts → redis-DiMkdHEl.d.mts} +1 -1
- package/dist/redis-stream-D6HzR1Z_.d.mts +232 -0
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{replyHelpers-ByllIXXV.mjs → replyHelpers-CK-FNO8E.mjs} +3 -21
- package/dist/{resourceToTools-ByZpgjeH.mjs → resourceToTools-C5coh64w.mjs} +224 -71
- package/dist/{routerShared-BqLRb5l7.mjs → routerShared-D6_fEGHh.mjs} +40 -36
- package/dist/{schemaIR-BlG9bY7v.mjs → schemaIR-7Vl611Qs.mjs} +1 -1
- package/dist/schemas/index.d.mts +100 -30
- package/dist/schemas/index.mjs +86 -29
- package/dist/scim/index.d.mts +264 -0
- package/dist/scim/index.mjs +963 -0
- package/dist/scope/index.d.mts +3 -3
- package/dist/scope/index.mjs +4 -4
- package/dist/{sse-V7aXc3bW.mjs → sse-Bz-5ZeTt.mjs} +1 -1
- package/dist/{store-helpers-BhrzxvyQ.mjs → store-helpers-BkIN9-vu.mjs} +1 -1
- package/dist/testing/index.d.mts +2 -8
- package/dist/testing/index.mjs +16 -24
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-BH7dEGvU.d.mts → types-BvqwCCSx.d.mts} +77 -29
- package/dist/{types-tgR4Pt8F.d.mts → types-CTYvcwHe.d.mts} +195 -1
- package/dist/{types-AOD8fxIw.mjs → types-C_s5moIu.mjs} +117 -1
- package/dist/{types-9beEMe25.d.mts → types-DQHFc8PM.d.mts} +1 -1
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +5 -5
- package/dist/{utils-CcYTj09l.mjs → utils-_h9B3c57.mjs} +1269 -1334
- package/dist/{versioning-M9lNLhO8.d.mts → versioning-DTTvc80y.d.mts} +1 -1
- package/package.json +24 -34
- package/skills/arc/SKILL.md +521 -785
- package/skills/arc/references/agent-auth.md +238 -0
- package/skills/arc/references/api-reference.md +187 -0
- package/skills/arc/references/auth.md +354 -7
- package/skills/arc/references/enterprise-auth.md +94 -0
- package/skills/arc/references/events.md +8 -6
- package/skills/arc/references/mcp.md +2 -2
- package/skills/arc/references/multi-tenancy.md +11 -2
- package/skills/arc/references/production.md +10 -9
- package/skills/arc/references/scim.md +247 -0
- package/skills/arc/references/testing.md +1 -1
- package/skills/arc-code-review/SKILL.md +141 -0
- package/skills/arc-code-review/references/anti-patterns.md +911 -0
- package/skills/arc-code-review/references/arc-cheatsheet.md +380 -0
- package/skills/arc-code-review/references/migration-recipes.md +700 -0
- package/skills/arc-code-review/references/mongokit-migration.md +386 -0
- package/skills/arc-code-review/references/scaffolding.md +230 -0
- package/skills/arc-code-review/references/severity.md +127 -0
- package/dist/EventTransport-CfVEGaEl.d.mts +0 -293
- package/dist/adapters/index.d.mts +0 -3
- package/dist/adapters/index.mjs +0 -2
- package/dist/adapters-D0tT2Tyo.mjs +0 -949
- package/dist/auth/mongoose.d.mts +0 -191
- package/dist/auth/mongoose.mjs +0 -73
- package/dist/core-DnUsRpuX.mjs +0 -1049
- package/dist/errorHandler-BQm8ZxTK.mjs +0 -173
- package/dist/errorHandler-Co3lnVmJ.d.mts +0 -114
- package/dist/errors-D5c-5BJL.mjs +0 -232
- package/dist/index-BbMrcvGp.d.mts +0 -362
- package/dist/redis-stream-CM8TXTix.d.mts +0 -110
- /package/dist/{HookSystem-CGsMd6oK.mjs → HookSystem-Iiebom92.mjs} +0 -0
- /package/dist/{actionPermissions-sUUKDhtP.mjs → actionPermissions-CyUkQu6O.mjs} +0 -0
- /package/dist/{caching-CheW3m-S.mjs → caching-SM8gghN6.mjs} +0 -0
- /package/dist/{constants-BhY1OHoH.mjs → constants-Cxde4rpC.mjs} +0 -0
- /package/dist/{elevation-s5ykdNHr.d.mts → elevation-BXOWoGCF.d.mts} +0 -0
- /package/dist/{externalPaths-Bapitwvd.d.mts → externalPaths-BD5nw6St.d.mts} +0 -0
- /package/dist/{interface-CkkWm5uR.d.mts → interface-DfLGcus7.d.mts} +0 -0
- /package/dist/{interface-Da0r7Lna.d.mts → interface-beEtJyWM.d.mts} +0 -0
- /package/dist/{keys-CARyUjiR.mjs → keys-CGcCbNyu.mjs} +0 -0
- /package/dist/{loadResources-CPpkyKfM.mjs → loadResources-DBMQg_Aj.mjs} +0 -0
- /package/dist/{memory-DikHSvWa.mjs → memory-UBydS5ku.mjs} +0 -0
- /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
- /package/dist/{pluralize-BneOJkpi.mjs → pluralize-DQgqgifU.mjs} +0 -0
- /package/dist/{registry-D63ee7fl.mjs → registry-I-ogLgL9.mjs} +0 -0
- /package/dist/{requestContext-C5XeK3VA.mjs → requestContext-SSaaTgW8.mjs} +0 -0
- /package/dist/{schemaConverter-B0oKLuqI.mjs → schemaConverter-De34B1ZG.mjs} +0 -0
- /package/dist/{sessionManager-D-oNWHz3.d.mts → sessionManager-C4Le_UB3.d.mts} +0 -0
- /package/dist/{storage-BwGQXUpd.d.mts → storage-Dfzt4VTl.d.mts} +0 -0
- /package/dist/{tracing-DokiEsuz.d.mts → tracing-QJVprktp.d.mts} +0 -0
- /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-BzkXkvVv.mjs} +0 -0
- /package/dist/{types-DV9WDfeg.mjs → types-D57iXYb8.mjs} +0 -0
- /package/dist/{versioning-CGPjkqAg.mjs → versioning-BUrT5aP4.mjs} +0 -0
- /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 {
|
|
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
|
|
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 {
|
|
222
|
+
export { withRetry as i, eventPlugin_exports as n, createDeadLetterPublisher as r, eventPlugin as t };
|