@karmaniverous/smoz 0.2.12 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.js +108 -35
- package/dist/cli/index.cjs +125 -127
- package/dist/index.d.ts +1 -1
- package/dist/mjs/cli/inline-server.js +1 -2
- package/dist/mjs/index.js +108 -35
- package/package.json +42 -43
package/dist/cjs/index.js
CHANGED
|
@@ -375,6 +375,98 @@ const wrapSerializer = (fn, opts) => {
|
|
|
375
375
|
});
|
|
376
376
|
};
|
|
377
377
|
|
|
378
|
+
/**
|
|
379
|
+
* @module Response shaping helpers for HTTP middleware.
|
|
380
|
+
*
|
|
381
|
+
* Shared utilities for detecting and normalising Lambda Proxy response
|
|
382
|
+
* envelopes, used by both the `after` and `onError` middleware paths.
|
|
383
|
+
* Also exports dedicated `onError` steps that apply CORS headers and
|
|
384
|
+
* response shaping after `errorHandler` — necessary because middy v6+
|
|
385
|
+
* does not re-run the `after` chain on the error path.
|
|
386
|
+
*/
|
|
387
|
+
/**
|
|
388
|
+
* Detect a shaped Lambda Proxy response.
|
|
389
|
+
*
|
|
390
|
+
* A response is "shaped" when it has a numeric `statusCode` property —
|
|
391
|
+
* the definitive marker of an API-Gateway-style response envelope.
|
|
392
|
+
* Missing `headers` or `body` are defaulted by {@link shapeResponse}
|
|
393
|
+
* rather than treated as "unshaped", because `{ statusCode, headers }`
|
|
394
|
+
* (no body) is a valid Lambda Proxy return value.
|
|
395
|
+
*/
|
|
396
|
+
const isShapedResponse = (v) => typeof v === 'object' &&
|
|
397
|
+
v !== null &&
|
|
398
|
+
'statusCode' in v &&
|
|
399
|
+
typeof v.statusCode === 'number';
|
|
400
|
+
/**
|
|
401
|
+
* Shape the response into a well-formed Lambda Proxy result.
|
|
402
|
+
*
|
|
403
|
+
* Shaped responses (detected via {@link isShapedResponse}) are normalised
|
|
404
|
+
* in-place — missing `headers` and `body` are defaulted. Non-shaped
|
|
405
|
+
* values are wrapped in a `200` envelope.
|
|
406
|
+
*/
|
|
407
|
+
const shapeResponse = (current, contentType) => {
|
|
408
|
+
let res;
|
|
409
|
+
if (isShapedResponse(current)) {
|
|
410
|
+
res = current;
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
res = { statusCode: 200, headers: {}, body: current };
|
|
414
|
+
}
|
|
415
|
+
// Default missing fields for shaped responses without body/headers.
|
|
416
|
+
if (res.body === undefined)
|
|
417
|
+
res.body = '';
|
|
418
|
+
if (res.body !== undefined && typeof res.body !== 'string') {
|
|
419
|
+
try {
|
|
420
|
+
res.body = JSON.stringify(res.body);
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
res.body = String(res.body);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const headers = res.headers ?? {};
|
|
427
|
+
headers['Content-Type'] = contentType;
|
|
428
|
+
res.headers = headers;
|
|
429
|
+
return res;
|
|
430
|
+
};
|
|
431
|
+
/**
|
|
432
|
+
* Apply CORS headers to error responses.
|
|
433
|
+
*
|
|
434
|
+
* middy v6+ does not re-run the `after` chain after `onError` handles an
|
|
435
|
+
* error, so the regular CORS `after` hook never fires for error responses.
|
|
436
|
+
* This dedicated `onError` step runs *after* the error handler has set
|
|
437
|
+
* `request.response`, ensuring CORS headers are present on error responses.
|
|
438
|
+
*/
|
|
439
|
+
const makeOnErrorCors = (opts) => {
|
|
440
|
+
const cors = httpCors({
|
|
441
|
+
credentials: true,
|
|
442
|
+
getOrigin: (o) => o,
|
|
443
|
+
...(opts?.cors ?? {}),
|
|
444
|
+
});
|
|
445
|
+
return tagStep({
|
|
446
|
+
onError: async (request) => {
|
|
447
|
+
if (request.response === undefined)
|
|
448
|
+
return;
|
|
449
|
+
if (cors.after)
|
|
450
|
+
await cors.after(request);
|
|
451
|
+
},
|
|
452
|
+
}, 'error-cors');
|
|
453
|
+
};
|
|
454
|
+
/**
|
|
455
|
+
* Shape error responses into well-formed Lambda Proxy results.
|
|
456
|
+
*
|
|
457
|
+
* Mirrors `makeShapeAndContentType` but runs on the `onError` path,
|
|
458
|
+
* ensuring error responses have proper `statusCode`, `headers`, and
|
|
459
|
+
* `body` structure even when the `after` chain is skipped.
|
|
460
|
+
*/
|
|
461
|
+
const makeOnErrorShape = (contentType) => tagStep({
|
|
462
|
+
onError: (request) => {
|
|
463
|
+
const container = request;
|
|
464
|
+
if (container.response === undefined)
|
|
465
|
+
return;
|
|
466
|
+
container.response = shapeResponse(container.response, contentType);
|
|
467
|
+
},
|
|
468
|
+
}, 'error-shape');
|
|
469
|
+
|
|
378
470
|
const makeHead = () => tagStep(shortCircuitHead, 'head');
|
|
379
471
|
const makeHeaderNormalizer = (opts) => tagStep(asApiMiddleware(httpHeaderNormalizer(opts?.headerNormalizer ?? { canonical: true })), 'header-normalizer');
|
|
380
472
|
const makeEventNormalizer = () => tagStep(asApiMiddleware(httpEventNormalizer()), 'event-normalizer');
|
|
@@ -529,31 +621,9 @@ const makeCors = (opts) => tagStep(asApiMiddleware(httpCors({
|
|
|
529
621
|
const makeShapeAndContentType = (contentType) => tagStep({
|
|
530
622
|
after: (request) => {
|
|
531
623
|
const container = request;
|
|
532
|
-
|
|
533
|
-
if (current === undefined)
|
|
624
|
+
if (container.response === undefined)
|
|
534
625
|
return;
|
|
535
|
-
|
|
536
|
-
current !== null &&
|
|
537
|
-
'statusCode' in current &&
|
|
538
|
-
'headers' in current &&
|
|
539
|
-
'body' in current;
|
|
540
|
-
let res;
|
|
541
|
-
if (looksShaped)
|
|
542
|
-
res = current;
|
|
543
|
-
else
|
|
544
|
-
res = { statusCode: 200, headers: {}, body: current };
|
|
545
|
-
if (res.body !== undefined && typeof res.body !== 'string') {
|
|
546
|
-
try {
|
|
547
|
-
res.body = JSON.stringify(res.body);
|
|
548
|
-
}
|
|
549
|
-
catch {
|
|
550
|
-
res.body = String(res.body);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
const headers = res.headers ?? {};
|
|
554
|
-
headers['Content-Type'] = contentType;
|
|
555
|
-
res.headers = headers;
|
|
556
|
-
request.response = res;
|
|
626
|
+
container.response = shapeResponse(container.response, contentType);
|
|
557
627
|
},
|
|
558
628
|
}, 'shape');
|
|
559
629
|
const makeSerializer = (contentType, opts) => tagStep(asApiMiddleware(httpResponseSerializer({
|
|
@@ -589,7 +659,15 @@ const buildDefaultPhases = (args) => {
|
|
|
589
659
|
makeShapeAndContentType(contentType),
|
|
590
660
|
makeSerializer(contentType, opts),
|
|
591
661
|
];
|
|
592
|
-
const onError = [
|
|
662
|
+
const onError = [
|
|
663
|
+
makeErrorExpose(),
|
|
664
|
+
makeErrorHandler(opts),
|
|
665
|
+
// Post-error-handler processing: middy v6+ does not re-run the after
|
|
666
|
+
// chain after onError, so CORS and response shaping must happen here.
|
|
667
|
+
// Shape first so CORS can mutate a well-formed headers object.
|
|
668
|
+
makeOnErrorShape(contentType),
|
|
669
|
+
makeOnErrorCors(opts),
|
|
670
|
+
];
|
|
593
671
|
return { before, after, onError };
|
|
594
672
|
};
|
|
595
673
|
const buildSafeDefaults = (args) => buildDefaultPhases(args);
|
|
@@ -789,14 +867,13 @@ function wrapHandler(functionConfig, business, opts) {
|
|
|
789
867
|
assertKeysSubset(envConfig.stage.paramsSchema, envConfig.stage.envKeys, 'stage.envKeys');
|
|
790
868
|
return async (event, context) => {
|
|
791
869
|
// Compose typed env schema and parse process.env
|
|
792
|
-
const all = deriveAllKeys(envConfig.global.envKeys, envConfig.stage.envKeys,
|
|
870
|
+
const all = deriveAllKeys(envConfig.global.envKeys, envConfig.stage.envKeys, functionConfig.fnEnvKeys ?? []);
|
|
793
871
|
const { globalPick, stagePick } = splitKeysBySchema(all, envConfig.global.paramsSchema, envConfig.stage.paramsSchema);
|
|
794
872
|
const envSchema = buildEnvSchema(globalPick, stagePick, envConfig.global.paramsSchema, envConfig.stage.paramsSchema);
|
|
795
873
|
const env = parseTypedEnv(envSchema, process.env);
|
|
796
874
|
const logger = console;
|
|
797
875
|
// Non-HTTP: call business directly
|
|
798
|
-
const httpTokens = opts?.httpEventTypeTokens ??
|
|
799
|
-
defaultHttpEventTypeTokens;
|
|
876
|
+
const httpTokens = opts?.httpEventTypeTokens ?? defaultHttpEventTypeTokens;
|
|
800
877
|
const isHttp = httpTokens.includes(functionConfig.eventType);
|
|
801
878
|
if (!isHttp) {
|
|
802
879
|
return business(event, context, { env, logger });
|
|
@@ -859,9 +936,7 @@ const createRegistry = (deps) => {
|
|
|
859
936
|
...(options.basePath ? { basePath: options.basePath } : {}),
|
|
860
937
|
...(options.httpContexts ? { httpContexts: options.httpContexts } : {}),
|
|
861
938
|
...(options.contentType ? { contentType: options.contentType } : {}),
|
|
862
|
-
...(mergedFnEnvKeys.length
|
|
863
|
-
? { fnEnvKeys: mergedFnEnvKeys }
|
|
864
|
-
: {}),
|
|
939
|
+
...(mergedFnEnvKeys.length ? { fnEnvKeys: mergedFnEnvKeys } : {}),
|
|
865
940
|
...(options.eventSchema ? { eventSchema: options.eventSchema } : {}),
|
|
866
941
|
...(options.responseSchema
|
|
867
942
|
? { responseSchema: options.responseSchema }
|
|
@@ -878,9 +953,7 @@ const createRegistry = (deps) => {
|
|
|
878
953
|
...(options.basePath ? { basePath: options.basePath } : {}),
|
|
879
954
|
...(options.httpContexts ? { httpContexts: options.httpContexts } : {}),
|
|
880
955
|
...(options.contentType ? { contentType: options.contentType } : {}),
|
|
881
|
-
...(mergedFnEnvKeys.length
|
|
882
|
-
? { fnEnvKeys: mergedFnEnvKeys }
|
|
883
|
-
: {}),
|
|
956
|
+
...(mergedFnEnvKeys.length ? { fnEnvKeys: mergedFnEnvKeys } : {}),
|
|
884
957
|
...(options.eventSchema ? { eventSchema: options.eventSchema } : {}),
|
|
885
958
|
...(options.responseSchema
|
|
886
959
|
? { responseSchema: options.responseSchema }
|
|
@@ -1047,7 +1120,7 @@ const buildAllServerlessFunctions = (registry, serverless, buildFnEnv) => {
|
|
|
1047
1120
|
.split(path.sep)
|
|
1048
1121
|
.join('/');
|
|
1049
1122
|
const handler = `${handlerFileRel}.${serverless.defaultHandlerFileExport}`;
|
|
1050
|
-
let events
|
|
1123
|
+
let events;
|
|
1051
1124
|
try {
|
|
1052
1125
|
const { method, basePath, contexts } = resolveHttpFromFunctionConfig({
|
|
1053
1126
|
functionName: r.functionName,
|
package/dist/cli/index.cjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
var
|
|
4
|
+
var cli = require('@karmaniverous/get-dotenv/cli');
|
|
5
5
|
var plugins = require('@karmaniverous/get-dotenv/plugins');
|
|
6
|
+
var cliHost = require('@karmaniverous/get-dotenv/cliHost');
|
|
6
7
|
var packageDirectory = require('package-directory');
|
|
7
8
|
var fs = require('node:fs');
|
|
8
9
|
var path = require('node:path');
|
|
@@ -1340,138 +1341,135 @@ const runInit = async (root, template = 'default', opts) => {
|
|
|
1340
1341
|
* delegates to existing implementations. No business logic here.
|
|
1341
1342
|
*/
|
|
1342
1343
|
const repoRoot = () => packageDirectory.packageDirectorySync() ?? process.cwd();
|
|
1343
|
-
const
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
await runInit(root, template, {
|
|
1366
|
-
...(yes ? { yes } : {}),
|
|
1367
|
-
...(noInstall ? { noInstall } : {}),
|
|
1368
|
-
...(pmRaw ? { install: pmRaw } : {}),
|
|
1369
|
-
...(conflict ? { conflict } : {}),
|
|
1370
|
-
});
|
|
1371
|
-
});
|
|
1372
|
-
// add
|
|
1373
|
-
cli
|
|
1374
|
-
.command('add')
|
|
1375
|
-
.description('Scaffold a function under app/functions')
|
|
1376
|
-
.argument('<spec>', 'spec: <eventType>/<segments...>/<method>')
|
|
1377
|
-
.action(async (spec) => {
|
|
1378
|
-
const root = repoRoot();
|
|
1379
|
-
if (typeof spec !== 'string' || !spec.trim()) {
|
|
1380
|
-
throw new Error('smoz add: missing <spec> (e.g., rest/foo/get)');
|
|
1381
|
-
}
|
|
1382
|
-
await runAdd(root, spec);
|
|
1383
|
-
});
|
|
1384
|
-
// register
|
|
1385
|
-
cli
|
|
1386
|
-
.command('register')
|
|
1387
|
-
.description('Generate side-effect register files')
|
|
1388
|
-
.action(async () => {
|
|
1389
|
-
const root = repoRoot();
|
|
1390
|
-
await runRegister(root);
|
|
1391
|
-
});
|
|
1392
|
-
// openapi
|
|
1393
|
-
cli
|
|
1394
|
-
.command('openapi')
|
|
1395
|
-
.description('Generate OpenAPI document (app/generated/openapi.json)')
|
|
1396
|
-
.option('-V, --verbose', 'verbose output')
|
|
1397
|
-
.action(async (opts) => {
|
|
1398
|
-
const root = repoRoot();
|
|
1399
|
-
await runOpenapi(root, { verbose: !!opts.verbose });
|
|
1344
|
+
const setupSmozCommands = (cli) => {
|
|
1345
|
+
// init
|
|
1346
|
+
cli
|
|
1347
|
+
.command('init')
|
|
1348
|
+
.description('Scaffold a new SMOZ app from a template')
|
|
1349
|
+
.option('-t, --template <name>', 'template name', 'default')
|
|
1350
|
+
.option('-y, --yes', 'assume “example” on conflicts')
|
|
1351
|
+
.option('--no-install', 'do not install dependencies')
|
|
1352
|
+
.option('--install <pm>', 'explicit package manager (npm|pnpm|yarn|bun)')
|
|
1353
|
+
.option('--conflict <policy>', 'overwrite|example|skip|ask')
|
|
1354
|
+
.action(async (opts) => {
|
|
1355
|
+
const root = repoRoot();
|
|
1356
|
+
const template = typeof opts.template === 'string' ? opts.template : 'default';
|
|
1357
|
+
const yes = Boolean(opts.yes);
|
|
1358
|
+
const noInstall = Boolean(opts['noInstall']);
|
|
1359
|
+
const pmRaw = opts.install ?? undefined;
|
|
1360
|
+
const conflict = opts.conflict ?? undefined;
|
|
1361
|
+
await runInit(root, template, {
|
|
1362
|
+
...(yes ? { yes } : {}),
|
|
1363
|
+
...(noInstall ? { noInstall } : {}),
|
|
1364
|
+
...(pmRaw ? { install: pmRaw } : {}),
|
|
1365
|
+
...(conflict ? { conflict } : {}),
|
|
1400
1366
|
});
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1367
|
+
});
|
|
1368
|
+
// add
|
|
1369
|
+
cli
|
|
1370
|
+
.command('add')
|
|
1371
|
+
.description('Scaffold a function under app/functions')
|
|
1372
|
+
.argument('<spec>', 'spec: <eventType>/<segments...>/<method>')
|
|
1373
|
+
.action(async (spec) => {
|
|
1374
|
+
const root = repoRoot();
|
|
1375
|
+
if (typeof spec !== 'string' || !spec.trim()) {
|
|
1376
|
+
throw new Error('smoz add: missing <spec> (e.g., rest/foo/get)');
|
|
1377
|
+
}
|
|
1378
|
+
await runAdd(root, spec);
|
|
1379
|
+
});
|
|
1380
|
+
// register
|
|
1381
|
+
cli
|
|
1382
|
+
.command('register')
|
|
1383
|
+
.description('Generate side-effect register files')
|
|
1384
|
+
.action(async () => {
|
|
1385
|
+
const root = repoRoot();
|
|
1386
|
+
await runRegister(root);
|
|
1387
|
+
});
|
|
1388
|
+
// openapi
|
|
1389
|
+
cli
|
|
1390
|
+
.command('openapi')
|
|
1391
|
+
.description('Generate OpenAPI document (app/generated/openapi.json)')
|
|
1392
|
+
.option('-V, --verbose', 'verbose output')
|
|
1393
|
+
.action(async (opts) => {
|
|
1394
|
+
const root = repoRoot();
|
|
1395
|
+
await runOpenapi(root, { verbose: !!opts.verbose });
|
|
1396
|
+
});
|
|
1397
|
+
// dev
|
|
1398
|
+
cli
|
|
1399
|
+
.command('dev')
|
|
1400
|
+
.description('Dev loop: register/openapi + local backend (inline|offline)')
|
|
1401
|
+
.option('-r, --register', 'run register step (default on)')
|
|
1402
|
+
.option('-R, --no-register', 'disable register step')
|
|
1403
|
+
.option('-o, --openapi', 'run openapi step (default on)')
|
|
1404
|
+
.option('-O, --no-openapi', 'disable openapi step')
|
|
1405
|
+
.option('-l, --local [mode]', 'local mode: inline | offline | false (default inline)')
|
|
1406
|
+
.option('-s, --stage <name>', 'stage name')
|
|
1407
|
+
.option('-p, --port <number>', 'port (0 = random free port)')
|
|
1408
|
+
.option('-V, --verbose', 'verbose output')
|
|
1409
|
+
.action(async (opts) => {
|
|
1410
|
+
const root = repoRoot();
|
|
1411
|
+
// register/openapi defaults (on); explicit negations take precedence.
|
|
1412
|
+
const register = opts.register === true
|
|
1413
|
+
? true
|
|
1414
|
+
: opts.noRegister
|
|
1415
|
+
? false
|
|
1416
|
+
: true;
|
|
1417
|
+
const openapi = opts.openapi === true
|
|
1418
|
+
? true
|
|
1419
|
+
: opts.noOpenapi
|
|
1420
|
+
? false
|
|
1421
|
+
: true;
|
|
1422
|
+
// local mode parsing
|
|
1423
|
+
const raw = opts.local;
|
|
1424
|
+
let local = 'inline';
|
|
1425
|
+
if (raw === 'inline' || raw === 'offline')
|
|
1426
|
+
local = raw;
|
|
1427
|
+
else if (raw === true)
|
|
1428
|
+
local = 'inline';
|
|
1429
|
+
else if (raw === 'false')
|
|
1430
|
+
local = false;
|
|
1431
|
+
const port = typeof opts.port === 'string'
|
|
1432
|
+
? Number(opts.port)
|
|
1433
|
+
: typeof opts.port === 'number'
|
|
1434
|
+
? opts.port
|
|
1435
|
+
: undefined;
|
|
1436
|
+
await runDev(root, {
|
|
1437
|
+
register,
|
|
1438
|
+
openapi,
|
|
1439
|
+
local,
|
|
1440
|
+
...(typeof opts.stage === 'string' ? { stage: opts.stage } : {}),
|
|
1441
|
+
...(typeof port === 'number' ? { port } : {}),
|
|
1442
|
+
verbose: !!opts.verbose,
|
|
1448
1443
|
});
|
|
1449
|
-
}
|
|
1444
|
+
});
|
|
1445
|
+
};
|
|
1446
|
+
const smozPlugin = () => cliHost.definePlugin({
|
|
1447
|
+
ns: 'smoz',
|
|
1448
|
+
setup: setupSmozCommands,
|
|
1450
1449
|
});
|
|
1451
1450
|
|
|
1452
1451
|
/**
|
|
1453
1452
|
* SMOZ CLI — plugin-first host built on get-dotenv.
|
|
1454
1453
|
*
|
|
1455
|
-
* -
|
|
1456
|
-
* and
|
|
1457
|
-
* -
|
|
1454
|
+
* - Create the supported GetDotenv CLI runner so root options, resolution hooks,
|
|
1455
|
+
* and plugin installation stay aligned with the get-dotenv host API.
|
|
1456
|
+
* - Install included plugins (cmd/batch/aws) and the SMOZ command plugin
|
|
1457
|
+
* (init/add/register/openapi/dev).
|
|
1458
1458
|
*/
|
|
1459
|
-
const main =
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
await cli.parseAsync();
|
|
1476
|
-
};
|
|
1459
|
+
const main = cli.createCli({
|
|
1460
|
+
alias: 'smoz',
|
|
1461
|
+
compose: (cli) => {
|
|
1462
|
+
const setupResult = smozPlugin().setup(cli);
|
|
1463
|
+
if (setupResult !== undefined) {
|
|
1464
|
+
throw new Error('smozPlugin root setup must remain synchronous.');
|
|
1465
|
+
}
|
|
1466
|
+
return cli
|
|
1467
|
+
.use(plugins.awsPlugin())
|
|
1468
|
+
.use(plugins.cmdPlugin({
|
|
1469
|
+
asDefault: true,
|
|
1470
|
+
optionAlias: '-c, --cmd <command...>',
|
|
1471
|
+
}))
|
|
1472
|
+
.use(plugins.batchPlugin());
|
|
1473
|
+
},
|
|
1474
|
+
});
|
|
1477
1475
|
void main();
|
package/dist/index.d.ts
CHANGED
|
@@ -264,7 +264,7 @@ type ConsoleLogger = Pick<Console, 'debug' | 'error' | 'info' | 'log'>;
|
|
|
264
264
|
*/
|
|
265
265
|
|
|
266
266
|
type ApiMiddleware$1 = MiddlewareObj<APIGatewayProxyEvent, Context>;
|
|
267
|
-
type StepId = 'head' | 'header-normalizer' | 'event-normalizer' | 'content-negotiation' | 'json-body-parser' | 'zod-before' | 'head-finalize' | 'zod-after' | 'error-expose' | 'error-handler' | 'cors' | 'preferred-media' | 'shape' | 'serializer';
|
|
267
|
+
type StepId = 'head' | 'header-normalizer' | 'event-normalizer' | 'content-negotiation' | 'json-body-parser' | 'zod-before' | 'head-finalize' | 'zod-after' | 'error-expose' | 'error-handler' | 'error-cors' | 'error-shape' | 'cors' | 'preferred-media' | 'shape' | 'serializer';
|
|
268
268
|
/** Attach a non-enumerable __id to a middleware step. */
|
|
269
269
|
declare const tagStep: (mw: ApiMiddleware$1, id: StepId) => ApiMiddleware$1;
|
|
270
270
|
/** Retrieve a step's id, if present. */
|
|
@@ -190,8 +190,7 @@ const loadHandlers = async (root, app) => {
|
|
|
190
190
|
if (typeof handler !== 'function')
|
|
191
191
|
continue;
|
|
192
192
|
for (const evt of def.events) {
|
|
193
|
-
const httpEvt = evt
|
|
194
|
-
.http;
|
|
193
|
+
const httpEvt = evt.http;
|
|
195
194
|
const method = (httpEvt?.method ?? '').toUpperCase();
|
|
196
195
|
const pattern = '/' + (httpEvt?.path ?? '').replace(/^\/+/, '');
|
|
197
196
|
if (!method || !pattern)
|
package/dist/mjs/index.js
CHANGED
|
@@ -373,6 +373,98 @@ const wrapSerializer = (fn, opts) => {
|
|
|
373
373
|
});
|
|
374
374
|
};
|
|
375
375
|
|
|
376
|
+
/**
|
|
377
|
+
* @module Response shaping helpers for HTTP middleware.
|
|
378
|
+
*
|
|
379
|
+
* Shared utilities for detecting and normalising Lambda Proxy response
|
|
380
|
+
* envelopes, used by both the `after` and `onError` middleware paths.
|
|
381
|
+
* Also exports dedicated `onError` steps that apply CORS headers and
|
|
382
|
+
* response shaping after `errorHandler` — necessary because middy v6+
|
|
383
|
+
* does not re-run the `after` chain on the error path.
|
|
384
|
+
*/
|
|
385
|
+
/**
|
|
386
|
+
* Detect a shaped Lambda Proxy response.
|
|
387
|
+
*
|
|
388
|
+
* A response is "shaped" when it has a numeric `statusCode` property —
|
|
389
|
+
* the definitive marker of an API-Gateway-style response envelope.
|
|
390
|
+
* Missing `headers` or `body` are defaulted by {@link shapeResponse}
|
|
391
|
+
* rather than treated as "unshaped", because `{ statusCode, headers }`
|
|
392
|
+
* (no body) is a valid Lambda Proxy return value.
|
|
393
|
+
*/
|
|
394
|
+
const isShapedResponse = (v) => typeof v === 'object' &&
|
|
395
|
+
v !== null &&
|
|
396
|
+
'statusCode' in v &&
|
|
397
|
+
typeof v.statusCode === 'number';
|
|
398
|
+
/**
|
|
399
|
+
* Shape the response into a well-formed Lambda Proxy result.
|
|
400
|
+
*
|
|
401
|
+
* Shaped responses (detected via {@link isShapedResponse}) are normalised
|
|
402
|
+
* in-place — missing `headers` and `body` are defaulted. Non-shaped
|
|
403
|
+
* values are wrapped in a `200` envelope.
|
|
404
|
+
*/
|
|
405
|
+
const shapeResponse = (current, contentType) => {
|
|
406
|
+
let res;
|
|
407
|
+
if (isShapedResponse(current)) {
|
|
408
|
+
res = current;
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
res = { statusCode: 200, headers: {}, body: current };
|
|
412
|
+
}
|
|
413
|
+
// Default missing fields for shaped responses without body/headers.
|
|
414
|
+
if (res.body === undefined)
|
|
415
|
+
res.body = '';
|
|
416
|
+
if (res.body !== undefined && typeof res.body !== 'string') {
|
|
417
|
+
try {
|
|
418
|
+
res.body = JSON.stringify(res.body);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
res.body = String(res.body);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const headers = res.headers ?? {};
|
|
425
|
+
headers['Content-Type'] = contentType;
|
|
426
|
+
res.headers = headers;
|
|
427
|
+
return res;
|
|
428
|
+
};
|
|
429
|
+
/**
|
|
430
|
+
* Apply CORS headers to error responses.
|
|
431
|
+
*
|
|
432
|
+
* middy v6+ does not re-run the `after` chain after `onError` handles an
|
|
433
|
+
* error, so the regular CORS `after` hook never fires for error responses.
|
|
434
|
+
* This dedicated `onError` step runs *after* the error handler has set
|
|
435
|
+
* `request.response`, ensuring CORS headers are present on error responses.
|
|
436
|
+
*/
|
|
437
|
+
const makeOnErrorCors = (opts) => {
|
|
438
|
+
const cors = httpCors({
|
|
439
|
+
credentials: true,
|
|
440
|
+
getOrigin: (o) => o,
|
|
441
|
+
...(opts?.cors ?? {}),
|
|
442
|
+
});
|
|
443
|
+
return tagStep({
|
|
444
|
+
onError: async (request) => {
|
|
445
|
+
if (request.response === undefined)
|
|
446
|
+
return;
|
|
447
|
+
if (cors.after)
|
|
448
|
+
await cors.after(request);
|
|
449
|
+
},
|
|
450
|
+
}, 'error-cors');
|
|
451
|
+
};
|
|
452
|
+
/**
|
|
453
|
+
* Shape error responses into well-formed Lambda Proxy results.
|
|
454
|
+
*
|
|
455
|
+
* Mirrors `makeShapeAndContentType` but runs on the `onError` path,
|
|
456
|
+
* ensuring error responses have proper `statusCode`, `headers`, and
|
|
457
|
+
* `body` structure even when the `after` chain is skipped.
|
|
458
|
+
*/
|
|
459
|
+
const makeOnErrorShape = (contentType) => tagStep({
|
|
460
|
+
onError: (request) => {
|
|
461
|
+
const container = request;
|
|
462
|
+
if (container.response === undefined)
|
|
463
|
+
return;
|
|
464
|
+
container.response = shapeResponse(container.response, contentType);
|
|
465
|
+
},
|
|
466
|
+
}, 'error-shape');
|
|
467
|
+
|
|
376
468
|
const makeHead = () => tagStep(shortCircuitHead, 'head');
|
|
377
469
|
const makeHeaderNormalizer = (opts) => tagStep(asApiMiddleware(httpHeaderNormalizer(opts?.headerNormalizer ?? { canonical: true })), 'header-normalizer');
|
|
378
470
|
const makeEventNormalizer = () => tagStep(asApiMiddleware(httpEventNormalizer()), 'event-normalizer');
|
|
@@ -527,31 +619,9 @@ const makeCors = (opts) => tagStep(asApiMiddleware(httpCors({
|
|
|
527
619
|
const makeShapeAndContentType = (contentType) => tagStep({
|
|
528
620
|
after: (request) => {
|
|
529
621
|
const container = request;
|
|
530
|
-
|
|
531
|
-
if (current === undefined)
|
|
622
|
+
if (container.response === undefined)
|
|
532
623
|
return;
|
|
533
|
-
|
|
534
|
-
current !== null &&
|
|
535
|
-
'statusCode' in current &&
|
|
536
|
-
'headers' in current &&
|
|
537
|
-
'body' in current;
|
|
538
|
-
let res;
|
|
539
|
-
if (looksShaped)
|
|
540
|
-
res = current;
|
|
541
|
-
else
|
|
542
|
-
res = { statusCode: 200, headers: {}, body: current };
|
|
543
|
-
if (res.body !== undefined && typeof res.body !== 'string') {
|
|
544
|
-
try {
|
|
545
|
-
res.body = JSON.stringify(res.body);
|
|
546
|
-
}
|
|
547
|
-
catch {
|
|
548
|
-
res.body = String(res.body);
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
const headers = res.headers ?? {};
|
|
552
|
-
headers['Content-Type'] = contentType;
|
|
553
|
-
res.headers = headers;
|
|
554
|
-
request.response = res;
|
|
624
|
+
container.response = shapeResponse(container.response, contentType);
|
|
555
625
|
},
|
|
556
626
|
}, 'shape');
|
|
557
627
|
const makeSerializer = (contentType, opts) => tagStep(asApiMiddleware(httpResponseSerializer({
|
|
@@ -587,7 +657,15 @@ const buildDefaultPhases = (args) => {
|
|
|
587
657
|
makeShapeAndContentType(contentType),
|
|
588
658
|
makeSerializer(contentType, opts),
|
|
589
659
|
];
|
|
590
|
-
const onError = [
|
|
660
|
+
const onError = [
|
|
661
|
+
makeErrorExpose(),
|
|
662
|
+
makeErrorHandler(opts),
|
|
663
|
+
// Post-error-handler processing: middy v6+ does not re-run the after
|
|
664
|
+
// chain after onError, so CORS and response shaping must happen here.
|
|
665
|
+
// Shape first so CORS can mutate a well-formed headers object.
|
|
666
|
+
makeOnErrorShape(contentType),
|
|
667
|
+
makeOnErrorCors(opts),
|
|
668
|
+
];
|
|
591
669
|
return { before, after, onError };
|
|
592
670
|
};
|
|
593
671
|
const buildSafeDefaults = (args) => buildDefaultPhases(args);
|
|
@@ -787,14 +865,13 @@ function wrapHandler(functionConfig, business, opts) {
|
|
|
787
865
|
assertKeysSubset(envConfig.stage.paramsSchema, envConfig.stage.envKeys, 'stage.envKeys');
|
|
788
866
|
return async (event, context) => {
|
|
789
867
|
// Compose typed env schema and parse process.env
|
|
790
|
-
const all = deriveAllKeys(envConfig.global.envKeys, envConfig.stage.envKeys,
|
|
868
|
+
const all = deriveAllKeys(envConfig.global.envKeys, envConfig.stage.envKeys, functionConfig.fnEnvKeys ?? []);
|
|
791
869
|
const { globalPick, stagePick } = splitKeysBySchema(all, envConfig.global.paramsSchema, envConfig.stage.paramsSchema);
|
|
792
870
|
const envSchema = buildEnvSchema(globalPick, stagePick, envConfig.global.paramsSchema, envConfig.stage.paramsSchema);
|
|
793
871
|
const env = parseTypedEnv(envSchema, process.env);
|
|
794
872
|
const logger = console;
|
|
795
873
|
// Non-HTTP: call business directly
|
|
796
|
-
const httpTokens = opts?.httpEventTypeTokens ??
|
|
797
|
-
defaultHttpEventTypeTokens;
|
|
874
|
+
const httpTokens = opts?.httpEventTypeTokens ?? defaultHttpEventTypeTokens;
|
|
798
875
|
const isHttp = httpTokens.includes(functionConfig.eventType);
|
|
799
876
|
if (!isHttp) {
|
|
800
877
|
return business(event, context, { env, logger });
|
|
@@ -857,9 +934,7 @@ const createRegistry = (deps) => {
|
|
|
857
934
|
...(options.basePath ? { basePath: options.basePath } : {}),
|
|
858
935
|
...(options.httpContexts ? { httpContexts: options.httpContexts } : {}),
|
|
859
936
|
...(options.contentType ? { contentType: options.contentType } : {}),
|
|
860
|
-
...(mergedFnEnvKeys.length
|
|
861
|
-
? { fnEnvKeys: mergedFnEnvKeys }
|
|
862
|
-
: {}),
|
|
937
|
+
...(mergedFnEnvKeys.length ? { fnEnvKeys: mergedFnEnvKeys } : {}),
|
|
863
938
|
...(options.eventSchema ? { eventSchema: options.eventSchema } : {}),
|
|
864
939
|
...(options.responseSchema
|
|
865
940
|
? { responseSchema: options.responseSchema }
|
|
@@ -876,9 +951,7 @@ const createRegistry = (deps) => {
|
|
|
876
951
|
...(options.basePath ? { basePath: options.basePath } : {}),
|
|
877
952
|
...(options.httpContexts ? { httpContexts: options.httpContexts } : {}),
|
|
878
953
|
...(options.contentType ? { contentType: options.contentType } : {}),
|
|
879
|
-
...(mergedFnEnvKeys.length
|
|
880
|
-
? { fnEnvKeys: mergedFnEnvKeys }
|
|
881
|
-
: {}),
|
|
954
|
+
...(mergedFnEnvKeys.length ? { fnEnvKeys: mergedFnEnvKeys } : {}),
|
|
882
955
|
...(options.eventSchema ? { eventSchema: options.eventSchema } : {}),
|
|
883
956
|
...(options.responseSchema
|
|
884
957
|
? { responseSchema: options.responseSchema }
|
|
@@ -1045,7 +1118,7 @@ const buildAllServerlessFunctions = (registry, serverless, buildFnEnv) => {
|
|
|
1045
1118
|
.split(sep)
|
|
1046
1119
|
.join('/');
|
|
1047
1120
|
const handler = `${handlerFileRel}.${serverless.defaultHandlerFileExport}`;
|
|
1048
|
-
let events
|
|
1121
|
+
let events;
|
|
1049
1122
|
try {
|
|
1050
1123
|
const { method, basePath, contexts } = resolveHttpFromFunctionConfig({
|
|
1051
1124
|
functionName: r.functionName,
|
package/package.json
CHANGED
|
@@ -13,64 +13,63 @@
|
|
|
13
13
|
"url": "https://github.com/karmaniverous/smoz/issues"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@karmaniverous/get-dotenv": "^
|
|
17
|
-
"@middy/core": "^
|
|
18
|
-
"@middy/http-content-negotiation": "^
|
|
19
|
-
"@middy/http-cors": "^
|
|
20
|
-
"@middy/http-error-handler": "^
|
|
21
|
-
"@middy/http-event-normalizer": "^
|
|
22
|
-
"@middy/http-header-normalizer": "^
|
|
23
|
-
"@middy/http-json-body-parser": "^
|
|
24
|
-
"@middy/http-response-serializer": "^
|
|
16
|
+
"@karmaniverous/get-dotenv": "^7.0.11",
|
|
17
|
+
"@middy/core": "^7.7.0",
|
|
18
|
+
"@middy/http-content-negotiation": "^7.7.0",
|
|
19
|
+
"@middy/http-cors": "^7.7.0",
|
|
20
|
+
"@middy/http-error-handler": "^7.7.0",
|
|
21
|
+
"@middy/http-event-normalizer": "^7.7.0",
|
|
22
|
+
"@middy/http-header-normalizer": "^7.7.0",
|
|
23
|
+
"@middy/http-json-body-parser": "^7.7.0",
|
|
24
|
+
"@middy/http-response-serializer": "^7.7.0",
|
|
25
25
|
"aws-lambda": "^1.0.7",
|
|
26
|
-
"chokidar": "^
|
|
26
|
+
"chokidar": "^5.0.0",
|
|
27
27
|
"http-errors": "^2.0.1",
|
|
28
|
-
"package-directory": "^8.
|
|
28
|
+
"package-directory": "^8.2.0",
|
|
29
29
|
"radash": "^12.1.1",
|
|
30
|
-
"zod": "^4.
|
|
30
|
+
"zod": "^4.4.3"
|
|
31
31
|
},
|
|
32
32
|
"description": "John Galt Services back end.",
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@dotenvx/dotenvx": "^
|
|
35
|
-
"@eslint/js": "^
|
|
34
|
+
"@dotenvx/dotenvx": "^2.10.0",
|
|
35
|
+
"@eslint/js": "^10.0.1",
|
|
36
36
|
"@rollup/plugin-alias": "^6.0.0",
|
|
37
37
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
38
|
-
"@serverless/typescript": "^4.
|
|
39
|
-
"@types/aws-lambda": "^8.10.
|
|
38
|
+
"@serverless/typescript": "^4.30.0",
|
|
39
|
+
"@types/aws-lambda": "^8.10.162",
|
|
40
40
|
"@types/fs-extra": "^11.0.4",
|
|
41
41
|
"@types/http-errors": "^2.0.5",
|
|
42
|
-
"@types/node": "^
|
|
43
|
-
"@types/serverless": "^3.12.
|
|
44
|
-
"@vitest/coverage-v8": "^4.
|
|
45
|
-
"auto-changelog": "^2.
|
|
46
|
-
"eslint": "^
|
|
42
|
+
"@types/node": "^26",
|
|
43
|
+
"@types/serverless": "^3.12.28",
|
|
44
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
45
|
+
"auto-changelog": "^2.6.0",
|
|
46
|
+
"eslint": "^10.7.0",
|
|
47
47
|
"eslint-config-prettier": "^10.1.8",
|
|
48
|
-
"eslint-plugin-prettier": "^5.5.
|
|
49
|
-
"eslint-plugin-simple-import-sort": "^
|
|
50
|
-
"fs-extra": "^11.3.
|
|
51
|
-
"knip": "^
|
|
52
|
-
"lefthook": "^2.
|
|
53
|
-
"prettier": "^3.
|
|
54
|
-
"release-it": "^
|
|
55
|
-
"rimraf": "^6.1.
|
|
56
|
-
"rollup": "^4.
|
|
57
|
-
"rollup-plugin-dts": "^6.
|
|
58
|
-
"serverless": "^4.
|
|
48
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
49
|
+
"eslint-plugin-simple-import-sort": "^13.0.0",
|
|
50
|
+
"fs-extra": "^11.3.6",
|
|
51
|
+
"knip": "^6.27.0",
|
|
52
|
+
"lefthook": "^2.1.10",
|
|
53
|
+
"prettier": "^3.9.5",
|
|
54
|
+
"release-it": "^20.2.1",
|
|
55
|
+
"rimraf": "^6.1.3",
|
|
56
|
+
"rollup": "^4.62.2",
|
|
57
|
+
"rollup-plugin-dts": "^6.4.1",
|
|
58
|
+
"serverless": "^4.39.0",
|
|
59
59
|
"serverless-apigateway-log-retention": "^1.1.0",
|
|
60
60
|
"serverless-deployment-bucket": "^1.6.0",
|
|
61
|
-
"serverless-domain-manager": "^
|
|
62
|
-
"serverless-offline": "^14.4
|
|
61
|
+
"serverless-domain-manager": "^10.2.0",
|
|
62
|
+
"serverless-offline": "^14.7.4",
|
|
63
63
|
"serverless-plugin-common-excludes": "^4.0.0",
|
|
64
|
-
"tsx": "^4.
|
|
65
|
-
"typedoc": "^0.28.
|
|
66
|
-
"typedoc-plugin-mdn-links": "^5.
|
|
64
|
+
"tsx": "^4.23.1",
|
|
65
|
+
"typedoc": "^0.28.20",
|
|
66
|
+
"typedoc-plugin-mdn-links": "^5.1.1",
|
|
67
67
|
"typedoc-plugin-replace-text": "^4.2.0",
|
|
68
68
|
"typedoc-plugin-zod": "^1.4.3",
|
|
69
|
-
"typescript": "^
|
|
70
|
-
"typescript-eslint": "^8.
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"zod-openapi": "^5.4.3"
|
|
69
|
+
"typescript": "^6.0.3",
|
|
70
|
+
"typescript-eslint": "^8.64.0",
|
|
71
|
+
"vitest": "^4.1.10",
|
|
72
|
+
"zod-openapi": "^6.0.0"
|
|
74
73
|
},
|
|
75
74
|
"engines": {
|
|
76
75
|
"node": ">=22.19.0"
|
|
@@ -184,7 +183,7 @@
|
|
|
184
183
|
"templates:lint": "eslint --fix -c templates/default/eslint.config.ts \"templates/default/**/*.{ts,tsx,js,jsx}\" \"templates/default/eslint.config.ts\""
|
|
185
184
|
},
|
|
186
185
|
"type": "module",
|
|
187
|
-
"version": "0.2.
|
|
186
|
+
"version": "0.2.14",
|
|
188
187
|
"volta": {
|
|
189
188
|
"node": "22.19.0"
|
|
190
189
|
}
|