@parsrun/server 0.1.0

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,880 @@
1
+ // src/context.ts
2
+ function error(code, message, details) {
3
+ return {
4
+ success: false,
5
+ error: { code, message, details: details ?? void 0 }
6
+ };
7
+ }
8
+ function generateRequestId() {
9
+ return crypto.randomUUID();
10
+ }
11
+
12
+ // src/middleware/error-handler.ts
13
+ var ApiError = class extends Error {
14
+ constructor(statusCode, code, message, details) {
15
+ super(message);
16
+ this.statusCode = statusCode;
17
+ this.code = code;
18
+ this.details = details;
19
+ this.name = "ApiError";
20
+ }
21
+ toResponse() {
22
+ return error(this.code, this.message, this.details);
23
+ }
24
+ };
25
+ var BadRequestError = class extends ApiError {
26
+ constructor(message = "Bad request", details) {
27
+ super(400, "BAD_REQUEST", message, details);
28
+ this.name = "BadRequestError";
29
+ }
30
+ };
31
+ var UnauthorizedError = class extends ApiError {
32
+ constructor(message = "Unauthorized", details) {
33
+ super(401, "UNAUTHORIZED", message, details);
34
+ this.name = "UnauthorizedError";
35
+ }
36
+ };
37
+ var ForbiddenError = class extends ApiError {
38
+ constructor(message = "Forbidden", details) {
39
+ super(403, "FORBIDDEN", message, details);
40
+ this.name = "ForbiddenError";
41
+ }
42
+ };
43
+ var NotFoundError = class extends ApiError {
44
+ constructor(message = "Not found", details) {
45
+ super(404, "NOT_FOUND", message, details);
46
+ this.name = "NotFoundError";
47
+ }
48
+ };
49
+ var ConflictError = class extends ApiError {
50
+ constructor(message = "Conflict", details) {
51
+ super(409, "CONFLICT", message, details);
52
+ this.name = "ConflictError";
53
+ }
54
+ };
55
+ var ValidationError = class extends ApiError {
56
+ constructor(message = "Validation failed", details) {
57
+ super(422, "VALIDATION_ERROR", message, details);
58
+ this.name = "ValidationError";
59
+ }
60
+ };
61
+ var RateLimitError = class extends ApiError {
62
+ constructor(message = "Too many requests", retryAfter) {
63
+ super(429, "RATE_LIMIT_EXCEEDED", message, { retryAfter });
64
+ this.retryAfter = retryAfter;
65
+ this.name = "RateLimitError";
66
+ }
67
+ };
68
+ var InternalError = class extends ApiError {
69
+ constructor(message = "Internal server error", details) {
70
+ super(500, "INTERNAL_ERROR", message, details);
71
+ this.name = "InternalError";
72
+ }
73
+ };
74
+ var ServiceUnavailableError = class extends ApiError {
75
+ constructor(message = "Service unavailable", details) {
76
+ super(503, "SERVICE_UNAVAILABLE", message, details);
77
+ this.name = "ServiceUnavailableError";
78
+ }
79
+ };
80
+ function errorHandler(options = {}) {
81
+ const {
82
+ includeStack = false,
83
+ onError,
84
+ errorTransport,
85
+ captureAllErrors = false,
86
+ shouldCapture
87
+ } = options;
88
+ return async (c, next) => {
89
+ try {
90
+ await next();
91
+ } catch (err) {
92
+ const error2 = err instanceof Error ? err : new Error(String(err));
93
+ const statusCode = error2 instanceof ApiError ? error2.statusCode : 500;
94
+ if (onError) {
95
+ onError(error2, c);
96
+ } else {
97
+ const logger = c.get("logger");
98
+ if (logger) {
99
+ logger.error("Request error", {
100
+ requestId: c.get("requestId"),
101
+ error: error2.message,
102
+ stack: error2.stack
103
+ });
104
+ }
105
+ }
106
+ if (errorTransport) {
107
+ const shouldCaptureError = shouldCapture ? shouldCapture(error2, statusCode) : captureAllErrors || statusCode >= 500;
108
+ if (shouldCaptureError) {
109
+ const user = c.get("user");
110
+ const tenant = c.get("tenant");
111
+ const errorContext = {
112
+ requestId: c.get("requestId"),
113
+ tags: {
114
+ path: c.req.path,
115
+ method: c.req.method,
116
+ statusCode: String(statusCode)
117
+ }
118
+ };
119
+ if (user?.id) {
120
+ errorContext.userId = user.id;
121
+ }
122
+ if (tenant?.id) {
123
+ errorContext.tenantId = tenant.id;
124
+ }
125
+ const extra = {
126
+ query: c.req.query()
127
+ };
128
+ if (error2 instanceof ApiError) {
129
+ extra["errorCode"] = error2.code;
130
+ }
131
+ errorContext.extra = extra;
132
+ Promise.resolve(
133
+ errorTransport.captureException(error2, errorContext)
134
+ ).catch(() => {
135
+ });
136
+ }
137
+ }
138
+ if (error2 instanceof ApiError) {
139
+ return c.json(error2.toResponse(), error2.statusCode);
140
+ }
141
+ const details = {};
142
+ if (includeStack && error2.stack) {
143
+ details["stack"] = error2.stack;
144
+ }
145
+ return c.json(
146
+ error("INTERNAL_ERROR", "An unexpected error occurred", details),
147
+ 500
148
+ );
149
+ }
150
+ };
151
+ }
152
+ function notFoundHandler(c) {
153
+ return c.json(
154
+ error("NOT_FOUND", `Route ${c.req.method} ${c.req.path} not found`),
155
+ 404
156
+ );
157
+ }
158
+
159
+ // src/middleware/auth.ts
160
+ function extractToken(c, header, prefix, cookie) {
161
+ const authHeader = c.req.header(header);
162
+ if (authHeader) {
163
+ if (prefix && authHeader.startsWith(`${prefix} `)) {
164
+ return authHeader.slice(prefix.length + 1);
165
+ }
166
+ return authHeader;
167
+ }
168
+ if (cookie) {
169
+ const cookieHeader = c.req.header("cookie");
170
+ if (cookieHeader) {
171
+ const cookies = cookieHeader.split(";").map((c2) => c2.trim());
172
+ for (const c2 of cookies) {
173
+ const [key, ...valueParts] = c2.split("=");
174
+ if (key === cookie) {
175
+ return valueParts.join("=");
176
+ }
177
+ }
178
+ }
179
+ }
180
+ return null;
181
+ }
182
+ function auth(options) {
183
+ const {
184
+ verify,
185
+ header = "authorization",
186
+ prefix = "Bearer",
187
+ cookie,
188
+ skip,
189
+ message = "Authentication required"
190
+ } = options;
191
+ return async (c, next) => {
192
+ if (skip?.(c)) {
193
+ return next();
194
+ }
195
+ const token = extractToken(c, header, prefix, cookie);
196
+ if (!token) {
197
+ throw new UnauthorizedError(message);
198
+ }
199
+ const payload = await verify(token);
200
+ if (!payload) {
201
+ throw new UnauthorizedError("Invalid or expired token");
202
+ }
203
+ const user = {
204
+ id: payload.sub,
205
+ email: payload.email,
206
+ tenantId: payload.tenantId,
207
+ role: payload.role,
208
+ permissions: payload.permissions ?? []
209
+ };
210
+ c.set("user", user);
211
+ await next();
212
+ };
213
+ }
214
+ function optionalAuth(options) {
215
+ const { verify, header = "authorization", prefix = "Bearer", cookie, skip } = options;
216
+ return async (c, next) => {
217
+ if (skip?.(c)) {
218
+ return next();
219
+ }
220
+ const token = extractToken(c, header, prefix, cookie);
221
+ if (token) {
222
+ try {
223
+ const payload = await verify(token);
224
+ if (payload) {
225
+ const user = {
226
+ id: payload.sub,
227
+ email: payload.email,
228
+ tenantId: payload.tenantId,
229
+ role: payload.role,
230
+ permissions: payload.permissions ?? []
231
+ };
232
+ c.set("user", user);
233
+ }
234
+ } catch {
235
+ }
236
+ }
237
+ await next();
238
+ };
239
+ }
240
+ function createAuthMiddleware(baseOptions) {
241
+ return {
242
+ auth: (options) => auth({ ...baseOptions, ...options }),
243
+ optionalAuth: (options) => optionalAuth({ ...baseOptions, ...options })
244
+ };
245
+ }
246
+
247
+ // src/middleware/cors.ts
248
+ var defaultCorsConfig = {
249
+ origin: "*",
250
+ credentials: false,
251
+ methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
252
+ allowedHeaders: ["Content-Type", "Authorization", "X-Request-ID", "X-CSRF-Token"],
253
+ exposedHeaders: ["X-Request-ID", "X-Total-Count"],
254
+ maxAge: 86400
255
+ // 24 hours
256
+ };
257
+ function isOriginAllowed(origin, config) {
258
+ if (config.origin === "*") return true;
259
+ if (typeof config.origin === "string") {
260
+ return origin === config.origin;
261
+ }
262
+ if (Array.isArray(config.origin)) {
263
+ return config.origin.includes(origin);
264
+ }
265
+ if (typeof config.origin === "function") {
266
+ return config.origin(origin);
267
+ }
268
+ return false;
269
+ }
270
+ function cors(config) {
271
+ const corsConfig = { ...defaultCorsConfig, ...config };
272
+ return async (c, next) => {
273
+ const origin = c.req.header("origin") ?? "";
274
+ if (c.req.method === "OPTIONS") {
275
+ const response = new Response(null, { status: 204 });
276
+ if (isOriginAllowed(origin, corsConfig)) {
277
+ response.headers.set("Access-Control-Allow-Origin", origin || "*");
278
+ }
279
+ if (corsConfig.credentials) {
280
+ response.headers.set("Access-Control-Allow-Credentials", "true");
281
+ }
282
+ if (corsConfig.methods) {
283
+ response.headers.set(
284
+ "Access-Control-Allow-Methods",
285
+ corsConfig.methods.join(", ")
286
+ );
287
+ }
288
+ if (corsConfig.allowedHeaders) {
289
+ response.headers.set(
290
+ "Access-Control-Allow-Headers",
291
+ corsConfig.allowedHeaders.join(", ")
292
+ );
293
+ }
294
+ if (corsConfig.maxAge) {
295
+ response.headers.set("Access-Control-Max-Age", String(corsConfig.maxAge));
296
+ }
297
+ return response;
298
+ }
299
+ await next();
300
+ if (isOriginAllowed(origin, corsConfig)) {
301
+ c.header("Access-Control-Allow-Origin", origin || "*");
302
+ }
303
+ if (corsConfig.credentials) {
304
+ c.header("Access-Control-Allow-Credentials", "true");
305
+ }
306
+ if (corsConfig.exposedHeaders) {
307
+ c.header("Access-Control-Expose-Headers", corsConfig.exposedHeaders.join(", "));
308
+ }
309
+ };
310
+ }
311
+
312
+ // src/middleware/csrf.ts
313
+ function generateRandomToken() {
314
+ const bytes = new Uint8Array(32);
315
+ crypto.getRandomValues(bytes);
316
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
317
+ }
318
+ function getCookie(c, name) {
319
+ const cookieHeader = c.req.header("cookie");
320
+ if (!cookieHeader) return void 0;
321
+ const cookies = cookieHeader.split(";").map((c2) => c2.trim());
322
+ for (const cookie of cookies) {
323
+ const [key, ...valueParts] = cookie.split("=");
324
+ if (key === name) {
325
+ return valueParts.join("=");
326
+ }
327
+ }
328
+ return void 0;
329
+ }
330
+ function csrf(options = {}) {
331
+ const {
332
+ cookieName = "_csrf",
333
+ headerName = "x-csrf-token",
334
+ methods = ["POST", "PUT", "PATCH", "DELETE"],
335
+ excludePaths = [],
336
+ skip,
337
+ generateToken = generateRandomToken,
338
+ cookie = {}
339
+ } = options;
340
+ const cookieOptions = {
341
+ secure: cookie.secure ?? true,
342
+ httpOnly: cookie.httpOnly ?? true,
343
+ sameSite: cookie.sameSite ?? "lax",
344
+ path: cookie.path ?? "/",
345
+ maxAge: cookie.maxAge ?? 86400
346
+ // 24 hours
347
+ };
348
+ return async (c, next) => {
349
+ if (skip?.(c)) {
350
+ return next();
351
+ }
352
+ const path = c.req.path;
353
+ if (excludePaths.some((p) => path.startsWith(p))) {
354
+ return next();
355
+ }
356
+ let token = getCookie(c, cookieName);
357
+ if (!token) {
358
+ token = generateToken();
359
+ const cookieValue = [
360
+ `${cookieName}=${token}`,
361
+ `Path=${cookieOptions.path}`,
362
+ `Max-Age=${cookieOptions.maxAge}`,
363
+ cookieOptions.sameSite && `SameSite=${cookieOptions.sameSite}`,
364
+ cookieOptions.secure && "Secure",
365
+ cookieOptions.httpOnly && "HttpOnly"
366
+ ].filter(Boolean).join("; ");
367
+ c.header("Set-Cookie", cookieValue);
368
+ }
369
+ c.set("csrfToken", token);
370
+ if (methods.includes(c.req.method)) {
371
+ const headerToken = c.req.header(headerName);
372
+ const bodyToken = await getBodyToken(c);
373
+ const providedToken = headerToken ?? bodyToken;
374
+ if (!providedToken || providedToken !== token) {
375
+ throw new ForbiddenError("Invalid CSRF token");
376
+ }
377
+ }
378
+ await next();
379
+ };
380
+ }
381
+ async function getBodyToken(c) {
382
+ try {
383
+ const contentType = c.req.header("content-type") ?? "";
384
+ if (contentType.includes("application/json")) {
385
+ const body = await c.req.json();
386
+ return body["_csrf"] ?? body["csrfToken"] ?? body["csrf_token"];
387
+ }
388
+ if (contentType.includes("application/x-www-form-urlencoded")) {
389
+ const body = await c.req.parseBody();
390
+ return body["_csrf"];
391
+ }
392
+ } catch {
393
+ }
394
+ return void 0;
395
+ }
396
+ function doubleSubmitCookie(options = {}) {
397
+ return csrf({
398
+ ...options,
399
+ cookie: {
400
+ ...options.cookie,
401
+ httpOnly: false
402
+ // Allow JS to read the cookie
403
+ }
404
+ });
405
+ }
406
+
407
+ // src/middleware/rate-limit.ts
408
+ var MemoryRateLimitStorage = class {
409
+ store = /* @__PURE__ */ new Map();
410
+ async get(key) {
411
+ const entry = this.store.get(key);
412
+ if (!entry || entry.expires < Date.now()) {
413
+ return 0;
414
+ }
415
+ return entry.count;
416
+ }
417
+ async increment(key, windowMs) {
418
+ const now = Date.now();
419
+ const entry = this.store.get(key);
420
+ if (!entry || entry.expires < now) {
421
+ this.store.set(key, { count: 1, expires: now + windowMs });
422
+ return 1;
423
+ }
424
+ entry.count++;
425
+ return entry.count;
426
+ }
427
+ async reset(key) {
428
+ this.store.delete(key);
429
+ }
430
+ /** Clean up expired entries */
431
+ cleanup() {
432
+ const now = Date.now();
433
+ for (const [key, entry] of this.store) {
434
+ if (entry.expires < now) {
435
+ this.store.delete(key);
436
+ }
437
+ }
438
+ }
439
+ };
440
+ var defaultStorage = null;
441
+ function getDefaultStorage() {
442
+ if (!defaultStorage) {
443
+ defaultStorage = new MemoryRateLimitStorage();
444
+ }
445
+ return defaultStorage;
446
+ }
447
+ function rateLimit(options = {}) {
448
+ const {
449
+ windowMs = 60 * 1e3,
450
+ // 1 minute
451
+ max = 100,
452
+ keyGenerator = defaultKeyGenerator,
453
+ skip,
454
+ storage = getDefaultStorage(),
455
+ message = "Too many requests, please try again later",
456
+ headers = true,
457
+ onLimitReached
458
+ } = options;
459
+ return async (c, next) => {
460
+ if (skip?.(c)) {
461
+ return next();
462
+ }
463
+ const key = `ratelimit:${keyGenerator(c)}`;
464
+ const current = await storage.increment(key, windowMs);
465
+ if (headers) {
466
+ c.header("X-RateLimit-Limit", String(max));
467
+ c.header("X-RateLimit-Remaining", String(Math.max(0, max - current)));
468
+ c.header("X-RateLimit-Reset", String(Math.ceil((Date.now() + windowMs) / 1e3)));
469
+ }
470
+ if (current > max) {
471
+ if (onLimitReached) {
472
+ onLimitReached(c, key);
473
+ }
474
+ const retryAfter = Math.ceil(windowMs / 1e3);
475
+ c.header("Retry-After", String(retryAfter));
476
+ throw new RateLimitError(message, retryAfter);
477
+ }
478
+ await next();
479
+ };
480
+ }
481
+ function defaultKeyGenerator(c) {
482
+ return c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ?? c.req.header("x-real-ip") ?? c.req.header("cf-connecting-ip") ?? "unknown";
483
+ }
484
+ function createRateLimiter(options = {}) {
485
+ const storage = options.storage ?? getDefaultStorage();
486
+ return {
487
+ middleware: rateLimit({ ...options, storage }),
488
+ storage,
489
+ reset: (key) => storage.reset(`ratelimit:${key}`),
490
+ get: (key) => storage.get(`ratelimit:${key}`)
491
+ };
492
+ }
493
+
494
+ // src/middleware/request-logger.ts
495
+ function formatBytes(bytes) {
496
+ if (bytes < 1024) return `${bytes}B`;
497
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
498
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
499
+ }
500
+ function requestLogger(options = {}) {
501
+ const {
502
+ skip,
503
+ format = "json",
504
+ includeBody = false,
505
+ maxBodyLength = 1e3
506
+ } = options;
507
+ return async (c, next) => {
508
+ if (skip?.(c)) {
509
+ return next();
510
+ }
511
+ const start = Date.now();
512
+ const logger = c.get("logger");
513
+ const requestId = c.get("requestId");
514
+ const method = c.req.method;
515
+ const path = c.req.path;
516
+ const query = c.req.query();
517
+ const userAgent = c.req.header("user-agent");
518
+ const ip = c.req.header("x-forwarded-for") ?? c.req.header("x-real-ip") ?? "unknown";
519
+ if (format === "json") {
520
+ logger?.debug("Request started", {
521
+ requestId,
522
+ method,
523
+ path,
524
+ query: Object.keys(query).length > 0 ? query : void 0,
525
+ ip,
526
+ userAgent
527
+ });
528
+ }
529
+ let requestBody;
530
+ if (includeBody && ["POST", "PUT", "PATCH"].includes(method)) {
531
+ try {
532
+ const contentType = c.req.header("content-type") ?? "";
533
+ if (contentType.includes("application/json")) {
534
+ const body = await c.req.text();
535
+ requestBody = body.length > maxBodyLength ? body.substring(0, maxBodyLength) + "..." : body;
536
+ }
537
+ } catch {
538
+ }
539
+ }
540
+ await next();
541
+ const duration = Date.now() - start;
542
+ const status = c.res.status;
543
+ const contentLength = c.res.headers.get("content-length");
544
+ const size = contentLength ? parseInt(contentLength, 10) : 0;
545
+ if (format === "json") {
546
+ const logData = {
547
+ requestId,
548
+ method,
549
+ path,
550
+ status,
551
+ duration: `${duration}ms`,
552
+ size: formatBytes(size)
553
+ };
554
+ if (requestBody) {
555
+ logData["requestBody"] = requestBody;
556
+ }
557
+ if (status >= 500) {
558
+ logger?.error("Request completed", logData);
559
+ } else if (status >= 400) {
560
+ logger?.warn("Request completed", logData);
561
+ } else {
562
+ logger?.info("Request completed", logData);
563
+ }
564
+ } else if (format === "combined") {
565
+ const log = `${ip} - - [${(/* @__PURE__ */ new Date()).toISOString()}] "${method} ${path}" ${status} ${size} "-" "${userAgent}" ${duration}ms`;
566
+ console.log(log);
567
+ } else {
568
+ const log = `${method} ${path} ${status} ${duration}ms`;
569
+ console.log(log);
570
+ }
571
+ };
572
+ }
573
+
574
+ // src/middleware/tracing.ts
575
+ function parseTraceparent(header) {
576
+ const match = header.match(
577
+ /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i
578
+ );
579
+ if (!match) return null;
580
+ const [, version, traceId, parentId, flags] = match;
581
+ if (traceId === "00000000000000000000000000000000") return null;
582
+ if (parentId === "0000000000000000") return null;
583
+ return {
584
+ version,
585
+ traceId,
586
+ parentId,
587
+ traceFlags: parseInt(flags, 16)
588
+ };
589
+ }
590
+ function generateTraceId() {
591
+ const bytes = new Uint8Array(16);
592
+ crypto.getRandomValues(bytes);
593
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
594
+ }
595
+ function generateSpanId() {
596
+ const bytes = new Uint8Array(8);
597
+ crypto.getRandomValues(bytes);
598
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
599
+ }
600
+ function createTraceparent(traceId, spanId, sampled = true) {
601
+ const flags = sampled ? "01" : "00";
602
+ return `00-${traceId}-${spanId}-${flags}`;
603
+ }
604
+ function tracing(options = {}) {
605
+ const {
606
+ headerName = "x-request-id",
607
+ generateId = generateRequestId,
608
+ propagate = false,
609
+ emitHeader = true,
610
+ trustIncoming = true
611
+ } = options;
612
+ return async (c, next) => {
613
+ let requestId;
614
+ if (trustIncoming) {
615
+ requestId = c.req.header(headerName) ?? generateId();
616
+ } else {
617
+ requestId = generateId();
618
+ }
619
+ c.set("requestId", requestId);
620
+ if (propagate) {
621
+ const traceparent = c.req.header("traceparent");
622
+ const tracestate = c.req.header("tracestate");
623
+ const spanId = generateSpanId();
624
+ c.set("spanId", spanId);
625
+ if (traceparent) {
626
+ const traceContext = parseTraceparent(traceparent);
627
+ if (traceContext) {
628
+ c.set("traceContext", traceContext);
629
+ if (tracestate) {
630
+ c.set("traceState", tracestate);
631
+ }
632
+ }
633
+ } else {
634
+ const traceId = generateTraceId();
635
+ c.set("traceContext", {
636
+ version: "00",
637
+ traceId,
638
+ parentId: spanId,
639
+ traceFlags: 1
640
+ // Sampled
641
+ });
642
+ }
643
+ }
644
+ if (emitHeader) {
645
+ c.header(headerName, requestId);
646
+ }
647
+ await next();
648
+ };
649
+ }
650
+ var tracingMiddleware = tracing;
651
+
652
+ // src/middleware/usage-tracking.ts
653
+ function usageTracking(options) {
654
+ const {
655
+ usageService,
656
+ featureKey = "api_calls",
657
+ quantity = 1,
658
+ skip,
659
+ trackOn = "response",
660
+ successOnly = true,
661
+ getCustomerId = (c) => c.get("user")?.id,
662
+ getTenantId = (c) => c.get("tenant")?.id ?? c.get("user")?.tenantId,
663
+ getSubscriptionId,
664
+ includeMetadata = true,
665
+ getIdempotencyKey
666
+ } = options;
667
+ return async (c, next) => {
668
+ if (trackOn === "request") {
669
+ await trackUsage(c);
670
+ return next();
671
+ }
672
+ await next();
673
+ if (skip?.(c)) return;
674
+ if (successOnly && c.res.status >= 400) return;
675
+ await trackUsage(c);
676
+ };
677
+ async function trackUsage(c) {
678
+ const customerId = getCustomerId(c);
679
+ const tenantId = getTenantId(c);
680
+ if (!customerId || !tenantId) return;
681
+ const resolvedFeatureKey = typeof featureKey === "function" ? featureKey(c) : featureKey;
682
+ const resolvedQuantity = typeof quantity === "function" ? quantity(c) : quantity;
683
+ const metadata = includeMetadata ? {
684
+ path: c.req.path,
685
+ method: c.req.method,
686
+ statusCode: c.res.status,
687
+ userAgent: c.req.header("user-agent")
688
+ } : void 0;
689
+ try {
690
+ const trackOptions = {
691
+ tenantId,
692
+ customerId,
693
+ featureKey: resolvedFeatureKey,
694
+ quantity: resolvedQuantity
695
+ };
696
+ const subscriptionId = getSubscriptionId?.(c);
697
+ if (subscriptionId !== void 0) {
698
+ trackOptions.subscriptionId = subscriptionId;
699
+ }
700
+ if (metadata !== void 0) {
701
+ trackOptions.metadata = metadata;
702
+ }
703
+ const idempotencyKey = getIdempotencyKey?.(c);
704
+ if (idempotencyKey !== void 0) {
705
+ trackOptions.idempotencyKey = idempotencyKey;
706
+ }
707
+ await usageService.trackUsage(trackOptions);
708
+ } catch (error2) {
709
+ const logger = c.get("logger");
710
+ if (logger) {
711
+ logger.error("Usage tracking failed", {
712
+ error: error2 instanceof Error ? error2.message : String(error2),
713
+ customerId,
714
+ featureKey: resolvedFeatureKey
715
+ });
716
+ }
717
+ }
718
+ }
719
+ }
720
+ function createUsageTracking(baseOptions) {
721
+ return (overrides) => {
722
+ return usageTracking({ ...baseOptions, ...overrides });
723
+ };
724
+ }
725
+
726
+ // src/middleware/quota-enforcement.ts
727
+ var QuotaExceededError = class extends Error {
728
+ constructor(featureKey, limit, currentUsage, requestedQuantity = 1) {
729
+ super(
730
+ `Quota exceeded for "${featureKey}": ${currentUsage}/${limit ?? "unlimited"} used`
731
+ );
732
+ this.featureKey = featureKey;
733
+ this.limit = limit;
734
+ this.currentUsage = currentUsage;
735
+ this.requestedQuantity = requestedQuantity;
736
+ this.name = "QuotaExceededError";
737
+ }
738
+ statusCode = 429;
739
+ code = "QUOTA_EXCEEDED";
740
+ };
741
+ function quotaEnforcement(options) {
742
+ const {
743
+ quotaManager,
744
+ featureKey,
745
+ quantity = 1,
746
+ skip,
747
+ getCustomerId = (c) => c.get("user")?.id,
748
+ includeHeaders = true,
749
+ onQuotaExceeded,
750
+ softLimit = false,
751
+ onQuotaWarning
752
+ } = options;
753
+ return async (c, next) => {
754
+ if (skip?.(c)) {
755
+ return next();
756
+ }
757
+ const customerId = getCustomerId(c);
758
+ if (!customerId) {
759
+ return next();
760
+ }
761
+ const resolvedFeatureKey = typeof featureKey === "function" ? featureKey(c) : featureKey;
762
+ const resolvedQuantity = typeof quantity === "function" ? quantity(c) : quantity;
763
+ try {
764
+ const result = await quotaManager.checkQuota(
765
+ customerId,
766
+ resolvedFeatureKey,
767
+ resolvedQuantity
768
+ );
769
+ if (includeHeaders) {
770
+ c.header("X-Quota-Limit", String(result.limit ?? "unlimited"));
771
+ c.header("X-Quota-Remaining", String(result.remaining ?? "unlimited"));
772
+ c.header("X-Quota-Used", String(result.currentUsage));
773
+ if (result.percentAfter !== null) {
774
+ c.header("X-Quota-Percent", String(result.percentAfter));
775
+ }
776
+ }
777
+ if (result.percentAfter !== null && result.percentAfter >= 80 && onQuotaWarning) {
778
+ onQuotaWarning(c, result, resolvedFeatureKey);
779
+ }
780
+ if (!result.allowed && !softLimit) {
781
+ if (onQuotaExceeded) {
782
+ const response = onQuotaExceeded(c, result, resolvedFeatureKey);
783
+ if (response) return response;
784
+ }
785
+ throw new QuotaExceededError(
786
+ resolvedFeatureKey,
787
+ result.limit,
788
+ result.currentUsage,
789
+ resolvedQuantity
790
+ );
791
+ }
792
+ await next();
793
+ } catch (error2) {
794
+ if (error2 instanceof QuotaExceededError) {
795
+ throw error2;
796
+ }
797
+ const logger = c.get("logger");
798
+ if (logger) {
799
+ logger.error("Quota check failed", {
800
+ error: error2 instanceof Error ? error2.message : String(error2),
801
+ customerId,
802
+ featureKey: resolvedFeatureKey
803
+ });
804
+ }
805
+ await next();
806
+ }
807
+ };
808
+ }
809
+ function createQuotaEnforcement(baseOptions) {
810
+ return (featureKey) => {
811
+ return quotaEnforcement({ ...baseOptions, featureKey });
812
+ };
813
+ }
814
+ function multiQuotaEnforcement(options) {
815
+ const { features } = options;
816
+ return async (c, next) => {
817
+ const customerId = (options.getCustomerId ?? ((ctx) => ctx.get("user")?.id))(c);
818
+ if (!customerId || options.skip?.(c)) {
819
+ return next();
820
+ }
821
+ for (const feature of features) {
822
+ const resolvedQuantity = typeof feature.quantity === "function" ? feature.quantity(c) : feature.quantity ?? 1;
823
+ const result = await options.quotaManager.checkQuota(
824
+ customerId,
825
+ feature.featureKey,
826
+ resolvedQuantity
827
+ );
828
+ if (!result.allowed && !options.softLimit) {
829
+ if (options.onQuotaExceeded) {
830
+ const response = options.onQuotaExceeded(c, result, feature.featureKey);
831
+ if (response) return response;
832
+ }
833
+ throw new QuotaExceededError(
834
+ feature.featureKey,
835
+ result.limit,
836
+ result.currentUsage,
837
+ resolvedQuantity
838
+ );
839
+ }
840
+ }
841
+ await next();
842
+ };
843
+ }
844
+ export {
845
+ ApiError,
846
+ BadRequestError,
847
+ ConflictError,
848
+ ForbiddenError,
849
+ InternalError,
850
+ MemoryRateLimitStorage,
851
+ NotFoundError,
852
+ QuotaExceededError,
853
+ RateLimitError,
854
+ ServiceUnavailableError,
855
+ UnauthorizedError,
856
+ ValidationError,
857
+ auth,
858
+ cors,
859
+ createAuthMiddleware,
860
+ createQuotaEnforcement,
861
+ createRateLimiter,
862
+ createTraceparent,
863
+ createUsageTracking,
864
+ csrf,
865
+ doubleSubmitCookie,
866
+ errorHandler,
867
+ generateSpanId,
868
+ generateTraceId,
869
+ multiQuotaEnforcement,
870
+ notFoundHandler,
871
+ optionalAuth,
872
+ parseTraceparent,
873
+ quotaEnforcement,
874
+ rateLimit,
875
+ requestLogger,
876
+ tracing,
877
+ tracingMiddleware,
878
+ usageTracking
879
+ };
880
+ //# sourceMappingURL=index.js.map