effortless-aws 0.27.0 → 0.28.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.
@@ -251,7 +251,8 @@ var toSeconds = (d) => {
251
251
 
252
252
  // src/handlers/auth.ts
253
253
  var AUTH_COOKIE_NAME = "__eff_session";
254
- var createAuthRuntime = (secret, defaultExpiresIn) => {
254
+ var createAuthRuntime = (secret, defaultExpiresIn, apiTokenVerify, apiTokenHeader, apiTokenCacheTtlSeconds) => {
255
+ const tokenCache = apiTokenCacheTtlSeconds ? /* @__PURE__ */ new Map() : void 0;
255
256
  const sign = (payload) => crypto.createHmac("sha256", secret).update(payload).digest("base64url");
256
257
  const cookieBase = `${AUTH_COOKIE_NAME}=`;
257
258
  const cookieAttrs = "; HttpOnly; Secure; SameSite=Lax; Path=/";
@@ -271,36 +272,58 @@ var createAuthRuntime = (secret, defaultExpiresIn) => {
271
272
  return void 0;
272
273
  }
273
274
  };
274
- return {
275
- forRequest(cookieValue) {
275
+ const extractTokenValue = (headerValue) => {
276
+ const isDefaultHeader = !apiTokenHeader || apiTokenHeader.toLowerCase() === "authorization";
277
+ if (isDefaultHeader && headerValue.toLowerCase().startsWith("bearer ")) {
278
+ return headerValue.slice(7);
279
+ }
280
+ return headerValue;
281
+ };
282
+ const buildHelpers = (sessionData) => ({
283
+ createSession(...args) {
284
+ const hasData = args.length > 0 && (typeof args[0] === "object" && args[0] !== null && !("expiresIn" in args[0]));
285
+ const data = hasData ? args[0] : void 0;
286
+ const options = hasData ? args[1] : args[0];
287
+ const seconds = options?.expiresIn ? toSeconds(options.expiresIn) : defaultExpiresIn;
288
+ const exp = Math.floor(Date.now() / 1e3) + seconds;
289
+ const payload = Buffer.from(JSON.stringify({ exp, ...data }), "utf-8").toString("base64url");
290
+ const sig = sign(payload);
276
291
  return {
277
- grant(...args) {
278
- const hasData = args.length > 0 && (typeof args[0] === "object" && args[0] !== null && !("expiresIn" in args[0]));
279
- const data = hasData ? args[0] : void 0;
280
- const options = hasData ? args[1] : args[0];
281
- const seconds = options?.expiresIn ? toSeconds(options.expiresIn) : defaultExpiresIn;
282
- const exp = Math.floor(Date.now() / 1e3) + seconds;
283
- const payload = Buffer.from(JSON.stringify({ exp, ...data }), "utf-8").toString("base64url");
284
- const sig = sign(payload);
285
- return {
286
- status: 200,
287
- body: { ok: true },
288
- headers: {
289
- "set-cookie": `${cookieBase}${payload}.${sig}${cookieAttrs}; Max-Age=${seconds}`
290
- }
291
- };
292
- },
293
- revoke() {
294
- return {
295
- status: 200,
296
- body: { ok: true },
297
- headers: {
298
- "set-cookie": `${cookieBase}${cookieAttrs}; Max-Age=0`
299
- }
300
- };
301
- },
302
- session: decodeSession(cookieValue)
292
+ status: 200,
293
+ body: { ok: true },
294
+ headers: {
295
+ "set-cookie": `${cookieBase}${payload}.${sig}${cookieAttrs}; Max-Age=${seconds}`
296
+ }
303
297
  };
298
+ },
299
+ clearSession() {
300
+ return {
301
+ status: 200,
302
+ body: { ok: true },
303
+ headers: {
304
+ "set-cookie": `${cookieBase}${cookieAttrs}; Max-Age=0`
305
+ }
306
+ };
307
+ },
308
+ session: sessionData
309
+ });
310
+ return {
311
+ async forRequest(cookieValue, authHeader, deps) {
312
+ if (authHeader && apiTokenVerify) {
313
+ const tokenValue = extractTokenValue(authHeader);
314
+ if (tokenCache) {
315
+ const cached = tokenCache.get(tokenValue);
316
+ if (cached && cached.expiresAt > Date.now()) {
317
+ return buildHelpers(cached.session);
318
+ }
319
+ }
320
+ const session = await apiTokenVerify(tokenValue, { deps });
321
+ if (tokenCache && apiTokenCacheTtlSeconds) {
322
+ tokenCache.set(tokenValue, { session, expiresAt: Date.now() + apiTokenCacheTtlSeconds * 1e3 });
323
+ }
324
+ return buildHelpers(session);
325
+ }
326
+ return buildHelpers(decodeSession(cookieValue));
304
327
  }
305
328
  };
306
329
  };
@@ -565,7 +588,14 @@ var createHandlerRuntime = (handler, handlerType, logLevel = "info", extraSetupA
565
588
  const secret = values.get(ssmPath);
566
589
  if (!secret) throw new Error(`Auth secret not found at ${ssmPath}`);
567
590
  const defaultExpires = handler.auth.expiresIn ? toSeconds(handler.auth.expiresIn) : 604800;
568
- resolvedAuthRuntime = createAuthRuntime(secret, defaultExpires);
591
+ const cacheTtlSeconds = handler.apiToken?.cacheTtl ? toSeconds(handler.apiToken.cacheTtl) : void 0;
592
+ resolvedAuthRuntime = createAuthRuntime(
593
+ secret,
594
+ defaultExpires,
595
+ handler.apiToken?.verify,
596
+ handler.apiToken?.header,
597
+ cacheTtlSeconds
598
+ );
569
599
  return resolvedAuthRuntime;
570
600
  };
571
601
  const getSetup = async () => {
@@ -582,7 +612,7 @@ var createHandlerRuntime = (handler, handlerType, logLevel = "info", extraSetupA
582
612
  }
583
613
  return ctx;
584
614
  };
585
- const commonArgs = async (cookieValue) => {
615
+ const commonArgs = async (cookieValue, authHeader) => {
586
616
  const args = {};
587
617
  if (handler.setup) args.ctx = await getSetup();
588
618
  const deps = getDeps();
@@ -591,7 +621,9 @@ var createHandlerRuntime = (handler, handlerType, logLevel = "info", extraSetupA
591
621
  if (params) args.config = params;
592
622
  if (handler.static) args.files = staticFiles;
593
623
  const authRuntime = await getAuthRuntime();
594
- if (authRuntime) args.auth = authRuntime.forRequest(cookieValue);
624
+ if (authRuntime) {
625
+ args.auth = await authRuntime.forRequest(cookieValue, authHeader, deps);
626
+ }
595
627
  return args;
596
628
  };
597
629
  const logExecution = (startTime, input, output) => {
package/dist/index.d.ts CHANGED
@@ -1225,8 +1225,8 @@ type AppHandler = {
1225
1225
  */
1226
1226
  declare const defineApp: (options: AppConfig) => AppHandler;
1227
1227
 
1228
- type CookieAuthConfig<_T = undefined> = {
1229
- /** Path to redirect unauthenticated users to */
1228
+ type AuthConfig<_T = undefined> = {
1229
+ /** Path to redirect unauthenticated users to (used by static sites). */
1230
1230
  loginPath: string;
1231
1231
  /** Paths that don't require authentication. Supports trailing `*` wildcard. */
1232
1232
  public?: string[];
@@ -1234,45 +1234,58 @@ type CookieAuthConfig<_T = undefined> = {
1234
1234
  expiresIn?: Duration;
1235
1235
  };
1236
1236
  /**
1237
- * Branded cookie auth object returned by `defineAuth()`.
1237
+ * Branded auth object returned by `defineAuth()`.
1238
1238
  * Pass to `defineApi({ auth })` and `defineStaticSite({ auth })`.
1239
1239
  */
1240
- type CookieAuth<T = undefined> = CookieAuthConfig<T> & {
1241
- readonly __brand: "effortless-cookie-auth";
1240
+ type Auth<T = undefined> = AuthConfig<T> & {
1241
+ readonly __brand: "effortless-auth";
1242
1242
  /** @internal phantom type marker for session data */
1243
1243
  readonly __session?: T;
1244
1244
  };
1245
+ /** API token authentication strategy. Verifies tokens from HTTP headers (e.g. Authorization: Bearer). */
1246
+ type ApiTokenStrategy<T, D = undefined> = {
1247
+ /** HTTP header to read the token from. Default: "authorization" (strips "Bearer " prefix). */
1248
+ header?: string;
1249
+ /** Verify the token value and return session data, or null if invalid. */
1250
+ verify: [D] extends [undefined] ? (value: string) => T | null | Promise<T | null> : (value: string, ctx: {
1251
+ deps: D;
1252
+ }) => T | null | Promise<T | null>;
1253
+ /** Cache verified token results for this duration. Avoids calling verify on every request. */
1254
+ cacheTtl?: Duration;
1255
+ };
1245
1256
  /**
1246
- * Define cookie-based authentication using HMAC-signed tokens.
1257
+ * Define authentication for API handlers and static sites.
1258
+ *
1259
+ * Session-based auth uses HMAC-signed cookies (auto-managed by the framework).
1247
1260
  *
1248
- * - Middleware (Lambda@Edge) verifies cookie signatures without external calls
1249
- * - API handler gets `auth.grant()` / `auth.revoke()` / `auth.session` helpers
1250
- * - Secret is auto-generated and stored in SSM Parameter Store
1261
+ * - Lambda@Edge middleware verifies cookie signatures for static sites
1262
+ * - API handler gets `auth.createSession()` / `auth.clearSession()` / `auth.session` helpers
1263
+ * - HMAC secret is auto-generated and stored in SSM Parameter Store
1251
1264
  *
1252
- * @typeParam T - Session data type. When provided, `grant(data)` requires typed payload
1265
+ * @typeParam T - Session data type. When provided, `createSession(data)` requires typed payload
1253
1266
  * and `auth.session` is typed as `T` in handler args.
1254
1267
  *
1255
1268
  * @example
1256
1269
  * ```typescript
1257
1270
  * type Session = { userId: string; role: "admin" | "user" };
1258
1271
  *
1259
- * const protect = defineAuth<Session>({
1272
+ * const auth = defineAuth<Session>({
1260
1273
  * loginPath: '/login',
1261
- * public: ['/login', '/assets/*'],
1274
+ * public: ['/login', '/api/login'],
1262
1275
  * expiresIn: '7d',
1263
1276
  * })
1264
1277
  *
1265
- * export const api = defineApi({ auth: protect, ... })
1266
- * export const webapp = defineStaticSite({ auth: protect, ... })
1278
+ * export const api = defineApi({ auth, ... })
1279
+ * export const webapp = defineStaticSite({ auth, ... })
1267
1280
  * ```
1268
1281
  */
1269
- declare const defineAuth: <T = undefined>(options: CookieAuthConfig<T>) => CookieAuth<T>;
1270
- /** Grant options for creating a session */
1271
- type GrantOptions = {
1282
+ declare const defineAuth: <T = undefined>(options: AuthConfig<T>) => Auth<T>;
1283
+ /** Options for creating a session */
1284
+ type SessionOptions = {
1272
1285
  expiresIn?: Duration;
1273
1286
  };
1274
- /** Grant response with Set-Cookie header */
1275
- type GrantResponse = {
1287
+ /** Session response with Set-Cookie header */
1288
+ type SessionResponse = {
1276
1289
  status: 200;
1277
1290
  body: {
1278
1291
  ok: true;
@@ -1284,19 +1297,19 @@ type GrantResponse = {
1284
1297
  * @typeParam T - Session data type (undefined = no custom data)
1285
1298
  */
1286
1299
  type AuthHelpers<T = undefined> = {
1287
- revoke(): {
1300
+ clearSession(): {
1288
1301
  status: 200;
1289
1302
  body: {
1290
1303
  ok: true;
1291
1304
  };
1292
1305
  headers: Record<string, string>;
1293
1306
  };
1294
- /** The current session data (decoded from cookie). Undefined if no valid session. */
1307
+ /** The current session data (from cookie or API token). Undefined if no valid session. */
1295
1308
  session: T extends undefined ? undefined : T | undefined;
1296
1309
  } & ([T] extends [undefined] ? {
1297
- grant(options?: GrantOptions): GrantResponse;
1310
+ createSession(options?: SessionOptions): SessionResponse;
1298
1311
  } : {
1299
- grant(data: T, options?: GrantOptions): GrantResponse;
1312
+ createSession(data: T, options?: SessionOptions): SessionResponse;
1300
1313
  });
1301
1314
 
1302
1315
  /** Any branded handler that deploys to API Gateway (HttpHandler, ApiHandler, etc.) */
@@ -1359,7 +1372,7 @@ type StaticSiteConfig = {
1359
1372
  /** Lambda@Edge middleware that runs before serving pages. Use for auth checks, redirects, etc. */
1360
1373
  middleware?: MiddlewareHandler;
1361
1374
  /** Cookie-based authentication. Auto-generates Lambda@Edge middleware that verifies signed cookies. */
1362
- auth?: CookieAuth<any>;
1375
+ auth?: Auth<any>;
1363
1376
  /** SEO: auto-generate sitemap.xml and robots.txt at deploy time, optionally submit URLs to Google Indexing API */
1364
1377
  seo?: StaticSiteSeo;
1365
1378
  };
@@ -1408,10 +1421,10 @@ type StaticSiteHandler = {
1408
1421
  */
1409
1422
  declare const defineStaticSite: (options: StaticSiteConfig) => StaticSiteHandler;
1410
1423
 
1411
- /** Extract session type T from CookieAuth<T> */
1412
- type SessionOf<A> = A extends CookieAuth<infer T> ? T : undefined;
1424
+ /** Extract session type T from Auth<T> */
1425
+ type SessionOf<A> = A extends Auth<infer T> ? T : undefined;
1413
1426
  /** GET route handler — no schema, no data */
1414
- type ApiGetHandlerFn<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined, ST extends boolean | undefined = undefined, A extends CookieAuth<any> | undefined = undefined> = (args: {
1427
+ type ApiGetHandlerFn<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined, ST extends boolean | undefined = undefined, A extends Auth<any> | undefined = undefined> = (args: {
1415
1428
  req: HttpRequest;
1416
1429
  } & HandlerArgs<C, D, P, S> & ([ST] extends [true] ? {
1417
1430
  stream: ResponseStream;
@@ -1419,7 +1432,7 @@ type ApiGetHandlerFn<C = undefined, D = undefined, P = undefined, S extends stri
1419
1432
  auth: AuthHelpers<SessionOf<A>>;
1420
1433
  })) => Promise<HttpResponse | void> | HttpResponse | void;
1421
1434
  /** POST handler — with typed data from schema */
1422
- type ApiPostHandlerFn<T = undefined, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined, ST extends boolean | undefined = undefined, A extends CookieAuth<any> | undefined = undefined> = (args: {
1435
+ type ApiPostHandlerFn<T = undefined, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined, ST extends boolean | undefined = undefined, A extends Auth<any> | undefined = undefined> = (args: {
1423
1436
  req: HttpRequest;
1424
1437
  } & ([T] extends [undefined] ? {} : {
1425
1438
  data: T;
@@ -1441,7 +1454,7 @@ type ApiConfig = {
1441
1454
  /** Lambda function settings (memory, timeout, permissions, etc.) */
1442
1455
  lambda?: LambdaWithPermissions;
1443
1456
  /** Base path prefix for all routes (e.g., "/api") */
1444
- basePath: string;
1457
+ basePath: `/${string}`;
1445
1458
  /** Enable response streaming. When true, the Lambda Function URL uses RESPONSE_STREAM invoke mode. */
1446
1459
  stream?: boolean;
1447
1460
  };
@@ -1451,15 +1464,17 @@ type ApiConfig = {
1451
1464
  * - `get` routes handle queries (path-based routing, no body)
1452
1465
  * - `post` handles commands (single entry point, discriminated union via `schema`)
1453
1466
  */
1454
- 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, A extends CookieAuth<any> | undefined = undefined> = {
1467
+ 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, A extends Auth<any> | undefined = undefined> = {
1455
1468
  /** Lambda function settings (memory, timeout, permissions, etc.) */
1456
1469
  lambda?: LambdaWithPermissions;
1457
1470
  /** Base path prefix for all routes (e.g., "/api") */
1458
- basePath: string;
1471
+ basePath: `/${string}`;
1459
1472
  /** Enable response streaming. When true, routes receive a `stream` arg for SSE. Routes can still return HttpResponse normally. */
1460
1473
  stream?: ST;
1461
- /** Cookie-based authentication. Injects `auth` helpers (grant/revoke) into handler args. */
1474
+ /** Session-based authentication. Injects `auth` helpers (createSession/clearSession/session) into handler args. */
1462
1475
  auth?: A;
1476
+ /** API token authentication for external clients (Bearer tokens, API keys). Has access to deps. */
1477
+ apiToken?: ApiTokenStrategy<SessionOf<A>, [D] extends [undefined] ? undefined : ResolveDeps<D>>;
1463
1478
  /**
1464
1479
  * Factory function to initialize shared state.
1465
1480
  * Called once on cold start, result is cached and reused across invocations.
@@ -1479,7 +1494,7 @@ type DefineApiOptions<T = undefined, C = undefined, D extends Record<string, Any
1479
1494
  /** Called after each invocation completes, right before Lambda freezes the process */
1480
1495
  onAfterInvoke?: (args: HandlerArgs<C, D, P, S>) => void | Promise<void>;
1481
1496
  /** GET routes — query handlers keyed by relative path (e.g., "/users/{id}") */
1482
- get?: Record<string, ApiGetHandlerFn<C, D, P, S, ST, A>>;
1497
+ get?: Record<`/${string}`, ApiGetHandlerFn<C, D, P, S, ST, A>>;
1483
1498
  /**
1484
1499
  * Schema for POST body validation. Use with discriminated unions:
1485
1500
  * ```typescript
@@ -1502,8 +1517,9 @@ type ApiHandler<T = undefined, C = undefined> = {
1502
1517
  readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);
1503
1518
  readonly config?: Record<string, unknown>;
1504
1519
  readonly static?: string[];
1505
- readonly auth?: CookieAuth;
1506
- readonly get?: Record<string, (...args: any[]) => any>;
1520
+ readonly auth?: Auth;
1521
+ readonly apiToken?: ApiTokenStrategy<any, any>;
1522
+ readonly get?: Record<`/${string}`, (...args: any[]) => any>;
1507
1523
  readonly post?: (...args: any[]) => any;
1508
1524
  };
1509
1525
  /**
@@ -1540,6 +1556,6 @@ type ApiHandler<T = undefined, C = undefined> = {
1540
1556
  * })
1541
1557
  * ```
1542
1558
  */
1543
- 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, A extends CookieAuth<any> | undefined = undefined>(options: DefineApiOptions<T, C, D, P, S, ST, A>) => ApiHandler<T, C>;
1559
+ 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, A extends Auth<any> | undefined = undefined>(options: DefineApiOptions<T, C, D, P, S, ST, A>) => ApiHandler<T, C>;
1544
1560
 
1545
- export { type AnyParamRef, type AnySecretRef, type ApiConfig, type ApiHandler, type AppConfig, type AppHandler, type AuthHelpers, type BucketClient, type BucketConfig, type BucketEvent, type BucketHandler, type ContentType, type CookieAuth, type CookieAuthConfig, type Duration, type EffortlessConfig, type EmailClient, type FailedRecord, type FifoQueueConfig, type FifoQueueHandler, type FifoQueueMessage, 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 QueueClient, type ResponseStream, type SecretRef, type SendEmailOptions, type SendMessageInput, type SkCondition, type StaticFiles, type StaticSiteConfig, type StaticSiteHandler, type StaticSiteSeo, type StreamView, type TableClient, type TableConfig, type TableHandler, type TableItem, type TableKey, type TableRecord, type UpdateActions, defineApi, defineApp, defineAuth, defineBucket, defineConfig, defineFifoQueue, defineMailer, defineStaticSite, defineTable, generateBase64, generateHex, generateUuid, param, result, secret, toSeconds, unsafeAs };
1561
+ export { type AnyParamRef, type AnySecretRef, type ApiConfig, type ApiHandler, type ApiTokenStrategy, type AppConfig, type AppHandler, type Auth, type AuthConfig, type AuthHelpers, type BucketClient, type BucketConfig, type BucketEvent, type BucketHandler, type ContentType, type Duration, type EffortlessConfig, type EmailClient, type FailedRecord, type FifoQueueConfig, type FifoQueueHandler, type FifoQueueMessage, 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 QueueClient, type ResponseStream, type SecretRef, type SendEmailOptions, type SendMessageInput, type SkCondition, type StaticFiles, type StaticSiteConfig, type StaticSiteHandler, type StaticSiteSeo, type StreamView, type TableClient, type TableConfig, type TableHandler, type TableItem, type TableKey, type TableRecord, type UpdateActions, defineApi, defineApp, defineAuth, defineBucket, defineConfig, defineFifoQueue, defineMailer, defineStaticSite, defineTable, generateBase64, generateHex, generateUuid, param, result, secret, toSeconds, unsafeAs };
package/dist/index.js CHANGED
@@ -82,7 +82,7 @@ var defineMailer = (options) => ({
82
82
 
83
83
  // src/handlers/define-api.ts
84
84
  var defineApi = (options) => {
85
- const { get, post, schema, onError, onAfterInvoke, setup, deps, config, auth: authConfig, static: staticFiles, ...__spec } = options;
85
+ const { get, post, schema, onError, onAfterInvoke, setup, deps, config, auth: authConfig, apiToken, static: staticFiles, ...__spec } = options;
86
86
  return {
87
87
  __brand: "effortless-api",
88
88
  __spec,
@@ -95,7 +95,8 @@ var defineApi = (options) => {
95
95
  ...deps ? { deps } : {},
96
96
  ...config ? { config } : {},
97
97
  ...staticFiles ? { static: staticFiles } : {},
98
- ...authConfig ? { auth: authConfig } : {}
98
+ ...authConfig ? { auth: authConfig } : {},
99
+ ...apiToken ? { apiToken } : {}
99
100
  };
100
101
  };
101
102
 
@@ -144,7 +145,7 @@ function unsafeAs() {
144
145
 
145
146
  // src/handlers/auth.ts
146
147
  var defineAuth = (options) => ({
147
- __brand: "effortless-cookie-auth",
148
+ __brand: "effortless-auth",
148
149
  ...options
149
150
  });
150
151
 
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","../src/handlers/auth.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 Lambda 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 lambda?: {\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/**\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, Duration } from \"./handler-options\";\nimport type { TableClient } from \"../runtime/table-client\";\nimport type { AnyDepHandler, ResolveDeps } from \"./handler-deps\";\nimport type { StaticFiles } from \"./shared\";\nimport type { HandlerArgs } from \"./handler-args\";\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 = {\n /** Lambda function settings (memory, timeout, permissions, etc.) */\n lambda?: 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 to gather records before invoking (default: `\"2s\"`). Accepts `\"5s\"`, `\"1m\"`, etc. */\n batchWindow?: Duration;\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<{ type: \"order\"; amount: number }>({\n * tagField: \"type\",\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 & HandlerArgs<C, D, P, S>\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 & HandlerArgs<C, D, P, S>\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 & HandlerArgs<C, D, P, S>\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?: (args: { error: unknown } & HandlerArgs<C, D, P, S>) => void;\n /** Called after each invocation completes, right before Lambda freezes the process */\n onAfterInvoke?: (args: HandlerArgs<C, D, P, S>) => void | Promise<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 * Pass a function returning the deps object: `deps: () => ({ orders })`.\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 = any> = {\n readonly __brand: \"effortless-table\";\n readonly __spec: TableConfig;\n readonly schema?: (input: unknown) => T;\n readonly onError?: (...args: any[]) => any;\n readonly onAfterInvoke?: (...args: any[]) => any;\n readonly setup?: (...args: any[]) => C | Promise<C>;\n readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);\n readonly config?: Record<string, unknown>;\n readonly static?: string[];\n readonly onRecord?: (...args: any[]) => any;\n readonly onBatchComplete?: (...args: any[]) => any;\n readonly onBatch?: (...args: any[]) => any;\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\n * ```typescript\n * type OrderData = { amount: number; status: string };\n *\n * export const orders = defineTable<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> => {\n const { onRecord, onBatchComplete, onBatch, onError, onAfterInvoke, schema, setup, deps, config, static: staticFiles, ...__spec } = options;\n return {\n __brand: \"effortless-table\",\n __spec,\n ...(schema ? { schema } : {}),\n ...(onError ? { onError } : {}),\n ...(onAfterInvoke ? { onAfterInvoke } : {}),\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>;\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 = {\n /** Lambda function settings (memory, timeout, permissions, etc.) */\n lambda?: 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 * lambda: { memory: 1024 },\n * });\n * ```\n */\nexport const defineApp = (options: AppConfig): AppHandler => ({\n __brand: \"effortless-app\",\n __spec: options,\n});\n","import type { CookieAuth } from \"./auth\";\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 /** Cookie-based authentication. Auto-generates Lambda@Edge middleware that verifies signed cookies. */\n auth?: CookieAuth<any>;\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, Duration } from \"./handler-options\";\nimport type { AnyDepHandler, ResolveDeps } from \"./handler-deps\";\nimport type { StaticFiles } from \"./shared\";\nimport type { HandlerArgs } from \"./handler-args\";\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 = {\n /** Lambda function settings (memory, timeout, permissions, etc.) */\n lambda?: LambdaWithPermissions;\n /** Number of messages per Lambda invocation (1-10 for FIFO, default: 10) */\n batchSize?: number;\n /** Maximum time to gather messages before invoking (default: 0). Accepts `\"5s\"`, `\"1m\"`, etc. */\n batchWindow?: Duration;\n /** Visibility timeout (default: max of timeout or 30s). Accepts `\"30s\"`, `\"5m\"`, etc. */\n visibilityTimeout?: Duration;\n /** Message retention period (default: `\"4d\"`). Accepts `\"1h\"`, `\"7d\"`, etc. */\n retentionPeriod?: Duration;\n /** Delivery delay for all messages in the queue (default: 0). Accepts `\"30s\"`, `\"5m\"`, etc. */\n delay?: Duration;\n /** Enable content-based deduplication (default: true) */\n contentBasedDeduplication?: boolean;\n /** Max number of receives before a message is sent to the dead-letter queue (default: 3) */\n maxReceiveCount?: number;\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 & HandlerArgs<C, D, P, S>\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 & HandlerArgs<C, D, P, S>\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?: (args: { error: unknown } & HandlerArgs<C, D, P, S>) => void;\n /** Called after each invocation completes, right before Lambda freezes the process */\n onAfterInvoke?: (args: HandlerArgs<C, D, P, S>) => void | Promise<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 * Pass a function returning the deps object: `deps: () => ({ orders })`.\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 = any> = {\n readonly __brand: \"effortless-fifo-queue\";\n readonly __spec: FifoQueueConfig;\n readonly schema?: (input: unknown) => T;\n readonly onError?: (...args: any[]) => any;\n readonly onAfterInvoke?: (...args: any[]) => any;\n readonly setup?: (...args: any[]) => C | Promise<C>;\n readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);\n readonly config?: Record<string, unknown>;\n readonly static?: string[];\n readonly onMessage?: (...args: any[]) => any;\n readonly onBatch?: (...args: any[]) => any;\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> => {\n const { onMessage, onBatch, onError, onAfterInvoke, schema, setup, deps, config, static: staticFiles, ...__spec } = options;\n return {\n __brand: \"effortless-fifo-queue\",\n __spec,\n ...(schema ? { schema } : {}),\n ...(onError ? { onError } : {}),\n ...(onAfterInvoke ? { onAfterInvoke } : {}),\n ...(setup ? { setup } : {}),\n ...(deps ? { deps } : {}),\n ...(config ? { config } : {}),\n ...(staticFiles ? { static: staticFiles } : {}),\n ...(onMessage ? { onMessage } : {}),\n ...(onBatch ? { onBatch } : {})\n } as FifoQueueHandler<T, C>;\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\";\nimport type { HandlerArgs } from \"./handler-args\";\n\n/**\n * Configuration options for defineBucket.\n */\nexport type BucketConfig = {\n /** Lambda function settings (memory, timeout, permissions, etc.) */\n lambda?: 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 & HandlerArgs<C, D, P, S>\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 & HandlerArgs<C, D, P, S>\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?: (args: { error: unknown } & HandlerArgs<C, D, P, S>) => void;\n /** Called after each invocation completes, right before Lambda freezes the process */\n onAfterInvoke?: (args: HandlerArgs<C, D, P, S>) => void | Promise<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 * Pass a function returning the deps object: `deps: () => ({ uploads })`.\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 = any> = {\n readonly __brand: \"effortless-bucket\";\n readonly __spec: BucketConfig;\n readonly onError?: (...args: any[]) => any;\n readonly onAfterInvoke?: (...args: any[]) => any;\n readonly setup?: (...args: any[]) => C | Promise<C>;\n readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);\n readonly config?: Record<string, unknown>;\n readonly static?: string[];\n readonly onObjectCreated?: (...args: any[]) => any;\n readonly onObjectRemoved?: (...args: any[]) => any;\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> => {\n const { onObjectCreated, onObjectRemoved, onError, onAfterInvoke, setup, deps, config, static: staticFiles, ...__spec } = options;\n return {\n __brand: \"effortless-bucket\",\n __spec,\n ...(onError ? { onError } : {}),\n ...(onAfterInvoke ? { onAfterInvoke } : {}),\n ...(setup ? { setup } : {}),\n ...(deps ? { deps } : {}),\n ...(config ? { config } : {}),\n ...(staticFiles ? { static: staticFiles } : {}),\n ...(onObjectCreated ? { onObjectCreated } : {}),\n ...(onObjectRemoved ? { onObjectRemoved } : {}),\n } as BucketHandler<C>;\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\";\nimport type { CookieAuth, AuthHelpers } from \"./auth\";\nimport type { HandlerArgs } from \"./handler-args\";\n\n/** Extract session type T from CookieAuth<T> */\ntype SessionOf<A> = A extends CookieAuth<infer T> ? T : undefined;\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 A extends CookieAuth<any> | undefined = undefined\n> =\n (args: { req: HttpRequest }\n & HandlerArgs<C, D, P, S>\n & ([ST] extends [true] ? { stream: ResponseStream } : {})\n & ([A] extends [undefined] ? {} : { auth: AuthHelpers<SessionOf<A>> })\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 A extends CookieAuth<any> | undefined = undefined\n> =\n (args: { req: HttpRequest }\n & ([T] extends [undefined] ? {} : { data: T })\n & HandlerArgs<C, D, P, S>\n & ([ST] extends [true] ? { stream: ResponseStream } : {})\n & ([A] extends [undefined] ? {} : { auth: AuthHelpers<SessionOf<A>> })\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 = {\n /** Lambda function settings (memory, timeout, permissions, etc.) */\n lambda?: 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 A extends CookieAuth<any> | undefined = undefined\n> = {\n /** Lambda function settings (memory, timeout, permissions, etc.) */\n lambda?: 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 /** Cookie-based authentication. Injects `auth` helpers (grant/revoke) into handler args. */\n auth?: A;\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.): `deps: () => ({ users })` */\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?: (args: { error: unknown; req: HttpRequest } & HandlerArgs<C, D, P, S>) => HttpResponse;\n /** Called after each invocation completes, right before Lambda freezes the process */\n onAfterInvoke?: (args: HandlerArgs<C, D, P, S>) => void | Promise<void>;\n\n /** GET routes — query handlers keyed by relative path (e.g., \"/users/{id}\") */\n get?: Record<string, ApiGetHandlerFn<C, D, P, S, ST, A>>;\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, A>;\n};\n\n/** Internal handler object created by defineApi */\nexport type ApiHandler<\n T = undefined,\n C = undefined,\n> = {\n readonly __brand: \"effortless-api\";\n readonly __spec: ApiConfig;\n readonly schema?: (input: unknown) => T;\n readonly onError?: (...args: any[]) => any;\n readonly onAfterInvoke?: (...args: any[]) => any;\n readonly setup?: (...args: any[]) => C | Promise<C>;\n readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);\n readonly config?: Record<string, unknown>;\n readonly static?: string[];\n readonly auth?: CookieAuth;\n readonly get?: Record<string, (...args: any[]) => any>;\n readonly post?: (...args: any[]) => any;\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 A extends CookieAuth<any> | undefined = undefined\n>(\n options: DefineApiOptions<T, C, D, P, S, ST, A>\n): ApiHandler<T, C> => {\n const { get, post, schema, onError, onAfterInvoke, setup, deps, config, auth: authConfig, 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 ...(onAfterInvoke ? { onAfterInvoke } : {}),\n ...(setup ? { setup } : {}),\n ...(deps ? { deps } : {}),\n ...(config ? { config } : {}),\n ...(staticFiles ? { static: staticFiles } : {}),\n ...(authConfig ? { auth: authConfig } : {}),\n } as ApiHandler<T, C>;\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(), unsafeAs(), 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// ============ Duration ============\n\n/**\n * Human-readable duration. Accepts a plain number (seconds) or a string\n * with a unit suffix: `\"30s\"`, `\"5m\"`, `\"1h\"`, `\"2d\"`.\n *\n * @example\n * ```typescript\n * timeout: 30 // 30 seconds\n * timeout: \"30s\" // 30 seconds\n * timeout: \"5m\" // 300 seconds\n * timeout: \"1h\" // 3600 seconds\n * retentionPeriod: \"4d\" // 345600 seconds\n * ```\n */\nexport type Duration = number | `${number}s` | `${number}m` | `${number}h` | `${number}d`;\n\n/** Convert a Duration to seconds. */\nexport const toSeconds = (d: Duration): number => {\n if (typeof d === \"number\") return d;\n const match = d.match(/^(\\d+(?:\\.\\d+)?)(s|m|h|d)$/);\n if (!match) throw new Error(`Invalid duration: \"${d}\"`);\n const n = Number(match[1]);\n const unit = match[2];\n if (unit === \"d\") return n * 86400;\n if (unit === \"h\") return n * 3600;\n if (unit === \"m\") return n * 60;\n return n;\n};\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 (default: 30s). Accepts seconds or duration string: `\"30s\"`, `\"5m\"` */\n timeout?: Duration;\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// ============ Secrets (SSM Parameters) ============\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnySecretRef = SecretRef<any>;\n\n/**\n * Reference to an SSM Parameter Store secret.\n *\n * @typeParam T - The resolved type after optional transform (default: string)\n */\nexport type SecretRef<T = string> = {\n readonly __brand: \"effortless-secret\";\n readonly key?: string;\n readonly generate?: () => string;\n readonly transform?: (raw: string) => T;\n};\n\n/**\n * Maps a config declaration to resolved value types.\n * `SecretRef<T>` resolves to `T`.\n *\n * @typeParam P - Record of config keys to SecretRef instances\n */\nexport type ResolveConfig<P> = {\n [K in keyof P]: P[K] extends SecretRef<infer T> ? T : string;\n};\n\n/** Options for `secret()` without a transform. */\nexport type SecretOptions = {\n /** Custom SSM key (default: derived from config property name in kebab-case) */\n key?: string;\n /** Generator function called at deploy time if the SSM parameter doesn't exist yet */\n generate?: () => string;\n};\n\n/** Options for `secret()` with a transform. */\nexport type SecretOptionsWithTransform<T> = SecretOptions & {\n /** Transform the raw SSM string value into a typed value */\n transform: (raw: string) => T;\n};\n\n/**\n * Declare an SSM Parameter Store secret.\n *\n * The SSM key is derived from the config property name (camelCase → kebab-case)\n * unless overridden with `key`. The full SSM path is `/${project}/${stage}/${key}`.\n *\n * @param options - Optional configuration (key override, generator, transform)\n * @returns A SecretRef used by the deployment and runtime systems\n *\n * @example Simple secret\n * ```typescript\n * config: {\n * dbUrl: secret(),\n * // → SSM path: /${project}/${stage}/db-url\n * }\n * ```\n *\n * @example Auto-generated secret\n * ```typescript\n * config: {\n * authSecret: secret({ generate: generateHex(64) }),\n * // → auto-creates SSM param if missing\n * }\n * ```\n *\n * @example With transform\n * ```typescript\n * config: {\n * appConfig: secret({ transform: TOML.parse }),\n * }\n * ```\n */\nexport function secret(): SecretRef<string>;\nexport function secret(options: SecretOptions): SecretRef<string>;\nexport function secret<T>(options: SecretOptionsWithTransform<T>): SecretRef<T>;\nexport function secret<T = string>(\n options?: SecretOptions | SecretOptionsWithTransform<T>\n): SecretRef<T> {\n return {\n __brand: \"effortless-secret\",\n ...(options?.key ? { key: options.key } : {}),\n ...(options?.generate ? { generate: options.generate } : {}),\n ...(\"transform\" in (options ?? {}) ? { transform: (options as SecretOptionsWithTransform<T>).transform } : {}),\n } as SecretRef<T>;\n}\n\n// ============ Secret generators ============\n\n/**\n * Returns a generator that produces a random hex string.\n * @param bytes - Number of random bytes (output will be 2x this length in hex chars)\n */\nexport const generateHex = (bytes: number) => (): string => {\n const crypto = require(\"crypto\") as typeof import(\"crypto\");\n return crypto.randomBytes(bytes).toString(\"hex\");\n};\n\n/**\n * Returns a generator that produces a random base64url string.\n * @param bytes - Number of random bytes\n */\nexport const generateBase64 = (bytes: number) => (): string => {\n const crypto = require(\"crypto\") as typeof import(\"crypto\");\n return crypto.randomBytes(bytes).toString(\"base64url\");\n};\n\n/**\n * Returns a generator that produces a random UUID v4.\n */\nexport const generateUuid = () => (): string => {\n const crypto = require(\"crypto\") as typeof import(\"crypto\");\n return crypto.randomUUID();\n};\n\n// ============ Backwards compatibility ============\n\n/** @deprecated Use `SecretRef` instead */\nexport type ParamRef<T = string> = SecretRef<T>;\n/** @deprecated Use `AnySecretRef` instead */\nexport type AnyParamRef = AnySecretRef;\n\n/**\n * @deprecated Use `secret()` instead. `param(\"key\")` is equivalent to `secret({ key: \"key\" })`.\n */\nexport function param(key: string): SecretRef<string>;\nexport function param<T>(key: string, transform: (raw: string) => T): SecretRef<T>;\nexport function param<T = string>(\n key: string,\n transform?: (raw: string) => T\n): SecretRef<T> {\n return {\n __brand: \"effortless-secret\",\n key,\n ...(transform ? { transform } : {}),\n } as SecretRef<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/**\n * Create a schema function that casts input to T without runtime validation.\n * Use when you need T inference alongside other generics (deps, config).\n * For handlers without deps/config, prefer `defineTable<Order>({...})`.\n * For untrusted input, prefer a real parser (Zod, Effect Schema).\n *\n * @example\n * ```typescript\n * export const orders = defineTable({\n * schema: unsafeAs<Order>(),\n * deps: () => ({ notifications }),\n * onRecord: async ({ record, deps }) => { ... }\n * });\n * ```\n */\nexport function unsafeAs<T>(): (input: unknown) => T {\n return (input: unknown) => input as T;\n}\n","import * as crypto from \"crypto\";\nimport type { Duration } from \"./handler-options\";\nimport { toSeconds } from \"./handler-options\";\n\n// ============ Cookie name ============\n\nexport const AUTH_COOKIE_NAME = \"__eff_session\";\n\n// ============ Auth config ============\n\nexport type CookieAuthConfig<_T = undefined> = {\n /** Path to redirect unauthenticated users to */\n loginPath: string;\n /** Paths that don't require authentication. Supports trailing `*` wildcard. */\n public?: string[];\n /** Default session lifetime (default: \"7d\"). Accepts seconds or duration string. */\n expiresIn?: Duration;\n};\n\n/**\n * Branded cookie auth object returned by `defineAuth()`.\n * Pass to `defineApi({ auth })` and `defineStaticSite({ auth })`.\n */\nexport type CookieAuth<T = undefined> = CookieAuthConfig<T> & {\n readonly __brand: \"effortless-cookie-auth\";\n /** @internal phantom type marker for session data */\n readonly __session?: T;\n};\n\n// ============ auth namespace ============\n\n/**\n * Define cookie-based authentication using HMAC-signed tokens.\n *\n * - Middleware (Lambda@Edge) verifies cookie signatures without external calls\n * - API handler gets `auth.grant()` / `auth.revoke()` / `auth.session` helpers\n * - Secret is auto-generated and stored in SSM Parameter Store\n *\n * @typeParam T - Session data type. When provided, `grant(data)` requires typed payload\n * and `auth.session` is typed as `T` in handler args.\n *\n * @example\n * ```typescript\n * type Session = { userId: string; role: \"admin\" | \"user\" };\n *\n * const protect = defineAuth<Session>({\n * loginPath: '/login',\n * public: ['/login', '/assets/*'],\n * expiresIn: '7d',\n * })\n *\n * export const api = defineApi({ auth: protect, ... })\n * export const webapp = defineStaticSite({ auth: protect, ... })\n * ```\n */\nexport const defineAuth = <T = undefined>(options: CookieAuthConfig<T>): CookieAuth<T> => ({\n __brand: \"effortless-cookie-auth\",\n ...options,\n}) as CookieAuth<T>;\n\n// ============ Runtime helpers (API Lambda) ============\n\n/** Grant options for creating a session */\ntype GrantOptions = { expiresIn?: Duration };\n/** Grant response with Set-Cookie header */\ntype GrantResponse = { status: 200; body: { ok: true }; headers: Record<string, string> };\n\n/**\n * Auth helpers injected into API handler callback args when `auth` is configured.\n * @typeParam T - Session data type (undefined = no custom data)\n */\nexport type AuthHelpers<T = undefined> =\n { /** Clear the session cookie. */\n revoke(): { status: 200; body: { ok: true }; headers: Record<string, string> };\n /** The current session data (decoded from cookie). Undefined if no valid session. */\n session: T extends undefined ? undefined : T | undefined;\n }\n & ([T] extends [undefined]\n ? { /** Create a signed session cookie. */ grant(options?: GrantOptions): GrantResponse }\n : { /** Create a signed session cookie with typed data. */ grant(data: T, options?: GrantOptions): GrantResponse });\n\n// ============ Cookie format ============\n// Payload: base64url(JSON.stringify({ exp, ...data }))\n// Cookie value: {payload}.{hmac-sha256(payload, secret)}\n\n/**\n * Auth runtime created once on cold start. Holds the HMAC key.\n * Call `forRequest(cookieValue)` per request to get typed helpers with decoded session.\n * @internal\n */\nexport type AuthRuntime = {\n forRequest(cookieValue: string | undefined): AuthHelpers<any>;\n};\n\n/**\n * Create auth runtime for an API handler.\n * Called once on cold start with the HMAC secret from SSM.\n * @internal\n */\nexport const createAuthRuntime = (secret: string, defaultExpiresIn: number): AuthRuntime => {\n const sign = (payload: string): string =>\n crypto.createHmac(\"sha256\", secret).update(payload).digest(\"base64url\");\n\n const cookieBase = `${AUTH_COOKIE_NAME}=`;\n const cookieAttrs = \"; HttpOnly; Secure; SameSite=Lax; Path=/\";\n\n const decodeSession = (cookieValue: string | undefined): unknown => {\n if (!cookieValue) return undefined;\n const dot = cookieValue.indexOf(\".\");\n if (dot === -1) return undefined;\n const payload = cookieValue.slice(0, dot);\n const sig = cookieValue.slice(dot + 1);\n if (sign(payload) !== sig) return undefined;\n try {\n const parsed = JSON.parse(Buffer.from(payload, \"base64url\").toString(\"utf-8\"));\n if (parsed.exp <= Math.floor(Date.now() / 1000)) return undefined;\n const { exp: _, ...data } = parsed;\n return Object.keys(data).length > 0 ? data : undefined;\n } catch { return undefined; }\n };\n\n return {\n forRequest(cookieValue) {\n return {\n grant(...args: unknown[]) {\n const hasData = args.length > 0 && (typeof args[0] === \"object\" && args[0] !== null && !(\"expiresIn\" in args[0]));\n const data = hasData ? args[0] as Record<string, unknown> : undefined;\n const options = (hasData ? args[1] : args[0]) as GrantOptions | undefined;\n const seconds = options?.expiresIn ? toSeconds(options.expiresIn) : defaultExpiresIn;\n const exp = Math.floor(Date.now() / 1000) + seconds;\n const payload = Buffer.from(JSON.stringify({ exp, ...data }), \"utf-8\").toString(\"base64url\");\n const sig = sign(payload);\n return {\n status: 200 as const,\n body: { ok: true as const },\n headers: {\n \"set-cookie\": `${cookieBase}${payload}.${sig}${cookieAttrs}; Max-Age=${seconds}`,\n },\n };\n },\n revoke() {\n return {\n status: 200 as const,\n body: { ok: true as const },\n headers: {\n \"set-cookie\": `${cookieBase}${cookieAttrs}; Max-Age=0`,\n },\n };\n },\n session: decodeSession(cookieValue),\n } as AuthHelpers<any>;\n },\n };\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":"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","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/auth.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 Lambda 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 lambda?: {\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/**\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, Duration } from \"./handler-options\";\nimport type { TableClient } from \"../runtime/table-client\";\nimport type { AnyDepHandler, ResolveDeps } from \"./handler-deps\";\nimport type { StaticFiles } from \"./shared\";\nimport type { HandlerArgs } from \"./handler-args\";\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 = {\n /** Lambda function settings (memory, timeout, permissions, etc.) */\n lambda?: 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 to gather records before invoking (default: `\"2s\"`). Accepts `\"5s\"`, `\"1m\"`, etc. */\n batchWindow?: Duration;\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<{ type: \"order\"; amount: number }>({\n * tagField: \"type\",\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 & HandlerArgs<C, D, P, S>\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 & HandlerArgs<C, D, P, S>\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 & HandlerArgs<C, D, P, S>\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?: (args: { error: unknown } & HandlerArgs<C, D, P, S>) => void;\n /** Called after each invocation completes, right before Lambda freezes the process */\n onAfterInvoke?: (args: HandlerArgs<C, D, P, S>) => void | Promise<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 * Pass a function returning the deps object: `deps: () => ({ orders })`.\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 = any> = {\n readonly __brand: \"effortless-table\";\n readonly __spec: TableConfig;\n readonly schema?: (input: unknown) => T;\n readonly onError?: (...args: any[]) => any;\n readonly onAfterInvoke?: (...args: any[]) => any;\n readonly setup?: (...args: any[]) => C | Promise<C>;\n readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);\n readonly config?: Record<string, unknown>;\n readonly static?: string[];\n readonly onRecord?: (...args: any[]) => any;\n readonly onBatchComplete?: (...args: any[]) => any;\n readonly onBatch?: (...args: any[]) => any;\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\n * ```typescript\n * type OrderData = { amount: number; status: string };\n *\n * export const orders = defineTable<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> => {\n const { onRecord, onBatchComplete, onBatch, onError, onAfterInvoke, schema, setup, deps, config, static: staticFiles, ...__spec } = options;\n return {\n __brand: \"effortless-table\",\n __spec,\n ...(schema ? { schema } : {}),\n ...(onError ? { onError } : {}),\n ...(onAfterInvoke ? { onAfterInvoke } : {}),\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>;\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 = {\n /** Lambda function settings (memory, timeout, permissions, etc.) */\n lambda?: 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 * lambda: { memory: 1024 },\n * });\n * ```\n */\nexport const defineApp = (options: AppConfig): AppHandler => ({\n __brand: \"effortless-app\",\n __spec: options,\n});\n","import type { Auth } from \"./auth\";\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 /** Cookie-based authentication. Auto-generates Lambda@Edge middleware that verifies signed cookies. */\n auth?: Auth<any>;\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, Duration } from \"./handler-options\";\nimport type { AnyDepHandler, ResolveDeps } from \"./handler-deps\";\nimport type { StaticFiles } from \"./shared\";\nimport type { HandlerArgs } from \"./handler-args\";\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 = {\n /** Lambda function settings (memory, timeout, permissions, etc.) */\n lambda?: LambdaWithPermissions;\n /** Number of messages per Lambda invocation (1-10 for FIFO, default: 10) */\n batchSize?: number;\n /** Maximum time to gather messages before invoking (default: 0). Accepts `\"5s\"`, `\"1m\"`, etc. */\n batchWindow?: Duration;\n /** Visibility timeout (default: max of timeout or 30s). Accepts `\"30s\"`, `\"5m\"`, etc. */\n visibilityTimeout?: Duration;\n /** Message retention period (default: `\"4d\"`). Accepts `\"1h\"`, `\"7d\"`, etc. */\n retentionPeriod?: Duration;\n /** Delivery delay for all messages in the queue (default: 0). Accepts `\"30s\"`, `\"5m\"`, etc. */\n delay?: Duration;\n /** Enable content-based deduplication (default: true) */\n contentBasedDeduplication?: boolean;\n /** Max number of receives before a message is sent to the dead-letter queue (default: 3) */\n maxReceiveCount?: number;\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 & HandlerArgs<C, D, P, S>\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 & HandlerArgs<C, D, P, S>\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?: (args: { error: unknown } & HandlerArgs<C, D, P, S>) => void;\n /** Called after each invocation completes, right before Lambda freezes the process */\n onAfterInvoke?: (args: HandlerArgs<C, D, P, S>) => void | Promise<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 * Pass a function returning the deps object: `deps: () => ({ orders })`.\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 = any> = {\n readonly __brand: \"effortless-fifo-queue\";\n readonly __spec: FifoQueueConfig;\n readonly schema?: (input: unknown) => T;\n readonly onError?: (...args: any[]) => any;\n readonly onAfterInvoke?: (...args: any[]) => any;\n readonly setup?: (...args: any[]) => C | Promise<C>;\n readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);\n readonly config?: Record<string, unknown>;\n readonly static?: string[];\n readonly onMessage?: (...args: any[]) => any;\n readonly onBatch?: (...args: any[]) => any;\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> => {\n const { onMessage, onBatch, onError, onAfterInvoke, schema, setup, deps, config, static: staticFiles, ...__spec } = options;\n return {\n __brand: \"effortless-fifo-queue\",\n __spec,\n ...(schema ? { schema } : {}),\n ...(onError ? { onError } : {}),\n ...(onAfterInvoke ? { onAfterInvoke } : {}),\n ...(setup ? { setup } : {}),\n ...(deps ? { deps } : {}),\n ...(config ? { config } : {}),\n ...(staticFiles ? { static: staticFiles } : {}),\n ...(onMessage ? { onMessage } : {}),\n ...(onBatch ? { onBatch } : {})\n } as FifoQueueHandler<T, C>;\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\";\nimport type { HandlerArgs } from \"./handler-args\";\n\n/**\n * Configuration options for defineBucket.\n */\nexport type BucketConfig = {\n /** Lambda function settings (memory, timeout, permissions, etc.) */\n lambda?: 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 & HandlerArgs<C, D, P, S>\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 & HandlerArgs<C, D, P, S>\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?: (args: { error: unknown } & HandlerArgs<C, D, P, S>) => void;\n /** Called after each invocation completes, right before Lambda freezes the process */\n onAfterInvoke?: (args: HandlerArgs<C, D, P, S>) => void | Promise<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 * Pass a function returning the deps object: `deps: () => ({ uploads })`.\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 = any> = {\n readonly __brand: \"effortless-bucket\";\n readonly __spec: BucketConfig;\n readonly onError?: (...args: any[]) => any;\n readonly onAfterInvoke?: (...args: any[]) => any;\n readonly setup?: (...args: any[]) => C | Promise<C>;\n readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);\n readonly config?: Record<string, unknown>;\n readonly static?: string[];\n readonly onObjectCreated?: (...args: any[]) => any;\n readonly onObjectRemoved?: (...args: any[]) => any;\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> => {\n const { onObjectCreated, onObjectRemoved, onError, onAfterInvoke, setup, deps, config, static: staticFiles, ...__spec } = options;\n return {\n __brand: \"effortless-bucket\",\n __spec,\n ...(onError ? { onError } : {}),\n ...(onAfterInvoke ? { onAfterInvoke } : {}),\n ...(setup ? { setup } : {}),\n ...(deps ? { deps } : {}),\n ...(config ? { config } : {}),\n ...(staticFiles ? { static: staticFiles } : {}),\n ...(onObjectCreated ? { onObjectCreated } : {}),\n ...(onObjectRemoved ? { onObjectRemoved } : {}),\n } as BucketHandler<C>;\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\";\nimport type { Auth, AuthHelpers, ApiTokenStrategy } from \"./auth\";\nimport type { HandlerArgs } from \"./handler-args\";\n\n/** Extract session type T from Auth<T> */\ntype SessionOf<A> = A extends Auth<infer T> ? T : undefined;\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 A extends Auth<any> | undefined = undefined\n> =\n (args: { req: HttpRequest }\n & HandlerArgs<C, D, P, S>\n & ([ST] extends [true] ? { stream: ResponseStream } : {})\n & ([A] extends [undefined] ? {} : { auth: AuthHelpers<SessionOf<A>> })\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 A extends Auth<any> | undefined = undefined\n> =\n (args: { req: HttpRequest }\n & ([T] extends [undefined] ? {} : { data: T })\n & HandlerArgs<C, D, P, S>\n & ([ST] extends [true] ? { stream: ResponseStream } : {})\n & ([A] extends [undefined] ? {} : { auth: AuthHelpers<SessionOf<A>> })\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 = {\n /** Lambda function settings (memory, timeout, permissions, etc.) */\n lambda?: 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 A extends Auth<any> | undefined = undefined\n> = {\n /** Lambda function settings (memory, timeout, permissions, etc.) */\n lambda?: 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 /** Session-based authentication. Injects `auth` helpers (createSession/clearSession/session) into handler args. */\n auth?: A;\n /** API token authentication for external clients (Bearer tokens, API keys). Has access to deps. */\n apiToken?: ApiTokenStrategy<SessionOf<A>, [D] extends [undefined] ? undefined : ResolveDeps<D>>;\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.): `deps: () => ({ users })` */\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?: (args: { error: unknown; req: HttpRequest } & HandlerArgs<C, D, P, S>) => HttpResponse;\n /** Called after each invocation completes, right before Lambda freezes the process */\n onAfterInvoke?: (args: HandlerArgs<C, D, P, S>) => void | Promise<void>;\n\n /** GET routes — query handlers keyed by relative path (e.g., \"/users/{id}\") */\n get?: Record<`/${string}`, ApiGetHandlerFn<C, D, P, S, ST, A>>;\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, A>;\n};\n\n/** Internal handler object created by defineApi */\nexport type ApiHandler<\n T = undefined,\n C = undefined,\n> = {\n readonly __brand: \"effortless-api\";\n readonly __spec: ApiConfig;\n readonly schema?: (input: unknown) => T;\n readonly onError?: (...args: any[]) => any;\n readonly onAfterInvoke?: (...args: any[]) => any;\n readonly setup?: (...args: any[]) => C | Promise<C>;\n readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);\n readonly config?: Record<string, unknown>;\n readonly static?: string[];\n readonly auth?: Auth;\n readonly apiToken?: ApiTokenStrategy<any, any>;\n readonly get?: Record<`/${string}`, (...args: any[]) => any>;\n readonly post?: (...args: any[]) => any;\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 A extends Auth<any> | undefined = undefined\n>(\n options: DefineApiOptions<T, C, D, P, S, ST, A>\n): ApiHandler<T, C> => {\n const { get, post, schema, onError, onAfterInvoke, setup, deps, config, auth: authConfig, apiToken, 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 ...(onAfterInvoke ? { onAfterInvoke } : {}),\n ...(setup ? { setup } : {}),\n ...(deps ? { deps } : {}),\n ...(config ? { config } : {}),\n ...(staticFiles ? { static: staticFiles } : {}),\n ...(authConfig ? { auth: authConfig } : {}),\n ...(apiToken ? { apiToken } : {}),\n } as ApiHandler<T, C>;\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(), unsafeAs(), 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// ============ Duration ============\n\n/**\n * Human-readable duration. Accepts a plain number (seconds) or a string\n * with a unit suffix: `\"30s\"`, `\"5m\"`, `\"1h\"`, `\"2d\"`.\n *\n * @example\n * ```typescript\n * timeout: 30 // 30 seconds\n * timeout: \"30s\" // 30 seconds\n * timeout: \"5m\" // 300 seconds\n * timeout: \"1h\" // 3600 seconds\n * retentionPeriod: \"4d\" // 345600 seconds\n * ```\n */\nexport type Duration = number | `${number}s` | `${number}m` | `${number}h` | `${number}d`;\n\n/** Convert a Duration to seconds. */\nexport const toSeconds = (d: Duration): number => {\n if (typeof d === \"number\") return d;\n const match = d.match(/^(\\d+(?:\\.\\d+)?)(s|m|h|d)$/);\n if (!match) throw new Error(`Invalid duration: \"${d}\"`);\n const n = Number(match[1]);\n const unit = match[2];\n if (unit === \"d\") return n * 86400;\n if (unit === \"h\") return n * 3600;\n if (unit === \"m\") return n * 60;\n return n;\n};\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 (default: 30s). Accepts seconds or duration string: `\"30s\"`, `\"5m\"` */\n timeout?: Duration;\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// ============ Secrets (SSM Parameters) ============\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnySecretRef = SecretRef<any>;\n\n/**\n * Reference to an SSM Parameter Store secret.\n *\n * @typeParam T - The resolved type after optional transform (default: string)\n */\nexport type SecretRef<T = string> = {\n readonly __brand: \"effortless-secret\";\n readonly key?: string;\n readonly generate?: () => string;\n readonly transform?: (raw: string) => T;\n};\n\n/**\n * Maps a config declaration to resolved value types.\n * `SecretRef<T>` resolves to `T`.\n *\n * @typeParam P - Record of config keys to SecretRef instances\n */\nexport type ResolveConfig<P> = {\n [K in keyof P]: P[K] extends SecretRef<infer T> ? T : string;\n};\n\n/** Options for `secret()` without a transform. */\nexport type SecretOptions = {\n /** Custom SSM key (default: derived from config property name in kebab-case) */\n key?: string;\n /** Generator function called at deploy time if the SSM parameter doesn't exist yet */\n generate?: () => string;\n};\n\n/** Options for `secret()` with a transform. */\nexport type SecretOptionsWithTransform<T> = SecretOptions & {\n /** Transform the raw SSM string value into a typed value */\n transform: (raw: string) => T;\n};\n\n/**\n * Declare an SSM Parameter Store secret.\n *\n * The SSM key is derived from the config property name (camelCase → kebab-case)\n * unless overridden with `key`. The full SSM path is `/${project}/${stage}/${key}`.\n *\n * @param options - Optional configuration (key override, generator, transform)\n * @returns A SecretRef used by the deployment and runtime systems\n *\n * @example Simple secret\n * ```typescript\n * config: {\n * dbUrl: secret(),\n * // → SSM path: /${project}/${stage}/db-url\n * }\n * ```\n *\n * @example Auto-generated secret\n * ```typescript\n * config: {\n * authSecret: secret({ generate: generateHex(64) }),\n * // → auto-creates SSM param if missing\n * }\n * ```\n *\n * @example With transform\n * ```typescript\n * config: {\n * appConfig: secret({ transform: TOML.parse }),\n * }\n * ```\n */\nexport function secret(): SecretRef<string>;\nexport function secret(options: SecretOptions): SecretRef<string>;\nexport function secret<T>(options: SecretOptionsWithTransform<T>): SecretRef<T>;\nexport function secret<T = string>(\n options?: SecretOptions | SecretOptionsWithTransform<T>\n): SecretRef<T> {\n return {\n __brand: \"effortless-secret\",\n ...(options?.key ? { key: options.key } : {}),\n ...(options?.generate ? { generate: options.generate } : {}),\n ...(\"transform\" in (options ?? {}) ? { transform: (options as SecretOptionsWithTransform<T>).transform } : {}),\n } as SecretRef<T>;\n}\n\n// ============ Secret generators ============\n\n/**\n * Returns a generator that produces a random hex string.\n * @param bytes - Number of random bytes (output will be 2x this length in hex chars)\n */\nexport const generateHex = (bytes: number) => (): string => {\n const crypto = require(\"crypto\") as typeof import(\"crypto\");\n return crypto.randomBytes(bytes).toString(\"hex\");\n};\n\n/**\n * Returns a generator that produces a random base64url string.\n * @param bytes - Number of random bytes\n */\nexport const generateBase64 = (bytes: number) => (): string => {\n const crypto = require(\"crypto\") as typeof import(\"crypto\");\n return crypto.randomBytes(bytes).toString(\"base64url\");\n};\n\n/**\n * Returns a generator that produces a random UUID v4.\n */\nexport const generateUuid = () => (): string => {\n const crypto = require(\"crypto\") as typeof import(\"crypto\");\n return crypto.randomUUID();\n};\n\n// ============ Backwards compatibility ============\n\n/** @deprecated Use `SecretRef` instead */\nexport type ParamRef<T = string> = SecretRef<T>;\n/** @deprecated Use `AnySecretRef` instead */\nexport type AnyParamRef = AnySecretRef;\n\n/**\n * @deprecated Use `secret()` instead. `param(\"key\")` is equivalent to `secret({ key: \"key\" })`.\n */\nexport function param(key: string): SecretRef<string>;\nexport function param<T>(key: string, transform: (raw: string) => T): SecretRef<T>;\nexport function param<T = string>(\n key: string,\n transform?: (raw: string) => T\n): SecretRef<T> {\n return {\n __brand: \"effortless-secret\",\n key,\n ...(transform ? { transform } : {}),\n } as SecretRef<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/**\n * Create a schema function that casts input to T without runtime validation.\n * Use when you need T inference alongside other generics (deps, config).\n * For handlers without deps/config, prefer `defineTable<Order>({...})`.\n * For untrusted input, prefer a real parser (Zod, Effect Schema).\n *\n * @example\n * ```typescript\n * export const orders = defineTable({\n * schema: unsafeAs<Order>(),\n * deps: () => ({ notifications }),\n * onRecord: async ({ record, deps }) => { ... }\n * });\n * ```\n */\nexport function unsafeAs<T>(): (input: unknown) => T {\n return (input: unknown) => input as T;\n}\n","import * as crypto from \"crypto\";\nimport type { Duration } from \"./handler-options\";\nimport { toSeconds } from \"./handler-options\";\n\n// ============ Cookie name ============\n\nexport const AUTH_COOKIE_NAME = \"__eff_session\";\n\n// ============ Auth config ============\n\nexport type AuthConfig<_T = undefined> = {\n /** Path to redirect unauthenticated users to (used by static sites). */\n loginPath: string;\n /** Paths that don't require authentication. Supports trailing `*` wildcard. */\n public?: string[];\n /** Default session lifetime (default: \"7d\"). Accepts seconds or duration string. */\n expiresIn?: Duration;\n};\n\n/**\n * Branded auth object returned by `defineAuth()`.\n * Pass to `defineApi({ auth })` and `defineStaticSite({ auth })`.\n */\nexport type Auth<T = undefined> = AuthConfig<T> & {\n readonly __brand: \"effortless-auth\";\n /** @internal phantom type marker for session data */\n readonly __session?: T;\n};\n\n// ============ API Token strategy (used by defineApi) ============\n\n/** API token authentication strategy. Verifies tokens from HTTP headers (e.g. Authorization: Bearer). */\nexport type ApiTokenStrategy<T, D = undefined> = {\n /** HTTP header to read the token from. Default: \"authorization\" (strips \"Bearer \" prefix). */\n header?: string;\n /** Verify the token value and return session data, or null if invalid. */\n verify: [D] extends [undefined]\n ? (value: string) => T | null | Promise<T | null>\n : (value: string, ctx: { deps: D }) => T | null | Promise<T | null>;\n /** Cache verified token results for this duration. Avoids calling verify on every request. */\n cacheTtl?: Duration;\n};\n\n// ============ defineAuth ============\n\n/**\n * Define authentication for API handlers and static sites.\n *\n * Session-based auth uses HMAC-signed cookies (auto-managed by the framework).\n *\n * - Lambda@Edge middleware verifies cookie signatures for static sites\n * - API handler gets `auth.createSession()` / `auth.clearSession()` / `auth.session` helpers\n * - HMAC secret is auto-generated and stored in SSM Parameter Store\n *\n * @typeParam T - Session data type. When provided, `createSession(data)` requires typed payload\n * and `auth.session` is typed as `T` in handler args.\n *\n * @example\n * ```typescript\n * type Session = { userId: string; role: \"admin\" | \"user\" };\n *\n * const auth = defineAuth<Session>({\n * loginPath: '/login',\n * public: ['/login', '/api/login'],\n * expiresIn: '7d',\n * })\n *\n * export const api = defineApi({ auth, ... })\n * export const webapp = defineStaticSite({ auth, ... })\n * ```\n */\nexport const defineAuth = <T = undefined>(options: AuthConfig<T>): Auth<T> => ({\n __brand: \"effortless-auth\",\n ...options,\n}) as Auth<T>;\n\n// ============ Runtime helpers (API Lambda) ============\n\n/** Options for creating a session */\ntype SessionOptions = { expiresIn?: Duration };\n/** Session response with Set-Cookie header */\ntype SessionResponse = { status: 200; body: { ok: true }; headers: Record<string, string> };\n\n/**\n * Auth helpers injected into API handler callback args when `auth` is configured.\n * @typeParam T - Session data type (undefined = no custom data)\n */\nexport type AuthHelpers<T = undefined> =\n { /** Clear the session cookie. */\n clearSession(): { status: 200; body: { ok: true }; headers: Record<string, string> };\n /** The current session data (from cookie or API token). Undefined if no valid session. */\n session: T extends undefined ? undefined : T | undefined;\n }\n & ([T] extends [undefined]\n ? { /** Create a signed session cookie. */ createSession(options?: SessionOptions): SessionResponse }\n : { /** Create a signed session cookie with typed data. */ createSession(data: T, options?: SessionOptions): SessionResponse });\n\n// ============ Cookie format ============\n// Payload: base64url(JSON.stringify({ exp, ...data }))\n// Cookie value: {payload}.{hmac-sha256(payload, secret)}\n\n/**\n * Auth runtime created once on cold start. Holds the HMAC key and optional token verifier.\n * Call `forRequest(cookieValue, authHeader, deps)` per request to get typed helpers.\n * @internal\n */\nexport type AuthRuntime = {\n forRequest(cookieValue: string | undefined, authHeader: string | undefined, deps?: Record<string, unknown>): Promise<AuthHelpers<any>>;\n};\n\n/**\n * Create auth runtime for an API handler.\n * Called once on cold start with the HMAC secret from SSM.\n * @internal\n */\nexport const createAuthRuntime = (\n secret: string,\n defaultExpiresIn: number,\n apiTokenVerify?: (value: string, ctx: { deps: unknown }) => unknown | Promise<unknown>,\n apiTokenHeader?: string,\n apiTokenCacheTtlSeconds?: number,\n): AuthRuntime => {\n // Token verification cache: token → { session, expiresAt }\n const tokenCache = apiTokenCacheTtlSeconds\n ? new Map<string, { session: unknown; expiresAt: number }>()\n : undefined;\n\n const sign = (payload: string): string =>\n crypto.createHmac(\"sha256\", secret).update(payload).digest(\"base64url\");\n\n const cookieBase = `${AUTH_COOKIE_NAME}=`;\n const cookieAttrs = \"; HttpOnly; Secure; SameSite=Lax; Path=/\";\n\n const decodeSession = (cookieValue: string | undefined): unknown => {\n if (!cookieValue) return undefined;\n const dot = cookieValue.indexOf(\".\");\n if (dot === -1) return undefined;\n const payload = cookieValue.slice(0, dot);\n const sig = cookieValue.slice(dot + 1);\n if (sign(payload) !== sig) return undefined;\n try {\n const parsed = JSON.parse(Buffer.from(payload, \"base64url\").toString(\"utf-8\"));\n if (parsed.exp <= Math.floor(Date.now() / 1000)) return undefined;\n const { exp: _, ...data } = parsed;\n return Object.keys(data).length > 0 ? data : undefined;\n } catch { return undefined; }\n };\n\n const extractTokenValue = (headerValue: string): string => {\n const isDefaultHeader = !apiTokenHeader || apiTokenHeader.toLowerCase() === \"authorization\";\n if (isDefaultHeader && headerValue.toLowerCase().startsWith(\"bearer \")) {\n return headerValue.slice(7);\n }\n return headerValue;\n };\n\n const buildHelpers = (sessionData: unknown): AuthHelpers<any> => ({\n createSession(...args: unknown[]) {\n const hasData = args.length > 0 && (typeof args[0] === \"object\" && args[0] !== null && !(\"expiresIn\" in args[0]));\n const data = hasData ? args[0] as Record<string, unknown> : undefined;\n const options = (hasData ? args[1] : args[0]) as SessionOptions | undefined;\n const seconds = options?.expiresIn ? toSeconds(options.expiresIn) : defaultExpiresIn;\n const exp = Math.floor(Date.now() / 1000) + seconds;\n const payload = Buffer.from(JSON.stringify({ exp, ...data }), \"utf-8\").toString(\"base64url\");\n const sig = sign(payload);\n return {\n status: 200 as const,\n body: { ok: true as const },\n headers: {\n \"set-cookie\": `${cookieBase}${payload}.${sig}${cookieAttrs}; Max-Age=${seconds}`,\n },\n };\n },\n clearSession() {\n return {\n status: 200 as const,\n body: { ok: true as const },\n headers: {\n \"set-cookie\": `${cookieBase}${cookieAttrs}; Max-Age=0`,\n },\n };\n },\n session: sessionData,\n } as AuthHelpers<any>);\n\n return {\n async forRequest(cookieValue, authHeader, deps) {\n // API token takes priority over cookie\n if (authHeader && apiTokenVerify) {\n const tokenValue = extractTokenValue(authHeader);\n\n // Check cache\n if (tokenCache) {\n const cached = tokenCache.get(tokenValue);\n if (cached && cached.expiresAt > Date.now()) {\n return buildHelpers(cached.session);\n }\n }\n\n const session = await apiTokenVerify(tokenValue, { deps });\n\n // Store in cache\n if (tokenCache && apiTokenCacheTtlSeconds) {\n tokenCache.set(tokenValue, { session, expiresAt: Date.now() + apiTokenCacheTtlSeconds * 1000 });\n }\n\n return buildHelpers(session);\n }\n\n // Fall back to cookie-based session\n return buildHelpers(decodeSession(cookieValue));\n },\n };\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":"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","names":[]}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  AUTH_COOKIE_NAME,
3
3
  createHandlerRuntime
4
- } from "../chunk-UXSZNW5A.js";
4
+ } from "../chunk-EZG3NX42.js";
5
5
 
6
6
  // src/runtime/wrap-api.ts
7
7
  var CONTENT_TYPE_MAP = {
@@ -24,7 +24,7 @@ var parseBody = (body, isBase64) => {
24
24
  }
25
25
  };
26
26
  var buildGetMatchers = (routes, basePath) => Object.entries(routes).map(([pattern, handler]) => {
27
- const fullPattern = basePath + pattern;
27
+ const fullPattern = (basePath + pattern).replace(/\/\/+/g, "/");
28
28
  const paramNames = [];
29
29
  const regexStr = fullPattern.replace(/\{(\w+)\}/g, (_, name) => {
30
30
  paramNames.push(name);
@@ -69,6 +69,14 @@ var notFound = () => ({
69
69
  headers: { "Content-Type": "application/json" },
70
70
  body: JSON.stringify({ error: "Not Found" })
71
71
  });
72
+ var unauthorized = () => ({
73
+ statusCode: 401,
74
+ headers: { "Content-Type": "application/json" },
75
+ body: JSON.stringify({ error: "Unauthorized" })
76
+ });
77
+ var isPublicPath = (path, patterns) => patterns.some(
78
+ (p) => p.endsWith("*") ? path.startsWith(p.slice(0, -1)) : path === p
79
+ );
72
80
  var wrapApi = (handler) => {
73
81
  const rt = createHandlerRuntime(handler, "api", handler.__spec.lambda?.logLevel ?? "info");
74
82
  const basePath = handler.__spec.basePath;
@@ -105,7 +113,18 @@ var wrapApi = (handler) => {
105
113
  const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${AUTH_COOKIE_NAME}=([^;]+)`));
106
114
  if (match) authCookie = match[1];
107
115
  }
108
- sharedArgs = await rt.commonArgs(authCookie);
116
+ const authHeaderName = handler.apiToken?.header ?? "authorization";
117
+ const authHeader = req.headers[authHeaderName] ?? req.headers[authHeaderName.toLowerCase()] ?? void 0;
118
+ sharedArgs = await rt.commonArgs(authCookie, authHeader);
119
+ if (handler.auth && sharedArgs.auth) {
120
+ const auth = sharedArgs.auth;
121
+ const publicPaths = handler.auth.public ?? [];
122
+ const routePath = req.path.replace(new RegExp(`^${basePath}`), "") || "/";
123
+ if (!auth.session && !isPublicPath(routePath, publicPaths) && !isPublicPath(req.path, publicPaths)) {
124
+ rt.logExecution(startTime, input, { status: 401 });
125
+ return unauthorized();
126
+ }
127
+ }
109
128
  if (req.method === "GET" || req.method === "HEAD") {
110
129
  const matched = matchRoute(getMatchers, req.path);
111
130
  if (!matched) {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createBucketClient,
3
3
  createHandlerRuntime
4
- } from "../chunk-UXSZNW5A.js";
4
+ } from "../chunk-EZG3NX42.js";
5
5
 
6
6
  // src/runtime/wrap-bucket.ts
7
7
  var ENV_DEP_SELF = "EFF_DEP_SELF";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createHandlerRuntime
3
- } from "../chunk-UXSZNW5A.js";
3
+ } from "../chunk-EZG3NX42.js";
4
4
 
5
5
  // src/runtime/wrap-fifo-queue.ts
6
6
  var parseMessages = (rawRecords, schema) => {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createHandlerRuntime,
3
3
  createTableClient
4
- } from "../chunk-UXSZNW5A.js";
4
+ } from "../chunk-EZG3NX42.js";
5
5
 
6
6
  // src/runtime/wrap-table-stream.ts
7
7
  import { unmarshall } from "@aws-sdk/util-dynamodb";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effortless-aws",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Code-first AWS Lambda framework. Export handlers, deploy with one command.",