@karmaniverous/smoz 0.2.12 → 0.2.13

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/dist/cjs/index.js CHANGED
@@ -375,6 +375,98 @@ const wrapSerializer = (fn, opts) => {
375
375
  });
376
376
  };
377
377
 
378
+ /**
379
+ * @module Response shaping helpers for HTTP middleware.
380
+ *
381
+ * Shared utilities for detecting and normalising Lambda Proxy response
382
+ * envelopes, used by both the `after` and `onError` middleware paths.
383
+ * Also exports dedicated `onError` steps that apply CORS headers and
384
+ * response shaping after `errorHandler` — necessary because middy v6+
385
+ * does not re-run the `after` chain on the error path.
386
+ */
387
+ /**
388
+ * Detect a shaped Lambda Proxy response.
389
+ *
390
+ * A response is "shaped" when it has a numeric `statusCode` property —
391
+ * the definitive marker of an API-Gateway-style response envelope.
392
+ * Missing `headers` or `body` are defaulted by {@link shapeResponse}
393
+ * rather than treated as "unshaped", because `{ statusCode, headers }`
394
+ * (no body) is a valid Lambda Proxy return value.
395
+ */
396
+ const isShapedResponse = (v) => typeof v === 'object' &&
397
+ v !== null &&
398
+ 'statusCode' in v &&
399
+ typeof v.statusCode === 'number';
400
+ /**
401
+ * Shape the response into a well-formed Lambda Proxy result.
402
+ *
403
+ * Shaped responses (detected via {@link isShapedResponse}) are normalised
404
+ * in-place — missing `headers` and `body` are defaulted. Non-shaped
405
+ * values are wrapped in a `200` envelope.
406
+ */
407
+ const shapeResponse = (current, contentType) => {
408
+ let res;
409
+ if (isShapedResponse(current)) {
410
+ res = current;
411
+ }
412
+ else {
413
+ res = { statusCode: 200, headers: {}, body: current };
414
+ }
415
+ // Default missing fields for shaped responses without body/headers.
416
+ if (res.body === undefined)
417
+ res.body = '';
418
+ if (res.body !== undefined && typeof res.body !== 'string') {
419
+ try {
420
+ res.body = JSON.stringify(res.body);
421
+ }
422
+ catch {
423
+ res.body = String(res.body);
424
+ }
425
+ }
426
+ const headers = res.headers ?? {};
427
+ headers['Content-Type'] = contentType;
428
+ res.headers = headers;
429
+ return res;
430
+ };
431
+ /**
432
+ * Apply CORS headers to error responses.
433
+ *
434
+ * middy v6+ does not re-run the `after` chain after `onError` handles an
435
+ * error, so the regular CORS `after` hook never fires for error responses.
436
+ * This dedicated `onError` step runs *after* the error handler has set
437
+ * `request.response`, ensuring CORS headers are present on error responses.
438
+ */
439
+ const makeOnErrorCors = (opts) => {
440
+ const cors = httpCors({
441
+ credentials: true,
442
+ getOrigin: (o) => o,
443
+ ...(opts?.cors ?? {}),
444
+ });
445
+ return tagStep({
446
+ onError: async (request) => {
447
+ if (request.response === undefined)
448
+ return;
449
+ if (cors.after)
450
+ await cors.after(request);
451
+ },
452
+ }, 'error-cors');
453
+ };
454
+ /**
455
+ * Shape error responses into well-formed Lambda Proxy results.
456
+ *
457
+ * Mirrors `makeShapeAndContentType` but runs on the `onError` path,
458
+ * ensuring error responses have proper `statusCode`, `headers`, and
459
+ * `body` structure even when the `after` chain is skipped.
460
+ */
461
+ const makeOnErrorShape = (contentType) => tagStep({
462
+ onError: (request) => {
463
+ const container = request;
464
+ if (container.response === undefined)
465
+ return;
466
+ container.response = shapeResponse(container.response, contentType);
467
+ },
468
+ }, 'error-shape');
469
+
378
470
  const makeHead = () => tagStep(shortCircuitHead, 'head');
379
471
  const makeHeaderNormalizer = (opts) => tagStep(asApiMiddleware(httpHeaderNormalizer(opts?.headerNormalizer ?? { canonical: true })), 'header-normalizer');
380
472
  const makeEventNormalizer = () => tagStep(asApiMiddleware(httpEventNormalizer()), 'event-normalizer');
