@getstrata/core 0.5.13 → 0.5.14

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
@@ -3,7 +3,7 @@
3
3
  Stable **Strata** framework surface for application modules.
4
4
 
5
5
  **Source:** `src/framework/public-api.ts` (monorepo)
6
- **Repository:** [EyK-26/strata](https://github.com/EyK-26/strata) directory `packages/strata-core`
6
+ **Repository:** [EyK-26/strata](https://github.com/EyK-26/strata), directory `packages/strata-core`
7
7
 
8
8
  WorkHub is the reference application built on Strata; import the framework from this package in your own modules.
9
9
 
@@ -15,21 +15,31 @@ Set `DATABASE_URL` before importing (connection is created lazily on first query
15
15
  process.env.DATABASE_URL ??= "postgresql://postgres:postgres@localhost:5432/myapp";
16
16
 
17
17
  import {
18
+ AdminResourceRegistry,
18
19
  BaseRepository,
19
20
  EtaViewEngine,
20
21
  FormRequest,
21
22
  Policy,
23
+ formatAdminValue,
22
24
  mailer,
23
25
  storage,
24
26
  withErrorHandling,
25
27
  } from "@getstrata/core";
26
28
  ```
27
29
 
28
- **Dependency:** `eta` is bundled as a direct dependency of `@getstrata/core` apps do not need to list it separately. The database driver is your app's choice WorkHub and getstrata use **Bun's built-in `Bun.sql`** client; bind it with `bindDatabaseConnection()`.
30
+ **Dependency:** `eta` is bundled as a direct dependency of `@getstrata/core`. Apps do not need to list it separately. The database driver is your app's choice. WorkHub and getstrata use **Bun's built-in `Bun.sql`** client; bind it with `bindDatabaseConnection()`.
29
31
 
30
- `orderBy` accepts explicit `{ column, direction }` objects or Laravel-style shorthand `{ published_at: "desc" }`.
32
+ `orderBy` accepts explicit `{ column, direction }` objects or column shorthand such as `{ published_at: "desc" }`.
31
33
 
32
- ## Build & verify (monorepo root)
34
+ ## Admin and queue helpers
35
+
36
+ Exports for admin dashboards and queue recovery:
37
+
38
+ - `AdminResourceRegistry`, `formatAdminValue`: read-only resource browsers
39
+ - `createFailedJobService`, `FailedJobService.delete()`: failed job persistence and cleanup
40
+ - `runQueueJob`, `jobRegistry`: dispatch retried jobs from admin UIs
41
+
42
+ ## Build and verify (monorepo root)
33
43
 
34
44
  ```bash
35
45
  bun run build:framework
@@ -41,9 +51,9 @@ bun run verify:framework # build + public API tests
41
51
  Package name: **`@getstrata/core`** (npm org [`@getstrata`](https://www.npmjs.com/org/getstrata)).
42
52
 
43
53
  1. Add `NPM_TOKEN` to GitHub repository secrets.
44
- 2. Tag a release: `git tag v0.2.0 && git push origin v0.2.0`
54
+ 2. Tag a release: `git tag v0.5.13 && git push origin v0.5.13`
45
55
  3. [Release workflow](../../.github/workflows/release.yml) builds and runs `npm publish --access public`.
46
56
 
47
- Previously published as `@eyk-workhub/framework@0.1.0` deprecated in favor of this package.
57
+ Previously published as `@eyk-workhub/framework@0.1.0`, deprecated in favor of this package.
48
58
 
49
59
  See [docs/PACKAGING.md](../../docs/PACKAGING.md) for boundaries and future extraction.
@@ -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,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, };
@@ -9,8 +9,11 @@ class LocalStorageDriver {
9
9
  constructor(rootDirectory) {
10
10
  this.rootDirectory = rootDirectory;
11
11
  }
12
+ resolveRootDirectory() {
13
+ return this.rootDirectory ?? process.env.STORAGE_PATH ?? "storage";
14
+ }
12
15
  resolvePath(path) {
13
- return join(this.rootDirectory, path.replace(/^\/+/, ""));
16
+ return join(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
14
17
  }
15
18
  async put(path, contents) {
16
19
  const absolutePath = this.resolvePath(path);
@@ -106,15 +109,22 @@ function createStorageDriver() {
106
109
  if (driver === "s3") {
107
110
  return new S3StorageDriver(createS3Client());
108
111
  }
109
- return new LocalStorageDriver(process.env.STORAGE_PATH ?? "storage");
112
+ return new LocalStorageDriver;
110
113
  }
111
- var defaultStorage = new StorageManager(createStorageDriver());
114
+ var defaultStorage = { current: null };
112
115
  function storage() {
113
- return defaultStorage;
116
+ if (!defaultStorage.current) {
117
+ defaultStorage.current = new StorageManager(createStorageDriver());
118
+ }
119
+ return defaultStorage.current;
120
+ }
121
+ function resetDefaultStorage() {
122
+ defaultStorage.current = null;
114
123
  }
115
124
  export {
116
125
  storage,
117
126
  resolveS3Config,
127
+ resetDefaultStorage,
118
128
  createStorageDriver,
119
129
  createS3Client,
120
130
  StorageManager,
@@ -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
@@ -180,6 +180,7 @@ function formatAdminValue(value, type = "text") {
180
180
  // ../../src/core/admin/registry.ts
181
181
  class AdminResourceRegistry {
182
182
  resources = new Map;
183
+ constructor() {}
183
184
  register(resource) {
184
185
  if (this.resources.has(resource.name)) {
185
186
  throw new Error(`Admin resource "${resource.name}" is already registered.`);
@@ -190,7 +191,12 @@ class AdminResourceRegistry {
190
191
  return this.resources.get(name);
191
192
  }
192
193
  list() {
193
- return [...this.resources.values()].map(({ handlers: _handlers, ...definition }) => definition);
194
+ const definitions = [];
195
+ for (const resource of this.resources.values()) {
196
+ const { handlers: _handlers, ...definition } = resource;
197
+ definitions.push(definition);
198
+ }
199
+ return definitions;
194
200
  }
195
201
  all() {
196
202
  return [...this.resources.values()];
@@ -208,6 +214,130 @@ function runWithAuthUser(user, callback) {
208
214
  function currentAuthUser() {
209
215
  return authContext.getStore() ?? null;
210
216
  }
217
+ // ../../src/core/auth/membershipContext.ts
218
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
219
+
220
+ // ../../src/config/database.ts
221
+ function readInteger(name, fallback) {
222
+ const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
223
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
224
+ }
225
+ var databaseConfig = {
226
+ url: process.env.DATABASE_URL ?? "",
227
+ poolMax: readInteger("DB_POOL_MAX", 10),
228
+ idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
229
+ maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
230
+ connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
231
+ };
232
+
233
+ // ../../src/core/database/connectionContext.ts
234
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
235
+ var activeConnection = new AsyncLocalStorage2;
236
+ function runWithDatabaseConnection(connection, callback) {
237
+ return activeConnection.run(connection, callback);
238
+ }
239
+ function getActiveDatabaseConnection(fallback) {
240
+ return activeConnection.getStore() ?? fallback;
241
+ }
242
+
243
+ // ../../src/db/connection/createConnection.ts
244
+ var {SQL } = globalThis.Bun;
245
+ function createDatabaseConnection(config) {
246
+ if (!config.url) {
247
+ throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
248
+ }
249
+ return new SQL({
250
+ url: config.url,
251
+ max: config.poolMax,
252
+ idleTimeout: config.idleTimeoutSeconds,
253
+ maxLifetime: config.maxLifetimeSeconds,
254
+ connectionTimeout: config.connectionTimeoutSeconds
255
+ });
256
+ }
257
+
258
+ // ../../src/db/connection/index.ts
259
+ var connectionHolder = {
260
+ connection: null
261
+ };
262
+ function getDatabase() {
263
+ if (!connectionHolder.connection) {
264
+ connectionHolder.connection = createDatabaseConnection(databaseConfig);
265
+ }
266
+ return connectionHolder.connection;
267
+ }
268
+ var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
269
+ function resolveDatabase() {
270
+ return getActiveDatabaseConnection(getDatabase());
271
+ }
272
+ function resolveDatabaseForProperty(property) {
273
+ if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
274
+ return getDatabase();
275
+ }
276
+ return resolveDatabase();
277
+ }
278
+ var db = new Proxy(function database() {}, {
279
+ apply(_target, _thisArg, args) {
280
+ return resolveDatabase()(...args);
281
+ },
282
+ get(_target, property) {
283
+ const connection = resolveDatabaseForProperty(property);
284
+ const value = connection[property];
285
+ return typeof value === "function" ? value.bind(connection) : value;
286
+ }
287
+ });
288
+ var connection_default = db;
289
+
290
+ // ../../src/modules/organization/memberRepository.ts
291
+ class OrganizationMemberRepository {
292
+ constructor() {}
293
+ async findMembership(userId, organizationId) {
294
+ const rows = await connection_default`
295
+ SELECT id, organization_id, user_id, role, created_at
296
+ FROM organization_member
297
+ WHERE user_id = ${userId} AND organization_id = ${organizationId}
298
+ LIMIT 1
299
+ `;
300
+ return rows[0] ?? null;
301
+ }
302
+ async listForUser(userId) {
303
+ return await connection_default`
304
+ SELECT id, organization_id, user_id, role, created_at
305
+ FROM organization_member
306
+ WHERE user_id = ${userId}
307
+ ORDER BY organization_id
308
+ `;
309
+ }
310
+ async listForOrganization(organizationId) {
311
+ return await connection_default`
312
+ SELECT id, organization_id, user_id, role, created_at
313
+ FROM organization_member
314
+ WHERE organization_id = ${organizationId}
315
+ ORDER BY id
316
+ `;
317
+ }
318
+ async addMember(input) {
319
+ const rows = await connection_default`
320
+ INSERT INTO organization_member (organization_id, user_id, role)
321
+ VALUES (${input.organizationId}, ${input.userId}, ${input.role ?? "member"})
322
+ RETURNING id, organization_id, user_id, role, created_at
323
+ `;
324
+ const row = rows[0];
325
+ if (!row) {
326
+ throw new Error("Organization member insert did not return a row.");
327
+ }
328
+ return row;
329
+ }
330
+ async removeMember(organizationId, userId) {
331
+ const rows = await connection_default`
332
+ DELETE FROM organization_member
333
+ WHERE organization_id = ${organizationId} AND user_id = ${userId}
334
+ RETURNING id
335
+ `;
336
+ return rows.length > 0;
337
+ }
338
+ }
339
+ var memberRepository_default = OrganizationMemberRepository;
340
+
211
341
  // ../../src/core/errors/http.ts
212
342
  class HttpError extends Error {
213
343
  status;
@@ -274,6 +404,84 @@ class PreconditionFailedError extends HttpError {
274
404
  }
275
405
  }
276
406
 
407
+ // ../../src/core/auth/accessControl.ts
408
+ var ROLE_RANK = {
409
+ member: 1,
410
+ admin: 2,
411
+ owner: 3
412
+ };
413
+ function isGlobalAdmin(user) {
414
+ return user?.role === "admin";
415
+ }
416
+ function hasMinimumOrgRole(role, minimum) {
417
+ if (!role) {
418
+ return false;
419
+ }
420
+ return ROLE_RANK[role] >= ROLE_RANK[minimum];
421
+ }
422
+ function requireAuthenticatedUser() {
423
+ const user = currentAuthUser();
424
+ if (!user) {
425
+ throw new ForbiddenError("Authentication required.");
426
+ }
427
+ return user;
428
+ }
429
+ function resolveUserId(user) {
430
+ const userId = typeof user.id === "number" ? user.id : Number(user.id);
431
+ if (!Number.isInteger(userId) || userId <= 0) {
432
+ throw new ForbiddenError("Invalid authenticated user.");
433
+ }
434
+ return userId;
435
+ }
436
+
437
+ // ../../src/core/auth/membershipContext.ts
438
+ var membershipContext = new AsyncLocalStorage3;
439
+ var membershipRepository = new memberRepository_default;
440
+ async function runWithMembershipContext(callback) {
441
+ const user = currentAuthUser();
442
+ if (!user || isGlobalAdmin(user)) {
443
+ return await callback();
444
+ }
445
+ const memberships = await membershipRepository.listForUser(resolveUserId(user));
446
+ const context = {
447
+ organizationIds: memberships.map((membership) => membership.organization_id),
448
+ rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
449
+ };
450
+ return await membershipContext.run(context, callback);
451
+ }
452
+ function currentOrgRole(organizationId) {
453
+ return membershipContext.getStore()?.rolesByOrganizationId.get(organizationId) ?? null;
454
+ }
455
+ function hasOrgMembership(organizationId) {
456
+ return currentOrgRole(organizationId) !== null;
457
+ }
458
+ function currentOrganizationIds() {
459
+ return membershipContext.getStore()?.organizationIds ?? [];
460
+ }
461
+ function hasMinimumOrgRole2(organizationId, minimum) {
462
+ const role = currentOrgRole(organizationId);
463
+ if (!role) {
464
+ return false;
465
+ }
466
+ const ranks = {
467
+ member: 1,
468
+ admin: 2,
469
+ owner: 3
470
+ };
471
+ return ranks[role] >= ranks[minimum];
472
+ }
473
+
474
+ // ../../src/core/auth/membershipContextMiddleware.ts
475
+ function createMembershipContextMiddleware() {
476
+ return async (_request, next) => {
477
+ return await runWithMembershipContext(async () => await next());
478
+ };
479
+ }
480
+
481
+ // ../../src/core/auth/membershipMiddleware.ts
482
+ function createMembershipMiddleware() {
483
+ return createMembershipContextMiddleware();
484
+ }
277
485
  // ../../src/core/auth/policy.ts
278
486
  class Policy {
279
487
  constructor() {}
@@ -1053,76 +1261,6 @@ function indexMorphToRelation(children, parentsByType, relation) {
1053
1261
  return result;
1054
1262
  }
1055
1263
 
1056
- // ../../src/config/database.ts
1057
- function readInteger(name, fallback) {
1058
- const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
1059
- return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
1060
- }
1061
- var databaseConfig = {
1062
- url: process.env.DATABASE_URL ?? "",
1063
- poolMax: readInteger("DB_POOL_MAX", 10),
1064
- idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
1065
- maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
1066
- connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
1067
- };
1068
-
1069
- // ../../src/core/database/connectionContext.ts
1070
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
1071
- var activeConnection = new AsyncLocalStorage2;
1072
- function runWithDatabaseConnection(connection, callback) {
1073
- return activeConnection.run(connection, callback);
1074
- }
1075
- function getActiveDatabaseConnection(fallback) {
1076
- return activeConnection.getStore() ?? fallback;
1077
- }
1078
-
1079
- // ../../src/db/connection/createConnection.ts
1080
- var {SQL } = globalThis.Bun;
1081
- function createDatabaseConnection(config) {
1082
- if (!config.url) {
1083
- throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
1084
- }
1085
- return new SQL({
1086
- url: config.url,
1087
- max: config.poolMax,
1088
- idleTimeout: config.idleTimeoutSeconds,
1089
- maxLifetime: config.maxLifetimeSeconds,
1090
- connectionTimeout: config.connectionTimeoutSeconds
1091
- });
1092
- }
1093
-
1094
- // ../../src/db/connection/index.ts
1095
- var connectionHolder = {
1096
- connection: null
1097
- };
1098
- function getDatabase() {
1099
- if (!connectionHolder.connection) {
1100
- connectionHolder.connection = createDatabaseConnection(databaseConfig);
1101
- }
1102
- return connectionHolder.connection;
1103
- }
1104
- var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
1105
- function resolveDatabase() {
1106
- return getActiveDatabaseConnection(getDatabase());
1107
- }
1108
- function resolveDatabaseForProperty(property) {
1109
- if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
1110
- return getDatabase();
1111
- }
1112
- return resolveDatabase();
1113
- }
1114
- var db = new Proxy(function database() {}, {
1115
- apply(_target, _thisArg, args) {
1116
- return resolveDatabase()(...args);
1117
- },
1118
- get(_target, property) {
1119
- const connection = resolveDatabaseForProperty(property);
1120
- const value = connection[property];
1121
- return typeof value === "function" ? value.bind(connection) : value;
1122
- }
1123
- });
1124
- var connection_default = db;
1125
-
1126
1264
  // ../../src/core/database/boundConnection.ts
1127
1265
  var boundConnectionHolder = {
1128
1266
  connection: null
@@ -3012,8 +3150,11 @@ class LocalStorageDriver {
3012
3150
  constructor(rootDirectory) {
3013
3151
  this.rootDirectory = rootDirectory;
3014
3152
  }
3153
+ resolveRootDirectory() {
3154
+ return this.rootDirectory ?? process.env.STORAGE_PATH ?? "storage";
3155
+ }
3015
3156
  resolvePath(path) {
3016
- return join3(this.rootDirectory, path.replace(/^\/+/, ""));
3157
+ return join3(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
3017
3158
  }
3018
3159
  async put(path, contents) {
3019
3160
  const absolutePath = this.resolvePath(path);
@@ -3109,11 +3250,17 @@ function createStorageDriver() {
3109
3250
  if (driver === "s3") {
3110
3251
  return new S3StorageDriver(createS3Client());
3111
3252
  }
3112
- return new LocalStorageDriver(process.env.STORAGE_PATH ?? "storage");
3253
+ return new LocalStorageDriver;
3113
3254
  }
3114
- var defaultStorage = new StorageManager(createStorageDriver());
3255
+ var defaultStorage = { current: null };
3115
3256
  function storage() {
3116
- return defaultStorage;
3257
+ if (!defaultStorage.current) {
3258
+ defaultStorage.current = new StorageManager(createStorageDriver());
3259
+ }
3260
+ return defaultStorage.current;
3261
+ }
3262
+ function resetDefaultStorage() {
3263
+ defaultStorage.current = null;
3117
3264
  }
3118
3265
 
3119
3266
  // ../../src/core/facades/index.ts
@@ -3197,12 +3344,62 @@ function readRequestCookie(request, name) {
3197
3344
  function readBunRequestCookie(request, name) {
3198
3345
  return request.cookies.get(name) ?? readRequestCookie(request, name);
3199
3346
  }
3347
+ // ../../src/config/cors.ts
3348
+ var corsConfig = {
3349
+ allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim()).filter(Boolean),
3350
+ allowedMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
3351
+ allowedHeaders: [
3352
+ "Authorization",
3353
+ "Content-Type",
3354
+ "X-Request-Id",
3355
+ "X-Tenant-Id",
3356
+ "X-Authenticated-User-Id",
3357
+ "X-Authenticated-User-Role",
3358
+ "If-Match",
3359
+ "If-None-Match"
3360
+ ],
3361
+ maxAgeSeconds: 86400
3362
+ };
3363
+
3364
+ // ../../src/core/http/corsMiddleware.ts
3365
+ function createCorsMiddleware() {
3366
+ return async (request, next) => {
3367
+ if (request.method === "OPTIONS") {
3368
+ return new Response(null, {
3369
+ status: 204,
3370
+ headers: buildCorsHeaders(request)
3371
+ });
3372
+ }
3373
+ const response = await next();
3374
+ const headers = new Headers(response.headers);
3375
+ for (const [key, value] of buildCorsHeaders(request)) {
3376
+ headers.set(key, value);
3377
+ }
3378
+ return new Response(response.body, {
3379
+ status: response.status,
3380
+ statusText: response.statusText,
3381
+ headers
3382
+ });
3383
+ };
3384
+ }
3385
+ function buildCorsHeaders(request) {
3386
+ const headers = new Headers;
3387
+ const origin = request.headers.get("origin");
3388
+ const allowedOrigins = corsConfig.allowedOrigins;
3389
+ const allowOrigin = allowedOrigins.includes("*") || origin && allowedOrigins.includes(origin) ? origin ?? "*" : allowedOrigins[0] ?? "*";
3390
+ headers.set("Access-Control-Allow-Origin", allowOrigin);
3391
+ headers.set("Access-Control-Allow-Methods", corsConfig.allowedMethods.join(", "));
3392
+ headers.set("Access-Control-Allow-Headers", corsConfig.allowedHeaders.join(", "));
3393
+ headers.set("Access-Control-Max-Age", String(corsConfig.maxAgeSeconds));
3394
+ headers.set("Vary", "Origin");
3395
+ return headers;
3396
+ }
3200
3397
  // ../../src/core/http/csrfToken.ts
3201
3398
  import { timingSafeEqual } from "crypto";
3202
3399
 
3203
3400
  // ../../src/core/http/requestMetaContext.ts
3204
- import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
3205
- var requestMetaContext = new AsyncLocalStorage3;
3401
+ import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
3402
+ var requestMetaContext = new AsyncLocalStorage4;
3206
3403
  function runWithRequestMeta(meta, callback) {
3207
3404
  return requestMetaContext.run(meta, callback);
3208
3405
  }
@@ -3434,9 +3631,122 @@ function applyConditionalGet(request, response, etag) {
3434
3631
  headers
3435
3632
  });
3436
3633
  }
3634
+ // ../../src/core/http/flashSession.ts
3635
+ import { createHmac, timingSafeEqual as timingSafeEqual2 } from "crypto";
3636
+ var FLASH_COOKIE = "workhub_flash";
3637
+ var FLASH_TTL_MS = 60 * 1000;
3638
+ function resolveFlashSecret() {
3639
+ return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
3640
+ }
3641
+ function signFlashPayload(payload, issuedAt) {
3642
+ const signature = createHmac("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
3643
+ return `${payload}.${issuedAt}.${signature}`;
3644
+ }
3645
+ function readFlashCookie(request) {
3646
+ const cookieHeader = request.headers.get("cookie");
3647
+ if (!cookieHeader) {
3648
+ return null;
3649
+ }
3650
+ for (const part of cookieHeader.split(";")) {
3651
+ const [name, ...rest] = part.trim().split("=");
3652
+ if (name === FLASH_COOKIE) {
3653
+ return decodeURIComponent(rest.join("="));
3654
+ }
3655
+ }
3656
+ return null;
3657
+ }
3658
+ function parseFlashCookie(cookieValue) {
3659
+ const parts = cookieValue.split(".");
3660
+ if (parts.length < 3) {
3661
+ return null;
3662
+ }
3663
+ const signature = parts.pop();
3664
+ const issuedAtRaw = parts.pop();
3665
+ const payload = parts.join(".");
3666
+ if (!signature || !issuedAtRaw || !payload) {
3667
+ return null;
3668
+ }
3669
+ const issuedAt = Number.parseInt(issuedAtRaw, 10);
3670
+ if (!Number.isFinite(issuedAt) || Date.now() - issuedAt > FLASH_TTL_MS) {
3671
+ return null;
3672
+ }
3673
+ const expectedSignature = signFlashPayload(payload, issuedAt).split(".").pop();
3674
+ if (!expectedSignature) {
3675
+ return null;
3676
+ }
3677
+ const expectedBuffer = Buffer.from(expectedSignature);
3678
+ const actualBuffer = Buffer.from(signature);
3679
+ if (expectedBuffer.length !== actualBuffer.length) {
3680
+ return null;
3681
+ }
3682
+ if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
3683
+ return null;
3684
+ }
3685
+ try {
3686
+ const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
3687
+ if (!parsed?.message || typeof parsed.message !== "string") {
3688
+ return null;
3689
+ }
3690
+ if (parsed.level !== "success" && parsed.level !== "error" && parsed.level !== "info") {
3691
+ return null;
3692
+ }
3693
+ return parsed;
3694
+ } catch {
3695
+ return null;
3696
+ }
3697
+ }
3698
+ function createFlashCookie(message) {
3699
+ const payload = Buffer.from(JSON.stringify(message), "utf8").toString("base64url");
3700
+ const issuedAt = Date.now();
3701
+ const value = signFlashPayload(payload, issuedAt);
3702
+ return `${FLASH_COOKIE}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=60`;
3703
+ }
3704
+ function clearFlashCookie() {
3705
+ return `${FLASH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
3706
+ }
3707
+ function pullFlash(request) {
3708
+ const cookieValue = readFlashCookie(request);
3709
+ if (!cookieValue) {
3710
+ return null;
3711
+ }
3712
+ return parseFlashCookie(cookieValue);
3713
+ }
3714
+ function flashResponse(response, message) {
3715
+ const headers = new Headers(response.headers);
3716
+ headers.append("set-cookie", createFlashCookie(message));
3717
+ return new Response(response.body, {
3718
+ status: response.status,
3719
+ statusText: response.statusText,
3720
+ headers
3721
+ });
3722
+ }
3723
+ function withFlashClear(response) {
3724
+ const headers = new Headers(response.headers);
3725
+ headers.append("set-cookie", clearFlashCookie());
3726
+ return new Response(response.body, {
3727
+ status: response.status,
3728
+ statusText: response.statusText,
3729
+ headers
3730
+ });
3731
+ }
3732
+
3733
+ // ../../src/core/http/flashMiddleware.ts
3734
+ function createFlashMiddleware() {
3735
+ return async (request, next) => {
3736
+ const flash = pullFlash(request);
3737
+ const meta = currentRequestMeta();
3738
+ return await runWithRequestMeta({ ...meta, request, flash }, async () => {
3739
+ const response = await next();
3740
+ if (flash) {
3741
+ return withFlashClear(response);
3742
+ }
3743
+ return response;
3744
+ });
3745
+ };
3746
+ }
3437
3747
  // ../../src/core/tenant/tenantContext.ts
3438
- import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
3439
- var tenantContext = new AsyncLocalStorage4;
3748
+ import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
3749
+ var tenantContext = new AsyncLocalStorage5;
3440
3750
  function runWithTenant(tenant, callback) {
3441
3751
  return tenantContext.run(tenant, callback);
3442
3752
  }
@@ -3846,7 +4156,7 @@ async function verifyPassword(password, passwordHash) {
3846
4156
  }
3847
4157
 
3848
4158
  // ../../src/core/crypto/fieldEncryption.ts
3849
- import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "crypto";
4159
+ import { createCipheriv, createDecipheriv, createHmac as createHmac2, randomBytes } from "crypto";
3850
4160
  var ENCRYPTION_PREFIX = "enc:v1:";
3851
4161
  var IV_LENGTH = 12;
3852
4162
  var TAG_LENGTH = 16;
@@ -3895,7 +4205,7 @@ function decryptField(value, key) {
3895
4205
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
3896
4206
  }
3897
4207
  function hashLookupValue(normalizedValue, key) {
3898
- return createHmac("sha256", key).update(normalizedValue).digest("hex");
4208
+ return createHmac2("sha256", key).update(normalizedValue).digest("hex");
3899
4209
  }
3900
4210
  function normalizeEmail(email) {
3901
4211
  return email.trim().toLowerCase();
@@ -3975,7 +4285,7 @@ function resolveDefaultTokenExpiryDays() {
3975
4285
  }
3976
4286
 
3977
4287
  // ../../src/core/security/totp.ts
3978
- import { createHmac as createHmac2 } from "crypto";
4288
+ import { createHmac as createHmac3 } from "crypto";
3979
4289
  function decodeBase32(input) {
3980
4290
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
3981
4291
  const normalized = input.replace(/=+$/u, "").toUpperCase();
@@ -3997,7 +4307,7 @@ function generateTotp(secret, counter, digits = 6) {
3997
4307
  const key = decodeBase32(secret);
3998
4308
  const buffer = Buffer.alloc(8);
3999
4309
  buffer.writeBigUInt64BE(BigInt(counter));
4000
- const digest = createHmac2("sha1", key).update(buffer).digest();
4310
+ const digest = createHmac3("sha1", key).update(buffer).digest();
4001
4311
  const lastByte = digest[digest.length - 1] ?? 0;
4002
4312
  const offset = lastByte & 15;
4003
4313
  const b0 = digest[offset] ?? 0;
@@ -4189,14 +4499,14 @@ var userTable = defineTable({
4189
4499
  });
4190
4500
 
4191
4501
  // ../../src/core/auth/tokenHash.ts
4192
- import { createHash, createHmac as createHmac3 } from "crypto";
4502
+ import { createHash, createHmac as createHmac4 } from "crypto";
4193
4503
  function resolveTokenPepper() {
4194
4504
  return process.env.TOKEN_HASH_PEPPER?.trim() ?? "workhub-dev-token-pepper";
4195
4505
  }
4196
4506
  function hashApiToken(token) {
4197
4507
  const pepper = resolveTokenPepper();
4198
4508
  if (pepper && pepper !== "workhub-dev-token-pepper") {
4199
- return createHmac3("sha256", pepper).update(token).digest("hex");
4509
+ return createHmac4("sha256", pepper).update(token).digest("hex");
4200
4510
  }
4201
4511
  return createHash("sha256").update(token).digest("hex");
4202
4512
  }
@@ -4204,105 +4514,6 @@ function hashApiToken(token) {
4204
4514
  // ../../src/modules/user/provider.ts
4205
4515
  var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
4206
4516
 
4207
- // ../../src/core/http/flashSession.ts
4208
- import { createHmac as createHmac4, timingSafeEqual as timingSafeEqual2 } from "crypto";
4209
- var FLASH_COOKIE = "workhub_flash";
4210
- var FLASH_TTL_MS = 60 * 1000;
4211
- function resolveFlashSecret() {
4212
- return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
4213
- }
4214
- function signFlashPayload(payload, issuedAt) {
4215
- const signature = createHmac4("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
4216
- return `${payload}.${issuedAt}.${signature}`;
4217
- }
4218
- function readFlashCookie(request) {
4219
- const cookieHeader = request.headers.get("cookie");
4220
- if (!cookieHeader) {
4221
- return null;
4222
- }
4223
- for (const part of cookieHeader.split(";")) {
4224
- const [name, ...rest] = part.trim().split("=");
4225
- if (name === FLASH_COOKIE) {
4226
- return decodeURIComponent(rest.join("="));
4227
- }
4228
- }
4229
- return null;
4230
- }
4231
- function parseFlashCookie(cookieValue) {
4232
- const parts = cookieValue.split(".");
4233
- if (parts.length < 3) {
4234
- return null;
4235
- }
4236
- const signature = parts.pop();
4237
- const issuedAtRaw = parts.pop();
4238
- const payload = parts.join(".");
4239
- if (!signature || !issuedAtRaw || !payload) {
4240
- return null;
4241
- }
4242
- const issuedAt = Number.parseInt(issuedAtRaw, 10);
4243
- if (!Number.isFinite(issuedAt) || Date.now() - issuedAt > FLASH_TTL_MS) {
4244
- return null;
4245
- }
4246
- const expectedSignature = signFlashPayload(payload, issuedAt).split(".").pop();
4247
- if (!expectedSignature) {
4248
- return null;
4249
- }
4250
- const expectedBuffer = Buffer.from(expectedSignature);
4251
- const actualBuffer = Buffer.from(signature);
4252
- if (expectedBuffer.length !== actualBuffer.length) {
4253
- return null;
4254
- }
4255
- if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
4256
- return null;
4257
- }
4258
- try {
4259
- const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
4260
- if (!parsed?.message || typeof parsed.message !== "string") {
4261
- return null;
4262
- }
4263
- if (parsed.level !== "success" && parsed.level !== "error" && parsed.level !== "info") {
4264
- return null;
4265
- }
4266
- return parsed;
4267
- } catch {
4268
- return null;
4269
- }
4270
- }
4271
- function createFlashCookie(message) {
4272
- const payload = Buffer.from(JSON.stringify(message), "utf8").toString("base64url");
4273
- const issuedAt = Date.now();
4274
- const value = signFlashPayload(payload, issuedAt);
4275
- return `${FLASH_COOKIE}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=60`;
4276
- }
4277
- function clearFlashCookie() {
4278
- return `${FLASH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
4279
- }
4280
- function pullFlash(request) {
4281
- const cookieValue = readFlashCookie(request);
4282
- if (!cookieValue) {
4283
- return null;
4284
- }
4285
- return parseFlashCookie(cookieValue);
4286
- }
4287
- function flashResponse(response, message) {
4288
- const headers = new Headers(response.headers);
4289
- headers.append("set-cookie", createFlashCookie(message));
4290
- return new Response(response.body, {
4291
- status: response.status,
4292
- statusText: response.statusText,
4293
- headers
4294
- });
4295
- }
4296
- function withFlashClear(response) {
4297
- const headers = new Headers(response.headers);
4298
- headers.append("set-cookie", clearFlashCookie());
4299
- return new Response(response.body, {
4300
- status: response.status,
4301
- statusText: response.statusText,
4302
- headers
4303
- });
4304
- }
4305
-
4306
4517
  // ../../src/core/view/webLayoutData.ts
4307
4518
  async function resolveWebLayoutData(container, request) {
4308
4519
  const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
@@ -4898,6 +5109,38 @@ function createMetricsMiddleware() {
4898
5109
  return response;
4899
5110
  };
4900
5111
  }
5112
+ // ../../src/core/http/requireAbilityMiddleware.ts
5113
+ function createRequireAbilityMiddleware(abilityChecker) {
5114
+ return (ability) => {
5115
+ return async (_request, next) => {
5116
+ const user = currentAuthUser();
5117
+ try {
5118
+ abilityChecker.requireAbility(user, ability);
5119
+ } catch (error) {
5120
+ if (error instanceof ForbiddenError) {
5121
+ return Response.json({ error: error.message }, { status: error.status });
5122
+ }
5123
+ throw error;
5124
+ }
5125
+ return await next();
5126
+ };
5127
+ };
5128
+ }
5129
+ // ../../src/core/http/requireGlobalAdminMiddleware.ts
5130
+ function createRequireGlobalAdminMiddleware() {
5131
+ return async (_request, next) => {
5132
+ const user = currentAuthUser();
5133
+ if (!isGlobalAdmin(user)) {
5134
+ logSecurityEvent("privilege_escalation_blocked", {
5135
+ required_role: "platform_admin",
5136
+ path: new URL(_request.url).pathname
5137
+ });
5138
+ const error = new ForbiddenError("Platform admin access required.");
5139
+ return Response.json({ error: error.message }, { status: error.status });
5140
+ }
5141
+ return await next();
5142
+ };
5143
+ }
4901
5144
  // ../../src/core/http/requireWebAuthMiddleware.ts
4902
5145
  function createRequireWebAuthMiddleware(auth2) {
4903
5146
  return async (request, next) => {
@@ -5104,6 +5347,29 @@ function resetGracefulShutdownForTests() {
5104
5347
  shutdownInstalled = false;
5105
5348
  shuttingDown = false;
5106
5349
  }
5350
+ // ../../src/core/logging/requestLoggingMiddleware.ts
5351
+ function createRequestLoggingMiddleware() {
5352
+ return async (request, next) => {
5353
+ return await runWithRequestMeta({
5354
+ ipAddress: request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip"),
5355
+ userAgent: request.headers.get("user-agent"),
5356
+ request
5357
+ }, async () => {
5358
+ const startedAt = performance.now();
5359
+ const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
5360
+ const response = await next();
5361
+ const durationMs = Math.round(performance.now() - startedAt);
5362
+ appLogger.info("HTTP request completed", {
5363
+ requestId,
5364
+ method: request.method,
5365
+ path: new URL(request.url).pathname,
5366
+ status: response.status,
5367
+ durationMs
5368
+ });
5369
+ return response;
5370
+ });
5371
+ };
5372
+ }
5107
5373
  // ../../src/core/mail/markdownMail.ts
5108
5374
  function escapeHtml(value) {
5109
5375
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -5604,6 +5870,208 @@ async function runDueScheduledTasks(schedule = appSchedule, now = new Date) {
5604
5870
  }
5605
5871
  return due.length;
5606
5872
  }
5873
+ // ../../src/core/security/publicReads.ts
5874
+ function isPublicReadsEnabled() {
5875
+ return isFeatureEnabled("publicReads");
5876
+ }
5877
+ function guestCanViewResource() {
5878
+ return isPublicReadsEnabled();
5879
+ }
5880
+ // ../../src/core/tenant/tenantMiddleware.ts
5881
+ import { createHash as createHash2 } from "crypto";
5882
+
5883
+ // ../../src/core/tenant/databaseTenantContext.ts
5884
+ async function runWithMigrationBypass(callback) {
5885
+ await connection_default`SELECT set_config('app.bypass_rls', 'true', false)`;
5886
+ try {
5887
+ return await callback();
5888
+ } finally {
5889
+ await connection_default`SELECT set_config('app.bypass_rls', 'false', false)`;
5890
+ }
5891
+ }
5892
+
5893
+ // ../../src/core/tenant/resolveTenant.ts
5894
+ async function resolveTenant(tenantId) {
5895
+ const rows = await connection_default`
5896
+ SELECT id, slug, plan, region
5897
+ FROM tenant
5898
+ WHERE id = ${tenantId}
5899
+ LIMIT 1
5900
+ `;
5901
+ const row = rows[0];
5902
+ return row ? { id: row.id, slug: row.slug, plan: row.plan, region: row.region ?? "eu" } : null;
5903
+ }
5904
+
5905
+ // ../../src/core/tenant/tenantDatabaseScope.ts
5906
+ async function applyTenantContextToTransaction(transaction, tenantId) {
5907
+ await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
5908
+ await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
5909
+ }
5910
+ async function runWithTenantDatabase(tenant, callback) {
5911
+ return await getDatabase().begin(async (transaction) => {
5912
+ await applyTenantContextToTransaction(transaction, tenant.id);
5913
+ return await runWithDatabaseConnection(transaction, async () => {
5914
+ return await runWithTenant(tenant, callback);
5915
+ });
5916
+ });
5917
+ }
5918
+
5919
+ // ../../src/core/tenant/tenantMiddleware.ts
5920
+ var DEFAULT_TENANT = {
5921
+ id: 1,
5922
+ slug: "default",
5923
+ plan: "enterprise",
5924
+ region: "eu"
5925
+ };
5926
+ async function resolveUserTenantId(userId) {
5927
+ return await runWithMigrationBypass(async () => {
5928
+ const rows = await connection_default`
5929
+ SELECT tenant_id
5930
+ FROM users
5931
+ WHERE id = ${userId}
5932
+ LIMIT 1
5933
+ `;
5934
+ return rows[0]?.tenant_id ?? DEFAULT_TENANT.id;
5935
+ });
5936
+ }
5937
+ function auditChecksum(payload) {
5938
+ return createHash2("sha256").update(JSON.stringify(payload)).digest("hex");
5939
+ }
5940
+ async function resolveTenantForRequest(request) {
5941
+ const user = currentAuthUser();
5942
+ const headerValue = request.headers.get("x-tenant-id")?.trim();
5943
+ const parsedHeader = headerValue !== undefined && headerValue.length > 0 ? Number.parseInt(headerValue, 10) : Number.NaN;
5944
+ if (user) {
5945
+ const userId = typeof user.id === "number" ? user.id : Number.parseInt(String(user.id), 10);
5946
+ if (Number.isInteger(userId) && userId > 0) {
5947
+ const userTenantId = await resolveUserTenantId(userId);
5948
+ if (!isGlobalAdmin(user)) {
5949
+ if (Number.isInteger(parsedHeader) && parsedHeader > 0 && parsedHeader !== userTenantId) {
5950
+ throw new ForbiddenError("Tenant header does not match your account.");
5951
+ }
5952
+ return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
5953
+ }
5954
+ if (Number.isInteger(parsedHeader) && parsedHeader > 0) {
5955
+ return await resolveTenant(parsedHeader) ?? DEFAULT_TENANT;
5956
+ }
5957
+ return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
5958
+ }
5959
+ }
5960
+ const tenantId = Number.isInteger(parsedHeader) && parsedHeader > 0 ? parsedHeader : DEFAULT_TENANT.id;
5961
+ return await resolveTenant(tenantId) ?? DEFAULT_TENANT;
5962
+ }
5963
+ function createTenantMiddleware() {
5964
+ return async (request, next) => {
5965
+ try {
5966
+ const tenant = await resolveTenantForRequest(request);
5967
+ return await runWithTenantDatabase(tenant, async () => {
5968
+ const response = await next();
5969
+ const headers = new Headers(response.headers);
5970
+ headers.set("x-tenant-id", String(tenant.id));
5971
+ headers.set("x-tenant-region", tenant.region);
5972
+ return new Response(response.body, {
5973
+ status: response.status,
5974
+ statusText: response.statusText,
5975
+ headers
5976
+ });
5977
+ });
5978
+ } catch (error) {
5979
+ if (error instanceof HttpError) {
5980
+ return Response.json({ error: error.message }, { status: error.status });
5981
+ }
5982
+ throw error;
5983
+ }
5984
+ };
5985
+ }
5986
+ // ../../src/core/tracing/otel.ts
5987
+ import { randomBytes as randomBytes2 } from "crypto";
5988
+ function randomHex(bytes) {
5989
+ return randomBytes2(bytes).toString("hex");
5990
+ }
5991
+ function createSpan(input) {
5992
+ const spanId = randomHex(8);
5993
+ return {
5994
+ traceId: input.traceId,
5995
+ spanId,
5996
+ name: input.name,
5997
+ startTimeUnixNano: String(Math.floor(input.startedAt * 1e6)),
5998
+ endTimeUnixNano: String(Math.floor(input.endedAt * 1e6)),
5999
+ attributes: Object.entries(input.attributes ?? {}).map(([key, value]) => ({
6000
+ key,
6001
+ value: { stringValue: value }
6002
+ })),
6003
+ status: { code: 1 }
6004
+ };
6005
+ }
6006
+ async function exportOtelSpan(span) {
6007
+ const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
6008
+ if (!endpoint) {
6009
+ return;
6010
+ }
6011
+ const serviceName = process.env.OTEL_SERVICE_NAME?.trim() ?? "workhub-api";
6012
+ const url = endpoint.endsWith("/v1/traces") ? endpoint : `${endpoint.replace(/\/$/, "")}/v1/traces`;
6013
+ await fetch(url, {
6014
+ method: "POST",
6015
+ headers: { "content-type": "application/json" },
6016
+ body: JSON.stringify({
6017
+ resourceSpans: [
6018
+ {
6019
+ resource: {
6020
+ attributes: [{ key: "service.name", value: { stringValue: serviceName } }]
6021
+ },
6022
+ scopeSpans: [{ spans: [span] }]
6023
+ }
6024
+ ]
6025
+ })
6026
+ });
6027
+ }
6028
+
6029
+ // ../../src/core/tracing/traceContext.ts
6030
+ import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
6031
+ var traceContextStorage = new AsyncLocalStorage6;
6032
+ function runWithTraceContext(context, callback) {
6033
+ return traceContextStorage.run(context, callback);
6034
+ }
6035
+ function currentTraceId() {
6036
+ return traceContextStorage.getStore()?.traceId ?? null;
6037
+ }
6038
+
6039
+ // ../../src/core/tracing/tracingMiddleware.ts
6040
+ function createTracingMiddleware() {
6041
+ return async (request, next) => {
6042
+ const traceId = (request.headers.get("x-trace-id") ?? crypto.randomUUID()).replace(/-/g, "");
6043
+ const spanId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
6044
+ const startedAt = performance.now();
6045
+ const path = new URL(request.url).pathname;
6046
+ return await runWithTraceContext({ traceId, spanId }, async () => {
6047
+ const response = await next();
6048
+ const endedAt = performance.now();
6049
+ const headers = new Headers(response.headers);
6050
+ headers.set("x-trace-id", traceId);
6051
+ headers.set("x-span-id", spanId);
6052
+ headers.set("traceparent", `00-${traceId}-${spanId}-01`);
6053
+ headers.set("server-timing", `app;dur=${(endedAt - startedAt).toFixed(2)}`);
6054
+ exportOtelSpan(createSpan({
6055
+ traceId,
6056
+ name: `${request.method} ${path}`,
6057
+ startedAt,
6058
+ endedAt,
6059
+ attributes: {
6060
+ "http.method": request.method,
6061
+ "http.route": path,
6062
+ "http.status_code": String(response.status)
6063
+ }
6064
+ })).catch(() => {
6065
+ return;
6066
+ });
6067
+ return new Response(response.body, {
6068
+ status: response.status,
6069
+ statusText: response.statusText,
6070
+ headers
6071
+ });
6072
+ });
6073
+ };
6074
+ }
5607
6075
  // ../../src/core/validation/rules.ts
5608
6076
  function required() {
5609
6077
  return (field, value) => {
@@ -5791,7 +6259,9 @@ export {
5791
6259
  resolveApplicationConfig,
5792
6260
  resolveApplicationCache,
5793
6261
  resolveApplicationAuth,
6262
+ resetDefaultStorage,
5794
6263
  required,
6264
+ requestIdMiddleware,
5795
6265
  renderMarkdownMail,
5796
6266
  registerShutdownHandler,
5797
6267
  registerModelRepository,
@@ -5820,6 +6290,7 @@ export {
5820
6290
  loadMigrationsFromDirectory,
5821
6291
  jsonResponse2 as jsonResponse,
5822
6292
  jobRegistry,
6293
+ isPublicReadsEnabled,
5823
6294
  isHtmxRequest,
5824
6295
  isEtagEnabled,
5825
6296
  installGracefulShutdownSignals,
@@ -5849,23 +6320,31 @@ export {
5849
6320
  currentAuthUser,
5850
6321
  createdResponse,
5851
6322
  createTrackedJob,
6323
+ createTracingMiddleware,
5852
6324
  createThrottleMiddleware,
6325
+ createTenantMiddleware,
5853
6326
  createSecurityHeadersMiddleware,
5854
6327
  createSchemaBuilder,
5855
6328
  createRequireWebAuthMiddleware,
6329
+ createRequireGlobalAdminMiddleware,
5856
6330
  createRequireAuthMiddleware,
6331
+ createRequireAbilityMiddleware,
6332
+ createRequestLoggingMiddleware,
5857
6333
  createQueueWorker,
5858
6334
  createQueue,
5859
6335
  createProductionQueue,
5860
6336
  createNotificationDispatcher,
5861
6337
  createMetricsMiddleware,
5862
6338
  createMemoryThrottleMiddleware,
6339
+ createMembershipMiddleware,
5863
6340
  createLoginThrottleMiddleware,
6341
+ createFlashMiddleware,
5864
6342
  createFailedJobService,
5865
6343
  createDatabaseConnection2 as createDatabaseConnection,
5866
6344
  createCsrfTokenCookie,
5867
6345
  createCsrfProtection,
5868
6346
  createCsrfMiddleware,
6347
+ createCorsMiddleware,
5869
6348
  createBodySizeLimitMiddleware,
5870
6349
  createAuthorizeMiddleware,
5871
6350
  createAuthMiddleware,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/core",
3
- "version": "0.5.13",
3
+ "version": "0.5.14",
4
4
  "description": "Strata — Laravel-inspired Bun framework public API",
5
5
  "type": "module",
6
6
  "license": "MIT",