@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
package/dist/entries/context.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/bootstrap/context.ts
|
|
3
|
-
import { setActiveApplicationContext
|
|
3
|
+
import { setActiveApplicationContext } from "@getstrata/core";
|
|
4
4
|
|
|
5
5
|
// ../../src/bootstrap/contracts.ts
|
|
6
6
|
class ServiceContainer {
|
|
@@ -217,9 +217,22 @@ var databaseConfig = {
|
|
|
217
217
|
connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
|
|
218
218
|
};
|
|
219
219
|
|
|
220
|
-
// ../../src/core/
|
|
220
|
+
// ../../src/core/runtime/asyncContextStore.ts
|
|
221
221
|
import { AsyncLocalStorage } from "async_hooks";
|
|
222
|
-
|
|
222
|
+
function createAsyncContextStore(key) {
|
|
223
|
+
const symbol = Symbol.for(key);
|
|
224
|
+
const globalRecord = globalThis;
|
|
225
|
+
const existing = globalRecord[symbol];
|
|
226
|
+
if (existing) {
|
|
227
|
+
return existing;
|
|
228
|
+
}
|
|
229
|
+
const store = new AsyncLocalStorage;
|
|
230
|
+
globalRecord[symbol] = store;
|
|
231
|
+
return store;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ../../src/core/database/connectionContext.ts
|
|
235
|
+
var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
|
|
223
236
|
function getActiveDatabaseConnection(fallback) {
|
|
224
237
|
return activeConnection.getStore() ?? fallback;
|
|
225
238
|
}
|
|
@@ -306,6 +319,7 @@ var db = new Proxy(function database() {}, {
|
|
|
306
319
|
return typeof value === "function" ? value.bind(connection) : value;
|
|
307
320
|
}
|
|
308
321
|
});
|
|
322
|
+
var connection_default = db;
|
|
309
323
|
|
|
310
324
|
// ../../src/modules/user/apiTokenTable.ts
|
|
311
325
|
import { defineTable } from "@getstrata/core/database";
|
|
@@ -534,8 +548,7 @@ class PreconditionFailedError extends HttpError {
|
|
|
534
548
|
}
|
|
535
549
|
|
|
536
550
|
// ../../src/core/auth/authContext.ts
|
|
537
|
-
|
|
538
|
-
var authContext = new AsyncLocalStorage2;
|
|
551
|
+
var authContext = createAsyncContextStore("@getstrata/authContext");
|
|
539
552
|
function currentAuthUser() {
|
|
540
553
|
return authContext.getStore() ?? null;
|
|
541
554
|
}
|
|
@@ -1409,6 +1422,9 @@ function discoverListeners() {
|
|
|
1409
1422
|
return appListeners;
|
|
1410
1423
|
}
|
|
1411
1424
|
|
|
1425
|
+
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
1426
|
+
import { resolveApplicationCache as resolveApplicationCache2, resolveApplicationQueue } from "@getstrata/core";
|
|
1427
|
+
|
|
1412
1428
|
// ../../src/core/cache/modelCacheTags.ts
|
|
1413
1429
|
function cacheTagsForModelWrite(tableName, action) {
|
|
1414
1430
|
const module = appModules.find((entry) => entry.tableName === tableName);
|
|
@@ -1441,313 +1457,6 @@ class InvalidateCacheTagsJob extends Job {
|
|
|
1441
1457
|
}
|
|
1442
1458
|
var invalidateCacheTagsJob_default = InvalidateCacheTagsJob;
|
|
1443
1459
|
|
|
1444
|
-
// ../../src/core/logging/logger.ts
|
|
1445
|
-
class Logger {
|
|
1446
|
-
channel;
|
|
1447
|
-
constructor(channel = "app") {
|
|
1448
|
-
this.channel = channel;
|
|
1449
|
-
}
|
|
1450
|
-
write(level, message, context = {}) {
|
|
1451
|
-
const entry = {
|
|
1452
|
-
level,
|
|
1453
|
-
channel: this.channel,
|
|
1454
|
-
message,
|
|
1455
|
-
timestamp: new Date().toISOString(),
|
|
1456
|
-
...context
|
|
1457
|
-
};
|
|
1458
|
-
const line = JSON.stringify(entry);
|
|
1459
|
-
if (level === "error") {
|
|
1460
|
-
console.error(line);
|
|
1461
|
-
return;
|
|
1462
|
-
}
|
|
1463
|
-
console.log(line);
|
|
1464
|
-
}
|
|
1465
|
-
debug(message, context) {
|
|
1466
|
-
this.write("debug", message, context);
|
|
1467
|
-
}
|
|
1468
|
-
info(message, context) {
|
|
1469
|
-
this.write("info", message, context);
|
|
1470
|
-
}
|
|
1471
|
-
warn(message, context) {
|
|
1472
|
-
this.write("warn", message, context);
|
|
1473
|
-
}
|
|
1474
|
-
error(message, context) {
|
|
1475
|
-
this.write("error", message, context);
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
var appLogger = new Logger("app");
|
|
1479
|
-
|
|
1480
|
-
// ../../src/bootstrap/applicationRegistry.ts
|
|
1481
|
-
var activeContext;
|
|
1482
|
-
function setActiveApplicationContext(context) {
|
|
1483
|
-
activeContext = context;
|
|
1484
|
-
}
|
|
1485
|
-
function requireActiveApplicationContext() {
|
|
1486
|
-
if (!activeContext) {
|
|
1487
|
-
throw new Error("The application context has not been bootstrapped.");
|
|
1488
|
-
}
|
|
1489
|
-
return activeContext;
|
|
1490
|
-
}
|
|
1491
|
-
function resolveApplicationCache() {
|
|
1492
|
-
return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
|
|
1493
|
-
}
|
|
1494
|
-
function resolveApplicationQueue() {
|
|
1495
|
-
return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
|
|
1496
|
-
}
|
|
1497
|
-
function resolveApplicationAuth() {
|
|
1498
|
-
return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
|
|
1499
|
-
}
|
|
1500
|
-
function resolveApplicationPolicyGate() {
|
|
1501
|
-
return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
|
|
1502
|
-
}
|
|
1503
|
-
function resolveApplicationConfig() {
|
|
1504
|
-
return requireActiveApplicationContext().config;
|
|
1505
|
-
}
|
|
1506
|
-
function resolveApplicationLogger() {
|
|
1507
|
-
return appLogger;
|
|
1508
|
-
}
|
|
1509
|
-
function resolveApplicationDependencies() {
|
|
1510
|
-
return requireActiveApplicationContext().dependencies;
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
1514
|
-
import { createHmac as createHmac2 } from "crypto";
|
|
1515
|
-
|
|
1516
|
-
// ../../src/core/database/boundConnection.ts
|
|
1517
|
-
var boundConnectionHolder = {
|
|
1518
|
-
connection: null
|
|
1519
|
-
};
|
|
1520
|
-
function getBoundDatabaseConnection() {
|
|
1521
|
-
return boundConnectionHolder.connection;
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
// ../../src/core/database/repositoryConnection.ts
|
|
1525
|
-
function resolveRepositoryConnection() {
|
|
1526
|
-
return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
|
|
1527
|
-
}
|
|
1528
|
-
var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
|
|
1529
|
-
apply(_target, _thisArg, args) {
|
|
1530
|
-
return resolveRepositoryConnection()(...args);
|
|
1531
|
-
},
|
|
1532
|
-
get(_target, property) {
|
|
1533
|
-
const connection = resolveRepositoryConnection();
|
|
1534
|
-
const value = connection[property];
|
|
1535
|
-
return typeof value === "function" ? value.bind(connection) : value;
|
|
1536
|
-
}
|
|
1537
|
-
});
|
|
1538
|
-
|
|
1539
|
-
// ../../src/core/security/safeUrl.ts
|
|
1540
|
-
import { lookup as dnsLookupImpl } from "dns/promises";
|
|
1541
|
-
var dnsLookup = dnsLookupImpl;
|
|
1542
|
-
var BLOCKED_HOSTNAMES = new Set([
|
|
1543
|
-
"localhost",
|
|
1544
|
-
"127.0.0.1",
|
|
1545
|
-
"0.0.0.0",
|
|
1546
|
-
"::1",
|
|
1547
|
-
"metadata.google.internal"
|
|
1548
|
-
]);
|
|
1549
|
-
function isPrivateIpv4(hostname) {
|
|
1550
|
-
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
1551
|
-
if (!match) {
|
|
1552
|
-
return false;
|
|
1553
|
-
}
|
|
1554
|
-
const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
|
|
1555
|
-
if (octets.some((octet) => octet < 0 || octet > 255)) {
|
|
1556
|
-
return true;
|
|
1557
|
-
}
|
|
1558
|
-
const [a = 0, b = 0] = octets;
|
|
1559
|
-
if (a === 10) {
|
|
1560
|
-
return true;
|
|
1561
|
-
}
|
|
1562
|
-
if (a === 127) {
|
|
1563
|
-
return true;
|
|
1564
|
-
}
|
|
1565
|
-
if (a === 0) {
|
|
1566
|
-
return true;
|
|
1567
|
-
}
|
|
1568
|
-
if (a === 169 && b === 254) {
|
|
1569
|
-
return true;
|
|
1570
|
-
}
|
|
1571
|
-
if (a === 172 && b >= 16 && b <= 31) {
|
|
1572
|
-
return true;
|
|
1573
|
-
}
|
|
1574
|
-
if (a === 192 && b === 168) {
|
|
1575
|
-
return true;
|
|
1576
|
-
}
|
|
1577
|
-
return false;
|
|
1578
|
-
}
|
|
1579
|
-
function isBlockedHostname(hostname) {
|
|
1580
|
-
const normalized = hostname.trim().toLowerCase();
|
|
1581
|
-
if (normalized.length === 0) {
|
|
1582
|
-
return true;
|
|
1583
|
-
}
|
|
1584
|
-
if (BLOCKED_HOSTNAMES.has(normalized)) {
|
|
1585
|
-
return true;
|
|
1586
|
-
}
|
|
1587
|
-
if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
|
|
1588
|
-
return true;
|
|
1589
|
-
}
|
|
1590
|
-
if (normalized.includes(":")) {
|
|
1591
|
-
return true;
|
|
1592
|
-
}
|
|
1593
|
-
return isPrivateIpv4(normalized);
|
|
1594
|
-
}
|
|
1595
|
-
function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
1596
|
-
let parsed;
|
|
1597
|
-
try {
|
|
1598
|
-
parsed = new URL(rawUrl);
|
|
1599
|
-
} catch {
|
|
1600
|
-
throw new BadRequestError("Webhook URL is invalid.");
|
|
1601
|
-
}
|
|
1602
|
-
if (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
|
|
1603
|
-
throw new BadRequestError("Webhook URL must use HTTPS.");
|
|
1604
|
-
}
|
|
1605
|
-
if (parsed.username || parsed.password) {
|
|
1606
|
-
throw new BadRequestError("Webhook URL must not include credentials.");
|
|
1607
|
-
}
|
|
1608
|
-
if (isBlockedHostname(parsed.hostname)) {
|
|
1609
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
1610
|
-
}
|
|
1611
|
-
return parsed;
|
|
1612
|
-
}
|
|
1613
|
-
function isBlockedIpAddress(address) {
|
|
1614
|
-
return isBlockedHostname(address.trim().toLowerCase());
|
|
1615
|
-
}
|
|
1616
|
-
async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
|
|
1617
|
-
const parsed = assertSafeOutboundUrl(rawUrl, options);
|
|
1618
|
-
if (options.resolveDns === false) {
|
|
1619
|
-
return parsed;
|
|
1620
|
-
}
|
|
1621
|
-
const hostname = parsed.hostname.trim().toLowerCase();
|
|
1622
|
-
const results = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
1623
|
-
if (results.some((result) => isBlockedIpAddress(result.address))) {
|
|
1624
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
1625
|
-
}
|
|
1626
|
-
return parsed;
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
// ../../src/core/security/safeFetch.ts
|
|
1630
|
-
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
1631
|
-
async function safeFetch(input, init = {}, options = {}) {
|
|
1632
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
1633
|
-
const maxRedirects = options.maxRedirects ?? 0;
|
|
1634
|
-
const resolveDns = options.resolveDns ?? appConfig.env === "production";
|
|
1635
|
-
const urlOptions = { allowHttp: options.allowHttp, resolveDns };
|
|
1636
|
-
const controller = new AbortController;
|
|
1637
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1638
|
-
try {
|
|
1639
|
-
let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
|
|
1640
|
-
let redirectCount = 0;
|
|
1641
|
-
while (true) {
|
|
1642
|
-
const response = await fetch(currentUrl, {
|
|
1643
|
-
...init,
|
|
1644
|
-
signal: controller.signal,
|
|
1645
|
-
redirect: "manual"
|
|
1646
|
-
});
|
|
1647
|
-
if (response.status >= 300 && response.status < 400) {
|
|
1648
|
-
const location = response.headers.get("location");
|
|
1649
|
-
if (!location || redirectCount >= maxRedirects) {
|
|
1650
|
-
return response;
|
|
1651
|
-
}
|
|
1652
|
-
currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
|
|
1653
|
-
redirectCount += 1;
|
|
1654
|
-
continue;
|
|
1655
|
-
}
|
|
1656
|
-
return response;
|
|
1657
|
-
}
|
|
1658
|
-
} finally {
|
|
1659
|
-
clearTimeout(timeout);
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
1664
|
-
class DispatchWebhookJob extends Job {
|
|
1665
|
-
maxAttempts = 3;
|
|
1666
|
-
backoffMs = 2000;
|
|
1667
|
-
async handle(payload) {
|
|
1668
|
-
const rows = await repositoryConnection`
|
|
1669
|
-
SELECT id, url, secret
|
|
1670
|
-
FROM webhook
|
|
1671
|
-
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
1672
|
-
LIMIT 1
|
|
1673
|
-
`;
|
|
1674
|
-
const webhook = rows[0];
|
|
1675
|
-
if (!webhook) {
|
|
1676
|
-
return;
|
|
1677
|
-
}
|
|
1678
|
-
const body = JSON.stringify({ event: payload.event, payload: payload.payload });
|
|
1679
|
-
const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
|
|
1680
|
-
assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
|
|
1681
|
-
let responseStatus = null;
|
|
1682
|
-
let errorMessage = null;
|
|
1683
|
-
try {
|
|
1684
|
-
const response = await safeFetch(webhook.url, {
|
|
1685
|
-
method: "POST",
|
|
1686
|
-
headers: {
|
|
1687
|
-
"content-type": "application/json",
|
|
1688
|
-
"x-workhub-signature": signature
|
|
1689
|
-
},
|
|
1690
|
-
body
|
|
1691
|
-
}, { allowHttp: appConfig.env !== "production" });
|
|
1692
|
-
responseStatus = response.status;
|
|
1693
|
-
if (!response.ok) {
|
|
1694
|
-
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
1695
|
-
}
|
|
1696
|
-
} catch (error) {
|
|
1697
|
-
errorMessage = error instanceof Error ? error.message : String(error);
|
|
1698
|
-
await repositoryConnection`
|
|
1699
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
1700
|
-
VALUES (
|
|
1701
|
-
${webhook.id},
|
|
1702
|
-
${payload.event},
|
|
1703
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
1704
|
-
${responseStatus},
|
|
1705
|
-
${errorMessage}
|
|
1706
|
-
)
|
|
1707
|
-
`;
|
|
1708
|
-
throw error instanceof Error ? error : new Error(errorMessage);
|
|
1709
|
-
}
|
|
1710
|
-
await repositoryConnection`
|
|
1711
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
1712
|
-
VALUES (
|
|
1713
|
-
${webhook.id},
|
|
1714
|
-
${payload.event},
|
|
1715
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
1716
|
-
${responseStatus}
|
|
1717
|
-
)
|
|
1718
|
-
`;
|
|
1719
|
-
}
|
|
1720
|
-
}
|
|
1721
|
-
var dispatchWebhookJob_default = DispatchWebhookJob;
|
|
1722
|
-
|
|
1723
|
-
// ../../src/core/queue/jobRegistry.ts
|
|
1724
|
-
class JobRegistry {
|
|
1725
|
-
constructor() {}
|
|
1726
|
-
factories = new Map;
|
|
1727
|
-
instances = new WeakMap;
|
|
1728
|
-
register(name, factory) {
|
|
1729
|
-
this.factories.set(name, factory);
|
|
1730
|
-
}
|
|
1731
|
-
resolveName(job) {
|
|
1732
|
-
return this.instances.get(job);
|
|
1733
|
-
}
|
|
1734
|
-
track(name, job) {
|
|
1735
|
-
this.instances.set(job, name);
|
|
1736
|
-
return job;
|
|
1737
|
-
}
|
|
1738
|
-
create(name) {
|
|
1739
|
-
const factory = this.factories.get(name);
|
|
1740
|
-
if (!factory) {
|
|
1741
|
-
return;
|
|
1742
|
-
}
|
|
1743
|
-
return factory();
|
|
1744
|
-
}
|
|
1745
|
-
names() {
|
|
1746
|
-
return [...this.factories.keys()];
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
var jobRegistry = new JobRegistry;
|
|
1750
|
-
|
|
1751
1460
|
// ../../src/core/pagination/index.ts
|
|
1752
1461
|
function buildPaginationMeta(input) {
|
|
1753
1462
|
const lastPage = Math.max(1, Math.ceil(input.total / input.perPage));
|
|
@@ -2287,6 +1996,29 @@ function indexMorphToRelation(children, parentsByType, relation) {
|
|
|
2287
1996
|
return result;
|
|
2288
1997
|
}
|
|
2289
1998
|
|
|
1999
|
+
// ../../src/core/database/boundConnection.ts
|
|
2000
|
+
var boundConnectionHolder = {
|
|
2001
|
+
connection: null
|
|
2002
|
+
};
|
|
2003
|
+
function getBoundDatabaseConnection() {
|
|
2004
|
+
return boundConnectionHolder.connection;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
// ../../src/core/database/repositoryConnection.ts
|
|
2008
|
+
function resolveRepositoryConnection() {
|
|
2009
|
+
return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
|
|
2010
|
+
}
|
|
2011
|
+
var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
|
|
2012
|
+
apply(_target, _thisArg, args) {
|
|
2013
|
+
return resolveRepositoryConnection()(...args);
|
|
2014
|
+
},
|
|
2015
|
+
get(_target, property) {
|
|
2016
|
+
const connection = resolveRepositoryConnection();
|
|
2017
|
+
const value = connection[property];
|
|
2018
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
2019
|
+
}
|
|
2020
|
+
});
|
|
2021
|
+
|
|
2290
2022
|
// ../../src/core/database/whereBuilder.ts
|
|
2291
2023
|
class WhereBuilder {
|
|
2292
2024
|
nodes = [];
|
|
@@ -3475,6 +3207,34 @@ class FailedJobService {
|
|
|
3475
3207
|
}
|
|
3476
3208
|
var failedJobService_default = FailedJobService;
|
|
3477
3209
|
|
|
3210
|
+
// ../../src/core/queue/jobRegistry.ts
|
|
3211
|
+
class JobRegistry {
|
|
3212
|
+
constructor() {}
|
|
3213
|
+
factories = new Map;
|
|
3214
|
+
instances = new WeakMap;
|
|
3215
|
+
register(name, factory) {
|
|
3216
|
+
this.factories.set(name, factory);
|
|
3217
|
+
}
|
|
3218
|
+
resolveName(job) {
|
|
3219
|
+
return this.instances.get(job);
|
|
3220
|
+
}
|
|
3221
|
+
track(name, job) {
|
|
3222
|
+
this.instances.set(job, name);
|
|
3223
|
+
return job;
|
|
3224
|
+
}
|
|
3225
|
+
create(name) {
|
|
3226
|
+
const factory = this.factories.get(name);
|
|
3227
|
+
if (!factory) {
|
|
3228
|
+
return;
|
|
3229
|
+
}
|
|
3230
|
+
return factory();
|
|
3231
|
+
}
|
|
3232
|
+
names() {
|
|
3233
|
+
return [...this.factories.keys()];
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
var jobRegistry = new JobRegistry;
|
|
3237
|
+
|
|
3478
3238
|
// ../../src/core/queue/jobRunner.ts
|
|
3479
3239
|
async function runQueueJob(envelope, failedJobs) {
|
|
3480
3240
|
const job = jobRegistry.create(envelope.name);
|
|
@@ -3589,19 +3349,211 @@ function createProductionQueue(driver, options = {}) {
|
|
|
3589
3349
|
return new ResilientQueue(failedJobs, driver === "async");
|
|
3590
3350
|
}
|
|
3591
3351
|
|
|
3592
|
-
// ../../src/
|
|
3593
|
-
|
|
3352
|
+
// ../../src/bootstrap/queue/defaultJobs.ts
|
|
3353
|
+
import { resolveApplicationCache } from "@getstrata/core";
|
|
3354
|
+
|
|
3355
|
+
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
3356
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
3357
|
+
|
|
3358
|
+
// ../../src/core/security/safeUrl.ts
|
|
3359
|
+
import { lookup as dnsLookupImpl } from "dns/promises";
|
|
3360
|
+
var dnsLookup = dnsLookupImpl;
|
|
3361
|
+
var BLOCKED_HOSTNAMES = new Set([
|
|
3362
|
+
"localhost",
|
|
3363
|
+
"127.0.0.1",
|
|
3364
|
+
"0.0.0.0",
|
|
3365
|
+
"::1",
|
|
3366
|
+
"metadata.google.internal"
|
|
3367
|
+
]);
|
|
3368
|
+
function isPrivateIpv4(hostname) {
|
|
3369
|
+
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
3370
|
+
if (!match) {
|
|
3371
|
+
return false;
|
|
3372
|
+
}
|
|
3373
|
+
const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
|
|
3374
|
+
if (octets.some((octet) => octet < 0 || octet > 255)) {
|
|
3375
|
+
return true;
|
|
3376
|
+
}
|
|
3377
|
+
const [a = 0, b = 0] = octets;
|
|
3378
|
+
if (a === 10) {
|
|
3379
|
+
return true;
|
|
3380
|
+
}
|
|
3381
|
+
if (a === 127) {
|
|
3382
|
+
return true;
|
|
3383
|
+
}
|
|
3384
|
+
if (a === 0) {
|
|
3385
|
+
return true;
|
|
3386
|
+
}
|
|
3387
|
+
if (a === 169 && b === 254) {
|
|
3388
|
+
return true;
|
|
3389
|
+
}
|
|
3390
|
+
if (a === 172 && b >= 16 && b <= 31) {
|
|
3391
|
+
return true;
|
|
3392
|
+
}
|
|
3393
|
+
if (a === 192 && b === 168) {
|
|
3394
|
+
return true;
|
|
3395
|
+
}
|
|
3396
|
+
return false;
|
|
3397
|
+
}
|
|
3398
|
+
function isBlockedHostname(hostname) {
|
|
3399
|
+
const normalized = hostname.trim().toLowerCase();
|
|
3400
|
+
if (normalized.length === 0) {
|
|
3401
|
+
return true;
|
|
3402
|
+
}
|
|
3403
|
+
if (BLOCKED_HOSTNAMES.has(normalized)) {
|
|
3404
|
+
return true;
|
|
3405
|
+
}
|
|
3406
|
+
if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
|
|
3407
|
+
return true;
|
|
3408
|
+
}
|
|
3409
|
+
if (normalized.includes(":")) {
|
|
3410
|
+
return true;
|
|
3411
|
+
}
|
|
3412
|
+
return isPrivateIpv4(normalized);
|
|
3413
|
+
}
|
|
3414
|
+
function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
3415
|
+
let parsed;
|
|
3416
|
+
try {
|
|
3417
|
+
parsed = new URL(rawUrl);
|
|
3418
|
+
} catch {
|
|
3419
|
+
throw new BadRequestError("Webhook URL is invalid.");
|
|
3420
|
+
}
|
|
3421
|
+
if (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
|
|
3422
|
+
throw new BadRequestError("Webhook URL must use HTTPS.");
|
|
3423
|
+
}
|
|
3424
|
+
if (parsed.username || parsed.password) {
|
|
3425
|
+
throw new BadRequestError("Webhook URL must not include credentials.");
|
|
3426
|
+
}
|
|
3427
|
+
if (isBlockedHostname(parsed.hostname)) {
|
|
3428
|
+
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
3429
|
+
}
|
|
3430
|
+
return parsed;
|
|
3431
|
+
}
|
|
3432
|
+
function isBlockedIpAddress(address) {
|
|
3433
|
+
return isBlockedHostname(address.trim().toLowerCase());
|
|
3434
|
+
}
|
|
3435
|
+
async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
|
|
3436
|
+
const parsed = assertSafeOutboundUrl(rawUrl, options);
|
|
3437
|
+
if (options.resolveDns === false) {
|
|
3438
|
+
return parsed;
|
|
3439
|
+
}
|
|
3440
|
+
const hostname = parsed.hostname.trim().toLowerCase();
|
|
3441
|
+
const results = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
3442
|
+
if (results.some((result) => isBlockedIpAddress(result.address))) {
|
|
3443
|
+
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
3444
|
+
}
|
|
3445
|
+
return parsed;
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
// ../../src/core/security/safeFetch.ts
|
|
3449
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
3450
|
+
async function safeFetch(input, init = {}, options = {}) {
|
|
3451
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
3452
|
+
const maxRedirects = options.maxRedirects ?? 0;
|
|
3453
|
+
const resolveDns = options.resolveDns ?? appConfig.env === "production";
|
|
3454
|
+
const urlOptions = { allowHttp: options.allowHttp, resolveDns };
|
|
3455
|
+
const controller = new AbortController;
|
|
3456
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
3457
|
+
try {
|
|
3458
|
+
let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
|
|
3459
|
+
let redirectCount = 0;
|
|
3460
|
+
while (true) {
|
|
3461
|
+
const response = await fetch(currentUrl, {
|
|
3462
|
+
...init,
|
|
3463
|
+
signal: controller.signal,
|
|
3464
|
+
redirect: "manual"
|
|
3465
|
+
});
|
|
3466
|
+
if (response.status >= 300 && response.status < 400) {
|
|
3467
|
+
const location = response.headers.get("location");
|
|
3468
|
+
if (!location || redirectCount >= maxRedirects) {
|
|
3469
|
+
return response;
|
|
3470
|
+
}
|
|
3471
|
+
currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
|
|
3472
|
+
redirectCount += 1;
|
|
3473
|
+
continue;
|
|
3474
|
+
}
|
|
3475
|
+
return response;
|
|
3476
|
+
}
|
|
3477
|
+
} finally {
|
|
3478
|
+
clearTimeout(timeout);
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
3483
|
+
class DispatchWebhookJob extends Job {
|
|
3484
|
+
maxAttempts = 3;
|
|
3485
|
+
backoffMs = 2000;
|
|
3486
|
+
async handle(payload) {
|
|
3487
|
+
const rows = await repositoryConnection`
|
|
3488
|
+
SELECT id, url, secret
|
|
3489
|
+
FROM webhook
|
|
3490
|
+
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
3491
|
+
LIMIT 1
|
|
3492
|
+
`;
|
|
3493
|
+
const webhook = rows[0];
|
|
3494
|
+
if (!webhook) {
|
|
3495
|
+
return;
|
|
3496
|
+
}
|
|
3497
|
+
const body = JSON.stringify({ event: payload.event, payload: payload.payload });
|
|
3498
|
+
const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
|
|
3499
|
+
assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
|
|
3500
|
+
let responseStatus = null;
|
|
3501
|
+
let errorMessage = null;
|
|
3502
|
+
try {
|
|
3503
|
+
const response = await safeFetch(webhook.url, {
|
|
3504
|
+
method: "POST",
|
|
3505
|
+
headers: {
|
|
3506
|
+
"content-type": "application/json",
|
|
3507
|
+
"x-workhub-signature": signature
|
|
3508
|
+
},
|
|
3509
|
+
body
|
|
3510
|
+
}, { allowHttp: appConfig.env !== "production" });
|
|
3511
|
+
responseStatus = response.status;
|
|
3512
|
+
if (!response.ok) {
|
|
3513
|
+
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
3514
|
+
}
|
|
3515
|
+
} catch (error) {
|
|
3516
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
3517
|
+
await repositoryConnection`
|
|
3518
|
+
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
3519
|
+
VALUES (
|
|
3520
|
+
${webhook.id},
|
|
3521
|
+
${payload.event},
|
|
3522
|
+
${JSON.stringify(payload.payload)}::jsonb,
|
|
3523
|
+
${responseStatus},
|
|
3524
|
+
${errorMessage}
|
|
3525
|
+
)
|
|
3526
|
+
`;
|
|
3527
|
+
throw error instanceof Error ? error : new Error(errorMessage);
|
|
3528
|
+
}
|
|
3529
|
+
await repositoryConnection`
|
|
3530
|
+
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
3531
|
+
VALUES (
|
|
3532
|
+
${webhook.id},
|
|
3533
|
+
${payload.event},
|
|
3534
|
+
${JSON.stringify(payload.payload)}::jsonb,
|
|
3535
|
+
${responseStatus}
|
|
3536
|
+
)
|
|
3537
|
+
`;
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
var dispatchWebhookJob_default = DispatchWebhookJob;
|
|
3541
|
+
|
|
3542
|
+
// ../../src/bootstrap/queue/defaultJobs.ts
|
|
3594
3543
|
function registerDefaultJobs() {
|
|
3595
3544
|
jobRegistry.register("cache.invalidate-tags", () => {
|
|
3596
3545
|
return new invalidateCacheTagsJob_default(resolveApplicationCache());
|
|
3597
3546
|
});
|
|
3598
3547
|
jobRegistry.register("webhook.dispatch", () => new dispatchWebhookJob_default);
|
|
3599
3548
|
}
|
|
3600
|
-
|
|
3549
|
+
|
|
3550
|
+
// ../../src/core/queue/createAppQueue.ts
|
|
3551
|
+
var FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
|
|
3552
|
+
function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(), registerJobs) {
|
|
3601
3553
|
return createProductionQueue(driver, {
|
|
3602
3554
|
redisUrl,
|
|
3603
3555
|
failedJobs,
|
|
3604
|
-
registerJobs
|
|
3556
|
+
registerJobs
|
|
3605
3557
|
});
|
|
3606
3558
|
}
|
|
3607
3559
|
|
|
@@ -3618,7 +3570,7 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus) {
|
|
|
3618
3570
|
let cache;
|
|
3619
3571
|
let queue;
|
|
3620
3572
|
try {
|
|
3621
|
-
cache =
|
|
3573
|
+
cache = resolveApplicationCache2();
|
|
3622
3574
|
queue = resolveApplicationQueue();
|
|
3623
3575
|
} catch {
|
|
3624
3576
|
return;
|
|
@@ -3709,7 +3661,7 @@ var queueProvider = {
|
|
|
3709
3661
|
config.set("queue.driver", driver);
|
|
3710
3662
|
const failedJobs = createFailedJobService();
|
|
3711
3663
|
container.set(FAILED_JOB_SERVICE_TOKEN, failedJobs);
|
|
3712
|
-
container.set(CORE_QUEUE_TOKEN, createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs));
|
|
3664
|
+
container.set(CORE_QUEUE_TOKEN, createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs, registerDefaultJobs));
|
|
3713
3665
|
}
|
|
3714
3666
|
};
|
|
3715
3667
|
var queue_default = queueProvider;
|
|
@@ -3852,8 +3804,7 @@ function isViewsEnabled() {
|
|
|
3852
3804
|
}
|
|
3853
3805
|
|
|
3854
3806
|
// ../../src/core/http/requestMetaContext.ts
|
|
3855
|
-
|
|
3856
|
-
var requestMetaContext = new AsyncLocalStorage3;
|
|
3807
|
+
var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
|
|
3857
3808
|
function currentRequestMeta() {
|
|
3858
3809
|
return requestMetaContext.getStore() ?? {
|
|
3859
3810
|
ipAddress: null,
|
|
@@ -4186,7 +4137,7 @@ function createAppContext() {
|
|
|
4186
4137
|
config,
|
|
4187
4138
|
dependencies
|
|
4188
4139
|
};
|
|
4189
|
-
|
|
4140
|
+
setActiveApplicationContext(appContext);
|
|
4190
4141
|
return appContext;
|
|
4191
4142
|
}
|
|
4192
4143
|
var cachedAppContext;
|