@getstrata/core 0.5.13 → 0.5.15

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 (46) hide show
  1. package/README.md +16 -6
  2. package/dist/bootstrap/contracts.d.ts +2 -0
  3. package/dist/bootstrap/httpKernel.d.ts +1 -2
  4. package/dist/core/admin/registry.d.ts +1 -0
  5. package/dist/core/auth/membershipService.d.ts +5 -1
  6. package/dist/core/database/baseRepository.d.ts +6 -1
  7. package/dist/core/database/connectionContext.d.ts +2 -1
  8. package/dist/core/database/defaultConnection.d.ts +6 -0
  9. package/dist/core/database/queryProxy.d.ts +3 -0
  10. package/dist/core/database/repositoryConnection.d.ts +3 -3
  11. package/dist/core/jobs/dispatchWebhookJob.d.ts +0 -1
  12. package/dist/core/security/safeFetch.d.ts +2 -0
  13. package/dist/core/security/safeUrl.d.ts +16 -1
  14. package/dist/core/storage/storage.d.ts +6 -3
  15. package/dist/core/tenant/tenantDatabaseScope.d.ts +2 -1
  16. package/dist/db/connection/index.d.ts +1 -1
  17. package/dist/entries/auth/accessControl.js +1 -113
  18. package/dist/entries/auth/authContext.js +1 -15
  19. package/dist/entries/auth/guard.js +1 -3044
  20. package/dist/entries/auth/membershipContext.js +1 -276
  21. package/dist/entries/auth/membershipScope.js +1 -390
  22. package/dist/entries/auth/membershipService.js +1 -480
  23. package/dist/entries/auth/policy.js +1 -134
  24. package/dist/entries/database.js +1 -2398
  25. package/dist/entries/http/csrfToken.js +0 -6
  26. package/dist/entries/http/middleware.js +1 -68
  27. package/dist/entries/http/requestMetaContext.js +1 -18
  28. package/dist/entries/http/webErrorResponse.js +94 -563
  29. package/dist/entries/http/webFormRequest.js +0 -131
  30. package/dist/entries/http.js +1 -4091
  31. package/dist/entries/jobs/dispatchWebhookJob.js +109 -102
  32. package/dist/entries/queue/createAppQueue.js +111 -631
  33. package/dist/entries/queue/jobRunner.js +0 -3
  34. package/dist/entries/queue/publicQueue.js +45 -546
  35. package/dist/entries/queue/queueMetrics.js +111 -631
  36. package/dist/entries/security/safeUrl.js +29 -0
  37. package/dist/entries/security/securityEvents.js +1 -41
  38. package/dist/entries/storage/storage.js +14 -4
  39. package/dist/entries/tenant/tenantContext.js +1 -30
  40. package/dist/entries/tenant/tenantMiddleware.js +1 -312
  41. package/dist/entries/tracing/traceContext.js +1 -15
  42. package/dist/entries/view.js +94 -563
  43. package/dist/framework/public-api.d.ts +36 -4
  44. package/dist/index.js +2270 -1666
  45. package/dist/modules/user/repository.d.ts +1 -0
  46. package/package.json +6 -5
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()];
@@ -199,15 +205,6 @@ class AdminResourceRegistry {
199
205
  this.resources.clear();
200
206
  }
201
207
  }
202
- // ../../src/core/auth/authContext.ts
203
- import { AsyncLocalStorage } from "async_hooks";
204
- var authContext = new AsyncLocalStorage;
205
- function runWithAuthUser(user, callback) {
206
- return authContext.run(user, callback);
207
- }
208
- function currentAuthUser() {
209
- return authContext.getStore() ?? null;
210
- }
211
208
  // ../../src/core/errors/http.ts
212
209
  class HttpError extends Error {
213
210
  status;
@@ -274,125 +271,97 @@ class PreconditionFailedError extends HttpError {
274
271
  }
275
272
  }
276
273
 
