@getstrata/bootstrap 0.2.5 → 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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @getstrata/bootstrap
2
2
 
3
- Application bootstrap for Strata sibling apps HttpKernel, service container, and web utilities.
3
+ Application bootstrap for Strata sibling apps: HttpKernel, service container, and web utilities.
4
4
 
5
5
  ## Install
6
6
 
@@ -20,6 +20,8 @@ import {
20
20
  } from "@getstrata/bootstrap";
21
21
  ```
22
22
 
23
+ `CookieSessionStore` reads optional `is_admin` from the user row for global admin routing via `wrapWebGlobalAdmin`.
24
+
23
25
  ## HttpKernel (API / module apps)
24
26
 
25
27
  ```typescript
@@ -1,5 +1,4 @@
1
- import type { Policy } from "../core/auth/policy";
2
- import { type Middleware, type RouteHandler } from "../core/http/middleware";
1
+ import { type Middleware, type Policy, type RouteHandler } from "@getstrata/core";
3
2
  import type { AppDependencies } from "./contracts";
4
3
  type MiddlewareGroupName = "api" | "authenticated" | "web";
5
4
  declare class HttpKernel {
@@ -1,4 +1,4 @@
1
- import { type Middleware } from "../core/http/middleware";
1
+ import { type Middleware } from "@getstrata/core";
2
2
  import type { AppDependencies } from "./contracts";
3
3
  declare function createDefaultMiddleware(dependencies: AppDependencies): Middleware[];
4
4
  export { createDefaultMiddleware };
@@ -1,6 +1,7 @@
1
1
  import type { AdminResource, AdminResourceDefinition } from "./types.ts";
2
2
  declare class AdminResourceRegistry {
3
3
  private readonly resources;
4
+ constructor();
4
5
  register<TEntity extends object>(resource: AdminResource<TEntity>): void;
5
6
  get(name: string): AdminResource<object> | undefined;
6
7
  list(): AdminResourceDefinition[];
@@ -12,8 +12,9 @@ interface S3StorageConfig {
12
12
  endpoint?: string;
13
13
  }
14
14
  declare class LocalStorageDriver implements StorageDriver {
15
- private readonly rootDirectory;
16
- constructor(rootDirectory: string);
15
+ private readonly rootDirectory?;
16
+ constructor(rootDirectory?: string | undefined);
17
+ private resolveRootDirectory;
17
18
  private resolvePath;
18
19
  put(path: string, contents: string | Uint8Array): Promise<string>;
19
20
  get(path: string): Promise<Uint8Array | null>;
@@ -37,5 +38,7 @@ declare function resolveS3Config(): S3StorageConfig;
37
38
  declare function createS3Client(config?: S3StorageConfig): S3Client;
38
39
  declare function createStorageDriver(): StorageDriver;
39
40
  declare function storage(): StorageManager;
41
+ /** Test hook: drop the process-wide storage singleton (e.g. after changing STORAGE_PATH). */
42
+ declare function resetDefaultStorage(): void;
40
43
  export type { S3StorageConfig, StorageDriver };
41
- export { createS3Client, createStorageDriver, LocalStorageDriver, resolveS3Config, S3StorageDriver, StorageManager, storage, };
44
+ export { createS3Client, createStorageDriver, LocalStorageDriver, resetDefaultStorage, resolveS3Config, S3StorageDriver, StorageManager, storage, };
@@ -10,6 +10,7 @@ export { AdminResourceRegistry, formatAdminValue, } from "../core/admin/index.ts
10
10
  export type { AbilityChecker } from "../core/auth/abilityChecker.ts";
11
11
  export type { AuthUser } from "../core/auth/authContext.ts";
12
12
  export { currentAuthUser, runWithAuthUser } from "../core/auth/authContext.ts";
13
+ export { createMembershipMiddleware } from "../core/auth/membershipMiddleware.ts";
13
14
  export { Policy, PolicyGate } from "../core/auth/policy.ts";
14
15
  export { default as CacheRepository } from "../core/cache/repository.ts";
15
16
  export { CACHE_TAGS } from "../core/cache/tags.ts";
@@ -40,16 +41,20 @@ export { EventBus } from "../core/events/eventBus.ts";
40
41
  export { auth, cache, config, events, log, mail, policyGate, queue, storage, } from "../core/facades/index.ts";
41
42
  export { createBodySizeLimitMiddleware } from "../core/http/bodySizeLimitMiddleware.ts";
42
43
  export { readBunRequestCookie, readRequestCookie } from "../core/http/cookies.ts";
44
+ export { createCorsMiddleware } from "../core/http/corsMiddleware.ts";
43
45
  export { createCsrfMiddleware } from "../core/http/csrfMiddleware.ts";
44
46
  export { createCsrfProtection } from "../core/http/csrfProtection.ts";
45
47
  export { createCsrfTokenCookie, readSubmittedCsrfToken, readSubmittedCsrfTokenFromBody, resolveCsrfToken, resolveCsrfTokenForRequest, verifyCsrfToken, } from "../core/http/csrfToken.ts";
46
48
  export { assertIfMatch, etagFromResource, isEtagEnabled, } from "../core/http/etag.ts";
49
+ export { createFlashMiddleware } from "../core/http/flashMiddleware.ts";
47
50
  export { FormRequest } from "../core/http/formRequest.ts";
48
- export { applyMiddlewareToRoutes, composeMiddleware, createAuthMiddleware, createAuthorizeMiddleware, createdResponse, createRequireAuthMiddleware, jsonResponse, noContentResponse, paginatedResponse, parsePaginationQuery, securedBindRouteModel, securedBindRouteModelByKey, withErrorHandling, withMiddleware, } from "../core/http/index.ts";
51
+ export { applyMiddlewareToRoutes, composeMiddleware, createAuthMiddleware, createAuthorizeMiddleware, createdResponse, createRequireAuthMiddleware, jsonResponse, noContentResponse, paginatedResponse, parsePaginationQuery, requestIdMiddleware, securedBindRouteModel, securedBindRouteModelByKey, withErrorHandling, withMiddleware, } from "../core/http/index.ts";
49
52
  export { createLoginThrottleMiddleware } from "../core/http/loginThrottleMiddleware.ts";
50
53
  export { createMemoryThrottleMiddleware } from "../core/http/memoryThrottleMiddleware.ts";
51
54
  export { createMetricsMiddleware, normalizeMetricPath } from "../core/http/metricsMiddleware.ts";
52
55
  export type { Middleware, RouteHandler } from "../core/http/middleware.ts";
56
+ export { createRequireAbilityMiddleware } from "../core/http/requireAbilityMiddleware.ts";
57
+ export { createRequireGlobalAdminMiddleware } from "../core/http/requireGlobalAdminMiddleware.ts";
53
58
  export { createRequireWebAuthMiddleware } from "../core/http/requireWebAuthMiddleware.ts";
54
59
  export { serializeDate, toPaginatedResourceCollection, toResourceCollection, } from "../core/http/resources.ts";
55
60
  export type { RouteRequest } from "../core/http/route.ts";
@@ -57,6 +62,7 @@ export { createSecurityHeadersMiddleware } from "../core/http/securityHeadersMid
57
62
  export { createThrottleMiddleware } from "../core/http/throttleMiddleware.ts";
58
63
  export { WebFormRequest } from "../core/http/webFormRequest.ts";
59
64
  export { installGracefulShutdownSignals, registerShutdownHandler, runGracefulShutdown, } from "../core/lifecycle/gracefulShutdown.ts";
65
+ export { createRequestLoggingMiddleware } from "../core/logging/requestLoggingMiddleware.ts";
60
66
  export type { MailDriver, MailMessage } from "../core/mail/mailer.ts";
61
67
  export { buildSmtpPayload, LogMailDriver, Mailer, mailer, } from "../core/mail/mailer.ts";
62
68
  export type { MarkdownMailLayoutOptions, RenderedMarkdownMail } from "../core/mail/markdownMail.ts";
@@ -73,8 +79,11 @@ export { AsyncQueue, createQueue, Job, SyncQueue } from "../core/queue/index.ts"
73
79
  export { createFailedJobService, createProductionQueue, createQueueWorker, createTrackedJob, FailedJobRepository, FailedJobService, jobRegistry, QueueWorker, RedisQueue, ResilientQueue, runQueueJob, } from "../core/queue/publicQueue.ts";
74
80
  export type { ScheduledTask } from "../core/scheduler/schedule.ts";
75
81
  export { appSchedule, runDueScheduledTasks, Schedule } from "../core/scheduler/schedule.ts";
82
+ export { isPublicReadsEnabled } from "../core/security/publicReads.ts";
76
83
  export type { StorageDriver } from "../core/storage/storage.ts";
77
- export { LocalStorageDriver, StorageManager } from "../core/storage/storage.ts";
84
+ export { LocalStorageDriver, resetDefaultStorage, StorageManager, } from "../core/storage/storage.ts";
85
+ export { createTenantMiddleware } from "../core/tenant/tenantMiddleware.ts";
86
+ export { createTracingMiddleware } from "../core/tracing/tracingMiddleware.ts";
78
87
  export type { ValidationRule, ValidationSchema } from "../core/validation/rules.ts";
79
88
  export { emailRule, maxLength, minLength, required, stringRule, validateObject, } from "../core/validation/rules.ts";
80
89
  export { DEFAULT_VIEWS_DIRECTORY, EtaViewEngine, htmlResponse, isHtmxRequest, resolveWebLayoutData, } from "../core/view/index.ts";
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
  }
@@ -3729,8 +3717,14 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus) {
3729
3717
  if (tags.length === 0) {
3730
3718
  return;
3731
3719
  }
3732
- const cache = resolveApplicationCache();
3733
- const queue = resolveApplicationQueue();
3720
+ let cache;
3721
+ let queue;
3722
+ try {
3723
+ cache = resolveApplicationCache();
3724
+ queue = resolveApplicationQueue();
3725
+ } catch {
3726
+ return;
3727
+ }
3734
3728
  const job = createTrackedJob("cache.invalidate-tags", new invalidateCacheTagsJob_default(cache));
3735
3729
  await queue.dispatch(job, { tags });
3736
3730
  });
