@beignet/core 0.0.51 → 0.0.53

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 (85) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +112 -20
  3. package/dist/application/index.d.ts +1 -1
  4. package/dist/application/index.d.ts.map +1 -1
  5. package/dist/application/index.js +5 -2
  6. package/dist/application/index.js.map +1 -1
  7. package/dist/encryption/index.d.ts +40 -0
  8. package/dist/encryption/index.d.ts.map +1 -0
  9. package/dist/encryption/index.js +134 -0
  10. package/dist/encryption/index.js.map +1 -0
  11. package/dist/events/index.d.ts +26 -2
  12. package/dist/events/index.d.ts.map +1 -1
  13. package/dist/events/index.js +84 -10
  14. package/dist/events/index.js.map +1 -1
  15. package/dist/events/payload-state.d.ts +14 -2
  16. package/dist/events/payload-state.d.ts.map +1 -1
  17. package/dist/events/payload-state.js +116 -4
  18. package/dist/events/payload-state.js.map +1 -1
  19. package/dist/events/transport.d.ts +25 -0
  20. package/dist/events/transport.d.ts.map +1 -0
  21. package/dist/events/transport.js +192 -0
  22. package/dist/events/transport.js.map +1 -0
  23. package/dist/openapi/index.d.ts.map +1 -1
  24. package/dist/openapi/index.js +27 -4
  25. package/dist/openapi/index.js.map +1 -1
  26. package/dist/outbox/index.d.ts.map +1 -1
  27. package/dist/outbox/index.js +10 -13
  28. package/dist/outbox/index.js.map +1 -1
  29. package/dist/ports/events.d.ts +4 -1
  30. package/dist/ports/events.d.ts.map +1 -1
  31. package/dist/ports/storage.d.ts +7 -0
  32. package/dist/ports/storage.d.ts.map +1 -1
  33. package/dist/ports/storage.js +4 -0
  34. package/dist/ports/storage.js.map +1 -1
  35. package/dist/ports/testing.d.ts +3 -2
  36. package/dist/ports/testing.d.ts.map +1 -1
  37. package/dist/ports/testing.js +7 -4
  38. package/dist/ports/testing.js.map +1 -1
  39. package/dist/ports/unit-of-work.d.ts +4 -4
  40. package/dist/ports/unit-of-work.d.ts.map +1 -1
  41. package/dist/ports/unit-of-work.js +8 -9
  42. package/dist/ports/unit-of-work.js.map +1 -1
  43. package/dist/search/index.js +2 -2
  44. package/dist/search/index.js.map +1 -1
  45. package/dist/server/instrumentation.d.ts +5 -5
  46. package/dist/server/instrumentation.d.ts.map +1 -1
  47. package/dist/server/instrumentation.js +3 -4
  48. package/dist/server/instrumentation.js.map +1 -1
  49. package/dist/server/request-executor.d.ts.map +1 -1
  50. package/dist/server/request-executor.js +13 -0
  51. package/dist/server/request-executor.js.map +1 -1
  52. package/dist/server/response-finalization.d.ts.map +1 -1
  53. package/dist/server/response-finalization.js +25 -13
  54. package/dist/server/response-finalization.js.map +1 -1
  55. package/dist/server/server.d.ts +8 -5
  56. package/dist/server/server.d.ts.map +1 -1
  57. package/dist/server/server.js +1 -3
  58. package/dist/server/server.js.map +1 -1
  59. package/dist/server/use-case-route.d.ts +82 -17
  60. package/dist/server/use-case-route.d.ts.map +1 -1
  61. package/dist/server/use-case-route.js +40 -4
  62. package/dist/server/use-case-route.js.map +1 -1
  63. package/dist/uploads/index.d.ts.map +1 -1
  64. package/dist/uploads/index.js +37 -16
  65. package/dist/uploads/index.js.map +1 -1
  66. package/package.json +6 -2
  67. package/skills/app-architecture/SKILL.md +29 -6
  68. package/src/application/index.ts +24 -3
  69. package/src/encryption/index.ts +198 -0
  70. package/src/events/index.ts +137 -10
  71. package/src/events/payload-state.ts +205 -6
  72. package/src/events/transport.ts +242 -0
  73. package/src/openapi/index.ts +41 -3
  74. package/src/outbox/index.ts +26 -18
  75. package/src/ports/events.ts +4 -1
  76. package/src/ports/storage.ts +10 -0
  77. package/src/ports/testing.ts +11 -4
  78. package/src/ports/unit-of-work.ts +20 -16
  79. package/src/search/index.ts +2 -2
  80. package/src/server/instrumentation.ts +10 -8
  81. package/src/server/request-executor.ts +19 -0
  82. package/src/server/response-finalization.ts +33 -15
  83. package/src/server/server.ts +9 -8
  84. package/src/server/use-case-route.ts +234 -27
  85. package/src/uploads/index.ts +34 -16
