@getstrata/bootstrap 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -81,9 +81,6 @@ var databaseConfig = {
81
81
  // ../../src/core/database/connectionContext.ts
82
82
  import { AsyncLocalStorage } from "async_hooks";
83
83
  var activeConnection = new AsyncLocalStorage;
84
- function runWithDatabaseConnection(connection, callback) {
85
- return activeConnection.run(connection, callback);
86
- }
87
84
  function getActiveDatabaseConnection(fallback) {
88
85
  return activeConnection.getStore() ?? fallback;
89
86
  }
@@ -207,12 +204,6 @@ class UnauthorizedError extends HttpError {
207
204
  }
208
205
  }
209
206
 
210
- class PayloadTooLargeError extends HttpError {
211
- constructor(message = "Payload Too Large", details) {
212
- super(413, message, details);
213
- }
214
- }
215
-
216
207
  // ../../src/core/security/safeUrl.ts
217
208
  var BLOCKED_HOSTNAMES = new Set([
218
209
  "localhost",
@@ -836,9 +827,6 @@ var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
836
827
  // ../../src/core/auth/authContext.ts
837
828
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
838
829
  var authContext = new AsyncLocalStorage2;
839
- function runWithAuthUser(user, callback) {
840
- return authContext.run(user, callback);
841
- }
842
830
  function currentAuthUser() {
843
831
  return authContext.getStore() ?? null;
844
832
  }
@@ -2347,6 +2335,39 @@ function indexBelongsToRelation(children, parents, relation) {
2347
2335
  }
2348
2336
  return result;
2349
2337
  }
2338
+ function indexMorphManyRelation(parents, children, relation) {
2339
+ const groups = new Map;
2340
+ for (const parent of parents) {
2341
+ groups.set(parent[relation.localKey], []);
2342
+ }
2343
+ for (const child of children) {
2344
+ if (child[relation.morphTypeKey] !== relation.morphType) {
2345
+ continue;
2346
+ }
2347
+ const key = child[relation.morphIdKey];
2348
+ const group = groups.get(key);
2349
+ if (!group) {
2350
+ continue;
2351
+ }
2352
+ group.push(child);
2353
+ }
2354
+ return groups;
2355
+ }
2356
+ function indexMorphToRelation(children, parentsByType, relation) {
2357
+ const result = new Map;
2358
+ for (const child of children) {
2359
+ const morphType = String(child[relation.morphTypeKey]);
2360
+ const parents = parentsByType.get(morphType);
2361
+ if (!parents) {
2362
+ continue;
2363
+ }
2364
+ const parent = parents.get(child[relation.morphIdKey]);
2365
+ if (parent) {
2366
+ result.set(child[relation.morphIdKey], parent);
2367
+ }
2368
+ }
2369
+ return result;
2370
+ }
2350
2371
 
2351
2372
  // ../../src/core/database/boundConnection.ts
2352
2373
  var boundConnectionHolder = {
@@ -2477,6 +2498,37 @@ class RepositoryQuery {
2477
2498
  });
2478
2499
  return this;
2479
2500
  }
2501
+ withMorphMany(as, relation, childRepository, options = {}) {
2502
+ this.eagerLoads.push({
2503
+ kind: "morphMany",
2504
+ as,
2505
+ relation,
2506
+ repository: childRepository,
2507
+ options
2508
+ });
2509
+ return this;
2510
+ }
2511
+ withMorphOne(as, relation, childRepository, options = {}) {
2512
+ this.eagerLoads.push({
2513
+ kind: "morphOne",
2514
+ as,
2515
+ relation,
2516
+ repository: childRepository,
2517
+ options
2518
+ });
2519
+ return this;
2520
+ }
2521
+ withMorphTo(as, relation, repositoriesByType, options = {}) {
2522
+ this.eagerLoads.push({
2523
+ kind: "morphTo",
2524
+ as,
2525
+ relation,
2526
+ repository: this.repository,
2527
+ morphRepositories: repositoriesByType,
2528
+ options
2529
+ });
2530
+ return this;
2531
+ }
2480
2532
  async get() {
2481
2533
  const rows = await this.repository.findAll(this.buildOptions());
2482
2534
  return await this.attach(rows);
@@ -2537,6 +2589,33 @@ class RepositoryQuery {
2537
2589
  }));
2538
2590
  continue;
2539
2591
  }
2592
+ if (load.kind === "morphMany") {
2593
+ const relation2 = load.relation;
2594
+ const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation2, load.options);
2595
+ result = result.map((row) => ({
2596
+ ...row,
2597
+ [load.as]: grouped2.get(row[relation2.localKey]) ?? []
2598
+ }));
2599
+ continue;
2600
+ }
2601
+ if (load.kind === "morphOne") {
2602
+ const relation2 = load.relation;
2603
+ const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation2, load.options);
2604
+ result = result.map((row) => ({
2605
+ ...row,
2606
+ [load.as]: grouped2.get(row[relation2.localKey])
2607
+ }));
2608
+ continue;
2609
+ }
2610
+ if (load.kind === "morphTo") {
2611
+ const relation2 = load.relation;
2612
+ const grouped2 = await this.repository.loadMorphToForChildren(rows, relation2, load.morphRepositories ?? new Map, load.options);
2613
+ result = result.map((row) => ({
2614
+ ...row,
2615
+ [load.as]: grouped2.get(row[relation2.morphIdKey])
2616
+ }));
2617
+ continue;
2618
+ }
2540
2619
  const relation = load.relation;
2541
2620
  const grouped = await this.repository.loadBelongsToForParents(rows, relation, load.repository, load.options);
