@karmaniverous/smoz 0.2.15 → 0.2.16

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 CHANGED
@@ -997,6 +997,12 @@ const serverlessConfigSchema = zod.z.object({
997
997
  defaultHandlerFileExport: zod.z.string().min(1),
998
998
  });
999
999
 
1000
+ /** Security context classification used throughout the handlers. */
1001
+ /**
1002
+ * HTTP security context tokens.
1003
+ */
1004
+ const httpContexts = ['my', 'private', 'public'];
1005
+
1000
1006
  const splitPath = (basePath) => basePath.split('/').filter(Boolean);
1001
1007
  /** Prefix non-public contexts and return path elements. */
1002
1008
  /**
@@ -1011,6 +1017,19 @@ const buildPathElements = (basePath, context) => {
1011
1017
  return context === 'public' ? parts : [context, ...parts];
1012
1018
  };
1013
1019
  const sanitizeBasePath = (p) => p.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
1020
+ /**
1021
+ * Infer the security context from a path that was built by
1022
+ * `buildPathElements`. Non-public contexts appear as the first
1023
+ * path segment; public paths have no context prefix.
1024
+ */
1025
+ const inferContextFromPath = (path) => {
1026
+ const firstSeg = splitPath(sanitizeBasePath(path))[0];
1027
+ return firstSeg !== undefined &&
1028
+ httpContexts.includes(firstSeg) &&
1029
+ firstSeg !== 'public'
1030
+ ? firstSeg
1031
+ : 'public';
1032
+ };
1014
1033
 
1015
1034
  const HTTP_METHODS = new Set([
1016
1035
  'get',
@@ -1129,11 +1148,15 @@ const buildAllServerlessFunctions = (registry, serverless, buildFnEnv) => {
1129
1148
  ...(r.basePath ? { basePath: r.basePath } : {}),
1130
1149
  ...(r.httpContexts ? { httpContexts: r.httpContexts } : {}),
1131
1150
  }, r.callerModuleUrl, r.endpointsRootAbs);
1132
- const path = `/${basePath.replace(/^\/+/, '')}`;
1133
1151
  const ctxs = contexts.length > 0 ? contexts : ['public'];
1134
- events = ctxs.map(() => ({
1135
- http: { method, path },
1136
- }));
1152
+ events = ctxs.map((ctx) => {
1153
+ const elems = buildPathElements(basePath, ctx);
1154
+ const ctxPath = `/${elems.join('/')}`;
1155
+ const ctxEventProps = serverless.httpContextEventMap[ctx];
1156
+ return {
1157
+ http: { method, path: ctxPath, ...ctxEventProps },
1158
+ };
1159
+ });
1137
1160
  }
