@getstrata/bootstrap 0.2.6 → 0.2.8

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 (39) hide show
  1. package/dist/bootstrap/contracts.d.ts +2 -0
  2. package/dist/bootstrap/http/securedRouteModelBinding.d.ts +11 -0
  3. package/dist/bootstrap/providers/storage.d.ts +3 -0
  4. package/dist/bootstrap/public-api.d.ts +1 -0
  5. package/dist/bootstrap/scimRoutes.d.ts +1 -1
  6. package/dist/core/auth/membershipScope.d.ts +16 -0
  7. package/dist/core/auth/membershipService.d.ts +24 -0
  8. package/dist/core/database/baseRepository.d.ts +6 -1
  9. package/dist/core/database/connectionContext.d.ts +2 -1
  10. package/dist/core/database/defaultConnection.d.ts +6 -0
  11. package/dist/core/database/queryProxy.d.ts +3 -0
  12. package/dist/core/database/repositoryConnection.d.ts +3 -3
  13. package/dist/core/http/securedRouteModelBinding.d.ts +2 -11
  14. package/dist/core/jobs/dispatchWebhookJob.d.ts +0 -1
  15. package/dist/core/queue/queueMetrics.d.ts +15 -0
  16. package/dist/core/security/safeFetch.d.ts +2 -0
  17. package/dist/core/security/safeUrl.d.ts +16 -1
  18. package/dist/core/tenant/tenantDatabaseScope.d.ts +2 -1
  19. package/dist/db/connection/index.d.ts +1 -1
  20. package/dist/domain/workhub.d.ts +35 -0
  21. package/dist/entries/applicationRegistry.js +185 -0
  22. package/dist/entries/config.js +42 -0
  23. package/dist/entries/context.js +4213 -0
  24. package/dist/entries/contracts.js +92 -0
  25. package/dist/entries/createWebRoutes.js +996 -0
  26. package/dist/entries/http/securedRouteModelBinding.js +383 -0
  27. package/dist/entries/httpKernel.js +264 -0
  28. package/dist/entries/providers/view.js +635 -0
  29. package/dist/entries/providers.js +4099 -0
  30. package/dist/framework/public-api.d.ts +30 -6
  31. package/dist/index.js +455 -110
  32. package/dist/modules/organization/repository.d.ts +14 -0
  33. package/dist/modules/organization/table.d.ts +3 -0
  34. package/dist/modules/organization/types.d.ts +10 -0
  35. package/dist/modules/scim/controller.d.ts +2 -1
  36. package/dist/modules/scim/scimResponse.d.ts +1 -1
  37. package/dist/modules/scim/service.d.ts +4 -7
  38. package/dist/modules/user/repository.d.ts +1 -0
  39. package/package.json +10 -4
package/dist/index.js CHANGED
@@ -65,18 +65,13 @@ var appConfig = {
65
65
  apiPrefix: process.env.API_PREFIX ?? "/api/v1"
66
66
  };
67
67
 
