@nrfcloud/lambda-helpers 1.0.28

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/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Nordic Semiconductor ASA | nordicsemi.no
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # `@nrfcloud/lambda-helpers`
2
+
3
+ <https://www.npmjs.com/package/@nrfcloud/lambda-helpers>
4
+
5
+ Helper functions for AWS Lambda functions used in nRF Cloud.
6
+
7
+ ## Install with NPM
8
+
9
+ ```bash
10
+ npm i (--save-prod|--save-dev) @nrfcloud/lambda-helpers
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```typescript
16
+ import { MetricUnit } from "@aws-lambda-powertools/metrics";
17
+ import { logMetrics } from "@aws-lambda-powertools/metrics/middleware";
18
+ import middy from "@middy/core";
19
+ import { metricsForComponent } from "@nrfcloud/lambda-helpers";
20
+
21
+ const { track, metrics } = metricsForComponent("my-function", "my-service");
22
+
23
+ export const handler = middy()
24
+ .use(logMetrics(metrics))
25
+ .handler(async () => {
26
+ track("some:metric", MetricUnit.Count, 1);
27
+ });
28
+ ```
29
+
30
+ ## TypeScript 6 and 7
31
+
32
+ This repo
33
+ [runs TypeScript 6 and 7 side by side](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/#running-side-by-side-with-typescript-6.0),
34
+ [so that eslint works](https://github.com/typescript-eslint/typescript-eslint/issues/10940#issuecomment-4922812181).
@@ -0,0 +1,4 @@
1
+ import { type ProblemDetail } from '@nrfcloud/problem-detail';
2
+ import type { Static } from '@sinclair/typebox';
3
+ import type { APIGatewayProxyStructuredResultV2 } from 'aws-lambda';
4
+ export declare const aProblem: (problem: Omit<Static<typeof ProblemDetail>, '@context'>, cacheForSeconds?: number) => APIGatewayProxyStructuredResultV2;
@@ -0,0 +1,12 @@
1
+ import { Context as ProblemDetailContext } from '@nrfcloud/problem-detail';
2
+ export const aProblem = (problem, cacheForSeconds = 0)=>({
3
+ statusCode: problem.status,
4
+ headers: {
5
+ 'content-type': 'application/problem+json',
6
+ 'Cache-Control': cacheForSeconds > 0 ? `public, max-age=${cacheForSeconds}` : 'no-store'
7
+ },
8
+ body: JSON.stringify({
9
+ '@context': ProblemDetailContext,
10
+ ...problem
11
+ })
12
+ });
@@ -0,0 +1,5 @@
1
+ import type { HttpStatusCode } from '@nrfcloud/problem-detail';
2
+ import type { APIGatewayProxyStructuredResultV2 } from 'aws-lambda';
3
+ export declare const aResponse: (status: HttpStatusCode, result?: {
4
+ '@context': URL;
5
+ } & Record<string, unknown>, cacheForSeconds?: number, headers?: APIGatewayProxyStructuredResultV2['headers']) => APIGatewayProxyStructuredResultV2;
@@ -0,0 +1,15 @@
1
+ export const aResponse = (status, result, cacheForSeconds = 60, headers)=>{
2
+ const body = result !== undefined ? JSON.stringify(result) : undefined;
3
+ return {
4
+ statusCode: status,
5
+ headers: {
6
+ ...body !== undefined ? {
7
+ 'content-type': 'application/json'
8
+ } : {},
9
+ 'content-length': `${body?.length ?? 0}`,
10
+ 'Cache-Control': cacheForSeconds > 0 ? `public, max-age=${cacheForSeconds}` : 'no-store',
11
+ ...headers ?? {}
12
+ },
13
+ body
14
+ };
15
+ };
@@ -0,0 +1,3 @@
1
+ import type { MiddlewareObj } from '@middy/core';
2
+ import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from 'aws-lambda';
3
+ export declare const addCORSHeaders: (AccessControlAllowOrigin?: string, AccessControlExposeHeaders?: Array<string>) => MiddlewareObj<APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2>;
@@ -0,0 +1,21 @@
1
+ export const addCORSHeaders = (AccessControlAllowOrigin = '*', AccessControlExposeHeaders = [
2
+ 'x-amzn-requestid',
3
+ 'etag',
4
+ 'apigw-requestid'
5
+ ])=>{
6
+ const setCORSHeaders = async (req)=>{
7
+ if (req.response === null) return;
8
+ req.response = {
9
+ ...req.response,
10
+ headers: {
11
+ ...req.response.headers ?? {},
12
+ 'Access-Control-Allow-Origin': AccessControlAllowOrigin,
13
+ 'Access-Control-Expose-Headers': AccessControlExposeHeaders.join(', ')
14
+ }
15
+ };
16
+ };
17
+ return {
18
+ after: setCORSHeaders,
19
+ onError: setCORSHeaders
20
+ };
21
+ };
@@ -0,0 +1,3 @@
1
+ import type { MiddlewareObj } from '@middy/core';
2
+ import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from 'aws-lambda';
3
+ export declare const addVersionHeader: (version: string) => MiddlewareObj<APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2>;
@@ -0,0 +1,16 @@
1
+ export const addVersionHeader = (version)=>{
2
+ const setVersionHeader = async (req)=>{
3
+ if (req.response === null) return;
4
+ req.response = {
5
+ ...req.response,
6
+ headers: {
7
+ ...req.response.headers ?? {},
8
+ 'x-backend-version': version
9
+ }
10
+ };
11
+ };
12
+ return {
13
+ after: setVersionHeader,
14
+ onError: setVersionHeader
15
+ };
16
+ };
@@ -0,0 +1,11 @@
1
+ import type { APIGatewayProxyEventHeaders } from 'aws-lambda';
2
+ export declare const corsHeaders: ({ headers, }: {
3
+ headers: APIGatewayProxyEventHeaders;
4
+ }, allowedMethods?: string[], cacheForSeconds?: number) => {
5
+ 'Access-Control-Allow-Headers': string;
6
+ 'Access-Control-Expose-Headers': string;
7
+ 'Access-Control-Allow-Methods': string;
8
+ 'Access-Control-Allow-Origin': string;
9
+ 'Access-Control-Max-Age': number;
10
+ Vary: 'Origin';
11
+ };
@@ -0,0 +1,34 @@
1
+ import { parseHeaders } from './parseHeaders.js';
2
+ const allowedDomains = [
3
+ /^https?:\/\/localhost:/,
4
+ /^https:\/\/hello\.nrfcloud\.com$/
5
+ ];
6
+ const defaultOrigin = 'https://hello.nrfcloud.com';
7
+ const origin = (event)=>{
8
+ const origin = parseHeaders(event.headers).get('origin') ?? defaultOrigin.toString();
9
+ if (allowedDomains.find((rx)=>rx.test(origin)) !== undefined) return origin;
10
+ return defaultOrigin;
11
+ };
12
+ export const corsHeaders = ({ headers }, allowedMethods = [
13
+ 'PUT',
14
+ 'DELETE',
15
+ 'POST',
16
+ 'GET',
17
+ 'PATCH'
18
+ ], cacheForSeconds = 600)=>({
19
+ 'Access-Control-Allow-Origin': origin({
20
+ headers
21
+ }),
22
+ 'Access-Control-Allow-Methods': allowedMethods.join(', '),
23
+ 'Access-Control-Allow-Headers': Array.from(new Set([
24
+ 'content-type',
25
+ 'accept',
26
+ 'if-match',
27
+ 'authorization',
28
+ 'origin'
29
+ ])).map((h)=>h.trim()).sort((h1, h2)=>h1.localeCompare(h2)).join(', '),
30
+ 'Access-Control-Expose-Headers': 'x-amzn-requestid, etag, apigw-requestid',
31
+ 'Access-Control-Max-Age': cacheForSeconds,
32
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin#cors_and_caching
33
+ Vary: 'Origin'
34
+ });
@@ -0,0 +1,3 @@
1
+ import type { MiddlewareObj } from '@middy/core';
2
+ import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from 'aws-lambda';
3
+ export declare const corsOPTIONS: (...allowedMethods: string[]) => MiddlewareObj<APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2>;
@@ -0,0 +1,30 @@
1
+ import { corsHeaders } from './corsHeaders.js';
2
+ export const corsOPTIONS = (...allowedMethods)=>{
3
+ const setCorsHeaders = async (req)=>{
4
+ if (req.response === null) {
5
+ console.error(`[corsOPTIONS]`, `Response is null`);
6
+ return;
7
+ }
8
+ req.response = {
9
+ ...req.response,
10
+ headers: {
11
+ ...req.response?.headers ?? {},
12
+ ...corsHeaders(req.event, allowedMethods)
13
+ }
14
+ };
15
+ };
16
+ return {
17
+ before: async (req)=>{
18
+ if (req.event.requestContext.http.method === 'OPTIONS') {
19
+ console.debug(`[corsOPTIONS]`, `Handling OPTIONS request`);
20
+ return {
21
+ statusCode: 200,
22
+ headers: corsHeaders(req.event, allowedMethods)
23
+ };
24
+ }
25
+ return undefined;
26
+ },
27
+ after: setCorsHeaders,
28
+ onError: setCorsHeaders
29
+ };
30
+ };
@@ -0,0 +1,4 @@
1
+ import type { APIGatewayProxyEventHeaders, APIGatewayProxyStructuredResultV2 } from 'aws-lambda';
2
+ export declare const corsResponse: (event: {
3
+ headers: APIGatewayProxyEventHeaders;
4
+ }) => APIGatewayProxyStructuredResultV2;
@@ -0,0 +1,5 @@
1
+ import { corsHeaders } from './corsHeaders.js';
2
+ export const corsResponse = (event)=>({
3
+ statusCode: 200,
4
+ headers: corsHeaders(event)
5
+ });
@@ -0,0 +1,15 @@
1
+ export * from './addCORSHeaders.ts';
2
+ export * from './addVersionHeader.ts';
3
+ export * from './aProblem.ts';
4
+ export * from './aResponse.ts';
5
+ export * from './corsHeaders.ts';
6
+ export * from './corsOPTIONS.ts';
7
+ export * from './corsResponse.ts';
8
+ export * from './logger.ts';
9
+ export * from './metrics.ts';
10
+ export * from './parseHeaders.ts';
11
+ export * from './problemResponse.ts';
12
+ export * from './requestLogger.ts';
13
+ export * from './tryAsJSON.ts';
14
+ export * from './validateInput.ts';
15
+ export * from './validateResponse.ts';
package/npm/export.js ADDED
@@ -0,0 +1,15 @@
1
+ export * from './addCORSHeaders.js';
2
+ export * from './addVersionHeader.js';
3
+ export * from './aProblem.js';
4
+ export * from './aResponse.js';
5
+ export * from './corsHeaders.js';
6
+ export * from './corsOPTIONS.js';
7
+ export * from './corsResponse.js';
8
+ export * from './logger.js';
9
+ export * from './metrics.js';
10
+ export * from './parseHeaders.js';
11
+ export * from './problemResponse.js';
12
+ export * from './requestLogger.js';
13
+ export * from './tryAsJSON.js';
14
+ export * from './validateInput.js';
15
+ export * from './validateResponse.js';
@@ -0,0 +1,7 @@
1
+ export type Logger = {
2
+ error: (message: string, detail?: unknown) => void;
3
+ debug: (...args: any[]) => void;
4
+ info: (...args: any[]) => void;
5
+ warn: (...args: any[]) => void;
6
+ };
7
+ export declare const logger: (serviceName: string) => Logger;
package/npm/logger.js ADDED
@@ -0,0 +1,15 @@
1
+ export const logger = (serviceName)=>({
2
+ error: (message, detail)=>{
3
+ console.error(`[${serviceName}]`, message);
4
+ if (detail !== undefined) console.error(JSON.stringify(detail));
5
+ },
6
+ debug: (...args)=>{
7
+ console.debug(`[${serviceName}]`, ...args.map((a)=>JSON.stringify(a)));
8
+ },
9
+ info: (...args)=>{
10
+ console.log(`[${serviceName}]`, ...args.map((a)=>JSON.stringify(a)));
11
+ },
12
+ warn: (...args)=>{
13
+ console.warn(`[${serviceName}]`, ...args.map((a)=>JSON.stringify(a)));
14
+ }
15
+ });
@@ -0,0 +1,9 @@
1
+ import { Metrics } from '@aws-lambda-powertools/metrics';
2
+ export type AddMetricsFn = (...args: Parameters<Metrics['addMetric']>) => void;
3
+ /**
4
+ * Manages the instantiation of a Metrics object (unless Metrics are disabled)
5
+ */
6
+ export declare const metricsForComponent: (component: string, namespace?: string) => {
7
+ metrics: Metrics;
8
+ track: AddMetricsFn;
9
+ };
package/npm/metrics.js ADDED
@@ -0,0 +1,24 @@
1
+ import { Metrics } from '@aws-lambda-powertools/metrics';
2
+ const registry = {};
3
+ /**
4
+ * Manages the instantiation of a Metrics object (unless Metrics are disabled)
5
+ */ export const metricsForComponent = (component, namespace = 'nrfcloud')=>{
6
+ const metricsEnabled = process.env.DISABLE_METRICS !== '1';
7
+ if (registry[component] === undefined) {
8
+ console.debug(`[Metrics]`, metricsEnabled ? `Enabled` : `Disabled`);
9
+ registry[component] = new Metrics({
10
+ namespace,
11
+ serviceName: component
12
+ });
13
+ }
14
+ const metrics = registry[component];
15
+ return {
16
+ metrics,
17
+ track: (...args)=>{
18
+ if (!metricsEnabled) {
19
+ return;
20
+ }
21
+ metrics.addMetric(...args);
22
+ }
23
+ };
24
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Returns a Map of headers with keys in lowercase.
3
+ * This is useful for consistent header handling, especially in environments
4
+ * where header keys may be case-insensitive.
5
+ */
6
+ export declare const parseHeaders: (headers: Record<string, string | undefined> | null) => ParsedHeaders;
7
+ export type ParsedHeaders = Pick<Map<string, string | null>, 'get' | 'has' | 'size' | 'keys' | 'values' | 'entries' | 'forEach'>;
8
+ export declare const CaseInsensitiveMap: (map: Map<string, string | null>) => ParsedHeaders;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Returns a Map of headers with keys in lowercase.
3
+ * This is useful for consistent header handling, especially in environments
4
+ * where header keys may be case-insensitive.
5
+ */ export const parseHeaders = (headers)=>{
6
+ if (headers === null) return new Map();
7
+ return CaseInsensitiveMap(Object.entries(headers).reduce((h, [key, value])=>{
8
+ h.set(key.toLowerCase(), value ?? null);
9
+ return h;
10
+ }, new Map()));
11
+ };
12
+ export const CaseInsensitiveMap = (map)=>({
13
+ get: (key)=>map.get(key.toLowerCase()),
14
+ has: (key)=>map.has(key.toLowerCase()),
15
+ size: map.size,
16
+ keys: ()=>map.keys(),
17
+ values: ()=>map.values(),
18
+ entries: ()=>map.entries(),
19
+ forEach: (callback, thisArg)=>map.forEach(callback, thisArg)
20
+ });
@@ -0,0 +1,3 @@
1
+ import type { MiddlewareObj } from '@middy/core';
2
+ import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2, Context as LambdaContext } from 'aws-lambda';
3
+ export declare const problemResponse: () => MiddlewareObj<APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2, Error, LambdaContext>;
@@ -0,0 +1,30 @@
1
+ import { HttpStatusCode, ProblemDetailError } from '@nrfcloud/problem-detail';
2
+ import { formatTypeBoxErrors } from '@nrfcloud/validate-with-typebox';
3
+ import { aProblem } from './aProblem.js';
4
+ import { ValidationFailedError } from './validateInput.js';
5
+ import { ResponseValidationFailedError } from './validateResponse.js';
6
+ export const problemResponse = ()=>({
7
+ onError: async (req)=>{
8
+ if (req.response !== undefined) return;
9
+ if (req.error instanceof ResponseValidationFailedError) {
10
+ req.response = aProblem({
11
+ title: 'Response validation failed',
12
+ status: HttpStatusCode.INTERNAL_SERVER_ERROR,
13
+ detail: formatTypeBoxErrors(req.error.errors)
14
+ });
15
+ } else if (req.error instanceof ValidationFailedError) {
16
+ req.response = aProblem({
17
+ title: 'Validation failed',
18
+ status: HttpStatusCode.BAD_REQUEST,
19
+ detail: formatTypeBoxErrors(req.error.errors)
20
+ });
21
+ } else if (req.error instanceof ProblemDetailError) {
22
+ req.response = aProblem(req.error.problem);
23
+ } else {
24
+ req.response = aProblem({
25
+ title: req.error instanceof Error ? req.error.message : 'Internal Server Error',
26
+ status: HttpStatusCode.INTERNAL_SERVER_ERROR
27
+ });
28
+ }
29
+ }
30
+ });
@@ -0,0 +1,2 @@
1
+ import inputOutputLogger from '@middy/input-output-logger';
2
+ export declare const requestLogger: () => ReturnType<typeof inputOutputLogger>;
@@ -0,0 +1,4 @@
1
+ import inputOutputLogger from '@middy/input-output-logger';
2
+ export const requestLogger = ()=>inputOutputLogger({
3
+ logger: (message)=>console.debug(JSON.stringify(message))
4
+ });
@@ -0,0 +1 @@
1
+ export declare const tryAsJSON: (body: unknown) => Record<string, any> | null;
@@ -0,0 +1,8 @@
1
+ export const tryAsJSON = (body)=>{
2
+ if (typeof body !== 'string') return null;
3
+ try {
4
+ return JSON.parse(body);
5
+ } catch {
6
+ return null;
7
+ }
8
+ };
@@ -0,0 +1,18 @@
1
+ import type { MiddlewareObj } from '@middy/core';
2
+ import type { Static, StaticDecode, TSchema } from '@sinclair/typebox';
3
+ import type { ValueError } from '@sinclair/typebox/compiler';
4
+ import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2, Context } from 'aws-lambda';
5
+ export declare class ValidationFailedError extends Error {
6
+ readonly errors: ValueError[];
7
+ constructor(errors: ValueError[], message?: string);
8
+ }
9
+ export declare const validateInput: <Schema extends TSchema>(schema: Schema, mapInput?: (e: APIGatewayProxyEventV2) => unknown) => MiddlewareObj<APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2, Error, Context & ValidInput<Schema>>;
10
+ export type ValidInput<Schema extends TSchema> = {
11
+ /**
12
+ * @deprecated Use `decodedInput` instead
13
+ *
14
+ * This will be removed in the next major version.
15
+ */
16
+ validInput: Static<Schema>;
17
+ decodedInput: StaticDecode<Schema>;
18
+ };
@@ -0,0 +1,44 @@
1
+ import { formatTypeBoxErrors, validateWithTypeBox } from '@nrfcloud/validate-with-typebox';
2
+ import { Value } from '@sinclair/typebox/value';
3
+ import { parseHeaders } from './parseHeaders.js';
4
+ import { tryAsJSON } from './tryAsJSON.js';
5
+ export class ValidationFailedError extends Error {
6
+ errors;
7
+ constructor(errors, message = 'Validation failed'){
8
+ super(message);
9
+ this.errors = errors;
10
+ this.name = 'ValidationFailedError';
11
+ }
12
+ }
13
+ export const validateInput = (schema, mapInput)=>{
14
+ const v = validateWithTypeBox(schema);
15
+ return {
16
+ before: async (req)=>{
17
+ let reqBody = {};
18
+ const headers = parseHeaders(req.event.headers);
19
+ const contentType = headers.get('content-type') ?? '';
20
+ if (contentType.includes('application/json') && (req.event.body?.length ?? 0) >= 2 // should be at least '{}'
21
+ ) {
22
+ reqBody = tryAsJSON(req.event.body) ?? {};
23
+ }
24
+ const input = mapInput?.(req.event) ?? {
25
+ ...req.event.pathParameters ?? {},
26
+ ...req.event.queryStringParameters ?? {},
27
+ ...reqBody
28
+ };
29
+ console.debug(`[validateInput]`, 'input', JSON.stringify(input));
30
+ const maybeValidInput = v(input);
31
+ if ('errors' in maybeValidInput) {
32
+ console.debug(`[validateInput]`, `Input not valid`, JSON.stringify({
33
+ input,
34
+ errors: formatTypeBoxErrors(maybeValidInput.errors)
35
+ }), schema.title);
36
+ throw new ValidationFailedError(maybeValidInput.errors);
37
+ }
38
+ console.debug(`[validateInput]`, `Input is valid`, schema.title);
39
+ req.context.validInput = maybeValidInput.value;
40
+ req.context.decodedInput = Value.Decode(schema, maybeValidInput.value);
41
+ return undefined;
42
+ }
43
+ };
44
+ };
@@ -0,0 +1,11 @@
1
+ import type middy from '@middy/core';
2
+ import type { TSchema } from '@sinclair/typebox';
3
+ import type { ValueError } from '@sinclair/typebox/errors';
4
+ import { ValidationFailedError } from './validateInput.ts';
5
+ export declare class ResponseValidationFailedError extends ValidationFailedError {
6
+ constructor(errors: ValueError[]);
7
+ }
8
+ /**
9
+ * Validate responses created with `aResponse`
10
+ */
11
+ export declare const validateResponse: <ResponseSchema extends TSchema>(schema: ResponseSchema) => middy.MiddlewareObj;
@@ -0,0 +1,35 @@
1
+ import { formatTypeBoxErrors, validateWithTypeBox } from '@nrfcloud/validate-with-typebox';
2
+ import { parseHeaders } from './parseHeaders.js';
3
+ import { tryAsJSON } from './tryAsJSON.js';
4
+ import { ValidationFailedError } from './validateInput.js';
5
+ export class ResponseValidationFailedError extends ValidationFailedError {
6
+ constructor(errors){
7
+ super(errors, 'Response validation failed');
8
+ this.name = 'ResponseValidationFailedError';
9
+ }
10
+ }
11
+ /**
12
+ * Validate responses created with `aResponse`
13
+ */ export const validateResponse = (schema)=>{
14
+ const validator = validateWithTypeBox(schema);
15
+ return {
16
+ after: async (req)=>{
17
+ const body = req.response?.body;
18
+ const headers = parseHeaders(req.response.headers);
19
+ const contentType = headers.get('content-type') ?? '';
20
+ if ((body?.length ?? 0) === 0) {
21
+ console.debug(`[validateResponse]`, `Response body is empty`);
22
+ }
23
+ if ((contentType?.includes('application/json') ?? false) === false) {
24
+ console.debug(`[validateResponse]`, `Response body is not JSON`);
25
+ }
26
+ const maybeValid = validator(tryAsJSON(req.response.body));
27
+ if ('errors' in maybeValid) {
28
+ console.error(`[validateResponse]`, `Response validation failed`, req.response.body, formatTypeBoxErrors(maybeValid.errors), schema.title);
29
+ throw new ResponseValidationFailedError(maybeValid.errors);
30
+ }
31
+ console.debug(`[validateResponse]`, `Response is valid`, schema.title);
32
+ return undefined;
33
+ }
34
+ };
35
+ };
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@nrfcloud/lambda-helpers",
3
+ "version": "1.0.28",
4
+ "description": "Helper functions for AWS Lambda functions used in nRF Cloud.",
5
+ "homepage": "https://github.com/nrfcloud/lambda-helpers#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/nrfcloud/lambda-helpers/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/nrfcloud/lambda-helpers.git"
12
+ },
13
+ "author": "Nordic Semiconductor ASA | nordicsemi.no",
14
+ "scripts": {
15
+ "test": "node --no-warnings --experimental-transform-types --test \"./*.spec.ts\" \"./.npm/*.spec.ts\"",
16
+ "prepublishOnly": "node --experimental-strip-types ./.npm/compile.ts && npx tsc -P ./.npm/tsconfig.npm.json --outDir ./npm"
17
+ },
18
+ "license": "BSD-3-Clause",
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "import": {
23
+ "types": "./npm/export.d.ts",
24
+ "default": "./npm/export.js"
25
+ }
26
+ }
27
+ },
28
+ "files": [
29
+ "npm",
30
+ "README.md"
31
+ ],
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "devDependencies": {
36
+ "@bifravst/prettier-config": "1.1.17",
37
+ "@nrfcloud/validate-with-typebox": "npm:@jsr/nrfcloud__validate-with-typebox@^1.0.5",
38
+ "@swc/core": "1.15.47",
39
+ "@types/aws-lambda": "8.10.162",
40
+ "@types/node": "25.9.5",
41
+ "@typescript/native": "npm:typescript@7.0.2",
42
+ "prettier": "3.9.6",
43
+ "typescript": "npm:@typescript/typescript6@6.0.2"
44
+ },
45
+ "prettier": "@bifravst/prettier-config",
46
+ "release": {
47
+ "branches": [
48
+ "main"
49
+ ],
50
+ "remoteTags": true,
51
+ "plugins": [
52
+ "@semantic-release/commit-analyzer",
53
+ "@semantic-release/release-notes-generator",
54
+ "@semantic-release/npm",
55
+ [
56
+ "@semantic-release/github",
57
+ {
58
+ "successCommentCondition": false,
59
+ "failCommentCondition": false
60
+ }
61
+ ]
62
+ ]
63
+ },
64
+ "engines": {
65
+ "node": ">=24.18.1 <25",
66
+ "npm": ">=12.0.2 <13"
67
+ },
68
+ "dependencies": {
69
+ "@aws-lambda-powertools/metrics": "2.34.0",
70
+ "@middy/core": "7.7.2",
71
+ "@middy/input-output-logger": "7.7.2",
72
+ "@nrfcloud/problem-detail": "npm:@jsr/nrfcloud__problem-detail@1.4.2",
73
+ "@sinclair/typebox": "0.34.52"
74
+ }
75
+ }