@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 +127 -25
- package/dist/index.d.ts +1 -1
- package/dist/mjs/index.js +127 -25
- package/package.json +1 -1
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
|
-
|
|
509
|
-
if (current === undefined)
|
|
624
|
+
if (container.response === undefined)
|
|
510
625
|
return;
|
|
511
|
-
|
|
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 = [
|
|
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
|
-
|
|
507
|
-
if (current === undefined)
|
|
622
|
+
if (container.response === undefined)
|
|
508
623
|
return;
|
|
509
|
-
|
|
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 = [
|
|
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.
|
|
187
|
+
"version": "0.2.13",
|
|
188
188
|
"volta": {
|
|
189
189
|
"node": "22.19.0"
|
|
190
190
|
}
|