@karmaniverous/smoz 0.1.2

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.
@@ -0,0 +1,706 @@
1
+ 'use strict';
2
+
3
+ var path = require('node:path');
4
+ var node_url = require('node:url');
5
+ var baseEventTypeMapSchema$1 = require('@/src/core/baseEventTypeMapSchema');
6
+ var buildStageArtifacts = require('@/src/core/buildStageArtifacts');
7
+ var httpTokens = require('@/src/core/httpTokens');
8
+ var registry = require('@/src/core/registry');
9
+ var serverlessConfig = require('@/src/core/serverlessConfig');
10
+ var buildOpenApi = require('@/src/openapi/buildOpenApi');
11
+ var buildServerless = require('@/src/serverless/buildServerless');
12
+ var zod = require('zod');
13
+ var stagesFactory = require('@/src/serverless/stagesFactory');
14
+ var middy = require('@middy/core');
15
+ var defineFunctionConfig = require('@/src/core/defineFunctionConfig');
16
+ var httpStackCustomization = require('@/src/http/middleware/httpStackCustomization');
17
+ var envBuilder = require('@/src/runtime/envBuilder');
18
+ require('@/src/http/middleware/combine');
19
+ var transformUtils = require('@/src/http/middleware/transformUtils');
20
+ var httpContentNegotiation = require('@middy/http-content-negotiation');
21
+ var httpCors = require('@middy/http-cors');
22
+ var httpErrorHandler = require('@middy/http-error-handler');
23
+ var httpEventNormalizer = require('@middy/http-event-normalizer');
24
+ var httpHeaderNormalizer = require('@middy/http-header-normalizer');
25
+ var httpJsonBodyParser = require('@middy/http-json-body-parser');
26
+ var httpResponseSerializer = require('@middy/http-response-serializer');
27
+ var asApiMiddleware = require('@/src/http/middleware/asApiMiddleware');
28
+ var httpZodValidator = require('@/src/http/middleware/httpZodValidator');
29
+ var shortCircuitHead = require('@/src/http/middleware/shortCircuitHead');
30
+ var wrapSerializer = require('@/src/http/middleware/wrapSerializer');
31
+
32
+ /**
33
+ * App (schema‑first)
34
+ *
35
+ * Central orchestrator for a SMOZ application. You provide:
36
+ * - Global/stage parameter schemas and env exposure keys
37
+ * - Serverless defaults (handler filename/export and context map)
38
+ * - Event‑type map schema (extendable: e.g., add 'step')
39
+ *
40
+ * The instance:
41
+ * - Validates configuration
42
+ * - Exposes env and stage artifacts for Serverless (provider.environment and params)
43
+ * - Provides a registry to define functions (HTTP and non‑HTTP)
44
+ * - Aggregates artifacts:
45
+ * - buildAllServerlessFunctions(): AWS['functions']
46
+ * - buildAllOpenApiPaths(): ZodOpenApiPathsObject
47
+ *
48
+ * @remarks See README for a full quick‑start, and typedoc for detailed API docs.
49
+ */
50
+ /**
51
+ * Application class. *
52
+ * @typeParam GlobalParamsSchema - Zod object schema for global parameters
53
+ * @typeParam StageParamsSchema - Zod object schema for per‑stage parameters
54
+ * @typeParam EventTypeMapSchema - Zod object schema mapping event tokens to runtime types
55
+ */ class App {
56
+ /** Helper alias for stage artifacts type */
57
+ static _stageArtifactsType = null;
58
+ appRootAbs;
59
+ // Schemas
60
+ globalParamsSchema;
61
+ stageParamsSchema;
62
+ eventTypeMapSchema; // Serverless config
63
+ serverless;
64
+ // Env exposure
65
+ global;
66
+ stage;
67
+ // Derived stage artifacts
68
+ stages;
69
+ environment;
70
+ buildFnEnv;
71
+ http;
72
+ // HTTP tokens for runtime decision
73
+ httpEventTypeTokens;
74
+ // Registry (delegated to src/app/registry)
75
+ registry;
76
+ constructor(init) {
77
+ this.appRootAbs = init.appRootAbs.replace(/\\/g, '/');
78
+ this.globalParamsSchema = init.globalParamsSchema;
79
+ this.stageParamsSchema = init.stageParamsSchema;
80
+ // Default to base schema when omitted (apply default INSIDE the function)
81
+ this.eventTypeMapSchema = (init.eventTypeMapSchema ??
82
+ baseEventTypeMapSchema$1.baseEventTypeMapSchema);
83
+ // Parse serverless input internally
84
+ this.serverless = serverlessConfig.serverlessConfigSchema.parse(init.serverless);
85
+ // Validate that eventTypeMapSchema includes base keys at runtime
86
+ httpTokens.validateEventTypeMapSchemaIncludesBase(this.eventTypeMapSchema.shape);
87
+ // Env exposure nodes
88
+ this.global = {
89
+ paramsSchema: this.globalParamsSchema,
90
+ envKeys: init.global.envKeys,
91
+ };
92
+ this.stage = {
93
+ paramsSchema: this.stageParamsSchema,
94
+ envKeys: init.stage.envKeys,
95
+ };
96
+ // Build stages/environment/fn-env via helper (applies “stage extends global” and parsing)
97
+ const { stages, environment, buildFnEnv } = buildStageArtifacts.buildStageArtifacts(this.globalParamsSchema, this.stageParamsSchema, { params: init.global.params, envKeys: init.global.envKeys }, { params: init.stage.params, envKeys: init.stage.envKeys });
98
+ this.stages = stages;
99
+ this.environment = environment;
100
+ this.buildFnEnv = buildFnEnv;
101
+ // HTTP tokens (runtime decision)
102
+ this.httpEventTypeTokens = (init.httpEventTypeTokens ??
103
+ httpTokens.defaultHttpEventTypeTokens);
104
+ // App-level HTTP customization
105
+ this.http = init.http ?? {};
106
+ // Initialize function registry
107
+ this.registry = registry.createRegistry({
108
+ httpEventTypeTokens: this.httpEventTypeTokens,
109
+ env: { global: this.global, stage: this.stage },
110
+ http: this.http,
111
+ // With exactOptionalPropertyTypes, do not pass an explicit undefined.
112
+ ...(init.functionDefaults
113
+ ? { functionDefaults: init.functionDefaults }
114
+ : {}),
115
+ });
116
+ } /**
117
+ * Ergonomic constructor for schema‑first inference.
118
+ *
119
+ * @param init - initialization object (schemas, serverless defaults, params/envKeys)
120
+ * @returns a new App instance
121
+ */
122
+ static create(init) {
123
+ return new App(init);
124
+ }
125
+ /**
126
+ * Register a function (HTTP or non‑HTTP).
127
+ *
128
+ * @typeParam EventType - A key from your eventTypeMapSchema (e.g., 'rest' | 'http' | 'sqs' | 'step')
129
+ * @typeParam EventSchema - Optional Zod schema validated BEFORE the handler (refines event shape)
130
+ * @typeParam ResponseSchema - Optional Zod schema validated AFTER the handler (refines response shape)
131
+ * @param options - per‑function configuration (method/basePath/httpContexts for HTTP; serverless extras for non‑HTTP)
132
+ * @returns a per‑function API: { handler(business), openapi(baseOperation), serverless(extras) }
133
+ */
134
+ defineFunction(options) {
135
+ const functionName = options.functionName ??
136
+ (() => {
137
+ const callerDir = path.dirname(node_url.fileURLToPath(options.callerModuleUrl));
138
+ const rel = path.relative(this.appRootAbs, callerDir).split(path.sep).join('/');
139
+ const parts = rel.split('/').filter(Boolean);
140
+ // Drop leading 'app' segment if present, per repo convention
141
+ if (parts[0] === 'app')
142
+ parts.shift();
143
+ return parts.join('_'); // underscore formatting
144
+ })();
145
+ return this.registry.defineFunction({
146
+ functionName,
147
+ eventType: options.eventType,
148
+ ...(options.method ? { method: options.method } : {}),
149
+ ...(options.basePath ? { basePath: options.basePath } : {}),
150
+ ...(options.httpContexts ? { httpContexts: options.httpContexts } : {}),
151
+ ...(options.contentType ? { contentType: options.contentType } : {}),
152
+ ...(options.eventSchema ? { eventSchema: options.eventSchema } : {}),
153
+ ...(options.responseSchema
154
+ ? { responseSchema: options.responseSchema }
155
+ : {}),
156
+ ...(options.fnEnvKeys ? { fnEnvKeys: options.fnEnvKeys } : {}),
157
+ callerModuleUrl: options.callerModuleUrl,
158
+ endpointsRootAbs: options.endpointsRootAbs,
159
+ });
160
+ }
161
+ /**
162
+ * Aggregate Serverless function definitions across the registry.
163
+ *
164
+ * @returns An AWS['functions'] object suitable for serverless.ts
165
+ */
166
+ buildAllServerlessFunctions() {
167
+ return buildServerless.buildAllServerlessFunctions(this.registry.values(), this.serverless, this.buildFnEnv);
168
+ }
169
+ /**
170
+ * Aggregate OpenAPI path items across the registry.
171
+ *
172
+ * @returns ZodOpenApiPathsObject to be embedded in a full OpenAPI document
173
+ */
174
+ buildAllOpenApiPaths() {
175
+ return buildOpenApi.buildAllOpenApiPaths(this.registry.values());
176
+ }
177
+ }
178
+
179
+ const baseEventTypeMapSchema = zod.z.object({
180
+ rest: zod.z.custom(),
181
+ http: zod.z.custom(),
182
+ alb: zod.z.custom(),
183
+ sqs: zod.z.custom(),
184
+ sns: zod.z.custom(),
185
+ s3: zod.z.custom(),
186
+ dynamodb: zod.z.custom(),
187
+ kinesis: zod.z.custom(),
188
+ eventbridge: zod.z.custom(),
189
+ 'cloudwatch-logs': zod.z.custom(),
190
+ ses: zod.z.custom(),
191
+ cloudfront: zod.z.custom(),
192
+ firehose: zod.z.custom(),
193
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- Upstream AWS types mark this as deprecated; we retain the token for compatibility with existing apps.
194
+ 'cognito-userpool': zod.z.custom(),
195
+ });
196
+ // Notes:
197
+ // - This list intentionally includes widely used, generic AWS events.
198
+ // - Apps can extend the schema with custom or specialized tokens:
199
+ // const EventMap = baseEventTypeMapSchema.extend({ step: z.custom<MyStepEvent>() })
200
+ // - Only tokens listed in your App's httpEventTypeTokens are treated as HTTP by the runtime.
201
+
202
+ /**
203
+ * Define a complete application configuration from schemas and authoring input.
204
+ *
205
+ * @typeParam GlobalParamsSchema - Global params schema
206
+ * @typeParam StageParamsSchema - Per‑stage params schema
207
+ * @param globalParamsSchema - schema for global params
208
+ * @param stageParamsSchema - schema for stage params
209
+ * @param input - serverless defaults and concrete params + env exposure
210
+ * @returns Typed configuration nodes and stage artifacts
211
+ * * @throws Error if envKeys include keys not present in their corresponding schema
212
+ */
213
+ function defineAppConfig(globalParamsSchema, stageParamsSchema, input) {
214
+ const assertKeysSubset = (schema, keys, label) => {
215
+ const allowed = new Set(Object.keys(schema.shape));
216
+ const bad = keys.filter((k) => !allowed.has(k));
217
+ if (bad.length)
218
+ throw new Error(`${label} contains unknown keys: ${bad.join(', ')}`);
219
+ };
220
+ assertKeysSubset(globalParamsSchema, input.global.envKeys, 'global.envKeys');
221
+ assertKeysSubset(stageParamsSchema, input.stage.envKeys, 'stage.envKeys');
222
+ const sf = stagesFactory.stagesFactory({
223
+ globalParamsSchema,
224
+ stageParamsSchema,
225
+ globalParams: input.global.params,
226
+ globalEnvKeys: input.global.envKeys,
227
+ stageEnvKeys: input.stage.envKeys,
228
+ stages: input.stage.params,
229
+ });
230
+ return {
231
+ serverless: input.serverless,
232
+ global: { paramsSchema: globalParamsSchema, envKeys: input.global.envKeys },
233
+ stage: { paramsSchema: stageParamsSchema, envKeys: input.stage.envKeys },
234
+ stages: sf.stages,
235
+ environment: sf.environment,
236
+ buildFnEnv: sf.buildFnEnv,
237
+ };
238
+ }
239
+
240
+ /** Narrow to API Gateway v1 events with safe property checks. */
241
+ /**
242
+ * Type guard for API Gateway v1 events. *
243
+ * @param evt - unknown event
244
+ * @returns true if the event looks like a v1 APIGatewayProxyEvent
245
+ */
246
+ const isV1 = (evt) => {
247
+ if (typeof evt !== 'object' || evt === null)
248
+ return false;
249
+ return 'httpMethod' in evt && 'requestContext' in evt;
250
+ };
251
+ /** Narrow to API Gateway v2 events with safe property checks. */
252
+ /**
253
+ * Type guard for API Gateway v2 events.
254
+ */
255
+ const isV2 = (evt) => {
256
+ if (typeof evt !== 'object' || evt === null)
257
+ return false;
258
+ if (!('version' in evt) || !('requestContext' in evt))
259
+ return false;
260
+ const rc = evt.requestContext;
261
+ return (!!rc && typeof rc === 'object' && 'http' in rc);
262
+ };
263
+ /** Case-insensitive single-value header lookup. */
264
+ const getHeader = (headers, key) => {
265
+ if (!headers)
266
+ return undefined;
267
+ const direct = headers[key];
268
+ if (typeof direct === 'string')
269
+ return direct;
270
+ const found = Object.keys(headers).find((k) => k.toLowerCase() === key.toLowerCase());
271
+ return found ? headers[found] : undefined;
272
+ };
273
+ /** Case-insensitive multi-value header lookup. */
274
+ const getMultiHeader = (headers, key) => {
275
+ if (!headers)
276
+ return undefined;
277
+ const direct = headers[key];
278
+ if (Array.isArray(direct) && direct.length > 0)
279
+ return direct[0];
280
+ const found = Object.keys(headers).find((k) => k.toLowerCase() === key.toLowerCase());
281
+ return found ? headers[found]?.[0] : undefined;
282
+ };
283
+ /** Get a header value from either single- or multi-value maps. */
284
+ const getHeaderFromEvent = (evt, key) => {
285
+ const single = getHeader(evt.headers, key);
286
+ if (typeof single === 'string')
287
+ return single;
288
+ const multi = getMultiHeader(evt
289
+ .multiValueHeaders, key);
290
+ return multi;
291
+ };
292
+ const hasAwsSig = (auth) => typeof auth === 'string' && auth.startsWith('AWS4-HMAC-SHA256');
293
+ const hasV1AccessKey = (evt) => {
294
+ const rc = evt.requestContext;
295
+ const ak = rc.identity?.accessKey;
296
+ return typeof ak === 'string' && ak.length > 0;
297
+ };
298
+ const hasAuthorizer = (evt) => {
299
+ // V1: any truthy authorizer object indicates an authenticated request
300
+ if (isV1(evt)) {
301
+ const rc = evt.requestContext;
302
+ return !!rc.authorizer;
303
+ }
304
+ // V2: authorizer may include jwt/iam/lambda shapes
305
+ const authHeader = getHeaderFromEvent(evt, 'authorization');
306
+ if (hasAwsSig(authHeader))
307
+ return true;
308
+ const rc = evt
309
+ .requestContext;
310
+ const { authorizer } = rc;
311
+ if (!authorizer || typeof authorizer !== 'object')
312
+ return false;
313
+ const a = authorizer;
314
+ return !!(a.jwt || a.iam || a.lambda);
315
+ };
316
+ /** Detect API key via header or v1 identity.apiKey. */
317
+ const hasApiKey = (evt) => {
318
+ const fromHeader = getHeaderFromEvent(evt, 'x-api-key');
319
+ if (typeof fromHeader === 'string')
320
+ return true;
321
+ // Some v1 events surface API key on requestContext.identity.apiKey
322
+ if (isV1(evt)) {
323
+ const rc = evt.requestContext;
324
+ const identKey = rc.identity?.apiKey;
325
+ if (typeof identKey === 'string' && identKey.length > 0)
326
+ return true;
327
+ const identKeyId = rc.identity?.apiKeyId;
328
+ if (typeof identKeyId === 'string' && identKeyId.length > 0)
329
+ return true;
330
+ }
331
+ return false;
332
+ };
333
+ /** Classify the security context from either API Gateway event version. */
334
+ /**
335
+ * Detect security context from API Gateway v1/v2 events.
336
+ *
337
+ * @param evt - unknown event
338
+ * @returns 'my' when authorized (Cognito/JWT/IAM), 'private' when API key present, else 'public'
339
+ */
340
+ const detectSecurityContext = (evt) => {
341
+ if (isV1(evt)) {
342
+ const auth = getHeaderFromEvent(evt, 'authorization');
343
+ if (hasAwsSig(auth) || hasV1AccessKey(evt) || hasAuthorizer(evt))
344
+ return 'my';
345
+ if (hasApiKey(evt))
346
+ return 'private';
347
+ return 'public';
348
+ }
349
+ if (isV2(evt)) {
350
+ const auth = getHeaderFromEvent(evt, 'authorization');
351
+ if (hasAwsSig(auth) || hasAuthorizer(evt))
352
+ return 'my';
353
+ if (hasApiKey(evt))
354
+ return 'private';
355
+ return 'public';
356
+ }
357
+ return 'public';
358
+ };
359
+
360
+ /**
361
+ * Wrap a business handler with SMOZ runtime. *
362
+ * - HTTP event tokens receive the full Middy pipeline (validation, shaping, CORS, etc.)
363
+ * - Non‑HTTP tokens bypass Middy and call the business function directly.
364
+ *
365
+ * @typeParam GlobalParamsSchema - global params schema type
366
+ * @typeParam StageParamsSchema - stage params schema type
367
+ * @typeParam EventTypeMap - event token → runtime type map
368
+ * @typeParam EventType - a key of EventTypeMap
369
+ * @typeParam EventSchema - optional Zod schema for event (validated before handler)
370
+ * @typeParam ResponseSchema - optional Zod schema for response (validated after handler)
371
+ * @param functionConfig - per‑function configuration (branded with env nodes)
372
+ * @param business - the business handler implementation
373
+ * @param opts - optional runtime overrides (e.g., widen HTTP tokens)
374
+ * @returns a Lambda‑compatible handler function
375
+ */
376
+ function wrapHandler(functionConfig, business, opts) {
377
+ const assertKeysSubset = (schema, keys, label) => {
378
+ const allowed = new Set(Object.keys(schema.shape));
379
+ const bad = keys.filter((k) => !allowed.has(k));
380
+ if (bad.length)
381
+ throw new Error(`${label} contains unknown keys: ${bad.join(', ')}`);
382
+ };
383
+ const envConfig = defineFunctionConfig.getEnvFromFunctionConfig(functionConfig);
384
+ assertKeysSubset(envConfig.global.paramsSchema, envConfig.global.envKeys, 'global.envKeys');
385
+ assertKeysSubset(envConfig.stage.paramsSchema, envConfig.stage.envKeys, 'stage.envKeys');
386
+ return async (event, context) => {
387
+ // Compose typed env schema and parse process.env
388
+ const all = envBuilder.deriveAllKeys(envConfig.global.envKeys, envConfig.stage.envKeys, (functionConfig.fnEnvKeys ?? []));
389
+ const { globalPick, stagePick } = envBuilder.splitKeysBySchema(all, envConfig.global.paramsSchema, envConfig.stage.paramsSchema);
390
+ const envSchema = envBuilder.buildEnvSchema(globalPick, stagePick, envConfig.global.paramsSchema, envConfig.stage.paramsSchema);
391
+ const env = envBuilder.parseTypedEnv(envSchema, process.env);
392
+ const logger = console;
393
+ // Non-HTTP: call business directly
394
+ const httpTokens$1 = opts?.httpEventTypeTokens ??
395
+ httpTokens.defaultHttpEventTypeTokens;
396
+ const isHttp = httpTokens$1.includes(functionConfig.eventType);
397
+ if (!isHttp) {
398
+ return business(event, context, { env, logger });
399
+ }
400
+ // HTTP: build middleware stack
401
+ const fnHttp = functionConfig.http;
402
+ const maybeContentType = functionConfig
403
+ .contentType;
404
+ const args = {
405
+ functionName: functionConfig.functionName,
406
+ logger,
407
+ ...(functionConfig.eventSchema
408
+ ? { eventSchema: functionConfig.eventSchema }
409
+ : {}),
410
+ ...(functionConfig.responseSchema
411
+ ? { responseSchema: functionConfig.responseSchema }
412
+ : {}),
413
+ ...(typeof maybeContentType === 'string' && maybeContentType.length > 0
414
+ ? { contentType: maybeContentType }
415
+ : {}),
416
+ ...(opts?.httpConfig ? { app: opts.httpConfig } : {}),
417
+ ...(fnHttp ? { fn: fnHttp } : {}),
418
+ };
419
+ const http = httpStackCustomization.computeHttpMiddleware(args);
420
+ const wrapped = middy(async (e, c) => business(e, c, {
421
+ env,
422
+ logger,
423
+ })).use(http);
424
+ return wrapped(event, context);
425
+ };
426
+ }
427
+
428
+ const makeHead = () => transformUtils.tagStep(shortCircuitHead.shortCircuitHead, 'head');
429
+ const makeHeaderNormalizer = (opts) => transformUtils.tagStep(asApiMiddleware.asApiMiddleware(httpHeaderNormalizer(opts?.headerNormalizer ?? { canonical: true })), 'header-normalizer');
430
+ const makeEventNormalizer = () => transformUtils.tagStep(asApiMiddleware.asApiMiddleware(httpEventNormalizer()), 'event-normalizer');
431
+ const makeContentNegotiation = (contentType, opts) => {
432
+ const availableMediaTypes = [contentType];
433
+ const merged = {
434
+ parseLanguages: false,
435
+ parseCharsets: false,
436
+ parseEncodings: false,
437
+ availableMediaTypes,
438
+ ...(opts?.contentNegotiation ?? {}),
439
+ };
440
+ return transformUtils.tagStep(asApiMiddleware.asApiMiddleware(httpContentNegotiation(merged)), 'content-negotiation');
441
+ };
442
+ const makeJsonBodyParser = (opts) => {
443
+ const inner = asApiMiddleware.asApiMiddleware(httpJsonBodyParser({
444
+ disableContentTypeError: true,
445
+ ...(opts?.jsonBodyParser ?? {}),
446
+ }));
447
+ const mw = {};
448
+ mw.before = async (request) => {
449
+ const event = request
450
+ .event;
451
+ if (!event)
452
+ return;
453
+ const method = (event.httpMethod ||
454
+ event
455
+ .requestContext?.http?.method ||
456
+ '').toUpperCase();
457
+ if (method === 'GET' || method === 'HEAD')
458
+ return;
459
+ if (!event.body)
460
+ return;
461
+ if (inner.before)
462
+ await inner.before(request);
463
+ };
464
+ return transformUtils.tagStep(mw, 'json-body-parser');
465
+ };
466
+ const makeZodBefore = (logger, eventSchema) => {
467
+ const base = httpZodValidator.httpZodValidator({
468
+ logger,
469
+ ...(eventSchema ? { eventSchema } : {}),
470
+ });
471
+ const mw = {};
472
+ if (base.before)
473
+ mw.before = base.before;
474
+ return transformUtils.tagStep(mw, 'zod-before');
475
+ };
476
+ const makeZodAfter = (logger, responseSchema) => {
477
+ const base = httpZodValidator.httpZodValidator({
478
+ logger,
479
+ ...(responseSchema ? { responseSchema } : {}),
480
+ });
481
+ const mw = {};
482
+ if (base.after)
483
+ mw.after = base.after;
484
+ return transformUtils.tagStep(mw, 'zod-after');
485
+ };
486
+ const makeHeadFinalize = (contentType) => transformUtils.tagStep({
487
+ after: (request) => {
488
+ const evt = request
489
+ .event;
490
+ if (!evt)
491
+ return;
492
+ const method = (evt.httpMethod ||
493
+ evt.requestContext?.http?.method ||
494
+ '').toUpperCase();
495
+ if (method !== 'HEAD')
496
+ return;
497
+ request.response = {
498
+ statusCode: 200,
499
+ headers: { 'Content-Type': contentType },
500
+ body: {},
501
+ };
502
+ },
503
+ }, 'head-finalize');
504
+ const makePreferredMedia = (contentType) => transformUtils.tagStep({
505
+ before: (request) => {
506
+ const req = request;
507
+ if (!Array.isArray(req.preferredMediaTypes))
508
+ req.preferredMediaTypes = [contentType];
509
+ const ri = request;
510
+ if (!ri.internal)
511
+ ri.internal = {};
512
+ const internal = ri.internal;
513
+ if (!Array.isArray(internal.preferredMediaTypes))
514
+ internal.preferredMediaTypes = [contentType];
515
+ },
516
+ after: (request) => {
517
+ const req = request;
518
+ if (!Array.isArray(req.preferredMediaTypes))
519
+ req.preferredMediaTypes = [contentType];
520
+ },
521
+ onError: (request) => {
522
+ const req = request;
523
+ if (!Array.isArray(req.preferredMediaTypes))
524
+ req.preferredMediaTypes = [contentType];
525
+ },
526
+ }, 'preferred-media');
527
+ const makeErrorExpose = (logger) => transformUtils.tagStep({
528
+ onError: (request) => {
529
+ const maybe = request.error;
530
+ if (!(maybe instanceof Error))
531
+ return;
532
+ const msg = typeof maybe.message === 'string' ? maybe.message : '';
533
+ maybe.expose = true;
534
+ if (typeof maybe.statusCode !== 'number' &&
535
+ ((typeof maybe.name === 'string' &&
536
+ maybe.name.toLowerCase().includes('zod')) ||
537
+ /invalid (event|response)/i.test(msg))) {
538
+ maybe.statusCode = 400;
539
+ }
540
+ },
541
+ }, 'error-expose');
542
+ const makeErrorHandler = (opts) => transformUtils.tagStep(asApiMiddleware.asApiMiddleware(httpErrorHandler({
543
+ ...(opts?.errorHandler ?? {}),
544
+ logger: (o) => {
545
+ const lg = opts?.logger ?? console;
546
+ if (typeof lg.error === 'function')
547
+ lg.error(o);
548
+ },
549
+ })), 'error-handler');
550
+ const makeCors = (opts) => transformUtils.tagStep(asApiMiddleware.asApiMiddleware(httpCors({
551
+ credentials: true,
552
+ getOrigin: (o) => o,
553
+ ...(opts?.cors ?? {}),
554
+ })), 'cors');
555
+ const makeShapeAndContentType = (contentType) => transformUtils.tagStep({
556
+ after: (request) => {
557
+ const container = request;
558
+ const current = container.response;
559
+ if (current === undefined)
560
+ return;
561
+ const looksShaped = typeof current === 'object' &&
562
+ current !== null &&
563
+ 'statusCode' in current &&
564
+ 'headers' in current &&
565
+ 'body' in current;
566
+ let res;
567
+ if (looksShaped)
568
+ res = current;
569
+ else
570
+ res = { statusCode: 200, headers: {}, body: current };
571
+ if (res.body !== undefined && typeof res.body !== 'string') {
572
+ try {
573
+ res.body = JSON.stringify(res.body);
574
+ }
575
+ catch {
576
+ res.body = String(res.body);
577
+ }
578
+ }
579
+ const headers = res.headers ?? {};
580
+ headers['Content-Type'] = contentType;
581
+ res.headers = headers;
582
+ request.response = res;
583
+ },
584
+ }, 'shape');
585
+ const makeSerializer = (contentType, opts) => transformUtils.tagStep(asApiMiddleware.asApiMiddleware(httpResponseSerializer({
586
+ serializers: [
587
+ {
588
+ regex: /^application\/(?:[a-z0-9.+-]*\+)?json$/i,
589
+ serializer: wrapSerializer.wrapSerializer(({ body }) => typeof body === 'string'
590
+ ? body
591
+ : (opts?.serializer?.json?.stringify ?? JSON.stringify)(body), {
592
+ label: opts?.serializer?.json?.label ?? 'json',
593
+ logger: opts?.logger ?? console,
594
+ }),
595
+ },
596
+ ],
597
+ defaultContentType: contentType,
598
+ })), 'serializer');
599
+ const buildDefaultPhases = (args) => {
600
+ const { contentType, logger, opts, eventSchema, responseSchema } = args;
601
+ const before = [
602
+ makeHead(),
603
+ makeHeaderNormalizer(opts),
604
+ makeEventNormalizer(),
605
+ makeContentNegotiation(contentType, opts),
606
+ makeJsonBodyParser(opts),
607
+ makeZodBefore(logger, eventSchema),
608
+ ];
609
+ const after = [
610
+ makeHeadFinalize(contentType),
611
+ makeZodAfter(logger, responseSchema),
612
+ makeErrorExpose(),
613
+ makeCors(opts),
614
+ makePreferredMedia(contentType),
615
+ makeShapeAndContentType(contentType),
616
+ makeSerializer(contentType, opts),
617
+ ];
618
+ const onError = [makeErrorExpose(), makeErrorHandler(opts)];
619
+ return { before, after, onError };
620
+ };
621
+ const buildSafeDefaults = (args) => buildDefaultPhases(args);
622
+
623
+ const ID_PROP = '__id';
624
+ /** Attach a non-enumerable __id to a middleware step. */
625
+ const tagStep = (mw, id) => {
626
+ if (!Object.prototype.hasOwnProperty.call(mw, ID_PROP)) {
627
+ Object.defineProperty(mw, ID_PROP, { value: id, enumerable: false });
628
+ }
629
+ return mw;
630
+ };
631
+ /** Retrieve a step's id, if present. */
632
+ const getId = (mw) => mw[ID_PROP];
633
+ /** Find index of a step by id. */
634
+ const findIndex = (list, id) => list.findIndex((m) => getId(m) === id);
635
+ /** Insert a step before the step with given id. */
636
+ const insertBefore = (list, id, mw) => {
637
+ const i = findIndex(list, id);
638
+ if (i < 0)
639
+ return list.slice();
640
+ return [...list.slice(0, i), mw, ...list.slice(i)];
641
+ };
642
+ /** Insert a step after the step with given id. */
643
+ const insertAfter = (list, id, mw) => {
644
+ const i = findIndex(list, id);
645
+ if (i < 0)
646
+ return list.slice();
647
+ return [...list.slice(0, i + 1), mw, ...list.slice(i + 1)];
648
+ };
649
+ /** Replace a step with given id. */
650
+ const replaceStep = (list, id, mw) => {
651
+ const i = findIndex(list, id);
652
+ if (i < 0)
653
+ return list.slice();
654
+ const out = list.slice();
655
+ out[i] = mw;
656
+ return out;
657
+ };
658
+ /** Remove a step with given id. */
659
+ const removeStep = (list, id) => {
660
+ const i = findIndex(list, id);
661
+ if (i < 0)
662
+ return list.slice();
663
+ return [...list.slice(0, i), ...list.slice(i + 1)];
664
+ };
665
+
666
+ /**
667
+ * Path utilities for cross-platform hygiene.
668
+ *
669
+ * - toPosixPath: normalize Windows backslashes to POSIX separators.
670
+ * - dirFromHere: resolve a directory from an import.meta.url, N levels up,
671
+ * returning a POSIX-normalized absolute path.
672
+ */
673
+ /** Normalize a path to POSIX separators. */
674
+ const toPosixPath = (p) => p.replace(/\\/g, '/');
675
+ /**
676
+ * Resolve a directory path relative to the current module URL and normalize it.
677
+ *
678
+ * @param metaUrl - typically import.meta.url
679
+ * @param levelsUp - how many directory levels to ascend (default: 1)
680
+ * @returns absolute, POSIX-normalized directory path
681
+ */
682
+ const dirFromHere = (metaUrl, levelsUp = 1) => {
683
+ // Build a URL like '../' repeated N times, resolved from metaUrl.
684
+ const up = Array.from({ length: Math.max(0, levelsUp) })
685
+ .map(() => '..')
686
+ .join('/');
687
+ const url = new URL(`${up}/`, metaUrl);
688
+ const abs = node_url.fileURLToPath(url);
689
+ return toPosixPath(abs);
690
+ };
691
+
692
+ exports.App = App;
693
+ exports.baseEventTypeMapSchema = baseEventTypeMapSchema;
694
+ exports.buildSafeDefaults = buildSafeDefaults;
695
+ exports.defineAppConfig = defineAppConfig;
696
+ exports.detectSecurityContext = detectSecurityContext;
697
+ exports.dirFromHere = dirFromHere;
698
+ exports.findIndex = findIndex;
699
+ exports.getId = getId;
700
+ exports.insertAfter = insertAfter;
701
+ exports.insertBefore = insertBefore;
702
+ exports.removeStep = removeStep;
703
+ exports.replaceStep = replaceStep;
704
+ exports.tagStep = tagStep;
705
+ exports.toPosixPath = toPosixPath;
706
+ exports.wrapHandler = wrapHandler;