Skip to content

@j0nathan-ll0yd/core ​

The core package provides handler factory functions, the withObservability() utility, decorators, response helpers, and shared types for Lambda development.

Handler Functions ​

defineEventBridgeHandler() ​

Create a fully-observable EventBridge Lambda handler with typed detail-type extraction.

typescript
import { defineEventBridgeHandler } from "@j0nathan-ll0yd/core";

function defineEventBridgeHandler<TDetailType extends string, TDetail = unknown>(options: {
  detailTypes: TDetailType[]; // Detail types this handler responds to
  source?: string; // Event source for filtering (informational)
  operationName?: string; // Metrics/tracing name (default: handler function name or 'EventBridgeHandler')
  timeout?: number; // Lambda timeout in seconds
  memorySize?: number; // Lambda memory in MB
  reservedConcurrency?: number; // Reserved concurrent executions
  ephemeralStorage?: number; // Ephemeral storage in MB
  deadLetterQueue?: boolean | { targetArn?: string };
  retryAttempts?: number; // 0-2
}): (
  handler: (params: EventBridgeHandlerParams<TDetailType, TDetail>) => Promise<EventBridgeResult>,
) => (
  event: EventBridgeEvent<TDetailType, TDetail>,
  context: Context,
) => Promise<EventBridgeResult>;

Usage:

typescript
import { defineEventBridgeHandler } from "@j0nathan-ll0yd/core";

const eb = defineEventBridgeHandler({
  detailTypes: ["ExportHealthData"],
  timeout: 60,
  operationName: "ExportHealthData",
});
export const handler = eb(async ({ detail, detailType, source, context }) => {
  const data = await HealthQueries.getTodayData();
  await exportToS3("health-export.json", data);
  return { processed: 1, detailType };
});

Built-in behaviour:

  • X-Ray annotations for detailType and eventSource
  • Structured log entry with detail type and source
  • Full observability via withObservability()

The annotations and the start log are emitted before the detail schema runs. A schema rejection throws out of the parse, and with them after it a failing invocation left no detailType annotation and no start line — the one trace an operator needs to see which event shape was rejected.

Dual triggers and SCHEDULED_EVENT_DETAIL_TYPE. A handler that declares both detailTypes and a schedule receives two event shapes on one function: the subscribed detail types, and CloudWatch's scheduled fire, whose detail-type is the string exported as SCHEDULED_EVENT_DETAIL_TYPE ('Scheduled Event') and whose detail is the rule's input ({} by default).

Neither schema form is applied to a scheduled fire. Its payload is not a subscribed payload, so parsing it against the subscribed schema is a category error — and the workaround it used to force was weakening that schema with .partial(), which gave up validation on the real events too. Keep the schema strict; the framework skips the scheduled shape for you.

When a schedule is present the params widen to a discriminated union on detailType, so narrowing gives the right detail type in each branch:

typescript
import { defineEventBridgeHandler, SCHEDULED_EVENT_DETAIL_TYPE } from "@j0nathan-ll0yd/core";

const eb = defineEventBridgeHandler({
  detailTypes: ["BroadcastUpdate"],
  detailSchema: broadcastUpdateSchema, // strict; never sees a scheduled fire
  schedule: { expression: "rate(30 minutes)" },
});
export const handler = eb(async ({ detailType, detail }) => {
  if (detailType === SCHEDULED_EVENT_DETAIL_TYPE) {
    return compose({ trigger: "schedule" }); // `detail` is `unknown` here
  }
  return compose({ trigger: detail.reason }); // parsed by broadcastUpdateSchema
});

A handler that deliberately SUBSCRIBES to the literal detail type 'Scheduled Event' — listing it in detailTypes — still has its schema applied to it. That is a subscription, not the schedule trigger.

defineScheduledHandler() ​

Create a fully-observable scheduled Lambda handler for CloudWatch cron/rate events.

typescript
import { defineScheduledHandler } from "@j0nathan-ll0yd/core";

function defineScheduledHandler(options: {
  operationName?: string; // Metrics/tracing name (default: handler function name or 'ScheduledHandler')
  schedule?: {
    expression: string; // 'rate(6 hours)' | 'cron(0 12 * * ? *)'
    enabled?: boolean; // Default: true
  };
  timeout?: number; // Lambda timeout in seconds
  memorySize?: number; // Lambda memory in MB
  reservedConcurrency?: number; // Reserved concurrent executions
  ephemeralStorage?: number; // Ephemeral storage in MB
  deadLetterQueue?: boolean | { targetArn?: string };
  retryAttempts?: number; // 0-2
}): (
  handler: (params: ScheduledHandlerParams) => Promise<ScheduledResult>,
) => (event: ScheduledEvent, context: Context) => Promise<ScheduledResult>;