1138
1161
  catch {
1139
1162
  events = r.serverlessExtras ?? [];
@@ -1480,12 +1503,15 @@ const dirFromHere = (metaUrl, levelsUp = 1) => {
1480
1503
 
1481
1504
  exports.App = App;
1482
1505
  exports.baseEventTypeMapSchema = baseEventTypeMapSchema;
1506
+ exports.buildPathElements = buildPathElements;
1483
1507
  exports.buildSafeDefaults = buildSafeDefaults;
1484
1508
  exports.defineAppConfig = defineAppConfig;
1485
1509
  exports.detectSecurityContext = detectSecurityContext;
1486
1510
  exports.dirFromHere = dirFromHere;
1487
1511
  exports.findIndex = findIndex;
1488
1512
  exports.getId = getId;
1513
+ exports.httpContexts = httpContexts;
1514
+ exports.inferContextFromPath = inferContextFromPath;
1489
1515
  exports.insertAfter = insertAfter;
1490
1516
  exports.insertBefore = insertBefore;
1491
1517
  exports.removeStep = removeStep;
package/dist/index.d.ts CHANGED
@@ -122,7 +122,8 @@ declare function buildStageArtifacts<GlobalParamsSchema extends ZodObject<ZodRaw
122
122
  /**
123
123
  * HTTP security context tokens.
124
124
  */
125
- type HttpContext = 'my' | 'private' | 'public';
125
+ declare const httpContexts: readonly ["my", "private", "public"];
126
+ type HttpContext = (typeof httpContexts)[number];
126
127
 
127
128
  /**
128
129
  * Extract the property type `K` from a union of object types `U`.
@@ -625,6 +626,22 @@ declare function wrapHandler<GlobalParamsSchema extends ZodObject<ZodRawShape>,
625
626
  */
626
627
  type LambdaEvent = Record<string, unknown>;
627
628
 
629
+ /** Prefix non-public contexts and return path elements. */
630
+ /**
631
+ * Build OpenAPI/serverless path elements from a base path and context.
632
+ *
633
+ * @param basePath - base path (e.g., 'users/:id')
634
+ * @param context - 'my' | 'private' | 'public' (public is unprefixed)
635
+ * @returns array of path segments without leading/trailing slashes
636
+ */
637
+ declare const buildPathElements: (basePath: string, context: HttpContext) => string[];
638
+ /**
639
+ * Infer the security context from a path that was built by
640
+ * `buildPathElements`. Non-public contexts appear as the first
641
+ * path segment; public paths have no context prefix.
642
+ */
643
+ declare const inferContextFromPath: (path: string) => HttpContext;
644
+
628
645
  /** Normalize a path to POSIX separators. */
629
646
  declare const toPosixPath: (p: string) => string;
630
647
  /**
@@ -636,5 +653,5 @@ declare const toPosixPath: (p: string) => string;
636
653
  */
637
654
  declare const dirFromHere: (metaUrl: string, levelsUp?: number) => string;
638
655
 
639
- export { App, baseEventTypeMapSchema, buildSafeDefaults, defineAppConfig, detectSecurityContext, dirFromHere, findIndex, getId, insertAfter, insertBefore, removeStep, replaceStep, tagStep, toPosixPath, wrapHandler };
656
+ export { App, baseEventTypeMapSchema, buildPathElements, buildSafeDefaults, defineAppConfig, detectSecurityContext, dirFromHere, findIndex, getId, httpContexts, inferContextFromPath, insertAfter, insertBefore, removeStep, replaceStep, tagStep, toPosixPath, wrapHandler };
640
657
  export type { AppHttpConfig, AppInit, ConsoleLogger, DefineAppConfigInput, DefineAppConfigOutput, EnvKeysNode, EnvSchemaNode, FunctionHttpConfig, GlobalEnvConfig, GlobalParamsNode, Handler, HandlerOptions, HttpContext, HttpProfile, HttpStackOptions, LambdaEvent, MethodKey, SecurityContextHttpEventMap, ShapedEvent, StageParamsNode };
@@ -48,6 +48,46 @@ const match = (segs, pathName) => {
48
48
  }
49
49
  return { ok: true, params };
50
50
  };
51
+ /**
52
+ * Decode JWT payload without signature validation (local dev only).
53
+ * Returns the parsed claims object, or an empty object on failure.
54
+ */
55
+ const decodeJwtClaims = (token) => {
56
+ try {
57
+ const parts = token.replace(/^Bearer\s+/i, '').split('.');
58
+ if (parts.length < 2)
59
+ return {};
60
+ const payload = Buffer.from(parts[1], 'base64url').toString('utf8');
61
+ const parsed = JSON.parse(payload);
62
+ return typeof parsed === 'object' && parsed !== null
63
+ ? parsed
64
+ : {};
65
+ }
66
+ catch {
67
+ return {};
68
+ }
69
+ };
70
+ /**
71
+ * Build a local-dev authorizer that makes `detectSecurityContext`
72
+ * return the correct context for the route.
73
+ *
74
+ * Only populates the authorizer when the client actually sends
75
+ * credentials, so `detectSecurityContext` reflects reality:
76
+ * - `my` route + JWT in Authorization header → decoded claims.
77
+ * - `private` route → no authorizer; the forwarded `x-api-key`
78
+ * header is enough for `detectSecurityContext`.
79
+ * - No credentials → empty authorizer → `public`.
80
+ */
81
+ const buildLocalAuthorizer = (context, authHeader) => {
82
+ if (context === 'my' && authHeader) {
83
+ const claims = decodeJwtClaims(authHeader);
84
+ if (Object.keys(claims).length > 0)
85
+ return { claims };
86
+ }
87
+ // Return undefined so detectSecurityContext sees no authorizer,
88
+ // rather than {} which it treats as truthy.
89
+ return undefined;
90
+ };
51
91
  const toEvent = async (req, route, params) => {
52
92
  const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
53
93
  const { single, multi } = toHeaders(req.headers);
@@ -90,7 +130,7 @@ const toEvent = async (req, route, params) => {
90
130
  requestTimeEpoch: Date.now(),
91
131
  resourceId: 'res',
92
132
  resourcePath: route.pattern,
93
- authorizer: {},
133
+ authorizer: buildLocalAuthorizer(route.context, single['authorization']),
94
134
  protocol: 'HTTP/1.1',
95
135
  },
96
136
  };
@@ -149,6 +189,28 @@ const loadApp = async (root) => {
149
189
  return app;
150
190
  };
151
191
 
192
+ /** Security context classification used throughout the handlers. */
193
+ /**
194
+ * HTTP security context tokens.
195
+ */
196
+ const httpContexts = ['my', 'private', 'public'];
197
+
198
+ const splitPath = (basePath) => basePath.split('/').filter(Boolean);
199
+ const sanitizeBasePath = (p) => p.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
200
+ /**
201
+ * Infer the security context from a path that was built by
202
+ * `buildPathElements`. Non-public contexts appear as the first
203
+ * path segment; public paths have no context prefix.
204
+ */
205
+ const inferContextFromPath = (path) => {
206
+ const firstSeg = splitPath(sanitizeBasePath(path))[0];
207
+ return firstSeg !== undefined &&
208
+ httpContexts.includes(firstSeg) &&
209
+ firstSeg !== 'public'
210
+ ? firstSeg
211
+ : 'public';
212
+ };
213
+
152
214
  const splitPattern = (p) => p
153
215
  .replace(/\\/g, '/')
154
216
  .replace(/^\/+|\/+$/g, '')
@@ -195,11 +257,13 @@ const loadHandlers = async (root, app) => {
195
257
  const pattern = '/' + (httpEvt?.path ?? '').replace(/^\/+/, '');
196
258
  if (!method || !pattern)
197
259
  continue;
260
+ const context = inferContextFromPath(pattern);
198
261
  routes.push({
199
262
  method,
200
263
  pattern,
201
264
  segs: splitPattern(pattern),
202
265
  handlerRef: `${moduleRel}.${exportName}`,
266
+ context,
203
267
  handler: handler,
204
268
  });
205
269
  }
package/dist/mjs/index.js CHANGED
@@ -995,6 +995,12 @@ const serverlessConfigSchema = z.object({
995
995
  defaultHandlerFileExport: z.string().min(1),
996
996
  });
997
997
 
998
+ /** Security context classification used throughout the handlers. */
999
+ /**
1000
+ * HTTP security context tokens.
1001
+ */
1002
+ const httpContexts = ['my', 'private', 'public'];
1003
+
998
1004
  const splitPath = (basePath) => basePath.split('/').filter(Boolean);
999
1005
  /** Prefix non-public contexts and return path elements. */
1000
1006
  /**
@@ -1009,6 +1015,19 @@ const buildPathElements = (basePath, context) => {
1009
1015
  return context === 'public' ? parts : [context, ...parts];
1010
1016
  };
1011
1017
  const sanitizeBasePath = (p) => p.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
1018
+ /**
1019
+ * Infer the security context from a path that was built by
1020
+ * `buildPathElements`. Non-public contexts appear as the first
1021
+ * path segment; public paths have no context prefix.
1022
+ */
1023
+ const inferContextFromPath = (path) => {
1024
+ const firstSeg = splitPath(sanitizeBasePath(path))[0];
1025
+ return firstSeg !== undefined &&
1026
+ httpContexts.includes(firstSeg) &&
1027
+ firstSeg !== 'public'
1028
+ ? firstSeg
1029
+ : 'public';
1030
+ };
1012
1031
 
1013
1032
  const HTTP_METHODS = new Set([
1014
1033
  'get',
@@ -1127,11 +1146,15 @@ const buildAllServerlessFunctions = (registry, serverless, buildFnEnv) => {
1127
1146
  ...(r.basePath ? { basePath: r.basePath } : {}),
1128
1147
  ...(r.httpContexts ? { httpContexts: r.httpContexts } : {}),
1129
1148
  }, r.callerModuleUrl, r.endpointsRootAbs);
1130
- const path = `/${basePath.replace(/^\/+/, '')}`;
1131
1149
  const ctxs = contexts.length > 0 ? contexts : ['public'];
1132
- events = ctxs.map(() => ({
1133
- http: { method, path },
1134
- }));
1150
+ events = ctxs.map((ctx) => {
1151
+ const elems = buildPathElements(basePath, ctx);
1152
+ const ctxPath = `/${elems.join('/')}`;
1153
+ const ctxEventProps = serverless.httpContextEventMap[ctx];
1154
+ return {
1155
+ http: { method, path: ctxPath, ...ctxEventProps },
1156
+ };
1157
+ });
1135
1158
  }
1136
1159
  catch {
1137
1160
  events = r.serverlessExtras ?? [];
@@ -1476,4 +1499,4 @@ const dirFromHere = (metaUrl, levelsUp = 1) => {
1476
1499
  return toPosixPath(abs);
1477
1500
  };
1478
1501
 
1479
- export { App, baseEventTypeMapSchema, buildSafeDefaults, defineAppConfig, detectSecurityContext, dirFromHere, findIndex, getId, insertAfter, insertBefore, removeStep, replaceStep, tagStep, toPosixPath, wrapHandler };
1502
+ export { App, baseEventTypeMapSchema, buildPathElements, buildSafeDefaults, defineAppConfig, detectSecurityContext, dirFromHere, findIndex, getId, httpContexts, inferContextFromPath, insertAfter, insertBefore, removeStep, replaceStep, tagStep, toPosixPath, wrapHandler };
package/package.json CHANGED
@@ -183,7 +183,7 @@
183
183
  "templates:lint": "eslint --fix -c templates/default/eslint.config.ts \"templates/default/**/*.{ts,tsx,js,jsx}\" \"templates/default/eslint.config.ts\""
184
184
  },
185
185
  "type": "module",
186
- "version": "0.2.15",
186
+ "version": "0.2.16",
187
187
  "volta": {
188
188
  "node": "22.19.0"
189
189
  }