genkitx-aws-bedrock
Version:
Genkit AI framework plugin for AWS Bedrock APIs.
565 lines • 22.2 kB
JavaScript
;
/**
* Copyright 2026 Xavier Portilla Edo
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __asyncValues = (this && this.__asyncValues) || function (o) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var m = o[Symbol.asyncIterator], i;
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.onCallGenkit = onCallGenkit;
exports.requireApiKey = requireApiKey;
exports.requireBearerToken = requireBearerToken;
exports.requireHeader = requireHeader;
exports.allowAll = allowAll;
exports.allOf = allOf;
exports.anyOf = anyOf;
const genkit_1 = require("genkit");
const context_1 = require("genkit/context");
/**
* Builds CORS headers based on options
*/
function buildCorsHeaders(corsOptions, requestOrigin) {
var _a, _b, _c, _d;
if (corsOptions === false) {
return {};
}
const opts = corsOptions === true || corsOptions === undefined ? {} : corsOptions;
const headers = {
"Content-Type": "application/json",
};
// Handle origin
const origin = (_a = opts.origin) !== null && _a !== void 0 ? _a : "*";
if (Array.isArray(origin)) {
// Check if request origin is in allowed list
if (requestOrigin && origin.includes(requestOrigin)) {
headers["Access-Control-Allow-Origin"] = requestOrigin;
}
// If request origin is not in the allowlist, don't set the header
}
else {
headers["Access-Control-Allow-Origin"] = origin;
}
// Handle methods
const methods = (_b = opts.methods) !== null && _b !== void 0 ? _b : ["POST", "OPTIONS"];
headers["Access-Control-Allow-Methods"] = methods.join(", ");
// Handle allowed headers
const allowedHeaders = (_c = opts.allowedHeaders) !== null && _c !== void 0 ? _c : [
"Content-Type",
"Authorization",
];
headers["Access-Control-Allow-Headers"] = allowedHeaders.join(", ");
// Handle exposed headers
if (opts.exposedHeaders && opts.exposedHeaders.length > 0) {
headers["Access-Control-Expose-Headers"] = opts.exposedHeaders.join(", ");
}
// Handle credentials
if (opts.credentials) {
headers["Access-Control-Allow-Credentials"] = "true";
}
// Handle max age
const maxAge = (_d = opts.maxAge) !== null && _d !== void 0 ? _d : 86400;
headers["Access-Control-Max-Age"] = String(maxAge);
return headers;
}
/**
* Parses the request body from an API Gateway event.
* Supports the Genkit callable protocol format where input is wrapped in { data: ... }
* as well as direct input format for convenience.
*/
function parseRequestBody(event) {
if (!event.body) {
return {};
}
const body = event.isBase64Encoded
? Buffer.from(event.body, "base64").toString("utf-8")
: event.body;
try {
const parsed = JSON.parse(body);
// Support callable protocol: { data: <input> }
if (parsed && typeof parsed === "object" && "data" in parsed) {
return parsed.data;
}
return parsed;
}
catch (_a) {
throw new genkit_1.UserFacingError("INVALID_ARGUMENT", "Invalid JSON in request body");
}
}
/**
* Gets the request origin from headers
*/
function getRequestOrigin(event) {
const headers = event.headers || {};
return headers["origin"] || headers["Origin"];
}
/**
* Converts Lambda event headers to lowercase record (as required by RequestData)
*/
function normalizeHeaders(headers) {
const result = {};
if (!headers)
return result;
for (const [key, value] of Object.entries(headers)) {
if (value !== undefined) {
result[key.toLowerCase()] = value;
}
}
return result;
}
/**
* Converts Lambda event to Genkit RequestData format
*/
function toRequestData(event, input) {
var _a, _b;
// Determine HTTP method
const method = "httpMethod" in event
? event.httpMethod
: ((_b = (_a = event.requestContext) === null || _a === void 0 ? void 0 : _a.http) === null || _b === void 0 ? void 0 : _b.method) || "POST";
return {
method: method,
headers: normalizeHeaders(event.headers),
input,
};
}
/**
* Implementation of onCallGenkit
*/
function onCallGenkit(optsOrFlow, flowArg) {
var _a;
let opts;
let flow;
if (arguments.length === 1) {
opts = {};
flow = optsOrFlow;
}
else {
opts = optsOrFlow;
flow = flowArg;
}
const flowName = ((_a = flow.__action) === null || _a === void 0 ? void 0 : _a.name) || "unknown";
const handler = (event, lambdaContext) => __awaiter(this, void 0, void 0, function* () {
const requestOrigin = getRequestOrigin(event);
const corsHeaders = buildCorsHeaders(opts.cors, requestOrigin);
// Handle OPTIONS preflight request
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 204,
headers: corsHeaders,
body: "",
};
}
// Debug logging
if (opts.debug) {
console.log(`[${flowName}] Event:`, JSON.stringify(event, null, 2));
console.log(`[${flowName}] Context:`, JSON.stringify(lambdaContext, null, 2));
}
try {
// Parse request body
const input = parseRequestBody(event);
// Build Lambda-specific context
const lambdaActionContext = {
lambda: {
event: {
requestContext: event.requestContext,
headers: event.headers,
queryStringParameters: event.queryStringParameters,
pathParameters: event.pathParameters,
},
context: {
functionName: lambdaContext.functionName,
functionVersion: lambdaContext.functionVersion,
invokedFunctionArn: lambdaContext.invokedFunctionArn,
memoryLimitInMB: lambdaContext.memoryLimitInMB,
awsRequestId: lambdaContext.awsRequestId,
},
},
};
// Run context provider if provided
let actionContext = lambdaActionContext;
if (opts.contextProvider) {
const requestData = toRequestData(event, input);
const providerContext = yield opts.contextProvider(requestData);
// Merge provider context with Lambda context
actionContext = Object.assign(Object.assign({}, lambdaActionContext), providerContext);
}
if (opts.debug) {
console.log(`[${flowName}] Running flow with input:`, input);
}
// Execute the flow with context
const runResult = yield flow.run(input, { context: actionContext });
const result = runResult.result;
if (opts.debug) {
console.log(`[${flowName}] Flow completed successfully`);
}
// Return success response (callable protocol)
return {
statusCode: 200,
headers: corsHeaders,
body: JSON.stringify({
result,
}),
};
}
catch (error) {
console.error(`[${flowName}] Error:`, error);
// Use custom error handler if provided
if (opts.onError) {
const customError = yield opts.onError(error instanceof Error ? error : new Error(String(error)));
return {
statusCode: customError.statusCode,
headers: corsHeaders,
body: JSON.stringify({
error: {
status: "INTERNAL",
message: customError.message,
},
}),
};
}
// Use Genkit's callable error format (same as express handler)
return {
statusCode: (0, context_1.getHttpStatus)(error),
headers: corsHeaders,
body: JSON.stringify((0, context_1.getCallableJSON)(error)),
};
}
});
// Attach additional properties to the handler
const callableFunction = handler;
callableFunction.flow = flow;
callableFunction.flowName = flowName;
// If streaming mode, return the streaming handler directly
if (opts.streaming) {
return awslambda.streamifyResponse((event, responseStream, lambdaCtx) => __awaiter(this, void 0, void 0, function* () {
var _a, e_1, _b, _c;
var _d, _e, _f, _g;
const requestOrigin = getRequestOrigin(event);
const corsHeaders = buildCorsHeaders(opts.cors, requestOrigin);
// Handle OPTIONS preflight
const method = ((_e = (_d = event.requestContext) === null || _d === void 0 ? void 0 : _d.http) === null || _e === void 0 ? void 0 : _e.method) || "POST";
if (method === "OPTIONS") {
const httpStream = awslambda.HttpResponseStream.from(responseStream, {
statusCode: 204,
headers: corsHeaders,
});
httpStream.end();
return;
}
if (opts.debug) {
console.log(`[${flowName}] Stream event:`, JSON.stringify(event, null, 2));
}
try {
const input = parseRequestBody(event);
// Build context
const lambdaActionContext = {
lambda: {
event: {
requestContext: event.requestContext,
headers: event.headers,
queryStringParameters: event.queryStringParameters,
pathParameters: event.pathParameters,
},
context: {
functionName: lambdaCtx.functionName,
functionVersion: lambdaCtx.functionVersion,
invokedFunctionArn: lambdaCtx.invokedFunctionArn,
memoryLimitInMB: lambdaCtx.memoryLimitInMB,
awsRequestId: lambdaCtx.awsRequestId,
},
},
};
let actionContext = lambdaActionContext;
if (opts.contextProvider) {
const requestData = toRequestData(event, input);
const providerContext = yield opts.contextProvider(requestData);
actionContext = Object.assign(Object.assign({}, lambdaActionContext), providerContext);
}
// Check if client wants SSE streaming
const acceptHeader = ((_f = event.headers) === null || _f === void 0 ? void 0 : _f["accept"]) || ((_g = event.headers) === null || _g === void 0 ? void 0 : _g["Accept"]) || "";
const clientWantsStreaming = acceptHeader.includes("text/event-stream");
if (clientWantsStreaming) {
// Real streaming: write SSE events incrementally
const httpStream = awslambda.HttpResponseStream.from(responseStream, {
statusCode: 200,
headers: Object.assign(Object.assign({}, corsHeaders), { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" }),
});
const { stream, output } = flow.stream(input, {
context: actionContext,
});
try {
for (var _h = true, stream_1 = __asyncValues(stream), stream_1_1; stream_1_1 = yield stream_1.next(), _a = stream_1_1.done, !_a; _h = true) {
_c = stream_1_1.value;
_h = false;
const chunk = _c;
httpStream.write(`data: ${JSON.stringify({ message: chunk })}\n\n`);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (!_h && !_a && (_b = stream_1.return)) yield _b.call(stream_1);
}
finally { if (e_1) throw e_1.error; }
}
const result = (yield output);
httpStream.write(`data: ${JSON.stringify({ result })}\n\n`);
httpStream.end();
if (opts.debug) {
console.log(`[${flowName}] Streaming flow completed successfully`);
}
}
else {
// Non-streaming: buffered JSON response
const runResult = yield flow.run(input, {
context: actionContext,
});
const result = runResult.result;
const httpStream = awslambda.HttpResponseStream.from(responseStream, {
statusCode: 200,
headers: corsHeaders,
});
httpStream.write(JSON.stringify({ result }));
httpStream.end();
}
}
catch (error) {
console.error(`[${flowName}] Stream error:`, error);
let statusCode = (0, context_1.getHttpStatus)(error);
let body;
if (opts.onError) {
const customError = yield opts.onError(error instanceof Error ? error : new Error(String(error)));
statusCode = customError.statusCode;
body = JSON.stringify({
error: {
status: "INTERNAL",
message: customError.message,
},
});
}
else {
body = JSON.stringify((0, context_1.getCallableJSON)(error));
}
const httpStream = awslambda.HttpResponseStream.from(responseStream, {
statusCode,
headers: corsHeaders,
});
httpStream.write(body);
httpStream.end();
}
}));
}
return callableFunction;
}
/**
* Creates a context provider that requires an API key in a specific header.
*
* @example
* ```typescript
* // Require API key to match a specific value
* export const handler = onCallGenkit(
* { contextProvider: requireApiKey('X-API-Key', process.env.API_KEY!) },
* myFlow
* );
*
* // Or with a custom validation function
* export const handler = onCallGenkit(
* {
* contextProvider: requireApiKey('X-API-Key', async (key) => {
* const valid = await validateApiKey(key);
* if (!valid) {
* throw new UserFacingError('PERMISSION_DENIED', 'Invalid API key');
* }
* })
* },
* myFlow
* );
* ```
*/
function requireApiKey(headerName, expectedValueOrValidator) {
const lowerHeaderName = headerName.toLowerCase();
return (request) => __awaiter(this, void 0, void 0, function* () {
const apiKey = request.headers[lowerHeaderName];
if (!apiKey) {
throw new genkit_1.UserFacingError("UNAUTHENTICATED", `Missing required header: ${headerName}`);
}
if (typeof expectedValueOrValidator === "string") {
if (apiKey !== expectedValueOrValidator) {
throw new genkit_1.UserFacingError("PERMISSION_DENIED", "Invalid API key");
}
}
else {
yield expectedValueOrValidator(apiKey);
}
return {
auth: { apiKey },
};
});
}
/**
* Creates a context provider that requires Bearer token authentication.
*
* @example
* ```typescript
* // With custom token validation
* export const handler = onCallGenkit(
* {
* contextProvider: requireBearerToken(async (token) => {
* const user = await verifyJWT(token);
* return { auth: { user } };
* })
* },
* myFlow
* );
* ```
*/
function requireBearerToken(validateToken) {
return (request) => __awaiter(this, void 0, void 0, function* () {
const authHeader = request.headers["authorization"];
if (!authHeader) {
throw new genkit_1.UserFacingError("UNAUTHENTICATED", "Missing Authorization header");
}
const match = authHeader.match(/^Bearer\s+(.+)$/i);
if (!match) {
throw new genkit_1.UserFacingError("UNAUTHENTICATED", "Invalid Authorization header format. Expected: Bearer <token>");
}
const token = match[1];
return yield validateToken(token);
});
}
/**
* Creates a context provider that requires a specific header to be present.
*
* @example
* ```typescript
* // Require header to exist
* export const handler = onCallGenkit(
* { contextProvider: requireHeader('X-Request-ID') },
* myFlow
* );
*
* // Require header to have specific value
* export const handler = onCallGenkit(
* { contextProvider: requireHeader('X-API-Version', '2.0') },
* myFlow
* );
* ```
*/
function requireHeader(headerName, expectedValue) {
const lowerHeaderName = headerName.toLowerCase();
return (request) => __awaiter(this, void 0, void 0, function* () {
const value = request.headers[lowerHeaderName];
if (!value) {
throw new genkit_1.UserFacingError("UNAUTHENTICATED", `Missing required header: ${headerName}`);
}
if (expectedValue !== undefined && value !== expectedValue) {
throw new genkit_1.UserFacingError("PERMISSION_DENIED", `Invalid value for header: ${headerName}`);
}
return {};
});
}
/**
* Creates a context provider that always allows requests (no authentication).
* Useful for public endpoints.
*
* @example
* ```typescript
* export const handler = onCallGenkit(
* { contextProvider: allowAll() },
* myPublicFlow
* );
* ```
*/
function allowAll() {
return () => __awaiter(this, void 0, void 0, function* () { return ({}); });
}
/**
* Combines multiple context providers. All providers must succeed.
* The returned context is a merge of all provider contexts.
*
* @example
* ```typescript
* export const handler = onCallGenkit(
* {
* contextProvider: allOf(
* requireHeader('X-Request-ID'),
* requireApiKey('X-API-Key', process.env.API_KEY!)
* )
* },
* myFlow
* );
* ```
*/
function allOf(...providers) {
return (request) => __awaiter(this, void 0, void 0, function* () {
let mergedContext = {};
for (const provider of providers) {
const context = yield provider(request);
mergedContext = Object.assign(Object.assign({}, mergedContext), context);
}
return mergedContext;
});
}
/**
* Tries context providers in order, returning the first one that succeeds.
* If all providers fail, throws the error from the last provider.
*
* @example
* ```typescript
* // Accept either API key or Bearer token
* export const handler = onCallGenkit(
* {
* contextProvider: anyOf(
* requireApiKey('X-API-Key', process.env.API_KEY!),
* requireBearerToken(async (token) => {
* const user = await verifyJWT(token);
* return { auth: { user } };
* })
* )
* },
* myFlow
* );
* ```
*/
function anyOf(...providers) {
return (request) => __awaiter(this, void 0, void 0, function* () {
let lastError;
for (const provider of providers) {
try {
const context = yield provider(request);
return context;
}
catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
}
}
throw lastError || new genkit_1.UserFacingError("UNAUTHENTICATED", "Unauthorized");
});
}
exports.default = onCallGenkit;
//# sourceMappingURL=aws_lambda.js.map