@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
package/dist/index.js
ADDED
|
@@ -0,0 +1,2094 @@
|
|
|
1
|
+
// src/app.ts
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { createLogger } from "@parsrun/core";
|
|
4
|
+
|
|
5
|
+
// src/context.ts
|
|
6
|
+
function success(data, meta) {
|
|
7
|
+
return {
|
|
8
|
+
success: true,
|
|
9
|
+
data,
|
|
10
|
+
meta: meta ?? void 0
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function error(code, message, details) {
|
|
14
|
+
return {
|
|
15
|
+
success: false,
|
|
16
|
+
error: { code, message, details: details ?? void 0 }
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function generateRequestId() {
|
|
20
|
+
return crypto.randomUUID();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/app.ts
|
|
24
|
+
function createServer(options) {
|
|
25
|
+
const app = new Hono({
|
|
26
|
+
strict: options.strict ?? false
|
|
27
|
+
});
|
|
28
|
+
const logger = options.logger ?? createLogger({ name: "pars-server" });
|
|
29
|
+
app.use("*", async (c, next) => {
|
|
30
|
+
c.set("db", options.database);
|
|
31
|
+
c.set("config", options);
|
|
32
|
+
c.set("logger", logger);
|
|
33
|
+
c.set("enabledModules", /* @__PURE__ */ new Set());
|
|
34
|
+
c.set("cookiePrefix", options.cookiePrefix);
|
|
35
|
+
c.set("custom", options.custom ?? {});
|
|
36
|
+
if (options.requestId !== false) {
|
|
37
|
+
const requestId = c.req.header("x-request-id") ?? generateRequestId();
|
|
38
|
+
c.set("requestId", requestId);
|
|
39
|
+
c.header("x-request-id", requestId);
|
|
40
|
+
}
|
|
41
|
+
await next();
|
|
42
|
+
});
|
|
43
|
+
return app;
|
|
44
|
+
}
|
|
45
|
+
function createRouter() {
|
|
46
|
+
return new Hono();
|
|
47
|
+
}
|
|
48
|
+
function createVersionedRouter(version) {
|
|
49
|
+
const router = new Hono();
|
|
50
|
+
const versionedRouter = new Hono();
|
|
51
|
+
versionedRouter.route(`/${version}`, router);
|
|
52
|
+
return versionedRouter;
|
|
53
|
+
}
|
|
54
|
+
function createModuleRouter(moduleName, options) {
|
|
55
|
+
const moduleRouter = new Hono();
|
|
56
|
+
if (options.middleware) {
|
|
57
|
+
for (const mw of options.middleware) {
|
|
58
|
+
moduleRouter.use("*", mw);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
options.routes(moduleRouter);
|
|
62
|
+
const wrappedRouter = new Hono();
|
|
63
|
+
wrappedRouter.route(`/${moduleName}`, moduleRouter);
|
|
64
|
+
return wrappedRouter;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/module-loader.ts
|
|
68
|
+
import { Hono as Hono2 } from "hono";
|
|
69
|
+
import { cors } from "hono/cors";
|
|
70
|
+
import { createLogger as createLogger2 } from "@parsrun/core";
|
|
71
|
+
var ModuleLoader = class {
|
|
72
|
+
app;
|
|
73
|
+
db;
|
|
74
|
+
enabledModules = /* @__PURE__ */ new Set();
|
|
75
|
+
moduleRegistry = /* @__PURE__ */ new Map();
|
|
76
|
+
logger;
|
|
77
|
+
config;
|
|
78
|
+
cookiePrefix;
|
|
79
|
+
initialized = false;
|
|
80
|
+
constructor(options) {
|
|
81
|
+
this.config = options.config;
|
|
82
|
+
this.db = options.config.database;
|
|
83
|
+
this.cookiePrefix = options.cookiePrefix;
|
|
84
|
+
this.logger = options.logger ?? createLogger2({ name: "ModuleLoader" });
|
|
85
|
+
this.app = new Hono2();
|
|
86
|
+
this.setupMiddleware();
|
|
87
|
+
this.setupCoreRoutes();
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Initialize the module loader
|
|
91
|
+
* Checks database connection
|
|
92
|
+
*/
|
|
93
|
+
async initialize() {
|
|
94
|
+
if (this.initialized) {
|
|
95
|
+
this.logger.warn("ModuleLoader already initialized");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
this.logger.info("Initializing ModuleLoader...");
|
|
99
|
+
try {
|
|
100
|
+
if (this.db.ping) {
|
|
101
|
+
const ok = await this.db.ping();
|
|
102
|
+
if (!ok) throw new Error("Database ping returned false");
|
|
103
|
+
} else {
|
|
104
|
+
await this.db.execute("SELECT 1");
|
|
105
|
+
}
|
|
106
|
+
this.logger.info("Database connection: OK");
|
|
107
|
+
} catch (error2) {
|
|
108
|
+
this.logger.error("Database connection failed", error2);
|
|
109
|
+
throw new Error("Database connection failed");
|
|
110
|
+
}
|
|
111
|
+
this.initialized = true;
|
|
112
|
+
this.logger.info("ModuleLoader initialized successfully");
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Setup core middleware
|
|
116
|
+
*/
|
|
117
|
+
setupMiddleware() {
|
|
118
|
+
this.app.use("*", async (c, next) => {
|
|
119
|
+
const requestId = generateRequestId();
|
|
120
|
+
const requestLogger2 = this.logger.child({ requestId });
|
|
121
|
+
c.set("db", this.db);
|
|
122
|
+
c.set("config", this.config);
|
|
123
|
+
c.set("enabledModules", this.enabledModules);
|
|
124
|
+
c.set("logger", requestLogger2);
|
|
125
|
+
c.set("requestId", requestId);
|
|
126
|
+
c.set("cookiePrefix", this.cookiePrefix);
|
|
127
|
+
c.set("custom", this.config.custom ?? {});
|
|
128
|
+
c.set("user", void 0);
|
|
129
|
+
c.set("tenant", void 0);
|
|
130
|
+
const start = Date.now();
|
|
131
|
+
await next();
|
|
132
|
+
const duration = Date.now() - start;
|
|
133
|
+
requestLogger2.debug("Request completed", {
|
|
134
|
+
method: c.req.method,
|
|
135
|
+
path: c.req.path,
|
|
136
|
+
status: c.res.status,
|
|
137
|
+
durationMs: duration
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
if (this.config.cors) {
|
|
141
|
+
this.app.use("*", cors(this.normalizeCorsConfig(this.config.cors)));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Normalize CORS config for Hono
|
|
146
|
+
*/
|
|
147
|
+
normalizeCorsConfig(config) {
|
|
148
|
+
const result = {
|
|
149
|
+
origin: typeof config.origin === "function" ? (origin) => config.origin(origin) ? origin : null : config.origin
|
|
150
|
+
};
|
|
151
|
+
if (config.credentials !== void 0) {
|
|
152
|
+
result.credentials = config.credentials;
|
|
153
|
+
}
|
|
154
|
+
if (config.methods !== void 0) {
|
|
155
|
+
result.allowMethods = config.methods;
|
|
156
|
+
}
|
|
157
|
+
if (config.allowedHeaders !== void 0) {
|
|
158
|
+
result.allowHeaders = config.allowedHeaders;
|
|
159
|
+
}
|
|
160
|
+
if (config.exposedHeaders !== void 0) {
|
|
161
|
+
result.exposeHeaders = config.exposedHeaders;
|
|
162
|
+
}
|
|
163
|
+
if (config.maxAge !== void 0) {
|
|
164
|
+
result.maxAge = config.maxAge;
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Setup core routes
|
|
170
|
+
*/
|
|
171
|
+
setupCoreRoutes() {
|
|
172
|
+
const basePath = this.config.basePath ?? "/api/v1";
|
|
173
|
+
this.app.get("/health", (c) => {
|
|
174
|
+
return c.json({
|
|
175
|
+
status: "ok",
|
|
176
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
this.app.get("/health/details", async (c) => {
|
|
180
|
+
let dbStatus = "unknown";
|
|
181
|
+
try {
|
|
182
|
+
if (this.db.ping) {
|
|
183
|
+
dbStatus = await this.db.ping() ? "ok" : "error";
|
|
184
|
+
} else {
|
|
185
|
+
await this.db.execute("SELECT 1");
|
|
186
|
+
dbStatus = "ok";
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
dbStatus = "error";
|
|
190
|
+
}
|
|
191
|
+
return c.json({
|
|
192
|
+
status: dbStatus === "ok" ? "ok" : "degraded",
|
|
193
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
194
|
+
services: {
|
|
195
|
+
database: dbStatus
|
|
196
|
+
},
|
|
197
|
+
modules: {
|
|
198
|
+
enabled: Array.from(this.enabledModules),
|
|
199
|
+
registered: Array.from(this.moduleRegistry.keys())
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
this.app.get(basePath, (c) => {
|
|
204
|
+
const endpoints = {
|
|
205
|
+
health: "/health",
|
|
206
|
+
features: `${basePath}/features`
|
|
207
|
+
};
|
|
208
|
+
for (const moduleName of this.enabledModules) {
|
|
209
|
+
endpoints[moduleName] = `${basePath}/${moduleName}`;
|
|
210
|
+
}
|
|
211
|
+
return c.json({
|
|
212
|
+
name: "Pars API",
|
|
213
|
+
version: "1.0.0",
|
|
214
|
+
endpoints
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
this.app.get(`${basePath}/features`, (c) => {
|
|
218
|
+
const features = Array.from(this.enabledModules).map((name) => {
|
|
219
|
+
const module = this.moduleRegistry.get(name);
|
|
220
|
+
return {
|
|
221
|
+
name,
|
|
222
|
+
version: module?.version ?? "1.0.0",
|
|
223
|
+
description: module?.description ?? "",
|
|
224
|
+
permissions: module?.permissions ?? {}
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
return c.json({
|
|
228
|
+
enabled: Array.from(this.enabledModules),
|
|
229
|
+
features
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Register a module
|
|
235
|
+
*/
|
|
236
|
+
registerModule(manifest) {
|
|
237
|
+
if (this.moduleRegistry.has(manifest.name)) {
|
|
238
|
+
this.logger.warn(`Module already registered: ${manifest.name}`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
this.moduleRegistry.set(manifest.name, manifest);
|
|
242
|
+
this.logger.info(`Registered module: ${manifest.name}`);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Enable a registered module
|
|
246
|
+
*/
|
|
247
|
+
async enableModule(moduleName) {
|
|
248
|
+
const module = this.moduleRegistry.get(moduleName);
|
|
249
|
+
if (!module) {
|
|
250
|
+
this.logger.error(`Module not found: ${moduleName}`);
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
if (this.enabledModules.has(moduleName)) {
|
|
254
|
+
this.logger.warn(`Module already enabled: ${moduleName}`);
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
if (module.dependencies) {
|
|
258
|
+
for (const dep of module.dependencies) {
|
|
259
|
+
if (!this.enabledModules.has(dep)) {
|
|
260
|
+
this.logger.error(
|
|
261
|
+
`Module ${moduleName} requires ${dep} to be enabled first`
|
|
262
|
+
);
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
this.logger.info(`Enabling module: ${moduleName}`);
|
|
269
|
+
if (module.onEnable) {
|
|
270
|
+
await module.onEnable();
|
|
271
|
+
}
|
|
272
|
+
module.registerRoutes(this.app);
|
|
273
|
+
this.enabledModules.add(moduleName);
|
|
274
|
+
this.logger.info(`Enabled module: ${moduleName}`);
|
|
275
|
+
return true;
|
|
276
|
+
} catch (error2) {
|
|
277
|
+
this.logger.error(`Failed to enable module ${moduleName}`, error2);
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Disable an enabled module
|
|
283
|
+
*/
|
|
284
|
+
async disableModule(moduleName) {
|
|
285
|
+
if (!this.enabledModules.has(moduleName)) {
|
|
286
|
+
this.logger.warn(`Module not enabled: ${moduleName}`);
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
const module = this.moduleRegistry.get(moduleName);
|
|
290
|
+
if (!module) {
|
|
291
|
+
this.logger.error(`Module not found: ${moduleName}`);
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
for (const [name, m] of this.moduleRegistry) {
|
|
295
|
+
if (this.enabledModules.has(name) && m.dependencies?.includes(moduleName)) {
|
|
296
|
+
this.logger.error(
|
|
297
|
+
`Cannot disable ${moduleName}: ${name} depends on it`
|
|
298
|
+
);
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
if (module.onDisable) {
|
|
304
|
+
await module.onDisable();
|
|
305
|
+
}
|
|
306
|
+
this.enabledModules.delete(moduleName);
|
|
307
|
+
this.logger.info(`Disabled module: ${moduleName}`);
|
|
308
|
+
return true;
|
|
309
|
+
} catch (error2) {
|
|
310
|
+
this.logger.error(`Failed to disable module ${moduleName}`, error2);
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Get the Hono app instance
|
|
316
|
+
*/
|
|
317
|
+
getApp() {
|
|
318
|
+
return this.app;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Get enabled modules
|
|
322
|
+
*/
|
|
323
|
+
getEnabledModules() {
|
|
324
|
+
return Array.from(this.enabledModules);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Get registered modules
|
|
328
|
+
*/
|
|
329
|
+
getRegisteredModules() {
|
|
330
|
+
return Array.from(this.moduleRegistry.keys());
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Check if module is enabled
|
|
334
|
+
*/
|
|
335
|
+
isModuleEnabled(moduleName) {
|
|
336
|
+
return this.enabledModules.has(moduleName);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Check if module is registered
|
|
340
|
+
*/
|
|
341
|
+
isModuleRegistered(moduleName) {
|
|
342
|
+
return this.moduleRegistry.has(moduleName);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Get database adapter
|
|
346
|
+
*/
|
|
347
|
+
getDatabase() {
|
|
348
|
+
return this.db;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Get logger
|
|
352
|
+
*/
|
|
353
|
+
getLogger() {
|
|
354
|
+
return this.logger;
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
function createModuleLoader(options) {
|
|
358
|
+
return new ModuleLoader(options);
|
|
359
|
+
}
|
|
360
|
+
function defineModule(manifest) {
|
|
361
|
+
return manifest;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// src/rls.ts
|
|
365
|
+
var DEFAULT_RLS_CONFIG = {
|
|
366
|
+
tenantIdColumn: "tenant_id",
|
|
367
|
+
sessionVariable: "app.current_tenant_id",
|
|
368
|
+
enabled: true
|
|
369
|
+
};
|
|
370
|
+
var RLSManager = class {
|
|
371
|
+
constructor(db, config = {}) {
|
|
372
|
+
this.db = db;
|
|
373
|
+
this.config = { ...DEFAULT_RLS_CONFIG, ...config };
|
|
374
|
+
}
|
|
375
|
+
config;
|
|
376
|
+
/**
|
|
377
|
+
* Set current tenant ID in database session
|
|
378
|
+
* This enables RLS policies to filter by tenant
|
|
379
|
+
*/
|
|
380
|
+
async setTenantId(tenantId) {
|
|
381
|
+
if (!this.config.enabled) return;
|
|
382
|
+
try {
|
|
383
|
+
const escapedTenantId = tenantId.replace(/'/g, "''");
|
|
384
|
+
await this.db.execute(`SET ${this.config.sessionVariable} = '${escapedTenantId}'`);
|
|
385
|
+
} catch (error2) {
|
|
386
|
+
console.error("Failed to set tenant ID for RLS:", error2);
|
|
387
|
+
throw new RLSError("Failed to set tenant context", "RLS_SET_FAILED", error2);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Clear current tenant ID from database session
|
|
392
|
+
*/
|
|
393
|
+
async clearTenantId() {
|
|
394
|
+
if (!this.config.enabled) return;
|
|
395
|
+
try {
|
|
396
|
+
await this.db.execute(`RESET ${this.config.sessionVariable}`);
|
|
397
|
+
} catch (error2) {
|
|
398
|
+
console.error("Failed to clear tenant ID for RLS:", error2);
|
|
399
|
+
throw new RLSError("Failed to clear tenant context", "RLS_CLEAR_FAILED", error2);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Execute a function with tenant context
|
|
404
|
+
* Automatically sets and clears tenant ID
|
|
405
|
+
*/
|
|
406
|
+
async withTenant(tenantId, operation) {
|
|
407
|
+
await this.setTenantId(tenantId);
|
|
408
|
+
try {
|
|
409
|
+
return await operation();
|
|
410
|
+
} finally {
|
|
411
|
+
await this.clearTenantId();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Check if RLS is enabled
|
|
416
|
+
*/
|
|
417
|
+
isEnabled() {
|
|
418
|
+
return this.config.enabled;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Get current configuration
|
|
422
|
+
*/
|
|
423
|
+
getConfig() {
|
|
424
|
+
return { ...this.config };
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
var RLSError = class extends Error {
|
|
428
|
+
constructor(message, code, cause) {
|
|
429
|
+
super(message);
|
|
430
|
+
this.code = code;
|
|
431
|
+
this.cause = cause;
|
|
432
|
+
this.name = "RLSError";
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
function createRLSManager(db, config) {
|
|
436
|
+
return new RLSManager(db, config);
|
|
437
|
+
}
|
|
438
|
+
function rlsMiddleware(config) {
|
|
439
|
+
return async (c, next) => {
|
|
440
|
+
const user = c.get("user");
|
|
441
|
+
const db = c.get("db");
|
|
442
|
+
if (!user?.tenantId || !db) {
|
|
443
|
+
return next();
|
|
444
|
+
}
|
|
445
|
+
const rls = new RLSManager(db, config);
|
|
446
|
+
try {
|
|
447
|
+
await rls.setTenantId(user.tenantId);
|
|
448
|
+
await next();
|
|
449
|
+
} finally {
|
|
450
|
+
await rls.clearTenantId();
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function generateRLSPolicy(tableName, options = {}) {
|
|
455
|
+
const {
|
|
456
|
+
tenantIdColumn = "tenant_id",
|
|
457
|
+
sessionVariable = "app.current_tenant_id",
|
|
458
|
+
policyName = `${tableName}_tenant_isolation`,
|
|
459
|
+
castType = "uuid"
|
|
460
|
+
} = options;
|
|
461
|
+
return `
|
|
462
|
+
-- Enable RLS on table
|
|
463
|
+
ALTER TABLE ${tableName} ENABLE ROW LEVEL SECURITY;
|
|
464
|
+
|
|
465
|
+
-- Force RLS for table owner too
|
|
466
|
+
ALTER TABLE ${tableName} FORCE ROW LEVEL SECURITY;
|
|
467
|
+
|
|
468
|
+
-- Create tenant isolation policy
|
|
469
|
+
DROP POLICY IF EXISTS ${policyName} ON ${tableName};
|
|
470
|
+
CREATE POLICY ${policyName} ON ${tableName}
|
|
471
|
+
FOR ALL
|
|
472
|
+
USING (${tenantIdColumn} = current_setting('${sessionVariable}', true)::${castType});
|
|
473
|
+
`.trim();
|
|
474
|
+
}
|
|
475
|
+
function generateDisableRLS(tableName) {
|
|
476
|
+
return `
|
|
477
|
+
ALTER TABLE ${tableName} DISABLE ROW LEVEL SECURITY;
|
|
478
|
+
ALTER TABLE ${tableName} NO FORCE ROW LEVEL SECURITY;
|
|
479
|
+
`.trim();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/rbac.ts
|
|
483
|
+
import { ForbiddenError, UnauthorizedError } from "@parsrun/core";
|
|
484
|
+
var InMemoryRBAC = class {
|
|
485
|
+
userPermissions = /* @__PURE__ */ new Map();
|
|
486
|
+
userRoles = /* @__PURE__ */ new Map();
|
|
487
|
+
rolePermissions = /* @__PURE__ */ new Map();
|
|
488
|
+
tenantMembers = /* @__PURE__ */ new Map();
|
|
489
|
+
/**
|
|
490
|
+
* Grant permission to user
|
|
491
|
+
*/
|
|
492
|
+
grantPermission(userId, permission) {
|
|
493
|
+
if (!this.userPermissions.has(userId)) {
|
|
494
|
+
this.userPermissions.set(userId, /* @__PURE__ */ new Set());
|
|
495
|
+
}
|
|
496
|
+
this.userPermissions.get(userId).add(permission);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Revoke permission from user
|
|
500
|
+
*/
|
|
501
|
+
revokePermission(userId, permission) {
|
|
502
|
+
this.userPermissions.get(userId)?.delete(permission);
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Assign role to user
|
|
506
|
+
*/
|
|
507
|
+
assignRole(userId, role) {
|
|
508
|
+
if (!this.userRoles.has(userId)) {
|
|
509
|
+
this.userRoles.set(userId, /* @__PURE__ */ new Set());
|
|
510
|
+
}
|
|
511
|
+
this.userRoles.get(userId).add(role);
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Remove role from user
|
|
515
|
+
*/
|
|
516
|
+
removeRole(userId, role) {
|
|
517
|
+
this.userRoles.get(userId)?.delete(role);
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Define role with permissions
|
|
521
|
+
*/
|
|
522
|
+
defineRole(roleName, permissions) {
|
|
523
|
+
this.rolePermissions.set(roleName, new Set(permissions));
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Add user to tenant
|
|
527
|
+
*/
|
|
528
|
+
addTenantMember(tenantId, userId) {
|
|
529
|
+
if (!this.tenantMembers.has(tenantId)) {
|
|
530
|
+
this.tenantMembers.set(tenantId, /* @__PURE__ */ new Set());
|
|
531
|
+
}
|
|
532
|
+
this.tenantMembers.get(tenantId).add(userId);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Remove user from tenant
|
|
536
|
+
*/
|
|
537
|
+
removeTenantMember(tenantId, userId) {
|
|
538
|
+
this.tenantMembers.get(tenantId)?.delete(userId);
|
|
539
|
+
}
|
|
540
|
+
// PermissionChecker implementation
|
|
541
|
+
async getUserPermissions(userId, _tenantId) {
|
|
542
|
+
const permissions = /* @__PURE__ */ new Set();
|
|
543
|
+
const direct = this.userPermissions.get(userId);
|
|
544
|
+
if (direct) {
|
|
545
|
+
direct.forEach((p) => permissions.add(p));
|
|
546
|
+
}
|
|
547
|
+
const roles = this.userRoles.get(userId);
|
|
548
|
+
if (roles) {
|
|
549
|
+
roles.forEach((role) => {
|
|
550
|
+
const rolePerms = this.rolePermissions.get(role);
|
|
551
|
+
if (rolePerms) {
|
|
552
|
+
rolePerms.forEach((p) => permissions.add(p));
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
return Array.from(permissions);
|
|
557
|
+
}
|
|
558
|
+
async getUserRoles(userId, _tenantId) {
|
|
559
|
+
return Array.from(this.userRoles.get(userId) ?? []);
|
|
560
|
+
}
|
|
561
|
+
async hasPermission(userId, check, tenantId) {
|
|
562
|
+
const permissions = await this.getUserPermissions(userId, tenantId);
|
|
563
|
+
const permissionName = `${check.resource}:${check.action}`;
|
|
564
|
+
if (permissions.includes(permissionName)) {
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
for (const perm of permissions) {
|
|
568
|
+
if (perm === "*") return true;
|
|
569
|
+
if (perm === `${check.resource}:*`) return true;
|
|
570
|
+
if (perm === `*:${check.action}`) return true;
|
|
571
|
+
}
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
async isTenantMember(userId, tenantId) {
|
|
575
|
+
return this.tenantMembers.get(tenantId)?.has(userId) ?? false;
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
var RBACService = class {
|
|
579
|
+
constructor(checker) {
|
|
580
|
+
this.checker = checker;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Get user's permissions
|
|
584
|
+
*/
|
|
585
|
+
async getUserPermissions(userId, tenantId) {
|
|
586
|
+
return this.checker.getUserPermissions(userId, tenantId);
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Get user's roles
|
|
590
|
+
*/
|
|
591
|
+
async getUserRoles(userId, tenantId) {
|
|
592
|
+
return this.checker.getUserRoles(userId, tenantId);
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Check if user has specific permission
|
|
596
|
+
*/
|
|
597
|
+
async hasPermission(userId, check, tenantId) {
|
|
598
|
+
return this.checker.hasPermission(userId, check, tenantId);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Check if user has any of the specified permissions
|
|
602
|
+
*/
|
|
603
|
+
async hasAnyPermission(userId, checks, tenantId) {
|
|
604
|
+
for (const check of checks) {
|
|
605
|
+
if (await this.hasPermission(userId, check, tenantId)) {
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Check if user has all specified permissions
|
|
613
|
+
*/
|
|
614
|
+
async hasAllPermissions(userId, checks, tenantId) {
|
|
615
|
+
for (const check of checks) {
|
|
616
|
+
if (!await this.hasPermission(userId, check, tenantId)) {
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Check if user is member of tenant
|
|
624
|
+
*/
|
|
625
|
+
async isTenantMember(userId, tenantId) {
|
|
626
|
+
return this.checker.isTenantMember(userId, tenantId);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Check if user has specific role
|
|
630
|
+
*/
|
|
631
|
+
async hasRole(userId, role, tenantId) {
|
|
632
|
+
const roles = await this.getUserRoles(userId, tenantId);
|
|
633
|
+
return roles.includes(role);
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Check if user has any of the specified roles
|
|
637
|
+
*/
|
|
638
|
+
async hasAnyRole(userId, roles, tenantId) {
|
|
639
|
+
const userRoles = await this.getUserRoles(userId, tenantId);
|
|
640
|
+
return roles.some((role) => userRoles.includes(role));
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
function createInMemoryRBAC() {
|
|
644
|
+
const checker = new InMemoryRBAC();
|
|
645
|
+
const rbac = new RBACService(checker);
|
|
646
|
+
return { rbac, checker };
|
|
647
|
+
}
|
|
648
|
+
function createRBACService(checker) {
|
|
649
|
+
return new RBACService(checker);
|
|
650
|
+
}
|
|
651
|
+
function requireAuth() {
|
|
652
|
+
return async (c, next) => {
|
|
653
|
+
const user = c.get("user");
|
|
654
|
+
if (!user) {
|
|
655
|
+
throw new UnauthorizedError("Authentication required");
|
|
656
|
+
}
|
|
657
|
+
return next();
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
function requirePermission(resource, action, options = {}) {
|
|
661
|
+
return async (c, next) => {
|
|
662
|
+
const user = c.get("user");
|
|
663
|
+
if (!user) {
|
|
664
|
+
throw new UnauthorizedError("Authentication required");
|
|
665
|
+
}
|
|
666
|
+
const check = {
|
|
667
|
+
resource,
|
|
668
|
+
action,
|
|
669
|
+
scope: options.scope ?? "tenant"
|
|
670
|
+
};
|
|
671
|
+
let hasPermission = false;
|
|
672
|
+
if (options.checker) {
|
|
673
|
+
hasPermission = await options.checker.hasPermission(user.id, check, user.tenantId);
|
|
674
|
+
} else {
|
|
675
|
+
hasPermission = checkUserPermission(user, check);
|
|
676
|
+
}
|
|
677
|
+
if (!hasPermission) {
|
|
678
|
+
throw new ForbiddenError(`Permission denied: ${resource}:${action}`);
|
|
679
|
+
}
|
|
680
|
+
return next();
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function requireAnyPermission(permissions, options = {}) {
|
|
684
|
+
return async (c, next) => {
|
|
685
|
+
const user = c.get("user");
|
|
686
|
+
if (!user) {
|
|
687
|
+
throw new UnauthorizedError("Authentication required");
|
|
688
|
+
}
|
|
689
|
+
let hasAny = false;
|
|
690
|
+
for (const perm of permissions) {
|
|
691
|
+
const check = { resource: perm.resource, action: perm.action };
|
|
692
|
+
if (options.checker) {
|
|
693
|
+
if (await options.checker.hasPermission(user.id, check, user.tenantId)) {
|
|
694
|
+
hasAny = true;
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
} else {
|
|
698
|
+
if (checkUserPermission(user, check)) {
|
|
699
|
+
hasAny = true;
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (!hasAny) {
|
|
705
|
+
throw new ForbiddenError("Insufficient permissions");
|
|
706
|
+
}
|
|
707
|
+
return next();
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
function requireRole(role) {
|
|
711
|
+
return async (c, next) => {
|
|
712
|
+
const user = c.get("user");
|
|
713
|
+
if (!user) {
|
|
714
|
+
throw new UnauthorizedError("Authentication required");
|
|
715
|
+
}
|
|
716
|
+
if (user.role !== role) {
|
|
717
|
+
throw new ForbiddenError(`Role required: ${role}`);
|
|
718
|
+
}
|
|
719
|
+
return next();
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function requireAnyRole(roles) {
|
|
723
|
+
return async (c, next) => {
|
|
724
|
+
const user = c.get("user");
|
|
725
|
+
if (!user) {
|
|
726
|
+
throw new UnauthorizedError("Authentication required");
|
|
727
|
+
}
|
|
728
|
+
if (!user.role || !roles.includes(user.role)) {
|
|
729
|
+
throw new ForbiddenError(`One of these roles required: ${roles.join(", ")}`);
|
|
730
|
+
}
|
|
731
|
+
return next();
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
function requireTenantMember(requiredRole) {
|
|
735
|
+
return async (c, next) => {
|
|
736
|
+
const user = c.get("user");
|
|
737
|
+
const tenant = c.get("tenant");
|
|
738
|
+
if (!user) {
|
|
739
|
+
throw new UnauthorizedError("Authentication required");
|
|
740
|
+
}
|
|
741
|
+
if (!tenant) {
|
|
742
|
+
throw new ForbiddenError("Tenant context required");
|
|
743
|
+
}
|
|
744
|
+
if (user.tenantId !== tenant.id) {
|
|
745
|
+
throw new ForbiddenError("Not a member of this tenant");
|
|
746
|
+
}
|
|
747
|
+
if (requiredRole && user.role !== requiredRole) {
|
|
748
|
+
throw new ForbiddenError(`Role required in tenant: ${requiredRole}`);
|
|
749
|
+
}
|
|
750
|
+
return next();
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
function checkUserPermission(user, check) {
|
|
754
|
+
const permissionName = `${check.resource}:${check.action}`;
|
|
755
|
+
for (const perm of user.permissions) {
|
|
756
|
+
if (perm === permissionName) return true;
|
|
757
|
+
if (perm === "*") return true;
|
|
758
|
+
if (perm === `${check.resource}:*`) return true;
|
|
759
|
+
if (perm === `*:${check.action}`) return true;
|
|
760
|
+
}
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
function parsePermission(permission) {
|
|
764
|
+
const [resource, action] = permission.split(":");
|
|
765
|
+
if (!resource || !action) {
|
|
766
|
+
throw new Error(`Invalid permission format: ${permission}`);
|
|
767
|
+
}
|
|
768
|
+
return { resource, action };
|
|
769
|
+
}
|
|
770
|
+
function createPermission(resource, action) {
|
|
771
|
+
return `${resource}:${action}`;
|
|
772
|
+
}
|
|
773
|
+
function crudPermissions(resource) {
|
|
774
|
+
return [
|
|
775
|
+
{ name: `${resource}:create`, resource, action: "create" },
|
|
776
|
+
{ name: `${resource}:read`, resource, action: "read" },
|
|
777
|
+
{ name: `${resource}:update`, resource, action: "update" },
|
|
778
|
+
{ name: `${resource}:delete`, resource, action: "delete" },
|
|
779
|
+
{ name: `${resource}:list`, resource, action: "list" }
|
|
780
|
+
];
|
|
781
|
+
}
|
|
782
|
+
var StandardRoles = {
|
|
783
|
+
OWNER: {
|
|
784
|
+
name: "owner",
|
|
785
|
+
displayName: "Owner",
|
|
786
|
+
description: "Full access to all resources",
|
|
787
|
+
permissions: ["*"],
|
|
788
|
+
isSystem: true
|
|
789
|
+
},
|
|
790
|
+
ADMIN: {
|
|
791
|
+
name: "admin",
|
|
792
|
+
displayName: "Administrator",
|
|
793
|
+
description: "Administrative access",
|
|
794
|
+
permissions: ["*:read", "*:create", "*:update", "*:list"],
|
|
795
|
+
isSystem: true
|
|
796
|
+
},
|
|
797
|
+
MEMBER: {
|
|
798
|
+
name: "member",
|
|
799
|
+
displayName: "Member",
|
|
800
|
+
description: "Standard member access",
|
|
801
|
+
permissions: ["*:read", "*:list"],
|
|
802
|
+
isSystem: true
|
|
803
|
+
},
|
|
804
|
+
VIEWER: {
|
|
805
|
+
name: "viewer",
|
|
806
|
+
displayName: "Viewer",
|
|
807
|
+
description: "Read-only access",
|
|
808
|
+
permissions: ["*:read", "*:list"],
|
|
809
|
+
isSystem: true
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
// src/middleware/error-handler.ts
|
|
814
|
+
var ApiError = class extends Error {
|
|
815
|
+
constructor(statusCode, code, message, details) {
|
|
816
|
+
super(message);
|
|
817
|
+
this.statusCode = statusCode;
|
|
818
|
+
this.code = code;
|
|
819
|
+
this.details = details;
|
|
820
|
+
this.name = "ApiError";
|
|
821
|
+
}
|
|
822
|
+
toResponse() {
|
|
823
|
+
return error(this.code, this.message, this.details);
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
var BadRequestError = class extends ApiError {
|
|
827
|
+
constructor(message = "Bad request", details) {
|
|
828
|
+
super(400, "BAD_REQUEST", message, details);
|
|
829
|
+
this.name = "BadRequestError";
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
var UnauthorizedError2 = class extends ApiError {
|
|
833
|
+
constructor(message = "Unauthorized", details) {
|
|
834
|
+
super(401, "UNAUTHORIZED", message, details);
|
|
835
|
+
this.name = "UnauthorizedError";
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
var ForbiddenError2 = class extends ApiError {
|
|
839
|
+
constructor(message = "Forbidden", details) {
|
|
840
|
+
super(403, "FORBIDDEN", message, details);
|
|
841
|
+
this.name = "ForbiddenError";
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
var NotFoundError = class extends ApiError {
|
|
845
|
+
constructor(message = "Not found", details) {
|
|
846
|
+
super(404, "NOT_FOUND", message, details);
|
|
847
|
+
this.name = "NotFoundError";
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
var ConflictError = class extends ApiError {
|
|
851
|
+
constructor(message = "Conflict", details) {
|
|
852
|
+
super(409, "CONFLICT", message, details);
|
|
853
|
+
this.name = "ConflictError";
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
var ValidationError = class extends ApiError {
|
|
857
|
+
constructor(message = "Validation failed", details) {
|
|
858
|
+
super(422, "VALIDATION_ERROR", message, details);
|
|
859
|
+
this.name = "ValidationError";
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
var RateLimitError = class extends ApiError {
|
|
863
|
+
constructor(message = "Too many requests", retryAfter) {
|
|
864
|
+
super(429, "RATE_LIMIT_EXCEEDED", message, { retryAfter });
|
|
865
|
+
this.retryAfter = retryAfter;
|
|
866
|
+
this.name = "RateLimitError";
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
var InternalError = class extends ApiError {
|
|
870
|
+
constructor(message = "Internal server error", details) {
|
|
871
|
+
super(500, "INTERNAL_ERROR", message, details);
|
|
872
|
+
this.name = "InternalError";
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
var ServiceUnavailableError = class extends ApiError {
|
|
876
|
+
constructor(message = "Service unavailable", details) {
|
|
877
|
+
super(503, "SERVICE_UNAVAILABLE", message, details);
|
|
878
|
+
this.name = "ServiceUnavailableError";
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
function errorHandler(options = {}) {
|
|
882
|
+
const {
|
|
883
|
+
includeStack = false,
|
|
884
|
+
onError,
|
|
885
|
+
errorTransport,
|
|
886
|
+
captureAllErrors = false,
|
|
887
|
+
shouldCapture
|
|
888
|
+
} = options;
|
|
889
|
+
return async (c, next) => {
|
|
890
|
+
try {
|
|
891
|
+
await next();
|
|
892
|
+
} catch (err) {
|
|
893
|
+
const error2 = err instanceof Error ? err : new Error(String(err));
|
|
894
|
+
const statusCode = error2 instanceof ApiError ? error2.statusCode : 500;
|
|
895
|
+
if (onError) {
|
|
896
|
+
onError(error2, c);
|
|
897
|
+
} else {
|
|
898
|
+
const logger = c.get("logger");
|
|
899
|
+
if (logger) {
|
|
900
|
+
logger.error("Request error", {
|
|
901
|
+
requestId: c.get("requestId"),
|
|
902
|
+
error: error2.message,
|
|
903
|
+
stack: error2.stack
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
if (errorTransport) {
|
|
908
|
+
const shouldCaptureError = shouldCapture ? shouldCapture(error2, statusCode) : captureAllErrors || statusCode >= 500;
|
|
909
|
+
if (shouldCaptureError) {
|
|
910
|
+
const user = c.get("user");
|
|
911
|
+
const tenant = c.get("tenant");
|
|
912
|
+
const errorContext = {
|
|
913
|
+
requestId: c.get("requestId"),
|
|
914
|
+
tags: {
|
|
915
|
+
path: c.req.path,
|
|
916
|
+
method: c.req.method,
|
|
917
|
+
statusCode: String(statusCode)
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
if (user?.id) {
|
|
921
|
+
errorContext.userId = user.id;
|
|
922
|
+
}
|
|
923
|
+
if (tenant?.id) {
|
|
924
|
+
errorContext.tenantId = tenant.id;
|
|
925
|
+
}
|
|
926
|
+
const extra = {
|
|
927
|
+
query: c.req.query()
|
|
928
|
+
};
|
|
929
|
+
if (error2 instanceof ApiError) {
|
|
930
|
+
extra["errorCode"] = error2.code;
|
|
931
|
+
}
|
|
932
|
+
errorContext.extra = extra;
|
|
933
|
+
Promise.resolve(
|
|
934
|
+
errorTransport.captureException(error2, errorContext)
|
|
935
|
+
).catch(() => {
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
if (error2 instanceof ApiError) {
|
|
940
|
+
return c.json(error2.toResponse(), error2.statusCode);
|
|
941
|
+
}
|
|
942
|
+
const details = {};
|
|
943
|
+
if (includeStack && error2.stack) {
|
|
944
|
+
details["stack"] = error2.stack;
|
|
945
|
+
}
|
|
946
|
+
return c.json(
|
|
947
|
+
error("INTERNAL_ERROR", "An unexpected error occurred", details),
|
|
948
|
+
500
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
function notFoundHandler(c) {
|
|
954
|
+
return c.json(
|
|
955
|
+
error("NOT_FOUND", `Route ${c.req.method} ${c.req.path} not found`),
|
|
956
|
+
404
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// src/middleware/auth.ts
|
|
961
|
+
function extractToken(c, header, prefix, cookie) {
|
|
962
|
+
const authHeader = c.req.header(header);
|
|
963
|
+
if (authHeader) {
|
|
964
|
+
if (prefix && authHeader.startsWith(`${prefix} `)) {
|
|
965
|
+
return authHeader.slice(prefix.length + 1);
|
|
966
|
+
}
|
|
967
|
+
return authHeader;
|
|
968
|
+
}
|
|
969
|
+
if (cookie) {
|
|
970
|
+
const cookieHeader = c.req.header("cookie");
|
|
971
|
+
if (cookieHeader) {
|
|
972
|
+
const cookies = cookieHeader.split(";").map((c2) => c2.trim());
|
|
973
|
+
for (const c2 of cookies) {
|
|
974
|
+
const [key, ...valueParts] = c2.split("=");
|
|
975
|
+
if (key === cookie) {
|
|
976
|
+
return valueParts.join("=");
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return null;
|
|
982
|
+
}
|
|
983
|
+
function auth(options) {
|
|
984
|
+
const {
|
|
985
|
+
verify,
|
|
986
|
+
header = "authorization",
|
|
987
|
+
prefix = "Bearer",
|
|
988
|
+
cookie,
|
|
989
|
+
skip,
|
|
990
|
+
message = "Authentication required"
|
|
991
|
+
} = options;
|
|
992
|
+
return async (c, next) => {
|
|
993
|
+
if (skip?.(c)) {
|
|
994
|
+
return next();
|
|
995
|
+
}
|
|
996
|
+
const token = extractToken(c, header, prefix, cookie);
|
|
997
|
+
if (!token) {
|
|
998
|
+
throw new UnauthorizedError2(message);
|
|
999
|
+
}
|
|
1000
|
+
const payload = await verify(token);
|
|
1001
|
+
if (!payload) {
|
|
1002
|
+
throw new UnauthorizedError2("Invalid or expired token");
|
|
1003
|
+
}
|
|
1004
|
+
const user = {
|
|
1005
|
+
id: payload.sub,
|
|
1006
|
+
email: payload.email,
|
|
1007
|
+
tenantId: payload.tenantId,
|
|
1008
|
+
role: payload.role,
|
|
1009
|
+
permissions: payload.permissions ?? []
|
|
1010
|
+
};
|
|
1011
|
+
c.set("user", user);
|
|
1012
|
+
await next();
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
function optionalAuth(options) {
|
|
1016
|
+
const { verify, header = "authorization", prefix = "Bearer", cookie, skip } = options;
|
|
1017
|
+
return async (c, next) => {
|
|
1018
|
+
if (skip?.(c)) {
|
|
1019
|
+
return next();
|
|
1020
|
+
}
|
|
1021
|
+
const token = extractToken(c, header, prefix, cookie);
|
|
1022
|
+
if (token) {
|
|
1023
|
+
try {
|
|
1024
|
+
const payload = await verify(token);
|
|
1025
|
+
if (payload) {
|
|
1026
|
+
const user = {
|
|
1027
|
+
id: payload.sub,
|
|
1028
|
+
email: payload.email,
|
|
1029
|
+
tenantId: payload.tenantId,
|
|
1030
|
+
role: payload.role,
|
|
1031
|
+
permissions: payload.permissions ?? []
|
|
1032
|
+
};
|
|
1033
|
+
c.set("user", user);
|
|
1034
|
+
}
|
|
1035
|
+
} catch {
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
await next();
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
function createAuthMiddleware(baseOptions) {
|
|
1042
|
+
return {
|
|
1043
|
+
auth: (options) => auth({ ...baseOptions, ...options }),
|
|
1044
|
+
optionalAuth: (options) => optionalAuth({ ...baseOptions, ...options })
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// src/middleware/cors.ts
|
|
1049
|
+
var defaultCorsConfig = {
|
|
1050
|
+
origin: "*",
|
|
1051
|
+
credentials: false,
|
|
1052
|
+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
1053
|
+
allowedHeaders: ["Content-Type", "Authorization", "X-Request-ID", "X-CSRF-Token"],
|
|
1054
|
+
exposedHeaders: ["X-Request-ID", "X-Total-Count"],
|
|
1055
|
+
maxAge: 86400
|
|
1056
|
+
// 24 hours
|
|
1057
|
+
};
|
|
1058
|
+
function isOriginAllowed(origin, config) {
|
|
1059
|
+
if (config.origin === "*") return true;
|
|
1060
|
+
if (typeof config.origin === "string") {
|
|
1061
|
+
return origin === config.origin;
|
|
1062
|
+
}
|
|
1063
|
+
if (Array.isArray(config.origin)) {
|
|
1064
|
+
return config.origin.includes(origin);
|
|
1065
|
+
}
|
|
1066
|
+
if (typeof config.origin === "function") {
|
|
1067
|
+
return config.origin(origin);
|
|
1068
|
+
}
|
|
1069
|
+
return false;
|
|
1070
|
+
}
|
|
1071
|
+
function cors2(config) {
|
|
1072
|
+
const corsConfig2 = { ...defaultCorsConfig, ...config };
|
|
1073
|
+
return async (c, next) => {
|
|
1074
|
+
const origin = c.req.header("origin") ?? "";
|
|
1075
|
+
if (c.req.method === "OPTIONS") {
|
|
1076
|
+
const response = new Response(null, { status: 204 });
|
|
1077
|
+
if (isOriginAllowed(origin, corsConfig2)) {
|
|
1078
|
+
response.headers.set("Access-Control-Allow-Origin", origin || "*");
|
|
1079
|
+
}
|
|
1080
|
+
if (corsConfig2.credentials) {
|
|
1081
|
+
response.headers.set("Access-Control-Allow-Credentials", "true");
|
|
1082
|
+
}
|
|
1083
|
+
if (corsConfig2.methods) {
|
|
1084
|
+
response.headers.set(
|
|
1085
|
+
"Access-Control-Allow-Methods",
|
|
1086
|
+
corsConfig2.methods.join(", ")
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
if (corsConfig2.allowedHeaders) {
|
|
1090
|
+
response.headers.set(
|
|
1091
|
+
"Access-Control-Allow-Headers",
|
|
1092
|
+
corsConfig2.allowedHeaders.join(", ")
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
if (corsConfig2.maxAge) {
|
|
1096
|
+
response.headers.set("Access-Control-Max-Age", String(corsConfig2.maxAge));
|
|
1097
|
+
}
|
|
1098
|
+
return response;
|
|
1099
|
+
}
|
|
1100
|
+
await next();
|
|
1101
|
+
if (isOriginAllowed(origin, corsConfig2)) {
|
|
1102
|
+
c.header("Access-Control-Allow-Origin", origin || "*");
|
|
1103
|
+
}
|
|
1104
|
+
if (corsConfig2.credentials) {
|
|
1105
|
+
c.header("Access-Control-Allow-Credentials", "true");
|
|
1106
|
+
}
|
|
1107
|
+
if (corsConfig2.exposedHeaders) {
|
|
1108
|
+
c.header("Access-Control-Expose-Headers", corsConfig2.exposedHeaders.join(", "));
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// src/middleware/csrf.ts
|
|
1114
|
+
function generateRandomToken() {
|
|
1115
|
+
const bytes = new Uint8Array(32);
|
|
1116
|
+
crypto.getRandomValues(bytes);
|
|
1117
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1118
|
+
}
|
|
1119
|
+
function getCookie(c, name) {
|
|
1120
|
+
const cookieHeader = c.req.header("cookie");
|
|
1121
|
+
if (!cookieHeader) return void 0;
|
|
1122
|
+
const cookies = cookieHeader.split(";").map((c2) => c2.trim());
|
|
1123
|
+
for (const cookie of cookies) {
|
|
1124
|
+
const [key, ...valueParts] = cookie.split("=");
|
|
1125
|
+
if (key === name) {
|
|
1126
|
+
return valueParts.join("=");
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return void 0;
|
|
1130
|
+
}
|
|
1131
|
+
function csrf(options = {}) {
|
|
1132
|
+
const {
|
|
1133
|
+
cookieName = "_csrf",
|
|
1134
|
+
headerName = "x-csrf-token",
|
|
1135
|
+
methods = ["POST", "PUT", "PATCH", "DELETE"],
|
|
1136
|
+
excludePaths = [],
|
|
1137
|
+
skip,
|
|
1138
|
+
generateToken = generateRandomToken,
|
|
1139
|
+
cookie = {}
|
|
1140
|
+
} = options;
|
|
1141
|
+
const cookieOptions = {
|
|
1142
|
+
secure: cookie.secure ?? true,
|
|
1143
|
+
httpOnly: cookie.httpOnly ?? true,
|
|
1144
|
+
sameSite: cookie.sameSite ?? "lax",
|
|
1145
|
+
path: cookie.path ?? "/",
|
|
1146
|
+
maxAge: cookie.maxAge ?? 86400
|
|
1147
|
+
// 24 hours
|
|
1148
|
+
};
|
|
1149
|
+
return async (c, next) => {
|
|
1150
|
+
if (skip?.(c)) {
|
|
1151
|
+
return next();
|
|
1152
|
+
}
|
|
1153
|
+
const path = c.req.path;
|
|
1154
|
+
if (excludePaths.some((p) => path.startsWith(p))) {
|
|
1155
|
+
return next();
|
|
1156
|
+
}
|
|
1157
|
+
let token = getCookie(c, cookieName);
|
|
1158
|
+
if (!token) {
|
|
1159
|
+
token = generateToken();
|
|
1160
|
+
const cookieValue = [
|
|
1161
|
+
`${cookieName}=${token}`,
|
|
1162
|
+
`Path=${cookieOptions.path}`,
|
|
1163
|
+
`Max-Age=${cookieOptions.maxAge}`,
|
|
1164
|
+
cookieOptions.sameSite && `SameSite=${cookieOptions.sameSite}`,
|
|
1165
|
+
cookieOptions.secure && "Secure",
|
|
1166
|
+
cookieOptions.httpOnly && "HttpOnly"
|
|
1167
|
+
].filter(Boolean).join("; ");
|
|
1168
|
+
c.header("Set-Cookie", cookieValue);
|
|
1169
|
+
}
|
|
1170
|
+
c.set("csrfToken", token);
|
|
1171
|
+
if (methods.includes(c.req.method)) {
|
|
1172
|
+
const headerToken = c.req.header(headerName);
|
|
1173
|
+
const bodyToken = await getBodyToken(c);
|
|
1174
|
+
const providedToken = headerToken ?? bodyToken;
|
|
1175
|
+
if (!providedToken || providedToken !== token) {
|
|
1176
|
+
throw new ForbiddenError2("Invalid CSRF token");
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
await next();
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
async function getBodyToken(c) {
|
|
1183
|
+
try {
|
|
1184
|
+
const contentType = c.req.header("content-type") ?? "";
|
|
1185
|
+
if (contentType.includes("application/json")) {
|
|
1186
|
+
const body = await c.req.json();
|
|
1187
|
+
return body["_csrf"] ?? body["csrfToken"] ?? body["csrf_token"];
|
|
1188
|
+
}
|
|
1189
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
1190
|
+
const body = await c.req.parseBody();
|
|
1191
|
+
return body["_csrf"];
|
|
1192
|
+
}
|
|
1193
|
+
} catch {
|
|
1194
|
+
}
|
|
1195
|
+
return void 0;
|
|
1196
|
+
}
|
|
1197
|
+
function doubleSubmitCookie(options = {}) {
|
|
1198
|
+
return csrf({
|
|
1199
|
+
...options,
|
|
1200
|
+
cookie: {
|
|
1201
|
+
...options.cookie,
|
|
1202
|
+
httpOnly: false
|
|
1203
|
+
// Allow JS to read the cookie
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// src/middleware/rate-limit.ts
|
|
1209
|
+
var MemoryRateLimitStorage = class {
|
|
1210
|
+
store = /* @__PURE__ */ new Map();
|
|
1211
|
+
async get(key) {
|
|
1212
|
+
const entry = this.store.get(key);
|
|
1213
|
+
if (!entry || entry.expires < Date.now()) {
|
|
1214
|
+
return 0;
|
|
1215
|
+
}
|
|
1216
|
+
return entry.count;
|
|
1217
|
+
}
|
|
1218
|
+
async increment(key, windowMs) {
|
|
1219
|
+
const now = Date.now();
|
|
1220
|
+
const entry = this.store.get(key);
|
|
1221
|
+
if (!entry || entry.expires < now) {
|
|
1222
|
+
this.store.set(key, { count: 1, expires: now + windowMs });
|
|
1223
|
+
return 1;
|
|
1224
|
+
}
|
|
1225
|
+
entry.count++;
|
|
1226
|
+
return entry.count;
|
|
1227
|
+
}
|
|
1228
|
+
async reset(key) {
|
|
1229
|
+
this.store.delete(key);
|
|
1230
|
+
}
|
|
1231
|
+
/** Clean up expired entries */
|
|
1232
|
+
cleanup() {
|
|
1233
|
+
const now = Date.now();
|
|
1234
|
+
for (const [key, entry] of this.store) {
|
|
1235
|
+
if (entry.expires < now) {
|
|
1236
|
+
this.store.delete(key);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
var defaultStorage = null;
|
|
1242
|
+
function getDefaultStorage() {
|
|
1243
|
+
if (!defaultStorage) {
|
|
1244
|
+
defaultStorage = new MemoryRateLimitStorage();
|
|
1245
|
+
}
|
|
1246
|
+
return defaultStorage;
|
|
1247
|
+
}
|
|
1248
|
+
function rateLimit(options = {}) {
|
|
1249
|
+
const {
|
|
1250
|
+
windowMs = 60 * 1e3,
|
|
1251
|
+
// 1 minute
|
|
1252
|
+
max = 100,
|
|
1253
|
+
keyGenerator = defaultKeyGenerator,
|
|
1254
|
+
skip,
|
|
1255
|
+
storage = getDefaultStorage(),
|
|
1256
|
+
message = "Too many requests, please try again later",
|
|
1257
|
+
headers = true,
|
|
1258
|
+
onLimitReached
|
|
1259
|
+
} = options;
|
|
1260
|
+
return async (c, next) => {
|
|
1261
|
+
if (skip?.(c)) {
|
|
1262
|
+
return next();
|
|
1263
|
+
}
|
|
1264
|
+
const key = `ratelimit:${keyGenerator(c)}`;
|
|
1265
|
+
const current = await storage.increment(key, windowMs);
|
|
1266
|
+
if (headers) {
|
|
1267
|
+
c.header("X-RateLimit-Limit", String(max));
|
|
1268
|
+
c.header("X-RateLimit-Remaining", String(Math.max(0, max - current)));
|
|
1269
|
+
c.header("X-RateLimit-Reset", String(Math.ceil((Date.now() + windowMs) / 1e3)));
|
|
1270
|
+
}
|
|
1271
|
+
if (current > max) {
|
|
1272
|
+
if (onLimitReached) {
|
|
1273
|
+
onLimitReached(c, key);
|
|
1274
|
+
}
|
|
1275
|
+
const retryAfter = Math.ceil(windowMs / 1e3);
|
|
1276
|
+
c.header("Retry-After", String(retryAfter));
|
|
1277
|
+
throw new RateLimitError(message, retryAfter);
|
|
1278
|
+
}
|
|
1279
|
+
await next();
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
function defaultKeyGenerator(c) {
|
|
1283
|
+
return c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ?? c.req.header("x-real-ip") ?? c.req.header("cf-connecting-ip") ?? "unknown";
|
|
1284
|
+
}
|
|
1285
|
+
function createRateLimiter(options = {}) {
|
|
1286
|
+
const storage = options.storage ?? getDefaultStorage();
|
|
1287
|
+
return {
|
|
1288
|
+
middleware: rateLimit({ ...options, storage }),
|
|
1289
|
+
storage,
|
|
1290
|
+
reset: (key) => storage.reset(`ratelimit:${key}`),
|
|
1291
|
+
get: (key) => storage.get(`ratelimit:${key}`)
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// src/middleware/request-logger.ts
|
|
1296
|
+
function formatBytes(bytes) {
|
|
1297
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
1298
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
1299
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
1300
|
+
}
|
|
1301
|
+
function requestLogger(options = {}) {
|
|
1302
|
+
const {
|
|
1303
|
+
skip,
|
|
1304
|
+
format = "json",
|
|
1305
|
+
includeBody = false,
|
|
1306
|
+
maxBodyLength = 1e3
|
|
1307
|
+
} = options;
|
|
1308
|
+
return async (c, next) => {
|
|
1309
|
+
if (skip?.(c)) {
|
|
1310
|
+
return next();
|
|
1311
|
+
}
|
|
1312
|
+
const start = Date.now();
|
|
1313
|
+
const logger = c.get("logger");
|
|
1314
|
+
const requestId = c.get("requestId");
|
|
1315
|
+
const method = c.req.method;
|
|
1316
|
+
const path = c.req.path;
|
|
1317
|
+
const query = c.req.query();
|
|
1318
|
+
const userAgent = c.req.header("user-agent");
|
|
1319
|
+
const ip = c.req.header("x-forwarded-for") ?? c.req.header("x-real-ip") ?? "unknown";
|
|
1320
|
+
if (format === "json") {
|
|
1321
|
+
logger?.debug("Request started", {
|
|
1322
|
+
requestId,
|
|
1323
|
+
method,
|
|
1324
|
+
path,
|
|
1325
|
+
query: Object.keys(query).length > 0 ? query : void 0,
|
|
1326
|
+
ip,
|
|
1327
|
+
userAgent
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
let requestBody;
|
|
1331
|
+
if (includeBody && ["POST", "PUT", "PATCH"].includes(method)) {
|
|
1332
|
+
try {
|
|
1333
|
+
const contentType = c.req.header("content-type") ?? "";
|
|
1334
|
+
if (contentType.includes("application/json")) {
|
|
1335
|
+
const body = await c.req.text();
|
|
1336
|
+
requestBody = body.length > maxBodyLength ? body.substring(0, maxBodyLength) + "..." : body;
|
|
1337
|
+
}
|
|
1338
|
+
} catch {
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
await next();
|
|
1342
|
+
const duration = Date.now() - start;
|
|
1343
|
+
const status2 = c.res.status;
|
|
1344
|
+
const contentLength = c.res.headers.get("content-length");
|
|
1345
|
+
const size = contentLength ? parseInt(contentLength, 10) : 0;
|
|
1346
|
+
if (format === "json") {
|
|
1347
|
+
const logData = {
|
|
1348
|
+
requestId,
|
|
1349
|
+
method,
|
|
1350
|
+
path,
|
|
1351
|
+
status: status2,
|
|
1352
|
+
duration: `${duration}ms`,
|
|
1353
|
+
size: formatBytes(size)
|
|
1354
|
+
};
|
|
1355
|
+
if (requestBody) {
|
|
1356
|
+
logData["requestBody"] = requestBody;
|
|
1357
|
+
}
|
|
1358
|
+
if (status2 >= 500) {
|
|
1359
|
+
logger?.error("Request completed", logData);
|
|
1360
|
+
} else if (status2 >= 400) {
|
|
1361
|
+
logger?.warn("Request completed", logData);
|
|
1362
|
+
} else {
|
|
1363
|
+
logger?.info("Request completed", logData);
|
|
1364
|
+
}
|
|
1365
|
+
} else if (format === "combined") {
|
|
1366
|
+
const log = `${ip} - - [${(/* @__PURE__ */ new Date()).toISOString()}] "${method} ${path}" ${status2} ${size} "-" "${userAgent}" ${duration}ms`;
|
|
1367
|
+
console.log(log);
|
|
1368
|
+
} else {
|
|
1369
|
+
const log = `${method} ${path} ${status2} ${duration}ms`;
|
|
1370
|
+
console.log(log);
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// src/middleware/usage-tracking.ts
|
|
1376
|
+
function usageTracking(options) {
|
|
1377
|
+
const {
|
|
1378
|
+
usageService,
|
|
1379
|
+
featureKey = "api_calls",
|
|
1380
|
+
quantity = 1,
|
|
1381
|
+
skip,
|
|
1382
|
+
trackOn = "response",
|
|
1383
|
+
successOnly = true,
|
|
1384
|
+
getCustomerId = (c) => c.get("user")?.id,
|
|
1385
|
+
getTenantId = (c) => c.get("tenant")?.id ?? c.get("user")?.tenantId,
|
|
1386
|
+
getSubscriptionId,
|
|
1387
|
+
includeMetadata = true,
|
|
1388
|
+
getIdempotencyKey
|
|
1389
|
+
} = options;
|
|
1390
|
+
return async (c, next) => {
|
|
1391
|
+
if (trackOn === "request") {
|
|
1392
|
+
await trackUsage(c);
|
|
1393
|
+
return next();
|
|
1394
|
+
}
|
|
1395
|
+
await next();
|
|
1396
|
+
if (skip?.(c)) return;
|
|
1397
|
+
if (successOnly && c.res.status >= 400) return;
|
|
1398
|
+
await trackUsage(c);
|
|
1399
|
+
};
|
|
1400
|
+
async function trackUsage(c) {
|
|
1401
|
+
const customerId = getCustomerId(c);
|
|
1402
|
+
const tenantId = getTenantId(c);
|
|
1403
|
+
if (!customerId || !tenantId) return;
|
|
1404
|
+
const resolvedFeatureKey = typeof featureKey === "function" ? featureKey(c) : featureKey;
|
|
1405
|
+
const resolvedQuantity = typeof quantity === "function" ? quantity(c) : quantity;
|
|
1406
|
+
const metadata = includeMetadata ? {
|
|
1407
|
+
path: c.req.path,
|
|
1408
|
+
method: c.req.method,
|
|
1409
|
+
statusCode: c.res.status,
|
|
1410
|
+
userAgent: c.req.header("user-agent")
|
|
1411
|
+
} : void 0;
|
|
1412
|
+
try {
|
|
1413
|
+
const trackOptions = {
|
|
1414
|
+
tenantId,
|
|
1415
|
+
customerId,
|
|
1416
|
+
featureKey: resolvedFeatureKey,
|
|
1417
|
+
quantity: resolvedQuantity
|
|
1418
|
+
};
|
|
1419
|
+
const subscriptionId = getSubscriptionId?.(c);
|
|
1420
|
+
if (subscriptionId !== void 0) {
|
|
1421
|
+
trackOptions.subscriptionId = subscriptionId;
|
|
1422
|
+
}
|
|
1423
|
+
if (metadata !== void 0) {
|
|
1424
|
+
trackOptions.metadata = metadata;
|
|
1425
|
+
}
|
|
1426
|
+
const idempotencyKey = getIdempotencyKey?.(c);
|
|
1427
|
+
if (idempotencyKey !== void 0) {
|
|
1428
|
+
trackOptions.idempotencyKey = idempotencyKey;
|
|
1429
|
+
}
|
|
1430
|
+
await usageService.trackUsage(trackOptions);
|
|
1431
|
+
} catch (error2) {
|
|
1432
|
+
const logger = c.get("logger");
|
|
1433
|
+
if (logger) {
|
|
1434
|
+
logger.error("Usage tracking failed", {
|
|
1435
|
+
error: error2 instanceof Error ? error2.message : String(error2),
|
|
1436
|
+
customerId,
|
|
1437
|
+
featureKey: resolvedFeatureKey
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
function createUsageTracking(baseOptions) {
|
|
1444
|
+
return (overrides) => {
|
|
1445
|
+
return usageTracking({ ...baseOptions, ...overrides });
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// src/middleware/quota-enforcement.ts
|
|
1450
|
+
var QuotaExceededError = class extends Error {
|
|
1451
|
+
constructor(featureKey, limit, currentUsage, requestedQuantity = 1) {
|
|
1452
|
+
super(
|
|
1453
|
+
`Quota exceeded for "${featureKey}": ${currentUsage}/${limit ?? "unlimited"} used`
|
|
1454
|
+
);
|
|
1455
|
+
this.featureKey = featureKey;
|
|
1456
|
+
this.limit = limit;
|
|
1457
|
+
this.currentUsage = currentUsage;
|
|
1458
|
+
this.requestedQuantity = requestedQuantity;
|
|
1459
|
+
this.name = "QuotaExceededError";
|
|
1460
|
+
}
|
|
1461
|
+
statusCode = 429;
|
|
1462
|
+
code = "QUOTA_EXCEEDED";
|
|
1463
|
+
};
|
|
1464
|
+
function quotaEnforcement(options) {
|
|
1465
|
+
const {
|
|
1466
|
+
quotaManager,
|
|
1467
|
+
featureKey,
|
|
1468
|
+
quantity = 1,
|
|
1469
|
+
skip,
|
|
1470
|
+
getCustomerId = (c) => c.get("user")?.id,
|
|
1471
|
+
includeHeaders = true,
|
|
1472
|
+
onQuotaExceeded,
|
|
1473
|
+
softLimit = false,
|
|
1474
|
+
onQuotaWarning
|
|
1475
|
+
} = options;
|
|
1476
|
+
return async (c, next) => {
|
|
1477
|
+
if (skip?.(c)) {
|
|
1478
|
+
return next();
|
|
1479
|
+
}
|
|
1480
|
+
const customerId = getCustomerId(c);
|
|
1481
|
+
if (!customerId) {
|
|
1482
|
+
return next();
|
|
1483
|
+
}
|
|
1484
|
+
const resolvedFeatureKey = typeof featureKey === "function" ? featureKey(c) : featureKey;
|
|
1485
|
+
const resolvedQuantity = typeof quantity === "function" ? quantity(c) : quantity;
|
|
1486
|
+
try {
|
|
1487
|
+
const result = await quotaManager.checkQuota(
|
|
1488
|
+
customerId,
|
|
1489
|
+
resolvedFeatureKey,
|
|
1490
|
+
resolvedQuantity
|
|
1491
|
+
);
|
|
1492
|
+
if (includeHeaders) {
|
|
1493
|
+
c.header("X-Quota-Limit", String(result.limit ?? "unlimited"));
|
|
1494
|
+
c.header("X-Quota-Remaining", String(result.remaining ?? "unlimited"));
|
|
1495
|
+
c.header("X-Quota-Used", String(result.currentUsage));
|
|
1496
|
+
if (result.percentAfter !== null) {
|
|
1497
|
+
c.header("X-Quota-Percent", String(result.percentAfter));
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
if (result.percentAfter !== null && result.percentAfter >= 80 && onQuotaWarning) {
|
|
1501
|
+
onQuotaWarning(c, result, resolvedFeatureKey);
|
|
1502
|
+
}
|
|
1503
|
+
if (!result.allowed && !softLimit) {
|
|
1504
|
+
if (onQuotaExceeded) {
|
|
1505
|
+
const response = onQuotaExceeded(c, result, resolvedFeatureKey);
|
|
1506
|
+
if (response) return response;
|
|
1507
|
+
}
|
|
1508
|
+
throw new QuotaExceededError(
|
|
1509
|
+
resolvedFeatureKey,
|
|
1510
|
+
result.limit,
|
|
1511
|
+
result.currentUsage,
|
|
1512
|
+
resolvedQuantity
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
await next();
|
|
1516
|
+
} catch (error2) {
|
|
1517
|
+
if (error2 instanceof QuotaExceededError) {
|
|
1518
|
+
throw error2;
|
|
1519
|
+
}
|
|
1520
|
+
const logger = c.get("logger");
|
|
1521
|
+
if (logger) {
|
|
1522
|
+
logger.error("Quota check failed", {
|
|
1523
|
+
error: error2 instanceof Error ? error2.message : String(error2),
|
|
1524
|
+
customerId,
|
|
1525
|
+
featureKey: resolvedFeatureKey
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
await next();
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
function createQuotaEnforcement(baseOptions) {
|
|
1533
|
+
return (featureKey) => {
|
|
1534
|
+
return quotaEnforcement({ ...baseOptions, featureKey });
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
function multiQuotaEnforcement(options) {
|
|
1538
|
+
const { features } = options;
|
|
1539
|
+
return async (c, next) => {
|
|
1540
|
+
const customerId = (options.getCustomerId ?? ((ctx) => ctx.get("user")?.id))(c);
|
|
1541
|
+
if (!customerId || options.skip?.(c)) {
|
|
1542
|
+
return next();
|
|
1543
|
+
}
|
|
1544
|
+
for (const feature of features) {
|
|
1545
|
+
const resolvedQuantity = typeof feature.quantity === "function" ? feature.quantity(c) : feature.quantity ?? 1;
|
|
1546
|
+
const result = await options.quotaManager.checkQuota(
|
|
1547
|
+
customerId,
|
|
1548
|
+
feature.featureKey,
|
|
1549
|
+
resolvedQuantity
|
|
1550
|
+
);
|
|
1551
|
+
if (!result.allowed && !options.softLimit) {
|
|
1552
|
+
if (options.onQuotaExceeded) {
|
|
1553
|
+
const response = options.onQuotaExceeded(c, result, feature.featureKey);
|
|
1554
|
+
if (response) return response;
|
|
1555
|
+
}
|
|
1556
|
+
throw new QuotaExceededError(
|
|
1557
|
+
feature.featureKey,
|
|
1558
|
+
result.limit,
|
|
1559
|
+
result.currentUsage,
|
|
1560
|
+
resolvedQuantity
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
await next();
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// src/validation/index.ts
|
|
1569
|
+
import {
|
|
1570
|
+
type,
|
|
1571
|
+
uuid,
|
|
1572
|
+
timestamp,
|
|
1573
|
+
email,
|
|
1574
|
+
url,
|
|
1575
|
+
nonEmptyString,
|
|
1576
|
+
positiveInt,
|
|
1577
|
+
nonNegativeInt,
|
|
1578
|
+
status,
|
|
1579
|
+
pagination,
|
|
1580
|
+
paginationMeta,
|
|
1581
|
+
cursorPagination,
|
|
1582
|
+
cursorPaginationMeta,
|
|
1583
|
+
uuidParam,
|
|
1584
|
+
paginationQuery,
|
|
1585
|
+
cursorPaginationQuery,
|
|
1586
|
+
searchQuery,
|
|
1587
|
+
dateRangeQuery,
|
|
1588
|
+
healthResponse,
|
|
1589
|
+
apiInfoResponse,
|
|
1590
|
+
corsConfig,
|
|
1591
|
+
serverRateLimitConfig,
|
|
1592
|
+
loggerConfig,
|
|
1593
|
+
serverConfig,
|
|
1594
|
+
authContext,
|
|
1595
|
+
requestContext,
|
|
1596
|
+
successResponse,
|
|
1597
|
+
errorResponse,
|
|
1598
|
+
paginatedResponse,
|
|
1599
|
+
cursorPaginatedResponse,
|
|
1600
|
+
parsError,
|
|
1601
|
+
validateWithSchema,
|
|
1602
|
+
safeValidate,
|
|
1603
|
+
isValid,
|
|
1604
|
+
formatErrors
|
|
1605
|
+
} from "@parsrun/types";
|
|
1606
|
+
import { type as type2, formatErrors as formatArkErrors } from "@parsrun/types";
|
|
1607
|
+
import {
|
|
1608
|
+
uuidParam as uuidParam2,
|
|
1609
|
+
paginationQuery as paginationQuery2,
|
|
1610
|
+
searchQuery as searchQuery2,
|
|
1611
|
+
dateRangeQuery as dateRangeQuery2
|
|
1612
|
+
} from "@parsrun/types";
|
|
1613
|
+
function formatValidationErrors(errors) {
|
|
1614
|
+
const formatted = formatArkErrors(errors);
|
|
1615
|
+
const result = {};
|
|
1616
|
+
for (const [key, value] of Object.entries(formatted)) {
|
|
1617
|
+
result[key] = [value];
|
|
1618
|
+
}
|
|
1619
|
+
return result;
|
|
1620
|
+
}
|
|
1621
|
+
function validateBody(schema, options = {}) {
|
|
1622
|
+
return async (c, next) => {
|
|
1623
|
+
let body;
|
|
1624
|
+
try {
|
|
1625
|
+
body = await c.req.json();
|
|
1626
|
+
} catch {
|
|
1627
|
+
throw new ValidationError("Invalid JSON body");
|
|
1628
|
+
}
|
|
1629
|
+
const result = schema(body);
|
|
1630
|
+
if (result instanceof type2.errors) {
|
|
1631
|
+
throw new ValidationError(
|
|
1632
|
+
options.messagePrefix ?? "Validation failed",
|
|
1633
|
+
{ errors: formatValidationErrors(result) }
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
c.set("validatedBody", result);
|
|
1637
|
+
await next();
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
function validateQuery(schema, options = {}) {
|
|
1641
|
+
return async (c, next) => {
|
|
1642
|
+
const query = c.req.query();
|
|
1643
|
+
const result = schema(query);
|
|
1644
|
+
if (result instanceof type2.errors) {
|
|
1645
|
+
throw new ValidationError(
|
|
1646
|
+
options.messagePrefix ?? "Invalid query parameters",
|
|
1647
|
+
{ errors: formatValidationErrors(result) }
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
c.set("validatedQuery", result);
|
|
1651
|
+
await next();
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
function validateParams(schema, options = {}) {
|
|
1655
|
+
return async (c, next) => {
|
|
1656
|
+
const params = c.req.param();
|
|
1657
|
+
const result = schema(params);
|
|
1658
|
+
if (result instanceof type2.errors) {
|
|
1659
|
+
throw new ValidationError(
|
|
1660
|
+
options.messagePrefix ?? "Invalid route parameters",
|
|
1661
|
+
{ errors: formatValidationErrors(result) }
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
c.set("validatedParams", result);
|
|
1665
|
+
await next();
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
function validateHeaders(schema, options = {}) {
|
|
1669
|
+
return async (c, next) => {
|
|
1670
|
+
const headers = {};
|
|
1671
|
+
c.req.raw.headers.forEach((value, key) => {
|
|
1672
|
+
headers[key.toLowerCase()] = value;
|
|
1673
|
+
});
|
|
1674
|
+
const result = schema(headers);
|
|
1675
|
+
if (result instanceof type2.errors) {
|
|
1676
|
+
throw new ValidationError(
|
|
1677
|
+
options.messagePrefix ?? "Invalid headers",
|
|
1678
|
+
{ errors: formatValidationErrors(result) }
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
c.set("validatedHeaders", result);
|
|
1682
|
+
await next();
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
function validate(schemas) {
|
|
1686
|
+
return async (c, next) => {
|
|
1687
|
+
if (schemas.params) {
|
|
1688
|
+
const params = c.req.param();
|
|
1689
|
+
const result = schemas.params(params);
|
|
1690
|
+
if (result instanceof type2.errors) {
|
|
1691
|
+
throw new ValidationError("Invalid route parameters", {
|
|
1692
|
+
errors: formatValidationErrors(result)
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
c.set("validatedParams", result);
|
|
1696
|
+
}
|
|
1697
|
+
if (schemas.query) {
|
|
1698
|
+
const query = c.req.query();
|
|
1699
|
+
const result = schemas.query(query);
|
|
1700
|
+
if (result instanceof type2.errors) {
|
|
1701
|
+
throw new ValidationError("Invalid query parameters", {
|
|
1702
|
+
errors: formatValidationErrors(result)
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
c.set("validatedQuery", result);
|
|
1706
|
+
}
|
|
1707
|
+
if (schemas.headers) {
|
|
1708
|
+
const headers = {};
|
|
1709
|
+
c.req.raw.headers.forEach((value, key) => {
|
|
1710
|
+
headers[key.toLowerCase()] = value;
|
|
1711
|
+
});
|
|
1712
|
+
const result = schemas.headers(headers);
|
|
1713
|
+
if (result instanceof type2.errors) {
|
|
1714
|
+
throw new ValidationError("Invalid headers", {
|
|
1715
|
+
errors: formatValidationErrors(result)
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
c.set("validatedHeaders", result);
|
|
1719
|
+
}
|
|
1720
|
+
if (schemas.body) {
|
|
1721
|
+
let body;
|
|
1722
|
+
try {
|
|
1723
|
+
body = await c.req.json();
|
|
1724
|
+
} catch {
|
|
1725
|
+
throw new ValidationError("Invalid JSON body");
|
|
1726
|
+
}
|
|
1727
|
+
const result = schemas.body(body);
|
|
1728
|
+
if (result instanceof type2.errors) {
|
|
1729
|
+
throw new ValidationError("Validation failed", {
|
|
1730
|
+
errors: formatValidationErrors(result)
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
c.set("validatedBody", result);
|
|
1734
|
+
}
|
|
1735
|
+
await next();
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// src/utils/pagination.ts
|
|
1740
|
+
var defaultOptions = {
|
|
1741
|
+
defaultLimit: 20,
|
|
1742
|
+
maxLimit: 100,
|
|
1743
|
+
pageParam: "page",
|
|
1744
|
+
limitParam: "limit"
|
|
1745
|
+
};
|
|
1746
|
+
function parsePagination(c, options = {}) {
|
|
1747
|
+
const opts = { ...defaultOptions, ...options };
|
|
1748
|
+
const pageStr = c.req.query(opts.pageParam);
|
|
1749
|
+
const limitStr = c.req.query(opts.limitParam);
|
|
1750
|
+
let page = pageStr ? parseInt(pageStr, 10) : 1;
|
|
1751
|
+
let limit = limitStr ? parseInt(limitStr, 10) : opts.defaultLimit;
|
|
1752
|
+
if (isNaN(page) || page < 1) page = 1;
|
|
1753
|
+
if (isNaN(limit) || limit < 1) limit = opts.defaultLimit;
|
|
1754
|
+
if (limit > opts.maxLimit) limit = opts.maxLimit;
|
|
1755
|
+
const offset = (page - 1) * limit;
|
|
1756
|
+
return { page, limit, offset };
|
|
1757
|
+
}
|
|
1758
|
+
function createPaginationMeta(params) {
|
|
1759
|
+
const { page, limit, total } = params;
|
|
1760
|
+
const totalPages = Math.ceil(total / limit);
|
|
1761
|
+
return {
|
|
1762
|
+
page,
|
|
1763
|
+
limit,
|
|
1764
|
+
total,
|
|
1765
|
+
totalPages,
|
|
1766
|
+
hasNext: page < totalPages,
|
|
1767
|
+
hasPrev: page > 1
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
function paginate(data, params) {
|
|
1771
|
+
return {
|
|
1772
|
+
data,
|
|
1773
|
+
pagination: createPaginationMeta(params)
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
function parseCursorPagination(c, options = {}) {
|
|
1777
|
+
const opts = { ...defaultOptions, ...options };
|
|
1778
|
+
const cursor = c.req.query("cursor") ?? void 0;
|
|
1779
|
+
const limitStr = c.req.query(opts.limitParam);
|
|
1780
|
+
const direction = c.req.query("direction") === "backward" ? "backward" : "forward";
|
|
1781
|
+
let limit = limitStr ? parseInt(limitStr, 10) : opts.defaultLimit;
|
|
1782
|
+
if (isNaN(limit) || limit < 1) limit = opts.defaultLimit;
|
|
1783
|
+
if (limit > opts.maxLimit) limit = opts.maxLimit;
|
|
1784
|
+
return { cursor, limit, direction };
|
|
1785
|
+
}
|
|
1786
|
+
function cursorPaginate(data, params) {
|
|
1787
|
+
const { cursor, limit } = params;
|
|
1788
|
+
const hasMore = data.length > limit;
|
|
1789
|
+
const items = hasMore ? data.slice(0, limit) : data;
|
|
1790
|
+
const lastItem = items[items.length - 1];
|
|
1791
|
+
const firstItem = items[0];
|
|
1792
|
+
return {
|
|
1793
|
+
data: items,
|
|
1794
|
+
pagination: {
|
|
1795
|
+
cursor,
|
|
1796
|
+
nextCursor: hasMore && lastItem ? lastItem.id : void 0,
|
|
1797
|
+
prevCursor: cursor && firstItem ? firstItem.id : void 0,
|
|
1798
|
+
hasMore,
|
|
1799
|
+
limit
|
|
1800
|
+
}
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
function setPaginationHeaders(c, meta) {
|
|
1804
|
+
c.header("X-Total-Count", String(meta.total));
|
|
1805
|
+
c.header("X-Total-Pages", String(meta.totalPages));
|
|
1806
|
+
c.header("X-Page", String(meta.page));
|
|
1807
|
+
c.header("X-Per-Page", String(meta.limit));
|
|
1808
|
+
c.header("X-Has-Next", String(meta.hasNext));
|
|
1809
|
+
c.header("X-Has-Prev", String(meta.hasPrev));
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// src/utils/response.ts
|
|
1813
|
+
function json(c, data, status2 = 200) {
|
|
1814
|
+
return c.json(success(data), status2);
|
|
1815
|
+
}
|
|
1816
|
+
function jsonWithMeta(c, data, meta, status2 = 200) {
|
|
1817
|
+
return c.json(success(data, meta), status2);
|
|
1818
|
+
}
|
|
1819
|
+
function jsonError(c, code, message, status2 = 400, details) {
|
|
1820
|
+
return c.json(error(code, message, details), status2);
|
|
1821
|
+
}
|
|
1822
|
+
function created(c, data, location) {
|
|
1823
|
+
if (location) {
|
|
1824
|
+
c.header("Location", location);
|
|
1825
|
+
}
|
|
1826
|
+
return c.json(success(data), 201);
|
|
1827
|
+
}
|
|
1828
|
+
function noContent(_c) {
|
|
1829
|
+
return new Response(null, { status: 204 });
|
|
1830
|
+
}
|
|
1831
|
+
function accepted(c, data) {
|
|
1832
|
+
if (data) {
|
|
1833
|
+
return c.json(success(data), 202);
|
|
1834
|
+
}
|
|
1835
|
+
return new Response(null, { status: 202 });
|
|
1836
|
+
}
|
|
1837
|
+
function redirect(c, url2, status2 = 302) {
|
|
1838
|
+
return c.redirect(url2, status2);
|
|
1839
|
+
}
|
|
1840
|
+
function stream(_c, callback, options = {}) {
|
|
1841
|
+
const encoder = new TextEncoder();
|
|
1842
|
+
const readableStream = new ReadableStream({
|
|
1843
|
+
async start(controller) {
|
|
1844
|
+
const write = async (chunk) => {
|
|
1845
|
+
controller.enqueue(encoder.encode(chunk));
|
|
1846
|
+
};
|
|
1847
|
+
try {
|
|
1848
|
+
await callback(write);
|
|
1849
|
+
} finally {
|
|
1850
|
+
controller.close();
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
});
|
|
1854
|
+
return new Response(readableStream, {
|
|
1855
|
+
headers: {
|
|
1856
|
+
"Content-Type": options.contentType ?? "text/event-stream",
|
|
1857
|
+
"Cache-Control": "no-cache",
|
|
1858
|
+
Connection: "keep-alive",
|
|
1859
|
+
...options.headers
|
|
1860
|
+
}
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
function sse(c, callback) {
|
|
1864
|
+
return stream(c, async (write) => {
|
|
1865
|
+
const send = async (event) => {
|
|
1866
|
+
let message = "";
|
|
1867
|
+
if (event.id) {
|
|
1868
|
+
message += `id: ${event.id}
|
|
1869
|
+
`;
|
|
1870
|
+
}
|
|
1871
|
+
if (event.event) {
|
|
1872
|
+
message += `event: ${event.event}
|
|
1873
|
+
`;
|
|
1874
|
+
}
|
|
1875
|
+
if (event.retry) {
|
|
1876
|
+
message += `retry: ${event.retry}
|
|
1877
|
+
`;
|
|
1878
|
+
}
|
|
1879
|
+
const data = typeof event.data === "string" ? event.data : JSON.stringify(event.data);
|
|
1880
|
+
message += `data: ${data}
|
|
1881
|
+
|
|
1882
|
+
`;
|
|
1883
|
+
await write(message);
|
|
1884
|
+
};
|
|
1885
|
+
await callback(send);
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
function download(c, data, filename, contentType = "application/octet-stream") {
|
|
1889
|
+
c.header("Content-Disposition", `attachment; filename="${filename}"`);
|
|
1890
|
+
c.header("Content-Type", contentType);
|
|
1891
|
+
if (typeof data === "string") {
|
|
1892
|
+
return c.body(data);
|
|
1893
|
+
}
|
|
1894
|
+
return new Response(data, {
|
|
1895
|
+
headers: c.res.headers
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// src/health.ts
|
|
1900
|
+
import { Hono as Hono3 } from "hono";
|
|
1901
|
+
var startTime = Date.now();
|
|
1902
|
+
async function checkDatabase(db) {
|
|
1903
|
+
const start = Date.now();
|
|
1904
|
+
try {
|
|
1905
|
+
if (db.ping) {
|
|
1906
|
+
await db.ping();
|
|
1907
|
+
}
|
|
1908
|
+
return {
|
|
1909
|
+
status: "healthy",
|
|
1910
|
+
latency: Date.now() - start
|
|
1911
|
+
};
|
|
1912
|
+
} catch (err) {
|
|
1913
|
+
return {
|
|
1914
|
+
status: "unhealthy",
|
|
1915
|
+
message: err instanceof Error ? err.message : "Database connection failed",
|
|
1916
|
+
latency: Date.now() - start
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
function aggregateStatus(checks) {
|
|
1921
|
+
const statuses = Object.values(checks).map((c) => c.status);
|
|
1922
|
+
if (statuses.some((s) => s === "unhealthy")) {
|
|
1923
|
+
return "unhealthy";
|
|
1924
|
+
}
|
|
1925
|
+
if (statuses.some((s) => s === "degraded")) {
|
|
1926
|
+
return "degraded";
|
|
1927
|
+
}
|
|
1928
|
+
return "healthy";
|
|
1929
|
+
}
|
|
1930
|
+
function createHealthRouter(options = {}) {
|
|
1931
|
+
const router = new Hono3();
|
|
1932
|
+
const { checks = {}, detailed = true } = options;
|
|
1933
|
+
router.get("/", async (c) => {
|
|
1934
|
+
const db = c.get("db");
|
|
1935
|
+
const results = {};
|
|
1936
|
+
results["database"] = await checkDatabase(db);
|
|
1937
|
+
const customCheckResults = await Promise.all(
|
|
1938
|
+
Object.entries(checks).map(async ([name, check]) => {
|
|
1939
|
+
try {
|
|
1940
|
+
const result = await check();
|
|
1941
|
+
return [name, result];
|
|
1942
|
+
} catch (err) {
|
|
1943
|
+
return [
|
|
1944
|
+
name,
|
|
1945
|
+
{
|
|
1946
|
+
status: "unhealthy",
|
|
1947
|
+
message: err instanceof Error ? err.message : "Check failed"
|
|
1948
|
+
}
|
|
1949
|
+
];
|
|
1950
|
+
}
|
|
1951
|
+
})
|
|
1952
|
+
);
|
|
1953
|
+
for (const [name, result] of customCheckResults) {
|
|
1954
|
+
results[name] = result;
|
|
1955
|
+
}
|
|
1956
|
+
const overallStatus = aggregateStatus(results);
|
|
1957
|
+
const response = {
|
|
1958
|
+
status: overallStatus,
|
|
1959
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1960
|
+
uptime: Math.floor((Date.now() - startTime) / 1e3),
|
|
1961
|
+
checks: detailed ? results : {}
|
|
1962
|
+
};
|
|
1963
|
+
const statusCode = overallStatus === "healthy" ? 200 : overallStatus === "degraded" ? 200 : 503;
|
|
1964
|
+
return c.json(response, statusCode);
|
|
1965
|
+
});
|
|
1966
|
+
router.get("/live", (c) => {
|
|
1967
|
+
return c.json({
|
|
1968
|
+
status: "ok",
|
|
1969
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1970
|
+
});
|
|
1971
|
+
});
|
|
1972
|
+
router.get("/ready", async (c) => {
|
|
1973
|
+
const db = c.get("db");
|
|
1974
|
+
const dbHealth = await checkDatabase(db);
|
|
1975
|
+
if (dbHealth.status === "unhealthy") {
|
|
1976
|
+
return c.json(
|
|
1977
|
+
{
|
|
1978
|
+
status: "not_ready",
|
|
1979
|
+
reason: "database",
|
|
1980
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1981
|
+
},
|
|
1982
|
+
503
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1985
|
+
return c.json({
|
|
1986
|
+
status: "ready",
|
|
1987
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1988
|
+
});
|
|
1989
|
+
});
|
|
1990
|
+
router.get("/startup", (c) => {
|
|
1991
|
+
return c.json({
|
|
1992
|
+
status: "started",
|
|
1993
|
+
uptime: Math.floor((Date.now() - startTime) / 1e3),
|
|
1994
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1995
|
+
});
|
|
1996
|
+
});
|
|
1997
|
+
return router;
|
|
1998
|
+
}
|
|
1999
|
+
async function healthHandler(c) {
|
|
2000
|
+
return c.json({
|
|
2001
|
+
status: "ok",
|
|
2002
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2003
|
+
uptime: Math.floor((Date.now() - startTime) / 1e3)
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
export {
|
|
2007
|
+
ApiError,
|
|
2008
|
+
BadRequestError,
|
|
2009
|
+
ConflictError,
|
|
2010
|
+
dateRangeQuery2 as DateRangeQuerySchema,
|
|
2011
|
+
ForbiddenError2 as ForbiddenError,
|
|
2012
|
+
InMemoryRBAC,
|
|
2013
|
+
InternalError,
|
|
2014
|
+
MemoryRateLimitStorage,
|
|
2015
|
+
ModuleLoader,
|
|
2016
|
+
NotFoundError,
|
|
2017
|
+
paginationQuery2 as PaginationQuerySchema,
|
|
2018
|
+
QuotaExceededError,
|
|
2019
|
+
RBACService,
|
|
2020
|
+
RLSError,
|
|
2021
|
+
RLSManager,
|
|
2022
|
+
RateLimitError,
|
|
2023
|
+
searchQuery2 as SearchQuerySchema,
|
|
2024
|
+
ServiceUnavailableError,
|
|
2025
|
+
StandardRoles,
|
|
2026
|
+
UnauthorizedError2 as UnauthorizedError,
|
|
2027
|
+
uuidParam2 as UuidParamSchema,
|
|
2028
|
+
ValidationError,
|
|
2029
|
+
accepted,
|
|
2030
|
+
auth,
|
|
2031
|
+
cors2 as cors,
|
|
2032
|
+
createAuthMiddleware,
|
|
2033
|
+
createHealthRouter,
|
|
2034
|
+
createInMemoryRBAC,
|
|
2035
|
+
createModuleLoader,
|
|
2036
|
+
createModuleRouter,
|
|
2037
|
+
createPaginationMeta,
|
|
2038
|
+
createPermission,
|
|
2039
|
+
createQuotaEnforcement,
|
|
2040
|
+
createRBACService,
|
|
2041
|
+
createRLSManager,
|
|
2042
|
+
createRateLimiter,
|
|
2043
|
+
createRouter,
|
|
2044
|
+
createServer,
|
|
2045
|
+
createUsageTracking,
|
|
2046
|
+
createVersionedRouter,
|
|
2047
|
+
created,
|
|
2048
|
+
crudPermissions,
|
|
2049
|
+
csrf,
|
|
2050
|
+
cursorPaginate,
|
|
2051
|
+
defineModule,
|
|
2052
|
+
doubleSubmitCookie,
|
|
2053
|
+
download,
|
|
2054
|
+
error,
|
|
2055
|
+
errorHandler,
|
|
2056
|
+
generateDisableRLS,
|
|
2057
|
+
generateRLSPolicy,
|
|
2058
|
+
generateRequestId,
|
|
2059
|
+
healthHandler,
|
|
2060
|
+
json,
|
|
2061
|
+
jsonError,
|
|
2062
|
+
jsonWithMeta,
|
|
2063
|
+
multiQuotaEnforcement,
|
|
2064
|
+
noContent,
|
|
2065
|
+
notFoundHandler,
|
|
2066
|
+
optionalAuth,
|
|
2067
|
+
paginate,
|
|
2068
|
+
parseCursorPagination,
|
|
2069
|
+
parsePagination,
|
|
2070
|
+
parsePermission,
|
|
2071
|
+
quotaEnforcement,
|
|
2072
|
+
rateLimit,
|
|
2073
|
+
redirect,
|
|
2074
|
+
requestLogger,
|
|
2075
|
+
requireAnyPermission,
|
|
2076
|
+
requireAnyRole,
|
|
2077
|
+
requireAuth,
|
|
2078
|
+
requirePermission,
|
|
2079
|
+
requireRole,
|
|
2080
|
+
requireTenantMember,
|
|
2081
|
+
rlsMiddleware,
|
|
2082
|
+
setPaginationHeaders,
|
|
2083
|
+
sse,
|
|
2084
|
+
stream,
|
|
2085
|
+
success,
|
|
2086
|
+
type,
|
|
2087
|
+
usageTracking,
|
|
2088
|
+
validate,
|
|
2089
|
+
validateBody,
|
|
2090
|
+
validateHeaders,
|
|
2091
|
+
validateParams,
|
|
2092
|
+
validateQuery
|
|
2093
|
+
};
|
|
2094
|
+
//# sourceMappingURL=index.js.map
|