@getstrata/bootstrap 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/bootstrap/contracts.d.ts +2 -0
  2. package/dist/bootstrap/providers/storage.d.ts +3 -0
  3. package/dist/bootstrap/scimRoutes.d.ts +1 -1
  4. package/dist/core/auth/membershipScope.d.ts +16 -0
  5. package/dist/core/auth/membershipService.d.ts +24 -0
  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/queue/queueMetrics.d.ts +15 -0
  13. package/dist/core/security/safeFetch.d.ts +2 -0
  14. package/dist/core/security/safeUrl.d.ts +16 -1
  15. package/dist/core/tenant/tenantDatabaseScope.d.ts +2 -1
  16. package/dist/db/connection/index.d.ts +1 -1
  17. package/dist/domain/workhub.d.ts +35 -0
  18. package/dist/entries/applicationRegistry.js +185 -0
  19. package/dist/entries/config.js +42 -0
  20. package/dist/entries/context.js +4208 -0
  21. package/dist/entries/contracts.js +92 -0
  22. package/dist/entries/createWebRoutes.js +996 -0
  23. package/dist/entries/httpKernel.js +264 -0
  24. package/dist/entries/providers/view.js +635 -0
  25. package/dist/entries/providers.js +4094 -0
  26. package/dist/framework/public-api.d.ts +29 -6
  27. package/dist/index.js +290 -109
  28. package/dist/modules/organization/repository.d.ts +14 -0
  29. package/dist/modules/organization/table.d.ts +3 -0
  30. package/dist/modules/organization/types.d.ts +10 -0
  31. package/dist/modules/scim/controller.d.ts +2 -1
  32. package/dist/modules/scim/scimResponse.d.ts +1 -1
  33. package/dist/modules/scim/service.d.ts +4 -7
  34. package/dist/modules/user/repository.d.ts +1 -0
  35. package/package.json +5 -4
@@ -8,10 +8,17 @@ export { ConfigStore, resolveService, ServiceContainer, } from "../bootstrap/con
8
8
  export type { AdminColumn, AdminColumnType, AdminResource, AdminResourceDefinition, AdminResourceHandlers, } from "../core/admin/index.ts";
9
9
  export { AdminResourceRegistry, formatAdminValue, } from "../core/admin/index.ts";
10
10
  export type { AbilityChecker } from "../core/auth/abilityChecker.ts";
11
+ export { isGlobalAdmin, resolveUserId } from "../core/auth/accessControl.ts";
11
12
  export type { AuthUser } from "../core/auth/authContext.ts";
12
- export { currentAuthUser, runWithAuthUser } from "../core/auth/authContext.ts";
13
+ export { authContext, currentAuthUser, runWithAuthUser } from "../core/auth/authContext.ts";
14
+ export type { AuthGuard } from "../core/auth/guard.ts";
15
+ export { ApiTokenGuard, AuthManager, CompositeGuard, DatabaseTokenGuard, GuestGuard, } from "../core/auth/guard.ts";
16
+ export { currentOrganizationIds, currentOrgRole, hasMinimumOrgRole, hasOrgMembership, } from "../core/auth/membershipContext.ts";
13
17
  export { createMembershipMiddleware } from "../core/auth/membershipMiddleware.ts";
18
+ export { appendOrganizationScope, appendProjectScope, assertOrganizationReadable, assertResourceInCurrentTenant, emptyPaginateResult, resolveOrganizationScope, scopedOrganizationIds, } from "../core/auth/membershipScope.ts";
19
+ export { default as MembershipService, resolveMembershipService, } from "../core/auth/membershipService.ts";
14
20
  export { Policy, PolicyGate } from "../core/auth/policy.ts";
21
+ export { createScimAuthMiddleware } from "../core/auth/scimAuthMiddleware.ts";
15
22
  export { default as CacheRepository } from "../core/cache/repository.ts";
