serverless-offline
Version:
Emulate AWS λ and API Gateway locally when developing your Serverless project
199 lines (171 loc) • 5.65 kB
JavaScript
import { Buffer } from "node:buffer"
import { env } from "node:process"
import { decodeJwt } from "jose"
import { log } from "../../../utils/log.js"
import {
detectEncoding,
formatToClfTime,
lowerCaseKeys,
nullIfEmpty,
parseHeaders,
parseQueryStringParametersForPayloadV2,
} from "../../../utils/index.js"
const { isArray } = Array
const { parse } = JSON
const { assign, entries } = Object
// https://www.serverless.com/framework/docs/providers/aws/events/http-api/
// https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
export default class LambdaProxyIntegrationEventV2 {
#additionalRequestContext = null
#routeKey = null
#request = null
#stage = null
constructor(request, stage, routeKey, additionalRequestContext) {
this.#additionalRequestContext = additionalRequestContext || {}
this.#routeKey = routeKey
this.#request = request
this.#stage = stage
}
create() {
const authContext =
(this.#request.auth &&
this.#request.auth.credentials &&
this.#request.auth.credentials.context) ||
{}
// AWS adds the lambda key to the auth context object
const lambdaAuthContext = { lambda: authContext }
let authAuthorizer
if (env.AUTHORIZER) {
try {
authAuthorizer = parse(env.AUTHORIZER)
} catch {
log.error(
"Could not parse process.env.AUTHORIZER, make sure it is correct JSON",
)
}
}
let body = this.#request.payload
let isBase64Encoded = false
const { rawHeaders } = this.#request.raw.req
// NOTE FIXME request.raw.req.rawHeaders can only be null for testing (hapi shot inject())
const headers = lowerCaseKeys(parseHeaders(rawHeaders || [])) || {}
if (headers["sls-offline-authorizer-override"]) {
try {
authAuthorizer = parse(headers["sls-offline-authorizer-override"])
} catch {
log.error(
"Could not parse header sls-offline-authorizer-override, make sure it is correct JSON",
)
}
}
if (body) {
if (
this.#request.raw.req.payload &&
detectEncoding(this.#request) === "binary"
) {
body = Buffer.from(this.#request.raw.req.payload).toString("base64")
headers["content-length"] = String(Buffer.byteLength(body, "base64"))
isBase64Encoded = true
}
if (typeof body !== "string") {
// this.#request.payload is NOT the same as the rawPayload
body = this.#request.rawPayload
}
if (
!headers["content-length"] &&
(typeof body === "string" ||
body instanceof Buffer ||
body instanceof ArrayBuffer)
) {
headers["content-length"] = String(Buffer.byteLength(body))
}
// Set a default Content-Type if not provided.
if (!headers["content-type"]) {
headers["content-type"] = "application/json"
}
} else if (body === undefined || body === "") {
body = null
}
// clone own props
const pathParams = { ...this.#request.params }
let token = headers.Authorization || headers.authorization
if (token && token.split(" ")[0] === "Bearer") {
;[, token] = token.split(" ")
}
let claims
let scopes
if (token) {
try {
claims = decodeJwt(token)
if (claims.scp || claims.scope) {
scopes = claims.scp || claims.scope.split(" ")
// In AWS HTTP Api the scope property is removed from the decoded JWT
// I'm leaving this property because I'm not sure how all of the authorizers
// for AWS REST Api handle JWT.
// claims = { ...claims }
// delete claims.scope
}
} catch {
// Do nothing
}
}
const {
headers: _headers,
info: { received, remoteAddress },
method,
} = this.#request
const httpMethod = method.toUpperCase()
const requestTime = formatToClfTime(received)
const requestTimeEpoch = received
const cookies = this.#request.state
? entries(this.#request.state).flatMap(([key, value]) => {
if (isArray(value)) {
return value.map((v) => `${key}=${v}`)
}
return `${key}=${value}`
})
: undefined
return {
body,
cookies,
headers,
isBase64Encoded,
pathParameters: nullIfEmpty(pathParams),
queryStringParameters: this.#request.url.search
? parseQueryStringParametersForPayloadV2(this.#request.url.searchParams)
: null,
rawPath: this.#request.url.pathname,
rawQueryString: this.#request.url.searchParams.toString(),
requestContext: {
accountId: "offlineContext_accountId",
apiId: "offlineContext_apiId",
authorizer:
authAuthorizer ||
assign(lambdaAuthContext, {
jwt: {
claims,
scopes,
},
}),
domainName: "offlineContext_domainName",
domainPrefix: "offlineContext_domainPrefix",
http: {
method: httpMethod,
path: this.#request.url.pathname,
protocol: "HTTP/1.1",
sourceIp: remoteAddress,
userAgent: _headers["user-agent"] || "",
},
operationName: this.#additionalRequestContext.operationName,
requestId: "offlineContext_resourceId",
routeKey: this.#routeKey,
stage: this.#stage,
time: requestTime,
timeEpoch: requestTimeEpoch,
},
routeKey: this.#routeKey,
stageVariables: null,
version: "2.0",
}
}
}