effortless-aws 0.19.0 → 0.21.0

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/index.d.ts CHANGED
@@ -382,6 +382,31 @@ type HttpResponse = {
382
382
  contentType?: ContentType;
383
383
  /** Response headers (use for custom content-types not covered by contentType) */
384
384
  headers?: Record<string, string>;
385
+ /**
386
+ * Set to `true` to return binary data.
387
+ * When enabled, `body` must be a base64-encoded string and the response
388
+ * will include `isBase64Encoded: true` so Lambda Function URLs / API Gateway
389
+ * decode it back to binary for the client.
390
+ */
391
+ binary?: boolean;
392
+ };
393
+ /** Response helpers for defineApi handlers */
394
+ declare const result: {
395
+ /** Return a JSON response */
396
+ json: (body: unknown, status?: number) => HttpResponse;
397
+ /** Return a binary response. Accepts a Buffer and converts to base64 automatically. */
398
+ binary: (body: Buffer, contentType: string, headers?: Record<string, string>) => HttpResponse;
399
+ };
400
+ /** Stream helper injected into route args when `stream: true` is set on defineApi */
401
+ type ResponseStream = {
402
+ /** Write a raw string chunk to the response stream */
403
+ write(chunk: string): void;
404
+ /** End the response stream */
405
+ end(): void;
406
+ /** Switch to SSE mode: sets Content-Type to text/event-stream */
407
+ sse(): void;
408
+ /** Write an SSE event: `data: JSON.stringify(data)\n\n` */
409
+ event(data: unknown): void;
385
410
  };
386
411
  /** Service for reading static files bundled into the Lambda ZIP */
387
412
  type StaticFiles = {
@@ -984,6 +1009,14 @@ type MiddlewareDeny = {
984
1009
  type MiddlewareResult = MiddlewareRedirect | MiddlewareDeny | void;
985
1010
  /** Function that runs before serving static files via Lambda@Edge */
986
1011
  type MiddlewareHandler = (request: MiddlewareRequest) => Promise<MiddlewareResult> | MiddlewareResult;
1012
+ /** SEO options for auto-generating sitemap.xml, robots.txt, and submitting to Google Indexing API */
1013
+ type StaticSiteSeo = {
1014
+ /** Sitemap filename (e.g. "sitemap.xml", "sitemap-v2.xml") */
1015
+ sitemap: string;
1016
+ /** Path to Google service account JSON key file for Indexing API batch submission.
1017
+ * Requires adding the service account email as an owner in Google Search Console. */
1018
+ googleIndexing?: string;
1019
+ };
987
1020
  /**
988
1021
  * Configuration for a static site handler (S3 + CloudFront)
989
1022
  */
@@ -1009,6 +1042,8 @@ type StaticSiteConfig = {
1009
1042
  errorPage?: string;
1010
1043
  /** Lambda@Edge middleware that runs before serving pages. Use for auth checks, redirects, etc. */
1011
1044
  middleware?: MiddlewareHandler;
1045
+ /** SEO: auto-generate sitemap.xml and robots.txt at deploy time, optionally submit URLs to Google Indexing API */
1046
+ seo?: StaticSiteSeo;
1012
1047
  };
1013
1048
  /**
1014
1049
  * Internal handler object created by defineStaticSite
@@ -1235,7 +1270,7 @@ type FifoQueueHandler<T = unknown, C = undefined, D = undefined, P = undefined,
1235
1270
  declare const defineFifoQueue: <T = unknown, C = undefined, D extends Record<string, AnyDepHandler> | undefined = undefined, P extends Record<string, AnyParamRef> | undefined = undefined, S extends string[] | undefined = undefined>(options: DefineFifoQueueOptions<T, C, D, P, S>) => FifoQueueHandler<T, C, D, P, S>;
1236
1271
 
1237
1272
  /** GET route handler — no schema, no data */
1238
- type ApiGetHandlerFn<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = (args: {
1273
+ type ApiGetHandlerFn<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined, ST extends boolean | undefined = undefined> = (args: {
1239
1274
  req: HttpRequest;
1240
1275
  } & ([C] extends [undefined] ? {} : {
1241
1276
  ctx: C;
@@ -1245,9 +1280,11 @@ type ApiGetHandlerFn<C = undefined, D = undefined, P = undefined, S extends stri
1245
1280
  config: ResolveConfig<P>;
1246
1281
  }) & ([S] extends [undefined] ? {} : {
1247
1282
  files: StaticFiles;
1248
- })) => Promise<HttpResponse> | HttpResponse;
1283
+ }) & ([ST] extends [true] ? {
1284
+ stream: ResponseStream;
1285
+ } : {})) => Promise<HttpResponse | void> | HttpResponse | void;
1249
1286
  /** POST handler — with typed data from schema */
1250
- type ApiPostHandlerFn<T = undefined, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = (args: {
1287
+ type ApiPostHandlerFn<T = undefined, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined, ST extends boolean | undefined = undefined> = (args: {
1251
1288
  req: HttpRequest;
1252
1289
  } & ([T] extends [undefined] ? {} : {
1253
1290
  data: T;
@@ -1259,7 +1296,9 @@ type ApiPostHandlerFn<T = undefined, C = undefined, D = undefined, P = undefined
1259
1296
  config: ResolveConfig<P>;
1260
1297
  }) & ([S] extends [undefined] ? {} : {
1261
1298
  files: StaticFiles;
1262
- })) => Promise<HttpResponse> | HttpResponse;
1299
+ }) & ([ST] extends [true] ? {
1300
+ stream: ResponseStream;
1301
+ } : {})) => Promise<HttpResponse | void> | HttpResponse | void;
1263
1302
  /** Setup factory — receives deps/config/files when declared */