Usage:

typescript
import { defineScheduledHandler } from "@j0nathan-ll0yd/core";

const scheduled = defineScheduledHandler({
  schedule: { expression: "rate(6 hours)" },
  timeout: 300,
  operationName: "DailyCleanup",
});
export const handler = scheduled(async ({ event, context }) => {
  const deleted = await cleanupExpiredRecords();
  return { deleted, processed: deleted };
});

defineSqsHandler() ​

Create a fully-observable SQS Lambda handler. The factory iterates the batch, parses each body as JSON (disable with parseBody: false), isolates per-record errors, and builds the SQSBatchResponse partial-batch-failure payload.

typescript
import { defineSqsHandler } from '@j0nathan-ll0yd/core'

function defineSqsHandler<TBody = unknown>(
  options: DefineSqsHandlerOptions<TBody>
): (handler: (record: SqsRecordContext<TBody>, metadata: WrapperMetadata) => Promise<void>) => BrandedLambda<...>

Usage:

typescript
const sqs = defineSqsHandler<{ fileId: string }>({
  queueName: "downloads",
  operationName: "ProcessDownload",
});
export const handler = sqs(async ({ body, messageId }) => {
  await processFile(body.fileId);
});

A record that throws is added to batchItemFailures by messageId; the rest of the batch still succeeds.

defineS3Handler() ​

Create a fully-observable S3 Lambda handler. The factory iterates records, URL-decodes each object key, and isolates per-record errors. Set trigger: 'eventbridge' for S3 to EventBridge to Lambda wiring; the default 'direct' handles S3 to Lambda notifications.

typescript
import { defineS3Handler } from '@j0nathan-ll0yd/core'

function defineS3Handler(
  options: DefineS3HandlerOptions
): (handler: (record: S3RecordContext, metadata: WrapperMetadata) => Promise<void>) => BrandedLambda<...>

Usage:

typescript
const s3 = defineS3Handler({ bucketName: "uploads", operationName: "IndexUpload" });
export const handler = s3(async ({ bucket, key, size }) => {
  await indexObject(bucket, key, size);
});

defineWebSocketHandler() ​

Create a fully-observable WebSocket API Gateway handler. The factory extracts connectionId, routeKey, and the parsed body from the event. Route keys are $connect, $disconnect, and $default.

typescript
import { defineWebSocketHandler } from "@j0nathan-ll0yd/core";

function defineWebSocketHandler(
  options: DefineWebSocketHandlerOptions,
): (
  handler: (params: WebSocketHandlerParams) => Promise<WebSocketResult>,
) => (event: APIGatewayProxyWebsocketEventV2, context: Context) => Promise<WebSocketResult>;

Usage:

typescript
const ws = defineWebSocketHandler({ routeKey: "$connect" });
export const handler = ws(async ({ connectionId, routeKey }) => {
  await storeConnection(connectionId);
  return { statusCode: 200 };
});

defineAuthorizerHandler() ​

Create a fully-observable API Gateway custom authorizer. Two overloads: type: 'token' receives a TokenAuthorizerHandlerParams, type: 'request' receives a RequestAuthorizerHandlerParams.

typescript
import { defineAuthorizerHandler } from "@j0nathan-ll0yd/core";

const auth = defineAuthorizerHandler({ type: "token", operationName: "ApiAuthorizer" });
export const handler = auth(async ({ authorizationToken, methodArn }) => {
  const user = await verify(authorizationToken);
  return buildAuthorizerResponse(user.id, "Allow", methodArn, { userId: user.id });
});

buildAuthorizerResponse() ​

Build the IAM policy document an authorizer must return.

typescript
function buildAuthorizerResponse(
  principalId: string,
  effect: "Allow" | "Deny",
  resource: string,
  context?: APIGatewayAuthorizerResultContext,
): APIGatewayAuthorizerResult;

Pass '*' as resource to authorize every method on the API. Values in context reach downstream handlers via event.requestContext.authorizer.

withObservability() ​

Internal utility that wraps any handler with full observability. This is used internally by all define*Handler functions -- you do not need to call it directly.

typescript
function withObservability<TEvent, TResult>(
  options: { operationName: string },
  handler: (event: TEvent, context: Context, metadata: WrapperMetadata) => Promise<TResult>,
): (event: TEvent, context: Context) => Promise<TResult>;

