@karmaniverous/smoz 0.2.11 → 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');
@@ -487,6 +579,30 @@ const makeErrorExpose = (logger) => tagStep({
487
579
  /invalid (event|response)/i.test(msg))) {
488
580
  maybe.statusCode = 400;
489
581
  }
582
+ // JSON-encode the error message so middy's httpErrorHandler emits
583
+ // application/json instead of text/plain. Guard against double-encoding
584
+ // since this step is registered in both the after and onError arrays.
585
+ try {
586
+ const parsed = JSON.parse(msg);
587
+ if (typeof parsed === 'object' &&
588
+ parsed !== null &&
589
+ 'statusCode' in parsed &&
590
+ 'message' in parsed)
591
+ return;
592
+ }
593
+ catch {
594
+ // not JSON — proceed with encoding
595
+ }
596
+ const statusCode = typeof maybe.statusCode === 'number'
597
+ ? maybe.statusCode
598
+ : 500;
599
+ // Ensure statusCode is set so middy's httpErrorHandler doesn't
600
+ // discard the message with its fallback replacement.
601
+ if (typeof maybe.statusCode !== 'number') {
602
+ maybe.statusCode = statusCode;
603
+ }
604
+ const message = msg || 'Internal Server Error';
605
+ maybe.message = JSON.stringify({ statusCode, message });
490
606
  },
491
607
  }, 'error-expose');
492
608
  const makeErrorHandler = (opts) => tagStep(asApiMiddleware(httpErrorHandler({
@@ -505,31 +621,9 @@ const makeCors = (opts) => tagStep(asApiMiddleware(httpCors({
505
621
  const makeShapeAndContentType = (contentType) => tagStep({
506
622
  after: (request) => {
507
623
  const container = request;
508
- const current = container.response;
509
- if (current === undefined)
624
+ if (container.response === undefined)
510
625
  return;
511
- const looksShaped = typeof current === 'object' &&
512
- current !== null &&
513
- 'statusCode' in current &&
514
- 'headers' in current &&
515
- 'body' in current;
516
- let res;
517
- if (looksShaped)
518
- res = current;
519
- else
520
- res = { statusCode: 200, headers: {}, body: current };
521
- if (res.body !== undefined && typeof res.body !== 'string') {
522
- try {
523
- res.body = JSON.stringify(res.body);
524
- }
525
- catch {
526
- res.body = String(res.body);
527
- }
528
- }
529
- const headers = res.headers ?? {};
530
- headers['Content-Type'] = contentType;
531
- res.headers = headers;
532
- request.response = res;
626
+ container.response = shapeResponse(container.response, contentType);
533
627
  },
534
628
  }, 'shape');
535
629
  const makeSerializer = (contentType, opts) => tagStep(asApiMiddleware(httpResponseSerializer({
@@ -565,7 +659,15 @@ const buildDefaultPhases = (args) => {
565
659
  makeShapeAndContentType(contentType),
566
660
  makeSerializer(contentType, opts),
567
661
  ];
568
- 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
+ ];
569
671
  return { before, after, onError };
570
672
  };
571
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');
@@ -485,6 +577,30 @@ const makeErrorExpose = (logger) => tagStep({
485
577
  /invalid (event|response)/i.test(msg))) {
486
578
  maybe.statusCode = 400;
487
579
  }
580
+ // JSON-encode the error message so middy's httpErrorHandler emits
581
+ // application/json instead of text/plain. Guard against double-encoding
582
+ // since this step is registered in both the after and onError arrays.
583
+ try {
584
+ const parsed = JSON.parse(msg);
585
+ if (typeof parsed === 'object' &&
586
+ parsed !== null &&
587
+ 'statusCode' in parsed &&
588
+ 'message' in parsed)
589
+ return;
590
+ }
591
+ catch {
592
+ // not JSON — proceed with encoding
593
+ }
594
+ const statusCode = typeof maybe.statusCode === 'number'
595
+ ? maybe.statusCode
596
+ : 500;
597
+ // Ensure statusCode is set so middy's httpErrorHandler doesn't
598
+ // discard the message with its fallback replacement.
599
+ if (typeof maybe.statusCode !== 'number') {
600
+ maybe.statusCode = statusCode;
601
+ }
602
+ const message = msg || 'Internal Server Error';
603
+ maybe.message = JSON.stringify({ statusCode, message });
488
604
  },
489
605
  }, 'error-expose');
490
606
  const makeErrorHandler = (opts) => tagStep(asApiMiddleware(httpErrorHandler({
@@ -503,31 +619,9 @@ const makeCors = (opts) => tagStep(asApiMiddleware(httpCors({
503
619
  const makeShapeAndContentType = (contentType) => tagStep({
504
620
  after: (request) => {
505
621
  const container = request;
506
- const current = container.response;
507
- if (current === undefined)
622
+ if (container.response === undefined)
508
623
  return;
509
- const looksShaped = typeof current === 'object' &&
510
- current !== null &&
511
- 'statusCode' in current &&
512
- 'headers' in current &&
513
- 'body' in current;
514
- let res;
515
- if (looksShaped)
516
- res = current;
517
- else
518
- res = { statusCode: 200, headers: {}, body: current };
519
- if (res.body !== undefined && typeof res.body !== 'string') {
520
- try {
521
- res.body = JSON.stringify(res.body);
522
- }
523
- catch {
524
- res.body = String(res.body);
525
- }
526
- }
527
- const headers = res.headers ?? {};
528
- headers['Content-Type'] = contentType;
529
- res.headers = headers;
530
- request.response = res;
624
+ container.response = shapeResponse(container.response, contentType);
531
625
  },
532
626
  }, 'shape');
533
627
  const makeSerializer = (contentType, opts) => tagStep(asApiMiddleware(httpResponseSerializer({
@@ -563,7 +657,15 @@ const buildDefaultPhases = (args) => {
563
657
  makeShapeAndContentType(contentType),
564
658
  makeSerializer(contentType, opts),
565
659
  ];
566
- 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
+ ];
567
669
  return { before, after, onError };
568
670
  };
569
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.11",
187
+ "version": "0.2.13",
188
188
  "volta": {
189
189
  "node": "22.19.0"
190
190
  }