@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.
- package/README.md +16 -6
- package/dist/bootstrap/contracts.d.ts +2 -0
- package/dist/bootstrap/httpKernel.d.ts +1 -2
- package/dist/core/admin/registry.d.ts +1 -0
- package/dist/core/auth/membershipService.d.ts +5 -1
- package/dist/core/database/baseRepository.d.ts +6 -1
- package/dist/core/database/connectionContext.d.ts +2 -1
- package/dist/core/database/defaultConnection.d.ts +6 -0
- package/dist/core/database/queryProxy.d.ts +3 -0
- package/dist/core/database/repositoryConnection.d.ts +3 -3
- package/dist/core/jobs/dispatchWebhookJob.d.ts +0 -1
- package/dist/core/security/safeFetch.d.ts +2 -0
- package/dist/core/security/safeUrl.d.ts +16 -1
- package/dist/core/storage/storage.d.ts +6 -3
- package/dist/core/tenant/tenantDatabaseScope.d.ts +2 -1
- package/dist/db/connection/index.d.ts +1 -1
- package/dist/entries/auth/accessControl.js +1 -113
- package/dist/entries/auth/authContext.js +1 -15
- package/dist/entries/auth/guard.js +1 -3044
- package/dist/entries/auth/membershipContext.js +1 -276
- package/dist/entries/auth/membershipScope.js +1 -390
- package/dist/entries/auth/membershipService.js +1 -480
- package/dist/entries/auth/policy.js +1 -134
- package/dist/entries/database.js +1 -2398
- package/dist/entries/http/csrfToken.js +0 -6
- package/dist/entries/http/middleware.js +1 -68
- package/dist/entries/http/requestMetaContext.js +1 -18
- package/dist/entries/http/webErrorResponse.js +94 -563
- package/dist/entries/http/webFormRequest.js +0 -131
- package/dist/entries/http.js +1 -4091
- package/dist/entries/jobs/dispatchWebhookJob.js +109 -102
- package/dist/entries/queue/createAppQueue.js +111 -631
- package/dist/entries/queue/jobRunner.js +0 -3
- package/dist/entries/queue/publicQueue.js +45 -546
- package/dist/entries/queue/queueMetrics.js +111 -631
- package/dist/entries/security/safeUrl.js +29 -0
- package/dist/entries/security/securityEvents.js +1 -41
- package/dist/entries/storage/storage.js +14 -4
- package/dist/entries/tenant/tenantContext.js +1 -30
- package/dist/entries/tenant/tenantMiddleware.js +1 -312
- package/dist/entries/tracing/traceContext.js +1 -15
- package/dist/entries/view.js +94 -563
- package/dist/framework/public-api.d.ts +36 -4
- package/dist/index.js +2270 -1666
- package/dist/modules/user/repository.d.ts +1 -0
- 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/
|
|
15
|
-
|
|
16
|
-
|
|
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/
|
|
38
|
-
var
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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/
|
|
53
|
-
var
|
|
53
|
+
// ../../src/core/database/defaultConnection.ts
|
|
54
|
+
var defaultPool = {
|
|
54
55
|
connection: null
|
|
55
56
|
};
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
67
|
-
if (
|
|
68
|
-
|
|
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
|
|
68
|
+
return defaultQuery.connection;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ../../src/core/database/repositoryConnection.ts
|
|
72
|
+
function resolveRepositoryConnection() {
|
|
73
|
+
return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
|
|
71
74
|
}
|
|
72
|
-
var
|
|
75
|
+
var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
|
|
73
76
|
apply(_target, _thisArg, args) {
|
|
74
|
-
return
|
|
77
|
+
return resolveRepositoryConnection()(...args);
|
|
75
78
|
},
|
|
76
79
|
get(_target, property) {
|
|
77
|
-
const connection =
|
|
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
|
-
|
|
92
|
-
|
|
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
|
|
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
|
|
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
|
|
338
|
+
await repositoryConnection`
|
|
332
339
|
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
333
340
|
VALUES (
|
|
334
341
|
${webhook.id},
|