Provides:

  • Cold start detection (ColdStart metric)
  • Logger context injection (operation name, Lambda context)
  • Correlation ID extraction and propagation
  • X-Ray tracing span per invocation
  • <OperationName>Attempt and <OperationName>Success CloudWatch metrics
  • Automatic metrics.publishStoredMetrics() in finally block

Decorators ​

Stage 3 decorators (no experimentalDecorators).

@RequiresDatabase(tables) ​

Class decorator that declares database table permissions for IAM policy extraction. Used on entity query classes or handler-adjacent code.

typescript
@RequiresDatabase([{ table: 'users', actions: ['read', 'write'] }])
class UserQueries { ... }

@RequiresServices(services) ​

Class decorator that declares AWS service permissions for IAM policy extraction.

typescript
@RequiresServices([{ service: 'sqs', actions: ['sendMessage'], resource: '*' }])
class NotificationService { ... }

@Traced(spanName?) ​

Method decorator that wraps a method with an OpenTelemetry span. Used on entity query methods and service functions -- not on handler functions (handlers get tracing automatically via withObservability()).

typescript
class UserQueries {
  @Traced('getUserById')
  static async getUserById(id: string) { ... }
}

@LogMetrics(options?) ​

Method decorator that publishes CloudWatch metrics. Used internally -- handler functions get metrics automatically.

@InjectContext() ​

Method decorator that injects Lambda context into the Powertools logger. Used internally -- handler functions get context injection automatically.

getDatabasePermissions(target) ​

Extract database permission declarations from a decorated class (used by CLI for IAM policy generation).

getServicePermissions(target) ​

Extract service permission declarations from a decorated class.

Response Helpers ​

buildValidatedResponse(context, statusCode, body, schema) ​

Build a response and validate the body against a Zod schema before sending. This is the recommended response builder for all API handlers.

typescript
import { buildValidatedResponse } from "@j0nathan-ll0yd/core";
import { z } from "@j0nathan-ll0yd/validation";

const ResponseSchema = z.object({ items: z.array(z.object({ id: z.string() })) });
return buildValidatedResponse(context, 200, { items }, ResponseSchema);

buildResponse(context, statusCode, body?, headers?) ​

Build a standard API Gateway response with correlation headers. Use when response validation is not needed.

typescript
return buildResponse(context, 204);
return buildResponse(context, 200, { message: "ok" });

buildErrorResponse(context, error, metadata?, requestInfo?) ​

Build an error response from a thrown error. Maps error types to HTTP status codes automatically. Used internally by defineApiHandler.

sanitizeErrorMessage(message) ​

Strip sensitive information from error messages before sending to clients.

getErrorMessage(error) ​

Safely extract an error message string from an unknown error value.

Types ​

WrapperMetadata ​

Metadata passed to all wrapped handlers for distributed tracing.

typescript
interface WrapperMetadata {
  traceId: string; // AWS request ID for this Lambda invocation
  correlationId: string; // Correlation ID for end-to-end request tracing
}

ApiHandlerParams ​

Parameters passed to API Gateway handlers without body validation.

typescript
interface ApiHandlerParams<TEvent = APIGatewayProxyEvent> {
  event: TEvent;
  context: Context;
  metadata: WrapperMetadata;
}

ValidatedApiParams<TBody> ​

Parameters passed to API Gateway handlers with body validation.

typescript
interface ValidatedApiParams<TBody, TEvent = APIGatewayProxyEvent> {
  event: TEvent;
  context: Context;
  metadata: WrapperMetadata;
  body: TBody;
}

EventBridgeHandlerParams<TDetailType, TDetail> ​

Parameters passed to EventBridge handler functions.

typescript
interface EventBridgeHandlerParams<TDetailType extends string, TDetail> {
  event: EventBridgeEvent<TDetailType, TDetail>;
  context: Context;
  metadata: WrapperMetadata;
  detailType: TDetailType;
  detail: TDetail;
  source: string;
}

ScheduledHandlerParams ​

Parameters passed to scheduled handler functions.

typescript
interface ScheduledHandlerParams {
  event: ScheduledEvent;
  context: Context;
  metadata: WrapperMetadata;
}

EventBridgeResult ​

typescript
interface EventBridgeResult {
  processed?: number;
  detailType?: string;
  [key: string]: unknown;
}

ScheduledResult ​

typescript
interface ScheduledResult {
  processed?: number;
  deleted?: number;
  [key: string]: unknown;
}