16
23
  export { CACHE_TAGS } from "../core/cache/tags.ts";
17
24
  export type { DatabaseConnection } from "../core/database/baseRepository.ts";
@@ -19,13 +26,16 @@ export { default as BaseRepository } from "../core/database/baseRepository.ts";
19
26
  export { bindDatabaseConnection } from "../core/database/bindConnection.ts";
20
27
  export { createDatabaseConnection } from "../core/database/connection.ts";
21
28
  export { getActiveDatabaseConnection, runWithDatabaseConnection, } from "../core/database/connectionContext.ts";
29
+ export { getDefaultDatabasePool, getDefaultDatabaseQuery, registerDefaultDatabasePool, } from "../core/database/defaultConnection.ts";
22
30
  export { withMigrationLock } from "../core/database/migrations/advisoryLock.ts";
23
31
  export { freshDatabase, getMigrationStatus, loadMigrationsFromDirectory, migrateDatabase, rollbackDatabase, } from "../core/database/migrations/runner.ts";
24
32
  export type { Migration, MigrationDatabase, MigrationStatus, } from "../core/database/migrations/types.ts";
25
33
  export type { CastType, GlobalScopeFn, ModelConstructor } from "../core/database/model.ts";
26
34
  export { applyCasts, dehydrateValue, filterMassAssignable, hydrateValue, Model, registerModelRepository, } from "../core/database/model.ts";
35
+ export { createDatabaseQueryProxy } from "../core/database/queryProxy.ts";
27
36
  export type { BelongsToManyRelation, BelongsToRelation, HasManyRelation, HasOneRelation, MorphManyRelation, MorphOneRelation, MorphToRelation, } from "../core/database/relationships.ts";
28
37
  export { belongsTo, belongsToMany, hasMany, hasOne, indexBelongsToManyRelation, indexBelongsToRelation, indexHasManyRelation, indexHasOneRelation, indexMorphManyRelation, indexMorphOneRelation, indexMorphToRelation, morphMany, morphOne, morphTo, } from "../core/database/relationships.ts";
38
+ export { repositoryConnection, resolveRepositoryConnection, } from "../core/database/repositoryConnection.ts";
29
39
  export { RepositoryQuery } from "../core/database/repositoryQuery.ts";
30
40
  export type { BlueprintAction, BlueprintCallback, ColumnKind, DatabaseDriver, ForeignKeyOptions, Grammar, IndexDefinition, IndexKind, ResolveDatabaseDriverOptions, SchemaBuilder, } from "../core/database/schema/index.ts";
31
41
  export { Blueprint, ColumnDefinition, compileBlueprint, createSchemaBuilder, ForeignIdColumnDefinition, grammarForDriver, inferReferencedTable, MySqlGrammar, PostgresGrammar, resolveDatabaseDriver, Schema, SqliteGrammar, UnsupportedSchemaFeatureError, } from "../core/database/schema/index.ts";
