@beignet/core 0.0.14 → 0.0.15
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/CHANGELOG.md +14 -0
- package/README.md +46 -4
- package/dist/ports/index.d.ts +13 -0
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js +5 -0
- package/dist/ports/index.js.map +1 -1
- package/dist/server/hooks/error-reporting.d.ts +79 -0
- package/dist/server/hooks/error-reporting.d.ts.map +1 -0
- package/dist/server/hooks/error-reporting.js +147 -0
- package/dist/server/hooks/error-reporting.js.map +1 -0
- package/dist/server/hooks/index.d.ts +1 -0
- package/dist/server/hooks/index.d.ts.map +1 -1
- package/dist/server/hooks/index.js +1 -0
- package/dist/server/hooks/index.js.map +1 -1
- package/dist/server/server.d.ts +5 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js.map +1 -1
- package/dist/server/use-case-route.d.ts +2 -2
- package/dist/webhooks/index.d.ts +195 -0
- package/dist/webhooks/index.d.ts.map +1 -0
- package/dist/webhooks/index.js +313 -0
- package/dist/webhooks/index.js.map +1 -0
- package/package.json +5 -1
- package/src/ports/index.ts +37 -0
- package/src/server/hooks/error-reporting.ts +249 -0
- package/src/server/hooks/index.ts +8 -0
- package/src/server/server.ts +5 -0
- package/src/server/use-case-route.ts +2 -2
- package/src/webhooks/index.ts +555 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @beignet/core/webhooks
|
|
3
|
+
*
|
|
4
|
+
* Provider-neutral inbound webhook primitives for Beignet applications.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Error thrown for invalid webhook definitions and inputs.
|
|
8
|
+
*/
|
|
9
|
+
export class WebhookOptionsError extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "WebhookOptionsError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Error thrown when verification fails.
|
|
17
|
+
*/
|
|
18
|
+
export class WebhookVerificationError extends Error {
|
|
19
|
+
webhookName;
|
|
20
|
+
provider;
|
|
21
|
+
code;
|
|
22
|
+
cause;
|
|
23
|
+
constructor(args) {
|
|
24
|
+
super(args.message);
|
|
25
|
+
this.name = "WebhookVerificationError";
|
|
26
|
+
this.webhookName = args.webhookName;
|
|
27
|
+
this.provider = args.provider;
|
|
28
|
+
this.code = args.code;
|
|
29
|
+
this.cause = args.cause;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Error thrown when a verified event fails payload validation.
|
|
34
|
+
*/
|
|
35
|
+
export class WebhookValidationError extends Error {
|
|
36
|
+
webhookName;
|
|
37
|
+
eventType;
|
|
38
|
+
issues;
|
|
39
|
+
constructor(args) {
|
|
40
|
+
super(`Webhook "${args.webhookName}" event "${args.eventType}" payload validation failed: ${formatIssues(args.issues)}`);
|
|
41
|
+
this.name = "WebhookValidationError";
|
|
42
|
+
this.webhookName = args.webhookName;
|
|
43
|
+
this.eventType = args.eventType;
|
|
44
|
+
this.issues = args.issues;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Define a typed inbound webhook surface.
|
|
49
|
+
*/
|
|
50
|
+
export function defineWebhook(name, options = {}) {
|
|
51
|
+
if (!name)
|
|
52
|
+
throw new WebhookOptionsError("Webhook name is required.");
|
|
53
|
+
return {
|
|
54
|
+
name,
|
|
55
|
+
provider: options.provider,
|
|
56
|
+
events: (options.events ?? {}),
|
|
57
|
+
verifier: options.verifier,
|
|
58
|
+
metadata: options.metadata,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Verify a raw webhook request and validate the matching event payload schema.
|
|
63
|
+
*/
|
|
64
|
+
export async function verifyWebhook(webhook, input, options = {}) {
|
|
65
|
+
validateWebhook(webhook);
|
|
66
|
+
const verifier = options.verifier ?? webhook.verifier;
|
|
67
|
+
if (!verifier) {
|
|
68
|
+
throw new WebhookOptionsError(`Webhook "${webhook.name}" does not have a verifier.`);
|
|
69
|
+
}
|
|
70
|
+
let event;
|
|
71
|
+
try {
|
|
72
|
+
event = await verifier.verify(input);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error instanceof WebhookVerificationError)
|
|
76
|
+
throw error;
|
|
77
|
+
throw new WebhookVerificationError({
|
|
78
|
+
message: `Webhook "${webhook.name}" verification failed.`,
|
|
79
|
+
webhookName: webhook.name,
|
|
80
|
+
provider: webhook.provider,
|
|
81
|
+
code: "verification_failed",
|
|
82
|
+
cause: error,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return parseWebhookEvent(webhook, event, options);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Validate a verified webhook event against its catalog entry.
|
|
89
|
+
*/
|
|
90
|
+
export async function parseWebhookEvent(webhook, event, options = {}) {
|
|
91
|
+
validateWebhook(webhook);
|
|
92
|
+
validateEvent(webhook, event);
|
|
93
|
+
const schema = webhook.events[event.type];
|
|
94
|
+
if (!schema) {
|
|
95
|
+
if (options.allowUnknownEvents ?? true)
|
|
96
|
+
return event;
|
|
97
|
+
throw new WebhookVerificationError({
|
|
98
|
+
message: `Webhook "${webhook.name}" received unknown event type "${event.type}".`,
|
|
99
|
+
webhookName: webhook.name,
|
|
100
|
+
provider: webhook.provider,
|
|
101
|
+
code: "unknown_event_type",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const result = await schema["~standard"].validate(event.payload);
|
|
105
|
+
if (result.issues?.length) {
|
|
106
|
+
throw new WebhookValidationError({
|
|
107
|
+
webhookName: webhook.name,
|
|
108
|
+
eventType: event.type,
|
|
109
|
+
issues: result.issues,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (!("value" in result)) {
|
|
113
|
+
throw new Error("Invalid Standard Schema result: missing value");
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
...event,
|
|
117
|
+
payload: result.value,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Create an in-memory verifier for tests.
|
|
122
|
+
*/
|
|
123
|
+
export function createMemoryWebhookVerifier(options = {}) {
|
|
124
|
+
const queuedEvents = [...(options.events ?? [])];
|
|
125
|
+
const verifiedEvents = [];
|
|
126
|
+
return {
|
|
127
|
+
get verifiedEvents() {
|
|
128
|
+
return verifiedEvents;
|
|
129
|
+
},
|
|
130
|
+
async verify() {
|
|
131
|
+
const event = queuedEvents.shift();
|
|
132
|
+
if (!event) {
|
|
133
|
+
throw new WebhookVerificationError({
|
|
134
|
+
message: "No memory webhook event is queued.",
|
|
135
|
+
code: "missing_memory_event",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
verifiedEvents.push(event);
|
|
139
|
+
return event;
|
|
140
|
+
},
|
|
141
|
+
queue(event) {
|
|
142
|
+
queuedEvents.push(event);
|
|
143
|
+
},
|
|
144
|
+
reset() {
|
|
145
|
+
queuedEvents.length = 0;
|
|
146
|
+
verifiedEvents.length = 0;
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Create a generic JSON + HMAC verifier.
|
|
152
|
+
*/
|
|
153
|
+
export function createHmacWebhookVerifier(options) {
|
|
154
|
+
if (!options.secret) {
|
|
155
|
+
throw new WebhookOptionsError("Webhook HMAC secret is required.");
|
|
156
|
+
}
|
|
157
|
+
const signatureHeader = normalizeHeaderName(options.signatureHeader ?? "x-webhook-signature");
|
|
158
|
+
const algorithm = options.algorithm ?? "SHA-256";
|
|
159
|
+
const eventIdPath = options.eventIdPath ?? "id";
|
|
160
|
+
const eventTypePath = options.eventTypePath ?? "type";
|
|
161
|
+
return {
|
|
162
|
+
async verify(input) {
|
|
163
|
+
const signature = input.signature ?? input.headers?.[signatureHeader];
|
|
164
|
+
if (!signature) {
|
|
165
|
+
throw new WebhookVerificationError({
|
|
166
|
+
message: `Missing ${signatureHeader} header.`,
|
|
167
|
+
provider: options.provider,
|
|
168
|
+
code: "missing_signature",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const expected = await hmacHex(algorithm, options.secret, input.rawBody);
|
|
172
|
+
const actual = normalizeSignature(signature, options.signaturePrefix);
|
|
173
|
+
if (!(await timingSafeStringEqual(actual, expected))) {
|
|
174
|
+
throw new WebhookVerificationError({
|
|
175
|
+
message: "Webhook signature is invalid.",
|
|
176
|
+
provider: options.provider,
|
|
177
|
+
code: "invalid_signature",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const payload = parseJsonBody(input.rawBody);
|
|
181
|
+
const id = readStringPath(payload, eventIdPath);
|
|
182
|
+
const type = readStringPath(payload, eventTypePath);
|
|
183
|
+
if (!id) {
|
|
184
|
+
throw new WebhookVerificationError({
|
|
185
|
+
message: `Webhook payload is missing string event ID at "${eventIdPath}".`,
|
|
186
|
+
provider: options.provider,
|
|
187
|
+
code: "missing_event_id",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (!type) {
|
|
191
|
+
throw new WebhookVerificationError({
|
|
192
|
+
message: `Webhook payload is missing string event type at "${eventTypePath}".`,
|
|
193
|
+
provider: options.provider,
|
|
194
|
+
code: "missing_event_type",
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
id,
|
|
199
|
+
type,
|
|
200
|
+
provider: options.provider,
|
|
201
|
+
payload,
|
|
202
|
+
raw: payload,
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function validateWebhook(webhook) {
|
|
208
|
+
if (!webhook.name)
|
|
209
|
+
throw new WebhookOptionsError("Webhook name is required.");
|
|
210
|
+
}
|
|
211
|
+
function validateEvent(webhook, event) {
|
|
212
|
+
if (!event.id) {
|
|
213
|
+
throw new WebhookVerificationError({
|
|
214
|
+
message: `Webhook "${webhook.name}" event is missing an ID.`,
|
|
215
|
+
webhookName: webhook.name,
|
|
216
|
+
provider: webhook.provider,
|
|
217
|
+
code: "missing_event_id",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (!event.type) {
|
|
221
|
+
throw new WebhookVerificationError({
|
|
222
|
+
message: `Webhook "${webhook.name}" event is missing a type.`,
|
|
223
|
+
webhookName: webhook.name,
|
|
224
|
+
provider: webhook.provider,
|
|
225
|
+
code: "missing_event_type",
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function formatPath(path) {
|
|
230
|
+
if (!path?.length)
|
|
231
|
+
return "";
|
|
232
|
+
return path
|
|
233
|
+
.map((segment) => typeof segment === "object" && segment !== null && "key" in segment
|
|
234
|
+
? String(segment.key)
|
|
235
|
+
: String(segment))
|
|
236
|
+
.join(".");
|
|
237
|
+
}
|
|
238
|
+
function formatIssues(issues) {
|
|
239
|
+
return issues
|
|
240
|
+
.map((issue) => {
|
|
241
|
+
const path = formatPath(issue.path);
|
|
242
|
+
return path ? `${path}: ${issue.message}` : issue.message;
|
|
243
|
+
})
|
|
244
|
+
.join("; ");
|
|
245
|
+
}
|
|
246
|
+
function normalizeHeaderName(name) {
|
|
247
|
+
return name.toLowerCase();
|
|
248
|
+
}
|
|
249
|
+
function normalizeSignature(signature, prefix) {
|
|
250
|
+
const trimmed = signature.trim();
|
|
251
|
+
if (!prefix)
|
|
252
|
+
return trimmed;
|
|
253
|
+
return trimmed.startsWith(prefix) ? trimmed.slice(prefix.length) : trimmed;
|
|
254
|
+
}
|
|
255
|
+
function rawBodyBytes(rawBody) {
|
|
256
|
+
if (typeof rawBody === "string")
|
|
257
|
+
return new TextEncoder().encode(rawBody);
|
|
258
|
+
if (rawBody instanceof Uint8Array)
|
|
259
|
+
return rawBody;
|
|
260
|
+
return new Uint8Array(rawBody);
|
|
261
|
+
}
|
|
262
|
+
function rawBodyText(rawBody) {
|
|
263
|
+
if (typeof rawBody === "string")
|
|
264
|
+
return rawBody;
|
|
265
|
+
return new TextDecoder().decode(rawBodyBytes(rawBody));
|
|
266
|
+
}
|
|
267
|
+
function parseJsonBody(rawBody) {
|
|
268
|
+
try {
|
|
269
|
+
return JSON.parse(rawBodyText(rawBody));
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
throw new WebhookVerificationError({
|
|
273
|
+
message: "Webhook payload must be valid JSON.",
|
|
274
|
+
code: "invalid_json",
|
|
275
|
+
cause: error,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function readStringPath(input, path) {
|
|
280
|
+
let value = input;
|
|
281
|
+
for (const segment of path.split(".")) {
|
|
282
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
value = value[segment];
|
|
286
|
+
}
|
|
287
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
288
|
+
}
|
|
289
|
+
async function hmacHex(algorithm, secret, rawBody) {
|
|
290
|
+
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: algorithm ?? "SHA-256" }, false, ["sign"]);
|
|
291
|
+
const bytes = rawBodyBytes(rawBody);
|
|
292
|
+
const data = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
293
|
+
const signature = await crypto.subtle.sign("HMAC", key, data);
|
|
294
|
+
return bytesToHex(new Uint8Array(signature));
|
|
295
|
+
}
|
|
296
|
+
function bytesToHex(bytes) {
|
|
297
|
+
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
298
|
+
}
|
|
299
|
+
async function timingSafeStringEqual(a, b) {
|
|
300
|
+
const encoder = new TextEncoder();
|
|
301
|
+
const [digestA, digestB] = await Promise.all([
|
|
302
|
+
crypto.subtle.digest("SHA-256", encoder.encode(a)),
|
|
303
|
+
crypto.subtle.digest("SHA-256", encoder.encode(b)),
|
|
304
|
+
]);
|
|
305
|
+
const bytesA = new Uint8Array(digestA);
|
|
306
|
+
const bytesB = new Uint8Array(digestB);
|
|
307
|
+
let mismatch = 0;
|
|
308
|
+
for (let index = 0; index < bytesA.length; index++) {
|
|
309
|
+
mismatch |= (bytesA[index] ?? 0) ^ (bytesB[index] ?? 0);
|
|
310
|
+
}
|
|
311
|
+
return mismatch === 0;
|
|
312
|
+
}
|
|
313
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/webhooks/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beignet/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Core framework primitives for Beignet",
|
|
6
6
|
"exports": {
|
|
@@ -130,6 +130,10 @@
|
|
|
130
130
|
"types": "./dist/uploads/client.d.ts",
|
|
131
131
|
"default": "./dist/uploads/client.js"
|
|
132
132
|
},
|
|
133
|
+
"./webhooks": {
|
|
134
|
+
"types": "./dist/webhooks/index.d.ts",
|
|
135
|
+
"default": "./dist/webhooks/index.js"
|
|
136
|
+
},
|
|
133
137
|
"./server": {
|
|
134
138
|
"types": "./dist/server/index.d.ts",
|
|
135
139
|
"default": "./dist/server/index.js"
|
package/src/ports/index.ts
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
* - `@beignet/core/outbox` owns durable outbox helpers and test adapters
|
|
38
38
|
* - `@beignet/core/payments` owns `PaymentsPort` and payment test adapters
|
|
39
39
|
* - `@beignet/core/schedules` owns schedule definitions and runners
|
|
40
|
+
* - `@beignet/core/webhooks` owns inbound webhook definitions and verifiers
|
|
40
41
|
*/
|
|
41
42
|
|
|
42
43
|
import { createUnboundPort } from "./unbound.js";
|
|
@@ -358,6 +359,42 @@ export {
|
|
|
358
359
|
SearchOptionsError,
|
|
359
360
|
searchDocuments,
|
|
360
361
|
} from "../search/index.js";
|
|
362
|
+
/**
|
|
363
|
+
* Webhook exports.
|
|
364
|
+
*
|
|
365
|
+
* Webhooks have a dedicated subpath. Re-export the common app-facing
|
|
366
|
+
* definitions and helpers here so production concerns can be imported from one
|
|
367
|
+
* shared port toolbox when useful.
|
|
368
|
+
*/
|
|
369
|
+
export type {
|
|
370
|
+
CreateHmacWebhookVerifierOptions,
|
|
371
|
+
CreateMemoryWebhookVerifierOptions,
|
|
372
|
+
DefineWebhookOptions,
|
|
373
|
+
InferSchemaOutput,
|
|
374
|
+
InferWebhookEvent,
|
|
375
|
+
MemoryWebhookVerifier,
|
|
376
|
+
VerifyWebhookInput,
|
|
377
|
+
VerifyWebhookOptions,
|
|
378
|
+
WebhookDef,
|
|
379
|
+
WebhookEvent,
|
|
380
|
+
WebhookEventSchemas,
|
|
381
|
+
WebhookHeaders,
|
|
382
|
+
WebhookRawBody,
|
|
383
|
+
WebhookVerifier,
|
|
384
|
+
} from "../webhooks/index.js";
|
|
385
|
+
/**
|
|
386
|
+
* Webhook helper exports.
|
|
387
|
+
*/
|
|
388
|
+
export {
|
|
389
|
+
createHmacWebhookVerifier,
|
|
390
|
+
createMemoryWebhookVerifier,
|
|
391
|
+
defineWebhook,
|
|
392
|
+
parseWebhookEvent,
|
|
393
|
+
verifyWebhook,
|
|
394
|
+
WebhookOptionsError,
|
|
395
|
+
WebhookValidationError,
|
|
396
|
+
WebhookVerificationError,
|
|
397
|
+
} from "../webhooks/index.js";
|
|
361
398
|
/**
|
|
362
399
|
* Audit log port exports.
|
|
363
400
|
*/
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error reporting hook utilities for @beignet/core/server
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { HttpContractConfig } from "../../contracts/index.js";
|
|
6
|
+
import type {
|
|
7
|
+
ErrorReporterPort,
|
|
8
|
+
ErrorReportOptions,
|
|
9
|
+
} from "../../error-reporting/index.js";
|
|
10
|
+
import { isAppError, SchemaValidationError } from "../../errors/index.js";
|
|
11
|
+
import {
|
|
12
|
+
IdempotencyConflictError,
|
|
13
|
+
IdempotencyInProgressError,
|
|
14
|
+
} from "../../idempotency/index.js";
|
|
15
|
+
import {
|
|
16
|
+
AuthUnauthorizedError,
|
|
17
|
+
EntitlementRequiredError,
|
|
18
|
+
GateAuthorizationError,
|
|
19
|
+
TenantRequiredError,
|
|
20
|
+
} from "../../ports/index.js";
|
|
21
|
+
import type {
|
|
22
|
+
HttpRequestLike,
|
|
23
|
+
ServerCaughtErrorHook,
|
|
24
|
+
ServerHook,
|
|
25
|
+
} from "../types.js";
|
|
26
|
+
|
|
27
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Minimal context shape used by `createErrorReportingHooks(...)`.
|
|
31
|
+
*/
|
|
32
|
+
export type ErrorReportingContext = {
|
|
33
|
+
ports?: {
|
|
34
|
+
errorReporter?: ErrorReporterPort;
|
|
35
|
+
};
|
|
36
|
+
requestId?: string;
|
|
37
|
+
traceId?: string;
|
|
38
|
+
spanId?: string;
|
|
39
|
+
parentSpanId?: string;
|
|
40
|
+
traceparent?: string;
|
|
41
|
+
actor?: {
|
|
42
|
+
type?: string;
|
|
43
|
+
id?: string;
|
|
44
|
+
};
|
|
45
|
+
tenant?: {
|
|
46
|
+
id?: string;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Arguments passed to error reporting hook callbacks.
|
|
52
|
+
*/
|
|
53
|
+
export type ErrorReportingHookArgs<Ctx> = Parameters<
|
|
54
|
+
ServerCaughtErrorHook<Ctx>
|
|
55
|
+
>[0];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Error reporter resolver accepted by `createErrorReportingHooks(...)`.
|
|
59
|
+
*/
|
|
60
|
+
export type ErrorReporterResolver<Ctx> = (
|
|
61
|
+
args: ErrorReportingHookArgs<Ctx>,
|
|
62
|
+
) => MaybePromise<ErrorReporterPort | undefined>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Options accepted by `createErrorReportingHooks(...)`.
|
|
66
|
+
*/
|
|
67
|
+
export interface ErrorReportingHooksOptions<Ctx> {
|
|
68
|
+
/**
|
|
69
|
+
* Reporter to use instead of `ctx.ports.errorReporter`.
|
|
70
|
+
*/
|
|
71
|
+
reporter?: ErrorReporterPort | ErrorReporterResolver<Ctx>;
|
|
72
|
+
/**
|
|
73
|
+
* Decide whether a caught error should be captured. The default reports
|
|
74
|
+
* unexpected failures and skips expected catalog/auth/validation outcomes.
|
|
75
|
+
*/
|
|
76
|
+
shouldReport?: (args: ErrorReportingHookArgs<Ctx>) => MaybePromise<boolean>;
|
|
77
|
+
/**
|
|
78
|
+
* Additional capture options. These are merged over Beignet's request,
|
|
79
|
+
* actor, tenant, contract, and trace metadata.
|
|
80
|
+
*/
|
|
81
|
+
reportOptions?:
|
|
82
|
+
| ErrorReportOptions
|
|
83
|
+
| ((args: ErrorReportingHookArgs<Ctx>) => MaybePromise<ErrorReportOptions>);
|
|
84
|
+
/**
|
|
85
|
+
* Observer for reporter failures. Reporter failures are otherwise ignored so
|
|
86
|
+
* error reporting cannot change the HTTP response.
|
|
87
|
+
*/
|
|
88
|
+
onReporterError?: (args: {
|
|
89
|
+
error: unknown;
|
|
90
|
+
reportingError: unknown;
|
|
91
|
+
ctx?: Ctx;
|
|
92
|
+
req: HttpRequestLike;
|
|
93
|
+
contract: HttpContractConfig;
|
|
94
|
+
}) => MaybePromise<void>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create server hooks that capture unexpected HTTP request failures through
|
|
99
|
+
* `ctx.ports.errorReporter`.
|
|
100
|
+
*
|
|
101
|
+
* The hook observes `onCaughtError`, never maps responses, and ignores capture
|
|
102
|
+
* failures by default so reporting cannot affect request handling.
|
|
103
|
+
*/
|
|
104
|
+
export function createErrorReportingHooks<
|
|
105
|
+
Ctx extends ErrorReportingContext = ErrorReportingContext,
|
|
106
|
+
>(config: ErrorReportingHooksOptions<Ctx> = {}): ServerHook<Ctx> {
|
|
107
|
+
return {
|
|
108
|
+
name: "error-reporting",
|
|
109
|
+
async onCaughtError(args) {
|
|
110
|
+
const shouldReport = config.shouldReport
|
|
111
|
+
? await config.shouldReport(args)
|
|
112
|
+
: shouldReportServerError(args.err);
|
|
113
|
+
if (!shouldReport) return;
|
|
114
|
+
|
|
115
|
+
const reporter = await resolveReporter(config.reporter, args);
|
|
116
|
+
if (!reporter) return;
|
|
117
|
+
|
|
118
|
+
const baseOptions = createDefaultReportOptions(args);
|
|
119
|
+
const customOptions =
|
|
120
|
+
typeof config.reportOptions === "function"
|
|
121
|
+
? await config.reportOptions(args)
|
|
122
|
+
: config.reportOptions;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
await reporter.captureException(
|
|
126
|
+
args.err,
|
|
127
|
+
mergeReportOptions(baseOptions, customOptions),
|
|
128
|
+
);
|
|
129
|
+
} catch (reportingError) {
|
|
130
|
+
try {
|
|
131
|
+
await config.onReporterError?.({
|
|
132
|
+
error: args.err,
|
|
133
|
+
reportingError,
|
|
134
|
+
ctx: args.ctx,
|
|
135
|
+
req: args.req,
|
|
136
|
+
contract: args.contract,
|
|
137
|
+
});
|
|
138
|
+
} catch {
|
|
139
|
+
// Reporter failure observers must not affect request handling.
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Default filter for HTTP error reporting.
|
|
148
|
+
*/
|
|
149
|
+
export function shouldReportServerError(error: unknown): boolean {
|
|
150
|
+
if (isAppError(error)) return false;
|
|
151
|
+
if (error instanceof AuthUnauthorizedError) return false;
|
|
152
|
+
if (error instanceof TenantRequiredError) return false;
|
|
153
|
+
if (error instanceof IdempotencyConflictError) return false;
|
|
154
|
+
if (error instanceof IdempotencyInProgressError) return false;
|
|
155
|
+
if (error instanceof GateAuthorizationError) return false;
|
|
156
|
+
if (error instanceof EntitlementRequiredError) return false;
|
|
157
|
+
if (error instanceof SchemaValidationError) return false;
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function resolveReporter<Ctx extends ErrorReportingContext>(
|
|
162
|
+
reporter: ErrorReportingHooksOptions<Ctx>["reporter"],
|
|
163
|
+
args: ErrorReportingHookArgs<Ctx>,
|
|
164
|
+
): Promise<ErrorReporterPort | undefined> {
|
|
165
|
+
if (typeof reporter === "function") {
|
|
166
|
+
return reporter(args);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (reporter) {
|
|
170
|
+
return reporter;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return args.ctx?.ports?.errorReporter;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function createDefaultReportOptions<Ctx extends ErrorReportingContext>(
|
|
177
|
+
args: ErrorReportingHookArgs<Ctx>,
|
|
178
|
+
): ErrorReportOptions {
|
|
179
|
+
const ctx = args.ctx;
|
|
180
|
+
const url = safeUrl(args.req.url);
|
|
181
|
+
const actor = ctx?.actor;
|
|
182
|
+
const tenant = ctx?.tenant;
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
level: "error",
|
|
186
|
+
mechanism: "beignet.server",
|
|
187
|
+
handled: false,
|
|
188
|
+
requestId: ctx?.requestId,
|
|
189
|
+
traceId: ctx?.traceId,
|
|
190
|
+
spanId: ctx?.spanId,
|
|
191
|
+
parentSpanId: ctx?.parentSpanId,
|
|
192
|
+
traceparent: ctx?.traceparent,
|
|
193
|
+
user: actor?.type === "user" && actor.id ? { id: actor.id } : undefined,
|
|
194
|
+
tags: {
|
|
195
|
+
"beignet.kind": "http",
|
|
196
|
+
"http.method": args.req.method,
|
|
197
|
+
"http.route": args.contract.path,
|
|
198
|
+
"http.contract": args.contract.name,
|
|
199
|
+
},
|
|
200
|
+
contexts: {
|
|
201
|
+
request: {
|
|
202
|
+
method: args.req.method,
|
|
203
|
+
path: url?.pathname ?? args.contract.path,
|
|
204
|
+
route: args.contract.path,
|
|
205
|
+
contract: args.contract.name,
|
|
206
|
+
},
|
|
207
|
+
actor:
|
|
208
|
+
actor?.type || actor?.id
|
|
209
|
+
? {
|
|
210
|
+
type: actor.type,
|
|
211
|
+
id: actor.id,
|
|
212
|
+
}
|
|
213
|
+
: undefined,
|
|
214
|
+
tenant: tenant?.id ? { id: tenant.id } : undefined,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function mergeReportOptions(
|
|
220
|
+
base: ErrorReportOptions,
|
|
221
|
+
custom: ErrorReportOptions | undefined,
|
|
222
|
+
): ErrorReportOptions {
|
|
223
|
+
if (!custom) return base;
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
...base,
|
|
227
|
+
...custom,
|
|
228
|
+
tags: {
|
|
229
|
+
...base.tags,
|
|
230
|
+
...custom.tags,
|
|
231
|
+
},
|
|
232
|
+
contexts: {
|
|
233
|
+
...base.contexts,
|
|
234
|
+
...custom.contexts,
|
|
235
|
+
},
|
|
236
|
+
extra: {
|
|
237
|
+
...base.extra,
|
|
238
|
+
...custom.extra,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function safeUrl(value: string): URL | undefined {
|
|
244
|
+
try {
|
|
245
|
+
return new URL(value);
|
|
246
|
+
} catch {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -16,6 +16,14 @@ export {
|
|
|
16
16
|
type CorsConfig,
|
|
17
17
|
createCorsHooks,
|
|
18
18
|
} from "./cors.js";
|
|
19
|
+
export {
|
|
20
|
+
createErrorReportingHooks,
|
|
21
|
+
type ErrorReporterResolver,
|
|
22
|
+
type ErrorReportingContext,
|
|
23
|
+
type ErrorReportingHookArgs,
|
|
24
|
+
type ErrorReportingHooksOptions,
|
|
25
|
+
shouldReportServerError,
|
|
26
|
+
} from "./error-reporting.js";
|
|
19
27
|
export {
|
|
20
28
|
defaultMapErrorToResponse,
|
|
21
29
|
type ErrorMappingConfig,
|
package/src/server/server.ts
CHANGED
|
@@ -2396,6 +2396,11 @@ export function contractsFromRoutes<
|
|
|
2396
2396
|
* ```
|
|
2397
2397
|
*/
|
|
2398
2398
|
export function defineRouteGroup<Ctx>(): RouteGroupBuilder<Ctx>;
|
|
2399
|
+
/**
|
|
2400
|
+
* @deprecated Prefer `defineRouteGroup<Ctx>()({ ... })`. TypeScript cannot
|
|
2401
|
+
* infer the route tuple generic when callers provide only `Ctx`, so the direct
|
|
2402
|
+
* form preserves context checking but erases per-route contract output checks.
|
|
2403
|
+
*/
|
|
2399
2404
|
export function defineRouteGroup<
|
|
2400
2405
|
Ctx,
|
|
2401
2406
|
// biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
|
|
@@ -223,8 +223,8 @@ export type UseCaseRouteDef<
|
|
|
223
223
|
/**
|
|
224
224
|
* Structural check that a use case accepts the context this route provides.
|
|
225
225
|
*
|
|
226
|
-
* Enforced through `run` parameter contravariance so it applies even
|
|
227
|
-
* contract types are erased
|
|
226
|
+
* Enforced through `run` parameter contravariance so it applies even at loose
|
|
227
|
+
* collection boundaries where contract types are erased.
|
|
228
228
|
*/
|
|
229
229
|
export type UseCaseAcceptsCtx<Ctx> = {
|
|
230
230
|
run: (args: { ctx: Ctx; input: never }) => Promise<unknown>;
|