@@ -529,31 +621,9 @@ const makeCors = (opts) => tagStep(asApiMiddleware(httpCors({
529
621
  const makeShapeAndContentType = (contentType) => tagStep({
530
622
  after: (request) => {
531
623
  const container = request;
532
- const current = container.response;
533
- if (current === undefined)
624
+ if (container.response === undefined)
534
625
  return;
535
- const looksShaped = typeof current === 'object' &&
536
- current !== null &&
537
- 'statusCode' in current &&
538
- 'headers' in current &&
539
- 'body' in current;
540
- let res;
541
- if (looksShaped)
542
- res = current;
543
- else
544
- res = { statusCode: 200, headers: {}, body: current };
545
- if (res.body !== undefined && typeof res.body !== 'string') {
546
- try {
547
- res.body = JSON.stringify(res.body);
548
- }
549
- catch {
550
- res.body = String(res.body);
551
- }
552
- }
553
- const headers = res.headers ?? {};
554
- headers['Content-Type'] = contentType;
555
- res.headers = headers;
556
- request.response = res;
626
+ container.response = shapeResponse(container.response, contentType);
557
627
  },
558
628
  }, 'shape');
559
629
  const makeSerializer = (contentType, opts) => tagStep(asApiMiddleware(httpResponseSerializer({
@@ -589,7 +659,15 @@ const buildDefaultPhases = (args) => {
589
659
  makeShapeAndContentType(contentType),
590
660
  makeSerializer(contentType, opts),
591
661
  ];
592
- const onError = [makeErrorExpose(), makeErrorHandler(opts)];
662
+ const onError = [
663
+ makeErrorExpose(),
664
+ makeErrorHandler(opts),
665
+ // Post-error-handler processing: middy v6+ does not re-run the after
666
+ // chain after onError, so CORS and response shaping must happen here.
667
+ // Shape first so CORS can mutate a well-formed headers object.
668
+ makeOnErrorShape(contentType),
669
+ makeOnErrorCors(opts),
670
+ ];
593
671
  return { before, after, onError };
594
672
  };
595
673
  const buildSafeDefaults = (args) => buildDefaultPhases(args);
package/dist/index.d.ts CHANGED
@@ -264,7 +264,7 @@ type ConsoleLogger = Pick<Console, 'debug' | 'error' | 'info' | 'log'>;
264
264
  */
265
265
 
266
266
  type ApiMiddleware$1 = MiddlewareObj<APIGatewayProxyEvent, Context>;
267
- type StepId = 'head' | 'header-normalizer' | 'event-normalizer' | 'content-negotiation' | 'json-body-parser' | 'zod-before' | 'head-finalize' | 'zod-after' | 'error-expose' | 'error-handler' | 'cors' | 'preferred-media' | 'shape' | 'serializer';
267
+ type StepId = 'head' | 'header-normalizer' | 'event-normalizer' | 'content-negotiation' | 'json-body-parser' | 'zod-before' | 'head-finalize' | 'zod-after' | 'error-expose' | 'error-handler' | 'error-cors' | 'error-shape' | 'cors' | 'preferred-media' | 'shape' | 'serializer';
268
268
  /** Attach a non-enumerable __id to a middleware step. */
269
269
  declare const tagStep: (mw: ApiMiddleware$1, id: StepId) => ApiMiddleware$1;
270
270
  /** Retrieve a step's id, if present. */
package/dist/mjs/index.js CHANGED
@@ -373,6 +373,98 @@ const wrapSerializer = (fn, opts) => {
373
373
  });
374
374
  };
375
375
 
376
+ /**
377
+ * @module Response shaping helpers for HTTP middleware.
378
+ *
379
+ * Shared utilities for detecting and normalising Lambda Proxy response
380
+ * envelopes, used by both the `after` and `onError` middleware paths.
381
+ * Also exports dedicated `onError` steps that apply CORS headers and
382
+ * response shaping after `errorHandler` — necessary because middy v6+
383
+ * does not re-run the `after` chain on the error path.
384
+ */
385
+ /**
386
+ * Detect a shaped Lambda Proxy response.
387
+ *
388
+ * A response is "shaped" when it has a numeric `statusCode` property —
389
+ * the definitive marker of an API-Gateway-style response envelope.
390
+ * Missing `headers` or `body` are defaulted by {@link shapeResponse}
391
+ * rather than treated as "unshaped", because `{ statusCode, headers }`
392
+ * (no body) is a valid Lambda Proxy return value.
393
+ */
394
+ const isShapedResponse = (v) => typeof v === 'object' &&
395
+ v !== null &&
396
+ 'statusCode' in v &&
397
+ typeof v.statusCode === 'number';
398
+ /**
399
+ * Shape the response into a well-formed Lambda Proxy result.
400
+ *
401
+ * Shaped responses (detected via {@link isShapedResponse}) are normalised
402
+ * in-place — missing `headers` and `body` are defaulted. Non-shaped
403
+ * values are wrapped in a `200` envelope.
404
+ */
405
+ const shapeResponse = (current, contentType) => {
406
+ let res;
407
+ if (isShapedResponse(current)) {
408
+ res = current;
409
+ }
410
+ else {
411
+ res = { statusCode: 200, headers: {}, body: current };
412
+ }
413
+ // Default missing fields for shaped responses without body/headers.
414
+ if (res.body === undefined)
415
+ res.body = '';
416
+ if (res.body !== undefined && typeof res.body !== 'string') {
417
+ try {
418
+ res.body = JSON.stringify(res.body);
419
+ }
420
+ catch {
421
+ res.body = String(res.body);
422
+ }
423
+ }
424
+ const headers = res.headers ?? {};
425
+ headers['Content-Type'] = contentType;
426
+ res.headers = headers;
427
+ return res;
428
+ };
429
+ /**
430
+ * Apply CORS headers to error responses.
431
+ *
432
+ * middy v6+ does not re-run the `after` chain after `onError` handles an
433
+ * error, so the regular CORS `after` hook never fires for error responses.
434
+ * This dedicated `onError` step runs *after* the error handler has set
435
+ * `request.response`, ensuring CORS headers are present on error responses.
436
+ */
437
+ const makeOnErrorCors = (opts) => {
438
+ const cors = httpCors({
439
+ credentials: true,
440
+ getOrigin: (o) => o,
441
+ ...(opts?.cors ?? {}),
442
+ });
443
+ return tagStep({
444
+ onError: async (request) => {
445
+ if (request.response === undefined)
446
+ return;
447
+ if (cors.after)
448
+ await cors.after(request);
449
+ },
450
+ }, 'error-cors');
451
+ };
452
+ /**
453
+ * Shape error responses into well-formed Lambda Proxy results.
454
+ *
455
+ * Mirrors `makeShapeAndContentType` but runs on the `onError` path,
456
+ * ensuring error responses have proper `statusCode`, `headers`, and
457
+ * `body` structure even when the `after` chain is skipped.
458
+ */
459
+ const makeOnErrorShape = (contentType) => tagStep({
460
+ onError: (request) => {
461
+ const container = request;
462
+ if (container.response === undefined)
463
+ return;
464
+ container.response = shapeResponse(container.response, contentType);
465
+ },
466
+ }, 'error-shape');
467
+
376
468
  const makeHead = () => tagStep(shortCircuitHead, 'head');
377
469
  const makeHeaderNormalizer = (opts) => tagStep(asApiMiddleware(httpHeaderNormalizer(opts?.headerNormalizer ?? { canonical: true })), 'header-normalizer');
378
470
  const makeEventNormalizer = () => tagStep(asApiMiddleware(httpEventNormalizer()), 'event-normalizer');
@@ -527,31 +619,9 @@ const makeCors = (opts) => tagStep(asApiMiddleware(httpCors({
527
619
  const makeShapeAndContentType = (contentType) => tagStep({
528
620
  after: (request) => {
529
621
  const container = request;
530
- const current = container.response;
531
- if (current === undefined)
622
+ if (container.response === undefined)
532
623
  return;
533
- const looksShaped = typeof current === 'object' &&
534
- current !== null &&
535
- 'statusCode' in current &&
536
- 'headers' in current &&
537
- 'body' in current;
538
- let res;
539
- if (looksShaped)
540
- res = current;
541
- else
542
- res = { statusCode: 200, headers: {}, body: current };
543
- if (res.body !== undefined && typeof res.body !== 'string') {
544
- try {
545
- res.body = JSON.stringify(res.body);
546
- }
547
- catch {
548
- res.body = String(res.body);
549
- }
550
- }
551
- const headers = res.headers ?? {};
552
- headers['Content-Type'] = contentType;
553
- res.headers = headers;
554
- request.response = res;
624
+ container.response = shapeResponse(container.response, contentType);
555
625
  },
556
626
  }, 'shape');
557
627
  const makeSerializer = (contentType, opts) => tagStep(asApiMiddleware(httpResponseSerializer({
@@ -587,7 +657,15 @@ const buildDefaultPhases = (args) => {
587
657
  makeShapeAndContentType(contentType),
588
658
  makeSerializer(contentType, opts),
589
659
  ];
590
- const onError = [makeErrorExpose(), makeErrorHandler(opts)];
660
+ const onError = [
661
+ makeErrorExpose(),
662
+ makeErrorHandler(opts),
663
+ // Post-error-handler processing: middy v6+ does not re-run the after
664
+ // chain after onError, so CORS and response shaping must happen here.
665
+ // Shape first so CORS can mutate a well-formed headers object.
666
+ makeOnErrorShape(contentType),
667
+ makeOnErrorCors(opts),
668
+ ];
591
669
  return { before, after, onError };
592
670
  };
593
671
  const buildSafeDefaults = (args) => buildDefaultPhases(args);
package/package.json CHANGED
@@ -184,7 +184,7 @@
184
184
  "templates:lint": "eslint --fix -c templates/default/eslint.config.ts \"templates/default/**/*.{ts,tsx,js,jsx}\" \"templates/default/eslint.config.ts\""
185
185
  },
186
186
  "type": "module",
187
- "version": "0.2.12",
187
+ "version": "0.2.13",
188
188
  "volta": {
189
189
  "node": "22.19.0"
190
190
  }