@getstrata/bootstrap 0.2.5 → 0.2.7
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 +3 -1
- package/dist/bootstrap/contracts.d.ts +2 -0
- package/dist/bootstrap/httpKernel.d.ts +1 -2
- package/dist/bootstrap/middleware.d.ts +1 -1
- package/dist/bootstrap/providers/storage.d.ts +3 -0
- package/dist/bootstrap/scimRoutes.d.ts +1 -1
- package/dist/core/admin/registry.d.ts +1 -0
- package/dist/core/auth/membershipScope.d.ts +16 -0
- package/dist/core/auth/membershipService.d.ts +24 -0
- package/dist/core/database/baseRepository.d.ts +6 -1
- package/dist/core/database/connectionContext.d.ts +2 -1
- package/dist/core/database/defaultConnection.d.ts +6 -0
- package/dist/core/database/queryProxy.d.ts +3 -0
- package/dist/core/database/repositoryConnection.d.ts +3 -3
- package/dist/core/jobs/dispatchWebhookJob.d.ts +0 -1
- package/dist/core/queue/queueMetrics.d.ts +15 -0
- package/dist/core/security/safeFetch.d.ts +2 -0
- package/dist/core/security/safeUrl.d.ts +16 -1
- package/dist/core/storage/storage.d.ts +6 -3
- package/dist/core/tenant/tenantDatabaseScope.d.ts +2 -1
- package/dist/db/connection/index.d.ts +1 -1
- package/dist/domain/workhub.d.ts +35 -0
- package/dist/entries/applicationRegistry.js +185 -0
- package/dist/entries/config.js +42 -0
- package/dist/entries/context.js +4208 -0
- package/dist/entries/contracts.js +92 -0
- package/dist/entries/createWebRoutes.js +996 -0
- package/dist/entries/httpKernel.js +264 -0
- package/dist/entries/providers/view.js +635 -0
- package/dist/entries/providers.js +4094 -0
- package/dist/framework/public-api.d.ts +36 -4
- package/dist/index.js +328 -1046
- package/dist/modules/organization/repository.d.ts +14 -0
- package/dist/modules/organization/table.d.ts +3 -0
- package/dist/modules/organization/types.d.ts +10 -0
- package/dist/modules/scim/controller.d.ts +2 -1
- package/dist/modules/scim/scimResponse.d.ts +1 -1
- package/dist/modules/scim/service.d.ts +4 -7
- package/dist/modules/user/repository.d.ts +1 -0
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -65,107 +65,80 @@ var appConfig = {
|
|
|
65
65
|
apiPrefix: process.env.API_PREFIX ?? "/api/v1"
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
-
// ../../src/
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
72
|
-
}
|
|
73
|
-
var databaseConfig = {
|
|
74
|
-
url: process.env.DATABASE_URL ?? "",
|
|
75
|
-
poolMax: readInteger("DB_POOL_MAX", 10),
|
|
76
|
-
idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
|
|
77
|
-
maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
|
|
78
|
-
connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
|
|
68
|
+
// ../../src/core/database/boundConnection.ts
|
|
69
|
+
var boundConnectionHolder = {
|
|
70
|
+
connection: null
|
|
79
71
|
};
|
|
72
|
+
function getBoundDatabaseConnection() {
|
|
73
|
+
return boundConnectionHolder.connection;
|
|
74
|
+
}
|
|
80
75
|
|
|
81
76
|
// ../../src/core/database/connectionContext.ts
|
|
82
77
|
import { AsyncLocalStorage } from "async_hooks";
|
|
83
78
|
var activeConnection = new AsyncLocalStorage;
|
|
84
|
-
function runWithDatabaseConnection(connection, callback) {
|
|
85
|
-
return activeConnection.run(connection, callback);
|
|
86
|
-
}
|
|
87
79
|
function getActiveDatabaseConnection(fallback) {
|
|
88
80
|
return activeConnection.getStore() ?? fallback;
|
|
89
81
|
}
|
|
90
82
|
|
|
91
|
-
// ../../src/
|
|
92
|
-
var
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
83
|
+
// ../../src/core/database/queryProxy.ts
|
|
84
|
+
var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
|
|
85
|
+
function createDatabaseQueryProxy(pool) {
|
|
86
|
+
function resolveDatabase() {
|
|
87
|
+
return getActiveDatabaseConnection(pool);
|
|
96
88
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
89
|
+
function resolveDatabaseForProperty(property) {
|
|
90
|
+
if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
|
|
91
|
+
return pool;
|
|
92
|
+
}
|
|
93
|
+
return resolveDatabase();
|
|
94
|
+
}
|
|
95
|
+
return new Proxy(function database() {}, {
|
|
96
|
+
apply(_target, _thisArg, args) {
|
|
97
|
+
return resolveDatabase()(...args);
|
|
98
|
+
},
|
|
99
|
+
get(_target, property) {
|
|
100
|
+
const connection = resolveDatabaseForProperty(property);
|
|
101
|
+
const value = connection[property];
|
|
102
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
103
|
+
}
|
|
103
104
|
});
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
// ../../src/
|
|
107
|
-
var
|
|
107
|
+
// ../../src/core/database/defaultConnection.ts
|
|
108
|
+
var defaultPool = {
|
|
108
109
|
connection: null
|
|
109
110
|
};
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
|
|
117
|
-
function resolveDatabase() {
|
|
118
|
-
return getActiveDatabaseConnection(getDatabase());
|
|
111
|
+
var defaultQuery = {
|
|
112
|
+
connection: null
|
|
113
|
+
};
|
|
114
|
+
function registerDefaultDatabasePool(connection) {
|
|
115
|
+
defaultPool.connection = connection;
|
|
116
|
+
defaultQuery.connection = createDatabaseQueryProxy(connection);
|
|
119
117
|
}
|
|
120
|
-
function
|
|
121
|
-
if (
|
|
122
|
-
|
|
118
|
+
function getDefaultDatabaseQuery() {
|
|
119
|
+
if (!defaultQuery.connection) {
|
|
120
|
+
throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
123
121
|
}
|
|
124
|
-
return
|
|
122
|
+
return defaultQuery.connection;
|
|
125
123
|
}
|
|
126
|
-
|
|
124
|
+
|
|
125
|
+
// ../../src/core/database/repositoryConnection.ts
|
|
126
|
+
function resolveRepositoryConnection() {
|
|
127
|
+
return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
|
|
128
|
+
}
|
|
129
|
+
var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
|
|
127
130
|
apply(_target, _thisArg, args) {
|
|
128
|
-
return
|
|
131
|
+
return resolveRepositoryConnection()(...args);
|
|
129
132
|
},
|
|
130
133
|
get(_target, property) {
|
|
131
|
-
const connection =
|
|
134
|
+
const connection = resolveRepositoryConnection();
|
|
132
135
|
const value = connection[property];
|
|
133
136
|
return typeof value === "function" ? value.bind(connection) : value;
|
|
134
137
|
}
|
|
135
138
|
});
|
|
136
|
-
var connection_default = db;
|
|
137
139
|
|
|
138
|
-
// ../../src/core/security/
|
|
139
|
-
|
|
140
|
-
async function safeFetch(input, init = {}, options = {}) {
|
|
141
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
142
|
-
const maxRedirects = options.maxRedirects ?? 0;
|
|
143
|
-
const controller = new AbortController;
|
|
144
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
145
|
-
try {
|
|
146
|
-
let currentUrl = input;
|
|
147
|
-
let redirectCount = 0;
|
|
148
|
-
while (true) {
|
|
149
|
-
const response = await fetch(currentUrl, {
|
|
150
|
-
...init,
|
|
151
|
-
signal: controller.signal,
|
|
152
|
-
redirect: "manual"
|
|
153
|
-
});
|
|
154
|
-
if (response.status >= 300 && response.status < 400) {
|
|
155
|
-
const location = response.headers.get("location");
|
|
156
|
-
if (!location || redirectCount >= maxRedirects) {
|
|
157
|
-
return response;
|
|
158
|
-
}
|
|
159
|
-
currentUrl = new URL(location, currentUrl).toString();
|
|
160
|
-
redirectCount += 1;
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
return response;
|
|
164
|
-
}
|
|
165
|
-
} finally {
|
|
166
|
-
clearTimeout(timeout);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
140
|
+
// ../../src/core/security/safeUrl.ts
|
|
141
|
+
import { lookup as dnsLookupImpl } from "dns/promises";
|
|
169
142
|
|
|
170
143
|
// ../../src/core/errors/http.ts
|
|
171
144
|
class HttpError extends Error {
|
|
@@ -207,13 +180,8 @@ class UnauthorizedError extends HttpError {
|
|
|
207
180
|
}
|
|
208
181
|
}
|
|
209
182
|
|
|
210
|
-
class PayloadTooLargeError extends HttpError {
|
|
211
|
-
constructor(message = "Payload Too Large", details) {
|
|
212
|
-
super(413, message, details);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
183
|
// ../../src/core/security/safeUrl.ts
|
|
184
|
+
var dnsLookup = dnsLookupImpl;
|
|
217
185
|
var BLOCKED_HOSTNAMES = new Set([
|
|
218
186
|
"localhost",
|
|
219
187
|
"127.0.0.1",
|
|
@@ -285,14 +253,63 @@ function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
|
285
253
|
}
|
|
286
254
|
return parsed;
|
|
287
255
|
}
|
|
256
|
+
function isBlockedIpAddress(address) {
|
|
257
|
+
return isBlockedHostname(address.trim().toLowerCase());
|
|
258
|
+
}
|
|
259
|
+
async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
|
|
260
|
+
const parsed = assertSafeOutboundUrl(rawUrl, options);
|
|
261
|
+
if (options.resolveDns === false) {
|
|
262
|
+
return parsed;
|
|
263
|
+
}
|
|
264
|
+
const hostname = parsed.hostname.trim().toLowerCase();
|
|
265
|
+
const results = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
266
|
+
if (results.some((result) => isBlockedIpAddress(result.address))) {
|
|
267
|
+
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
268
|
+
}
|
|
269
|
+
return parsed;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ../../src/core/security/safeFetch.ts
|
|
273
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
274
|
+
async function safeFetch(input, init = {}, options = {}) {
|
|
275
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
276
|
+
const maxRedirects = options.maxRedirects ?? 0;
|
|
277
|
+
const resolveDns = options.resolveDns ?? appConfig.env === "production";
|
|
278
|
+
const urlOptions = { allowHttp: options.allowHttp, resolveDns };
|
|
279
|
+
const controller = new AbortController;
|
|
280
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
281
|
+
try {
|
|
282
|
+
let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
|
|
283
|
+
let redirectCount = 0;
|
|
284
|
+
while (true) {
|
|
285
|
+
const response = await fetch(currentUrl, {
|
|
286
|
+
...init,
|
|
287
|
+
signal: controller.signal,
|
|
288
|
+
redirect: "manual"
|
|
289
|
+
});
|
|
290
|
+
if (response.status >= 300 && response.status < 400) {
|
|
291
|
+
const location = response.headers.get("location");
|
|
292
|
+
if (!location || redirectCount >= maxRedirects) {
|
|
293
|
+
return response;
|
|
294
|
+
}
|
|
295
|
+
currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
|
|
296
|
+
redirectCount += 1;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
return response;
|
|
300
|
+
}
|
|
301
|
+
} finally {
|
|
302
|
+
clearTimeout(timeout);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
288
305
|
|
|
289
306
|
// ../../src/core/tenant/databaseTenantContext.ts
|
|
290
307
|
async function runWithMigrationBypass(callback) {
|
|
291
|
-
await
|
|
308
|
+
await repositoryConnection`SELECT set_config('app.bypass_rls', 'true', false)`;
|
|
292
309
|
try {
|
|
293
310
|
return await callback();
|
|
294
311
|
} finally {
|
|
295
|
-
await
|
|
312
|
+
await repositoryConnection`SELECT set_config('app.bypass_rls', 'false', false)`;
|
|
296
313
|
}
|
|
297
314
|
}
|
|
298
315
|
|
|
@@ -349,7 +366,7 @@ async function exportPendingAuditLogs() {
|
|
|
349
366
|
return 0;
|
|
350
367
|
}
|
|
351
368
|
return await runWithMigrationBypass(async () => {
|
|
352
|
-
const rows = await
|
|
369
|
+
const rows = await repositoryConnection`
|
|
353
370
|
SELECT
|
|
354
371
|
id,
|
|
355
372
|
user_id,
|
|
@@ -393,13 +410,13 @@ async function exportPendingAuditLogs() {
|
|
|
393
410
|
...process.env.SIEM_EXPORT_TOKEN ? { authorization: `Bearer ${process.env.SIEM_EXPORT_TOKEN}` } : {}
|
|
394
411
|
},
|
|
395
412
|
body
|
|
396
|
-
});
|
|
413
|
+
}, { allowHttp: appConfig.env !== "production" });
|
|
397
414
|
if (!response.ok) {
|
|
398
415
|
throw new Error(`SIEM export failed with status ${response.status}.`);
|
|
399
416
|
}
|
|
400
417
|
const ids = rows.map((row) => row.id);
|
|
401
418
|
for (const id of ids) {
|
|
402
|
-
await
|
|
419
|
+
await repositoryConnection`UPDATE audit_log SET exported_at = NOW() WHERE id = ${id}`;
|
|
403
420
|
}
|
|
404
421
|
return rows.length;
|
|
405
422
|
});
|
|
@@ -559,7 +576,8 @@ class ConfigStore {
|
|
|
559
576
|
}
|
|
560
577
|
var requiredDependencyKeys = [
|
|
561
578
|
"container",
|
|
562
|
-
"cache"
|
|
579
|
+
"cache",
|
|
580
|
+
"storage"
|
|
563
581
|
];
|
|
564
582
|
function getRequiredDependency(dependencies, key) {
|
|
565
583
|
const dependency = dependencies[key];
|
|
@@ -652,6 +670,60 @@ import { SamlProvider } from "@getstrata/core/auth/oauth/samlProvider";
|
|
|
652
670
|
// ../../src/modules/user/apiTokenRepository.ts
|
|
653
671
|
import { BaseRepository } from "@getstrata/core/database";
|
|
654
672
|
|
|
673
|
+
// ../../src/config/database.ts
|
|
674
|
+
function readInteger(name, fallback) {
|
|
675
|
+
const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
|
|
676
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
677
|
+
}
|
|
678
|
+
var databaseConfig = {
|
|
679
|
+
url: process.env.DATABASE_URL ?? "",
|
|
680
|
+
poolMax: readInteger("DB_POOL_MAX", 10),
|
|
681
|
+
idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
|
|
682
|
+
maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
|
|
683
|
+
connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
// ../../src/db/connection/createConnection.ts
|
|
687
|
+
var {SQL } = globalThis.Bun;
|
|
688
|
+
function createDatabaseConnection(config) {
|
|
689
|
+
if (!config.url) {
|
|
690
|
+
throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
|
|
691
|
+
}
|
|
692
|
+
return new SQL({
|
|
693
|
+
url: config.url,
|
|
694
|
+
max: config.poolMax,
|
|
695
|
+
idleTimeout: config.idleTimeoutSeconds,
|
|
696
|
+
maxLifetime: config.maxLifetimeSeconds,
|
|
697
|
+
connectionTimeout: config.connectionTimeoutSeconds
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// ../../src/db/connection/index.ts
|
|
702
|
+
var connectionHolder = {
|
|
703
|
+
connection: null
|
|
704
|
+
};
|
|
705
|
+
function getDatabase() {
|
|
706
|
+
if (!connectionHolder.connection) {
|
|
707
|
+
connectionHolder.connection = createDatabaseConnection(databaseConfig);
|
|
708
|
+
registerDefaultDatabasePool(connectionHolder.connection);
|
|
709
|
+
}
|
|
710
|
+
return connectionHolder.connection;
|
|
711
|
+
}
|
|
712
|
+
function getDb() {
|
|
713
|
+
getDatabase();
|
|
714
|
+
return getDefaultDatabaseQuery();
|
|
715
|
+
}
|
|
716
|
+
var db = new Proxy(function database() {}, {
|
|
717
|
+
apply(_target, _thisArg, args) {
|
|
718
|
+
return getDb()(...args);
|
|
719
|
+
},
|
|
720
|
+
get(_target, property) {
|
|
721
|
+
const connection = getDb();
|
|
722
|
+
const value = connection[property];
|
|
723
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
|
|
655
727
|
// ../../src/modules/user/apiTokenTable.ts
|
|
656
728
|
import { defineTable } from "@getstrata/core/database";
|
|
657
729
|
var apiTokenTable = defineTable({
|
|
@@ -836,9 +908,6 @@ var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
|
|
|
836
908
|
// ../../src/core/auth/authContext.ts
|
|
837
909
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
838
910
|
var authContext = new AsyncLocalStorage2;
|
|
839
|
-
function runWithAuthUser(user, callback) {
|
|
840
|
-
return authContext.run(user, callback);
|
|
841
|
-
}
|
|
842
911
|
function currentAuthUser() {
|
|
843
912
|
return authContext.getStore() ?? null;
|
|
844
913
|
}
|
|
@@ -1753,13 +1822,10 @@ function resolveApplicationQueue() {
|
|
|
1753
1822
|
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
1754
1823
|
import { createHmac as createHmac2 } from "crypto";
|
|
1755
1824
|
class DispatchWebhookJob extends Job {
|
|
1756
|
-
constructor() {
|
|
1757
|
-
super();
|
|
1758
|
-
}
|
|
1759
1825
|
maxAttempts = 3;
|
|
1760
1826
|
backoffMs = 2000;
|
|
1761
1827
|
async handle(payload) {
|
|
1762
|
-
const rows = await
|
|
1828
|
+
const rows = await repositoryConnection`
|
|
1763
1829
|
SELECT id, url, secret
|
|
1764
1830
|
FROM webhook
|
|
1765
1831
|
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
@@ -1782,14 +1848,14 @@ class DispatchWebhookJob extends Job {
|
|
|
1782
1848
|
"x-workhub-signature": signature
|
|
1783
1849
|
},
|
|
1784
1850
|
body
|
|
1785
|
-
});
|
|
1851
|
+
}, { allowHttp: appConfig.env !== "production" });
|
|
1786
1852
|
responseStatus = response.status;
|
|
1787
1853
|
if (!response.ok) {
|
|
1788
1854
|
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
1789
1855
|
}
|
|
1790
1856
|
} catch (error) {
|
|
1791
1857
|
errorMessage = error instanceof Error ? error.message : String(error);
|
|
1792
|
-
await
|
|
1858
|
+
await repositoryConnection`
|
|
1793
1859
|
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
1794
1860
|
VALUES (
|
|
1795
1861
|
${webhook.id},
|
|
@@ -1801,7 +1867,7 @@ class DispatchWebhookJob extends Job {
|
|
|
1801
1867
|
`;
|
|
1802
1868
|
throw error instanceof Error ? error : new Error(errorMessage);
|
|
1803
1869
|
}
|
|
1804
|
-
await
|
|
1870
|
+
await repositoryConnection`
|
|
1805
1871
|
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
1806
1872
|
VALUES (
|
|
1807
1873
|
${webhook.id},
|
|
@@ -2381,26 +2447,6 @@ function indexMorphToRelation(children, parentsByType, relation) {
|
|
|
2381
2447
|
return result;
|
|
2382
2448
|
}
|
|
2383
2449
|
|
|
2384
|
-
// ../../src/core/database/boundConnection.ts
|
|
2385
|
-
var boundConnectionHolder = {
|
|
2386
|
-
connection: null
|
|
2387
|
-
};
|
|
2388
|
-
function getBoundDatabaseConnection() {
|
|
2389
|
-
return boundConnectionHolder.connection;
|
|
2390
|
-
}
|
|
2391
|
-
|
|
2392
|
-
// ../../src/core/database/repositoryConnection.ts
|
|
2393
|
-
function resolveRepositoryConnection() {
|
|
2394
|
-
return getBoundDatabaseConnection() ?? connection_default;
|
|
2395
|
-
}
|
|
2396
|
-
var repositoryConnection = new Proxy({}, {
|
|
2397
|
-
get(_target, property) {
|
|
2398
|
-
const connection = resolveRepositoryConnection();
|
|
2399
|
-
const value = connection[property];
|
|
2400
|
-
return typeof value === "function" ? value.bind(connection) : value;
|
|
2401
|
-
}
|
|
2402
|
-
});
|
|
2403
|
-
|
|
2404
2450
|
// ../../src/core/database/whereBuilder.ts
|
|
2405
2451
|
class WhereBuilder {
|
|
2406
2452
|
nodes = [];
|
|
@@ -3729,8 +3775,14 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus) {
|
|
|
3729
3775
|
if (tags.length === 0) {
|
|
3730
3776
|
return;
|
|
3731
3777
|
}
|
|
3732
|
-
|
|
3733
|
-
|
|
3778
|
+
let cache;
|
|
3779
|
+
let queue;
|
|
3780
|
+
try {
|
|
3781
|
+
cache = resolveApplicationCache();
|
|
3782
|
+
queue = resolveApplicationQueue();
|
|
3783
|
+
} catch {
|
|
3784
|
+
return;
|
|
3785
|
+
}
|
|
3734
3786
|
const job = createTrackedJob("cache.invalidate-tags", new invalidateCacheTagsJob_default(cache));
|
|
3735
3787
|
await queue.dispatch(job, { tags });
|
|
3736
3788
|
});
|
|
@@ -3822,6 +3874,128 @@ var queueProvider = {
|
|
|
3822
3874
|
};
|
|
3823
3875
|
var queue_default = queueProvider;
|
|
3824
3876
|
|
|
3877
|
+
// ../../src/core/storage/storage.ts
|
|
3878
|
+
import { mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
3879
|
+
import { dirname, join as join3 } from "path";
|
|
3880
|
+
var {S3Client } = globalThis.Bun;
|
|
3881
|
+
|
|
3882
|
+
class LocalStorageDriver {
|
|
3883
|
+
rootDirectory;
|
|
3884
|
+
constructor(rootDirectory) {
|
|
3885
|
+
this.rootDirectory = rootDirectory;
|
|
3886
|
+
}
|
|
3887
|
+
resolveRootDirectory() {
|
|
3888
|
+
return this.rootDirectory ?? process.env.STORAGE_PATH ?? "storage";
|
|
3889
|
+
}
|
|
3890
|
+
resolvePath(path) {
|
|
3891
|
+
return join3(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
|
|
3892
|
+
}
|
|
3893
|
+
async put(path, contents) {
|
|
3894
|
+
const absolutePath = this.resolvePath(path);
|
|
3895
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
3896
|
+
await writeFile(absolutePath, contents);
|
|
3897
|
+
return path;
|
|
3898
|
+
}
|
|
3899
|
+
async get(path) {
|
|
3900
|
+
try {
|
|
3901
|
+
return await readFile(this.resolvePath(path));
|
|
3902
|
+
} catch {
|
|
3903
|
+
return null;
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
async delete(path) {
|
|
3907
|
+
try {
|
|
3908
|
+
await unlink(this.resolvePath(path));
|
|
3909
|
+
return true;
|
|
3910
|
+
} catch {
|
|
3911
|
+
return false;
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3916
|
+
class S3StorageDriver {
|
|
3917
|
+
client;
|
|
3918
|
+
constructor(client) {
|
|
3919
|
+
this.client = client;
|
|
3920
|
+
}
|
|
3921
|
+
async put(path, contents) {
|
|
3922
|
+
await this.client.write(path.replace(/^\/+/, ""), contents);
|
|
3923
|
+
return path;
|
|
3924
|
+
}
|
|
3925
|
+
async get(path) {
|
|
3926
|
+
const normalizedPath = path.replace(/^\/+/, "");
|
|
3927
|
+
const file = this.client.file(normalizedPath);
|
|
3928
|
+
if (!await file.exists()) {
|
|
3929
|
+
return null;
|
|
3930
|
+
}
|
|
3931
|
+
return new Uint8Array(await file.arrayBuffer());
|
|
3932
|
+
}
|
|
3933
|
+
async delete(path) {
|
|
3934
|
+
try {
|
|
3935
|
+
await this.client.unlink(path.replace(/^\/+/, ""));
|
|
3936
|
+
return true;
|
|
3937
|
+
} catch {
|
|
3938
|
+
return false;
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
class StorageManager {
|
|
3944
|
+
driver;
|
|
3945
|
+
constructor(driver) {
|
|
3946
|
+
this.driver = driver;
|
|
3947
|
+
}
|
|
3948
|
+
put(path, contents) {
|
|
3949
|
+
return this.driver.put(path, contents);
|
|
3950
|
+
}
|
|
3951
|
+
get(path) {
|
|
3952
|
+
return this.driver.get(path);
|
|
3953
|
+
}
|
|
3954
|
+
delete(path) {
|
|
3955
|
+
return this.driver.delete(path);
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
function resolveS3Config() {
|
|
3959
|
+
const accessKeyId = process.env.AWS_ACCESS_KEY_ID?.trim();
|
|
3960
|
+
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY?.trim();
|
|
3961
|
+
const bucket = process.env.AWS_BUCKET?.trim();
|
|
3962
|
+
if (!accessKeyId || !secretAccessKey || !bucket) {
|
|
3963
|
+
throw new Error('STORAGE_DRIVER="s3" requires AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_BUCKET.');
|
|
3964
|
+
}
|
|
3965
|
+
return {
|
|
3966
|
+
accessKeyId,
|
|
3967
|
+
secretAccessKey,
|
|
3968
|
+
bucket,
|
|
3969
|
+
...process.env.AWS_REGION?.trim() ? { region: process.env.AWS_REGION.trim() } : {},
|
|
3970
|
+
...process.env.AWS_ENDPOINT?.trim() ? { endpoint: process.env.AWS_ENDPOINT.trim() } : {}
|
|
3971
|
+
};
|
|
3972
|
+
}
|
|
3973
|
+
function createS3Client(config = resolveS3Config()) {
|
|
3974
|
+
return new S3Client({
|
|
3975
|
+
accessKeyId: config.accessKeyId,
|
|
3976
|
+
secretAccessKey: config.secretAccessKey,
|
|
3977
|
+
bucket: config.bucket,
|
|
3978
|
+
...config.region ? { region: config.region } : {},
|
|
3979
|
+
...config.endpoint ? { endpoint: config.endpoint } : {}
|
|
3980
|
+
});
|
|
3981
|
+
}
|
|
3982
|
+
function createStorageDriver() {
|
|
3983
|
+
const driver = process.env.STORAGE_DRIVER ?? "local";
|
|
3984
|
+
if (driver === "s3") {
|
|
3985
|
+
return new S3StorageDriver(createS3Client());
|
|
3986
|
+
}
|
|
3987
|
+
return new LocalStorageDriver;
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3990
|
+
// ../../src/bootstrap/providers/storage.ts
|
|
3991
|
+
var storageProvider = {
|
|
3992
|
+
name: "core.storage",
|
|
3993
|
+
register({ dependencies }) {
|
|
3994
|
+
dependencies.storage = new StorageManager(createStorageDriver());
|
|
3995
|
+
}
|
|
3996
|
+
};
|
|
3997
|
+
var storage_default = storageProvider;
|
|
3998
|
+
|
|
3825
3999
|
// ../../src/config/frontend.ts
|
|
3826
4000
|
function readFrontendMode() {
|
|
3827
4001
|
const mode = (process.env.FRONTEND_MODE ?? "api").trim();
|
|
@@ -3840,9 +4014,6 @@ function isViewsEnabled() {
|
|
|
3840
4014
|
// ../../src/core/http/requestMetaContext.ts
|
|
3841
4015
|
import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
|
|
3842
4016
|
var requestMetaContext = new AsyncLocalStorage3;
|
|
3843
|
-
function runWithRequestMeta(meta, callback) {
|
|
3844
|
-
return requestMetaContext.run(meta, callback);
|
|
3845
|
-
}
|
|
3846
4017
|
function currentRequestMeta() {
|
|
3847
4018
|
return requestMetaContext.getStore() ?? {
|
|
3848
4019
|
ipAddress: null,
|
|
@@ -3851,9 +4022,9 @@ function currentRequestMeta() {
|
|
|
3851
4022
|
}
|
|
3852
4023
|
|
|
3853
4024
|
// ../../src/core/view/etaViewEngine.ts
|
|
3854
|
-
import { join as
|
|
4025
|
+
import { join as join4 } from "path";
|
|
3855
4026
|
import { Eta } from "eta";
|
|
3856
|
-
var DEFAULT_VIEWS_DIRECTORY =
|
|
4027
|
+
var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
|
|
3857
4028
|
var DEFAULT_LAYOUT = "layouts/app.eta";
|
|
3858
4029
|
|
|
3859
4030
|
class EtaViewEngine {
|
|
@@ -3892,9 +4063,6 @@ function htmlResponse(html, init = {}) {
|
|
|
3892
4063
|
}
|
|
3893
4064
|
});
|
|
3894
4065
|
}
|
|
3895
|
-
// ../../src/core/http/csrfToken.ts
|
|
3896
|
-
import { timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
3897
|
-
|
|
3898
4066
|
// ../../src/core/http/cookies.ts
|
|
3899
4067
|
function readRequestCookie(request, name) {
|
|
3900
4068
|
const cookies = request.cookies;
|
|
@@ -3929,14 +4097,6 @@ function resolveCsrfSecret() {
|
|
|
3929
4097
|
function csrfVerifyOptions() {
|
|
3930
4098
|
return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
|
|
3931
4099
|
}
|
|
3932
|
-
function tokensMatch(left, right) {
|
|
3933
|
-
const leftBuffer = Buffer.from(left);
|
|
3934
|
-
const rightBuffer = Buffer.from(right);
|
|
3935
|
-
if (leftBuffer.length !== rightBuffer.length) {
|
|
3936
|
-
return false;
|
|
3937
|
-
}
|
|
3938
|
-
return timingSafeEqual2(leftBuffer, rightBuffer);
|
|
3939
|
-
}
|
|
3940
4100
|
function createCsrfTokenCookie() {
|
|
3941
4101
|
const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
|
|
3942
4102
|
return {
|
|
@@ -3951,45 +4111,6 @@ function resolveCsrfToken(request) {
|
|
|
3951
4111
|
}
|
|
3952
4112
|
return createCsrfTokenCookie();
|
|
3953
4113
|
}
|
|
3954
|
-
function readSubmittedCsrfToken(request) {
|
|
3955
|
-
const headerToken = request.headers.get("x-csrf-token")?.trim();
|
|
3956
|
-
if (headerToken) {
|
|
3957
|
-
return headerToken;
|
|
3958
|
-
}
|
|
3959
|
-
return null;
|
|
3960
|
-
}
|
|
3961
|
-
async function readSubmittedCsrfTokenFromBody(request) {
|
|
3962
|
-
const headerToken = readSubmittedCsrfToken(request);
|
|
3963
|
-
if (headerToken) {
|
|
3964
|
-
return headerToken;
|
|
3965
|
-
}
|
|
3966
|
-
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
3967
|
-
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
3968
|
-
const formData = await request.clone().formData();
|
|
3969
|
-
const field = formData.get("_token");
|
|
3970
|
-
if (typeof field === "string" && field.trim().length > 0) {
|
|
3971
|
-
return field.trim();
|
|
3972
|
-
}
|
|
3973
|
-
const legacyField = formData.get("_csrf");
|
|
3974
|
-
if (typeof legacyField === "string" && legacyField.trim().length > 0) {
|
|
3975
|
-
return legacyField.trim();
|
|
3976
|
-
}
|
|
3977
|
-
}
|
|
3978
|
-
return null;
|
|
3979
|
-
}
|
|
3980
|
-
function verifyCsrfToken(request, submittedToken) {
|
|
3981
|
-
if (!submittedToken) {
|
|
3982
|
-
return false;
|
|
3983
|
-
}
|
|
3984
|
-
const cookieValue = readRequestCookie(request, CSRF_COOKIE);
|
|
3985
|
-
if (!cookieValue) {
|
|
3986
|
-
return false;
|
|
3987
|
-
}
|
|
3988
|
-
if (!tokensMatch(submittedToken, cookieValue)) {
|
|
3989
|
-
return false;
|
|
3990
|
-
}
|
|
3991
|
-
return Bun.CSRF.verify(submittedToken, csrfVerifyOptions());
|
|
3992
|
-
}
|
|
3993
4114
|
function resolveCsrfTokenForRequest(request) {
|
|
3994
4115
|
const metaToken = currentRequestMeta().csrfToken;
|
|
3995
4116
|
if (metaToken) {
|
|
@@ -3999,7 +4120,7 @@ function resolveCsrfTokenForRequest(request) {
|
|
|
3999
4120
|
}
|
|
4000
4121
|
|
|
4001
4122
|
// ../../src/core/http/flashSession.ts
|
|
4002
|
-
import { createHmac as createHmac3, timingSafeEqual as
|
|
4123
|
+
import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
4003
4124
|
var FLASH_COOKIE = "workhub_flash";
|
|
4004
4125
|
var FLASH_TTL_MS = 60 * 1000;
|
|
4005
4126
|
function resolveFlashSecret() {
|
|
@@ -4046,7 +4167,7 @@ function parseFlashCookie(cookieValue) {
|
|
|
4046
4167
|
if (expectedBuffer.length !== actualBuffer.length) {
|
|
4047
4168
|
return null;
|
|
4048
4169
|
}
|
|
4049
|
-
if (!
|
|
4170
|
+
if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
|
|
4050
4171
|
return null;
|
|
4051
4172
|
}
|
|
4052
4173
|
try {
|
|
@@ -4062,9 +4183,6 @@ function parseFlashCookie(cookieValue) {
|
|
|
4062
4183
|
return null;
|
|
4063
4184
|
}
|
|
4064
4185
|
}
|
|
4065
|
-
function clearFlashCookie() {
|
|
4066
|
-
return `${FLASH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
|
|
4067
|
-
}
|
|
4068
4186
|
function pullFlash(request) {
|
|
4069
4187
|
const cookieValue = readFlashCookie(request);
|
|
4070
4188
|
if (!cookieValue) {
|
|
@@ -4072,15 +4190,6 @@ function pullFlash(request) {
|
|
|
4072
4190
|
}
|
|
4073
4191
|
return parseFlashCookie(cookieValue);
|
|
4074
4192
|
}
|
|
4075
|
-
function withFlashClear(response) {
|
|
4076
|
-
const headers = new Headers(response.headers);
|
|
4077
|
-
headers.append("set-cookie", clearFlashCookie());
|
|
4078
|
-
return new Response(response.body, {
|
|
4079
|
-
status: response.status,
|
|
4080
|
-
statusText: response.statusText,
|
|
4081
|
-
headers
|
|
4082
|
-
});
|
|
4083
|
-
}
|
|
4084
4193
|
|
|
4085
4194
|
// ../../src/core/view/webLayoutData.ts
|
|
4086
4195
|
async function resolveWebLayoutData(container, request) {
|
|
@@ -4140,6 +4249,7 @@ var viewProvider = {
|
|
|
4140
4249
|
var coreProviders = [
|
|
4141
4250
|
config_default,
|
|
4142
4251
|
cache_default,
|
|
4252
|
+
storage_default,
|
|
4143
4253
|
auth_default,
|
|
4144
4254
|
events_default,
|
|
4145
4255
|
policy_default,
|
|
@@ -4240,7 +4350,7 @@ function createAppContext() {
|
|
|
4240
4350
|
return appContext;
|
|
4241
4351
|
}
|
|
4242
4352
|
// ../../src/bootstrap/createWebRoutes.ts
|
|
4243
|
-
import { join as
|
|
4353
|
+
import { join as join5 } from "path";
|
|
4244
4354
|
|
|
4245
4355
|
// ../../src/core/http/middleware.ts
|
|
4246
4356
|
function isRouteHandler(value) {
|
|
@@ -4272,17 +4382,6 @@ function composeMiddleware(...middleware) {
|
|
|
4272
4382
|
};
|
|
4273
4383
|
};
|
|
4274
4384
|
}
|
|
4275
|
-
async function requestIdMiddleware(request, next) {
|
|
4276
|
-
const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
|
|
4277
|
-
const response = await next();
|
|
4278
|
-
const headers = new Headers(response.headers);
|
|
4279
|
-
headers.set("x-request-id", requestId);
|
|
4280
|
-
return new Response(response.body, {
|
|
4281
|
-
status: response.status,
|
|
4282
|
-
statusText: response.statusText,
|
|
4283
|
-
headers
|
|
4284
|
-
});
|
|
4285
|
-
}
|
|
4286
4385
|
function wrapRouteHandler(handler, middleware) {
|
|
4287
4386
|
if (isMethodRouteMap(handler)) {
|
|
4288
4387
|
const wrapped = {};
|
|
@@ -4304,6 +4403,32 @@ function applyMiddlewareToRoutes(routes, middleware) {
|
|
|
4304
4403
|
return wrapped;
|
|
4305
4404
|
}
|
|
4306
4405
|
|
|
4406
|
+
// ../../src/bootstrap/httpKernel.ts
|
|
4407
|
+
import {
|
|
4408
|
+
createAuthMiddleware,
|
|
4409
|
+
createAuthorizeMiddleware,
|
|
4410
|
+
createBodySizeLimitMiddleware,
|
|
4411
|
+
createCorsMiddleware,
|
|
4412
|
+
createCsrfMiddleware,
|
|
4413
|
+
createFlashMiddleware,
|
|
4414
|
+
createLoginThrottleMiddleware,
|
|
4415
|
+
createMembershipMiddleware,
|
|
4416
|
+
createMemoryThrottleMiddleware,
|
|
4417
|
+
createMetricsMiddleware,
|
|
4418
|
+
createRequestLoggingMiddleware,
|
|
4419
|
+
createRequireAbilityMiddleware,
|
|
4420
|
+
createRequireAuthMiddleware,
|
|
4421
|
+
createRequireGlobalAdminMiddleware,
|
|
4422
|
+
createRequireWebAuthMiddleware,
|
|
4423
|
+
createSecurityHeadersMiddleware,
|
|
4424
|
+
createTenantMiddleware,
|
|
4425
|
+
createThrottleMiddleware,
|
|
4426
|
+
createTracingMiddleware,
|
|
4427
|
+
isPublicReadsEnabled,
|
|
4428
|
+
requestIdMiddleware,
|
|
4429
|
+
withMiddleware
|
|
4430
|
+
} from "@getstrata/core";
|
|
4431
|
+
|
|
4307
4432
|
// ../../src/config/rateLimit.ts
|
|
4308
4433
|
var LOCAL_LOGIN_RATE_LIMIT = {
|
|
4309
4434
|
maxAttempts: 100,
|
|
@@ -4338,849 +4463,6 @@ function resolveRegisterRateLimit() {
|
|
|
4338
4463
|
};
|
|
4339
4464
|
}
|
|
4340
4465
|
|
|
4341
|
-
// ../../src/core/auth/membershipContext.ts
|
|
4342
|
-
import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
|
|
4343
|
-
|
|
4344
|
-
// ../../src/modules/organization/memberRepository.ts
|
|
4345
|
-
class OrganizationMemberRepository {
|
|
4346
|
-
constructor() {}
|
|
4347
|
-
async findMembership(userId, organizationId) {
|
|
4348
|
-
const rows = await connection_default`
|
|
4349
|
-
SELECT id, organization_id, user_id, role, created_at
|
|
4350
|
-
FROM organization_member
|
|
4351
|
-
WHERE user_id = ${userId} AND organization_id = ${organizationId}
|
|
4352
|
-
LIMIT 1
|
|
4353
|
-
`;
|
|
4354
|
-
return rows[0] ?? null;
|
|
4355
|
-
}
|
|
4356
|
-
async listForUser(userId) {
|
|
4357
|
-
return await connection_default`
|
|
4358
|
-
SELECT id, organization_id, user_id, role, created_at
|
|
4359
|
-
FROM organization_member
|
|
4360
|
-
WHERE user_id = ${userId}
|
|
4361
|
-
ORDER BY organization_id
|
|
4362
|
-
`;
|
|
4363
|
-
}
|
|
4364
|
-
async listForOrganization(organizationId) {
|
|
4365
|
-
return await connection_default`
|
|
4366
|
-
SELECT id, organization_id, user_id, role, created_at
|
|
4367
|
-
FROM organization_member
|
|
4368
|
-
WHERE organization_id = ${organizationId}
|
|
4369
|
-
ORDER BY id
|
|
4370
|
-
`;
|
|
4371
|
-
}
|
|
4372
|
-
async addMember(input) {
|
|
4373
|
-
const rows = await connection_default`
|
|
4374
|
-
INSERT INTO organization_member (organization_id, user_id, role)
|
|
4375
|
-
VALUES (${input.organizationId}, ${input.userId}, ${input.role ?? "member"})
|
|
4376
|
-
RETURNING id, organization_id, user_id, role, created_at
|
|
4377
|
-
`;
|
|
4378
|
-
const row = rows[0];
|
|
4379
|
-
if (!row) {
|
|
4380
|
-
throw new Error("Organization member insert did not return a row.");
|
|
4381
|
-
}
|
|
4382
|
-
return row;
|
|
4383
|
-
}
|
|
4384
|
-
async removeMember(organizationId, userId) {
|
|
4385
|
-
const rows = await connection_default`
|
|
4386
|
-
DELETE FROM organization_member
|
|
4387
|
-
WHERE organization_id = ${organizationId} AND user_id = ${userId}
|
|
4388
|
-
RETURNING id
|
|
4389
|
-
`;
|
|
4390
|
-
return rows.length > 0;
|
|
4391
|
-
}
|
|
4392
|
-
}
|
|
4393
|
-
var memberRepository_default = OrganizationMemberRepository;
|
|
4394
|
-
|
|
4395
|
-
// ../../src/core/auth/accessControl.ts
|
|
4396
|
-
function isGlobalAdmin(user) {
|
|
4397
|
-
return user?.role === "admin";
|
|
4398
|
-
}
|
|
4399
|
-
function resolveUserId(user) {
|
|
4400
|
-
const userId = typeof user.id === "number" ? user.id : Number(user.id);
|
|
4401
|
-
if (!Number.isInteger(userId) || userId <= 0) {
|
|
4402
|
-
throw new ForbiddenError("Invalid authenticated user.");
|
|
4403
|
-
}
|
|
4404
|
-
return userId;
|
|
4405
|
-
}
|
|
4406
|
-
|
|
4407
|
-
// ../../src/core/auth/membershipContext.ts
|
|
4408
|
-
var membershipContext = new AsyncLocalStorage4;
|
|
4409
|
-
var membershipRepository = new memberRepository_default;
|
|
4410
|
-
async function runWithMembershipContext(callback) {
|
|
4411
|
-
const user = currentAuthUser();
|
|
4412
|
-
if (!user || isGlobalAdmin(user)) {
|
|
4413
|
-
return await callback();
|
|
4414
|
-
}
|
|
4415
|
-
const memberships = await membershipRepository.listForUser(resolveUserId(user));
|
|
4416
|
-
const context = {
|
|
4417
|
-
organizationIds: memberships.map((membership) => membership.organization_id),
|
|
4418
|
-
rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
|
|
4419
|
-
};
|
|
4420
|
-
return await membershipContext.run(context, callback);
|
|
4421
|
-
}
|
|
4422
|
-
|
|
4423
|
-
// ../../src/core/auth/membershipContextMiddleware.ts
|
|
4424
|
-
function createMembershipContextMiddleware() {
|
|
4425
|
-
return async (_request, next) => {
|
|
4426
|
-
return await runWithMembershipContext(async () => await next());
|
|
4427
|
-
};
|
|
4428
|
-
}
|
|
4429
|
-
|
|
4430
|
-
// ../../src/core/auth/membershipMiddleware.ts
|
|
4431
|
-
function createMembershipMiddleware() {
|
|
4432
|
-
return createMembershipContextMiddleware();
|
|
4433
|
-
}
|
|
4434
|
-
|
|
4435
|
-
// ../../src/core/http/authMiddleware.ts
|
|
4436
|
-
function createAuthMiddleware(auth) {
|
|
4437
|
-
return async (request, next) => {
|
|
4438
|
-
const user = await auth.resolve(request);
|
|
4439
|
-
return await runWithAuthUser(user, async () => {
|
|
4440
|
-
const response = await next();
|
|
4441
|
-
if (user) {
|
|
4442
|
-
const headers = new Headers(response.headers);
|
|
4443
|
-
headers.set("x-authenticated-user-id", String(user.id));
|
|
4444
|
-
return new Response(response.body, {
|
|
4445
|
-
status: response.status,
|
|
4446
|
-
statusText: response.statusText,
|
|
4447
|
-
headers
|
|
4448
|
-
});
|
|
4449
|
-
}
|
|
4450
|
-
return response;
|
|
4451
|
-
});
|
|
4452
|
-
};
|
|
4453
|
-
}
|
|
4454
|
-
|
|
4455
|
-
// ../../src/core/http/authorizeMiddleware.ts
|
|
4456
|
-
function createAuthorizeMiddleware(gate, auth, resource, action) {
|
|
4457
|
-
return async (request, next) => {
|
|
4458
|
-
const user = await auth.resolve(request);
|
|
4459
|
-
if (!gate.allows(resource, action, user)) {
|
|
4460
|
-
const error = new ForbiddenError;
|
|
4461
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
4462
|
-
}
|
|
4463
|
-
return await next();
|
|
4464
|
-
};
|
|
4465
|
-
}
|
|
4466
|
-
|
|
4467
|
-
// ../../src/core/http/bodySizeLimitMiddleware.ts
|
|
4468
|
-
var DEFAULT_MAX_BODY_BYTES = 1048576;
|
|
4469
|
-
function resolveMaxBodyBytes() {
|
|
4470
|
-
const raw = process.env.MAX_REQUEST_BODY_BYTES?.trim();
|
|
4471
|
-
if (!raw) {
|
|
4472
|
-
return DEFAULT_MAX_BODY_BYTES;
|
|
4473
|
-
}
|
|
4474
|
-
const parsed = Number.parseInt(raw, 10);
|
|
4475
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
4476
|
-
return DEFAULT_MAX_BODY_BYTES;
|
|
4477
|
-
}
|
|
4478
|
-
return parsed;
|
|
4479
|
-
}
|
|
4480
|
-
function createBodySizeLimitMiddleware(maxBytes = resolveMaxBodyBytes()) {
|
|
4481
|
-
return async (request, next) => {
|
|
4482
|
-
const contentLength = request.headers.get("content-length");
|
|
4483
|
-
if (contentLength) {
|
|
4484
|
-
const bytes = Number.parseInt(contentLength, 10);
|
|
4485
|
-
if (Number.isInteger(bytes) && bytes > maxBytes) {
|
|
4486
|
-
const error = new PayloadTooLargeError(`Request body exceeds the ${maxBytes} byte limit.`);
|
|
4487
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
4488
|
-
}
|
|
4489
|
-
}
|
|
4490
|
-
return await next();
|
|
4491
|
-
};
|
|
4492
|
-
}
|
|
4493
|
-
|
|
4494
|
-
// ../../src/config/cors.ts
|
|
4495
|
-
var corsConfig = {
|
|
4496
|
-
allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim()).filter(Boolean),
|
|
4497
|
-
allowedMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
|
|
4498
|
-
allowedHeaders: [
|
|
4499
|
-
"Authorization",
|
|
4500
|
-
"Content-Type",
|
|
4501
|
-
"X-Request-Id",
|
|
4502
|
-
"X-Tenant-Id",
|
|
4503
|
-
"X-Authenticated-User-Id",
|
|
4504
|
-
"X-Authenticated-User-Role",
|
|
4505
|
-
"If-Match",
|
|
4506
|
-
"If-None-Match"
|
|
4507
|
-
],
|
|
4508
|
-
maxAgeSeconds: 86400
|
|
4509
|
-
};
|
|
4510
|
-
|
|
4511
|
-
// ../../src/core/http/corsMiddleware.ts
|
|
4512
|
-
function createCorsMiddleware() {
|
|
4513
|
-
return async (request, next) => {
|
|
4514
|
-
if (request.method === "OPTIONS") {
|
|
4515
|
-
return new Response(null, {
|
|
4516
|
-
status: 204,
|
|
4517
|
-
headers: buildCorsHeaders(request)
|
|
4518
|
-
});
|
|
4519
|
-
}
|
|
4520
|
-
const response = await next();
|
|
4521
|
-
const headers = new Headers(response.headers);
|
|
4522
|
-
for (const [key, value] of buildCorsHeaders(request)) {
|
|
4523
|
-
headers.set(key, value);
|
|
4524
|
-
}
|
|
4525
|
-
return new Response(response.body, {
|
|
4526
|
-
status: response.status,
|
|
4527
|
-
statusText: response.statusText,
|
|
4528
|
-
headers
|
|
4529
|
-
});
|
|
4530
|
-
};
|
|
4531
|
-
}
|
|
4532
|
-
function buildCorsHeaders(request) {
|
|
4533
|
-
const headers = new Headers;
|
|
4534
|
-
const origin = request.headers.get("origin");
|
|
4535
|
-
const allowedOrigins = corsConfig.allowedOrigins;
|
|
4536
|
-
const allowOrigin = allowedOrigins.includes("*") || origin && allowedOrigins.includes(origin) ? origin ?? "*" : allowedOrigins[0] ?? "*";
|
|
4537
|
-
headers.set("Access-Control-Allow-Origin", allowOrigin);
|
|
4538
|
-
headers.set("Access-Control-Allow-Methods", corsConfig.allowedMethods.join(", "));
|
|
4539
|
-
headers.set("Access-Control-Allow-Headers", corsConfig.allowedHeaders.join(", "));
|
|
4540
|
-
headers.set("Access-Control-Max-Age", String(corsConfig.maxAgeSeconds));
|
|
4541
|
-
headers.set("Vary", "Origin");
|
|
4542
|
-
return headers;
|
|
4543
|
-
}
|
|
4544
|
-
|
|
4545
|
-
// ../../src/core/http/csrfMiddleware.ts
|
|
4546
|
-
var MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4547
|
-
function appendSetCookie(response, cookie) {
|
|
4548
|
-
const headers = new Headers(response.headers);
|
|
4549
|
-
headers.append("set-cookie", cookie);
|
|
4550
|
-
return new Response(response.body, {
|
|
4551
|
-
status: response.status,
|
|
4552
|
-
statusText: response.statusText,
|
|
4553
|
-
headers
|
|
4554
|
-
});
|
|
4555
|
-
}
|
|
4556
|
-
function createCsrfMiddleware() {
|
|
4557
|
-
return async (request, next) => {
|
|
4558
|
-
const method = request.method.toUpperCase();
|
|
4559
|
-
if (!MUTATING_METHODS.has(method)) {
|
|
4560
|
-
const csrf = resolveCsrfToken(request);
|
|
4561
|
-
const meta = currentRequestMeta();
|
|
4562
|
-
meta.csrfToken = csrf.token;
|
|
4563
|
-
const response = await next();
|
|
4564
|
-
if (!csrf.cookie) {
|
|
4565
|
-
return response;
|
|
4566
|
-
}
|
|
4567
|
-
return appendSetCookie(response, csrf.cookie);
|
|
4568
|
-
}
|
|
4569
|
-
const submitted = await readSubmittedCsrfTokenFromBody(request);
|
|
4570
|
-
if (!verifyCsrfToken(request, submitted)) {
|
|
4571
|
-
throw new ForbiddenError("Invalid or missing CSRF token.");
|
|
4572
|
-
}
|
|
4573
|
-
return await next();
|
|
4574
|
-
};
|
|
4575
|
-
}
|
|
4576
|
-
|
|
4577
|
-
// ../../src/core/http/flashMiddleware.ts
|
|
4578
|
-
function createFlashMiddleware() {
|
|
4579
|
-
return async (request, next) => {
|
|
4580
|
-
const flash = pullFlash(request);
|
|
4581
|
-
const meta = currentRequestMeta();
|
|
4582
|
-
return await runWithRequestMeta({ ...meta, request, flash }, async () => {
|
|
4583
|
-
const response = await next();
|
|
4584
|
-
if (flash) {
|
|
4585
|
-
return withFlashClear(response);
|
|
4586
|
-
}
|
|
4587
|
-
return response;
|
|
4588
|
-
});
|
|
4589
|
-
};
|
|
4590
|
-
}
|
|
4591
|
-
|
|
4592
|
-
// ../../src/core/http/loginThrottleMiddleware.ts
|
|
4593
|
-
var {RedisClient: RedisClient3 } = globalThis.Bun;
|
|
4594
|
-
function resolveLoginIdentity(request) {
|
|
4595
|
-
return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
|
|
4596
|
-
}
|
|
4597
|
-
async function resolveLoginEmail(request) {
|
|
4598
|
-
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
4599
|
-
try {
|
|
4600
|
-
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
4601
|
-
const formData = await request.clone().formData();
|
|
4602
|
-
const email = formData.get("email");
|
|
4603
|
-
return typeof email === "string" ? email.trim().toLowerCase() : "unknown";
|
|
4604
|
-
}
|
|
4605
|
-
const payload = await request.clone().json();
|
|
4606
|
-
return typeof payload.email === "string" ? payload.email.trim().toLowerCase() : "unknown";
|
|
4607
|
-
} catch {
|
|
4608
|
-
return "unknown";
|
|
4609
|
-
}
|
|
4610
|
-
}
|
|
4611
|
-
function createLoginThrottleMiddleware(options) {
|
|
4612
|
-
const client = new RedisClient3(options.redisUrl);
|
|
4613
|
-
const prefix = options.keyPrefix ?? "workhub:login-throttle:";
|
|
4614
|
-
return async (request, next) => {
|
|
4615
|
-
const identity = resolveLoginIdentity(request);
|
|
4616
|
-
const email = await resolveLoginEmail(request);
|
|
4617
|
-
const throttleKey = `${prefix}${identity}:${email}`;
|
|
4618
|
-
const attempts = Number(await client.incr(throttleKey));
|
|
4619
|
-
if (attempts === 1) {
|
|
4620
|
-
await client.expire(throttleKey, options.decaySeconds);
|
|
4621
|
-
}
|
|
4622
|
-
if (attempts > options.maxAttempts) {
|
|
4623
|
-
return Response.json({ error: "Too many login attempts. Try again later." }, {
|
|
4624
|
-
status: 429,
|
|
4625
|
-
headers: {
|
|
4626
|
-
"retry-after": String(options.decaySeconds)
|
|
4627
|
-
}
|
|
4628
|
-
});
|
|
4629
|
-
}
|
|
4630
|
-
return await next();
|
|
4631
|
-
};
|
|
4632
|
-
}
|
|
4633
|
-
|
|
4634
|
-
// ../../src/core/http/memoryThrottleMiddleware.ts
|
|
4635
|
-
var buckets = new Map;
|
|
4636
|
-
function createMemoryThrottleMiddleware(options) {
|
|
4637
|
-
const prefix = options.keyPrefix ?? "workhub:memory-throttle:";
|
|
4638
|
-
return async (request, next) => {
|
|
4639
|
-
const path = new URL(request.url).pathname;
|
|
4640
|
-
const identity = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("authorization")?.slice(0, 32) ?? "unknown";
|
|
4641
|
-
const key = `${prefix}${identity}:${path}`;
|
|
4642
|
-
const now = Date.now();
|
|
4643
|
-
const existing = buckets.get(key);
|
|
4644
|
-
if (!existing || existing.resetAt <= now) {
|
|
4645
|
-
buckets.set(key, { count: 1, resetAt: now + options.decaySeconds * 1000 });
|
|
4646
|
-
return await next();
|
|
4647
|
-
}
|
|
4648
|
-
existing.count += 1;
|
|
4649
|
-
if (existing.count > options.maxAttempts) {
|
|
4650
|
-
return Response.json({ error: "Too many requests." }, {
|
|
4651
|
-
status: 429,
|
|
4652
|
-
headers: {
|
|
4653
|
-
"retry-after": String(options.decaySeconds)
|
|
4654
|
-
}
|
|
4655
|
-
});
|
|
4656
|
-
}
|
|
4657
|
-
return await next();
|
|
4658
|
-
};
|
|
4659
|
-
}
|
|
4660
|
-
|
|
4661
|
-
// ../../src/core/metrics/prometheus.ts
|
|
4662
|
-
class PrometheusRegistry {
|
|
4663
|
-
httpRequestsTotal = new Map;
|
|
4664
|
-
httpRequestDurationMs = new Map;
|
|
4665
|
-
incrementHttpRequest(labels) {
|
|
4666
|
-
const key = this.metricKey(labels);
|
|
4667
|
-
this.httpRequestsTotal.set(key, (this.httpRequestsTotal.get(key) ?? 0) + 1);
|
|
4668
|
-
}
|
|
4669
|
-
observeHttpDuration(labels, durationMs) {
|
|
4670
|
-
const key = this.metricKey(labels);
|
|
4671
|
-
const samples = this.httpRequestDurationMs.get(key) ?? [];
|
|
4672
|
-
samples.push(durationMs);
|
|
4673
|
-
this.httpRequestDurationMs.set(key, samples);
|
|
4674
|
-
}
|
|
4675
|
-
renderMetrics() {
|
|
4676
|
-
const lines = [
|
|
4677
|
-
"# HELP http_requests_total Total HTTP requests processed.",
|
|
4678
|
-
"# TYPE http_requests_total counter"
|
|
4679
|
-
];
|
|
4680
|
-
for (const [key, value] of this.httpRequestsTotal) {
|
|
4681
|
-
lines.push(`http_requests_total{${key}} ${value}`);
|
|
4682
|
-
}
|
|
4683
|
-
lines.push("# HELP http_request_duration_ms_sum Sum of HTTP request durations in milliseconds.", "# TYPE http_request_duration_ms_sum counter");
|
|
4684
|
-
for (const [key, samples] of this.httpRequestDurationMs) {
|
|
4685
|
-
const sum = samples.reduce((total, sample) => total + sample, 0);
|
|
4686
|
-
lines.push(`http_request_duration_ms_sum{${key}} ${sum}`);
|
|
4687
|
-
}
|
|
4688
|
-
return `${lines.join(`
|
|
4689
|
-
`)}
|
|
4690
|
-
`;
|
|
4691
|
-
}
|
|
4692
|
-
resetForTests() {
|
|
4693
|
-
this.httpRequestsTotal.clear();
|
|
4694
|
-
this.httpRequestDurationMs.clear();
|
|
4695
|
-
}
|
|
4696
|
-
getHttpRequestSummary() {
|
|
4697
|
-
const byStatus = {};
|
|
4698
|
-
const pathCounts = new Map;
|
|
4699
|
-
let totalRequests = 0;
|
|
4700
|
-
for (const [key, count] of this.httpRequestsTotal) {
|
|
4701
|
-
totalRequests += count;
|
|
4702
|
-
const method = key.match(/method="([^"]+)"/)?.[1] ?? "GET";
|
|
4703
|
-
const path = key.match(/path="([^"]+)"/)?.[1] ?? "/";
|
|
4704
|
-
const status = key.match(/status="([^"]+)"/)?.[1] ?? "200";
|
|
4705
|
-
byStatus[status] = (byStatus[status] ?? 0) + count;
|
|
4706
|
-
const pathKey = `${method} ${path}`;
|
|
4707
|
-
const existing = pathCounts.get(pathKey);
|
|
4708
|
-
if (existing) {
|
|
4709
|
-
existing.count += count;
|
|
4710
|
-
} else {
|
|
4711
|
-
pathCounts.set(pathKey, { method, path, count });
|
|
4712
|
-
}
|
|
4713
|
-
}
|
|
4714
|
-
const topPaths = Array.from(pathCounts.values()).sort((left, right) => right.count - left.count).slice(0, 10);
|
|
4715
|
-
return {
|
|
4716
|
-
totalRequests,
|
|
4717
|
-
byStatus,
|
|
4718
|
-
topPaths
|
|
4719
|
-
};
|
|
4720
|
-
}
|
|
4721
|
-
metricKey(labels) {
|
|
4722
|
-
return `method="${labels.method}",path="${labels.path}",status="${labels.status}"`;
|
|
4723
|
-
}
|
|
4724
|
-
}
|
|
4725
|
-
var prometheusRegistry = new PrometheusRegistry;
|
|
4726
|
-
|
|
4727
|
-
// ../../src/core/http/metricsMiddleware.ts
|
|
4728
|
-
function normalizeMetricPath(pathname) {
|
|
4729
|
-
return pathname.replace(/\/\d+/g, "/:id").replace(/\/[0-9a-f-]{36}/gi, "/:id");
|
|
4730
|
-
}
|
|
4731
|
-
function createMetricsMiddleware() {
|
|
4732
|
-
return async (request, next) => {
|
|
4733
|
-
const startedAt = performance.now();
|
|
4734
|
-
const response = await next();
|
|
4735
|
-
const durationMs = performance.now() - startedAt;
|
|
4736
|
-
const path = normalizeMetricPath(new URL(request.url).pathname);
|
|
4737
|
-
const labels = {
|
|
4738
|
-
method: request.method,
|
|
4739
|
-
path,
|
|
4740
|
-
status: String(response.status)
|
|
4741
|
-
};
|
|
4742
|
-
prometheusRegistry.incrementHttpRequest(labels);
|
|
4743
|
-
prometheusRegistry.observeHttpDuration(labels, durationMs);
|
|
4744
|
-
return response;
|
|
4745
|
-
};
|
|
4746
|
-
}
|
|
4747
|
-
|
|
4748
|
-
// ../../src/core/http/requireAbilityMiddleware.ts
|
|
4749
|
-
function createRequireAbilityMiddleware(abilityChecker) {
|
|
4750
|
-
return (ability) => {
|
|
4751
|
-
return async (_request, next) => {
|
|
4752
|
-
const user = currentAuthUser();
|
|
4753
|
-
try {
|
|
4754
|
-
abilityChecker.requireAbility(user, ability);
|
|
4755
|
-
} catch (error) {
|
|
4756
|
-
if (error instanceof ForbiddenError) {
|
|
4757
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
4758
|
-
}
|
|
4759
|
-
throw error;
|
|
4760
|
-
}
|
|
4761
|
-
return await next();
|
|
4762
|
-
};
|
|
4763
|
-
};
|
|
4764
|
-
}
|
|
4765
|
-
|
|
4766
|
-
// ../../src/core/http/requireAuthMiddleware.ts
|
|
4767
|
-
function createRequireAuthMiddleware(auth) {
|
|
4768
|
-
return async (request, next) => {
|
|
4769
|
-
if (!await auth.check(request)) {
|
|
4770
|
-
const error = new UnauthorizedError;
|
|
4771
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
4772
|
-
}
|
|
4773
|
-
return await next();
|
|
4774
|
-
};
|
|
4775
|
-
}
|
|
4776
|
-
|
|
4777
|
-
// ../../src/core/security/securityEvents.ts
|
|
4778
|
-
function logSecurityEvent2(event, details = {}) {
|
|
4779
|
-
const meta = currentRequestMeta();
|
|
4780
|
-
const user = currentAuthUser();
|
|
4781
|
-
console.log(JSON.stringify({
|
|
4782
|
-
level: "security",
|
|
4783
|
-
event,
|
|
4784
|
-
timestamp: new Date().toISOString(),
|
|
4785
|
-
ip_address: meta.ipAddress ?? null,
|
|
4786
|
-
user_agent: meta.userAgent ?? null,
|
|
4787
|
-
user_id: user?.id ?? null,
|
|
4788
|
-
...details
|
|
4789
|
-
}));
|
|
4790
|
-
}
|
|
4791
|
-
|
|
4792
|
-
// ../../src/core/http/requireGlobalAdminMiddleware.ts
|
|
4793
|
-
function createRequireGlobalAdminMiddleware() {
|
|
4794
|
-
return async (_request, next) => {
|
|
4795
|
-
const user = currentAuthUser();
|
|
4796
|
-
if (!isGlobalAdmin(user)) {
|
|
4797
|
-
logSecurityEvent2("privilege_escalation_blocked", {
|
|
4798
|
-
required_role: "platform_admin",
|
|
4799
|
-
path: new URL(_request.url).pathname
|
|
4800
|
-
});
|
|
4801
|
-
const error = new ForbiddenError("Platform admin access required.");
|
|
4802
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
4803
|
-
}
|
|
4804
|
-
return await next();
|
|
4805
|
-
};
|
|
4806
|
-
}
|
|
4807
|
-
|
|
4808
|
-
// ../../src/core/http/contentNegotiation.ts
|
|
4809
|
-
function requestPrefersJson(request) {
|
|
4810
|
-
if (!request) {
|
|
4811
|
-
return true;
|
|
4812
|
-
}
|
|
4813
|
-
if (request.headers.get("HX-Request") === "true") {
|
|
4814
|
-
return false;
|
|
4815
|
-
}
|
|
4816
|
-
const accept = request.headers.get("accept")?.toLowerCase() ?? "";
|
|
4817
|
-
if (accept.includes("text/html")) {
|
|
4818
|
-
return false;
|
|
4819
|
-
}
|
|
4820
|
-
if (accept.includes("application/json")) {
|
|
4821
|
-
return true;
|
|
4822
|
-
}
|
|
4823
|
-
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
4824
|
-
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
4825
|
-
return false;
|
|
4826
|
-
}
|
|
4827
|
-
const pathname = new URL(request.url).pathname;
|
|
4828
|
-
return pathname.startsWith("/api/");
|
|
4829
|
-
}
|
|
4830
|
-
|
|
4831
|
-
// ../../src/core/http/requireWebAuthMiddleware.ts
|
|
4832
|
-
function createRequireWebAuthMiddleware(auth) {
|
|
4833
|
-
return async (request, next) => {
|
|
4834
|
-
const user = await auth.resolve(request);
|
|
4835
|
-
if (user) {
|
|
4836
|
-
return await next();
|
|
4837
|
-
}
|
|
4838
|
-
if (requestPrefersJson(request)) {
|
|
4839
|
-
throw new UnauthorizedError;
|
|
4840
|
-
}
|
|
4841
|
-
const redirectTarget = encodeURIComponent(new URL(request.url).pathname);
|
|
4842
|
-
return Response.redirect(`/login?redirect=${redirectTarget}`, 302);
|
|
4843
|
-
};
|
|
4844
|
-
}
|
|
4845
|
-
|
|
4846
|
-
// ../../src/core/http/routeMiddleware.ts
|
|
4847
|
-
function withMiddleware(...middleware) {
|
|
4848
|
-
const wrap = composeMiddleware(...middleware);
|
|
4849
|
-
return (handler) => {
|
|
4850
|
-
return wrap(handler);
|
|
4851
|
-
};
|
|
4852
|
-
}
|
|
4853
|
-
|
|
4854
|
-
// ../../src/config/contentSecurityPolicy.ts
|
|
4855
|
-
function strictApiContentSecurityPolicy() {
|
|
4856
|
-
return "default-src 'none'; frame-ancestors 'none'; base-uri 'none'";
|
|
4857
|
-
}
|
|
4858
|
-
function serverHtmxContentSecurityPolicy() {
|
|
4859
|
-
return [
|
|
4860
|
-
"default-src 'self'",
|
|
4861
|
-
"script-src 'self' https://unpkg.com",
|
|
4862
|
-
"style-src 'self'",
|
|
4863
|
-
"connect-src 'self'",
|
|
4864
|
-
"img-src 'self'",
|
|
4865
|
-
"font-src 'self'",
|
|
4866
|
-
"form-action 'self'",
|
|
4867
|
-
"frame-ancestors 'none'",
|
|
4868
|
-
"base-uri 'self'"
|
|
4869
|
-
].join("; ");
|
|
4870
|
-
}
|
|
4871
|
-
function spaContentSecurityPolicy() {
|
|
4872
|
-
return [
|
|
4873
|
-
"default-src 'self'",
|
|
4874
|
-
"script-src 'self'",
|
|
4875
|
-
"style-src 'self'",
|
|
4876
|
-
"connect-src 'self'",
|
|
4877
|
-
"img-src 'self'",
|
|
4878
|
-
"font-src 'self'",
|
|
4879
|
-
"frame-ancestors 'none'",
|
|
4880
|
-
"base-uri 'self'"
|
|
4881
|
-
].join("; ");
|
|
4882
|
-
}
|
|
4883
|
-
function resolveContentSecurityPolicy(response) {
|
|
4884
|
-
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
4885
|
-
if (!contentType.includes("text/html")) {
|
|
4886
|
-
return strictApiContentSecurityPolicy();
|
|
4887
|
-
}
|
|
4888
|
-
const frontendMode = (process.env.FRONTEND_MODE ?? "api").trim();
|
|
4889
|
-
if (frontendMode === "server-htmx") {
|
|
4890
|
-
return serverHtmxContentSecurityPolicy();
|
|
4891
|
-
}
|
|
4892
|
-
if (frontendMode === "spa-react") {
|
|
4893
|
-
return spaContentSecurityPolicy();
|
|
4894
|
-
}
|
|
4895
|
-
return strictApiContentSecurityPolicy();
|
|
4896
|
-
}
|
|
4897
|
-
|
|
4898
|
-
// ../../src/core/http/securityHeadersMiddleware.ts
|
|
4899
|
-
function createSecurityHeadersMiddleware() {
|
|
4900
|
-
return async (_request, next) => {
|
|
4901
|
-
const response = await next();
|
|
4902
|
-
const headers = new Headers(response.headers);
|
|
4903
|
-
headers.set("X-Content-Type-Options", "nosniff");
|
|
4904
|
-
headers.set("X-Frame-Options", "DENY");
|
|
4905
|
-
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
4906
|
-
headers.set("X-XSS-Protection", "0");
|
|
4907
|
-
headers.set("Content-Security-Policy", resolveContentSecurityPolicy(response));
|
|
4908
|
-
if (appConfig.env === "production") {
|
|
4909
|
-
headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
|
4910
|
-
}
|
|
4911
|
-
return new Response(response.body, {
|
|
4912
|
-
status: response.status,
|
|
4913
|
-
statusText: response.statusText,
|
|
4914
|
-
headers
|
|
4915
|
-
});
|
|
4916
|
-
};
|
|
4917
|
-
}
|
|
4918
|
-
|
|
4919
|
-
// ../../src/core/http/throttleMiddleware.ts
|
|
4920
|
-
var {RedisClient: RedisClient4 } = globalThis.Bun;
|
|
4921
|
-
|
|
4922
|
-
// ../../src/core/tenant/tenantContext.ts
|
|
4923
|
-
import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
|
|
4924
|
-
var tenantContext = new AsyncLocalStorage5;
|
|
4925
|
-
function runWithTenant(tenant, callback) {
|
|
4926
|
-
return tenantContext.run(tenant, callback);
|
|
4927
|
-
}
|
|
4928
|
-
function currentTenant() {
|
|
4929
|
-
return tenantContext.getStore() ?? null;
|
|
4930
|
-
}
|
|
4931
|
-
function rateLimitMultiplierForPlan(plan) {
|
|
4932
|
-
switch (plan) {
|
|
4933
|
-
case "enterprise":
|
|
4934
|
-
return 4;
|
|
4935
|
-
case "pro":
|
|
4936
|
-
return 2;
|
|
4937
|
-
default:
|
|
4938
|
-
return 1;
|
|
4939
|
-
}
|
|
4940
|
-
}
|
|
4941
|
-
|
|
4942
|
-
// ../../src/core/http/throttleMiddleware.ts
|
|
4943
|
-
function resolveThrottleIdentity(request) {
|
|
4944
|
-
const user = currentAuthUser();
|
|
4945
|
-
if (user?.tokenId !== undefined) {
|
|
4946
|
-
return `token:${user.tokenId}`;
|
|
4947
|
-
}
|
|
4948
|
-
if (user) {
|
|
4949
|
-
return `user:${user.id}`;
|
|
4950
|
-
}
|
|
4951
|
-
return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
|
|
4952
|
-
}
|
|
4953
|
-
function createThrottleMiddleware(options) {
|
|
4954
|
-
const client = new RedisClient4(options.redisUrl);
|
|
4955
|
-
const prefix = options.keyPrefix ?? "workhub:throttle:";
|
|
4956
|
-
return async (request, next) => {
|
|
4957
|
-
const identity = resolveThrottleIdentity(request);
|
|
4958
|
-
const path = new URL(request.url).pathname;
|
|
4959
|
-
const throttleKey = `${prefix}${identity}:${path}`;
|
|
4960
|
-
const attempts = Number(await client.incr(throttleKey));
|
|
4961
|
-
if (attempts === 1) {
|
|
4962
|
-
await client.expire(throttleKey, options.decaySeconds);
|
|
4963
|
-
}
|
|
4964
|
-
const maxAttempts = options.maxAttempts * rateLimitMultiplierForPlan(currentTenant()?.plan ?? "free");
|
|
4965
|
-
if (attempts > maxAttempts) {
|
|
4966
|
-
return Response.json({ error: "Too many requests." }, {
|
|
4967
|
-
status: 429,
|
|
4968
|
-
headers: {
|
|
4969
|
-
"retry-after": String(options.decaySeconds)
|
|
4970
|
-
}
|
|
4971
|
-
});
|
|
4972
|
-
}
|
|
4973
|
-
return await next();
|
|
4974
|
-
};
|
|
4975
|
-
}
|
|
4976
|
-
|
|
4977
|
-
// ../../src/core/logging/requestLoggingMiddleware.ts
|
|
4978
|
-
function createRequestLoggingMiddleware() {
|
|
4979
|
-
return async (request, next) => {
|
|
4980
|
-
return await runWithRequestMeta({
|
|
4981
|
-
ipAddress: request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip"),
|
|
4982
|
-
userAgent: request.headers.get("user-agent"),
|
|
4983
|
-
request
|
|
4984
|
-
}, async () => {
|
|
4985
|
-
const startedAt = performance.now();
|
|
4986
|
-
const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
|
|
4987
|
-
const response = await next();
|
|
4988
|
-
const durationMs = Math.round(performance.now() - startedAt);
|
|
4989
|
-
appLogger.info("HTTP request completed", {
|
|
4990
|
-
requestId,
|
|
4991
|
-
method: request.method,
|
|
4992
|
-
path: new URL(request.url).pathname,
|
|
4993
|
-
status: response.status,
|
|
4994
|
-
durationMs
|
|
4995
|
-
});
|
|
4996
|
-
return response;
|
|
4997
|
-
});
|
|
4998
|
-
};
|
|
4999
|
-
}
|
|
5000
|
-
|
|
5001
|
-
// ../../src/core/security/publicReads.ts
|
|
5002
|
-
function isPublicReadsEnabled() {
|
|
5003
|
-
return isFeatureEnabled("publicReads");
|
|
5004
|
-
}
|
|
5005
|
-
|
|
5006
|
-
// ../../src/core/tenant/resolveTenant.ts
|
|
5007
|
-
async function resolveTenant(tenantId) {
|
|
5008
|
-
const rows = await connection_default`
|
|
5009
|
-
SELECT id, slug, plan, region
|
|
5010
|
-
FROM tenant
|
|
5011
|
-
WHERE id = ${tenantId}
|
|
5012
|
-
LIMIT 1
|
|
5013
|
-
`;
|
|
5014
|
-
const row = rows[0];
|
|
5015
|
-
return row ? { id: row.id, slug: row.slug, plan: row.plan, region: row.region ?? "eu" } : null;
|
|
5016
|
-
}
|
|
5017
|
-
|
|
5018
|
-
// ../../src/core/tenant/tenantDatabaseScope.ts
|
|
5019
|
-
async function applyTenantContextToTransaction(transaction, tenantId) {
|
|
5020
|
-
await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
|
|
5021
|
-
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
|
|
5022
|
-
}
|
|
5023
|
-
async function runWithTenantDatabase(tenant, callback) {
|
|
5024
|
-
return await getDatabase().begin(async (transaction) => {
|
|
5025
|
-
await applyTenantContextToTransaction(transaction, tenant.id);
|
|
5026
|
-
return await runWithDatabaseConnection(transaction, async () => {
|
|
5027
|
-
return await runWithTenant(tenant, callback);
|
|
5028
|
-
});
|
|
5029
|
-
});
|
|
5030
|
-
}
|
|
5031
|
-
|
|
5032
|
-
// ../../src/core/tenant/tenantMiddleware.ts
|
|
5033
|
-
var DEFAULT_TENANT = {
|
|
5034
|
-
id: 1,
|
|
5035
|
-
slug: "default",
|
|
5036
|
-
plan: "enterprise",
|
|
5037
|
-
region: "eu"
|
|
5038
|
-
};
|
|
5039
|
-
async function resolveUserTenantId(userId) {
|
|
5040
|
-
return await runWithMigrationBypass(async () => {
|
|
5041
|
-
const rows = await connection_default`
|
|
5042
|
-
SELECT tenant_id
|
|
5043
|
-
FROM users
|
|
5044
|
-
WHERE id = ${userId}
|
|
5045
|
-
LIMIT 1
|
|
5046
|
-
`;
|
|
5047
|
-
return rows[0]?.tenant_id ?? DEFAULT_TENANT.id;
|
|
5048
|
-
});
|
|
5049
|
-
}
|
|
5050
|
-
async function resolveTenantForRequest(request) {
|
|
5051
|
-
const user = currentAuthUser();
|
|
5052
|
-
const headerValue = request.headers.get("x-tenant-id")?.trim();
|
|
5053
|
-
const parsedHeader = headerValue !== undefined && headerValue.length > 0 ? Number.parseInt(headerValue, 10) : Number.NaN;
|
|
5054
|
-
if (user) {
|
|
5055
|
-
const userId = typeof user.id === "number" ? user.id : Number.parseInt(String(user.id), 10);
|
|
5056
|
-
if (Number.isInteger(userId) && userId > 0) {
|
|
5057
|
-
const userTenantId = await resolveUserTenantId(userId);
|
|
5058
|
-
if (!isGlobalAdmin(user)) {
|
|
5059
|
-
if (Number.isInteger(parsedHeader) && parsedHeader > 0 && parsedHeader !== userTenantId) {
|
|
5060
|
-
throw new ForbiddenError("Tenant header does not match your account.");
|
|
5061
|
-
}
|
|
5062
|
-
return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
|
|
5063
|
-
}
|
|
5064
|
-
if (Number.isInteger(parsedHeader) && parsedHeader > 0) {
|
|
5065
|
-
return await resolveTenant(parsedHeader) ?? DEFAULT_TENANT;
|
|
5066
|
-
}
|
|
5067
|
-
return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
|
|
5068
|
-
}
|
|
5069
|
-
}
|
|
5070
|
-
const tenantId = Number.isInteger(parsedHeader) && parsedHeader > 0 ? parsedHeader : DEFAULT_TENANT.id;
|
|
5071
|
-
return await resolveTenant(tenantId) ?? DEFAULT_TENANT;
|
|
5072
|
-
}
|
|
5073
|
-
function createTenantMiddleware() {
|
|
5074
|
-
return async (request, next) => {
|
|
5075
|
-
try {
|
|
5076
|
-
const tenant = await resolveTenantForRequest(request);
|
|
5077
|
-
return await runWithTenantDatabase(tenant, async () => {
|
|
5078
|
-
const response = await next();
|
|
5079
|
-
const headers = new Headers(response.headers);
|
|
5080
|
-
headers.set("x-tenant-id", String(tenant.id));
|
|
5081
|
-
headers.set("x-tenant-region", tenant.region);
|
|
5082
|
-
return new Response(response.body, {
|
|
5083
|
-
status: response.status,
|
|
5084
|
-
statusText: response.statusText,
|
|
5085
|
-
headers
|
|
5086
|
-
});
|
|
5087
|
-
});
|
|
5088
|
-
} catch (error) {
|
|
5089
|
-
if (error instanceof HttpError) {
|
|
5090
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
5091
|
-
}
|
|
5092
|
-
throw error;
|
|
5093
|
-
}
|
|
5094
|
-
};
|
|
5095
|
-
}
|
|
5096
|
-
|
|
5097
|
-
// ../../src/core/tracing/otel.ts
|
|
5098
|
-
import { randomBytes } from "crypto";
|
|
5099
|
-
function randomHex(bytes) {
|
|
5100
|
-
return randomBytes(bytes).toString("hex");
|
|
5101
|
-
}
|
|
5102
|
-
function createSpan(input) {
|
|
5103
|
-
const spanId = randomHex(8);
|
|
5104
|
-
return {
|
|
5105
|
-
traceId: input.traceId,
|
|
5106
|
-
spanId,
|
|
5107
|
-
name: input.name,
|
|
5108
|
-
startTimeUnixNano: String(Math.floor(input.startedAt * 1e6)),
|
|
5109
|
-
endTimeUnixNano: String(Math.floor(input.endedAt * 1e6)),
|
|
5110
|
-
attributes: Object.entries(input.attributes ?? {}).map(([key, value]) => ({
|
|
5111
|
-
key,
|
|
5112
|
-
value: { stringValue: value }
|
|
5113
|
-
})),
|
|
5114
|
-
status: { code: 1 }
|
|
5115
|
-
};
|
|
5116
|
-
}
|
|
5117
|
-
async function exportOtelSpan(span) {
|
|
5118
|
-
const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
|
|
5119
|
-
if (!endpoint) {
|
|
5120
|
-
return;
|
|
5121
|
-
}
|
|
5122
|
-
const serviceName = process.env.OTEL_SERVICE_NAME?.trim() ?? "workhub-api";
|
|
5123
|
-
const url = endpoint.endsWith("/v1/traces") ? endpoint : `${endpoint.replace(/\/$/, "")}/v1/traces`;
|
|
5124
|
-
await fetch(url, {
|
|
5125
|
-
method: "POST",
|
|
5126
|
-
headers: { "content-type": "application/json" },
|
|
5127
|
-
body: JSON.stringify({
|
|
5128
|
-
resourceSpans: [
|
|
5129
|
-
{
|
|
5130
|
-
resource: {
|
|
5131
|
-
attributes: [{ key: "service.name", value: { stringValue: serviceName } }]
|
|
5132
|
-
},
|
|
5133
|
-
scopeSpans: [{ spans: [span] }]
|
|
5134
|
-
}
|
|
5135
|
-
]
|
|
5136
|
-
})
|
|
5137
|
-
});
|
|
5138
|
-
}
|
|
5139
|
-
|
|
5140
|
-
// ../../src/core/tracing/traceContext.ts
|
|
5141
|
-
import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
|
|
5142
|
-
var traceContextStorage = new AsyncLocalStorage6;
|
|
5143
|
-
function runWithTraceContext(context, callback) {
|
|
5144
|
-
return traceContextStorage.run(context, callback);
|
|
5145
|
-
}
|
|
5146
|
-
|
|
5147
|
-
// ../../src/core/tracing/tracingMiddleware.ts
|
|
5148
|
-
function createTracingMiddleware() {
|
|
5149
|
-
return async (request, next) => {
|
|
5150
|
-
const traceId = (request.headers.get("x-trace-id") ?? crypto.randomUUID()).replace(/-/g, "");
|
|
5151
|
-
const spanId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
|
|
5152
|
-
const startedAt = performance.now();
|
|
5153
|
-
const path = new URL(request.url).pathname;
|
|
5154
|
-
return await runWithTraceContext({ traceId, spanId }, async () => {
|
|
5155
|
-
const response = await next();
|
|
5156
|
-
const endedAt = performance.now();
|
|
5157
|
-
const headers = new Headers(response.headers);
|
|
5158
|
-
headers.set("x-trace-id", traceId);
|
|
5159
|
-
headers.set("x-span-id", spanId);
|
|
5160
|
-
headers.set("traceparent", `00-${traceId}-${spanId}-01`);
|
|
5161
|
-
headers.set("server-timing", `app;dur=${(endedAt - startedAt).toFixed(2)}`);
|
|
5162
|
-
exportOtelSpan(createSpan({
|
|
5163
|
-
traceId,
|
|
5164
|
-
name: `${request.method} ${path}`,
|
|
5165
|
-
startedAt,
|
|
5166
|
-
endedAt,
|
|
5167
|
-
attributes: {
|
|
5168
|
-
"http.method": request.method,
|
|
5169
|
-
"http.route": path,
|
|
5170
|
-
"http.status_code": String(response.status)
|
|
5171
|
-
}
|
|
5172
|
-
})).catch(() => {
|
|
5173
|
-
return;
|
|
5174
|
-
});
|
|
5175
|
-
return new Response(response.body, {
|
|
5176
|
-
status: response.status,
|
|
5177
|
-
statusText: response.statusText,
|
|
5178
|
-
headers
|
|
5179
|
-
});
|
|
5180
|
-
});
|
|
5181
|
-
};
|
|
5182
|
-
}
|
|
5183
|
-
|
|
5184
4466
|
// ../../src/bootstrap/httpKernel.ts
|
|
5185
4467
|
class HttpKernel {
|
|
5186
4468
|
dependencies;
|
|
@@ -5402,7 +4684,7 @@ function createWebRoutes(dependencies) {
|
|
|
5402
4684
|
registerRoute("GET", "/assets/*", ["global", "web"]);
|
|
5403
4685
|
const pathname = new URL(request.url).pathname;
|
|
5404
4686
|
const relativePath = pathname.replace(/^\//, "");
|
|
5405
|
-
const file = Bun.file(
|
|
4687
|
+
const file = Bun.file(join5(process.cwd(), "public", relativePath));
|
|
5406
4688
|
if (!await file.exists()) {
|
|
5407
4689
|
return htmlResponse("Not Found", { status: 404 });
|
|
5408
4690
|
}
|
|
@@ -5576,7 +4858,7 @@ function createWebServer(options) {
|
|
|
5576
4858
|
});
|
|
5577
4859
|
}
|
|
5578
4860
|
// ../../src/bootstrap/web/session.ts
|
|
5579
|
-
import { createHash, randomBytes
|
|
4861
|
+
import { createHash, randomBytes } from "crypto";
|
|
5580
4862
|
class CookieSessionStore {
|
|
5581
4863
|
sql;
|
|
5582
4864
|
secret;
|
|
@@ -5602,7 +4884,7 @@ class CookieSessionStore {
|
|
|
5602
4884
|
return header.includes("Secure") ? header : `${header}; Secure`;
|
|
5603
4885
|
}
|
|
5604
4886
|
async create(user) {
|
|
5605
|
-
const id =
|
|
4887
|
+
const id = randomBytes(32).toString("hex");
|
|
5606
4888
|
const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
|
|
5607
4889
|
await this.sql.unsafe(`INSERT INTO sessions (id, user_id, expires_at) VALUES ($1, $2, $3)`, [
|
|
5608
4890
|
id,
|