@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
@@ -11,75 +11,77 @@ var appConfig = {
11
11
  apiPrefix: process.env.API_PREFIX ?? "/api/v1"
12
12
  };
13
13
 
14
- // ../../src/config/database.ts
15
- function readInteger(name, fallback) {
16
- const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
17
- return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
18
- }
19
- var databaseConfig = {
20
- url: process.env.DATABASE_URL ?? "",
21
- poolMax: readInteger("DB_POOL_MAX", 10),
22
- idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
23
- maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
24
- connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
14
+ // ../../src/core/database/boundConnection.ts
15
+ var boundConnectionHolder = {
16
+ connection: null
25
17
  };
18
+ function getBoundDatabaseConnection() {
19
+ return boundConnectionHolder.connection;
20
+ }
26
21
 
27
22
  // ../../src/core/database/connectionContext.ts
28
23
  import { AsyncLocalStorage } from "async_hooks";
29
24
  var activeConnection = new AsyncLocalStorage;
30
- function runWithDatabaseConnection(connection, callback) {
31
- return activeConnection.run(connection, callback);
32
- }
33
25
  function getActiveDatabaseConnection(fallback) {
34
26
  return activeConnection.getStore() ?? fallback;
35
27
  }
36
28
 
37
- // ../../src/db/connection/createConnection.ts
38
- var {SQL } = globalThis.Bun;
39
- function createDatabaseConnection(config) {
40
- if (!config.url) {
41
- throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
42
- }
43
- return new SQL({
44
- url: config.url,
45
- max: config.poolMax,
46
- idleTimeout: config.idleTimeoutSeconds,
47
- maxLifetime: config.maxLifetimeSeconds,
48
- connectionTimeout: config.connectionTimeoutSeconds
29
+ // ../../src/core/database/queryProxy.ts
30
+ var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
31
+ function createDatabaseQueryProxy(pool) {
32
+ function resolveDatabase() {
33
+ return getActiveDatabaseConnection(pool);
34
+ }
35
+ function resolveDatabaseForProperty(property) {
36
+ if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
37
+ return pool;
38
+ }
39
+ return resolveDatabase();
40
+ }
41
+ return new Proxy(function database() {}, {
42
+ apply(_target, _thisArg, args) {
43
+ return resolveDatabase()(...args);
44
+ },
45
+ get(_target, property) {
46
+ const connection = resolveDatabaseForProperty(property);
47
+ const value = connection[property];
48
+ return typeof value === "function" ? value.bind(connection) : value;
49
+ }
49
50
  });
50
51
  }
51
52
 
52
- // ../../src/db/connection/index.ts
53
- var connectionHolder = {
53
+ // ../../src/core/database/defaultConnection.ts
54
+ var defaultPool = {
54
55
  connection: null
55
56
  };
56
- function getDatabase() {
57
- if (!connectionHolder.connection) {
58
- connectionHolder.connection = createDatabaseConnection(databaseConfig);
59
- }
60
- return connectionHolder.connection;
61
- }
62
- var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
63
- function resolveDatabase() {
64
- return getActiveDatabaseConnection(getDatabase());
57
+ var defaultQuery = {
58
+ connection: null
59
+ };
60
+ function registerDefaultDatabasePool(connection) {
61
+ defaultPool.connection = connection;
62
+ defaultQuery.connection = createDatabaseQueryProxy(connection);
65
63
  }
66
- function resolveDatabaseForProperty(property) {
67
- if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
68
- return getDatabase();
64
+ function getDefaultDatabaseQuery() {
65
+ if (!defaultQuery.connection) {
66
+ throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
69
67
  }
70
- return resolveDatabase();
68
+ return defaultQuery.connection;
69
+ }
70
+
71
+ // ../../src/core/database/repositoryConnection.ts
72
+ function resolveRepositoryConnection() {
73
+ return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
71
74
  }
72
- var db = new Proxy(function database() {}, {
75
+ var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
73
76
  apply(_target, _thisArg, args) {
74
- return resolveDatabase()(...args);
77
+ return resolveRepositoryConnection()(...args);
75
78
  },
76
79
  get(_target, property) {
77
- const connection = resolveDatabaseForProperty(property);
80
+ const connection = resolveRepositoryConnection();
78
81
  const value = connection[property];
79
82
  return typeof value === "function" ? value.bind(connection) : value;
80
83
  }
81
84
  });
82
- var connection_default = db;
83
85
 
84
86
  // ../../src/core/queue/index.ts
85
87
  class Job {
@@ -88,56 +90,8 @@ class Job {
88
90
  priority;
89
91
  }
90
92
 
91
- class SyncQueue {
92
- async dispatch(job, payload) {
93
- await job.handle(payload);
94
- }
95
- }
96
-
97
- class AsyncQueue {
98
- async dispatch(job, payload) {
99
- setTimeout(() => {
100
- job.handle(payload).catch((error) => {
101
- console.error("[AsyncQueue] Job failed:", error);
102
- });
103
- }, 0);
104
- }
105
- }
106
- function createQueue(driver) {
107
- return driver === "async" ? new AsyncQueue : new SyncQueue;
108
- }
109
-
110
- // ../../src/core/security/safeFetch.ts
111
- var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
112
- async function safeFetch(input, init = {}, options = {}) {
113
- const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
114
- const maxRedirects = options.maxRedirects ?? 0;
115
- const controller = new AbortController;
116
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
117
- try {
118
- let currentUrl = input;
119
- let redirectCount = 0;
120
- while (true) {
121
- const response = await fetch(currentUrl, {
122
- ...init,
123
- signal: controller.signal,
124
- redirect: "manual"
125
- });
126
- if (response.status >= 300 && response.status < 400) {
127
- const location = response.headers.get("location");
128
- if (!location || redirectCount >= maxRedirects) {
129
- return response;
130
- }
131
- currentUrl = new URL(location, currentUrl).toString();
132
- redirectCount += 1;
133
- continue;
134
- }
135
- return response;
136
- }
137
- } finally {
138
- clearTimeout(timeout);
139
- }
140
- }
93
+ // ../../src/core/security/safeUrl.ts
94
+ import { lookup as dnsLookupImpl } from "dns/promises";
141
95
 
142
96
  // ../../src/core/errors/http.ts
143
97
  class HttpError extends Error {
@@ -206,6 +160,7 @@ class PreconditionFailedError extends HttpError {
206
160
  }
207
161
 
208
162
  // ../../src/core/security/safeUrl.ts
163
+ var dnsLookup = dnsLookupImpl;
209
164
  var BLOCKED_HOSTNAMES = new Set([
210
165
  "localhost",
211
166
  "127.0.0.1",
@@ -277,16 +232,68 @@ function assertSafeOutboundUrl(rawUrl, options = {}) {
277
232
  }
278
233
  return parsed;
279
234
  }
235
+ function isBlockedIpAddress(address) {
236
+ return isBlockedHostname(address.trim().toLowerCase());
237
+ }
238
+ async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
239
+ const parsed = assertSafeOutboundUrl(rawUrl, options);
240
+ if (options.resolveDns === false) {
241
+ return parsed;
242
+ }
243
+ const hostname = parsed.hostname.trim().toLowerCase();
244
+ const results = await dnsLookup(hostname, { all: true, verbatim: true });
245
+ if (results.some((result) => isBlockedIpAddress(result.address))) {
246
+ throw new BadRequestError("Webhook URL targets a blocked host.");
247
+ }
248
+ return parsed;
249
+ }
250
+ function setDnsLookupForTests(lookupFn) {
251
+ dnsLookup = lookupFn;
252
+ }
253
+ function resetDnsLookupForTests() {
254
+ dnsLookup = dnsLookupImpl;
255
+ }
256
+
257
+ // ../../src/core/security/safeFetch.ts
258
+ var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
259
+ async function safeFetch(input, init = {}, options = {}) {
260
+ const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
261
+ const maxRedirects = options.maxRedirects ?? 0;
262
+ const resolveDns = options.resolveDns ?? appConfig.env === "production";
263
+ const urlOptions = { allowHttp: options.allowHttp, resolveDns };
264
+ const controller = new AbortController;
265
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
266
+ try {
267
+ let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
268
+ let redirectCount = 0;
269
+ while (true) {
270
+ const response = await fetch(currentUrl, {
271
+ ...init,
272
+ signal: controller.signal,
273
+ redirect: "manual"
274
+ });
275
+ if (response.status >= 300 && response.status < 400) {
276
+ const location = response.headers.get("location");
277
+ if (!location || redirectCount >= maxRedirects) {
278
+ return response;
279
+ }
280
+ currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
281
+ redirectCount += 1;
282
+ continue;
283
+ }
284
+ return response;
285
+ }
286
+ } finally {
287
+ clearTimeout(timeout);
288
+ }
289
+ }
280
290
 
281
291
  // ../../src/core/jobs/dispatchWebhookJob.ts
282
292
  class DispatchWebhookJob extends Job {
283
- constructor() {
284
- super();
285
- }
286
293
  maxAttempts = 3;
287
294
  backoffMs = 2000;
288
295
  async handle(payload) {
289
- const rows = await connection_default`
296
+ const rows = await repositoryConnection`
290
297
  SELECT id, url, secret
291
298
  FROM webhook
292
299
  WHERE id = ${payload.webhookId} AND active = TRUE
@@ -309,14 +316,14 @@ class DispatchWebhookJob extends Job {
309
316
  "x-workhub-signature": signature
310
317
  },
311
318
  body
312
- });
319
+ }, { allowHttp: appConfig.env !== "production" });
313
320
  responseStatus = response.status;
314
321
  if (!response.ok) {
315
322
  throw new Error(`Webhook delivery failed with status ${response.status}.`);
316
323
  }
317
324
  } catch (error) {
318
325
  errorMessage = error instanceof Error ? error.message : String(error);
319
- await connection_default`
326
+ await repositoryConnection`
320
327
  INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
321
328
  VALUES (
322
329
  ${webhook.id},
@@ -328,7 +335,7 @@ class DispatchWebhookJob extends Job {
328
335
  `;
329
336
  throw error instanceof Error ? error : new Error(errorMessage);
330
337
  }
331
- await connection_default`
338
+ await repositoryConnection`
332
339
  INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
333
340
  VALUES (
334
341
  ${webhook.id},