@getstrata/bootstrap 0.2.8 → 0.2.10
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/dist/bootstrap/membershipService.d.ts +3 -0
- package/dist/bootstrap/public-api.d.ts +3 -1
- package/dist/bootstrap/queue/defaultJobs.d.ts +2 -0
- package/dist/core/auth/authContext.d.ts +1 -2
- package/dist/core/auth/membershipContext.d.ts +1 -2
- package/dist/core/auth/membershipService.d.ts +1 -2
- package/dist/core/http/requestMetaContext.d.ts +1 -2
- package/dist/core/queue/createAppQueue.d.ts +3 -3
- package/dist/core/runtime/asyncContextStore.d.ts +3 -0
- package/dist/core/tenant/tenantContext.d.ts +1 -2
- package/dist/core/tracing/traceContext.d.ts +1 -2
- package/dist/entries/applicationRegistry.js +15 -2
- package/dist/entries/context.js +272 -321
- package/dist/entries/createWebRoutes.js +18 -6
- package/dist/entries/http/securedRouteModelBinding.js +19 -179
- package/dist/entries/membershipService.js +317 -0
- package/dist/entries/providers/view.js +18 -6
- package/dist/entries/providers.js +279 -413
- package/dist/entries/queue/defaultJobs.js +390 -0
- package/dist/index.js +288 -125
- package/package.json +13 -3
|
@@ -106,9 +106,22 @@ var databaseConfig = {
|
|
|
106
106
|
connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
|
|
107
107
|
};
|
|
108
108
|
|
|
109
|
-
// ../../src/core/
|
|
109
|
+
// ../../src/core/runtime/asyncContextStore.ts
|
|
110
110
|
import { AsyncLocalStorage } from "async_hooks";
|
|
111
|
-
|
|
111
|
+
function createAsyncContextStore(key) {
|
|
112
|
+
const symbol = Symbol.for(key);
|
|
113
|
+
const globalRecord = globalThis;
|
|
114
|
+
const existing = globalRecord[symbol];
|
|
115
|
+
if (existing) {
|
|
116
|
+
return existing;
|
|
117
|
+
}
|
|
118
|
+
const store = new AsyncLocalStorage;
|
|
119
|
+
globalRecord[symbol] = store;
|
|
120
|
+
return store;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ../../src/core/database/connectionContext.ts
|
|
124
|
+
var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
|
|
112
125
|
function getActiveDatabaseConnection(fallback) {
|
|
113
126
|
return activeConnection.getStore() ?? fallback;
|
|
114
127
|
}
|
|
@@ -195,6 +208,7 @@ var db = new Proxy(function database() {}, {
|
|
|
195
208
|
return typeof value === "function" ? value.bind(connection) : value;
|
|
196
209
|
}
|
|
197
210
|
});
|
|
211
|
+
var connection_default = db;
|
|
198
212
|
|
|
199
213
|
// ../../src/modules/user/apiTokenTable.ts
|
|
200
214
|
import { defineTable } from "@getstrata/core/database";
|
|
@@ -423,8 +437,7 @@ class PreconditionFailedError extends HttpError {
|
|
|
423
437
|
}
|
|
424
438
|
|
|
425
439
|
// ../../src/core/auth/authContext.ts
|
|
426
|
-
|
|
427
|
-
var authContext = new AsyncLocalStorage2;
|
|
440
|
+
var authContext = createAsyncContextStore("@getstrata/authContext");
|
|
428
441
|
function currentAuthUser() {
|
|
429
442
|
return authContext.getStore() ?? null;
|
|
430
443
|
}
|
|
@@ -1298,6 +1311,9 @@ function discoverListeners() {
|
|
|
1298
1311
|
return appListeners;
|
|
1299
1312
|
}
|
|
1300
1313
|
|
|
1314
|
+
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
1315
|
+
import { resolveApplicationCache as resolveApplicationCache2, resolveApplicationQueue } from "@getstrata/core";
|
|
1316
|
+
|
|
1301
1317
|
// ../../src/bootstrap/discoverModules.ts
|
|
1302
1318
|
import { readdirSync as readdirSync2 } from "fs";
|
|
1303
1319
|
import { join as join2 } from "path";
|
|
@@ -1340,410 +1356,18 @@ class Job {
|
|
|
1340
1356
|
priority;
|
|
1341
1357
|
}
|
|
1342
1358
|
|
|
1343
|
-
// ../../src/core/jobs/invalidateCacheTagsJob.ts
|
|
1344
|
-
class InvalidateCacheTagsJob extends Job {
|
|
1345
|
-
cache;
|
|
1346
|
-
constructor(cache) {
|
|
1347
|
-
super();
|
|
1348
|
-
this.cache = cache;
|
|
1349
|
-
}
|
|
1350
|
-
async handle(payload) {
|
|
1351
|
-
await this.cache.tags(...payload.tags).flush();
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
var invalidateCacheTagsJob_default = InvalidateCacheTagsJob;
|
|
1355
|
-
|
|
1356
|
-
// ../../src/core/logging/logger.ts
|
|
1357
|
-
class Logger {
|
|
1358
|
-
channel;
|
|
1359
|
-
constructor(channel = "app") {
|
|
1360
|
-
this.channel = channel;
|
|
1361
|
-
}
|
|
1362
|
-
write(level, message, context = {}) {
|
|
1363
|
-
const entry = {
|
|
1364
|
-
level,
|
|
1365
|
-
channel: this.channel,
|
|
1366
|
-
message,
|
|
1367
|
-
timestamp: new Date().toISOString(),
|
|
1368
|
-
...context
|
|
1369
|
-
};
|
|
1370
|
-
const line = JSON.stringify(entry);
|
|
1371
|
-
if (level === "error") {
|
|
1372
|
-
console.error(line);
|
|
1373
|
-
return;
|
|
1374
|
-
}
|
|
1375
|
-
console.log(line);
|
|
1376
|
-
}
|
|
1377
|
-
debug(message, context) {
|
|
1378
|
-
this.write("debug", message, context);
|
|
1379
|
-
}
|
|
1380
|
-
info(message, context) {
|
|
1381
|
-
this.write("info", message, context);
|
|
1382
|
-
}
|
|
1383
|
-
warn(message, context) {
|
|
1384
|
-
this.write("warn", message, context);
|
|
1385
|
-
}
|
|
1386
|
-
error(message, context) {
|
|
1387
|
-
this.write("error", message, context);
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
var appLogger = new Logger("app");
|
|
1391
|
-
|
|
1392
|
-
// ../../src/bootstrap/contracts.ts
|
|
1393
|
-
class ServiceContainer {
|
|
1394
|
-
services = new Map;
|
|
1395
|
-
singletonFactories = new Map;
|
|
1396
|
-
bindings = new Map;
|
|
1397
|
-
set(key, value) {
|
|
1398
|
-
this.singletonFactories.delete(key);
|
|
1399
|
-
this.bindings.delete(key);
|
|
1400
|
-
this.services.set(key, value);
|
|
1401
|
-
return value;
|
|
1402
|
-
}
|
|
1403
|
-
singleton(key, factory) {
|
|
1404
|
-
this.bindings.delete(key);
|
|
1405
|
-
this.services.delete(key);
|
|
1406
|
-
this.singletonFactories.set(key, factory);
|
|
1407
|
-
}
|
|
1408
|
-
bind(key, factory) {
|
|
1409
|
-
this.singletonFactories.delete(key);
|
|
1410
|
-
this.services.delete(key);
|
|
1411
|
-
this.bindings.set(key, factory);
|
|
1412
|
-
}
|
|
1413
|
-
get(key) {
|
|
1414
|
-
if (this.services.has(key)) {
|
|
1415
|
-
return this.services.get(key);
|
|
1416
|
-
}
|
|
1417
|
-
const singletonFactory = this.singletonFactories.get(key);
|
|
1418
|
-
if (singletonFactory) {
|
|
1419
|
-
const value = singletonFactory(this);
|
|
1420
|
-
this.services.set(key, value);
|
|
1421
|
-
return value;
|
|
1422
|
-
}
|
|
1423
|
-
const binding = this.bindings.get(key);
|
|
1424
|
-
if (binding) {
|
|
1425
|
-
return binding(this);
|
|
1426
|
-
}
|
|
1427
|
-
throw new Error(`Service "${key}" is not registered.`);
|
|
1428
|
-
}
|
|
1429
|
-
resolve(key) {
|
|
1430
|
-
return this.get(key);
|
|
1431
|
-
}
|
|
1432
|
-
has(key) {
|
|
1433
|
-
return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
class ConfigStore {
|
|
1438
|
-
values = new Map;
|
|
1439
|
-
set(key, value) {
|
|
1440
|
-
this.values.set(key, value);
|
|
1441
|
-
return value;
|
|
1442
|
-
}
|
|
1443
|
-
get(key) {
|
|
1444
|
-
return this.values.get(key);
|
|
1445
|
-
}
|
|
1446
|
-
require(key) {
|
|
1447
|
-
if (!this.values.has(key)) {
|
|
1448
|
-
throw new Error(`Config key "${key}" is not defined.`);
|
|
1449
|
-
}
|
|
1450
|
-
return this.values.get(key);
|
|
1451
|
-
}
|
|
1452
|
-
has(key) {
|
|
1453
|
-
return this.values.has(key);
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
var requiredDependencyKeys = [
|
|
1457
|
-
"container",
|
|
1458
|
-
"cache",
|
|
1459
|
-
"storage"
|
|
1460
|
-
];
|
|
1461
|
-
function getRequiredDependency(dependencies, key) {
|
|
1462
|
-
const dependency = dependencies[key];
|
|
1463
|
-
if (dependency === undefined) {
|
|
1464
|
-
throw new Error(`Required dependency "${key}" is not registered.`);
|
|
1465
|
-
}
|
|
1466
|
-
return dependency;
|
|
1467
|
-
}
|
|
1468
|
-
function assertAppDependenciesComplete(dependencies) {
|
|
1469
|
-
for (const key of requiredDependencyKeys) {
|
|
1470
|
-
getRequiredDependency(dependencies, key);
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
function resolveService(dependencies, token) {
|
|
1474
|
-
return dependencies.container.resolve(token);
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
// ../../src/bootstrap/applicationRegistry.ts
|
|
1478
|
-
var activeContext;
|
|
1479
|
-
function setActiveApplicationContext(context) {
|
|
1480
|
-
activeContext = context;
|
|
1481
|
-
}
|
|
1482
|
-
function requireActiveApplicationContext() {
|
|
1483
|
-
if (!activeContext) {
|
|
1484
|
-
throw new Error("The application context has not been bootstrapped.");
|
|
1485
|
-
}
|
|
1486
|
-
return activeContext;
|
|
1487
|
-
}
|
|
1488
|
-
function resolveApplicationCache() {
|
|
1489
|
-
return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
|
|
1490
|
-
}
|
|
1491
|
-
function resolveApplicationQueue() {
|
|
1492
|
-
return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
|
|
1493
|
-
}
|
|
1494
|
-
function resolveApplicationAuth() {
|
|
1495
|
-
return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
|
|
1496
|
-
}
|
|
1497
|
-
function resolveApplicationPolicyGate() {
|
|
1498
|
-
return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
|
|
1499
|
-
}
|
|
1500
|
-
function resolveApplicationConfig() {
|
|
1501
|
-
return requireActiveApplicationContext().config;
|
|
1502
|
-
}
|
|
1503
|
-
function resolveApplicationLogger() {
|
|
1504
|
-
return appLogger;
|
|
1505
|
-
}
|
|
1506
|
-
function resolveApplicationDependencies() {
|
|
1507
|
-
return requireActiveApplicationContext().dependencies;
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
1511
|
-
import { createHmac as createHmac2 } from "crypto";
|
|
1512
|
-
|
|
1513
|
-
// ../../src/core/database/boundConnection.ts
|
|
1514
|
-
var boundConnectionHolder = {
|
|
1515
|
-
connection: null
|
|
1516
|
-
};
|
|
1517
|
-
function getBoundDatabaseConnection() {
|
|
1518
|
-
return boundConnectionHolder.connection;
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
// ../../src/core/database/repositoryConnection.ts
|
|
1522
|
-
function resolveRepositoryConnection() {
|
|
1523
|
-
return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
|
|
1524
|
-
}
|
|
1525
|
-
var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
|
|
1526
|
-
apply(_target, _thisArg, args) {
|
|
1527
|
-
return resolveRepositoryConnection()(...args);
|
|
1528
|
-
},
|
|
1529
|
-
get(_target, property) {
|
|
1530
|
-
const connection = resolveRepositoryConnection();
|
|
1531
|
-
const value = connection[property];
|
|
1532
|
-
return typeof value === "function" ? value.bind(connection) : value;
|
|
1533
|
-
}
|
|
1534
|
-
});
|
|
1535
|
-
|
|
1536
|
-
// ../../src/core/security/safeUrl.ts
|
|
1537
|
-
import { lookup as dnsLookupImpl } from "dns/promises";
|
|
1538
|
-
var dnsLookup = dnsLookupImpl;
|
|
1539
|
-
var BLOCKED_HOSTNAMES = new Set([
|
|
1540
|
-
"localhost",
|
|
1541
|
-
"127.0.0.1",
|
|
1542
|
-
"0.0.0.0",
|
|
1543
|
-
"::1",
|
|
1544
|
-
"metadata.google.internal"
|
|
1545
|
-
]);
|
|
1546
|
-
function isPrivateIpv4(hostname) {
|
|
1547
|
-
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
1548
|
-
if (!match) {
|
|
1549
|
-
return false;
|
|
1550
|
-
}
|
|
1551
|
-
const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
|
|
1552
|
-
if (octets.some((octet) => octet < 0 || octet > 255)) {
|
|
1553
|
-
return true;
|
|
1554
|
-
}
|
|
1555
|
-
const [a = 0, b = 0] = octets;
|
|
1556
|
-
if (a === 10) {
|
|
1557
|
-
return true;
|
|
1558
|
-
}
|
|
1559
|
-
if (a === 127) {
|
|
1560
|
-
return true;
|
|
1561
|
-
}
|
|
1562
|
-
if (a === 0) {
|
|
1563
|
-
return true;
|
|
1564
|
-
}
|
|
1565
|
-
if (a === 169 && b === 254) {
|
|
1566
|
-
return true;
|
|
1567
|
-
}
|
|
1568
|
-
if (a === 172 && b >= 16 && b <= 31) {
|
|
1569
|
-
return true;
|
|
1570
|
-
}
|
|
1571
|
-
if (a === 192 && b === 168) {
|
|
1572
|
-
return true;
|
|
1573
|
-
}
|
|
1574
|
-
return false;
|
|
1575
|
-
}
|
|
1576
|
-
function isBlockedHostname(hostname) {
|
|
1577
|
-
const normalized = hostname.trim().toLowerCase();
|
|
1578
|
-
if (normalized.length === 0) {
|
|
1579
|
-
return true;
|
|
1580
|
-
}
|
|
1581
|
-
if (BLOCKED_HOSTNAMES.has(normalized)) {
|
|
1582
|
-
return true;
|
|
1583
|
-
}
|
|
1584
|
-
if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
|
|
1585
|
-
return true;
|
|
1586
|
-
}
|
|
1587
|
-
if (normalized.includes(":")) {
|
|
1588
|
-
return true;
|
|
1589
|
-
}
|
|
1590
|
-
return isPrivateIpv4(normalized);
|
|
1591
|
-
}
|
|
1592
|
-
function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
1593
|
-
let parsed;
|
|
1594
|
-
try {
|
|
1595
|
-
parsed = new URL(rawUrl);
|
|
1596
|
-
} catch {
|
|
1597
|
-
throw new BadRequestError("Webhook URL is invalid.");
|
|
1598
|
-
}
|
|
1599
|
-
if (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
|
|
1600
|
-
throw new BadRequestError("Webhook URL must use HTTPS.");
|
|
1601
|
-
}
|
|
1602
|
-
if (parsed.username || parsed.password) {
|
|
1603
|
-
throw new BadRequestError("Webhook URL must not include credentials.");
|
|
1604
|
-
}
|
|
1605
|
-
if (isBlockedHostname(parsed.hostname)) {
|
|
1606
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
1607
|
-
}
|
|
1608
|
-
return parsed;
|
|
1609
|
-
}
|
|
1610
|
-
function isBlockedIpAddress(address) {
|
|
1611
|
-
return isBlockedHostname(address.trim().toLowerCase());
|
|
1612
|
-
}
|
|
1613
|
-
async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
|
|
1614
|
-
const parsed = assertSafeOutboundUrl(rawUrl, options);
|
|
1615
|
-
if (options.resolveDns === false) {
|
|
1616
|
-
return parsed;
|
|
1617
|
-
}
|
|
1618
|
-
const hostname = parsed.hostname.trim().toLowerCase();
|
|
1619
|
-
const results = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
1620
|
-
if (results.some((result) => isBlockedIpAddress(result.address))) {
|
|
1621
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
1622
|
-
}
|
|
1623
|
-
return parsed;
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
// ../../src/core/security/safeFetch.ts
|
|
1627
|
-
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
1628
|
-
async function safeFetch(input, init = {}, options = {}) {
|
|
1629
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
1630
|
-
const maxRedirects = options.maxRedirects ?? 0;
|
|
1631
|
-
const resolveDns = options.resolveDns ?? appConfig.env === "production";
|
|
1632
|
-
const urlOptions = { allowHttp: options.allowHttp, resolveDns };
|
|
1633
|
-
const controller = new AbortController;
|
|
1634
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1635
|
-
try {
|
|
1636
|
-
let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
|
|
1637
|
-
let redirectCount = 0;
|
|
1638
|
-
while (true) {
|
|
1639
|
-
const response = await fetch(currentUrl, {
|
|
1640
|
-
...init,
|
|
1641
|
-
signal: controller.signal,
|
|
1642
|
-
redirect: "manual"
|
|
1643
|
-
});
|
|
1644
|
-
if (response.status >= 300 && response.status < 400) {
|
|
1645
|
-
const location = response.headers.get("location");
|
|
1646
|
-
if (!location || redirectCount >= maxRedirects) {
|
|
1647
|
-
return response;
|
|
1648
|
-
}
|
|
1649
|
-
currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
|
|
1650
|
-
redirectCount += 1;
|
|
1651
|
-
continue;
|
|
1652
|
-
}
|
|
1653
|
-
return response;
|
|
1654
|
-
}
|
|
1655
|
-
} finally {
|
|
1656
|
-
clearTimeout(timeout);
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
1661
|
-
class DispatchWebhookJob extends Job {
|
|
1662
|
-
maxAttempts = 3;
|
|
1663
|
-
backoffMs = 2000;
|
|
1664
|
-
async handle(payload) {
|
|
1665
|
-
const rows = await repositoryConnection`
|
|
1666
|
-
SELECT id, url, secret
|
|
1667
|
-
FROM webhook
|
|
1668
|
-
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
1669
|
-
LIMIT 1
|
|
1670
|
-
`;
|
|
1671
|
-
const webhook = rows[0];
|
|
1672
|
-
if (!webhook) {
|
|
1673
|
-
return;
|
|
1674
|
-
}
|
|
1675
|
-
const body = JSON.stringify({ event: payload.event, payload: payload.payload });
|
|
1676
|
-
const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
|
|
1677
|
-
assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
|
|
1678
|
-
let responseStatus = null;
|
|
1679
|
-
let errorMessage = null;
|
|
1680
|
-
try {
|
|
1681
|
-
const response = await safeFetch(webhook.url, {
|
|
1682
|
-
method: "POST",
|
|
1683
|
-
headers: {
|
|
1684
|
-
"content-type": "application/json",
|
|
1685
|
-
"x-workhub-signature": signature
|
|
1686
|
-
},
|
|
1687
|
-
body
|
|
1688
|
-
}, { allowHttp: appConfig.env !== "production" });
|
|
1689
|
-
responseStatus = response.status;
|
|
1690
|
-
if (!response.ok) {
|
|
1691
|
-
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
1692
|
-
}
|
|
1693
|
-
} catch (error) {
|
|
1694
|
-
errorMessage = error instanceof Error ? error.message : String(error);
|
|
1695
|
-
await repositoryConnection`
|
|
1696
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
1697
|
-
VALUES (
|
|
1698
|
-
${webhook.id},
|
|
1699
|
-
${payload.event},
|
|
1700
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
1701
|
-
${responseStatus},
|
|
1702
|
-
${errorMessage}
|
|
1703
|
-
)
|
|
1704
|
-
`;
|
|
1705
|
-
throw error instanceof Error ? error : new Error(errorMessage);
|
|
1706
|
-
}
|
|
1707
|
-
await repositoryConnection`
|
|
1708
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
1709
|
-
VALUES (
|
|
1710
|
-
${webhook.id},
|
|
1711
|
-
${payload.event},
|
|
1712
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
1713
|
-
${responseStatus}
|
|
1714
|
-
)
|
|
1715
|
-
`;
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
var dispatchWebhookJob_default = DispatchWebhookJob;
|
|
1719
|
-
|
|
1720
|
-
// ../../src/core/queue/jobRegistry.ts
|
|
1721
|
-
class JobRegistry {
|
|
1722
|
-
constructor() {}
|
|
1723
|
-
factories = new Map;
|
|
1724
|
-
instances = new WeakMap;
|
|
1725
|
-
register(name, factory) {
|
|
1726
|
-
this.factories.set(name, factory);
|
|
1727
|
-
}
|
|
1728
|
-
resolveName(job) {
|
|
1729
|
-
return this.instances.get(job);
|
|
1730
|
-
}
|
|
1731
|
-
track(name, job) {
|
|
1732
|
-
this.instances.set(job, name);
|
|
1733
|
-
return job;
|
|
1734
|
-
}
|
|
1735
|
-
create(name) {
|
|
1736
|
-
const factory = this.factories.get(name);
|
|
1737
|
-
if (!factory) {
|
|
1738
|
-
return;
|
|
1739
|
-
}
|
|
1740
|
-
return factory();
|
|
1359
|
+
// ../../src/core/jobs/invalidateCacheTagsJob.ts
|
|
1360
|
+
class InvalidateCacheTagsJob extends Job {
|
|
1361
|
+
cache;
|
|
1362
|
+
constructor(cache) {
|
|
1363
|
+
super();
|
|
1364
|
+
this.cache = cache;
|
|
1741
1365
|
}
|
|
1742
|
-
|
|
1743
|
-
|
|
1366
|
+
async handle(payload) {
|
|
1367
|
+
await this.cache.tags(...payload.tags).flush();
|
|
1744
1368
|
}
|
|
1745
1369
|
}
|
|
1746
|
-
var
|
|
1370
|
+
var invalidateCacheTagsJob_default = InvalidateCacheTagsJob;
|
|
1747
1371
|
|
|
1748
1372
|
// ../../src/core/pagination/index.ts
|
|
1749
1373
|
function buildPaginationMeta(input) {
|
|
@@ -2284,6 +1908,29 @@ function indexMorphToRelation(children, parentsByType, relation) {
|
|
|
2284
1908
|
return result;
|
|
2285
1909
|
}
|
|
2286
1910
|
|
|
1911
|
+
// ../../src/core/database/boundConnection.ts
|
|
1912
|
+
var boundConnectionHolder = {
|
|
1913
|
+
connection: null
|
|
1914
|
+
};
|
|
1915
|
+
function getBoundDatabaseConnection() {
|
|
1916
|
+
return boundConnectionHolder.connection;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// ../../src/core/database/repositoryConnection.ts
|
|
1920
|
+
function resolveRepositoryConnection() {
|
|
1921
|
+
return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
|
|
1922
|
+
}
|
|
1923
|
+
var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
|
|
1924
|
+
apply(_target, _thisArg, args) {
|
|
1925
|
+
return resolveRepositoryConnection()(...args);
|
|
1926
|
+
},
|
|
1927
|
+
get(_target, property) {
|
|
1928
|
+
const connection = resolveRepositoryConnection();
|
|
1929
|
+
const value = connection[property];
|
|
1930
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
1931
|
+
}
|
|
1932
|
+
});
|
|
1933
|
+
|
|
2287
1934
|
// ../../src/core/database/whereBuilder.ts
|
|
2288
1935
|
class WhereBuilder {
|
|
2289
1936
|
nodes = [];
|
|
@@ -3472,6 +3119,34 @@ class FailedJobService {
|
|
|
3472
3119
|
}
|
|
3473
3120
|
var failedJobService_default = FailedJobService;
|
|
3474
3121
|
|
|
3122
|
+
// ../../src/core/queue/jobRegistry.ts
|
|
3123
|
+
class JobRegistry {
|
|
3124
|
+
constructor() {}
|
|
3125
|
+
factories = new Map;
|
|
3126
|
+
instances = new WeakMap;
|
|
3127
|
+
register(name, factory) {
|
|
3128
|
+
this.factories.set(name, factory);
|
|
3129
|
+
}
|
|
3130
|
+
resolveName(job) {
|
|
3131
|
+
return this.instances.get(job);
|
|
3132
|
+
}
|
|
3133
|
+
track(name, job) {
|
|
3134
|
+
this.instances.set(job, name);
|
|
3135
|
+
return job;
|
|
3136
|
+
}
|
|
3137
|
+
create(name) {
|
|
3138
|
+
const factory = this.factories.get(name);
|
|
3139
|
+
if (!factory) {
|
|
3140
|
+
return;
|
|
3141
|
+
}
|
|
3142
|
+
return factory();
|
|
3143
|
+
}
|
|
3144
|
+
names() {
|
|
3145
|
+
return [...this.factories.keys()];
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
var jobRegistry = new JobRegistry;
|
|
3149
|
+
|
|
3475
3150
|
// ../../src/core/queue/jobRunner.ts
|
|
3476
3151
|
async function runQueueJob(envelope, failedJobs) {
|
|
3477
3152
|
const job = jobRegistry.create(envelope.name);
|
|
@@ -3586,19 +3261,211 @@ function createProductionQueue(driver, options = {}) {
|
|
|
3586
3261
|
return new ResilientQueue(failedJobs, driver === "async");
|
|
3587
3262
|
}
|
|
3588
3263
|
|
|
3589
|
-
// ../../src/
|
|
3590
|
-
|
|
3264
|
+
// ../../src/bootstrap/queue/defaultJobs.ts
|
|
3265
|
+
import { resolveApplicationCache } from "@getstrata/core";
|
|
3266
|
+
|
|
3267
|
+
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
3268
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
3269
|
+
|
|
3270
|
+
// ../../src/core/security/safeUrl.ts
|
|
3271
|
+
import { lookup as dnsLookupImpl } from "dns/promises";
|
|
3272
|
+
var dnsLookup = dnsLookupImpl;
|
|
3273
|
+
var BLOCKED_HOSTNAMES = new Set([
|
|
3274
|
+
"localhost",
|
|
3275
|
+
"127.0.0.1",
|
|
3276
|
+
"0.0.0.0",
|
|
3277
|
+
"::1",
|
|
3278
|
+
"metadata.google.internal"
|
|
3279
|
+
]);
|
|
3280
|
+
function isPrivateIpv4(hostname) {
|
|
3281
|
+
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
3282
|
+
if (!match) {
|
|
3283
|
+
return false;
|
|
3284
|
+
}
|
|
3285
|
+
const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
|
|
3286
|
+
if (octets.some((octet) => octet < 0 || octet > 255)) {
|
|
3287
|
+
return true;
|
|
3288
|
+
}
|
|
3289
|
+
const [a = 0, b = 0] = octets;
|
|
3290
|
+
if (a === 10) {
|
|
3291
|
+
return true;
|
|
3292
|
+
}
|
|
3293
|
+
if (a === 127) {
|
|
3294
|
+
return true;
|
|
3295
|
+
}
|
|
3296
|
+
if (a === 0) {
|
|
3297
|
+
return true;
|
|
3298
|
+
}
|
|
3299
|
+
if (a === 169 && b === 254) {
|
|
3300
|
+
return true;
|
|
3301
|
+
}
|
|
3302
|
+
if (a === 172 && b >= 16 && b <= 31) {
|
|
3303
|
+
return true;
|
|
3304
|
+
}
|
|
3305
|
+
if (a === 192 && b === 168) {
|
|
3306
|
+
return true;
|
|
3307
|
+
}
|
|
3308
|
+
return false;
|
|
3309
|
+
}
|
|
3310
|
+
function isBlockedHostname(hostname) {
|
|
3311
|
+
const normalized = hostname.trim().toLowerCase();
|
|
3312
|
+
if (normalized.length === 0) {
|
|
3313
|
+
return true;
|
|
3314
|
+
}
|
|
3315
|
+
if (BLOCKED_HOSTNAMES.has(normalized)) {
|
|
3316
|
+
return true;
|
|
3317
|
+
}
|
|
3318
|
+
if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
|
|
3319
|
+
return true;
|
|
3320
|
+
}
|
|
3321
|
+
if (normalized.includes(":")) {
|
|
3322
|
+
return true;
|
|
3323
|
+
}
|
|
3324
|
+
return isPrivateIpv4(normalized);
|
|
3325
|
+
}
|
|
3326
|
+
function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
3327
|
+
let parsed;
|
|
3328
|
+
try {
|
|
3329
|
+
parsed = new URL(rawUrl);
|
|
3330
|
+
} catch {
|
|
3331
|
+
throw new BadRequestError("Webhook URL is invalid.");
|
|
3332
|
+
}
|
|
3333
|
+
if (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
|
|
3334
|
+
throw new BadRequestError("Webhook URL must use HTTPS.");
|
|
3335
|
+
}
|
|
3336
|
+
if (parsed.username || parsed.password) {
|
|
3337
|
+
throw new BadRequestError("Webhook URL must not include credentials.");
|
|
3338
|
+
}
|
|
3339
|
+
if (isBlockedHostname(parsed.hostname)) {
|
|
3340
|
+
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
3341
|
+
}
|
|
3342
|
+
return parsed;
|
|
3343
|
+
}
|
|
3344
|
+
function isBlockedIpAddress(address) {
|
|
3345
|
+
return isBlockedHostname(address.trim().toLowerCase());
|
|
3346
|
+
}
|
|
3347
|
+
async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
|
|
3348
|
+
const parsed = assertSafeOutboundUrl(rawUrl, options);
|
|
3349
|
+
if (options.resolveDns === false) {
|
|
3350
|
+
return parsed;
|
|
3351
|
+
}
|
|
3352
|
+
const hostname = parsed.hostname.trim().toLowerCase();
|
|
3353
|
+
const results = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
3354
|
+
if (results.some((result) => isBlockedIpAddress(result.address))) {
|
|
3355
|
+
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
3356
|
+
}
|
|
3357
|
+
return parsed;
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
// ../../src/core/security/safeFetch.ts
|
|
3361
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
3362
|
+
async function safeFetch(input, init = {}, options = {}) {
|
|
3363
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
3364
|
+
const maxRedirects = options.maxRedirects ?? 0;
|
|
3365
|
+
const resolveDns = options.resolveDns ?? appConfig.env === "production";
|
|
3366
|
+
const urlOptions = { allowHttp: options.allowHttp, resolveDns };
|
|
3367
|
+
const controller = new AbortController;
|
|
3368
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
3369
|
+
try {
|
|
3370
|
+
let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
|
|
3371
|
+
let redirectCount = 0;
|
|
3372
|
+
while (true) {
|
|
3373
|
+
const response = await fetch(currentUrl, {
|
|
3374
|
+
...init,
|
|
3375
|
+
signal: controller.signal,
|
|
3376
|
+
redirect: "manual"
|
|
3377
|
+
});
|
|
3378
|
+
if (response.status >= 300 && response.status < 400) {
|
|
3379
|
+
const location = response.headers.get("location");
|
|
3380
|
+
if (!location || redirectCount >= maxRedirects) {
|
|
3381
|
+
return response;
|
|
3382
|
+
}
|
|
3383
|
+
currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
|
|
3384
|
+
redirectCount += 1;
|
|
3385
|
+
continue;
|
|
3386
|
+
}
|
|
3387
|
+
return response;
|
|
3388
|
+
}
|
|
3389
|
+
} finally {
|
|
3390
|
+
clearTimeout(timeout);
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
|
|
3394
|
+
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
3395
|
+
class DispatchWebhookJob extends Job {
|
|
3396
|
+
maxAttempts = 3;
|
|
3397
|
+
backoffMs = 2000;
|
|
3398
|
+
async handle(payload) {
|
|
3399
|
+
const rows = await repositoryConnection`
|
|
3400
|
+
SELECT id, url, secret
|
|
3401
|
+
FROM webhook
|
|
3402
|
+
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
3403
|
+
LIMIT 1
|
|
3404
|
+
`;
|
|
3405
|
+
const webhook = rows[0];
|
|
3406
|
+
if (!webhook) {
|
|
3407
|
+
return;
|
|
3408
|
+
}
|
|
3409
|
+
const body = JSON.stringify({ event: payload.event, payload: payload.payload });
|
|
3410
|
+
const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
|
|
3411
|
+
assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
|
|
3412
|
+
let responseStatus = null;
|
|
3413
|
+
let errorMessage = null;
|
|
3414
|
+
try {
|
|
3415
|
+
const response = await safeFetch(webhook.url, {
|
|
3416
|
+
method: "POST",
|
|
3417
|
+
headers: {
|
|
3418
|
+
"content-type": "application/json",
|
|
3419
|
+
"x-workhub-signature": signature
|
|
3420
|
+
},
|
|
3421
|
+
body
|
|
3422
|
+
}, { allowHttp: appConfig.env !== "production" });
|
|
3423
|
+
responseStatus = response.status;
|
|
3424
|
+
if (!response.ok) {
|
|
3425
|
+
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
3426
|
+
}
|
|
3427
|
+
} catch (error) {
|
|
3428
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
3429
|
+
await repositoryConnection`
|
|
3430
|
+
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
3431
|
+
VALUES (
|
|
3432
|
+
${webhook.id},
|
|
3433
|
+
${payload.event},
|
|
3434
|
+
${JSON.stringify(payload.payload)}::jsonb,
|
|
3435
|
+
${responseStatus},
|
|
3436
|
+
${errorMessage}
|
|
3437
|
+
)
|
|
3438
|
+
`;
|
|
3439
|
+
throw error instanceof Error ? error : new Error(errorMessage);
|
|
3440
|
+
}
|
|
3441
|
+
await repositoryConnection`
|
|
3442
|
+
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
3443
|
+
VALUES (
|
|
3444
|
+
${webhook.id},
|
|
3445
|
+
${payload.event},
|
|
3446
|
+
${JSON.stringify(payload.payload)}::jsonb,
|
|
3447
|
+
${responseStatus}
|
|
3448
|
+
)
|
|
3449
|
+
`;
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
var dispatchWebhookJob_default = DispatchWebhookJob;
|
|
3453
|
+
|
|
3454
|
+
// ../../src/bootstrap/queue/defaultJobs.ts
|
|
3591
3455
|
function registerDefaultJobs() {
|
|
3592
3456
|
jobRegistry.register("cache.invalidate-tags", () => {
|
|
3593
3457
|
return new invalidateCacheTagsJob_default(resolveApplicationCache());
|
|
3594
3458
|
});
|
|
3595
3459
|
jobRegistry.register("webhook.dispatch", () => new dispatchWebhookJob_default);
|
|
3596
3460
|
}
|
|
3597
|
-
|
|
3461
|
+
|
|
3462
|
+
// ../../src/core/queue/createAppQueue.ts
|
|
3463
|
+
var FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
|
|
3464
|
+
function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(), registerJobs) {
|
|
3598
3465
|
return createProductionQueue(driver, {
|
|
3599
3466
|
redisUrl,
|
|
3600
3467
|
failedJobs,
|
|
3601
|
-
registerJobs
|
|
3468
|
+
registerJobs
|
|
3602
3469
|
});
|
|
3603
3470
|
}
|
|
3604
3471
|
|
|
@@ -3615,7 +3482,7 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus) {
|
|
|
3615
3482
|
let cache;
|
|
3616
3483
|
let queue;
|
|
3617
3484
|
try {
|
|
3618
|
-
cache =
|
|
3485
|
+
cache = resolveApplicationCache2();
|
|
3619
3486
|
queue = resolveApplicationQueue();
|
|
3620
3487
|
} catch {
|
|
3621
3488
|
return;
|
|
@@ -3706,7 +3573,7 @@ var queueProvider = {
|
|
|
3706
3573
|
config.set("queue.driver", driver);
|
|
3707
3574
|
const failedJobs = createFailedJobService();
|
|
3708
3575
|
container.set(FAILED_JOB_SERVICE_TOKEN, failedJobs);
|
|
3709
|
-
container.set(CORE_QUEUE_TOKEN, createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs));
|
|
3576
|
+
container.set(CORE_QUEUE_TOKEN, createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs, registerDefaultJobs));
|
|
3710
3577
|
}
|
|
3711
3578
|
};
|
|
3712
3579
|
var queue_default = queueProvider;
|
|
@@ -3849,8 +3716,7 @@ function isViewsEnabled() {
|
|
|
3849
3716
|
}
|
|
3850
3717
|
|
|
3851
3718
|
// ../../src/core/http/requestMetaContext.ts
|
|
3852
|
-
|
|
3853
|
-
var requestMetaContext = new AsyncLocalStorage3;
|
|
3719
|
+
var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
|
|
3854
3720
|
function currentRequestMeta() {
|
|
3855
3721
|
return requestMetaContext.getStore() ?? {
|
|
3856
3722
|
ipAddress: null,
|