effortless-aws 0.36.1 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,48 +2,49 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/effortless-aws)](https://www.npmjs.com/package/effortless-aws)
4
4
 
5
- Code-first AWS Lambda framework. Export handlers, deploy with one command. No YAML, no CloudFormation, no state files.
5
+ You write handlers. The framework builds, bundles, provisions AWS resources, wires IAM permissions, and deploys all from your TypeScript code.
6
6
 
7
- ```bash
8
- npm install effortless-aws
9
- ```
7
+ Your TypeScript is the single source of truth — for code and infrastructure.
10
8
 
11
- ## What it looks like
9
+ ## You write this
12
10
 
13
11
  ```typescript
14
- import { defineApi } from "effortless-aws";
12
+ import { defineApi, defineTable } from "effortless-aws";
13
+
14
+ const db = defineTable<Order>();
15
15
 
16
- export const hello = defineApi({ basePath: "/hello" })
17
- .get("/", async ({ ok }) => ok({ message: "Hello!" }));
16
+ export const api = defineApi({ basePath: "/orders" })
17
+ .deps(() => ({ db }))
18
+ .get("/{id}", async ({ params, deps, ok }) => {
19
+ const order = await deps.db.get(params.id);
20
+ return ok(order);
21
+ });
18
22
  ```
19
23
 
20
- ## Handlers
24
+ ## You run this
21
25
 
22
- | Handler | Description |
23
- |---------|-------------|
24
- | `defineApi` | HTTP API with typed GET/POST routes via Lambda Function URL |
25
- | `defineApp` | SSR framework deployment (Nuxt, Next.js) via CloudFront |
26
- | `defineTable` | DynamoDB table with stream processing |
27
- | `defineFifoQueue` | SQS FIFO queue consumer |
28
- | `defineBucket` | S3 bucket with event triggers |
29
- | `defineMailer` | SES email sending |
30
- | `defineStaticSite` | CloudFront + S3 static site with optional middleware |
26
+ ```bash
27
+ eff deploy
28
+ ```
31
29
 
32
- ## Features
30
+ ## The framework handles the rest
33
31
 
34
- - **Infrastructure from code** — export a handler, get the AWS resources
35
- - **Typed everything** — `defineTable<Order>()` gives you typed `put()`, typed `deps.orders.get()`, typed `record.new`
36
- - **Cross-handler deps** — `.deps(() => ({ orders }))` auto-wires IAM and injects a typed `TableClient`
37
- - **SSM params** — `.config(({ defineSecret }) => ...)` fetches secrets from Parameter Store at cold start
38
- - **Static files** — `static: ["templates/*.ejs"]` bundles files into the Lambda ZIP
39
- - **Cold start caching** — `setup` factory runs once per cold start, cached across invocations
32
+ From the example above, `eff deploy` will:
40
33
 
41
- Deploy with [`@effortless-aws/cli`](https://www.npmjs.com/package/@effortless-aws/cli).
34
+ - **Bundle** your code with esbuild and package dependencies into a Lambda layer
35
+ - **Create a DynamoDB table** with streams and indexes — from `defineTable<Order>()`
36
+ - **Create a Lambda** with a public HTTP endpoint — from `defineApi()`
37
+ - **Wire IAM permissions** so the API can read/write the table — from `.deps(() => ({ db }))`
38
+ - **Type everything** — `deps.db.get()` returns `Order`, no casts, no `as any`
39
+
40
+ The same principle works for S3 buckets, SQS queues, SES email, static sites, SSR apps, cron jobs — define a handler, the infrastructure follows.
42
41
 
43
42
  ## Documentation
44
43
 
45
44
  Full docs, examples, and API reference: **[effortless-aws.website](https://effortless-aws.website)**
46
45
 
46
+ Deploy with [`@effortless-aws/cli`](https://www.npmjs.com/package/@effortless-aws/cli).
47
+
47
48
  ## License
48
49
 
49
50
  MIT
@@ -0,0 +1,11 @@
1
+ import {
2
+ AUTH_COOKIE_NAME,
3
+ createAuthRuntime,
4
+ signCfCookies
5
+ } from "./chunk-4UKWMWM5.js";
6
+ import "./chunk-VONNUUEN.js";
7
+ export {
8
+ AUTH_COOKIE_NAME,
9
+ createAuthRuntime,
10
+ signCfCookies
11
+ };
@@ -0,0 +1,9 @@
1
+ import {
2
+ createBucketClient,
3
+ createBucketClientWithEntities
4
+ } from "./chunk-UU3AKDJU.js";
5
+ import "./chunk-U56MLLWP.js";
6
+ export {
7
+ createBucketClient,
8
+ createBucketClientWithEntities
9
+ };
@@ -0,0 +1,268 @@
1
+ import {
2
+ toSeconds
3
+ } from "./chunk-VONNUUEN.js";
4
+
5
+ // src/runtime/handler-utils.ts
6
+ import { readFileSync } from "fs";
7
+ import { join } from "path";
8
+ var lazyTableClient = async (name, opts) => {
9
+ const { createTableClient } = await import("./table-client-BBLIC3EV.js");
10
+ return createTableClient(name, opts);
11
+ };
12
+ var lazyBucketClient = async (name) => {
13
+ const { createBucketClient } = await import("./bucket-client-FRWISKEB.js");
14
+ return createBucketClient(name);
15
+ };
16
+ var lazyBucketClientWithEntities = async (name, config) => {
17
+ const { createBucketClientWithEntities } = await import("./bucket-client-FRWISKEB.js");
18
+ return createBucketClientWithEntities(name, config);
19
+ };
20
+ var lazyEmailClient = async () => {
21
+ const { createEmailClient } = await import("./email-client-RMDQDOYI.js");
22
+ return createEmailClient();
23
+ };
24
+ var lazyQueueClient = async (name) => {
25
+ const { createQueueClient } = await import("./queue-client-WDAJYGQO.js");
26
+ return createQueueClient(name);
27
+ };
28
+ var lazyWorkerClient = async (name) => {
29
+ const { createWorkerClient } = await import("./worker-client-ZS25XCC2.js");
30
+ return createWorkerClient(name);
31
+ };
32
+ var lazyGetParameters = async (paths) => {
33
+ const { getParameters } = await import("./ssm-client-2XDJC3HF.js");
34
+ return getParameters(paths);
35
+ };
36
+ var lazyCreateAuthRuntime = async (secret, defaultExpiresIn, apiTokenVerify, apiTokenHeader, apiTokenCacheTtlSeconds, cfSigningConfig) => {
37
+ const { createAuthRuntime } = await import("./auth-YWMYFMBX.js");
38
+ return createAuthRuntime(secret, defaultExpiresIn, apiTokenVerify, apiTokenHeader, apiTokenCacheTtlSeconds, cfSigningConfig);
39
+ };
40
+ var ENV_DEP_PREFIX = "EFF_DEP_";
41
+ var ENV_PARAM_PREFIX = "EFF_PARAM_";
42
+ var LOG_RANK = { error: 0, info: 1, debug: 2 };
43
+ var truncate = (value, maxLength = 4096) => {
44
+ if (value === void 0 || value === null) return value;
45
+ const str = typeof value === "string" ? value : JSON.stringify(value);
46
+ if (str.length <= maxLength) return value;
47
+ return str.slice(0, maxLength) + "...[truncated]";
48
+ };
49
+ var DEP_FACTORIES = {
50
+ table: (name, depHandler) => {
51
+ const tagField = depHandler?.__spec?.tagField;
52
+ return lazyTableClient(name, tagField ? { tagField } : void 0);
53
+ },
54
+ bucket: (name, depHandler) => {
55
+ const entities = depHandler?.__spec?.entities;
56
+ if (entities && Object.keys(entities).length > 0) {
57
+ const config = {};
58
+ for (const [entityName, entityOpts] of Object.entries(entities)) {
59
+ config[entityName] = entityOpts.cache ? { cacheSeconds: toSeconds(entityOpts.cache) } : {};
60
+ }
61
+ return lazyBucketClientWithEntities(name, config);
62
+ }
63
+ return lazyBucketClient(name);
64
+ },
65
+ mailer: () => lazyEmailClient(),
66
+ queue: (name) => lazyQueueClient(name),
67
+ worker: (name) => lazyWorkerClient(name)
68
+ };
69
+ var parseDepValue = (raw) => {
70
+ const idx = raw.indexOf(":");
71
+ return { type: raw.slice(0, idx), name: raw.slice(idx + 1) };
72
+ };
73
+ var resolveDepsInput = (deps) => typeof deps === "function" ? deps() : deps;
74
+ var buildDeps = async (rawDeps) => {
75
+ const deps = resolveDepsInput(rawDeps);
76
+ if (!deps) return void 0;
77
+ const result = {};
78
+ const entries = Object.keys(deps).map((key) => {
79
+ const raw = process.env[`${ENV_DEP_PREFIX}${key}`];
80
+ if (!raw) throw new Error(`Missing environment variable ${ENV_DEP_PREFIX}${key} for dep "${key}"`);
81
+ const { type, name } = parseDepValue(raw);
82
+ const factory = DEP_FACTORIES[type];
83
+ if (!factory) throw new Error(`Unknown dep type "${type}" for dep "${key}"`);
84
+ return { key, promise: factory(name, deps[key]) };
85
+ });
86
+ for (const { key, promise } of entries) {
87
+ result[key] = await promise;
88
+ }
89
+ return result;
90
+ };
91
+ var buildParams = async (params) => {
92
+ if (!params) return void 0;
93
+ const entries = [];
94
+ for (const propName of Object.keys(params)) {
95
+ const ssmPath = process.env[`${ENV_PARAM_PREFIX}${propName}`];
96
+ if (!ssmPath) {
97
+ throw new Error(`Missing environment variable ${ENV_PARAM_PREFIX}${propName} for param "${propName}"`);
98
+ }
99
+ entries.push({ propName, ssmPath });
100
+ }
101
+ if (entries.length === 0) return void 0;
102
+ const values = await lazyGetParameters(entries.map((e) => e.ssmPath));
103
+ const result = {};
104
+ for (const { propName, ssmPath } of entries) {
105
+ const raw = values.get(ssmPath) ?? "";
106
+ const ref = params[propName];
107
+ const transform = typeof ref === "object" && ref !== null && "transform" in ref && typeof ref.transform === "function" ? ref.transform : void 0;
108
+ result[propName] = transform ? transform(raw) : raw;
109
+ }
110
+ return result;
111
+ };
112
+ var resolvePath = (filePath) => join(process.cwd(), filePath);
113
+ var staticFiles = {
114
+ read: (filePath) => readFileSync(resolvePath(filePath), "utf-8"),
115
+ readBuffer: (filePath) => readFileSync(resolvePath(filePath)),
116
+ path: resolvePath
117
+ };
118
+ var createHandlerRuntime = (handler, handlerType, logLevel = "info", extraSetupArgs) => {
119
+ const handlerName = process.env.EFF_HANDLER ?? "unknown";
120
+ const rank = LOG_RANK[logLevel];
121
+ let ctx = null;
122
+ let resolvedAuthRuntime = null;
123
+ let depsPromise = null;
124
+ const getDeps = () => depsPromise ??= buildDeps(handler.deps);
125
+ let configPromise = null;
126
+ const getConfig = () => configPromise ??= buildParams(handler.config);
127
+ let resolvedCfSigningConfig = null;
128
+ const getCfSigningConfig = async () => {
129
+ if (resolvedCfSigningConfig !== null) return resolvedCfSigningConfig;
130
+ const cfSigningKeySsmPath = process.env.EFF_CF_SIGNING_KEY;
131
+ const cfKeyPairId = process.env.EFF_CF_KEY_PAIR_ID;
132
+ const cfDomain = process.env.EFF_CF_DOMAIN;
133
+ if (!cfSigningKeySsmPath || !cfKeyPairId || !cfDomain) {
134
+ resolvedCfSigningConfig = void 0;
135
+ return void 0;
136
+ }
137
+ const values = await lazyGetParameters([cfSigningKeySsmPath]);
138
+ const privateKey = values.get(cfSigningKeySsmPath);
139
+ if (!privateKey) {
140
+ resolvedCfSigningConfig = void 0;
141
+ return void 0;
142
+ }
143
+ resolvedCfSigningConfig = { privateKey, keyPairId: cfKeyPairId, domain: cfDomain };
144
+ return resolvedCfSigningConfig;
145
+ };
146
+ const getAuthRuntime = async () => {
147
+ if (resolvedAuthRuntime !== null) return resolvedAuthRuntime;
148
+ if (!handler.authFn) {
149
+ resolvedAuthRuntime = void 0;
150
+ return void 0;
151
+ }
152
+ const config = await getConfig();
153
+ const deps = await getDeps();
154
+ const authArgs = {};
155
+ if (config) authArgs.config = config;
156
+ if (deps) authArgs.deps = deps;
157
+ const authOpts = await handler.authFn(authArgs);
158
+ if (!authOpts?.secret) {
159
+ resolvedAuthRuntime = void 0;
160
+ return void 0;
161
+ }
162
+ const secret = authOpts.secret;
163
+ resolvedAuthHeaderName = authOpts.apiToken?.header;
164
+ const defaultExpires = authOpts.expiresIn ? toSeconds(authOpts.expiresIn) : 604800;
165
+ const apiToken = authOpts.apiToken;
166
+ const cacheTtlSeconds = apiToken?.cacheTtl ? toSeconds(apiToken.cacheTtl) : void 0;
167
+ const rawVerify = apiToken?.verify;
168
+ const wrappedVerify = rawVerify ? (args) => rawVerify(args.value) : void 0;
169
+ const cfSigningConfig = await getCfSigningConfig();
170
+ resolvedAuthRuntime = await lazyCreateAuthRuntime(
171
+ secret,
172
+ defaultExpires,
173
+ wrappedVerify,
174
+ apiToken?.header,
175
+ cacheTtlSeconds,
176
+ cfSigningConfig
177
+ );
178
+ return resolvedAuthRuntime;
179
+ };
180
+ const getSetup = async () => {
181
+ if (ctx !== null) return ctx;
182
+ if (handler.setup) {
183
+ const config = await getConfig();
184
+ const deps = await getDeps();
185
+ const args = {};
186
+ if (config) args.config = config;
187
+ if (deps) args.deps = deps;
188
+ if (handler.static) args.files = staticFiles;
189
+ if (extraSetupArgs) Object.assign(args, await extraSetupArgs());
190
+ ctx = await handler.setup(args);
191
+ }
192
+ return ctx;
193
+ };
194
+ let resolvedAuthHeaderName;
195
+ const commonArgs = async (cookieValue, authHeader, headers) => {
196
+ const args = {};
197
+ if (handler.setup) args.ctx = await getSetup();
198
+ const deps = await getDeps();
199
+ if (deps) args.deps = deps;
200
+ const config = await getConfig();
201
+ if (config) args.config = config;
202
+ if (handler.static) args.files = staticFiles;
203
+ const authRuntime = await getAuthRuntime();
204
+ if (authRuntime) {
205
+ let finalAuthHeader = authHeader;
206
+ if (finalAuthHeader === void 0 && headers && resolvedAuthHeaderName) {
207
+ finalAuthHeader = headers[resolvedAuthHeaderName] ?? headers[resolvedAuthHeaderName.toLowerCase()] ?? void 0;
208
+ }
209
+ args.auth = await authRuntime.forRequest(cookieValue, finalAuthHeader);
210
+ }
211
+ return args;
212
+ };
213
+ const logExecution = (startTime, input, output) => {
214
+ if (rank < LOG_RANK.info) return;
215
+ const entry = {
216
+ level: "info",
217
+ handler: handlerName,
218
+ type: handlerType,
219
+ ms: Date.now() - startTime
220
+ };
221
+ if (rank >= LOG_RANK.debug) {
222
+ entry.input = truncate(input);
223
+ entry.output = truncate(output);
224
+ }
225
+ console.log(JSON.stringify(entry));
226
+ };
227
+ const logError = (startTime, input, error) => {
228
+ const entry = {
229
+ level: "error",
230
+ handler: handlerName,
231
+ type: handlerType,
232
+ ms: Date.now() - startTime,
233
+ error: error instanceof Error ? error.message : String(error)
234
+ };
235
+ if (rank >= LOG_RANK.debug) {
236
+ entry.input = truncate(input);
237
+ }
238
+ console.error(JSON.stringify(entry));
239
+ };
240
+ const noop = () => {
241
+ };
242
+ const saved = { log: console.log, info: console.info, debug: console.debug };
243
+ const patchConsole = () => {
244
+ if (rank < LOG_RANK.debug) console.debug = noop;
245
+ if (rank < LOG_RANK.info) {
246
+ console.log = noop;
247
+ console.info = noop;
248
+ }
249
+ };
250
+ const restoreConsole = () => {
251
+ console.log = saved.log;
252
+ console.info = saved.info;
253
+ console.debug = saved.debug;
254
+ };
255
+ const preload = async () => {
256
+ await getDeps();
257
+ await getConfig();
258
+ await getAuthRuntime();
259
+ await getSetup();
260
+ };
261
+ return { preload, commonArgs, logExecution, logError, patchConsole, restoreConsole, handlerName };
262
+ };
263
+
264
+ export {
265
+ buildDeps,
266
+ buildParams,
267
+ createHandlerRuntime
268
+ };
@@ -0,0 +1,113 @@
1
+ import {
2
+ toSeconds
3
+ } from "./chunk-VONNUUEN.js";
4
+
5
+ // src/handlers/auth.ts
6
+ import * as crypto from "crypto";
7
+ var cfBase64Encode = (buffer) => buffer.toString("base64").replace(/\+/g, "-").replace(/=/g, "_").replace(/\//g, "~");
8
+ var signCfCookies = (policy, config) => {
9
+ const ttlSeconds = toSeconds(policy.ttl);
10
+ const expireTime = Math.floor(Date.now() / 1e3) + ttlSeconds;
11
+ const resource = config.domain === "*" ? `https://*${policy.path}` : `https://${config.domain}${policy.path}`;
12
+ const policyJson = JSON.stringify({
13
+ Statement: [{
14
+ Resource: resource,
15
+ Condition: {
16
+ DateLessThan: { "AWS:EpochTime": expireTime }
17
+ }
18
+ }]
19
+ });
20
+ const policyBase64 = cfBase64Encode(Buffer.from(policyJson, "utf-8"));
21
+ const signature = cfBase64Encode(
22
+ crypto.sign("sha1", Buffer.from(policyJson, "utf-8"), config.privateKey)
23
+ );
24
+ const cookieAttrs = `; Secure; SameSite=Lax; Path=/; Max-Age=${ttlSeconds}`;
25
+ return [
26
+ `CloudFront-Policy=${policyBase64}${cookieAttrs}`,
27
+ `CloudFront-Signature=${signature}${cookieAttrs}`,
28
+ `CloudFront-Key-Pair-Id=${config.keyPairId}${cookieAttrs}`
29
+ ];
30
+ };
31
+ var AUTH_COOKIE_NAME = "__eff_session";
32
+ var createAuthRuntime = (secret, defaultExpiresIn, apiTokenVerify, apiTokenHeader, apiTokenCacheTtlSeconds, cfSigningConfig) => {
33
+ const tokenCache = apiTokenCacheTtlSeconds ? /* @__PURE__ */ new Map() : void 0;
34
+ const sign2 = (payload) => crypto.createHmac("sha256", secret).update(payload).digest("base64url");
35
+ const cookieBase = `${AUTH_COOKIE_NAME}=`;
36
+ const cookieAttrs = "; HttpOnly; Secure; SameSite=Lax; Path=/";
37
+ const decodeSession = (cookieValue) => {
38
+ if (!cookieValue) return void 0;
39
+ const dot = cookieValue.indexOf(".");
40
+ if (dot === -1) return void 0;
41
+ const payload = cookieValue.slice(0, dot);
42
+ const sig = cookieValue.slice(dot + 1);
43
+ if (sign2(payload) !== sig) return void 0;
44
+ try {
45
+ const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
46
+ if (parsed.exp <= Math.floor(Date.now() / 1e3)) return void 0;
47
+ const { exp: _, ...data } = parsed;
48
+ return Object.keys(data).length > 0 ? data : void 0;
49
+ } catch {
50
+ return void 0;
51
+ }
52
+ };
53
+ const extractTokenValue = (headerValue) => {
54
+ const isDefaultHeader = !apiTokenHeader || apiTokenHeader.toLowerCase() === "authorization";
55
+ if (isDefaultHeader && headerValue.toLowerCase().startsWith("bearer ")) {
56
+ return headerValue.slice(7);
57
+ }
58
+ return headerValue;
59
+ };
60
+ const buildHelpers = (sessionData) => ({
61
+ createSession(data, options) {
62
+ const seconds = options?.expiresIn ? toSeconds(options.expiresIn) : defaultExpiresIn;
63
+ const exp = Math.floor(Date.now() / 1e3) + seconds;
64
+ const payload = Buffer.from(JSON.stringify({ exp, ...data }), "utf-8").toString("base64url");
65
+ const sig = sign2(payload);
66
+ const sessionCookie = `${cookieBase}${payload}.${sig}${cookieAttrs}; Max-Age=${seconds}`;
67
+ const cfCookies = options?.cdnPolicy && cfSigningConfig ? signCfCookies(options.cdnPolicy, cfSigningConfig) : void 0;
68
+ return {
69
+ status: 200,
70
+ body: { ok: true },
71
+ headers: {
72
+ "set-cookie": sessionCookie
73
+ },
74
+ ...cfCookies ? { cookies: [sessionCookie, ...cfCookies] } : {}
75
+ };
76
+ },
77
+ clearSession() {
78
+ return {
79
+ status: 200,
80
+ body: { ok: true },
81
+ headers: {
82
+ "set-cookie": `${cookieBase}${cookieAttrs}; Max-Age=0`
83
+ }
84
+ };
85
+ },
86
+ session: sessionData
87
+ });
88
+ return {
89
+ async forRequest(cookieValue, authHeader) {
90
+ if (authHeader && apiTokenVerify) {
91
+ const tokenValue = extractTokenValue(authHeader);
92
+ if (tokenCache) {
93
+ const cached = tokenCache.get(tokenValue);
94
+ if (cached && cached.expiresAt > Date.now()) {
95
+ return buildHelpers(cached.session);
96
+ }
97
+ }
98
+ const session = await apiTokenVerify({ value: tokenValue });
99
+ if (tokenCache && apiTokenCacheTtlSeconds) {
100
+ tokenCache.set(tokenValue, { session, expiresAt: Date.now() + apiTokenCacheTtlSeconds * 1e3 });
101
+ }
102
+ return buildHelpers(session);
103
+ }
104
+ return buildHelpers(decodeSession(cookieValue));
105
+ }
106
+ };
107
+ };
108
+
109
+ export {
110
+ signCfCookies,
111
+ AUTH_COOKIE_NAME,
112
+ createAuthRuntime
113
+ };