2542
2621
  result = result.map((row) => ({
@@ -2813,6 +2892,56 @@ class BaseRepository5 {
2813
2892
  }, options);
2814
2893
  return indexBelongsToRelation(children, parents, relation);
2815
2894
  }
2895
+ async loadMorphManyForParents(parents, relation, options = {}) {
2896
+ if (parents.length === 0) {
2897
+ return indexMorphManyRelation(parents, [], relation);
2898
+ }
2899
+ const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
2900
+ const children = await this.findWhere({
2901
+ [relation.morphTypeKey]: relation.morphType,
2902
+ [relation.morphIdKey]: parentIds
2903
+ }, options);
2904
+ return indexMorphManyRelation(parents, children, relation);
2905
+ }
2906
+ async loadMorphOneForParents(parents, relation, options = {}) {
2907
+ const grouped = await this.loadMorphManyForParents(parents, relation, options);
2908
+ const result = new Map;
2909
+ for (const parent of parents) {
2910
+ const matches = grouped.get(parent[relation.localKey]) ?? [];
2911
+ result.set(parent[relation.localKey], matches[0]);
2912
+ }
2913
+ return result;
2914
+ }
2915
+ async loadMorphToForChildren(children, relation, repositoriesByType, options = {}) {
2916
+ if (children.length === 0) {
2917
+ return new Map;
2918
+ }
2919
+ const idsByType = new Map;
2920
+ for (const child of children) {
2921
+ const morphType = String(child[relation.morphTypeKey]);
2922
+ const morphId = child[relation.morphIdKey];
2923
+ const ids = idsByType.get(morphType) ?? new Set;
2924
+ ids.add(morphId);
2925
+ idsByType.set(morphType, ids);
2926
+ }
2927
+ const parentsByType = new Map;
2928
+ for (const [morphType, ids] of idsByType) {
2929
+ const repository = repositoriesByType.get(morphType);
2930
+ if (!repository) {
2931
+ continue;
2932
+ }
2933
+ const ownerKey = repository.getTable().primaryKey;
2934
+ const parents = await repository.withConnection(this.connection).findWhere({
2935
+ [ownerKey]: [...ids]
2936
+ }, options);
2937
+ const indexed = new Map;
2938
+ for (const parent of parents) {
2939
+ indexed.set(parent[ownerKey], parent);
2940
+ }
2941
+ parentsByType.set(morphType, indexed);
2942
+ }
2943
+ return indexMorphToRelation(children, parentsByType, relation);
2944
+ }
2816
2945
  }
2817
2946
  var baseRepository_default = BaseRepository5;
2818
2947
  // ../../src/core/database/model.ts
@@ -3429,6 +3558,12 @@ class FailedJobService {
3429
3558
  await this.repository.deleteById(id);
3430
3559
  return failedJob;
3431
3560
  }
3561
+ async delete(id) {
3562
+ const deleted = await this.repository.deleteById(id);
3563
+ if (!deleted) {
3564
+ throw new Error(`Failed job ${id} not found.`);
3565
+ }
3566
+ }
3432
3567
  async flush() {
3433
3568
  const jobs = await this.repository.findAll();
3434
3569
  let deleted = 0;
@@ -3442,9 +3577,6 @@ class FailedJobService {
3442
3577
  }
3443
3578
  var failedJobService_default = FailedJobService;
3444
3579
 
3445
- // ../../src/core/queue/redisQueue.ts
3446
- var {RedisClient: RedisClient2 } = globalThis.Bun;
3447
-
3448
3580
  // ../../src/core/queue/jobRunner.ts
3449
3581
  async function runQueueJob(envelope, failedJobs) {
3450
3582
  const job = jobRegistry.create(envelope.name);
@@ -3476,6 +3608,7 @@ async function runQueueJob(envelope, failedJobs) {
3476
3608
  }
3477
3609
 
3478
3610
  // ../../src/core/queue/redisQueue.ts
3611
+ var {RedisClient: RedisClient2 } = globalThis.Bun;
3479
3612
  var QUEUE_LIST_KEY = "workhub:queue:default";
3480
3613
  var QUEUE_HIGH_KEY = "workhub:queue:high";
3481
3614
  var QUEUE_LOW_KEY = "workhub:queue:low";
@@ -3584,8 +3717,14 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus) {
3584
3717
  if (tags.length === 0) {
3585
3718
  return;
3586
3719
  }
3587
- const cache = resolveApplicationCache();
3588
- const queue = resolveApplicationQueue();
3720
+ let cache;
3721
+ let queue;
3722
+ try {
3723
+ cache = resolveApplicationCache();
3724
+ queue = resolveApplicationQueue();
3725
+ } catch {
3726
+ return;
3727
+ }
3589
3728
  const job = createTrackedJob("cache.invalidate-tags", new invalidateCacheTagsJob_default(cache));
3590
3729
  await queue.dispatch(job, { tags });
3591
3730
  });
@@ -3695,9 +3834,6 @@ function isViewsEnabled() {
3695
3834
  // ../../src/core/http/requestMetaContext.ts
3696
3835
  import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
3697
3836
  var requestMetaContext = new AsyncLocalStorage3;
3698
- function runWithRequestMeta(meta, callback) {
3699
- return requestMetaContext.run(meta, callback);
3700
- }
3701
3837
  function currentRequestMeta() {
3702
3838
  return requestMetaContext.getStore() ?? {
3703
3839
  ipAddress: null,
@@ -3747,9 +3883,6 @@ function htmlResponse(html, init = {}) {
3747
3883
  }
3748
3884
  });
3749
3885
  }
3750
- // ../../src/core/http/csrfToken.ts
3751
- import { timingSafeEqual as timingSafeEqual2 } from "crypto";
3752
-
3753
3886
  // ../../src/core/http/cookies.ts
