sst-http 0.1.6

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/index.js ADDED
@@ -0,0 +1,512 @@
1
+ // src/registry.ts
2
+ var routeMeta = /* @__PURE__ */ new Map();
3
+ var parameterMeta = /* @__PURE__ */ new Map();
4
+ var options = {
5
+ inferPathFromName: false
6
+ };
7
+ function configureRoutes(next) {
8
+ options = {
9
+ ...options,
10
+ ...next
11
+ };
12
+ }
13
+ function registerRoute(target, method, explicitPath) {
14
+ const handler = target;
15
+ const pathInput = explicitPath ?? inferPath(handler);
16
+ const path = pathInput?.startsWith("/") ? pathInput : pathInput ? `/${pathInput}` : void 0;
17
+ if (!path) {
18
+ const name = handler.name || "<anonymous>";
19
+ throw new Error(`Route for "${name}" is missing a path. Provide one or enable name inference.`);
20
+ }
21
+ const current = routeMeta.get(handler) ?? {};
22
+ routeMeta.set(handler, {
23
+ ...current,
24
+ method,
25
+ path
26
+ });
27
+ }
28
+ function registerFirebaseAuth(target, cfg) {
29
+ const handler = target;
30
+ const current = routeMeta.get(handler) ?? {};
31
+ routeMeta.set(handler, {
32
+ ...current,
33
+ auth: {
34
+ type: "firebase",
35
+ ...cfg
36
+ }
37
+ });
38
+ }
39
+ function registerParameter(target, meta) {
40
+ const handler = target;
41
+ const list = parameterMeta.get(handler) ?? [];
42
+ list.push(meta);
43
+ list.sort((a, b) => a.index - b.index);
44
+ parameterMeta.set(handler, list);
45
+ }
46
+ function getRegisteredRoutes() {
47
+ const routes = [];
48
+ for (const [handler, meta] of routeMeta.entries()) {
49
+ if (!meta.method || !meta.path) {
50
+ const name = handler.name || "<anonymous>";
51
+ throw new Error(`Route for "${name}" is incomplete. Ensure it has an HTTP method decorator.`);
52
+ }
53
+ routes.push({
54
+ handler,
55
+ method: meta.method,
56
+ path: meta.path,
57
+ auth: meta.auth,
58
+ parameters: [...parameterMeta.get(handler) ?? []]
59
+ });
60
+ }
61
+ return routes;
62
+ }
63
+ function inferPath(handler) {
64
+ if (!options.inferPathFromName) {
65
+ return void 0;
66
+ }
67
+ const name = handler.name;
68
+ if (!name) {
69
+ return void 0;
70
+ }
71
+ const slug = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase();
72
+ return slug ? `/${slug}` : void 0;
73
+ }
74
+
75
+ // src/router.ts
76
+ import { match } from "path-to-regexp";
77
+ var Router = class {
78
+ routes;
79
+ constructor(entries) {
80
+ this.routes = entries.map((entry) => ({
81
+ entry,
82
+ matcher: match(normalizeRoutePath(entry.path), { decode: decodeURIComponent })
83
+ }));
84
+ }
85
+ find(method, pathname) {
86
+ const allowed = /* @__PURE__ */ new Set();
87
+ for (const route of this.routes) {
88
+ const result = route.matcher(pathname);
89
+ if (!result) {
90
+ continue;
91
+ }
92
+ allowed.add(route.entry.method);
93
+ if (route.entry.method === method) {
94
+ return {
95
+ type: "found",
96
+ entry: route.entry,
97
+ params: normalizeParams(result.params)
98
+ };
99
+ }
100
+ }
101
+ if (allowed.size > 0) {
102
+ return {
103
+ type: "method-not-allowed",
104
+ allowedMethods: [...allowed]
105
+ };
106
+ }
107
+ return void 0;
108
+ }
109
+ };
110
+ function normalizeParams(params) {
111
+ const normalized = {};
112
+ for (const [key, value] of Object.entries(params)) {
113
+ normalized[key] = Array.isArray(value) ? value[value.length - 1] ?? "" : value;
114
+ }
115
+ return normalized;
116
+ }
117
+ function normalizeRoutePath(path) {
118
+ return path.replace(/\{([^/{}]+)\}/g, ":$1");
119
+ }
120
+
121
+ // src/runtime.ts
122
+ var HttpError = class extends Error {
123
+ statusCode;
124
+ headers;
125
+ details;
126
+ constructor(statusCode, message, options2) {
127
+ super(message);
128
+ this.name = "HttpError";
129
+ this.statusCode = statusCode;
130
+ this.headers = options2?.headers;
131
+ this.details = options2?.details;
132
+ if ("cause" in (options2 ?? {})) {
133
+ this.cause = options2?.cause;
134
+ }
135
+ }
136
+ };
137
+ function json(status, data, headers = {}) {
138
+ return {
139
+ statusCode: status,
140
+ body: JSON.stringify(data ?? null),
141
+ headers: {
142
+ "content-type": "application/json; charset=utf-8",
143
+ ...headers
144
+ }
145
+ };
146
+ }
147
+ function text(status, body, headers = {}) {
148
+ return {
149
+ statusCode: status,
150
+ body,
151
+ headers: {
152
+ "content-type": "text/plain; charset=utf-8",
153
+ ...headers
154
+ }
155
+ };
156
+ }
157
+ function noContent(headers = {}) {
158
+ return {
159
+ statusCode: 204,
160
+ headers,
161
+ body: ""
162
+ };
163
+ }
164
+ function createHandler() {
165
+ const routes = getRegisteredRoutes();
166
+ const router = new Router(routes);
167
+ return async (event, lambdaContext) => {
168
+ const method = extractMethod(event);
169
+ const path = extractPath(event);
170
+ const preferV2 = isHttpApiEvent(event);
171
+ if (!method || !path) {
172
+ return formatResponse(text(400, "Invalid request"), preferV2);
173
+ }
174
+ const normalizedMethod = method.toUpperCase();
175
+ if (!isSupportedMethod(normalizedMethod)) {
176
+ return formatResponse(text(405, "Method Not Allowed"), preferV2);
177
+ }
178
+ const match2 = router.find(normalizedMethod, path);
179
+ if (!match2) {
180
+ return formatResponse(text(404, "Not Found"), preferV2);
181
+ }
182
+ if (match2.type === "method-not-allowed") {
183
+ return formatResponse({
184
+ statusCode: 405,
185
+ headers: {
186
+ Allow: match2.allowedMethods.join(", ")
187
+ },
188
+ body: ""
189
+ }, preferV2);
190
+ }
191
+ const { entry, params } = match2;
192
+ const headers = normalizeHeaders(event.headers ?? {});
193
+ const query = extractQuery(event);
194
+ let bodyValue = void 0;
195
+ let bodyParsed = false;
196
+ const requiresJson = entry.parameters.some((p) => p.type === "body");
197
+ const ensureBody = () => {
198
+ if (!bodyParsed) {
199
+ bodyValue = parseBody(event, headers, requiresJson);
200
+ bodyParsed = true;
201
+ }
202
+ return bodyValue;
203
+ };
204
+ const ctxResponse = {
205
+ json,
206
+ text,
207
+ noContent
208
+ };
209
+ const ctx = {
210
+ event,
211
+ lambdaContext,
212
+ params,
213
+ query,
214
+ body: void 0,
215
+ headers,
216
+ auth: extractAuthClaims(event, entry),
217
+ response: ctxResponse
218
+ };
219
+ const getBody = (schema) => {
220
+ const current = ensureBody();
221
+ if (schema && typeof schema.parse === "function") {
222
+ try {
223
+ bodyValue = schema.parse(current);
224
+ } catch (error) {
225
+ throw new HttpError(400, "Body validation failed", { cause: error });
226
+ }
227
+ }
228
+ ctx.body = bodyValue;
229
+ return bodyValue;
230
+ };
231
+ try {
232
+ ctx.body = ensureBody();
233
+ const args = buildHandlerArguments(entry, ctx, getBody);
234
+ const result = await entry.handler(...args);
235
+ if (result === void 0) {
236
+ return formatResponse(noContent(), preferV2);
237
+ }
238
+ return formatResponse(result, preferV2);
239
+ } catch (error) {
240
+ return handleError(error, preferV2);
241
+ }
242
+ };
243
+ }
244
+ function buildHandlerArguments(entry, ctx, getBody) {
245
+ const maxIndex = entry.parameters.reduce((max, meta) => Math.max(max, meta.index), -1);
246
+ const length = Math.max(entry.handler.length, maxIndex + 1, 1);
247
+ const args = new Array(length).fill(void 0);
248
+ for (const meta of entry.parameters) {
249
+ switch (meta.type) {
250
+ case "body": {
251
+ args[meta.index] = getBody(meta.schema);
252
+ break;
253
+ }
254
+ case "query": {
255
+ args[meta.index] = ctx.query;
256
+ break;
257
+ }
258
+ case "param": {
259
+ args[meta.index] = ctx.params;
260
+ break;
261
+ }
262
+ case "headers": {
263
+ args[meta.index] = ctx.headers;
264
+ break;
265
+ }
266
+ case "req": {
267
+ args[meta.index] = ctx.event;
268
+ break;
269
+ }
270
+ case "res": {
271
+ args[meta.index] = ctx.response;
272
+ break;
273
+ }
274
+ default: {
275
+ args[meta.index] = ctx;
276
+ }
277
+ }
278
+ }
279
+ for (let i = 0; i < length; i += 1) {
280
+ if (args[i] === void 0) {
281
+ args[i] = ctx;
282
+ }
283
+ }
284
+ return args;
285
+ }
286
+ function parseBody(event, headers, forceJson) {
287
+ const raw = extractRawBody(event);
288
+ if (raw === void 0) {
289
+ return void 0;
290
+ }
291
+ const contentType = headers["content-type"];
292
+ const shouldParse = forceJson || isJsonContentType(contentType);
293
+ if (!shouldParse) {
294
+ return raw;
295
+ }
296
+ if (raw.trim() === "") {
297
+ return void 0;
298
+ }
299
+ try {
300
+ return JSON.parse(raw);
301
+ } catch (error) {
302
+ throw new HttpError(400, "Invalid JSON body", { cause: error });
303
+ }
304
+ }
305
+ function extractRawBody(event) {
306
+ if (!event.body) {
307
+ return void 0;
308
+ }
309
+ if (event.isBase64Encoded) {
310
+ return Buffer.from(event.body, "base64").toString("utf8");
311
+ }
312
+ return event.body;
313
+ }
314
+ function isJsonContentType(contentType) {
315
+ if (!contentType) {
316
+ return false;
317
+ }
318
+ return contentType.includes("application/json") || contentType.includes("+json");
319
+ }
320
+ function normalizeHeaders(headers) {
321
+ const normalized = {};
322
+ for (const [key, value] of Object.entries(headers)) {
323
+ if (typeof value === "undefined") {
324
+ continue;
325
+ }
326
+ normalized[key.toLowerCase()] = value;
327
+ }
328
+ return normalized;
329
+ }
330
+ function extractQuery(event) {
331
+ const single = event.queryStringParameters ?? event.queryStringParameters ?? {};
332
+ const multi = event.multiValueQueryStringParameters ?? {};
333
+ const query = {};
334
+ for (const [key, value] of Object.entries(single ?? {})) {
335
+ query[key] = value ?? void 0;
336
+ }
337
+ for (const [key, value] of Object.entries(multi ?? {})) {
338
+ if (!value || value.length === 0) {
339
+ continue;
340
+ }
341
+ query[key] = value[value.length - 1];
342
+ }
343
+ return query;
344
+ }
345
+ function extractMethod(event) {
346
+ return event.requestContext?.http?.method || event.httpMethod || void 0;
347
+ }
348
+ function extractPath(event) {
349
+ return event.rawPath || event.path || void 0;
350
+ }
351
+ function extractAuthClaims(event, entry) {
352
+ if (!entry.auth || entry.auth.type !== "firebase") {
353
+ return void 0;
354
+ }
355
+ const ctxV2 = event.requestContext;
356
+ const ctxV1 = event.requestContext;
357
+ const claims = ctxV2?.authorizer?.jwt?.claims || ctxV1?.authorizer?.claims;
358
+ return claims ?? void 0;
359
+ }
360
+ function handleError(error, preferV2) {
361
+ if (error instanceof HttpError) {
362
+ return formatResponse({
363
+ statusCode: error.statusCode,
364
+ headers: {
365
+ "content-type": "application/json; charset=utf-8",
366
+ ...error.headers
367
+ },
368
+ body: error.details ? JSON.stringify({ message: error.message, details: error.details }) : JSON.stringify({ message: error.message })
369
+ }, preferV2);
370
+ }
371
+ console.error("Unhandled error in sst-http handler", error);
372
+ return formatResponse({
373
+ statusCode: 500,
374
+ headers: {
375
+ "content-type": "application/json; charset=utf-8"
376
+ },
377
+ body: JSON.stringify({ message: "Internal Server Error" })
378
+ }, preferV2);
379
+ }
380
+ function formatResponse(result, preferV2) {
381
+ if (typeof result === "string") {
382
+ result = { statusCode: 200, body: result };
383
+ }
384
+ const normalized = {
385
+ statusCode: result.statusCode ?? 200,
386
+ headers: result.headers,
387
+ body: result.body ?? "",
388
+ cookies: "cookies" in result ? result.cookies : void 0,
389
+ isBase64Encoded: result.isBase64Encoded
390
+ };
391
+ if (preferV2) {
392
+ const response2 = {
393
+ statusCode: normalized.statusCode,
394
+ headers: normalized.headers,
395
+ body: normalized.body
396
+ };
397
+ if (normalized.cookies) {
398
+ response2.cookies = normalized.cookies;
399
+ }
400
+ if (typeof normalized.isBase64Encoded === "boolean") {
401
+ response2.isBase64Encoded = normalized.isBase64Encoded;
402
+ }
403
+ return response2;
404
+ }
405
+ const response = {
406
+ statusCode: normalized.statusCode,
407
+ headers: normalized.headers,
408
+ body: normalized.body
409
+ };
410
+ if (typeof normalized.isBase64Encoded === "boolean") {
411
+ response.isBase64Encoded = normalized.isBase64Encoded;
412
+ }
413
+ return response;
414
+ }
415
+ function isHttpApiEvent(event) {
416
+ return event.version === "2.0";
417
+ }
418
+ var SUPPORTED_METHODS = /* @__PURE__ */ new Set([
419
+ "GET",
420
+ "POST",
421
+ "PUT",
422
+ "PATCH",
423
+ "DELETE",
424
+ "HEAD",
425
+ "OPTIONS"
426
+ ]);
427
+ function isSupportedMethod(value) {
428
+ return SUPPORTED_METHODS.has(value);
429
+ }
430
+
431
+ // src/decorators.ts
432
+ function resolveHandler(target, propertyKey, descriptor) {
433
+ if (descriptor?.value && typeof descriptor.value === "function") {
434
+ return descriptor.value;
435
+ }
436
+ if (typeof target === "function" && propertyKey === void 0) {
437
+ return target;
438
+ }
439
+ if (target && propertyKey && typeof target[propertyKey] === "function") {
440
+ return target[propertyKey];
441
+ }
442
+ throw new Error("Unable to determine decorated function. Ensure decorators are applied to functions.");
443
+ }
444
+ function createRouteDecorator(method) {
445
+ return (path) => (target, propertyKey, descriptor) => {
446
+ const handler = resolveHandler(target, propertyKey, descriptor);
447
+ registerRoute(handler, method, path);
448
+ };
449
+ }
450
+ function createParameterDecorator(type, schema) {
451
+ return (target, propertyKey, parameterIndex) => {
452
+ const handler = resolveHandler(target, propertyKey);
453
+ registerParameter(handler, {
454
+ index: parameterIndex,
455
+ type,
456
+ schema
457
+ });
458
+ };
459
+ }
460
+ var Get = createRouteDecorator("GET");
461
+ var Post = createRouteDecorator("POST");
462
+ var Put = createRouteDecorator("PUT");
463
+ var Patch = createRouteDecorator("PATCH");
464
+ var Delete = createRouteDecorator("DELETE");
465
+ var Head = createRouteDecorator("HEAD");
466
+ var Options = createRouteDecorator("OPTIONS");
467
+ function FirebaseAuth(options2) {
468
+ return (target, propertyKey, descriptor) => {
469
+ const handler = resolveHandler(target, propertyKey, descriptor);
470
+ registerFirebaseAuth(handler, options2);
471
+ };
472
+ }
473
+ function Body(schema) {
474
+ return createParameterDecorator("body", schema);
475
+ }
476
+ function Query() {
477
+ return createParameterDecorator("query");
478
+ }
479
+ function Param() {
480
+ return createParameterDecorator("param");
481
+ }
482
+ function Headers() {
483
+ return createParameterDecorator("headers");
484
+ }
485
+ function Req() {
486
+ return createParameterDecorator("req");
487
+ }
488
+ function Res() {
489
+ return createParameterDecorator("res");
490
+ }
491
+ export {
492
+ Body,
493
+ Delete,
494
+ FirebaseAuth,
495
+ Get,
496
+ Head,
497
+ Headers,
498
+ HttpError,
499
+ Options,
500
+ Param,
501
+ Patch,
502
+ Post,
503
+ Put,
504
+ Query,
505
+ Req,
506
+ Res,
507
+ configureRoutes,
508
+ createHandler,
509
+ json,
510
+ noContent,
511
+ text
512
+ };