@smplcty/dev-backend 0.1.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 ADDED
@@ -0,0 +1,110 @@
1
+ # @smplcty/dev-backend
2
+
3
+ Local Lambda dev server. Reads a routes config, loads env vars, and
4
+ serves Lambda handlers over HTTP with API Gateway v2 event
5
+ translation.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pnpm dlx @smplcty/dev-backend
11
+ ```
12
+
13
+ Or as a dev dependency:
14
+
15
+ ```sh
16
+ pnpm add -D @smplcty/dev-backend
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```sh
22
+ dev-backend [config-path]
23
+ ```
24
+
25
+ Default config path is `back-end.routes.json` in the current
26
+ directory.
27
+
28
+ ## Config
29
+
30
+ `back-end.routes.json` — committed to your frontend repo:
31
+
32
+ ```json
33
+ {
34
+ "port": 4000,
35
+ "envFile": ".env.back-end",
36
+ "routes": {
37
+ "/graphql": "../handle-graphql/dist/index.js",
38
+ "/sign-in": {
39
+ "handler": "../../mabulu-inc/sign-in/dist/index.js",
40
+ "envFile": ".env.sign-in"
41
+ },
42
+ "/sign-in/verify": {
43
+ "handler": "../../mabulu-inc/sign-in-verify/dist/index.js",
44
+ "envFile": ".env.sign-in"
45
+ },
46
+ "/ai-insights": "../handle-ai-insights/dist/index.js"
47
+ }
48
+ }
49
+ ```
50
+
51
+ ### Route formats
52
+
53
+ **String** — just a handler path. Uses the base `envFile`:
54
+
55
+ ```json
56
+ "/graphql": "../handle-graphql/dist/index.js"
57
+ ```
58
+
59
+ **Object** — handler path plus a per-route env file override:
60
+
61
+ ```json
62
+ "/sign-in": {
63
+ "handler": "../../mabulu-inc/sign-in/dist/index.js",
64
+ "envFile": ".env.sign-in"
65
+ }
66
+ ```
67
+
68
+ Per-route env files override the base env file's values for that
69
+ handler only. This solves the case where two handlers need different
70
+ `DATABASE_URL` values (e.g. graphql vs sign-in connect to different
71
+ databases).
72
+
73
+ All paths are resolved relative to the config file's location.
74
+
75
+ ### Env files
76
+
77
+ `.env.back-end` — shared variables (gitignored):
78
+
79
+ ```
80
+ DATABASE_URL=postgresql://...
81
+ TWILIO_ACCOUNT_SID=AC...
82
+ TWILIO_AUTH_TOKEN=...
83
+ TWILIO_VERIFY_SERVICE_SID=VA...
84
+ LOG_LEVEL=debug
85
+ ```
86
+
87
+ `.env.sign-in` — overrides for sign-in handlers (gitignored):
88
+
89
+ ```
90
+ DATABASE_URL=postgresql://...different-db
91
+ ```
92
+
93
+ Env files use standard `KEY=VALUE` format. Comments (`#`) and blank
94
+ lines are skipped. Surrounding quotes are stripped. Existing env vars
95
+ are not overridden.
96
+
97
+ ## How it works
98
+
99
+ 1. Loads the base env file into `process.env`.
100
+ 2. For each route, if it has a per-route `envFile`, temporarily
101
+ overrides the relevant env vars, imports the handler module
102
+ (so module-level init like Pool creation uses the right values),
103
+ then restores the originals.
104
+ 3. Starts an HTTP server that translates incoming requests into
105
+ API Gateway v2 proxy events and calls the matching handler.
106
+ 4. CORS headers are set on all responses.
107
+
108
+ ## License
109
+
110
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import { loadConfig } from './config.js';
3
+ import { loadEnvFile } from './env.js';
4
+ import { loadHandlers } from './handlers.js';
5
+ import { startServer } from './server.js';
6
+ const configPath = process.argv[2] ?? 'back-end.routes.json';
7
+ console.log(`Loading config from ${configPath}`);
8
+ const config = loadConfig(configPath);
9
+ console.log(`Loading env from ${config.envFile}`);
10
+ loadEnvFile(config.envFile);
11
+ console.log('Loading handlers:');
12
+ const handlers = await loadHandlers(config.routes);
13
+ if (Object.keys(handlers).length === 0) {
14
+ console.error('No handlers loaded — exiting');
15
+ process.exit(1);
16
+ }
17
+ startServer(handlers, config.port);
18
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,sBAAsB,CAAC;AAE7D,OAAO,CAAC,GAAG,CAAC,uBAAuB,UAAU,EAAE,CAAC,CAAC;AACjD,MAAM,MAAM,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;AAEtC,OAAO,CAAC,GAAG,CAAC,oBAAoB,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;AAClD,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AAE5B,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;AACjC,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AAEnD,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IACvC,OAAO,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;IAC9C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,WAAW,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC"}
@@ -0,0 +1,22 @@
1
+ export interface RouteConfig {
2
+ /** Absolute path to the handler module. */
3
+ handler: string;
4
+ /** Optional env file to load before importing this handler.
5
+ * Overrides values from the base envFile. Useful when handlers
6
+ * need different DATABASE_URL values. */
7
+ envFile?: string;
8
+ }
9
+ export interface Config {
10
+ /** HTTP port. Default 4000. */
11
+ port: number;
12
+ /** Path to the base .env file. Default ".env.back-end". */
13
+ envFile: string;
14
+ /** Route → handler config. */
15
+ routes: Record<string, RouteConfig>;
16
+ }
17
+ /**
18
+ * Load and validate the config file. Paths are resolved relative
19
+ * to the config file's directory.
20
+ */
21
+ export declare function loadConfig(configPath: string): Config;
22
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,WAAW;IAC1B,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB;;8CAE0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,MAAM;IACrB,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,OAAO,EAAE,MAAM,CAAC;IAChB,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACrC;AAUD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAiCrD"}
package/dist/config.js ADDED
@@ -0,0 +1,39 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve, dirname } from 'node:path';
3
+ /**
4
+ * Load and validate the config file. Paths are resolved relative
5
+ * to the config file's directory.
6
+ */
7
+ export function loadConfig(configPath) {
8
+ const absPath = resolve(configPath);
9
+ const dir = dirname(absPath);
10
+ let raw;
11
+ try {
12
+ raw = JSON.parse(readFileSync(absPath, 'utf-8'));
13
+ }
14
+ catch (err) {
15
+ const msg = err instanceof Error ? err.message : String(err);
16
+ throw new Error(`Failed to read config ${absPath}: ${msg}`, { cause: err });
17
+ }
18
+ if (!raw.routes || typeof raw.routes !== 'object') {
19
+ throw new Error('Config must have a "routes" object');
20
+ }
21
+ const routes = {};
22
+ for (const [path, entry] of Object.entries(raw.routes)) {
23
+ if (typeof entry === 'string') {
24
+ routes[path] = { handler: resolve(dir, entry) };
25
+ }
26
+ else {
27
+ routes[path] = {
28
+ handler: resolve(dir, entry.handler),
29
+ envFile: entry.envFile ? resolve(dir, entry.envFile) : undefined,
30
+ };
31
+ }
32
+ }
33
+ return {
34
+ port: raw.port ?? 4000,
35
+ envFile: resolve(dir, raw.envFile ?? '.env.back-end'),
36
+ routes,
37
+ };
38
+ }
39
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA4B7C;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,UAAkB;IAC3C,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACpC,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE7B,IAAI,GAAc,CAAC;IACnB,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAc,CAAC;IAChE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7D,MAAM,IAAI,KAAK,CAAC,yBAAyB,OAAO,KAAK,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;IAC9E,CAAC;IAED,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;IACxD,CAAC;IAED,MAAM,MAAM,GAAgC,EAAE,CAAC;IAC/C,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QACvD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9B,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,CAAC;QAClD,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,IAAI,CAAC,GAAG;gBACb,OAAO,EAAE,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,OAAO,CAAC;gBACpC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS;aACjE,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,IAAI,IAAI,IAAI;QACtB,OAAO,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,OAAO,IAAI,eAAe,CAAC;QACrD,MAAM;KACP,CAAC;AACJ,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=config.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.test.d.ts","sourceRoot":"","sources":["../src/config.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { writeFileSync, mkdtempSync, rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { loadConfig } from './config.js';
6
+ describe('loadConfig', () => {
7
+ it('loads a minimal config with string routes', () => {
8
+ const dir = mkdtempSync(join(tmpdir(), 'dev-backend-test-'));
9
+ const configPath = join(dir, 'routes.json');
10
+ writeFileSync(configPath, JSON.stringify({
11
+ routes: { '/graphql': './handler.js' },
12
+ }));
13
+ const config = loadConfig(configPath);
14
+ expect(config.port).toBe(4000);
15
+ expect(config.envFile).toBe(join(dir, '.env.back-end'));
16
+ expect(config.routes['/graphql']?.handler).toBe(join(dir, 'handler.js'));
17
+ expect(config.routes['/graphql']?.envFile).toBeUndefined();
18
+ rmSync(dir, { recursive: true });
19
+ });
20
+ it('loads object routes with envFile', () => {
21
+ const dir = mkdtempSync(join(tmpdir(), 'dev-backend-test-'));
22
+ const configPath = join(dir, 'routes.json');
23
+ writeFileSync(configPath, JSON.stringify({
24
+ port: 5000,
25
+ envFile: '.env.custom',
26
+ routes: {
27
+ '/api': {
28
+ handler: '../api/dist/index.js',
29
+ envFile: '.env.api',
30
+ },
31
+ },
32
+ }));
33
+ const config = loadConfig(configPath);
34
+ expect(config.port).toBe(5000);
35
+ expect(config.envFile).toBe(join(dir, '.env.custom'));
36
+ expect(config.routes['/api']?.envFile).toBe(join(dir, '.env.api'));
37
+ rmSync(dir, { recursive: true });
38
+ });
39
+ it('throws on missing routes', () => {
40
+ const dir = mkdtempSync(join(tmpdir(), 'dev-backend-test-'));
41
+ const configPath = join(dir, 'routes.json');
42
+ writeFileSync(configPath, JSON.stringify({}));
43
+ expect(() => loadConfig(configPath)).toThrow('routes');
44
+ rmSync(dir, { recursive: true });
45
+ });
46
+ it('throws on missing file', () => {
47
+ expect(() => loadConfig('/nonexistent/routes.json')).toThrow('Failed to read');
48
+ });
49
+ });
50
+ //# sourceMappingURL=config.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.test.js","sourceRoot":"","sources":["../src/config.test.ts"],"names":[],"mappings":"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"}
package/dist/env.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Parse a .env file and load its variables into process.env.
3
+ * Does not override existing env vars.
4
+ */
5
+ export declare function loadEnvFile(filePath: string): void;
6
+ //# sourceMappingURL=env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CA+BlD"}
package/dist/env.js ADDED
@@ -0,0 +1,32 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ /**
3
+ * Parse a .env file and load its variables into process.env.
4
+ * Does not override existing env vars.
5
+ */
6
+ export function loadEnvFile(filePath) {
7
+ if (!existsSync(filePath)) {
8
+ console.warn(`env file not found: ${filePath} — skipping`);
9
+ return;
10
+ }
11
+ const content = readFileSync(filePath, 'utf-8');
12
+ for (const line of content.split('\n')) {
13
+ const trimmed = line.trim();
14
+ if (trimmed.length === 0 || trimmed.startsWith('#'))
15
+ continue;
16
+ const eqIndex = trimmed.indexOf('=');
17
+ if (eqIndex === -1)
18
+ continue;
19
+ const key = trimmed.slice(0, eqIndex).trim();
20
+ let value = trimmed.slice(eqIndex + 1).trim();
21
+ // Strip surrounding quotes
22
+ if ((value.startsWith('"') && value.endsWith('"')) ||
23
+ (value.startsWith("'") && value.endsWith("'"))) {
24
+ value = value.slice(1, -1);
25
+ }
26
+ // Don't override existing env vars
27
+ if (process.env[key] === undefined) {
28
+ process.env[key] = value;
29
+ }
30
+ }
31
+ }
32
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEnD;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,OAAO,CAAC,IAAI,CAAC,uBAAuB,QAAQ,aAAa,CAAC,CAAC;QAC3D,OAAO;IACT,CAAC;IAED,MAAM,OAAO,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAEhD,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACvC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAE9D,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,OAAO,KAAK,CAAC,CAAC;YAAE,SAAS;QAE7B,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;QAC7C,IAAI,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAE9C,2BAA2B;QAC3B,IACE,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;YAC9C,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAC9C,CAAC;YACD,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC7B,CAAC;QAED,mCAAmC;QACnC,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,SAAS,EAAE,CAAC;YACnC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QAC3B,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=env.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.test.d.ts","sourceRoot":"","sources":["../src/env.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,56 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import { writeFileSync, mkdtempSync, rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { loadEnvFile } from './env.js';
6
+ describe('loadEnvFile', () => {
7
+ const dirs = [];
8
+ afterEach(() => {
9
+ for (const d of dirs)
10
+ rmSync(d, { recursive: true });
11
+ dirs.length = 0;
12
+ });
13
+ function tmpEnv(content) {
14
+ const dir = mkdtempSync(join(tmpdir(), 'dev-backend-env-'));
15
+ dirs.push(dir);
16
+ const p = join(dir, '.env');
17
+ writeFileSync(p, content);
18
+ return p;
19
+ }
20
+ it('loads key=value pairs into process.env', () => {
21
+ const key = `TEST_ENV_${Date.now()}`;
22
+ const path = tmpEnv(`${key}=hello`);
23
+ loadEnvFile(path);
24
+ expect(process.env[key]).toBe('hello');
25
+ delete process.env[key];
26
+ });
27
+ it('does not override existing env vars', () => {
28
+ const key = `TEST_ENV_EXISTING_${Date.now()}`;
29
+ process.env[key] = 'original';
30
+ const path = tmpEnv(`${key}=override`);
31
+ loadEnvFile(path);
32
+ expect(process.env[key]).toBe('original');
33
+ delete process.env[key];
34
+ });
35
+ it('strips surrounding quotes', () => {
36
+ const key1 = `TEST_DQ_${Date.now()}`;
37
+ const key2 = `TEST_SQ_${Date.now()}`;
38
+ const path = tmpEnv(`${key1}="double"\n${key2}='single'`);
39
+ loadEnvFile(path);
40
+ expect(process.env[key1]).toBe('double');
41
+ expect(process.env[key2]).toBe('single');
42
+ delete process.env[key1];
43
+ delete process.env[key2];
44
+ });
45
+ it('skips comments and blank lines', () => {
46
+ const key = `TEST_COMMENT_${Date.now()}`;
47
+ const path = tmpEnv(`# comment\n\n${key}=yes`);
48
+ loadEnvFile(path);
49
+ expect(process.env[key]).toBe('yes');
50
+ delete process.env[key];
51
+ });
52
+ it('warns on missing file without throwing', () => {
53
+ expect(() => loadEnvFile('/nonexistent/.env')).not.toThrow();
54
+ });
55
+ });
56
+ //# sourceMappingURL=env.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.test.js","sourceRoot":"","sources":["../src/env.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ /**
3
+ * Build an API Gateway v2 (HTTP API) proxy event from an incoming
4
+ * Node.js HTTP request.
5
+ */
6
+ export declare function buildEvent(req: IncomingMessage, path: string, query: string | undefined): Promise<Record<string, unknown>>;
7
+ /**
8
+ * Decompress a gzipped response body returned by a handler.
9
+ */
10
+ export declare function decompressResponse(body: string): string;
11
+ //# sourceMappingURL=event.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event.d.ts","sourceRoot":"","sources":["../src/event.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAGjD;;;GAGG;AACH,wBAAsB,UAAU,CAC9B,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GAAG,SAAS,GACxB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CA0BlC;AAoCD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD"}
package/dist/event.js ADDED
@@ -0,0 +1,70 @@
1
+ import { gunzipSync } from 'node:zlib';
2
+ /**
3
+ * Build an API Gateway v2 (HTTP API) proxy event from an incoming
4
+ * Node.js HTTP request.
5
+ */
6
+ export async function buildEvent(req, path, query) {
7
+ const body = await readBody(req);
8
+ const method = req.method ?? 'GET';
9
+ return {
10
+ version: '2.0',
11
+ headers: req.headers,
12
+ method,
13
+ queryStringParameters: parseQuery(query),
14
+ isBase64Encoded: false,
15
+ rawQueryString: query ?? '',
16
+ requestContext: {
17
+ http: {
18
+ method,
19
+ path,
20
+ protocol: 'HTTP/1.1',
21
+ sourceIp: '127.0.0.1',
22
+ userAgent: req.headers['user-agent'] ?? '',
23
+ },
24
+ requestId: crypto.randomUUID(),
25
+ time: new Date().toISOString(),
26
+ },
27
+ rawPath: path,
28
+ routeKey: `${method} ${path}`,
29
+ body,
30
+ };
31
+ }
32
+ function parseQuery(query) {
33
+ if (!query)
34
+ return undefined;
35
+ const params = {};
36
+ for (const part of query.split('&')) {
37
+ const eqIndex = part.indexOf('=');
38
+ if (eqIndex === -1) {
39
+ params[decodeURIComponent(part)] = '';
40
+ }
41
+ else {
42
+ params[decodeURIComponent(part.slice(0, eqIndex))] =
43
+ decodeURIComponent(part.slice(eqIndex + 1));
44
+ }
45
+ }
46
+ return params;
47
+ }
48
+ function readBody(req) {
49
+ return new Promise((resolve, reject) => {
50
+ const chunks = [];
51
+ req.on('data', (chunk) => chunks.push(chunk));
52
+ req.on('error', reject);
53
+ req.on('end', () => {
54
+ const buf = Buffer.concat(chunks);
55
+ if (req.headers['content-encoding'] === 'gzip') {
56
+ resolve(buf.toString('base64'));
57
+ }
58
+ else {
59
+ resolve(buf.toString());
60
+ }
61
+ });
62
+ });
63
+ }
64
+ /**
65
+ * Decompress a gzipped response body returned by a handler.
66
+ */
67
+ export function decompressResponse(body) {
68
+ return gunzipSync(Buffer.from(body, 'base64')).toString();
69
+ }
70
+ //# sourceMappingURL=event.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event.js","sourceRoot":"","sources":["../src/event.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { RouteConfig } from './config.js';
2
+ export type Handler = (event: Record<string, unknown>) => Promise<{
3
+ statusCode: number;
4
+ body?: string;
5
+ headers?: Record<string, string>;
6
+ }>;
7
+ /**
8
+ * Import all handler modules. For routes with a per-route envFile,
9
+ * temporarily override process.env before importing so the handler's
10
+ * module-level init (e.g. Pool creation) picks up the right values.
11
+ */
12
+ export declare function loadHandlers(routes: Record<string, RouteConfig>): Promise<Record<string, Handler>>;
13
+ //# sourceMappingURL=handlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../src/handlers.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,MAAM,OAAO,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;IAChE,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,CAAC,CAAC;AAEH;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GAClC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CA6ClC"}
@@ -0,0 +1,68 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { pathToFileURL } from 'node:url';
3
+ import { loadEnvFile } from './env.js';
4
+ /**
5
+ * Import all handler modules. For routes with a per-route envFile,
6
+ * temporarily override process.env before importing so the handler's
7
+ * module-level init (e.g. Pool creation) picks up the right values.
8
+ */
9
+ export async function loadHandlers(routes) {
10
+ const handlers = {};
11
+ for (const [path, route] of Object.entries(routes)) {
12
+ // Snapshot env vars that the route's envFile might override
13
+ const overrides = new Map();
14
+ if (route.envFile) {
15
+ const keys = readEnvKeys(route.envFile);
16
+ for (const key of keys) {
17
+ overrides.set(key, process.env[key]);
18
+ delete process.env[key];
19
+ }
20
+ loadEnvFile(route.envFile);
21
+ }
22
+ try {
23
+ const mod = (await import(pathToFileURL(route.handler).href));
24
+ const handler = mod.handler;
25
+ if (typeof handler !== 'function') {
26
+ console.error(` ${path}: ${route.handler} does not export a handler function — skipping`);
27
+ continue;
28
+ }
29
+ handlers[path] = handler;
30
+ console.log(` ${path} → ${route.handler}`);
31
+ }
32
+ catch (err) {
33
+ console.error(` ${path}: failed to import ${route.handler}:`, err);
34
+ continue;
35
+ }
36
+ finally {
37
+ for (const [key, original] of overrides) {
38
+ if (original === undefined) {
39
+ delete process.env[key];
40
+ }
41
+ else {
42
+ process.env[key] = original;
43
+ }
44
+ }
45
+ }
46
+ }
47
+ return handlers;
48
+ }
49
+ /**
50
+ * Parse an env file to extract just the key names.
51
+ */
52
+ function readEnvKeys(filePath) {
53
+ if (!existsSync(filePath))
54
+ return [];
55
+ const keys = [];
56
+ const content = readFileSync(filePath, 'utf-8');
57
+ for (const line of content.split('\n')) {
58
+ const trimmed = line.trim();
59
+ if (trimmed.length === 0 || trimmed.startsWith('#'))
60
+ continue;
61
+ const eqIndex = trimmed.indexOf('=');
62
+ if (eqIndex === -1)
63
+ continue;
64
+ keys.push(trimmed.slice(0, eqIndex).trim());
65
+ }
66
+ return keys;
67
+ }
68
+ //# sourceMappingURL=handlers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handlers.js","sourceRoot":"","sources":["../src/handlers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export { loadConfig, type Config, type RouteConfig } from './config.js';
2
+ export { loadEnvFile } from './env.js';
3
+ export { loadHandlers, type Handler } from './handlers.js';
4
+ export { startServer } from './server.js';
5
+ export { buildEvent, decompressResponse } from './event.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,KAAK,OAAO,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { loadConfig } from './config.js';
2
+ export { loadEnvFile } from './env.js';
3
+ export { loadHandlers } from './handlers.js';
4
+ export { startServer } from './server.js';
5
+ export { buildEvent, decompressResponse } from './event.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAiC,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,YAAY,EAAgB,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { Handler } from './handlers.js';
2
+ /**
3
+ * Start the HTTP server that routes requests to Lambda handlers.
4
+ */
5
+ export declare function startServer(handlers: Record<string, Handler>, port: number): void;
6
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAE7C;;GAEG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,IAAI,EAAE,MAAM,GACX,IAAI,CAkDN"}
package/dist/server.js ADDED
@@ -0,0 +1,55 @@
1
+ import { createServer } from 'node:http';
2
+ import { buildEvent, decompressResponse } from './event.js';
3
+ /**
4
+ * Start the HTTP server that routes requests to Lambda handlers.
5
+ */
6
+ export function startServer(handlers, port) {
7
+ const server = createServer(async (req, res) => {
8
+ const url = req.url ?? '/';
9
+ const [rawPath, query] = url.replace('//', '/').split('?');
10
+ console.log(`${req.method} ${rawPath}${query ? `?${query}` : ''}`);
11
+ // CORS preflight
12
+ if (req.method === 'OPTIONS') {
13
+ setCorsHeaders(res);
14
+ res.statusCode = 204;
15
+ res.end();
16
+ return;
17
+ }
18
+ const handler = handlers[rawPath];
19
+ if (!handler) {
20
+ setCorsHeaders(res);
21
+ res.statusCode = 404;
22
+ res.setHeader('Content-Type', 'application/json');
23
+ res.end(JSON.stringify({ message: `No handler for ${rawPath}` }));
24
+ return;
25
+ }
26
+ try {
27
+ const event = await buildEvent(req, rawPath, query);
28
+ const response = await handler(event);
29
+ setCorsHeaders(res);
30
+ res.statusCode = response.statusCode;
31
+ res.setHeader('Content-Type', 'application/json');
32
+ let body = response.body ?? '';
33
+ if (response.headers?.['Content-Encoding'] === 'gzip') {
34
+ body = decompressResponse(body);
35
+ }
36
+ res.end(body);
37
+ }
38
+ catch (err) {
39
+ console.error(`Error handling ${rawPath}:`, err);
40
+ setCorsHeaders(res);
41
+ res.statusCode = 500;
42
+ res.setHeader('Content-Type', 'application/json');
43
+ res.end(JSON.stringify({ message: 'Internal server error' }));
44
+ }
45
+ });
46
+ server.listen(port, () => {
47
+ console.log(`\ndev-backend listening on http://localhost:${port}/`);
48
+ });
49
+ }
50
+ function setCorsHeaders(res) {
51
+ res.setHeader('Access-Control-Allow-Origin', '*');
52
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
53
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
54
+ }
55
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@smplcty/dev-backend",
3
+ "version": "0.1.0",
4
+ "description": "Local Lambda dev server — reads a routes config, loads .env, serves Lambda handlers over HTTP with API Gateway v2 event translation.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "bin": {
9
+ "dev-backend": "./dist/cli.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "registry": "https://registry.npmjs.org/"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/mabulu-inc/dev-backend.git"
23
+ },
24
+ "homepage": "https://github.com/mabulu-inc/dev-backend#readme",
25
+ "bugs": {
26
+ "url": "https://github.com/mabulu-inc/dev-backend/issues"
27
+ },
28
+ "keywords": [
29
+ "lambda",
30
+ "dev-server",
31
+ "api-gateway",
32
+ "local"
33
+ ],
34
+ "engines": {
35
+ "node": ">=20"
36
+ },
37
+ "devDependencies": {
38
+ "@eslint/js": "^10.0.1",
39
+ "@types/node": "^25.5.2",
40
+ "eslint": "^10.2.0",
41
+ "typescript": "^6.0.2",
42
+ "typescript-eslint": "^8.58.1",
43
+ "vitest": "^4.1.4"
44
+ },
45
+ "scripts": {
46
+ "build": "tsc -p tsconfig.build.json",
47
+ "clean": "rm -rf dist",
48
+ "lint": "eslint src",
49
+ "typecheck": "tsc --noEmit",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest"
52
+ }
53
+ }