Configuration ​

defineConfig(config) ​

Type-safe project configuration for mantle.config.ts.

typescript
import { defineConfig } from "@j0nathan-ll0yd/core";

export default defineConfig({
  name: "my-app",
  database: { provider: "aurora-dsql" },
  eventbridge: { bus: "my-app-events" },
  storage: [
    {
      name: "uploads",
      cloudfront: true,
      corsOrigins: ["https://example.com"],
    },
  ],
});

MantleConfig ​

typescript
interface MantleConfig {
  name: string;
  database?: { provider: "aurora-dsql" | "aurora-serverless-v2" | "neon" };
  auth?: { provider: string };
  features?: { observability?: boolean; resilience?: boolean };
  storage?: StorageBucketConfig[];
  eventbridge?: EventBridgeConfig;
}

EventBridgeConfig ​

typescript
interface EventBridgeConfig {
  bus: string;
}

StorageBucketConfig ​

typescript
interface StorageBucketConfig {
  name: string;
  cloudfront?: boolean;
  cloudfrontPriceClass?: string;
  corsOrigins?: string[];
  corsMethods?: string[];
  responseHeaders?: {
    corsOrigins?: string[];
    corsMethods?: string[];
    maxAgeSec?: number;
  };
  intelligentTiering?: boolean;
  versioning?: boolean;
}

defineLambda(config) ​

Build-time macro for per-Lambda configuration. No-op at runtime -- the CLI extracts config via ts-morph AST analysis.

For EventBridge and scheduled handlers, LambdaConfig properties are passed directly as options to defineEventBridgeHandler and defineScheduledHandler. defineLambda() is only needed for standalone handlers that have no corresponding define*Handler call.

typescript
import { defineLambda } from "@j0nathan-ll0yd/core";

// Only needed for standalone handlers
defineLambda({
  timeout: 60,
  memorySize: 256,
  deadLetterQueue: true,
  retryAttempts: 2,
});

LambdaConfig ​

typescript
interface LambdaConfig {
  timeout?: number;
  memorySize?: number; // Default: 128
  reservedConcurrency?: number;
  ephemeralStorage?: number; // Default: 512
  schedule?: {
    expression: string; // 'rate(6 hours)' | 'cron(0 12 * * ? *)'
    enabled?: boolean; // Default: true
  };
  eventbridge?: {
    detailTypes: string[];
  };
  deadLetterQueue?:
    | boolean
    | {
        targetArn?: string;
      };
  retryAttempts?: number; // 0-2
}

Utility Functions ​

extractCorrelationId(event, context) ​

Extract or generate a correlation ID from any event type (API Gateway, SQS, EventBridge, S3, Scheduled).

appendCorrelationToLogger(correlationId, traceId) ​

Append correlation and trace IDs to the Powertools logger context.

validateStaticBearerToken(event, expectedToken) ​

Validate a static bearer token from an API Gateway event's Authorization header. Throws UnauthorizedError if the token is missing or does not match.

getHeader(event, name) ​

Read a request header case-insensitively from an API Gateway v1 or v2 event. Returns undefined when absent. HTTP/2 lowercases header names, so never index event.headers directly.

typescript
const contentType = getHeader(event, "Content-Type");

getHeaderRequired(event, name) ​

Same lookup, but throws ValidationError when the header is absent.

typescript
const signature = getHeaderRequired(event, "X-Hub-Signature-256");

getStaticAsset(key) ​

Resolve a static asset that the CLI wired into the Lambda environment from storage[].assets. Reads the ASSET_<KEY>_KEY and ASSET_<KEY>_URL environment variables and throws when either is missing.

typescript
const logo = getStaticAsset("logo.png");
logo.key; // 'assets/logo.png'
logo.url; // 'https://cdn.example.com/assets/logo.png'

assertNever(value, message?) ​

Exhaustiveness check for discriminated unions. Always throws; the compiler rejects the call when a case is unhandled.

typescript
switch (status) {
  case "active":
    return handleActive();
  case "inactive":
    return handleInactive();
  default:
    return assertNever(status);
}

Event Emission ​

Emit EventBridge events through these helpers rather than the raw putEvents API. They read EVENT_BUS_NAME and EVENT_SOURCE once per cold start and, by default, log and swallow failures so event emission never breaks the primary handler path.

emitEvent(options) ​

Publish a single typed event.

typescript
import { emitEvent } from "@j0nathan-ll0yd/core";