@@ -3840,9 +3834,6 @@ function isViewsEnabled() {
3840
3834
  // ../../src/core/http/requestMetaContext.ts
3841
3835
  import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
3842
3836
  var requestMetaContext = new AsyncLocalStorage3;
3843
- function runWithRequestMeta(meta, callback) {
3844
- return requestMetaContext.run(meta, callback);
3845
- }
3846
3837
  function currentRequestMeta() {
3847
3838
  return requestMetaContext.getStore() ?? {
3848
3839
  ipAddress: null,
@@ -3892,9 +3883,6 @@ function htmlResponse(html, init = {}) {
3892
3883
  }
3893
3884
  });
3894
3885
  }
3895
- // ../../src/core/http/csrfToken.ts
3896
- import { timingSafeEqual as timingSafeEqual2 } from "crypto";
3897
-
3898
3886
  // ../../src/core/http/cookies.ts
3899
3887
  function readRequestCookie(request, name) {
3900
3888
  const cookies = request.cookies;
@@ -3929,14 +3917,6 @@ function resolveCsrfSecret() {
3929
3917
  function csrfVerifyOptions() {
3930
3918
  return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
3931
3919
  }
3932
- function tokensMatch(left, right) {
3933
- const leftBuffer = Buffer.from(left);
3934
- const rightBuffer = Buffer.from(right);
3935
- if (leftBuffer.length !== rightBuffer.length) {
3936
- return false;
3937
- }
3938
- return timingSafeEqual2(leftBuffer, rightBuffer);
3939
- }
3940
3920
  function createCsrfTokenCookie() {
3941
3921
  const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
3942
3922
  return {
@@ -3951,45 +3931,6 @@ function resolveCsrfToken(request) {
3951
3931
  }
3952
3932
  return createCsrfTokenCookie();
3953
3933
  }
3954
- function readSubmittedCsrfToken(request) {
3955
- const headerToken = request.headers.get("x-csrf-token")?.trim();
3956
- if (headerToken) {
3957
- return headerToken;
3958
- }
3959
- return null;
3960
- }
3961
- async function readSubmittedCsrfTokenFromBody(request) {
3962
- const headerToken = readSubmittedCsrfToken(request);
3963
- if (headerToken) {
3964
- return headerToken;
3965
- }
3966
- const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
3967
- if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
3968
- const formData = await request.clone().formData();
3969
- const field = formData.get("_token");
3970
- if (typeof field === "string" && field.trim().length > 0) {
3971
- return field.trim();
3972
- }
3973
- const legacyField = formData.get("_csrf");
3974
- if (typeof legacyField === "string" && legacyField.trim().length > 0) {
3975
- return legacyField.trim();
3976
- }
3977
- }
3978
- return null;
3979
- }
3980
- function verifyCsrfToken(request, submittedToken) {
3981
- if (!submittedToken) {
3982
- return false;
3983
- }
3984
- const cookieValue = readRequestCookie(request, CSRF_COOKIE);
3985
- if (!cookieValue) {
3986
- return false;
3987
- }
3988
- if (!tokensMatch(submittedToken, cookieValue)) {
3989
- return false;
3990
- }
3991
- return Bun.CSRF.verify(submittedToken, csrfVerifyOptions());
3992
- }
3993
3934
  function resolveCsrfTokenForRequest(request) {
3994
3935
  const metaToken = currentRequestMeta().csrfToken;
3995
3936
  if (metaToken) {
@@ -3999,7 +3940,7 @@ function resolveCsrfTokenForRequest(request) {
3999
3940
  }
4000
3941
 
4001
3942
  // ../../src/core/http/flashSession.ts
4002
- import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual3 } from "crypto";
3943
+ import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual2 } from "crypto";
4003
3944
  var FLASH_COOKIE = "workhub_flash";
4004
3945
  var FLASH_TTL_MS = 60 * 1000;
4005
3946
  function resolveFlashSecret() {
@@ -4046,7 +3987,7 @@ function parseFlashCookie(cookieValue) {
4046
3987
  if (expectedBuffer.length !== actualBuffer.length) {
4047
3988
  return null;
4048
3989
  }
4049
- if (!timingSafeEqual3(expectedBuffer, actualBuffer)) {
3990
+ if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
4050
3991
  return null;
4051
3992
  }
4052
3993
  try {
@@ -4062,9 +4003,6 @@ function parseFlashCookie(cookieValue) {
4062
4003
  return null;
4063
4004
  }
4064
4005
  }
4065
- function clearFlashCookie() {
4066
- return `${FLASH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
4067
- }
4068
4006
  function pullFlash(request) {
4069
4007
  const cookieValue = readFlashCookie(request);
4070
4008
  if (!cookieValue) {
@@ -4072,15 +4010,6 @@ function pullFlash(request) {
4072
4010
  }
4073
4011
  return parseFlashCookie(cookieValue);
4074
4012
  }
4075
- function withFlashClear(response) {
4076
- const headers = new Headers(response.headers);
4077
- headers.append("set-cookie", clearFlashCookie());
4078
- return new Response(response.body, {
4079
- status: response.status,
4080
- statusText: response.statusText,
4081
- headers
4082
- });
4083
- }
4084
4013
 
4085
4014
  // ../../src/core/view/webLayoutData.ts
4086
4015
  async function resolveWebLayoutData(container, request) {
@@ -4272,17 +4201,6 @@ function composeMiddleware(...middleware) {
4272
4201
  };
4273
4202
  };
4274
4203
  }
4275
- async function requestIdMiddleware(request, next) {
4276
- const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
4277
- const response = await next();
4278
- const headers = new Headers(response.headers);
4279
- headers.set("x-request-id", requestId);
4280
- return new Response(response.body, {
4281
- status: response.status,
4282
- statusText: response.statusText,
4283
- headers
4284
- });
4285
- }
4286
4204
  function wrapRouteHandler(handler, middleware) {
4287
4205
  if (isMethodRouteMap(handler)) {
4288
4206
  const wrapped = {};
@@ -4304,6 +4222,32 @@ function applyMiddlewareToRoutes(routes, middleware) {
4304
4222
  return wrapped;
4305
4223
  }
4306
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
+
4307
4251
  // ../../src/config/rateLimit.ts
4308
4252
  var LOCAL_LOGIN_RATE_LIMIT = {
4309
4253
  maxAttempts: 100,
@@ -4338,849 +4282,6 @@ function resolveRegisterRateLimit() {
4338
4282
  };
4339
4283
  }
4340
4284
 
4341
- // ../../src/core/auth/membershipContext.ts
4342
- import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
4343
-
4344
- // ../../src/modules/organization/memberRepository.ts
4345
- class OrganizationMemberRepository {
4346
- constructor() {}
4347
- async findMembership(userId, organizationId) {
4348
- const rows = await connection_default`
4349
- SELECT id, organization_id, user_id, role, created_at
4350
- FROM organization_member
4351
- WHERE user_id = ${userId} AND organization_id = ${organizationId}
4352
- LIMIT 1
4353
- `;
4354
- return rows[0] ?? null;
4355
- }
4356
- async listForUser(userId) {
4357
- return await connection_default`
4358
- SELECT id, organization_id, user_id, role, created_at
4359
- FROM organization_member
4360
- WHERE user_id = ${userId}
4361
- ORDER BY organization_id
4362
- `;
4363
- }
4364
- async listForOrganization(organizationId) {
4365
- return await connection_default`
4366
- SELECT id, organization_id, user_id, role, created_at
4367
- FROM organization_member
4368
- WHERE organization_id = ${organizationId}
4369
- ORDER BY id
4370
- `;
4371
- }
4372
- async addMember(input) {
4373
- const rows = await connection_default`
4374
- INSERT INTO organization_member (organization_id, user_id, role)
4375
- VALUES (${input.organizationId}, ${input.userId}, ${input.role ?? "member"})
4376
- RETURNING id, organization_id, user_id, role, created_at
4377
- `;
4378
- const row = rows[0];
4379
- if (!row) {
4380
- throw new Error("Organization member insert did not return a row.");
4381
- }
4382
- return row;
4383
- }
4384
- async removeMember(organizationId, userId) {
4385
- const rows = await connection_default`
4386
- DELETE FROM organization_member
4387
- WHERE organization_id = ${organizationId} AND user_id = ${userId}
4388
- RETURNING id
4389
- `;
4390
- return rows.length > 0;
4391
- }
4392
- }
4393
- var memberRepository_default = OrganizationMemberRepository;
4394
-
4395
- // ../../src/core/auth/accessControl.ts
4396
- function isGlobalAdmin(user) {
4397
- return user?.role === "admin";
4398
- }
4399
- function resolveUserId(user) {
4400
- const userId = typeof user.id === "number" ? user.id : Number(user.id);
4401
- if (!Number.isInteger(userId) || userId <= 0) {
4402
- throw new ForbiddenError("Invalid authenticated user.");
4403
- }
4404
- return userId;
4405
- }
4406
-
4407
- // ../../src/core/auth/membershipContext.ts
4408
- var membershipContext = new AsyncLocalStorage4;
4409
- var membershipRepository = new memberRepository_default;
4410
- async function runWithMembershipContext(callback) {
4411
- const user = currentAuthUser();
4412
- if (!user || isGlobalAdmin(user)) {
4413
- return await callback();
4414
- }
4415
- const memberships = await membershipRepository.listForUser(resolveUserId(user));
4416
- const context = {
4417
- organizationIds: memberships.map((membership) => membership.organization_id),
4418
- rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
4419
- };
4420
- return await membershipContext.run(context, callback);
4421
- }
4422
-
4423
- // ../../src/core/auth/membershipContextMiddleware.ts
4424
- function createMembershipContextMiddleware() {
4425
- return async (_request, next) => {
4426
- return await runWithMembershipContext(async () => await next());
4427
- };
4428
- }
4429
-
4430
- // ../../src/core/auth/membershipMiddleware.ts
4431
- function createMembershipMiddleware() {
4432
- return createMembershipContextMiddleware();
4433
- }
4434
-
4435
- // ../../src/core/http/authMiddleware.ts
4436
- function createAuthMiddleware(auth) {
4437
- return async (request, next) => {
4438
- const user = await auth.resolve(request);
4439
- return await runWithAuthUser(user, async () => {
4440
- const response = await next();
4441
- if (user) {
4442
- const headers = new Headers(response.headers);
4443
- headers.set("x-authenticated-user-id", String(user.id));
4444
- return new Response(response.body, {
4445
- status: response.status,
4446
- statusText: response.statusText,
4447
- headers
4448
- });
4449
- }
4450
- return response;
4451
- });
4452
- };
4453
- }
4454
-
4455
- // ../../src/core/http/authorizeMiddleware.ts
4456
- function createAuthorizeMiddleware(gate, auth, resource, action) {
4457
- return async (request, next) => {
4458
- const user = await auth.resolve(request);
4459
- if (!gate.allows(resource, action, user)) {
4460
- const error = new ForbiddenError;
4461
- return Response.json({ error: error.message }, { status: error.status });
4462
- }
4463
- return await next();
4464
- };
4465
- }
4466
-
4467
- // ../../src/core/http/bodySizeLimitMiddleware.ts
4468
- var DEFAULT_MAX_BODY_BYTES = 1048576;
4469
- function resolveMaxBodyBytes() {
4470
- const raw = process.env.MAX_REQUEST_BODY_BYTES?.trim();
4471
- if (!raw) {
4472
- return DEFAULT_MAX_BODY_BYTES;
4473
- }
4474
- const parsed = Number.parseInt(raw, 10);
4475
- if (!Number.isInteger(parsed) || parsed <= 0) {
4476
- return DEFAULT_MAX_BODY_BYTES;
4477
- }
4478
- return parsed;
4479
- }
4480
- function createBodySizeLimitMiddleware(maxBytes = resolveMaxBodyBytes()) {
4481
- return async (request, next) => {
4482
- const contentLength = request.headers.get("content-length");
4483
- if (contentLength) {
4484
- const bytes = Number.parseInt(contentLength, 10);
4485
- if (Number.isInteger(bytes) && bytes > maxBytes) {
4486
- const error = new PayloadTooLargeError(`Request body exceeds the ${maxBytes} byte limit.`);
4487
- return Response.json({ error: error.message }, { status: error.status });
4488
- }
4489
- }
4490
- return await next();
4491
- };
4492
- }
4493
-
4494
- // ../../src/config/cors.ts
4495
- var corsConfig = {
4496
- allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim()).filter(Boolean),
4497
- allowedMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
4498
- allowedHeaders: [
4499
- "Authorization",
4500
- "Content-Type",
4501
- "X-Request-Id",
4502
- "X-Tenant-Id",
4503
- "X-Authenticated-User-Id",
4504
- "X-Authenticated-User-Role",
4505
- "If-Match",
4506
- "If-None-Match"
4507
- ],
4508
- maxAgeSeconds: 86400
4509
- };
4510
-
4511
- // ../../src/core/http/corsMiddleware.ts
4512
- function createCorsMiddleware() {
4513
- return async (request, next) => {
4514
- if (request.method === "OPTIONS") {
4515
- return new Response(null, {
4516
- status: 204,
4517
- headers: buildCorsHeaders(request)
4518
- });
4519
- }
4520
- const response = await next();
4521
- const headers = new Headers(response.headers);
4522
- for (const [key, value] of buildCorsHeaders(request)) {
4523
- headers.set(key, value);
4524
- }
4525
- return new Response(response.body, {
4526
- status: response.status,
4527
- statusText: response.statusText,
4528
- headers
4529
- });
4530
- };
4531
- }
4532
- function buildCorsHeaders(request) {
4533
- const headers = new Headers;
4534
- const origin = request.headers.get("origin");
4535
- const allowedOrigins = corsConfig.allowedOrigins;
4536
- const allowOrigin = allowedOrigins.includes("*") || origin && allowedOrigins.includes(origin) ? origin ?? "*" : allowedOrigins[0] ?? "*";
4537
- headers.set("Access-Control-Allow-Origin", allowOrigin);
4538
- headers.set("Access-Control-Allow-Methods", corsConfig.allowedMethods.join(", "));
4539
- headers.set("Access-Control-Allow-Headers", corsConfig.allowedHeaders.join(", "));
4540
- headers.set("Access-Control-Max-Age", String(corsConfig.maxAgeSeconds));
4541
- headers.set("Vary", "Origin");
4542
- return headers;
4543
- }
4544
-
4545
- // ../../src/core/http/csrfMiddleware.ts
4546
- var MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
4547
- function appendSetCookie(response, cookie) {
4548
- const headers = new Headers(response.headers);
4549
- headers.append("set-cookie", cookie);
4550
- return new Response(response.body, {
4551
- status: response.status,
4552
- statusText: response.statusText,
4553
- headers
4554
- });
4555
- }
4556
- function createCsrfMiddleware() {
4557
- return async (request, next) => {
4558
- const method = request.method.toUpperCase();
4559
- if (!MUTATING_METHODS.has(method)) {
4560
- const csrf = resolveCsrfToken(request);
4561
- const meta = currentRequestMeta();
4562
- meta.csrfToken = csrf.token;
4563
- const response = await next();
4564
- if (!csrf.cookie) {
4565
- return response;
4566
- }
4567
- return appendSetCookie(response, csrf.cookie);
4568
- }
4569
- const submitted = await readSubmittedCsrfTokenFromBody(request);
4570
- if (!verifyCsrfToken(request, submitted)) {
4571
- throw new ForbiddenError("Invalid or missing CSRF token.");
4572
- }
4573
- return await next();
4574
- };
4575
- }
4576
-
4577
- // ../../src/core/http/flashMiddleware.ts
4578
- function createFlashMiddleware() {
4579
- return async (request, next) => {
4580
- const flash = pullFlash(request);
4581
- const meta = currentRequestMeta();
4582
- return await runWithRequestMeta({ ...meta, request, flash }, async () => {
4583
- const response = await next();
4584
- if (flash) {
4585
- return withFlashClear(response);
4586
- }
4587
- return response;
4588
- });
4589
- };
4590
- }
4591
-
4592
- // ../../src/core/http/loginThrottleMiddleware.ts
4593
- var {RedisClient: RedisClient3 } = globalThis.Bun;
4594
- function resolveLoginIdentity(request) {
4595
- return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
4596
- }
4597
- async function resolveLoginEmail(request) {
4598
- const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
4599
- try {
4600
- if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
4601
- const formData = await request.clone().formData();
4602
- const email = formData.get("email");
4603
- return typeof email === "string" ? email.trim().toLowerCase() : "unknown";
4604
- }
4605
- const payload = await request.clone().json();
4606
- return typeof payload.email === "string" ? payload.email.trim().toLowerCase() : "unknown";
4607
- } catch {
4608
- return "unknown";
4609
- }
4610
- }
4611
- function createLoginThrottleMiddleware(options) {
4612
- const client = new RedisClient3(options.redisUrl);
4613
- const prefix = options.keyPrefix ?? "workhub:login-throttle:";
4614
- return async (request, next) => {
4615
- const identity = resolveLoginIdentity(request);
4616
- const email = await resolveLoginEmail(request);
4617
- const throttleKey = `${prefix}${identity}:${email}`;
4618
- const attempts = Number(await client.incr(throttleKey));
4619
- if (attempts === 1) {
4620
- await client.expire(throttleKey, options.decaySeconds);
4621
- }
4622
- if (attempts > options.maxAttempts) {
4623
- return Response.json({ error: "Too many login attempts. Try again later." }, {
4624
- status: 429,
4625
- headers: {
4626
- "retry-after": String(options.decaySeconds)
4627
- }
4628
- });
4629
- }
4630
- return await next();
4631
- };
4632
- }
4633
-
4634
- // ../../src/core/http/memoryThrottleMiddleware.ts
4635
- var buckets = new Map;
4636
- function createMemoryThrottleMiddleware(options) {
4637
- const prefix = options.keyPrefix ?? "workhub:memory-throttle:";
4638
- return async (request, next) => {
4639
- const path = new URL(request.url).pathname;
4640
- const identity = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("authorization")?.slice(0, 32) ?? "unknown";
4641
- const key = `${prefix}${identity}:${path}`;
4642
- const now = Date.now();
4643
- const existing = buckets.get(key);
4644
- if (!existing || existing.resetAt <= now) {
4645
- buckets.set(key, { count: 1, resetAt: now + options.decaySeconds * 1000 });
4646
- return await next();
4647
- }
4648
- existing.count += 1;
4649
- if (existing.count > options.maxAttempts) {
4650
- return Response.json({ error: "Too many requests." }, {
4651
- status: 429,
4652
- headers: {
4653
- "retry-after": String(options.decaySeconds)
4654
- }
4655
- });
4656
- }
4657
- return await next();
4658
- };
4659
- }
4660
-
4661
- // ../../src/core/metrics/prometheus.ts
4662
- class PrometheusRegistry {
4663
- httpRequestsTotal = new Map;
4664
- httpRequestDurationMs = new Map;
4665
- incrementHttpRequest(labels) {
4666
- const key = this.metricKey(labels);
4667
- this.httpRequestsTotal.set(key, (this.httpRequestsTotal.get(key) ?? 0) + 1);
4668
- }
4669
- observeHttpDuration(labels, durationMs) {
4670
- const key = this.metricKey(labels);
4671
- const samples = this.httpRequestDurationMs.get(key) ?? [];
4672
- samples.push(durationMs);
4673
- this.httpRequestDurationMs.set(key, samples);
4674
- }
4675
- renderMetrics() {
4676
- const lines = [
4677
- "# HELP http_requests_total Total HTTP requests processed.",
4678
- "# TYPE http_requests_total counter"
4679
- ];
4680
- for (const [key, value] of this.httpRequestsTotal) {
4681
- lines.push(`http_requests_total{${key}} ${value}`);
4682
- }
4683
- lines.push("# HELP http_request_duration_ms_sum Sum of HTTP request durations in milliseconds.", "# TYPE http_request_duration_ms_sum counter");
4684
- for (const [key, samples] of this.httpRequestDurationMs) {
4685
- const sum = samples.reduce((total, sample) => total + sample, 0);
4686
- lines.push(`http_request_duration_ms_sum{${key}} ${sum}`);
4687
- }
4688
- return `${lines.join(`
4689
- `)}
4690
- `;
4691
- }
4692
- resetForTests() {
4693
- this.httpRequestsTotal.clear();
4694
- this.httpRequestDurationMs.clear();
4695
- }
4696
- getHttpRequestSummary() {
4697
- const byStatus = {};
4698
- const pathCounts = new Map;
4699
- let totalRequests = 0;
4700
- for (const [key, count] of this.httpRequestsTotal) {
4701
- totalRequests += count;
4702
- const method = key.match(/method="([^"]+)"/)?.[1] ?? "GET";
4703
- const path = key.match(/path="([^"]+)"/)?.[1] ?? "/";
4704
- const status = key.match(/status="([^"]+)"/)?.[1] ?? "200";
4705
- byStatus[status] = (byStatus[status] ?? 0) + count;
4706
- const pathKey = `${method} ${path}`;
4707
- const existing = pathCounts.get(pathKey);
4708
- if (existing) {
4709
- existing.count += count;
4710
- } else {
4711
- pathCounts.set(pathKey, { method, path, count });
4712
- }
4713
- }
4714
- const topPaths = Array.from(pathCounts.values()).sort((left, right) => right.count - left.count).slice(0, 10);
4715
- return {
4716
- totalRequests,
4717
- byStatus,
4718
- topPaths
4719
- };
4720
- }
4721
- metricKey(labels) {
4722
- return `method="${labels.method}",path="${labels.path}",status="${labels.status}"`;
4723
- }
4724
- }
4725
- var prometheusRegistry = new PrometheusRegistry;
4726
-
4727
- // ../../src/core/http/metricsMiddleware.ts
4728
- function normalizeMetricPath(pathname) {
4729
- return pathname.replace(/\/\d+/g, "/:id").replace(/\/[0-9a-f-]{36}/gi, "/:id");
4730
- }
4731
- function createMetricsMiddleware() {
4732
- return async (request, next) => {
4733
- const startedAt = performance.now();
4734
- const response = await next();
4735
- const durationMs = performance.now() - startedAt;
4736
- const path = normalizeMetricPath(new URL(request.url).pathname);
4737
- const labels = {
4738
- method: request.method,
4739
- path,
4740
- status: String(response.status)
4741
- };
4742
- prometheusRegistry.incrementHttpRequest(labels);
4743
- prometheusRegistry.observeHttpDuration(labels, durationMs);
4744
- return response;
4745
- };
4746
- }
4747
-
4748
- // ../../src/core/http/requireAbilityMiddleware.ts
4749
- function createRequireAbilityMiddleware(abilityChecker) {
4750
- return (ability) => {
4751
- return async (_request, next) => {
4752
- const user = currentAuthUser();
4753
- try {
4754
- abilityChecker.requireAbility(user, ability);
4755
- } catch (error) {
4756
- if (error instanceof ForbiddenError) {
4757
- return Response.json({ error: error.message }, { status: error.status });
4758
- }
4759
- throw error;
4760
- }
4761
- return await next();
4762
- };
4763
- };
4764
- }
4765
-
4766
- // ../../src/core/http/requireAuthMiddleware.ts
4767
- function createRequireAuthMiddleware(auth) {
4768
- return async (request, next) => {
4769
- if (!await auth.check(request)) {
4770
- const error = new UnauthorizedError;
4771
- return Response.json({ error: error.message }, { status: error.status });
4772
- }
4773
- return await next();
4774
- };
4775
- }
4776
-
4777
- // ../../src/core/security/securityEvents.ts
4778
- function logSecurityEvent2(event, details = {}) {
4779
- const meta = currentRequestMeta();
4780
- const user = currentAuthUser();
4781
- console.log(JSON.stringify({
4782
- level: "security",
4783
- event,
4784
- timestamp: new Date().toISOString(),
4785
- ip_address: meta.ipAddress ?? null,
4786
- user_agent: meta.userAgent ?? null,
4787
- user_id: user?.id ?? null,
4788
- ...details
4789
- }));
4790
- }
4791
-
4792
- // ../../src/core/http/requireGlobalAdminMiddleware.ts
4793
- function createRequireGlobalAdminMiddleware() {
4794
- return async (_request, next) => {
4795
- const user = currentAuthUser();
4796
- if (!isGlobalAdmin(user)) {
4797
- logSecurityEvent2("privilege_escalation_blocked", {
4798
- required_role: "platform_admin",
4799
- path: new URL(_request.url).pathname
4800
- });
4801
- const error = new ForbiddenError("Platform admin access required.");
4802
- return Response.json({ error: error.message }, { status: error.status });
4803
- }
4804
- return await next();
4805
- };
4806
- }
4807
-
4808
- // ../../src/core/http/contentNegotiation.ts
4809
- function requestPrefersJson(request) {
4810
- if (!request) {
4811
- return true;
4812
- }
4813
- if (request.headers.get("HX-Request") === "true") {
4814
- return false;
4815
- }
4816
- const accept = request.headers.get("accept")?.toLowerCase() ?? "";
4817
- if (accept.includes("text/html")) {
4818
- return false;
4819
- }
4820
- if (accept.includes("application/json")) {
4821
- return true;
4822
- }
4823
- const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
4824
- if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
4825
- return false;
4826
- }
4827
- const pathname = new URL(request.url).pathname;
4828
- return pathname.startsWith("/api/");
4829
- }
4830
-
4831
- // ../../src/core/http/requireWebAuthMiddleware.ts
4832
- function createRequireWebAuthMiddleware(auth) {
4833
- return async (request, next) => {
4834
- const user = await auth.resolve(request);
4835
- if (user) {
4836
- return await next();
4837
- }
4838
- if (requestPrefersJson(request)) {
4839
- throw new UnauthorizedError;
4840
- }
4841
- const redirectTarget = encodeURIComponent(new URL(request.url).pathname);
4842
- return Response.redirect(`/login?redirect=${redirectTarget}`, 302);
4843
- };
4844
- }
4845
-
4846
- // ../../src/core/http/routeMiddleware.ts
4847
- function withMiddleware(...middleware) {
4848
- const wrap = composeMiddleware(...middleware);
4849
- return (handler) => {
4850
- return wrap(handler);
4851
- };
4852
- }
4853
-
4854
- // ../../src/config/contentSecurityPolicy.ts
4855
- function strictApiContentSecurityPolicy() {
4856
- return "default-src 'none'; frame-ancestors 'none'; base-uri 'none'";
4857
- }
4858
- function serverHtmxContentSecurityPolicy() {
4859
- return [
4860
- "default-src 'self'",
4861
- "script-src 'self' https://unpkg.com",
4862
- "style-src 'self'",
4863
- "connect-src 'self'",
4864
- "img-src 'self'",
4865
- "font-src 'self'",
4866
- "form-action 'self'",
4867
- "frame-ancestors 'none'",
4868
- "base-uri 'self'"
4869
- ].join("; ");
4870
- }
4871
- function spaContentSecurityPolicy() {
4872
- return [
4873
- "default-src 'self'",
4874
- "script-src 'self'",
4875
- "style-src 'self'",
4876
- "connect-src 'self'",
4877
- "img-src 'self'",
4878
- "font-src 'self'",
4879
- "frame-ancestors 'none'",
4880
- "base-uri 'self'"
4881
- ].join("; ");
4882
- }
4883
- function resolveContentSecurityPolicy(response) {
4884
- const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
4885
- if (!contentType.includes("text/html")) {
4886
- return strictApiContentSecurityPolicy();
4887
- }
4888
- const frontendMode = (process.env.FRONTEND_MODE ?? "api").trim();
4889
- if (frontendMode === "server-htmx") {
4890
- return serverHtmxContentSecurityPolicy();
4891
- }
4892
- if (frontendMode === "spa-react") {
4893
- return spaContentSecurityPolicy();
4894
- }
4895
- return strictApiContentSecurityPolicy();
4896
- }
4897
-
4898
- // ../../src/core/http/securityHeadersMiddleware.ts
4899
- function createSecurityHeadersMiddleware() {
4900
- return async (_request, next) => {
4901
- const response = await next();
4902
- const headers = new Headers(response.headers);
4903
- headers.set("X-Content-Type-Options", "nosniff");
4904
- headers.set("X-Frame-Options", "DENY");
4905
- headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
4906
- headers.set("X-XSS-Protection", "0");
4907
- headers.set("Content-Security-Policy", resolveContentSecurityPolicy(response));
4908
- if (appConfig.env === "production") {
4909
- headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
4910
- }
4911
- return new Response(response.body, {
4912
- status: response.status,
4913
- statusText: response.statusText,
4914
- headers
4915
- });
4916
- };
4917
- }
4918
-
4919
- // ../../src/core/http/throttleMiddleware.ts
4920
- var {RedisClient: RedisClient4 } = globalThis.Bun;
4921
-
4922
- // ../../src/core/tenant/tenantContext.ts
4923
- import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
4924
- var tenantContext = new AsyncLocalStorage5;
4925
- function runWithTenant(tenant, callback) {
4926
- return tenantContext.run(tenant, callback);
4927
- }
4928
- function currentTenant() {
4929
- return tenantContext.getStore() ?? null;
4930
- }
4931
- function rateLimitMultiplierForPlan(plan) {
4932
- switch (plan) {
4933
- case "enterprise":
4934
- return 4;
4935
- case "pro":
4936
- return 2;
4937
- default:
4938
- return 1;
4939
- }
4940
- }
4941
-
4942
- // ../../src/core/http/throttleMiddleware.ts
4943
- function resolveThrottleIdentity(request) {
4944
- const user = currentAuthUser();
4945
- if (user?.tokenId !== undefined) {
4946
- return `token:${user.tokenId}`;
4947
- }
4948
- if (user) {
4949
- return `user:${user.id}`;
4950
- }
4951
- return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
4952
- }
4953
- function createThrottleMiddleware(options) {
4954
- const client = new RedisClient4(options.redisUrl);
4955
- const prefix = options.keyPrefix ?? "workhub:throttle:";
4956
- return async (request, next) => {
4957
- const identity = resolveThrottleIdentity(request);
4958
- const path = new URL(request.url).pathname;
4959
- const throttleKey = `${prefix}${identity}:${path}`;
4960
- const attempts = Number(await client.incr(throttleKey));
4961
- if (attempts === 1) {
4962
- await client.expire(throttleKey, options.decaySeconds);
4963
- }
4964
- const maxAttempts = options.maxAttempts * rateLimitMultiplierForPlan(currentTenant()?.plan ?? "free");
4965
- if (attempts > maxAttempts) {
4966
- return Response.json({ error: "Too many requests." }, {
4967
- status: 429,
4968
- headers: {
4969
- "retry-after": String(options.decaySeconds)
4970
- }
4971
- });
4972
- }
4973
- return await next();
4974
- };
4975
- }
4976
-
4977
- // ../../src/core/logging/requestLoggingMiddleware.ts
4978
- function createRequestLoggingMiddleware() {
4979
- return async (request, next) => {
4980
- return await runWithRequestMeta({
4981
- ipAddress: request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip"),
4982
- userAgent: request.headers.get("user-agent"),
4983
- request
4984
- }, async () => {
4985
- const startedAt = performance.now();
4986
- const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
4987
- const response = await next();
4988
- const durationMs = Math.round(performance.now() - startedAt);
4989
- appLogger.info("HTTP request completed", {
4990
- requestId,
4991
- method: request.method,
4992
- path: new URL(request.url).pathname,
4993
- status: response.status,
4994
- durationMs
4995
- });
4996
- return response;
4997
- });
4998
- };
4999
- }
5000
-
5001
- // ../../src/core/security/publicReads.ts
5002
- function isPublicReadsEnabled() {
5003
- return isFeatureEnabled("publicReads");
5004
- }
5005
-
5006
- // ../../src/core/tenant/resolveTenant.ts
5007
- async function resolveTenant(tenantId) {
5008
- const rows = await connection_default`
5009
- SELECT id, slug, plan, region
5010
- FROM tenant
5011
- WHERE id = ${tenantId}
5012
- LIMIT 1
5013
- `;
5014
- const row = rows[0];
5015
- return row ? { id: row.id, slug: row.slug, plan: row.plan, region: row.region ?? "eu" } : null;
5016
- }
5017
-
5018
- // ../../src/core/tenant/tenantDatabaseScope.ts
5019
- async function applyTenantContextToTransaction(transaction, tenantId) {
5020
- await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
5021
- await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
5022
- }
5023
- async function runWithTenantDatabase(tenant, callback) {
5024
- return await getDatabase().begin(async (transaction) => {
5025
- await applyTenantContextToTransaction(transaction, tenant.id);
5026
- return await runWithDatabaseConnection(transaction, async () => {
5027
- return await runWithTenant(tenant, callback);
5028
- });
5029
- });
5030
- }
5031
-
5032
- // ../../src/core/tenant/tenantMiddleware.ts
5033
- var DEFAULT_TENANT = {
5034
- id: 1,
5035
- slug: "default",
5036
- plan: "enterprise",
5037
- region: "eu"
5038
- };
5039
- async function resolveUserTenantId(userId) {
5040
- return await runWithMigrationBypass(async () => {
5041
- const rows = await connection_default`
5042
- SELECT tenant_id
5043
- FROM users
5044
- WHERE id = ${userId}
5045
- LIMIT 1
5046
- `;
5047
- return rows[0]?.tenant_id ?? DEFAULT_TENANT.id;
5048
- });
5049
- }
5050
- async function resolveTenantForRequest(request) {
5051
- const user = currentAuthUser();
5052
- const headerValue = request.headers.get("x-tenant-id")?.trim();
5053
- const parsedHeader = headerValue !== undefined && headerValue.length > 0 ? Number.parseInt(headerValue, 10) : Number.NaN;
5054
- if (user) {
5055
- const userId = typeof user.id === "number" ? user.id : Number.parseInt(String(user.id), 10);
5056
- if (Number.isInteger(userId) && userId > 0) {
5057
- const userTenantId = await resolveUserTenantId(userId);
5058
- if (!isGlobalAdmin(user)) {
5059
- if (Number.isInteger(parsedHeader) && parsedHeader > 0 && parsedHeader !== userTenantId) {
5060
- throw new ForbiddenError("Tenant header does not match your account.");
5061
- }
5062
- return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
5063
- }
5064
- if (Number.isInteger(parsedHeader) && parsedHeader > 0) {
5065
- return await resolveTenant(parsedHeader) ?? DEFAULT_TENANT;
5066
- }
5067
- return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
5068
- }
5069
- }
5070
- const tenantId = Number.isInteger(parsedHeader) && parsedHeader > 0 ? parsedHeader : DEFAULT_TENANT.id;
5071
- return await resolveTenant(tenantId) ?? DEFAULT_TENANT;
5072
- }
5073
- function createTenantMiddleware() {
5074
- return async (request, next) => {
5075
- try {
5076
- const tenant = await resolveTenantForRequest(request);
5077
- return await runWithTenantDatabase(tenant, async () => {
5078
- const response = await next();
5079
- const headers = new Headers(response.headers);
5080
- headers.set("x-tenant-id", String(tenant.id));
5081
- headers.set("x-tenant-region", tenant.region);
5082
- return new Response(response.body, {
5083
- status: response.status,
5084
- statusText: response.statusText,
5085
- headers
5086
- });
5087
- });
5088
- } catch (error) {
5089
- if (error instanceof HttpError) {
5090
- return Response.json({ error: error.message }, { status: error.status });
5091
- }
5092
- throw error;
5093
- }
5094
- };
5095
- }
5096
-
5097
- // ../../src/core/tracing/otel.ts
5098
- import { randomBytes } from "crypto";
5099
- function randomHex(bytes) {
5100
- return randomBytes(bytes).toString("hex");
5101
- }
5102
- function createSpan(input) {
5103
- const spanId = randomHex(8);
5104
- return {
5105
- traceId: input.traceId,
5106
- spanId,
5107
- name: input.name,
5108
- startTimeUnixNano: String(Math.floor(input.startedAt * 1e6)),
5109
- endTimeUnixNano: String(Math.floor(input.endedAt * 1e6)),
5110
- attributes: Object.entries(input.attributes ?? {}).map(([key, value]) => ({
5111
- key,
5112
- value: { stringValue: value }
5113
- })),
5114
- status: { code: 1 }
5115
- };
5116
- }
5117
- async function exportOtelSpan(span) {
5118
- const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
5119
- if (!endpoint) {
5120
- return;
5121
- }
5122
- const serviceName = process.env.OTEL_SERVICE_NAME?.trim() ?? "workhub-api";
5123
- const url = endpoint.endsWith("/v1/traces") ? endpoint : `${endpoint.replace(/\/$/, "")}/v1/traces`;
5124
- await fetch(url, {
5125
- method: "POST",
5126
- headers: { "content-type": "application/json" },
5127
- body: JSON.stringify({
5128
- resourceSpans: [
5129
- {
5130
- resource: {
5131
- attributes: [{ key: "service.name", value: { stringValue: serviceName } }]
5132
- },
5133
- scopeSpans: [{ spans: [span] }]
5134
- }
5135
- ]
5136
- })
5137
- });
5138
- }
5139
-
5140
- // ../../src/core/tracing/traceContext.ts
5141
- import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
5142
- var traceContextStorage = new AsyncLocalStorage6;
5143
- function runWithTraceContext(context, callback) {
5144
- return traceContextStorage.run(context, callback);
5145
- }
5146
-
5147
- // ../../src/core/tracing/tracingMiddleware.ts
5148
- function createTracingMiddleware() {
5149
- return async (request, next) => {
5150
- const traceId = (request.headers.get("x-trace-id") ?? crypto.randomUUID()).replace(/-/g, "");
5151
- const spanId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
5152
- const startedAt = performance.now();
5153
- const path = new URL(request.url).pathname;
5154
- return await runWithTraceContext({ traceId, spanId }, async () => {
5155
- const response = await next();
5156
- const endedAt = performance.now();
5157
- const headers = new Headers(response.headers);
5158
- headers.set("x-trace-id", traceId);
5159
- headers.set("x-span-id", spanId);
5160
- headers.set("traceparent", `00-${traceId}-${spanId}-01`);
5161
- headers.set("server-timing", `app;dur=${(endedAt - startedAt).toFixed(2)}`);
5162
- exportOtelSpan(createSpan({
5163
- traceId,
5164
- name: `${request.method} ${path}`,
5165
- startedAt,
5166
- endedAt,
5167
- attributes: {
5168
- "http.method": request.method,
5169
- "http.route": path,
5170
- "http.status_code": String(response.status)
5171
- }
5172
- })).catch(() => {
5173
- return;
5174
- });
5175
- return new Response(response.body, {
5176
- status: response.status,
5177
- statusText: response.statusText,
5178
- headers
5179
- });
5180
- });
5181
- };
5182
- }
5183
-
5184
4285
  // ../../src/bootstrap/httpKernel.ts
5185
4286
  class HttpKernel {
5186
4287
  dependencies;
@@ -5576,7 +4677,7 @@ function createWebServer(options) {
5576
4677
  });
5577
4678
  }
5578
4679
  // ../../src/bootstrap/web/session.ts
5579
- import { createHash, randomBytes as randomBytes2 } from "crypto";
4680
+ import { createHash, randomBytes } from "crypto";
5580
4681
  class CookieSessionStore {
5581
4682
  sql;
5582
4683
  secret;
@@ -5602,7 +4703,7 @@ class CookieSessionStore {
5602
4703
  return header.includes("Secure") ? header : `${header}; Secure`;
5603
4704
  }
5604
4705
  async create(user) {
5605
- const id = randomBytes2(32).toString("hex");
4706
+ const id = randomBytes(32).toString("hex");
5606
4707
  const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
5607
4708
  await this.sql.unsafe(`INSERT INTO sessions (id, user_id, expires_at) VALUES ($1, $2, $3)`, [
5608
4709
  id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/bootstrap",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Strata application bootstrap — HttpKernel, DI, web session helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -73,7 +73,7 @@
73
73
  "access": "public"
74
74
  },
75
75
  "peerDependencies": {
76
- "@getstrata/core": "^0.5.9",
76
+ "@getstrata/core": "^0.5.14",
77
77
  "typescript": "^5.9.0"
78
78
  }
79
79
  }