68
- // ../../src/config/database.ts
69
- function readInteger(name, fallback) {
70
- const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
71
- return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
72
- }
73
- var databaseConfig = {
74
- url: process.env.DATABASE_URL ?? "",
75
- poolMax: readInteger("DB_POOL_MAX", 10),
76
- idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
77
- maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
78
- connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
68
+ // ../../src/core/database/boundConnection.ts
69
+ var boundConnectionHolder = {
70
+ connection: null
79
71
  };
72
+ function getBoundDatabaseConnection() {
73
+ return boundConnectionHolder.connection;
74
+ }
80
75
 
81
76
  // ../../src/core/database/connectionContext.ts
82
77
  import { AsyncLocalStorage } from "async_hooks";
@@ -85,84 +80,65 @@ function getActiveDatabaseConnection(fallback) {
85
80
  return activeConnection.getStore() ?? fallback;
86
81
  }
87
82
 
88
- // ../../src/db/connection/createConnection.ts
89
- var {SQL } = globalThis.Bun;
90
- function createDatabaseConnection(config) {
91
- if (!config.url) {
92
- throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
83
+ // ../../src/core/database/queryProxy.ts
84
+ var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
85
+ function createDatabaseQueryProxy(pool) {
86
+ function resolveDatabase() {
87
+ return getActiveDatabaseConnection(pool);
93
88
  }
94
- return new SQL({
95
- url: config.url,
96
- max: config.poolMax,
97
- idleTimeout: config.idleTimeoutSeconds,
98
- maxLifetime: config.maxLifetimeSeconds,
99
- connectionTimeout: config.connectionTimeoutSeconds
89
+ function resolveDatabaseForProperty(property) {
90
+ if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
91
+ return pool;
92
+ }
93
+ return resolveDatabase();
94
+ }
95
+ return new Proxy(function database() {}, {
96
+ apply(_target, _thisArg, args) {
97
+ return resolveDatabase()(...args);
98
+ },
99
+ get(_target, property) {
100
+ const connection = resolveDatabaseForProperty(property);
101
+ const value = connection[property];
102
+ return typeof value === "function" ? value.bind(connection) : value;
103
+ }
100
104
  });
101
105
  }
102
106
 
103
- // ../../src/db/connection/index.ts
104
- var connectionHolder = {
107
+ // ../../src/core/database/defaultConnection.ts
108
+ var defaultPool = {
105
109
  connection: null
106
110
  };
107
- function getDatabase() {
108
- if (!connectionHolder.connection) {
109
- connectionHolder.connection = createDatabaseConnection(databaseConfig);
110
- }
111
- return connectionHolder.connection;
112
- }
113
- var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
114
- function resolveDatabase() {
115
- return getActiveDatabaseConnection(getDatabase());
111
+ var defaultQuery = {
112
+ connection: null
113
+ };
114
+ function registerDefaultDatabasePool(connection) {
115
+ defaultPool.connection = connection;
116
+ defaultQuery.connection = createDatabaseQueryProxy(connection);
116
117
  }
117
- function resolveDatabaseForProperty(property) {
118
- if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
119
- return getDatabase();
118
+ function getDefaultDatabaseQuery() {
119
+ if (!defaultQuery.connection) {
120
+ throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
120
121
  }
121
- return resolveDatabase();
122
+ return defaultQuery.connection;
122
123
  }
123
- var db = new Proxy(function database() {}, {
124
+
125
+ // ../../src/core/database/repositoryConnection.ts
126
+ function resolveRepositoryConnection() {
127
+ return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
128
+ }
129
+ var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
124
130
  apply(_target, _thisArg, args) {
125
- return resolveDatabase()(...args);
131
+ return resolveRepositoryConnection()(...args);
126
132
  },
127
133
  get(_target, property) {
128
- const connection = resolveDatabaseForProperty(property);
134
+ const connection = resolveRepositoryConnection();
129
135
  const value = connection[property];
130
136
  return typeof value === "function" ? value.bind(connection) : value;
131
137
  }
132
138
  });
133
- var connection_default = db;
134
139
 
135
- // ../../src/core/security/safeFetch.ts
136
- var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
137
- async function safeFetch(input, init = {}, options = {}) {
138
- const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
139
- const maxRedirects = options.maxRedirects ?? 0;
140
- const controller = new AbortController;
141
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
142
- try {
143
- let currentUrl = input;
144
- let redirectCount = 0;
145
- while (true) {
146
- const response = await fetch(currentUrl, {
147
- ...init,
148
- signal: controller.signal,
149
- redirect: "manual"
150
- });
151
- if (response.status >= 300 && response.status < 400) {
152
- const location = response.headers.get("location");
153
- if (!location || redirectCount >= maxRedirects) {
154
- return response;
155
- }
156
- currentUrl = new URL(location, currentUrl).toString();
157
- redirectCount += 1;
158
- continue;
159
- }
160
- return response;
161
- }
162
- } finally {
163
- clearTimeout(timeout);
164
- }
165
- }
140
+ // ../../src/core/security/safeUrl.ts
141
+ import { lookup as dnsLookupImpl } from "dns/promises";
166
142
 
167
143
  // ../../src/core/errors/http.ts
168
144
  class HttpError extends Error {
@@ -203,8 +179,14 @@ class UnauthorizedError extends HttpError {
203
179
  super(401, message, details);
204
180
  }
205
181
  }
182
+ class PreconditionFailedError extends HttpError {
183
+ constructor(message = "Precondition Failed", details) {
184
+ super(412, message, details);
185
+ }
186
+ }
206
187
 
207
188
  // ../../src/core/security/safeUrl.ts
189
+ var dnsLookup = dnsLookupImpl;
208
190
  var BLOCKED_HOSTNAMES = new Set([
209
191
  "localhost",
210
192
  "127.0.0.1",
@@ -276,14 +258,63 @@ function assertSafeOutboundUrl(rawUrl, options = {}) {
276
258
  }
277
259
  return parsed;
278
260
  }
261
+ function isBlockedIpAddress(address) {
262
+ return isBlockedHostname(address.trim().toLowerCase());
263
+ }
264
+ async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
265
+ const parsed = assertSafeOutboundUrl(rawUrl, options);
266
+ if (options.resolveDns === false) {
267
+ return parsed;
268
+ }
269
+ const hostname = parsed.hostname.trim().toLowerCase();
270
+ const results = await dnsLookup(hostname, { all: true, verbatim: true });
271
+ if (results.some((result) => isBlockedIpAddress(result.address))) {
272
+ throw new BadRequestError("Webhook URL targets a blocked host.");
273
+ }
274
+ return parsed;
275
+ }
276
+
277
+ // ../../src/core/security/safeFetch.ts
278
+ var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
279
+ async function safeFetch(input, init = {}, options = {}) {
280
+ const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
281
+ const maxRedirects = options.maxRedirects ?? 0;
282
+ const resolveDns = options.resolveDns ?? appConfig.env === "production";
283
+ const urlOptions = { allowHttp: options.allowHttp, resolveDns };
284
+ const controller = new AbortController;
285
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
286
+ try {
287
+ let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
288
+ let redirectCount = 0;
289
+ while (true) {
290
+ const response = await fetch(currentUrl, {
291
+ ...init,
292
+ signal: controller.signal,
293
+ redirect: "manual"
294
+ });
295
+ if (response.status >= 300 && response.status < 400) {
296
+ const location = response.headers.get("location");
297
+ if (!location || redirectCount >= maxRedirects) {
298
+ return response;
299
+ }
300
+ currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
301
+ redirectCount += 1;
302
+ continue;
303
+ }
304
+ return response;
305
+ }
306
+ } finally {
307
+ clearTimeout(timeout);
308
+ }
309
+ }
279
310
 
280
311
  // ../../src/core/tenant/databaseTenantContext.ts
281
312
  async function runWithMigrationBypass(callback) {
282
- await connection_default`SELECT set_config('app.bypass_rls', 'true', false)`;
313
+ await repositoryConnection`SELECT set_config('app.bypass_rls', 'true', false)`;
283
314
  try {
284
315
  return await callback();
285
316
  } finally {
286
- await connection_default`SELECT set_config('app.bypass_rls', 'false', false)`;
317
+ await repositoryConnection`SELECT set_config('app.bypass_rls', 'false', false)`;
287
318
  }
288
319
  }
289
320
 
@@ -340,7 +371,7 @@ async function exportPendingAuditLogs() {
340
371
  return 0;
341
372
  }
342
373
  return await runWithMigrationBypass(async () => {
343
- const rows = await connection_default`
374
+ const rows = await repositoryConnection`
344
375
  SELECT
345
376
  id,
346
377
  user_id,
@@ -384,13 +415,13 @@ async function exportPendingAuditLogs() {
384
415
  ...process.env.SIEM_EXPORT_TOKEN ? { authorization: `Bearer ${process.env.SIEM_EXPORT_TOKEN}` } : {}
385
416
  },
386
417
  body
387
- });
418
+ }, { allowHttp: appConfig.env !== "production" });
388
419
  if (!response.ok) {
389
420
  throw new Error(`SIEM export failed with status ${response.status}.`);
390
421
  }
391
422
  const ids = rows.map((row) => row.id);
392
423
  for (const id of ids) {
393
- await connection_default`UPDATE audit_log SET exported_at = NOW() WHERE id = ${id}`;
424
+ await repositoryConnection`UPDATE audit_log SET exported_at = NOW() WHERE id = ${id}`;
394
425
  }
395
426
  return rows.length;
396
427
  });
@@ -550,7 +581,8 @@ class ConfigStore {
550
581
  }
551
582
  var requiredDependencyKeys = [
552
583
  "container",
553
- "cache"
584
+ "cache",
585
+ "storage"
554
586
  ];
555
587
  function getRequiredDependency(dependencies, key) {
556
588
  const dependency = dependencies[key];
@@ -643,6 +675,60 @@ import { SamlProvider } from "@getstrata/core/auth/oauth/samlProvider";
643
675
  // ../../src/modules/user/apiTokenRepository.ts
644
676
  import { BaseRepository } from "@getstrata/core/database";
645
677
 
678
+ // ../../src/config/database.ts
679
+ function readInteger(name, fallback) {
680
+ const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
681
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
682
+ }
683
+ var databaseConfig = {
684
+ url: process.env.DATABASE_URL ?? "",
685
+ poolMax: readInteger("DB_POOL_MAX", 10),
686
+ idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
687
+ maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
688
+ connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
689
+ };
690
+
691
+ // ../../src/db/connection/createConnection.ts
692
+ var {SQL } = globalThis.Bun;
693
+ function createDatabaseConnection(config) {
694
+ if (!config.url) {
695
+ throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
696
+ }
697
+ return new SQL({
698
+ url: config.url,
699
+ max: config.poolMax,
700
+ idleTimeout: config.idleTimeoutSeconds,
701
+ maxLifetime: config.maxLifetimeSeconds,
702
+ connectionTimeout: config.connectionTimeoutSeconds
703
+ });
704
+ }
705
+
706
+ // ../../src/db/connection/index.ts
707
+ var connectionHolder = {
708
+ connection: null
709
+ };
710
+ function getDatabase() {
711
+ if (!connectionHolder.connection) {
712
+ connectionHolder.connection = createDatabaseConnection(databaseConfig);
713
+ registerDefaultDatabasePool(connectionHolder.connection);
714
+ }
715
+ return connectionHolder.connection;
716
+ }
717
+ function getDb() {
718
+ getDatabase();
719
+ return getDefaultDatabaseQuery();
720
+ }
721
+ var db = new Proxy(function database() {}, {
722
+ apply(_target, _thisArg, args) {
723
+ return getDb()(...args);
724
+ },
725
+ get(_target, property) {
726
+ const connection = getDb();
727
+ const value = connection[property];
728
+ return typeof value === "function" ? value.bind(connection) : value;
729
+ }
730
+ });
731
+
646
732
  // ../../src/modules/user/apiTokenTable.ts
647
733
  import { defineTable } from "@getstrata/core/database";
648
734
  var apiTokenTable = defineTable({
@@ -1737,17 +1823,20 @@ function resolveApplicationCache() {
1737
1823
  function resolveApplicationQueue() {
1738
1824
  return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
1739
1825
  }
1826
+ function resolveApplicationAuth() {
1827
+ return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
1828
+ }
1829
+ function resolveApplicationPolicyGate() {
1830
+ return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
1831
+ }
1740
1832
 
1741
1833
  // ../../src/core/jobs/dispatchWebhookJob.ts
1742
1834
  import { createHmac as createHmac2 } from "crypto";
1743
1835
  class DispatchWebhookJob extends Job {
1744
- constructor() {
1745
- super();
1746
- }
1747
1836
  maxAttempts = 3;
1748
1837
  backoffMs = 2000;
1749
1838
  async handle(payload) {
1750
- const rows = await connection_default`
1839
+ const rows = await repositoryConnection`
1751
1840
  SELECT id, url, secret
1752
1841
  FROM webhook
1753
1842
  WHERE id = ${payload.webhookId} AND active = TRUE
@@ -1770,14 +1859,14 @@ class DispatchWebhookJob extends Job {
1770
1859
  "x-workhub-signature": signature
1771
1860
  },
1772
1861
  body
1773
- });
1862
+ }, { allowHttp: appConfig.env !== "production" });
1774
1863
  responseStatus = response.status;
1775
1864
  if (!response.ok) {
1776
1865
  throw new Error(`Webhook delivery failed with status ${response.status}.`);
1777
1866
  }
1778
1867
  } catch (error) {
1779
1868
  errorMessage = error instanceof Error ? error.message : String(error);
1780
- await connection_default`
1869
+ await repositoryConnection`
1781
1870
  INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
1782
1871
  VALUES (
1783
1872
  ${webhook.id},
@@ -1789,7 +1878,7 @@ class DispatchWebhookJob extends Job {
1789
1878
  `;
1790
1879
  throw error instanceof Error ? error : new Error(errorMessage);
1791
1880
  }
1792
- await connection_default`
1881
+ await repositoryConnection`
1793
1882
  INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
1794
1883
  VALUES (
1795
1884
  ${webhook.id},
@@ -2369,26 +2458,6 @@ function indexMorphToRelation(children, parentsByType, relation) {
2369
2458
  return result;
2370
2459
  }
2371
2460
 
2372
- // ../../src/core/database/boundConnection.ts
2373
- var boundConnectionHolder = {
2374
- connection: null
2375
- };
2376
- function getBoundDatabaseConnection() {
2377
- return boundConnectionHolder.connection;
2378
- }
2379
-
2380
- // ../../src/core/database/repositoryConnection.ts
2381
- function resolveRepositoryConnection() {
2382
- return getBoundDatabaseConnection() ?? connection_default;
2383
- }
2384
- var repositoryConnection = new Proxy({}, {
2385
- get(_target, property) {
2386
- const connection = resolveRepositoryConnection();
2387
- const value = connection[property];
2388
- return typeof value === "function" ? value.bind(connection) : value;
2389
- }
2390
- });
2391
-
2392
2461
  // ../../src/core/database/whereBuilder.ts
2393
2462
  class WhereBuilder {
2394
2463
  nodes = [];
@@ -3816,6 +3885,128 @@ var queueProvider = {
3816
3885
  };
3817
3886
  var queue_default = queueProvider;
3818
3887
 
3888
+ // ../../src/core/storage/storage.ts
3889
+ import { mkdir, readFile, unlink, writeFile } from "fs/promises";
3890
+ import { dirname, join as join3 } from "path";
3891
+ var {S3Client } = globalThis.Bun;
3892
+
3893
+ class LocalStorageDriver {
3894
+ rootDirectory;
3895
+ constructor(rootDirectory) {
3896
+ this.rootDirectory = rootDirectory;
3897
+ }
3898
+ resolveRootDirectory() {
3899
+ return this.rootDirectory ?? process.env.STORAGE_PATH ?? "storage";
3900
+ }
3901
+ resolvePath(path) {
3902
+ return join3(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
3903
+ }
3904
+ async put(path, contents) {
3905
+ const absolutePath = this.resolvePath(path);
3906
+ await mkdir(dirname(absolutePath), { recursive: true });
3907
+ await writeFile(absolutePath, contents);
3908
+ return path;
3909
+ }
3910
+ async get(path) {
3911
+ try {
3912
+ return await readFile(this.resolvePath(path));
3913
+ } catch {
3914
+ return null;
3915
+ }
3916
+ }
3917
+ async delete(path) {
3918
+ try {
3919
+ await unlink(this.resolvePath(path));
3920
+ return true;
3921
+ } catch {
3922
+ return false;
3923
+ }
3924
+ }
3925
+ }
3926
+
3927
+ class S3StorageDriver {
3928
+ client;
3929
+ constructor(client) {
3930
+ this.client = client;
3931
+ }
3932
+ async put(path, contents) {
3933
+ await this.client.write(path.replace(/^\/+/, ""), contents);
3934
+ return path;
3935
+ }
3936
+ async get(path) {
3937
+ const normalizedPath = path.replace(/^\/+/, "");
3938
+ const file = this.client.file(normalizedPath);
3939
+ if (!await file.exists()) {
3940
+ return null;
3941
+ }
3942
+ return new Uint8Array(await file.arrayBuffer());
3943
+ }
3944
+ async delete(path) {
3945
+ try {
3946
+ await this.client.unlink(path.replace(/^\/+/, ""));
3947
+ return true;
3948
+ } catch {
3949
+ return false;
3950
+ }
3951
+ }
3952
+ }
3953
+
3954
+ class StorageManager {
3955
+ driver;
3956
+ constructor(driver) {
3957
+ this.driver = driver;
3958
+ }
3959
+ put(path, contents) {
3960
+ return this.driver.put(path, contents);
3961
+ }
3962
+ get(path) {
3963
+ return this.driver.get(path);
3964
+ }
3965
+ delete(path) {
3966
+ return this.driver.delete(path);
3967
+ }
3968
+ }
3969
+ function resolveS3Config() {
3970
+ const accessKeyId = process.env.AWS_ACCESS_KEY_ID?.trim();
3971
+ const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY?.trim();
3972
+ const bucket = process.env.AWS_BUCKET?.trim();
3973
+ if (!accessKeyId || !secretAccessKey || !bucket) {
3974
+ throw new Error('STORAGE_DRIVER="s3" requires AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_BUCKET.');
3975
+ }
3976
+ return {
3977
+ accessKeyId,
3978
+ secretAccessKey,
3979
+ bucket,
3980
+ ...process.env.AWS_REGION?.trim() ? { region: process.env.AWS_REGION.trim() } : {},
3981
+ ...process.env.AWS_ENDPOINT?.trim() ? { endpoint: process.env.AWS_ENDPOINT.trim() } : {}
3982
+ };
3983
+ }
3984
+ function createS3Client(config = resolveS3Config()) {
3985
+ return new S3Client({
3986
+ accessKeyId: config.accessKeyId,
3987
+ secretAccessKey: config.secretAccessKey,
3988
+ bucket: config.bucket,
3989
+ ...config.region ? { region: config.region } : {},
3990
+ ...config.endpoint ? { endpoint: config.endpoint } : {}
3991
+ });
3992
+ }
3993
+ function createStorageDriver() {
3994
+ const driver = process.env.STORAGE_DRIVER ?? "local";
3995
+ if (driver === "s3") {
3996
+ return new S3StorageDriver(createS3Client());
3997
+ }
3998
+ return new LocalStorageDriver;
3999
+ }
4000
+
4001
+ // ../../src/bootstrap/providers/storage.ts
4002
+ var storageProvider = {
4003
+ name: "core.storage",
4004
+ register({ dependencies }) {
4005
+ dependencies.storage = new StorageManager(createStorageDriver());
4006
+ }
4007
+ };
4008
+ var storage_default = storageProvider;
4009
+
3819
4010
  // ../../src/config/frontend.ts
3820
4011
  function readFrontendMode() {
3821
4012
  const mode = (process.env.FRONTEND_MODE ?? "api").trim();
@@ -3842,9 +4033,9 @@ function currentRequestMeta() {
3842
4033
  }
3843
4034
 
3844
4035
  // ../../src/core/view/etaViewEngine.ts
3845
- import { join as join3 } from "path";
4036
+ import { join as join4 } from "path";
3846
4037
  import { Eta } from "eta";
3847
- var DEFAULT_VIEWS_DIRECTORY = join3(process.cwd(), "resources/views");
4038
+ var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
3848
4039
  var DEFAULT_LAYOUT = "layouts/app.eta";
3849
4040
 
3850
4041
  class EtaViewEngine {
@@ -4069,6 +4260,7 @@ var viewProvider = {
4069
4260
  var coreProviders = [
4070
4261
  config_default,
4071
4262
  cache_default,
4263
+ storage_default,
4072
4264
  auth_default,
4073
4265
  events_default,
4074
4266
  policy_default,
@@ -4169,7 +4361,7 @@ function createAppContext() {
4169
4361
  return appContext;
4170
4362
  }
4171
4363
  // ../../src/bootstrap/createWebRoutes.ts
4172
- import { join as join4 } from "path";
4364
+ import { join as join5 } from "path";
4173
4365
 
4174
4366
  // ../../src/core/http/middleware.ts
4175
4367
  function isRouteHandler(value) {
@@ -4503,7 +4695,7 @@ function createWebRoutes(dependencies) {
4503
4695
  registerRoute("GET", "/assets/*", ["global", "web"]);
4504
4696
  const pathname = new URL(request.url).pathname;
4505
4697
  const relativePath = pathname.replace(/^\//, "");
4506
- const file = Bun.file(join4(process.cwd(), "public", relativePath));
4698
+ const file = Bun.file(join5(process.cwd(), "public", relativePath));
4507
4699
  if (!await file.exists()) {
4508
4700
  return htmlResponse("Not Found", { status: 404 });
4509
4701
  }
@@ -4521,6 +4713,157 @@ function mergeWebRoutes(dependencies, routes) {
4521
4713
  ...routes
4522
4714
  };
4523
4715
  }
4716
+ // ../../src/core/crypto/nonCryptographicHash.ts
4717
+ function nonCryptographicDigest(input) {
4718
+ return Bun.hash(input).toString(16);
4719
+ }
4720
+
4721
+ // ../../src/core/http/etag.ts
4722
+ function isEtagEnabled() {
4723
+ return (process.env.FEATURE_ETAG ?? "true") !== "false";
4724
+ }
4725
+ function formatWeakEtag(digest) {
4726
+ return `W/"${digest}"`;
4727
+ }
4728
+ function etagFromResource(resource) {
4729
+ const version = resource.updated_at ?? resource.created_at ?? "";
4730
+ const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
4731
+ const digest = nonCryptographicDigest(`${String(resource.id ?? "0")}:${versionText}`).slice(0, 32);
4732
+ return formatWeakEtag(digest);
4733
+ }
4734
+ function normalizeEtag(value) {
4735
+ return value.trim();
4736
+ }
4737
+ function etagValuesMatch(left, right) {
4738
+ return normalizeEtag(left) === normalizeEtag(right);
4739
+ }
4740
+ function parseEtagList(header) {
4741
+ if (!header) {
4742
+ return [];
4743
+ }
4744
+ return header.split(",").map((value) => normalizeEtag(value)).filter(Boolean);
4745
+ }
4746
+ function ifNoneMatchSatisfied(request, etag) {
4747
+ const header = request.headers.get("if-none-match");
4748
+ if (!header) {
4749
+ return false;
4750
+ }
4751
+ if (header.trim() === "*") {
4752
+ return true;
4753
+ }
4754
+ return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
4755
+ }
4756
+ function ifMatchSatisfied(request, etag) {
4757
+ const header = request.headers.get("if-match");
4758
+ if (!header) {
4759
+ return false;
4760
+ }
4761
+ if (header.trim() === "*") {
4762
+ return true;
4763
+ }
4764
+ return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
4765
+ }
4766
+ function assertIfMatch(request, etag, options = {}) {
4767
+ const header = request.headers.get("if-match");
4768
+ if (!header) {
4769
+ if (options.required) {
4770
+ throw new PreconditionFailedError("If-Match header is required.");
4771
+ }
4772
+ return;
4773
+ }
4774
+ if (!ifMatchSatisfied(request, etag)) {
4775
+ throw new PreconditionFailedError("Resource ETag does not match If-Match.");
4776
+ }
4777
+ }
4778
+ function applyEtagHeaders(headers, etag) {
4779
+ const next = new Headers(headers);
4780
+ next.set("ETag", etag);
4781
+ next.set("Cache-Control", "private, must-revalidate");
4782
+ next.append("Vary", "Authorization");
4783
+ next.append("Vary", "X-Tenant-Id");
4784
+ return next;
4785
+ }
4786
+ function notModifiedResponse(etag) {
4787
+ return new Response(null, {
4788
+ status: 304,
4789
+ headers: applyEtagHeaders(new Headers, etag)
4790
+ });
4791
+ }
4792
+ function applyConditionalGet(request, response, etag) {
4793
+ if (!isEtagEnabled()) {
4794
+ return response;
4795
+ }
4796
+ if (ifNoneMatchSatisfied(request, etag)) {
4797
+ return notModifiedResponse(etag);
4798
+ }
4799
+ const headers = applyEtagHeaders(new Headers(response.headers), etag);
4800
+ return new Response(response.body, {
4801
+ status: response.status,
4802
+ statusText: response.statusText,
4803
+ headers
4804
+ });
4805
+ }
4806
+
4807
+ // ../../src/core/tenant/tenantContext.ts
4808
+ import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
4809
+ var tenantContext = new AsyncLocalStorage4;
4810
+
4811
+ // ../../src/core/http/validation.ts
4812
+ function parsePositiveIntParam(value, name = "id") {
4813
+ const parsed = Number.parseInt(value, 10);
4814
+ if (!Number.isInteger(parsed) || parsed <= 0) {
4815
+ throw new BadRequestError(`Invalid ${name}. Expected a positive integer.`);
4816
+ }
4817
+ return parsed;
4818
+ }
4819
+
4820
+ // ../../src/bootstrap/http/securedRouteModelBinding.ts
4821
+ function isMutatingPolicyAction(action) {
4822
+ return action === "update" || action === "delete";
4823
+ }
4824
+ function securedBindRouteModel(param, resolver, authorization, handler) {
4825
+ return async (request) => {
4826
+ const id = parsePositiveIntParam(String(request.params[param]), String(param));
4827
+ const model = await resolver(id, request);
4828
+ const gate = resolveApplicationPolicyGate();
4829
+ const auth = resolveApplicationAuth();
4830
+ const user = currentAuthUser() ?? await auth.resolve(request);
4831
+ gate.authorize(authorization.resource, authorization.action, user, model);
4832
+ if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
4833
+ assertIfMatch(request, etagFromResource(model), {
4834
+ required: authorization.requireIfMatch ?? true
4835
+ });
4836
+ }
4837
+ const response = await handler(request, model);
4838
+ if (isEtagEnabled() && authorization.action === "view") {
4839
+ return applyConditionalGet(request, response, etagFromResource(model));
4840
+ }
4841
+ return response;
4842
+ };
4843
+ }
4844
+ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
4845
+ return async (request) => {
4846
+ const key = String(request.params[param] ?? "").trim();
4847
+ if (!key) {
4848
+ throw new BadRequestError(`Missing route parameter "${String(param)}".`);
4849
+ }
4850
+ const model = await resolver(key, request);
4851
+ const gate = resolveApplicationPolicyGate();
4852
+ const auth = resolveApplicationAuth();
4853
+ const user = currentAuthUser() ?? await auth.resolve(request);
4854
+ gate.authorize(authorization.resource, authorization.action, user, model);
4855
+ if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
4856
+ assertIfMatch(request, etagFromResource(model), {
4857
+ required: authorization.requireIfMatch ?? true
4858
+ });
4859
+ }
4860
+ const response = await handler(request, model);
4861
+ if (isEtagEnabled() && authorization.action === "view") {
4862
+ return applyConditionalGet(request, response, etagFromResource(model));
4863
+ }
4864
+ return response;
4865
+ };
4866
+ }
4524
4867
  // ../../src/bootstrap/prefixRouteMap.ts
4525
4868
  function prefixRouteMap(prefix, routes) {
4526
4869
  const normalizedPrefix = prefix.replace(/\/$/, "");
@@ -4580,7 +4923,7 @@ async function parseFormBody(request) {
4580
4923
  return { fields, files };
4581
4924
  }
4582
4925
  // ../../src/bootstrap/web/routing.ts
4583
- import { securedBindRouteModelByKey, withErrorHandling } from "@getstrata/core";
4926
+ import { withErrorHandling } from "@getstrata/core";
4584
4927
  function routeParams(request) {
4585
4928
  const normalized = {};
4586
4929
  const raw = request.params;
@@ -4755,6 +5098,8 @@ export {
4755
5098
  toRouteRequest,
4756
5099
  slugify,
4757
5100
  setActiveApplicationContext2 as setActiveApplicationContext,
5101
+ securedBindRouteModelByKey,
5102
+ securedBindRouteModel,
4758
5103
  scheduleRunCommand,
4759
5104
  runProviderPhase,
4760
5105
  runDueScheduledTasks,