@getstrata/bootstrap 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.
Files changed (107) hide show
  1. package/README.md +29 -0
  2. package/dist/bootstrap/applicationRegistry.d.ts +16 -0
  3. package/dist/bootstrap/config.d.ts +20 -0
  4. package/dist/bootstrap/contracts.d.ts +63 -0
  5. package/dist/bootstrap/httpKernel.d.ts +26 -0
  6. package/dist/bootstrap/prefixRouteMap.d.ts +2 -0
  7. package/dist/bootstrap/public-api.d.ts +10 -0
  8. package/dist/bootstrap/web/forms.d.ts +10 -0
  9. package/dist/bootstrap/web/index.d.ts +7 -0
  10. package/dist/bootstrap/web/server.d.ts +7 -0
  11. package/dist/bootstrap/web/session.d.ts +23 -0
  12. package/dist/bootstrap/web/slug.d.ts +1 -0
  13. package/dist/config/app.d.ts +10 -0
  14. package/dist/config/contentSecurityPolicy.d.ts +5 -0
  15. package/dist/config/cors.d.ts +9 -0
  16. package/dist/config/database.d.ts +10 -0
  17. package/dist/config/features.d.ts +18 -0
  18. package/dist/config/frontend.d.ts +6 -0
  19. package/dist/config/rateLimit.d.ts +9 -0
  20. package/dist/core/auth/accessControl.d.ts +8 -0
  21. package/dist/core/auth/authContext.d.ts +12 -0
  22. package/dist/core/auth/guard.d.ts +36 -0
  23. package/dist/core/auth/membershipContext.d.ts +16 -0
  24. package/dist/core/auth/membershipContextMiddleware.d.ts +3 -0
  25. package/dist/core/auth/membershipMiddleware.d.ts +2 -0
  26. package/dist/core/auth/oauth/oidcProvider.d.ts +18 -0
  27. package/dist/core/auth/oauth/providers.d.ts +22 -0
  28. package/dist/core/auth/oauth/samlProvider.d.ts +9 -0
  29. package/dist/core/auth/oauth/types.d.ts +11 -0
  30. package/dist/core/auth/password.d.ts +3 -0
  31. package/dist/core/auth/policy.d.ts +15 -0
  32. package/dist/core/auth/tokenHash.d.ts +3 -0
  33. package/dist/core/crypto/fieldEncryption.d.ts +13 -0
  34. package/dist/core/crypto/mfaSecret.d.ts +3 -0
  35. package/dist/core/database/baseRepository.d.ts +44 -0
  36. package/dist/core/database/connection.d.ts +7 -0
  37. package/dist/core/database/connectionContext.d.ts +7 -0
  38. package/dist/core/database/errors.d.ts +12 -0
  39. package/dist/core/database/index.d.ts +11 -0
  40. package/dist/core/database/query.d.ts +51 -0
  41. package/dist/core/database/relationships.d.ts +26 -0
  42. package/dist/core/database/table.d.ts +13 -0
  43. package/dist/core/database/transaction.d.ts +3 -0
  44. package/dist/core/database/types.d.ts +29 -0
  45. package/dist/core/errors/http.d.ts +33 -0
  46. package/dist/core/events/eventBus.d.ts +10 -0
  47. package/dist/core/events/index.d.ts +4 -0
  48. package/dist/core/http/authMiddleware.d.ts +5 -0
  49. package/dist/core/http/authorizeMiddleware.d.ts +5 -0
  50. package/dist/core/http/bodySizeLimitMiddleware.d.ts +4 -0
  51. package/dist/core/http/contentNegotiation.d.ts +2 -0
  52. package/dist/core/http/corsMiddleware.d.ts +3 -0
  53. package/dist/core/http/csrfMiddleware.d.ts +3 -0
  54. package/dist/core/http/csrfToken.d.ts +14 -0
  55. package/dist/core/http/flashMiddleware.d.ts +3 -0
  56. package/dist/core/http/flashSession.d.ts +12 -0
  57. package/dist/core/http/loginThrottleMiddleware.d.ts +11 -0
  58. package/dist/core/http/memoryThrottleMiddleware.d.ts +9 -0
  59. package/dist/core/http/metricsMiddleware.d.ts +4 -0
  60. package/dist/core/http/middleware.d.ts +8 -0
  61. package/dist/core/http/requestMetaContext.d.ts +16 -0
  62. package/dist/core/http/requireAbilityMiddleware.d.ts +4 -0
  63. package/dist/core/http/requireAuthMiddleware.d.ts +4 -0
  64. package/dist/core/http/requireGlobalAdminMiddleware.d.ts +3 -0
  65. package/dist/core/http/requireWebAuthMiddleware.d.ts +4 -0
  66. package/dist/core/http/routeMiddleware.d.ts +3 -0
  67. package/dist/core/http/securityHeadersMiddleware.d.ts +3 -0
  68. package/dist/core/http/throttleMiddleware.d.ts +11 -0
  69. package/dist/core/logging/logger.d.ts +17 -0
  70. package/dist/core/logging/requestLoggingMiddleware.d.ts +3 -0
  71. package/dist/core/metrics/prometheus.d.ts +26 -0
  72. package/dist/core/pagination/index.d.ts +17 -0
  73. package/dist/core/queue/index.d.ts +19 -0
  74. package/dist/core/security/publicReads.d.ts +3 -0
  75. package/dist/core/security/securityEvents.d.ts +6 -0
  76. package/dist/core/security/tokenExpiry.d.ts +2 -0
  77. package/dist/core/security/totp.d.ts +3 -0
  78. package/dist/core/tenant/databaseTenantContext.d.ts +2 -0
  79. package/dist/core/tenant/resolveTenant.d.ts +3 -0
  80. package/dist/core/tenant/tenantContext.d.ts +14 -0
  81. package/dist/core/tenant/tenantDatabaseScope.d.ts +7 -0
  82. package/dist/core/tenant/tenantMiddleware.d.ts +6 -0
  83. package/dist/core/tracing/otel.d.ts +26 -0
  84. package/dist/core/tracing/traceContext.d.ts +10 -0
  85. package/dist/core/tracing/tracingMiddleware.d.ts +3 -0
  86. package/dist/db/connection/createConnection.d.ts +6 -0
  87. package/dist/db/connection/index.d.ts +11 -0
  88. package/dist/domain/abilities.d.ts +5 -0
  89. package/dist/domain/auth.d.ts +4 -0
  90. package/dist/index.js +1751 -0
  91. package/dist/modules/organization/memberRepository.d.ts +14 -0
  92. package/dist/modules/organization/memberTypes.d.ts +9 -0
  93. package/dist/modules/user/apiTokenRepository.d.ts +8 -0
  94. package/dist/modules/user/apiTokenTable.d.ts +3 -0
  95. package/dist/modules/user/authService.d.ts +23 -0
  96. package/dist/modules/user/notificationRepository.d.ts +6 -0
  97. package/dist/modules/user/notificationService.d.ts +25 -0
  98. package/dist/modules/user/notificationTable.d.ts +3 -0
  99. package/dist/modules/user/notificationTypes.d.ts +12 -0
  100. package/dist/modules/user/oauthIdentityRepository.d.ts +9 -0
  101. package/dist/modules/user/provider.d.ts +11 -0
  102. package/dist/modules/user/repository.d.ts +12 -0
  103. package/dist/modules/user/table.d.ts +3 -0
  104. package/dist/modules/user/tokenService.d.ts +29 -0
  105. package/dist/modules/user/types.d.ts +46 -0
  106. package/dist/types/services.d.ts +16 -0
  107. package/package.json +38 -0