@@ -40,24 +50,29 @@ export { BadRequestError, ConflictError, ForbiddenError, NotFoundError, Precondi
40
50
  export { EventBus } from "../core/events/eventBus.ts";
41
51
  export { auth, cache, config, events, log, mail, policyGate, queue, storage, } from "../core/facades/index.ts";
42
52
  export { createBodySizeLimitMiddleware } from "../core/http/bodySizeLimitMiddleware.ts";
53
+ export { conditionalJsonResponse } from "../core/http/conditionalResponse.ts";
43
54
  export { readBunRequestCookie, readRequestCookie } from "../core/http/cookies.ts";
44
55
  export { createCorsMiddleware } from "../core/http/corsMiddleware.ts";
45
56
  export { createCsrfMiddleware } from "../core/http/csrfMiddleware.ts";
46
57
  export { createCsrfProtection } from "../core/http/csrfProtection.ts";
47
58
  export { createCsrfTokenCookie, readSubmittedCsrfToken, readSubmittedCsrfTokenFromBody, resolveCsrfToken, resolveCsrfTokenForRequest, verifyCsrfToken, } from "../core/http/csrfToken.ts";
48
- export { assertIfMatch, etagFromResource, isEtagEnabled, } from "../core/http/etag.ts";
59
+ export { applyConditionalGet, assertIfMatch, type EtagVersioned, etagFromResource, isEtagEnabled, } from "../core/http/etag.ts";
49
60
  export { createFlashMiddleware } from "../core/http/flashMiddleware.ts";
50
61
  export { FormRequest } from "../core/http/formRequest.ts";
51
- export { applyMiddlewareToRoutes, composeMiddleware, createAuthMiddleware, createAuthorizeMiddleware, createdResponse, createRequireAuthMiddleware, jsonResponse, noContentResponse, paginatedResponse, parsePaginationQuery, requestIdMiddleware, securedBindRouteModel, securedBindRouteModelByKey, withErrorHandling, withMiddleware, } from "../core/http/index.ts";
62
+ export { applyMiddlewareToRoutes, bindRouteModel, buildRequestCacheKey, composeMiddleware, createAuthMiddleware, createAuthorizeMiddleware, createdResponse, createRequireAuthMiddleware, jsonResponse, noContentResponse, paginatedResponse, parsePaginationQuery, parsePositiveIntParam, requestIdMiddleware, securedBindRouteModel, securedBindRouteModelByKey, withErrorHandling, withMiddleware, wrapRouteHandler, } from "../core/http/index.ts";
52
63
  export { createLoginThrottleMiddleware } from "../core/http/loginThrottleMiddleware.ts";
53
64
  export { createMemoryThrottleMiddleware } from "../core/http/memoryThrottleMiddleware.ts";
54
65
  export { createMetricsMiddleware, normalizeMetricPath } from "../core/http/metricsMiddleware.ts";
55
66
  export type { Middleware, RouteHandler } from "../core/http/middleware.ts";
67
+ export { type ParsedUpload, parseMultipartUpload } from "../core/http/parseMultipartUpload.ts";
68
+ export type { RequestMeta } from "../core/http/requestMetaContext.ts";
69
+ export { currentRequestMeta, runWithRequestMeta } from "../core/http/requestMetaContext.ts";
56
70
  export { createRequireAbilityMiddleware } from "../core/http/requireAbilityMiddleware.ts";
57
71
  export { createRequireGlobalAdminMiddleware } from "../core/http/requireGlobalAdminMiddleware.ts";
58
72
  export { createRequireWebAuthMiddleware } from "../core/http/requireWebAuthMiddleware.ts";
59
73
  export { serializeDate, toPaginatedResourceCollection, toResourceCollection, } from "../core/http/resources.ts";
60
74
  export type { RouteRequest } from "../core/http/route.ts";
75
+ export { createScimThrottleMiddleware } from "../core/http/scimThrottleMiddleware.ts";
61
76
  export { createSecurityHeadersMiddleware } from "../core/http/securityHeadersMiddleware.ts";
62
77
  export { createThrottleMiddleware } from "../core/http/throttleMiddleware.ts";
63
78
  export { WebFormRequest } from "../core/http/webFormRequest.ts";
@@ -77,12 +92,20 @@ export type { CursorPaginatedResult, PaginatedResult, PaginationMeta, } from "..
77
92
  export type { Queue, QueuePriority } from "../core/queue/index.ts";
78
93
  export { AsyncQueue, createQueue, Job, SyncQueue } from "../core/queue/index.ts";
79
94
  export { createFailedJobService, createProductionQueue, createQueueWorker, createTrackedJob, FailedJobRepository, FailedJobService, jobRegistry, QueueWorker, RedisQueue, ResilientQueue, runQueueJob, } from "../core/queue/publicQueue.ts";
95
+ export type { QueueMetricsSnapshot } from "../core/queue/queueMetrics.ts";
96
+ export { collectQueueMetrics } from "../core/queue/queueMetrics.ts";
80
97
  export type { ScheduledTask } from "../core/scheduler/schedule.ts";
81
98
  export { appSchedule, runDueScheduledTasks, Schedule } from "../core/scheduler/schedule.ts";
82
- export { isPublicReadsEnabled } from "../core/security/publicReads.ts";
99
+ export { guestCanViewResource, isPublicReadsEnabled } from "../core/security/publicReads.ts";
100
+ export type { SecurityEventDetails } from "../core/security/securityEvents.ts";
101
+ export { logSecurityEvent } from "../core/security/securityEvents.ts";
83
102
  export type { StorageDriver } from "../core/storage/storage.ts";
84
- export { LocalStorageDriver, resetDefaultStorage, StorageManager, } from "../core/storage/storage.ts";
85
- export { createTenantMiddleware } from "../core/tenant/tenantMiddleware.ts";
103
+ export { createStorageDriver, LocalStorageDriver, resetDefaultStorage, StorageManager, } from "../core/storage/storage.ts";
104
+ export { currentTenant, currentTenantId, runWithTenant } from "../core/tenant/tenantContext.ts";
105
+ export { isInsideTenantDatabaseScope, runWithTenantDatabase, } from "../core/tenant/tenantDatabaseScope.ts";
106
+ export { auditChecksum, createTenantMiddleware, DEFAULT_TENANT, resolveUserTenantId, } from "../core/tenant/tenantMiddleware.ts";
107
+ export type { TraceContext } from "../core/tracing/traceContext.ts";
108
+ export { currentTraceId, runWithTraceContext } from "../core/tracing/traceContext.ts";
86
109
  export { createTracingMiddleware } from "../core/tracing/tracingMiddleware.ts";
87
110
  export type { ValidationRule, ValidationSchema } from "../core/validation/rules.ts";
88
111
  export { emailRule, maxLength, minLength, required, stringRule, validateObject, } from "../core/validation/rules.ts";
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 {
@@ -205,6 +181,7 @@ class UnauthorizedError extends HttpError {
205
181
  }
206
182
 
207
183
  // ../../src/core/security/safeUrl.ts
184
+ var dnsLookup = dnsLookupImpl;
208
185
  var BLOCKED_HOSTNAMES = new Set([
209
186
  "localhost",
210
187
  "127.0.0.1",
@@ -276,14 +253,63 @@ function assertSafeOutboundUrl(rawUrl, options = {}) {
276
253
  }
277
254
  return parsed;
278
255
  }
256
+ function isBlockedIpAddress(address) {
257
+ return isBlockedHostname(address.trim().toLowerCase());
258
+ }
259
+ async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
260
+ const parsed = assertSafeOutboundUrl(rawUrl, options);
261
+ if (options.resolveDns === false) {
262
+ return parsed;
263
+ }
264
+ const hostname = parsed.hostname.trim().toLowerCase();
265
+ const results = await dnsLookup(hostname, { all: true, verbatim: true });
266
+ if (results.some((result) => isBlockedIpAddress(result.address))) {
267
+ throw new BadRequestError("Webhook URL targets a blocked host.");
268
+ }
269
+ return parsed;
270
+ }
271
+
272
+ // ../../src/core/security/safeFetch.ts
273
+ var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
274
+ async function safeFetch(input, init = {}, options = {}) {
275
+ const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
276
+ const maxRedirects = options.maxRedirects ?? 0;
277
+ const resolveDns = options.resolveDns ?? appConfig.env === "production";
278
+ const urlOptions = { allowHttp: options.allowHttp, resolveDns };
279
+ const controller = new AbortController;
280
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
281
+ try {
282
+ let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
283
+ let redirectCount = 0;
284
+ while (true) {
285
+ const response = await fetch(currentUrl, {
286
+ ...init,
287
+ signal: controller.signal,
288
+ redirect: "manual"
289
+ });
290
+ if (response.status >= 300 && response.status < 400) {
291
+ const location = response.headers.get("location");
292
+ if (!location || redirectCount >= maxRedirects) {
293
+ return response;
294
+ }
295
+ currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
296
+ redirectCount += 1;
297
+ continue;
298
+ }
299
+ return response;
300
+ }
301
+ } finally {
302
+ clearTimeout(timeout);
303
+ }
304
+ }
279
305
 
280
306
  // ../../src/core/tenant/databaseTenantContext.ts
281
307
  async function runWithMigrationBypass(callback) {
282
- await connection_default`SELECT set_config('app.bypass_rls', 'true', false)`;
308
+ await repositoryConnection`SELECT set_config('app.bypass_rls', 'true', false)`;
283
309
  try {
284
310
  return await callback();
285
311
  } finally {
286
- await connection_default`SELECT set_config('app.bypass_rls', 'false', false)`;
312
+ await repositoryConnection`SELECT set_config('app.bypass_rls', 'false', false)`;
287
313
  }
288
314
  }
289
315
 
@@ -340,7 +366,7 @@ async function exportPendingAuditLogs() {
340
366
  return 0;
341
367
  }
342
368
  return await runWithMigrationBypass(async () => {
343
- const rows = await connection_default`
369
+ const rows = await repositoryConnection`
344
370
  SELECT
345
371
  id,
346
372
  user_id,
@@ -384,13 +410,13 @@ async function exportPendingAuditLogs() {
384
410
  ...process.env.SIEM_EXPORT_TOKEN ? { authorization: `Bearer ${process.env.SIEM_EXPORT_TOKEN}` } : {}
385
411
  },
386
412
  body
387
- });
413
+ }, { allowHttp: appConfig.env !== "production" });
388
414
  if (!response.ok) {
389
415
  throw new Error(`SIEM export failed with status ${response.status}.`);
390
416
  }
