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