1264
1303
  type SetupFactory<C, D, P, S extends string[] | undefined = undefined> = (args: ([D] extends [undefined] ? {} : {
1265
1304
  deps: ResolveDeps<D>;
@@ -1272,6 +1311,8 @@ type SetupFactory<C, D, P, S extends string[] | undefined = undefined> = (args:
1272
1311
  type ApiConfig = LambdaWithPermissions & {
1273
1312
  /** Base path prefix for all routes (e.g., "/api") */
1274
1313
  basePath: string;
1314
+ /** Enable response streaming. When true, the Lambda Function URL uses RESPONSE_STREAM invoke mode. */
1315
+ stream?: boolean;
1275
1316
  };
1276
1317
  /**
1277
1318
  * Options for defining a CQRS-style API endpoint.
@@ -1279,9 +1320,11 @@ type ApiConfig = LambdaWithPermissions & {
1279
1320
  * - `get` routes handle queries (path-based routing, no body)
1280
1321
  * - `post` handles commands (single entry point, discriminated union via `schema`)
1281
1322
  */
1282
- type DefineApiOptions<T = undefined, C = undefined, D extends Record<string, AnyDepHandler> | undefined = undefined, P extends Record<string, AnyParamRef> | undefined = undefined, S extends string[] | undefined = undefined> = LambdaWithPermissions & {
1323
+ type DefineApiOptions<T = undefined, C = undefined, D extends Record<string, AnyDepHandler> | undefined = undefined, P extends Record<string, AnyParamRef> | undefined = undefined, S extends string[] | undefined = undefined, ST extends boolean | undefined = undefined> = LambdaWithPermissions & {
1283
1324
  /** Base path prefix for all routes (e.g., "/api") */
1284
1325
  basePath: string;
1326
+ /** Enable response streaming. When true, routes receive a `stream` arg for SSE. Routes can still return HttpResponse normally. */
1327
+ stream?: ST;
1285
1328
  /**
1286
1329
  * Factory function to initialize shared state.
1287
1330
  * Called once on cold start, result is cached and reused across invocations.
@@ -1296,7 +1339,7 @@ type DefineApiOptions<T = undefined, C = undefined, D extends Record<string, Any
1296
1339
  /** Error handler called when schema validation or handler throws */
1297
1340
  onError?: (error: unknown, req: HttpRequest) => HttpResponse;
1298
1341
  /** GET routes — query handlers keyed by relative path (e.g., "/users/{id}") */
1299
- get?: Record<string, ApiGetHandlerFn<C, D, P, S>>;
1342
+ get?: Record<string, ApiGetHandlerFn<C, D, P, S, ST>>;
1300
1343
  /**
1301
1344
  * Schema for POST body validation. Use with discriminated unions:
1302
1345
  * ```typescript
@@ -1306,10 +1349,10 @@ type DefineApiOptions<T = undefined, C = undefined, D extends Record<string, Any
1306
1349
  */
1307
1350
  schema?: (input: unknown) => T;
1308
1351
  /** POST handler — single entry point for commands */
1309
- post?: ApiPostHandlerFn<T, C, D, P, S>;
1352
+ post?: ApiPostHandlerFn<T, C, D, P, S, ST>;
1310
1353
  };
1311
1354
  /** Internal handler object created by defineApi */
1312
- type ApiHandler<T = undefined, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = {
1355
+ type ApiHandler<T = undefined, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined, ST extends boolean | undefined = undefined> = {
1313
1356
  readonly __brand: "effortless-api";
1314
1357
  readonly __spec: ApiConfig;
1315
1358
  readonly schema?: (input: unknown) => T;
@@ -1318,8 +1361,8 @@ type ApiHandler<T = undefined, C = undefined, D = undefined, P = undefined, S ex
1318
1361
  readonly deps?: D;
1319
1362
  readonly config?: P;
1320
1363
  readonly static?: string[];
1321
- readonly get?: Record<string, ApiGetHandlerFn<C, D, P, S>>;
1322
- readonly post?: ApiPostHandlerFn<T, C, D, P, S>;
1364
+ readonly get?: Record<string, ApiGetHandlerFn<C, D, P, S, ST>>;
1365
+ readonly post?: ApiPostHandlerFn<T, C, D, P, S, ST>;
1323
1366
  };
1324
1367
  /**
1325
1368
  * Define a CQRS-style API with typed GET routes and POST commands.
@@ -1355,6 +1398,6 @@ type ApiHandler<T = undefined, C = undefined, D = undefined, P = undefined, S ex
1355
1398
  * })
1356
1399
  * ```
1357
1400
  */
1358
- declare const defineApi: <T = undefined, C = undefined, D extends Record<string, AnyDepHandler> | undefined = undefined, P extends Record<string, AnyParamRef> | undefined = undefined, S extends string[] | undefined = undefined>(options: DefineApiOptions<T, C, D, P, S>) => ApiHandler<T, C, D, P, S>;
1401
+ declare const defineApi: <T = undefined, C = undefined, D extends Record<string, AnyDepHandler> | undefined = undefined, P extends Record<string, AnyParamRef> | undefined = undefined, S extends string[] | undefined = undefined, ST extends boolean | undefined = undefined>(options: DefineApiOptions<T, C, D, P, S, ST>) => ApiHandler<T, C, D, P, S, ST>;
1359
1402
 
1360
- export { type AnyParamRef, type ApiConfig, type ApiGetHandlerFn, type ApiHandler, type ApiPostHandlerFn, type AppConfig, type AppHandler, type BucketClient, type BucketConfig, type BucketEvent, type BucketHandler, type BucketObjectCreatedFn, type BucketObjectRemovedFn, type ContentType, type DefineApiOptions, type DefineBucketOptions, type DefineFifoQueueOptions, type DefineTableOptions, type EffortlessConfig, type EmailClient, type FailedRecord, type FifoQueueBatchFn, type FifoQueueConfig, type FifoQueueHandler, type FifoQueueMessage, type FifoQueueMessageFn, type HttpMethod, type HttpRequest, type HttpResponse, type LambdaConfig, type LambdaWithPermissions, type LogLevel, type MailerConfig, type MailerHandler, type MiddlewareDeny, type MiddlewareHandler, type MiddlewareRedirect, type MiddlewareRequest, type MiddlewareResult, type ParamRef, type Permission, type PutInput, type PutOptions, type QueryByTagParams, type QueryParams, type ResolveConfig, type ResolveDeps, type SendEmailOptions, type SkCondition, type StaticFiles, type StaticSiteConfig, type StaticSiteHandler, type StreamView, type TableBatchCompleteFn, type TableBatchFn, type TableClient, type TableConfig, type TableHandler, type TableItem, type TableKey, type TableRecord, type TableRecordFn, type UpdateActions, defineApi, defineApp, defineBucket, defineConfig, defineFifoQueue, defineMailer, defineStaticSite, defineTable, param, typed };
1403
+ export { type AnyParamRef, type ApiConfig, type ApiGetHandlerFn, type ApiHandler, type ApiPostHandlerFn, type AppConfig, type AppHandler, type BucketClient, type BucketConfig, type BucketEvent, type BucketHandler, type BucketObjectCreatedFn, type BucketObjectRemovedFn, type ContentType, type DefineApiOptions, type DefineBucketOptions, type DefineFifoQueueOptions, type DefineTableOptions, type EffortlessConfig, type EmailClient, type FailedRecord, type FifoQueueBatchFn, type FifoQueueConfig, type FifoQueueHandler, type FifoQueueMessage, type FifoQueueMessageFn, type HttpMethod, type HttpRequest, type HttpResponse, type LambdaConfig, type LambdaWithPermissions, type LogLevel, type MailerConfig, type MailerHandler, type MiddlewareDeny, type MiddlewareHandler, type MiddlewareRedirect, type MiddlewareRequest, type MiddlewareResult, type ParamRef, type Permission, type PutInput, type PutOptions, type QueryByTagParams, type QueryParams, type ResolveConfig, type ResolveDeps, type ResponseStream, type SendEmailOptions, type SkCondition, type StaticFiles, type StaticSiteConfig, type StaticSiteHandler, type StaticSiteSeo, type StreamView, type TableBatchCompleteFn, type TableBatchFn, type TableClient, type TableConfig, type TableHandler, type TableItem, type TableKey, type TableRecord, type TableRecordFn, type UpdateActions, defineApi, defineApp, defineBucket, defineConfig, defineFifoQueue, defineMailer, defineStaticSite, defineTable, param, result, typed };
package/dist/index.js CHANGED
@@ -98,6 +98,19 @@ function param(key, transform) {
98
98
  function typed() {
99
99
  return (input) => input;
100
100
  }
101
+
102
+ // src/handlers/shared.ts
103
+ var result = {
104
+ /** Return a JSON response */
105
+ json: (body, status = 200) => ({ status, body }),
106
+ /** Return a binary response. Accepts a Buffer and converts to base64 automatically. */
107
+ binary: (body, contentType, headers) => ({
108
+ status: 200,
109
+ body: body.toString("base64"),
110
+ binary: true,
111
+ headers: { "content-type": contentType, ...headers }
112
+ })
113
+ };
101
114
  export {
102
115
  defineApi,
103
116
  defineApp,
@@ -108,6 +121,7 @@ export {
108
121
  defineStaticSite,
109
122
  defineTable,
110
123
  param,
124
+ result,
111
125
  typed
112
126
  };
113
127
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/config.ts","../src/handlers/define-table.ts","../src/handlers/define-app.ts","../src/handlers/define-static-site.ts","../src/handlers/define-fifo-queue.ts","../src/handlers/define-bucket.ts","../src/handlers/define-mailer.ts","../src/handlers/define-api.ts","../src/handlers/handler-options.ts"],"sourcesContent":["/**\n * Configuration for an Effortless project.\n *\n * @example\n * ```typescript\n * // effortless.config.ts\n * import { defineConfig } from \"effortless-aws\";\n *\n * export default defineConfig({\n * name: \"my-service\",\n * region: \"eu-central-1\",\n * handlers: \"src\",\n * });\n * ```\n */\nexport type EffortlessConfig = {\n /**\n * Project root directory. All relative paths (handlers, server, assets, etc.)\n * are resolved from this directory.\n * Resolved relative to where the CLI runs (process.cwd()).\n * @default \".\" (current working directory)\n */\n root?: string;\n\n /**\n * Project name used for resource naming and tagging.\n * This becomes part of Lambda function names, IAM roles, etc.\n */\n name: string;\n\n /**\n * Default AWS region for all handlers.\n * Can be overridden per-handler or via CLI `--region` flag.\n * @default \"eu-central-1\"\n */\n region?: string;\n\n /**\n * Deployment stage (e.g., \"dev\", \"staging\", \"prod\").\n * Used for resource isolation and tagging.\n * @default \"dev\"\n */\n stage?: string;\n\n /**\n * Glob patterns or directory paths to scan for handlers.\n * Used by `eff deploy` (without file argument) to auto-discover handlers.\n *\n * @example\n * ```typescript\n * // Single directory - scans for all .ts files\n * handlers: \"src\"\n *\n * // Glob patterns\n * handlers: [\"src/**\\/*.ts\", \"lib/**\\/*.handler.ts\"]\n * ```\n */\n handlers?: string | string[];\n\n /**\n * Default settings applied to all handlers unless overridden.\n *\n * All Lambdas run on ARM64 (Graviton2) architecture — ~20% cheaper than x86_64\n * with better price-performance for most workloads.\n */\n defaults?: {\n /**\n * Lambda memory in MB. AWS allocates proportional CPU —\n * 1769 MB gives one full vCPU.\n * @default 256\n */\n memory?: number;\n\n /**\n * Lambda timeout as a human-readable string.\n * AWS maximum is 15 minutes.\n * @example \"30 seconds\", \"5 minutes\"\n */\n timeout?: string;\n\n /**\n * Node.js Lambda runtime version.\n * @default \"nodejs24.x\"\n */\n runtime?: string;\n };\n};\n\n/**\n * Helper function for type-safe configuration.\n * Returns the config object as-is, but provides TypeScript autocompletion.\n *\n * @example\n * ```typescript\n * import { defineConfig } from \"effortless-aws\";\n *\n * export default defineConfig({\n * name: \"my-service\",\n * region: \"eu-central-1\",\n * handlers: \"src\",\n * });\n * ```\n */\nexport const defineConfig = (config: EffortlessConfig): EffortlessConfig => config;\n","import type { LambdaWithPermissions, AnyParamRef, ResolveConfig, TableItem } from \"./handler-options\";\nimport type { TableClient } from \"../runtime/table-client\";\nimport type { AnyDepHandler, ResolveDeps } from \"./handler-deps\";\nimport type { StaticFiles } from \"./shared\";\n\n/** DynamoDB attribute types for keys */\nexport type KeyType = \"string\" | \"number\" | \"binary\";\n\n/**\n * DynamoDB table key definition\n */\nexport type TableKey = {\n /** Attribute name */\n name: string;\n /** Attribute type */\n type: KeyType;\n};\n\n/** DynamoDB Streams view type - determines what data is captured in stream records */\nexport type StreamView = \"NEW_AND_OLD_IMAGES\" | \"NEW_IMAGE\" | \"OLD_IMAGE\" | \"KEYS_ONLY\";\n\n/**\n * Configuration options for defineTable (single-table design).\n *\n * Tables always use `pk (S)` + `sk (S)` keys, `tag (S)` discriminator,\n * `data (M)` for domain fields, and `ttl (N)` for optional expiration.\n */\nexport type TableConfig = LambdaWithPermissions & {\n /** DynamoDB billing mode (default: \"PAY_PER_REQUEST\") */\n billingMode?: \"PAY_PER_REQUEST\" | \"PROVISIONED\";\n /** Stream view type - what data to include in stream records (default: \"NEW_AND_OLD_IMAGES\") */\n streamView?: StreamView;\n /** Number of records to process in each Lambda invocation (1-10000, default: 100) */\n batchSize?: number;\n /** Maximum time in seconds to gather records before invoking (0-300, default: 2) */\n batchWindow?: number;\n /** Where to start reading the stream (default: \"LATEST\") */\n startingPosition?: \"LATEST\" | \"TRIM_HORIZON\";\n /**\n * Name of the field in `data` that serves as the entity type discriminant.\n * Effortless auto-copies `data[tagField]` to the top-level DynamoDB `tag` attribute on `put()`.\n * Defaults to `\"tag\"`.\n *\n * @example\n * ```typescript\n * export const orders = defineTable({\n * tagField: \"type\",\n * schema: typed<{ type: \"order\"; amount: number }>(),\n * onRecord: async ({ record }) => { ... }\n * });\n * ```\n */\n tagField?: string;\n};\n\n/**\n * DynamoDB stream record passed to onRecord callback.\n *\n * `new` and `old` are full `TableItem<T>` objects with the single-table envelope.\n *\n * @typeParam T - Type of the domain data (inside `data`)\n */\nexport type TableRecord<T = Record<string, unknown>> = {\n /** Type of modification: INSERT, MODIFY, or REMOVE */\n eventName: \"INSERT\" | \"MODIFY\" | \"REMOVE\";\n /** New item value (present for INSERT and MODIFY) */\n new?: TableItem<T>;\n /** Old item value (present for MODIFY and REMOVE) */\n old?: TableItem<T>;\n /** Primary key of the affected item */\n keys: { pk: string; sk: string };\n /** Sequence number for ordering */\n sequenceNumber?: string;\n /** Approximate timestamp when the modification occurred */\n timestamp?: number;\n};\n\n/**\n * Information about a failed record during batch processing\n *\n * @typeParam T - Type of the domain data\n */\nexport type FailedRecord<T = Record<string, unknown>> = {\n /** The record that failed to process */\n record: TableRecord<T>;\n /** The error that occurred */\n error: unknown;\n};\n\n/**\n * Setup factory type for table handlers.\n * Always receives `table: TableClient<T>` (self-client for the handler's own table).\n * Also receives `deps` and/or `config` when declared.\n */\ntype SetupFactory<C, T, D, P, S extends string[] | undefined = undefined> = (args:\n & { table: TableClient<T> }\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P & {}> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => C | Promise<C>;\n\n/**\n * Callback function type for processing a single DynamoDB stream record\n */\nexport type TableRecordFn<T = Record<string, unknown>, C = undefined, R = void, D = undefined, P = undefined, S extends string[] | undefined = undefined> =\n (args: { record: TableRecord<T>; table: TableClient<T> }\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => Promise<R>;\n\n/**\n * Callback function type for processing accumulated batch results\n */\nexport type TableBatchCompleteFn<T = Record<string, unknown>, C = undefined, R = void, D = undefined, P = undefined, S extends string[] | undefined = undefined> =\n (args: { results: R[]; failures: FailedRecord<T>[]; table: TableClient<T> }\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => Promise<void>;\n\n/**\n * Callback function type for processing all records in a batch at once\n */\nexport type TableBatchFn<T = Record<string, unknown>, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> =\n (args: { records: TableRecord<T>[]; table: TableClient<T> }\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => Promise<void>;\n\n/** Base options shared by all defineTable variants */\ntype DefineTableBase<T = Record<string, unknown>, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = Omit<TableConfig, \"tagField\"> & {\n /** Name of the field in `data` that serves as the entity type discriminant (default: `\"tag\"`). */\n tagField?: Extract<keyof T, string>;\n /**\n * Decode/validate function for the `data` portion of stream record items.\n * Called with the unmarshalled `data` attribute; should return typed data or throw on validation failure.\n * When provided, T is inferred from the return type — no need to specify generic parameters.\n */\n schema?: (input: unknown) => T;\n /**\n * Error handler called when onRecord, onBatch, or onBatchComplete throws.\n * Receives the error. If not provided, defaults to `console.error`.\n */\n onError?: (error: unknown) => void;\n /**\n * Factory function to initialize shared state for callbacks.\n * Called once on cold start, result is cached and reused across invocations.\n * When deps/params are declared, receives them as argument.\n * Supports both sync and async return values.\n */\n setup?: SetupFactory<C, T, D, P, S>;\n /**\n * Dependencies on other handlers (tables, queues, etc.).\n * Typed clients are injected into the handler via the `deps` argument.\n */\n deps?: D;\n /**\n * SSM Parameter Store parameters.\n * Declare with `param()` helper. Values are fetched and cached at cold start.\n * Typed values are injected into the handler via the `config` argument.\n */\n config?: P;\n /**\n * Static file glob patterns to bundle into the Lambda ZIP.\n * Files are accessible at runtime via the `files` callback argument.\n */\n static?: S;\n};\n\n/** Per-record processing: onRecord with optional onBatchComplete */\ntype DefineTableWithOnRecord<T = Record<string, unknown>, C = undefined, R = void, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineTableBase<T, C, D, P, S> & {\n onRecord: TableRecordFn<T, C, R, D, P, S>;\n onBatchComplete?: TableBatchCompleteFn<T, C, R, D, P, S>;\n onBatch?: never;\n};\n\n/** Batch processing: onBatch processes all records at once */\ntype DefineTableWithOnBatch<T = Record<string, unknown>, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineTableBase<T, C, D, P, S> & {\n onBatch: TableBatchFn<T, C, D, P, S>;\n onRecord?: never;\n onBatchComplete?: never;\n};\n\n/** Resource-only: no handler, just creates the table */\ntype DefineTableResourceOnly<T = Record<string, unknown>, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineTableBase<T, C, D, P, S> & {\n onRecord?: never;\n onBatch?: never;\n onBatchComplete?: never;\n};\n\nexport type DefineTableOptions<\n T = Record<string, unknown>,\n C = undefined,\n R = void,\n D extends Record<string, AnyDepHandler> | undefined = undefined,\n P extends Record<string, AnyParamRef> | undefined = undefined,\n S extends string[] | undefined = undefined\n> =\n | DefineTableWithOnRecord<T, C, R, D, P, S>\n | DefineTableWithOnBatch<T, C, D, P, S>\n | DefineTableResourceOnly<T, C, D, P, S>;\n\n/**\n * Internal handler object created by defineTable\n * @internal\n */\nexport type TableHandler<T = Record<string, unknown>, C = undefined, R = void, D = undefined, P = undefined, S extends string[] | undefined = undefined> = {\n readonly __brand: \"effortless-table\";\n readonly __spec: TableConfig;\n readonly schema?: (input: unknown) => T;\n readonly onError?: (error: unknown) => void;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n readonly setup?: (...args: any[]) => C | Promise<C>;\n readonly deps?: D;\n readonly config?: P;\n readonly static?: string[];\n readonly onRecord?: TableRecordFn<T, C, R, D, P, S>;\n readonly onBatchComplete?: TableBatchCompleteFn<T, C, R, D, P, S>;\n readonly onBatch?: TableBatchFn<T, C, D, P, S>;\n};\n\n/**\n * Define a DynamoDB table with optional stream handler (single-table design).\n *\n * Creates a table with fixed key schema: `pk (S)` + `sk (S)`, plus `tag (S)`,\n * `data (M)`, and `ttl (N)` attributes. TTL is always enabled.\n *\n * @example Table with stream handler (typed)\n * ```typescript\n * type OrderData = { amount: number; status: string };\n *\n * export const orders = defineTable({\n * schema: typed<OrderData>(),\n * streamView: \"NEW_AND_OLD_IMAGES\",\n * batchSize: 10,\n * onRecord: async ({ record }) => {\n * if (record.eventName === \"INSERT\") {\n * console.log(\"New order:\", record.new?.data.amount);\n * }\n * }\n * });\n * ```\n *\n * @example Table only (no Lambda)\n * ```typescript\n * export const users = defineTable({});\n * ```\n */\nexport const defineTable = <\n T = Record<string, unknown>,\n C = undefined,\n R = void,\n D extends Record<string, AnyDepHandler> | undefined = undefined,\n P extends Record<string, AnyParamRef> | undefined = undefined,\n S extends string[] | undefined = undefined\n>(\n options: DefineTableOptions<T, C, R, D, P, S>\n): TableHandler<T, C, R, D, P, S> => {\n const { onRecord, onBatchComplete, onBatch, onError, schema, setup, deps, config, static: staticFiles, ...__spec } = options;\n return {\n __brand: \"effortless-table\",\n __spec,\n ...(schema ? { schema } : {}),\n ...(onError ? { onError } : {}),\n ...(setup ? { setup } : {}),\n ...(deps ? { deps } : {}),\n ...(config ? { config } : {}),\n ...(staticFiles ? { static: staticFiles } : {}),\n ...(onRecord ? { onRecord } : {}),\n ...(onBatchComplete ? { onBatchComplete } : {}),\n ...(onBatch ? { onBatch } : {})\n } as TableHandler<T, C, R, D, P, S>;\n};\n","import type { LambdaWithPermissions } from \"./handler-options\";\n\n/**\n * Configuration for deploying an SSR framework (Nuxt, Astro, etc.)\n * via CloudFront + S3 (static assets) + Lambda Function URL (server-side rendering).\n */\nexport type AppConfig = LambdaWithPermissions & {\n /** Directory containing the Lambda server handler (e.g., \".output/server\").\n * Must contain an `index.mjs` (or `index.js`) that exports a `handler` function. */\n server: string;\n /** Directory containing static assets for S3 (e.g., \".output/public\") */\n assets: string;\n /** Base URL path (default: \"/\") */\n path?: string;\n /** Shell command to build the framework output (e.g., \"nuxt build\") */\n build?: string;\n /** Custom domain name. String or stage-keyed record (e.g., { prod: \"app.example.com\" }). */\n domain?: string | Record<string, string>;\n /** CloudFront route overrides: path patterns forwarded to API Gateway instead of the SSR Lambda.\n * Keys are CloudFront path patterns (e.g., \"/api/*\"), values are handler references.\n * Example: `routes: { \"/api/*\": api }` */\n routes?: Record<string, { readonly __brand: string }>;\n};\n\n/**\n * Internal handler object created by defineApp\n * @internal\n */\nexport type AppHandler = {\n readonly __brand: \"effortless-app\";\n readonly __spec: AppConfig;\n};\n\n/**\n * Deploy an SSR framework application via CloudFront + Lambda Function URL.\n *\n * Static assets from the `assets` directory are served via S3 + CloudFront CDN.\n * Server-rendered pages are handled by a Lambda function using the framework's\n * built output from the `server` directory.\n *\n * For static-only sites (no SSR), use {@link defineStaticSite} instead.\n *\n * @param options - App configuration: server directory, assets directory, optional build command\n * @returns Handler object used by the deployment system\n *\n * @example Nuxt SSR\n * ```typescript\n * export const app = defineApp({\n * build: \"nuxt build\",\n * server: \".output/server\",\n * assets: \".output/public\",\n * memory: 1024,\n * });\n * ```\n */\nexport const defineApp = (options: AppConfig): AppHandler => ({\n __brand: \"effortless-app\",\n __spec: options,\n});\n","/** Any branded handler that deploys to API Gateway (HttpHandler, ApiHandler, etc.) */\ntype AnyRoutableHandler = { readonly __brand: string };\n\n/** Simplified request object passed to middleware */\nexport type MiddlewareRequest = {\n uri: string;\n method: string;\n querystring: string;\n headers: Record<string, string>;\n cookies: Record<string, string>;\n};\n\n/** Redirect the user to another URL */\nexport type MiddlewareRedirect = {\n redirect: string;\n status?: 301 | 302 | 307 | 308;\n};\n\n/** Deny access with a 403 status */\nexport type MiddlewareDeny = {\n status: 403;\n body?: string;\n};\n\n/** Middleware return type: redirect, deny, or void (continue serving) */\nexport type MiddlewareResult = MiddlewareRedirect | MiddlewareDeny | void;\n\n/** Function that runs before serving static files via Lambda@Edge */\nexport type MiddlewareHandler = (\n request: MiddlewareRequest\n) => Promise<MiddlewareResult> | MiddlewareResult;\n\n/**\n * Configuration for a static site handler (S3 + CloudFront)\n */\nexport type StaticSiteConfig = {\n /** Handler name. Defaults to export name if not specified */\n name?: string;\n /** Directory containing the static site files, relative to project root */\n dir: string;\n /** Default file for directory requests (default: \"index.html\") */\n index?: string;\n /** SPA mode: serve index.html for all paths that don't match a file (default: false) */\n spa?: boolean;\n /** Shell command to run before deploy to generate site content (e.g., \"npx astro build\") */\n build?: string;\n /** Custom domain name. Accepts a string (same domain for all stages) or a Record mapping stage names to domains (e.g., `{ prod: \"example.com\", dev: \"dev.example.com\" }`). Requires an ACM certificate in us-east-1. If the cert also covers www, a 301 redirect from www to non-www is set up automatically. */\n domain?: string | Record<string, string>;\n /** CloudFront route overrides: path patterns forwarded to API Gateway instead of S3.\n * Keys are CloudFront path patterns (e.g., \"/api/*\"), values are HTTP handlers.\n * Example: `routes: { \"/api/*\": api }` */\n routes?: Record<string, AnyRoutableHandler>;\n /** Custom 404 error page path relative to `dir` (e.g. \"404.html\").\n * For non-SPA sites only. If not set, a default page is generated automatically. */\n errorPage?: string;\n /** Lambda@Edge middleware that runs before serving pages. Use for auth checks, redirects, etc. */\n middleware?: MiddlewareHandler;\n};\n\n/**\n * Internal handler object created by defineStaticSite\n * @internal\n */\nexport type StaticSiteHandler = {\n readonly __brand: \"effortless-static-site\";\n readonly __spec: StaticSiteConfig;\n};\n\n/**\n * Deploy a static site via S3 + CloudFront CDN.\n *\n * @param options - Static site configuration: directory, optional SPA mode, build command\n * @returns Handler object used by the deployment system\n *\n * @example Documentation site\n * ```typescript\n * export const docs = defineStaticSite({\n * dir: \"dist\",\n * build: \"npx astro build\",\n * });\n * ```\n *\n * @example SPA with client-side routing\n * ```typescript\n * export const app = defineStaticSite({\n * dir: \"dist\",\n * spa: true,\n * build: \"npm run build\",\n * });\n * ```\n *\n * @example Protected site with middleware (Lambda@Edge)\n * ```typescript\n * export const admin = defineStaticSite({\n * dir: \"admin/dist\",\n * middleware: async (request) => {\n * if (!request.cookies.session) {\n * return { redirect: \"/login\" };\n * }\n * },\n * });\n * ```\n */\nexport const defineStaticSite = (options: StaticSiteConfig): StaticSiteHandler => ({\n __brand: \"effortless-static-site\",\n __spec: options,\n});\n","import type { LambdaWithPermissions, AnyParamRef, ResolveConfig } from \"./handler-options\";\nimport type { AnyDepHandler, ResolveDeps } from \"./handler-deps\";\nimport type { StaticFiles } from \"./shared\";\n\n/**\n * Parsed SQS FIFO message passed to the handler callbacks.\n *\n * @typeParam T - Type of the decoded message body (from schema function)\n */\nexport type FifoQueueMessage<T = unknown> = {\n /** Unique message identifier */\n messageId: string;\n /** Receipt handle for acknowledgement */\n receiptHandle: string;\n /** Parsed message body (JSON-decoded, then optionally schema-validated) */\n body: T;\n /** Raw unparsed message body string */\n rawBody: string;\n /** Message group ID (FIFO ordering key) */\n messageGroupId: string;\n /** Message deduplication ID */\n messageDeduplicationId?: string;\n /** SQS message attributes */\n messageAttributes: Record<string, { dataType?: string; stringValue?: string }>;\n /** Approximate first receive timestamp */\n approximateFirstReceiveTimestamp?: string;\n /** Approximate receive count */\n approximateReceiveCount?: string;\n /** Sent timestamp */\n sentTimestamp?: string;\n};\n\n/**\n * Configuration options for a FIFO queue handler\n */\nexport type FifoQueueConfig = LambdaWithPermissions & {\n /** Number of messages per Lambda invocation (1-10 for FIFO, default: 10) */\n batchSize?: number;\n /** Maximum time in seconds to gather messages before invoking (0-300, default: 0) */\n batchWindow?: number;\n /** Visibility timeout in seconds (default: max of timeout or 30) */\n visibilityTimeout?: number;\n /** Message retention period in seconds (60-1209600, default: 345600 = 4 days) */\n retentionPeriod?: number;\n /** Enable content-based deduplication (default: true) */\n contentBasedDeduplication?: boolean;\n};\n\n/**\n * Setup factory type — always receives an args object.\n * Args include `deps` and/or `config` when declared (empty `{}` otherwise).\n */\ntype SetupFactory<C, D, P, S extends string[] | undefined = undefined> =\n (args:\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P & {}> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => C | Promise<C>;\n\n/**\n * Per-message handler function.\n * Called once per message in the batch. Failures are reported individually.\n */\nexport type FifoQueueMessageFn<T = unknown, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> =\n (args: { message: FifoQueueMessage<T> }\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => Promise<void>;\n\n/**\n * Batch handler function.\n * Called once with all messages in the batch.\n */\nexport type FifoQueueBatchFn<T = unknown, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> =\n (args: { messages: FifoQueueMessage<T>[] }\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => Promise<void>;\n\n/** Base options shared by all defineFifoQueue variants */\ntype DefineFifoQueueBase<T = unknown, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = FifoQueueConfig & {\n /**\n * Decode/validate function for the message body.\n * Called with the JSON-parsed body; should return typed data or throw on validation failure.\n */\n schema?: (input: unknown) => T;\n /**\n * Error handler called when onMessage or onBatch throws.\n * If not provided, defaults to `console.error`.\n */\n onError?: (error: unknown) => void;\n /**\n * Factory function to initialize shared state for the handler.\n * Called once on cold start, result is cached and reused across invocations.\n * When deps/params are declared, receives them as argument.\n */\n setup?: SetupFactory<C, D, P, S>;\n /**\n * Dependencies on other handlers (tables, queues, etc.).\n * Typed clients are injected into the handler via the `deps` argument.\n */\n deps?: D;\n /**\n * SSM Parameter Store parameters.\n * Declare with `param()` helper. Values are fetched and cached at cold start.\n */\n config?: P;\n /**\n * Static file glob patterns to bundle into the Lambda ZIP.\n * Files are accessible at runtime via the `files` callback argument.\n */\n static?: S;\n};\n\n/** Per-message processing */\ntype DefineFifoQueueWithOnMessage<T = unknown, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineFifoQueueBase<T, C, D, P, S> & {\n onMessage: FifoQueueMessageFn<T, C, D, P, S>;\n onBatch?: never;\n};\n\n/** Batch processing: all messages at once */\ntype DefineFifoQueueWithOnBatch<T = unknown, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineFifoQueueBase<T, C, D, P, S> & {\n onBatch: FifoQueueBatchFn<T, C, D, P, S>;\n onMessage?: never;\n};\n\nexport type DefineFifoQueueOptions<\n T = unknown,\n C = undefined,\n D extends Record<string, AnyDepHandler> | undefined = undefined,\n P extends Record<string, AnyParamRef> | undefined = undefined,\n S extends string[] | undefined = undefined\n> =\n | DefineFifoQueueWithOnMessage<T, C, D, P, S>\n | DefineFifoQueueWithOnBatch<T, C, D, P, S>;\n\n/**\n * Internal handler object created by defineFifoQueue\n * @internal\n */\nexport type FifoQueueHandler<T = unknown, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = {\n readonly __brand: \"effortless-fifo-queue\";\n readonly __spec: FifoQueueConfig;\n readonly schema?: (input: unknown) => T;\n readonly onError?: (error: unknown) => void;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n readonly setup?: (...args: any[]) => C | Promise<C>;\n readonly deps?: D;\n readonly config?: P;\n readonly static?: string[];\n readonly onMessage?: FifoQueueMessageFn<T, C, D, P, S>;\n readonly onBatch?: FifoQueueBatchFn<T, C, D, P, S>;\n};\n\n/**\n * Define a FIFO SQS queue with a Lambda message handler\n *\n * Creates:\n * - SQS FIFO queue (with `.fifo` suffix)\n * - Lambda function triggered by the queue\n * - Event source mapping with partial batch failure support\n *\n * @example Per-message processing\n * ```typescript\n * type OrderEvent = { orderId: string; action: string };\n *\n * export const orderQueue = defineFifoQueue<OrderEvent>({\n * onMessage: async ({ message }) => {\n * console.log(\"Processing order:\", message.body.orderId);\n * }\n * });\n * ```\n *\n * @example Batch processing with schema\n * ```typescript\n * export const notifications = defineFifoQueue({\n * schema: (input) => NotificationSchema.parse(input),\n * batchSize: 5,\n * onBatch: async ({ messages }) => {\n * await sendAll(messages.map(m => m.body));\n * }\n * });\n * ```\n */\nexport const defineFifoQueue = <\n T = unknown,\n C = undefined,\n D extends Record<string, AnyDepHandler> | undefined = undefined,\n P extends Record<string, AnyParamRef> | undefined = undefined,\n S extends string[] | undefined = undefined\n>(\n options: DefineFifoQueueOptions<T, C, D, P, S>\n): FifoQueueHandler<T, C, D, P, S> => {\n const { onMessage, onBatch, onError, schema, setup, deps, config, static: staticFiles, ...__spec } = options;\n return {\n __brand: \"effortless-fifo-queue\",\n __spec,\n ...(schema ? { schema } : {}),\n ...(onError ? { onError } : {}),\n ...(setup ? { setup } : {}),\n ...(deps ? { deps } : {}),\n ...(config ? { config } : {}),\n ...(staticFiles ? { static: staticFiles } : {}),\n ...(onMessage ? { onMessage } : {}),\n ...(onBatch ? { onBatch } : {})\n } as FifoQueueHandler<T, C, D, P, S>;\n};\n","import type { LambdaWithPermissions, AnyParamRef, ResolveConfig } from \"./handler-options\";\nimport type { AnyDepHandler, ResolveDeps } from \"./handler-deps\";\nimport type { StaticFiles } from \"./shared\";\nimport type { BucketClient } from \"../runtime/bucket-client\";\n\n/**\n * Configuration options for defineBucket.\n */\nexport type BucketConfig = LambdaWithPermissions & {\n /** S3 key prefix filter for event notifications (e.g., \"uploads/\") */\n prefix?: string;\n /** S3 key suffix filter for event notifications (e.g., \".jpg\") */\n suffix?: string;\n};\n\n/**\n * S3 event record passed to onObjectCreated/onObjectRemoved callbacks.\n */\nexport type BucketEvent = {\n /** S3 event name (e.g., \"ObjectCreated:Put\", \"ObjectRemoved:Delete\") */\n eventName: string;\n /** Object key (path within the bucket) */\n key: string;\n /** Object size in bytes (present for created events) */\n size?: number;\n /** Object ETag (present for created events) */\n eTag?: string;\n /** ISO 8601 timestamp of when the event occurred */\n eventTime?: string;\n /** S3 bucket name */\n bucketName: string;\n};\n\n/**\n * Callback function type for S3 ObjectCreated events\n */\nexport type BucketObjectCreatedFn<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> =\n (args: { event: BucketEvent; bucket: BucketClient }\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => Promise<void>;\n\n/**\n * Callback function type for S3 ObjectRemoved events\n */\nexport type BucketObjectRemovedFn<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> =\n (args: { event: BucketEvent; bucket: BucketClient }\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => Promise<void>;\n\n/**\n * Setup factory type for bucket handlers.\n * Always receives `bucket: BucketClient` (self-client for the handler's own bucket).\n * Also receives `deps` and/or `config` when declared.\n */\ntype SetupFactory<C, D, P, S extends string[] | undefined = undefined> = (args:\n & { bucket: BucketClient }\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P & {}> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => C | Promise<C>;\n\n/** Base options shared by all defineBucket variants */\ntype DefineBucketBase<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = BucketConfig & {\n /**\n * Error handler called when onObjectCreated or onObjectRemoved throws.\n * If not provided, defaults to `console.error`.\n */\n onError?: (error: unknown) => void;\n /**\n * Factory function to initialize shared state for callbacks.\n * Called once on cold start, result is cached and reused across invocations.\n * Always receives `bucket: BucketClient` (self-client). When deps/config\n * are declared, receives them as well.\n */\n setup?: SetupFactory<C, D, P, S>;\n /**\n * Dependencies on other handlers (tables, buckets, etc.).\n * Typed clients are injected into the handler via the `deps` argument.\n */\n deps?: D;\n /**\n * SSM Parameter Store parameters.\n * Declare with `param()` helper. Values are fetched and cached at cold start.\n */\n config?: P;\n /**\n * Static file glob patterns to bundle into the Lambda ZIP.\n * Files are accessible at runtime via the `files` callback argument.\n */\n static?: S;\n};\n\n/** With event handlers (at least one callback) */\ntype DefineBucketWithHandlers<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineBucketBase<C, D, P, S> & {\n onObjectCreated?: BucketObjectCreatedFn<C, D, P, S>;\n onObjectRemoved?: BucketObjectRemovedFn<C, D, P, S>;\n};\n\n/** Resource-only: no Lambda, just creates the bucket */\ntype DefineBucketResourceOnly<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineBucketBase<C, D, P, S> & {\n onObjectCreated?: never;\n onObjectRemoved?: never;\n};\n\nexport type DefineBucketOptions<\n C = undefined,\n D extends Record<string, AnyDepHandler> | undefined = undefined,\n P extends Record<string, AnyParamRef> | undefined = undefined,\n S extends string[] | undefined = undefined\n> =\n | DefineBucketWithHandlers<C, D, P, S>\n | DefineBucketResourceOnly<C, D, P, S>;\n\n/**\n * Internal handler object created by defineBucket\n * @internal\n */\nexport type BucketHandler<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = {\n readonly __brand: \"effortless-bucket\";\n readonly __spec: BucketConfig;\n readonly onError?: (error: unknown) => void;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n readonly setup?: (...args: any[]) => C | Promise<C>;\n readonly deps?: D;\n readonly config?: P;\n readonly static?: string[];\n readonly onObjectCreated?: BucketObjectCreatedFn<C, D, P, S>;\n readonly onObjectRemoved?: BucketObjectRemovedFn<C, D, P, S>;\n};\n\n/**\n * Define an S3 bucket with optional event handlers.\n *\n * Creates an S3 bucket. When event handlers are provided, also creates a Lambda\n * function triggered by S3 event notifications.\n *\n * @example Bucket with event handler\n * ```typescript\n * export const uploads = defineBucket({\n * prefix: \"images/\",\n * suffix: \".jpg\",\n * onObjectCreated: async ({ event, bucket }) => {\n * const file = await bucket.get(event.key);\n * console.log(\"New upload:\", event.key, file?.body.length);\n * }\n * });\n * ```\n *\n * @example Resource-only bucket (no Lambda)\n * ```typescript\n * export const assets = defineBucket({});\n * ```\n *\n * @example As a dependency\n * ```typescript\n * export const api = defineApi({\n * basePath: \"/process\",\n * deps: { uploads },\n * post: async ({ req, deps }) => {\n * await deps.uploads.put(\"output.jpg\", buffer);\n * return { status: 200, body: \"OK\" };\n * },\n * });\n * ```\n */\nexport const defineBucket = <\n C = undefined,\n D extends Record<string, AnyDepHandler> | undefined = undefined,\n P extends Record<string, AnyParamRef> | undefined = undefined,\n S extends string[] | undefined = undefined\n>(\n options: DefineBucketOptions<C, D, P, S>\n): BucketHandler<C, D, P, S> => {\n const { onObjectCreated, onObjectRemoved, onError, setup, deps, config, static: staticFiles, ...__spec } = options;\n return {\n __brand: \"effortless-bucket\",\n __spec,\n ...(onError ? { onError } : {}),\n ...(setup ? { setup } : {}),\n ...(deps ? { deps } : {}),\n ...(config ? { config } : {}),\n ...(staticFiles ? { static: staticFiles } : {}),\n ...(onObjectCreated ? { onObjectCreated } : {}),\n ...(onObjectRemoved ? { onObjectRemoved } : {}),\n } as BucketHandler<C, D, P, S>;\n};\n","/**\n * Configuration options for defining a mailer (SES email identity)\n */\nexport type MailerConfig = {\n /** Domain to verify and send emails from (e.g., \"myapp.com\") */\n domain: string;\n};\n\n/**\n * Internal handler object created by defineMailer\n * @internal\n */\nexport type MailerHandler = {\n readonly __brand: \"effortless-mailer\";\n readonly __spec: MailerConfig;\n};\n\n/**\n * Define an email sender backed by Amazon SES.\n *\n * Creates an SES Email Identity for the specified domain and provides\n * a typed `EmailClient` to other handlers via `deps`.\n *\n * On first deploy, DKIM DNS records are printed to the console.\n * Add them to your DNS provider to verify the domain.\n *\n * @param options - Mailer configuration with the domain to send from\n * @returns Handler object used by the deployment system and as a `deps` value\n *\n * @example Basic mailer with HTTP handler\n * ```typescript\n * export const mailer = defineMailer({ domain: \"myapp.com\" });\n *\n * export const api = defineApi({\n * basePath: \"/signup\",\n * deps: { mailer },\n * post: async ({ req, deps }) => {\n * await deps.mailer.send({\n * from: \"hello@myapp.com\",\n * to: req.body.email,\n * subject: \"Welcome!\",\n * html: \"<h1>Hi!</h1>\",\n * });\n * return { status: 200, body: { ok: true } };\n * },\n * });\n * ```\n */\nexport const defineMailer = (options: MailerConfig): MailerHandler => ({\n __brand: \"effortless-mailer\",\n __spec: options,\n});\n","import type { LambdaWithPermissions, AnyParamRef, ResolveConfig } from \"./handler-options\";\nimport type { AnyDepHandler, ResolveDeps } from \"./handler-deps\";\nimport type { StaticFiles } from \"./shared\";\nimport type { HttpRequest, HttpResponse } from \"./shared\";\n\n/** GET route handler — no schema, no data */\nexport type ApiGetHandlerFn<\n C = undefined,\n D = undefined,\n P = undefined,\n S extends string[] | undefined = undefined\n> =\n (args: { req: HttpRequest }\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => Promise<HttpResponse> | HttpResponse;\n\n/** POST handler — with typed data from schema */\nexport type ApiPostHandlerFn<\n T = undefined,\n C = undefined,\n D = undefined,\n P = undefined,\n S extends string[] | undefined = undefined\n> =\n (args: { req: HttpRequest }\n & ([T] extends [undefined] ? {} : { data: T })\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => Promise<HttpResponse> | HttpResponse;\n\n/** Setup factory — receives deps/config/files when declared */\ntype SetupFactory<C, D, P, S extends string[] | undefined = undefined> =\n (args:\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P & {}> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => C | Promise<C>;\n\n/** Static config extracted by AST (no runtime callbacks) */\nexport type ApiConfig = LambdaWithPermissions & {\n /** Base path prefix for all routes (e.g., \"/api\") */\n basePath: string;\n};\n\n/**\n * Options for defining a CQRS-style API endpoint.\n *\n * - `get` routes handle queries (path-based routing, no body)\n * - `post` handles commands (single entry point, discriminated union via `schema`)\n */\nexport type DefineApiOptions<\n T = undefined,\n C = undefined,\n D extends Record<string, AnyDepHandler> | undefined = undefined,\n P extends Record<string, AnyParamRef> | undefined = undefined,\n S extends string[] | undefined = undefined\n> = LambdaWithPermissions & {\n /** Base path prefix for all routes (e.g., \"/api\") */\n basePath: string;\n /**\n * Factory function to initialize shared state.\n * Called once on cold start, result is cached and reused across invocations.\n */\n setup?: SetupFactory<C, D, P, S>;\n /** Dependencies on other handlers (tables, queues, etc.) */\n deps?: D;\n /** SSM Parameter Store parameters */\n config?: P;\n /** Static file glob patterns to bundle into the Lambda ZIP */\n static?: S;\n /** Error handler called when schema validation or handler throws */\n onError?: (error: unknown, req: HttpRequest) => HttpResponse;\n\n /** GET routes — query handlers keyed by relative path (e.g., \"/users/{id}\") */\n get?: Record<string, ApiGetHandlerFn<C, D, P, S>>;\n\n /**\n * Schema for POST body validation. Use with discriminated unions:\n * ```typescript\n * schema: Action.parse,\n * post: async ({ data }) => { switch (data.action) { ... } }\n * ```\n */\n schema?: (input: unknown) => T;\n /** POST handler — single entry point for commands */\n post?: ApiPostHandlerFn<T, C, D, P, S>;\n};\n\n/** Internal handler object created by defineApi */\nexport type ApiHandler<\n T = undefined,\n C = undefined,\n D = undefined,\n P = undefined,\n S extends string[] | undefined = undefined\n> = {\n readonly __brand: \"effortless-api\";\n readonly __spec: ApiConfig;\n readonly schema?: (input: unknown) => T;\n readonly onError?: (error: unknown, req: HttpRequest) => HttpResponse;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n readonly setup?: (...args: any[]) => C | Promise<C>;\n readonly deps?: D;\n readonly config?: P;\n readonly static?: string[];\n readonly get?: Record<string, ApiGetHandlerFn<C, D, P, S>>;\n readonly post?: ApiPostHandlerFn<T, C, D, P, S>;\n};\n\n/**\n * Define a CQRS-style API with typed GET routes and POST commands.\n *\n * GET routes handle queries — path-based routing, no request body.\n * POST handles commands — single entry point with discriminated union schema.\n * Deploys as a single Lambda (fat Lambda) with one API Gateway catch-all route.\n *\n * @example\n * ```typescript\n * export default defineApi({\n * basePath: \"/api\",\n * deps: { users },\n *\n * get: {\n * \"/users\": async ({ req, deps }) => ({\n * status: 200,\n * body: await deps.users.scan()\n * }),\n * \"/users/{id}\": async ({ req, deps }) => ({\n * status: 200,\n * body: await deps.users.get(req.params.id)\n * }),\n * },\n *\n * schema: Action.parse,\n * post: async ({ data, deps }) => {\n * switch (data.action) {\n * case \"create\": return { status: 201, body: await deps.users.put(data) }\n * case \"delete\": return { status: 200, body: await deps.users.delete(data.id) }\n * }\n * },\n * })\n * ```\n */\nexport const defineApi = <\n T = undefined,\n C = undefined,\n D extends Record<string, AnyDepHandler> | undefined = undefined,\n P extends Record<string, AnyParamRef> | undefined = undefined,\n S extends string[] | undefined = undefined\n>(\n options: DefineApiOptions<T, C, D, P, S>\n): ApiHandler<T, C, D, P, S> => {\n const { get, post, schema, onError, setup, deps, config, static: staticFiles, ...__spec } = options;\n return {\n __brand: \"effortless-api\",\n __spec,\n ...(get ? { get } : {}),\n ...(post ? { post } : {}),\n ...(schema ? { schema } : {}),\n ...(onError ? { onError } : {}),\n ...(setup ? { setup } : {}),\n ...(deps ? { deps } : {}),\n ...(config ? { config } : {}),\n ...(staticFiles ? { static: staticFiles } : {}),\n } as ApiHandler<T, C, D, P, S>;\n};\n","// Public helpers — this file must have ZERO heavy imports (no effect, no AWS SDK, no deploy code).\n// It is the source of truth for param(), typed(), and related types used by the public API.\n\n// ============ Permissions ============\n\ntype AwsService =\n | \"dynamodb\"\n | \"s3\"\n | \"sqs\"\n | \"sns\"\n | \"ses\"\n | \"ssm\"\n | \"lambda\"\n | \"events\"\n | \"secretsmanager\"\n | \"cognito-idp\"\n | \"logs\";\n\nexport type Permission = `${AwsService}:${string}` | (string & {});\n\n// ============ Lambda config ============\n\n/** Logging verbosity level for Lambda handlers */\nexport type LogLevel = \"error\" | \"info\" | \"debug\";\n\n/**\n * Common Lambda configuration shared by all handler types.\n */\nexport type LambdaConfig = {\n /** Lambda memory in MB (default: 256) */\n memory?: number;\n /** Lambda timeout in seconds (default: 30) */\n timeout?: number;\n /** Logging verbosity: \"error\" (errors only), \"info\" (+ execution summary), \"debug\" (+ input/output). Default: \"info\" */\n logLevel?: LogLevel;\n};\n\n/**\n * Lambda configuration with additional IAM permissions.\n * Used by handler types that support custom permissions (http, table, fifo-queue).\n */\nexport type LambdaWithPermissions = LambdaConfig & {\n /** Additional IAM permissions for the Lambda */\n permissions?: Permission[];\n};\n\n// ============ Params ============\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyParamRef = ParamRef<any> | string;\n\n/**\n * Reference to an SSM Parameter Store parameter.\n *\n * @typeParam T - The resolved type after optional transform (default: string)\n */\nexport type ParamRef<T = string> = {\n readonly __brand: \"effortless-param\";\n readonly key: string;\n readonly transform?: (raw: string) => T;\n};\n\n/**\n * Maps a config declaration to resolved value types.\n * Plain strings resolve to `string`, `ParamRef<T>` resolves to `T`.\n *\n * @typeParam P - Record of config keys to string or ParamRef instances\n */\nexport type ResolveConfig<P> = {\n [K in keyof P]: P[K] extends ParamRef<infer T> ? T : string;\n};\n\n/**\n * Declare an SSM Parameter Store parameter.\n *\n * The key is combined with project and stage at deploy time to form the full\n * SSM path: `/${project}/${stage}/${key}`.\n *\n * @param key - Parameter key (e.g., \"database-url\")\n * @param transform - Optional function to transform the raw string value\n * @returns A ParamRef used by the deployment and runtime systems\n *\n * @example Simple string parameter\n * ```typescript\n * config: {\n * dbUrl: param(\"database-url\"),\n * }\n * ```\n *\n * @example With transform (e.g., TOML parsing)\n * ```typescript\n * import TOML from \"smol-toml\";\n *\n * config: {\n * appConfig: param(\"app-config\", TOML.parse),\n * }\n * ```\n */\nexport function param(key: string): ParamRef<string>;\nexport function param<T>(key: string, transform: (raw: string) => T): ParamRef<T>;\nexport function param<T = string>(\n key: string,\n transform?: (raw: string) => T\n): ParamRef<T> {\n return {\n __brand: \"effortless-param\",\n key,\n ...(transform ? { transform } : {}),\n } as ParamRef<T>;\n}\n\n// ============ Single-table types ============\n\n/**\n * DynamoDB table key (always pk + sk strings in single-table design).\n */\nexport type TableKey = { pk: string; sk: string };\n\n/**\n * Full DynamoDB item in the single-table design.\n *\n * Every item has a fixed envelope: `pk`, `sk`, `tag`, `data`, and optional `ttl`.\n * `tag` is stored as a top-level DynamoDB attribute (GSI-ready).\n * If your domain type `T` includes a `tag` field, effortless auto-copies\n * `data.tag` to the top-level `tag` on `put()`, so you don't have to pass it twice.\n *\n * @typeParam T - The domain data type stored in the `data` attribute\n */\nexport type TableItem<T> = {\n pk: string;\n sk: string;\n tag: string;\n data: T;\n ttl?: number;\n};\n\n/**\n * Input type for `TableClient.put()`.\n *\n * Unlike `TableItem<T>`, this does NOT include `tag` — effortless auto-extracts\n * the top-level DynamoDB `tag` attribute from your data using the configured\n * tag field (defaults to `data.tag`, configurable via `defineTable({ tag: \"type\" })`).\n *\n * @typeParam T - The domain data type stored in the `data` attribute\n */\nexport type PutInput<T> = {\n pk: string;\n sk: string;\n data: T;\n ttl?: number;\n};\n\n// ============ Typed helper ============\n\n/**\n * Type-only schema helper for handlers.\n *\n * Use this instead of explicit generic parameters like `defineTable<Order>(...)`.\n * It enables TypeScript to infer all generic types from the options object,\n * avoiding the partial-inference problem where specifying one generic\n * forces all others to their defaults.\n *\n * At runtime this is a no-op identity function — it simply returns the input unchanged.\n * The type narrowing happens entirely at the TypeScript level.\n *\n * @example Resource-only table\n * ```typescript\n * type User = { id: string; email: string };\n *\n * export const users = defineTable({\n * schema: typed<User>(),\n * });\n * ```\n *\n * @example Table with stream handler\n * ```typescript\n * export const orders = defineTable({\n * schema: typed<Order>(),\n * setup: async () => ({ db: createClient() }),\n * onRecord: async ({ record, ctx }) => {\n * // record.new.data is Order, ctx is { db: Client } — all inferred\n * },\n * });\n * ```\n */\nexport function typed<T>(): (input: unknown) => T {\n return (input: unknown) => input as T;\n}\n"],"mappings":";AAuGO,IAAM,eAAe,CAAC,WAA+C;;;ACsJrE,IAAM,cAAc,CAQzB,YACmC;AACnC,QAAM,EAAE,UAAU,iBAAiB,SAAS,SAAS,QAAQ,OAAO,MAAM,QAAQ,QAAQ,aAAa,GAAG,OAAO,IAAI;AACrH,SAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,IACA,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,IACzB,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,IACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B,GAAI,cAAc,EAAE,QAAQ,YAAY,IAAI,CAAC;AAAA,IAC7C,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,IAC/B,GAAI,kBAAkB,EAAE,gBAAgB,IAAI,CAAC;AAAA,IAC7C,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC/B;AACF;;;AC9NO,IAAM,YAAY,CAAC,aAAoC;AAAA,EAC5D,SAAS;AAAA,EACT,QAAQ;AACV;;;AC6CO,IAAM,mBAAmB,CAAC,aAAkD;AAAA,EACjF,SAAS;AAAA,EACT,QAAQ;AACV;;;ACkFO,IAAM,kBAAkB,CAO7B,YACoC;AACpC,QAAM,EAAE,WAAW,SAAS,SAAS,QAAQ,OAAO,MAAM,QAAQ,QAAQ,aAAa,GAAG,OAAO,IAAI;AACrG,SAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,IACA,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,IACzB,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,IACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B,GAAI,cAAc,EAAE,QAAQ,YAAY,IAAI,CAAC;AAAA,IAC7C,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,IACjC,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC/B;AACF;;;ACvCO,IAAM,eAAe,CAM1B,YAC8B;AAC9B,QAAM,EAAE,iBAAiB,iBAAiB,SAAS,OAAO,MAAM,QAAQ,QAAQ,aAAa,GAAG,OAAO,IAAI;AAC3G,SAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,IACA,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,IACzB,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,IACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B,GAAI,cAAc,EAAE,QAAQ,YAAY,IAAI,CAAC;AAAA,IAC7C,GAAI,kBAAkB,EAAE,gBAAgB,IAAI,CAAC;AAAA,IAC7C,GAAI,kBAAkB,EAAE,gBAAgB,IAAI,CAAC;AAAA,EAC/C;AACF;;;AC/IO,IAAM,eAAe,CAAC,aAA0C;AAAA,EACrE,SAAS;AAAA,EACT,QAAQ;AACV;;;ACiGO,IAAM,YAAY,CAOvB,YAC8B;AAC9B,QAAM,EAAE,KAAK,MAAM,QAAQ,SAAS,OAAO,MAAM,QAAQ,QAAQ,aAAa,GAAG,OAAO,IAAI;AAC5F,SAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,IACA,GAAI,MAAM,EAAE,IAAI,IAAI,CAAC;AAAA,IACrB,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,IACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,IACzB,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,IACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B,GAAI,cAAc,EAAE,QAAQ,YAAY,IAAI,CAAC;AAAA,EAC/C;AACF;;;ACtEO,SAAS,MACd,KACA,WACa;AACb,SAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,IACA,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,EACnC;AACF;AA4EO,SAAS,QAAkC;AAChD,SAAO,CAAC,UAAmB;AAC7B;","names":[]}
1
+ {"version":3,"sources":["../src/config.ts","../src/handlers/define-table.ts","../src/handlers/define-app.ts","../src/handlers/define-static-site.ts","../src/handlers/define-fifo-queue.ts","../src/handlers/define-bucket.ts","../src/handlers/define-mailer.ts","../src/handlers/define-api.ts","../src/handlers/handler-options.ts","../src/handlers/shared.ts"],"sourcesContent":["/**\n * Configuration for an Effortless project.\n *\n * @example\n * ```typescript\n * // effortless.config.ts\n * import { defineConfig } from \"effortless-aws\";\n *\n * export default defineConfig({\n * name: \"my-service\",\n * region: \"eu-central-1\",\n * handlers: \"src\",\n * });\n * ```\n */\nexport type EffortlessConfig = {\n /**\n * Project root directory. All relative paths (handlers, server, assets, etc.)\n * are resolved from this directory.\n * Resolved relative to where the CLI runs (process.cwd()).\n * @default \".\" (current working directory)\n */\n root?: string;\n\n /**\n * Project name used for resource naming and tagging.\n * This becomes part of Lambda function names, IAM roles, etc.\n */\n name: string;\n\n /**\n * Default AWS region for all handlers.\n * Can be overridden per-handler or via CLI `--region` flag.\n * @default \"eu-central-1\"\n */\n region?: string;\n\n /**\n * Deployment stage (e.g., \"dev\", \"staging\", \"prod\").\n * Used for resource isolation and tagging.\n * @default \"dev\"\n */\n stage?: string;\n\n /**\n * Glob patterns or directory paths to scan for handlers.\n * Used by `eff deploy` (without file argument) to auto-discover handlers.\n *\n * @example\n * ```typescript\n * // Single directory - scans for all .ts files\n * handlers: \"src\"\n *\n * // Glob patterns\n * handlers: [\"src/**\\/*.ts\", \"lib/**\\/*.handler.ts\"]\n * ```\n */\n handlers?: string | string[];\n\n /**\n * Default settings applied to all handlers unless overridden.\n *\n * All Lambdas run on ARM64 (Graviton2) architecture — ~20% cheaper than x86_64\n * with better price-performance for most workloads.\n */\n defaults?: {\n /**\n * Lambda memory in MB. AWS allocates proportional CPU —\n * 1769 MB gives one full vCPU.\n * @default 256\n */\n memory?: number;\n\n /**\n * Lambda timeout as a human-readable string.\n * AWS maximum is 15 minutes.\n * @example \"30 seconds\", \"5 minutes\"\n */\n timeout?: string;\n\n /**\n * Node.js Lambda runtime version.\n * @default \"nodejs24.x\"\n */\n runtime?: string;\n };\n};\n\n/**\n * Helper function for type-safe configuration.\n * Returns the config object as-is, but provides TypeScript autocompletion.\n *\n * @example\n * ```typescript\n * import { defineConfig } from \"effortless-aws\";\n *\n * export default defineConfig({\n * name: \"my-service\",\n * region: \"eu-central-1\",\n * handlers: \"src\",\n * });\n * ```\n */\nexport const defineConfig = (config: EffortlessConfig): EffortlessConfig => config;\n","import type { LambdaWithPermissions, AnyParamRef, ResolveConfig, TableItem } from \"./handler-options\";\nimport type { TableClient } from \"../runtime/table-client\";\nimport type { AnyDepHandler, ResolveDeps } from \"./handler-deps\";\nimport type { StaticFiles } from \"./shared\";\n\n/** DynamoDB attribute types for keys */\nexport type KeyType = \"string\" | \"number\" | \"binary\";\n\n/**\n * DynamoDB table key definition\n */\nexport type TableKey = {\n /** Attribute name */\n name: string;\n /** Attribute type */\n type: KeyType;\n};\n\n/** DynamoDB Streams view type - determines what data is captured in stream records */\nexport type StreamView = \"NEW_AND_OLD_IMAGES\" | \"NEW_IMAGE\" | \"OLD_IMAGE\" | \"KEYS_ONLY\";\n\n/**\n * Configuration options for defineTable (single-table design).\n *\n * Tables always use `pk (S)` + `sk (S)` keys, `tag (S)` discriminator,\n * `data (M)` for domain fields, and `ttl (N)` for optional expiration.\n */\nexport type TableConfig = LambdaWithPermissions & {\n /** DynamoDB billing mode (default: \"PAY_PER_REQUEST\") */\n billingMode?: \"PAY_PER_REQUEST\" | \"PROVISIONED\";\n /** Stream view type - what data to include in stream records (default: \"NEW_AND_OLD_IMAGES\") */\n streamView?: StreamView;\n /** Number of records to process in each Lambda invocation (1-10000, default: 100) */\n batchSize?: number;\n /** Maximum time in seconds to gather records before invoking (0-300, default: 2) */\n batchWindow?: number;\n /** Where to start reading the stream (default: \"LATEST\") */\n startingPosition?: \"LATEST\" | \"TRIM_HORIZON\";\n /**\n * Name of the field in `data` that serves as the entity type discriminant.\n * Effortless auto-copies `data[tagField]` to the top-level DynamoDB `tag` attribute on `put()`.\n * Defaults to `\"tag\"`.\n *\n * @example\n * ```typescript\n * export const orders = defineTable({\n * tagField: \"type\",\n * schema: typed<{ type: \"order\"; amount: number }>(),\n * onRecord: async ({ record }) => { ... }\n * });\n * ```\n */\n tagField?: string;\n};\n\n/**\n * DynamoDB stream record passed to onRecord callback.\n *\n * `new` and `old` are full `TableItem<T>` objects with the single-table envelope.\n *\n * @typeParam T - Type of the domain data (inside `data`)\n */\nexport type TableRecord<T = Record<string, unknown>> = {\n /** Type of modification: INSERT, MODIFY, or REMOVE */\n eventName: \"INSERT\" | \"MODIFY\" | \"REMOVE\";\n /** New item value (present for INSERT and MODIFY) */\n new?: TableItem<T>;\n /** Old item value (present for MODIFY and REMOVE) */\n old?: TableItem<T>;\n /** Primary key of the affected item */\n keys: { pk: string; sk: string };\n /** Sequence number for ordering */\n sequenceNumber?: string;\n /** Approximate timestamp when the modification occurred */\n timestamp?: number;\n};\n\n/**\n * Information about a failed record during batch processing\n *\n * @typeParam T - Type of the domain data\n */\nexport type FailedRecord<T = Record<string, unknown>> = {\n /** The record that failed to process */\n record: TableRecord<T>;\n /** The error that occurred */\n error: unknown;\n};\n\n/**\n * Setup factory type for table handlers.\n * Always receives `table: TableClient<T>` (self-client for the handler's own table).\n * Also receives `deps` and/or `config` when declared.\n */\ntype SetupFactory<C, T, D, P, S extends string[] | undefined = undefined> = (args:\n & { table: TableClient<T> }\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P & {}> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => C | Promise<C>;\n\n/**\n * Callback function type for processing a single DynamoDB stream record\n */\nexport type TableRecordFn<T = Record<string, unknown>, C = undefined, R = void, D = undefined, P = undefined, S extends string[] | undefined = undefined> =\n (args: { record: TableRecord<T>; table: TableClient<T> }\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => Promise<R>;\n\n/**\n * Callback function type for processing accumulated batch results\n */\nexport type TableBatchCompleteFn<T = Record<string, unknown>, C = undefined, R = void, D = undefined, P = undefined, S extends string[] | undefined = undefined> =\n (args: { results: R[]; failures: FailedRecord<T>[]; table: TableClient<T> }\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => Promise<void>;\n\n/**\n * Callback function type for processing all records in a batch at once\n */\nexport type TableBatchFn<T = Record<string, unknown>, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> =\n (args: { records: TableRecord<T>[]; table: TableClient<T> }\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => Promise<void>;\n\n/** Base options shared by all defineTable variants */\ntype DefineTableBase<T = Record<string, unknown>, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = Omit<TableConfig, \"tagField\"> & {\n /** Name of the field in `data` that serves as the entity type discriminant (default: `\"tag\"`). */\n tagField?: Extract<keyof T, string>;\n /**\n * Decode/validate function for the `data` portion of stream record items.\n * Called with the unmarshalled `data` attribute; should return typed data or throw on validation failure.\n * When provided, T is inferred from the return type — no need to specify generic parameters.\n */\n schema?: (input: unknown) => T;\n /**\n * Error handler called when onRecord, onBatch, or onBatchComplete throws.\n * Receives the error. If not provided, defaults to `console.error`.\n */\n onError?: (error: unknown) => void;\n /**\n * Factory function to initialize shared state for callbacks.\n * Called once on cold start, result is cached and reused across invocations.\n * When deps/params are declared, receives them as argument.\n * Supports both sync and async return values.\n */\n setup?: SetupFactory<C, T, D, P, S>;\n /**\n * Dependencies on other handlers (tables, queues, etc.).\n * Typed clients are injected into the handler via the `deps` argument.\n */\n deps?: D;\n /**\n * SSM Parameter Store parameters.\n * Declare with `param()` helper. Values are fetched and cached at cold start.\n * Typed values are injected into the handler via the `config` argument.\n */\n config?: P;\n /**\n * Static file glob patterns to bundle into the Lambda ZIP.\n * Files are accessible at runtime via the `files` callback argument.\n */\n static?: S;\n};\n\n/** Per-record processing: onRecord with optional onBatchComplete */\ntype DefineTableWithOnRecord<T = Record<string, unknown>, C = undefined, R = void, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineTableBase<T, C, D, P, S> & {\n onRecord: TableRecordFn<T, C, R, D, P, S>;\n onBatchComplete?: TableBatchCompleteFn<T, C, R, D, P, S>;\n onBatch?: never;\n};\n\n/** Batch processing: onBatch processes all records at once */\ntype DefineTableWithOnBatch<T = Record<string, unknown>, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineTableBase<T, C, D, P, S> & {\n onBatch: TableBatchFn<T, C, D, P, S>;\n onRecord?: never;\n onBatchComplete?: never;\n};\n\n/** Resource-only: no handler, just creates the table */\ntype DefineTableResourceOnly<T = Record<string, unknown>, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineTableBase<T, C, D, P, S> & {\n onRecord?: never;\n onBatch?: never;\n onBatchComplete?: never;\n};\n\nexport type DefineTableOptions<\n T = Record<string, unknown>,\n C = undefined,\n R = void,\n D extends Record<string, AnyDepHandler> | undefined = undefined,\n P extends Record<string, AnyParamRef> | undefined = undefined,\n S extends string[] | undefined = undefined\n> =\n | DefineTableWithOnRecord<T, C, R, D, P, S>\n | DefineTableWithOnBatch<T, C, D, P, S>\n | DefineTableResourceOnly<T, C, D, P, S>;\n\n/**\n * Internal handler object created by defineTable\n * @internal\n */\nexport type TableHandler<T = Record<string, unknown>, C = undefined, R = void, D = undefined, P = undefined, S extends string[] | undefined = undefined> = {\n readonly __brand: \"effortless-table\";\n readonly __spec: TableConfig;\n readonly schema?: (input: unknown) => T;\n readonly onError?: (error: unknown) => void;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n readonly setup?: (...args: any[]) => C | Promise<C>;\n readonly deps?: D;\n readonly config?: P;\n readonly static?: string[];\n readonly onRecord?: TableRecordFn<T, C, R, D, P, S>;\n readonly onBatchComplete?: TableBatchCompleteFn<T, C, R, D, P, S>;\n readonly onBatch?: TableBatchFn<T, C, D, P, S>;\n};\n\n/**\n * Define a DynamoDB table with optional stream handler (single-table design).\n *\n * Creates a table with fixed key schema: `pk (S)` + `sk (S)`, plus `tag (S)`,\n * `data (M)`, and `ttl (N)` attributes. TTL is always enabled.\n *\n * @example Table with stream handler (typed)\n * ```typescript\n * type OrderData = { amount: number; status: string };\n *\n * export const orders = defineTable({\n * schema: typed<OrderData>(),\n * streamView: \"NEW_AND_OLD_IMAGES\",\n * batchSize: 10,\n * onRecord: async ({ record }) => {\n * if (record.eventName === \"INSERT\") {\n * console.log(\"New order:\", record.new?.data.amount);\n * }\n * }\n * });\n * ```\n *\n * @example Table only (no Lambda)\n * ```typescript\n * export const users = defineTable({});\n * ```\n */\nexport const defineTable = <\n T = Record<string, unknown>,\n C = undefined,\n R = void,\n D extends Record<string, AnyDepHandler> | undefined = undefined,\n P extends Record<string, AnyParamRef> | undefined = undefined,\n S extends string[] | undefined = undefined\n>(\n options: DefineTableOptions<T, C, R, D, P, S>\n): TableHandler<T, C, R, D, P, S> => {\n const { onRecord, onBatchComplete, onBatch, onError, schema, setup, deps, config, static: staticFiles, ...__spec } = options;\n return {\n __brand: \"effortless-table\",\n __spec,\n ...(schema ? { schema } : {}),\n ...(onError ? { onError } : {}),\n ...(setup ? { setup } : {}),\n ...(deps ? { deps } : {}),\n ...(config ? { config } : {}),\n ...(staticFiles ? { static: staticFiles } : {}),\n ...(onRecord ? { onRecord } : {}),\n ...(onBatchComplete ? { onBatchComplete } : {}),\n ...(onBatch ? { onBatch } : {})\n } as TableHandler<T, C, R, D, P, S>;\n};\n","import type { LambdaWithPermissions } from \"./handler-options\";\n\n/**\n * Configuration for deploying an SSR framework (Nuxt, Astro, etc.)\n * via CloudFront + S3 (static assets) + Lambda Function URL (server-side rendering).\n */\nexport type AppConfig = LambdaWithPermissions & {\n /** Directory containing the Lambda server handler (e.g., \".output/server\").\n * Must contain an `index.mjs` (or `index.js`) that exports a `handler` function. */\n server: string;\n /** Directory containing static assets for S3 (e.g., \".output/public\") */\n assets: string;\n /** Base URL path (default: \"/\") */\n path?: string;\n /** Shell command to build the framework output (e.g., \"nuxt build\") */\n build?: string;\n /** Custom domain name. String or stage-keyed record (e.g., { prod: \"app.example.com\" }). */\n domain?: string | Record<string, string>;\n /** CloudFront route overrides: path patterns forwarded to API Gateway instead of the SSR Lambda.\n * Keys are CloudFront path patterns (e.g., \"/api/*\"), values are handler references.\n * Example: `routes: { \"/api/*\": api }` */\n routes?: Record<string, { readonly __brand: string }>;\n};\n\n/**\n * Internal handler object created by defineApp\n * @internal\n */\nexport type AppHandler = {\n readonly __brand: \"effortless-app\";\n readonly __spec: AppConfig;\n};\n\n/**\n * Deploy an SSR framework application via CloudFront + Lambda Function URL.\n *\n * Static assets from the `assets` directory are served via S3 + CloudFront CDN.\n * Server-rendered pages are handled by a Lambda function using the framework's\n * built output from the `server` directory.\n *\n * For static-only sites (no SSR), use {@link defineStaticSite} instead.\n *\n * @param options - App configuration: server directory, assets directory, optional build command\n * @returns Handler object used by the deployment system\n *\n * @example Nuxt SSR\n * ```typescript\n * export const app = defineApp({\n * build: \"nuxt build\",\n * server: \".output/server\",\n * assets: \".output/public\",\n * memory: 1024,\n * });\n * ```\n */\nexport const defineApp = (options: AppConfig): AppHandler => ({\n __brand: \"effortless-app\",\n __spec: options,\n});\n","/** Any branded handler that deploys to API Gateway (HttpHandler, ApiHandler, etc.) */\ntype AnyRoutableHandler = { readonly __brand: string };\n\n/** Simplified request object passed to middleware */\nexport type MiddlewareRequest = {\n uri: string;\n method: string;\n querystring: string;\n headers: Record<string, string>;\n cookies: Record<string, string>;\n};\n\n/** Redirect the user to another URL */\nexport type MiddlewareRedirect = {\n redirect: string;\n status?: 301 | 302 | 307 | 308;\n};\n\n/** Deny access with a 403 status */\nexport type MiddlewareDeny = {\n status: 403;\n body?: string;\n};\n\n/** Middleware return type: redirect, deny, or void (continue serving) */\nexport type MiddlewareResult = MiddlewareRedirect | MiddlewareDeny | void;\n\n/** Function that runs before serving static files via Lambda@Edge */\nexport type MiddlewareHandler = (\n request: MiddlewareRequest\n) => Promise<MiddlewareResult> | MiddlewareResult;\n\n/** SEO options for auto-generating sitemap.xml, robots.txt, and submitting to Google Indexing API */\nexport type StaticSiteSeo = {\n /** Sitemap filename (e.g. \"sitemap.xml\", \"sitemap-v2.xml\") */\n sitemap: string;\n /** Path to Google service account JSON key file for Indexing API batch submission.\n * Requires adding the service account email as an owner in Google Search Console. */\n googleIndexing?: string;\n};\n\n/**\n * Configuration for a static site handler (S3 + CloudFront)\n */\nexport type StaticSiteConfig = {\n /** Handler name. Defaults to export name if not specified */\n name?: string;\n /** Directory containing the static site files, relative to project root */\n dir: string;\n /** Default file for directory requests (default: \"index.html\") */\n index?: string;\n /** SPA mode: serve index.html for all paths that don't match a file (default: false) */\n spa?: boolean;\n /** Shell command to run before deploy to generate site content (e.g., \"npx astro build\") */\n build?: string;\n /** Custom domain name. Accepts a string (same domain for all stages) or a Record mapping stage names to domains (e.g., `{ prod: \"example.com\", dev: \"dev.example.com\" }`). Requires an ACM certificate in us-east-1. If the cert also covers www, a 301 redirect from www to non-www is set up automatically. */\n domain?: string | Record<string, string>;\n /** CloudFront route overrides: path patterns forwarded to API Gateway instead of S3.\n * Keys are CloudFront path patterns (e.g., \"/api/*\"), values are HTTP handlers.\n * Example: `routes: { \"/api/*\": api }` */\n routes?: Record<string, AnyRoutableHandler>;\n /** Custom 404 error page path relative to `dir` (e.g. \"404.html\").\n * For non-SPA sites only. If not set, a default page is generated automatically. */\n errorPage?: string;\n /** Lambda@Edge middleware that runs before serving pages. Use for auth checks, redirects, etc. */\n middleware?: MiddlewareHandler;\n /** SEO: auto-generate sitemap.xml and robots.txt at deploy time, optionally submit URLs to Google Indexing API */\n seo?: StaticSiteSeo;\n};\n\n/**\n * Internal handler object created by defineStaticSite\n * @internal\n */\nexport type StaticSiteHandler = {\n readonly __brand: \"effortless-static-site\";\n readonly __spec: StaticSiteConfig;\n};\n\n/**\n * Deploy a static site via S3 + CloudFront CDN.\n *\n * @param options - Static site configuration: directory, optional SPA mode, build command\n * @returns Handler object used by the deployment system\n *\n * @example Documentation site\n * ```typescript\n * export const docs = defineStaticSite({\n * dir: \"dist\",\n * build: \"npx astro build\",\n * });\n * ```\n *\n * @example SPA with client-side routing\n * ```typescript\n * export const app = defineStaticSite({\n * dir: \"dist\",\n * spa: true,\n * build: \"npm run build\",\n * });\n * ```\n *\n * @example Protected site with middleware (Lambda@Edge)\n * ```typescript\n * export const admin = defineStaticSite({\n * dir: \"admin/dist\",\n * middleware: async (request) => {\n * if (!request.cookies.session) {\n * return { redirect: \"/login\" };\n * }\n * },\n * });\n * ```\n */\nexport const defineStaticSite = (options: StaticSiteConfig): StaticSiteHandler => ({\n __brand: \"effortless-static-site\",\n __spec: options,\n});\n","import type { LambdaWithPermissions, AnyParamRef, ResolveConfig } from \"./handler-options\";\nimport type { AnyDepHandler, ResolveDeps } from \"./handler-deps\";\nimport type { StaticFiles } from \"./shared\";\n\n/**\n * Parsed SQS FIFO message passed to the handler callbacks.\n *\n * @typeParam T - Type of the decoded message body (from schema function)\n */\nexport type FifoQueueMessage<T = unknown> = {\n /** Unique message identifier */\n messageId: string;\n /** Receipt handle for acknowledgement */\n receiptHandle: string;\n /** Parsed message body (JSON-decoded, then optionally schema-validated) */\n body: T;\n /** Raw unparsed message body string */\n rawBody: string;\n /** Message group ID (FIFO ordering key) */\n messageGroupId: string;\n /** Message deduplication ID */\n messageDeduplicationId?: string;\n /** SQS message attributes */\n messageAttributes: Record<string, { dataType?: string; stringValue?: string }>;\n /** Approximate first receive timestamp */\n approximateFirstReceiveTimestamp?: string;\n /** Approximate receive count */\n approximateReceiveCount?: string;\n /** Sent timestamp */\n sentTimestamp?: string;\n};\n\n/**\n * Configuration options for a FIFO queue handler\n */\nexport type FifoQueueConfig = LambdaWithPermissions & {\n /** Number of messages per Lambda invocation (1-10 for FIFO, default: 10) */\n batchSize?: number;\n /** Maximum time in seconds to gather messages before invoking (0-300, default: 0) */\n batchWindow?: number;\n /** Visibility timeout in seconds (default: max of timeout or 30) */\n visibilityTimeout?: number;\n /** Message retention period in seconds (60-1209600, default: 345600 = 4 days) */\n retentionPeriod?: number;\n /** Enable content-based deduplication (default: true) */\n contentBasedDeduplication?: boolean;\n};\n\n/**\n * Setup factory type — always receives an args object.\n * Args include `deps` and/or `config` when declared (empty `{}` otherwise).\n */\ntype SetupFactory<C, D, P, S extends string[] | undefined = undefined> =\n (args:\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P & {}> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => C | Promise<C>;\n\n/**\n * Per-message handler function.\n * Called once per message in the batch. Failures are reported individually.\n */\nexport type FifoQueueMessageFn<T = unknown, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> =\n (args: { message: FifoQueueMessage<T> }\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => Promise<void>;\n\n/**\n * Batch handler function.\n * Called once with all messages in the batch.\n */\nexport type FifoQueueBatchFn<T = unknown, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> =\n (args: { messages: FifoQueueMessage<T>[] }\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => Promise<void>;\n\n/** Base options shared by all defineFifoQueue variants */\ntype DefineFifoQueueBase<T = unknown, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = FifoQueueConfig & {\n /**\n * Decode/validate function for the message body.\n * Called with the JSON-parsed body; should return typed data or throw on validation failure.\n */\n schema?: (input: unknown) => T;\n /**\n * Error handler called when onMessage or onBatch throws.\n * If not provided, defaults to `console.error`.\n */\n onError?: (error: unknown) => void;\n /**\n * Factory function to initialize shared state for the handler.\n * Called once on cold start, result is cached and reused across invocations.\n * When deps/params are declared, receives them as argument.\n */\n setup?: SetupFactory<C, D, P, S>;\n /**\n * Dependencies on other handlers (tables, queues, etc.).\n * Typed clients are injected into the handler via the `deps` argument.\n */\n deps?: D;\n /**\n * SSM Parameter Store parameters.\n * Declare with `param()` helper. Values are fetched and cached at cold start.\n */\n config?: P;\n /**\n * Static file glob patterns to bundle into the Lambda ZIP.\n * Files are accessible at runtime via the `files` callback argument.\n */\n static?: S;\n};\n\n/** Per-message processing */\ntype DefineFifoQueueWithOnMessage<T = unknown, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineFifoQueueBase<T, C, D, P, S> & {\n onMessage: FifoQueueMessageFn<T, C, D, P, S>;\n onBatch?: never;\n};\n\n/** Batch processing: all messages at once */\ntype DefineFifoQueueWithOnBatch<T = unknown, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineFifoQueueBase<T, C, D, P, S> & {\n onBatch: FifoQueueBatchFn<T, C, D, P, S>;\n onMessage?: never;\n};\n\nexport type DefineFifoQueueOptions<\n T = unknown,\n C = undefined,\n D extends Record<string, AnyDepHandler> | undefined = undefined,\n P extends Record<string, AnyParamRef> | undefined = undefined,\n S extends string[] | undefined = undefined\n> =\n | DefineFifoQueueWithOnMessage<T, C, D, P, S>\n | DefineFifoQueueWithOnBatch<T, C, D, P, S>;\n\n/**\n * Internal handler object created by defineFifoQueue\n * @internal\n */\nexport type FifoQueueHandler<T = unknown, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = {\n readonly __brand: \"effortless-fifo-queue\";\n readonly __spec: FifoQueueConfig;\n readonly schema?: (input: unknown) => T;\n readonly onError?: (error: unknown) => void;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n readonly setup?: (...args: any[]) => C | Promise<C>;\n readonly deps?: D;\n readonly config?: P;\n readonly static?: string[];\n readonly onMessage?: FifoQueueMessageFn<T, C, D, P, S>;\n readonly onBatch?: FifoQueueBatchFn<T, C, D, P, S>;\n};\n\n/**\n * Define a FIFO SQS queue with a Lambda message handler\n *\n * Creates:\n * - SQS FIFO queue (with `.fifo` suffix)\n * - Lambda function triggered by the queue\n * - Event source mapping with partial batch failure support\n *\n * @example Per-message processing\n * ```typescript\n * type OrderEvent = { orderId: string; action: string };\n *\n * export const orderQueue = defineFifoQueue<OrderEvent>({\n * onMessage: async ({ message }) => {\n * console.log(\"Processing order:\", message.body.orderId);\n * }\n * });\n * ```\n *\n * @example Batch processing with schema\n * ```typescript\n * export const notifications = defineFifoQueue({\n * schema: (input) => NotificationSchema.parse(input),\n * batchSize: 5,\n * onBatch: async ({ messages }) => {\n * await sendAll(messages.map(m => m.body));\n * }\n * });\n * ```\n */\nexport const defineFifoQueue = <\n T = unknown,\n C = undefined,\n D extends Record<string, AnyDepHandler> | undefined = undefined,\n P extends Record<string, AnyParamRef> | undefined = undefined,\n S extends string[] | undefined = undefined\n>(\n options: DefineFifoQueueOptions<T, C, D, P, S>\n): FifoQueueHandler<T, C, D, P, S> => {\n const { onMessage, onBatch, onError, schema, setup, deps, config, static: staticFiles, ...__spec } = options;\n return {\n __brand: \"effortless-fifo-queue\",\n __spec,\n ...(schema ? { schema } : {}),\n ...(onError ? { onError } : {}),\n ...(setup ? { setup } : {}),\n ...(deps ? { deps } : {}),\n ...(config ? { config } : {}),\n ...(staticFiles ? { static: staticFiles } : {}),\n ...(onMessage ? { onMessage } : {}),\n ...(onBatch ? { onBatch } : {})\n } as FifoQueueHandler<T, C, D, P, S>;\n};\n","import type { LambdaWithPermissions, AnyParamRef, ResolveConfig } from \"./handler-options\";\nimport type { AnyDepHandler, ResolveDeps } from \"./handler-deps\";\nimport type { StaticFiles } from \"./shared\";\nimport type { BucketClient } from \"../runtime/bucket-client\";\n\n/**\n * Configuration options for defineBucket.\n */\nexport type BucketConfig = LambdaWithPermissions & {\n /** S3 key prefix filter for event notifications (e.g., \"uploads/\") */\n prefix?: string;\n /** S3 key suffix filter for event notifications (e.g., \".jpg\") */\n suffix?: string;\n};\n\n/**\n * S3 event record passed to onObjectCreated/onObjectRemoved callbacks.\n */\nexport type BucketEvent = {\n /** S3 event name (e.g., \"ObjectCreated:Put\", \"ObjectRemoved:Delete\") */\n eventName: string;\n /** Object key (path within the bucket) */\n key: string;\n /** Object size in bytes (present for created events) */\n size?: number;\n /** Object ETag (present for created events) */\n eTag?: string;\n /** ISO 8601 timestamp of when the event occurred */\n eventTime?: string;\n /** S3 bucket name */\n bucketName: string;\n};\n\n/**\n * Callback function type for S3 ObjectCreated events\n */\nexport type BucketObjectCreatedFn<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> =\n (args: { event: BucketEvent; bucket: BucketClient }\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => Promise<void>;\n\n/**\n * Callback function type for S3 ObjectRemoved events\n */\nexport type BucketObjectRemovedFn<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> =\n (args: { event: BucketEvent; bucket: BucketClient }\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => Promise<void>;\n\n/**\n * Setup factory type for bucket handlers.\n * Always receives `bucket: BucketClient` (self-client for the handler's own bucket).\n * Also receives `deps` and/or `config` when declared.\n */\ntype SetupFactory<C, D, P, S extends string[] | undefined = undefined> = (args:\n & { bucket: BucketClient }\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P & {}> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => C | Promise<C>;\n\n/** Base options shared by all defineBucket variants */\ntype DefineBucketBase<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = BucketConfig & {\n /**\n * Error handler called when onObjectCreated or onObjectRemoved throws.\n * If not provided, defaults to `console.error`.\n */\n onError?: (error: unknown) => void;\n /**\n * Factory function to initialize shared state for callbacks.\n * Called once on cold start, result is cached and reused across invocations.\n * Always receives `bucket: BucketClient` (self-client). When deps/config\n * are declared, receives them as well.\n */\n setup?: SetupFactory<C, D, P, S>;\n /**\n * Dependencies on other handlers (tables, buckets, etc.).\n * Typed clients are injected into the handler via the `deps` argument.\n */\n deps?: D;\n /**\n * SSM Parameter Store parameters.\n * Declare with `param()` helper. Values are fetched and cached at cold start.\n */\n config?: P;\n /**\n * Static file glob patterns to bundle into the Lambda ZIP.\n * Files are accessible at runtime via the `files` callback argument.\n */\n static?: S;\n};\n\n/** With event handlers (at least one callback) */\ntype DefineBucketWithHandlers<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineBucketBase<C, D, P, S> & {\n onObjectCreated?: BucketObjectCreatedFn<C, D, P, S>;\n onObjectRemoved?: BucketObjectRemovedFn<C, D, P, S>;\n};\n\n/** Resource-only: no Lambda, just creates the bucket */\ntype DefineBucketResourceOnly<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineBucketBase<C, D, P, S> & {\n onObjectCreated?: never;\n onObjectRemoved?: never;\n};\n\nexport type DefineBucketOptions<\n C = undefined,\n D extends Record<string, AnyDepHandler> | undefined = undefined,\n P extends Record<string, AnyParamRef> | undefined = undefined,\n S extends string[] | undefined = undefined\n> =\n | DefineBucketWithHandlers<C, D, P, S>\n | DefineBucketResourceOnly<C, D, P, S>;\n\n/**\n * Internal handler object created by defineBucket\n * @internal\n */\nexport type BucketHandler<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = {\n readonly __brand: \"effortless-bucket\";\n readonly __spec: BucketConfig;\n readonly onError?: (error: unknown) => void;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n readonly setup?: (...args: any[]) => C | Promise<C>;\n readonly deps?: D;\n readonly config?: P;\n readonly static?: string[];\n readonly onObjectCreated?: BucketObjectCreatedFn<C, D, P, S>;\n readonly onObjectRemoved?: BucketObjectRemovedFn<C, D, P, S>;\n};\n\n/**\n * Define an S3 bucket with optional event handlers.\n *\n * Creates an S3 bucket. When event handlers are provided, also creates a Lambda\n * function triggered by S3 event notifications.\n *\n * @example Bucket with event handler\n * ```typescript\n * export const uploads = defineBucket({\n * prefix: \"images/\",\n * suffix: \".jpg\",\n * onObjectCreated: async ({ event, bucket }) => {\n * const file = await bucket.get(event.key);\n * console.log(\"New upload:\", event.key, file?.body.length);\n * }\n * });\n * ```\n *\n * @example Resource-only bucket (no Lambda)\n * ```typescript\n * export const assets = defineBucket({});\n * ```\n *\n * @example As a dependency\n * ```typescript\n * export const api = defineApi({\n * basePath: \"/process\",\n * deps: { uploads },\n * post: async ({ req, deps }) => {\n * await deps.uploads.put(\"output.jpg\", buffer);\n * return { status: 200, body: \"OK\" };\n * },\n * });\n * ```\n */\nexport const defineBucket = <\n C = undefined,\n D extends Record<string, AnyDepHandler> | undefined = undefined,\n P extends Record<string, AnyParamRef> | undefined = undefined,\n S extends string[] | undefined = undefined\n>(\n options: DefineBucketOptions<C, D, P, S>\n): BucketHandler<C, D, P, S> => {\n const { onObjectCreated, onObjectRemoved, onError, setup, deps, config, static: staticFiles, ...__spec } = options;\n return {\n __brand: \"effortless-bucket\",\n __spec,\n ...(onError ? { onError } : {}),\n ...(setup ? { setup } : {}),\n ...(deps ? { deps } : {}),\n ...(config ? { config } : {}),\n ...(staticFiles ? { static: staticFiles } : {}),\n ...(onObjectCreated ? { onObjectCreated } : {}),\n ...(onObjectRemoved ? { onObjectRemoved } : {}),\n } as BucketHandler<C, D, P, S>;\n};\n","/**\n * Configuration options for defining a mailer (SES email identity)\n */\nexport type MailerConfig = {\n /** Domain to verify and send emails from (e.g., \"myapp.com\") */\n domain: string;\n};\n\n/**\n * Internal handler object created by defineMailer\n * @internal\n */\nexport type MailerHandler = {\n readonly __brand: \"effortless-mailer\";\n readonly __spec: MailerConfig;\n};\n\n/**\n * Define an email sender backed by Amazon SES.\n *\n * Creates an SES Email Identity for the specified domain and provides\n * a typed `EmailClient` to other handlers via `deps`.\n *\n * On first deploy, DKIM DNS records are printed to the console.\n * Add them to your DNS provider to verify the domain.\n *\n * @param options - Mailer configuration with the domain to send from\n * @returns Handler object used by the deployment system and as a `deps` value\n *\n * @example Basic mailer with HTTP handler\n * ```typescript\n * export const mailer = defineMailer({ domain: \"myapp.com\" });\n *\n * export const api = defineApi({\n * basePath: \"/signup\",\n * deps: { mailer },\n * post: async ({ req, deps }) => {\n * await deps.mailer.send({\n * from: \"hello@myapp.com\",\n * to: req.body.email,\n * subject: \"Welcome!\",\n * html: \"<h1>Hi!</h1>\",\n * });\n * return { status: 200, body: { ok: true } };\n * },\n * });\n * ```\n */\nexport const defineMailer = (options: MailerConfig): MailerHandler => ({\n __brand: \"effortless-mailer\",\n __spec: options,\n});\n","import type { LambdaWithPermissions, AnyParamRef, ResolveConfig } from \"./handler-options\";\nimport type { AnyDepHandler, ResolveDeps } from \"./handler-deps\";\nimport type { StaticFiles, ResponseStream } from \"./shared\";\nimport type { HttpRequest, HttpResponse } from \"./shared\";\n\n/** GET route handler — no schema, no data */\nexport type ApiGetHandlerFn<\n C = undefined,\n D = undefined,\n P = undefined,\n S extends string[] | undefined = undefined,\n ST extends boolean | undefined = undefined\n> =\n (args: { req: HttpRequest }\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n & ([ST] extends [true] ? { stream: ResponseStream } : {})\n ) => Promise<HttpResponse | void> | HttpResponse | void;\n\n/** POST handler — with typed data from schema */\nexport type ApiPostHandlerFn<\n T = undefined,\n C = undefined,\n D = undefined,\n P = undefined,\n S extends string[] | undefined = undefined,\n ST extends boolean | undefined = undefined\n> =\n (args: { req: HttpRequest }\n & ([T] extends [undefined] ? {} : { data: T })\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n & ([ST] extends [true] ? { stream: ResponseStream } : {})\n ) => Promise<HttpResponse | void> | HttpResponse | void;\n\n/** Setup factory — receives deps/config/files when declared */\ntype SetupFactory<C, D, P, S extends string[] | undefined = undefined> =\n (args:\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { config: ResolveConfig<P & {}> })\n & ([S] extends [undefined] ? {} : { files: StaticFiles })\n ) => C | Promise<C>;\n\n/** Static config extracted by AST (no runtime callbacks) */\nexport type ApiConfig = LambdaWithPermissions & {\n /** Base path prefix for all routes (e.g., \"/api\") */\n basePath: string;\n /** Enable response streaming. When true, the Lambda Function URL uses RESPONSE_STREAM invoke mode. */\n stream?: boolean;\n};\n\n/**\n * Options for defining a CQRS-style API endpoint.\n *\n * - `get` routes handle queries (path-based routing, no body)\n * - `post` handles commands (single entry point, discriminated union via `schema`)\n */\nexport type DefineApiOptions<\n T = undefined,\n C = undefined,\n D extends Record<string, AnyDepHandler> | undefined = undefined,\n P extends Record<string, AnyParamRef> | undefined = undefined,\n S extends string[] | undefined = undefined,\n ST extends boolean | undefined = undefined\n> = LambdaWithPermissions & {\n /** Base path prefix for all routes (e.g., \"/api\") */\n basePath: string;\n /** Enable response streaming. When true, routes receive a `stream` arg for SSE. Routes can still return HttpResponse normally. */\n stream?: ST;\n /**\n * Factory function to initialize shared state.\n * Called once on cold start, result is cached and reused across invocations.\n */\n setup?: SetupFactory<C, D, P, S>;\n /** Dependencies on other handlers (tables, queues, etc.) */\n deps?: D;\n /** SSM Parameter Store parameters */\n config?: P;\n /** Static file glob patterns to bundle into the Lambda ZIP */\n static?: S;\n /** Error handler called when schema validation or handler throws */\n onError?: (error: unknown, req: HttpRequest) => HttpResponse;\n\n /** GET routes — query handlers keyed by relative path (e.g., \"/users/{id}\") */\n get?: Record<string, ApiGetHandlerFn<C, D, P, S, ST>>;\n\n /**\n * Schema for POST body validation. Use with discriminated unions:\n * ```typescript\n * schema: Action.parse,\n * post: async ({ data }) => { switch (data.action) { ... } }\n * ```\n */\n schema?: (input: unknown) => T;\n /** POST handler — single entry point for commands */\n post?: ApiPostHandlerFn<T, C, D, P, S, ST>;\n};\n\n/** Internal handler object created by defineApi */\nexport type ApiHandler<\n T = undefined,\n C = undefined,\n D = undefined,\n P = undefined,\n S extends string[] | undefined = undefined,\n ST extends boolean | undefined = undefined\n> = {\n readonly __brand: \"effortless-api\";\n readonly __spec: ApiConfig;\n readonly schema?: (input: unknown) => T;\n readonly onError?: (error: unknown, req: HttpRequest) => HttpResponse;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n readonly setup?: (...args: any[]) => C | Promise<C>;\n readonly deps?: D;\n readonly config?: P;\n readonly static?: string[];\n readonly get?: Record<string, ApiGetHandlerFn<C, D, P, S, ST>>;\n readonly post?: ApiPostHandlerFn<T, C, D, P, S, ST>;\n};\n\n/**\n * Define a CQRS-style API with typed GET routes and POST commands.\n *\n * GET routes handle queries — path-based routing, no request body.\n * POST handles commands — single entry point with discriminated union schema.\n * Deploys as a single Lambda (fat Lambda) with one API Gateway catch-all route.\n *\n * @example\n * ```typescript\n * export default defineApi({\n * basePath: \"/api\",\n * deps: { users },\n *\n * get: {\n * \"/users\": async ({ req, deps }) => ({\n * status: 200,\n * body: await deps.users.scan()\n * }),\n * \"/users/{id}\": async ({ req, deps }) => ({\n * status: 200,\n * body: await deps.users.get(req.params.id)\n * }),\n * },\n *\n * schema: Action.parse,\n * post: async ({ data, deps }) => {\n * switch (data.action) {\n * case \"create\": return { status: 201, body: await deps.users.put(data) }\n * case \"delete\": return { status: 200, body: await deps.users.delete(data.id) }\n * }\n * },\n * })\n * ```\n */\nexport const defineApi = <\n T = undefined,\n C = undefined,\n D extends Record<string, AnyDepHandler> | undefined = undefined,\n P extends Record<string, AnyParamRef> | undefined = undefined,\n S extends string[] | undefined = undefined,\n ST extends boolean | undefined = undefined\n>(\n options: DefineApiOptions<T, C, D, P, S, ST>\n): ApiHandler<T, C, D, P, S, ST> => {\n const { get, post, schema, onError, setup, deps, config, static: staticFiles, ...__spec } = options;\n return {\n __brand: \"effortless-api\",\n __spec,\n ...(get ? { get } : {}),\n ...(post ? { post } : {}),\n ...(schema ? { schema } : {}),\n ...(onError ? { onError } : {}),\n ...(setup ? { setup } : {}),\n ...(deps ? { deps } : {}),\n ...(config ? { config } : {}),\n ...(staticFiles ? { static: staticFiles } : {}),\n } as ApiHandler<T, C, D, P, S, ST>;\n};\n","// Public helpers — this file must have ZERO heavy imports (no effect, no AWS SDK, no deploy code).\n// It is the source of truth for param(), typed(), and related types used by the public API.\n\n// ============ Permissions ============\n\ntype AwsService =\n | \"dynamodb\"\n | \"s3\"\n | \"sqs\"\n | \"sns\"\n | \"ses\"\n | \"ssm\"\n | \"lambda\"\n | \"events\"\n | \"secretsmanager\"\n | \"cognito-idp\"\n | \"logs\";\n\nexport type Permission = `${AwsService}:${string}` | (string & {});\n\n// ============ Lambda config ============\n\n/** Logging verbosity level for Lambda handlers */\nexport type LogLevel = \"error\" | \"info\" | \"debug\";\n\n/**\n * Common Lambda configuration shared by all handler types.\n */\nexport type LambdaConfig = {\n /** Lambda memory in MB (default: 256) */\n memory?: number;\n /** Lambda timeout in seconds (default: 30) */\n timeout?: number;\n /** Logging verbosity: \"error\" (errors only), \"info\" (+ execution summary), \"debug\" (+ input/output). Default: \"info\" */\n logLevel?: LogLevel;\n};\n\n/**\n * Lambda configuration with additional IAM permissions.\n * Used by handler types that support custom permissions (http, table, fifo-queue).\n */\nexport type LambdaWithPermissions = LambdaConfig & {\n /** Additional IAM permissions for the Lambda */\n permissions?: Permission[];\n};\n\n// ============ Params ============\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyParamRef = ParamRef<any> | string;\n\n/**\n * Reference to an SSM Parameter Store parameter.\n *\n * @typeParam T - The resolved type after optional transform (default: string)\n */\nexport type ParamRef<T = string> = {\n readonly __brand: \"effortless-param\";\n readonly key: string;\n readonly transform?: (raw: string) => T;\n};\n\n/**\n * Maps a config declaration to resolved value types.\n * Plain strings resolve to `string`, `ParamRef<T>` resolves to `T`.\n *\n * @typeParam P - Record of config keys to string or ParamRef instances\n */\nexport type ResolveConfig<P> = {\n [K in keyof P]: P[K] extends ParamRef<infer T> ? T : string;\n};\n\n/**\n * Declare an SSM Parameter Store parameter.\n *\n * The key is combined with project and stage at deploy time to form the full\n * SSM path: `/${project}/${stage}/${key}`.\n *\n * @param key - Parameter key (e.g., \"database-url\")\n * @param transform - Optional function to transform the raw string value\n * @returns A ParamRef used by the deployment and runtime systems\n *\n * @example Simple string parameter\n * ```typescript\n * config: {\n * dbUrl: param(\"database-url\"),\n * }\n * ```\n *\n * @example With transform (e.g., TOML parsing)\n * ```typescript\n * import TOML from \"smol-toml\";\n *\n * config: {\n * appConfig: param(\"app-config\", TOML.parse),\n * }\n * ```\n */\nexport function param(key: string): ParamRef<string>;\nexport function param<T>(key: string, transform: (raw: string) => T): ParamRef<T>;\nexport function param<T = string>(\n key: string,\n transform?: (raw: string) => T\n): ParamRef<T> {\n return {\n __brand: \"effortless-param\",\n key,\n ...(transform ? { transform } : {}),\n } as ParamRef<T>;\n}\n\n// ============ Single-table types ============\n\n/**\n * DynamoDB table key (always pk + sk strings in single-table design).\n */\nexport type TableKey = { pk: string; sk: string };\n\n/**\n * Full DynamoDB item in the single-table design.\n *\n * Every item has a fixed envelope: `pk`, `sk`, `tag`, `data`, and optional `ttl`.\n * `tag` is stored as a top-level DynamoDB attribute (GSI-ready).\n * If your domain type `T` includes a `tag` field, effortless auto-copies\n * `data.tag` to the top-level `tag` on `put()`, so you don't have to pass it twice.\n *\n * @typeParam T - The domain data type stored in the `data` attribute\n */\nexport type TableItem<T> = {\n pk: string;\n sk: string;\n tag: string;\n data: T;\n ttl?: number;\n};\n\n/**\n * Input type for `TableClient.put()`.\n *\n * Unlike `TableItem<T>`, this does NOT include `tag` — effortless auto-extracts\n * the top-level DynamoDB `tag` attribute from your data using the configured\n * tag field (defaults to `data.tag`, configurable via `defineTable({ tag: \"type\" })`).\n *\n * @typeParam T - The domain data type stored in the `data` attribute\n */\nexport type PutInput<T> = {\n pk: string;\n sk: string;\n data: T;\n ttl?: number;\n};\n\n// ============ Typed helper ============\n\n/**\n * Type-only schema helper for handlers.\n *\n * Use this instead of explicit generic parameters like `defineTable<Order>(...)`.\n * It enables TypeScript to infer all generic types from the options object,\n * avoiding the partial-inference problem where specifying one generic\n * forces all others to their defaults.\n *\n * At runtime this is a no-op identity function — it simply returns the input unchanged.\n * The type narrowing happens entirely at the TypeScript level.\n *\n * @example Resource-only table\n * ```typescript\n * type User = { id: string; email: string };\n *\n * export const users = defineTable({\n * schema: typed<User>(),\n * });\n * ```\n *\n * @example Table with stream handler\n * ```typescript\n * export const orders = defineTable({\n * schema: typed<Order>(),\n * setup: async () => ({ db: createClient() }),\n * onRecord: async ({ record, ctx }) => {\n * // record.new.data is Order, ctx is { db: Client } — all inferred\n * },\n * });\n * ```\n */\nexport function typed<T>(): (input: unknown) => T {\n return (input: unknown) => input as T;\n}\n","/** HTTP methods supported by Lambda Function URLs */\nexport type HttpMethod = \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\" | \"ANY\";\n\n/** Short content-type aliases for common response formats */\nexport type ContentType = \"json\" | \"html\" | \"text\" | \"css\" | \"js\" | \"xml\" | \"csv\" | \"svg\";\n\n/**\n * Incoming HTTP request object passed to the handler\n */\nexport type HttpRequest = {\n /** HTTP method (GET, POST, etc.) */\n method: string;\n /** Request path (e.g., \"/users/123\") */\n path: string;\n /** Request headers */\n headers: Record<string, string | undefined>;\n /** Query string parameters */\n query: Record<string, string | undefined>;\n /** Path parameters extracted from route (e.g., {id} -> params.id) */\n params: Record<string, string | undefined>;\n /** Parsed request body (JSON parsed if Content-Type is application/json) */\n body: unknown;\n /** Raw unparsed request body */\n rawBody?: string;\n};\n\n/**\n * HTTP response returned from the handler\n */\nexport type HttpResponse = {\n /** HTTP status code (e.g., 200, 404, 500) */\n status: number;\n /** Response body — JSON-serialized by default, or sent as string when contentType is set */\n body?: unknown;\n /**\n * Short content-type alias. Resolves to full MIME type automatically:\n * - `\"json\"` → `application/json` (default if omitted)\n * - `\"html\"` → `text/html; charset=utf-8`\n * - `\"text\"` → `text/plain; charset=utf-8`\n * - `\"css\"` → `text/css; charset=utf-8`\n * - `\"js\"` → `application/javascript; charset=utf-8`\n * - `\"xml\"` → `application/xml; charset=utf-8`\n * - `\"csv\"` → `text/csv; charset=utf-8`\n * - `\"svg\"` → `image/svg+xml; charset=utf-8`\n */\n contentType?: ContentType;\n /** Response headers (use for custom content-types not covered by contentType) */\n headers?: Record<string, string>;\n /**\n * Set to `true` to return binary data.\n * When enabled, `body` must be a base64-encoded string and the response\n * will include `isBase64Encoded: true` so Lambda Function URLs / API Gateway\n * decode it back to binary for the client.\n */\n binary?: boolean;\n};\n\n/** Response helpers for defineApi handlers */\nexport const result = {\n /** Return a JSON response */\n json: (body: unknown, status: number = 200): HttpResponse => ({ status, body }),\n /** Return a binary response. Accepts a Buffer and converts to base64 automatically. */\n binary: (body: Buffer, contentType: string, headers?: Record<string, string>): HttpResponse => ({\n status: 200,\n body: body.toString(\"base64\"),\n binary: true,\n headers: { \"content-type\": contentType, ...headers },\n }),\n};\n\n/** Stream helper injected into route args when `stream: true` is set on defineApi */\nexport type ResponseStream = {\n /** Write a raw string chunk to the response stream */\n write(chunk: string): void;\n /** End the response stream */\n end(): void;\n /** Switch to SSE mode: sets Content-Type to text/event-stream */\n sse(): void;\n /** Write an SSE event: `data: JSON.stringify(data)\\n\\n` */\n event(data: unknown): void;\n};\n\n/** Service for reading static files bundled into the Lambda ZIP */\nexport type StaticFiles = {\n /** Read file as UTF-8 string */\n read(path: string): string;\n /** Read file as Buffer (for binary content) */\n readBuffer(path: string): Buffer;\n /** Resolve absolute path to the bundled file */\n path(path: string): string;\n};\n"],"mappings":";AAuGO,IAAM,eAAe,CAAC,WAA+C;;;ACsJrE,IAAM,cAAc,CAQzB,YACmC;AACnC,QAAM,EAAE,UAAU,iBAAiB,SAAS,SAAS,QAAQ,OAAO,MAAM,QAAQ,QAAQ,aAAa,GAAG,OAAO,IAAI;AACrH,SAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,IACA,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,IACzB,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,IACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B,GAAI,cAAc,EAAE,QAAQ,YAAY,IAAI,CAAC;AAAA,IAC7C,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,IAC/B,GAAI,kBAAkB,EAAE,gBAAgB,IAAI,CAAC;AAAA,IAC7C,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC/B;AACF;;;AC9NO,IAAM,YAAY,CAAC,aAAoC;AAAA,EAC5D,SAAS;AAAA,EACT,QAAQ;AACV;;;ACwDO,IAAM,mBAAmB,CAAC,aAAkD;AAAA,EACjF,SAAS;AAAA,EACT,QAAQ;AACV;;;ACuEO,IAAM,kBAAkB,CAO7B,YACoC;AACpC,QAAM,EAAE,WAAW,SAAS,SAAS,QAAQ,OAAO,MAAM,QAAQ,QAAQ,aAAa,GAAG,OAAO,IAAI;AACrG,SAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,IACA,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,IACzB,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,IACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B,GAAI,cAAc,EAAE,QAAQ,YAAY,IAAI,CAAC;AAAA,IAC7C,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,IACjC,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC/B;AACF;;;ACvCO,IAAM,eAAe,CAM1B,YAC8B;AAC9B,QAAM,EAAE,iBAAiB,iBAAiB,SAAS,OAAO,MAAM,QAAQ,QAAQ,aAAa,GAAG,OAAO,IAAI;AAC3G,SAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,IACA,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,IACzB,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,IACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B,GAAI,cAAc,EAAE,QAAQ,YAAY,IAAI,CAAC;AAAA,IAC7C,GAAI,kBAAkB,EAAE,gBAAgB,IAAI,CAAC;AAAA,IAC7C,GAAI,kBAAkB,EAAE,gBAAgB,IAAI,CAAC;AAAA,EAC/C;AACF;;;AC/IO,IAAM,eAAe,CAAC,aAA0C;AAAA,EACrE,SAAS;AAAA,EACT,QAAQ;AACV;;;AC2GO,IAAM,YAAY,CAQvB,YACkC;AAClC,QAAM,EAAE,KAAK,MAAM,QAAQ,SAAS,OAAO,MAAM,QAAQ,QAAQ,aAAa,GAAG,OAAO,IAAI;AAC5F,SAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,IACA,GAAI,MAAM,EAAE,IAAI,IAAI,CAAC;AAAA,IACrB,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,IACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,IACzB,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,IACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B,GAAI,cAAc,EAAE,QAAQ,YAAY,IAAI,CAAC;AAAA,EAC/C;AACF;;;ACjFO,SAAS,MACd,KACA,WACa;AACb,SAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,IACA,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,EACnC;AACF;AA4EO,SAAS,QAAkC;AAChD,SAAO,CAAC,UAAmB;AAC7B;;;ACjIO,IAAM,SAAS;AAAA;AAAA,EAEpB,MAAM,CAAC,MAAe,SAAiB,SAAuB,EAAE,QAAQ,KAAK;AAAA;AAAA,EAE7E,QAAQ,CAAC,MAAc,aAAqB,aAAoD;AAAA,IAC9F,QAAQ;AAAA,IACR,MAAM,KAAK,SAAS,QAAQ;AAAA,IAC5B,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,aAAa,GAAG,QAAQ;AAAA,EACrD;AACF;","names":[]}
@@ -51,7 +51,7 @@ var matchRoute = (matchers, path) => {
51
51
  var toResult = (r) => {
52
52
  const resolved = r.contentType ? CONTENT_TYPE_MAP[r.contentType] : void 0;
53
53
  const customContentType = resolved ?? r.headers?.["content-type"] ?? r.headers?.["Content-Type"];
54
- const isJson = !customContentType || customContentType === "application/json";
54
+ const isJson = !r.binary && (!customContentType || customContentType === "application/json");
55
55
  return {
56
56
  statusCode: r.status,
57
57
  headers: {
@@ -59,7 +59,8 @@ var toResult = (r) => {
59
59
  ...r.headers,
60
60
  ...resolved ? { "Content-Type": resolved } : {}
61
61
  },
62
- body: isJson ? JSON.stringify(r.body) : String(r.body ?? "")
62
+ body: r.binary ? String(r.body ?? "") : isJson ? JSON.stringify(r.body) : String(r.body ?? ""),
63
+ ...r.binary ? { isBase64Encoded: true } : {}
63
64
  };
64
65
  };
65
66
  var notFound = () => ({
@@ -70,6 +71,7 @@ var notFound = () => ({
70
71
  var wrapApi = (handler) => {
71
72
  const rt = createHandlerRuntime(handler, "api", handler.__spec.logLevel ?? "info");
72
73
  const basePath = handler.__spec.basePath;
74
+ const isStream = handler.__spec.stream === true;
73
75
  const getMatchers = handler.get ? buildGetMatchers(handler.get, basePath) : [];
74
76
  const defaultError = (error, status) => {
75
77
  console.error(`[effortless:${rt.handlerName}]`, error);
@@ -82,7 +84,7 @@ var wrapApi = (handler) => {
82
84
  })
83
85
  };
84
86
  };
85
- return async (event) => {
87
+ const handleRequest = async (event, streamCtx) => {
86
88
  const startTime = Date.now();
87
89
  rt.patchConsole();
88
90
  try {
@@ -105,10 +107,15 @@ var wrapApi = (handler) => {
105
107
  }
106
108
  req.params = { ...req.params, ...matched.params };
107
109
  const args = { req, ...sharedArgs };
110
+ if (streamCtx) args.stream = streamCtx.stream;
108
111
  try {
109
112
  const response = await matched.handler(args);
110
- rt.logExecution(startTime, input, response.body);
111
- return toResult(response);
113
+ if (response) {
114
+ rt.logExecution(startTime, input, response.body);
115
+ return toResult(response);
116
+ }
117
+ rt.logExecution(startTime, input, "[stream]");
118
+ return void 0;
112
119
  } catch (error) {
113
120
  rt.logError(startTime, input, error);
114
121
  return handler.onError ? toResult(handler.onError(error, req)) : defaultError(error, 500);
@@ -116,6 +123,7 @@ var wrapApi = (handler) => {
116
123
  }
117
124
  if (req.method === "POST" && handler.post) {
118
125
  const args = { req, ...sharedArgs };
126
+ if (streamCtx) args.stream = streamCtx.stream;
119
127
  if (handler.schema) {
120
128
  try {
121
129
  args.data = handler.schema(req.body);
@@ -126,8 +134,12 @@ var wrapApi = (handler) => {
126
134
  }
127
135
  try {
128
136
  const response = await handler.post(args);
129
- rt.logExecution(startTime, input, response.body);
130
- return toResult(response);
137
+ if (response) {
138
+ rt.logExecution(startTime, input, response.body);
139
+ return toResult(response);
140
+ }
141
+ rt.logExecution(startTime, input, "[stream]");
142
+ return void 0;
131
143
  } catch (error) {
132
144
  rt.logError(startTime, input, error);
133
145
  return handler.onError ? toResult(handler.onError(error, req)) : defaultError(error, 500);
@@ -139,6 +151,64 @@ var wrapApi = (handler) => {
139
151
  rt.restoreConsole();
140
152
  }
141
153
  };
154
+ if (isStream) {
155
+ const streamify = globalThis.awslambda?.streamifyResponse;
156
+ if (!streamify) {
157
+ return async (event) => handleRequest(event);
158
+ }
159
+ return streamify(async (event, rawStream) => {
160
+ const HttpResponseStream = globalThis.awslambda?.HttpResponseStream;
161
+ let streamUsed = false;
162
+ let sseMode = false;
163
+ let httpStream = null;
164
+ const ensureHttpStream = (contentType) => {
165
+ if (!httpStream) {
166
+ httpStream = HttpResponseStream ? HttpResponseStream.from(rawStream, {
167
+ statusCode: 200,
168
+ headers: { "Content-Type": contentType ?? "text/plain; charset=utf-8" }
169
+ }) : rawStream;
170
+ }
171
+ return httpStream;
172
+ };
173
+ const stream = {
174
+ write: (chunk) => {
175
+ streamUsed = true;
176
+ const s = ensureHttpStream(sseMode ? "text/event-stream" : void 0);
177
+ s.write(chunk);
178
+ },
179
+ end: () => {
180
+ streamUsed = true;
181
+ const s = ensureHttpStream();
182
+ s.end();
183
+ },
184
+ sse: () => {
185
+ sseMode = true;
186
+ streamUsed = true;
187
+ ensureHttpStream("text/event-stream");
188
+ },
189
+ event: (data) => {
190
+ streamUsed = true;
191
+ const s = ensureHttpStream("text/event-stream");
192
+ sseMode = true;
193
+ s.write(`data: ${JSON.stringify(data)}
194
+
195
+ `);
196
+ }
197
+ };
198
+ const result = await handleRequest(event, { rawStream, httpStream, stream });
199
+ if (result && !streamUsed) {
200
+ const hs = HttpResponseStream ? HttpResponseStream.from(rawStream, { statusCode: result.statusCode, headers: result.headers }) : rawStream;
201
+ hs.write(result.body);
202
+ hs.end();
203
+ } else if (!streamUsed) {
204
+ rawStream.end();
205
+ }
206
+ });
207
+ }
208
+ return async (event) => {
209
+ const result = await handleRequest(event);
210
+ return result ?? notFound();
211
+ };
142
212
  };
143
213
  export {
144
214
  wrapApi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effortless-aws",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Code-first AWS Lambda framework. Export handlers, deploy with one command.",