@getstrata/core 0.5.54 → 0.5.56
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 -13
- package/dist/core/http/index.d.ts +5 -10
- package/dist/core/http/response.d.ts +6 -0
- package/dist/core/media/imageTransform.d.ts +13 -0
- package/dist/core/scheduler/osCron.d.ts +14 -0
- package/dist/core/terminal/runShell.d.ts +6 -0
- package/dist/core/view/assertEtaHtmlSource.d.ts +5 -0
- package/dist/core/view/etaViewEngine.d.ts +4 -0
- package/dist/entries/audit/exportAuditLogs.js +2 -2
- package/dist/entries/audit/siemFormatter.js +2 -2
- package/dist/entries/auth/accessControl.js +4 -4
- package/dist/entries/auth/membershipScope.js +6 -6
- package/dist/entries/auth/oauth/providers.js +2 -2
- package/dist/entries/auth/password.js +2 -2
- package/dist/entries/auth/policy.js +2 -2
- package/dist/entries/auth/sessionCookie.js +4 -4
- package/dist/entries/auth/sessionGuard.js +0 -91
- package/dist/entries/auth/tokenHash.js +2 -2
- package/dist/entries/config/envSchema.js +2 -2
- package/dist/entries/contracts/container.js +2 -2
- package/dist/entries/contracts/di.js +2 -2
- package/dist/entries/contracts/serviceTokens.js +6 -6
- package/dist/entries/crypto/fieldEncryption.js +8 -8
- package/dist/entries/crypto/mfaSecret.js +2 -2
- package/dist/entries/database/errors.js +2 -2
- package/dist/entries/database/migrations.js +8 -8
- package/dist/entries/database/model.js +5 -5
- package/dist/entries/database/query.js +19 -19
- package/dist/entries/database/relationships.js +13 -13
- package/dist/entries/database/schema.js +13 -13
- package/dist/entries/database/seeders.js +2 -2
- package/dist/entries/facades.js +8 -8
- package/dist/entries/http/authMiddleware.js +2 -2
- package/dist/entries/http/bodySizeLimitMiddleware.js +2 -2
- package/dist/entries/http/cookies.js +2 -2
- package/dist/entries/http/csrfProtection.js +2 -2
- package/dist/entries/http/csrfToken.js +6 -6
- package/dist/entries/http/etag.js +8 -8
- package/dist/entries/http/flashSession.js +4 -4
- package/dist/entries/http/formRequest.js +2 -2
- package/dist/entries/http/loginThrottleMiddleware.js +2 -2
- package/dist/entries/http/metricsMiddleware.js +2 -2
- package/dist/entries/http/pagination.js +4 -4
- package/dist/entries/http/parseFormBody.js +2 -2
- package/dist/entries/http/parseMultipartUpload.js +2 -2
- package/dist/entries/http/resources.js +2 -2
- package/dist/entries/http/response.js +968 -0
- package/dist/entries/http/securedRouteModelBinding.js +2 -2
- package/dist/entries/http/throttleMiddleware.js +2 -2
- package/dist/entries/http/validation.js +13 -13
- package/dist/entries/http/webErrorResponse.js +184 -96
- package/dist/entries/lifecycle/gracefulShutdown.js +3 -3
- package/dist/entries/logging/logger.js +2 -2
- package/dist/entries/mail/mailer.js +6 -6
- package/dist/entries/mail/markdownMail.js +3 -3
- package/dist/entries/mail/markdownMailable.js +2 -2
- package/dist/entries/media/imageTransform.js +40 -0
- package/dist/entries/metrics/prometheus.js +2 -2
- package/dist/entries/openapi/generator.js +2 -2
- package/dist/entries/queue/createAppQueue.js +4 -4
- package/dist/entries/queue/publicQueue.js +10 -10
- package/dist/entries/queue/queueMetrics.js +2 -2
- package/dist/entries/queue/redisQueue.js +7 -7
- package/dist/entries/queue.js +3 -3
- package/dist/entries/scheduler/osCron.js +27 -0
- package/dist/entries/scheduler/schedule.js +2 -2
- package/dist/entries/security/oauthState.js +3 -3
- package/dist/entries/security/publicReads.js +2 -2
- package/dist/entries/security/safeFetch.js +2 -2
- package/dist/entries/security/safeUrl.js +5 -5
- package/dist/entries/security/scimTenantTokens.js +2 -2
- package/dist/entries/security/totp.js +2 -2
- package/dist/entries/storage/storage.js +7 -7
- package/dist/entries/tenant/tenantDatabaseScope.js +2 -2
- package/dist/entries/terminal/runShell.js +18 -0
- package/dist/entries/validation/rules.js +12 -12
- package/dist/entries/view.js +186 -98
- package/dist/framework/public-api.d.ts +2 -1
- package/dist/index.js +1820 -1858
- package/package.json +22 -2
package/dist/index.js
CHANGED
|
@@ -196,13 +196,6 @@ var ADMIN_ABILITIES = [
|
|
|
196
196
|
"webhooks:write",
|
|
197
197
|
"audit:read"
|
|
198
198
|
];
|
|
199
|
-
var PLATFORM_ADMIN_ABILITIES = ["*"];
|
|
200
|
-
function resolveAbilitiesForRole(role) {
|
|
201
|
-
if (role === "admin") {
|
|
202
|
-
return [...PLATFORM_ADMIN_ABILITIES];
|
|
203
|
-
}
|
|
204
|
-
return [...MEMBER_ABILITIES];
|
|
205
|
-
}
|
|
206
199
|
|
|
207
200
|
// ../../src/core/contracts/serviceTokens.ts
|
|
208
201
|
var CORE_QUEUE_TOKEN = "core.queue";
|
|
@@ -1608,65 +1601,6 @@ var apiTokenTable = defineTable({
|
|
|
1608
1601
|
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
1609
1602
|
});
|
|
1610
1603
|
|
|
1611
|
-
// ../../src/core/auth/password.ts
|
|
1612
|
-
async function verifyPassword(password, passwordHash) {
|
|
1613
|
-
return await Bun.password.verify(password, passwordHash);
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
// ../../src/core/crypto/fieldEncryption.ts
|
|
1617
|
-
import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "crypto";
|
|
1618
|
-
var ENCRYPTION_PREFIX = "enc:v1:";
|
|
1619
|
-
var IV_LENGTH = 12;
|
|
1620
|
-
var TAG_LENGTH = 16;
|
|
1621
|
-
function resolveEncryptionKey() {
|
|
1622
|
-
const raw = process.env.KMS_ENCRYPTION_KEY?.trim();
|
|
1623
|
-
if (!raw) {
|
|
1624
|
-
return null;
|
|
1625
|
-
}
|
|
1626
|
-
if (/^[0-9a-f]{64}$/i.test(raw)) {
|
|
1627
|
-
return Buffer.from(raw, "hex");
|
|
1628
|
-
}
|
|
1629
|
-
const decoded = Buffer.from(raw, "base64");
|
|
1630
|
-
if (decoded.length === 32) {
|
|
1631
|
-
return decoded;
|
|
1632
|
-
}
|
|
1633
|
-
throw new Error("KMS_ENCRYPTION_KEY must be 32 bytes (hex or base64).");
|
|
1634
|
-
}
|
|
1635
|
-
function isFieldEncryptionEnabled() {
|
|
1636
|
-
const featureFlag = process.env.FEATURE_FIELD_ENCRYPTION;
|
|
1637
|
-
if (featureFlag === "false") {
|
|
1638
|
-
return false;
|
|
1639
|
-
}
|
|
1640
|
-
if (featureFlag === "true") {
|
|
1641
|
-
return true;
|
|
1642
|
-
}
|
|
1643
|
-
return (process.env.APP_ENV ?? "local") === "production";
|
|
1644
|
-
}
|
|
1645
|
-
function decryptField(value, key) {
|
|
1646
|
-
if (!value.startsWith(ENCRYPTION_PREFIX)) {
|
|
1647
|
-
return value;
|
|
1648
|
-
}
|
|
1649
|
-
const payload = Buffer.from(value.slice(ENCRYPTION_PREFIX.length), "base64");
|
|
1650
|
-
const iv = payload.subarray(0, IV_LENGTH);
|
|
1651
|
-
const tag = payload.subarray(payload.length - TAG_LENGTH);
|
|
1652
|
-
const ciphertext = payload.subarray(IV_LENGTH, payload.length - TAG_LENGTH);
|
|
1653
|
-
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
1654
|
-
decipher.setAuthTag(tag);
|
|
1655
|
-
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
// ../../src/core/crypto/mfaSecret.ts
|
|
1659
|
-
function revealMfaSecret(stored) {
|
|
1660
|
-
if (!stored) {
|
|
1661
|
-
return null;
|
|
1662
|
-
}
|
|
1663
|
-
const key = resolveEncryptionKey();
|
|
1664
|
-
if (!isFieldEncryptionEnabled() || !key || !stored.startsWith("enc:v1:")) {
|
|
1665
|
-
return stored;
|
|
1666
|
-
}
|
|
1667
|
-
return decryptField(stored, key);
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
1604
|
// ../../src/core/http/requestMetaContext.ts
|
|
1671
1605
|
var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
|
|
1672
1606
|
function runWithRequestMeta(meta, callback) {
|
|
@@ -1694,66 +1628,6 @@ function logSecurityEvent(event, details = {}) {
|
|
|
1694
1628
|
}));
|
|
1695
1629
|
}
|
|
1696
1630
|
|
|
1697
|
-
// ../../src/core/security/tokenExpiry.ts
|
|
1698
|
-
function resolveDefaultTokenExpiryDays() {
|
|
1699
|
-
const raw = process.env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim();
|
|
1700
|
-
if (!raw) {
|
|
1701
|
-
return null;
|
|
1702
|
-
}
|
|
1703
|
-
const parsed = Number.parseInt(raw, 10);
|
|
1704
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
1705
|
-
return null;
|
|
1706
|
-
}
|
|
1707
|
-
return parsed;
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
// ../../src/core/security/totp.ts
|
|
1711
|
-
import { createHmac as createHmac2 } from "crypto";
|
|
1712
|
-
function decodeBase32(input) {
|
|
1713
|
-
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
1714
|
-
const normalized = input.replace(/=+$/u, "").toUpperCase();
|
|
1715
|
-
let bits = "";
|
|
1716
|
-
for (const char of normalized) {
|
|
1717
|
-
const value = alphabet.indexOf(char);
|
|
1718
|
-
if (value === -1) {
|
|
1719
|
-
throw new Error("Invalid base32 character in MFA secret.");
|
|
1720
|
-
}
|
|
1721
|
-
bits += value.toString(2).padStart(5, "0");
|
|
1722
|
-
}
|
|
1723
|
-
const bytes = [];
|
|
1724
|
-
for (let index = 0;index + 8 <= bits.length; index += 8) {
|
|
1725
|
-
bytes.push(Number.parseInt(bits.slice(index, index + 8), 2));
|
|
1726
|
-
}
|
|
1727
|
-
return Buffer.from(bytes);
|
|
1728
|
-
}
|
|
1729
|
-
function generateTotp(secret, counter, digits = 6) {
|
|
1730
|
-
const key = decodeBase32(secret);
|
|
1731
|
-
const buffer = Buffer.alloc(8);
|
|
1732
|
-
buffer.writeBigUInt64BE(BigInt(counter));
|
|
1733
|
-
const digest = createHmac2("sha1", key).update(buffer).digest();
|
|
1734
|
-
const lastByte = digest[digest.length - 1] ?? 0;
|
|
1735
|
-
const offset = lastByte & 15;
|
|
1736
|
-
const b0 = digest[offset] ?? 0;
|
|
1737
|
-
const b1 = digest[offset + 1] ?? 0;
|
|
1738
|
-
const b2 = digest[offset + 2] ?? 0;
|
|
1739
|
-
const b3 = digest[offset + 3] ?? 0;
|
|
1740
|
-
const code = (b0 & 127) << 24 | (b1 & 255) << 16 | (b2 & 255) << 8 | b3 & 255;
|
|
1741
|
-
return String(code % 10 ** digits).padStart(digits, "0");
|
|
1742
|
-
}
|
|
1743
|
-
function verifyTotp(secret, token, window = 1) {
|
|
1744
|
-
const normalized = token.trim();
|
|
1745
|
-
if (!/^\d{6}$/u.test(normalized)) {
|
|
1746
|
-
return false;
|
|
1747
|
-
}
|
|
1748
|
-
const timestep = Math.floor(Date.now() / 30000);
|
|
1749
|
-
for (let offset = -window;offset <= window; offset += 1) {
|
|
1750
|
-
if (generateTotp(secret, timestep + offset) === normalized) {
|
|
1751
|
-
return true;
|
|
1752
|
-
}
|
|
1753
|
-
}
|
|
1754
|
-
return false;
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
1631
|
// ../../src/core/tenant/tenantContext.ts
|
|
1758
1632
|
var tenantContext = createAsyncContextStore("@getstrata/tenantContext");
|
|
1759
1633
|
function runWithTenant(tenant, callback) {
|
|
@@ -1776,99 +1650,6 @@ function rateLimitMultiplierForPlan(plan) {
|
|
|
1776
1650
|
}
|
|
1777
1651
|
}
|
|
1778
1652
|
|
|
1779
|
-
// ../../src/modules/user/authService.ts
|
|
1780
|
-
class AuthService {
|
|
1781
|
-
users;
|
|
1782
|
-
tokens;
|
|
1783
|
-
oauthIdentities;
|
|
1784
|
-
oauthProviders = new Map;
|
|
1785
|
-
constructor(users, tokens, oauthIdentities) {
|
|
1786
|
-
this.users = users;
|
|
1787
|
-
this.tokens = tokens;
|
|
1788
|
-
this.oauthIdentities = oauthIdentities;
|
|
1789
|
-
}
|
|
1790
|
-
registerOAuthProvider(provider) {
|
|
1791
|
-
this.oauthProviders.set(provider.name, provider);
|
|
1792
|
-
}
|
|
1793
|
-
getOAuthProvider(name) {
|
|
1794
|
-
return this.oauthProviders.get(name);
|
|
1795
|
-
}
|
|
1796
|
-
async loginWithPassword(email, password, options = {}) {
|
|
1797
|
-
const user = await this.users.findByEmail(email);
|
|
1798
|
-
if (!user?.password_hash) {
|
|
1799
|
-
logSecurityEvent("auth_login_failed", { reason: "unknown_user", email });
|
|
1800
|
-
throw new UnauthorizedError("Invalid credentials.");
|
|
1801
|
-
}
|
|
1802
|
-
const valid = await verifyPassword(password, user.password_hash);
|
|
1803
|
-
if (!valid) {
|
|
1804
|
-
logSecurityEvent("auth_login_failed", { reason: "invalid_password", user_id: user.id });
|
|
1805
|
-
throw new UnauthorizedError("Invalid credentials.");
|
|
1806
|
-
}
|
|
1807
|
-
if (isFeatureEnabled("emailVerification") && !user.email_verified_at) {
|
|
1808
|
-
logSecurityEvent("auth_login_blocked", { reason: "email_unverified", user_id: user.id });
|
|
1809
|
-
throw new UnauthorizedError("Email address is not verified.");
|
|
1810
|
-
}
|
|
1811
|
-
if (isFeatureEnabled("mfa") && user.mfa_enabled) {
|
|
1812
|
-
const mfaSecret = revealMfaSecret(user.mfa_secret);
|
|
1813
|
-
if (!mfaSecret || !options.mfaCode || !verifyTotp(mfaSecret, options.mfaCode)) {
|
|
1814
|
-
logSecurityEvent("auth_login_failed", { reason: "invalid_mfa", user_id: user.id });
|
|
1815
|
-
throw new UnauthorizedError("Invalid MFA code.");
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
logSecurityEvent("auth_login_success", { user_id: user.id, method: "password" });
|
|
1819
|
-
return await this.tokens.createToken(user.id, {
|
|
1820
|
-
name: "password-login",
|
|
1821
|
-
abilities: resolveAbilitiesForRole(user.role),
|
|
1822
|
-
expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
|
|
1823
|
-
});
|
|
1824
|
-
}
|
|
1825
|
-
async loginWithOAuth(providerName, code) {
|
|
1826
|
-
const provider = this.oauthProviders.get(providerName);
|
|
1827
|
-
if (!provider) {
|
|
1828
|
-
throw new UnauthorizedError("Unsupported OAuth provider.");
|
|
1829
|
-
}
|
|
1830
|
-
const profile = await provider.exchangeCode(code);
|
|
1831
|
-
const user = await this.findOrCreateOAuthUser(providerName, profile);
|
|
1832
|
-
logSecurityEvent("auth_login_success", { user_id: user.id, method: `oauth:${providerName}` });
|
|
1833
|
-
return await this.tokens.createToken(user.id, {
|
|
1834
|
-
name: `${providerName}-oauth`,
|
|
1835
|
-
abilities: resolveAbilitiesForRole(user.role),
|
|
1836
|
-
expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
|
|
1837
|
-
});
|
|
1838
|
-
}
|
|
1839
|
-
buildOAuthAuthorizationUrl(providerName, state) {
|
|
1840
|
-
const provider = this.oauthProviders.get(providerName);
|
|
1841
|
-
if (!provider) {
|
|
1842
|
-
throw new UnauthorizedError("Unsupported OAuth provider.");
|
|
1843
|
-
}
|
|
1844
|
-
return provider.getAuthorizationUrl(state);
|
|
1845
|
-
}
|
|
1846
|
-
async findOrCreateOAuthUser(providerName, profile) {
|
|
1847
|
-
const existingIdentity = await this.oauthIdentities.findByProviderUser(providerName, profile.providerUserId);
|
|
1848
|
-
if (existingIdentity) {
|
|
1849
|
-
return await this.users.findByIdOrThrow(existingIdentity.user_id);
|
|
1850
|
-
}
|
|
1851
|
-
const existingUser = await this.users.findByEmail(profile.email);
|
|
1852
|
-
const user = existingUser ?? await this.users.create({
|
|
1853
|
-
name: profile.name,
|
|
1854
|
-
email: profile.email,
|
|
1855
|
-
role: "member",
|
|
1856
|
-
tenant_id: currentTenantId(),
|
|
1857
|
-
email_verified_at: new Date,
|
|
1858
|
-
created_at: new Date,
|
|
1859
|
-
updated_at: new Date
|
|
1860
|
-
});
|
|
1861
|
-
await this.oauthIdentities.create({
|
|
1862
|
-
user_id: user.id,
|
|
1863
|
-
provider: providerName,
|
|
1864
|
-
provider_user_id: profile.providerUserId,
|
|
1865
|
-
email: profile.email,
|
|
1866
|
-
created_at: new Date
|
|
1867
|
-
});
|
|
1868
|
-
return user;
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
1653
|
// ../../src/modules/user/notificationTable.ts
|
|
1873
1654
|
var notificationTable = defineTable({
|
|
1874
1655
|
name: "notification",
|
|
@@ -4840,14 +4621,14 @@ function createCsrfProtection(secret, options = {}) {
|
|
|
4840
4621
|
};
|
|
4841
4622
|
}
|
|
4842
4623
|
// ../../src/core/http/flashSession.ts
|
|
4843
|
-
import { createHmac
|
|
4624
|
+
import { createHmac, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
4844
4625
|
var FLASH_COOKIE = "workhub_flash";
|
|
4845
4626
|
var FLASH_TTL_MS = 60 * 1000;
|
|
4846
4627
|
function resolveFlashSecret() {
|
|
4847
4628
|
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
|
|
4848
4629
|
}
|
|
4849
4630
|
function signFlashPayload(payload, issuedAt) {
|
|
4850
|
-
const signature =
|
|
4631
|
+
const signature = createHmac("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
|
|
4851
4632
|
return `${payload}.${issuedAt}.${signature}`;
|
|
4852
4633
|
}
|
|
4853
4634
|
function readFlashCookie(request) {
|
|
@@ -4983,164 +4764,6 @@ class FormRequest {
|
|
|
4983
4764
|
return await parseJsonBody(request, (payload) => this.parse(payload));
|
|
4984
4765
|
}
|
|
4985
4766
|
}
|
|
4986
|
-
// ../../src/config/frontend.ts
|
|
4987
|
-
function readFrontendMode() {
|
|
4988
|
-
const mode = (process.env.FRONTEND_MODE ?? "api").trim();
|
|
4989
|
-
if (mode === "server-htmx") {
|
|
4990
|
-
return "server-htmx";
|
|
4991
|
-
}
|
|
4992
|
-
if (mode === "spa-react") {
|
|
4993
|
-
return "spa-react";
|
|
4994
|
-
}
|
|
4995
|
-
return "api";
|
|
4996
|
-
}
|
|
4997
|
-
function isViewsEnabled() {
|
|
4998
|
-
return readFrontendMode() === "server-htmx";
|
|
4999
|
-
}
|
|
5000
|
-
|
|
5001
|
-
// ../../src/core/view/etaViewEngine.ts
|
|
5002
|
-
import { join as join4 } from "path";
|
|
5003
|
-
import { Eta } from "eta";
|
|
5004
|
-
var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
|
|
5005
|
-
var DEFAULT_LAYOUT = "layouts/app.eta";
|
|
5006
|
-
|
|
5007
|
-
class EtaViewEngine {
|
|
5008
|
-
eta;
|
|
5009
|
-
resolveLayoutData;
|
|
5010
|
-
constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
|
|
5011
|
-
this.eta = new Eta({
|
|
5012
|
-
views: viewsDirectory,
|
|
5013
|
-
autoTrim: false
|
|
5014
|
-
});
|
|
5015
|
-
this.resolveLayoutData = resolveLayoutData;
|
|
5016
|
-
}
|
|
5017
|
-
async render(name, data = {}, options = {}) {
|
|
5018
|
-
const template = name.endsWith(".eta") ? name : `${name}.eta`;
|
|
5019
|
-
const layoutData = this.resolveLayoutData ? await this.resolveLayoutData() : {};
|
|
5020
|
-
const mergedData = { ...layoutData, ...data };
|
|
5021
|
-
const body = await this.eta.renderAsync(template, mergedData);
|
|
5022
|
-
const layout = options.layout ?? DEFAULT_LAYOUT;
|
|
5023
|
-
if (layout === false) {
|
|
5024
|
-
return body;
|
|
5025
|
-
}
|
|
5026
|
-
const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
|
|
5027
|
-
return await this.eta.renderAsync(layoutTemplate, {
|
|
5028
|
-
...mergedData,
|
|
5029
|
-
body
|
|
5030
|
-
});
|
|
5031
|
-
}
|
|
5032
|
-
}
|
|
5033
|
-
// ../../src/core/view/htmlResponse.ts
|
|
5034
|
-
function htmlResponse(html, init = {}) {
|
|
5035
|
-
return new Response(html, {
|
|
5036
|
-
status: init.status ?? 200,
|
|
5037
|
-
statusText: init.statusText,
|
|
5038
|
-
headers: {
|
|
5039
|
-
"Content-Type": "text/html; charset=utf-8"
|
|
5040
|
-
}
|
|
5041
|
-
});
|
|
5042
|
-
}
|
|
5043
|
-
function isHtmxRequest(request) {
|
|
5044
|
-
return request.headers.get("HX-Request") === "true";
|
|
5045
|
-
}
|
|
5046
|
-
// ../../src/core/view/webLayoutData.ts
|
|
5047
|
-
async function resolveWebLayoutData(container, request) {
|
|
5048
|
-
const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
|
|
5049
|
-
const flash = request ? currentRequestMeta().flash ?? pullFlash(request) : null;
|
|
5050
|
-
const authUser = currentAuthUser();
|
|
5051
|
-
if (!authUser) {
|
|
5052
|
-
return { authUser: null, csrfToken, flash };
|
|
5053
|
-
}
|
|
5054
|
-
const userId = Number(authUser.id);
|
|
5055
|
-
if (!Number.isInteger(userId) || userId <= 0) {
|
|
5056
|
-
return { authUser: null, csrfToken, flash };
|
|
5057
|
-
}
|
|
5058
|
-
if (!container.has(tokenServiceToken)) {
|
|
5059
|
-
return {
|
|
5060
|
-
authUser: {
|
|
5061
|
-
id: userId,
|
|
5062
|
-
email: "",
|
|
5063
|
-
role: authUser.role ?? "member"
|
|
5064
|
-
},
|
|
5065
|
-
csrfToken,
|
|
5066
|
-
flash
|
|
5067
|
-
};
|
|
5068
|
-
}
|
|
5069
|
-
const tokenService = container.resolve(tokenServiceToken);
|
|
5070
|
-
try {
|
|
5071
|
-
const user = await tokenService.findByIdOrThrow(userId);
|
|
5072
|
-
return {
|
|
5073
|
-
authUser: {
|
|
5074
|
-
id: userId,
|
|
5075
|
-
email: user.email ?? "",
|
|
5076
|
-
role: authUser.role ?? user.role ?? "member"
|
|
5077
|
-
},
|
|
5078
|
-
csrfToken,
|
|
5079
|
-
flash
|
|
5080
|
-
};
|
|
5081
|
-
} catch {
|
|
5082
|
-
return { authUser: null, csrfToken, flash };
|
|
5083
|
-
}
|
|
5084
|
-
}
|
|
5085
|
-
// ../../src/core/http/contentNegotiation.ts
|
|
5086
|
-
function requestPrefersJson(request) {
|
|
5087
|
-
if (!request) {
|
|
5088
|
-
return true;
|
|
5089
|
-
}
|
|
5090
|
-
if (request.headers.get("HX-Request") === "true") {
|
|
5091
|
-
return false;
|
|
5092
|
-
}
|
|
5093
|
-
const accept = request.headers.get("accept")?.toLowerCase() ?? "";
|
|
5094
|
-
if (accept.includes("text/html")) {
|
|
5095
|
-
return false;
|
|
5096
|
-
}
|
|
5097
|
-
if (accept.includes("application/json")) {
|
|
5098
|
-
return true;
|
|
5099
|
-
}
|
|
5100
|
-
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
5101
|
-
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
5102
|
-
return false;
|
|
5103
|
-
}
|
|
5104
|
-
const pathname = new URL(request.url).pathname;
|
|
5105
|
-
return pathname.startsWith("/api/");
|
|
5106
|
-
}
|
|
5107
|
-
|
|
5108
|
-
// ../../src/core/http/webErrorResponse.ts
|
|
5109
|
-
function normalizeFieldErrors(details) {
|
|
5110
|
-
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
|
5111
|
-
return {};
|
|
5112
|
-
}
|
|
5113
|
-
const errors = {};
|
|
5114
|
-
for (const [field, messages] of Object.entries(details)) {
|
|
5115
|
-
if (Array.isArray(messages)) {
|
|
5116
|
-
errors[field] = messages.map(String);
|
|
5117
|
-
continue;
|
|
5118
|
-
}
|
|
5119
|
-
if (typeof messages === "string") {
|
|
5120
|
-
errors[field] = [messages];
|
|
5121
|
-
}
|
|
5122
|
-
}
|
|
5123
|
-
return errors;
|
|
5124
|
-
}
|
|
5125
|
-
function webErrorResponse(error, request) {
|
|
5126
|
-
if (!request || !isViewsEnabled() || requestPrefersJson(request)) {
|
|
5127
|
-
return null;
|
|
5128
|
-
}
|
|
5129
|
-
const mappedError = error instanceof HttpError ? error : mapDatabaseError(error);
|
|
5130
|
-
if (mappedError instanceof UnauthorizedError) {
|
|
5131
|
-
const redirectTarget = encodeURIComponent(new URL(request.url).pathname);
|
|
5132
|
-
return Response.redirect(`/login?redirect=${redirectTarget}`, 302);
|
|
5133
|
-
}
|
|
5134
|
-
if (mappedError instanceof ValidationError) {
|
|
5135
|
-
const errors = normalizeFieldErrors(mappedError.details);
|
|
5136
|
-
const fieldSummary = Object.entries(errors).flatMap(([field, messages]) => messages.map((message) => `${field}: ${message}`)).join(`
|
|
5137
|
-
`);
|
|
5138
|
-
return htmlResponse(`<section class="page-header"><h1>Validation failed</h1><pre>${fieldSummary || mappedError.message}</pre><p><a href="javascript:history.back()">Go back</a></p></section>`, { status: mappedError.status });
|
|
5139
|
-
}
|
|
5140
|
-
return htmlResponse(`<section class="page-header"><h1>${mappedError.message}</h1></section>`, {
|
|
5141
|
-
status: mappedError.status
|
|
5142
|
-
});
|
|
5143
|
-
}
|
|
5144
4767
|
// ../../src/core/http/authMiddleware.ts
|
|
5145
4768
|
function createAuthMiddleware(auth2) {
|
|
5146
4769
|
return async (request, next) => {
|
|
@@ -5263,118 +4886,33 @@ function parsePaginationQuery(request) {
|
|
|
5263
4886
|
function paginatedResponse(data, meta, init = {}) {
|
|
5264
4887
|
return Response.json({ data, meta }, init);
|
|
5265
4888
|
}
|
|
5266
|
-
// ../../src/
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
4889
|
+
// ../../src/config/uploads.ts
|
|
4890
|
+
var DEFAULT_MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
|
|
4891
|
+
var ALLOWED_UPLOAD_MIME_TYPES = new Set([
|
|
4892
|
+
"application/pdf",
|
|
4893
|
+
"application/json",
|
|
4894
|
+
"application/zip",
|
|
4895
|
+
"application/x-zip-compressed",
|
|
4896
|
+
"image/jpeg",
|
|
4897
|
+
"image/png",
|
|
4898
|
+
"image/gif",
|
|
4899
|
+
"image/webp",
|
|
4900
|
+
"text/plain",
|
|
4901
|
+
"text/csv"
|
|
4902
|
+
]);
|
|
4903
|
+
function resolveMaxUploadBytes() {
|
|
4904
|
+
const raw = process.env.MAX_UPLOAD_BYTES?.trim() ?? process.env.MAX_REQUEST_BODY_BYTES?.trim();
|
|
4905
|
+
if (!raw) {
|
|
4906
|
+
return DEFAULT_MAX_UPLOAD_BYTES;
|
|
4907
|
+
}
|
|
4908
|
+
const parsed = Number.parseInt(raw, 10);
|
|
4909
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
4910
|
+
return DEFAULT_MAX_UPLOAD_BYTES;
|
|
4911
|
+
}
|
|
4912
|
+
return parsed;
|
|
5279
4913
|
}
|
|
5280
|
-
function
|
|
5281
|
-
return
|
|
5282
|
-
}
|
|
5283
|
-
function toPaginatedResourceCollection(items, meta, transformer) {
|
|
5284
|
-
return {
|
|
5285
|
-
data: toResourceCollection(items, transformer),
|
|
5286
|
-
meta
|
|
5287
|
-
};
|
|
5288
|
-
}
|
|
5289
|
-
// ../../src/core/http/routeMiddleware.ts
|
|
5290
|
-
function withMiddleware(...middleware) {
|
|
5291
|
-
const wrap = composeMiddleware(...middleware);
|
|
5292
|
-
return (handler) => {
|
|
5293
|
-
return wrap(handler);
|
|
5294
|
-
};
|
|
5295
|
-
}
|
|
5296
|
-
// ../../src/core/http/routeModelBinding.ts
|
|
5297
|
-
function bindRouteModel(param, resolver, handler) {
|
|
5298
|
-
return async (request) => {
|
|
5299
|
-
const id = parsePositiveIntParam(String(request.params[param]), String(param));
|
|
5300
|
-
const model = await resolver(id, request);
|
|
5301
|
-
return await handler(request, model);
|
|
5302
|
-
};
|
|
5303
|
-
}
|
|
5304
|
-
// ../../src/core/http/securedRouteModelBinding.ts
|
|
5305
|
-
function isMutatingPolicyAction(action) {
|
|
5306
|
-
return action === "update" || action === "delete";
|
|
5307
|
-
}
|
|
5308
|
-
function securedBindRouteModel(param, resolver, authorization, handler) {
|
|
5309
|
-
return async (request) => {
|
|
5310
|
-
const id = parsePositiveIntParam(String(request.params[param]), String(param));
|
|
5311
|
-
const model = await resolver(id, request);
|
|
5312
|
-
const gate = resolveApplicationPolicyGate();
|
|
5313
|
-
const auth2 = resolveApplicationAuth();
|
|
5314
|
-
const user = currentAuthUser() ?? await auth2.resolve(request);
|
|
5315
|
-
gate.authorize(authorization.resource, authorization.action, user, model);
|
|
5316
|
-
if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
|
|
5317
|
-
assertIfMatch(request, etagFromResource(model), {
|
|
5318
|
-
required: authorization.requireIfMatch ?? true
|
|
5319
|
-
});
|
|
5320
|
-
}
|
|
5321
|
-
const response = await handler(request, model);
|
|
5322
|
-
if (isEtagEnabled() && authorization.action === "view") {
|
|
5323
|
-
return applyConditionalGet(request, response, etagFromResource(model));
|
|
5324
|
-
}
|
|
5325
|
-
return response;
|
|
5326
|
-
};
|
|
5327
|
-
}
|
|
5328
|
-
function securedBindRouteModelByKey(param, resolver, authorization, handler) {
|
|
5329
|
-
return async (request) => {
|
|
5330
|
-
const key = String(request.params[param] ?? "").trim();
|
|
5331
|
-
if (!key) {
|
|
5332
|
-
throw new BadRequestError(`Missing route parameter "${String(param)}".`);
|
|
5333
|
-
}
|
|
5334
|
-
const model = await resolver(key, request);
|
|
5335
|
-
const gate = resolveApplicationPolicyGate();
|
|
5336
|
-
const auth2 = resolveApplicationAuth();
|
|
5337
|
-
const user = currentAuthUser() ?? await auth2.resolve(request);
|
|
5338
|
-
gate.authorize(authorization.resource, authorization.action, user, model);
|
|
5339
|
-
if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
|
|
5340
|
-
assertIfMatch(request, etagFromResource(model), {
|
|
5341
|
-
required: authorization.requireIfMatch ?? true
|
|
5342
|
-
});
|
|
5343
|
-
}
|
|
5344
|
-
const response = await handler(request, model);
|
|
5345
|
-
if (isEtagEnabled() && authorization.action === "view") {
|
|
5346
|
-
return applyConditionalGet(request, response, etagFromResource(model));
|
|
5347
|
-
}
|
|
5348
|
-
return response;
|
|
5349
|
-
};
|
|
5350
|
-
}
|
|
5351
|
-
// ../../src/config/uploads.ts
|
|
5352
|
-
var DEFAULT_MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
|
|
5353
|
-
var ALLOWED_UPLOAD_MIME_TYPES = new Set([
|
|
5354
|
-
"application/pdf",
|
|
5355
|
-
"application/json",
|
|
5356
|
-
"application/zip",
|
|
5357
|
-
"application/x-zip-compressed",
|
|
5358
|
-
"image/jpeg",
|
|
5359
|
-
"image/png",
|
|
5360
|
-
"image/gif",
|
|
5361
|
-
"image/webp",
|
|
5362
|
-
"text/plain",
|
|
5363
|
-
"text/csv"
|
|
5364
|
-
]);
|
|
5365
|
-
function resolveMaxUploadBytes() {
|
|
5366
|
-
const raw = process.env.MAX_UPLOAD_BYTES?.trim() ?? process.env.MAX_REQUEST_BODY_BYTES?.trim();
|
|
5367
|
-
if (!raw) {
|
|
5368
|
-
return DEFAULT_MAX_UPLOAD_BYTES;
|
|
5369
|
-
}
|
|
5370
|
-
const parsed = Number.parseInt(raw, 10);
|
|
5371
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
5372
|
-
return DEFAULT_MAX_UPLOAD_BYTES;
|
|
5373
|
-
}
|
|
5374
|
-
return parsed;
|
|
5375
|
-
}
|
|
5376
|
-
function normalizeMimeType(mimeType) {
|
|
5377
|
-
return mimeType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
4914
|
+
function normalizeMimeType(mimeType) {
|
|
4915
|
+
return mimeType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
5378
4916
|
}
|
|
5379
4917
|
function isAllowedMimeType(mimeType) {
|
|
5380
4918
|
const normalized = normalizeMimeType(mimeType);
|
|
@@ -5421,1529 +4959,1953 @@ async function parseMultipartUpload(request, fieldName = "file") {
|
|
|
5421
4959
|
contents: new Uint8Array(await value.arrayBuffer())
|
|
5422
4960
|
};
|
|
5423
4961
|
}
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
return jsonResponse2(data, { ...init, status: init.status ?? 201 });
|
|
4962
|
+
// ../../src/core/http/requireAuthMiddleware.ts
|
|
4963
|
+
function createRequireAuthMiddleware(auth2) {
|
|
4964
|
+
return async (request, next) => {
|
|
4965
|
+
if (!await auth2.check(request)) {
|
|
4966
|
+
const error = new UnauthorizedError;
|
|
4967
|
+
return Response.json({ error: error.message }, { status: error.status });
|
|
4968
|
+
}
|
|
4969
|
+
return await next();
|
|
4970
|
+
};
|
|
5434
4971
|
}
|
|
5435
|
-
|
|
5436
|
-
|
|
4972
|
+
// ../../src/core/http/resources.ts
|
|
4973
|
+
function serializeDate(value) {
|
|
4974
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
5437
4975
|
}
|
|
5438
|
-
function
|
|
5439
|
-
|
|
5440
|
-
return Response.json({
|
|
5441
|
-
error: mappedError.message,
|
|
5442
|
-
...mappedError.details === undefined ? {} : { details: mappedError.details }
|
|
5443
|
-
}, { status: mappedError.status });
|
|
4976
|
+
function toResourceCollection(items, transformer) {
|
|
4977
|
+
return items.map(transformer);
|
|
5444
4978
|
}
|
|
5445
|
-
function
|
|
5446
|
-
return
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
} catch (error) {
|
|
5450
|
-
const request = args.find((arg) => arg instanceof Request);
|
|
5451
|
-
const webResponse = webErrorResponse(error, request);
|
|
5452
|
-
if (webResponse) {
|
|
5453
|
-
return webResponse;
|
|
5454
|
-
}
|
|
5455
|
-
return errorResponse(error);
|
|
5456
|
-
}
|
|
4979
|
+
function toPaginatedResourceCollection(items, meta, transformer) {
|
|
4980
|
+
return {
|
|
4981
|
+
data: toResourceCollection(items, transformer),
|
|
4982
|
+
meta
|
|
5457
4983
|
};
|
|
5458
4984
|
}
|
|
5459
|
-
// ../../src/
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
5468
|
-
const formData = await request.clone().formData();
|
|
5469
|
-
const email = formData.get("email");
|
|
5470
|
-
return typeof email === "string" ? email.trim().toLowerCase() : "unknown";
|
|
5471
|
-
}
|
|
5472
|
-
const payload = await request.clone().json();
|
|
5473
|
-
return typeof payload.email === "string" ? payload.email.trim().toLowerCase() : "unknown";
|
|
5474
|
-
} catch {
|
|
5475
|
-
return "unknown";
|
|
4985
|
+
// ../../src/config/frontend.ts
|
|
4986
|
+
function readFrontendMode() {
|
|
4987
|
+
const mode = (process.env.FRONTEND_MODE ?? "api").trim();
|
|
4988
|
+
if (mode === "server-htmx") {
|
|
4989
|
+
return "server-htmx";
|
|
4990
|
+
}
|
|
4991
|
+
if (mode === "spa-react") {
|
|
4992
|
+
return "spa-react";
|
|
5476
4993
|
}
|
|
4994
|
+
return "api";
|
|
5477
4995
|
}
|
|
5478
|
-
function
|
|
5479
|
-
|
|
5480
|
-
const prefix = options.keyPrefix ?? "workhub:login-throttle:";
|
|
5481
|
-
return async (request, next) => {
|
|
5482
|
-
const identity = resolveLoginIdentity(request);
|
|
5483
|
-
const email = await resolveLoginEmail(request);
|
|
5484
|
-
const throttleKey = `${prefix}${identity}:${email}`;
|
|
5485
|
-
const attempts = Number(await client.incr(throttleKey));
|
|
5486
|
-
if (attempts === 1) {
|
|
5487
|
-
await client.expire(throttleKey, options.decaySeconds);
|
|
5488
|
-
}
|
|
5489
|
-
if (attempts > options.maxAttempts) {
|
|
5490
|
-
return Response.json({ error: "Too many login attempts. Try again later." }, {
|
|
5491
|
-
status: 429,
|
|
5492
|
-
headers: {
|
|
5493
|
-
"retry-after": String(options.decaySeconds)
|
|
5494
|
-
}
|
|
5495
|
-
});
|
|
5496
|
-
}
|
|
5497
|
-
return await next();
|
|
5498
|
-
};
|
|
4996
|
+
function isViewsEnabled() {
|
|
4997
|
+
return readFrontendMode() === "server-htmx";
|
|
5499
4998
|
}
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
4999
|
+
|
|
5000
|
+
// ../../src/core/view/etaViewEngine.ts
|
|
5001
|
+
import { join as join4, relative } from "path";
|
|
5002
|
+
import { Eta } from "eta";
|
|
5003
|
+
|
|
5004
|
+
// ../../src/core/view/assertEtaHtmlSource.ts
|
|
5005
|
+
var HTML_TAGS = new Set([
|
|
5006
|
+
"a",
|
|
5007
|
+
"abbr",
|
|
5008
|
+
"address",
|
|
5009
|
+
"article",
|
|
5010
|
+
"aside",
|
|
5011
|
+
"audio",
|
|
5012
|
+
"b",
|
|
5013
|
+
"blockquote",
|
|
5014
|
+
"body",
|
|
5015
|
+
"button",
|
|
5016
|
+
"canvas",
|
|
5017
|
+
"caption",
|
|
5018
|
+
"cite",
|
|
5019
|
+
"code",
|
|
5020
|
+
"col",
|
|
5021
|
+
"colgroup",
|
|
5022
|
+
"data",
|
|
5023
|
+
"datalist",
|
|
5024
|
+
"dd",
|
|
5025
|
+
"del",
|
|
5026
|
+
"details",
|
|
5027
|
+
"dfn",
|
|
5028
|
+
"dialog",
|
|
5029
|
+
"div",
|
|
5030
|
+
"dl",
|
|
5031
|
+
"dt",
|
|
5032
|
+
"em",
|
|
5033
|
+
"fieldset",
|
|
5034
|
+
"figcaption",
|
|
5035
|
+
"figure",
|
|
5036
|
+
"footer",
|
|
5037
|
+
"form",
|
|
5038
|
+
"h1",
|
|
5039
|
+
"h2",
|
|
5040
|
+
"h3",
|
|
5041
|
+
"h4",
|
|
5042
|
+
"h5",
|
|
5043
|
+
"h6",
|
|
5044
|
+
"header",
|
|
5045
|
+
"hgroup",
|
|
5046
|
+
"hr",
|
|
5047
|
+
"html",
|
|
5048
|
+
"i",
|
|
5049
|
+
"iframe",
|
|
5050
|
+
"img",
|
|
5051
|
+
"input",
|
|
5052
|
+
"ins",
|
|
5053
|
+
"kbd",
|
|
5054
|
+
"label",
|
|
5055
|
+
"legend",
|
|
5056
|
+
"li",
|
|
5057
|
+
"main",
|
|
5058
|
+
"map",
|
|
5059
|
+
"mark",
|
|
5060
|
+
"menu",
|
|
5061
|
+
"meter",
|
|
5062
|
+
"nav",
|
|
5063
|
+
"object",
|
|
5064
|
+
"ol",
|
|
5065
|
+
"optgroup",
|
|
5066
|
+
"option",
|
|
5067
|
+
"output",
|
|
5068
|
+
"p",
|
|
5069
|
+
"picture",
|
|
5070
|
+
"pre",
|
|
5071
|
+
"progress",
|
|
5072
|
+
"q",
|
|
5073
|
+
"rp",
|
|
5074
|
+
"rt",
|
|
5075
|
+
"ruby",
|
|
5076
|
+
"s",
|
|
5077
|
+
"samp",
|
|
5078
|
+
"script",
|
|
5079
|
+
"search",
|
|
5080
|
+
"section",
|
|
5081
|
+
"select",
|
|
5082
|
+
"slot",
|
|
5083
|
+
"small",
|
|
5084
|
+
"source",
|
|
5085
|
+
"span",
|
|
5086
|
+
"strong",
|
|
5087
|
+
"style",
|
|
5088
|
+
"sub",
|
|
5089
|
+
"summary",
|
|
5090
|
+
"sup",
|
|
5091
|
+
"table",
|
|
5092
|
+
"tbody",
|
|
5093
|
+
"td",
|
|
5094
|
+
"template",
|
|
5095
|
+
"textarea",
|
|
5096
|
+
"tfoot",
|
|
5097
|
+
"th",
|
|
5098
|
+
"thead",
|
|
5099
|
+
"time",
|
|
5100
|
+
"title",
|
|
5101
|
+
"tr",
|
|
5102
|
+
"track",
|
|
5103
|
+
"u",
|
|
5104
|
+
"ul",
|
|
5105
|
+
"var",
|
|
5106
|
+
"video",
|
|
5107
|
+
"wbr"
|
|
5108
|
+
]);
|
|
5109
|
+
var TAG_PATTERN = "([a-z][a-z0-9]*)";
|
|
5110
|
+
var CLASS_OR_ID_PATTERN = "[.#][\\w-]+";
|
|
5111
|
+
var CLASS_ID_SHORTHAND = new RegExp(`^${TAG_PATTERN}(?:${CLASS_OR_ID_PATTERN})+`);
|
|
5112
|
+
var ATTRIBUTE_SHORTHAND = new RegExp(`^${TAG_PATTERN}(?:${CLASS_OR_ID_PATTERN})*\\s+[\\w:-]+=`);
|
|
5113
|
+
var TAG_SHORTHAND = new RegExp(`^${TAG_PATTERN}(?:>|\\s+\\S|$)`);
|
|
5114
|
+
function htmlTagName(match) {
|
|
5115
|
+
const tag = match?.[1];
|
|
5116
|
+
return tag && HTML_TAGS.has(tag) ? tag : undefined;
|
|
5525
5117
|
}
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
httpRequestDurationMs = new Map;
|
|
5530
|
-
incrementHttpRequest(labels) {
|
|
5531
|
-
const key = this.metricKey(labels);
|
|
5532
|
-
this.httpRequestsTotal.set(key, (this.httpRequestsTotal.get(key) ?? 0) + 1);
|
|
5118
|
+
function describePugLikeLine(line) {
|
|
5119
|
+
if (/^\|(?:\s|$)/.test(line)) {
|
|
5120
|
+
return "piped text";
|
|
5533
5121
|
}
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
const samples = this.httpRequestDurationMs.get(key) ?? [];
|
|
5537
|
-
samples.push(durationMs);
|
|
5538
|
-
this.httpRequestDurationMs.set(key, samples);
|
|
5122
|
+
if (htmlTagName(line.match(CLASS_ID_SHORTHAND))) {
|
|
5123
|
+
return "class/id shorthand";
|
|
5539
5124
|
}
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5125
|
+
if (htmlTagName(line.match(ATTRIBUTE_SHORTHAND))) {
|
|
5126
|
+
return "attribute shorthand";
|
|
5127
|
+
}
|
|
5128
|
+
if (htmlTagName(line.match(TAG_SHORTHAND))) {
|
|
5129
|
+
return "tag shorthand";
|
|
5130
|
+
}
|
|
5131
|
+
return;
|
|
5132
|
+
}
|
|
5133
|
+
function updateSkipBlock(lower, skipBlock) {
|
|
5134
|
+
if (skipBlock) {
|
|
5135
|
+
if (skipBlock === "comment" && lower.includes("-->")) {
|
|
5136
|
+
return null;
|
|
5547
5137
|
}
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
const sum = samples.reduce((total, sample) => total + sample, 0);
|
|
5551
|
-
lines.push(`http_request_duration_ms_sum{${key}} ${sum}`);
|
|
5138
|
+
if (skipBlock !== "comment" && lower.includes(`</${skipBlock}`)) {
|
|
5139
|
+
return null;
|
|
5552
5140
|
}
|
|
5553
|
-
return
|
|
5554
|
-
`)}
|
|
5555
|
-
`;
|
|
5141
|
+
return skipBlock;
|
|
5556
5142
|
}
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
this.httpRequestDurationMs.clear();
|
|
5143
|
+
if (lower.startsWith("<!--") && !lower.includes("-->")) {
|
|
5144
|
+
return "comment";
|
|
5560
5145
|
}
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
const pathCounts = new Map;
|
|
5564
|
-
let totalRequests = 0;
|
|
5565
|
-
for (const [key, count] of this.httpRequestsTotal) {
|
|
5566
|
-
totalRequests += count;
|
|
5567
|
-
const method = key.match(/method="([^"]+)"/)?.[1] ?? "GET";
|
|
5568
|
-
const path = key.match(/path="([^"]+)"/)?.[1] ?? "/";
|
|
5569
|
-
const status = key.match(/status="([^"]+)"/)?.[1] ?? "200";
|
|
5570
|
-
byStatus[status] = (byStatus[status] ?? 0) + count;
|
|
5571
|
-
const pathKey = `${method} ${path}`;
|
|
5572
|
-
const existing = pathCounts.get(pathKey);
|
|
5573
|
-
if (existing) {
|
|
5574
|
-
existing.count += count;
|
|
5575
|
-
} else {
|
|
5576
|
-
pathCounts.set(pathKey, { method, path, count });
|
|
5577
|
-
}
|
|
5578
|
-
}
|
|
5579
|
-
const topPaths = Array.from(pathCounts.values()).sort((left, right) => right.count - left.count).slice(0, 10);
|
|
5580
|
-
return {
|
|
5581
|
-
totalRequests,
|
|
5582
|
-
byStatus,
|
|
5583
|
-
topPaths
|
|
5584
|
-
};
|
|
5146
|
+
if (lower.startsWith("<script") && !lower.includes("</script")) {
|
|
5147
|
+
return "script";
|
|
5585
5148
|
}
|
|
5586
|
-
|
|
5587
|
-
return
|
|
5149
|
+
if (lower.startsWith("<style") && !lower.includes("</style")) {
|
|
5150
|
+
return "style";
|
|
5588
5151
|
}
|
|
5152
|
+
return null;
|
|
5589
5153
|
}
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
function createMetricsMiddleware() {
|
|
5597
|
-
return async (request, next) => {
|
|
5598
|
-
const startedAt = performance.now();
|
|
5599
|
-
const response = await next();
|
|
5600
|
-
const durationMs = performance.now() - startedAt;
|
|
5601
|
-
const path = normalizeMetricPath(new URL(request.url).pathname);
|
|
5602
|
-
const labels = {
|
|
5603
|
-
method: request.method,
|
|
5604
|
-
path,
|
|
5605
|
-
status: String(response.status)
|
|
5606
|
-
};
|
|
5607
|
-
prometheusRegistry.incrementHttpRequest(labels);
|
|
5608
|
-
prometheusRegistry.observeHttpDuration(labels, durationMs);
|
|
5609
|
-
return response;
|
|
5610
|
-
};
|
|
5611
|
-
}
|
|
5612
|
-
// ../../src/core/http/requireAbilityMiddleware.ts
|
|
5613
|
-
function createRequireAbilityMiddleware(abilityChecker) {
|
|
5614
|
-
return (ability) => {
|
|
5615
|
-
return async (_request, next) => {
|
|
5616
|
-
const user = currentAuthUser();
|
|
5617
|
-
try {
|
|
5618
|
-
abilityChecker.requireAbility(user, ability);
|
|
5619
|
-
} catch (error) {
|
|
5620
|
-
if (error instanceof ForbiddenError) {
|
|
5621
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
5622
|
-
}
|
|
5623
|
-
throw error;
|
|
5624
|
-
}
|
|
5625
|
-
return await next();
|
|
5626
|
-
};
|
|
5627
|
-
};
|
|
5628
|
-
}
|
|
5629
|
-
// ../../src/core/http/requireGlobalAdminMiddleware.ts
|
|
5630
|
-
function createRequireGlobalAdminMiddleware() {
|
|
5631
|
-
return async (_request, next) => {
|
|
5632
|
-
const user = currentAuthUser();
|
|
5633
|
-
if (!isGlobalAdmin(user)) {
|
|
5634
|
-
logSecurityEvent("privilege_escalation_blocked", {
|
|
5635
|
-
required_role: "platform_admin",
|
|
5636
|
-
path: new URL(_request.url).pathname
|
|
5637
|
-
});
|
|
5638
|
-
const error = new ForbiddenError("Platform admin access required.");
|
|
5639
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
5154
|
+
function assertEtaHtmlSource(templateName, source) {
|
|
5155
|
+
let skipBlock = null;
|
|
5156
|
+
for (const rawLine of source.split(/\r?\n/)) {
|
|
5157
|
+
const trimmed = rawLine.trim();
|
|
5158
|
+
if (!trimmed) {
|
|
5159
|
+
continue;
|
|
5640
5160
|
}
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
return async (request, next) => {
|
|
5647
|
-
const user = await auth2.resolve(request);
|
|
5648
|
-
if (user) {
|
|
5649
|
-
return await next();
|
|
5161
|
+
const lower = trimmed.toLowerCase();
|
|
5162
|
+
const nextSkipBlock = updateSkipBlock(lower, skipBlock);
|
|
5163
|
+
if (skipBlock || nextSkipBlock) {
|
|
5164
|
+
skipBlock = nextSkipBlock;
|
|
5165
|
+
continue;
|
|
5650
5166
|
}
|
|
5651
|
-
if (
|
|
5652
|
-
|
|
5167
|
+
if (trimmed.startsWith("<") || trimmed.startsWith("<%")) {
|
|
5168
|
+
continue;
|
|
5653
5169
|
}
|
|
5654
|
-
const
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
}
|
|
5658
|
-
// ../../src/core/http/scimThrottleMiddleware.ts
|
|
5659
|
-
var {RedisClient: RedisClient3 } = globalThis.Bun;
|
|
5660
|
-
function createScimThrottleMiddleware(options) {
|
|
5661
|
-
const client = options.redisUrl ? new RedisClient3(options.redisUrl) : null;
|
|
5662
|
-
return async (request, next) => {
|
|
5663
|
-
const identity = request.headers.get("authorization")?.slice("Bearer ".length, "Bearer ".length + 16) ?? request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
|
|
5664
|
-
const key = `workhub:scim-throttle:${identity}`;
|
|
5665
|
-
if (client) {
|
|
5666
|
-
const attempts = Number(await client.incr(key));
|
|
5667
|
-
if (attempts === 1) {
|
|
5668
|
-
await client.expire(key, options.decaySeconds);
|
|
5669
|
-
}
|
|
5670
|
-
if (attempts > options.maxAttempts) {
|
|
5671
|
-
return Response.json({ error: "Too many SCIM requests." }, { status: 429, headers: { "retry-after": String(options.decaySeconds) } });
|
|
5672
|
-
}
|
|
5170
|
+
const kind = describePugLikeLine(trimmed);
|
|
5171
|
+
if (kind) {
|
|
5172
|
+
throw new Error(`Eta view "${templateName}" contains Pug-like markup (${kind}: ${trimmed}). Eta views must be HTML + <% %> tags, not Pug.`);
|
|
5673
5173
|
}
|
|
5674
|
-
return await next();
|
|
5675
|
-
};
|
|
5676
|
-
}
|
|
5677
|
-
// ../../src/config/app.ts
|
|
5678
|
-
var appConfig = {
|
|
5679
|
-
name: "WorkHub",
|
|
5680
|
-
env: process.env.APP_ENV ?? "local",
|
|
5681
|
-
debug: (process.env.APP_DEBUG ?? "true") !== "false",
|
|
5682
|
-
url: process.env.APP_URL ?? "http://localhost:3000",
|
|
5683
|
-
apiPrefix: process.env.API_PREFIX ?? "/api/v1"
|
|
5684
|
-
};
|
|
5685
|
-
|
|
5686
|
-
// ../../src/config/contentSecurityPolicy.ts
|
|
5687
|
-
function strictApiContentSecurityPolicy() {
|
|
5688
|
-
return "default-src 'none'; frame-ancestors 'none'; base-uri 'none'";
|
|
5689
|
-
}
|
|
5690
|
-
function serverHtmxContentSecurityPolicy() {
|
|
5691
|
-
return [
|
|
5692
|
-
"default-src 'self'",
|
|
5693
|
-
"script-src 'self' https://unpkg.com",
|
|
5694
|
-
"style-src 'self'",
|
|
5695
|
-
"connect-src 'self'",
|
|
5696
|
-
"img-src 'self'",
|
|
5697
|
-
"font-src 'self'",
|
|
5698
|
-
"form-action 'self'",
|
|
5699
|
-
"frame-ancestors 'none'",
|
|
5700
|
-
"base-uri 'self'"
|
|
5701
|
-
].join("; ");
|
|
5702
|
-
}
|
|
5703
|
-
function spaContentSecurityPolicy() {
|
|
5704
|
-
return [
|
|
5705
|
-
"default-src 'self'",
|
|
5706
|
-
"script-src 'self'",
|
|
5707
|
-
"style-src 'self'",
|
|
5708
|
-
"connect-src 'self'",
|
|
5709
|
-
"img-src 'self'",
|
|
5710
|
-
"font-src 'self'",
|
|
5711
|
-
"frame-ancestors 'none'",
|
|
5712
|
-
"base-uri 'self'"
|
|
5713
|
-
].join("; ");
|
|
5714
|
-
}
|
|
5715
|
-
function resolveContentSecurityPolicy(response) {
|
|
5716
|
-
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
5717
|
-
if (!contentType.includes("text/html")) {
|
|
5718
|
-
return strictApiContentSecurityPolicy();
|
|
5719
|
-
}
|
|
5720
|
-
const frontendMode = (process.env.FRONTEND_MODE ?? "api").trim();
|
|
5721
|
-
if (frontendMode === "server-htmx") {
|
|
5722
|
-
return serverHtmxContentSecurityPolicy();
|
|
5723
|
-
}
|
|
5724
|
-
if (frontendMode === "spa-react") {
|
|
5725
|
-
return spaContentSecurityPolicy();
|
|
5726
5174
|
}
|
|
5727
|
-
return strictApiContentSecurityPolicy();
|
|
5728
5175
|
}
|
|
5729
5176
|
|
|
5730
|
-
// ../../src/core/
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
|
5742
|
-
}
|
|
5743
|
-
return new Response(response.body, {
|
|
5744
|
-
status: response.status,
|
|
5745
|
-
statusText: response.statusText,
|
|
5746
|
-
headers
|
|
5177
|
+
// ../../src/core/view/etaViewEngine.ts
|
|
5178
|
+
var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
|
|
5179
|
+
var DEFAULT_LAYOUT = "layouts/app.eta";
|
|
5180
|
+
|
|
5181
|
+
class EtaViewEngine {
|
|
5182
|
+
eta;
|
|
5183
|
+
resolveLayoutData;
|
|
5184
|
+
constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
|
|
5185
|
+
this.eta = new Eta({
|
|
5186
|
+
views: viewsDirectory,
|
|
5187
|
+
autoTrim: false
|
|
5747
5188
|
});
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
return `token:${user.tokenId}`;
|
|
5189
|
+
this.resolveLayoutData = resolveLayoutData;
|
|
5190
|
+
const readFile2 = this.eta.readFile?.bind(this.eta);
|
|
5191
|
+
this.eta.readFile = (path) => {
|
|
5192
|
+
const source = readFile2 ? readFile2(path) : "";
|
|
5193
|
+
assertEtaHtmlSource(relative(viewsDirectory, path) || path, source);
|
|
5194
|
+
return source;
|
|
5195
|
+
};
|
|
5756
5196
|
}
|
|
5757
|
-
|
|
5758
|
-
|
|
5197
|
+
async render(name, data = {}, options = {}) {
|
|
5198
|
+
const template = name.endsWith(".eta") ? name : `${name}.eta`;
|
|
5199
|
+
const layoutData = this.resolveLayoutData ? await this.resolveLayoutData() : {};
|
|
5200
|
+
const mergedData = { ...layoutData, ...data };
|
|
5201
|
+
const body = await this.eta.renderAsync(template, mergedData);
|
|
5202
|
+
const layout = options.layout ?? DEFAULT_LAYOUT;
|
|
5203
|
+
if (layout === false) {
|
|
5204
|
+
return body;
|
|
5205
|
+
}
|
|
5206
|
+
const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
|
|
5207
|
+
return await this.eta.renderAsync(layoutTemplate, {
|
|
5208
|
+
...mergedData,
|
|
5209
|
+
body
|
|
5210
|
+
});
|
|
5759
5211
|
}
|
|
5760
|
-
return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
|
|
5761
5212
|
}
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
const attempts = Number(await client.incr(throttleKey));
|
|
5770
|
-
if (attempts === 1) {
|
|
5771
|
-
await client.expire(throttleKey, options.decaySeconds);
|
|
5772
|
-
}
|
|
5773
|
-
const maxAttempts = options.maxAttempts * rateLimitMultiplierForPlan(currentTenant()?.plan ?? "free");
|
|
5774
|
-
if (attempts > maxAttempts) {
|
|
5775
|
-
return Response.json({ error: "Too many requests." }, {
|
|
5776
|
-
status: 429,
|
|
5777
|
-
headers: {
|
|
5778
|
-
"retry-after": String(options.decaySeconds)
|
|
5779
|
-
}
|
|
5780
|
-
});
|
|
5213
|
+
// ../../src/core/view/htmlResponse.ts
|
|
5214
|
+
function htmlResponse(html, init = {}) {
|
|
5215
|
+
return new Response(html, {
|
|
5216
|
+
status: init.status ?? 200,
|
|
5217
|
+
statusText: init.statusText,
|
|
5218
|
+
headers: {
|
|
5219
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
5781
5220
|
}
|
|
5782
|
-
|
|
5783
|
-
};
|
|
5221
|
+
});
|
|
5784
5222
|
}
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
5788
|
-
if (!contentType.includes("application/x-www-form-urlencoded") && !contentType.includes("multipart/form-data")) {
|
|
5789
|
-
throw new BadRequestError("Expected a form submission.");
|
|
5790
|
-
}
|
|
5791
|
-
return formDataToRecord(await request.formData());
|
|
5223
|
+
function isHtmxRequest(request) {
|
|
5224
|
+
return request.headers.get("HX-Request") === "true";
|
|
5792
5225
|
}
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5226
|
+
// ../../src/core/view/webLayoutData.ts
|
|
5227
|
+
async function resolveWebLayoutData(container, request) {
|
|
5228
|
+
const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
|
|
5229
|
+
const flash = request ? currentRequestMeta().flash ?? pullFlash(request) : null;
|
|
5230
|
+
const authUser = currentAuthUser();
|
|
5231
|
+
if (!authUser) {
|
|
5232
|
+
return { authUser: null, csrfToken, flash };
|
|
5233
|
+
}
|
|
5234
|
+
const userId = Number(authUser.id);
|
|
5235
|
+
if (!Number.isInteger(userId) || userId <= 0) {
|
|
5236
|
+
return { authUser: null, csrfToken, flash };
|
|
5237
|
+
}
|
|
5238
|
+
if (!container.has(tokenServiceToken)) {
|
|
5239
|
+
return {
|
|
5240
|
+
authUser: {
|
|
5241
|
+
id: userId,
|
|
5242
|
+
email: "",
|
|
5243
|
+
role: authUser.role ?? "member"
|
|
5244
|
+
},
|
|
5245
|
+
csrfToken,
|
|
5246
|
+
flash
|
|
5247
|
+
};
|
|
5248
|
+
}
|
|
5249
|
+
const tokenService = container.resolve(tokenServiceToken);
|
|
5250
|
+
try {
|
|
5251
|
+
const user = await tokenService.findByIdOrThrow(userId);
|
|
5252
|
+
return {
|
|
5253
|
+
authUser: {
|
|
5254
|
+
id: userId,
|
|
5255
|
+
email: user.email ?? "",
|
|
5256
|
+
role: authUser.role ?? user.role ?? "member"
|
|
5257
|
+
},
|
|
5258
|
+
csrfToken,
|
|
5259
|
+
flash
|
|
5260
|
+
};
|
|
5261
|
+
} catch {
|
|
5262
|
+
return { authUser: null, csrfToken, flash };
|
|
5799
5263
|
}
|
|
5800
|
-
return values;
|
|
5801
5264
|
}
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
authorize(_request) {
|
|
5265
|
+
// ../../src/core/http/contentNegotiation.ts
|
|
5266
|
+
function requestPrefersJson(request) {
|
|
5267
|
+
if (!request) {
|
|
5806
5268
|
return true;
|
|
5807
5269
|
}
|
|
5808
|
-
|
|
5809
|
-
return
|
|
5270
|
+
if (request.headers.get("HX-Request") === "true") {
|
|
5271
|
+
return false;
|
|
5810
5272
|
}
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
}
|
|
5815
|
-
const payload = requestPrefersJson(request) ? await parseJsonBody(request, (body) => body) : await parseFormBody(request);
|
|
5816
|
-
try {
|
|
5817
|
-
return this.validatePayload(payload);
|
|
5818
|
-
} catch (error) {
|
|
5819
|
-
if (error instanceof ValidationError) {
|
|
5820
|
-
throw error;
|
|
5821
|
-
}
|
|
5822
|
-
throw error;
|
|
5823
|
-
}
|
|
5273
|
+
const accept = request.headers.get("accept")?.toLowerCase() ?? "";
|
|
5274
|
+
if (accept.includes("text/html")) {
|
|
5275
|
+
return false;
|
|
5824
5276
|
}
|
|
5277
|
+
if (accept.includes("application/json")) {
|
|
5278
|
+
return true;
|
|
5279
|
+
}
|
|
5280
|
+
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
5281
|
+
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
5282
|
+
return false;
|
|
5283
|
+
}
|
|
5284
|
+
const pathname = new URL(request.url).pathname;
|
|
5285
|
+
return pathname.startsWith("/api/");
|
|
5825
5286
|
}
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
shutdownHandlers.set(name, handler);
|
|
5832
|
-
return () => {
|
|
5833
|
-
shutdownHandlers.delete(name);
|
|
5834
|
-
};
|
|
5835
|
-
}
|
|
5836
|
-
async function runGracefulShutdown(signal) {
|
|
5837
|
-
if (shuttingDown) {
|
|
5838
|
-
return;
|
|
5287
|
+
|
|
5288
|
+
// ../../src/core/http/webErrorResponse.ts
|
|
5289
|
+
function normalizeFieldErrors(details) {
|
|
5290
|
+
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
|
5291
|
+
return {};
|
|
5839
5292
|
}
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5293
|
+
const errors = {};
|
|
5294
|
+
for (const [field, messages] of Object.entries(details)) {
|
|
5295
|
+
if (Array.isArray(messages)) {
|
|
5296
|
+
errors[field] = messages.map(String);
|
|
5297
|
+
continue;
|
|
5298
|
+
}
|
|
5299
|
+
if (typeof messages === "string") {
|
|
5300
|
+
errors[field] = [messages];
|
|
5848
5301
|
}
|
|
5849
5302
|
}
|
|
5303
|
+
return errors;
|
|
5850
5304
|
}
|
|
5851
|
-
function
|
|
5852
|
-
if (
|
|
5853
|
-
return;
|
|
5305
|
+
function webErrorResponse(error, request) {
|
|
5306
|
+
if (!request || !isViewsEnabled() || requestPrefersJson(request)) {
|
|
5307
|
+
return null;
|
|
5854
5308
|
}
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
process.exit(0);
|
|
5860
|
-
});
|
|
5861
|
-
});
|
|
5309
|
+
const mappedError = error instanceof HttpError ? error : mapDatabaseError(error);
|
|
5310
|
+
if (mappedError instanceof UnauthorizedError) {
|
|
5311
|
+
const redirectTarget = encodeURIComponent(new URL(request.url).pathname);
|
|
5312
|
+
return Response.redirect(`/login?redirect=${redirectTarget}`, 302);
|
|
5862
5313
|
}
|
|
5314
|
+
if (mappedError instanceof ValidationError) {
|
|
5315
|
+
const errors = normalizeFieldErrors(mappedError.details);
|
|
5316
|
+
const fieldSummary = Object.entries(errors).flatMap(([field, messages]) => messages.map((message) => `${field}: ${message}`)).join(`
|
|
5317
|
+
`);
|
|
5318
|
+
return htmlResponse(`<section class="page-header"><h1>Validation failed</h1><pre>${fieldSummary || mappedError.message}</pre><p><a href="javascript:history.back()">Go back</a></p></section>`, { status: mappedError.status });
|
|
5319
|
+
}
|
|
5320
|
+
return htmlResponse(`<section class="page-header"><h1>${mappedError.message}</h1></section>`, {
|
|
5321
|
+
status: mappedError.status
|
|
5322
|
+
});
|
|
5863
5323
|
}
|
|
5864
|
-
// ../../src/core/logging/requestLoggingMiddleware.ts
|
|
5865
|
-
function createRequestLoggingMiddleware() {
|
|
5866
|
-
return async (request, next) => {
|
|
5867
|
-
return await runWithRequestMeta({
|
|
5868
|
-
ipAddress: request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip"),
|
|
5869
|
-
userAgent: request.headers.get("user-agent"),
|
|
5870
|
-
request
|
|
5871
|
-
}, async () => {
|
|
5872
|
-
const startedAt = performance.now();
|
|
5873
|
-
const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
|
|
5874
|
-
const response = await next();
|
|
5875
|
-
const durationMs = Math.round(performance.now() - startedAt);
|
|
5876
|
-
appLogger.info("HTTP request completed", {
|
|
5877
|
-
requestId,
|
|
5878
|
-
method: request.method,
|
|
5879
|
-
path: new URL(request.url).pathname,
|
|
5880
|
-
status: response.status,
|
|
5881
|
-
durationMs
|
|
5882
|
-
});
|
|
5883
|
-
return response;
|
|
5884
|
-
});
|
|
5885
|
-
};
|
|
5886
|
-
}
|
|
5887
|
-
// ../../src/core/mail/markdownMail.ts
|
|
5888
|
-
function escapeHtml(value) {
|
|
5889
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5890
|
-
}
|
|
5891
|
-
function stripMarkdown(markdown) {
|
|
5892
|
-
return markdown.replace(/^#{1,6}\s+/gm, "").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/^[-*]\s+/gm, "\u2022 ").replace(/```[\s\S]*?```/g, "").replace(/\n{3,}/g, `
|
|
5893
5324
|
|
|
5894
|
-
|
|
5325
|
+
// ../../src/core/http/response.ts
|
|
5326
|
+
function jsonResponse2(data, init = {}) {
|
|
5327
|
+
return Response.json(data, {
|
|
5328
|
+
status: init.status ?? 200,
|
|
5329
|
+
headers: init.headers
|
|
5330
|
+
});
|
|
5895
5331
|
}
|
|
5896
|
-
function
|
|
5897
|
-
|
|
5898
|
-
return escaped.replace(/^### (.+)$/gm, "<h3>$1</h3>").replace(/^## (.+)$/gm, "<h2>$1</h2>").replace(/^# (.+)$/gm, "<h1>$1</h1>").replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>").replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>').replace(/^[-*]\s+(.+)$/gm, "<li>$1</li>").replace(/(<li>[\s\S]*?<\/li>\n?)+/g, (block) => `<ul>${block}</ul>`).replace(/```(\w+)?\n([\s\S]*?)```/g, "<pre><code>$2</code></pre>").split(/\n\n+/).map((block) => {
|
|
5899
|
-
if (block.startsWith("<")) {
|
|
5900
|
-
return block;
|
|
5901
|
-
}
|
|
5902
|
-
return `<p>${block.replace(/\n/g, " ")}</p>`;
|
|
5903
|
-
}).join(`
|
|
5904
|
-
`);
|
|
5332
|
+
function createdResponse(data, init = {}) {
|
|
5333
|
+
return jsonResponse2(data, { ...init, status: init.status ?? 201 });
|
|
5905
5334
|
}
|
|
5906
|
-
function
|
|
5907
|
-
|
|
5908
|
-
const preview = escapeHtml(options.preview ?? "");
|
|
5909
|
-
const footer = escapeHtml(options.footer ?? "Sent by GetStrata");
|
|
5910
|
-
return `<!DOCTYPE html>
|
|
5911
|
-
<html lang="en">
|
|
5912
|
-
<head>
|
|
5913
|
-
<meta charset="utf-8">
|
|
5914
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5915
|
-
<title>${title}</title>
|
|
5916
|
-
<style>
|
|
5917
|
-
body { font-family: system-ui, sans-serif; line-height: 1.5; color: #111827; background: #f9fafb; margin: 0; padding: 24px; }
|
|
5918
|
-
.container { max-width: 640px; margin: 0 auto; background: #ffffff; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }
|
|
5919
|
-
.header { padding: 20px 24px; border-bottom: 1px solid #e5e7eb; font-weight: 600; }
|
|
5920
|
-
.content { padding: 24px; }
|
|
5921
|
-
.footer { padding: 16px 24px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 14px; }
|
|
5922
|
-
a { color: #2563eb; }
|
|
5923
|
-
code { background: #f3f4f6; padding: 2px 4px; border-radius: 4px; }
|
|
5924
|
-
pre { background: #111827; color: #f9fafb; padding: 12px; border-radius: 6px; overflow-x: auto; }
|
|
5925
|
-
</style>
|
|
5926
|
-
</head>
|
|
5927
|
-
<body>
|
|
5928
|
-
${preview ? `<span style="display:none;max-height:0;overflow:hidden;">${preview}</span>` : ""}
|
|
5929
|
-
<div class="container">
|
|
5930
|
-
<div class="header">${title}</div>
|
|
5931
|
-
<div class="content">${bodyHtml}</div>
|
|
5932
|
-
<div class="footer">${footer}</div>
|
|
5933
|
-
</div>
|
|
5934
|
-
</body>
|
|
5935
|
-
</html>`;
|
|
5335
|
+
function noContentResponse() {
|
|
5336
|
+
return new Response(null, { status: 204 });
|
|
5936
5337
|
}
|
|
5937
|
-
function
|
|
5938
|
-
const
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5338
|
+
function errorResponse(error) {
|
|
5339
|
+
const mappedError = error instanceof HttpError ? error : mapDatabaseError(error);
|
|
5340
|
+
return Response.json({
|
|
5341
|
+
error: mappedError.message,
|
|
5342
|
+
...mappedError.details === undefined ? {} : { details: mappedError.details }
|
|
5343
|
+
}, { status: mappedError.status });
|
|
5942
5344
|
}
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5345
|
+
function withErrorHandling(handler) {
|
|
5346
|
+
return async (...args) => {
|
|
5347
|
+
try {
|
|
5348
|
+
return await handler(...args);
|
|
5349
|
+
} catch (error) {
|
|
5350
|
+
const request = args.find((arg) => arg instanceof Request);
|
|
5351
|
+
const webResponse = webErrorResponse(error, request);
|
|
5352
|
+
if (webResponse) {
|
|
5353
|
+
return webResponse;
|
|
5354
|
+
}
|
|
5355
|
+
return errorResponse(error);
|
|
5356
|
+
}
|
|
5954
5357
|
};
|
|
5955
5358
|
}
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
const
|
|
5980
|
-
|
|
5981
|
-
|
|
5359
|
+
// ../../src/core/http/routeMiddleware.ts
|
|
5360
|
+
function withMiddleware(...middleware) {
|
|
5361
|
+
const wrap = composeMiddleware(...middleware);
|
|
5362
|
+
return (handler) => {
|
|
5363
|
+
return wrap(handler);
|
|
5364
|
+
};
|
|
5365
|
+
}
|
|
5366
|
+
// ../../src/core/http/routeModelBinding.ts
|
|
5367
|
+
function bindRouteModel(param, resolver, handler) {
|
|
5368
|
+
return async (request) => {
|
|
5369
|
+
const id = parsePositiveIntParam(String(request.params[param]), String(param));
|
|
5370
|
+
const model = await resolver(id, request);
|
|
5371
|
+
return await handler(request, model);
|
|
5372
|
+
};
|
|
5373
|
+
}
|
|
5374
|
+
// ../../src/core/http/securedRouteModelBinding.ts
|
|
5375
|
+
function isMutatingPolicyAction(action) {
|
|
5376
|
+
return action === "update" || action === "delete";
|
|
5377
|
+
}
|
|
5378
|
+
function securedBindRouteModel(param, resolver, authorization, handler) {
|
|
5379
|
+
return async (request) => {
|
|
5380
|
+
const id = parsePositiveIntParam(String(request.params[param]), String(param));
|
|
5381
|
+
const model = await resolver(id, request);
|
|
5382
|
+
const gate = resolveApplicationPolicyGate();
|
|
5383
|
+
const auth2 = resolveApplicationAuth();
|
|
5384
|
+
const user = currentAuthUser() ?? await auth2.resolve(request);
|
|
5385
|
+
gate.authorize(authorization.resource, authorization.action, user, model);
|
|
5386
|
+
if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
|
|
5387
|
+
assertIfMatch(request, etagFromResource(model), {
|
|
5388
|
+
required: authorization.requireIfMatch ?? true
|
|
5389
|
+
});
|
|
5982
5390
|
}
|
|
5983
|
-
const
|
|
5984
|
-
if (
|
|
5985
|
-
return;
|
|
5391
|
+
const response = await handler(request, model);
|
|
5392
|
+
if (isEtagEnabled() && authorization.action === "view") {
|
|
5393
|
+
return applyConditionalGet(request, response, etagFromResource(model));
|
|
5986
5394
|
}
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5395
|
+
return response;
|
|
5396
|
+
};
|
|
5397
|
+
}
|
|
5398
|
+
function securedBindRouteModelByKey(param, resolver, authorization, handler) {
|
|
5399
|
+
return async (request) => {
|
|
5400
|
+
const key = String(request.params[param] ?? "").trim();
|
|
5401
|
+
if (!key) {
|
|
5402
|
+
throw new BadRequestError(`Missing route parameter "${String(param)}".`);
|
|
5994
5403
|
}
|
|
5995
|
-
await
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
return;
|
|
5404
|
+
const model = await resolver(key, request);
|
|
5405
|
+
const gate = resolveApplicationPolicyGate();
|
|
5406
|
+
const auth2 = resolveApplicationAuth();
|
|
5407
|
+
const user = currentAuthUser() ?? await auth2.resolve(request);
|
|
5408
|
+
gate.authorize(authorization.resource, authorization.action, user, model);
|
|
5409
|
+
if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
|
|
5410
|
+
assertIfMatch(request, etagFromResource(model), {
|
|
5411
|
+
required: authorization.requireIfMatch ?? true
|
|
5412
|
+
});
|
|
6005
5413
|
}
|
|
6006
|
-
const
|
|
6007
|
-
if (
|
|
6008
|
-
return;
|
|
5414
|
+
const response = await handler(request, model);
|
|
5415
|
+
if (isEtagEnabled() && authorization.action === "view") {
|
|
5416
|
+
return applyConditionalGet(request, response, etagFromResource(model));
|
|
6009
5417
|
}
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
...payload
|
|
6013
|
-
});
|
|
6014
|
-
}
|
|
6015
|
-
}
|
|
6016
|
-
function createNotificationDispatcher(mailer2, databaseStore) {
|
|
6017
|
-
return new NotificationDispatcher(mailer2, databaseStore ?? null);
|
|
6018
|
-
}
|
|
6019
|
-
// ../../src/core/notifications/notification.ts
|
|
6020
|
-
class Notification {
|
|
6021
|
-
via(_notifiable) {
|
|
6022
|
-
throw new Error("Notification subclasses must implement via().");
|
|
6023
|
-
}
|
|
6024
|
-
toMail(_notifiable) {
|
|
6025
|
-
return null;
|
|
6026
|
-
}
|
|
6027
|
-
toDatabase(_notifiable) {
|
|
6028
|
-
return null;
|
|
6029
|
-
}
|
|
5418
|
+
return response;
|
|
5419
|
+
};
|
|
6030
5420
|
}
|
|
6031
|
-
// ../../src/core/
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
priority;
|
|
5421
|
+
// ../../src/core/http/loginThrottleMiddleware.ts
|
|
5422
|
+
var {RedisClient: RedisClient2 } = globalThis.Bun;
|
|
5423
|
+
function resolveLoginIdentity(request) {
|
|
5424
|
+
return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
|
|
6036
5425
|
}
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
5426
|
+
async function resolveLoginEmail(request) {
|
|
5427
|
+
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
5428
|
+
try {
|
|
5429
|
+
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
5430
|
+
const formData = await request.clone().formData();
|
|
5431
|
+
const email = formData.get("email");
|
|
5432
|
+
return typeof email === "string" ? email.trim().toLowerCase() : "unknown";
|
|
5433
|
+
}
|
|
5434
|
+
const payload = await request.clone().json();
|
|
5435
|
+
return typeof payload.email === "string" ? payload.email.trim().toLowerCase() : "unknown";
|
|
5436
|
+
} catch {
|
|
5437
|
+
return "unknown";
|
|
6041
5438
|
}
|
|
6042
5439
|
}
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
5440
|
+
function createLoginThrottleMiddleware(options) {
|
|
5441
|
+
const client = new RedisClient2(options.redisUrl);
|
|
5442
|
+
const prefix = options.keyPrefix ?? "workhub:login-throttle:";
|
|
5443
|
+
return async (request, next) => {
|
|
5444
|
+
const identity = resolveLoginIdentity(request);
|
|
5445
|
+
const email = await resolveLoginEmail(request);
|
|
5446
|
+
const throttleKey = `${prefix}${identity}:${email}`;
|
|
5447
|
+
const attempts = Number(await client.incr(throttleKey));
|
|
5448
|
+
if (attempts === 1) {
|
|
5449
|
+
await client.expire(throttleKey, options.decaySeconds);
|
|
5450
|
+
}
|
|
5451
|
+
if (attempts > options.maxAttempts) {
|
|
5452
|
+
return Response.json({ error: "Too many login attempts. Try again later." }, {
|
|
5453
|
+
status: 429,
|
|
5454
|
+
headers: {
|
|
5455
|
+
"retry-after": String(options.decaySeconds)
|
|
5456
|
+
}
|
|
6049
5457
|
});
|
|
6050
|
-
}
|
|
6051
|
-
|
|
5458
|
+
}
|
|
5459
|
+
return await next();
|
|
5460
|
+
};
|
|
6052
5461
|
}
|
|
6053
|
-
|
|
6054
|
-
|
|
5462
|
+
// ../../src/core/http/memoryThrottleMiddleware.ts
|
|
5463
|
+
var buckets = new Map;
|
|
5464
|
+
function createMemoryThrottleMiddleware(options) {
|
|
5465
|
+
const prefix = options.keyPrefix ?? "workhub:memory-throttle:";
|
|
5466
|
+
return async (request, next) => {
|
|
5467
|
+
const path = new URL(request.url).pathname;
|
|
5468
|
+
const identity = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("authorization")?.slice(0, 32) ?? "unknown";
|
|
5469
|
+
const key = `${prefix}${identity}:${path}`;
|
|
5470
|
+
const now = Date.now();
|
|
5471
|
+
const existing = buckets.get(key);
|
|
5472
|
+
if (!existing || existing.resetAt <= now) {
|
|
5473
|
+
buckets.set(key, { count: 1, resetAt: now + options.decaySeconds * 1000 });
|
|
5474
|
+
return await next();
|
|
5475
|
+
}
|
|
5476
|
+
existing.count += 1;
|
|
5477
|
+
if (existing.count > options.maxAttempts) {
|
|
5478
|
+
return Response.json({ error: "Too many requests." }, {
|
|
5479
|
+
status: 429,
|
|
5480
|
+
headers: {
|
|
5481
|
+
"retry-after": String(options.decaySeconds)
|
|
5482
|
+
}
|
|
5483
|
+
});
|
|
5484
|
+
}
|
|
5485
|
+
return await next();
|
|
5486
|
+
};
|
|
6055
5487
|
}
|
|
6056
|
-
// ../../src/core/
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
// ../../src/core/queue/failedJobRepository.ts
|
|
6065
|
-
class FailedJobRepository extends BaseRepository {
|
|
6066
|
-
constructor() {
|
|
6067
|
-
super(failedJobTable);
|
|
5488
|
+
// ../../src/core/metrics/prometheus.ts
|
|
5489
|
+
class PrometheusRegistry {
|
|
5490
|
+
httpRequestsTotal = new Map;
|
|
5491
|
+
httpRequestDurationMs = new Map;
|
|
5492
|
+
incrementHttpRequest(labels) {
|
|
5493
|
+
const key = this.metricKey(labels);
|
|
5494
|
+
this.httpRequestsTotal.set(key, (this.httpRequestsTotal.get(key) ?? 0) + 1);
|
|
6068
5495
|
}
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
repository;
|
|
6075
|
-
constructor(repository) {
|
|
6076
|
-
this.repository = repository;
|
|
5496
|
+
observeHttpDuration(labels, durationMs) {
|
|
5497
|
+
const key = this.metricKey(labels);
|
|
5498
|
+
const samples = this.httpRequestDurationMs.get(key) ?? [];
|
|
5499
|
+
samples.push(durationMs);
|
|
5500
|
+
this.httpRequestDurationMs.set(key, samples);
|
|
6077
5501
|
}
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
5502
|
+
renderMetrics() {
|
|
5503
|
+
const lines = [
|
|
5504
|
+
"# HELP http_requests_total Total HTTP requests processed.",
|
|
5505
|
+
"# TYPE http_requests_total counter"
|
|
5506
|
+
];
|
|
5507
|
+
for (const [key, value] of this.httpRequestsTotal) {
|
|
5508
|
+
lines.push(`http_requests_total{${key}} ${value}`);
|
|
5509
|
+
}
|
|
5510
|
+
lines.push("# HELP http_request_duration_ms_sum Sum of HTTP request durations in milliseconds.", "# TYPE http_request_duration_ms_sum counter");
|
|
5511
|
+
for (const [key, samples] of this.httpRequestDurationMs) {
|
|
5512
|
+
const sum = samples.reduce((total, sample) => total + sample, 0);
|
|
5513
|
+
lines.push(`http_request_duration_ms_sum{${key}} ${sum}`);
|
|
5514
|
+
}
|
|
5515
|
+
return `${lines.join(`
|
|
5516
|
+
`)}
|
|
5517
|
+
`;
|
|
6085
5518
|
}
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
orderBy: { column: "failed_at", direction: "DESC" }
|
|
6090
|
-
});
|
|
6091
|
-
}
|
|
6092
|
-
async retry(id) {
|
|
6093
|
-
const failedJob = await this.repository.findByIdOrThrow(id, (jobId) => new Error(`Failed job ${jobId} not found.`));
|
|
6094
|
-
await this.repository.deleteById(id);
|
|
6095
|
-
return failedJob;
|
|
6096
|
-
}
|
|
6097
|
-
async delete(id) {
|
|
6098
|
-
const deleted = await this.repository.deleteById(id);
|
|
6099
|
-
if (!deleted) {
|
|
6100
|
-
throw new Error(`Failed job ${id} not found.`);
|
|
6101
|
-
}
|
|
5519
|
+
resetForTests() {
|
|
5520
|
+
this.httpRequestsTotal.clear();
|
|
5521
|
+
this.httpRequestDurationMs.clear();
|
|
6102
5522
|
}
|
|
6103
|
-
|
|
6104
|
-
const
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
5523
|
+
getHttpRequestSummary() {
|
|
5524
|
+
const byStatus = {};
|
|
5525
|
+
const pathCounts = new Map;
|
|
5526
|
+
let totalRequests = 0;
|
|
5527
|
+
for (const [key, count] of this.httpRequestsTotal) {
|
|
5528
|
+
totalRequests += count;
|
|
5529
|
+
const method = key.match(/method="([^"]+)"/)?.[1] ?? "GET";
|
|
5530
|
+
const path = key.match(/path="([^"]+)"/)?.[1] ?? "/";
|
|
5531
|
+
const status = key.match(/status="([^"]+)"/)?.[1] ?? "200";
|
|
5532
|
+
byStatus[status] = (byStatus[status] ?? 0) + count;
|
|
5533
|
+
const pathKey = `${method} ${path}`;
|
|
5534
|
+
const existing = pathCounts.get(pathKey);
|
|
5535
|
+
if (existing) {
|
|
5536
|
+
existing.count += count;
|
|
5537
|
+
} else {
|
|
5538
|
+
pathCounts.set(pathKey, { method, path, count });
|
|
6109
5539
|
}
|
|
6110
5540
|
}
|
|
6111
|
-
|
|
5541
|
+
const topPaths = Array.from(pathCounts.values()).sort((left, right) => right.count - left.count).slice(0, 10);
|
|
5542
|
+
return {
|
|
5543
|
+
totalRequests,
|
|
5544
|
+
byStatus,
|
|
5545
|
+
topPaths
|
|
5546
|
+
};
|
|
5547
|
+
}
|
|
5548
|
+
metricKey(labels) {
|
|
5549
|
+
return `method="${labels.method}",path="${labels.path}",status="${labels.status}"`;
|
|
6112
5550
|
}
|
|
6113
5551
|
}
|
|
6114
|
-
var
|
|
5552
|
+
var prometheusRegistry = new PrometheusRegistry;
|
|
6115
5553
|
|
|
6116
|
-
// ../../src/core/
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
5554
|
+
// ../../src/core/http/metricsMiddleware.ts
|
|
5555
|
+
function normalizeMetricPath(pathname) {
|
|
5556
|
+
return pathname.replace(/\/\d+/g, "/:id").replace(/\/[0-9a-f-]{36}/gi, "/:id");
|
|
5557
|
+
}
|
|
5558
|
+
function createMetricsMiddleware() {
|
|
5559
|
+
return async (request, next) => {
|
|
5560
|
+
const startedAt = performance.now();
|
|
5561
|
+
const response = await next();
|
|
5562
|
+
const durationMs = performance.now() - startedAt;
|
|
5563
|
+
const path = normalizeMetricPath(new URL(request.url).pathname);
|
|
5564
|
+
const labels = {
|
|
5565
|
+
method: request.method,
|
|
5566
|
+
path,
|
|
5567
|
+
status: String(response.status)
|
|
5568
|
+
};
|
|
5569
|
+
prometheusRegistry.incrementHttpRequest(labels);
|
|
5570
|
+
prometheusRegistry.observeHttpDuration(labels, durationMs);
|
|
5571
|
+
return response;
|
|
5572
|
+
};
|
|
5573
|
+
}
|
|
5574
|
+
// ../../src/core/http/requireAbilityMiddleware.ts
|
|
5575
|
+
function createRequireAbilityMiddleware(abilityChecker) {
|
|
5576
|
+
return (ability) => {
|
|
5577
|
+
return async (_request, next) => {
|
|
5578
|
+
const user = currentAuthUser();
|
|
5579
|
+
try {
|
|
5580
|
+
abilityChecker.requireAbility(user, ability);
|
|
5581
|
+
} catch (error) {
|
|
5582
|
+
if (error instanceof ForbiddenError) {
|
|
5583
|
+
return Response.json({ error: error.message }, { status: error.status });
|
|
5584
|
+
}
|
|
5585
|
+
throw error;
|
|
5586
|
+
}
|
|
5587
|
+
return await next();
|
|
5588
|
+
};
|
|
5589
|
+
};
|
|
5590
|
+
}
|
|
5591
|
+
// ../../src/core/http/requireGlobalAdminMiddleware.ts
|
|
5592
|
+
function createRequireGlobalAdminMiddleware() {
|
|
5593
|
+
return async (_request, next) => {
|
|
5594
|
+
const user = currentAuthUser();
|
|
5595
|
+
if (!isGlobalAdmin(user)) {
|
|
5596
|
+
logSecurityEvent("privilege_escalation_blocked", {
|
|
5597
|
+
required_role: "platform_admin",
|
|
5598
|
+
path: new URL(_request.url).pathname
|
|
5599
|
+
});
|
|
5600
|
+
const error = new ForbiddenError("Platform admin access required.");
|
|
5601
|
+
return Response.json({ error: error.message }, { status: error.status });
|
|
6134
5602
|
}
|
|
6135
|
-
return
|
|
6136
|
-
}
|
|
6137
|
-
names() {
|
|
6138
|
-
return [...this.factories.keys()];
|
|
6139
|
-
}
|
|
5603
|
+
return await next();
|
|
5604
|
+
};
|
|
6140
5605
|
}
|
|
6141
|
-
|
|
6142
|
-
function
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
5606
|
+
// ../../src/core/http/requireWebAuthMiddleware.ts
|
|
5607
|
+
function createRequireWebAuthMiddleware(auth2) {
|
|
5608
|
+
return async (request, next) => {
|
|
5609
|
+
const user = await auth2.resolve(request);
|
|
5610
|
+
if (user) {
|
|
5611
|
+
return await next();
|
|
5612
|
+
}
|
|
5613
|
+
if (requestPrefersJson(request)) {
|
|
5614
|
+
throw new UnauthorizedError;
|
|
5615
|
+
}
|
|
5616
|
+
const redirectTarget = encodeURIComponent(new URL(request.url).pathname);
|
|
5617
|
+
return Response.redirect(`/login?redirect=${redirectTarget}`, 302);
|
|
5618
|
+
};
|
|
6150
5619
|
}
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
5620
|
+
// ../../src/core/http/scimThrottleMiddleware.ts
|
|
5621
|
+
var {RedisClient: RedisClient3 } = globalThis.Bun;
|
|
5622
|
+
function createScimThrottleMiddleware(options) {
|
|
5623
|
+
const client = options.redisUrl ? new RedisClient3(options.redisUrl) : null;
|
|
5624
|
+
return async (request, next) => {
|
|
5625
|
+
const identity = request.headers.get("authorization")?.slice("Bearer ".length, "Bearer ".length + 16) ?? request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
|
|
5626
|
+
const key = `workhub:scim-throttle:${identity}`;
|
|
5627
|
+
if (client) {
|
|
5628
|
+
const attempts = Number(await client.incr(key));
|
|
5629
|
+
if (attempts === 1) {
|
|
5630
|
+
await client.expire(key, options.decaySeconds);
|
|
5631
|
+
}
|
|
5632
|
+
if (attempts > options.maxAttempts) {
|
|
5633
|
+
return Response.json({ error: "Too many SCIM requests." }, { status: 429, headers: { "retry-after": String(options.decaySeconds) } });
|
|
5634
|
+
}
|
|
5635
|
+
}
|
|
5636
|
+
return await next();
|
|
5637
|
+
};
|
|
5638
|
+
}
|
|
5639
|
+
// ../../src/config/app.ts
|
|
5640
|
+
var appConfig = {
|
|
5641
|
+
name: "WorkHub",
|
|
5642
|
+
env: process.env.APP_ENV ?? "local",
|
|
5643
|
+
debug: (process.env.APP_DEBUG ?? "true") !== "false",
|
|
5644
|
+
url: process.env.APP_URL ?? "http://localhost:3000",
|
|
5645
|
+
apiPrefix: process.env.API_PREFIX ?? "/api/v1"
|
|
6158
5646
|
};
|
|
6159
5647
|
|
|
6160
|
-
// ../../src/
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
5648
|
+
// ../../src/config/contentSecurityPolicy.ts
|
|
5649
|
+
function strictApiContentSecurityPolicy() {
|
|
5650
|
+
return "default-src 'none'; frame-ancestors 'none'; base-uri 'none'";
|
|
5651
|
+
}
|
|
5652
|
+
function serverHtmxContentSecurityPolicy() {
|
|
5653
|
+
return [
|
|
5654
|
+
"default-src 'self'",
|
|
5655
|
+
"script-src 'self' https://unpkg.com",
|
|
5656
|
+
"style-src 'self'",
|
|
5657
|
+
"connect-src 'self'",
|
|
5658
|
+
"img-src 'self'",
|
|
5659
|
+
"font-src 'self'",
|
|
5660
|
+
"form-action 'self'",
|
|
5661
|
+
"frame-ancestors 'none'",
|
|
5662
|
+
"base-uri 'self'"
|
|
5663
|
+
].join("; ");
|
|
5664
|
+
}
|
|
5665
|
+
function spaContentSecurityPolicy() {
|
|
5666
|
+
return [
|
|
5667
|
+
"default-src 'self'",
|
|
5668
|
+
"script-src 'self'",
|
|
5669
|
+
"style-src 'self'",
|
|
5670
|
+
"connect-src 'self'",
|
|
5671
|
+
"img-src 'self'",
|
|
5672
|
+
"font-src 'self'",
|
|
5673
|
+
"frame-ancestors 'none'",
|
|
5674
|
+
"base-uri 'self'"
|
|
5675
|
+
].join("; ");
|
|
5676
|
+
}
|
|
5677
|
+
function resolveContentSecurityPolicy(response) {
|
|
5678
|
+
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
5679
|
+
if (!contentType.includes("text/html")) {
|
|
5680
|
+
return strictApiContentSecurityPolicy();
|
|
6165
5681
|
}
|
|
6166
|
-
const
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
}
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
if (nextAttempt < maxAttempts) {
|
|
6173
|
-
const backoffMs = job.backoffMs ?? queueConfig.backoffMs;
|
|
6174
|
-
await new Promise((resolve) => setTimeout(resolve, backoffMs * nextAttempt));
|
|
6175
|
-
await runQueueJob({
|
|
6176
|
-
...envelope,
|
|
6177
|
-
attempts: nextAttempt
|
|
6178
|
-
}, failedJobs);
|
|
6179
|
-
return;
|
|
6180
|
-
}
|
|
6181
|
-
await failedJobs.recordFailure({
|
|
6182
|
-
jobName: envelope.name,
|
|
6183
|
-
payload: envelope.payload,
|
|
6184
|
-
exception: error instanceof Error ? error.stack ?? error.message : String(error)
|
|
6185
|
-
});
|
|
6186
|
-
throw error;
|
|
5682
|
+
const frontendMode = (process.env.FRONTEND_MODE ?? "api").trim();
|
|
5683
|
+
if (frontendMode === "server-htmx") {
|
|
5684
|
+
return serverHtmxContentSecurityPolicy();
|
|
5685
|
+
}
|
|
5686
|
+
if (frontendMode === "spa-react") {
|
|
5687
|
+
return spaContentSecurityPolicy();
|
|
6187
5688
|
}
|
|
5689
|
+
return strictApiContentSecurityPolicy();
|
|
6188
5690
|
}
|
|
6189
5691
|
|
|
6190
|
-
// ../../src/core/
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
5692
|
+
// ../../src/core/http/securityHeadersMiddleware.ts
|
|
5693
|
+
function createSecurityHeadersMiddleware() {
|
|
5694
|
+
return async (_request, next) => {
|
|
5695
|
+
const response = await next();
|
|
5696
|
+
const headers = new Headers(response.headers);
|
|
5697
|
+
headers.set("X-Content-Type-Options", "nosniff");
|
|
5698
|
+
headers.set("X-Frame-Options", "DENY");
|
|
5699
|
+
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
5700
|
+
headers.set("X-XSS-Protection", "0");
|
|
5701
|
+
headers.set("Content-Security-Policy", resolveContentSecurityPolicy(response));
|
|
5702
|
+
if (appConfig.env === "production") {
|
|
5703
|
+
headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
|
5704
|
+
}
|
|
5705
|
+
return new Response(response.body, {
|
|
5706
|
+
status: response.status,
|
|
5707
|
+
statusText: response.statusText,
|
|
5708
|
+
headers
|
|
5709
|
+
});
|
|
5710
|
+
};
|
|
6205
5711
|
}
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
return null;
|
|
6213
|
-
}
|
|
6214
|
-
if (!parsed || typeof parsed !== "object") {
|
|
6215
|
-
console.error("[QueueWorker] Ignoring non-object queue payload");
|
|
6216
|
-
return null;
|
|
6217
|
-
}
|
|
6218
|
-
const envelope = parsed;
|
|
6219
|
-
if (typeof envelope.name !== "string" || envelope.name.length === 0) {
|
|
6220
|
-
console.error("[QueueWorker] Ignoring queue payload without job name");
|
|
6221
|
-
return null;
|
|
6222
|
-
}
|
|
6223
|
-
if (!jobRegistry.create(envelope.name)) {
|
|
6224
|
-
console.error(`[QueueWorker] Ignoring unknown job name: ${envelope.name}`);
|
|
6225
|
-
return null;
|
|
5712
|
+
// ../../src/core/http/throttleMiddleware.ts
|
|
5713
|
+
var {RedisClient: RedisClient4 } = globalThis.Bun;
|
|
5714
|
+
function resolveThrottleIdentity(request) {
|
|
5715
|
+
const user = currentAuthUser();
|
|
5716
|
+
if (user?.tokenId !== undefined) {
|
|
5717
|
+
return `token:${user.tokenId}`;
|
|
6226
5718
|
}
|
|
6227
|
-
if (
|
|
6228
|
-
|
|
6229
|
-
return null;
|
|
5719
|
+
if (user) {
|
|
5720
|
+
return `user:${user.id}`;
|
|
6230
5721
|
}
|
|
6231
|
-
return
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
5722
|
+
return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
|
|
5723
|
+
}
|
|
5724
|
+
function createThrottleMiddleware(options) {
|
|
5725
|
+
const client = new RedisClient4(options.redisUrl);
|
|
5726
|
+
const prefix = options.keyPrefix ?? "workhub:throttle:";
|
|
5727
|
+
return async (request, next) => {
|
|
5728
|
+
const identity = resolveThrottleIdentity(request);
|
|
5729
|
+
const path = new URL(request.url).pathname;
|
|
5730
|
+
const throttleKey = `${prefix}${identity}:${path}`;
|
|
5731
|
+
const attempts = Number(await client.incr(throttleKey));
|
|
5732
|
+
if (attempts === 1) {
|
|
5733
|
+
await client.expire(throttleKey, options.decaySeconds);
|
|
5734
|
+
}
|
|
5735
|
+
const maxAttempts = options.maxAttempts * rateLimitMultiplierForPlan(currentTenant()?.plan ?? "free");
|
|
5736
|
+
if (attempts > maxAttempts) {
|
|
5737
|
+
return Response.json({ error: "Too many requests." }, {
|
|
5738
|
+
status: 429,
|
|
5739
|
+
headers: {
|
|
5740
|
+
"retry-after": String(options.decaySeconds)
|
|
5741
|
+
}
|
|
5742
|
+
});
|
|
5743
|
+
}
|
|
5744
|
+
return await next();
|
|
6235
5745
|
};
|
|
6236
5746
|
}
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
5747
|
+
// ../../src/core/http/parseFormBody.ts
|
|
5748
|
+
async function parseFormBody(request) {
|
|
5749
|
+
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
5750
|
+
if (!contentType.includes("application/x-www-form-urlencoded") && !contentType.includes("multipart/form-data")) {
|
|
5751
|
+
throw new BadRequestError("Expected a form submission.");
|
|
6242
5752
|
}
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
5753
|
+
return formDataToRecord(await request.formData());
|
|
5754
|
+
}
|
|
5755
|
+
function formDataToRecord(formData) {
|
|
5756
|
+
const values = {};
|
|
5757
|
+
for (const [key, value] of formData.entries()) {
|
|
5758
|
+
if (typeof value === "string") {
|
|
5759
|
+
values[key] = value;
|
|
6247
5760
|
}
|
|
6248
|
-
const envelope = {
|
|
6249
|
-
name,
|
|
6250
|
-
payload,
|
|
6251
|
-
attempts: 0
|
|
6252
|
-
};
|
|
6253
|
-
const queueKey = queueKeyForPriority(job.priority);
|
|
6254
|
-
await this.client.lpush(queueKey, JSON.stringify(envelope));
|
|
6255
5761
|
}
|
|
5762
|
+
return values;
|
|
6256
5763
|
}
|
|
6257
5764
|
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
stopping = false;
|
|
6263
|
-
client;
|
|
6264
|
-
constructor(redisUrl, failedJobs, timeoutSeconds = 5) {
|
|
6265
|
-
this.failedJobs = failedJobs;
|
|
6266
|
-
this.timeoutSeconds = timeoutSeconds;
|
|
6267
|
-
this.client = new RedisClient5(redisUrl);
|
|
6268
|
-
}
|
|
6269
|
-
requestStop() {
|
|
6270
|
-
this.stopping = true;
|
|
5765
|
+
// ../../src/core/http/webFormRequest.ts
|
|
5766
|
+
class WebFormRequest {
|
|
5767
|
+
authorize(_request) {
|
|
5768
|
+
return true;
|
|
6271
5769
|
}
|
|
6272
|
-
|
|
6273
|
-
return this.
|
|
5770
|
+
validatePayload(payload) {
|
|
5771
|
+
return this.parse(payload);
|
|
6274
5772
|
}
|
|
6275
|
-
async
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
result = await this.client.brpop(queueKey, 1);
|
|
6279
|
-
if (result) {
|
|
6280
|
-
break;
|
|
6281
|
-
}
|
|
6282
|
-
}
|
|
6283
|
-
if (!result) {
|
|
6284
|
-
result = await this.client.brpop(QUEUE_LIST_KEY, this.timeoutSeconds);
|
|
6285
|
-
}
|
|
6286
|
-
if (!result) {
|
|
6287
|
-
return false;
|
|
6288
|
-
}
|
|
6289
|
-
const [, rawPayload] = result;
|
|
6290
|
-
const envelope = parseQueueJobEnvelope(rawPayload);
|
|
6291
|
-
if (!envelope) {
|
|
6292
|
-
return true;
|
|
5773
|
+
async validate(request) {
|
|
5774
|
+
if (!await this.authorize(request)) {
|
|
5775
|
+
throw new ForbiddenError;
|
|
6293
5776
|
}
|
|
5777
|
+
const payload = requestPrefersJson(request) ? await parseJsonBody(request, (body) => body) : await parseFormBody(request);
|
|
6294
5778
|
try {
|
|
6295
|
-
|
|
5779
|
+
return this.validatePayload(payload);
|
|
6296
5780
|
} catch (error) {
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
async run() {
|
|
6302
|
-
this.running = true;
|
|
6303
|
-
while (!this.stopping) {
|
|
6304
|
-
await this.processNext();
|
|
5781
|
+
if (error instanceof ValidationError) {
|
|
5782
|
+
throw error;
|
|
5783
|
+
}
|
|
5784
|
+
throw error;
|
|
6305
5785
|
}
|
|
6306
|
-
this.running = false;
|
|
6307
|
-
}
|
|
6308
|
-
close() {
|
|
6309
|
-
this.client.close();
|
|
6310
5786
|
}
|
|
6311
5787
|
}
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
5788
|
+
// ../../src/core/lifecycle/gracefulShutdown.ts
|
|
5789
|
+
var shutdownHandlers = new Map;
|
|
5790
|
+
var shutdownInstalled = false;
|
|
5791
|
+
var shuttingDown = false;
|
|
5792
|
+
function registerShutdownHandler(name, handler) {
|
|
5793
|
+
shutdownHandlers.set(name, handler);
|
|
5794
|
+
return () => {
|
|
5795
|
+
shutdownHandlers.delete(name);
|
|
5796
|
+
};
|
|
5797
|
+
}
|
|
5798
|
+
async function runGracefulShutdown(signal) {
|
|
5799
|
+
if (shuttingDown) {
|
|
5800
|
+
return;
|
|
6320
5801
|
}
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
attempts: 0
|
|
6330
|
-
};
|
|
6331
|
-
if (this.asyncDispatch) {
|
|
6332
|
-
setTimeout(() => {
|
|
6333
|
-
runQueueJob(envelope, this.failedJobs).catch((error) => {
|
|
6334
|
-
console.error("[ResilientQueue] Job failed:", error);
|
|
6335
|
-
});
|
|
6336
|
-
}, 0);
|
|
6337
|
-
return;
|
|
5802
|
+
shuttingDown = true;
|
|
5803
|
+
console.log(`[shutdown] Received ${signal}, draining ${shutdownHandlers.size} handler(s)...`);
|
|
5804
|
+
for (const [name, handler] of shutdownHandlers) {
|
|
5805
|
+
try {
|
|
5806
|
+
await handler();
|
|
5807
|
+
console.log(`[shutdown] Completed ${name}`);
|
|
5808
|
+
} catch (error) {
|
|
5809
|
+
console.error(`[shutdown] Failed ${name}:`, error);
|
|
6338
5810
|
}
|
|
6339
|
-
await runQueueJob(envelope, this.failedJobs);
|
|
6340
5811
|
}
|
|
6341
5812
|
}
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
if (driver === "redis") {
|
|
6354
|
-
if (!options.redisUrl) {
|
|
6355
|
-
throw new Error('QUEUE_DRIVER="redis" requires REDIS_URL to be set.');
|
|
6356
|
-
}
|
|
6357
|
-
return new RedisQueue(options.redisUrl);
|
|
5813
|
+
function installGracefulShutdownSignals(signals = ["SIGINT", "SIGTERM"]) {
|
|
5814
|
+
if (shutdownInstalled) {
|
|
5815
|
+
return;
|
|
5816
|
+
}
|
|
5817
|
+
shutdownInstalled = true;
|
|
5818
|
+
for (const signal of signals) {
|
|
5819
|
+
process.on(signal, () => {
|
|
5820
|
+
runGracefulShutdown(signal).finally(() => {
|
|
5821
|
+
process.exit(0);
|
|
5822
|
+
});
|
|
5823
|
+
});
|
|
6358
5824
|
}
|
|
6359
|
-
return new ResilientQueue(failedJobs, driver === "async");
|
|
6360
|
-
}
|
|
6361
|
-
function createQueueWorker(redisUrl, failedJobs = createFailedJobService()) {
|
|
6362
|
-
return new QueueWorker(redisUrl, failedJobs);
|
|
6363
5825
|
}
|
|
6364
|
-
// ../../src/core/
|
|
6365
|
-
|
|
6366
|
-
async
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
5826
|
+
// ../../src/core/logging/requestLoggingMiddleware.ts
|
|
5827
|
+
function createRequestLoggingMiddleware() {
|
|
5828
|
+
return async (request, next) => {
|
|
5829
|
+
return await runWithRequestMeta({
|
|
5830
|
+
ipAddress: request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip"),
|
|
5831
|
+
userAgent: request.headers.get("user-agent"),
|
|
5832
|
+
request
|
|
5833
|
+
}, async () => {
|
|
5834
|
+
const startedAt = performance.now();
|
|
5835
|
+
const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
|
|
5836
|
+
const response = await next();
|
|
5837
|
+
const durationMs = Math.round(performance.now() - startedAt);
|
|
5838
|
+
appLogger.info("HTTP request completed", {
|
|
5839
|
+
requestId,
|
|
5840
|
+
method: request.method,
|
|
5841
|
+
path: new URL(request.url).pathname,
|
|
5842
|
+
status: response.status,
|
|
5843
|
+
durationMs
|
|
5844
|
+
});
|
|
5845
|
+
return response;
|
|
5846
|
+
});
|
|
6378
5847
|
};
|
|
6379
5848
|
}
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
}
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
}
|
|
5849
|
+
// ../../src/core/mail/markdownMail.ts
|
|
5850
|
+
function escapeHtml(value) {
|
|
5851
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5852
|
+
}
|
|
5853
|
+
function stripMarkdown(markdown) {
|
|
5854
|
+
return markdown.replace(/^#{1,6}\s+/gm, "").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/^[-*]\s+/gm, "\u2022 ").replace(/```[\s\S]*?```/g, "").replace(/\n{3,}/g, `
|
|
5855
|
+
|
|
5856
|
+
`).trim();
|
|
5857
|
+
}
|
|
5858
|
+
function markdownToHtml(markdown) {
|
|
5859
|
+
const escaped = markdown.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
5860
|
+
return escaped.replace(/^### (.+)$/gm, "<h3>$1</h3>").replace(/^## (.+)$/gm, "<h2>$1</h2>").replace(/^# (.+)$/gm, "<h1>$1</h1>").replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>").replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>').replace(/^[-*]\s+(.+)$/gm, "<li>$1</li>").replace(/(<li>[\s\S]*?<\/li>\n?)+/g, (block) => `<ul>${block}</ul>`).replace(/```(\w+)?\n([\s\S]*?)```/g, "<pre><code>$2</code></pre>").split(/\n\n+/).map((block) => {
|
|
5861
|
+
if (block.startsWith("<")) {
|
|
5862
|
+
return block;
|
|
5863
|
+
}
|
|
5864
|
+
return `<p>${block.replace(/\n/g, " ")}</p>`;
|
|
5865
|
+
}).join(`
|
|
5866
|
+
`);
|
|
5867
|
+
}
|
|
5868
|
+
function wrapMarkdownMailLayout(bodyHtml, options = {}) {
|
|
5869
|
+
const title = escapeHtml(options.title ?? "GetStrata");
|
|
5870
|
+
const preview = escapeHtml(options.preview ?? "");
|
|
5871
|
+
const footer = escapeHtml(options.footer ?? "Sent by GetStrata");
|
|
5872
|
+
return `<!DOCTYPE html>
|
|
5873
|
+
<html lang="en">
|
|
5874
|
+
<head>
|
|
5875
|
+
<meta charset="utf-8">
|
|
5876
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5877
|
+
<title>${title}</title>
|
|
5878
|
+
<style>
|
|
5879
|
+
body { font-family: system-ui, sans-serif; line-height: 1.5; color: #111827; background: #f9fafb; margin: 0; padding: 24px; }
|
|
5880
|
+
.container { max-width: 640px; margin: 0 auto; background: #ffffff; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }
|
|
5881
|
+
.header { padding: 20px 24px; border-bottom: 1px solid #e5e7eb; font-weight: 600; }
|
|
5882
|
+
.content { padding: 24px; }
|
|
5883
|
+
.footer { padding: 16px 24px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 14px; }
|
|
5884
|
+
a { color: #2563eb; }
|
|
5885
|
+
code { background: #f3f4f6; padding: 2px 4px; border-radius: 4px; }
|
|
5886
|
+
pre { background: #111827; color: #f9fafb; padding: 12px; border-radius: 6px; overflow-x: auto; }
|
|
5887
|
+
</style>
|
|
5888
|
+
</head>
|
|
5889
|
+
<body>
|
|
5890
|
+
${preview ? `<span style="display:none;max-height:0;overflow:hidden;">${preview}</span>` : ""}
|
|
5891
|
+
<div class="container">
|
|
5892
|
+
<div class="header">${title}</div>
|
|
5893
|
+
<div class="content">${bodyHtml}</div>
|
|
5894
|
+
<div class="footer">${footer}</div>
|
|
5895
|
+
</div>
|
|
5896
|
+
</body>
|
|
5897
|
+
</html>`;
|
|
5898
|
+
}
|
|
5899
|
+
function renderMarkdownMail(markdown, options = {}) {
|
|
5900
|
+
const bodyHtml = markdownToHtml(markdown.trim());
|
|
5901
|
+
const html = wrapMarkdownMailLayout(bodyHtml, options);
|
|
5902
|
+
const text = stripMarkdown(markdown);
|
|
5903
|
+
return { html, text };
|
|
5904
|
+
}
|
|
5905
|
+
// ../../src/core/mail/markdownMailable.ts
|
|
5906
|
+
function buildMarkdownMailMessage(input) {
|
|
5907
|
+
const rendered = renderMarkdownMail(input.markdown, {
|
|
5908
|
+
title: input.layout?.title ?? input.subject,
|
|
5909
|
+
...input.layout
|
|
5910
|
+
});
|
|
6409
5911
|
return {
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
5912
|
+
to: input.to,
|
|
5913
|
+
subject: input.subject,
|
|
5914
|
+
body: rendered.text,
|
|
5915
|
+
html: rendered.html
|
|
6413
5916
|
};
|
|
6414
5917
|
}
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
5918
|
+
async function sendMarkdownMail(mailer2, input) {
|
|
5919
|
+
await mailer2.send(buildMarkdownMailMessage(input));
|
|
5920
|
+
}
|
|
5921
|
+
// ../../src/core/notifications/dispatcher.ts
|
|
5922
|
+
class NotificationDispatcher {
|
|
5923
|
+
mailer;
|
|
5924
|
+
databaseStore;
|
|
5925
|
+
constructor(mailer2, databaseStore = null) {
|
|
5926
|
+
this.mailer = mailer2;
|
|
5927
|
+
this.databaseStore = databaseStore;
|
|
6421
5928
|
}
|
|
6422
|
-
|
|
6423
|
-
const
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
5929
|
+
async send(notifiable, notification) {
|
|
5930
|
+
for (const channel of notification.via(notifiable)) {
|
|
5931
|
+
if (channel === "mail") {
|
|
5932
|
+
await this.sendMail(notifiable, notification);
|
|
5933
|
+
continue;
|
|
6427
5934
|
}
|
|
6428
|
-
if (
|
|
6429
|
-
|
|
6430
|
-
return Number.isInteger(interval) && interval > 0 && minute % interval === 0;
|
|
5935
|
+
if (channel === "database") {
|
|
5936
|
+
await this.sendDatabase(notifiable, notification);
|
|
6431
5937
|
}
|
|
6432
|
-
|
|
5938
|
+
}
|
|
5939
|
+
}
|
|
5940
|
+
async sendMail(notifiable, notification) {
|
|
5941
|
+
const routed = notifiable.routeNotificationFor("mail");
|
|
5942
|
+
if (routed === null) {
|
|
5943
|
+
return;
|
|
5944
|
+
}
|
|
5945
|
+
const message = notification.toMail(notifiable);
|
|
5946
|
+
if (!message) {
|
|
5947
|
+
return;
|
|
5948
|
+
}
|
|
5949
|
+
if (message.markdown) {
|
|
5950
|
+
await this.mailer.send(buildMarkdownMailMessage({
|
|
5951
|
+
to: String(routed),
|
|
5952
|
+
subject: message.subject,
|
|
5953
|
+
markdown: message.markdown
|
|
5954
|
+
}));
|
|
5955
|
+
return;
|
|
5956
|
+
}
|
|
5957
|
+
await this.mailer.send({
|
|
5958
|
+
to: String(routed),
|
|
5959
|
+
subject: message.subject,
|
|
5960
|
+
body: message.body ?? "",
|
|
5961
|
+
...message.html ? { html: message.html } : {}
|
|
6433
5962
|
});
|
|
6434
5963
|
}
|
|
6435
|
-
|
|
6436
|
-
|
|
5964
|
+
async sendDatabase(notifiable, notification) {
|
|
5965
|
+
if (!this.databaseStore) {
|
|
5966
|
+
return;
|
|
5967
|
+
}
|
|
5968
|
+
const payload = notification.toDatabase(notifiable);
|
|
5969
|
+
if (!payload) {
|
|
5970
|
+
return;
|
|
5971
|
+
}
|
|
5972
|
+
await this.databaseStore.create({
|
|
5973
|
+
userId: Number(notifiable.getNotificationKey()),
|
|
5974
|
+
...payload
|
|
5975
|
+
});
|
|
6437
5976
|
}
|
|
6438
5977
|
}
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
const due = schedule.dueTasks(now);
|
|
6442
|
-
for (const task of due) {
|
|
6443
|
-
await task.run();
|
|
6444
|
-
}
|
|
6445
|
-
return due.length;
|
|
5978
|
+
function createNotificationDispatcher(mailer2, databaseStore) {
|
|
5979
|
+
return new NotificationDispatcher(mailer2, databaseStore ?? null);
|
|
6446
5980
|
}
|
|
6447
|
-
// ../../src/core/
|
|
6448
|
-
|
|
6449
|
-
|
|
5981
|
+
// ../../src/core/notifications/notification.ts
|
|
5982
|
+
class Notification {
|
|
5983
|
+
via(_notifiable) {
|
|
5984
|
+
throw new Error("Notification subclasses must implement via().");
|
|
5985
|
+
}
|
|
5986
|
+
toMail(_notifiable) {
|
|
5987
|
+
return null;
|
|
5988
|
+
}
|
|
5989
|
+
toDatabase(_notifiable) {
|
|
5990
|
+
return null;
|
|
5991
|
+
}
|
|
6450
5992
|
}
|
|
6451
|
-
|
|
6452
|
-
|
|
5993
|
+
// ../../src/core/queue/index.ts
|
|
5994
|
+
class Job {
|
|
5995
|
+
maxAttempts;
|
|
5996
|
+
backoffMs;
|
|
5997
|
+
priority;
|
|
6453
5998
|
}
|
|
6454
|
-
// ../../src/core/tenant/tenantMiddleware.ts
|
|
6455
|
-
import { createHash } from "crypto";
|
|
6456
5999
|
|
|
6457
|
-
|
|
6458
|
-
async
|
|
6459
|
-
|
|
6460
|
-
try {
|
|
6461
|
-
return await callback();
|
|
6462
|
-
} finally {
|
|
6463
|
-
await repositoryConnection`SELECT set_config('app.bypass_rls', 'false', false)`;
|
|
6000
|
+
class SyncQueue {
|
|
6001
|
+
async dispatch(job, payload) {
|
|
6002
|
+
await job.handle(payload);
|
|
6464
6003
|
}
|
|
6465
6004
|
}
|
|
6466
6005
|
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
};
|
|
6474
|
-
|
|
6475
|
-
return await runWithMigrationBypass(async () => {
|
|
6476
|
-
const rows = await repositoryConnection`
|
|
6477
|
-
SELECT tenant_id
|
|
6478
|
-
FROM users
|
|
6479
|
-
WHERE id = ${userId}
|
|
6480
|
-
LIMIT 1
|
|
6481
|
-
`;
|
|
6482
|
-
return rows[0]?.tenant_id ?? DEFAULT_TENANT.id;
|
|
6483
|
-
});
|
|
6006
|
+
class AsyncQueue {
|
|
6007
|
+
async dispatch(job, payload) {
|
|
6008
|
+
setTimeout(() => {
|
|
6009
|
+
job.handle(payload).catch((error) => {
|
|
6010
|
+
console.error("[AsyncQueue] Job failed:", error);
|
|
6011
|
+
});
|
|
6012
|
+
}, 0);
|
|
6013
|
+
}
|
|
6484
6014
|
}
|
|
6485
|
-
function
|
|
6486
|
-
return
|
|
6015
|
+
function createQueue(driver) {
|
|
6016
|
+
return driver === "async" ? new AsyncQueue : new SyncQueue;
|
|
6487
6017
|
}
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
|
|
6501
|
-
}
|
|
6502
|
-
if (Number.isInteger(parsedHeader) && parsedHeader > 0) {
|
|
6503
|
-
return await resolveTenant(parsedHeader) ?? DEFAULT_TENANT;
|
|
6504
|
-
}
|
|
6505
|
-
return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
|
|
6506
|
-
}
|
|
6018
|
+
// ../../src/core/queue/failedJobTable.ts
|
|
6019
|
+
var failedJobTable = defineTable({
|
|
6020
|
+
name: "failed_job",
|
|
6021
|
+
primaryKey: "id",
|
|
6022
|
+
columns: ["id", "job_name", "payload", "exception", "failed_at"],
|
|
6023
|
+
defaultOrderBy: { column: "failed_at", direction: "DESC" }
|
|
6024
|
+
});
|
|
6025
|
+
|
|
6026
|
+
// ../../src/core/queue/failedJobRepository.ts
|
|
6027
|
+
class FailedJobRepository extends BaseRepository {
|
|
6028
|
+
constructor() {
|
|
6029
|
+
super(failedJobTable);
|
|
6507
6030
|
}
|
|
6508
|
-
const tenantId = Number.isInteger(parsedHeader) && parsedHeader > 0 ? parsedHeader : DEFAULT_TENANT.id;
|
|
6509
|
-
return await resolveTenant(tenantId) ?? DEFAULT_TENANT;
|
|
6510
6031
|
}
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6032
|
+
var failedJobRepository_default = FailedJobRepository;
|
|
6033
|
+
|
|
6034
|
+
// ../../src/core/queue/failedJobService.ts
|
|
6035
|
+
class FailedJobService {
|
|
6036
|
+
repository;
|
|
6037
|
+
constructor(repository) {
|
|
6038
|
+
this.repository = repository;
|
|
6039
|
+
}
|
|
6040
|
+
async recordFailure(input) {
|
|
6041
|
+
return await this.repository.create({
|
|
6042
|
+
job_name: input.jobName,
|
|
6043
|
+
payload: input.payload,
|
|
6044
|
+
exception: input.exception,
|
|
6045
|
+
failed_at: new Date
|
|
6046
|
+
});
|
|
6047
|
+
}
|
|
6048
|
+
listRecent(limit = 50) {
|
|
6049
|
+
return this.repository.findAll({
|
|
6050
|
+
limit,
|
|
6051
|
+
orderBy: { column: "failed_at", direction: "DESC" }
|
|
6052
|
+
});
|
|
6053
|
+
}
|
|
6054
|
+
async retry(id) {
|
|
6055
|
+
const failedJob = await this.repository.findByIdOrThrow(id, (jobId) => new Error(`Failed job ${jobId} not found.`));
|
|
6056
|
+
await this.repository.deleteById(id);
|
|
6057
|
+
return failedJob;
|
|
6058
|
+
}
|
|
6059
|
+
async delete(id) {
|
|
6060
|
+
const deleted = await this.repository.deleteById(id);
|
|
6061
|
+
if (!deleted) {
|
|
6062
|
+
throw new Error(`Failed job ${id} not found.`);
|
|
6516
6063
|
}
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
return new Response(response.body, {
|
|
6525
|
-
status: response.status,
|
|
6526
|
-
statusText: response.statusText,
|
|
6527
|
-
headers
|
|
6528
|
-
});
|
|
6529
|
-
});
|
|
6530
|
-
} catch (error) {
|
|
6531
|
-
if (error instanceof HttpError) {
|
|
6532
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
6064
|
+
}
|
|
6065
|
+
async flush() {
|
|
6066
|
+
const jobs = await this.repository.findAll();
|
|
6067
|
+
let deleted = 0;
|
|
6068
|
+
for (const job of jobs) {
|
|
6069
|
+
if (await this.repository.deleteById(job.id)) {
|
|
6070
|
+
deleted += 1;
|
|
6533
6071
|
}
|
|
6534
|
-
throw error;
|
|
6535
6072
|
}
|
|
6536
|
-
|
|
6073
|
+
return deleted;
|
|
6074
|
+
}
|
|
6537
6075
|
}
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6076
|
+
var failedJobService_default = FailedJobService;
|
|
6077
|
+
|
|
6078
|
+
// ../../src/core/queue/jobRegistry.ts
|
|
6079
|
+
class JobRegistry {
|
|
6080
|
+
factories = new Map;
|
|
6081
|
+
instances = new WeakMap;
|
|
6082
|
+
register(name, factory) {
|
|
6083
|
+
this.factories.set(name, factory);
|
|
6084
|
+
}
|
|
6085
|
+
resolveName(job) {
|
|
6086
|
+
return this.instances.get(job);
|
|
6087
|
+
}
|
|
6088
|
+
track(name, job) {
|
|
6089
|
+
this.instances.set(job, name);
|
|
6090
|
+
return job;
|
|
6091
|
+
}
|
|
6092
|
+
create(name) {
|
|
6093
|
+
const factory = this.factories.get(name);
|
|
6094
|
+
if (!factory) {
|
|
6095
|
+
return;
|
|
6096
|
+
}
|
|
6097
|
+
return factory();
|
|
6098
|
+
}
|
|
6099
|
+
names() {
|
|
6100
|
+
return [...this.factories.keys()];
|
|
6101
|
+
}
|
|
6542
6102
|
}
|
|
6543
|
-
|
|
6544
|
-
|
|
6103
|
+
var JOB_REGISTRY_KEY = Symbol.for("@getstrata/jobRegistry");
|
|
6104
|
+
function readSharedJobRegistry() {
|
|
6105
|
+
const globalRegistry = globalThis[JOB_REGISTRY_KEY];
|
|
6106
|
+
if (globalRegistry) {
|
|
6107
|
+
return globalRegistry;
|
|
6108
|
+
}
|
|
6109
|
+
const registry = new JobRegistry;
|
|
6110
|
+
globalThis[JOB_REGISTRY_KEY] = registry;
|
|
6111
|
+
return registry;
|
|
6545
6112
|
}
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6113
|
+
var jobRegistry = readSharedJobRegistry();
|
|
6114
|
+
|
|
6115
|
+
// ../../src/config/queue.ts
|
|
6116
|
+
var queueConfig = {
|
|
6117
|
+
driver: process.env.QUEUE_DRIVER === "redis" || process.env.QUEUE_DRIVER === "async" || process.env.QUEUE_DRIVER === "sync" ? process.env.QUEUE_DRIVER : DEFAULT_QUEUE_DRIVER,
|
|
6118
|
+
maxAttempts: Number(process.env.QUEUE_MAX_ATTEMPTS ?? "3"),
|
|
6119
|
+
backoffMs: Number(process.env.QUEUE_BACKOFF_MS ?? "1000")
|
|
6120
|
+
};
|
|
6121
|
+
|
|
6122
|
+
// ../../src/core/queue/jobRunner.ts
|
|
6123
|
+
async function runQueueJob(envelope, failedJobs) {
|
|
6124
|
+
const job = jobRegistry.create(envelope.name);
|
|
6125
|
+
if (!job) {
|
|
6126
|
+
throw new Error(`Unknown job "${envelope.name}".`);
|
|
6127
|
+
}
|
|
6128
|
+
const attempts = envelope.attempts ?? 0;
|
|
6129
|
+
try {
|
|
6130
|
+
await job.handle(envelope.payload);
|
|
6131
|
+
} catch (error) {
|
|
6132
|
+
const nextAttempt = attempts + 1;
|
|
6133
|
+
const maxAttempts = job.maxAttempts ?? queueConfig.maxAttempts;
|
|
6134
|
+
if (nextAttempt < maxAttempts) {
|
|
6135
|
+
const backoffMs = job.backoffMs ?? queueConfig.backoffMs;
|
|
6136
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs * nextAttempt));
|
|
6137
|
+
await runQueueJob({
|
|
6138
|
+
...envelope,
|
|
6139
|
+
attempts: nextAttempt
|
|
6140
|
+
}, failedJobs);
|
|
6141
|
+
return;
|
|
6142
|
+
}
|
|
6143
|
+
await failedJobs.recordFailure({
|
|
6144
|
+
jobName: envelope.name,
|
|
6145
|
+
payload: envelope.payload,
|
|
6146
|
+
exception: error instanceof Error ? error.stack ?? error.message : String(error)
|
|
6147
|
+
});
|
|
6148
|
+
throw error;
|
|
6149
|
+
}
|
|
6550
6150
|
}
|
|
6551
|
-
|
|
6552
|
-
|
|
6151
|
+
|
|
6152
|
+
// ../../src/core/queue/redisQueue.ts
|
|
6153
|
+
var {RedisClient: RedisClient5 } = globalThis.Bun;
|
|
6154
|
+
var QUEUE_LIST_KEY = "workhub:queue:default";
|
|
6155
|
+
var QUEUE_HIGH_KEY = "workhub:queue:high";
|
|
6156
|
+
var QUEUE_LOW_KEY = "workhub:queue:low";
|
|
6157
|
+
var QUEUE_KEYS = [QUEUE_HIGH_KEY, QUEUE_LIST_KEY, QUEUE_LOW_KEY];
|
|
6158
|
+
function queueKeyForPriority(priority = "default") {
|
|
6159
|
+
switch (priority) {
|
|
6160
|
+
case "high":
|
|
6161
|
+
return QUEUE_HIGH_KEY;
|
|
6162
|
+
case "low":
|
|
6163
|
+
return QUEUE_LOW_KEY;
|
|
6164
|
+
default:
|
|
6165
|
+
return QUEUE_LIST_KEY;
|
|
6166
|
+
}
|
|
6167
|
+
}
|
|
6168
|
+
function parseQueueJobEnvelope(rawPayload) {
|
|
6169
|
+
let parsed;
|
|
6170
|
+
try {
|
|
6171
|
+
parsed = JSON.parse(rawPayload);
|
|
6172
|
+
} catch {
|
|
6173
|
+
console.error("[QueueWorker] Ignoring malformed queue payload");
|
|
6174
|
+
return null;
|
|
6175
|
+
}
|
|
6176
|
+
if (!parsed || typeof parsed !== "object") {
|
|
6177
|
+
console.error("[QueueWorker] Ignoring non-object queue payload");
|
|
6178
|
+
return null;
|
|
6179
|
+
}
|
|
6180
|
+
const envelope = parsed;
|
|
6181
|
+
if (typeof envelope.name !== "string" || envelope.name.length === 0) {
|
|
6182
|
+
console.error("[QueueWorker] Ignoring queue payload without job name");
|
|
6183
|
+
return null;
|
|
6184
|
+
}
|
|
6185
|
+
if (!jobRegistry.create(envelope.name)) {
|
|
6186
|
+
console.error(`[QueueWorker] Ignoring unknown job name: ${envelope.name}`);
|
|
6187
|
+
return null;
|
|
6188
|
+
}
|
|
6189
|
+
if (envelope.payload !== undefined && (typeof envelope.payload !== "object" || envelope.payload === null)) {
|
|
6190
|
+
console.error("[QueueWorker] Ignoring queue payload with invalid payload object");
|
|
6191
|
+
return null;
|
|
6192
|
+
}
|
|
6553
6193
|
return {
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
startTimeUnixNano: String(Math.floor(input.startedAt * 1e6)),
|
|
6558
|
-
endTimeUnixNano: String(Math.floor(input.endedAt * 1e6)),
|
|
6559
|
-
attributes: Object.entries(input.attributes ?? {}).map(([key, value]) => ({
|
|
6560
|
-
key,
|
|
6561
|
-
value: { stringValue: value }
|
|
6562
|
-
})),
|
|
6563
|
-
status: { code: 1 }
|
|
6194
|
+
name: envelope.name,
|
|
6195
|
+
payload: envelope.payload ?? {},
|
|
6196
|
+
attempts: typeof envelope.attempts === "number" ? envelope.attempts : 0
|
|
6564
6197
|
};
|
|
6565
6198
|
}
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6199
|
+
|
|
6200
|
+
class RedisQueue {
|
|
6201
|
+
client;
|
|
6202
|
+
constructor(redisUrl) {
|
|
6203
|
+
this.client = new RedisClient5(redisUrl);
|
|
6570
6204
|
}
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
headers: { "content-type": "application/json" },
|
|
6576
|
-
body: JSON.stringify({
|
|
6577
|
-
resourceSpans: [
|
|
6578
|
-
{
|
|
6579
|
-
resource: {
|
|
6580
|
-
attributes: [{ key: "service.name", value: { stringValue: serviceName } }]
|
|
6581
|
-
},
|
|
6582
|
-
scopeSpans: [{ spans: [span] }]
|
|
6583
|
-
}
|
|
6584
|
-
]
|
|
6585
|
-
})
|
|
6586
|
-
});
|
|
6587
|
-
}
|
|
6588
|
-
|
|
6589
|
-
// ../../src/core/tracing/tracingMiddleware.ts
|
|
6590
|
-
function createTracingMiddleware() {
|
|
6591
|
-
return async (request, next) => {
|
|
6592
|
-
const traceId = (request.headers.get("x-trace-id") ?? crypto.randomUUID()).replace(/-/g, "");
|
|
6593
|
-
const spanId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
|
|
6594
|
-
const startedAt = performance.now();
|
|
6595
|
-
const path = new URL(request.url).pathname;
|
|
6596
|
-
return await runWithTraceContext({ traceId, spanId }, async () => {
|
|
6597
|
-
const response = await next();
|
|
6598
|
-
const endedAt = performance.now();
|
|
6599
|
-
const headers = new Headers(response.headers);
|
|
6600
|
-
headers.set("x-trace-id", traceId);
|
|
6601
|
-
headers.set("x-span-id", spanId);
|
|
6602
|
-
headers.set("traceparent", `00-${traceId}-${spanId}-01`);
|
|
6603
|
-
headers.set("server-timing", `app;dur=${(endedAt - startedAt).toFixed(2)}`);
|
|
6604
|
-
exportOtelSpan(createSpan({
|
|
6605
|
-
traceId,
|
|
6606
|
-
name: `${request.method} ${path}`,
|
|
6607
|
-
startedAt,
|
|
6608
|
-
endedAt,
|
|
6609
|
-
attributes: {
|
|
6610
|
-
"http.method": request.method,
|
|
6611
|
-
"http.route": path,
|
|
6612
|
-
"http.status_code": String(response.status)
|
|
6613
|
-
}
|
|
6614
|
-
})).catch(() => {
|
|
6615
|
-
return;
|
|
6616
|
-
});
|
|
6617
|
-
return new Response(response.body, {
|
|
6618
|
-
status: response.status,
|
|
6619
|
-
statusText: response.statusText,
|
|
6620
|
-
headers
|
|
6621
|
-
});
|
|
6622
|
-
});
|
|
6623
|
-
};
|
|
6624
|
-
}
|
|
6625
|
-
// ../../src/core/validation/rules.ts
|
|
6626
|
-
function required() {
|
|
6627
|
-
return (field, value) => {
|
|
6628
|
-
if (value === undefined || value === null || typeof value === "string" && value.trim() === "") {
|
|
6629
|
-
return `"${field}" is required.`;
|
|
6630
|
-
}
|
|
6631
|
-
return;
|
|
6632
|
-
};
|
|
6633
|
-
}
|
|
6634
|
-
function stringRule() {
|
|
6635
|
-
return (field, value) => {
|
|
6636
|
-
if (value === undefined || value === null) {
|
|
6637
|
-
return;
|
|
6638
|
-
}
|
|
6639
|
-
if (typeof value !== "string") {
|
|
6640
|
-
return `"${field}" must be a string.`;
|
|
6205
|
+
async dispatch(job, payload) {
|
|
6206
|
+
const name = jobRegistry.resolveName(job);
|
|
6207
|
+
if (!name) {
|
|
6208
|
+
throw new Error("Job is not registered with the queue worker registry.");
|
|
6641
6209
|
}
|
|
6642
|
-
|
|
6643
|
-
|
|
6210
|
+
const envelope = {
|
|
6211
|
+
name,
|
|
6212
|
+
payload,
|
|
6213
|
+
attempts: 0
|
|
6214
|
+
};
|
|
6215
|
+
const queueKey = queueKeyForPriority(job.priority);
|
|
6216
|
+
await this.client.lpush(queueKey, JSON.stringify(envelope));
|
|
6217
|
+
}
|
|
6644
6218
|
}
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6219
|
+
|
|
6220
|
+
class QueueWorker {
|
|
6221
|
+
failedJobs;
|
|
6222
|
+
timeoutSeconds;
|
|
6223
|
+
running = false;
|
|
6224
|
+
stopping = false;
|
|
6225
|
+
client;
|
|
6226
|
+
constructor(redisUrl, failedJobs, timeoutSeconds = 5) {
|
|
6227
|
+
this.failedJobs = failedJobs;
|
|
6228
|
+
this.timeoutSeconds = timeoutSeconds;
|
|
6229
|
+
this.client = new RedisClient5(redisUrl);
|
|
6230
|
+
}
|
|
6231
|
+
requestStop() {
|
|
6232
|
+
this.stopping = true;
|
|
6233
|
+
}
|
|
6234
|
+
isRunning() {
|
|
6235
|
+
return this.running;
|
|
6236
|
+
}
|
|
6237
|
+
async processNext() {
|
|
6238
|
+
let result = null;
|
|
6239
|
+
for (const queueKey of QUEUE_KEYS) {
|
|
6240
|
+
result = await this.client.brpop(queueKey, 1);
|
|
6241
|
+
if (result) {
|
|
6242
|
+
break;
|
|
6243
|
+
}
|
|
6649
6244
|
}
|
|
6650
|
-
if (
|
|
6651
|
-
|
|
6245
|
+
if (!result) {
|
|
6246
|
+
result = await this.client.brpop(QUEUE_LIST_KEY, this.timeoutSeconds);
|
|
6652
6247
|
}
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
}
|
|
6656
|
-
function maxLength(maximum) {
|
|
6657
|
-
return (field, value) => {
|
|
6658
|
-
if (typeof value !== "string") {
|
|
6659
|
-
return;
|
|
6248
|
+
if (!result) {
|
|
6249
|
+
return false;
|
|
6660
6250
|
}
|
|
6661
|
-
|
|
6662
|
-
|
|
6251
|
+
const [, rawPayload] = result;
|
|
6252
|
+
const envelope = parseQueueJobEnvelope(rawPayload);
|
|
6253
|
+
if (!envelope) {
|
|
6254
|
+
return true;
|
|
6663
6255
|
}
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
}
|
|
6667
|
-
|
|
6668
|
-
return (field, value) => {
|
|
6669
|
-
if (typeof value !== "string") {
|
|
6670
|
-
return;
|
|
6256
|
+
try {
|
|
6257
|
+
await runQueueJob(envelope, this.failedJobs);
|
|
6258
|
+
} catch (error) {
|
|
6259
|
+
console.error("[QueueWorker] Job failed:", error);
|
|
6671
6260
|
}
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6261
|
+
return true;
|
|
6262
|
+
}
|
|
6263
|
+
async run() {
|
|
6264
|
+
this.running = true;
|
|
6265
|
+
while (!this.stopping) {
|
|
6266
|
+
await this.processNext();
|
|
6675
6267
|
}
|
|
6676
|
-
|
|
6677
|
-
}
|
|
6268
|
+
this.running = false;
|
|
6269
|
+
}
|
|
6270
|
+
close() {
|
|
6271
|
+
this.client.close();
|
|
6272
|
+
}
|
|
6678
6273
|
}
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6274
|
+
|
|
6275
|
+
// ../../src/core/queue/resilientQueue.ts
|
|
6276
|
+
class ResilientQueue {
|
|
6277
|
+
failedJobs;
|
|
6278
|
+
asyncDispatch;
|
|
6279
|
+
constructor(failedJobs, asyncDispatch = false) {
|
|
6280
|
+
this.failedJobs = failedJobs;
|
|
6281
|
+
this.asyncDispatch = asyncDispatch;
|
|
6682
6282
|
}
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
const messages = rules.map((rule) => rule(field, body[field], body)).filter((message) => message !== undefined);
|
|
6688
|
-
if (messages.length > 0) {
|
|
6689
|
-
errors[field] = messages;
|
|
6690
|
-
continue;
|
|
6283
|
+
async dispatch(job, payload) {
|
|
6284
|
+
const name = jobRegistry.resolveName(job);
|
|
6285
|
+
if (!name) {
|
|
6286
|
+
throw new Error("Job is not registered with the queue worker registry.");
|
|
6691
6287
|
}
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6288
|
+
const envelope = {
|
|
6289
|
+
name,
|
|
6290
|
+
payload,
|
|
6291
|
+
attempts: 0
|
|
6292
|
+
};
|
|
6293
|
+
if (this.asyncDispatch) {
|
|
6294
|
+
setTimeout(() => {
|
|
6295
|
+
runQueueJob(envelope, this.failedJobs).catch((error) => {
|
|
6296
|
+
console.error("[ResilientQueue] Job failed:", error);
|
|
6297
|
+
});
|
|
6298
|
+
}, 0);
|
|
6299
|
+
return;
|
|
6695
6300
|
}
|
|
6301
|
+
await runQueueJob(envelope, this.failedJobs);
|
|
6696
6302
|
}
|
|
6697
|
-
|
|
6698
|
-
|
|
6303
|
+
}
|
|
6304
|
+
|
|
6305
|
+
// ../../src/core/queue/publicQueue.ts
|
|
6306
|
+
function createFailedJobService() {
|
|
6307
|
+
return new failedJobService_default(new failedJobRepository_default);
|
|
6308
|
+
}
|
|
6309
|
+
function createTrackedJob(name, job) {
|
|
6310
|
+
return jobRegistry.track(name, job);
|
|
6311
|
+
}
|
|
6312
|
+
function createProductionQueue(driver, options = {}) {
|
|
6313
|
+
options.registerJobs?.();
|
|
6314
|
+
const failedJobs = options.failedJobs ?? createFailedJobService();
|
|
6315
|
+
if (driver === "redis") {
|
|
6316
|
+
if (!options.redisUrl) {
|
|
6317
|
+
throw new Error('QUEUE_DRIVER="redis" requires REDIS_URL to be set.');
|
|
6318
|
+
}
|
|
6319
|
+
return new RedisQueue(options.redisUrl);
|
|
6699
6320
|
}
|
|
6700
|
-
return
|
|
6321
|
+
return new ResilientQueue(failedJobs, driver === "async");
|
|
6701
6322
|
}
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6323
|
+
function createQueueWorker(redisUrl, failedJobs = createFailedJobService()) {
|
|
6324
|
+
return new QueueWorker(redisUrl, failedJobs);
|
|
6325
|
+
}
|
|
6326
|
+
// ../../src/core/queue/queueMetrics.ts
|
|
6327
|
+
var {RedisClient: RedisClient6 } = globalThis.Bun;
|
|
6328
|
+
async function readRedisQueueDepth(redisUrl) {
|
|
6329
|
+
const client = new RedisClient6(redisUrl);
|
|
6330
|
+
const [high, defaultQueue, low] = await Promise.all([
|
|
6331
|
+
client.llen(QUEUE_HIGH_KEY),
|
|
6332
|
+
client.llen(QUEUE_LIST_KEY),
|
|
6333
|
+
client.llen(QUEUE_LOW_KEY)
|
|
6334
|
+
]);
|
|
6335
|
+
return {
|
|
6336
|
+
high: Number(high ?? 0),
|
|
6337
|
+
default: Number(defaultQueue ?? 0),
|
|
6338
|
+
low: Number(low ?? 0),
|
|
6339
|
+
total: Number(high ?? 0) + Number(defaultQueue ?? 0) + Number(low ?? 0)
|
|
6340
|
+
};
|
|
6341
|
+
}
|
|
6342
|
+
async function collectQueueMetrics() {
|
|
6343
|
+
const driver = queueConfig.driver;
|
|
6344
|
+
const failedJobs = createFailedJobService();
|
|
6345
|
+
const failedCount = (await failedJobs.listRecent(1000)).length;
|
|
6346
|
+
if (driver !== "redis") {
|
|
6347
|
+
return {
|
|
6348
|
+
driver,
|
|
6349
|
+
pending: {
|
|
6350
|
+
high: 0,
|
|
6351
|
+
default: 0,
|
|
6352
|
+
low: 0,
|
|
6353
|
+
total: 0
|
|
6354
|
+
},
|
|
6355
|
+
failedCount
|
|
6356
|
+
};
|
|
6357
|
+
}
|
|
6358
|
+
const redisUrl = process.env.REDIS_URL;
|
|
6359
|
+
if (!redisUrl) {
|
|
6360
|
+
return {
|
|
6361
|
+
driver,
|
|
6362
|
+
pending: {
|
|
6363
|
+
high: 0,
|
|
6364
|
+
default: 0,
|
|
6365
|
+
low: 0,
|
|
6366
|
+
total: 0
|
|
6367
|
+
},
|
|
6368
|
+
failedCount
|
|
6369
|
+
};
|
|
6370
|
+
}
|
|
6371
|
+
return {
|
|
6372
|
+
driver,
|
|
6373
|
+
pending: await readRedisQueueDepth(redisUrl),
|
|
6374
|
+
failedCount
|
|
6375
|
+
};
|
|
6376
|
+
}
|
|
6377
|
+
// ../../src/core/scheduler/schedule.ts
|
|
6378
|
+
class Schedule {
|
|
6379
|
+
tasks = [];
|
|
6380
|
+
command(expression, name, run) {
|
|
6381
|
+
this.tasks.push({ expression, name, run });
|
|
6382
|
+
return this;
|
|
6383
|
+
}
|
|
6384
|
+
dueTasks(now = new Date) {
|
|
6385
|
+
const minute = now.getMinutes();
|
|
6386
|
+
return this.tasks.filter((task) => {
|
|
6387
|
+
if (task.expression === "* * * * *") {
|
|
6388
|
+
return true;
|
|
6389
|
+
}
|
|
6390
|
+
if (task.expression.startsWith("*/")) {
|
|
6391
|
+
const interval = Number.parseInt(task.expression.slice(2), 10);
|
|
6392
|
+
return Number.isInteger(interval) && interval > 0 && minute % interval === 0;
|
|
6393
|
+
}
|
|
6394
|
+
return false;
|
|
6395
|
+
});
|
|
6396
|
+
}
|
|
6397
|
+
tasksList() {
|
|
6398
|
+
return [...this.tasks];
|
|
6399
|
+
}
|
|
6400
|
+
}
|
|
6401
|
+
var appSchedule = new Schedule;
|
|
6402
|
+
async function runDueScheduledTasks(schedule = appSchedule, now = new Date) {
|
|
6403
|
+
const due = schedule.dueTasks(now);
|
|
6404
|
+
for (const task of due) {
|
|
6405
|
+
await task.run();
|
|
6406
|
+
}
|
|
6407
|
+
return due.length;
|
|
6408
|
+
}
|
|
6409
|
+
// ../../src/core/security/publicReads.ts
|
|
6410
|
+
function isPublicReadsEnabled() {
|
|
6411
|
+
return isFeatureEnabled("publicReads");
|
|
6412
|
+
}
|
|
6413
|
+
function guestCanViewResource() {
|
|
6414
|
+
return isPublicReadsEnabled();
|
|
6415
|
+
}
|
|
6416
|
+
// ../../src/core/tenant/tenantMiddleware.ts
|
|
6417
|
+
import { createHash } from "crypto";
|
|
6418
|
+
|
|
6419
|
+
// ../../src/core/tenant/databaseTenantContext.ts
|
|
6420
|
+
async function runWithMigrationBypass(callback) {
|
|
6421
|
+
await repositoryConnection`SELECT set_config('app.bypass_rls', 'true', false)`;
|
|
6422
|
+
try {
|
|
6423
|
+
return await callback();
|
|
6424
|
+
} finally {
|
|
6425
|
+
await repositoryConnection`SELECT set_config('app.bypass_rls', 'false', false)`;
|
|
6426
|
+
}
|
|
6427
|
+
}
|
|
6428
|
+
|
|
6429
|
+
// ../../src/core/tenant/tenantMiddleware.ts
|
|
6430
|
+
var DEFAULT_TENANT = {
|
|
6431
|
+
id: 1,
|
|
6432
|
+
slug: "default",
|
|
6433
|
+
plan: "enterprise",
|
|
6434
|
+
region: "eu"
|
|
6435
|
+
};
|
|
6436
|
+
async function resolveUserTenantId(userId) {
|
|
6437
|
+
return await runWithMigrationBypass(async () => {
|
|
6438
|
+
const rows = await repositoryConnection`
|
|
6439
|
+
SELECT tenant_id
|
|
6440
|
+
FROM users
|
|
6441
|
+
WHERE id = ${userId}
|
|
6442
|
+
LIMIT 1
|
|
6443
|
+
`;
|
|
6444
|
+
return rows[0]?.tenant_id ?? DEFAULT_TENANT.id;
|
|
6445
|
+
});
|
|
6446
|
+
}
|
|
6447
|
+
function auditChecksum(payload) {
|
|
6448
|
+
return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
6449
|
+
}
|
|
6450
|
+
async function resolveTenantForRequest(request) {
|
|
6451
|
+
const user = currentAuthUser();
|
|
6452
|
+
const headerValue = request.headers.get("x-tenant-id")?.trim();
|
|
6453
|
+
const parsedHeader = headerValue !== undefined && headerValue.length > 0 ? Number.parseInt(headerValue, 10) : Number.NaN;
|
|
6454
|
+
if (user) {
|
|
6455
|
+
const userId = typeof user.id === "number" ? user.id : Number.parseInt(String(user.id), 10);
|
|
6456
|
+
if (Number.isInteger(userId) && userId > 0) {
|
|
6457
|
+
const userTenantId = await resolveUserTenantId(userId);
|
|
6458
|
+
if (!isGlobalAdmin(user)) {
|
|
6459
|
+
if (Number.isInteger(parsedHeader) && parsedHeader > 0 && parsedHeader !== userTenantId) {
|
|
6460
|
+
throw new ForbiddenError("Tenant header does not match your account.");
|
|
6461
|
+
}
|
|
6462
|
+
return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
|
|
6463
|
+
}
|
|
6464
|
+
if (Number.isInteger(parsedHeader) && parsedHeader > 0) {
|
|
6465
|
+
return await resolveTenant(parsedHeader) ?? DEFAULT_TENANT;
|
|
6466
|
+
}
|
|
6467
|
+
return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
|
|
6468
|
+
}
|
|
6469
|
+
}
|
|
6470
|
+
const tenantId = Number.isInteger(parsedHeader) && parsedHeader > 0 ? parsedHeader : DEFAULT_TENANT.id;
|
|
6471
|
+
return await resolveTenant(tenantId) ?? DEFAULT_TENANT;
|
|
6472
|
+
}
|
|
6473
|
+
function createTenantMiddleware() {
|
|
6474
|
+
return async (request, next) => {
|
|
6475
|
+
const pathname = new URL(request.url).pathname;
|
|
6476
|
+
if (pathname.startsWith("/scim/")) {
|
|
6477
|
+
return await next();
|
|
6478
|
+
}
|
|
6479
|
+
try {
|
|
6480
|
+
const tenant = await resolveTenantForRequest(request);
|
|
6481
|
+
return await runWithTenantDatabase(tenant, async () => {
|
|
6482
|
+
const response = await next();
|
|
6483
|
+
const headers = new Headers(response.headers);
|
|
6484
|
+
headers.set("x-tenant-id", String(tenant.id));
|
|
6485
|
+
headers.set("x-tenant-region", tenant.region);
|
|
6486
|
+
return new Response(response.body, {
|
|
6487
|
+
status: response.status,
|
|
6488
|
+
statusText: response.statusText,
|
|
6489
|
+
headers
|
|
6490
|
+
});
|
|
6491
|
+
});
|
|
6492
|
+
} catch (error) {
|
|
6493
|
+
if (error instanceof HttpError) {
|
|
6494
|
+
return Response.json({ error: error.message }, { status: error.status });
|
|
6495
|
+
}
|
|
6496
|
+
throw error;
|
|
6497
|
+
}
|
|
6498
|
+
};
|
|
6499
|
+
}
|
|
6500
|
+
// ../../src/core/tracing/traceContext.ts
|
|
6501
|
+
var traceContextStorage = createAsyncContextStore("@getstrata/traceContext");
|
|
6502
|
+
function runWithTraceContext(context, callback) {
|
|
6503
|
+
return traceContextStorage.run(context, callback);
|
|
6504
|
+
}
|
|
6505
|
+
function currentTraceId() {
|
|
6506
|
+
return traceContextStorage.getStore()?.traceId ?? null;
|
|
6507
|
+
}
|
|
6508
|
+
// ../../src/core/tracing/otel.ts
|
|
6509
|
+
import { randomBytes } from "crypto";
|
|
6510
|
+
function randomHex(bytes) {
|
|
6511
|
+
return randomBytes(bytes).toString("hex");
|
|
6512
|
+
}
|
|
6513
|
+
function createSpan(input) {
|
|
6514
|
+
const spanId = randomHex(8);
|
|
6515
|
+
return {
|
|
6516
|
+
traceId: input.traceId,
|
|
6517
|
+
spanId,
|
|
6518
|
+
name: input.name,
|
|
6519
|
+
startTimeUnixNano: String(Math.floor(input.startedAt * 1e6)),
|
|
6520
|
+
endTimeUnixNano: String(Math.floor(input.endedAt * 1e6)),
|
|
6521
|
+
attributes: Object.entries(input.attributes ?? {}).map(([key, value]) => ({
|
|
6522
|
+
key,
|
|
6523
|
+
value: { stringValue: value }
|
|
6524
|
+
})),
|
|
6525
|
+
status: { code: 1 }
|
|
6526
|
+
};
|
|
6527
|
+
}
|
|
6528
|
+
async function exportOtelSpan(span) {
|
|
6529
|
+
const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
|
|
6530
|
+
if (!endpoint) {
|
|
6531
|
+
return;
|
|
6532
|
+
}
|
|
6533
|
+
const serviceName = process.env.OTEL_SERVICE_NAME?.trim() ?? "workhub-api";
|
|
6534
|
+
const url = endpoint.endsWith("/v1/traces") ? endpoint : `${endpoint.replace(/\/$/, "")}/v1/traces`;
|
|
6535
|
+
await fetch(url, {
|
|
6536
|
+
method: "POST",
|
|
6537
|
+
headers: { "content-type": "application/json" },
|
|
6538
|
+
body: JSON.stringify({
|
|
6539
|
+
resourceSpans: [
|
|
6540
|
+
{
|
|
6541
|
+
resource: {
|
|
6542
|
+
attributes: [{ key: "service.name", value: { stringValue: serviceName } }]
|
|
6543
|
+
},
|
|
6544
|
+
scopeSpans: [{ spans: [span] }]
|
|
6545
|
+
}
|
|
6546
|
+
]
|
|
6547
|
+
})
|
|
6548
|
+
});
|
|
6549
|
+
}
|
|
6550
|
+
|
|
6551
|
+
// ../../src/core/tracing/tracingMiddleware.ts
|
|
6552
|
+
function createTracingMiddleware() {
|
|
6553
|
+
return async (request, next) => {
|
|
6554
|
+
const traceId = (request.headers.get("x-trace-id") ?? crypto.randomUUID()).replace(/-/g, "");
|
|
6555
|
+
const spanId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
|
|
6556
|
+
const startedAt = performance.now();
|
|
6557
|
+
const path = new URL(request.url).pathname;
|
|
6558
|
+
return await runWithTraceContext({ traceId, spanId }, async () => {
|
|
6559
|
+
const response = await next();
|
|
6560
|
+
const endedAt = performance.now();
|
|
6561
|
+
const headers = new Headers(response.headers);
|
|
6562
|
+
headers.set("x-trace-id", traceId);
|
|
6563
|
+
headers.set("x-span-id", spanId);
|
|
6564
|
+
headers.set("traceparent", `00-${traceId}-${spanId}-01`);
|
|
6565
|
+
headers.set("server-timing", `app;dur=${(endedAt - startedAt).toFixed(2)}`);
|
|
6566
|
+
exportOtelSpan(createSpan({
|
|
6567
|
+
traceId,
|
|
6568
|
+
name: `${request.method} ${path}`,
|
|
6569
|
+
startedAt,
|
|
6570
|
+
endedAt,
|
|
6571
|
+
attributes: {
|
|
6572
|
+
"http.method": request.method,
|
|
6573
|
+
"http.route": path,
|
|
6574
|
+
"http.status_code": String(response.status)
|
|
6575
|
+
}
|
|
6576
|
+
})).catch(() => {
|
|
6577
|
+
return;
|
|
6578
|
+
});
|
|
6579
|
+
return new Response(response.body, {
|
|
6580
|
+
status: response.status,
|
|
6581
|
+
statusText: response.statusText,
|
|
6582
|
+
headers
|
|
6583
|
+
});
|
|
6584
|
+
});
|
|
6585
|
+
};
|
|
6586
|
+
}
|
|
6587
|
+
// ../../src/core/validation/rules.ts
|
|
6588
|
+
function required() {
|
|
6589
|
+
return (field, value) => {
|
|
6590
|
+
if (value === undefined || value === null || typeof value === "string" && value.trim() === "") {
|
|
6591
|
+
return `"${field}" is required.`;
|
|
6592
|
+
}
|
|
6593
|
+
return;
|
|
6594
|
+
};
|
|
6595
|
+
}
|
|
6596
|
+
function stringRule() {
|
|
6597
|
+
return (field, value) => {
|
|
6598
|
+
if (value === undefined || value === null) {
|
|
6599
|
+
return;
|
|
6600
|
+
}
|
|
6601
|
+
if (typeof value !== "string") {
|
|
6602
|
+
return `"${field}" must be a string.`;
|
|
6603
|
+
}
|
|
6604
|
+
return;
|
|
6605
|
+
};
|
|
6606
|
+
}
|
|
6607
|
+
function minLength(minimum) {
|
|
6608
|
+
return (field, value) => {
|
|
6609
|
+
if (typeof value !== "string") {
|
|
6610
|
+
return;
|
|
6611
|
+
}
|
|
6612
|
+
if (value.trim().length < minimum) {
|
|
6613
|
+
return `"${field}" must be at least ${minimum} characters.`;
|
|
6614
|
+
}
|
|
6615
|
+
return;
|
|
6616
|
+
};
|
|
6617
|
+
}
|
|
6618
|
+
function maxLength(maximum) {
|
|
6619
|
+
return (field, value) => {
|
|
6620
|
+
if (typeof value !== "string") {
|
|
6621
|
+
return;
|
|
6622
|
+
}
|
|
6623
|
+
if (value.trim().length > maximum) {
|
|
6624
|
+
return `"${field}" must be at most ${maximum} characters.`;
|
|
6625
|
+
}
|
|
6626
|
+
return;
|
|
6627
|
+
};
|
|
6628
|
+
}
|
|
6629
|
+
function emailRule() {
|
|
6630
|
+
return (field, value) => {
|
|
6631
|
+
if (typeof value !== "string") {
|
|
6632
|
+
return;
|
|
6633
|
+
}
|
|
6634
|
+
const normalized = value.trim();
|
|
6635
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized)) {
|
|
6636
|
+
return `"${field}" must be a valid email address.`;
|
|
6637
|
+
}
|
|
6638
|
+
return;
|
|
6639
|
+
};
|
|
6640
|
+
}
|
|
6641
|
+
function validateObject(payload, schema) {
|
|
6642
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
|
|
6643
|
+
throw new ValidationError("Request body must be a JSON object.");
|
|
6644
|
+
}
|
|
6645
|
+
const body = payload;
|
|
6646
|
+
const errors = {};
|
|
6647
|
+
const output = {};
|
|
6648
|
+
for (const [field, rules] of Object.entries(schema)) {
|
|
6649
|
+
const messages = rules.map((rule) => rule(field, body[field], body)).filter((message) => message !== undefined);
|
|
6650
|
+
if (messages.length > 0) {
|
|
6651
|
+
errors[field] = messages;
|
|
6652
|
+
continue;
|
|
6653
|
+
}
|
|
6654
|
+
if (field in body && body[field] !== undefined) {
|
|
6655
|
+
const rawValue = body[field];
|
|
6656
|
+
output[field] = typeof rawValue === "string" ? rawValue.trim() : rawValue;
|
|
6657
|
+
}
|
|
6658
|
+
}
|
|
6659
|
+
if (Object.keys(errors).length > 0) {
|
|
6660
|
+
throw new ValidationError("The given data was invalid.", errors);
|
|
6661
|
+
}
|
|
6662
|
+
return output;
|
|
6663
|
+
}
|
|
6664
|
+
export {
|
|
6665
|
+
AdminResourceRegistry,
|
|
6947
6666
|
ApiTokenGuard,
|
|
6948
|
-
|
|
6667
|
+
AsyncQueue,
|
|
6668
|
+
AuthManager,
|
|
6669
|
+
BadRequestError,
|
|
6670
|
+
baseRepository_default as BaseRepository,
|
|
6671
|
+
Blueprint,
|
|
6672
|
+
CACHE_TAGS,
|
|
6673
|
+
repository_default2 as CacheRepository,
|
|
6674
|
+
ColumnDefinition,
|
|
6675
|
+
CompositeGuard,
|
|
6676
|
+
ConfigStore,
|
|
6677
|
+
ConflictError,
|
|
6678
|
+
DEFAULT_TENANT,
|
|
6679
|
+
DEFAULT_VIEWS_DIRECTORY,
|
|
6680
|
+
DatabaseTokenGuard,
|
|
6681
|
+
EtaViewEngine,
|
|
6682
|
+
EventBus,
|
|
6683
|
+
failedJobRepository_default as FailedJobRepository,
|
|
6684
|
+
failedJobService_default as FailedJobService,
|
|
6685
|
+
ForbiddenError,
|
|
6686
|
+
ForeignIdColumnDefinition,
|
|
6687
|
+
FormRequest,
|
|
6688
|
+
GuestGuard,
|
|
6689
|
+
HttpError,
|
|
6690
|
+
Job,
|
|
6691
|
+
LocalStorageDriver,
|
|
6692
|
+
LogMailDriver,
|
|
6693
|
+
Mailer,
|
|
6694
|
+
membershipService_default as MembershipService,
|
|
6695
|
+
Model,
|
|
6696
|
+
MySqlGrammar,
|
|
6697
|
+
NotFoundError,
|
|
6698
|
+
Notification,
|
|
6699
|
+
NotificationDispatcher,
|
|
6700
|
+
PayloadTooLargeError,
|
|
6701
|
+
Policy,
|
|
6702
|
+
PolicyGate,
|
|
6703
|
+
PostgresGrammar,
|
|
6704
|
+
PreconditionFailedError,
|
|
6705
|
+
PrometheusRegistry,
|
|
6706
|
+
QueueWorker,
|
|
6707
|
+
RedisQueue,
|
|
6708
|
+
RepositoryQuery,
|
|
6709
|
+
ResilientQueue,
|
|
6710
|
+
Schedule,
|
|
6711
|
+
Schema,
|
|
6712
|
+
ServiceContainer,
|
|
6713
|
+
SqliteGrammar,
|
|
6714
|
+
StorageManager,
|
|
6715
|
+
SyncQueue,
|
|
6716
|
+
UnauthorizedError,
|
|
6717
|
+
UnprocessableEntityError,
|
|
6718
|
+
UnsupportedSchemaFeatureError,
|
|
6719
|
+
ValidationError,
|
|
6720
|
+
WebFormRequest,
|
|
6721
|
+
WhereBuilder,
|
|
6722
|
+
appSchedule,
|
|
6723
|
+
appendOrganizationScope,
|
|
6724
|
+
appendProjectScope,
|
|
6725
|
+
applyCasts,
|
|
6726
|
+
applyConditionalGet,
|
|
6727
|
+
applyMiddlewareToRoutes,
|
|
6728
|
+
assertIfMatch,
|
|
6729
|
+
assertOrganizationReadable,
|
|
6730
|
+
assertResourceInCurrentTenant,
|
|
6731
|
+
auditChecksum,
|
|
6732
|
+
auth,
|
|
6733
|
+
authContext,
|
|
6734
|
+
belongsTo,
|
|
6735
|
+
belongsToMany,
|
|
6736
|
+
bindDatabaseConnection2 as bindDatabaseConnection,
|
|
6737
|
+
bindRouteModel,
|
|
6738
|
+
buildMarkdownMailMessage,
|
|
6739
|
+
buildRequestCacheKey,
|
|
6740
|
+
buildSmtpPayload,
|
|
6741
|
+
cache,
|
|
6742
|
+
collectQueueMetrics,
|
|
6743
|
+
compileBlueprint,
|
|
6744
|
+
composeMiddleware,
|
|
6745
|
+
conditionalJsonResponse,
|
|
6746
|
+
config,
|
|
6747
|
+
createAuthMiddleware,
|
|
6748
|
+
createAuthorizeMiddleware,
|
|
6749
|
+
createBodySizeLimitMiddleware,
|
|
6750
|
+
createCacheStore,
|
|
6751
|
+
createCorsMiddleware,
|
|
6752
|
+
createCsrfMiddleware,
|
|
6753
|
+
createCsrfProtection,
|
|
6754
|
+
createCsrfTokenCookie,
|
|
6755
|
+
createDatabaseConnection2 as createDatabaseConnection,
|
|
6756
|
+
createDatabaseQueryProxy,
|
|
6757
|
+
createFailedJobService,
|
|
6758
|
+
createFlashMiddleware,
|
|
6759
|
+
createLoginThrottleMiddleware,
|
|
6760
|
+
createMembershipMiddleware,
|
|
6761
|
+
createMemoryThrottleMiddleware,
|
|
6762
|
+
createMetricsMiddleware,
|
|
6763
|
+
createNotificationDispatcher,
|
|
6764
|
+
createProductionQueue,
|
|
6765
|
+
createQueue,
|
|
6766
|
+
createQueueWorker,
|
|
6767
|
+
createRequestLoggingMiddleware,
|
|
6768
|
+
createRequireAbilityMiddleware,
|
|
6769
|
+
createRequireAuthMiddleware,
|
|
6770
|
+
createRequireGlobalAdminMiddleware,
|
|
6771
|
+
createRequireWebAuthMiddleware,
|
|
6772
|
+
createSchemaBuilder,
|
|
6773
|
+
createScimAuthMiddleware,
|
|
6774
|
+
createScimThrottleMiddleware,
|
|
6775
|
+
createSecurityHeadersMiddleware,
|
|
6776
|
+
createStorageDriver,
|
|
6777
|
+
createTenantMiddleware,
|
|
6778
|
+
createThrottleMiddleware,
|
|
6779
|
+
createTracingMiddleware,
|
|
6780
|
+
createTrackedJob,
|
|
6781
|
+
createdResponse,
|
|
6782
|
+
currentAuthUser,
|
|
6783
|
+
currentOrgRole,
|
|
6784
|
+
currentOrganizationIds,
|
|
6785
|
+
currentRequestMeta,
|
|
6786
|
+
currentTenant,
|
|
6787
|
+
currentTenantId,
|
|
6788
|
+
currentTraceId,
|
|
6789
|
+
defineTable,
|
|
6790
|
+
dehydrateValue,
|
|
6791
|
+
emailRule,
|
|
6792
|
+
emptyPaginateResult,
|
|
6793
|
+
etagFromResource,
|
|
6794
|
+
eventBus,
|
|
6795
|
+
events,
|
|
6796
|
+
filterMassAssignable,
|
|
6797
|
+
formatAdminValue,
|
|
6798
|
+
freshDatabase,
|
|
6799
|
+
getActiveDatabaseConnection,
|
|
6800
|
+
getDefaultDatabasePool,
|
|
6801
|
+
getDefaultDatabaseQuery,
|
|
6802
|
+
getMigrationStatus,
|
|
6803
|
+
grammarForDriver,
|
|
6804
|
+
guestCanViewResource,
|
|
6805
|
+
hasMany,
|
|
6806
|
+
hasMinimumOrgRole2 as hasMinimumOrgRole,
|
|
6807
|
+
hasOne,
|
|
6808
|
+
hasOrgMembership,
|
|
6809
|
+
htmlResponse,
|
|
6810
|
+
hydrateValue,
|
|
6811
|
+
indexBelongsToManyRelation,
|
|
6812
|
+
indexBelongsToRelation,
|
|
6813
|
+
indexHasManyRelation,
|
|
6814
|
+
indexHasOneRelation,
|
|
6815
|
+
indexMorphManyRelation,
|
|
6816
|
+
indexMorphOneRelation,
|
|
6817
|
+
indexMorphToRelation,
|
|
6818
|
+
inferReferencedTable,
|
|
6819
|
+
installGracefulShutdownSignals,
|
|
6820
|
+
isEtagEnabled,
|
|
6821
|
+
isGlobalAdmin,
|
|
6822
|
+
isHtmxRequest,
|
|
6823
|
+
isInsideTenantDatabaseScope,
|
|
6824
|
+
isPublicReadsEnabled,
|
|
6825
|
+
jobRegistry,
|
|
6826
|
+
jsonResponse2 as jsonResponse,
|
|
6827
|
+
loadMigrationsFromDirectory,
|
|
6828
|
+
loadSeedersFromDirectory,
|
|
6829
|
+
log,
|
|
6830
|
+
logSecurityEvent,
|
|
6831
|
+
mail,
|
|
6832
|
+
mailer,
|
|
6833
|
+
markdownToHtml,
|
|
6834
|
+
maxLength,
|
|
6835
|
+
migrateDatabase,
|
|
6836
|
+
minLength,
|
|
6837
|
+
modelEventName,
|
|
6838
|
+
morphMany,
|
|
6839
|
+
morphOne,
|
|
6840
|
+
morphTo,
|
|
6841
|
+
noContentResponse,
|
|
6842
|
+
normalizeMetricPath,
|
|
6843
|
+
paginatedResponse,
|
|
6844
|
+
parseMultipartUpload,
|
|
6845
|
+
parsePaginationQuery,
|
|
6846
|
+
parsePositiveIntParam,
|
|
6847
|
+
policyGate,
|
|
6848
|
+
prometheusRegistry,
|
|
6849
|
+
queue,
|
|
6850
|
+
rateLimitMultiplierForPlan,
|
|
6851
|
+
readBunRequestCookie,
|
|
6852
|
+
readRequestCookie,
|
|
6853
|
+
readSubmittedCsrfToken,
|
|
6854
|
+
readSubmittedCsrfTokenFromBody,
|
|
6855
|
+
registerDefaultDatabasePool,
|
|
6856
|
+
registerModelRepository,
|
|
6857
|
+
registerShutdownHandler,
|
|
6858
|
+
renderMarkdownMail,
|
|
6859
|
+
repositoryConnection,
|
|
6860
|
+
requestIdMiddleware,
|
|
6861
|
+
required,
|
|
6862
|
+
resetDefaultStorage,
|
|
6863
|
+
resolveApplicationAuth,
|
|
6864
|
+
resolveApplicationCache,
|
|
6865
|
+
resolveApplicationConfig,
|
|
6866
|
+
resolveApplicationDependencies,
|
|
6867
|
+
resolveApplicationEventBus,
|
|
6868
|
+
resolveApplicationLogger,
|
|
6869
|
+
resolveApplicationPolicyGate,
|
|
6870
|
+
resolveApplicationQueue,
|
|
6871
|
+
resolveCsrfToken,
|
|
6872
|
+
resolveCsrfTokenForRequest,
|
|
6873
|
+
resolveDatabaseDriver,
|
|
6874
|
+
resolveMembershipService,
|
|
6875
|
+
resolveOrganizationScope,
|
|
6876
|
+
resolveRepositoryConnection,
|
|
6877
|
+
resolveService,
|
|
6878
|
+
resolveUserId,
|
|
6879
|
+
resolveUserTenantId,
|
|
6880
|
+
resolveWebLayoutData,
|
|
6881
|
+
rollbackDatabase,
|
|
6882
|
+
runDueScheduledTasks,
|
|
6883
|
+
runGracefulShutdown,
|
|
6884
|
+
runInTransaction,
|
|
6885
|
+
runQueueJob,
|
|
6886
|
+
runSeedersFromDirectory,
|
|
6887
|
+
runWithAuthUser,
|
|
6888
|
+
runWithDatabaseConnection,
|
|
6889
|
+
runWithRequestMeta,
|
|
6890
|
+
runWithTenant,
|
|
6891
|
+
runWithTenantDatabase,
|
|
6892
|
+
runWithTraceContext,
|
|
6893
|
+
scopedOrganizationIds,
|
|
6894
|
+
securedBindRouteModel,
|
|
6895
|
+
securedBindRouteModelByKey,
|
|
6896
|
+
sendMarkdownMail,
|
|
6897
|
+
serializeDate,
|
|
6898
|
+
setActiveApplicationContext,
|
|
6899
|
+
storageFacade as storage,
|
|
6900
|
+
stringRule,
|
|
6901
|
+
stripMarkdown,
|
|
6902
|
+
toPaginatedResourceCollection,
|
|
6903
|
+
toResourceCollection,
|
|
6904
|
+
validateObject,
|
|
6905
|
+
verifyCsrfToken,
|
|
6906
|
+
withErrorHandling,
|
|
6907
|
+
withMiddleware,
|
|
6908
|
+
withMigrationLock,
|
|
6909
|
+
wrapMarkdownMailLayout,
|
|
6910
|
+
wrapRouteHandler
|
|
6949
6911
|
};
|