await emitEvent({ detailType: "UserCreated", detail: { userId }, metadata });

Options: detailType (required), detail, metadata, busName, source, suppressErrors (default true). Passing metadata from the handler params propagates the correlation ID as _correlationId inside the event detail.

emitEvents(events, batchOptions?) ​

Publish many events in one putEvents call. Each entry may override busName and source; batchOptions supplies the shared defaults.

typescript
await emitEvents(
  [
    { detailType: "UserCreated", detail: { userId } },
    { detailType: "AuditLogged", detail: { action: "signup" } },
  ],
  { metadata },
);

createBoundEmitEvent(metadata) ​

Return emitEvent and emitEvents pre-bound to a handler's metadata, so correlation propagation cannot be forgotten at a call site. Every define*Handler factory passes the bound emitters to the handler.

typescript
const { emitEvent, emitEvents } = createBoundEmitEvent(metadata);
await emitEvent({ detailType: "UserCreated", detail: { userId } });

Result Type ​

A lightweight Result<T, E> for operations whose failure is expected and should not throw.

typescript
type Result<T, E = Error> =
  | { readonly ok: true; readonly value: T }
  | { readonly ok: false; readonly error: E };
FunctionPurpose
ok(value)Build a success result.
err(error)Build a failure result.
isOk(result)Type guard narrowing to the success branch.
isErr(result)Type guard narrowing to the failure branch.
unwrap(result)Return the value, or throw the contained error.
mapResult(result, fn)Apply fn to the value of a success result, passing failures through.
typescript
const parsed = parseInput(raw);
if (isErr(parsed)) return buildErrorResponse(context, parsed.error);
const upper = mapResult(parsed, (value) => value.toUpperCase());
return buildResponse(context, 200, unwrap(upper));

Branded Types ​

@j0nathan-ll0yd/core re-exports the framework type vocabulary from @j0nathan-ll0yd/types so handlers need one import: ApiGatewayRouteKey, BearerToken, CloudFrontDistributionId, DsqlClusterArn, EventBusName, LambdaArn, S3BucketName, and SqsQueueUrl. Wrap at the environment-read site, not at each call site.

typescript
const bucket = S3BucketName(getRequiredEnv("DATA_BUCKET"));

Deployment Provenance ​

Fail-closed loaders for the deployment facts mantle deploy binds. See Deployment Provenance for the end-to-end flow, and exportToS3 for the writer that stamps them.

FunctionReturnsRequires
loadDeploymentProvenance()DeploymentProvenanceENVIRONMENT, SOURCE_REVISION, DEPLOYED_AT
loadSourceRevisionProvenance()DeploymentProvenanceENVIRONMENT, SOURCE_REVISION
loadSourceRevision()stringSOURCE_REVISION

Each throws ValidationError when a required variable is absent, blank, or malformed. A Lambda deployed outside the provenance-aware path fails before it can publish an untraceable artifact.

loadSourceRevisionProvenance() is the honest subset: use it for a writer whose artifact no contract rule describes, so it claims the revision and nothing more.

typescript
import { loadDeploymentProvenance } from "@j0nathan-ll0yd/core";
import { exportToS3 } from "@j0nathan-ll0yd/aws";

const provenance = loadDeploymentProvenance();
await exportToS3({ bucket, key: "llms.txt.json", data, provenance });

PROVENANCE_ENV_VARS ​

Maps each DeploymentProvenance field to the environment variable it is bound from. The CLI generator reads this map, so the wire names have one definition.

typescript
const PROVENANCE_ENV_VARS = {
  sourceRevision: "SOURCE_REVISION",
  deployedAt: "DEPLOYED_AT",
  workflowRef: "DEPLOYMENT_WORKFLOW_REF",
  workflowRunId: "DEPLOYMENT_WORKFLOW_RUN_ID",
  workflowRunAttempt: "DEPLOYMENT_WORKFLOW_RUN_ATTEMPT",
} as const;

Constants ​

UserStatus ​

Authentication state of the caller on an API handler's context.

typescript
const UserStatus = { Authenticated: "Authenticated", Anonymous: "Anonymous" } as const;

Test Seams ​

Exported for tests only. They reset module-level caches that would otherwise leak between test cases. Never call them from handler or service code.

FunctionResets
_resetCorsCache()The parsed CORS configuration cached by the response helpers.
_setRequestOrigin(origin)The request origin the response helpers echo back in CORS headers.
_resetEmitEventCache()The cached EVENT_BUS_NAME and EVENT_SOURCE values.