277
- // ../../src/core/auth/policy.ts
278
- class Policy {
279
- constructor() {}
280
- view(_user, _resource) {
281
- return false;
282
- }
283
- create(_user) {
284
- return false;
285
- }
286
- update(_user, _resource) {
287
- return false;
288
- }
289
- delete(_user, _resource) {
290
- return false;
291
- }
274
+ // ../../src/core/auth/authContext.ts
275
+ import { AsyncLocalStorage } from "async_hooks";
276
+ var authContext = new AsyncLocalStorage;
277
+ function runWithAuthUser(user, callback) {
278
+ return authContext.run(user, callback);
279
+ }
280
+ function currentAuthUser() {
281
+ return authContext.getStore() ?? null;
292
282
  }
293
- var BLOCKED_POLICY_ACTIONS = new Set([
294
- "constructor",
295
- "toString",
296
- "valueOf",
297
- "hasOwnProperty",
298
- "isPrototypeOf",
299
- "propertyIsEnumerable",
300
- "__proto__"
301
- ]);
302
283
 
303
- class PolicyGate {
304
- constructor() {}
305
- policies = new Map;
306
- register(resource, policy) {
307
- this.policies.set(resource, policy);
308
- }
309
- allows(resource, action, user, model) {
310
- const policy = this.policies.get(resource);
311
- if (!policy) {
312
- return false;
313
- }
314
- if (BLOCKED_POLICY_ACTIONS.has(action) || !(action in policy)) {
315
- return false;
316
- }
317
- const handler = policy[action];
318
- if (typeof handler !== "function") {
319
- return false;
320
- }
321
- const resolvedUser = user === undefined ? currentAuthUser() : user;
322
- return model === undefined ? handler.call(policy, resolvedUser) : handler.call(policy, resolvedUser, model);
323
- }
324
- authorize(resource, action, user, model) {
325
- if (!this.allows(resource, action, user, model)) {
326
- throw new ForbiddenError;
327
- }
328
- }
284
+ // ../../src/core/auth/accessControl.ts
285
+ var ROLE_RANK = {
286
+ member: 1,
287
+ admin: 2,
288
+ owner: 3
289
+ };
290
+ function isGlobalAdmin(user) {
291
+ return user?.role === "admin";
329
292
  }
330
- // ../../src/core/cache/taggedCache.ts
331
- class TaggedCache {
332
- store;
333
- tags;
334
- constructor(store, tags) {
335
- this.store = store;
336
- this.tags = tags;
293
+ function hasMinimumOrgRole(role, minimum) {
294
+ if (!role) {
295
+ return false;
337
296
  }
338
- async remember(key, callback, ttlMs) {
339
- const value = await this.store.getOrSet(key, callback, ttlMs);
340
- await this.store.attachTags(key, this.tags);
341
- return value;
297
+ return ROLE_RANK[role] >= ROLE_RANK[minimum];
298
+ }
299
+ function resolveUserId(user) {
300
+ const userId = typeof user.id === "number" ? user.id : Number(user.id);
301
+ if (!Number.isInteger(userId) || userId <= 0) {
302
+ throw new ForbiddenError("Invalid authenticated user.");
342
303
  }
343
- async flush() {
344
- return this.store.flushTags(this.tags);
304
+ return userId;
305
+ }
306
+ // ../../src/domain/abilities.ts
307
+ var MEMBER_ABILITIES = [
308
+ "organizations:read",
309
+ "projects:read",
310
+ "projects:create",
311
+ "tasks:read",
312
+ "tasks:create",
313
+ "comments:read",
314
+ "comments:create",
315
+ "attachments:read",
316
+ "attachments:create",
317
+ "auth:tokens:read",
318
+ "auth:tokens:write"
319
+ ];
320
+ var ADMIN_ABILITIES = [
321
+ ...MEMBER_ABILITIES,
322
+ "organizations:create",
323
+ "organizations:update",
324
+ "organizations:delete",
325
+ "projects:update",
326
+ "projects:delete",
327
+ "tasks:update",
328
+ "tasks:delete",
329
+ "comments:update",
330
+ "comments:delete",
331
+ "attachments:delete",
332
+ "webhooks:read",
333
+ "webhooks:write",
334
+ "audit:read"
335
+ ];
336
+ var PLATFORM_ADMIN_ABILITIES = ["*"];
337
+ function resolveAbilitiesForRole(role) {
338
+ if (role === "admin") {
339
+ return [...PLATFORM_ADMIN_ABILITIES];
345
340
  }
341
+ return [...MEMBER_ABILITIES];
346
342
  }
347
- var taggedCache_default = TaggedCache;
348
343
 
349
- // ../../src/core/cache/repository.ts
350
- class CacheRepository {
351
- store;
352
- constructor(store) {
353
- this.store = store;
354
- }
355
- async get(key) {
356
- return this.store.get(key);
357
- }
358
- async remember(key, callback, ttlMs) {
359
- return this.store.getOrSet(key, callback, ttlMs);
360
- }
361
- async forget(key) {
362
- return this.store.invalidate(key);
363
- }
364
- async flush() {
365
- await this.store.clear();
366
- }
367
- tags(...names) {
368
- return new taggedCache_default(this.store, names);
369
- }
370
- async getOrSet(key, loader, ttlMs) {
371
- return this.remember(key, loader, ttlMs);
372
- }
373
- async invalidate(key) {
374
- return this.forget(key);
375
- }
376
- async invalidateByPrefix(prefix) {
377
- return this.store.invalidateByPrefix(prefix);
378
- }
379
- async clear() {
380
- await this.flush();
381
- }
382
- async size() {
383
- return this.store.size();
384
- }
344
+ // ../../src/config/features.ts
345
+ function readFeatureFlags() {
346
+ return {
347
+ webhooks: (process.env.FEATURE_WEBHOOKS ?? "true") !== "false",
348
+ fullTextSearch: (process.env.FEATURE_SEARCH ?? "true") !== "false",
349
+ auditLog: (process.env.FEATURE_AUDIT_LOG ?? "true") !== "false",
350
+ oauthLogin: (process.env.FEATURE_OAUTH ?? "true") !== "false",
351
+ samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
352
+ scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
353
+ billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
354
+ siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
355
+ publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
356
+ emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
357
+ mfa: (process.env.FEATURE_MFA ?? "false") === "true"
358
+ };
385
359
  }
386
- var repository_default = CacheRepository;
387
- // ../../src/core/cache/tags.ts
388
- var CACHE_TAGS = {
389
- organizations: "organizations",
390
- projects: "projects",
391
- tasks: "tasks",
392
- comments: "comments",
393
- attachments: "attachments",
394
- reports: "reports"
395
- };
360
+ var featureFlags = readFeatureFlags();
361
+ function isFeatureEnabled(feature) {
362
+ return readFeatureFlags()[feature];
363
+ }
364
+
396
365
  // ../../src/core/events/eventBus.ts
397
366
  class EventBus {
398
367
  constructor() {}
@@ -598,14 +567,6 @@ function appendWhereParts(tableName, where, params) {
598
567
  }
599
568
  return clauses.join(" AND ");
600
569
  }
601
- function buildWhereClause(tableName, where = {}) {
602
- const params = [];
603
- const body = appendWhereParts(tableName, where, params);
604
- return {
605
- clause: body.length > 0 ? ` WHERE ${body}` : "",
606
- params
607
- };
608
- }
609
570
  function buildWhereNodeClause(tableName, node, params) {
610
571
  if ("where" in node) {
611
572
  return appendWhereParts(tableName, node.where, params);
@@ -1053,18 +1014,19 @@ function indexMorphToRelation(children, parentsByType, relation) {
1053
1014
  return result;
1054
1015
  }
1055
1016
 
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)
1017
+ // ../../src/core/database/boundConnection.ts
1018
+ var boundConnectionHolder = {
1019
+ connection: null
1067
1020
  };
1021
+ function bindDatabaseConnection(connection) {
1022
+ boundConnectionHolder.connection = connection;
1023
+ }
1024
+ function getBoundDatabaseConnection() {
1025
+ return boundConnectionHolder.connection;
1026
+ }
1027
+ function resetBoundDatabaseConnection() {
1028
+ boundConnectionHolder.connection = null;
1029
+ }
1068
1030
 
1069
1031
  // ../../src/core/database/connectionContext.ts
1070
1032
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
@@ -1075,72 +1037,68 @@ function runWithDatabaseConnection(connection, callback) {
1075
1037
  function getActiveDatabaseConnection(fallback) {
1076
1038
  return activeConnection.getStore() ?? fallback;
1077
1039
  }
1040
+ function hasActiveDatabaseConnection() {
1041
+ return activeConnection.getStore() !== undefined;
1042
+ }
1078
1043
 
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.");
1044
+ // ../../src/core/database/queryProxy.ts
1045
+ var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
1046
+ function createDatabaseQueryProxy(pool) {
1047
+ function resolveDatabase() {
1048
+ return getActiveDatabaseConnection(pool);
1084
1049
  }
1085
- return new SQL({
1086
- url: config.url,
1087
- max: config.poolMax,
1088
- idleTimeout: config.idleTimeoutSeconds,
1089
- maxLifetime: config.maxLifetimeSeconds,
1090
- connectionTimeout: config.connectionTimeoutSeconds
1050
+ function resolveDatabaseForProperty(property) {
1051
+ if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
1052
+ return pool;
1053
+ }
1054
+ return resolveDatabase();
1055
+ }
1056
+ return new Proxy(function database() {}, {
1057
+ apply(_target, _thisArg, args) {
1058
+ return resolveDatabase()(...args);
1059
+ },
1060
+ get(_target, property) {
1061
+ const connection = resolveDatabaseForProperty(property);
1062
+ const value = connection[property];
1063
+ return typeof value === "function" ? value.bind(connection) : value;
1064
+ }
1091
1065
  });
1092
1066
  }
1093
1067
 
1094
- // ../../src/db/connection/index.ts
1095
- var connectionHolder = {
1068
+ // ../../src/core/database/defaultConnection.ts
1069
+ var defaultPool = {
1096
1070
  connection: null
1097
1071
  };
1098
- function getDatabase() {
1099
- if (!connectionHolder.connection) {
1100
- connectionHolder.connection = createDatabaseConnection(databaseConfig);
1101
- }
1102
- return connectionHolder.connection;
1072
+ var defaultQuery = {
1073
+ connection: null
1074
+ };
1075
+ function registerDefaultDatabasePool(connection) {
1076
+ defaultPool.connection = connection;
1077
+ defaultQuery.connection = createDatabaseQueryProxy(connection);
1103
1078
  }
1104
- var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
1105
- function resolveDatabase() {
1106
- return getActiveDatabaseConnection(getDatabase());
1079
+ function getDefaultDatabasePool() {
1080
+ if (!defaultPool.connection) {
1081
+ throw new Error("Default database pool is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
1082
+ }
1083
+ return defaultPool.connection;
1107
1084
  }
1108
- function resolveDatabaseForProperty(property) {
1109
- if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
1110
- return getDatabase();
1085
+ function getDefaultDatabaseQuery() {
1086
+ if (!defaultQuery.connection) {
1087
+ throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
1111
1088
  }
1112
- return resolveDatabase();
1089
+ return defaultQuery.connection;
1113
1090
  }
1114
- var db = new Proxy(function database() {}, {
1091
+
1092
+ // ../../src/core/database/repositoryConnection.ts
1093
+ function resolveRepositoryConnection() {
1094
+ return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
1095
+ }
1096
+ var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
1115
1097
  apply(_target, _thisArg, args) {
1116
- return resolveDatabase()(...args);
1098
+ return resolveRepositoryConnection()(...args);
1117
1099
  },
1118
1100
  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
- // ../../src/core/database/boundConnection.ts
1127
- var boundConnectionHolder = {
1128
- connection: null
1129
- };
1130
- function bindDatabaseConnection(connection) {
1131
- boundConnectionHolder.connection = connection;
1132
- }
1133
- function getBoundDatabaseConnection() {
1134
- return boundConnectionHolder.connection;
1135
- }
1136
-
1137
- // ../../src/core/database/repositoryConnection.ts
1138
- function resolveRepositoryConnection() {
1139
- return getBoundDatabaseConnection() ?? connection_default;
1140
- }
1141
- var repositoryConnection = new Proxy({}, {
1142
- get(_target, property) {
1143
- const connection = resolveRepositoryConnection();
1101
+ const connection = resolveRepositoryConnection();
1144
1102
  const value = connection[property];
1145
1103
  return typeof value === "function" ? value.bind(connection) : value;
1146
1104
  }
@@ -1701,130 +1659,14 @@ class BaseRepository {
1701
1659
  }
1702
1660
  }
1703
1661
  var baseRepository_default = BaseRepository;
1704
- // ../../src/core/database/bindConnection.ts
1705
- function bindDatabaseConnection2(connection) {
1706
- bindDatabaseConnection(connection);
1707
- }
1708
1662
  // ../../src/core/database/connection.ts
1709
- function createDatabaseConnection2(source) {
1663
+ function createDatabaseConnection(source) {
1710
1664
  return {
1711
1665
  async unsafe(query, params = []) {
1712
1666
  return await source.unsafe(query, params);
1713
1667
  }
1714
1668
  };
1715
1669
  }
1716
- // ../../src/core/database/migrations/advisoryLock.ts
1717
- var MIGRATION_LOCK_KEY = 42424242;
1718
- async function withMigrationLock(db2, callback, lockKey = MIGRATION_LOCK_KEY) {
1719
- await db2.unsafe("SELECT pg_advisory_lock($1)", [lockKey]);
1720
- try {
1721
- return await callback();
1722
- } finally {
1723
- await db2.unsafe("SELECT pg_advisory_unlock($1)", [lockKey]);
1724
- }
1725
- }
1726
- // ../../src/core/database/migrations/runner.ts
1727
- import { readdir } from "fs/promises";
1728
- import { join } from "path";
1729
- import { pathToFileURL } from "url";
1730
- var MIGRATIONS_TABLE = "framework_migrations";
1731
- async function ensureMigrationsTable(db2) {
1732
- await db2.unsafe(`
1733
- CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
1734
- name TEXT PRIMARY KEY,
1735
- batch INTEGER NOT NULL,
1736
- run_on TIMESTAMPTZ NOT NULL DEFAULT NOW()
1737
- )
1738
- `);
1739
- }
1740
- async function getAppliedMigrations(db2) {
1741
- await ensureMigrationsTable(db2);
1742
- return await db2.unsafe(`
1743
- SELECT name, batch
1744
- FROM ${MIGRATIONS_TABLE}
1745
- ORDER BY batch ASC, name ASC
1746
- `);
1747
- }
1748
- async function loadMigrationsFromDirectory(directory) {
1749
- const entries = await readdir(directory);
1750
- const migrationFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && entry !== "types.ts" && entry !== "index.ts" && entry !== "runner.ts").sort();
1751
- const loadedMigrations = await Promise.all(migrationFiles.map(async (fileName) => {
1752
- const moduleUrl = pathToFileURL(join(directory, fileName)).href;
1753
- const module = await import(moduleUrl);
1754
- return module.default;
1755
- }));
1756
- return loadedMigrations.filter((migration) => migration?.name !== undefined);
1757
- }
1758
- async function getMigrationStatus(db2, migrations) {
1759
- const applied = await getAppliedMigrations(db2);
1760
- const appliedByName = new Map(applied.map(({ name, batch }) => [name, Number(batch)]));
1761
- return migrations.map(({ name }) => ({
1762
- name,
1763
- status: appliedByName.has(name) ? "up" : "pending",
1764
- batch: appliedByName.get(name) ?? null
1765
- }));
1766
- }
1767
- async function runPendingMigrations(db2, migrations, options = {}) {
1768
- const applied = await getAppliedMigrations(db2);
1769
- const appliedNames = new Set(applied.map(({ name }) => name));
1770
- const nextBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0) + 1;
1771
- const pendingMigrations = migrations.filter(({ name }) => !appliedNames.has(name));
1772
- for (const migration of pendingMigrations) {
1773
- options.onMigration?.(migration.name);
1774
- await migration.up(db2);
1775
- const inserted = await db2.unsafe(`INSERT INTO ${MIGRATIONS_TABLE} (name, batch) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING RETURNING name`, [migration.name, nextBatch]);
1776
- if (inserted.length === 0) {
1777
- throw new Error(`Migration ${migration.name} was applied but not recorded.`);
1778
- }
1779
- }
1780
- return pendingMigrations.length;
1781
- }
1782
- async function migrateDatabase(db2, migrations, options = {}) {
1783
- const { advisoryLock = false, onMigration } = options;
1784
- if (advisoryLock) {
1785
- return withMigrationLock(db2, () => runPendingMigrations(db2, migrations, { onMigration }));
1786
- }
1787
- return runPendingMigrations(db2, migrations, { onMigration });
1788
- }
1789
- async function rollbackDatabase(db2, migrations, options = {}) {
1790
- const applied = await getAppliedMigrations(db2);
1791
- if (applied.length === 0) {
1792
- return 0;
1793
- }
1794
- const lastBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0);
1795
- const migrationsToRollback = new Set(applied.filter(({ batch }) => Number(batch) === lastBatch).map(({ name }) => name));
1796
- let rolledBack = 0;
1797
- for (const migration of [...migrations].reverse()) {
1798
- if (!migrationsToRollback.has(migration.name)) {
1799
- continue;
1800
- }
1801
- options.onMigration?.(migration.name);
1802
- await migration.down(db2);
1803
- await db2.unsafe(`DELETE FROM ${MIGRATIONS_TABLE} WHERE name = $1`, [migration.name]);
1804
- rolledBack += 1;
1805
- }
1806
- return rolledBack;
1807
- }
1808
- async function freshDatabase(db2, migrations, options = {}) {
1809
- const runFresh = async () => {
1810
- const applied = await getAppliedMigrations(db2);
1811
- const appliedNames = new Set(applied.map(({ name }) => name));
1812
- const appliedMigrations = migrations.filter(({ name }) => appliedNames.has(name));
1813
- for (const migration of [...appliedMigrations].reverse()) {
1814
- options.onMigration?.(migration.name);
1815
- await migration.down(db2);
1816
- }
1817
- if (appliedMigrations.length > 0) {
1818
- await db2.unsafe(`DELETE FROM ${MIGRATIONS_TABLE}`);
1819
- }
1820
- await runPendingMigrations(db2, migrations, options);
1821
- };
1822
- if (options.advisoryLock) {
1823
- await withMigrationLock(db2, runFresh);
1824
- return;
1825
- }
1826
- await runFresh();
1827
- }
1828
1670
  // ../../src/core/database/model.ts
1829
1671
  var modelRepositories = new WeakMap;
1830
1672
  var modelGlobalScopes = new WeakMap;
@@ -2736,9 +2578,9 @@ class SchemaBuilder {
2736
2578
  toSql() {
2737
2579
  return [...this.#statements];
2738
2580
  }
2739
- async execute(db2) {
2581
+ async execute(db) {
2740
2582
  for (const statement of this.#statements) {
2741
- await db2.unsafe(statement);
2583
+ await db.unsafe(statement);
2742
2584
  }
2743
2585
  }
2744
2586
  }
@@ -2747,45 +2589,20 @@ class Schema {
2747
2589
  static builder(driver) {
2748
2590
  return new SchemaBuilder(driver ?? resolveDatabaseDriver());
2749
2591
  }
2750
- static async run(db2, driver, callback) {
2592
+ static async run(db, driver, callback) {
2751
2593
  const schema = Schema.builder(driver);
2752
2594
  await callback(schema);
2753
- await schema.execute(db2);
2595
+ await schema.execute(db);
2754
2596
  }
2755
2597
  }
2756
- function createSchemaBuilder(db2, driver) {
2598
+ function createSchemaBuilder(db, driver) {
2757
2599
  const builder = Schema.builder(driver);
2758
2600
  return Object.assign(builder, {
2759
2601
  async commit() {
2760
- await builder.execute(db2);
2602
+ await builder.execute(db);
2761
2603
  }
2762
2604
  });
2763
2605
  }
2764
- // ../../src/core/database/seeders/runner.ts
2765
- import { readdir as readdir2 } from "fs/promises";
2766
- import { join as join2 } from "path";
2767
- import { pathToFileURL as pathToFileURL2 } from "url";
2768
- async function loadSeedersFromDirectory(directory) {
2769
- const entries = await readdir2(directory);
2770
- const seederFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && entry !== "types.ts" && entry !== "index.ts" && entry !== "runner.ts").sort();
2771
- const loadedSeeders = await Promise.all(seederFiles.map(async (fileName) => {
2772
- const moduleUrl = pathToFileURL2(join2(directory, fileName)).href;
2773
- const module = await import(moduleUrl);
2774
- return module.default;
2775
- }));
2776
- return loadedSeeders.filter((seeder) => seeder?.name !== undefined);
2777
- }
2778
- async function runSeedersFromDirectory(directory, db2, options) {
2779
- const seeders = await loadSeedersFromDirectory(directory);
2780
- if (seeders.length === 0) {
2781
- return 0;
2782
- }
2783
- for (const seeder of seeders) {
2784
- options?.onSeeder?.(seeder.name);
2785
- await seeder.run(db2);
2786
- }
2787
- return seeders.length;
2788
- }
2789
2606
  // ../../src/core/database/table.ts
2790
2607
  function defineTable(definition) {
2791
2608
  return definition;
@@ -2800,1419 +2617,1830 @@ async function runInTransaction(operation) {
2800
2617
  throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
2801
2618
  }
2802
2619
  return await pool.begin(async (transaction) => {
2803
- return await operation(createDatabaseConnection2(transaction));
2620
+ return await operation(createDatabaseConnection(transaction));
2804
2621
  });
2805
2622
  }
2806
- // ../../src/core/mail/mailer.ts
2807
- function resolveSmtpConfig() {
2808
- const host = process.env.MAIL_HOST?.trim();
2809
- if (!host) {
2810
- throw new Error('MAIL_DRIVER="smtp" requires MAIL_HOST to be set.');
2811
- }
2812
- const from = process.env.MAIL_FROM?.trim();
2813
- if (!from) {
2814
- throw new Error('MAIL_DRIVER="smtp" requires MAIL_FROM to be set.');
2623
+ // ../../src/config/database.ts
2624
+ function readInteger(name, fallback) {
2625
+ const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
2626
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
2627
+ }
2628
+ var databaseConfig = {
2629
+ url: process.env.DATABASE_URL ?? "",
2630
+ poolMax: readInteger("DB_POOL_MAX", 10),
2631
+ idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
2632
+ maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
2633
+ connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
2634
+ };
2635
+
2636
+ // ../../src/db/connection/createConnection.ts
2637
+ var {SQL } = globalThis.Bun;
2638
+ function createDatabaseConnection2(config) {
2639
+ if (!config.url) {
2640
+ throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
2815
2641
  }
2816
- const port = Number.parseInt(process.env.MAIL_PORT ?? "587", 10);
2817
- if (!Number.isInteger(port) || port <= 0) {
2818
- throw new Error('Environment variable "MAIL_PORT" must be a positive integer.');
2642
+ return new SQL({
2643
+ url: config.url,
2644
+ max: config.poolMax,
2645
+ idleTimeout: config.idleTimeoutSeconds,
2646
+ maxLifetime: config.maxLifetimeSeconds,
2647
+ connectionTimeout: config.connectionTimeoutSeconds
2648
+ });
2649
+ }
2650
+
2651
+ // ../../src/db/connection/index.ts
2652
+ var connectionHolder = {
2653
+ connection: null
2654
+ };
2655
+ function getDatabase() {
2656
+ if (!connectionHolder.connection) {
2657
+ connectionHolder.connection = createDatabaseConnection2(databaseConfig);
2658
+ registerDefaultDatabasePool(connectionHolder.connection);
2819
2659
  }
2820
- return {
2821
- host,
2822
- port,
2823
- from,
2824
- secure: (process.env.MAIL_SECURE ?? "false") === "true",
2825
- ...process.env.MAIL_USERNAME?.trim() ? { username: process.env.MAIL_USERNAME.trim() } : {},
2826
- ...process.env.MAIL_PASSWORD?.trim() ? { password: process.env.MAIL_PASSWORD.trim() } : {}
2827
- };
2660
+ return connectionHolder.connection;
2828
2661
  }
2829
- function encodeBase64(value) {
2830
- return Buffer.from(value, "utf8").toString("base64");
2662
+ function getDb() {
2663
+ getDatabase();
2664
+ return getDefaultDatabaseQuery();
2831
2665
  }
2832
- function parseSmtpResponses(buffer) {
2833
- const responses = [];
2834
- let remainder = buffer;
2835
- while (remainder.includes(`\r
2836
- `)) {
2837
- const index = remainder.indexOf(`\r
2838
- `);
2839
- const line = remainder.slice(0, index);
2840
- remainder = remainder.slice(index + 2);
2841
- if (line.length >= 4 && line[3] === "-") {
2842
- continue;
2843
- }
2844
- responses.push(line);
2666
+ var db = new Proxy(function database() {}, {
2667
+ apply(_target, _thisArg, args) {
2668
+ return getDb()(...args);
2669
+ },
2670
+ get(_target, property) {
2671
+ const connection = getDb();
2672
+ const value = connection[property];
2673
+ return typeof value === "function" ? value.bind(connection) : value;
2845
2674
  }
2846
- return { responses, remainder };
2675
+ });
2676
+ var connection_default = db;
2677
+
2678
+ // ../../src/modules/user/apiTokenTable.ts
2679
+ var apiTokenTable = defineTable({
2680
+ name: "api_token",
2681
+ primaryKey: "id",
2682
+ columns: [
2683
+ "id",
2684
+ "user_id",
2685
+ "name",
2686
+ "token_hash",
2687
+ "abilities",
2688
+ "last_used_at",
2689
+ "expires_at",
2690
+ "created_at"
2691
+ ],
2692
+ defaultOrderBy: { column: "id", direction: "ASC" }
2693
+ });
2694
+
2695
+ // ../../src/core/auth/password.ts
2696
+ async function verifyPassword(password, passwordHash) {
2697
+ return await Bun.password.verify(password, passwordHash);
2847
2698
  }
2848
- async function waitForSmtpResponse(readResponse, expectedCodes) {
2849
- const response = await readResponse();
2850
- const code = response.slice(0, 3);
2851
- if (!expectedCodes.includes(code)) {
2852
- throw new Error(`Unexpected SMTP response: ${response}`);
2699
+
2700
+ // ../../src/core/crypto/fieldEncryption.ts
2701
+ import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "crypto";
2702
+ var ENCRYPTION_PREFIX = "enc:v1:";
2703
+ var IV_LENGTH = 12;
2704
+ var TAG_LENGTH = 16;
2705
+ function resolveEncryptionKey() {
2706
+ const raw = process.env.KMS_ENCRYPTION_KEY?.trim();
2707
+ if (!raw) {
2708
+ return null;
2853
2709
  }
2854
- return response;
2710
+ if (/^[0-9a-f]{64}$/i.test(raw)) {
2711
+ return Buffer.from(raw, "hex");
2712
+ }
2713
+ const decoded = Buffer.from(raw, "base64");
2714
+ if (decoded.length === 32) {
2715
+ return decoded;
2716
+ }
2717
+ throw new Error("KMS_ENCRYPTION_KEY must be 32 bytes (hex or base64).");
2855
2718
  }
2856
- async function openSmtpConnection(config) {
2857
- let buffer = "";
2858
- const waiters = [];
2859
- const readResponse = () => new Promise((resolve, reject) => {
2860
- const parsed = parseSmtpResponses(buffer);
2861
- if (parsed.responses.length > 0) {
2862
- buffer = parsed.remainder;
2863
- resolve(parsed.responses.shift());
2864
- return;
2865
- }
2866
- waiters.push({ resolve, reject });
2867
- });
2868
- const socket = await Bun.connect({
2869
- hostname: config.host,
2870
- port: config.port,
2871
- socket: {
2872
- open() {},
2873
- data(_socket, chunk) {
2874
- buffer += Buffer.from(chunk).toString("utf8");
2875
- const parsed = parseSmtpResponses(buffer);
2876
- buffer = parsed.remainder;
2877
- while (parsed.responses.length > 0 && waiters.length > 0) {
2878
- const response = parsed.responses.shift();
2879
- waiters.shift()?.resolve(response);
2880
- }
2881
- },
2882
- error(_socket, error) {
2883
- const pending = waiters.splice(0);
2884
- for (const waiter of pending) {
2885
- waiter.reject(error instanceof Error ? error : new Error(String(error)));
2886
- }
2887
- }
2888
- }
2889
- });
2890
- return { socket, readResponse };
2719
+ function isFieldEncryptionEnabled() {
2720
+ const featureFlag = process.env.FEATURE_FIELD_ENCRYPTION;
2721
+ if (featureFlag === "false") {
2722
+ return false;
2723
+ }
2724
+ if (featureFlag === "true") {
2725
+ return true;
2726
+ }
2727
+ return (process.env.APP_ENV ?? "local") === "production";
2891
2728
  }
2892
- async function defaultSmtpTransport(config, message) {
2893
- const { socket, readResponse } = await openSmtpConnection(config);
2894
- try {
2895
- await waitForSmtpResponse(readResponse, ["220"]);
2896
- await socket.write(`EHLO workhub.local\r
2897
- `);
2898
- await waitForSmtpResponse(readResponse, ["250"]);
2899
- if (config.username && config.password) {
2900
- await socket.write(`AUTH LOGIN\r
2901
- `);
2902
- await waitForSmtpResponse(readResponse, ["334"]);
2903
- await socket.write(`${encodeBase64(config.username)}\r
2904
- `);
2905
- await waitForSmtpResponse(readResponse, ["334"]);
2906
- await socket.write(`${encodeBase64(config.password)}\r
2907
- `);
2908
- await waitForSmtpResponse(readResponse, ["235"]);
2729
+ function decryptField(value, key) {
2730
+ if (!value.startsWith(ENCRYPTION_PREFIX)) {
2731
+ return value;
2732
+ }
2733
+ const payload = Buffer.from(value.slice(ENCRYPTION_PREFIX.length), "base64");
2734
+ const iv = payload.subarray(0, IV_LENGTH);
2735
+ const tag = payload.subarray(payload.length - TAG_LENGTH);
2736
+ const ciphertext = payload.subarray(IV_LENGTH, payload.length - TAG_LENGTH);
2737
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
2738
+ decipher.setAuthTag(tag);
2739
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
2740
+ }
2741
+
2742
+ // ../../src/core/crypto/mfaSecret.ts
2743
+ function revealMfaSecret(stored) {
2744
+ if (!stored) {
2745
+ return null;
2746
+ }
2747
+ const key = resolveEncryptionKey();
2748
+ if (!isFieldEncryptionEnabled() || !key || !stored.startsWith("enc:v1:")) {
2749
+ return stored;
2750
+ }
2751
+ return decryptField(stored, key);
2752
+ }
2753
+
2754
+ // ../../src/core/http/requestMetaContext.ts
2755
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
2756
+ var requestMetaContext = new AsyncLocalStorage3;
2757
+ function runWithRequestMeta(meta, callback) {
2758
+ return requestMetaContext.run(meta, callback);
2759
+ }
2760
+ function currentRequestMeta() {
2761
+ return requestMetaContext.getStore() ?? {
2762
+ ipAddress: null,
2763
+ userAgent: null
2764
+ };
2765
+ }
2766
+
2767
+ // ../../src/core/security/securityEvents.ts
2768
+ function logSecurityEvent(event, details = {}) {
2769
+ const meta = currentRequestMeta();
2770
+ const user = currentAuthUser();
2771
+ console.log(JSON.stringify({
2772
+ level: "security",
2773
+ event,
2774
+ timestamp: new Date().toISOString(),
2775
+ ip_address: meta.ipAddress ?? null,
2776
+ user_agent: meta.userAgent ?? null,
2777
+ user_id: user?.id ?? null,
2778
+ ...details
2779
+ }));
2780
+ }
2781
+
2782
+ // ../../src/core/security/tokenExpiry.ts
2783
+ function resolveDefaultTokenExpiryDays() {
2784
+ const raw = process.env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim();
2785
+ if (!raw) {
2786
+ return null;
2787
+ }
2788
+ const parsed = Number.parseInt(raw, 10);
2789
+ if (!Number.isInteger(parsed) || parsed <= 0) {
2790
+ return null;
2791
+ }
2792
+ return parsed;
2793
+ }
2794
+
2795
+ // ../../src/core/security/totp.ts
2796
+ import { createHmac as createHmac2 } from "crypto";
2797
+ function decodeBase32(input) {
2798
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
2799
+ const normalized = input.replace(/=+$/u, "").toUpperCase();
2800
+ let bits = "";
2801
+ for (const char of normalized) {
2802
+ const value = alphabet.indexOf(char);
2803
+ if (value === -1) {
2804
+ throw new Error("Invalid base32 character in MFA secret.");
2909
2805
  }
2910
- await socket.write(`MAIL FROM:<${config.from}>\r
2911
- `);
2912
- await waitForSmtpResponse(readResponse, ["250"]);
2913
- await socket.write(`RCPT TO:<${message.to}>\r
2914
- `);
2915
- await waitForSmtpResponse(readResponse, ["250", "251"]);
2916
- await socket.write(`DATA\r
2917
- `);
2918
- await waitForSmtpResponse(readResponse, ["354"]);
2919
- const payload = buildSmtpPayload(config.from, message);
2920
- await socket.write(payload);
2921
- await waitForSmtpResponse(readResponse, ["250"]);
2922
- await socket.write(`QUIT\r
2923
- `);
2924
- await waitForSmtpResponse(readResponse, ["221"]);
2925
- } finally {
2926
- socket.end();
2806
+ bits += value.toString(2).padStart(5, "0");
2927
2807
  }
2808
+ const bytes = [];
2809
+ for (let index = 0;index + 8 <= bits.length; index += 8) {
2810
+ bytes.push(Number.parseInt(bits.slice(index, index + 8), 2));
2811
+ }
2812
+ return Buffer.from(bytes);
2928
2813
  }
2929
- function buildSmtpPayload(from, message) {
2930
- const headers = [
2931
- `From: ${from}`,
2932
- `To: ${message.to}`,
2933
- `Subject: ${message.subject}`,
2934
- "MIME-Version: 1.0"
2935
- ];
2936
- if (message.html) {
2937
- const boundary = `strata-${Date.now().toString(36)}`;
2938
- headers.push(`Content-Type: multipart/alternative; boundary="${boundary}"`);
2939
- const parts = [
2940
- `--${boundary}`,
2941
- "Content-Type: text/plain; charset=utf-8",
2942
- "",
2943
- message.body,
2944
- `--${boundary}`,
2945
- "Content-Type: text/html; charset=utf-8",
2946
- "",
2947
- message.html,
2948
- `--${boundary}--`,
2949
- ""
2950
- ];
2951
- return [...headers, "", ...parts, ".", ""].join(`\r
2952
- `);
2814
+ function generateTotp(secret, counter, digits = 6) {
2815
+ const key = decodeBase32(secret);
2816
+ const buffer = Buffer.alloc(8);
2817
+ buffer.writeBigUInt64BE(BigInt(counter));
2818
+ const digest = createHmac2("sha1", key).update(buffer).digest();
2819
+ const lastByte = digest[digest.length - 1] ?? 0;
2820
+ const offset = lastByte & 15;
2821
+ const b0 = digest[offset] ?? 0;
2822
+ const b1 = digest[offset + 1] ?? 0;
2823
+ const b2 = digest[offset + 2] ?? 0;
2824
+ const b3 = digest[offset + 3] ?? 0;
2825
+ const code = (b0 & 127) << 24 | (b1 & 255) << 16 | (b2 & 255) << 8 | b3 & 255;
2826
+ return String(code % 10 ** digits).padStart(digits, "0");
2827
+ }
2828
+ function verifyTotp(secret, token, window = 1) {
2829
+ const normalized = token.trim();
2830
+ if (!/^\d{6}$/u.test(normalized)) {
2831
+ return false;
2953
2832
  }
2954
- headers.push("Content-Type: text/plain; charset=utf-8");
2955
- return [...headers, "", message.body, ".", ""].join(`\r
2956
- `);
2833
+ const timestep = Math.floor(Date.now() / 30000);
2834
+ for (let offset = -window;offset <= window; offset += 1) {
2835
+ if (generateTotp(secret, timestep + offset) === normalized) {
2836
+ return true;
2837
+ }
2838
+ }
2839
+ return false;
2957
2840
  }
2958
2841
 
2959
- class LogMailDriver {
2960
- async send(message) {
2961
- console.log(JSON.stringify({
2962
- level: "info",
2963
- channel: "mail",
2964
- to: message.to,
2965
- subject: message.subject,
2966
- body: message.body,
2967
- ...message.html ? { html: message.html } : {}
2968
- }));
2842
+ // ../../src/core/tenant/tenantContext.ts
2843
+ import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
2844
+ var tenantContext = new AsyncLocalStorage4;
2845
+ function runWithTenant(tenant, callback) {
2846
+ return tenantContext.run(tenant, callback);
2847
+ }
2848
+ function currentTenant() {
2849
+ return tenantContext.getStore() ?? null;
2850
+ }
2851
+ function currentTenantId() {
2852
+ return currentTenant()?.id ?? 1;
2853
+ }
2854
+ function rateLimitMultiplierForPlan(plan) {
2855
+ switch (plan) {
2856
+ case "enterprise":
2857
+ return 4;
2858
+ case "pro":
2859
+ return 2;
2860
+ default:
2861
+ return 1;
2969
2862
  }
2970
2863
  }
2971
2864
 
2972
- class SmtpMailDriver {
2973
- config;
2974
- transport;
2975
- constructor(config, transport = defaultSmtpTransport) {
2976
- this.config = config;
2977
- this.transport = transport;
2865
+ // ../../src/modules/user/authService.ts
2866
+ class AuthService {
2867
+ users;
2868
+ tokens;
2869
+ oauthIdentities;
2870
+ oauthProviders = new Map;
2871
+ constructor(users, tokens, oauthIdentities) {
2872
+ this.users = users;
2873
+ this.tokens = tokens;
2874
+ this.oauthIdentities = oauthIdentities;
2978
2875
  }
2979
- send(message) {
2980
- return this.transport(this.config, message);
2876
+ registerOAuthProvider(provider) {
2877
+ this.oauthProviders.set(provider.name, provider);
2878
+ }
2879
+ getOAuthProvider(name) {
2880
+ return this.oauthProviders.get(name);
2881
+ }
2882
+ async loginWithPassword(email, password, options = {}) {
2883
+ const user = await this.users.findByEmail(email);
2884
+ if (!user?.password_hash) {
2885
+ logSecurityEvent("auth_login_failed", { reason: "unknown_user", email });
2886
+ throw new UnauthorizedError("Invalid credentials.");
2887
+ }
2888
+ const valid = await verifyPassword(password, user.password_hash);
2889
+ if (!valid) {
2890
+ logSecurityEvent("auth_login_failed", { reason: "invalid_password", user_id: user.id });
2891
+ throw new UnauthorizedError("Invalid credentials.");
2892
+ }
2893
+ if (isFeatureEnabled("emailVerification") && !user.email_verified_at) {
2894
+ logSecurityEvent("auth_login_blocked", { reason: "email_unverified", user_id: user.id });
2895
+ throw new UnauthorizedError("Email address is not verified.");
2896
+ }
2897
+ if (isFeatureEnabled("mfa") && user.mfa_enabled) {
2898
+ const mfaSecret = revealMfaSecret(user.mfa_secret);
2899
+ if (!mfaSecret || !options.mfaCode || !verifyTotp(mfaSecret, options.mfaCode)) {
2900
+ logSecurityEvent("auth_login_failed", { reason: "invalid_mfa", user_id: user.id });
2901
+ throw new UnauthorizedError("Invalid MFA code.");
2902
+ }
2903
+ }
2904
+ logSecurityEvent("auth_login_success", { user_id: user.id, method: "password" });
2905
+ return await this.tokens.createToken(user.id, {
2906
+ name: "password-login",
2907
+ abilities: resolveAbilitiesForRole(user.role),
2908
+ expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
2909
+ });
2910
+ }
2911
+ async loginWithOAuth(providerName, code) {
2912
+ const provider = this.oauthProviders.get(providerName);
2913
+ if (!provider) {
2914
+ throw new UnauthorizedError("Unsupported OAuth provider.");
2915
+ }
2916
+ const profile = await provider.exchangeCode(code);
2917
+ const user = await this.findOrCreateOAuthUser(providerName, profile);
2918
+ logSecurityEvent("auth_login_success", { user_id: user.id, method: `oauth:${providerName}` });
2919
+ return await this.tokens.createToken(user.id, {
2920
+ name: `${providerName}-oauth`,
2921
+ abilities: resolveAbilitiesForRole(user.role),
2922
+ expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
2923
+ });
2924
+ }
2925
+ buildOAuthAuthorizationUrl(providerName, state) {
2926
+ const provider = this.oauthProviders.get(providerName);
2927
+ if (!provider) {
2928
+ throw new UnauthorizedError("Unsupported OAuth provider.");
2929
+ }
2930
+ return provider.getAuthorizationUrl(state);
2931
+ }
2932
+ async findOrCreateOAuthUser(providerName, profile) {
2933
+ const existingIdentity = await this.oauthIdentities.findByProviderUser(providerName, profile.providerUserId);
2934
+ if (existingIdentity) {
2935
+ return await this.users.findByIdOrThrow(existingIdentity.user_id);
2936
+ }
2937
+ const existingUser = await this.users.findByEmail(profile.email);
2938
+ const user = existingUser ?? await this.users.create({
2939
+ name: profile.name,
2940
+ email: profile.email,
2941
+ role: "member",
2942
+ tenant_id: currentTenantId(),
2943
+ email_verified_at: new Date,
2944
+ created_at: new Date,
2945
+ updated_at: new Date
2946
+ });
2947
+ await this.oauthIdentities.create({
2948
+ user_id: user.id,
2949
+ provider: providerName,
2950
+ provider_user_id: profile.providerUserId,
2951
+ email: profile.email,
2952
+ created_at: new Date
2953
+ });
2954
+ return user;
2981
2955
  }
2982
2956
  }
2983
2957
 
2984
- class Mailer {
2985
- driver;
2986
- constructor(driver) {
2987
- this.driver = driver;
2988
- }
2989
- send(message) {
2990
- return this.driver.send(message);
2958
+ // ../../src/modules/user/notificationTable.ts
2959
+ var notificationTable = defineTable({
2960
+ name: "notification",
2961
+ primaryKey: "id",
2962
+ columns: ["id", "user_id", "tenant_id", "type", "title", "body", "data", "read_at", "created_at"],
2963
+ defaultOrderBy: { column: "created_at", direction: "DESC" }
2964
+ });
2965
+
2966
+ // ../../src/modules/user/oauthIdentityRepository.ts
2967
+ var oauthIdentityTable = defineTable({
2968
+ name: "oauth_identity",
2969
+ primaryKey: "id",
2970
+ columns: ["id", "user_id", "provider", "provider_user_id", "email", "created_at"]
2971
+ });
2972
+
2973
+ // ../../src/modules/user/table.ts
2974
+ var userTable = defineTable({
2975
+ name: "users",
2976
+ primaryKey: "id",
2977
+ columns: [
2978
+ "id",
2979
+ "name",
2980
+ "email",
2981
+ "email_lookup",
2982
+ "role",
2983
+ "tenant_id",
2984
+ "password_hash",
2985
+ "email_verified_at",
2986
+ "mfa_secret",
2987
+ "mfa_enabled",
2988
+ "created_at",
2989
+ "updated_at"
2990
+ ],
2991
+ defaultOrderBy: { column: "id", direction: "ASC" }
2992
+ });
2993
+
2994
+ // ../../src/modules/user/provider.ts
2995
+ var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
2996
+
2997
+ // ../../src/core/auth/guard.ts
2998
+ function devHeaderAbilities(role) {
2999
+ if (role === "admin") {
3000
+ return [...ADMIN_ABILITIES];
2991
3001
  }
3002
+ return [...MEMBER_ABILITIES];
2992
3003
  }
2993
- function createMailDriver() {
2994
- const driver = process.env.MAIL_DRIVER ?? "log";
2995
- if (driver === "smtp") {
2996
- return new SmtpMailDriver(resolveSmtpConfig());
3004
+
3005
+ class GuestGuard {
3006
+ resolve(request) {
3007
+ const userId = request.headers.get("x-authenticated-user-id");
3008
+ if (!userId) {
3009
+ return null;
3010
+ }
3011
+ const role = request.headers.get("x-authenticated-user-role");
3012
+ return {
3013
+ id: userId,
3014
+ abilities: devHeaderAbilities(role),
3015
+ ...role ? { role } : {}
3016
+ };
2997
3017
  }
2998
- return new LogMailDriver;
2999
- }
3000
- var appMailer = new Mailer(createMailDriver());
3001
- function mailer() {
3002
- return appMailer;
3003
3018
  }
3004
3019
 
3005
- // ../../src/core/storage/storage.ts
3006
- import { mkdir, readFile, unlink, writeFile } from "fs/promises";
3007
- import { dirname, join as join3 } from "path";
3008
- var {S3Client } = globalThis.Bun;
3009
-
3010
- class LocalStorageDriver {
3011
- rootDirectory;
3012
- constructor(rootDirectory) {
3013
- this.rootDirectory = rootDirectory;
3014
- }
3015
- resolvePath(path) {
3016
- return join3(this.rootDirectory, path.replace(/^\/+/, ""));
3017
- }
3018
- async put(path, contents) {
3019
- const absolutePath = this.resolvePath(path);
3020
- await mkdir(dirname(absolutePath), { recursive: true });
3021
- await writeFile(absolutePath, contents);
3022
- return path;
3020
+ class ApiTokenGuard {
3021
+ options;
3022
+ constructor(options) {
3023
+ this.options = options;
3023
3024
  }
3024
- async get(path) {
3025
- try {
3026
- return await readFile(this.resolvePath(path));
3027
- } catch {
3025
+ resolve(request) {
3026
+ const authorization = request.headers.get("authorization");
3027
+ if (!authorization?.startsWith("Bearer ")) {
3028
3028
  return null;
3029
3029
  }
3030
- }
3031
- async delete(path) {
3032
- try {
3033
- await unlink(this.resolvePath(path));
3034
- return true;
3035
- } catch {
3036
- return false;
3030
+ const token = authorization.slice("Bearer ".length).trim();
3031
+ if (token !== this.options.token) {
3032
+ return null;
3037
3033
  }
3034
+ return this.options.user;
3038
3035
  }
3039
3036
  }
3040
3037
 
3041
- class S3StorageDriver {
3042
- client;
3043
- constructor(client) {
3044
- this.client = client;
3045
- }
3046
- async put(path, contents) {
3047
- await this.client.write(path.replace(/^\/+/, ""), contents);
3048
- return path;
3049
- }
3050
- async get(path) {
3051
- const normalizedPath = path.replace(/^\/+/, "");
3052
- const file = this.client.file(normalizedPath);
3053
- if (!await file.exists()) {
3038
+ class DatabaseTokenGuard {
3039
+ container;
3040
+ constructor(container) {
3041
+ this.container = container;
3042
+ }
3043
+ async resolve(request) {
3044
+ const authorization = request.headers.get("authorization");
3045
+ if (!authorization?.startsWith("Bearer ")) {
3054
3046
  return null;
3055
3047
  }
3056
- return new Uint8Array(await file.arrayBuffer());
3048
+ const token = authorization.slice("Bearer ".length).trim();
3049
+ if (!token) {
3050
+ return null;
3051
+ }
3052
+ if (!this.container.has(tokenServiceToken)) {
3053
+ return null;
3054
+ }
3055
+ const tokenService = this.container.resolve(tokenServiceToken);
3056
+ return await tokenService.resolveUserFromToken(token);
3057
3057
  }
3058
- async delete(path) {
3059
- try {
3060
- await this.client.unlink(path.replace(/^\/+/, ""));
3061
- return true;
3062
- } catch {
3063
- return false;
3058
+ }
3059
+
3060
+ class CompositeGuard {
3061
+ guards;
3062
+ constructor(guards) {
3063
+ this.guards = guards;
3064
+ }
3065
+ async resolve(request) {
3066
+ for (const guard of this.guards) {
3067
+ const user = await Promise.resolve(guard.resolve(request));
3068
+ if (user) {
3069
+ return user;
3070
+ }
3064
3071
  }
3072
+ return null;
3065
3073
  }
3066
3074
  }
3067
3075
 
3068
- class StorageManager {
3069
- driver;
3070
- constructor(driver) {
3071
- this.driver = driver;
3076
+ class AuthManager {
3077
+ guard;
3078
+ constructor(guard) {
3079
+ this.guard = guard;
3072
3080
  }
3073
- put(path, contents) {
3074
- return this.driver.put(path, contents);
3081
+ async resolve(request) {
3082
+ if (request) {
3083
+ return await Promise.resolve(this.guard.resolve(request));
3084
+ }
3085
+ return currentAuthUser();
3075
3086
  }
3076
- get(path) {
3077
- return this.driver.get(path);
3087
+ user(request) {
3088
+ return this.resolve(request);
3078
3089
  }
3079
- delete(path) {
3080
- return this.driver.delete(path);
3090
+ async check(request) {
3091
+ return await this.user(request) !== null;
3081
3092
  }
3082
- }
3083
- function resolveS3Config() {
3084
- const accessKeyId = process.env.AWS_ACCESS_KEY_ID?.trim();
3085
- const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY?.trim();
3086
- const bucket = process.env.AWS_BUCKET?.trim();
3087
- if (!accessKeyId || !secretAccessKey || !bucket) {
3088
- throw new Error('STORAGE_DRIVER="s3" requires AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_BUCKET.');
3093
+ async requireUser(request) {
3094
+ const user = await this.user(request);
3095
+ if (!user) {
3096
+ throw new UnauthorizedError;
3097
+ }
3098
+ return user;
3089
3099
  }
3090
- return {
3091
- accessKeyId,
3092
- secretAccessKey,
3093
- bucket,
3094
- ...process.env.AWS_REGION?.trim() ? { region: process.env.AWS_REGION.trim() } : {},
3095
- ...process.env.AWS_ENDPOINT?.trim() ? { endpoint: process.env.AWS_ENDPOINT.trim() } : {}
3096
- };
3097
- }
3098
- function createS3Client(config = resolveS3Config()) {
3099
- return new S3Client({
3100
- accessKeyId: config.accessKeyId,
3101
- secretAccessKey: config.secretAccessKey,
3102
- bucket: config.bucket,
3103
- ...config.region ? { region: config.region } : {},
3104
- ...config.endpoint ? { endpoint: config.endpoint } : {}
3105
- });
3106
3100
  }
3107
- function createStorageDriver() {
3108
- const driver = process.env.STORAGE_DRIVER ?? "local";
3109
- if (driver === "s3") {
3110
- return new S3StorageDriver(createS3Client());
3101
+ // ../../src/core/auth/membershipContext.ts
3102
+ import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
3103
+
3104
+ // ../../src/modules/organization/memberRepository.ts
3105
+ class OrganizationMemberRepository {
3106
+ constructor() {}
3107
+ async findMembership(userId, organizationId) {
3108
+ const rows = await connection_default`
3109
+ SELECT id, organization_id, user_id, role, created_at
3110
+ FROM organization_member
3111
+ WHERE user_id = ${userId} AND organization_id = ${organizationId}
3112
+ LIMIT 1
3113
+ `;
3114
+ return rows[0] ?? null;
3111
3115
  }
3112
- return new LocalStorageDriver(process.env.STORAGE_PATH ?? "storage");
3113
- }
3114
- var defaultStorage = new StorageManager(createStorageDriver());
3115
- function storage() {
3116
- return defaultStorage;
3117
- }
3116
+ async listForUser(userId) {
3117
+ return await connection_default`
3118
+ SELECT id, organization_id, user_id, role, created_at
3119
+ FROM organization_member
3120
+ WHERE user_id = ${userId}
3121
+ ORDER BY organization_id
3122
+ `;
3123
+ }
3124
+ async listForOrganization(organizationId) {
3125
+ return await connection_default`
3126
+ SELECT id, organization_id, user_id, role, created_at
3127
+ FROM organization_member
3128
+ WHERE organization_id = ${organizationId}
3129
+ ORDER BY id
3130
+ `;
3131
+ }
3132
+ async addMember(input) {
3133
+ const rows = await connection_default`
3134
+ INSERT INTO organization_member (organization_id, user_id, role)
3135
+ VALUES (${input.organizationId}, ${input.userId}, ${input.role ?? "member"})
3136
+ RETURNING id, organization_id, user_id, role, created_at
3137
+ `;
3138
+ const row = rows[0];
3139
+ if (!row) {
3140
+ throw new Error("Organization member insert did not return a row.");
3141
+ }
3142
+ return row;
3143
+ }
3144
+ async removeMember(organizationId, userId) {
3145
+ const rows = await connection_default`
3146
+ DELETE FROM organization_member
3147
+ WHERE organization_id = ${organizationId} AND user_id = ${userId}
3148
+ RETURNING id
3149
+ `;
3150
+ return rows.length > 0;
3151
+ }
3152
+ }
3153
+ var memberRepository_default = OrganizationMemberRepository;
3118
3154
 
3119
- // ../../src/core/facades/index.ts
3120
- function cache() {
3121
- return resolveApplicationCache();
3122
- }
3123
- function auth() {
3124
- return resolveApplicationAuth();
3155
+ // ../../src/core/auth/membershipContext.ts
3156
+ var membershipContext = new AsyncLocalStorage5;
3157
+ var membershipRepository = new memberRepository_default;
3158
+ async function runWithMembershipContext(callback) {
3159
+ const user = currentAuthUser();
3160
+ if (!user || isGlobalAdmin(user)) {
3161
+ return await callback();
3162
+ }
3163
+ const memberships = await membershipRepository.listForUser(resolveUserId(user));
3164
+ const context = {
3165
+ organizationIds: memberships.map((membership) => membership.organization_id),
3166
+ rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
3167
+ };
3168
+ return await membershipContext.run(context, callback);
3125
3169
  }
3126
- function policyGate() {
3127
- return resolveApplicationPolicyGate();
3170
+ function currentOrgRole(organizationId) {
3171
+ return membershipContext.getStore()?.rolesByOrganizationId.get(organizationId) ?? null;
3128
3172
  }
3129
- function queue() {
3130
- return resolveApplicationQueue();
3173
+ function hasOrgMembership(organizationId) {
3174
+ return currentOrgRole(organizationId) !== null;
3131
3175
  }
3132
- function events() {
3133
- return eventBus;
3176
+ function currentOrganizationIds() {
3177
+ return membershipContext.getStore()?.organizationIds ?? [];
3134
3178
  }
3135
- function config(key) {
3136
- return resolveApplicationConfig().get(key);
3179
+ function hasMinimumOrgRole2(organizationId, minimum) {
3180
+ const role = currentOrgRole(organizationId);
3181
+ if (!role) {
3182
+ return false;
3183
+ }
3184
+ const ranks = {
3185
+ member: 1,
3186
+ admin: 2,
3187
+ owner: 3
3188
+ };
3189
+ return ranks[role] >= ranks[minimum];
3137
3190
  }
3138
- function log() {
3139
- return resolveApplicationLogger();
3191
+ // ../../src/core/auth/membershipContextMiddleware.ts
3192
+ function createMembershipContextMiddleware() {
3193
+ return async (_request, next) => {
3194
+ return await runWithMembershipContext(async () => await next());
3195
+ };
3140
3196
  }
3141
- function mail() {
3142
- return mailer();
3197
+
3198
+ // ../../src/core/auth/membershipMiddleware.ts
3199
+ function createMembershipMiddleware() {
3200
+ return createMembershipContextMiddleware();
3143
3201
  }
3144
- function storageFacade() {
3145
- return storage();
3202
+ // ../../src/core/auth/membershipScope.ts
3203
+ function resolveOrganizationScope() {
3204
+ const user = currentAuthUser();
3205
+ if (!user) {
3206
+ return null;
3207
+ }
3208
+ if (isGlobalAdmin(user)) {
3209
+ return null;
3210
+ }
3211
+ return currentOrganizationIds();
3146
3212
  }
3147
- // ../../src/core/http/bodySizeLimitMiddleware.ts
3148
- var DEFAULT_MAX_BODY_BYTES = 1048576;
3149
- function resolveMaxBodyBytes() {
3150
- const raw = process.env.MAX_REQUEST_BODY_BYTES?.trim();
3151
- if (!raw) {
3152
- return DEFAULT_MAX_BODY_BYTES;
3213
+ function scopedOrganizationIds(requestedOrganizationId) {
3214
+ const scope = resolveOrganizationScope();
3215
+ if (scope === null) {
3216
+ return requestedOrganizationId === undefined ? null : [requestedOrganizationId];
3153
3217
  }
3154
- const parsed = Number.parseInt(raw, 10);
3155
- if (!Number.isInteger(parsed) || parsed <= 0) {
3156
- return DEFAULT_MAX_BODY_BYTES;
3218
+ if (requestedOrganizationId !== undefined) {
3219
+ return scope.includes(requestedOrganizationId) ? [requestedOrganizationId] : [];
3157
3220
  }
3158
- return parsed;
3221
+ return scope;
3159
3222
  }
3160
- function createBodySizeLimitMiddleware(maxBytes = resolveMaxBodyBytes()) {
3161
- return async (request, next) => {
3162
- const contentLength = request.headers.get("content-length");
3163
- if (contentLength) {
3164
- const bytes = Number.parseInt(contentLength, 10);
3165
- if (Number.isInteger(bytes) && bytes > maxBytes) {
3166
- const error = new PayloadTooLargeError(`Request body exceeds the ${maxBytes} byte limit.`);
3167
- return Response.json({ error: error.message }, { status: error.status });
3168
- }
3169
- }
3170
- return await next();
3223
+ function appendOrganizationScope(where, requestedOrganizationId) {
3224
+ const organizationIds = scopedOrganizationIds(requestedOrganizationId);
3225
+ if (organizationIds === null) {
3226
+ return where;
3227
+ }
3228
+ if (organizationIds.length === 0) {
3229
+ return {
3230
+ ...where,
3231
+ organization_id: [-1]
3232
+ };
3233
+ }
3234
+ return {
3235
+ ...where,
3236
+ organization_id: organizationIds.length === 1 ? organizationIds[0] : organizationIds
3171
3237
  };
3172
3238
  }
3173
- // ../../src/core/http/cookies.ts
3174
- function readRequestCookie(request, name) {
3175
- const cookies = request.cookies;
3176
- if (cookies && typeof cookies.get === "function") {
3177
- const value = cookies.get(name);
3178
- if (value) {
3179
- return value;
3239
+ function appendProjectScope(where, accessibleProjectIds, requestedProjectId) {
3240
+ if (accessibleProjectIds === null) {
3241
+ if (requestedProjectId === undefined) {
3242
+ return where;
3180
3243
  }
3244
+ return {
3245
+ ...where,
3246
+ project_id: requestedProjectId
3247
+ };
3181
3248
  }
3182
- const header = request.headers.get("cookie");
3183
- if (!header) {
3184
- return null;
3185
- }
3186
- for (const part of header.split(";")) {
3187
- const idx = part.indexOf("=");
3188
- if (idx === -1)
3189
- continue;
3190
- const cookieName = part.slice(0, idx).trim();
3191
- if (cookieName !== name)
3192
- continue;
3193
- return decodeURIComponent(part.slice(idx + 1).trim());
3249
+ if (accessibleProjectIds.length === 0) {
3250
+ return {
3251
+ ...where,
3252
+ project_id: [-1]
3253
+ };
3194
3254
  }
3195
- return null;
3196
- }
3197
- function readBunRequestCookie(request, name) {
3198
- return request.cookies.get(name) ?? readRequestCookie(request, name);
3199
- }
3200
- // ../../src/core/http/csrfToken.ts
3201
- import { timingSafeEqual } from "crypto";
3202
-
3203
- // ../../src/core/http/requestMetaContext.ts
3204
- import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
3205
- var requestMetaContext = new AsyncLocalStorage3;
3206
- function runWithRequestMeta(meta, callback) {
3207
- return requestMetaContext.run(meta, callback);
3208
- }
3209
- function currentRequestMeta() {
3210
- return requestMetaContext.getStore() ?? {
3211
- ipAddress: null,
3212
- userAgent: null
3213
- };
3214
- }
3215
-
3216
- // ../../src/core/http/csrfToken.ts
3217
- var CSRF_COOKIE = "workhub_csrf";
3218
- var CSRF_TTL_MS = 60 * 60 * 1000;
3219
- function resolveCsrfSecret() {
3220
- return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
3221
- }
3222
- function csrfVerifyOptions() {
3223
- return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
3224
- }
3225
- function tokensMatch(left, right) {
3226
- const leftBuffer = Buffer.from(left);
3227
- const rightBuffer = Buffer.from(right);
3228
- if (leftBuffer.length !== rightBuffer.length) {
3229
- return false;
3255
+ if (requestedProjectId !== undefined) {
3256
+ return {
3257
+ ...where,
3258
+ project_id: accessibleProjectIds.includes(requestedProjectId) ? requestedProjectId : -1
3259
+ };
3230
3260
  }
3231
- return timingSafeEqual(leftBuffer, rightBuffer);
3261
+ return {
3262
+ ...where,
3263
+ project_id: accessibleProjectIds
3264
+ };
3232
3265
  }
3233
- function createCsrfTokenCookie() {
3234
- const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
3266
+ function emptyPaginateResult(page, perPage) {
3235
3267
  return {
3236
- token,
3237
- cookie: `${CSRF_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}`
3268
+ data: [],
3269
+ meta: {
3270
+ page,
3271
+ per_page: perPage,
3272
+ total: 0,
3273
+ last_page: 1
3274
+ }
3238
3275
  };
3239
3276
  }
3240
- function resolveCsrfToken(request) {
3241
- const cookieValue = readRequestCookie(request, CSRF_COOKIE);
3242
- if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
3243
- return { token: cookieValue };
3277
+ function assertResourceInCurrentTenant(resourceTenantId, resourceLabel, resourceId) {
3278
+ if (resourceTenantId !== currentTenantId()) {
3279
+ throw new NotFoundError(`${resourceLabel} ${resourceId} not found.`);
3244
3280
  }
3245
- return createCsrfTokenCookie();
3246
3281
  }
3247
- function readSubmittedCsrfToken(request) {
3248
- const headerToken = request.headers.get("x-csrf-token")?.trim();
3249
- if (headerToken) {
3250
- return headerToken;
3282
+ function assertOrganizationReadable(organizationId) {
3283
+ const user = currentAuthUser();
3284
+ if (!user || isGlobalAdmin(user)) {
3285
+ return;
3286
+ }
3287
+ const organizationIds = scopedOrganizationIds();
3288
+ if (organizationIds !== null && !organizationIds.includes(organizationId)) {
3289
+ throw new NotFoundError(`Organization ${organizationId} not found.`);
3251
3290
  }
3252
- return null;
3253
3291
  }
3254
- async function readSubmittedCsrfTokenFromBody(request) {
3255
- const headerToken = readSubmittedCsrfToken(request);
3256
- if (headerToken) {
3257
- return headerToken;
3292
+ // ../../src/core/auth/membershipService.ts
3293
+ class MembershipService {
3294
+ members;
3295
+ constructor(members = membershipRepository) {
3296
+ this.members = members;
3258
3297
  }
3259
- const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
3260
- if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
3261
- const formData = await request.clone().formData();
3262
- const field = formData.get("_token");
3263
- if (typeof field === "string" && field.trim().length > 0) {
3264
- return field.trim();
3298
+ async listOrganizationIdsForUser(userId) {
3299
+ const memberships = await this.members.listForUser(userId);
3300
+ return memberships.map((membership) => membership.organization_id);
3301
+ }
3302
+ async getOrgRole(userId, organizationId) {
3303
+ const membership = await this.members.findMembership(userId, organizationId);
3304
+ return membership?.role ?? null;
3305
+ }
3306
+ async requireOrgAccess(organizationId, minimumRole = "member", user = currentAuthUser()) {
3307
+ if (!user) {
3308
+ throw new ForbiddenError("Authentication required.");
3265
3309
  }
3266
- const legacyField = formData.get("_csrf");
3267
- if (typeof legacyField === "string" && legacyField.trim().length > 0) {
3268
- return legacyField.trim();
3310
+ if (isGlobalAdmin(user)) {
3311
+ return "owner";
3269
3312
  }
3313
+ const role = await this.getOrgRole(resolveUserId(user), organizationId);
3314
+ if (!role || !hasMinimumOrgRole(role, minimumRole)) {
3315
+ throw new ForbiddenError("Organization membership required.");
3316
+ }
3317
+ return role;
3318
+ }
3319
+ async filterAccessibleOrganizationIds(organizationIds, user = currentAuthUser()) {
3320
+ if (!user) {
3321
+ return [];
3322
+ }
3323
+ if (isGlobalAdmin(user)) {
3324
+ return organizationIds;
3325
+ }
3326
+ const allowed = new Set(await this.listOrganizationIdsForUser(resolveUserId(user)));
3327
+ return organizationIds.filter((organizationId) => allowed.has(organizationId));
3328
+ }
3329
+ async addOwnerOnOrganizationCreate(organizationId, userId) {
3330
+ await this.members.addMember({
3331
+ organizationId,
3332
+ userId,
3333
+ role: "owner"
3334
+ });
3335
+ }
3336
+ listMembersForOrganization(organizationId) {
3337
+ return this.members.listForOrganization(organizationId);
3338
+ }
3339
+ addMember(input) {
3340
+ return this.members.addMember(input);
3341
+ }
3342
+ removeMember(organizationId, userId) {
3343
+ return this.members.removeMember(organizationId, userId);
3270
3344
  }
3271
- return null;
3272
3345
  }
3273
- function verifyCsrfToken(request, submittedToken) {
3274
- if (!submittedToken) {
3346
+ function resolveMembershipService() {
3347
+ const dependencies = resolveApplicationDependencies();
3348
+ if (dependencies.container.has("core.membership")) {
3349
+ return dependencies.container.resolve("core.membership");
3350
+ }
3351
+ return new MembershipService;
3352
+ }
3353
+ var membershipService_default = MembershipService;
3354
+ // ../../src/core/auth/policy.ts
3355
+ class Policy {
3356
+ constructor() {}
3357
+ view(_user, _resource) {
3275
3358
  return false;
3276
3359
  }
3277
- const cookieValue = readRequestCookie(request, CSRF_COOKIE);
3278
- if (!cookieValue) {
3360
+ create(_user) {
3279
3361
  return false;
3280
3362
  }
3281
- if (!tokensMatch(submittedToken, cookieValue)) {
3363
+ update(_user, _resource) {
3282
3364
  return false;
3283
3365
  }
3284
- return Bun.CSRF.verify(submittedToken, csrfVerifyOptions());
3285
- }
3286
- function resolveCsrfTokenForRequest(request) {
3287
- const metaToken = currentRequestMeta().csrfToken;
3288
- if (metaToken) {
3289
- return metaToken;
3366
+ delete(_user, _resource) {
3367
+ return false;
3290
3368
  }
3291
- return resolveCsrfToken(request).token;
3292
3369
  }
3370
+ var BLOCKED_POLICY_ACTIONS = new Set([
3371
+ "constructor",
3372
+ "toString",
3373
+ "valueOf",
3374
+ "hasOwnProperty",
3375
+ "isPrototypeOf",
3376
+ "propertyIsEnumerable",
3377
+ "__proto__"
3378
+ ]);
3293
3379
 
3294
- // ../../src/core/http/csrfMiddleware.ts
3295
- var MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
3296
- function appendSetCookie(response, cookie) {
3297
- const headers = new Headers(response.headers);
3298
- headers.append("set-cookie", cookie);
3299
- return new Response(response.body, {
3300
- status: response.status,
3301
- statusText: response.statusText,
3302
- headers
3303
- });
3304
- }
3305
- function createCsrfMiddleware() {
3306
- return async (request, next) => {
3307
- const method = request.method.toUpperCase();
3308
- if (!MUTATING_METHODS.has(method)) {
3309
- const csrf = resolveCsrfToken(request);
3310
- const meta = currentRequestMeta();
3311
- meta.csrfToken = csrf.token;
3312
- const response = await next();
3313
- if (!csrf.cookie) {
3314
- return response;
3315
- }
3316
- return appendSetCookie(response, csrf.cookie);
3380
+ class PolicyGate {
3381
+ constructor() {}
3382
+ policies = new Map;
3383
+ register(resource, policy) {
3384
+ this.policies.set(resource, policy);
3385
+ }
3386
+ allows(resource, action, user, model) {
3387
+ const policy = this.policies.get(resource);
3388
+ if (!policy) {
3389
+ return false;
3317
3390
  }
3318
- const submitted = await readSubmittedCsrfTokenFromBody(request);
3319
- if (!verifyCsrfToken(request, submitted)) {
3320
- throw new ForbiddenError("Invalid or missing CSRF token.");
3391
+ if (BLOCKED_POLICY_ACTIONS.has(action) || !(action in policy)) {
3392
+ return false;
3321
3393
  }
3322
- return await next();
3323
- };
3394
+ const handler = policy[action];
3395
+ if (typeof handler !== "function") {
3396
+ return false;
3397
+ }
3398
+ const resolvedUser = user === undefined ? currentAuthUser() : user;
3399
+ return model === undefined ? handler.call(policy, resolvedUser) : handler.call(policy, resolvedUser, model);
3400
+ }
3401
+ authorize(resource, action, user, model) {
3402
+ if (!this.allows(resource, action, user, model)) {
3403
+ throw new ForbiddenError;
3404
+ }
3405
+ }
3324
3406
  }
3325
- // ../../src/core/http/csrfProtection.ts
3326
- var DEFAULT_CSRF_TTL_MS = 60 * 60 * 1000;
3327
- function createCsrfProtection(secret, options = {}) {
3328
- const expiresIn = options.expiresIn ?? DEFAULT_CSRF_TTL_MS;
3329
- const maxAge = options.maxAge ?? expiresIn;
3330
- return {
3331
- generate(_sessionKey) {
3332
- return Bun.CSRF.generate(secret, { expiresIn });
3333
- },
3334
- verify(token, _sessionKey) {
3335
- if (!token) {
3336
- return false;
3337
- }
3338
- return Bun.CSRF.verify(token, { secret, maxAge });
3339
- },
3340
- secret
3341
- };
3407
+ // ../../src/core/database/bindConnection.ts
3408
+ function bindDatabaseConnection2(connection) {
3409
+ bindDatabaseConnection(connection);
3342
3410
  }
3343
- // ../../src/core/crypto/nonCryptographicHash.ts
3344
- function nonCryptographicDigest(input) {
3345
- return Bun.hash(input).toString(16);
3411
+
3412
+ // ../../src/domain/scim.ts
3413
+ var TEST_SCIM_BEARER_TOKEN = "workhub-scim-test-token";
3414
+
3415
+ // ../../src/core/security/timingSafeCompare.ts
3416
+ import { timingSafeEqual } from "crypto";
3417
+ function timingSafeCompareString(left, right) {
3418
+ const leftBuffer = Buffer.from(left);
3419
+ const rightBuffer = Buffer.from(right);
3420
+ if (leftBuffer.length !== rightBuffer.length) {
3421
+ return false;
3422
+ }
3423
+ return timingSafeEqual(leftBuffer, rightBuffer);
3346
3424
  }
3347
3425
 
3348
- // ../../src/core/http/etag.ts
3349
- function isEtagEnabled() {
3350
- return (process.env.FEATURE_ETAG ?? "true") !== "false";
3426
+ // ../../src/core/security/scimTenantTokens.ts
3427
+ function parseScimTenantTokens(raw) {
3428
+ const tokens = new Map;
3429
+ if (!raw?.trim()) {
3430
+ return tokens;
3431
+ }
3432
+ for (const entry of raw.split(",")) {
3433
+ const [tenantPart, tokenPart] = entry.split(":");
3434
+ if (!tenantPart || !tokenPart) {
3435
+ continue;
3436
+ }
3437
+ const tenantId = Number.parseInt(tenantPart.trim(), 10);
3438
+ const token = tokenPart.trim();
3439
+ if (Number.isInteger(tenantId) && tenantId > 0 && token.length > 0) {
3440
+ tokens.set(tenantId, token);
3441
+ }
3442
+ }
3443
+ return tokens;
3351
3444
  }
3352
- function formatWeakEtag(digest) {
3353
- return `W/"${digest}"`;
3445
+ function resolveScimTenantFromToken(token) {
3446
+ const tenantTokens = parseScimTenantTokens(process.env.SCIM_TENANT_TOKENS);
3447
+ for (const [tenantId, expectedToken] of tenantTokens) {
3448
+ if (timingSafeCompareString(token, expectedToken)) {
3449
+ return tenantId;
3450
+ }
3451
+ }
3452
+ const fallbackToken = process.env.SCIM_BEARER_TOKEN ?? TEST_SCIM_BEARER_TOKEN;
3453
+ if (timingSafeCompareString(token, fallbackToken)) {
3454
+ return 1;
3455
+ }
3456
+ return null;
3354
3457
  }
3355
- function computeEtagFromJson(data) {
3356
- const digest = nonCryptographicDigest(JSON.stringify(data)).slice(0, 32);
3357
- return formatWeakEtag(digest);
3458
+
3459
+ // ../../src/core/tenant/resolveTenant.ts
3460
+ async function resolveTenant(tenantId) {
3461
+ const rows = await repositoryConnection`
3462
+ SELECT id, slug, plan, region
3463
+ FROM tenant
3464
+ WHERE id = ${tenantId}
3465
+ LIMIT 1
3466
+ `;
3467
+ const row = rows[0];
3468
+ return row ? { id: row.id, slug: row.slug, plan: row.plan, region: row.region ?? "eu" } : null;
3358
3469
  }
3359
- function etagFromResource(resource) {
3360
- const version = resource.updated_at ?? resource.created_at ?? "";
3361
- const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
3362
- const digest = nonCryptographicDigest(`${String(resource.id ?? "0")}:${versionText}`).slice(0, 32);
3363
- return formatWeakEtag(digest);
3470
+
3471
+ // ../../src/core/tenant/tenantDatabaseScope.ts
3472
+ async function applyTenantContextToTransaction(transaction, tenantId) {
3473
+ await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
3474
+ await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
3475
+ }
3476
+ async function runWithTenantDatabase(tenant, callback) {
3477
+ if (hasActiveDatabaseConnection()) {
3478
+ const activeConnection2 = getActiveDatabaseConnection(getDefaultDatabasePool());
3479
+ await applyTenantContextToTransaction(activeConnection2, tenant.id);
3480
+ return await runWithTenant(tenant, callback);
3481
+ }
3482
+ return await getDefaultDatabasePool().begin(async (transaction) => {
3483
+ await applyTenantContextToTransaction(transaction, tenant.id);
3484
+ return await runWithDatabaseConnection(transaction, async () => {
3485
+ return await runWithTenant(tenant, callback);
3486
+ });
3487
+ });
3364
3488
  }
3365
- function normalizeEtag(value) {
3366
- return value.trim();
3489
+ function isInsideTenantDatabaseScope(tenantId = currentTenant()?.id) {
3490
+ return hasActiveDatabaseConnection() && currentTenant()?.id === tenantId;
3367
3491
  }
3368
- function etagValuesMatch(left, right) {
3369
- return normalizeEtag(left) === normalizeEtag(right);
3492
+
3493
+ // ../../src/core/auth/scimAuthMiddleware.ts
3494
+ function createScimAuthMiddleware() {
3495
+ return async (request, next) => {
3496
+ const authorization = request.headers.get("authorization");
3497
+ if (!authorization?.startsWith("Bearer ")) {
3498
+ return jsonScimError("SCIM bearer token required.", 401);
3499
+ }
3500
+ const token = authorization.slice("Bearer ".length).trim();
3501
+ const tenantId = resolveScimTenantFromToken(token);
3502
+ if (tenantId === null) {
3503
+ return jsonScimError("Invalid SCIM bearer token.", 401);
3504
+ }
3505
+ const tenant = await resolveTenant(tenantId);
3506
+ if (!tenant) {
3507
+ return jsonScimError("SCIM tenant not found.", 401);
3508
+ }
3509
+ return await runWithTenantDatabase(tenant, async () => {
3510
+ bindDatabaseConnection2(getActiveDatabaseConnection(getDefaultDatabasePool()));
3511
+ try {
3512
+ return await next();
3513
+ } finally {
3514
+ resetBoundDatabaseConnection();
3515
+ }
3516
+ });
3517
+ };
3370
3518
  }
3371
- function parseEtagList(header) {
3372
- if (!header) {
3373
- return [];
3374
- }
3375
- return header.split(",").map((value) => normalizeEtag(value)).filter(Boolean);
3519
+ function jsonScimError(detail, status) {
3520
+ return Response.json({
3521
+ schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
3522
+ detail,
3523
+ status: String(status)
3524
+ }, {
3525
+ status,
3526
+ headers: { "content-type": "application/scim+json" }
3527
+ });
3376
3528
  }
3377
- function ifNoneMatchSatisfied(request, etag) {
3378
- const header = request.headers.get("if-none-match");
3379
- if (!header) {
3380
- return false;
3529
+ // ../../src/core/cache/taggedCache.ts
3530
+ class TaggedCache {
3531
+ store;
3532
+ tags;
3533
+ constructor(store, tags) {
3534
+ this.store = store;
3535
+ this.tags = tags;
3381
3536
  }
3382
- if (header.trim() === "*") {
3383
- return true;
3537
+ async remember(key, callback, ttlMs) {
3538
+ const value = await this.store.getOrSet(key, callback, ttlMs);
3539
+ await this.store.attachTags(key, this.tags);
3540
+ return value;
3541
+ }
3542
+ async flush() {
3543
+ return this.store.flushTags(this.tags);
3384
3544
  }
3385
- return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
3386
3545
  }
3387
- function ifMatchSatisfied(request, etag) {
3388
- const header = request.headers.get("if-match");
3389
- if (!header) {
3390
- return false;
3546
+ var taggedCache_default = TaggedCache;
3547
+
3548
+ // ../../src/core/cache/repository.ts
3549
+ class CacheRepository {
3550
+ store;
3551
+ constructor(store) {
3552
+ this.store = store;
3391
3553
  }
3392
- if (header.trim() === "*") {
3393
- return true;
3554
+ async get(key) {
3555
+ return this.store.get(key);
3556
+ }
3557
+ async remember(key, callback, ttlMs) {
3558
+ return this.store.getOrSet(key, callback, ttlMs);
3559
+ }
3560
+ async forget(key) {
3561
+ return this.store.invalidate(key);
3562
+ }
3563
+ async flush() {
3564
+ await this.store.clear();
3565
+ }
3566
+ tags(...names) {
3567
+ return new taggedCache_default(this.store, names);
3568
+ }
3569
+ async getOrSet(key, loader, ttlMs) {
3570
+ return this.remember(key, loader, ttlMs);
3571
+ }
3572
+ async invalidate(key) {
3573
+ return this.forget(key);
3574
+ }
3575
+ async invalidateByPrefix(prefix) {
3576
+ return this.store.invalidateByPrefix(prefix);
3394
3577
  }
3395
- return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
3396
- }
3397
- function assertIfMatch(request, etag, options = {}) {
3398
- const header = request.headers.get("if-match");
3399
- if (!header) {
3400
- if (options.required) {
3401
- throw new PreconditionFailedError("If-Match header is required.");
3402
- }
3403
- return;
3578
+ async clear() {
3579
+ await this.flush();
3404
3580
  }
3405
- if (!ifMatchSatisfied(request, etag)) {
3406
- throw new PreconditionFailedError("Resource ETag does not match If-Match.");
3581
+ async size() {
3582
+ return this.store.size();
3407
3583
  }
3408
3584
  }
3409
- function applyEtagHeaders(headers, etag) {
3410
- const next = new Headers(headers);
3411
- next.set("ETag", etag);
3412
- next.set("Cache-Control", "private, must-revalidate");
3413
- next.append("Vary", "Authorization");
3414
- next.append("Vary", "X-Tenant-Id");
3415
- return next;
3416
- }
3417
- function notModifiedResponse(etag) {
3418
- return new Response(null, {
3419
- status: 304,
3420
- headers: applyEtagHeaders(new Headers, etag)
3421
- });
3422
- }
3423
- function applyConditionalGet(request, response, etag) {
3424
- if (!isEtagEnabled()) {
3425
- return response;
3426
- }
3427
- if (ifNoneMatchSatisfied(request, etag)) {
3428
- return notModifiedResponse(etag);
3585
+ var repository_default2 = CacheRepository;
3586
+ // ../../src/core/cache/tags.ts
3587
+ var CACHE_TAGS = {
3588
+ organizations: "organizations",
3589
+ projects: "projects",
3590
+ tasks: "tasks",
3591
+ comments: "comments",
3592
+ attachments: "attachments",
3593
+ reports: "reports"
3594
+ };
3595
+ // ../../src/core/database/migrations/advisoryLock.ts
3596
+ var MIGRATION_LOCK_KEY = 42424242;
3597
+ async function withMigrationLock(db2, callback, lockKey = MIGRATION_LOCK_KEY) {
3598
+ await db2.unsafe("SELECT pg_advisory_lock($1)", [lockKey]);
3599
+ try {
3600
+ return await callback();
3601
+ } finally {
3602
+ await db2.unsafe("SELECT pg_advisory_unlock($1)", [lockKey]);
3429
3603
  }
3430
- const headers = applyEtagHeaders(new Headers(response.headers), etag);
3431
- return new Response(response.body, {
3432
- status: response.status,
3433
- statusText: response.statusText,
3434
- headers
3435
- });
3436
3604
  }
3437
- // ../../src/core/tenant/tenantContext.ts
3438
- import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
3439
- var tenantContext = new AsyncLocalStorage4;
3440
- function runWithTenant(tenant, callback) {
3441
- return tenantContext.run(tenant, callback);
3605
+ // ../../src/core/database/migrations/runner.ts
3606
+ import { readdir } from "fs/promises";
3607
+ import { join } from "path";
3608
+ import { pathToFileURL } from "url";
3609
+ var MIGRATIONS_TABLE = "framework_migrations";
3610
+ async function ensureMigrationsTable(db2) {
3611
+ await db2.unsafe(`
3612
+ CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
3613
+ name TEXT PRIMARY KEY,
3614
+ batch INTEGER NOT NULL,
3615
+ run_on TIMESTAMPTZ NOT NULL DEFAULT NOW()
3616
+ )
3617
+ `);
3442
3618
  }
3443
- function currentTenant() {
3444
- return tenantContext.getStore() ?? null;
3619
+ async function getAppliedMigrations(db2) {
3620
+ await ensureMigrationsTable(db2);
3621
+ return await db2.unsafe(`
3622
+ SELECT name, batch
3623
+ FROM ${MIGRATIONS_TABLE}
3624
+ ORDER BY batch ASC, name ASC
3625
+ `);
3445
3626
  }
3446
- function currentTenantId() {
3447
- return currentTenant()?.id ?? 1;
3627
+ async function loadMigrationsFromDirectory(directory) {
3628
+ const entries = await readdir(directory);
3629
+ const migrationFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && entry !== "types.ts" && entry !== "index.ts" && entry !== "runner.ts").sort();
3630
+ const loadedMigrations = await Promise.all(migrationFiles.map(async (fileName) => {
3631
+ const moduleUrl = pathToFileURL(join(directory, fileName)).href;
3632
+ const module = await import(moduleUrl);
3633
+ return module.default;
3634
+ }));
3635
+ return loadedMigrations.filter((migration) => migration?.name !== undefined);
3448
3636
  }
3449
- function rateLimitMultiplierForPlan(plan) {
3450
- switch (plan) {
3451
- case "enterprise":
3452
- return 4;
3453
- case "pro":
3454
- return 2;
3455
- default:
3456
- return 1;
3457
- }
3637
+ async function getMigrationStatus(db2, migrations) {
3638
+ const applied = await getAppliedMigrations(db2);
3639
+ const appliedByName = new Map(applied.map(({ name, batch }) => [name, Number(batch)]));
3640
+ return migrations.map(({ name }) => ({
3641
+ name,
3642
+ status: appliedByName.has(name) ? "up" : "pending",
3643
+ batch: appliedByName.get(name) ?? null
3644
+ }));
3458
3645
  }
3459
-
3460
- // ../../src/core/http/validation.ts
3461
- function buildRequestCacheKey(fallbackPath, request) {
3462
- if (!request) {
3463
- return fallbackPath;
3646
+ async function runPendingMigrations(db2, migrations, options = {}) {
3647
+ const applied = await getAppliedMigrations(db2);
3648
+ const appliedNames = new Set(applied.map(({ name }) => name));
3649
+ const nextBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0) + 1;
3650
+ const pendingMigrations = migrations.filter(({ name }) => !appliedNames.has(name));
3651
+ for (const migration of pendingMigrations) {
3652
+ options.onMigration?.(migration.name);
3653
+ await migration.up(db2);
3654
+ const inserted = await db2.unsafe(`INSERT INTO ${MIGRATIONS_TABLE} (name, batch) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING RETURNING name`, [migration.name, nextBatch]);
3655
+ if (inserted.length === 0) {
3656
+ throw new Error(`Migration ${migration.name} was applied but not recorded.`);
3657
+ }
3464
3658
  }
3465
- const url = new URL(request.url);
3466
- const user = currentAuthUser();
3467
- const authScope = user ? `u:${user.id}` : "guest";
3468
- const tenantScope = `t:${currentTenantId()}`;
3469
- return `${authScope}|${tenantScope}|${url.pathname}${url.search}`;
3659
+ return pendingMigrations.length;
3470
3660
  }
3471
- function getQueryParams(request) {
3472
- if (!request) {
3473
- return new URLSearchParams;
3661
+ async function migrateDatabase(db2, migrations, options = {}) {
3662
+ const { advisoryLock = false, onMigration } = options;
3663
+ if (advisoryLock) {
3664
+ return withMigrationLock(db2, () => runPendingMigrations(db2, migrations, { onMigration }));
3474
3665
  }
3475
- return new URL(request.url).searchParams;
3666
+ return runPendingMigrations(db2, migrations, { onMigration });
3476
3667
  }
3477
- function parseOptionalPositiveIntQueryParam(params, name) {
3478
- const value = params.get(name);
3479
- if (value === null || value.trim() === "") {
3480
- return;
3668
+ async function rollbackDatabase(db2, migrations, options = {}) {
3669
+ const applied = await getAppliedMigrations(db2);
3670
+ if (applied.length === 0) {
3671
+ return 0;
3481
3672
  }
3482
- const parsed = Number.parseInt(value, 10);
3483
- if (!Number.isInteger(parsed) || parsed <= 0) {
3484
- throw new BadRequestError(`Invalid query parameter "${name}". Expected a positive integer.`);
3673
+ const lastBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0);
3674
+ const migrationsToRollback = new Set(applied.filter(({ batch }) => Number(batch) === lastBatch).map(({ name }) => name));
3675
+ let rolledBack = 0;
3676
+ for (const migration of [...migrations].reverse()) {
3677
+ if (!migrationsToRollback.has(migration.name)) {
3678
+ continue;
3679
+ }
3680
+ options.onMigration?.(migration.name);
3681
+ await migration.down(db2);
3682
+ await db2.unsafe(`DELETE FROM ${MIGRATIONS_TABLE} WHERE name = $1`, [migration.name]);
3683
+ rolledBack += 1;
3485
3684
  }
3486
- return parsed;
3685
+ return rolledBack;
3487
3686
  }
3488
- function parseOptionalBooleanQueryParam(params, name) {
3489
- const value = params.get(name);
3490
- if (value === null || value.trim() === "") {
3687
+ async function freshDatabase(db2, migrations, options = {}) {
3688
+ const runFresh = async () => {
3689
+ const applied = await getAppliedMigrations(db2);
3690
+ const appliedNames = new Set(applied.map(({ name }) => name));
3691
+ const appliedMigrations = migrations.filter(({ name }) => appliedNames.has(name));
3692
+ for (const migration of [...appliedMigrations].reverse()) {
3693
+ options.onMigration?.(migration.name);
3694
+ await migration.down(db2);
3695
+ }
3696
+ if (appliedMigrations.length > 0) {
3697
+ await db2.unsafe(`DELETE FROM ${MIGRATIONS_TABLE}`);
3698
+ }
3699
+ await runPendingMigrations(db2, migrations, options);
3700
+ };
3701
+ if (options.advisoryLock) {
3702
+ await withMigrationLock(db2, runFresh);
3491
3703
  return;
3492
3704
  }
3493
- switch (value.toLowerCase()) {
3494
- case "true":
3495
- case "1":
3496
- return true;
3497
- case "false":
3498
- case "0":
3499
- return false;
3500
- default:
3501
- throw new BadRequestError(`Invalid query parameter "${name}". Expected a boolean.`);
3502
- }
3705
+ await runFresh();
3503
3706
  }
3504
- function parseOptionalEnumQueryParam(params, name, allowedValues) {
3505
- const value = params.get(name);
3506
- if (value === null || value.trim() === "") {
3507
- return;
3508
- }
3509
- if (!allowedValues.includes(value)) {
3510
- throw new BadRequestError(`Invalid query parameter "${name}". Expected one of: ${allowedValues.join(", ")}.`);
3511
- }
3512
- return value;
3707
+ // ../../src/core/database/seeders/runner.ts
3708
+ import { readdir as readdir2 } from "fs/promises";
3709
+ import { join as join2 } from "path";
3710
+ import { pathToFileURL as pathToFileURL2 } from "url";
3711
+ async function loadSeedersFromDirectory(directory) {
3712
+ const entries = await readdir2(directory);
3713
+ const seederFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && entry !== "types.ts" && entry !== "index.ts" && entry !== "runner.ts").sort();
3714
+ const loadedSeeders = await Promise.all(seederFiles.map(async (fileName) => {
3715
+ const moduleUrl = pathToFileURL2(join2(directory, fileName)).href;
3716
+ const module = await import(moduleUrl);
3717
+ return module.default;
3718
+ }));
3719
+ return loadedSeeders.filter((seeder) => seeder?.name !== undefined);
3513
3720
  }
3514
- function expectObject(value, label = "request body") {
3515
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
3516
- throw new BadRequestError(`${label} must be a JSON object.`);
3721
+ async function runSeedersFromDirectory(directory, db2, options) {
3722
+ const seeders = await loadSeedersFromDirectory(directory);
3723
+ if (seeders.length === 0) {
3724
+ return 0;
3517
3725
  }
3518
- return value;
3519
- }
3520
- async function parseJsonBody(request, validator) {
3521
- let payload;
3522
- try {
3523
- payload = await request.json();
3524
- } catch {
3525
- throw new BadRequestError("Request body must be valid JSON.");
3726
+ for (const seeder of seeders) {
3727
+ options?.onSeeder?.(seeder.name);
3728
+ await seeder.run(db2);
3526
3729
  }
3527
- return validator(payload);
3730
+ return seeders.length;
3528
3731
  }
3529
- function readRequiredString(payload, field, options = {}) {
3530
- const value = payload[field];
3531
- if (typeof value !== "string" || value.trim() === "") {
3532
- throw new BadRequestError(`"${field}" is required and must be a string.`);
3533
- }
3534
- const trimmed = value.trim();
3535
- if (options.minLength !== undefined && trimmed.length < options.minLength) {
3536
- throw new BadRequestError(`"${field}" must be at least ${options.minLength} characters.`);
3732
+ // ../../src/core/mail/mailer.ts
3733
+ function resolveSmtpConfig() {
3734
+ const host = process.env.MAIL_HOST?.trim();
3735
+ if (!host) {
3736
+ throw new Error('MAIL_DRIVER="smtp" requires MAIL_HOST to be set.');
3537
3737
  }
3538
- if (options.maxLength !== undefined && trimmed.length > options.maxLength) {
3539
- throw new BadRequestError(`"${field}" must be at most ${options.maxLength} characters.`);
3738
+ const from = process.env.MAIL_FROM?.trim();
3739
+ if (!from) {
3740
+ throw new Error('MAIL_DRIVER="smtp" requires MAIL_FROM to be set.');
3540
3741
  }
3541
- if (options.pattern && !options.pattern.test(trimmed)) {
3542
- throw new BadRequestError(`"${field}" has an invalid format.`);
3742
+ const port = Number.parseInt(process.env.MAIL_PORT ?? "587", 10);
3743
+ if (!Number.isInteger(port) || port <= 0) {
3744
+ throw new Error('Environment variable "MAIL_PORT" must be a positive integer.');
3543
3745
  }
3544
- return trimmed;
3746
+ return {
3747
+ host,
3748
+ port,
3749
+ from,
3750
+ secure: (process.env.MAIL_SECURE ?? "false") === "true",
3751
+ ...process.env.MAIL_USERNAME?.trim() ? { username: process.env.MAIL_USERNAME.trim() } : {},
3752
+ ...process.env.MAIL_PASSWORD?.trim() ? { password: process.env.MAIL_PASSWORD.trim() } : {}
3753
+ };
3545
3754
  }
3546
- function readOptionalString(payload, field, options = {}) {
3547
- if (!(field in payload) || payload[field] === undefined) {
3548
- return;
3755
+ function encodeBase64(value) {
3756
+ return Buffer.from(value, "utf8").toString("base64");
3757
+ }
3758
+ function parseSmtpResponses(buffer) {
3759
+ const responses = [];
3760
+ let remainder = buffer;
3761
+ while (remainder.includes(`\r
3762
+ `)) {
3763
+ const index = remainder.indexOf(`\r
3764
+ `);
3765
+ const line = remainder.slice(0, index);
3766
+ remainder = remainder.slice(index + 2);
3767
+ if (line.length >= 4 && line[3] === "-") {
3768
+ continue;
3769
+ }
3770
+ responses.push(line);
3549
3771
  }
3550
- return readRequiredString(payload, field, options);
3772
+ return { responses, remainder };
3551
3773
  }
3552
- function readRequiredEnum(payload, field, allowedValues) {
3553
- const value = readRequiredString(payload, field);
3554
- if (!allowedValues.includes(value)) {
3555
- throw new BadRequestError(`"${field}" must be one of: ${allowedValues.join(", ")}.`);
3774
+ async function waitForSmtpResponse(readResponse, expectedCodes) {
3775
+ const response = await readResponse();
3776
+ const code = response.slice(0, 3);
3777
+ if (!expectedCodes.includes(code)) {
3778
+ throw new Error(`Unexpected SMTP response: ${response}`);
3556
3779
  }
3557
- return value;
3780
+ return response;
3558
3781
  }
3559
- function readOptionalEnum(payload, field, allowedValues) {
3560
- if (!(field in payload) || payload[field] === undefined) {
3561
- return;
3562
- }
3563
- return readRequiredEnum(payload, field, allowedValues);
3782
+ async function openSmtpConnection(config) {
3783
+ let buffer = "";
3784
+ const waiters = [];
3785
+ const readResponse = () => new Promise((resolve, reject) => {
3786
+ const parsed = parseSmtpResponses(buffer);
3787
+ if (parsed.responses.length > 0) {
3788
+ buffer = parsed.remainder;
3789
+ resolve(parsed.responses.shift());
3790
+ return;
3791
+ }
3792
+ waiters.push({ resolve, reject });
3793
+ });
3794
+ const socket = await Bun.connect({
3795
+ hostname: config.host,
3796
+ port: config.port,
3797
+ socket: {
3798
+ open() {},
3799
+ data(_socket, chunk) {
3800
+ buffer += Buffer.from(chunk).toString("utf8");
3801
+ const parsed = parseSmtpResponses(buffer);
3802
+ buffer = parsed.remainder;
3803
+ while (parsed.responses.length > 0 && waiters.length > 0) {
3804
+ const response = parsed.responses.shift();
3805
+ waiters.shift()?.resolve(response);
3806
+ }
3807
+ },
3808
+ error(_socket, error) {
3809
+ const pending = waiters.splice(0);
3810
+ for (const waiter of pending) {
3811
+ waiter.reject(error instanceof Error ? error : new Error(String(error)));
3812
+ }
3813
+ }
3814
+ }
3815
+ });
3816
+ return { socket, readResponse };
3564
3817
  }
3565
- function readRequiredPositiveInt(payload, field) {
3566
- const value = payload[field];
3567
- if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
3568
- throw new BadRequestError(`"${field}" is required and must be a positive integer.`);
3818
+ async function defaultSmtpTransport(config, message) {
3819
+ const { socket, readResponse } = await openSmtpConnection(config);
3820
+ try {
3821
+ await waitForSmtpResponse(readResponse, ["220"]);
3822
+ await socket.write(`EHLO workhub.local\r
3823
+ `);
3824
+ await waitForSmtpResponse(readResponse, ["250"]);
3825
+ if (config.username && config.password) {
3826
+ await socket.write(`AUTH LOGIN\r
3827
+ `);
3828
+ await waitForSmtpResponse(readResponse, ["334"]);
3829
+ await socket.write(`${encodeBase64(config.username)}\r
3830
+ `);
3831
+ await waitForSmtpResponse(readResponse, ["334"]);
3832
+ await socket.write(`${encodeBase64(config.password)}\r
3833
+ `);
3834
+ await waitForSmtpResponse(readResponse, ["235"]);
3835
+ }
3836
+ await socket.write(`MAIL FROM:<${config.from}>\r
3837
+ `);
3838
+ await waitForSmtpResponse(readResponse, ["250"]);
3839
+ await socket.write(`RCPT TO:<${message.to}>\r
3840
+ `);
3841
+ await waitForSmtpResponse(readResponse, ["250", "251"]);
3842
+ await socket.write(`DATA\r
3843
+ `);
3844
+ await waitForSmtpResponse(readResponse, ["354"]);
3845
+ const payload = buildSmtpPayload(config.from, message);
3846
+ await socket.write(payload);
3847
+ await waitForSmtpResponse(readResponse, ["250"]);
3848
+ await socket.write(`QUIT\r
3849
+ `);
3850
+ await waitForSmtpResponse(readResponse, ["221"]);
3851
+ } finally {
3852
+ socket.end();
3569
3853
  }
3570
- return value;
3571
3854
  }
3572
- function readOptionalPositiveInt(payload, field) {
3573
- if (!(field in payload) || payload[field] === undefined) {
3574
- return;
3855
+ function buildSmtpPayload(from, message) {
3856
+ const headers = [
3857
+ `From: ${from}`,
3858
+ `To: ${message.to}`,
3859
+ `Subject: ${message.subject}`,
3860
+ "MIME-Version: 1.0"
3861
+ ];
3862
+ if (message.html) {
3863
+ const boundary = `strata-${Date.now().toString(36)}`;
3864
+ headers.push(`Content-Type: multipart/alternative; boundary="${boundary}"`);
3865
+ const parts = [
3866
+ `--${boundary}`,
3867
+ "Content-Type: text/plain; charset=utf-8",
3868
+ "",
3869
+ message.body,
3870
+ `--${boundary}`,
3871
+ "Content-Type: text/html; charset=utf-8",
3872
+ "",
3873
+ message.html,
3874
+ `--${boundary}--`,
3875
+ ""
3876
+ ];
3877
+ return [...headers, "", ...parts, ".", ""].join(`\r
3878
+ `);
3575
3879
  }
3576
- return readRequiredPositiveInt(payload, field);
3880
+ headers.push("Content-Type: text/plain; charset=utf-8");
3881
+ return [...headers, "", message.body, ".", ""].join(`\r
3882
+ `);
3577
3883
  }
3578
- function parsePositiveIntParam(value, name = "id") {
3579
- const parsed = Number.parseInt(value, 10);
3580
- if (!Number.isInteger(parsed) || parsed <= 0) {
3581
- throw new BadRequestError(`Invalid ${name}. Expected a positive integer.`);
3884
+
3885
+ class LogMailDriver {
3886
+ async send(message) {
3887
+ console.log(JSON.stringify({
3888
+ level: "info",
3889
+ channel: "mail",
3890
+ to: message.to,
3891
+ subject: message.subject,
3892
+ body: message.body,
3893
+ ...message.html ? { html: message.html } : {}
3894
+ }));
3582
3895
  }
3583
- return parsed;
3584
3896
  }
3585
3897
 
3586
- // ../../src/core/http/formRequest.ts
3587
- class FormRequest {
3588
- authorize(_request) {
3589
- return true;
3898
+ class SmtpMailDriver {
3899
+ config;
3900
+ transport;
3901
+ constructor(config, transport = defaultSmtpTransport) {
3902
+ this.config = config;
3903
+ this.transport = transport;
3590
3904
  }
3591
- async validate(request) {
3592
- if (!await this.authorize(request)) {
3593
- throw new ForbiddenError;
3594
- }
3595
- return await parseJsonBody(request, (payload) => this.parse(payload));
3905
+ send(message) {
3906
+ return this.transport(this.config, message);
3596
3907
  }
3597
3908
  }
3598
3909
 
3599
- class QueryFormRequest {
3600
- validate(request) {
3601
- return this.parseQuery(request);
3910
+ class Mailer {
3911
+ driver;
3912
+ constructor(driver) {
3913
+ this.driver = driver;
3602
3914
  }
3603
- }
3604
- // ../../src/config/frontend.ts
3605
- function readFrontendMode() {
3606
- const mode = (process.env.FRONTEND_MODE ?? "api").trim();
3607
- if (mode === "server-htmx") {
3608
- return "server-htmx";
3915
+ send(message) {
3916
+ return this.driver.send(message);
3609
3917
  }
3610
- if (mode === "spa-react") {
3611
- return "spa-react";
3918
+ }
3919
+ function createMailDriver() {
3920
+ const driver = process.env.MAIL_DRIVER ?? "log";
3921
+ if (driver === "smtp") {
3922
+ return new SmtpMailDriver(resolveSmtpConfig());
3612
3923
  }
3613
- return "api";
3924
+ return new LogMailDriver;
3614
3925
  }
3615
- function isViewsEnabled() {
3616
- return readFrontendMode() === "server-htmx";
3926
+ var appMailer = new Mailer(createMailDriver());
3927
+ function mailer() {
3928
+ return appMailer;
3617
3929
  }
3618
3930
 
3619
- // ../../src/core/view/etaViewEngine.ts
3620
- import { join as join4 } from "path";
3621
- import { Eta } from "eta";
3622
- var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
3623
- var DEFAULT_LAYOUT = "layouts/app.eta";
3931
+ // ../../src/core/storage/storage.ts
3932
+ import { mkdir, readFile, unlink, writeFile } from "fs/promises";
3933
+ import { dirname, join as join3 } from "path";
3934
+ var {S3Client } = globalThis.Bun;
3624
3935
 
3625
- class EtaViewEngine {
3626
- eta;
3627
- resolveLayoutData;
3628
- constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
3629
- this.eta = new Eta({
3630
- views: viewsDirectory,
3631
- autoTrim: false
3632
- });
3633
- this.resolveLayoutData = resolveLayoutData;
3936
+ class LocalStorageDriver {
3937
+ rootDirectory;
3938
+ constructor(rootDirectory) {
3939
+ this.rootDirectory = rootDirectory;
3634
3940
  }
3635
- async render(name, data = {}, options = {}) {
3636
- const template = name.endsWith(".eta") ? name : `${name}.eta`;
3637
- const layoutData = this.resolveLayoutData ? await this.resolveLayoutData() : {};
3638
- const mergedData = { ...layoutData, ...data };
3639
- const body = await this.eta.renderAsync(template, mergedData);
3640
- const layout = options.layout ?? DEFAULT_LAYOUT;
3641
- if (layout === false) {
3642
- return body;
3643
- }
3644
- const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
3645
- return await this.eta.renderAsync(layoutTemplate, {
3646
- ...mergedData,
3647
- body
3648
- });
3941
+ resolveRootDirectory() {
3942
+ return this.rootDirectory ?? process.env.STORAGE_PATH ?? "storage";
3649
3943
  }
3650
- }
3651
- // ../../src/core/view/htmlResponse.ts
3652
- function htmlResponse(html, init = {}) {
3653
- return new Response(html, {
3654
- status: init.status ?? 200,
3655
- statusText: init.statusText,
3656
- headers: {
3657
- "Content-Type": "text/html; charset=utf-8"
3658
- }
3659
- });
3660
- }
3661
- function isHtmxRequest(request) {
3662
- return request.headers.get("HX-Request") === "true";
3663
- }
3664
- // ../../src/core/auth/oauth/oidcProvider.ts
3665
- class OidcProvider {
3666
- options;
3667
- name;
3668
- constructor(options) {
3669
- this.options = options;
3670
- this.name = options.name;
3671
- }
3672
- getAuthorizationUrl(state) {
3673
- const params = new URLSearchParams({
3674
- client_id: this.options.clientId,
3675
- redirect_uri: this.options.redirectUri,
3676
- response_type: "code",
3677
- scope: (this.options.scopes ?? ["openid", "email", "profile"]).join(" "),
3678
- state
3679
- });
3680
- return `${this.options.issuer.replace(/\/$/, "")}/authorize?${params.toString()}`;
3681
- }
3682
- async exchangeCode(code) {
3683
- const tokenResponse = await fetch(`${this.options.issuer.replace(/\/$/, "")}/token`, {
3684
- method: "POST",
3685
- headers: { "content-type": "application/x-www-form-urlencoded" },
3686
- body: new URLSearchParams({
3687
- grant_type: "authorization_code",
3688
- code,
3689
- redirect_uri: this.options.redirectUri,
3690
- client_id: this.options.clientId,
3691
- client_secret: this.options.clientSecret
3692
- })
3693
- });
3694
- const tokenBody = await tokenResponse.json();
3695
- if (!tokenBody.access_token) {
3696
- throw new Error("OIDC token exchange failed.");
3697
- }
3698
- const profileResponse = await fetch(`${this.options.issuer.replace(/\/$/, "")}/userinfo`, {
3699
- headers: { authorization: `Bearer ${tokenBody.access_token}` }
3700
- });
3701
- const profile = await profileResponse.json();
3702
- return {
3703
- providerUserId: profile.sub,
3704
- email: profile.email ?? `${profile.sub}@oidc.local`,
3705
- name: profile.name ?? profile.sub
3706
- };
3944
+ resolvePath(path) {
3945
+ return join3(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
3707
3946
  }
3708
- }
3709
-
3710
- // ../../src/core/auth/oauth/providers.ts
3711
- class GitHubOAuthProvider {
3712
- options;
3713
- name = "github";
3714
- constructor(options) {
3715
- this.options = options;
3947
+ async put(path, contents) {
3948
+ const absolutePath = this.resolvePath(path);
3949
+ await mkdir(dirname(absolutePath), { recursive: true });
3950
+ await writeFile(absolutePath, contents);
3951
+ return path;
3716
3952
  }
3717
- getAuthorizationUrl(state) {
3718
- const params = new URLSearchParams({
3719
- client_id: this.options.clientId,
3720
- redirect_uri: this.options.redirectUri,
3721
- scope: "read:user user:email",
3722
- state
3723
- });
3724
- return `https://github.com/login/oauth/authorize?${params.toString()}`;
3725
- }
3726
- async exchangeCode(code) {
3727
- const tokenResponse = await fetch("https://github.com/login/oauth/access_token", {
3728
- method: "POST",
3729
- headers: {
3730
- accept: "application/json",
3731
- "content-type": "application/json"
3732
- },
3733
- body: JSON.stringify({
3734
- client_id: this.options.clientId,
3735
- client_secret: this.options.clientSecret,
3736
- code,
3737
- redirect_uri: this.options.redirectUri
3738
- })
3739
- });
3740
- const tokenBody = await tokenResponse.json();
3741
- if (!tokenBody.access_token) {
3742
- throw new Error("GitHub OAuth token exchange failed.");
3743
- }
3744
- const profileResponse = await fetch("https://api.github.com/user", {
3745
- headers: {
3746
- authorization: `Bearer ${tokenBody.access_token}`,
3747
- accept: "application/json",
3748
- "user-agent": "workhub"
3749
- }
3750
- });
3751
- const profile = await profileResponse.json();
3752
- return {
3753
- providerUserId: String(profile.id),
3754
- email: profile.email ?? `${profile.login}@users.noreply.github.com`,
3755
- name: profile.name ?? profile.login
3756
- };
3953
+ async get(path) {
3954
+ try {
3955
+ return await readFile(this.resolvePath(path));
3956
+ } catch {
3957
+ return null;
3958
+ }
3959
+ }
3960
+ async delete(path) {
3961
+ try {
3962
+ await unlink(this.resolvePath(path));
3963
+ return true;
3964
+ } catch {
3965
+ return false;
3966
+ }
3757
3967
  }
3758
3968
  }
3759
3969
 
3760
- class MockOAuthProvider {
3761
- profile;
3762
- name = "mock";
3763
- constructor(profile) {
3764
- this.profile = profile;
3970
+ class S3StorageDriver {
3971
+ client;
3972
+ constructor(client) {
3973
+ this.client = client;
3974
+ }
3975
+ async put(path, contents) {
3976
+ await this.client.write(path.replace(/^\/+/, ""), contents);
3977
+ return path;
3765
3978
  }
3766
- getAuthorizationUrl(state) {
3767
- return `https://mock.oauth/authorize?state=${encodeURIComponent(state)}`;
3979
+ async get(path) {
3980
+ const normalizedPath = path.replace(/^\/+/, "");
3981
+ const file = this.client.file(normalizedPath);
3982
+ if (!await file.exists()) {
3983
+ return null;
3984
+ }
3985
+ return new Uint8Array(await file.arrayBuffer());
3768
3986
  }
3769
- async exchangeCode(code) {
3770
- if (code !== "valid-code") {
3771
- throw new Error("Invalid OAuth code.");
3987
+ async delete(path) {
3988
+ try {
3989
+ await this.client.unlink(path.replace(/^\/+/, ""));
3990
+ return true;
3991
+ } catch {
3992
+ return false;
3772
3993
  }
3773
- return this.profile;
3774
3994
  }
3775
3995
  }
3776
3996
 
3777
- // ../../src/core/auth/oauth/samlProvider.ts
3778
- class SamlProvider {
3779
- loginUrl;
3780
- name = "saml";
3781
- constructor(loginUrl) {
3782
- this.loginUrl = loginUrl;
3997
+ class StorageManager {
3998
+ driver;
3999
+ constructor(driver) {
4000
+ this.driver = driver;
3783
4001
  }
3784
- getAuthorizationUrl(state) {
3785
- return `${this.loginUrl}?state=${encodeURIComponent(state)}`;
4002
+ put(path, contents) {
4003
+ return this.driver.put(path, contents);
3786
4004
  }
3787
- async exchangeCode(code) {
3788
- if (!code.startsWith("saml:")) {
3789
- throw new Error("Invalid SAML assertion reference.");
3790
- }
3791
- const [, email, name] = code.split(":");
3792
- return {
3793
- providerUserId: email ?? "saml-user",
3794
- email: email ?? "saml-user@workhub.test",
3795
- name: name ?? "SAML User"
3796
- };
4005
+ get(path) {
4006
+ return this.driver.get(path);
4007
+ }
4008
+ delete(path) {
4009
+ return this.driver.delete(path);
3797
4010
  }
3798
4011
  }
3799
-
3800
- // ../../src/config/features.ts
3801
- function readFeatureFlags() {
4012
+ function resolveS3Config() {
4013
+ const accessKeyId = process.env.AWS_ACCESS_KEY_ID?.trim();
4014
+ const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY?.trim();
4015
+ const bucket = process.env.AWS_BUCKET?.trim();
4016
+ if (!accessKeyId || !secretAccessKey || !bucket) {
4017
+ throw new Error('STORAGE_DRIVER="s3" requires AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_BUCKET.');
4018
+ }
3802
4019
  return {
3803
- webhooks: (process.env.FEATURE_WEBHOOKS ?? "true") !== "false",
3804
- fullTextSearch: (process.env.FEATURE_SEARCH ?? "true") !== "false",
3805
- auditLog: (process.env.FEATURE_AUDIT_LOG ?? "true") !== "false",
3806
- oauthLogin: (process.env.FEATURE_OAUTH ?? "true") !== "false",
3807
- samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
3808
- scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
3809
- billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
3810
- siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
3811
- publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
3812
- emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
3813
- mfa: (process.env.FEATURE_MFA ?? "false") === "true"
4020
+ accessKeyId,
4021
+ secretAccessKey,
4022
+ bucket,
4023
+ ...process.env.AWS_REGION?.trim() ? { region: process.env.AWS_REGION.trim() } : {},
4024
+ ...process.env.AWS_ENDPOINT?.trim() ? { endpoint: process.env.AWS_ENDPOINT.trim() } : {}
3814
4025
  };
3815
4026
  }
3816
- var featureFlags = readFeatureFlags();
3817
- function isFeatureEnabled(feature) {
3818
- return readFeatureFlags()[feature];
3819
- }
3820
- // ../../src/modules/user/apiTokenTable.ts
3821
- var apiTokenTable = defineTable({
3822
- name: "api_token",
3823
- primaryKey: "id",
3824
- columns: [
3825
- "id",
3826
- "user_id",
3827
- "name",
3828
- "token_hash",
3829
- "abilities",
3830
- "last_used_at",
3831
- "expires_at",
3832
- "created_at"
3833
- ],
3834
- defaultOrderBy: { column: "id", direction: "ASC" }
3835
- });
3836
-
3837
- // ../../src/core/auth/password.ts
3838
- async function hashPassword(password) {
3839
- return await Bun.password.hash(password, {
3840
- algorithm: "bcrypt",
3841
- cost: 10
4027
+ function createS3Client(config = resolveS3Config()) {
4028
+ return new S3Client({
4029
+ accessKeyId: config.accessKeyId,
4030
+ secretAccessKey: config.secretAccessKey,
4031
+ bucket: config.bucket,
4032
+ ...config.region ? { region: config.region } : {},
4033
+ ...config.endpoint ? { endpoint: config.endpoint } : {}
3842
4034
  });
3843
4035
  }
3844
- async function verifyPassword(password, passwordHash) {
3845
- return await Bun.password.verify(password, passwordHash);
3846
- }
3847
-
3848
- // ../../src/core/crypto/fieldEncryption.ts
3849
- import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "crypto";
3850
- var ENCRYPTION_PREFIX = "enc:v1:";
3851
- var IV_LENGTH = 12;
3852
- var TAG_LENGTH = 16;
3853
- function resolveEncryptionKey() {
3854
- const raw = process.env.KMS_ENCRYPTION_KEY?.trim();
3855
- if (!raw) {
3856
- return null;
3857
- }
3858
- if (/^[0-9a-f]{64}$/i.test(raw)) {
3859
- return Buffer.from(raw, "hex");
3860
- }
3861
- const decoded = Buffer.from(raw, "base64");
3862
- if (decoded.length === 32) {
3863
- return decoded;
4036
+ function createStorageDriver() {
4037
+ const driver = process.env.STORAGE_DRIVER ?? "local";
4038
+ if (driver === "s3") {
4039
+ return new S3StorageDriver(createS3Client());
3864
4040
  }
3865
- throw new Error("KMS_ENCRYPTION_KEY must be 32 bytes (hex or base64).");
4041
+ return new LocalStorageDriver;
3866
4042
  }
3867
- function isFieldEncryptionEnabled() {
3868
- const featureFlag = process.env.FEATURE_FIELD_ENCRYPTION;
3869
- if (featureFlag === "false") {
3870
- return false;
3871
- }
3872
- if (featureFlag === "true") {
3873
- return true;
4043
+ var defaultStorage = { current: null };
4044
+ function storage() {
4045
+ if (!defaultStorage.current) {
4046
+ defaultStorage.current = new StorageManager(createStorageDriver());
3874
4047
  }
3875
- return (process.env.APP_ENV ?? "local") === "production";
4048
+ return defaultStorage.current;
3876
4049
  }
3877
- function encryptField(plaintext, key) {
3878
- const iv = randomBytes(IV_LENGTH);
3879
- const cipher = createCipheriv("aes-256-gcm", key, iv);
3880
- const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
3881
- const tag = cipher.getAuthTag();
3882
- const payload = Buffer.concat([iv, encrypted, tag]).toString("base64");
3883
- return `${ENCRYPTION_PREFIX}${payload}`;
4050
+ function resetDefaultStorage() {
4051
+ defaultStorage.current = null;
3884
4052
  }
3885
- function decryptField(value, key) {
3886
- if (!value.startsWith(ENCRYPTION_PREFIX)) {
3887
- return value;
3888
- }
3889
- const payload = Buffer.from(value.slice(ENCRYPTION_PREFIX.length), "base64");
3890
- const iv = payload.subarray(0, IV_LENGTH);
3891
- const tag = payload.subarray(payload.length - TAG_LENGTH);
3892
- const ciphertext = payload.subarray(IV_LENGTH, payload.length - TAG_LENGTH);
3893
- const decipher = createDecipheriv("aes-256-gcm", key, iv);
3894
- decipher.setAuthTag(tag);
3895
- return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
4053
+
4054
+ // ../../src/core/facades/index.ts
4055
+ function cache() {
4056
+ return resolveApplicationCache();
3896
4057
  }
3897
- function hashLookupValue(normalizedValue, key) {
3898
- return createHmac("sha256", key).update(normalizedValue).digest("hex");
4058
+ function auth() {
4059
+ return resolveApplicationAuth();
3899
4060
  }
3900
- function normalizeEmail(email) {
3901
- return email.trim().toLowerCase();
4061
+ function policyGate() {
4062
+ return resolveApplicationPolicyGate();
3902
4063
  }
3903
- function protectEmail(email) {
3904
- const normalized = normalizeEmail(email);
3905
- const key = resolveEncryptionKey();
3906
- if (!key || !isFieldEncryptionEnabled()) {
3907
- return { storedEmail: normalized, emailLookup: normalized };
3908
- }
3909
- return {
3910
- storedEmail: encryptField(normalized, key),
3911
- emailLookup: hashLookupValue(normalized, key)
3912
- };
4064
+ function queue() {
4065
+ return resolveApplicationQueue();
3913
4066
  }
3914
- function revealEmail(storedEmail) {
3915
- const key = resolveEncryptionKey();
3916
- if (!key || !storedEmail.startsWith(ENCRYPTION_PREFIX)) {
3917
- return storedEmail;
3918
- }
3919
- return decryptField(storedEmail, key);
4067
+ function events() {
4068
+ return eventBus;
3920
4069
  }
3921
- function emailLookupForQuery(email) {
3922
- const normalized = normalizeEmail(email);
3923
- const key = resolveEncryptionKey();
3924
- if (!key || !isFieldEncryptionEnabled()) {
3925
- return normalized;
3926
- }
3927
- return hashLookupValue(normalized, key);
4070
+ function config(key) {
4071
+ return resolveApplicationConfig().get(key);
3928
4072
  }
3929
-
3930
- // ../../src/core/crypto/mfaSecret.ts
3931
- function protectMfaSecret(secret) {
3932
- const key = resolveEncryptionKey();
3933
- if (!isFieldEncryptionEnabled() || !key) {
3934
- return secret;
3935
- }
3936
- return encryptField(secret, key);
4073
+ function log() {
4074
+ return resolveApplicationLogger();
3937
4075
  }
3938
- function revealMfaSecret(stored) {
3939
- if (!stored) {
3940
- return null;
4076
+ function mail() {
4077
+ return mailer();
4078
+ }
4079
+ function storageFacade() {
4080
+ return storage();
4081
+ }
4082
+ // ../../src/core/http/bodySizeLimitMiddleware.ts
4083
+ var DEFAULT_MAX_BODY_BYTES = 1048576;
4084
+ function resolveMaxBodyBytes() {
4085
+ const raw = process.env.MAX_REQUEST_BODY_BYTES?.trim();
4086
+ if (!raw) {
4087
+ return DEFAULT_MAX_BODY_BYTES;
3941
4088
  }
3942
- const key = resolveEncryptionKey();
3943
- if (!isFieldEncryptionEnabled() || !key || !stored.startsWith("enc:v1:")) {
3944
- return stored;
4089
+ const parsed = Number.parseInt(raw, 10);
4090
+ if (!Number.isInteger(parsed) || parsed <= 0) {
4091
+ return DEFAULT_MAX_BODY_BYTES;
3945
4092
  }
3946
- return decryptField(stored, key);
4093
+ return parsed;
4094
+ }
4095
+ function createBodySizeLimitMiddleware(maxBytes = resolveMaxBodyBytes()) {
4096
+ return async (request, next) => {
4097
+ const contentLength = request.headers.get("content-length");
4098
+ if (contentLength) {
4099
+ const bytes = Number.parseInt(contentLength, 10);
4100
+ if (Number.isInteger(bytes) && bytes > maxBytes) {
4101
+ const error = new PayloadTooLargeError(`Request body exceeds the ${maxBytes} byte limit.`);
4102
+ return Response.json({ error: error.message }, { status: error.status });
4103
+ }
4104
+ }
4105
+ return await next();
4106
+ };
4107
+ }
4108
+ // ../../src/core/crypto/nonCryptographicHash.ts
4109
+ function nonCryptographicDigest(input) {
4110
+ return Bun.hash(input).toString(16);
3947
4111
  }
3948
4112
 
3949
- // ../../src/core/security/securityEvents.ts
3950
- function logSecurityEvent(event, details = {}) {
3951
- const meta = currentRequestMeta();
3952
- const user = currentAuthUser();
3953
- console.log(JSON.stringify({
3954
- level: "security",
3955
- event,
3956
- timestamp: new Date().toISOString(),
3957
- ip_address: meta.ipAddress ?? null,
3958
- user_agent: meta.userAgent ?? null,
3959
- user_id: user?.id ?? null,
3960
- ...details
3961
- }));
4113
+ // ../../src/core/http/etag.ts
4114
+ function isEtagEnabled() {
4115
+ return (process.env.FEATURE_ETAG ?? "true") !== "false";
4116
+ }
4117
+ function formatWeakEtag(digest) {
4118
+ return `W/"${digest}"`;
4119
+ }
4120
+ function computeEtagFromJson(data) {
4121
+ const digest = nonCryptographicDigest(JSON.stringify(data)).slice(0, 32);
4122
+ return formatWeakEtag(digest);
4123
+ }
4124
+ function etagFromResource(resource) {
4125
+ const version = resource.updated_at ?? resource.created_at ?? "";
4126
+ const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
4127
+ const digest = nonCryptographicDigest(`${String(resource.id ?? "0")}:${versionText}`).slice(0, 32);
4128
+ return formatWeakEtag(digest);
4129
+ }
4130
+ function normalizeEtag(value) {
4131
+ return value.trim();
4132
+ }
4133
+ function etagValuesMatch(left, right) {
4134
+ return normalizeEtag(left) === normalizeEtag(right);
4135
+ }
4136
+ function parseEtagList(header) {
4137
+ if (!header) {
4138
+ return [];
4139
+ }
4140
+ return header.split(",").map((value) => normalizeEtag(value)).filter(Boolean);
4141
+ }
4142
+ function ifNoneMatchSatisfied(request, etag) {
4143
+ const header = request.headers.get("if-none-match");
4144
+ if (!header) {
4145
+ return false;
4146
+ }
4147
+ if (header.trim() === "*") {
4148
+ return true;
4149
+ }
4150
+ return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
3962
4151
  }
3963
-
3964
- // ../../src/core/security/tokenExpiry.ts
3965
- function resolveDefaultTokenExpiryDays() {
3966
- const raw = process.env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim();
3967
- if (!raw) {
3968
- return null;
4152
+ function ifMatchSatisfied(request, etag) {
4153
+ const header = request.headers.get("if-match");
4154
+ if (!header) {
4155
+ return false;
3969
4156
  }
3970
- const parsed = Number.parseInt(raw, 10);
3971
- if (!Number.isInteger(parsed) || parsed <= 0) {
3972
- return null;
4157
+ if (header.trim() === "*") {
4158
+ return true;
3973
4159
  }
3974
- return parsed;
4160
+ return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
3975
4161
  }
3976
-
3977
- // ../../src/core/security/totp.ts
3978
- import { createHmac as createHmac2 } from "crypto";
3979
- function decodeBase32(input) {
3980
- const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
3981
- const normalized = input.replace(/=+$/u, "").toUpperCase();
3982
- let bits = "";
3983
- for (const char of normalized) {
3984
- const value = alphabet.indexOf(char);
3985
- if (value === -1) {
3986
- throw new Error("Invalid base32 character in MFA secret.");
4162
+ function assertIfMatch(request, etag, options = {}) {
4163
+ const header = request.headers.get("if-match");
4164
+ if (!header) {
4165
+ if (options.required) {
4166
+ throw new PreconditionFailedError("If-Match header is required.");
3987
4167
  }
3988
- bits += value.toString(2).padStart(5, "0");
4168
+ return;
3989
4169
  }
3990
- const bytes = [];
3991
- for (let index = 0;index + 8 <= bits.length; index += 8) {
3992
- bytes.push(Number.parseInt(bits.slice(index, index + 8), 2));
4170
+ if (!ifMatchSatisfied(request, etag)) {
4171
+ throw new PreconditionFailedError("Resource ETag does not match If-Match.");
3993
4172
  }
3994
- return Buffer.from(bytes);
3995
4173
  }
3996
- function generateTotp(secret, counter, digits = 6) {
3997
- const key = decodeBase32(secret);
3998
- const buffer = Buffer.alloc(8);
3999
- buffer.writeBigUInt64BE(BigInt(counter));
4000
- const digest = createHmac2("sha1", key).update(buffer).digest();
4001
- const lastByte = digest[digest.length - 1] ?? 0;
4002
- const offset = lastByte & 15;
4003
- const b0 = digest[offset] ?? 0;
4004
- const b1 = digest[offset + 1] ?? 0;
4005
- const b2 = digest[offset + 2] ?? 0;
4006
- const b3 = digest[offset + 3] ?? 0;
4007
- const code = (b0 & 127) << 24 | (b1 & 255) << 16 | (b2 & 255) << 8 | b3 & 255;
4008
- return String(code % 10 ** digits).padStart(digits, "0");
4174
+ function applyEtagHeaders(headers, etag) {
4175
+ const next = new Headers(headers);
4176
+ next.set("ETag", etag);
4177
+ next.set("Cache-Control", "private, must-revalidate");
4178
+ next.append("Vary", "Authorization");
4179
+ next.append("Vary", "X-Tenant-Id");
4180
+ return next;
4009
4181
  }
4010
- function verifyTotp(secret, token, window = 1) {
4011
- const normalized = token.trim();
4012
- if (!/^\d{6}$/u.test(normalized)) {
4013
- return false;
4182
+ function notModifiedResponse(etag) {
4183
+ return new Response(null, {
4184
+ status: 304,
4185
+ headers: applyEtagHeaders(new Headers, etag)
4186
+ });
4187
+ }
4188
+ function applyConditionalGet(request, response, etag) {
4189
+ if (!isEtagEnabled()) {
4190
+ return response;
4014
4191
  }
4015
- const timestep = Math.floor(Date.now() / 30000);
4016
- for (let offset = -window;offset <= window; offset += 1) {
4017
- if (generateTotp(secret, timestep + offset) === normalized) {
4018
- return true;
4019
- }
4192
+ if (ifNoneMatchSatisfied(request, etag)) {
4193
+ return notModifiedResponse(etag);
4020
4194
  }
4021
- return false;
4195
+ const headers = applyEtagHeaders(new Headers(response.headers), etag);
4196
+ return new Response(response.body, {
4197
+ status: response.status,
4198
+ statusText: response.statusText,
4199
+ headers
4200
+ });
4022
4201
  }
4023
4202
 
4024
- // ../../src/domain/abilities.ts
4025
- var MEMBER_ABILITIES = [
4026
- "organizations:read",
4027
- "projects:read",
4028
- "projects:create",
4029
- "tasks:read",
4030
- "tasks:create",
4031
- "comments:read",
4032
- "comments:create",
4033
- "attachments:read",
4034
- "attachments:create",
4035
- "auth:tokens:read",
4036
- "auth:tokens:write"
4037
- ];
4038
- var ADMIN_ABILITIES = [
4039
- ...MEMBER_ABILITIES,
4040
- "organizations:create",
4041
- "organizations:update",
4042
- "organizations:delete",
4043
- "projects:update",
4044
- "projects:delete",
4045
- "tasks:update",
4046
- "tasks:delete",
4047
- "comments:update",
4048
- "comments:delete",
4049
- "attachments:delete",
4050
- "webhooks:read",
4051
- "webhooks:write",
4052
- "audit:read"
4053
- ];
4054
- var PLATFORM_ADMIN_ABILITIES = ["*"];
4055
- function resolveAbilitiesForRole(role) {
4056
- if (role === "admin") {
4057
- return [...PLATFORM_ADMIN_ABILITIES];
4203
+ // ../../src/core/http/conditionalResponse.ts
4204
+ function jsonResponse(data, init = {}) {
4205
+ return Response.json(data, {
4206
+ status: init.status ?? 200,
4207
+ headers: init.headers
4208
+ });
4209
+ }
4210
+ function conditionalJsonResponse(request, data, init = {}) {
4211
+ if (!request || !isEtagEnabled()) {
4212
+ return jsonResponse(data, init);
4058
4213
  }
4059
- return [...MEMBER_ABILITIES];
4214
+ const etag = computeEtagFromJson(data);
4215
+ if (ifNoneMatchSatisfied(request, etag)) {
4216
+ return notModifiedResponse(etag);
4217
+ }
4218
+ const response = jsonResponse(data, init);
4219
+ const headers = new Headers(response.headers);
4220
+ headers.set("ETag", etag);
4221
+ headers.set("Cache-Control", "private, must-revalidate");
4222
+ headers.append("Vary", "Authorization");
4223
+ headers.append("Vary", "X-Tenant-Id");
4224
+ return new Response(response.body, {
4225
+ status: response.status,
4226
+ statusText: response.statusText,
4227
+ headers
4228
+ });
4060
4229
  }
4061
-
4062
- // ../../src/modules/user/authService.ts
4063
- class AuthService {
4064
- users;
4065
- tokens;
4066
- oauthIdentities;
4067
- oauthProviders = new Map;
4068
- constructor(users, tokens, oauthIdentities) {
4069
- this.users = users;
4070
- this.tokens = tokens;
4071
- this.oauthIdentities = oauthIdentities;
4230
+ // ../../src/core/http/cookies.ts
4231
+ function readRequestCookie(request, name) {
4232
+ const cookies = request.cookies;
4233
+ if (cookies && typeof cookies.get === "function") {
4234
+ const value = cookies.get(name);
4235
+ if (value) {
4236
+ return value;
4237
+ }
4072
4238
  }
4073
- registerOAuthProvider(provider) {
4074
- this.oauthProviders.set(provider.name, provider);
4239
+ const header = request.headers.get("cookie");
4240
+ if (!header) {
4241
+ return null;
4075
4242
  }
4076
- getOAuthProvider(name) {
4077
- return this.oauthProviders.get(name);
4243
+ for (const part of header.split(";")) {
4244
+ const idx = part.indexOf("=");
4245
+ if (idx === -1)
4246
+ continue;
4247
+ const cookieName = part.slice(0, idx).trim();
4248
+ if (cookieName !== name)
4249
+ continue;
4250
+ return decodeURIComponent(part.slice(idx + 1).trim());
4078
4251
  }
4079
- async loginWithPassword(email, password, options = {}) {
4080
- const user = await this.users.findByEmail(email);
4081
- if (!user?.password_hash) {
4082
- logSecurityEvent("auth_login_failed", { reason: "unknown_user", email });
4083
- throw new UnauthorizedError("Invalid credentials.");
4084
- }
4085
- const valid = await verifyPassword(password, user.password_hash);
4086
- if (!valid) {
4087
- logSecurityEvent("auth_login_failed", { reason: "invalid_password", user_id: user.id });
4088
- throw new UnauthorizedError("Invalid credentials.");
4089
- }
4090
- if (isFeatureEnabled("emailVerification") && !user.email_verified_at) {
4091
- logSecurityEvent("auth_login_blocked", { reason: "email_unverified", user_id: user.id });
4092
- throw new UnauthorizedError("Email address is not verified.");
4252
+ return null;
4253
+ }
4254
+ function readBunRequestCookie(request, name) {
4255
+ return request.cookies.get(name) ?? readRequestCookie(request, name);
4256
+ }
4257
+ // ../../src/config/cors.ts
4258
+ var corsConfig = {
4259
+ allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim()).filter(Boolean),
4260
+ allowedMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
4261
+ allowedHeaders: [
4262
+ "Authorization",
4263
+ "Content-Type",
4264
+ "X-Request-Id",
4265
+ "X-Tenant-Id",
4266
+ "X-Authenticated-User-Id",
4267
+ "X-Authenticated-User-Role",
4268
+ "If-Match",
4269
+ "If-None-Match"
4270
+ ],
4271
+ maxAgeSeconds: 86400
4272
+ };
4273
+
4274
+ // ../../src/core/http/corsMiddleware.ts
4275
+ function createCorsMiddleware() {
4276
+ return async (request, next) => {
4277
+ if (request.method === "OPTIONS") {
4278
+ return new Response(null, {
4279
+ status: 204,
4280
+ headers: buildCorsHeaders(request)
4281
+ });
4093
4282
  }
4094
- if (isFeatureEnabled("mfa") && user.mfa_enabled) {
4095
- const mfaSecret = revealMfaSecret(user.mfa_secret);
4096
- if (!mfaSecret || !options.mfaCode || !verifyTotp(mfaSecret, options.mfaCode)) {
4097
- logSecurityEvent("auth_login_failed", { reason: "invalid_mfa", user_id: user.id });
4098
- throw new UnauthorizedError("Invalid MFA code.");
4099
- }
4283
+ const response = await next();
4284
+ const headers = new Headers(response.headers);
4285
+ for (const [key, value] of buildCorsHeaders(request)) {
4286
+ headers.set(key, value);
4100
4287
  }
4101
- logSecurityEvent("auth_login_success", { user_id: user.id, method: "password" });
4102
- return await this.tokens.createToken(user.id, {
4103
- name: "password-login",
4104
- abilities: resolveAbilitiesForRole(user.role),
4105
- expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
4288
+ return new Response(response.body, {
4289
+ status: response.status,
4290
+ statusText: response.statusText,
4291
+ headers
4106
4292
  });
4293
+ };
4294
+ }
4295
+ function buildCorsHeaders(request) {
4296
+ const headers = new Headers;
4297
+ const origin = request.headers.get("origin");
4298
+ const allowedOrigins = corsConfig.allowedOrigins;
4299
+ const allowOrigin = allowedOrigins.includes("*") || origin && allowedOrigins.includes(origin) ? origin ?? "*" : allowedOrigins[0] ?? "*";
4300
+ headers.set("Access-Control-Allow-Origin", allowOrigin);
4301
+ headers.set("Access-Control-Allow-Methods", corsConfig.allowedMethods.join(", "));
4302
+ headers.set("Access-Control-Allow-Headers", corsConfig.allowedHeaders.join(", "));
4303
+ headers.set("Access-Control-Max-Age", String(corsConfig.maxAgeSeconds));
4304
+ headers.set("Vary", "Origin");
4305
+ return headers;
4306
+ }
4307
+ // ../../src/core/http/csrfToken.ts
4308
+ import { timingSafeEqual as timingSafeEqual2 } from "crypto";
4309
+ var CSRF_COOKIE = "workhub_csrf";
4310
+ var CSRF_TTL_MS = 60 * 60 * 1000;
4311
+ function resolveCsrfSecret() {
4312
+ return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
4313
+ }
4314
+ function csrfVerifyOptions() {
4315
+ return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
4316
+ }
4317
+ function tokensMatch(left, right) {
4318
+ const leftBuffer = Buffer.from(left);
4319
+ const rightBuffer = Buffer.from(right);
4320
+ if (leftBuffer.length !== rightBuffer.length) {
4321
+ return false;
4322
+ }
4323
+ return timingSafeEqual2(leftBuffer, rightBuffer);
4324
+ }
4325
+ function createCsrfTokenCookie() {
4326
+ const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
4327
+ return {
4328
+ token,
4329
+ cookie: `${CSRF_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}`
4330
+ };
4331
+ }
4332
+ function resolveCsrfToken(request) {
4333
+ const cookieValue = readRequestCookie(request, CSRF_COOKIE);
4334
+ if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
4335
+ return { token: cookieValue };
4107
4336
  }
4108
- async loginWithOAuth(providerName, code) {
4109
- const provider = this.oauthProviders.get(providerName);
4110
- if (!provider) {
4111
- throw new UnauthorizedError("Unsupported OAuth provider.");
4112
- }
4113
- const profile = await provider.exchangeCode(code);
4114
- const user = await this.findOrCreateOAuthUser(providerName, profile);
4115
- logSecurityEvent("auth_login_success", { user_id: user.id, method: `oauth:${providerName}` });
4116
- return await this.tokens.createToken(user.id, {
4117
- name: `${providerName}-oauth`,
4118
- abilities: resolveAbilitiesForRole(user.role),
4119
- expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
4120
- });
4337
+ return createCsrfTokenCookie();
4338
+ }
4339
+ function readSubmittedCsrfToken(request) {
4340
+ const headerToken = request.headers.get("x-csrf-token")?.trim();
4341
+ if (headerToken) {
4342
+ return headerToken;
4121
4343
  }
4122
- buildOAuthAuthorizationUrl(providerName, state) {
4123
- const provider = this.oauthProviders.get(providerName);
4124
- if (!provider) {
4125
- throw new UnauthorizedError("Unsupported OAuth provider.");
4126
- }
4127
- return provider.getAuthorizationUrl(state);
4344
+ return null;
4345
+ }
4346
+ async function readSubmittedCsrfTokenFromBody(request) {
4347
+ const headerToken = readSubmittedCsrfToken(request);
4348
+ if (headerToken) {
4349
+ return headerToken;
4128
4350
  }
4129
- async findOrCreateOAuthUser(providerName, profile) {
4130
- const existingIdentity = await this.oauthIdentities.findByProviderUser(providerName, profile.providerUserId);
4131
- if (existingIdentity) {
4132
- return await this.users.findByIdOrThrow(existingIdentity.user_id);
4351
+ const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
4352
+ if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
4353
+ const formData = await request.clone().formData();
4354
+ const field = formData.get("_token");
4355
+ if (typeof field === "string" && field.trim().length > 0) {
4356
+ return field.trim();
4357
+ }
4358
+ const legacyField = formData.get("_csrf");
4359
+ if (typeof legacyField === "string" && legacyField.trim().length > 0) {
4360
+ return legacyField.trim();
4133
4361
  }
4134
- const existingUser = await this.users.findByEmail(profile.email);
4135
- const user = existingUser ?? await this.users.create({
4136
- name: profile.name,
4137
- email: profile.email,
4138
- role: "member",
4139
- tenant_id: currentTenantId(),
4140
- email_verified_at: new Date,
4141
- created_at: new Date,
4142
- updated_at: new Date
4143
- });
4144
- await this.oauthIdentities.create({
4145
- user_id: user.id,
4146
- provider: providerName,
4147
- provider_user_id: profile.providerUserId,
4148
- email: profile.email,
4149
- created_at: new Date
4150
- });
4151
- return user;
4152
4362
  }
4363
+ return null;
4153
4364
  }
4154
-
4155
- // ../../src/modules/user/notificationTable.ts
4156
- var notificationTable = defineTable({
4157
- name: "notification",
4158
- primaryKey: "id",
4159
- columns: ["id", "user_id", "tenant_id", "type", "title", "body", "data", "read_at", "created_at"],
4160
- defaultOrderBy: { column: "created_at", direction: "DESC" }
4161
- });
4162
-
4163
- // ../../src/modules/user/oauthIdentityRepository.ts
4164
- var oauthIdentityTable = defineTable({
4165
- name: "oauth_identity",
4166
- primaryKey: "id",
4167
- columns: ["id", "user_id", "provider", "provider_user_id", "email", "created_at"]
4168
- });
4169
-
4170
- // ../../src/modules/user/table.ts
4171
- var userTable = defineTable({
4172
- name: "users",
4173
- primaryKey: "id",
4174
- columns: [
4175
- "id",
4176
- "name",
4177
- "email",
4178
- "email_lookup",
4179
- "role",
4180
- "tenant_id",
4181
- "password_hash",
4182
- "email_verified_at",
4183
- "mfa_secret",
4184
- "mfa_enabled",
4185
- "created_at",
4186
- "updated_at"
4187
- ],
4188
- defaultOrderBy: { column: "id", direction: "ASC" }
4189
- });
4190
-
4191
- // ../../src/core/auth/tokenHash.ts
4192
- import { createHash, createHmac as createHmac3 } from "crypto";
4193
- function resolveTokenPepper() {
4194
- return process.env.TOKEN_HASH_PEPPER?.trim() ?? "workhub-dev-token-pepper";
4365
+ function verifyCsrfToken(request, submittedToken) {
4366
+ if (!submittedToken) {
4367
+ return false;
4368
+ }
4369
+ const cookieValue = readRequestCookie(request, CSRF_COOKIE);
4370
+ if (!cookieValue) {
4371
+ return false;
4372
+ }
4373
+ if (!tokensMatch(submittedToken, cookieValue)) {
4374
+ return false;
4375
+ }
4376
+ return Bun.CSRF.verify(submittedToken, csrfVerifyOptions());
4195
4377
  }
4196
- function hashApiToken(token) {
4197
- const pepper = resolveTokenPepper();
4198
- if (pepper && pepper !== "workhub-dev-token-pepper") {
4199
- return createHmac3("sha256", pepper).update(token).digest("hex");
4378
+ function resolveCsrfTokenForRequest(request) {
4379
+ const metaToken = currentRequestMeta().csrfToken;
4380
+ if (metaToken) {
4381
+ return metaToken;
4200
4382
  }
4201
- return createHash("sha256").update(token).digest("hex");
4383
+ return resolveCsrfToken(request).token;
4202
4384
  }
4203
4385
 
4204
- // ../../src/modules/user/provider.ts
4205
- var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
4206
-
4386
+ // ../../src/core/http/csrfMiddleware.ts
4387
+ var MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
4388
+ function appendSetCookie(response, cookie) {
4389
+ const headers = new Headers(response.headers);
4390
+ headers.append("set-cookie", cookie);
4391
+ return new Response(response.body, {
4392
+ status: response.status,
4393
+ statusText: response.statusText,
4394
+ headers
4395
+ });
4396
+ }
4397
+ function createCsrfMiddleware() {
4398
+ return async (request, next) => {
4399
+ const method = request.method.toUpperCase();
4400
+ if (!MUTATING_METHODS.has(method)) {
4401
+ const csrf = resolveCsrfToken(request);
4402
+ const meta = currentRequestMeta();
4403
+ meta.csrfToken = csrf.token;
4404
+ const response = await next();
4405
+ if (!csrf.cookie) {
4406
+ return response;
4407
+ }
4408
+ return appendSetCookie(response, csrf.cookie);
4409
+ }
4410
+ const submitted = await readSubmittedCsrfTokenFromBody(request);
4411
+ if (!verifyCsrfToken(request, submitted)) {
4412
+ throw new ForbiddenError("Invalid or missing CSRF token.");
4413
+ }
4414
+ return await next();
4415
+ };
4416
+ }
4417
+ // ../../src/core/http/csrfProtection.ts
4418
+ var DEFAULT_CSRF_TTL_MS = 60 * 60 * 1000;
4419
+ function createCsrfProtection(secret, options = {}) {
4420
+ const expiresIn = options.expiresIn ?? DEFAULT_CSRF_TTL_MS;
4421
+ const maxAge = options.maxAge ?? expiresIn;
4422
+ return {
4423
+ generate(_sessionKey) {
4424
+ return Bun.CSRF.generate(secret, { expiresIn });
4425
+ },
4426
+ verify(token, _sessionKey) {
4427
+ if (!token) {
4428
+ return false;
4429
+ }
4430
+ return Bun.CSRF.verify(token, { secret, maxAge });
4431
+ },
4432
+ secret
4433
+ };
4434
+ }
4207
4435
  // ../../src/core/http/flashSession.ts
4208
- import { createHmac as createHmac4, timingSafeEqual as timingSafeEqual2 } from "crypto";
4436
+ import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual3 } from "crypto";
4209
4437
  var FLASH_COOKIE = "workhub_flash";
4210
4438
  var FLASH_TTL_MS = 60 * 1000;
4211
4439
  function resolveFlashSecret() {
4212
4440
  return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
4213
4441
  }
4214
4442
  function signFlashPayload(payload, issuedAt) {
4215
- const signature = createHmac4("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
4443
+ const signature = createHmac3("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
4216
4444
  return `${payload}.${issuedAt}.${signature}`;
4217
4445
  }
4218
4446
  function readFlashCookie(request) {
@@ -4252,7 +4480,7 @@ function parseFlashCookie(cookieValue) {
4252
4480
  if (expectedBuffer.length !== actualBuffer.length) {
4253
4481
  return null;
4254
4482
  }
4255
- if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
4483
+ if (!timingSafeEqual3(expectedBuffer, actualBuffer)) {
4256
4484
  return null;
4257
4485
  }
4258
4486
  try {
@@ -4268,41 +4496,146 @@ function parseFlashCookie(cookieValue) {
4268
4496
  return null;
4269
4497
  }
4270
4498
  }
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`;
4499
+ function clearFlashCookie() {
4500
+ return `${FLASH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
4501
+ }
4502
+ function pullFlash(request) {
4503
+ const cookieValue = readFlashCookie(request);
4504
+ if (!cookieValue) {
4505
+ return null;
4506
+ }
4507
+ return parseFlashCookie(cookieValue);
4508
+ }
4509
+ function withFlashClear(response) {
4510
+ const headers = new Headers(response.headers);
4511
+ headers.append("set-cookie", clearFlashCookie());
4512
+ return new Response(response.body, {
4513
+ status: response.status,
4514
+ statusText: response.statusText,
4515
+ headers
4516
+ });
4517
+ }
4518
+
4519
+ // ../../src/core/http/flashMiddleware.ts
4520
+ function createFlashMiddleware() {
4521
+ return async (request, next) => {
4522
+ const flash = pullFlash(request);
4523
+ const meta = currentRequestMeta();
4524
+ return await runWithRequestMeta({ ...meta, request, flash }, async () => {
4525
+ const response = await next();
4526
+ if (flash) {
4527
+ return withFlashClear(response);
4528
+ }
4529
+ return response;
4530
+ });
4531
+ };
4532
+ }
4533
+ // ../../src/core/http/validation.ts
4534
+ function buildRequestCacheKey(fallbackPath, request) {
4535
+ if (!request) {
4536
+ return fallbackPath;
4537
+ }
4538
+ const url = new URL(request.url);
4539
+ const user = currentAuthUser();
4540
+ const authScope = user ? `u:${user.id}` : "guest";
4541
+ const tenantScope = `t:${currentTenantId()}`;
4542
+ return `${authScope}|${tenantScope}|${url.pathname}${url.search}`;
4543
+ }
4544
+ function getQueryParams(request) {
4545
+ if (!request) {
4546
+ return new URLSearchParams;
4547
+ }
4548
+ return new URL(request.url).searchParams;
4549
+ }
4550
+ async function parseJsonBody(request, validator) {
4551
+ let payload;
4552
+ try {
4553
+ payload = await request.json();
4554
+ } catch {
4555
+ throw new BadRequestError("Request body must be valid JSON.");
4556
+ }
4557
+ return validator(payload);
4558
+ }
4559
+ function parsePositiveIntParam(value, name = "id") {
4560
+ const parsed = Number.parseInt(value, 10);
4561
+ if (!Number.isInteger(parsed) || parsed <= 0) {
4562
+ throw new BadRequestError(`Invalid ${name}. Expected a positive integer.`);
4563
+ }
4564
+ return parsed;
4565
+ }
4566
+
4567
+ // ../../src/core/http/formRequest.ts
4568
+ class FormRequest {
4569
+ authorize(_request) {
4570
+ return true;
4571
+ }
4572
+ async validate(request) {
4573
+ if (!await this.authorize(request)) {
4574
+ throw new ForbiddenError;
4575
+ }
4576
+ return await parseJsonBody(request, (payload) => this.parse(payload));
4577
+ }
4578
+ }
4579
+ // ../../src/config/frontend.ts
4580
+ function readFrontendMode() {
4581
+ const mode = (process.env.FRONTEND_MODE ?? "api").trim();
4582
+ if (mode === "server-htmx") {
4583
+ return "server-htmx";
4584
+ }
4585
+ if (mode === "spa-react") {
4586
+ return "spa-react";
4587
+ }
4588
+ return "api";
4589
+ }
4590
+ function isViewsEnabled() {
4591
+ return readFrontendMode() === "server-htmx";
4279
4592
  }
4280
- function pullFlash(request) {
4281
- const cookieValue = readFlashCookie(request);
4282
- if (!cookieValue) {
4283
- return null;
4593
+
4594
+ // ../../src/core/view/etaViewEngine.ts
4595
+ import { join as join4 } from "path";
4596
+ import { Eta } from "eta";
4597
+ var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
4598
+ var DEFAULT_LAYOUT = "layouts/app.eta";
4599
+
4600
+ class EtaViewEngine {
4601
+ eta;
4602
+ resolveLayoutData;
4603
+ constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
4604
+ this.eta = new Eta({
4605
+ views: viewsDirectory,
4606
+ autoTrim: false
4607
+ });
4608
+ this.resolveLayoutData = resolveLayoutData;
4609
+ }
4610
+ async render(name, data = {}, options = {}) {
4611
+ const template = name.endsWith(".eta") ? name : `${name}.eta`;
4612
+ const layoutData = this.resolveLayoutData ? await this.resolveLayoutData() : {};
4613
+ const mergedData = { ...layoutData, ...data };
4614
+ const body = await this.eta.renderAsync(template, mergedData);
4615
+ const layout = options.layout ?? DEFAULT_LAYOUT;
4616
+ if (layout === false) {
4617
+ return body;
4618
+ }
4619
+ const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
4620
+ return await this.eta.renderAsync(layoutTemplate, {
4621
+ ...mergedData,
4622
+ body
4623
+ });
4284
4624
  }
4285
- return parseFlashCookie(cookieValue);
4286
4625
  }
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
4626
+ // ../../src/core/view/htmlResponse.ts
4627
+ function htmlResponse(html, init = {}) {
4628
+ return new Response(html, {
4629
+ status: init.status ?? 200,
4630
+ statusText: init.statusText,
4631
+ headers: {
4632
+ "Content-Type": "text/html; charset=utf-8"
4633
+ }
4294
4634
  });
4295
4635
  }
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
- });
4636
+ function isHtmxRequest(request) {
4637
+ return request.headers.get("HX-Request") === "true";
4304
4638
  }
4305
-
4306
4639
  // ../../src/core/view/webLayoutData.ts
4307
4640
  async function resolveWebLayoutData(container, request) {
4308
4641
  const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
@@ -4423,7 +4756,7 @@ function createAuthMiddleware(auth2) {
4423
4756
  // ../../src/core/http/authorizeMiddleware.ts
4424
4757
  function createAuthorizeMiddleware(gate, auth2, resource, action) {
4425
4758
  return async (request, next) => {
4426
- const user = await auth2.resolve(request);
4759
+ const user = currentAuthUser() ?? await auth2.resolve(request);
4427
4760
  if (!gate.allows(resource, action, user)) {
4428
4761
  const error = new ForbiddenError;
4429
4762
  return Response.json({ error: error.message }, { status: error.status });
@@ -4431,33 +4764,6 @@ function createAuthorizeMiddleware(gate, auth2, resource, action) {
4431
4764
  return await next();
4432
4765
  };
4433
4766
  }
4434
- // ../../src/core/http/conditionalResponse.ts
4435
- function jsonResponse(data, init = {}) {
4436
- return Response.json(data, {
4437
- status: init.status ?? 200,
4438
- headers: init.headers
4439
- });
4440
- }
4441
- function conditionalJsonResponse(request, data, init = {}) {
4442
- if (!request || !isEtagEnabled()) {
4443
- return jsonResponse(data, init);
4444
- }
4445
- const etag = computeEtagFromJson(data);
4446
- if (ifNoneMatchSatisfied(request, etag)) {
4447
- return notModifiedResponse(etag);
4448
- }
4449
- const response = jsonResponse(data, init);
4450
- const headers = new Headers(response.headers);
4451
- headers.set("ETag", etag);
4452
- headers.set("Cache-Control", "private, must-revalidate");
4453
- headers.append("Vary", "Authorization");
4454
- headers.append("Vary", "X-Tenant-Id");
4455
- return new Response(response.body, {
4456
- status: response.status,
4457
- statusText: response.statusText,
4458
- headers
4459
- });
4460
- }
4461
4767
  // ../../src/core/http/middleware.ts
4462
4768
  function isRouteHandler(value) {
4463
4769
  return typeof value === "function";
@@ -4598,7 +4904,8 @@ function securedBindRouteModel(param, resolver, authorization, handler) {
4598
4904
  const model = await resolver(id, request);
4599
4905
  const gate = resolveApplicationPolicyGate();
4600
4906
  const auth2 = resolveApplicationAuth();
4601
- gate.authorize(authorization.resource, authorization.action, await auth2.resolve(request), model);
4907
+ const user = currentAuthUser() ?? await auth2.resolve(request);
4908
+ gate.authorize(authorization.resource, authorization.action, user, model);
4602
4909
  if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
4603
4910
  assertIfMatch(request, etagFromResource(model), {
4604
4911
  required: authorization.requireIfMatch ?? true
@@ -4620,7 +4927,8 @@ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
4620
4927
  const model = await resolver(key, request);
4621
4928
  const gate = resolveApplicationPolicyGate();
4622
4929
  const auth2 = resolveApplicationAuth();
4623
- gate.authorize(authorization.resource, authorization.action, await auth2.resolve(request), model);
4930
+ const user = currentAuthUser() ?? await auth2.resolve(request);
4931
+ gate.authorize(authorization.resource, authorization.action, user, model);
4624
4932
  if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
4625
4933
  assertIfMatch(request, etagFromResource(model), {
4626
4934
  required: authorization.requireIfMatch ?? true
@@ -4706,10 +5014,6 @@ async function parseMultipartUpload(request, fieldName = "file") {
4706
5014
  contents: new Uint8Array(await value.arrayBuffer())
4707
5015
  };
4708
5016
  }
4709
- // ../../src/core/http/route.ts
4710
- function getRouteParams(request) {
4711
- return request.params;
4712
- }
4713
5017
 
4714
5018
  // ../../src/core/http/index.ts
4715
5019
  function jsonResponse2(data, init = {}) {
@@ -4898,6 +5202,38 @@ function createMetricsMiddleware() {
4898
5202
  return response;
4899
5203
  };
4900
5204
  }
5205
+ // ../../src/core/http/requireAbilityMiddleware.ts
5206
+ function createRequireAbilityMiddleware(abilityChecker) {
5207
+ return (ability) => {
5208
+ return async (_request, next) => {
5209
+ const user = currentAuthUser();
5210
+ try {
5211
+ abilityChecker.requireAbility(user, ability);
5212
+ } catch (error) {
5213
+ if (error instanceof ForbiddenError) {
5214
+ return Response.json({ error: error.message }, { status: error.status });
5215
+ }
5216
+ throw error;
5217
+ }
5218
+ return await next();
5219
+ };
5220
+ };
5221
+ }
5222
+ // ../../src/core/http/requireGlobalAdminMiddleware.ts
5223
+ function createRequireGlobalAdminMiddleware() {
5224
+ return async (_request, next) => {
5225
+ const user = currentAuthUser();
5226
+ if (!isGlobalAdmin(user)) {
5227
+ logSecurityEvent("privilege_escalation_blocked", {
5228
+ required_role: "platform_admin",
5229
+ path: new URL(_request.url).pathname
5230
+ });
5231
+ const error = new ForbiddenError("Platform admin access required.");
5232
+ return Response.json({ error: error.message }, { status: error.status });
5233
+ }
5234
+ return await next();
5235
+ };
5236
+ }
4901
5237
  // ../../src/core/http/requireWebAuthMiddleware.ts
4902
5238
  function createRequireWebAuthMiddleware(auth2) {
4903
5239
  return async (request, next) => {
@@ -4912,6 +5248,25 @@ function createRequireWebAuthMiddleware(auth2) {
4912
5248
  return Response.redirect(`/login?redirect=${redirectTarget}`, 302);
4913
5249
  };
4914
5250
  }
5251
+ // ../../src/core/http/scimThrottleMiddleware.ts
5252
+ var {RedisClient: RedisClient2 } = globalThis.Bun;
5253
+ function createScimThrottleMiddleware(options) {
5254
+ const client = options.redisUrl ? new RedisClient2(options.redisUrl) : null;
5255
+ return async (request, next) => {
5256
+ const identity = request.headers.get("authorization")?.slice("Bearer ".length, "Bearer ".length + 16) ?? request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
5257
+ const key = `workhub:scim-throttle:${identity}`;
5258
+ if (client) {
5259
+ const attempts = Number(await client.incr(key));
5260
+ if (attempts === 1) {
5261
+ await client.expire(key, options.decaySeconds);
5262
+ }
5263
+ if (attempts > options.maxAttempts) {
5264
+ return Response.json({ error: "Too many SCIM requests." }, { status: 429, headers: { "retry-after": String(options.decaySeconds) } });
5265
+ }
5266
+ }
5267
+ return await next();
5268
+ };
5269
+ }
4915
5270
  // ../../src/config/app.ts
4916
5271
  var appConfig = {
4917
5272
  name: "WorkHub",
@@ -4986,7 +5341,7 @@ function createSecurityHeadersMiddleware() {
4986
5341
  };
4987
5342
  }
4988
5343
  // ../../src/core/http/throttleMiddleware.ts
4989
- var {RedisClient: RedisClient2 } = globalThis.Bun;
5344
+ var {RedisClient: RedisClient3 } = globalThis.Bun;
4990
5345
  function resolveThrottleIdentity(request) {
4991
5346
  const user = currentAuthUser();
4992
5347
  if (user?.tokenId !== undefined) {
@@ -4998,7 +5353,7 @@ function resolveThrottleIdentity(request) {
4998
5353
  return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
4999
5354
  }
5000
5355
  function createThrottleMiddleware(options) {
5001
- const client = new RedisClient2(options.redisUrl);
5356
+ const client = new RedisClient3(options.redisUrl);
5002
5357
  const prefix = options.keyPrefix ?? "workhub:throttle:";
5003
5358
  return async (request, next) => {
5004
5359
  const identity = resolveThrottleIdentity(request);
@@ -5099,10 +5454,28 @@ function installGracefulShutdownSignals(signals = ["SIGINT", "SIGTERM"]) {
5099
5454
  });
5100
5455
  }
5101
5456
  }
5102
- function resetGracefulShutdownForTests() {
5103
- shutdownHandlers.clear();
5104
- shutdownInstalled = false;
5105
- shuttingDown = false;
5457
+ // ../../src/core/logging/requestLoggingMiddleware.ts
5458
+ function createRequestLoggingMiddleware() {
5459
+ return async (request, next) => {
5460
+ return await runWithRequestMeta({
5461
+ ipAddress: request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip"),
5462
+ userAgent: request.headers.get("user-agent"),
5463
+ request
5464
+ }, async () => {
5465
+ const startedAt = performance.now();
5466
+ const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
5467
+ const response = await next();
5468
+ const durationMs = Math.round(performance.now() - startedAt);
5469
+ appLogger.info("HTTP request completed", {
5470
+ requestId,
5471
+ method: request.method,
5472
+ path: new URL(request.url).pathname,
5473
+ status: response.status,
5474
+ durationMs
5475
+ });
5476
+ return response;
5477
+ });
5478
+ };
5106
5479
  }
5107
5480
  // ../../src/core/mail/markdownMail.ts
5108
5481
  function escapeHtml(value) {
@@ -5399,7 +5772,7 @@ async function runQueueJob(envelope, failedJobs) {
5399
5772
  }
5400
5773
 
5401
5774
  // ../../src/core/queue/redisQueue.ts
5402
- var {RedisClient: RedisClient3 } = globalThis.Bun;
5775
+ var {RedisClient: RedisClient4 } = globalThis.Bun;
5403
5776
  var QUEUE_LIST_KEY = "workhub:queue:default";
5404
5777
  var QUEUE_HIGH_KEY = "workhub:queue:high";
5405
5778
  var QUEUE_LOW_KEY = "workhub:queue:low";
@@ -5449,7 +5822,7 @@ function parseQueueJobEnvelope(rawPayload) {
5449
5822
  class RedisQueue {
5450
5823
  client;
5451
5824
  constructor(redisUrl) {
5452
- this.client = new RedisClient3(redisUrl);
5825
+ this.client = new RedisClient4(redisUrl);
5453
5826
  }
5454
5827
  async dispatch(job, payload) {
5455
5828
  const name = jobRegistry.resolveName(job);
@@ -5475,7 +5848,7 @@ class QueueWorker {
5475
5848
  constructor(redisUrl, failedJobs, timeoutSeconds = 5) {
5476
5849
  this.failedJobs = failedJobs;
5477
5850
  this.timeoutSeconds = timeoutSeconds;
5478
- this.client = new RedisClient3(redisUrl);
5851
+ this.client = new RedisClient4(redisUrl);
5479
5852
  }
5480
5853
  requestStop() {
5481
5854
  this.stopping = true;
@@ -5572,6 +5945,68 @@ function createProductionQueue(driver, options = {}) {
5572
5945
  function createQueueWorker(redisUrl, failedJobs = createFailedJobService()) {
5573
5946
  return new QueueWorker(redisUrl, failedJobs);
5574
5947
  }
5948
+ // ../../src/core/queue/queueMetrics.ts
5949
+ var {RedisClient: RedisClient5 } = globalThis.Bun;
5950
+
5951
+ // ../../src/core/security/safeUrl.ts
5952
+ var BLOCKED_HOSTNAMES = new Set([
5953
+ "localhost",
5954
+ "127.0.0.1",
5955
+ "0.0.0.0",
5956
+ "::1",
5957
+ "metadata.google.internal"
5958
+ ]);
5959
+
5960
+ // ../../src/core/queue/queueMetrics.ts
5961
+ async function readRedisQueueDepth(redisUrl) {
5962
+ const client = new RedisClient5(redisUrl);
5963
+ const [high, defaultQueue, low] = await Promise.all([
5964
+ client.llen(QUEUE_HIGH_KEY),
5965
+ client.llen(QUEUE_LIST_KEY),
5966
+ client.llen(QUEUE_LOW_KEY)
5967
+ ]);
5968
+ return {
5969
+ high: Number(high ?? 0),
5970
+ default: Number(defaultQueue ?? 0),
5971
+ low: Number(low ?? 0),
5972
+ total: Number(high ?? 0) + Number(defaultQueue ?? 0) + Number(low ?? 0)
5973
+ };
5974
+ }
5975
+ async function collectQueueMetrics() {
5976
+ const driver = queueConfig.driver;
5977
+ const failedJobs = createFailedJobService();
5978
+ const failedCount = (await failedJobs.listRecent(1000)).length;
5979
+ if (driver !== "redis") {
5980
+ return {
5981
+ driver,
5982
+ pending: {
5983
+ high: 0,
5984
+ default: 0,
5985
+ low: 0,
5986
+ total: 0
5987
+ },
5988
+ failedCount
5989
+ };
5990
+ }
5991
+ const redisUrl = process.env.REDIS_URL;
5992
+ if (!redisUrl) {
5993
+ return {
5994
+ driver,
5995
+ pending: {
5996
+ high: 0,
5997
+ default: 0,
5998
+ low: 0,
5999
+ total: 0
6000
+ },
6001
+ failedCount
6002
+ };
6003
+ }
6004
+ return {
6005
+ driver,
6006
+ pending: await readRedisQueueDepth(redisUrl),
6007
+ failedCount
6008
+ };
6009
+ }
5575
6010
  // ../../src/core/scheduler/schedule.ts
5576
6011
  class Schedule {
5577
6012
  tasks = [];
@@ -5604,6 +6039,185 @@ async function runDueScheduledTasks(schedule = appSchedule, now = new Date) {
5604
6039
  }
5605
6040
  return due.length;
5606
6041
  }
6042
+ // ../../src/core/security/publicReads.ts
6043
+ function isPublicReadsEnabled() {
6044
+ return isFeatureEnabled("publicReads");
6045
+ }
6046
+ function guestCanViewResource() {
6047
+ return isPublicReadsEnabled();
6048
+ }
6049
+ // ../../src/core/tenant/tenantMiddleware.ts
6050
+ import { createHash } from "crypto";
6051
+
6052
+ // ../../src/core/tenant/databaseTenantContext.ts
6053
+ async function runWithMigrationBypass(callback) {
6054
+ await repositoryConnection`SELECT set_config('app.bypass_rls', 'true', false)`;
6055
+ try {
6056
+ return await callback();
6057
+ } finally {
6058
+ await repositoryConnection`SELECT set_config('app.bypass_rls', 'false', false)`;
6059
+ }
6060
+ }
6061
+
6062
+ // ../../src/core/tenant/tenantMiddleware.ts
6063
+ var DEFAULT_TENANT = {
6064
+ id: 1,
6065
+ slug: "default",
6066
+ plan: "enterprise",
6067
+ region: "eu"
6068
+ };
6069
+ async function resolveUserTenantId(userId) {
6070
+ return await runWithMigrationBypass(async () => {
6071
+ const rows = await repositoryConnection`
6072
+ SELECT tenant_id
6073
+ FROM users
6074
+ WHERE id = ${userId}
6075
+ LIMIT 1
6076
+ `;
6077
+ return rows[0]?.tenant_id ?? DEFAULT_TENANT.id;
6078
+ });
6079
+ }
6080
+ function auditChecksum(payload) {
6081
+ return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
6082
+ }
6083
+ async function resolveTenantForRequest(request) {
6084
+ const user = currentAuthUser();
6085
+ const headerValue = request.headers.get("x-tenant-id")?.trim();
6086
+ const parsedHeader = headerValue !== undefined && headerValue.length > 0 ? Number.parseInt(headerValue, 10) : Number.NaN;
6087
+ if (user) {
6088
+ const userId = typeof user.id === "number" ? user.id : Number.parseInt(String(user.id), 10);
6089
+ if (Number.isInteger(userId) && userId > 0) {
6090
+ const userTenantId = await resolveUserTenantId(userId);
6091
+ if (!isGlobalAdmin(user)) {
6092
+ if (Number.isInteger(parsedHeader) && parsedHeader > 0 && parsedHeader !== userTenantId) {
6093
+ throw new ForbiddenError("Tenant header does not match your account.");
6094
+ }
6095
+ return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
6096
+ }
6097
+ if (Number.isInteger(parsedHeader) && parsedHeader > 0) {
6098
+ return await resolveTenant(parsedHeader) ?? DEFAULT_TENANT;
6099
+ }
6100
+ return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
6101
+ }
6102
+ }
6103
+ const tenantId = Number.isInteger(parsedHeader) && parsedHeader > 0 ? parsedHeader : DEFAULT_TENANT.id;
6104
+ return await resolveTenant(tenantId) ?? DEFAULT_TENANT;
6105
+ }
6106
+ function createTenantMiddleware() {
6107
+ return async (request, next) => {
6108
+ const pathname = new URL(request.url).pathname;
6109
+ if (pathname.startsWith("/scim/")) {
6110
+ return await next();
6111
+ }
6112
+ try {
6113
+ const tenant = await resolveTenantForRequest(request);
6114
+ return await runWithTenantDatabase(tenant, async () => {
6115
+ const response = await next();
6116
+ const headers = new Headers(response.headers);
6117
+ headers.set("x-tenant-id", String(tenant.id));
6118
+ headers.set("x-tenant-region", tenant.region);
6119
+ return new Response(response.body, {
6120
+ status: response.status,
6121
+ statusText: response.statusText,
6122
+ headers
6123
+ });
6124
+ });
6125
+ } catch (error) {
6126
+ if (error instanceof HttpError) {
6127
+ return Response.json({ error: error.message }, { status: error.status });
6128
+ }
6129
+ throw error;
6130
+ }
6131
+ };
6132
+ }
6133
+ // ../../src/core/tracing/traceContext.ts
6134
+ import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
6135
+ var traceContextStorage = new AsyncLocalStorage6;
6136
+ function runWithTraceContext(context, callback) {
6137
+ return traceContextStorage.run(context, callback);
6138
+ }
6139
+ function currentTraceId() {
6140
+ return traceContextStorage.getStore()?.traceId ?? null;
6141
+ }
6142
+ // ../../src/core/tracing/otel.ts
6143
+ import { randomBytes as randomBytes2 } from "crypto";
6144
+ function randomHex(bytes) {
6145
+ return randomBytes2(bytes).toString("hex");
6146
+ }
6147
+ function createSpan(input) {
6148
+ const spanId = randomHex(8);
6149
+ return {
6150
+ traceId: input.traceId,
6151
+ spanId,
6152
+ name: input.name,
6153
+ startTimeUnixNano: String(Math.floor(input.startedAt * 1e6)),
6154
+ endTimeUnixNano: String(Math.floor(input.endedAt * 1e6)),
6155
+ attributes: Object.entries(input.attributes ?? {}).map(([key, value]) => ({
6156
+ key,
6157
+ value: { stringValue: value }
6158
+ })),
6159
+ status: { code: 1 }
6160
+ };
6161
+ }
6162
+ async function exportOtelSpan(span) {
6163
+ const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
6164
+ if (!endpoint) {
6165
+ return;
6166
+ }
6167
+ const serviceName = process.env.OTEL_SERVICE_NAME?.trim() ?? "workhub-api";
6168
+ const url = endpoint.endsWith("/v1/traces") ? endpoint : `${endpoint.replace(/\/$/, "")}/v1/traces`;
6169
+ await fetch(url, {
6170
+ method: "POST",
6171
+ headers: { "content-type": "application/json" },
6172
+ body: JSON.stringify({
6173
+ resourceSpans: [
6174
+ {
6175
+ resource: {
6176
+ attributes: [{ key: "service.name", value: { stringValue: serviceName } }]
6177
+ },
6178
+ scopeSpans: [{ spans: [span] }]
6179
+ }
6180
+ ]
6181
+ })
6182
+ });
6183
+ }
6184
+
6185
+ // ../../src/core/tracing/tracingMiddleware.ts
6186
+ function createTracingMiddleware() {
6187
+ return async (request, next) => {
6188
+ const traceId = (request.headers.get("x-trace-id") ?? crypto.randomUUID()).replace(/-/g, "");
6189
+ const spanId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
6190
+ const startedAt = performance.now();
6191
+ const path = new URL(request.url).pathname;
6192
+ return await runWithTraceContext({ traceId, spanId }, async () => {
6193
+ const response = await next();
6194
+ const endedAt = performance.now();
6195
+ const headers = new Headers(response.headers);
6196
+ headers.set("x-trace-id", traceId);
6197
+ headers.set("x-span-id", spanId);
6198
+ headers.set("traceparent", `00-${traceId}-${spanId}-01`);
6199
+ headers.set("server-timing", `app;dur=${(endedAt - startedAt).toFixed(2)}`);
6200
+ exportOtelSpan(createSpan({
6201
+ traceId,
6202
+ name: `${request.method} ${path}`,
6203
+ startedAt,
6204
+ endedAt,
6205
+ attributes: {
6206
+ "http.method": request.method,
6207
+ "http.route": path,
6208
+ "http.status_code": String(response.status)
6209
+ }
6210
+ })).catch(() => {
6211
+ return;
6212
+ });
6213
+ return new Response(response.body, {
6214
+ status: response.status,
6215
+ statusText: response.statusText,
6216
+ headers
6217
+ });
6218
+ });
6219
+ };
6220
+ }
5607
6221
  // ../../src/core/validation/rules.ts
5608
6222
  function required() {
5609
6223
  return (field, value) => {
@@ -5646,45 +6260,6 @@ function maxLength(maximum) {
5646
6260
  return;
5647
6261
  };
5648
6262
  }
5649
- function pattern(expression) {
5650
- return (field, value) => {
5651
- if (typeof value !== "string") {
5652
- return;
5653
- }
5654
- if (!expression.test(value.trim())) {
5655
- return `"${field}" has an invalid format.`;
5656
- }
5657
- return;
5658
- };
5659
- }
5660
- function enumRule(allowedValues) {
5661
- return (field, value) => {
5662
- if (typeof value !== "string") {
5663
- return;
5664
- }
5665
- if (!allowedValues.includes(value)) {
5666
- return `"${field}" must be one of: ${allowedValues.join(", ")}.`;
5667
- }
5668
- return;
5669
- };
5670
- }
5671
- function optional() {
5672
- return () => {
5673
- return;
5674
- };
5675
- }
5676
- function integerRule() {
5677
- return (field, value) => {
5678
- if (value === undefined || value === null || value === "") {
5679
- return;
5680
- }
5681
- const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
5682
- if (!Number.isInteger(parsed)) {
5683
- return `"${field}" must be an integer.`;
5684
- }
5685
- return;
5686
- };
5687
- }
5688
6263
  function emailRule() {
5689
6264
  return (field, value) => {
5690
6265
  if (typeof value !== "string") {
@@ -5697,40 +6272,6 @@ function emailRule() {
5697
6272
  return;
5698
6273
  };
5699
6274
  }
5700
- function confirmed(fieldName) {
5701
- return (field, value, payload) => {
5702
- const confirmationKey = `${fieldName}_confirmation`;
5703
- const confirmation = payload[confirmationKey];
5704
- if (value !== confirmation) {
5705
- return `"${field}" confirmation does not match.`;
5706
- }
5707
- return;
5708
- };
5709
- }
5710
- function positiveIntegerRule() {
5711
- return (field, value) => {
5712
- if (value === undefined || value === null || value === "") {
5713
- return;
5714
- }
5715
- const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
5716
- if (!Number.isInteger(parsed) || parsed <= 0) {
5717
- return `"${field}" must be a positive integer.`;
5718
- }
5719
- return;
5720
- };
5721
- }
5722
- function integerRange(minimum, maximum) {
5723
- return (field, value) => {
5724
- if (value === undefined || value === null || value === "") {
5725
- return;
5726
- }
5727
- const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
5728
- if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum) {
5729
- return `"${field}" must be an integer between ${minimum} and ${maximum}.`;
5730
- }
5731
- return;
5732
- };
5733
- }
5734
6275
  function validateObject(payload, schema) {
5735
6276
  if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
5736
6277
  throw new ValidationError("Request body must be a JSON object.");
@@ -5755,6 +6296,7 @@ function validateObject(payload, schema) {
5755
6296
  return output;
5756
6297
  }
5757
6298
  export {
6299
+ wrapRouteHandler,
5758
6300
  wrapMarkdownMailLayout,
5759
6301
  withMigrationLock,
5760
6302
  withMiddleware,
@@ -5771,6 +6313,11 @@ export {
5771
6313
  sendMarkdownMail,
5772
6314
  securedBindRouteModelByKey,
5773
6315
  securedBindRouteModel,
6316
+ scopedOrganizationIds,
6317
+ runWithTraceContext,
6318
+ runWithTenantDatabase,
6319
+ runWithTenant,
6320
+ runWithRequestMeta,
5774
6321
  runWithDatabaseConnection,
5775
6322
  runWithAuthUser,
5776
6323
  runSeedersFromDirectory,
@@ -5780,7 +6327,12 @@ export {
5780
6327
  runDueScheduledTasks,
5781
6328
  rollbackDatabase,
5782
6329
  resolveWebLayoutData,
6330
+ resolveUserTenantId,
6331
+ resolveUserId,
5783
6332
  resolveService,
6333
+ resolveRepositoryConnection,
6334
+ resolveOrganizationScope,
6335
+ resolveMembershipService,
5784
6336
  resolveDatabaseDriver,
5785
6337
  resolveCsrfTokenForRequest,
5786
6338
  resolveCsrfToken,
@@ -5791,10 +6343,14 @@ export {
5791
6343
  resolveApplicationConfig,
5792
6344
  resolveApplicationCache,
5793
6345
  resolveApplicationAuth,
6346
+ resetDefaultStorage,
5794
6347
  required,
6348
+ requestIdMiddleware,
6349
+ repositoryConnection,
5795
6350
  renderMarkdownMail,
5796
6351
  registerShutdownHandler,
5797
6352
  registerModelRepository,
6353
+ registerDefaultDatabasePool,
5798
6354
  readSubmittedCsrfTokenFromBody,
5799
6355
  readSubmittedCsrfToken,
5800
6356
  readRequestCookie,
@@ -5802,7 +6358,9 @@ export {
5802
6358
  queue,
5803
6359
  prometheusRegistry,
5804
6360
  policyGate,
6361
+ parsePositiveIntParam,
5805
6362
  parsePaginationQuery,
6363
+ parseMultipartUpload,
5806
6364
  paginatedResponse,
5807
6365
  normalizeMetricPath,
5808
6366
  noContentResponse,
@@ -5815,12 +6373,16 @@ export {
5815
6373
  markdownToHtml,
5816
6374
  mailer,
5817
6375
  mail,
6376
+ logSecurityEvent,
5818
6377
  log,
5819
6378
  loadSeedersFromDirectory,
5820
6379
  loadMigrationsFromDirectory,
5821
6380
  jsonResponse2 as jsonResponse,
5822
6381
  jobRegistry,
6382
+ isPublicReadsEnabled,
6383
+ isInsideTenantDatabaseScope,
5823
6384
  isHtmxRequest,
6385
+ isGlobalAdmin,
5824
6386
  isEtagEnabled,
5825
6387
  installGracefulShutdownSignals,
5826
6388
  inferReferencedTable,
@@ -5833,55 +6395,90 @@ export {
5833
6395
  indexBelongsToManyRelation,
5834
6396
  hydrateValue,
5835
6397
  htmlResponse,
6398
+ hasOrgMembership,
5836
6399
  hasOne,
6400
+ hasMinimumOrgRole2 as hasMinimumOrgRole,
5837
6401
  hasMany,
6402
+ guestCanViewResource,
5838
6403
  grammarForDriver,
5839
6404
  getMigrationStatus,
6405
+ getDefaultDatabaseQuery,
6406
+ getDefaultDatabasePool,
5840
6407
  getActiveDatabaseConnection,
5841
6408
  freshDatabase,
5842
6409
  formatAdminValue,
5843
6410
  filterMassAssignable,
5844
6411
  events,
5845
6412
  etagFromResource,
6413
+ emptyPaginateResult,
5846
6414
  emailRule,
5847
6415
  dehydrateValue,
5848
6416
  defineTable,
6417
+ currentTraceId,
6418
+ currentTenantId,
6419
+ currentTenant,
6420
+ currentRequestMeta,
6421
+ currentOrganizationIds,
6422
+ currentOrgRole,
5849
6423
  currentAuthUser,
5850
6424
  createdResponse,
5851
6425
  createTrackedJob,
6426
+ createTracingMiddleware,
5852
6427
  createThrottleMiddleware,
6428
+ createTenantMiddleware,
6429
+ createStorageDriver,
5853
6430
  createSecurityHeadersMiddleware,
6431
+ createScimThrottleMiddleware,
6432
+ createScimAuthMiddleware,
5854
6433
  createSchemaBuilder,
5855
6434
  createRequireWebAuthMiddleware,
6435
+ createRequireGlobalAdminMiddleware,
5856
6436
  createRequireAuthMiddleware,
6437
+ createRequireAbilityMiddleware,
6438
+ createRequestLoggingMiddleware,
5857
6439
  createQueueWorker,
5858
6440
  createQueue,
5859
6441
  createProductionQueue,
5860
6442
  createNotificationDispatcher,
5861
6443
  createMetricsMiddleware,
5862
6444
  createMemoryThrottleMiddleware,
6445
+ createMembershipMiddleware,
5863
6446
  createLoginThrottleMiddleware,
6447
+ createFlashMiddleware,
5864
6448
  createFailedJobService,
5865
- createDatabaseConnection2 as createDatabaseConnection,
6449
+ createDatabaseQueryProxy,
6450
+ createDatabaseConnection,
5866
6451
  createCsrfTokenCookie,
5867
6452
  createCsrfProtection,
5868
6453
  createCsrfMiddleware,
6454
+ createCorsMiddleware,
5869
6455
  createBodySizeLimitMiddleware,
5870
6456
  createAuthorizeMiddleware,
5871
6457
  createAuthMiddleware,
5872
6458
  config,
6459
+ conditionalJsonResponse,
5873
6460
  composeMiddleware,
5874
6461
  compileBlueprint,
6462
+ collectQueueMetrics,
5875
6463
  cache,
5876
6464
  buildSmtpPayload,
6465
+ buildRequestCacheKey,
5877
6466
  buildMarkdownMailMessage,
6467
+ bindRouteModel,
5878
6468
  bindDatabaseConnection2 as bindDatabaseConnection,
5879
6469
  belongsToMany,
5880
6470
  belongsTo,
6471
+ authContext,
5881
6472
  auth,
6473
+ auditChecksum,
6474
+ assertResourceInCurrentTenant,
6475
+ assertOrganizationReadable,
5882
6476
  assertIfMatch,
5883
6477
  applyMiddlewareToRoutes,
6478
+ applyConditionalGet,
5884
6479
  applyCasts,
6480
+ appendProjectScope,
6481
+ appendOrganizationScope,
5885
6482
  appSchedule,
5886
6483
  WhereBuilder,
5887
6484
  WebFormRequest,
@@ -5909,10 +6506,12 @@ export {
5909
6506
  NotFoundError,
5910
6507
  MySqlGrammar,
5911
6508
  Model,
6509
+ membershipService_default as MembershipService,
5912
6510
  Mailer,
5913
6511
  LogMailDriver,
5914
6512
  LocalStorageDriver,
5915
6513
  Job,
6514
+ GuestGuard,
5916
6515
  FormRequest,
5917
6516
  ForeignIdColumnDefinition,
5918
6517
  ForbiddenError,
@@ -5920,15 +6519,20 @@ export {
5920
6519
  failedJobRepository_default as FailedJobRepository,
5921
6520
  EventBus,
5922
6521
  EtaViewEngine,
6522
+ DatabaseTokenGuard,
5923
6523
  DEFAULT_VIEWS_DIRECTORY,
6524
+ DEFAULT_TENANT,
5924
6525
  ConflictError,
5925
6526
  ConfigStore,
6527
+ CompositeGuard,
5926
6528
  ColumnDefinition,
5927
- repository_default as CacheRepository,
6529
+ repository_default2 as CacheRepository,
5928
6530
  CACHE_TAGS,
5929
6531
  Blueprint,
5930
6532
  baseRepository_default as BaseRepository,
5931
6533
  BadRequestError,
6534
+ AuthManager,
5932
6535
  AsyncQueue,
6536
+ ApiTokenGuard,
5933
6537
  AdminResourceRegistry
5934
6538
  };