3754
3887
  function readRequestCookie(request, name) {
3755
3888
  const cookies = request.cookies;
@@ -3784,14 +3917,6 @@ function resolveCsrfSecret() {
3784
3917
  function csrfVerifyOptions() {
3785
3918
  return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
3786
3919
  }
3787
- function tokensMatch(left, right) {
3788
- const leftBuffer = Buffer.from(left);
3789
- const rightBuffer = Buffer.from(right);
3790
- if (leftBuffer.length !== rightBuffer.length) {
3791
- return false;
3792
- }
3793
- return timingSafeEqual2(leftBuffer, rightBuffer);
3794
- }
3795
3920
  function createCsrfTokenCookie() {
3796
3921
  const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
3797
3922
  return {
@@ -3806,45 +3931,6 @@ function resolveCsrfToken(request) {
3806
3931
  }
3807
3932
  return createCsrfTokenCookie();
3808
3933
  }
3809
- function readSubmittedCsrfToken(request) {
3810
- const headerToken = request.headers.get("x-csrf-token")?.trim();
3811
- if (headerToken) {
3812
- return headerToken;
3813
- }
3814
- return null;
3815
- }
3816
- async function readSubmittedCsrfTokenFromBody(request) {
3817
- const headerToken = readSubmittedCsrfToken(request);
3818
- if (headerToken) {
3819
- return headerToken;
3820
- }
3821
- const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
3822
- if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
3823
- const formData = await request.clone().formData();
3824
- const field = formData.get("_token");
3825
- if (typeof field === "string" && field.trim().length > 0) {
3826
- return field.trim();
3827
- }
3828
- const legacyField = formData.get("_csrf");
3829
- if (typeof legacyField === "string" && legacyField.trim().length > 0) {
3830
- return legacyField.trim();
3831
- }
3832
- }
3833
- return null;
3834
- }
3835
- function verifyCsrfToken(request, submittedToken) {
3836
- if (!submittedToken) {
3837
- return false;
3838
- }
3839
- const cookieValue = readRequestCookie(request, CSRF_COOKIE);
3840
- if (!cookieValue) {
3841
- return false;
3842
- }
3843
- if (!tokensMatch(submittedToken, cookieValue)) {
3844
- return false;
3845
- }
3846
- return Bun.CSRF.verify(submittedToken, csrfVerifyOptions());
3847
- }
3848
3934
  function resolveCsrfTokenForRequest(request) {
3849
3935
  const metaToken = currentRequestMeta().csrfToken;
3850
3936
  if (metaToken) {
@@ -3854,7 +3940,7 @@ function resolveCsrfTokenForRequest(request) {
3854
3940
  }
3855
3941
 
3856
3942
  // ../../src/core/http/flashSession.ts
3857
- import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual3 } from "crypto";
3943
+ import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual2 } from "crypto";
3858
3944
  var FLASH_COOKIE = "workhub_flash";
3859
3945
  var FLASH_TTL_MS = 60 * 1000;
3860
3946
  function resolveFlashSecret() {
@@ -3901,7 +3987,7 @@ function parseFlashCookie(cookieValue) {
3901
3987
  if (expectedBuffer.length !== actualBuffer.length) {
3902
3988
  return null;
3903
3989
  }
3904
- if (!timingSafeEqual3(expectedBuffer, actualBuffer)) {
3990
+ if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
3905
3991
  return null;
3906
3992
  }
3907
3993
  try {
@@ -3917,9 +4003,6 @@ function parseFlashCookie(cookieValue) {
3917
4003
  return null;
3918
4004
  }
3919
4005
  }
3920
- function clearFlashCookie() {
3921
- return `${FLASH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
3922
- }
3923
4006
  function pullFlash(request) {
3924
4007
  const cookieValue = readFlashCookie(request);
3925
4008
  if (!cookieValue) {
@@ -3927,15 +4010,6 @@ function pullFlash(request) {
3927
4010
  }
3928
4011
  return parseFlashCookie(cookieValue);
3929
4012
  }
3930
- function withFlashClear(response) {
3931
- const headers = new Headers(response.headers);
3932
- headers.append("set-cookie", clearFlashCookie());
3933
- return new Response(response.body, {
3934
- status: response.status,
3935
- statusText: response.statusText,
3936
- headers
3937
- });
3938
- }
3939
4013
 
3940
4014
  // ../../src/core/view/webLayoutData.ts
3941
4015
  async function resolveWebLayoutData(container, request) {
@@ -4127,17 +4201,6 @@ function composeMiddleware(...middleware) {
4127
4201
  };
4128
4202
  };
4129
4203
  }
4130
- async function requestIdMiddleware(request, next) {
4131
- const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
4132
- const response = await next();
4133
- const headers = new Headers(response.headers);
4134
- headers.set("x-request-id", requestId);
4135
- return new Response(response.body, {
4136
- status: response.status,
4137
- statusText: response.statusText,
4138
- headers
4139
- });
4140
- }
4141
4204
  function wrapRouteHandler(handler, middleware) {
4142
4205
  if (isMethodRouteMap(handler)) {
4143
4206
  const wrapped = {};
@@ -4159,6 +4222,32 @@ function applyMiddlewareToRoutes(routes, middleware) {
4159
4222
  return wrapped;
4160
4223
  }
4161
4224
 
4225
+ // ../../src/bootstrap/httpKernel.ts
4226
+ import {
4227
+ createAuthMiddleware,
4228
+ createAuthorizeMiddleware,
4229
+ createBodySizeLimitMiddleware,
4230
+ createCorsMiddleware,
4231
+ createCsrfMiddleware,
4232
+ createFlashMiddleware,
4233
+ createLoginThrottleMiddleware,
4234
+ createMembershipMiddleware,
4235
+ createMemoryThrottleMiddleware,
4236
+ createMetricsMiddleware,
4237
+ createRequestLoggingMiddleware,
4238
+ createRequireAbilityMiddleware,
4239
+ createRequireAuthMiddleware,
4240
+ createRequireGlobalAdminMiddleware,
4241
+ createRequireWebAuthMiddleware,
4242
+ createSecurityHeadersMiddleware,
4243
+ createTenantMiddleware,
4244
+ createThrottleMiddleware,
4245
+ createTracingMiddleware,
4246
+ isPublicReadsEnabled,
4247
+ requestIdMiddleware,
4248
+ withMiddleware
4249
+ } from "@getstrata/core";
4250
+
4162
4251
  // ../../src/config/rateLimit.ts
4163
4252
  var LOCAL_LOGIN_RATE_LIMIT = {
4164
4253
  maxAttempts: 100,
@@ -4193,849 +4282,6 @@ function resolveRegisterRateLimit() {
4193
4282
  };
4194
4283
  }
4195
4284
 
4196
- // ../../src/core/auth/membershipContext.ts
4197
- import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
4198
-
4199
- // ../../src/modules/organization/memberRepository.ts
4200
- class OrganizationMemberRepository {
4201
- constructor() {}
4202
- async findMembership(userId, organizationId) {
4203
- const rows = await connection_default`
4204
- SELECT id, organization_id, user_id, role, created_at
4205
- FROM organization_member
4206
- WHERE user_id = ${userId} AND organization_id = ${organizationId}
4207
- LIMIT 1
4208
- `;
4209
- return rows[0] ?? null;
4210
- }
4211
- async listForUser(userId) {
4212
- return await connection_default`
4213
- SELECT id, organization_id, user_id, role, created_at
4214
- FROM organization_member
4215
- WHERE user_id = ${userId}
4216
- ORDER BY organization_id
4217
- `;
4218
- }
4219
- async listForOrganization(organizationId) {
4220
- return await connection_default`
4221
- SELECT id, organization_id, user_id, role, created_at
4222
- FROM organization_member
4223
- WHERE organization_id = ${organizationId}
4224
- ORDER BY id
4225
- `;
4226
- }
4227
- async addMember(input) {
4228
- const rows = await connection_default`
4229
- INSERT INTO organization_member (organization_id, user_id, role)
4230
- VALUES (${input.organizationId}, ${input.userId}, ${input.role ?? "member"})
4231
- RETURNING id, organization_id, user_id, role, created_at
4232
- `;
4233
- const row = rows[0];
4234
- if (!row) {
4235
- throw new Error("Organization member insert did not return a row.");
4236
- }
4237
- return row;
4238
- }
4239
- async removeMember(organizationId, userId) {
4240
- const rows = await connection_default`
4241
- DELETE FROM organization_member
4242
- WHERE organization_id = ${organizationId} AND user_id = ${userId}
4243
- RETURNING id
4244
- `;
4245
- return rows.length > 0;
4246
- }
4247
- }
4248
- var memberRepository_default = OrganizationMemberRepository;
4249
-
4250
- // ../../src/core/auth/accessControl.ts
4251
- function isGlobalAdmin(user) {
4252
- return user?.role === "admin";
4253
- }
4254
- function resolveUserId(user) {
4255
- const userId = typeof user.id === "number" ? user.id : Number(user.id);
4256
- if (!Number.isInteger(userId) || userId <= 0) {
4257
- throw new ForbiddenError("Invalid authenticated user.");
4258
- }
4259
- return userId;
4260
- }
4261
-
4262
- // ../../src/core/auth/membershipContext.ts
4263
- var membershipContext = new AsyncLocalStorage4;
4264
- var membershipRepository = new memberRepository_default;
4265
- async function runWithMembershipContext(callback) {
4266
- const user = currentAuthUser();
4267
- if (!user || isGlobalAdmin(user)) {
4268
- return await callback();
4269
- }
4270
- const memberships = await membershipRepository.listForUser(resolveUserId(user));
4271
- const context = {
4272
- organizationIds: memberships.map((membership) => membership.organization_id),
4273
- rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
4274
- };
4275
- return await membershipContext.run(context, callback);
4276
- }
4277
-
4278
- // ../../src/core/auth/membershipContextMiddleware.ts
4279
- function createMembershipContextMiddleware() {
4280
- return async (_request, next) => {
4281
- return await runWithMembershipContext(async () => await next());
4282
- };
4283
- }
4284
-
4285
- // ../../src/core/auth/membershipMiddleware.ts
4286
- function createMembershipMiddleware() {
4287
- return createMembershipContextMiddleware();
4288
- }
4289
-
4290
- // ../../src/core/http/authMiddleware.ts
4291
- function createAuthMiddleware(auth) {
4292
- return async (request, next) => {
4293
- const user = await auth.resolve(request);
4294
- return await runWithAuthUser(user, async () => {
4295
- const response = await next();
4296
- if (user) {
4297
- const headers = new Headers(response.headers);
4298
- headers.set("x-authenticated-user-id", String(user.id));
4299
- return new Response(response.body, {
4300
- status: response.status,
4301
- statusText: response.statusText,
4302
- headers
4303
- });
4304
- }
4305
- return response;
4306
- });
4307
- };
4308
- }
4309
-
4310
- // ../../src/core/http/authorizeMiddleware.ts
4311
- function createAuthorizeMiddleware(gate, auth, resource, action) {
4312
- return async (request, next) => {
4313
- const user = await auth.resolve(request);
4314
- if (!gate.allows(resource, action, user)) {
4315
- const error = new ForbiddenError;
4316
- return Response.json({ error: error.message }, { status: error.status });
4317
- }
4318
- return await next();
4319
- };
4320
- }
4321
-
4322
- // ../../src/core/http/bodySizeLimitMiddleware.ts
4323
- var DEFAULT_MAX_BODY_BYTES = 1048576;
4324
- function resolveMaxBodyBytes() {
4325
- const raw = process.env.MAX_REQUEST_BODY_BYTES?.trim();
4326
- if (!raw) {
4327
- return DEFAULT_MAX_BODY_BYTES;
4328
- }
4329
- const parsed = Number.parseInt(raw, 10);
4330
- if (!Number.isInteger(parsed) || parsed <= 0) {
4331
- return DEFAULT_MAX_BODY_BYTES;
4332
- }
4333
- return parsed;
4334
- }
4335
- function createBodySizeLimitMiddleware(maxBytes = resolveMaxBodyBytes()) {
4336
- return async (request, next) => {
4337
- const contentLength = request.headers.get("content-length");
4338
- if (contentLength) {
4339
- const bytes = Number.parseInt(contentLength, 10);
4340
- if (Number.isInteger(bytes) && bytes > maxBytes) {
4341
- const error = new PayloadTooLargeError(`Request body exceeds the ${maxBytes} byte limit.`);
4342
- return Response.json({ error: error.message }, { status: error.status });
4343
- }
4344
- }
4345
- return await next();
4346
- };
4347
- }
4348
-
4349
- // ../../src/config/cors.ts
4350
- var corsConfig = {
4351
- allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim()).filter(Boolean),
4352
- allowedMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
4353
- allowedHeaders: [
4354
- "Authorization",
4355
- "Content-Type",
4356
- "X-Request-Id",
4357
- "X-Tenant-Id",
4358
- "X-Authenticated-User-Id",
4359
- "X-Authenticated-User-Role",
4360
- "If-Match",
4361
- "If-None-Match"
4362
- ],
4363
- maxAgeSeconds: 86400
4364
- };
4365
-
4366
- // ../../src/core/http/corsMiddleware.ts
4367
- function createCorsMiddleware() {
4368
- return async (request, next) => {
4369
- if (request.method === "OPTIONS") {
4370
- return new Response(null, {
4371
- status: 204,
4372
- headers: buildCorsHeaders(request)
4373
- });
4374
- }
4375
- const response = await next();
4376
- const headers = new Headers(response.headers);
4377
- for (const [key, value] of buildCorsHeaders(request)) {
4378
- headers.set(key, value);
4379
- }
4380
- return new Response(response.body, {
4381
- status: response.status,
4382
- statusText: response.statusText,
4383
- headers
4384
- });
4385
- };
4386
- }
4387
- function buildCorsHeaders(request) {
4388
- const headers = new Headers;
4389
- const origin = request.headers.get("origin");
4390
- const allowedOrigins = corsConfig.allowedOrigins;
4391
- const allowOrigin = allowedOrigins.includes("*") || origin && allowedOrigins.includes(origin) ? origin ?? "*" : allowedOrigins[0] ?? "*";
4392
- headers.set("Access-Control-Allow-Origin", allowOrigin);
4393
- headers.set("Access-Control-Allow-Methods", corsConfig.allowedMethods.join(", "));
4394
- headers.set("Access-Control-Allow-Headers", corsConfig.allowedHeaders.join(", "));
4395
- headers.set("Access-Control-Max-Age", String(corsConfig.maxAgeSeconds));
4396
- headers.set("Vary", "Origin");
4397
- return headers;
4398
- }
4399
-
4400
- // ../../src/core/http/csrfMiddleware.ts
4401
- var MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
4402
- function appendSetCookie(response, cookie) {
4403
- const headers = new Headers(response.headers);
4404
- headers.append("set-cookie", cookie);
4405
- return new Response(response.body, {
4406
- status: response.status,
4407
- statusText: response.statusText,
4408
- headers
4409
- });
4410
- }
4411
- function createCsrfMiddleware() {
4412
- return async (request, next) => {
4413
- const method = request.method.toUpperCase();
4414
- if (!MUTATING_METHODS.has(method)) {
4415
- const csrf = resolveCsrfToken(request);
4416
- const meta = currentRequestMeta();
4417
- meta.csrfToken = csrf.token;
4418
- const response = await next();
4419
- if (!csrf.cookie) {
4420
- return response;
4421
- }
4422
- return appendSetCookie(response, csrf.cookie);
4423
- }
4424
- const submitted = await readSubmittedCsrfTokenFromBody(request);
4425
- if (!verifyCsrfToken(request, submitted)) {
4426
- throw new ForbiddenError("Invalid or missing CSRF token.");
4427
- }
4428
- return await next();
4429
- };
4430
- }
4431
-
4432
- // ../../src/core/http/flashMiddleware.ts
4433
- function createFlashMiddleware() {
4434
- return async (request, next) => {
4435
- const flash = pullFlash(request);
4436
- const meta = currentRequestMeta();
4437
- return await runWithRequestMeta({ ...meta, request, flash }, async () => {
4438
- const response = await next();
4439
- if (flash) {
4440
- return withFlashClear(response);
4441
- }
4442
- return response;
4443
- });
4444
- };
4445
- }
4446
-
4447
- // ../../src/core/http/loginThrottleMiddleware.ts
4448
- var {RedisClient: RedisClient3 } = globalThis.Bun;
4449
- function resolveLoginIdentity(request) {
4450
- return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
4451
- }
4452
- async function resolveLoginEmail(request) {
4453
- const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
4454
- try {
4455
- if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
4456
- const formData = await request.clone().formData();
4457
- const email = formData.get("email");
4458
- return typeof email === "string" ? email.trim().toLowerCase() : "unknown";
4459
- }
4460
- const payload = await request.clone().json();
4461
- return typeof payload.email === "string" ? payload.email.trim().toLowerCase() : "unknown";
4462
- } catch {
4463
- return "unknown";
4464
- }
4465
- }
4466
- function createLoginThrottleMiddleware(options) {
4467
- const client = new RedisClient3(options.redisUrl);
4468
- const prefix = options.keyPrefix ?? "workhub:login-throttle:";
4469
- return async (request, next) => {
4470
- const identity = resolveLoginIdentity(request);
4471
- const email = await resolveLoginEmail(request);
4472
- const throttleKey = `${prefix}${identity}:${email}`;
4473
- const attempts = Number(await client.incr(throttleKey));
4474
- if (attempts === 1) {
4475
- await client.expire(throttleKey, options.decaySeconds);
4476
- }
4477
- if (attempts > options.maxAttempts) {
4478
- return Response.json({ error: "Too many login attempts. Try again later." }, {
4479
- status: 429,
4480
- headers: {
4481
- "retry-after": String(options.decaySeconds)
4482
- }
4483
- });
4484
- }
4485
- return await next();
4486
- };
4487
- }
4488
-
4489
- // ../../src/core/http/memoryThrottleMiddleware.ts
4490
- var buckets = new Map;
4491
- function createMemoryThrottleMiddleware(options) {
4492
- const prefix = options.keyPrefix ?? "workhub:memory-throttle:";
4493
- return async (request, next) => {
4494
- const path = new URL(request.url).pathname;
4495
- const identity = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("authorization")?.slice(0, 32) ?? "unknown";
4496
- const key = `${prefix}${identity}:${path}`;
4497
- const now = Date.now();
4498
- const existing = buckets.get(key);
4499
- if (!existing || existing.resetAt <= now) {
4500
- buckets.set(key, { count: 1, resetAt: now + options.decaySeconds * 1000 });
4501
- return await next();
4502
- }
4503
- existing.count += 1;
4504
- if (existing.count > options.maxAttempts) {
4505
- return Response.json({ error: "Too many requests." }, {
4506
- status: 429,
4507
- headers: {
4508
- "retry-after": String(options.decaySeconds)
4509
- }
4510
- });
4511
- }
4512
- return await next();
4513
- };
4514
- }
4515
-
4516
- // ../../src/core/metrics/prometheus.ts
4517
- class PrometheusRegistry {
4518
- httpRequestsTotal = new Map;
4519
- httpRequestDurationMs = new Map;
4520
- incrementHttpRequest(labels) {
4521
- const key = this.metricKey(labels);
4522
- this.httpRequestsTotal.set(key, (this.httpRequestsTotal.get(key) ?? 0) + 1);
4523
- }
4524
- observeHttpDuration(labels, durationMs) {
4525
- const key = this.metricKey(labels);
4526
- const samples = this.httpRequestDurationMs.get(key) ?? [];
4527
- samples.push(durationMs);
4528
- this.httpRequestDurationMs.set(key, samples);
4529
- }
4530
- renderMetrics() {
4531
- const lines = [
4532
- "# HELP http_requests_total Total HTTP requests processed.",
4533
- "# TYPE http_requests_total counter"
4534
- ];
4535
- for (const [key, value] of this.httpRequestsTotal) {
4536
- lines.push(`http_requests_total{${key}} ${value}`);
4537
- }
4538
- lines.push("# HELP http_request_duration_ms_sum Sum of HTTP request durations in milliseconds.", "# TYPE http_request_duration_ms_sum counter");
4539
- for (const [key, samples] of this.httpRequestDurationMs) {
4540
- const sum = samples.reduce((total, sample) => total + sample, 0);
4541
- lines.push(`http_request_duration_ms_sum{${key}} ${sum}`);
4542
- }
4543
- return `${lines.join(`
4544
- `)}
4545
- `;
4546
- }
4547
- resetForTests() {
4548
- this.httpRequestsTotal.clear();
4549
- this.httpRequestDurationMs.clear();
4550
- }
4551
- getHttpRequestSummary() {
4552
- const byStatus = {};
4553
- const pathCounts = new Map;
4554
- let totalRequests = 0;
4555
- for (const [key, count] of this.httpRequestsTotal) {
4556
- totalRequests += count;
4557
- const method = key.match(/method="([^"]+)"/)?.[1] ?? "GET";
4558
- const path = key.match(/path="([^"]+)"/)?.[1] ?? "/";
4559
- const status = key.match(/status="([^"]+)"/)?.[1] ?? "200";
4560
- byStatus[status] = (byStatus[status] ?? 0) + count;
4561
- const pathKey = `${method} ${path}`;
4562
- const existing = pathCounts.get(pathKey);
4563
- if (existing) {
4564
- existing.count += count;
4565
- } else {
4566
- pathCounts.set(pathKey, { method, path, count });
4567
- }
4568
- }
4569
- const topPaths = Array.from(pathCounts.values()).sort((left, right) => right.count - left.count).slice(0, 10);
4570
- return {
4571
- totalRequests,
4572
- byStatus,
4573
- topPaths
4574
- };
4575
- }
4576
- metricKey(labels) {
4577
- return `method="${labels.method}",path="${labels.path}",status="${labels.status}"`;
4578
- }
4579
- }
4580
- var prometheusRegistry = new PrometheusRegistry;
4581
-
4582
- // ../../src/core/http/metricsMiddleware.ts
4583
- function normalizeMetricPath(pathname) {
4584
- return pathname.replace(/\/\d+/g, "/:id").replace(/\/[0-9a-f-]{36}/gi, "/:id");
4585
- }
4586
- function createMetricsMiddleware() {
4587
- return async (request, next) => {
4588
- const startedAt = performance.now();
4589
- const response = await next();
4590
- const durationMs = performance.now() - startedAt;
4591
- const path = normalizeMetricPath(new URL(request.url).pathname);
4592
- const labels = {
4593
- method: request.method,
4594
- path,
4595
- status: String(response.status)
4596
- };
4597
- prometheusRegistry.incrementHttpRequest(labels);
4598
- prometheusRegistry.observeHttpDuration(labels, durationMs);
4599
- return response;
4600
- };
4601
- }
4602
-
4603
- // ../../src/core/http/requireAbilityMiddleware.ts
4604
- function createRequireAbilityMiddleware(abilityChecker) {
4605
- return (ability) => {
4606
- return async (_request, next) => {
4607
- const user = currentAuthUser();
4608
- try {
4609
- abilityChecker.requireAbility(user, ability);
4610
- } catch (error) {
4611
- if (error instanceof ForbiddenError) {
4612
- return Response.json({ error: error.message }, { status: error.status });
4613
- }
4614
- throw error;
4615
- }
4616
- return await next();
4617
- };
4618
- };
4619
- }
4620
-
4621
- // ../../src/core/http/requireAuthMiddleware.ts
4622
- function createRequireAuthMiddleware(auth) {
4623
- return async (request, next) => {
4624
- if (!await auth.check(request)) {
4625
- const error = new UnauthorizedError;
4626
- return Response.json({ error: error.message }, { status: error.status });
4627
- }
4628
- return await next();
4629
- };
4630
- }
4631
-
4632
- // ../../src/core/security/securityEvents.ts
4633
- function logSecurityEvent2(event, details = {}) {
4634
- const meta = currentRequestMeta();
4635
- const user = currentAuthUser();
4636
- console.log(JSON.stringify({
4637
- level: "security",
4638
- event,
4639
- timestamp: new Date().toISOString(),
4640
- ip_address: meta.ipAddress ?? null,
4641
- user_agent: meta.userAgent ?? null,
4642
- user_id: user?.id ?? null,
4643
- ...details
4644
- }));
4645
- }
4646
-
4647
- // ../../src/core/http/requireGlobalAdminMiddleware.ts
4648
- function createRequireGlobalAdminMiddleware() {
4649
- return async (_request, next) => {
4650
- const user = currentAuthUser();
4651
- if (!isGlobalAdmin(user)) {
4652
- logSecurityEvent2("privilege_escalation_blocked", {
4653
- required_role: "platform_admin",
4654
- path: new URL(_request.url).pathname
4655
- });
4656
- const error = new ForbiddenError("Platform admin access required.");
4657
- return Response.json({ error: error.message }, { status: error.status });
4658
- }
4659
- return await next();
4660
- };
4661
- }
4662
-
4663
- // ../../src/core/http/contentNegotiation.ts
4664
- function requestPrefersJson(request) {
4665
- if (!request) {
4666
- return true;
4667
- }
4668
- if (request.headers.get("HX-Request") === "true") {
4669
- return false;
4670
- }
4671
- const accept = request.headers.get("accept")?.toLowerCase() ?? "";
4672
- if (accept.includes("text/html")) {
4673
- return false;
4674
- }
4675
- if (accept.includes("application/json")) {
4676
- return true;
4677
- }
4678
- const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
4679
- if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
4680
- return false;
4681
- }
4682
- const pathname = new URL(request.url).pathname;
4683
- return pathname.startsWith("/api/");
4684
- }
4685
-
4686
- // ../../src/core/http/requireWebAuthMiddleware.ts
4687
- function createRequireWebAuthMiddleware(auth) {
4688
- return async (request, next) => {
4689
- const user = await auth.resolve(request);
4690
- if (user) {
4691
- return await next();
4692
- }
4693
- if (requestPrefersJson(request)) {
4694
- throw new UnauthorizedError;
4695
- }
4696
- const redirectTarget = encodeURIComponent(new URL(request.url).pathname);
4697
- return Response.redirect(`/login?redirect=${redirectTarget}`, 302);
4698
- };
4699
- }
4700
-
4701
- // ../../src/core/http/routeMiddleware.ts
4702
- function withMiddleware(...middleware) {
4703
- const wrap = composeMiddleware(...middleware);
4704
- return (handler) => {
4705
- return wrap(handler);
4706
- };
4707
- }
4708
-
4709
- // ../../src/config/contentSecurityPolicy.ts
4710
- function strictApiContentSecurityPolicy() {
4711
- return "default-src 'none'; frame-ancestors 'none'; base-uri 'none'";
4712
- }
4713
- function serverHtmxContentSecurityPolicy() {
4714
- return [
4715
- "default-src 'self'",
4716
- "script-src 'self' https://unpkg.com",
4717
- "style-src 'self'",
4718
- "connect-src 'self'",
4719
- "img-src 'self'",
4720
- "font-src 'self'",
4721
- "form-action 'self'",
4722
- "frame-ancestors 'none'",
4723
- "base-uri 'self'"
4724
- ].join("; ");
4725
- }
4726
- function spaContentSecurityPolicy() {
4727
- return [
4728
- "default-src 'self'",
4729
- "script-src 'self'",
4730
- "style-src 'self'",
4731
- "connect-src 'self'",
4732
- "img-src 'self'",
4733
- "font-src 'self'",
4734
- "frame-ancestors 'none'",
4735
- "base-uri 'self'"
4736
- ].join("; ");
4737
- }
4738
- function resolveContentSecurityPolicy(response) {
4739
- const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
4740
- if (!contentType.includes("text/html")) {
4741
- return strictApiContentSecurityPolicy();
4742
- }
4743
- const frontendMode = (process.env.FRONTEND_MODE ?? "api").trim();
4744
- if (frontendMode === "server-htmx") {
4745
- return serverHtmxContentSecurityPolicy();
4746
- }
4747
- if (frontendMode === "spa-react") {
4748
- return spaContentSecurityPolicy();
4749
- }
4750
- return strictApiContentSecurityPolicy();
4751
- }
4752
-
4753
- // ../../src/core/http/securityHeadersMiddleware.ts
4754
- function createSecurityHeadersMiddleware() {
4755
- return async (_request, next) => {
4756
- const response = await next();
4757
- const headers = new Headers(response.headers);
4758
- headers.set("X-Content-Type-Options", "nosniff");
4759
- headers.set("X-Frame-Options", "DENY");
4760
- headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
4761
- headers.set("X-XSS-Protection", "0");
4762
- headers.set("Content-Security-Policy", resolveContentSecurityPolicy(response));
4763
- if (appConfig.env === "production") {
4764
- headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
4765
- }
4766
- return new Response(response.body, {
4767
- status: response.status,
4768
- statusText: response.statusText,
4769
- headers
4770
- });
4771
- };
4772
- }
4773
-
4774
- // ../../src/core/http/throttleMiddleware.ts
4775
- var {RedisClient: RedisClient4 } = globalThis.Bun;
4776
-
4777
- // ../../src/core/tenant/tenantContext.ts
4778
- import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
4779
- var tenantContext = new AsyncLocalStorage5;
4780
- function runWithTenant(tenant, callback) {
4781
- return tenantContext.run(tenant, callback);
4782
- }
4783
- function currentTenant() {
4784
- return tenantContext.getStore() ?? null;
4785
- }
4786
- function rateLimitMultiplierForPlan(plan) {
4787
- switch (plan) {
4788
- case "enterprise":
4789
- return 4;
4790
- case "pro":
4791
- return 2;
4792
- default:
4793
- return 1;
4794
- }
4795
- }
4796
-
4797
- // ../../src/core/http/throttleMiddleware.ts
4798
- function resolveThrottleIdentity(request) {
4799
- const user = currentAuthUser();
4800
- if (user?.tokenId !== undefined) {
4801
- return `token:${user.tokenId}`;
4802
- }
4803
- if (user) {
4804
- return `user:${user.id}`;
4805
- }
4806
- return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
4807
- }
4808
- function createThrottleMiddleware(options) {
4809
- const client = new RedisClient4(options.redisUrl);
4810
- const prefix = options.keyPrefix ?? "workhub:throttle:";
4811
- return async (request, next) => {
4812
- const identity = resolveThrottleIdentity(request);
4813
- const path = new URL(request.url).pathname;
4814
- const throttleKey = `${prefix}${identity}:${path}`;
4815
- const attempts = Number(await client.incr(throttleKey));
4816
- if (attempts === 1) {
4817
- await client.expire(throttleKey, options.decaySeconds);
4818
- }
4819
- const maxAttempts = options.maxAttempts * rateLimitMultiplierForPlan(currentTenant()?.plan ?? "free");
4820
- if (attempts > maxAttempts) {
4821
- return Response.json({ error: "Too many requests." }, {
4822
- status: 429,
4823
- headers: {
4824
- "retry-after": String(options.decaySeconds)
4825
- }
4826
- });
4827
- }
4828
- return await next();
4829
- };
4830
- }
4831
-
4832
- // ../../src/core/logging/requestLoggingMiddleware.ts
4833
- function createRequestLoggingMiddleware() {
4834
- return async (request, next) => {
4835
- return await runWithRequestMeta({
4836
- ipAddress: request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip"),
4837
- userAgent: request.headers.get("user-agent"),
4838
- request
4839
- }, async () => {
4840
- const startedAt = performance.now();
4841
- const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
4842
- const response = await next();
4843
- const durationMs = Math.round(performance.now() - startedAt);
4844
- appLogger.info("HTTP request completed", {
4845
- requestId,
4846
- method: request.method,
4847
- path: new URL(request.url).pathname,
4848
- status: response.status,
4849
- durationMs
4850
- });
4851
- return response;
4852
- });
4853
- };
4854
- }
4855
-
4856
- // ../../src/core/security/publicReads.ts
4857
- function isPublicReadsEnabled() {
4858
- return isFeatureEnabled("publicReads");
4859
- }
4860
-
4861
- // ../../src/core/tenant/resolveTenant.ts
4862
- async function resolveTenant(tenantId) {
4863
- const rows = await connection_default`
4864
- SELECT id, slug, plan, region
4865
- FROM tenant
4866
- WHERE id = ${tenantId}
4867
- LIMIT 1
4868
- `;
4869
- const row = rows[0];
4870
- return row ? { id: row.id, slug: row.slug, plan: row.plan, region: row.region ?? "eu" } : null;
4871
- }
4872
-
4873
- // ../../src/core/tenant/tenantDatabaseScope.ts
4874
- async function applyTenantContextToTransaction(transaction, tenantId) {
4875
- await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
4876
- await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
4877
- }
4878
- async function runWithTenantDatabase(tenant, callback) {
4879
- return await getDatabase().begin(async (transaction) => {
4880
- await applyTenantContextToTransaction(transaction, tenant.id);
4881
- return await runWithDatabaseConnection(transaction, async () => {
4882
- return await runWithTenant(tenant, callback);
4883
- });
4884
- });
4885
- }
4886
-
4887
- // ../../src/core/tenant/tenantMiddleware.ts
4888
- var DEFAULT_TENANT = {
4889
- id: 1,
4890
- slug: "default",
4891
- plan: "enterprise",
4892
- region: "eu"
4893
- };
4894
- async function resolveUserTenantId(userId) {
4895
- return await runWithMigrationBypass(async () => {
4896
- const rows = await connection_default`
4897
- SELECT tenant_id
4898
- FROM users
4899
- WHERE id = ${userId}
4900
- LIMIT 1
4901
- `;
4902
- return rows[0]?.tenant_id ?? DEFAULT_TENANT.id;
4903
- });
4904
- }
4905
- async function resolveTenantForRequest(request) {
4906
- const user = currentAuthUser();
4907
- const headerValue = request.headers.get("x-tenant-id")?.trim();
4908
- const parsedHeader = headerValue !== undefined && headerValue.length > 0 ? Number.parseInt(headerValue, 10) : Number.NaN;
4909
- if (user) {
4910
- const userId = typeof user.id === "number" ? user.id : Number.parseInt(String(user.id), 10);
4911
- if (Number.isInteger(userId) && userId > 0) {
4912
- const userTenantId = await resolveUserTenantId(userId);
4913
- if (!isGlobalAdmin(user)) {
4914
- if (Number.isInteger(parsedHeader) && parsedHeader > 0 && parsedHeader !== userTenantId) {
4915
- throw new ForbiddenError("Tenant header does not match your account.");
4916
- }
4917
- return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
4918
- }
4919
- if (Number.isInteger(parsedHeader) && parsedHeader > 0) {
4920
- return await resolveTenant(parsedHeader) ?? DEFAULT_TENANT;
4921
- }
4922
- return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
4923
- }
4924
- }
4925
- const tenantId = Number.isInteger(parsedHeader) && parsedHeader > 0 ? parsedHeader : DEFAULT_TENANT.id;
4926
- return await resolveTenant(tenantId) ?? DEFAULT_TENANT;
4927
- }
4928
- function createTenantMiddleware() {
4929
- return async (request, next) => {
4930
- try {
4931
- const tenant = await resolveTenantForRequest(request);
4932
- return await runWithTenantDatabase(tenant, async () => {
4933
- const response = await next();
4934
- const headers = new Headers(response.headers);
4935
- headers.set("x-tenant-id", String(tenant.id));
4936
- headers.set("x-tenant-region", tenant.region);
4937
- return new Response(response.body, {
4938
- status: response.status,
4939
- statusText: response.statusText,
4940
- headers
4941
- });
4942
- });
4943
- } catch (error) {
4944
- if (error instanceof HttpError) {
4945
- return Response.json({ error: error.message }, { status: error.status });
4946
- }
4947
- throw error;
4948
- }
4949
- };
4950
- }
4951
-
4952
- // ../../src/core/tracing/otel.ts
4953
- import { randomBytes } from "crypto";
4954
- function randomHex(bytes) {
4955
- return randomBytes(bytes).toString("hex");
4956
- }
4957
- function createSpan(input) {
4958
- const spanId = randomHex(8);
4959
- return {
4960
- traceId: input.traceId,
4961
- spanId,
4962
- name: input.name,
4963
- startTimeUnixNano: String(Math.floor(input.startedAt * 1e6)),
4964
- endTimeUnixNano: String(Math.floor(input.endedAt * 1e6)),
4965
- attributes: Object.entries(input.attributes ?? {}).map(([key, value]) => ({
4966
- key,
4967
- value: { stringValue: value }
4968
- })),
4969
- status: { code: 1 }
4970
- };
4971
- }
4972
- async function exportOtelSpan(span) {
4973
- const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
4974
- if (!endpoint) {
4975
- return;
4976
- }
4977
- const serviceName = process.env.OTEL_SERVICE_NAME?.trim() ?? "workhub-api";
4978
- const url = endpoint.endsWith("/v1/traces") ? endpoint : `${endpoint.replace(/\/$/, "")}/v1/traces`;
4979
- await fetch(url, {
4980
- method: "POST",
4981
- headers: { "content-type": "application/json" },
4982
- body: JSON.stringify({
4983
- resourceSpans: [
4984
- {
4985
- resource: {
4986
- attributes: [{ key: "service.name", value: { stringValue: serviceName } }]
4987
- },
4988
- scopeSpans: [{ spans: [span] }]
4989
- }
4990
- ]
4991
- })
4992
- });
4993
- }
4994
-
4995
- // ../../src/core/tracing/traceContext.ts
4996
- import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
4997
- var traceContextStorage = new AsyncLocalStorage6;
4998
- function runWithTraceContext(context, callback) {
4999
- return traceContextStorage.run(context, callback);
5000
- }
5001
-
5002
- // ../../src/core/tracing/tracingMiddleware.ts
5003
- function createTracingMiddleware() {
5004
- return async (request, next) => {
5005
- const traceId = (request.headers.get("x-trace-id") ?? crypto.randomUUID()).replace(/-/g, "");
5006
- const spanId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
5007
- const startedAt = performance.now();
5008
- const path = new URL(request.url).pathname;
5009
- return await runWithTraceContext({ traceId, spanId }, async () => {
5010
- const response = await next();
5011
- const endedAt = performance.now();
5012
- const headers = new Headers(response.headers);
5013
- headers.set("x-trace-id", traceId);
5014
- headers.set("x-span-id", spanId);
5015
- headers.set("traceparent", `00-${traceId}-${spanId}-01`);
5016
- headers.set("server-timing", `app;dur=${(endedAt - startedAt).toFixed(2)}`);
5017
- exportOtelSpan(createSpan({
5018
- traceId,
5019
- name: `${request.method} ${path}`,
5020
- startedAt,
5021
- endedAt,
5022
- attributes: {
5023
- "http.method": request.method,
5024
- "http.route": path,
5025
- "http.status_code": String(response.status)
5026
- }
5027
- })).catch(() => {
5028
- return;
5029
- });
5030
- return new Response(response.body, {
5031
- status: response.status,
5032
- statusText: response.statusText,
5033
- headers
5034
- });
5035
- });
5036
- };
5037
- }
5038
-
5039
4285
  // ../../src/bootstrap/httpKernel.ts
5040
4286
  class HttpKernel {
5041
4287
  dependencies;
@@ -5431,7 +4677,7 @@ function createWebServer(options) {
5431
4677
  });
5432
4678
  }
5433
4679
  // ../../src/bootstrap/web/session.ts
5434
- import { createHash, randomBytes as randomBytes2 } from "crypto";
4680
+ import { createHash, randomBytes } from "crypto";
5435
4681
  class CookieSessionStore {
5436
4682
  sql;
5437
4683
  secret;
@@ -5457,7 +4703,7 @@ class CookieSessionStore {
5457
4703
  return header.includes("Secure") ? header : `${header}; Secure`;
5458
4704
  }
5459
4705
  async create(user) {
5460
- const id = randomBytes2(32).toString("hex");
4706
+ const id = randomBytes(32).toString("hex");
5461
4707
  const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
5462
4708
  await this.sql.unsafe(`INSERT INTO sessions (id, user_id, expires_at) VALUES ($1, $2, $3)`, [
5463
4709
  id,
@@ -5478,7 +4724,8 @@ class CookieSessionStore {
5478
4724
  if (!sessionId || !signature || signature !== this.sign(sessionId)) {
5479
4725
  return null;
5480
4726
  }
5481
- const rows = await this.sql.unsafe(`SELECT s.id, s.user_id, s.expires_at, u.name, u.email, u.learn_subscriber
4727
+ const rows = await this.sql.unsafe(`SELECT s.id, s.user_id, s.expires_at, u.name, u.email, u.learn_subscriber,
4728
+ COALESCE(u.is_admin, false) AS is_admin
5482
4729
  FROM sessions s
5483
4730
  INNER JOIN users u ON u.id = s.user_id
5484
4731
  WHERE s.id = $1 AND s.expires_at > NOW()`, [sessionId]);
@@ -5489,7 +4736,8 @@ class CookieSessionStore {
5489
4736
  id: row.user_id,
5490
4737
  name: row.name,
5491
4738
  email: row.email,
5492
- learn_subscriber: row.learn_subscriber
4739
+ learn_subscriber: row.learn_subscriber,
4740
+ is_admin: row.is_admin
5493
4741
  };
5494
4742
  }
5495
4743
  sign(value) {