@getstrata/bootstrap 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +3 -1
  2. package/dist/bootstrap/contracts.d.ts +2 -0
  3. package/dist/bootstrap/httpKernel.d.ts +1 -2
  4. package/dist/bootstrap/middleware.d.ts +1 -1
  5. package/dist/bootstrap/providers/storage.d.ts +3 -0
  6. package/dist/bootstrap/scimRoutes.d.ts +1 -1
  7. package/dist/core/admin/registry.d.ts +1 -0
  8. package/dist/core/auth/membershipScope.d.ts +16 -0
  9. package/dist/core/auth/membershipService.d.ts +24 -0
  10. package/dist/core/database/baseRepository.d.ts +6 -1
  11. package/dist/core/database/connectionContext.d.ts +2 -1
  12. package/dist/core/database/defaultConnection.d.ts +6 -0
  13. package/dist/core/database/queryProxy.d.ts +3 -0
  14. package/dist/core/database/repositoryConnection.d.ts +3 -3
  15. package/dist/core/jobs/dispatchWebhookJob.d.ts +0 -1
  16. package/dist/core/queue/queueMetrics.d.ts +15 -0
  17. package/dist/core/security/safeFetch.d.ts +2 -0
  18. package/dist/core/security/safeUrl.d.ts +16 -1
  19. package/dist/core/storage/storage.d.ts +6 -3
  20. package/dist/core/tenant/tenantDatabaseScope.d.ts +2 -1
  21. package/dist/db/connection/index.d.ts +1 -1
  22. package/dist/domain/workhub.d.ts +35 -0
  23. package/dist/entries/applicationRegistry.js +185 -0
  24. package/dist/entries/config.js +42 -0
  25. package/dist/entries/context.js +4208 -0
  26. package/dist/entries/contracts.js +92 -0
  27. package/dist/entries/createWebRoutes.js +996 -0
  28. package/dist/entries/httpKernel.js +264 -0
  29. package/dist/entries/providers/view.js +635 -0
  30. package/dist/entries/providers.js +4094 -0
  31. package/dist/framework/public-api.d.ts +36 -4
  32. package/dist/index.js +328 -1046
  33. package/dist/modules/organization/repository.d.ts +14 -0
  34. package/dist/modules/organization/table.d.ts +3 -0
  35. package/dist/modules/organization/types.d.ts +10 -0
  36. package/dist/modules/scim/controller.d.ts +2 -1
  37. package/dist/modules/scim/scimResponse.d.ts +1 -1
  38. package/dist/modules/scim/service.d.ts +4 -7
  39. package/dist/modules/user/repository.d.ts +1 -0
  40. package/package.json +5 -4
package/dist/index.js CHANGED
@@ -65,107 +65,80 @@ var appConfig = {
65
65
  apiPrefix: process.env.API_PREFIX ?? "/api/v1"
66
66
  };
67
67
 
