@beignet/core 0.0.43 → 0.0.45
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 +15 -0
- package/README.md +41 -4
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +9 -4
- package/dist/application/index.js.map +1 -1
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +45 -8
- package/dist/client/client.js.map +1 -1
- package/dist/events/index.js +2 -2
- package/dist/events/index.js.map +1 -1
- package/dist/idempotency/index.d.ts +26 -0
- package/dist/idempotency/index.d.ts.map +1 -1
- package/dist/idempotency/index.js +116 -47
- package/dist/idempotency/index.js.map +1 -1
- package/dist/jobs/index.js +2 -2
- package/dist/jobs/index.js.map +1 -1
- package/dist/notifications/index.js +1 -1
- package/dist/notifications/index.js.map +1 -1
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +9 -9
- package/dist/outbox/index.js.map +1 -1
- package/dist/ports/unit-of-work.d.ts.map +1 -1
- package/dist/ports/unit-of-work.js +2 -2
- package/dist/ports/unit-of-work.js.map +1 -1
- package/dist/server/hooks/idempotency.d.ts.map +1 -1
- package/dist/server/hooks/idempotency.js +37 -9
- package/dist/server/hooks/idempotency.js.map +1 -1
- package/dist/server/request-preparation.d.ts.map +1 -1
- package/dist/server/request-preparation.js +19 -11
- package/dist/server/request-preparation.js.map +1 -1
- package/dist/server/use-case-route.d.ts +2 -0
- package/dist/server/use-case-route.d.ts.map +1 -1
- package/dist/server/use-case-route.js +3 -1
- package/dist/server/use-case-route.js.map +1 -1
- package/dist/webhooks/index.d.ts.map +1 -1
- package/dist/webhooks/index.js +1 -1
- package/dist/webhooks/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/app-architecture/SKILL.md +44 -2
- package/src/application/index.ts +10 -12
- package/src/client/client.ts +48 -8
- package/src/events/index.ts +2 -2
- package/src/idempotency/index.ts +148 -57
- package/src/jobs/index.ts +2 -2
- package/src/notifications/index.ts +1 -1
- package/src/outbox/index.ts +13 -9
- package/src/ports/unit-of-work.ts +6 -2
- package/src/server/hooks/idempotency.ts +44 -8
- package/src/server/request-preparation.ts +21 -10
- package/src/server/use-case-route.ts +7 -1
- package/src/webhooks/index.ts +4 -1
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* Idempotency hooks for @beignet/core/server
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import {
|
|
6
|
+
type ErrorReporterPort,
|
|
7
|
+
tryReportException,
|
|
8
|
+
} from "../../error-reporting/index.js";
|
|
5
9
|
import { AppError, httpErrors } from "../../errors/index.js";
|
|
6
10
|
import {
|
|
7
11
|
createIdempotencyFingerprint,
|
|
@@ -35,6 +39,10 @@ export type IdempotencyPorts = {
|
|
|
35
39
|
idempotency: IdempotencyPort;
|
|
36
40
|
};
|
|
37
41
|
|
|
42
|
+
type OptionalErrorReporterPorts = IdempotencyPorts & {
|
|
43
|
+
errorReporter?: ErrorReporterPort;
|
|
44
|
+
};
|
|
45
|
+
|
|
38
46
|
/**
|
|
39
47
|
* Minimal context shape required for actor- and tenant-scoped idempotency.
|
|
40
48
|
*/
|
|
@@ -252,6 +260,7 @@ export function createIdempotencyHooks<Ctx extends CtxWithIdempotency>(
|
|
|
252
260
|
},
|
|
253
261
|
[responseFinalizerHook]: async ({
|
|
254
262
|
req,
|
|
263
|
+
ctx,
|
|
255
264
|
response,
|
|
256
265
|
error,
|
|
257
266
|
native,
|
|
@@ -292,14 +301,41 @@ export function createIdempotencyHooks<Ctx extends CtxWithIdempotency>(
|
|
|
292
301
|
|
|
293
302
|
// Framework-owned responses, errors, non-2xx responses, and native
|
|
294
303
|
// `Response` results release the reservation. Streams are not replayable.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
304
|
+
try {
|
|
305
|
+
await port.fail({
|
|
306
|
+
namespace,
|
|
307
|
+
key,
|
|
308
|
+
scope,
|
|
309
|
+
fingerprint,
|
|
310
|
+
reservationToken,
|
|
311
|
+
error,
|
|
312
|
+
});
|
|
313
|
+
} catch (settlementError) {
|
|
314
|
+
if (error === undefined) {
|
|
315
|
+
throw settlementError;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
await tryReportException({
|
|
319
|
+
reporter: ctx
|
|
320
|
+
? (ctx.ports as OptionalErrorReporterPorts).errorReporter
|
|
321
|
+
: undefined,
|
|
322
|
+
error: settlementError,
|
|
323
|
+
reportOptions: {
|
|
324
|
+
level: "error",
|
|
325
|
+
mechanism: "beignet.idempotency.settlement",
|
|
326
|
+
handled: true,
|
|
327
|
+
tags: {
|
|
328
|
+
"idempotency.namespace": namespace,
|
|
329
|
+
},
|
|
330
|
+
contexts: {
|
|
331
|
+
idempotency: {
|
|
332
|
+
namespace,
|
|
333
|
+
key,
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
}
|
|
303
339
|
return undefined;
|
|
304
340
|
},
|
|
305
341
|
};
|
|
@@ -52,6 +52,16 @@ class RequestBodyTooLargeError extends Error {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
class MalformedJsonBodyError extends Error {
|
|
56
|
+
readonly parseError: unknown;
|
|
57
|
+
|
|
58
|
+
constructor(parseError: unknown) {
|
|
59
|
+
super("Request body contains malformed JSON.");
|
|
60
|
+
this.name = "MalformedJsonBodyError";
|
|
61
|
+
this.parseError = parseError;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
function contractDiagnostics(contract: HttpContractConfig) {
|
|
56
66
|
return {
|
|
57
67
|
contract: contract.name,
|
|
@@ -243,16 +253,15 @@ async function parseBody(
|
|
|
243
253
|
if (contentType.includes("application/json")) {
|
|
244
254
|
const text = await readLimitedRequestText(bodyReq, maxBytes);
|
|
245
255
|
if (text === "") return undefined;
|
|
246
|
-
|
|
256
|
+
try {
|
|
257
|
+
return JSON.parse(text);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
throw new MalformedJsonBodyError(error);
|
|
260
|
+
}
|
|
247
261
|
}
|
|
248
262
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
return text === "" ? undefined : text;
|
|
252
|
-
} catch (error) {
|
|
253
|
-
if (error instanceof RequestBodyTooLargeError) throw error;
|
|
254
|
-
return undefined;
|
|
255
|
-
}
|
|
263
|
+
const text = await readLimitedRequestText(bodyReq, maxBytes);
|
|
264
|
+
return text === "" ? undefined : text;
|
|
256
265
|
}
|
|
257
266
|
|
|
258
267
|
export async function prepareRequestInputs(args: {
|
|
@@ -364,9 +373,11 @@ export async function prepareRequestInputs(args: {
|
|
|
364
373
|
contract,
|
|
365
374
|
400,
|
|
366
375
|
"INVALID_BODY",
|
|
367
|
-
|
|
376
|
+
error instanceof MalformedJsonBodyError
|
|
377
|
+
? "Malformed JSON"
|
|
378
|
+
: "Could not read request body",
|
|
368
379
|
"body",
|
|
369
|
-
error,
|
|
380
|
+
error instanceof MalformedJsonBodyError ? error.parseError : error,
|
|
370
381
|
),
|
|
371
382
|
};
|
|
372
383
|
}
|
|
@@ -293,12 +293,17 @@ const USE_CASE_TRUSTED_RUN_KEY: unique symbol = Symbol.for(
|
|
|
293
293
|
"beignet.useCase.trustedRun",
|
|
294
294
|
);
|
|
295
295
|
|
|
296
|
+
const USE_CASE_OUTPUT_VALIDATED_KEY: unique symbol = Symbol.for(
|
|
297
|
+
"beignet.useCase.outputValidated",
|
|
298
|
+
);
|
|
299
|
+
|
|
296
300
|
type RuntimeUseCase = AnyUseCaseLike & {
|
|
297
301
|
run: (args: { ctx: unknown; input: unknown }) => Promise<unknown>;
|
|
298
302
|
[USE_CASE_TRUSTED_RUN_KEY]?: (args: {
|
|
299
303
|
ctx: unknown;
|
|
300
304
|
input: unknown;
|
|
301
305
|
}) => Promise<unknown>;
|
|
306
|
+
[USE_CASE_OUTPUT_VALIDATED_KEY]?: boolean;
|
|
302
307
|
};
|
|
303
308
|
|
|
304
309
|
/**
|
|
@@ -368,7 +373,8 @@ function computeResponseExemption(
|
|
|
368
373
|
def: RuntimeUseCaseRouteDef,
|
|
369
374
|
status: number,
|
|
370
375
|
): number | undefined {
|
|
371
|
-
return
|
|
376
|
+
return def.useCase[USE_CASE_OUTPUT_VALIDATED_KEY] === true &&
|
|
377
|
+
contract.responses[status] === def.useCase.outputSchema
|
|
372
378
|
? status
|
|
373
379
|
: undefined;
|
|
374
380
|
}
|
package/src/webhooks/index.ts
CHANGED
|
@@ -468,7 +468,10 @@ export function createHmacWebhookVerifier(
|
|
|
468
468
|
timestamp,
|
|
469
469
|
});
|
|
470
470
|
const expected = await hmacHex(algorithm, options.secret, signedBody);
|
|
471
|
-
const actual = normalizeSignature(
|
|
471
|
+
const actual = normalizeSignature(
|
|
472
|
+
signature,
|
|
473
|
+
options.signaturePrefix,
|
|
474
|
+
).toLowerCase();
|
|
472
475
|
if (!(await timingSafeStringEqual(actual, expected))) {
|
|
473
476
|
throw new WebhookVerificationError({
|
|
474
477
|
message: "Webhook signature is invalid.",
|