package/dist/index.js ADDED
@@ -0,0 +1,1751 @@
1
+ // @bun
2
+ // ../../src/core/logging/logger.ts
3
+ class Logger {
4
+ channel;
5
+ constructor(channel = "app") {
6
+ this.channel = channel;
7
+ }
8
+ write(level, message, context = {}) {
9
+ const entry = {
10
+ level,
11
+ channel: this.channel,
12
+ message,
13
+ timestamp: new Date().toISOString(),
14
+ ...context
15
+ };
16
+ const line = JSON.stringify(entry);
17
+ if (level === "error") {
18
+ console.error(line);
19
+ return;
20
+ }
21
+ console.log(line);
22
+ }
23
+ debug(message, context) {
24
+ this.write("debug", message, context);
25
+ }
26
+ info(message, context) {
27
+ this.write("info", message, context);
28
+ }
29
+ warn(message, context) {
30
+ this.write("warn", message, context);
31
+ }
32
+ error(message, context) {
33
+ this.write("error", message, context);
34
+ }
35
+ }
36
+ var appLogger = new Logger("app");
37
+
38
+ // ../../src/bootstrap/config.ts
39
+ var APP_PORT_CONFIG_KEY = "app.port";
40
+ var REDIS_URL_CONFIG_KEY = "cache.redisUrl";
41
+ var DATABASE_URL_CONFIG_KEY = "database.url";
42
+ var CORE_CONFIG_TOKEN = "core.config";
43
+ var CORE_CACHE_TOKEN = "core.cache";
44
+ var CORE_QUEUE_TOKEN = "core.queue";
45
+ var CORE_POLICY_GATE_TOKEN = "core.policyGate";
46
+ var CORE_AUTH_TOKEN = "core.auth";
47
+ var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
48
+ var DEFAULT_APP_PORT = 3000;
49
+
50
+ // ../../src/bootstrap/contracts.ts
51
+ class ServiceContainer {
52
+ services = new Map;
53
+ singletonFactories = new Map;
54
+ bindings = new Map;
55
+ set(key, value) {
56
+ this.singletonFactories.delete(key);
57
+ this.bindings.delete(key);
58
+ this.services.set(key, value);
59
+ return value;
60
+ }
61
+ singleton(key, factory) {
62
+ this.bindings.delete(key);
63
+ this.services.delete(key);
64
+ this.singletonFactories.set(key, factory);
65
+ }
66
+ bind(key, factory) {
67
+ this.singletonFactories.delete(key);
68
+ this.services.delete(key);
69
+ this.bindings.set(key, factory);
70
+ }
71
+ get(key) {
72
+ if (this.services.has(key)) {
73
+ return this.services.get(key);
74
+ }
75
+ const singletonFactory = this.singletonFactories.get(key);
76
+ if (singletonFactory) {
77
+ const value = singletonFactory(this);
78
+ this.services.set(key, value);
79
+ return value;
80
+ }
81
+ const binding = this.bindings.get(key);
82
+ if (binding) {
83
+ return binding(this);
84
+ }
85
+ throw new Error(`Service "${key}" is not registered.`);
86
+ }
87
+ resolve(key) {
88
+ return this.get(key);
89
+ }
90
+ has(key) {
91
+ return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
92
+ }
93
+ }
94
+
95
+ class ConfigStore {
96
+ values = new Map;
97
+ set(key, value) {
98
+ this.values.set(key, value);
99
+ return value;
100
+ }
101
+ get(key) {
102
+ return this.values.get(key);
103
+ }
104
+ require(key) {
105
+ if (!this.values.has(key)) {
106
+ throw new Error(`Config key "${key}" is not defined.`);
107
+ }
108
+ return this.values.get(key);
109
+ }
110
+ has(key) {
111
+ return this.values.has(key);
112
+ }
113
+ }
114
+ var requiredDependencyKeys = [
115
+ "container",
116
+ "cache"
117
+ ];
118
+ function getRequiredDependency(dependencies, key) {
119
+ const dependency = dependencies[key];
120
+ if (dependency === undefined) {
121
+ throw new Error(`Required dependency "${key}" is not registered.`);
122
+ }
123
+ return dependency;
124
+ }
125
+ function assertAppDependenciesComplete(dependencies) {
126
+ for (const key of requiredDependencyKeys) {
127
+ getRequiredDependency(dependencies, key);
128
+ }
129
+ }
130
+ function resolveService(dependencies, token) {
131
+ return dependencies.container.resolve(token);
132
+ }
133
+
134
+ // ../../src/bootstrap/applicationRegistry.ts
135
+ var activeContext;
136
+ function setActiveApplicationContext(context) {
137
+ activeContext = context;
138
+ }
139
+ function requireActiveApplicationContext() {
140
+ if (!activeContext) {
141
+ throw new Error("The application context has not been bootstrapped.");
142
+ }
143
+ return activeContext;
144
+ }
145
+ function resolveApplicationQueue() {
146
+ return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
147
+ }
148
+ // ../../src/config/frontend.ts
149
+ function readFrontendMode() {
150
+ const mode = (process.env.FRONTEND_MODE ?? "api").trim();
151
+ if (mode === "server-htmx") {
152
+ return "server-htmx";
153
+ }
154
+ if (mode === "spa-react") {
155
+ return "spa-react";
156
+ }
157
+ return "api";
158
+ }
159
+ function isViewsEnabled() {
160
+ return readFrontendMode() === "server-htmx";
161
+ }
162
+
163
+ // ../../src/config/rateLimit.ts
164
+ var LOCAL_LOGIN_RATE_LIMIT = {
165
+ maxAttempts: 100,
166
+ decaySeconds: 60
167
+ };
168
+ var PRODUCTION_LOGIN_RATE_LIMIT = {
169
+ maxAttempts: 5,
170
+ decaySeconds: 900
171
+ };
172
+ function isLocalAppEnv() {
173
+ return (process.env.APP_ENV ?? "local") === "local";
174
+ }
175
+ function parsePositiveInt(value, fallback) {
176
+ const parsed = Number(value);
177
+ if (!Number.isFinite(parsed) || parsed <= 0) {
178
+ return fallback;
179
+ }
180
+ return Math.trunc(parsed);
181
+ }
182
+ function resolveLoginRateLimit() {
183
+ const defaults = isLocalAppEnv() ? LOCAL_LOGIN_RATE_LIMIT : PRODUCTION_LOGIN_RATE_LIMIT;
184
+ return {
185
+ maxAttempts: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
186
+ decaySeconds: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_WINDOW_SECONDS, defaults.decaySeconds)
187
+ };
188
+ }
189
+
190
+ // ../../src/core/auth/membershipContext.ts
191
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
192
+
193
+ // ../../src/config/database.ts
194
+ function readInteger(name, fallback) {
195
+ const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
196
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
197
+ }
198
+ var databaseConfig = {
199
+ url: process.env.DATABASE_URL ?? "",
200
+ poolMax: readInteger("DB_POOL_MAX", 10),
201
+ idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
202
+ maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
203
+ connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
204
+ };
205
+
206
+ // ../../src/core/database/connectionContext.ts
207
+ import { AsyncLocalStorage } from "async_hooks";
208
+ var activeConnection = new AsyncLocalStorage;
209
+ function runWithDatabaseConnection(connection, callback) {
210
+ return activeConnection.run(connection, callback);
211
+ }
212
+ function getActiveDatabaseConnection(fallback) {
213
+ return activeConnection.getStore() ?? fallback;
214
+ }
215
+
216
+ // ../../src/db/connection/createConnection.ts
217
+ var {SQL } = globalThis.Bun;
218
+ function createDatabaseConnection(config) {
219
+ if (!config.url) {
220
+ throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
221
+ }
222
+ return new SQL({
223
+ url: config.url,
224
+ max: config.poolMax,
225
+ idleTimeout: config.idleTimeoutSeconds,
226
+ maxLifetime: config.maxLifetimeSeconds,
227
+ connectionTimeout: config.connectionTimeoutSeconds
228
+ });
229
+ }
230
+
231
+ // ../../src/db/connection/index.ts
232
+ var connectionHolder = {
233
+ connection: null
234
+ };
235
+ function getDatabase() {
236
+ if (!connectionHolder.connection) {
237
+ connectionHolder.connection = createDatabaseConnection(databaseConfig);
238
+ }
239
+ return connectionHolder.connection;
240
+ }
241
+ var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
242
+ function resolveDatabase() {
243
+ return getActiveDatabaseConnection(getDatabase());
244
+ }
245
+ function resolveDatabaseForProperty(property) {
246
+ if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
247
+ return getDatabase();
248
+ }
249
+ return resolveDatabase();
250
+ }
251
+ var db = new Proxy(function database() {}, {
252
+ apply(_target, _thisArg, args) {
253
+ return resolveDatabase()(...args);
254
+ },
255
+ get(_target, property) {
256
+ const connection = resolveDatabaseForProperty(property);
257
+ const value = connection[property];
258
+ return typeof value === "function" ? value.bind(connection) : value;
259
+ }
260
+ });
261
+ var connection_default = db;
262
+
263
+ // ../../src/modules/organization/memberRepository.ts
264
+ class OrganizationMemberRepository {
265
+ constructor() {}
266
+ async findMembership(userId, organizationId) {
267
+ const rows = await connection_default`
268
+ SELECT id, organization_id, user_id, role, created_at
269
+ FROM organization_member
270
+ WHERE user_id = ${userId} AND organization_id = ${organizationId}
271
+ LIMIT 1
272
+ `;
273
+ return rows[0] ?? null;
274
+ }
275
+ async listForUser(userId) {
276
+ return await connection_default`
277
+ SELECT id, organization_id, user_id, role, created_at
278
+ FROM organization_member
279
+ WHERE user_id = ${userId}
280
+ ORDER BY organization_id
281
+ `;
282
+ }
283
+ async listForOrganization(organizationId) {
284
+ return await connection_default`
285
+ SELECT id, organization_id, user_id, role, created_at
286
+ FROM organization_member
287
+ WHERE organization_id = ${organizationId}
288
+ ORDER BY id
289
+ `;
290
+ }
291
+ async addMember(input) {
292
+ const rows = await connection_default`
293
+ INSERT INTO organization_member (organization_id, user_id, role)
294
+ VALUES (${input.organizationId}, ${input.userId}, ${input.role ?? "member"})
295
+ RETURNING id, organization_id, user_id, role, created_at
296
+ `;
297
+ const row = rows[0];
298
+ if (!row) {
299
+ throw new Error("Organization member insert did not return a row.");
300
+ }
301
+ return row;
302
+ }
303
+ async removeMember(organizationId, userId) {
304
+ const rows = await connection_default`
305
+ DELETE FROM organization_member
306
+ WHERE organization_id = ${organizationId} AND user_id = ${userId}
307
+ RETURNING id
308
+ `;
309
+ return rows.length > 0;
310
+ }
311
+ }
312
+ var memberRepository_default = OrganizationMemberRepository;
313
+
314
+ // ../../src/core/errors/http.ts
315
+ class HttpError extends Error {
316
+ status;
317
+ details;
318
+ constructor(status, message, details) {
319
+ super(message);
320
+ this.name = new.target.name;
321
+ this.status = status;
322
+ this.details = details;
323
+ }
324
+ }
325
+ class ForbiddenError extends HttpError {
326
+ constructor(message = "Forbidden", details) {
327
+ super(403, message, details);
328
+ }
329
+ }
330
+
331
+ class UnauthorizedError extends HttpError {
332
+ constructor(message = "Unauthorized", details) {
333
+ super(401, message, details);
334
+ }
335
+ }
336
+
337
+ class PayloadTooLargeError extends HttpError {
338
+ constructor(message = "Payload Too Large", details) {
339
+ super(413, message, details);
340
+ }
341
+ }
342
+
343
+ // ../../src/core/auth/authContext.ts
344
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
345
+ var authContext = new AsyncLocalStorage2;
346
+ function runWithAuthUser(user, callback) {
347
+ return authContext.run(user, callback);
348
+ }
349
+ function currentAuthUser() {
350
+ return authContext.getStore() ?? null;
351
+ }
352
+
353
+ // ../../src/core/auth/accessControl.ts
354
+ function isGlobalAdmin(user) {
355
+ return user?.role === "admin";
356
+ }
357
+ function resolveUserId(user) {
358
+ const userId = typeof user.id === "number" ? user.id : Number(user.id);
359
+ if (!Number.isInteger(userId) || userId <= 0) {
360
+ throw new ForbiddenError("Invalid authenticated user.");
361
+ }
362
+ return userId;
363
+ }
364
+
365
+ // ../../src/core/auth/membershipContext.ts
366
+ var membershipContext = new AsyncLocalStorage3;
367
+ var membershipRepository = new memberRepository_default;
368
+ async function runWithMembershipContext(callback) {
369
+ const user = currentAuthUser();
370
+ if (!user || isGlobalAdmin(user)) {
371
+ return await callback();
372
+ }
373
+ const memberships = await membershipRepository.listForUser(resolveUserId(user));
374
+ const context = {
375
+ organizationIds: memberships.map((membership) => membership.organization_id),
376
+ rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
377
+ };
378
+ return await membershipContext.run(context, callback);
379
+ }
380
+
381
+ // ../../src/core/auth/membershipContextMiddleware.ts
382
+ function createMembershipContextMiddleware() {
383
+ return async (_request, next) => {
384
+ return await runWithMembershipContext(async () => await next());
385
+ };
386
+ }
387
+
388
+ // ../../src/core/auth/membershipMiddleware.ts
389
+ function createMembershipMiddleware() {
390
+ return createMembershipContextMiddleware();
391
+ }
392
+
393
+ // ../../src/core/http/authMiddleware.ts
394
+ function createAuthMiddleware(auth) {
395
+ return async (request, next) => {
396
+ const user = await auth.resolve(request);
397
+ return await runWithAuthUser(user, async () => {
398
+ const response = await next();
399
+ if (user) {
400
+ const headers = new Headers(response.headers);
401
+ headers.set("x-authenticated-user-id", String(user.id));
402
+ return new Response(response.body, {
403
+ status: response.status,
404
+ statusText: response.statusText,
405
+ headers
406
+ });
407
+ }
408
+ return response;
409
+ });
410
+ };
411
+ }
412
+
413
+ // ../../src/core/http/authorizeMiddleware.ts
414
+ function createAuthorizeMiddleware(gate, auth, resource, action) {
415
+ return async (request, next) => {
416
+ const user = await auth.resolve(request);
417
+ if (!gate.allows(resource, action, user)) {
418
+ const error = new ForbiddenError;
419
+ return Response.json({ error: error.message }, { status: error.status });
420
+ }
421
+ return await next();
422
+ };
423
+ }
424
+
425
+ // ../../src/core/http/bodySizeLimitMiddleware.ts
426
+ var DEFAULT_MAX_BODY_BYTES = 1048576;
427
+ function resolveMaxBodyBytes() {
428
+ const raw = process.env.MAX_REQUEST_BODY_BYTES?.trim();
429
+ if (!raw) {
430
+ return DEFAULT_MAX_BODY_BYTES;
431
+ }
432
+ const parsed = Number.parseInt(raw, 10);
433
+ if (!Number.isInteger(parsed) || parsed <= 0) {
434
+ return DEFAULT_MAX_BODY_BYTES;
435
+ }
436
+ return parsed;
437
+ }
438
+ function createBodySizeLimitMiddleware(maxBytes = resolveMaxBodyBytes()) {
439
+ return async (request, next) => {
440
+ const contentLength = request.headers.get("content-length");
441
+ if (contentLength) {
442
+ const bytes = Number.parseInt(contentLength, 10);
443
+ if (Number.isInteger(bytes) && bytes > maxBytes) {
444
+ const error = new PayloadTooLargeError(`Request body exceeds the ${maxBytes} byte limit.`);
445
+ return Response.json({ error: error.message }, { status: error.status });
446
+ }
447
+ }
448
+ return await next();
449
+ };
450
+ }
451
+
452
+ // ../../src/config/cors.ts
453
+ var corsConfig = {
454
+ allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim()).filter(Boolean),
455
+ allowedMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
456
+ allowedHeaders: [
457
+ "Authorization",
458
+ "Content-Type",
459
+ "X-Request-Id",
460
+ "X-Tenant-Id",
461
+ "X-Authenticated-User-Id",
462
+ "X-Authenticated-User-Role",
463
+ "If-Match",
464
+ "If-None-Match"
465
+ ],
466
+ maxAgeSeconds: 86400
467
+ };
468
+
469
+ // ../../src/core/http/corsMiddleware.ts
470
+ function createCorsMiddleware() {
471
+ return async (request, next) => {
472
+ if (request.method === "OPTIONS") {
473
+ return new Response(null, {
474
+ status: 204,
475
+ headers: buildCorsHeaders(request)
476
+ });
477
+ }
478
+ const response = await next();
479
+ const headers = new Headers(response.headers);
480
+ for (const [key, value] of buildCorsHeaders(request)) {
481
+ headers.set(key, value);
482
+ }
483
+ return new Response(response.body, {
484
+ status: response.status,
485
+ statusText: response.statusText,
486
+ headers
487
+ });
488
+ };
489
+ }
490
+ function buildCorsHeaders(request) {
491
+ const headers = new Headers;
492
+ const origin = request.headers.get("origin");
493
+ const allowedOrigins = corsConfig.allowedOrigins;
494
+ const allowOrigin = allowedOrigins.includes("*") || origin && allowedOrigins.includes(origin) ? origin ?? "*" : allowedOrigins[0] ?? "*";
495
+ headers.set("Access-Control-Allow-Origin", allowOrigin);
496
+ headers.set("Access-Control-Allow-Methods", corsConfig.allowedMethods.join(", "));
497
+ headers.set("Access-Control-Allow-Headers", corsConfig.allowedHeaders.join(", "));
498
+ headers.set("Access-Control-Max-Age", String(corsConfig.maxAgeSeconds));
499
+ headers.set("Vary", "Origin");
500
+ return headers;
501
+ }
502
+
503
+ // ../../src/core/http/csrfToken.ts
504
+ import { createHmac, randomBytes, timingSafeEqual } from "crypto";
505
+
506
+ // ../../src/core/http/requestMetaContext.ts
507
+ import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
508
+ var requestMetaContext = new AsyncLocalStorage4;
509
+ function runWithRequestMeta(meta, callback) {
510
+ return requestMetaContext.run(meta, callback);
511
+ }
512
+ function currentRequestMeta() {
513
+ return requestMetaContext.getStore() ?? {
514
+ ipAddress: null,
515
+ userAgent: null
516
+ };
517
+ }
518
+
519
+ // ../../src/core/http/csrfToken.ts
520
+ var CSRF_COOKIE = "workhub_csrf";
521
+ var CSRF_TTL_MS = 60 * 60 * 1000;
522
+ function resolveCsrfSecret() {
523
+ return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
524
+ }
525
+ function signCsrfToken(token, issuedAt) {
526
+ const payload = `${token}.${issuedAt}`;
527
+ const signature = createHmac("sha256", resolveCsrfSecret()).update(payload).digest("hex");
528
+ return `${payload}.${signature}`;
529
+ }
530
+ function readCsrfCookie(request) {
531
+ const cookieHeader = request.headers.get("cookie");
532
+ if (!cookieHeader) {
533
+ return null;
534
+ }
535
+ for (const part of cookieHeader.split(";")) {
536
+ const [name, ...rest] = part.trim().split("=");
537
+ if (name === CSRF_COOKIE) {
538
+ return decodeURIComponent(rest.join("="));
539
+ }
540
+ }
541
+ return null;
542
+ }
543
+ function parseSignedCsrfValue(cookieValue) {
544
+ const parts = cookieValue.split(".");
545
+ if (parts.length !== 3) {
546
+ return null;
547
+ }
548
+ const [token, issuedAtRaw, cookieSignature] = parts;
549
+ if (!token || !issuedAtRaw || !cookieSignature) {
550
+ return null;
551
+ }
552
+ const issuedAt = Number.parseInt(issuedAtRaw, 10);
553
+ if (!Number.isFinite(issuedAt)) {
554
+ return null;
555
+ }
556
+ if (Date.now() - issuedAt > CSRF_TTL_MS) {
557
+ return null;
558
+ }
559
+ const expectedSignature = signCsrfToken(token, issuedAt).split(".").pop();
560
+ if (!expectedSignature) {
561
+ return null;
562
+ }
563
+ const expectedBuffer = Buffer.from(expectedSignature);
564
+ const actualBuffer = Buffer.from(cookieSignature);
565
+ if (expectedBuffer.length !== actualBuffer.length) {
566
+ return null;
567
+ }
568
+ if (!timingSafeEqual(expectedBuffer, actualBuffer)) {
569
+ return null;
570
+ }
571
+ return { token, issuedAt };
572
+ }
573
+ function createCsrfTokenCookie() {
574
+ const token = randomBytes(24).toString("hex");
575
+ const issuedAt = Date.now();
576
+ const value = signCsrfToken(token, issuedAt);
577
+ return {
578
+ token,
579
+ cookie: `${CSRF_COOKIE}=${encodeURIComponent(value)}; Path=/; SameSite=Lax; Max-Age=3600`
580
+ };
581
+ }
582
+ function resolveCsrfToken(request) {
583
+ const cookieValue = readCsrfCookie(request);
584
+ if (cookieValue) {
585
+ const parsed = parseSignedCsrfValue(cookieValue);
586
+ if (parsed) {
587
+ return { token: parsed.token };
588
+ }
589
+ }
590
+ return createCsrfTokenCookie();
591
+ }
592
+ function readSubmittedCsrfToken(request) {
593
+ const headerToken = request.headers.get("x-csrf-token")?.trim();
594
+ if (headerToken) {
595
+ return headerToken;
596
+ }
597
+ return null;
598
+ }
599
+ async function readSubmittedCsrfTokenFromBody(request) {
600
+ const headerToken = readSubmittedCsrfToken(request);
601
+ if (headerToken) {
602
+ return headerToken;
603
+ }
604
+ const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
605
+ if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
606
+ const formData = await request.clone().formData();
607
+ const field = formData.get("_token");
608
+ if (typeof field === "string" && field.trim().length > 0) {
609
+ return field.trim();
610
+ }
611
+ }
612
+ return null;
613
+ }
614
+ function verifyCsrfToken(request, submittedToken) {
615
+ if (!submittedToken) {
616
+ return false;
617
+ }
618
+ const cookieValue = readCsrfCookie(request);
619
+ if (!cookieValue) {
620
+ return false;
621
+ }
622
+ const parsed = parseSignedCsrfValue(cookieValue);
623
+ if (!parsed) {
624
+ return false;
625
+ }
626
+ const submittedBuffer = Buffer.from(submittedToken);
627
+ const expectedBuffer = Buffer.from(parsed.token);
628
+ if (submittedBuffer.length !== expectedBuffer.length) {
629
+ return false;
630
+ }
631
+ return timingSafeEqual(submittedBuffer, expectedBuffer);
632
+ }
633
+
634
+ // ../../src/core/http/csrfMiddleware.ts
635
+ var MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
636
+ function appendSetCookie(response, cookie) {
637
+ const headers = new Headers(response.headers);
638
+ headers.append("set-cookie", cookie);
639
+ return new Response(response.body, {
640
+ status: response.status,
641
+ statusText: response.statusText,
642
+ headers
643
+ });
644
+ }
645
+ function createCsrfMiddleware() {
646
+ return async (request, next) => {
647
+ const method = request.method.toUpperCase();
648
+ if (!MUTATING_METHODS.has(method)) {
649
+ const csrf = resolveCsrfToken(request);
650
+ const meta = currentRequestMeta();
651
+ meta.csrfToken = csrf.token;
652
+ const response = await next();
653
+ if (!csrf.cookie) {
654
+ return response;
655
+ }
656
+ return appendSetCookie(response, csrf.cookie);
657
+ }
658
+ const submitted = await readSubmittedCsrfTokenFromBody(request);
659
+ if (!verifyCsrfToken(request, submitted)) {
660
+ throw new ForbiddenError("Invalid or missing CSRF token.");
661
+ }
662
+ return await next();
663
+ };
664
+ }
665
+
666
+ // ../../src/core/http/flashSession.ts
667
+ import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
668
+ var FLASH_COOKIE = "workhub_flash";
669
+ var FLASH_TTL_MS = 60 * 1000;
670
+ function resolveFlashSecret() {
671
+ return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
672
+ }
673
+ function signFlashPayload(payload, issuedAt) {
674
+ const signature = createHmac2("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
675
+ return `${payload}.${issuedAt}.${signature}`;
676
+ }
677
+ function readFlashCookie(request) {
678
+ const cookieHeader = request.headers.get("cookie");
679
+ if (!cookieHeader) {
680
+ return null;
681
+ }
682
+ for (const part of cookieHeader.split(";")) {
683
+ const [name, ...rest] = part.trim().split("=");
684
+ if (name === FLASH_COOKIE) {
685
+ return decodeURIComponent(rest.join("="));
686
+ }
687
+ }
688
+ return null;
689
+ }
690
+ function parseFlashCookie(cookieValue) {
691
+ const parts = cookieValue.split(".");
692
+ if (parts.length < 3) {
693
+ return null;
694
+ }
695
+ const signature = parts.pop();
696
+ const issuedAtRaw = parts.pop();
697
+ const payload = parts.join(".");
698
+ if (!signature || !issuedAtRaw || !payload) {
699
+ return null;
700
+ }
701
+ const issuedAt = Number.parseInt(issuedAtRaw, 10);
702
+ if (!Number.isFinite(issuedAt) || Date.now() - issuedAt > FLASH_TTL_MS) {
703
+ return null;
704
+ }
705
+ const expectedSignature = signFlashPayload(payload, issuedAt).split(".").pop();
706
+ if (!expectedSignature) {
707
+ return null;
708
+ }
709
+ const expectedBuffer = Buffer.from(expectedSignature);
710
+ const actualBuffer = Buffer.from(signature);
711
+ if (expectedBuffer.length !== actualBuffer.length) {
712
+ return null;
713
+ }
714
+ if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
715
+ return null;
716
+ }
717
+ try {
718
+ const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
719
+ if (!parsed?.message || typeof parsed.message !== "string") {
720
+ return null;
721
+ }
722
+ if (parsed.level !== "success" && parsed.level !== "error" && parsed.level !== "info") {
723
+ return null;
724
+ }
725
+ return parsed;
726
+ } catch {
727
+ return null;
728
+ }
729
+ }
730
+ function clearFlashCookie() {
731
+ return `${FLASH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
732
+ }
733
+ function pullFlash(request) {
734
+ const cookieValue = readFlashCookie(request);
735
+ if (!cookieValue) {
736
+ return null;
737
+ }
738
+ return parseFlashCookie(cookieValue);
739
+ }
740
+ function withFlashClear(response) {
741
+ const headers = new Headers(response.headers);
742
+ headers.append("set-cookie", clearFlashCookie());
743
+ return new Response(response.body, {
744
+ status: response.status,
745
+ statusText: response.statusText,
746
+ headers
747
+ });
748
+ }
749
+
750
+ // ../../src/core/http/flashMiddleware.ts
751
+ function createFlashMiddleware() {
752
+ return async (request, next) => {
753
+ const flash = pullFlash(request);
754
+ const meta = currentRequestMeta();
755
+ return await runWithRequestMeta({ ...meta, request, flash }, async () => {
756
+ const response = await next();
757
+ if (flash) {
758
+ return withFlashClear(response);
759
+ }
760
+ return response;
761
+ });
762
+ };
763
+ }
764
+
765
+ // ../../src/core/http/loginThrottleMiddleware.ts
766
+ var {RedisClient } = globalThis.Bun;
767
+ function resolveLoginIdentity(request) {
768
+ return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
769
+ }
770
+ async function resolveLoginEmail(request) {
771
+ const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
772
+ try {
773
+ if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
774
+ const formData = await request.clone().formData();
775
+ const email = formData.get("email");
776
+ return typeof email === "string" ? email.trim().toLowerCase() : "unknown";
777
+ }
778
+ const payload = await request.clone().json();
779
+ return typeof payload.email === "string" ? payload.email.trim().toLowerCase() : "unknown";
780
+ } catch {
781
+ return "unknown";
782
+ }
783
+ }
784
+ function createLoginThrottleMiddleware(options) {
785
+ const client = new RedisClient(options.redisUrl);
786
+ const prefix = options.keyPrefix ?? "workhub:login-throttle:";
787
+ return async (request, next) => {
788
+ const identity = resolveLoginIdentity(request);
789
+ const email = await resolveLoginEmail(request);
790
+ const throttleKey = `${prefix}${identity}:${email}`;
791
+ const attempts = Number(await client.incr(throttleKey));
792
+ if (attempts === 1) {
793
+ await client.expire(throttleKey, options.decaySeconds);
794
+ }
795
+ if (attempts > options.maxAttempts) {
796
+ return Response.json({ error: "Too many login attempts. Try again later." }, {
797
+ status: 429,
798
+ headers: {
799
+ "retry-after": String(options.decaySeconds)
800
+ }
801
+ });
802
+ }
803
+ return await next();
804
+ };
805
+ }
806
+
807
+ // ../../src/core/http/memoryThrottleMiddleware.ts
808
+ var buckets = new Map;
809
+ function createMemoryThrottleMiddleware(options) {
810
+ const prefix = options.keyPrefix ?? "workhub:memory-throttle:";
811
+ return async (request, next) => {
812
+ const path = new URL(request.url).pathname;
813
+ const identity = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("authorization")?.slice(0, 32) ?? "unknown";
814
+ const key = `${prefix}${identity}:${path}`;
815
+ const now = Date.now();
816
+ const existing = buckets.get(key);
817
+ if (!existing || existing.resetAt <= now) {
818
+ buckets.set(key, { count: 1, resetAt: now + options.decaySeconds * 1000 });
819
+ return await next();
820
+ }
821
+ existing.count += 1;
822
+ if (existing.count > options.maxAttempts) {
823
+ return Response.json({ error: "Too many requests." }, {
824
+ status: 429,
825
+ headers: {
826
+ "retry-after": String(options.decaySeconds)
827
+ }
828
+ });
829
+ }
830
+ return await next();
831
+ };
832
+ }
833
+
834
+ // ../../src/core/metrics/prometheus.ts
835
+ class PrometheusRegistry {
836
+ httpRequestsTotal = new Map;
837
+ httpRequestDurationMs = new Map;
838
+ incrementHttpRequest(labels) {
839
+ const key = this.metricKey(labels);
840
+ this.httpRequestsTotal.set(key, (this.httpRequestsTotal.get(key) ?? 0) + 1);
841
+ }
842
+ observeHttpDuration(labels, durationMs) {
843
+ const key = this.metricKey(labels);
844
+ const samples = this.httpRequestDurationMs.get(key) ?? [];
845
+ samples.push(durationMs);
846
+ this.httpRequestDurationMs.set(key, samples);
847
+ }
848
+ renderMetrics() {
849
+ const lines = [
850
+ "# HELP http_requests_total Total HTTP requests processed.",
851
+ "# TYPE http_requests_total counter"
852
+ ];
853
+ for (const [key, value] of this.httpRequestsTotal) {
854
+ lines.push(`http_requests_total{${key}} ${value}`);
855
+ }
856
+ lines.push("# HELP http_request_duration_ms_sum Sum of HTTP request durations in milliseconds.", "# TYPE http_request_duration_ms_sum counter");
857
+ for (const [key, samples] of this.httpRequestDurationMs) {
858
+ const sum = samples.reduce((total, sample) => total + sample, 0);
859
+ lines.push(`http_request_duration_ms_sum{${key}} ${sum}`);
860
+ }
861
+ return `${lines.join(`
862
+ `)}
863
+ `;
864
+ }
865
+ resetForTests() {
866
+ this.httpRequestsTotal.clear();
867
+ this.httpRequestDurationMs.clear();
868
+ }
869
+ getHttpRequestSummary() {
870
+ const byStatus = {};
871
+ const pathCounts = new Map;
872
+ let totalRequests = 0;
873
+ for (const [key, count] of this.httpRequestsTotal) {
874
+ totalRequests += count;
875
+ const method = key.match(/method="([^"]+)"/)?.[1] ?? "GET";
876
+ const path = key.match(/path="([^"]+)"/)?.[1] ?? "/";
877
+ const status = key.match(/status="([^"]+)"/)?.[1] ?? "200";
878
+ byStatus[status] = (byStatus[status] ?? 0) + count;
879
+ const pathKey = `${method} ${path}`;
880
+ const existing = pathCounts.get(pathKey);
881
+ if (existing) {
882
+ existing.count += count;
883
+ } else {
884
+ pathCounts.set(pathKey, { method, path, count });
885
+ }
886
+ }
887
+ const topPaths = Array.from(pathCounts.values()).sort((left, right) => right.count - left.count).slice(0, 10);
888
+ return {
889
+ totalRequests,
890
+ byStatus,
891
+ topPaths
892
+ };
893
+ }
894
+ metricKey(labels) {
895
+ return `method="${labels.method}",path="${labels.path}",status="${labels.status}"`;
896
+ }
897
+ }
898
+ var prometheusRegistry = new PrometheusRegistry;
899
+
900
+ // ../../src/core/http/metricsMiddleware.ts
901
+ function normalizeMetricPath(pathname) {
902
+ return pathname.replace(/\/\d+/g, "/:id").replace(/\/[0-9a-f-]{36}/gi, "/:id");
903
+ }
904
+ function createMetricsMiddleware() {
905
+ return async (request, next) => {
906
+ const startedAt = performance.now();
907
+ const response = await next();
908
+ const durationMs = performance.now() - startedAt;
909
+ const path = normalizeMetricPath(new URL(request.url).pathname);
910
+ const labels = {
911
+ method: request.method,
912
+ path,
913
+ status: String(response.status)
914
+ };
915
+ prometheusRegistry.incrementHttpRequest(labels);
916
+ prometheusRegistry.observeHttpDuration(labels, durationMs);
917
+ return response;
918
+ };
919
+ }
920
+
921
+ // ../../src/core/http/middleware.ts
922
+ function composeMiddleware(...middleware) {
923
+ return (handler) => {
924
+ return async (request) => {
925
+ let index = 0;
926
+ const dispatch = async () => {
927
+ if (index >= middleware.length) {
928
+ return await handler(request);
929
+ }
930
+ const current = middleware[index];
931
+ index += 1;
932
+ if (!current) {
933
+ return await handler(request);
934
+ }
935
+ return await current(request, dispatch);
936
+ };
937
+ return await dispatch();
938
+ };
939
+ };
940
+ }
941
+ async function requestIdMiddleware(request, next) {
942
+ const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
943
+ const response = await next();
944
+ const headers = new Headers(response.headers);
945
+ headers.set("x-request-id", requestId);
946
+ return new Response(response.body, {
947
+ status: response.status,
948
+ statusText: response.statusText,
949
+ headers
950
+ });
951
+ }
952
+
953
+ // ../../src/core/http/requireAbilityMiddleware.ts
954
+ function createRequireAbilityMiddleware(tokenService) {
955
+ return (ability) => {
956
+ return async (_request, next) => {
957
+ const user = currentAuthUser();
958
+ try {
959
+ tokenService.requireAbility(user, ability);
960
+ } catch (error) {
961
+ if (error instanceof ForbiddenError) {
962
+ return Response.json({ error: error.message }, { status: error.status });
963
+ }
964
+ throw error;
965
+ }
966
+ return await next();
967
+ };
968
+ };
969
+ }
970
+
971
+ // ../../src/core/http/requireAuthMiddleware.ts
972
+ function createRequireAuthMiddleware(auth) {
973
+ return async (request, next) => {
974
+ if (!await auth.check(request)) {
975
+ const error = new UnauthorizedError;
976
+ return Response.json({ error: error.message }, { status: error.status });
977
+ }
978
+ return await next();
979
+ };
980
+ }
981
+
982
+ // ../../src/core/security/securityEvents.ts
983
+ function logSecurityEvent(event, details = {}) {
984
+ const meta = currentRequestMeta();
985
+ const user = currentAuthUser();
986
+ console.log(JSON.stringify({
987
+ level: "security",
988
+ event,
989
+ timestamp: new Date().toISOString(),
990
+ ip_address: meta.ipAddress ?? null,
991
+ user_agent: meta.userAgent ?? null,
992
+ user_id: user?.id ?? null,
993
+ ...details
994
+ }));
995
+ }
996
+
997
+ // ../../src/core/http/requireGlobalAdminMiddleware.ts
998
+ function createRequireGlobalAdminMiddleware() {
999
+ return async (_request, next) => {
1000
+ const user = currentAuthUser();
1001
+ if (!isGlobalAdmin(user)) {
1002
+ logSecurityEvent("privilege_escalation_blocked", {
1003
+ required_role: "platform_admin",
1004
+ path: new URL(_request.url).pathname
1005
+ });
1006
+ const error = new ForbiddenError("Platform admin access required.");
1007
+ return Response.json({ error: error.message }, { status: error.status });
1008
+ }
1009
+ return await next();
1010
+ };
1011
+ }
1012
+
1013
+ // ../../src/core/http/contentNegotiation.ts
1014
+ function requestPrefersJson(request) {
1015
+ if (!request) {
1016
+ return true;
1017
+ }
1018
+ if (request.headers.get("HX-Request") === "true") {
1019
+ return false;
1020
+ }
1021
+ const accept = request.headers.get("accept")?.toLowerCase() ?? "";
1022
+ if (accept.includes("text/html")) {
1023
+ return false;
1024
+ }
1025
+ if (accept.includes("application/json")) {
1026
+ return true;
1027
+ }
1028
+ const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
1029
+ if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
1030
+ return false;
1031
+ }
1032
+ const pathname = new URL(request.url).pathname;
1033
+ return pathname.startsWith("/api/");
1034
+ }
1035
+
1036
+ // ../../src/core/http/requireWebAuthMiddleware.ts
1037
+ function createRequireWebAuthMiddleware(auth) {
1038
+ return async (request, next) => {
1039
+ const user = await auth.resolve(request);
1040
+ if (user) {
1041
+ return await next();
1042
+ }
1043
+ if (requestPrefersJson(request)) {
1044
+ throw new UnauthorizedError;
1045
+ }
1046
+ const redirectTarget = encodeURIComponent(new URL(request.url).pathname);
1047
+ return Response.redirect(`/login?redirect=${redirectTarget}`, 302);
1048
+ };
1049
+ }
1050
+
1051
+ // ../../src/core/http/routeMiddleware.ts
1052
+ function withMiddleware(...middleware) {
1053
+ const wrap = composeMiddleware(...middleware);
1054
+ return (handler) => {
1055
+ return wrap(handler);
1056
+ };
1057
+ }
1058
+
1059
+ // ../../src/config/app.ts
1060
+ var appConfig = {
1061
+ name: "WorkHub",
1062
+ env: process.env.APP_ENV ?? "local",
1063
+ debug: (process.env.APP_DEBUG ?? "true") !== "false",
1064
+ url: process.env.APP_URL ?? "http://localhost:3000",
1065
+ apiPrefix: process.env.API_PREFIX ?? "/api/v1"
1066
+ };
1067
+
1068
+ // ../../src/config/contentSecurityPolicy.ts
1069
+ function strictApiContentSecurityPolicy() {
1070
+ return "default-src 'none'; frame-ancestors 'none'; base-uri 'none'";
1071
+ }
1072
+ function serverHtmxContentSecurityPolicy() {
1073
+ return [
1074
+ "default-src 'self'",
1075
+ "script-src 'self' https://unpkg.com",
1076
+ "style-src 'self'",
1077
+ "connect-src 'self'",
1078
+ "img-src 'self'",
1079
+ "font-src 'self'",
1080
+ "form-action 'self'",
1081
+ "frame-ancestors 'none'",
1082
+ "base-uri 'self'"
1083
+ ].join("; ");
1084
+ }
1085
+ function spaContentSecurityPolicy() {
1086
+ return [
1087
+ "default-src 'self'",
1088
+ "script-src 'self'",
1089
+ "style-src 'self'",
1090
+ "connect-src 'self'",
1091
+ "img-src 'self'",
1092
+ "font-src 'self'",
1093
+ "frame-ancestors 'none'",
1094
+ "base-uri 'self'"
1095
+ ].join("; ");
1096
+ }
1097
+ function resolveContentSecurityPolicy(response) {
1098
+ const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
1099
+ if (!contentType.includes("text/html")) {
1100
+ return strictApiContentSecurityPolicy();
1101
+ }
1102
+ const frontendMode = (process.env.FRONTEND_MODE ?? "api").trim();
1103
+ if (frontendMode === "server-htmx") {
1104
+ return serverHtmxContentSecurityPolicy();
1105
+ }
1106
+ if (frontendMode === "spa-react") {
1107
+ return spaContentSecurityPolicy();
1108
+ }
1109
+ return strictApiContentSecurityPolicy();
1110
+ }
1111
+
1112
+ // ../../src/core/http/securityHeadersMiddleware.ts
1113
+ function createSecurityHeadersMiddleware() {
1114
+ return async (_request, next) => {
1115
+ const response = await next();
1116
+ const headers = new Headers(response.headers);
1117
+ headers.set("X-Content-Type-Options", "nosniff");
1118
+ headers.set("X-Frame-Options", "DENY");
1119
+ headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
1120
+ headers.set("X-XSS-Protection", "0");
1121
+ headers.set("Content-Security-Policy", resolveContentSecurityPolicy(response));
1122
+ if (appConfig.env === "production") {
1123
+ headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
1124
+ }
1125
+ return new Response(response.body, {
1126
+ status: response.status,
1127
+ statusText: response.statusText,
1128
+ headers
1129
+ });
1130
+ };
1131
+ }
1132
+
1133
+ // ../../src/core/http/throttleMiddleware.ts
1134
+ var {RedisClient: RedisClient2 } = globalThis.Bun;
1135
+
1136
+ // ../../src/core/tenant/tenantContext.ts
1137
+ import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
1138
+ var tenantContext = new AsyncLocalStorage5;
1139
+ function runWithTenant(tenant, callback) {
1140
+ return tenantContext.run(tenant, callback);
1141
+ }
1142
+ function currentTenant() {
1143
+ return tenantContext.getStore() ?? null;
1144
+ }
1145
+ function rateLimitMultiplierForPlan(plan) {
1146
+ switch (plan) {
1147
+ case "enterprise":
1148
+ return 4;
1149
+ case "pro":
1150
+ return 2;
1151
+ default:
1152
+ return 1;
1153
+ }
1154
+ }
1155
+
1156
+ // ../../src/core/http/throttleMiddleware.ts
1157
+ function resolveThrottleIdentity(request) {
1158
+ const user = currentAuthUser();
1159
+ if (user?.tokenId !== undefined) {
1160
+ return `token:${user.tokenId}`;
1161
+ }
1162
+ if (user) {
1163
+ return `user:${user.id}`;
1164
+ }
1165
+ return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
1166
+ }
1167
+ function createThrottleMiddleware(options) {
1168
+ const client = new RedisClient2(options.redisUrl);
1169
+ const prefix = options.keyPrefix ?? "workhub:throttle:";
1170
+ return async (request, next) => {
1171
+ const identity = resolveThrottleIdentity(request);
1172
+ const path = new URL(request.url).pathname;
1173
+ const throttleKey = `${prefix}${identity}:${path}`;
1174
+ const attempts = Number(await client.incr(throttleKey));
1175
+ if (attempts === 1) {
1176
+ await client.expire(throttleKey, options.decaySeconds);
1177
+ }
1178
+ const maxAttempts = options.maxAttempts * rateLimitMultiplierForPlan(currentTenant()?.plan ?? "free");
1179
+ if (attempts > maxAttempts) {
1180
+ return Response.json({ error: "Too many requests." }, {
1181
+ status: 429,
1182
+ headers: {
1183
+ "retry-after": String(options.decaySeconds)
1184
+ }
1185
+ });
1186
+ }
1187
+ return await next();
1188
+ };
1189
+ }
1190
+
1191
+ // ../../src/core/logging/requestLoggingMiddleware.ts
1192
+ function createRequestLoggingMiddleware() {
1193
+ return async (request, next) => {
1194
+ return await runWithRequestMeta({
1195
+ ipAddress: request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip"),
1196
+ userAgent: request.headers.get("user-agent"),
1197
+ request
1198
+ }, async () => {
1199
+ const startedAt = performance.now();
1200
+ const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
1201
+ const response = await next();
1202
+ const durationMs = Math.round(performance.now() - startedAt);
1203
+ appLogger.info("HTTP request completed", {
1204
+ requestId,
1205
+ method: request.method,
1206
+ path: new URL(request.url).pathname,
1207
+ status: response.status,
1208
+ durationMs
1209
+ });
1210
+ return response;
1211
+ });
1212
+ };
1213
+ }
1214
+
1215
+ // ../../src/config/features.ts
1216
+ function readFeatureFlags() {
1217
+ return {
1218
+ webhooks: (process.env.FEATURE_WEBHOOKS ?? "true") !== "false",
1219
+ fullTextSearch: (process.env.FEATURE_SEARCH ?? "true") !== "false",
1220
+ auditLog: (process.env.FEATURE_AUDIT_LOG ?? "true") !== "false",
1221
+ oauthLogin: (process.env.FEATURE_OAUTH ?? "true") !== "false",
1222
+ samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
1223
+ scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
1224
+ billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
1225
+ siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
1226
+ publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
1227
+ emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
1228
+ mfa: (process.env.FEATURE_MFA ?? "false") === "true"
1229
+ };
1230
+ }
1231
+ var featureFlags = readFeatureFlags();
1232
+ function isFeatureEnabled(feature) {
1233
+ return readFeatureFlags()[feature];
1234
+ }
1235
+
1236
+ // ../../src/core/security/publicReads.ts
1237
+ function isPublicReadsEnabled() {
1238
+ return isFeatureEnabled("publicReads");
1239
+ }
1240
+
1241
+ // ../../src/core/tenant/databaseTenantContext.ts
1242
+ async function runWithMigrationBypass(callback) {
1243
+ await connection_default`SELECT set_config('app.bypass_rls', 'true', false)`;
1244
+ try {
1245
+ return await callback();
1246
+ } finally {
1247
+ await connection_default`SELECT set_config('app.bypass_rls', 'false', false)`;
1248
+ }
1249
+ }
1250
+
1251
+ // ../../src/core/tenant/resolveTenant.ts
1252
+ async function resolveTenant(tenantId) {
1253
+ const rows = await connection_default`
1254
+ SELECT id, slug, plan, region
1255
+ FROM tenant
1256
+ WHERE id = ${tenantId}
1257
+ LIMIT 1
1258
+ `;
1259
+ const row = rows[0];
1260
+ return row ? { id: row.id, slug: row.slug, plan: row.plan, region: row.region ?? "eu" } : null;
1261
+ }
1262
+
1263
+ // ../../src/core/tenant/tenantDatabaseScope.ts
1264
+ async function applyTenantContextToTransaction(transaction, tenantId) {
1265
+ await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
1266
+ await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
1267
+ }
1268
+ async function runWithTenantDatabase(tenant, callback) {
1269
+ return await getDatabase().begin(async (transaction) => {
1270
+ await applyTenantContextToTransaction(transaction, tenant.id);
1271
+ return await runWithDatabaseConnection(transaction, async () => {
1272
+ return await runWithTenant(tenant, callback);
1273
+ });
1274
+ });
1275
+ }
1276
+
1277
+ // ../../src/core/tenant/tenantMiddleware.ts
1278
+ var DEFAULT_TENANT = {
1279
+ id: 1,
1280
+ slug: "default",
1281
+ plan: "enterprise",
1282
+ region: "eu"
1283
+ };
1284
+ async function resolveUserTenantId(userId) {
1285
+ return await runWithMigrationBypass(async () => {
1286
+ const rows = await connection_default`
1287
+ SELECT tenant_id
1288
+ FROM users
1289
+ WHERE id = ${userId}
1290
+ LIMIT 1
1291
+ `;
1292
+ return rows[0]?.tenant_id ?? DEFAULT_TENANT.id;
1293
+ });
1294
+ }
1295
+ async function resolveTenantForRequest(request) {
1296
+ const user = currentAuthUser();
1297
+ const headerValue = request.headers.get("x-tenant-id")?.trim();
1298
+ const parsedHeader = headerValue !== undefined && headerValue.length > 0 ? Number.parseInt(headerValue, 10) : Number.NaN;
1299
+ if (user) {
1300
+ const userId = typeof user.id === "number" ? user.id : Number.parseInt(String(user.id), 10);
1301
+ if (Number.isInteger(userId) && userId > 0) {
1302
+ const userTenantId = await resolveUserTenantId(userId);
1303
+ if (!isGlobalAdmin(user)) {
1304
+ if (Number.isInteger(parsedHeader) && parsedHeader > 0 && parsedHeader !== userTenantId) {
1305
+ throw new ForbiddenError("Tenant header does not match your account.");
1306
+ }
1307
+ return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
1308
+ }
1309
+ if (Number.isInteger(parsedHeader) && parsedHeader > 0) {
1310
+ return await resolveTenant(parsedHeader) ?? DEFAULT_TENANT;
1311
+ }
1312
+ return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
1313
+ }
1314
+ }
1315
+ const tenantId = Number.isInteger(parsedHeader) && parsedHeader > 0 ? parsedHeader : DEFAULT_TENANT.id;
1316
+ return await resolveTenant(tenantId) ?? DEFAULT_TENANT;
1317
+ }
1318
+ function createTenantMiddleware() {
1319
+ return async (request, next) => {
1320
+ try {
1321
+ const tenant = await resolveTenantForRequest(request);
1322
+ return await runWithTenantDatabase(tenant, async () => {
1323
+ const response = await next();
1324
+ const headers = new Headers(response.headers);
1325
+ headers.set("x-tenant-id", String(tenant.id));
1326
+ headers.set("x-tenant-region", tenant.region);
1327
+ return new Response(response.body, {
1328
+ status: response.status,
1329
+ statusText: response.statusText,
1330
+ headers
1331
+ });
1332
+ });
1333
+ } catch (error) {
1334
+ if (error instanceof HttpError) {
1335
+ return Response.json({ error: error.message }, { status: error.status });
1336
+ }
1337
+ throw error;
1338
+ }
1339
+ };
1340
+ }
1341
+
1342
+ // ../../src/core/tracing/otel.ts
1343
+ import { randomBytes as randomBytes2 } from "crypto";
1344
+ function randomHex(bytes) {
1345
+ return randomBytes2(bytes).toString("hex");
1346
+ }
1347
+ function createSpan(input) {
1348
+ const spanId = randomHex(8);
1349
+ return {
1350
+ traceId: input.traceId,
1351
+ spanId,
1352
+ name: input.name,
1353
+ startTimeUnixNano: String(Math.floor(input.startedAt * 1e6)),
1354
+ endTimeUnixNano: String(Math.floor(input.endedAt * 1e6)),
1355
+ attributes: Object.entries(input.attributes ?? {}).map(([key, value]) => ({
1356
+ key,
1357
+ value: { stringValue: value }
1358
+ })),
1359
+ status: { code: 1 }
1360
+ };
1361
+ }
1362
+ async function exportOtelSpan(span) {
1363
+ const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
1364
+ if (!endpoint) {
1365
+ return;
1366
+ }
1367
+ const serviceName = process.env.OTEL_SERVICE_NAME?.trim() ?? "workhub-api";
1368
+ const url = endpoint.endsWith("/v1/traces") ? endpoint : `${endpoint.replace(/\/$/, "")}/v1/traces`;
1369
+ await fetch(url, {
1370
+ method: "POST",
1371
+ headers: { "content-type": "application/json" },
1372
+ body: JSON.stringify({
1373
+ resourceSpans: [
1374
+ {
1375
+ resource: {
1376
+ attributes: [{ key: "service.name", value: { stringValue: serviceName } }]
1377
+ },
1378
+ scopeSpans: [{ spans: [span] }]
1379
+ }
1380
+ ]
1381
+ })
1382
+ });
1383
+ }
1384
+
1385
+ // ../../src/core/tracing/traceContext.ts
1386
+ import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
1387
+ var traceContextStorage = new AsyncLocalStorage6;
1388
+ function runWithTraceContext(context, callback) {
1389
+ return traceContextStorage.run(context, callback);
1390
+ }
1391
+
1392
+ // ../../src/core/tracing/tracingMiddleware.ts
1393
+ function createTracingMiddleware() {
1394
+ return async (request, next) => {
1395
+ const traceId = (request.headers.get("x-trace-id") ?? crypto.randomUUID()).replace(/-/g, "");
1396
+ const spanId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
1397
+ const startedAt = performance.now();
1398
+ const path = new URL(request.url).pathname;
1399
+ return await runWithTraceContext({ traceId, spanId }, async () => {
1400
+ const response = await next();
1401
+ const endedAt = performance.now();
1402
+ const headers = new Headers(response.headers);
1403
+ headers.set("x-trace-id", traceId);
1404
+ headers.set("x-span-id", spanId);
1405
+ headers.set("traceparent", `00-${traceId}-${spanId}-01`);
1406
+ headers.set("server-timing", `app;dur=${(endedAt - startedAt).toFixed(2)}`);
1407
+ exportOtelSpan(createSpan({
1408
+ traceId,
1409
+ name: `${request.method} ${path}`,
1410
+ startedAt,
1411
+ endedAt,
1412
+ attributes: {
1413
+ "http.method": request.method,
1414
+ "http.route": path,
1415
+ "http.status_code": String(response.status)
1416
+ }
1417
+ })).catch(() => {
1418
+ return;
1419
+ });
1420
+ return new Response(response.body, {
1421
+ status: response.status,
1422
+ statusText: response.statusText,
1423
+ headers
1424
+ });
1425
+ });
1426
+ };
1427
+ }
1428
+
1429
+ // ../../src/bootstrap/httpKernel.ts
1430
+ class HttpKernel {
1431
+ dependencies;
1432
+ constructor(dependencies) {
1433
+ this.dependencies = dependencies;
1434
+ }
1435
+ globalMiddleware() {
1436
+ const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
1437
+ return [
1438
+ createCorsMiddleware(),
1439
+ createSecurityHeadersMiddleware(),
1440
+ createBodySizeLimitMiddleware(),
1441
+ createTracingMiddleware(),
1442
+ createMetricsMiddleware(),
1443
+ createRequestLoggingMiddleware(),
1444
+ requestIdMiddleware,
1445
+ createAuthMiddleware(auth),
1446
+ createMembershipMiddleware(),
1447
+ createTenantMiddleware()
1448
+ ];
1449
+ }
1450
+ group(name) {
1451
+ const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
1452
+ switch (name) {
1453
+ case "authenticated":
1454
+ return [createRequireAuthMiddleware(auth)];
1455
+ case "web":
1456
+ return isViewsEnabled() ? [createFlashMiddleware(), createCsrfMiddleware()] : [];
1457
+ case "api": {
1458
+ if (!this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
1459
+ return [];
1460
+ }
1461
+ const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
1462
+ const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
1463
+ if (!redisUrl) {
1464
+ const maxAttempts2 = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
1465
+ return [
1466
+ createMemoryThrottleMiddleware({
1467
+ maxAttempts: Number.isFinite(maxAttempts2) ? maxAttempts2 : 120,
1468
+ decaySeconds: 60
1469
+ })
1470
+ ];
1471
+ }
1472
+ const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
1473
+ return [
1474
+ createThrottleMiddleware({
1475
+ redisUrl,
1476
+ maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
1477
+ decaySeconds: 60
1478
+ })
1479
+ ];
1480
+ }
1481
+ default:
1482
+ throw new Error(`Unknown middleware group "${name}".`);
1483
+ }
1484
+ }
1485
+ wrap(groups, handler) {
1486
+ const names = Array.isArray(groups) ? groups : [groups];
1487
+ const middleware = names.flatMap((name) => this.group(name));
1488
+ if (middleware.length === 0) {
1489
+ return handler;
1490
+ }
1491
+ return withMiddleware(...middleware)(handler);
1492
+ }
1493
+ wrapApi(handler) {
1494
+ return this.wrap(["api", "authenticated"], handler);
1495
+ }
1496
+ wrapWeb(handler) {
1497
+ return handler;
1498
+ }
1499
+ wrapWebPublicRead(handler) {
1500
+ if (isPublicReadsEnabled()) {
1501
+ return this.wrapWeb(handler);
1502
+ }
1503
+ return this.wrapWebAuthenticated(handler);
1504
+ }
1505
+ wrapWebAuthenticated(handler) {
1506
+ const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
1507
+ return withMiddleware(createRequireWebAuthMiddleware(auth))(handler);
1508
+ }
1509
+ wrapWebAbility(ability, handler) {
1510
+ const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
1511
+ const tokenService = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
1512
+ const requireAbility = createRequireAbilityMiddleware(tokenService);
1513
+ const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
1514
+ return withMiddleware(...middleware)(handler);
1515
+ }
1516
+ wrapWebGlobalAdmin(handler) {
1517
+ const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
1518
+ const middleware = [createRequireWebAuthMiddleware(auth), createRequireGlobalAdminMiddleware()];
1519
+ return withMiddleware(...middleware)(handler);
1520
+ }
1521
+ wrapAuthenticated(handler) {
1522
+ return this.wrap("authenticated", handler);
1523
+ }
1524
+ wrapPublicRead(handler) {
1525
+ if (isPublicReadsEnabled()) {
1526
+ return handler;
1527
+ }
1528
+ return this.wrapAuthenticated(handler);
1529
+ }
1530
+ wrapGlobalAdmin(handler) {
1531
+ const middleware = [...this.group("authenticated"), createRequireGlobalAdminMiddleware()];
1532
+ return withMiddleware(...middleware)(handler);
1533
+ }
1534
+ wrapAbility(ability, handler) {
1535
+ const tokenService = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
1536
+ const requireAbility = createRequireAbilityMiddleware(tokenService);
1537
+ const middleware = [...this.group("authenticated"), requireAbility(ability)];
1538
+ return withMiddleware(...middleware)(handler);
1539
+ }
1540
+ wrapPolicy(resource, action, handler) {
1541
+ const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
1542
+ const gate = this.dependencies.container.resolve(CORE_POLICY_GATE_TOKEN);
1543
+ return withMiddleware(createAuthorizeMiddleware(gate, auth, resource, action))(handler);
1544
+ }
1545
+ wrapLogin(handler) {
1546
+ const middleware = [];
1547
+ const loginRateLimit = resolveLoginRateLimit();
1548
+ if (this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
1549
+ const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
1550
+ const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
1551
+ if (redisUrl) {
1552
+ middleware.push(createLoginThrottleMiddleware({
1553
+ redisUrl,
1554
+ maxAttempts: loginRateLimit.maxAttempts,
1555
+ decaySeconds: loginRateLimit.decaySeconds
1556
+ }));
1557
+ } else {
1558
+ middleware.push(createMemoryThrottleMiddleware({
1559
+ maxAttempts: loginRateLimit.maxAttempts,
1560
+ decaySeconds: loginRateLimit.decaySeconds,
1561
+ keyPrefix: "workhub:login-throttle:"
1562
+ }));
1563
+ }
1564
+ } else {
1565
+ middleware.push(createMemoryThrottleMiddleware({
1566
+ maxAttempts: loginRateLimit.maxAttempts,
1567
+ decaySeconds: loginRateLimit.decaySeconds,
1568
+ keyPrefix: "workhub:login-throttle:"
1569
+ }));
1570
+ }
1571
+ if (middleware.length === 0) {
1572
+ return handler;
1573
+ }
1574
+ return withMiddleware(...middleware)(handler);
1575
+ }
1576
+ }
1577
+ function createHttpKernel(dependencies) {
1578
+ return new HttpKernel(dependencies);
1579
+ }
1580
+ // ../../src/bootstrap/prefixRouteMap.ts
1581
+ function prefixRouteMap(prefix, routes) {
1582
+ const normalizedPrefix = prefix.replace(/\/$/, "");
1583
+ const prefixed = {};
1584
+ for (const [path, handler] of Object.entries(routes)) {
1585
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
1586
+ prefixed[`${normalizedPrefix}${normalizedPath}`] = handler;
1587
+ }
1588
+ return prefixed;
1589
+ }
1590
+ // ../../src/bootstrap/web/forms.ts
1591
+ async function parseFormBody(request) {
1592
+ const contentType = request.headers.get("content-type") ?? "";
1593
+ const fields = {};
1594
+ const files = {};
1595
+ if (contentType.includes("application/x-www-form-urlencoded")) {
1596
+ const text = await request.text();
1597
+ for (const pair of text.split("&")) {
1598
+ const idx = pair.indexOf("=");
1599
+ if (idx === -1)
1600
+ continue;
1601
+ const key = decodeURIComponent(pair.slice(0, idx).replace(/\+/g, " "));
1602
+ const value = decodeURIComponent(pair.slice(idx + 1).replace(/\+/g, " "));
1603
+ fields[key] = value;
1604
+ }
1605
+ return { fields, files };
1606
+ }
1607
+ if (contentType.includes("multipart/form-data")) {
1608
+ const form = await request.formData();
1609
+ for (const [key, value] of form.entries()) {
1610
+ if (value instanceof File) {
1611
+ files[key] = value;
1612
+ } else {
1613
+ fields[key] = String(value);
1614
+ }
1615
+ }
1616
+ }
1617
+ return { fields, files };
1618
+ }
1619
+ function createCsrfProtection(secret) {
1620
+ const tokenCache = new Map;
1621
+ function generate(sessionKey) {
1622
+ const token = `${sessionKey}.${Date.now().toString(36)}.${Math.random().toString(36).slice(2)}`;
1623
+ tokenCache.set(token, Date.now());
1624
+ return token;
1625
+ }
1626
+ function verify(token, maxAgeMs = 60 * 60 * 1000) {
1627
+ if (!token || !tokenCache.has(token))
1628
+ return false;
1629
+ const created = tokenCache.get(token) ?? 0;
1630
+ if (Date.now() - created > maxAgeMs) {
1631
+ tokenCache.delete(token);
1632
+ return false;
1633
+ }
1634
+ return true;
1635
+ }
1636
+ return { generate, verify, secret };
1637
+ }
1638
+ // ../../src/bootstrap/web/server.ts
1639
+ function createWebServer(options) {
1640
+ const publicDir = options.publicDir ?? "./public";
1641
+ return Bun.serve({
1642
+ port: options.port,
1643
+ async fetch(request) {
1644
+ await options.onRequest?.(request);
1645
+ const url = new URL(request.url);
1646
+ if (url.pathname.startsWith("/assets/")) {
1647
+ const file = Bun.file(`${publicDir}${url.pathname}`);
1648
+ if (await file.exists()) {
1649
+ return new Response(file);
1650
+ }
1651
+ }
1652
+ const response = await options.handle(request);
1653
+ return response ?? new Response("Not Found", { status: 404 });
1654
+ }
1655
+ });
1656
+ }
1657
+ // ../../src/bootstrap/web/session.ts
1658
+ import { createHash, randomBytes as randomBytes3 } from "crypto";
1659
+
1660
+ class CookieSessionStore {
1661
+ sql;
1662
+ secret;
1663
+ cookieName;
1664
+ maxAgeSeconds;
1665
+ constructor(sql, secret, cookieName = "strata_session", maxAgeSeconds = 60 * 60 * 24 * 14) {
1666
+ this.sql = sql;
1667
+ this.secret = secret;
1668
+ this.cookieName = cookieName;
1669
+ this.maxAgeSeconds = maxAgeSeconds;
1670
+ }
1671
+ cookieHeader(_user, sessionId) {
1672
+ const payload = `${sessionId}.${this.sign(sessionId)}`;
1673
+ return `${this.cookieName}=${payload}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${this.maxAgeSeconds}`;
1674
+ }
1675
+ clearCookieHeader() {
1676
+ return `${this.cookieName}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
1677
+ }
1678
+ async create(user) {
1679
+ const id = randomBytes3(32).toString("hex");
1680
+ const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
1681
+ await this.sql.unsafe(`INSERT INTO sessions (id, user_id, expires_at) VALUES ($1, $2, $3)`, [
1682
+ id,
1683
+ user.id,
1684
+ expires
1685
+ ]);
1686
+ return id;
1687
+ }
1688
+ async destroy(sessionId) {
1689
+ await this.sql.unsafe(`DELETE FROM sessions WHERE id = $1`, [sessionId]);
1690
+ }
1691
+ async read(request) {
1692
+ const cookie = this.parseCookie(request.headers.get("cookie") ?? "");
1693
+ const raw = cookie[this.cookieName];
1694
+ if (!raw)
1695
+ return null;
1696
+ const [sessionId, signature] = raw.split(".");
1697
+ if (!sessionId || !signature || signature !== this.sign(sessionId)) {
1698
+ return null;
1699
+ }
1700
+ const rows = await this.sql.unsafe(`SELECT s.id, s.user_id, s.expires_at, u.name, u.email
1701
+ FROM sessions s
1702
+ INNER JOIN users u ON u.id = s.user_id
1703
+ WHERE s.id = $1 AND s.expires_at > NOW()`, [sessionId]);
1704
+ const row = rows[0];
1705
+ if (!row)
1706
+ return null;
1707
+ return { id: row.user_id, name: row.name, email: row.email };
1708
+ }
1709
+ sign(value) {
1710
+ return createHash("sha256").update(`${value}.${this.secret}`).digest("hex").slice(0, 32);
1711
+ }
1712
+ parseCookie(header) {
1713
+ const out = {};
1714
+ for (const part of header.split(";")) {
1715
+ const idx = part.indexOf("=");
1716
+ if (idx === -1)
1717
+ continue;
1718
+ out[part.slice(0, idx).trim()] = decodeURIComponent(part.slice(idx + 1).trim());
1719
+ }
1720
+ return out;
1721
+ }
1722
+ }
1723
+ // ../../src/bootstrap/web/slug.ts
1724
+ function slugify(value) {
1725
+ return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
1726
+ }
1727
+ export {
1728
+ slugify,
1729
+ setActiveApplicationContext,
1730
+ resolveService,
1731
+ resolveApplicationQueue,
1732
+ prefixRouteMap,
1733
+ parseFormBody,
1734
+ getRequiredDependency,
1735
+ createWebServer,
1736
+ createHttpKernel,
1737
+ createCsrfProtection,
1738
+ assertAppDependenciesComplete,
1739
+ ServiceContainer,
1740
+ REDIS_URL_CONFIG_KEY,
1741
+ DEFAULT_APP_PORT,
1742
+ DATABASE_URL_CONFIG_KEY,
1743
+ CookieSessionStore,
1744
+ CORE_TOKEN_SERVICE_TOKEN,
1745
+ CORE_QUEUE_TOKEN,
1746
+ CORE_POLICY_GATE_TOKEN,
1747
+ CORE_CONFIG_TOKEN,
1748
+ CORE_CACHE_TOKEN,
1749
+ CORE_AUTH_TOKEN,
1750
+ APP_PORT_CONFIG_KEY
1751
+ };