68
- // ../../src/config/database.ts
69
- function readInteger(name, fallback) {
70
- const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
71
- return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
72
- }
73
- var databaseConfig = {
74
- url: process.env.DATABASE_URL ?? "",
75
- poolMax: readInteger("DB_POOL_MAX", 10),
76
- idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
77
- maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
78
- connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
68
+ // ../../src/core/database/boundConnection.ts
69
+ var boundConnectionHolder = {
70
+ connection: null
79
71
  };
72
+ function getBoundDatabaseConnection() {
73
+ return boundConnectionHolder.connection;
74
+ }
80
75
 
81
76
  // ../../src/core/database/connectionContext.ts
82
77
  import { AsyncLocalStorage } from "async_hooks";
83
78
  var activeConnection = new AsyncLocalStorage;
84
- function runWithDatabaseConnection(connection, callback) {
85
- return activeConnection.run(connection, callback);
86
- }
87
79
  function getActiveDatabaseConnection(fallback) {
88
80
  return activeConnection.getStore() ?? fallback;
89
81
  }
90
82
 
91
- // ../../src/db/connection/createConnection.ts
92
- var {SQL } = globalThis.Bun;
93
- function createDatabaseConnection(config) {
94
- if (!config.url) {
95
- throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
83
+ // ../../src/core/database/queryProxy.ts
84
+ var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
85
+ function createDatabaseQueryProxy(pool) {
86
+ function resolveDatabase() {
87
+ return getActiveDatabaseConnection(pool);
96
88
  }
97
- return new SQL({
98
- url: config.url,
99
- max: config.poolMax,
100
- idleTimeout: config.idleTimeoutSeconds,
101
- maxLifetime: config.maxLifetimeSeconds,
102
- connectionTimeout: config.connectionTimeoutSeconds
89
+ function resolveDatabaseForProperty(property) {
90
+ if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
91
+ return pool;
92
+ }
93
+ return resolveDatabase();
94
+ }
95
+ return new Proxy(function database() {}, {
96
+ apply(_target, _thisArg, args) {
97
+ return resolveDatabase()(...args);
98
+ },
99
+ get(_target, property) {
100
+ const connection = resolveDatabaseForProperty(property);
101
+ const value = connection[property];
102
+ return typeof value === "function" ? value.bind(connection) : value;
103
+ }
103
104
  });
104
105
  }
105
106
 
106
- // ../../src/db/connection/index.ts
107
- var connectionHolder = {
107
+ // ../../src/core/database/defaultConnection.ts
108
+ var defaultPool = {
108
109
  connection: null
109
110
  };
110
- function getDatabase() {
111
- if (!connectionHolder.connection) {
112
- connectionHolder.connection = createDatabaseConnection(databaseConfig);
113
- }
114
- return connectionHolder.connection;
115
- }
116
- var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
117
- function resolveDatabase() {
118
- return getActiveDatabaseConnection(getDatabase());
111
+ var defaultQuery = {
112
+ connection: null
113
+ };
114
+ function registerDefaultDatabasePool(connection) {
115
+ defaultPool.connection = connection;
116
+ defaultQuery.connection = createDatabaseQueryProxy(connection);
119
117
  }
120
- function resolveDatabaseForProperty(property) {
121
- if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
122
- return getDatabase();
118
+ function getDefaultDatabaseQuery() {
119
+ if (!defaultQuery.connection) {
120
+ throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
123
121
  }
124
- return resolveDatabase();
122
+ return defaultQuery.connection;
125
123
  }
126
- var db = new Proxy(function database() {}, {
124
+
125
+ // ../../src/core/database/repositoryConnection.ts
126
+ function resolveRepositoryConnection() {
127
+ return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
128
+ }
129
+ var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
127
130
  apply(_target, _thisArg, args) {
128
- return resolveDatabase()(...args);
131
+ return resolveRepositoryConnection()(...args);
129
132
  },
130
133
  get(_target, property) {
131
- const connection = resolveDatabaseForProperty(property);
134
+ const connection = resolveRepositoryConnection();
132
135
  const value = connection[property];
133
136
  return typeof value === "function" ? value.bind(connection) : value;
134
137
  }
135
138
  });
136
- var connection_default = db;
137
139
 
138
- // ../../src/core/security/safeFetch.ts
139
- var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
140
- async function safeFetch(input, init = {}, options = {}) {
141
- const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
142
- const maxRedirects = options.maxRedirects ?? 0;
143
- const controller = new AbortController;
144
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
145
- try {
146
- let currentUrl = input;
147
- let redirectCount = 0;
148
- while (true) {
149
- const response = await fetch(currentUrl, {
150
- ...init,
151
- signal: controller.signal,
152
- redirect: "manual"
153
- });
154
- if (response.status >= 300 && response.status < 400) {
155
- const location = response.headers.get("location");
156
- if (!location || redirectCount >= maxRedirects) {
157
- return response;
158
- }
159
- currentUrl = new URL(location, currentUrl).toString();
160
- redirectCount += 1;
161
- continue;
162
- }
163
- return response;
164
- }
165
- } finally {
166
- clearTimeout(timeout);
167
- }
168
- }
140
+ // ../../src/core/security/safeUrl.ts
141
+ import { lookup as dnsLookupImpl } from "dns/promises";
169
142
 
170
143
  // ../../src/core/errors/http.ts
171
144
  class HttpError extends Error {
@@ -207,13 +180,8 @@ class UnauthorizedError extends HttpError {
207
180
  }
208
181
  }
209
182
 
210
- class PayloadTooLargeError extends HttpError {
211
- constructor(message = "Payload Too Large", details) {
212
- super(413, message, details);
213
- }
214
- }
215
-
216
183
  // ../../src/core/security/safeUrl.ts
184
+ var dnsLookup = dnsLookupImpl;
217
185
  var BLOCKED_HOSTNAMES = new Set([
218
186
  "localhost",
219
187
  "127.0.0.1",
@@ -285,14 +253,63 @@ function assertSafeOutboundUrl(rawUrl, options = {}) {
285
253
  }
286
254
  return parsed;
287
255
  }
256
+ function isBlockedIpAddress(address) {
257
+ return isBlockedHostname(address.trim().toLowerCase());
258
+ }
259
+ async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
260
+ const parsed = assertSafeOutboundUrl(rawUrl, options);
261
+ if (options.resolveDns === false) {
262
+ return parsed;
263
+ }
264
+ const hostname = parsed.hostname.trim().toLowerCase();
265
+ const results = await dnsLookup(hostname, { all: true, verbatim: true });
266
+ if (results.some((result) => isBlockedIpAddress(result.address))) {
267
+ throw new BadRequestError("Webhook URL targets a blocked host.");
268
+ }
269
+ return parsed;
270
+ }
271
+
272
+ // ../../src/core/security/safeFetch.ts
273
+ var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
274
+ async function safeFetch(input, init = {}, options = {}) {
275
+ const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
276
+ const maxRedirects = options.maxRedirects ?? 0;
277
+ const resolveDns = options.resolveDns ?? appConfig.env === "production";
278
+ const urlOptions = { allowHttp: options.allowHttp, resolveDns };
279
+ const controller = new AbortController;
280
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
281
+ try {
282
+ let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
283
+ let redirectCount = 0;
284
+ while (true) {
285
+ const response = await fetch(currentUrl, {
286
+ ...init,
287
+ signal: controller.signal,
288
+ redirect: "manual"
289
+ });
290
+ if (response.status >= 300 && response.status < 400) {
291
+ const location = response.headers.get("location");
292
+ if (!location || redirectCount >= maxRedirects) {
293
+ return response;
294
+ }
295
+ currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
296
+ redirectCount += 1;
297
+ continue;
298
+ }
299
+ return response;
300
+ }
301
+ } finally {
302
+ clearTimeout(timeout);
303
+ }
304
+ }
288
305
 
289
306
  // ../../src/core/tenant/databaseTenantContext.ts
290
307
  async function runWithMigrationBypass(callback) {
291
- await connection_default`SELECT set_config('app.bypass_rls', 'true', false)`;
308
+ await repositoryConnection`SELECT set_config('app.bypass_rls', 'true', false)`;
292
309
  try {
293
310
  return await callback();
294
311
  } finally {
295
- await connection_default`SELECT set_config('app.bypass_rls', 'false', false)`;
312
+ await repositoryConnection`SELECT set_config('app.bypass_rls', 'false', false)`;
296
313
  }
297
314
  }
298
315
 
@@ -349,7 +366,7 @@ async function exportPendingAuditLogs() {
349
366
  return 0;
350
367
  }
351
368
  return await runWithMigrationBypass(async () => {
352
- const rows = await connection_default`
369
+ const rows = await repositoryConnection`
353
370
  SELECT
354
371
  id,
355
372
  user_id,
@@ -393,13 +410,13 @@ async function exportPendingAuditLogs() {
393
410
  ...process.env.SIEM_EXPORT_TOKEN ? { authorization: `Bearer ${process.env.SIEM_EXPORT_TOKEN}` } : {}
394
411
  },
395
412
  body
396
- });
413
+ }, { allowHttp: appConfig.env !== "production" });
397
414
  if (!response.ok) {
398
415
  throw new Error(`SIEM export failed with status ${response.status}.`);
399
416
  }
400
417
  const ids = rows.map((row) => row.id);
401
418
  for (const id of ids) {
402
- await connection_default`UPDATE audit_log SET exported_at = NOW() WHERE id = ${id}`;
419
+ await repositoryConnection`UPDATE audit_log SET exported_at = NOW() WHERE id = ${id}`;
403
420
  }
404
421
  return rows.length;
405
422
  });
@@ -559,7 +576,8 @@ class ConfigStore {
559
576
  }
560
577
  var requiredDependencyKeys = [
561
578
  "container",
562
- "cache"
579
+ "cache",
580
+ "storage"
563
581
  ];
564
582
  function getRequiredDependency(dependencies, key) {
565
583
  const dependency = dependencies[key];
@@ -652,6 +670,60 @@ import { SamlProvider } from "@getstrata/core/auth/oauth/samlProvider";
652
670
  // ../../src/modules/user/apiTokenRepository.ts
653
671
  import { BaseRepository } from "@getstrata/core/database";
654
672
 
673
+ // ../../src/config/database.ts
674
+ function readInteger(name, fallback) {
675
+ const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
676
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
677
+ }
678
+ var databaseConfig = {
679
+ url: process.env.DATABASE_URL ?? "",
680
+ poolMax: readInteger("DB_POOL_MAX", 10),
681
+ idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
682
+ maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
683
+ connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
684
+ };
685
+
686
+ // ../../src/db/connection/createConnection.ts
687
+ var {SQL } = globalThis.Bun;
688
+ function createDatabaseConnection(config) {
689
+ if (!config.url) {
690
+ throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
691
+ }
692
+ return new SQL({
693
+ url: config.url,
694
+ max: config.poolMax,
695
+ idleTimeout: config.idleTimeoutSeconds,
696
+ maxLifetime: config.maxLifetimeSeconds,
697
+ connectionTimeout: config.connectionTimeoutSeconds
698
+ });
699
+ }
700
+
701
+ // ../../src/db/connection/index.ts
702
+ var connectionHolder = {
703
+ connection: null
704
+ };
705
+ function getDatabase() {
706
+ if (!connectionHolder.connection) {
707
+ connectionHolder.connection = createDatabaseConnection(databaseConfig);
708
+ registerDefaultDatabasePool(connectionHolder.connection);
709
+ }
710
+ return connectionHolder.connection;
711
+ }
712
+ function getDb() {
713
+ getDatabase();
714
+ return getDefaultDatabaseQuery();
715
+ }
716
+ var db = new Proxy(function database() {}, {
717
+ apply(_target, _thisArg, args) {
718
+ return getDb()(...args);
719
+ },
720
+ get(_target, property) {
721
+ const connection = getDb();
722
+ const value = connection[property];
723
+ return typeof value === "function" ? value.bind(connection) : value;
724
+ }
725
+ });
726
+
655
727
  // ../../src/modules/user/apiTokenTable.ts
656
728
  import { defineTable } from "@getstrata/core/database";
657
729
  var apiTokenTable = defineTable({
@@ -836,9 +908,6 @@ var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
836
908
  // ../../src/core/auth/authContext.ts
837
909
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
838
910
  var authContext = new AsyncLocalStorage2;
839
- function runWithAuthUser(user, callback) {
840
- return authContext.run(user, callback);
841
- }
842
911
  function currentAuthUser() {
843
912
  return authContext.getStore() ?? null;
844
913
  }
@@ -1753,13 +1822,10 @@ function resolveApplicationQueue() {
1753
1822
  // ../../src/core/jobs/dispatchWebhookJob.ts
1754
1823
  import { createHmac as createHmac2 } from "crypto";
1755
1824
  class DispatchWebhookJob extends Job {
1756
- constructor() {
1757
- super();
1758
- }
1759
1825
  maxAttempts = 3;
1760
1826
  backoffMs = 2000;
1761
1827
  async handle(payload) {
1762
- const rows = await connection_default`
1828
+ const rows = await repositoryConnection`
1763
1829
  SELECT id, url, secret
1764
1830
  FROM webhook
1765
1831
  WHERE id = ${payload.webhookId} AND active = TRUE
@@ -1782,14 +1848,14 @@ class DispatchWebhookJob extends Job {
1782
1848
  "x-workhub-signature": signature
1783
1849
  },
1784
1850
  body
1785
- });
1851
+ }, { allowHttp: appConfig.env !== "production" });
1786
1852
  responseStatus = response.status;
1787
1853
  if (!response.ok) {
1788
1854
  throw new Error(`Webhook delivery failed with status ${response.status}.`);
1789
1855
  }
1790
1856
  } catch (error) {
1791
1857
  errorMessage = error instanceof Error ? error.message : String(error);
1792
- await connection_default`
1858
+ await repositoryConnection`
1793
1859
  INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
1794
1860
  VALUES (
1795
1861
  ${webhook.id},
@@ -1801,7 +1867,7 @@ class DispatchWebhookJob extends Job {
1801
1867
  `;
1802
1868
  throw error instanceof Error ? error : new Error(errorMessage);
1803
1869
  }
1804
- await connection_default`
1870
+ await repositoryConnection`
1805
1871
  INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
1806
1872
  VALUES (
1807
1873
  ${webhook.id},
@@ -2381,26 +2447,6 @@ function indexMorphToRelation(children, parentsByType, relation) {
2381
2447
  return result;
2382
2448
  }
2383
2449
 
2384
- // ../../src/core/database/boundConnection.ts
2385
- var boundConnectionHolder = {
2386
- connection: null
2387
- };
2388
- function getBoundDatabaseConnection() {
2389
- return boundConnectionHolder.connection;
2390
- }
2391
-
2392
- // ../../src/core/database/repositoryConnection.ts
2393
- function resolveRepositoryConnection() {
2394
- return getBoundDatabaseConnection() ?? connection_default;
2395
- }
2396
- var repositoryConnection = new Proxy({}, {
2397
- get(_target, property) {
2398
- const connection = resolveRepositoryConnection();
2399
- const value = connection[property];
2400
- return typeof value === "function" ? value.bind(connection) : value;
2401
- }
2402
- });
2403
-
2404
2450
  // ../../src/core/database/whereBuilder.ts
2405
2451
  class WhereBuilder {
2406
2452
  nodes = [];
@@ -3729,8 +3775,14 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus) {
3729
3775
  if (tags.length === 0) {
3730
3776
  return;
3731
3777
  }
3732
- const cache = resolveApplicationCache();
3733
- const queue = resolveApplicationQueue();
3778
+ let cache;
3779
+ let queue;
3780
+ try {
3781
+ cache = resolveApplicationCache();
3782
+ queue = resolveApplicationQueue();
3783
+ } catch {
3784
+ return;
3785
+ }
3734
3786
  const job = createTrackedJob("cache.invalidate-tags", new invalidateCacheTagsJob_default(cache));
3735
3787
  await queue.dispatch(job, { tags });
3736
3788
  });
@@ -3822,6 +3874,128 @@ var queueProvider = {
3822
3874
  };
3823
3875
  var queue_default = queueProvider;
3824
3876
 
3877
+ // ../../src/core/storage/storage.ts
3878
+ import { mkdir, readFile, unlink, writeFile } from "fs/promises";
3879
+ import { dirname, join as join3 } from "path";
3880
+ var {S3Client } = globalThis.Bun;
3881
+
3882
+ class LocalStorageDriver {
3883
+ rootDirectory;
3884
+ constructor(rootDirectory) {
3885
+ this.rootDirectory = rootDirectory;
3886
+ }
3887
+ resolveRootDirectory() {
3888
+ return this.rootDirectory ?? process.env.STORAGE_PATH ?? "storage";
3889
+ }
3890
+ resolvePath(path) {
3891
+ return join3(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
3892
+ }
3893
+ async put(path, contents) {
3894
+ const absolutePath = this.resolvePath(path);
3895
+ await mkdir(dirname(absolutePath), { recursive: true });
3896
+ await writeFile(absolutePath, contents);
3897
+ return path;
3898
+ }
3899
+ async get(path) {
3900
+ try {
3901
+ return await readFile(this.resolvePath(path));
3902
+ } catch {
3903
+ return null;
3904
+ }
3905
+ }
3906
+ async delete(path) {
3907
+ try {
3908
+ await unlink(this.resolvePath(path));
3909
+ return true;
3910
+ } catch {
3911
+ return false;
3912
+ }
3913
+ }
3914
+ }
3915
+
3916
+ class S3StorageDriver {
3917
+ client;
3918
+ constructor(client) {
3919
+ this.client = client;
3920
+ }
3921
+ async put(path, contents) {
3922
+ await this.client.write(path.replace(/^\/+/, ""), contents);
3923
+ return path;
3924
+ }
3925
+ async get(path) {
3926
+ const normalizedPath = path.replace(/^\/+/, "");
3927
+ const file = this.client.file(normalizedPath);
3928
+ if (!await file.exists()) {
3929
+ return null;
3930
+ }
3931
+ return new Uint8Array(await file.arrayBuffer());
3932
+ }
3933
+ async delete(path) {
3934
+ try {
3935
+ await this.client.unlink(path.replace(/^\/+/, ""));
3936
+ return true;
3937
+ } catch {
3938
+ return false;
3939
+ }
3940
+ }
3941
+ }
3942
+
3943
+ class StorageManager {
3944
+ driver;
3945
+ constructor(driver) {
3946
+ this.driver = driver;
3947
+ }
3948
+ put(path, contents) {
3949
+ return this.driver.put(path, contents);
3950
+ }
3951
+ get(path) {
3952
+ return this.driver.get(path);
3953
+ }
3954
+ delete(path) {
3955
+ return this.driver.delete(path);
3956
+ }
3957
+ }
3958
+ function resolveS3Config() {
3959
+ const accessKeyId = process.env.AWS_ACCESS_KEY_ID?.trim();
3960
+ const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY?.trim();
3961
+ const bucket = process.env.AWS_BUCKET?.trim();
3962
+ if (!accessKeyId || !secretAccessKey || !bucket) {
3963
+ throw new Error('STORAGE_DRIVER="s3" requires AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_BUCKET.');
3964
+ }
3965
+ return {
3966
+ accessKeyId,
3967
+ secretAccessKey,
3968
+ bucket,
3969
+ ...process.env.AWS_REGION?.trim() ? { region: process.env.AWS_REGION.trim() } : {},
3970
+ ...process.env.AWS_ENDPOINT?.trim() ? { endpoint: process.env.AWS_ENDPOINT.trim() } : {}
3971
+ };
3972
+ }
3973
+ function createS3Client(config = resolveS3Config()) {
3974
+ return new S3Client({
3975
+ accessKeyId: config.accessKeyId,
3976
+ secretAccessKey: config.secretAccessKey,
3977
+ bucket: config.bucket,
3978
+ ...config.region ? { region: config.region } : {},
3979
+ ...config.endpoint ? { endpoint: config.endpoint } : {}
3980
+ });
3981
+ }
3982
+ function createStorageDriver() {
3983
+ const driver = process.env.STORAGE_DRIVER ?? "local";
3984
+ if (driver === "s3") {
3985
+ return new S3StorageDriver(createS3Client());
3986
+ }
3987
+ return new LocalStorageDriver;
3988
+ }
3989
+
3990
+ // ../../src/bootstrap/providers/storage.ts
3991
+ var storageProvider = {
3992
+ name: "core.storage",
3993
+ register({ dependencies }) {
3994
+ dependencies.storage = new StorageManager(createStorageDriver());
3995
+ }
3996
+ };
3997
+ var storage_default = storageProvider;
3998
+
3825
3999
  // ../../src/config/frontend.ts
3826
4000
  function readFrontendMode() {
3827
4001
  const mode = (process.env.FRONTEND_MODE ?? "api").trim();
@@ -3840,9 +4014,6 @@ function isViewsEnabled() {
3840
4014
  // ../../src/core/http/requestMetaContext.ts
3841
4015
  import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
3842
4016
  var requestMetaContext = new AsyncLocalStorage3;
3843
- function runWithRequestMeta(meta, callback) {
3844
- return requestMetaContext.run(meta, callback);
3845
- }
3846
4017
  function currentRequestMeta() {
3847
4018
  return requestMetaContext.getStore() ?? {
3848
4019
  ipAddress: null,
@@ -3851,9 +4022,9 @@ function currentRequestMeta() {
3851
4022
  }
3852
4023
 
3853
4024
  // ../../src/core/view/etaViewEngine.ts
3854
- import { join as join3 } from "path";
4025
+ import { join as join4 } from "path";
3855
4026
  import { Eta } from "eta";
3856
- var DEFAULT_VIEWS_DIRECTORY = join3(process.cwd(), "resources/views");
4027
+ var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
3857
4028
  var DEFAULT_LAYOUT = "layouts/app.eta";
3858
4029
 
3859
4030
  class EtaViewEngine {
@@ -3892,9 +4063,6 @@ function htmlResponse(html, init = {}) {
3892
4063
  }
3893
4064
  });
3894
4065
  }
3895
- // ../../src/core/http/csrfToken.ts
3896
- import { timingSafeEqual as timingSafeEqual2 } from "crypto";
3897
-
3898
4066
  // ../../src/core/http/cookies.ts
3899
4067
  function readRequestCookie(request, name) {
3900
4068
  const cookies = request.cookies;
@@ -3929,14 +4097,6 @@ function resolveCsrfSecret() {
3929
4097
  function csrfVerifyOptions() {
3930
4098
  return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
3931
4099
  }
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
4100
  function createCsrfTokenCookie() {
3941
4101
  const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
3942
4102
  return {
@@ -3951,45 +4111,6 @@ function resolveCsrfToken(request) {
3951
4111
  }
3952
4112
  return createCsrfTokenCookie();
3953
4113
  }
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
4114
  function resolveCsrfTokenForRequest(request) {
3994
4115
  const metaToken = currentRequestMeta().csrfToken;
3995
4116
  if (metaToken) {
@@ -3999,7 +4120,7 @@ function resolveCsrfTokenForRequest(request) {
3999
4120
  }
4000
4121
 
4001
4122
  // ../../src/core/http/flashSession.ts
4002
- import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual3 } from "crypto";
4123
+ import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual2 } from "crypto";
4003
4124
  var FLASH_COOKIE = "workhub_flash";
4004
4125
  var FLASH_TTL_MS = 60 * 1000;
4005
4126
  function resolveFlashSecret() {
@@ -4046,7 +4167,7 @@ function parseFlashCookie(cookieValue) {
4046
4167
  if (expectedBuffer.length !== actualBuffer.length) {
4047
4168
  return null;
4048
4169
  }
4049
- if (!timingSafeEqual3(expectedBuffer, actualBuffer)) {
4170
+ if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
4050
4171
  return null;
4051
4172
  }
4052
4173
  try {
@@ -4062,9 +4183,6 @@ function parseFlashCookie(cookieValue) {
4062
4183
  return null;
4063
4184
  }
4064
4185
  }
4065
- function clearFlashCookie() {
4066
- return `${FLASH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
4067
- }
4068
4186
  function pullFlash(request) {
4069
4187
  const cookieValue = readFlashCookie(request);
4070
4188
  if (!cookieValue) {
@@ -4072,15 +4190,6 @@ function pullFlash(request) {
4072
4190
  }
4073
4191
  return parseFlashCookie(cookieValue);
4074
4192
  }
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
4193
 
4085
4194
  // ../../src/core/view/webLayoutData.ts
4086
4195
  async function resolveWebLayoutData(container, request) {
@@ -4140,6 +4249,7 @@ var viewProvider = {
4140
4249
  var coreProviders = [
4141
4250
  config_default,
4142
4251
  cache_default,
4252
+ storage_default,
4143
4253
  auth_default,
4144
4254
  events_default,
4145
4255
  policy_default,
@@ -4240,7 +4350,7 @@ function createAppContext() {
4240
4350
  return appContext;
4241
4351
  }
4242
4352
  // ../../src/bootstrap/createWebRoutes.ts
4243
- import { join as join4 } from "path";
4353
+ import { join as join5 } from "path";
4244
4354
 
4245
4355
  // ../../src/core/http/middleware.ts
4246
4356
  function isRouteHandler(value) {
@@ -4272,17 +4382,6 @@ function composeMiddleware(...middleware) {
4272
4382
  };
4273
4383
  };
4274
4384
  }
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
4385
  function wrapRouteHandler(handler, middleware) {
4287
4386
  if (isMethodRouteMap(handler)) {
4288
4387
  const wrapped = {};
@@ -4304,6 +4403,32 @@ function applyMiddlewareToRoutes(routes, middleware) {
4304
4403
  return wrapped;
4305
4404
  }
4306
4405
 
4406
+ // ../../src/bootstrap/httpKernel.ts
4407
+ import {
4408
+ createAuthMiddleware,
4409
+ createAuthorizeMiddleware,
4410
+ createBodySizeLimitMiddleware,
4411
+ createCorsMiddleware,
4412
+ createCsrfMiddleware,
4413
+ createFlashMiddleware,
4414
+ createLoginThrottleMiddleware,
4415
+ createMembershipMiddleware,
4416
+ createMemoryThrottleMiddleware,
4417
+ createMetricsMiddleware,
4418
+ createRequestLoggingMiddleware,
4419
+ createRequireAbilityMiddleware,
4420
+ createRequireAuthMiddleware,
4421
+ createRequireGlobalAdminMiddleware,
4422
+ createRequireWebAuthMiddleware,
4423
+ createSecurityHeadersMiddleware,
4424
+ createTenantMiddleware,
4425
+ createThrottleMiddleware,
4426
+ createTracingMiddleware,
4427
+ isPublicReadsEnabled,
4428
+ requestIdMiddleware,
4429
+ withMiddleware
4430
+ } from "@getstrata/core";
4431
+
4307
4432
  // ../../src/config/rateLimit.ts
4308
4433
  var LOCAL_LOGIN_RATE_LIMIT = {
4309
4434
  maxAttempts: 100,
@@ -4338,849 +4463,6 @@ function resolveRegisterRateLimit() {
4338
4463
  };
4339
4464
  }
4340
4465
 
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
4466
  // ../../src/bootstrap/httpKernel.ts
5185
4467
  class HttpKernel {
5186
4468
  dependencies;
@@ -5402,7 +4684,7 @@ function createWebRoutes(dependencies) {
5402
4684
  registerRoute("GET", "/assets/*", ["global", "web"]);
5403
4685
  const pathname = new URL(request.url).pathname;
5404
4686
  const relativePath = pathname.replace(/^\//, "");
5405
- const file = Bun.file(join4(process.cwd(), "public", relativePath));
4687
+ const file = Bun.file(join5(process.cwd(), "public", relativePath));
5406
4688
  if (!await file.exists()) {
5407
4689
  return htmlResponse("Not Found", { status: 404 });
5408
4690
  }
@@ -5576,7 +4858,7 @@ function createWebServer(options) {
5576
4858
  });
5577
4859
  }
5578
4860
  // ../../src/bootstrap/web/session.ts
5579
- import { createHash, randomBytes as randomBytes2 } from "crypto";
4861
+ import { createHash, randomBytes } from "crypto";
5580
4862
  class CookieSessionStore {
5581
4863
  sql;
5582
4864
  secret;
@@ -5602,7 +4884,7 @@ class CookieSessionStore {
5602
4884
  return header.includes("Secure") ? header : `${header}; Secure`;
5603
4885
  }
5604
4886
  async create(user) {
5605
- const id = randomBytes2(32).toString("hex");
4887
+ const id = randomBytes(32).toString("hex");
5606
4888
  const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
5607
4889
  await this.sql.unsafe(`INSERT INTO sessions (id, user_id, expires_at) VALUES ($1, $2, $3)`, [
5608
4890
  id,