391
417
  const ids = rows.map((row) => row.id);
392
418
  for (const id of ids) {
393
- await connection_default`UPDATE audit_log SET exported_at = NOW() WHERE id = ${id}`;
419
+ await repositoryConnection`UPDATE audit_log SET exported_at = NOW() WHERE id = ${id}`;
394
420
  }
395
421
  return rows.length;
396
422
  });
@@ -550,7 +576,8 @@ class ConfigStore {
550
576
  }
551
577
  var requiredDependencyKeys = [
552
578
  "container",
553
- "cache"
579
+ "cache",
580
+ "storage"
554
581
  ];
555
582
  function getRequiredDependency(dependencies, key) {
556
583
  const dependency = dependencies[key];
@@ -643,6 +670,60 @@ import { SamlProvider } from "@getstrata/core/auth/oauth/samlProvider";
643
670
  // ../../src/modules/user/apiTokenRepository.ts
644
671
  import { BaseRepository } from "@getstrata/core/database";
645
672
 
673
+ // ../../src/config/database.ts
674
+ function readInteger(name, fallback) {
675
+ const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
676
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
677
+ }
678
+ var databaseConfig = {
679
+ url: process.env.DATABASE_URL ?? "",
680
+ poolMax: readInteger("DB_POOL_MAX", 10),
681
+ idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
682
+ maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
683
+ connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
684
+ };
685
+
686
+ // ../../src/db/connection/createConnection.ts
687
+ var {SQL } = globalThis.Bun;
688
+ function createDatabaseConnection(config) {
689
+ if (!config.url) {
690
+ throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
691
+ }
692
+ return new SQL({
693
+ url: config.url,
694
+ max: config.poolMax,
695
+ idleTimeout: config.idleTimeoutSeconds,
696
+ maxLifetime: config.maxLifetimeSeconds,
697
+ connectionTimeout: config.connectionTimeoutSeconds
698
+ });
699
+ }
700
+
701
+ // ../../src/db/connection/index.ts
702
+ var connectionHolder = {
703
+ connection: null
704
+ };
705
+ function getDatabase() {
706
+ if (!connectionHolder.connection) {
707
+ connectionHolder.connection = createDatabaseConnection(databaseConfig);
708
+ registerDefaultDatabasePool(connectionHolder.connection);
709
+ }
710
+ return connectionHolder.connection;
711
+ }
712
+ function getDb() {
713
+ getDatabase();
714
+ return getDefaultDatabaseQuery();
715
+ }
716
+ var db = new Proxy(function database() {}, {
717
+ apply(_target, _thisArg, args) {
718
+ return getDb()(...args);
719
+ },
720
+ get(_target, property) {
721
+ const connection = getDb();
722
+ const value = connection[property];
723
+ return typeof value === "function" ? value.bind(connection) : value;
724
+ }
725
+ });
726
+
646
727
  // ../../src/modules/user/apiTokenTable.ts
647
728
  import { defineTable } from "@getstrata/core/database";
648
729
  var apiTokenTable = defineTable({
@@ -1741,13 +1822,10 @@ function resolveApplicationQueue() {
1741
1822
  // ../../src/core/jobs/dispatchWebhookJob.ts
1742
1823
  import { createHmac as createHmac2 } from "crypto";
1743
1824
  class DispatchWebhookJob extends Job {
1744
- constructor() {
1745
- super();
1746
- }
1747
1825
  maxAttempts = 3;
1748
1826
  backoffMs = 2000;
1749
1827
  async handle(payload) {
1750
- const rows = await connection_default`
1828
+ const rows = await repositoryConnection`
1751
1829
  SELECT id, url, secret
1752
1830
  FROM webhook
1753
1831
  WHERE id = ${payload.webhookId} AND active = TRUE
@@ -1770,14 +1848,14 @@ class DispatchWebhookJob extends Job {
1770
1848
  "x-workhub-signature": signature
1771
1849
  },
1772
1850
  body
1773
- });
1851
+ }, { allowHttp: appConfig.env !== "production" });
1774
1852
  responseStatus = response.status;
1775
1853
  if (!response.ok) {
1776
1854
  throw new Error(`Webhook delivery failed with status ${response.status}.`);
1777
1855
  }
1778
1856
  } catch (error) {
1779
1857
  errorMessage = error instanceof Error ? error.message : String(error);
1780
- await connection_default`
1858
+ await repositoryConnection`
1781
1859
  INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
1782
1860
  VALUES (
1783
1861
  ${webhook.id},
@@ -1789,7 +1867,7 @@ class DispatchWebhookJob extends Job {
1789
1867
  `;
1790
1868
  throw error instanceof Error ? error : new Error(errorMessage);
1791
1869
  }
1792
- await connection_default`
1870
+ await repositoryConnection`
1793
1871
  INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
1794
1872
  VALUES (
1795
1873
  ${webhook.id},
@@ -2369,26 +2447,6 @@ function indexMorphToRelation(children, parentsByType, relation) {
2369
2447
  return result;
2370
2448
  }
2371
2449
 
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
2450
  // ../../src/core/database/whereBuilder.ts
2393
2451
  class WhereBuilder {
2394
2452
  nodes = [];
@@ -3816,6 +3874,128 @@ var queueProvider = {
3816
3874
  };
3817
3875
  var queue_default = queueProvider;
3818
3876
 
3877
+ // ../../src/core/storage/storage.ts
3878
+ import { mkdir, readFile, unlink, writeFile } from "fs/promises";
3879
+ import { dirname, join as join3 } from "path";
3880
+ var {S3Client } = globalThis.Bun;
3881
+
3882
+ class LocalStorageDriver {
3883
+ rootDirectory;
3884
+ constructor(rootDirectory) {
3885
+ this.rootDirectory = rootDirectory;
3886
+ }
3887
+ resolveRootDirectory() {
3888
+ return this.rootDirectory ?? process.env.STORAGE_PATH ?? "storage";
3889
+ }
3890
+ resolvePath(path) {
3891
+ return join3(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
3892
+ }
3893
+ async put(path, contents) {
3894
+ const absolutePath = this.resolvePath(path);
3895
+ await mkdir(dirname(absolutePath), { recursive: true });
3896
+ await writeFile(absolutePath, contents);
3897
+ return path;
3898
+ }
3899
+ async get(path) {
3900
+ try {
3901
+ return await readFile(this.resolvePath(path));
3902
+ } catch {
3903
+ return null;
3904
+ }
3905
+ }
3906
+ async delete(path) {
3907
+ try {
3908
+ await unlink(this.resolvePath(path));
3909
+ return true;
3910
+ } catch {
3911
+ return false;
3912
+ }
3913
+ }
3914
+ }
3915
+
3916
+ class S3StorageDriver {
3917
+ client;
3918
+ constructor(client) {
3919
+ this.client = client;
3920
+ }
3921
+ async put(path, contents) {
3922
+ await this.client.write(path.replace(/^\/+/, ""), contents);
3923
+ return path;
3924
+ }
3925
+ async get(path) {
3926
+ const normalizedPath = path.replace(/^\/+/, "");
3927
+ const file = this.client.file(normalizedPath);
3928
+ if (!await file.exists()) {
3929
+ return null;
3930
+ }
3931
+ return new Uint8Array(await file.arrayBuffer());
3932
+ }
3933
+ async delete(path) {
3934
+ try {
3935
+ await this.client.unlink(path.replace(/^\/+/, ""));
3936
+ return true;
3937
+ } catch {
3938
+ return false;
3939
+ }
3940
+ }
3941
+ }
3942
+
3943
+ class StorageManager {
3944
+ driver;
3945
+ constructor(driver) {
3946
+ this.driver = driver;
3947
+ }
3948
+ put(path, contents) {
3949
+ return this.driver.put(path, contents);
3950
+ }
3951
+ get(path) {
3952
+ return this.driver.get(path);
3953
+ }
3954
+ delete(path) {
3955
+ return this.driver.delete(path);
3956
+ }
3957
+ }
3958
+ function resolveS3Config() {
3959
+ const accessKeyId = process.env.AWS_ACCESS_KEY_ID?.trim();
3960
+ const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY?.trim();
3961
+ const bucket = process.env.AWS_BUCKET?.trim();
3962
+ if (!accessKeyId || !secretAccessKey || !bucket) {
3963
+ throw new Error('STORAGE_DRIVER="s3" requires AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_BUCKET.');
3964
+ }
3965
+ return {
3966
+ accessKeyId,
3967
+ secretAccessKey,
3968
+ bucket,
3969
+ ...process.env.AWS_REGION?.trim() ? { region: process.env.AWS_REGION.trim() } : {},
3970
+ ...process.env.AWS_ENDPOINT?.trim() ? { endpoint: process.env.AWS_ENDPOINT.trim() } : {}
3971
+ };
3972
+ }
3973
+ function createS3Client(config = resolveS3Config()) {
3974
+ return new S3Client({
3975
+ accessKeyId: config.accessKeyId,
3976
+ secretAccessKey: config.secretAccessKey,
3977
+ bucket: config.bucket,
3978
+ ...config.region ? { region: config.region } : {},
3979
+ ...config.endpoint ? { endpoint: config.endpoint } : {}
3980
+ });
3981
+ }
3982
+ function createStorageDriver() {
3983
+ const driver = process.env.STORAGE_DRIVER ?? "local";
3984
+ if (driver === "s3") {
3985
+ return new S3StorageDriver(createS3Client());
3986
+ }
3987
+ return new LocalStorageDriver;
3988
+ }
3989
+
3990
+ // ../../src/bootstrap/providers/storage.ts
3991
+ var storageProvider = {
3992
+ name: "core.storage",
3993
+ register({ dependencies }) {
3994
+ dependencies.storage = new StorageManager(createStorageDriver());
3995
+ }
3996
+ };
3997
+ var storage_default = storageProvider;
3998
+
3819
3999
  // ../../src/config/frontend.ts
3820
4000
  function readFrontendMode() {
3821
4001
  const mode = (process.env.FRONTEND_MODE ?? "api").trim();
@@ -3842,9 +4022,9 @@ function currentRequestMeta() {
3842
4022
  }
3843
4023
 
3844
4024
  // ../../src/core/view/etaViewEngine.ts
3845
- import { join as join3 } from "path";
4025
+ import { join as join4 } from "path";
3846
4026
  import { Eta } from "eta";
3847
- var DEFAULT_VIEWS_DIRECTORY = join3(process.cwd(), "resources/views");
4027
+ var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
3848
4028
  var DEFAULT_LAYOUT = "layouts/app.eta";
3849
4029
 
3850
4030
  class EtaViewEngine {
@@ -4069,6 +4249,7 @@ var viewProvider = {
4069
4249
  var coreProviders = [
4070
4250
  config_default,
4071
4251
  cache_default,
4252
+ storage_default,
4072
4253
  auth_default,
4073
4254
  events_default,
4074
4255
  policy_default,
@@ -4169,7 +4350,7 @@ function createAppContext() {
4169
4350
  return appContext;
4170
4351
  }
4171
4352
  // ../../src/bootstrap/createWebRoutes.ts
4172
- import { join as join4 } from "path";
4353
+ import { join as join5 } from "path";
4173
4354
 
4174
4355
  // ../../src/core/http/middleware.ts
4175
4356
  function isRouteHandler(value) {
@@ -4503,7 +4684,7 @@ function createWebRoutes(dependencies) {
4503
4684
  registerRoute("GET", "/assets/*", ["global", "web"]);
4504
4685
  const pathname = new URL(request.url).pathname;
4505
4686
  const relativePath = pathname.replace(/^\//, "");
4506
- const file = Bun.file(join4(process.cwd(), "public", relativePath));
4687
+ const file = Bun.file(join5(process.cwd(), "public", relativePath));
4507
4688
  if (!await file.exists()) {
4508
4689
  return htmlResponse("Not Found", { status: 404 });
4509
4690
  }
@@ -0,0 +1,14 @@
1
+ import { BaseRepository } from "@getstrata/core/database";
2
+ import type { OrganizationRecord } from "./types";
3
+ declare class OrganizationRepository extends BaseRepository<OrganizationRecord, "id"> {
4
+ constructor();
5
+ findBySlug(slug: string): Promise<OrganizationRecord | null>;
6
+ listForTenant(options: {
7
+ limit: number;
8
+ offset: number;
9
+ tenantId?: number;
10
+ }): Promise<OrganizationRecord[]>;
11
+ countForTenant(tenantId?: number): Promise<number>;
12
+ findForTenantOrThrow(id: number, tenantId?: number): Promise<OrganizationRecord>;
13
+ }
14
+ export default OrganizationRepository;
@@ -0,0 +1,3 @@
1
+ import type { OrganizationRecord } from "./types";
2
+ declare const organizationTable: import("@getstrata/core/database").TableDefinition<OrganizationRecord, "id">;
3
+ export { organizationTable };
@@ -0,0 +1,10 @@
1
+ interface OrganizationRecord {
2
+ id: number;
3
+ tenant_id: number;
4
+ name: string;
5
+ slug: string;
6
+ created_at: Date;
7
+ updated_at: Date;
8
+ deleted_at: Date | null;
9
+ }
10
+ export type { OrganizationRecord };