@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.
- package/README.md +142 -0
- package/dist/app.d.ts +87 -0
- package/dist/app.js +59 -0
- package/dist/app.js.map +1 -0
- package/dist/context.d.ts +208 -0
- package/dist/context.js +23 -0
- package/dist/context.js.map +1 -0
- package/dist/health.d.ts +81 -0
- package/dist/health.js +112 -0
- package/dist/health.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +2094 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/index.d.ts +888 -0
- package/dist/middleware/index.js +880 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/module-loader.d.ts +125 -0
- package/dist/module-loader.js +309 -0
- package/dist/module-loader.js.map +1 -0
- package/dist/rbac.d.ts +171 -0
- package/dist/rbac.js +347 -0
- package/dist/rbac.js.map +1 -0
- package/dist/rls.d.ts +114 -0
- package/dist/rls.js +126 -0
- package/dist/rls.js.map +1 -0
- package/dist/utils/index.d.ts +262 -0
- package/dist/utils/index.js +193 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/validation/index.d.ts +118 -0
- package/dist/validation/index.js +245 -0
- package/dist/validation/index.js.map +1 -0
- package/package.json +80 -0
|
@@ -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
|