@@ -312,18 +312,18 @@ function getDeclaredCatalogErrorsForStatus(
312
312
  );
313
313
  }
314
314
 
315
- async function validateCatalogErrorResponse<C extends HttpContractConfig>(
315
+ async function parseCatalogErrorResponse<C extends HttpContractConfig>(
316
316
  contract: C,
317
317
  res: HttpResponseLike,
318
- ): Promise<void> {
318
+ ): Promise<HttpResponseLike> {
319
319
  const body = res.body;
320
- if (res.status < 400 || !isErrorResponseBody(body)) return;
320
+ if (res.status < 400 || !isErrorResponseBody(body)) return res;
321
321
 
322
322
  const declaredErrors = getDeclaredCatalogErrorsForStatus(
323
323
  contract,
324
324
  res.status,
325
325
  );
326
- if (declaredErrors.length === 0) return;
326
+ if (declaredErrors.length === 0) return res;
327
327
 
328
328
  const matchingError = declaredErrors.find(
329
329
  (error) => error.code === body.code,
@@ -346,7 +346,18 @@ async function validateCatalogErrorResponse<C extends HttpContractConfig>(
346
346
 
347
347
  if (matchingError.details && body.details !== undefined) {
348
348
  try {
349
- await parseStandardSchema(matchingError.details, body.details);
349
+ const parsedDetails = await parseStandardSchema(
350
+ matchingError.details,
351
+ body.details,
352
+ );
353
+ const { details: _details, ...bodyWithoutDetails } = body;
354
+ return {
355
+ ...res,
356
+ body:
357
+ parsedDetails === undefined
358
+ ? bodyWithoutDetails
359
+ : { ...bodyWithoutDetails, details: parsedDetails },
360
+ };
350
361
  } catch (error) {
351
362
  if (error instanceof SchemaValidationError) {
352
363
  throw new ResponseContractViolationError({
@@ -360,18 +371,20 @@ async function validateCatalogErrorResponse<C extends HttpContractConfig>(
360
371
  throw error;
361
372
  }
362
373
  }
374
+
375
+ return res;
363
376
  }
364
377
 
365
- async function validateResponseAgainstContract<C extends HttpContractConfig>(
378
+ async function parseResponseAgainstContract<C extends HttpContractConfig>(
366
379
  contract: C,
367
380
  res: HttpResponseLike,
368
381
  responseValidationExemptStatus?: number,
369
- ): Promise<void> {
382
+ ): Promise<HttpResponseLike> {
370
383
  const statusKey = String(res.status);
371
384
  const hasDeclaredStatus = Object.hasOwn(contract.responses, statusKey);
372
385
 
373
386
  if (!hasDeclaredStatus) {
374
- if (Object.keys(contract.responses).length === 0) return;
387
+ if (Object.keys(contract.responses).length === 0) return res;
375
388
 
376
389
  throw new ResponseContractViolationError({
377
390
  code: "UNDECLARED_RESPONSE_STATUS",
@@ -400,19 +413,22 @@ async function validateResponseAgainstContract<C extends HttpContractConfig>(
400
413
  }),
401
414
  });
402
415
  }
403
- return;
416
+ return res;
404
417
  }
405
418
 
406
- if (!responseSchema) return;
419
+ if (!responseSchema) return res;
407
420
 
408
421
  // Binder routes whose use case output schema is the same object as the
409
422
  // declared success response schema skip the redundant success-status parse.
410
423
  // Error statuses and undeclared statuses are validated unchanged.
411
- if (res.status === responseValidationExemptStatus) return;
424
+ if (res.status === responseValidationExemptStatus) return res;
412
425
 
413
426
  try {
414
- await parseStandardSchema(responseSchema, res.body);
415
- await validateCatalogErrorResponse(contract, res);
427
+ const parsed = {
428
+ ...res,
429
+ body: await parseStandardSchema(responseSchema, res.body),
430
+ };
431
+ return await parseCatalogErrorResponse(contract, parsed);
416
432
  } catch (error) {
417
433
  if (error instanceof SchemaValidationError) {
418
434
  throw new ResponseContractViolationError({
@@ -463,7 +479,7 @@ export async function finalizeResponse<C extends HttpContractConfig>(
463
479
  const normalized = normalizeResponse(res);
464
480
  validateHttpResponseSemantics(contract, normalized);
465
481
  if (options.validateContract ?? true) {
466
- await validateResponseAgainstContract(
482
+ return parseResponseAgainstContract(
467
483
  contract,
468
484
  normalized,
469
485
  responseValidationExemptStatus,
@@ -490,6 +506,8 @@ export function defaultErrorResponse(
490
506
  ctx?: unknown,
491
507
  ): HttpResponseLike {
492
508
  const requestId = getRequestIdFromContext(ctx);
509
+ const exposeErrorDetails =
510
+ process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test";
493
511
  return {
494
512
  status: 500,
495
513
  body: createErrorResponseBody({
@@ -497,7 +515,7 @@ export function defaultErrorResponse(
497
515
  message: "Internal server error",
498
516
  requestId,
499
517
  details:
500
- process.env.NODE_ENV !== "production" && err instanceof Error
518
+ exposeErrorDetails && err instanceof Error
501
519
  ? {
502
520
  error: {
503
521
  message: err.message,
@@ -204,15 +204,18 @@ export type CreateServerOptions<
204
204
  * `ports.devtools`) when one is installed.
205
205
  *
206
206
  * Pass `false` to disable headers and event recording. Context factories
207
- * still receive `requestId` and `trace` arguments.
207
+ * still receive `requestId` and `trace` arguments, and request-scoped
208
+ * correlation remains available to ambient audit wrappers.
208
209
  */
209
210
  instrumentation?: ServerInstrumentationOptions<Ctx> | false;
210
211
  /**
211
- * Whether route-owned responses are validated against the contract's
212
- * declared statuses and response schemas before they are sent.
212
+ * Whether route-owned responses are parsed against the contract's declared
213
+ * statuses and response schemas before they are sent. The parsed schema
214
+ * output becomes the response body.
213
215
  *
214
- * Disable this to trade response guarantees for throughput, mirroring the
215
- * client-side `validateResponses` option.
216
+ * Disable this to send route-owned handler bodies as-is without validation,
217
+ * unknown-key stripping, or transforms, mirroring the client-side
218
+ * `validateResponses` option.
216
219
  *
217
220
  * @default true
218
221
  */
@@ -407,9 +410,7 @@ export async function createServer<
407
410
  options.instrumentation,
408
411
  );
409
412
  const hooks = [
410
- ...(instrumentation.hook
411
- ? [instrumentation.hook as ServerHook<Ctx, FinalPorts>]
412
- : []),
413
+ instrumentation.hook as ServerHook<Ctx, FinalPorts>,
413
414
  ...((options.hooks ?? []) as ServerHook<Ctx, FinalPorts>[]),
414
415
  ];
415
416
  const contracts = options.routes ? contractsFromRoutes(options.routes) : [];
@@ -149,20 +149,143 @@ export type UseCaseRouteInputParts<C extends HttpContractConfig> = {
149
149
  body: InferBody<C>;
150
150
  };
151
151
 
152
+ type SegmentPathParam<Segment extends string> = Segment extends `:${infer Name}`
153
+ ? Name
154
+ : Segment extends `[${infer Name}]`
155
+ ? Name
156
+ : never;
157
+
158
+ type PathParamNames<Path extends string> = string extends Path
159
+ ? never
160
+ : Path extends `${infer Segment}/${infer Rest}`
161
+ ? SegmentPathParam<Segment> | PathParamNames<Rest>
162
+ : SegmentPathParam<Path>;
163
+
164
+ type EmptyBinderInput = Record<never, never>;
165
+
166
+ declare const UNMERGEABLE_BINDER_INPUT: unique symbol;
167
+
168
+ type UnmergeableBinderInput = {
169
+ readonly [UNMERGEABLE_BINDER_INPUT]: true;
170
+ };
171
+
172
+ type IsAny<T> = 0 extends 1 & T ? true : false;
173
+
174
+ type BinderObject<T> =
175
+ IsAny<T> extends true
176
+ ? UnmergeableBinderInput
177
+ : [T] extends [object]
178
+ ? [Extract<T, readonly unknown[]>] extends [never]
179
+ ? T
180
+ : UnmergeableBinderInput
181
+ : UnmergeableBinderInput;
182
+
183
+ type MergeBinderObjects<LowerPrecedence, HigherPrecedence> = [
184
+ BinderObject<LowerPrecedence>,
185
+ ] extends [UnmergeableBinderInput]
186
+ ? UnmergeableBinderInput
187
+ : [BinderObject<HigherPrecedence>] extends [UnmergeableBinderInput]
188
+ ? UnmergeableBinderInput
189
+ : Omit<
190
+ BinderObject<LowerPrecedence>,
191
+ keyof BinderObject<HigherPrecedence>
192
+ > &
193
+ BinderObject<HigherPrecedence>;
194
+
195
+ type InferredPathInput<C extends HttpContractConfig> = string extends C["path"]
196
+ ? UnmergeableBinderInput
197
+ : [PathParamNames<C["path"]>] extends [never]
198
+ ? EmptyBinderInput
199
+ : { [K in PathParamNames<C["path"]>]: string };
200
+
201
+ type BinderPathInput<C extends HttpContractConfig> =
202
+ C["pathParams"] extends StandardSchemaV1
203
+ ? InferOutput<C["pathParams"]>
204
+ : InferredPathInput<C>;
205
+
206
+ type BinderQueryInput<C extends HttpContractConfig> =
207
+ C["query"] extends StandardSchemaV1
208
+ ? InferOutput<C["query"]>
209
+ : EmptyBinderInput;
210
+
211
+ type BinderBodyInput<C extends HttpContractConfig> =
212
+ C["body"] extends StandardSchemaV1
213
+ ? InferOutput<C["body"]>
214
+ : EmptyBinderInput;
215
+
216
+ type HasPathSchema<C extends HttpContractConfig> =
217
+ C["pathParams"] extends StandardSchemaV1 ? true : false;
218
+
219
+ type HasQuerySchema<C extends HttpContractConfig> =
220
+ C["query"] extends StandardSchemaV1 ? true : false;
221
+
222
+ type HasBodySchema<C extends HttpContractConfig> =
223
+ C["body"] extends StandardSchemaV1 ? true : false;
224
+
225
+ type HasInferredPathInput<C extends HttpContractConfig> =
226
+ string extends C["path"]
227
+ ? true
228
+ : [PathParamNames<C["path"]>] extends [never]
229
+ ? false
230
+ : true;
231
+
232
+ type MergedBinderInput<C extends HttpContractConfig> = MergeBinderObjects<
233
+ MergeBinderObjects<BinderQueryInput<C>, BinderBodyInput<C>>,
234
+ BinderPathInput<C>
235
+ >;
236
+
237
+ /**
238
+ * Input produced when a binder route omits an explicit `input` mapper.
239
+ *
240
+ * A sole declared request schema passes through unchanged when the literal
241
+ * path has no additional inferred parameters. Every other supported default
242
+ * binding merges object inputs with path over body over query precedence.
243
+ */
244
+ type DefaultBinderRouteInput<C extends HttpContractConfig> =
245
+ HasPathSchema<C> extends true
246
+ ? HasQuerySchema<C> extends true
247
+ ? MergedBinderInput<C>
248
+ : HasBodySchema<C> extends true
249
+ ? MergedBinderInput<C>
250
+ : BinderPathInput<C>
251
+ : HasQuerySchema<C> extends true
252
+ ? HasBodySchema<C> extends true
253
+ ? MergedBinderInput<C>
254
+ : HasInferredPathInput<C> extends true
255
+ ? MergedBinderInput<C>
256
+ : BinderQueryInput<C>
257
+ : HasBodySchema<C> extends true
258
+ ? HasInferredPathInput<C> extends true
259
+ ? MergedBinderInput<C>
260
+ : BinderBodyInput<C>
261
+ : BinderPathInput<C>;
262
+
263
+ type UseCaseRouteInputMode = "default" | "mapped";
264
+
152
265
  /**
153
266
  * Constraint that checks a use case against the route that binds it.
154
267
  *
155
268
  * Produces a readable branded mismatch object on the `useCase` property when
156
- * the use case requires a context the server does not provide, or when its
157
- * output does not match the contract's declared success response schema.
269
+ * the use case requires a context the server does not provide, when its
270
+ * output does not match the contract's declared success response schema, or
271
+ * when the default binder input does not satisfy the use case input.
158
272
  */
159
- export type UseCaseFitsRoute<Ctx, C extends HttpContractConfig, UC> = [
273
+ export type UseCaseFitsRoute<
160
274
  Ctx,
161
- ] extends [UseCaseRouteCtx<UC>]
275
+ C extends HttpContractConfig,
276
+ UC,
277
+ InputMode extends UseCaseRouteInputMode = "default",
278
+ > = [Ctx] extends [UseCaseRouteCtx<UC>]
162
279
  ? [UseCaseRouteOutput<UC>] extends [
163
280
  SuccessBodyFromKeys<C["responses"], Success2xxKeys<C["responses"]>>,
164
281
  ]
165
- ? unknown
282
+ ? InputMode extends "mapped"
283
+ ? unknown
284
+ : [DefaultBinderRouteInput<C>] extends [UseCaseRouteInput<UC>]
285
+ ? unknown
286
+ : {
287
+ "~beignetError": "default binder input does not match the use case input; add an input mapper";
288
+ }
166
289
  : {
167
290
  "~beignetError": "useCase output does not match the contract's success response schema";
168
291
  }
@@ -185,21 +308,34 @@ type UseCaseRouteShape<
185
308
  * Route-scoped hooks that run after group hooks and before the use case.
186
309
  */
187
310
  hooks?: Hooks;
188
- /**
189
- * Use case bound directly to the contract.
190
- */
191
- useCase: UC & UseCaseFitsRoute<HandlerCtx, C, UC>;
192
- /**
193
- * Map parsed request parts to the use case input.
194
- *
195
- * A sole declared path, query, or body schema is passed through unchanged
196
- * when no additional path, query, or object body values are present.
197
- * Otherwise `defaultBinderInput` merges query, body, and path objects (path
198
- * wins collisions) and never merges headers.
199
- */
200
- input?: (parts: UseCaseRouteInputParts<C>) => UseCaseRouteInput<UC>;
201
311
  handle?: never;
202
- } & BinderStatusOption<C>;
312
+ } & (
313
+ | {
314
+ /**
315
+ * Use case bound directly to the contract. The default binder input
316
+ * must satisfy the use case input type.
317
+ */
318
+ useCase: UC & UseCaseFitsRoute<HandlerCtx, C, UC>;
319
+ input?: never;
320
+ }
321
+ | {
322
+ /**
323
+ * Use case bound directly to the contract through an explicit input
324
+ * mapper.
325
+ */
326
+ useCase: UC & UseCaseFitsRoute<HandlerCtx, C, UC, "mapped">;
327
+ /**
328
+ * Map parsed request parts to the use case input.
329
+ *
330
+ * A sole declared path, query, or body schema is passed through
331
+ * unchanged when no additional path, query, or object body values are
332
+ * present. Otherwise `defaultBinderInput` merges query, body, and path
333
+ * objects (path wins collisions) and never merges headers.
334
+ */
335
+ input: (parts: UseCaseRouteInputParts<C>) => UseCaseRouteInput<UC>;
336
+ }
337
+ ) &
338
+ BinderStatusOption<C>;
203
339
 
204
340
  /**
205
341
  * Route registration that binds a contract directly to a use case.
@@ -271,11 +407,25 @@ export type ValidatedRouteInput<Ctx, E> = E extends {
271
407
  ? {
272
408
  contract: CL;
273
409
  hooks?: HooksOf<E>;
274
- useCase: UC &
275
- UseCaseFitsRoute<Ctx & AddedCtxFromHooks<HooksOf<E>>, C, UC>;
276
- input?: (parts: UseCaseRouteInputParts<C>) => UseCaseRouteInput<UC>;
277
410
  handle?: never;
278
- } & BinderStatusOption<C>
411
+ } & (
412
+ | {
413
+ useCase: UC &
414
+ UseCaseFitsRoute<Ctx & AddedCtxFromHooks<HooksOf<E>>, C, UC>;
415
+ input?: never;
416
+ }
417
+ | {
418
+ useCase: UC &
419
+ UseCaseFitsRoute<
420
+ Ctx & AddedCtxFromHooks<HooksOf<E>>,
421
+ C,
422
+ UC,
423
+ "mapped"
424
+ >;
425
+ input: (parts: UseCaseRouteInputParts<C>) => UseCaseRouteInput<UC>;
426
+ }
427
+ ) &
428
+ BinderStatusOption<C>
279
429
  : unknown
280
430
  : unknown;
281
431
 
@@ -322,6 +472,49 @@ export type RuntimeUseCaseRouteDef = {
322
472
  status?: number;
323
473
  };
324
474
 
475
+ type UseCaseInputValidationFailure = Error & {
476
+ name: "UseCaseValidationError";
477
+ phase: "input";
478
+ useCaseName: string;
479
+ };
480
+
481
+ function isUseCaseInputValidationFailure(
482
+ error: unknown,
483
+ useCaseName: string,
484
+ ): error is UseCaseInputValidationFailure {
485
+ if (!(error instanceof Error)) return false;
486
+ const candidate = error as Partial<UseCaseInputValidationFailure>;
487
+ return (
488
+ candidate.name === "UseCaseValidationError" &&
489
+ candidate.phase === "input" &&
490
+ candidate.useCaseName === useCaseName
491
+ );
492
+ }
493
+
494
+ /**
495
+ * Internal framework error raised when a type-erased binder route produces an
496
+ * input that the bound use case rejects.
497
+ */
498
+ export class UseCaseRouteInputValidationError extends Error {
499
+ readonly code = "USE_CASE_INPUT_VALIDATION_ERROR";
500
+ readonly contractName: string;
501
+ readonly useCaseName: string;
502
+
503
+ constructor(args: {
504
+ contractName: string;
505
+ useCaseName: string;
506
+ cause: UseCaseInputValidationFailure;
507
+ }) {
508
+ super(
509
+ `Default binder input for contract "${args.contractName}" does not satisfy use case "${args.useCaseName}". Add an explicit input mapper.`,
510
+ { cause: args.cause },
511
+ );
512
+ this.name = "UseCaseRouteInputValidationError";
513
+ this.contractName = args.contractName;
514
+ this.useCaseName = args.useCaseName;
515
+ }
516
+ }
517
+
325
518
  function isPlainObject(value: unknown): value is Record<string, unknown> {
326
519
  return typeof value === "object" && value !== null && !Array.isArray(value);
327
520
  }
@@ -458,10 +651,24 @@ export function createUseCaseRouteHandler<Ctx, C extends HttpContractConfig>(
458
651
  : defaultBinderInput(parts);
459
652
  const run = passSingle && trustedRun ? trustedRun : def.useCase.run;
460
653
 
461
- return {
462
- status,
463
- body: await run.call(def.useCase, { ctx, input }),
464
- };
654
+ try {
655
+ return {
656
+ status,
657
+ body: await run.call(def.useCase, { ctx, input }),
658
+ };
659
+ } catch (error) {
660
+ if (
661
+ !def.input &&
662
+ isUseCaseInputValidationFailure(error, def.useCase.name)
663
+ ) {
664
+ throw new UseCaseRouteInputValidationError({
665
+ contractName: contract.name,
666
+ useCaseName: def.useCase.name,
667
+ cause: error,
668
+ });
669
+ }
670
+ throw error;
671
+ }
465
672
  };
466
673
 
467
674
  return {
@@ -968,29 +968,47 @@ export function createUploadRouter<Ctx>(
968
968
  },
969
969
  });
970
970
  }
971
- const object = needsUploadBodyVerification(upload, file)
971
+ const verifyBody = needsUploadBodyVerification(upload, file);
972
+ const objectBody = verifyBody
972
973
  ? await options.storage.get(file.key)
974
+ : null;
975
+ const object = verifyBody
976
+ ? objectBody
973
977
  : await options.storage.stat(file.key);
974
978
  if (!object) {
975
979
  throw new UploadObjectNotFoundError({
976
980
  message: `Uploaded object "${file.key}" was not found.`,
977
981
  });
978
982
  }
979
- assertStoredObject(upload, file, object);
980
- const verified = await verifyStoredUploadFile(upload, file, object);
981
- const completedObject = storageObjectMetadata(object);
982
- const completedFile = {
983
- ...file,
984
- ...(verified.checksum ? { checksum: verified.checksum } : {}),
985
- object: completedObject,
986
- };
987
- await assertVerifiedFile(upload, {
988
- ctx,
989
- metadata,
990
- file: completedFile,
991
- storage: options.storage,
992
- });
993
- files.push(completedFile);
983
+ let operationFailed = false;
984
+ try {
985
+ assertStoredObject(upload, file, object);
986
+ const verified = await verifyStoredUploadFile(upload, file, object);
987
+ const completedObject = storageObjectMetadata(object);
988
+ const completedFile = {
989
+ ...file,
990
+ ...(verified.checksum ? { checksum: verified.checksum } : {}),
991
+ object: completedObject,
992
+ };
993
+ await assertVerifiedFile(upload, {
994
+ ctx,
995
+ metadata,
996
+ file: completedFile,
997
+ storage: options.storage,
998
+ });
999
+ files.push(completedFile);
1000
+ } catch (error) {
1001
+ operationFailed = true;
1002
+ throw error;
1003
+ } finally {
1004
+ if (objectBody && !objectBody.bodyUsed) {
1005
+ if (operationFailed) {
1006
+ await objectBody.cancel().catch(() => undefined);
1007
+ } else {
1008
+ await objectBody.cancel();
1009
+ }
1010
+ }
1011
+ }
994
1012
  }
995
1013
 
996
1014
  const result = await upload.onComplete?.({ ctx, metadata, files });