@beignet/core 0.0.44 → 0.0.46

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 (51) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +71 -4
  3. package/dist/application/index.d.ts.map +1 -1
  4. package/dist/application/index.js +9 -4
  5. package/dist/application/index.js.map +1 -1
  6. package/dist/client/client.d.ts.map +1 -1
  7. package/dist/client/client.js +45 -8
  8. package/dist/client/client.js.map +1 -1
  9. package/dist/events/index.js +2 -2
  10. package/dist/events/index.js.map +1 -1
  11. package/dist/idempotency/index.d.ts +26 -0
  12. package/dist/idempotency/index.d.ts.map +1 -1
  13. package/dist/idempotency/index.js +116 -47
  14. package/dist/idempotency/index.js.map +1 -1
  15. package/dist/jobs/index.js +2 -2
  16. package/dist/jobs/index.js.map +1 -1
  17. package/dist/notifications/index.js +1 -1
  18. package/dist/notifications/index.js.map +1 -1
  19. package/dist/outbox/index.d.ts.map +1 -1
  20. package/dist/outbox/index.js +9 -9
  21. package/dist/outbox/index.js.map +1 -1
  22. package/dist/ports/unit-of-work.d.ts.map +1 -1
  23. package/dist/ports/unit-of-work.js +2 -2
  24. package/dist/ports/unit-of-work.js.map +1 -1
  25. package/dist/server/hooks/idempotency.d.ts.map +1 -1
  26. package/dist/server/hooks/idempotency.js +37 -9
  27. package/dist/server/hooks/idempotency.js.map +1 -1
  28. package/dist/server/request-preparation.d.ts.map +1 -1
  29. package/dist/server/request-preparation.js +19 -11
  30. package/dist/server/request-preparation.js.map +1 -1
  31. package/dist/server/use-case-route.d.ts +2 -0
  32. package/dist/server/use-case-route.d.ts.map +1 -1
  33. package/dist/server/use-case-route.js +3 -1
  34. package/dist/server/use-case-route.js.map +1 -1
  35. package/dist/webhooks/index.d.ts.map +1 -1
  36. package/dist/webhooks/index.js +1 -1
  37. package/dist/webhooks/index.js.map +1 -1
  38. package/package.json +3 -1
  39. package/skills/app-architecture/SKILL.md +11 -0
  40. package/src/application/index.ts +10 -12
  41. package/src/client/client.ts +48 -8
  42. package/src/events/index.ts +2 -2
  43. package/src/idempotency/index.ts +148 -57
  44. package/src/jobs/index.ts +2 -2
  45. package/src/notifications/index.ts +1 -1
  46. package/src/outbox/index.ts +13 -9
  47. package/src/ports/unit-of-work.ts +6 -2
  48. package/src/server/hooks/idempotency.ts +44 -8
  49. package/src/server/request-preparation.ts +21 -10
  50. package/src/server/use-case-route.ts +7 -1
  51. package/src/webhooks/index.ts +4 -1
@@ -245,8 +245,12 @@ export function createDomainEventRecorder(): BufferedDomainEventRecorder {
245
245
  async flush(eventBus) {
246
246
  while (records.length > 0) {
247
247
  const record = records[0];
248
- const payload = await parseEventPayload(record.event, record.payload);
249
- await eventBus.publish(record.event, payload, record.options);
248
+ await parseEventPayload(record.event, record.payload);
249
+ await eventBus.publish(
250
+ record.event,
251
+ record.payload as never,
252
+ record.options,
253
+ );
250
254
  records.shift();
251
255
  }
252
256
  },
@@ -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
- await port.fail({
296
- namespace,
297
- key,
298
- scope,
299
- fingerprint,
300
- reservationToken,
301
- error,
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
- return JSON.parse(text);
256
+ try {
257
+ return JSON.parse(text);
258
+ } catch (error) {
259
+ throw new MalformedJsonBodyError(error);
260
+ }
247
261
  }
248
262
 
249
- try {
250
- const text = await readLimitedRequestText(bodyReq, maxBytes);
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
- "Malformed JSON",
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 contract.responses[status] === def.useCase.outputSchema
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
  }
@@ -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(signature, options.signaturePrefix);
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.",