@getstrata/bootstrap 0.2.4 → 0.2.6
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/httpKernel.d.ts +1 -2
- package/dist/bootstrap/middleware.d.ts +1 -1
- package/dist/bootstrap/web/session.d.ts +1 -0
- package/dist/core/admin/formatValue.d.ts +3 -0
- package/dist/core/admin/index.d.ts +3 -0
- package/dist/core/admin/registry.d.ts +11 -0
- package/dist/core/admin/types.d.ts +24 -0
- package/dist/core/database/baseRepository.d.ts +4 -1
- package/dist/core/database/index.d.ts +2 -2
- package/dist/core/database/relationships.d.ts +46 -2
- package/dist/core/database/repositoryQuery.d.ts +4 -1
- package/dist/core/mail/mailer.d.ts +3 -1
- package/dist/core/mail/markdownMail.d.ts +15 -0
- package/dist/core/mail/markdownMailable.d.ts +12 -0
- package/dist/core/notifications/dispatcher.d.ts +13 -0
- package/dist/core/notifications/index.d.ts +3 -0
- package/dist/core/notifications/notification.d.ts +7 -0
- package/dist/core/notifications/types.d.ts +25 -0
- package/dist/core/queue/failedJobService.d.ts +1 -0
- package/dist/core/queue/publicQueue.d.ts +2 -1
- package/dist/core/storage/storage.d.ts +6 -3
- package/dist/framework/public-api.d.ts +23 -6
- package/dist/index.js +190 -942
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -81,9 +81,6 @@ var databaseConfig = {
|
|
|
81
81
|
// ../../src/core/database/connectionContext.ts
|
|
82
82
|
import { AsyncLocalStorage } from "async_hooks";
|
|
83
83
|
var activeConnection = new AsyncLocalStorage;
|
|
84
|
-
function runWithDatabaseConnection(connection, callback) {
|
|
85
|
-
return activeConnection.run(connection, callback);
|
|
86
|
-
}
|
|
87
84
|
function getActiveDatabaseConnection(fallback) {
|
|
88
85
|
return activeConnection.getStore() ?? fallback;
|
|
89
86
|
}
|
|
@@ -207,12 +204,6 @@ class UnauthorizedError extends HttpError {
|
|
|
207
204
|
}
|
|
208
205
|
}
|
|
209
206
|
|
|
210
|
-
class PayloadTooLargeError extends HttpError {
|
|
211
|
-
constructor(message = "Payload Too Large", details) {
|
|
212
|
-
super(413, message, details);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
207
|
// ../../src/core/security/safeUrl.ts
|
|
217
208
|
var BLOCKED_HOSTNAMES = new Set([
|
|
218
209
|
"localhost",
|
|
@@ -836,9 +827,6 @@ var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
|
|
|
836
827
|
// ../../src/core/auth/authContext.ts
|
|
837
828
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
838
829
|
var authContext = new AsyncLocalStorage2;
|
|
839
|
-
function runWithAuthUser(user, callback) {
|
|
840
|
-
return authContext.run(user, callback);
|
|
841
|
-
}
|
|
842
830
|
function currentAuthUser() {
|
|
843
831
|
return authContext.getStore() ?? null;
|
|
844
832
|
}
|
|
@@ -2347,6 +2335,39 @@ function indexBelongsToRelation(children, parents, relation) {
|
|
|
2347
2335
|
}
|
|
2348
2336
|
return result;
|
|
2349
2337
|
}
|
|
2338
|
+
function indexMorphManyRelation(parents, children, relation) {
|
|
2339
|
+
const groups = new Map;
|
|
2340
|
+
for (const parent of parents) {
|
|
2341
|
+
groups.set(parent[relation.localKey], []);
|
|
2342
|
+
}
|
|
2343
|
+
for (const child of children) {
|
|
2344
|
+
if (child[relation.morphTypeKey] !== relation.morphType) {
|
|
2345
|
+
continue;
|
|
2346
|
+
}
|
|
2347
|
+
const key = child[relation.morphIdKey];
|
|
2348
|
+
const group = groups.get(key);
|
|
2349
|
+
if (!group) {
|
|
2350
|
+
continue;
|
|
2351
|
+
}
|
|
2352
|
+
group.push(child);
|
|
2353
|
+
}
|
|
2354
|
+
return groups;
|
|
2355
|
+
}
|
|
2356
|
+
function indexMorphToRelation(children, parentsByType, relation) {
|
|
2357
|
+
const result = new Map;
|
|
2358
|
+
for (const child of children) {
|
|
2359
|
+
const morphType = String(child[relation.morphTypeKey]);
|
|
2360
|
+
const parents = parentsByType.get(morphType);
|
|
2361
|
+
if (!parents) {
|
|
2362
|
+
continue;
|
|
2363
|
+
}
|
|
2364
|
+
const parent = parents.get(child[relation.morphIdKey]);
|
|
2365
|
+
if (parent) {
|
|
2366
|
+
result.set(child[relation.morphIdKey], parent);
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
return result;
|
|
2370
|
+
}
|
|
2350
2371
|
|
|
2351
2372
|
// ../../src/core/database/boundConnection.ts
|
|
2352
2373
|
var boundConnectionHolder = {
|
|
@@ -2477,6 +2498,37 @@ class RepositoryQuery {
|
|
|
2477
2498
|
});
|
|
2478
2499
|
return this;
|
|
2479
2500
|
}
|
|
2501
|
+
withMorphMany(as, relation, childRepository, options = {}) {
|
|
2502
|
+
this.eagerLoads.push({
|
|
2503
|
+
kind: "morphMany",
|
|
2504
|
+
as,
|
|
2505
|
+
relation,
|
|
2506
|
+
repository: childRepository,
|
|
2507
|
+
options
|
|
2508
|
+
});
|
|
2509
|
+
return this;
|
|
2510
|
+
}
|
|
2511
|
+
withMorphOne(as, relation, childRepository, options = {}) {
|
|
2512
|
+
this.eagerLoads.push({
|
|
2513
|
+
kind: "morphOne",
|
|
2514
|
+
as,
|
|
2515
|
+
relation,
|
|
2516
|
+
repository: childRepository,
|
|
2517
|
+
options
|
|
2518
|
+
});
|
|
2519
|
+
return this;
|
|
2520
|
+
}
|
|
2521
|
+
withMorphTo(as, relation, repositoriesByType, options = {}) {
|
|
2522
|
+
this.eagerLoads.push({
|
|
2523
|
+
kind: "morphTo",
|
|
2524
|
+
as,
|
|
2525
|
+
relation,
|
|
2526
|
+
repository: this.repository,
|
|
2527
|
+
morphRepositories: repositoriesByType,
|
|
2528
|
+
options
|
|
2529
|
+
});
|
|
2530
|
+
return this;
|
|
2531
|
+
}
|
|
2480
2532
|
async get() {
|
|
2481
2533
|
const rows = await this.repository.findAll(this.buildOptions());
|
|
2482
2534
|
return await this.attach(rows);
|
|
@@ -2537,6 +2589,33 @@ class RepositoryQuery {
|
|
|
2537
2589
|
}));
|
|
2538
2590
|
continue;
|
|
2539
2591
|
}
|
|
2592
|
+
if (load.kind === "morphMany") {
|
|
2593
|
+
const relation2 = load.relation;
|
|
2594
|
+
const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation2, load.options);
|
|
2595
|
+
result = result.map((row) => ({
|
|
2596
|
+
...row,
|
|
2597
|
+
[load.as]: grouped2.get(row[relation2.localKey]) ?? []
|
|
2598
|
+
}));
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2601
|
+
if (load.kind === "morphOne") {
|
|
2602
|
+
const relation2 = load.relation;
|
|
2603
|
+
const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation2, load.options);
|
|
2604
|
+
result = result.map((row) => ({
|
|
2605
|
+
...row,
|
|
2606
|
+
[load.as]: grouped2.get(row[relation2.localKey])
|
|
2607
|
+
}));
|
|
2608
|
+
continue;
|
|
2609
|
+
}
|
|
2610
|
+
if (load.kind === "morphTo") {
|
|
2611
|
+
const relation2 = load.relation;
|
|
2612
|
+
const grouped2 = await this.repository.loadMorphToForChildren(rows, relation2, load.morphRepositories ?? new Map, load.options);
|
|
2613
|
+
result = result.map((row) => ({
|
|
2614
|
+
...row,
|
|
2615
|
+
[load.as]: grouped2.get(row[relation2.morphIdKey])
|
|
2616
|
+
}));
|
|
2617
|
+
continue;
|
|
2618
|
+
}
|
|
2540
2619
|
const relation = load.relation;
|
|
2541
2620
|
const grouped = await this.repository.loadBelongsToForParents(rows, relation, load.repository, load.options);
|
|
2542
2621
|
result = result.map((row) => ({
|
|
@@ -2813,6 +2892,56 @@ class BaseRepository5 {
|
|
|
2813
2892
|
}, options);
|
|
2814
2893
|
return indexBelongsToRelation(children, parents, relation);
|
|
2815
2894
|
}
|
|
2895
|
+
async loadMorphManyForParents(parents, relation, options = {}) {
|
|
2896
|
+
if (parents.length === 0) {
|
|
2897
|
+
return indexMorphManyRelation(parents, [], relation);
|
|
2898
|
+
}
|
|
2899
|
+
const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
|
|
2900
|
+
const children = await this.findWhere({
|
|
2901
|
+
[relation.morphTypeKey]: relation.morphType,
|
|
2902
|
+
[relation.morphIdKey]: parentIds
|
|
2903
|
+
}, options);
|
|
2904
|
+
return indexMorphManyRelation(parents, children, relation);
|
|
2905
|
+
}
|
|
2906
|
+
async loadMorphOneForParents(parents, relation, options = {}) {
|
|
2907
|
+
const grouped = await this.loadMorphManyForParents(parents, relation, options);
|
|
2908
|
+
const result = new Map;
|
|
2909
|
+
for (const parent of parents) {
|
|
2910
|
+
const matches = grouped.get(parent[relation.localKey]) ?? [];
|
|
2911
|
+
result.set(parent[relation.localKey], matches[0]);
|
|
2912
|
+
}
|
|
2913
|
+
return result;
|
|
2914
|
+
}
|
|
2915
|
+
async loadMorphToForChildren(children, relation, repositoriesByType, options = {}) {
|
|
2916
|
+
if (children.length === 0) {
|
|
2917
|
+
return new Map;
|
|
2918
|
+
}
|
|
2919
|
+
const idsByType = new Map;
|
|
2920
|
+
for (const child of children) {
|
|
2921
|
+
const morphType = String(child[relation.morphTypeKey]);
|
|
2922
|
+
const morphId = child[relation.morphIdKey];
|
|
2923
|
+
const ids = idsByType.get(morphType) ?? new Set;
|
|
2924
|
+
ids.add(morphId);
|
|
2925
|
+
idsByType.set(morphType, ids);
|
|
2926
|
+
}
|
|
2927
|
+
const parentsByType = new Map;
|
|
2928
|
+
for (const [morphType, ids] of idsByType) {
|
|
2929
|
+
const repository = repositoriesByType.get(morphType);
|
|
2930
|
+
if (!repository) {
|
|
2931
|
+
continue;
|
|
2932
|
+
}
|
|
2933
|
+
const ownerKey = repository.getTable().primaryKey;
|
|
2934
|
+
const parents = await repository.withConnection(this.connection).findWhere({
|
|
2935
|
+
[ownerKey]: [...ids]
|
|
2936
|
+
}, options);
|
|
2937
|
+
const indexed = new Map;
|
|
2938
|
+
for (const parent of parents) {
|
|
2939
|
+
indexed.set(parent[ownerKey], parent);
|
|
2940
|
+
}
|
|
2941
|
+
parentsByType.set(morphType, indexed);
|
|
2942
|
+
}
|
|
2943
|
+
return indexMorphToRelation(children, parentsByType, relation);
|
|
2944
|
+
}
|
|
2816
2945
|
}
|
|
2817
2946
|
var baseRepository_default = BaseRepository5;
|
|
2818
2947
|
// ../../src/core/database/model.ts
|
|
@@ -3429,6 +3558,12 @@ class FailedJobService {
|
|
|
3429
3558
|
await this.repository.deleteById(id);
|
|
3430
3559
|
return failedJob;
|
|
3431
3560
|
}
|
|
3561
|
+
async delete(id) {
|
|
3562
|
+
const deleted = await this.repository.deleteById(id);
|
|
3563
|
+
if (!deleted) {
|
|
3564
|
+
throw new Error(`Failed job ${id} not found.`);
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3432
3567
|
async flush() {
|
|
3433
3568
|
const jobs = await this.repository.findAll();
|
|
3434
3569
|
let deleted = 0;
|
|
@@ -3442,9 +3577,6 @@ class FailedJobService {
|
|
|
3442
3577
|
}
|
|
3443
3578
|
var failedJobService_default = FailedJobService;
|
|
3444
3579
|
|
|
3445
|
-
// ../../src/core/queue/redisQueue.ts
|
|
3446
|
-
var {RedisClient: RedisClient2 } = globalThis.Bun;
|
|
3447
|
-
|
|
3448
3580
|
// ../../src/core/queue/jobRunner.ts
|
|
3449
3581
|
async function runQueueJob(envelope, failedJobs) {
|
|
3450
3582
|
const job = jobRegistry.create(envelope.name);
|
|
@@ -3476,6 +3608,7 @@ async function runQueueJob(envelope, failedJobs) {
|
|
|
3476
3608
|
}
|
|
3477
3609
|
|
|
3478
3610
|
// ../../src/core/queue/redisQueue.ts
|
|
3611
|
+
var {RedisClient: RedisClient2 } = globalThis.Bun;
|
|
3479
3612
|
var QUEUE_LIST_KEY = "workhub:queue:default";
|
|
3480
3613
|
var QUEUE_HIGH_KEY = "workhub:queue:high";
|
|
3481
3614
|
var QUEUE_LOW_KEY = "workhub:queue:low";
|
|
@@ -3584,8 +3717,14 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus) {
|
|
|
3584
3717
|
if (tags.length === 0) {
|
|
3585
3718
|
return;
|
|
3586
3719
|
}
|
|
3587
|
-
|
|
3588
|
-
|
|
3720
|
+
let cache;
|
|
3721
|
+
let queue;
|
|
3722
|
+
try {
|
|
3723
|
+
cache = resolveApplicationCache();
|
|
3724
|
+
queue = resolveApplicationQueue();
|
|
3725
|
+
} catch {
|
|
3726
|
+
return;
|
|
3727
|
+
}
|
|
3589
3728
|
const job = createTrackedJob("cache.invalidate-tags", new invalidateCacheTagsJob_default(cache));
|
|
3590
3729
|
await queue.dispatch(job, { tags });
|
|
3591
3730
|
});
|
|
@@ -3695,9 +3834,6 @@ function isViewsEnabled() {
|
|
|
3695
3834
|
// ../../src/core/http/requestMetaContext.ts
|
|
3696
3835
|
import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
|
|
3697
3836
|
var requestMetaContext = new AsyncLocalStorage3;
|
|
3698
|
-
function runWithRequestMeta(meta, callback) {
|
|
3699
|
-
return requestMetaContext.run(meta, callback);
|
|
3700
|
-
}
|
|
3701
3837
|
function currentRequestMeta() {
|
|
3702
3838
|
return requestMetaContext.getStore() ?? {
|
|
3703
3839
|
ipAddress: null,
|
|
@@ -3747,9 +3883,6 @@ function htmlResponse(html, init = {}) {
|
|
|
3747
3883
|
}
|
|
3748
3884
|
});
|
|
3749
3885
|
}
|
|
3750
|
-
// ../../src/core/http/csrfToken.ts
|
|
3751
|
-
import { timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
3752
|
-
|
|
3753
3886
|
// ../../src/core/http/cookies.ts
|
|
3754
3887
|
function readRequestCookie(request, name) {
|
|
3755
3888
|
const cookies = request.cookies;
|
|
@@ -3784,14 +3917,6 @@ function resolveCsrfSecret() {
|
|
|
3784
3917
|
function csrfVerifyOptions() {
|
|
3785
3918
|
return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
|
|
3786
3919
|
}
|
|
3787
|
-
function tokensMatch(left, right) {
|
|
3788
|
-
const leftBuffer = Buffer.from(left);
|
|
3789
|
-
const rightBuffer = Buffer.from(right);
|
|
3790
|
-
if (leftBuffer.length !== rightBuffer.length) {
|
|
3791
|
-
return false;
|
|
3792
|
-
}
|
|
3793
|
-
return timingSafeEqual2(leftBuffer, rightBuffer);
|
|
3794
|
-
}
|
|
3795
3920
|
function createCsrfTokenCookie() {
|
|
3796
3921
|
const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
|
|
3797
3922
|
return {
|
|
@@ -3806,45 +3931,6 @@ function resolveCsrfToken(request) {
|
|
|
3806
3931
|
}
|
|
3807
3932
|
return createCsrfTokenCookie();
|
|
3808
3933
|
}
|
|
3809
|
-
function readSubmittedCsrfToken(request) {
|
|
3810
|
-
const headerToken = request.headers.get("x-csrf-token")?.trim();
|
|
3811
|
-
if (headerToken) {
|
|
3812
|
-
return headerToken;
|
|
3813
|
-
}
|
|
3814
|
-
return null;
|
|
3815
|
-
}
|
|
3816
|
-
async function readSubmittedCsrfTokenFromBody(request) {
|
|
3817
|
-
const headerToken = readSubmittedCsrfToken(request);
|
|
3818
|
-
if (headerToken) {
|
|
3819
|
-
return headerToken;
|
|
3820
|
-
}
|
|
3821
|
-
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
3822
|
-
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
3823
|
-
const formData = await request.clone().formData();
|
|
3824
|
-
const field = formData.get("_token");
|
|
3825
|
-
if (typeof field === "string" && field.trim().length > 0) {
|
|
3826
|
-
return field.trim();
|
|
3827
|
-
}
|
|
3828
|
-
const legacyField = formData.get("_csrf");
|
|
3829
|
-
if (typeof legacyField === "string" && legacyField.trim().length > 0) {
|
|
3830
|
-
return legacyField.trim();
|
|
3831
|
-
}
|
|
3832
|
-
}
|
|
3833
|
-
return null;
|
|
3834
|
-
}
|
|
3835
|
-
function verifyCsrfToken(request, submittedToken) {
|
|
3836
|
-
if (!submittedToken) {
|
|
3837
|
-
return false;
|
|
3838
|
-
}
|
|
3839
|
-
const cookieValue = readRequestCookie(request, CSRF_COOKIE);
|
|
3840
|
-
if (!cookieValue) {
|
|
3841
|
-
return false;
|
|
3842
|
-
}
|
|
3843
|
-
if (!tokensMatch(submittedToken, cookieValue)) {
|
|
3844
|
-
return false;
|
|
3845
|
-
}
|
|
3846
|
-
return Bun.CSRF.verify(submittedToken, csrfVerifyOptions());
|
|
3847
|
-
}
|
|
3848
3934
|
function resolveCsrfTokenForRequest(request) {
|
|
3849
3935
|
const metaToken = currentRequestMeta().csrfToken;
|
|
3850
3936
|
if (metaToken) {
|
|
@@ -3854,7 +3940,7 @@ function resolveCsrfTokenForRequest(request) {
|
|
|
3854
3940
|
}
|
|
3855
3941
|
|
|
3856
3942
|
// ../../src/core/http/flashSession.ts
|
|
3857
|
-
import { createHmac as createHmac3, timingSafeEqual as
|
|
3943
|
+
import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
3858
3944
|
var FLASH_COOKIE = "workhub_flash";
|
|
3859
3945
|
var FLASH_TTL_MS = 60 * 1000;
|
|
3860
3946
|
function resolveFlashSecret() {
|
|
@@ -3901,7 +3987,7 @@ function parseFlashCookie(cookieValue) {
|
|
|
3901
3987
|
if (expectedBuffer.length !== actualBuffer.length) {
|
|
3902
3988
|
return null;
|
|
3903
3989
|
}
|
|
3904
|
-
if (!
|
|
3990
|
+
if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
|
|
3905
3991
|
return null;
|
|
3906
3992
|
}
|
|
3907
3993
|
try {
|
|
@@ -3917,9 +4003,6 @@ function parseFlashCookie(cookieValue) {
|
|
|
3917
4003
|
return null;
|
|
3918
4004
|
}
|
|
3919
4005
|
}
|
|
3920
|
-
function clearFlashCookie() {
|
|
3921
|
-
return `${FLASH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
|
|
3922
|
-
}
|
|
3923
4006
|
function pullFlash(request) {
|
|
3924
4007
|
const cookieValue = readFlashCookie(request);
|
|
3925
4008
|
if (!cookieValue) {
|
|
@@ -3927,15 +4010,6 @@ function pullFlash(request) {
|
|
|
3927
4010
|
}
|
|
3928
4011
|
return parseFlashCookie(cookieValue);
|
|
3929
4012
|
}
|
|
3930
|
-
function withFlashClear(response) {
|
|
3931
|
-
const headers = new Headers(response.headers);
|
|
3932
|
-
headers.append("set-cookie", clearFlashCookie());
|
|
3933
|
-
return new Response(response.body, {
|
|
3934
|
-
status: response.status,
|
|
3935
|
-
statusText: response.statusText,
|
|
3936
|
-
headers
|
|
3937
|
-
});
|
|
3938
|
-
}
|
|
3939
4013
|
|
|
3940
4014
|
// ../../src/core/view/webLayoutData.ts
|
|
3941
4015
|
async function resolveWebLayoutData(container, request) {
|
|
@@ -4127,17 +4201,6 @@ function composeMiddleware(...middleware) {
|
|
|
4127
4201
|
};
|
|
4128
4202
|
};
|
|
4129
4203
|
}
|
|
4130
|
-
async function requestIdMiddleware(request, next) {
|
|
4131
|
-
const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
|
|
4132
|
-
const response = await next();
|
|
4133
|
-
const headers = new Headers(response.headers);
|
|
4134
|
-
headers.set("x-request-id", requestId);
|
|
4135
|
-
return new Response(response.body, {
|
|
4136
|
-
status: response.status,
|
|
4137
|
-
statusText: response.statusText,
|
|
4138
|
-
headers
|
|
4139
|
-
});
|
|
4140
|
-
}
|
|
4141
4204
|
function wrapRouteHandler(handler, middleware) {
|
|
4142
4205
|
if (isMethodRouteMap(handler)) {
|
|
4143
4206
|
const wrapped = {};
|
|
@@ -4159,6 +4222,32 @@ function applyMiddlewareToRoutes(routes, middleware) {
|
|
|
4159
4222
|
return wrapped;
|
|
4160
4223
|
}
|
|
4161
4224
|
|
|
4225
|
+
// ../../src/bootstrap/httpKernel.ts
|
|
4226
|
+
import {
|
|
4227
|
+
createAuthMiddleware,
|
|
4228
|
+
createAuthorizeMiddleware,
|
|
4229
|
+
createBodySizeLimitMiddleware,
|
|
4230
|
+
createCorsMiddleware,
|
|
4231
|
+
createCsrfMiddleware,
|
|
4232
|
+
createFlashMiddleware,
|
|
4233
|
+
createLoginThrottleMiddleware,
|
|
4234
|
+
createMembershipMiddleware,
|
|
4235
|
+
createMemoryThrottleMiddleware,
|
|
4236
|
+
createMetricsMiddleware,
|
|
4237
|
+
createRequestLoggingMiddleware,
|
|
4238
|
+
createRequireAbilityMiddleware,
|
|
4239
|
+
createRequireAuthMiddleware,
|
|
4240
|
+
createRequireGlobalAdminMiddleware,
|
|
4241
|
+
createRequireWebAuthMiddleware,
|
|
4242
|
+
createSecurityHeadersMiddleware,
|
|
4243
|
+
createTenantMiddleware,
|
|
4244
|
+
createThrottleMiddleware,
|
|
4245
|
+
createTracingMiddleware,
|
|
4246
|
+
isPublicReadsEnabled,
|
|
4247
|
+
requestIdMiddleware,
|
|
4248
|
+
withMiddleware
|
|
4249
|
+
} from "@getstrata/core";
|
|
4250
|
+
|
|
4162
4251
|
// ../../src/config/rateLimit.ts
|
|
4163
4252
|
var LOCAL_LOGIN_RATE_LIMIT = {
|
|
4164
4253
|
maxAttempts: 100,
|
|
@@ -4193,849 +4282,6 @@ function resolveRegisterRateLimit() {
|
|
|
4193
4282
|
};
|
|
4194
4283
|
}
|
|
4195
4284
|
|
|
4196
|
-
// ../../src/core/auth/membershipContext.ts
|
|
4197
|
-
import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
|
|
4198
|
-
|
|
4199
|
-
// ../../src/modules/organization/memberRepository.ts
|
|
4200
|
-
class OrganizationMemberRepository {
|
|
4201
|
-
constructor() {}
|
|
4202
|
-
async findMembership(userId, organizationId) {
|
|
4203
|
-
const rows = await connection_default`
|
|
4204
|
-
SELECT id, organization_id, user_id, role, created_at
|
|
4205
|
-
FROM organization_member
|
|
4206
|
-
WHERE user_id = ${userId} AND organization_id = ${organizationId}
|
|
4207
|
-
LIMIT 1
|
|
4208
|
-
`;
|
|
4209
|
-
return rows[0] ?? null;
|
|
4210
|
-
}
|
|
4211
|
-
async listForUser(userId) {
|
|
4212
|
-
return await connection_default`
|
|
4213
|
-
SELECT id, organization_id, user_id, role, created_at
|
|
4214
|
-
FROM organization_member
|
|
4215
|
-
WHERE user_id = ${userId}
|
|
4216
|
-
ORDER BY organization_id
|
|
4217
|
-
`;
|
|
4218
|
-
}
|
|
4219
|
-
async listForOrganization(organizationId) {
|
|
4220
|
-
return await connection_default`
|
|
4221
|
-
SELECT id, organization_id, user_id, role, created_at
|
|
4222
|
-
FROM organization_member
|
|
4223
|
-
WHERE organization_id = ${organizationId}
|
|
4224
|
-
ORDER BY id
|
|
4225
|
-
`;
|
|
4226
|
-
}
|
|
4227
|
-
async addMember(input) {
|
|
4228
|
-
const rows = await connection_default`
|
|
4229
|
-
INSERT INTO organization_member (organization_id, user_id, role)
|
|
4230
|
-
VALUES (${input.organizationId}, ${input.userId}, ${input.role ?? "member"})
|
|
4231
|
-
RETURNING id, organization_id, user_id, role, created_at
|
|
4232
|
-
`;
|
|
4233
|
-
const row = rows[0];
|
|
4234
|
-
if (!row) {
|
|
4235
|
-
throw new Error("Organization member insert did not return a row.");
|
|
4236
|
-
}
|
|
4237
|
-
return row;
|
|
4238
|
-
}
|
|
4239
|
-
async removeMember(organizationId, userId) {
|
|
4240
|
-
const rows = await connection_default`
|
|
4241
|
-
DELETE FROM organization_member
|
|
4242
|
-
WHERE organization_id = ${organizationId} AND user_id = ${userId}
|
|
4243
|
-
RETURNING id
|
|
4244
|
-
`;
|
|
4245
|
-
return rows.length > 0;
|
|
4246
|
-
}
|
|
4247
|
-
}
|
|
4248
|
-
var memberRepository_default = OrganizationMemberRepository;
|
|
4249
|
-
|
|
4250
|
-
// ../../src/core/auth/accessControl.ts
|
|
4251
|
-
function isGlobalAdmin(user) {
|
|
4252
|
-
return user?.role === "admin";
|
|
4253
|
-
}
|
|
4254
|
-
function resolveUserId(user) {
|
|
4255
|
-
const userId = typeof user.id === "number" ? user.id : Number(user.id);
|
|
4256
|
-
if (!Number.isInteger(userId) || userId <= 0) {
|
|
4257
|
-
throw new ForbiddenError("Invalid authenticated user.");
|
|
4258
|
-
}
|
|
4259
|
-
return userId;
|
|
4260
|
-
}
|
|
4261
|
-
|
|
4262
|
-
// ../../src/core/auth/membershipContext.ts
|
|
4263
|
-
var membershipContext = new AsyncLocalStorage4;
|
|
4264
|
-
var membershipRepository = new memberRepository_default;
|
|
4265
|
-
async function runWithMembershipContext(callback) {
|
|
4266
|
-
const user = currentAuthUser();
|
|
4267
|
-
if (!user || isGlobalAdmin(user)) {
|
|
4268
|
-
return await callback();
|
|
4269
|
-
}
|
|
4270
|
-
const memberships = await membershipRepository.listForUser(resolveUserId(user));
|
|
4271
|
-
const context = {
|
|
4272
|
-
organizationIds: memberships.map((membership) => membership.organization_id),
|
|
4273
|
-
rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
|
|
4274
|
-
};
|
|
4275
|
-
return await membershipContext.run(context, callback);
|
|
4276
|
-
}
|
|
4277
|
-
|
|
4278
|
-
// ../../src/core/auth/membershipContextMiddleware.ts
|
|
4279
|
-
function createMembershipContextMiddleware() {
|
|
4280
|
-
return async (_request, next) => {
|
|
4281
|
-
return await runWithMembershipContext(async () => await next());
|
|
4282
|
-
};
|
|
4283
|
-
}
|
|
4284
|
-
|
|
4285
|
-
// ../../src/core/auth/membershipMiddleware.ts
|
|
4286
|
-
function createMembershipMiddleware() {
|
|
4287
|
-
return createMembershipContextMiddleware();
|
|
4288
|
-
}
|
|
4289
|
-
|
|
4290
|
-
// ../../src/core/http/authMiddleware.ts
|
|
4291
|
-
function createAuthMiddleware(auth) {
|
|
4292
|
-
return async (request, next) => {
|
|
4293
|
-
const user = await auth.resolve(request);
|
|
4294
|
-
return await runWithAuthUser(user, async () => {
|
|
4295
|
-
const response = await next();
|
|
4296
|
-
if (user) {
|
|
4297
|
-
const headers = new Headers(response.headers);
|
|
4298
|
-
headers.set("x-authenticated-user-id", String(user.id));
|
|
4299
|
-
return new Response(response.body, {
|
|
4300
|
-
status: response.status,
|
|
4301
|
-
statusText: response.statusText,
|
|
4302
|
-
headers
|
|
4303
|
-
});
|
|
4304
|
-
}
|
|
4305
|
-
return response;
|
|
4306
|
-
});
|
|
4307
|
-
};
|
|
4308
|
-
}
|
|
4309
|
-
|
|
4310
|
-
// ../../src/core/http/authorizeMiddleware.ts
|
|
4311
|
-
function createAuthorizeMiddleware(gate, auth, resource, action) {
|
|
4312
|
-
return async (request, next) => {
|
|
4313
|
-
const user = await auth.resolve(request);
|
|
4314
|
-
if (!gate.allows(resource, action, user)) {
|
|
4315
|
-
const error = new ForbiddenError;
|
|
4316
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
4317
|
-
}
|
|
4318
|
-
return await next();
|
|
4319
|
-
};
|
|
4320
|
-
}
|
|
4321
|
-
|
|
4322
|
-
// ../../src/core/http/bodySizeLimitMiddleware.ts
|
|
4323
|
-
var DEFAULT_MAX_BODY_BYTES = 1048576;
|
|
4324
|
-
function resolveMaxBodyBytes() {
|
|
4325
|
-
const raw = process.env.MAX_REQUEST_BODY_BYTES?.trim();
|
|
4326
|
-
if (!raw) {
|
|
4327
|
-
return DEFAULT_MAX_BODY_BYTES;
|
|
4328
|
-
}
|
|
4329
|
-
const parsed = Number.parseInt(raw, 10);
|
|
4330
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
4331
|
-
return DEFAULT_MAX_BODY_BYTES;
|
|
4332
|
-
}
|
|
4333
|
-
return parsed;
|
|
4334
|
-
}
|
|
4335
|
-
function createBodySizeLimitMiddleware(maxBytes = resolveMaxBodyBytes()) {
|
|
4336
|
-
return async (request, next) => {
|
|
4337
|
-
const contentLength = request.headers.get("content-length");
|
|
4338
|
-
if (contentLength) {
|
|
4339
|
-
const bytes = Number.parseInt(contentLength, 10);
|
|
4340
|
-
if (Number.isInteger(bytes) && bytes > maxBytes) {
|
|
4341
|
-
const error = new PayloadTooLargeError(`Request body exceeds the ${maxBytes} byte limit.`);
|
|
4342
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
4343
|
-
}
|
|
4344
|
-
}
|
|
4345
|
-
return await next();
|
|
4346
|
-
};
|
|
4347
|
-
}
|
|
4348
|
-
|
|
4349
|
-
// ../../src/config/cors.ts
|
|
4350
|
-
var corsConfig = {
|
|
4351
|
-
allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim()).filter(Boolean),
|
|
4352
|
-
allowedMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
|
|
4353
|
-
allowedHeaders: [
|
|
4354
|
-
"Authorization",
|
|
4355
|
-
"Content-Type",
|
|
4356
|
-
"X-Request-Id",
|
|
4357
|
-
"X-Tenant-Id",
|
|
4358
|
-
"X-Authenticated-User-Id",
|
|
4359
|
-
"X-Authenticated-User-Role",
|
|
4360
|
-
"If-Match",
|
|
4361
|
-
"If-None-Match"
|
|
4362
|
-
],
|
|
4363
|
-
maxAgeSeconds: 86400
|
|
4364
|
-
};
|
|
4365
|
-
|
|
4366
|
-
// ../../src/core/http/corsMiddleware.ts
|
|
4367
|
-
function createCorsMiddleware() {
|
|
4368
|
-
return async (request, next) => {
|
|
4369
|
-
if (request.method === "OPTIONS") {
|
|
4370
|
-
return new Response(null, {
|
|
4371
|
-
status: 204,
|
|
4372
|
-
headers: buildCorsHeaders(request)
|
|
4373
|
-
});
|
|
4374
|
-
}
|
|
4375
|
-
const response = await next();
|
|
4376
|
-
const headers = new Headers(response.headers);
|
|
4377
|
-
for (const [key, value] of buildCorsHeaders(request)) {
|
|
4378
|
-
headers.set(key, value);
|
|
4379
|
-
}
|
|
4380
|
-
return new Response(response.body, {
|
|
4381
|
-
status: response.status,
|
|
4382
|
-
statusText: response.statusText,
|
|
4383
|
-
headers
|
|
4384
|
-
});
|
|
4385
|
-
};
|
|
4386
|
-
}
|
|
4387
|
-
function buildCorsHeaders(request) {
|
|
4388
|
-
const headers = new Headers;
|
|
4389
|
-
const origin = request.headers.get("origin");
|
|
4390
|
-
const allowedOrigins = corsConfig.allowedOrigins;
|
|
4391
|
-
const allowOrigin = allowedOrigins.includes("*") || origin && allowedOrigins.includes(origin) ? origin ?? "*" : allowedOrigins[0] ?? "*";
|
|
4392
|
-
headers.set("Access-Control-Allow-Origin", allowOrigin);
|
|
4393
|
-
headers.set("Access-Control-Allow-Methods", corsConfig.allowedMethods.join(", "));
|
|
4394
|
-
headers.set("Access-Control-Allow-Headers", corsConfig.allowedHeaders.join(", "));
|
|
4395
|
-
headers.set("Access-Control-Max-Age", String(corsConfig.maxAgeSeconds));
|
|
4396
|
-
headers.set("Vary", "Origin");
|
|
4397
|
-
return headers;
|
|
4398
|
-
}
|
|
4399
|
-
|
|
4400
|
-
// ../../src/core/http/csrfMiddleware.ts
|
|
4401
|
-
var MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4402
|
-
function appendSetCookie(response, cookie) {
|
|
4403
|
-
const headers = new Headers(response.headers);
|
|
4404
|
-
headers.append("set-cookie", cookie);
|
|
4405
|
-
return new Response(response.body, {
|
|
4406
|
-
status: response.status,
|
|
4407
|
-
statusText: response.statusText,
|
|
4408
|
-
headers
|
|
4409
|
-
});
|
|
4410
|
-
}
|
|
4411
|
-
function createCsrfMiddleware() {
|
|
4412
|
-
return async (request, next) => {
|
|
4413
|
-
const method = request.method.toUpperCase();
|
|
4414
|
-
if (!MUTATING_METHODS.has(method)) {
|
|
4415
|
-
const csrf = resolveCsrfToken(request);
|
|
4416
|
-
const meta = currentRequestMeta();
|
|
4417
|
-
meta.csrfToken = csrf.token;
|
|
4418
|
-
const response = await next();
|
|
4419
|
-
if (!csrf.cookie) {
|
|
4420
|
-
return response;
|
|
4421
|
-
}
|
|
4422
|
-
return appendSetCookie(response, csrf.cookie);
|
|
4423
|
-
}
|
|
4424
|
-
const submitted = await readSubmittedCsrfTokenFromBody(request);
|
|
4425
|
-
if (!verifyCsrfToken(request, submitted)) {
|
|
4426
|
-
throw new ForbiddenError("Invalid or missing CSRF token.");
|
|
4427
|
-
}
|
|
4428
|
-
return await next();
|
|
4429
|
-
};
|
|
4430
|
-
}
|
|
4431
|
-
|
|
4432
|
-
// ../../src/core/http/flashMiddleware.ts
|
|
4433
|
-
function createFlashMiddleware() {
|
|
4434
|
-
return async (request, next) => {
|
|
4435
|
-
const flash = pullFlash(request);
|
|
4436
|
-
const meta = currentRequestMeta();
|
|
4437
|
-
return await runWithRequestMeta({ ...meta, request, flash }, async () => {
|
|
4438
|
-
const response = await next();
|
|
4439
|
-
if (flash) {
|
|
4440
|
-
return withFlashClear(response);
|
|
4441
|
-
}
|
|
4442
|
-
return response;
|
|
4443
|
-
});
|
|
4444
|
-
};
|
|
4445
|
-
}
|
|
4446
|
-
|
|
4447
|
-
// ../../src/core/http/loginThrottleMiddleware.ts
|
|
4448
|
-
var {RedisClient: RedisClient3 } = globalThis.Bun;
|
|
4449
|
-
function resolveLoginIdentity(request) {
|
|
4450
|
-
return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
|
|
4451
|
-
}
|
|
4452
|
-
async function resolveLoginEmail(request) {
|
|
4453
|
-
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
4454
|
-
try {
|
|
4455
|
-
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
4456
|
-
const formData = await request.clone().formData();
|
|
4457
|
-
const email = formData.get("email");
|
|
4458
|
-
return typeof email === "string" ? email.trim().toLowerCase() : "unknown";
|
|
4459
|
-
}
|
|
4460
|
-
const payload = await request.clone().json();
|
|
4461
|
-
return typeof payload.email === "string" ? payload.email.trim().toLowerCase() : "unknown";
|
|
4462
|
-
} catch {
|
|
4463
|
-
return "unknown";
|
|
4464
|
-
}
|
|
4465
|
-
}
|
|
4466
|
-
function createLoginThrottleMiddleware(options) {
|
|
4467
|
-
const client = new RedisClient3(options.redisUrl);
|
|
4468
|
-
const prefix = options.keyPrefix ?? "workhub:login-throttle:";
|
|
4469
|
-
return async (request, next) => {
|
|
4470
|
-
const identity = resolveLoginIdentity(request);
|
|
4471
|
-
const email = await resolveLoginEmail(request);
|
|
4472
|
-
const throttleKey = `${prefix}${identity}:${email}`;
|
|
4473
|
-
const attempts = Number(await client.incr(throttleKey));
|
|
4474
|
-
if (attempts === 1) {
|
|
4475
|
-
await client.expire(throttleKey, options.decaySeconds);
|
|
4476
|
-
}
|
|
4477
|
-
if (attempts > options.maxAttempts) {
|
|
4478
|
-
return Response.json({ error: "Too many login attempts. Try again later." }, {
|
|
4479
|
-
status: 429,
|
|
4480
|
-
headers: {
|
|
4481
|
-
"retry-after": String(options.decaySeconds)
|
|
4482
|
-
}
|
|
4483
|
-
});
|
|
4484
|
-
}
|
|
4485
|
-
return await next();
|
|
4486
|
-
};
|
|
4487
|
-
}
|
|
4488
|
-
|
|
4489
|
-
// ../../src/core/http/memoryThrottleMiddleware.ts
|
|
4490
|
-
var buckets = new Map;
|
|
4491
|
-
function createMemoryThrottleMiddleware(options) {
|
|
4492
|
-
const prefix = options.keyPrefix ?? "workhub:memory-throttle:";
|
|
4493
|
-
return async (request, next) => {
|
|
4494
|
-
const path = new URL(request.url).pathname;
|
|
4495
|
-
const identity = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("authorization")?.slice(0, 32) ?? "unknown";
|
|
4496
|
-
const key = `${prefix}${identity}:${path}`;
|
|
4497
|
-
const now = Date.now();
|
|
4498
|
-
const existing = buckets.get(key);
|
|
4499
|
-
if (!existing || existing.resetAt <= now) {
|
|
4500
|
-
buckets.set(key, { count: 1, resetAt: now + options.decaySeconds * 1000 });
|
|
4501
|
-
return await next();
|
|
4502
|
-
}
|
|
4503
|
-
existing.count += 1;
|
|
4504
|
-
if (existing.count > options.maxAttempts) {
|
|
4505
|
-
return Response.json({ error: "Too many requests." }, {
|
|
4506
|
-
status: 429,
|
|
4507
|
-
headers: {
|
|
4508
|
-
"retry-after": String(options.decaySeconds)
|
|
4509
|
-
}
|
|
4510
|
-
});
|
|
4511
|
-
}
|
|
4512
|
-
return await next();
|
|
4513
|
-
};
|
|
4514
|
-
}
|
|
4515
|
-
|
|
4516
|
-
// ../../src/core/metrics/prometheus.ts
|
|
4517
|
-
class PrometheusRegistry {
|
|
4518
|
-
httpRequestsTotal = new Map;
|
|
4519
|
-
httpRequestDurationMs = new Map;
|
|
4520
|
-
incrementHttpRequest(labels) {
|
|
4521
|
-
const key = this.metricKey(labels);
|
|
4522
|
-
this.httpRequestsTotal.set(key, (this.httpRequestsTotal.get(key) ?? 0) + 1);
|
|
4523
|
-
}
|
|
4524
|
-
observeHttpDuration(labels, durationMs) {
|
|
4525
|
-
const key = this.metricKey(labels);
|
|
4526
|
-
const samples = this.httpRequestDurationMs.get(key) ?? [];
|
|
4527
|
-
samples.push(durationMs);
|
|
4528
|
-
this.httpRequestDurationMs.set(key, samples);
|
|
4529
|
-
}
|
|
4530
|
-
renderMetrics() {
|
|
4531
|
-
const lines = [
|
|
4532
|
-
"# HELP http_requests_total Total HTTP requests processed.",
|
|
4533
|
-
"# TYPE http_requests_total counter"
|
|
4534
|
-
];
|
|
4535
|
-
for (const [key, value] of this.httpRequestsTotal) {
|
|
4536
|
-
lines.push(`http_requests_total{${key}} ${value}`);
|
|
4537
|
-
}
|
|
4538
|
-
lines.push("# HELP http_request_duration_ms_sum Sum of HTTP request durations in milliseconds.", "# TYPE http_request_duration_ms_sum counter");
|
|
4539
|
-
for (const [key, samples] of this.httpRequestDurationMs) {
|
|
4540
|
-
const sum = samples.reduce((total, sample) => total + sample, 0);
|
|
4541
|
-
lines.push(`http_request_duration_ms_sum{${key}} ${sum}`);
|
|
4542
|
-
}
|
|
4543
|
-
return `${lines.join(`
|
|
4544
|
-
`)}
|
|
4545
|
-
`;
|
|
4546
|
-
}
|
|
4547
|
-
resetForTests() {
|
|
4548
|
-
this.httpRequestsTotal.clear();
|
|
4549
|
-
this.httpRequestDurationMs.clear();
|
|
4550
|
-
}
|
|
4551
|
-
getHttpRequestSummary() {
|
|
4552
|
-
const byStatus = {};
|
|
4553
|
-
const pathCounts = new Map;
|
|
4554
|
-
let totalRequests = 0;
|
|
4555
|
-
for (const [key, count] of this.httpRequestsTotal) {
|
|
4556
|
-
totalRequests += count;
|
|
4557
|
-
const method = key.match(/method="([^"]+)"/)?.[1] ?? "GET";
|
|
4558
|
-
const path = key.match(/path="([^"]+)"/)?.[1] ?? "/";
|
|
4559
|
-
const status = key.match(/status="([^"]+)"/)?.[1] ?? "200";
|
|
4560
|
-
byStatus[status] = (byStatus[status] ?? 0) + count;
|
|
4561
|
-
const pathKey = `${method} ${path}`;
|
|
4562
|
-
const existing = pathCounts.get(pathKey);
|
|
4563
|
-
if (existing) {
|
|
4564
|
-
existing.count += count;
|
|
4565
|
-
} else {
|
|
4566
|
-
pathCounts.set(pathKey, { method, path, count });
|
|
4567
|
-
}
|
|
4568
|
-
}
|
|
4569
|
-
const topPaths = Array.from(pathCounts.values()).sort((left, right) => right.count - left.count).slice(0, 10);
|
|
4570
|
-
return {
|
|
4571
|
-
totalRequests,
|
|
4572
|
-
byStatus,
|
|
4573
|
-
topPaths
|
|
4574
|
-
};
|
|
4575
|
-
}
|
|
4576
|
-
metricKey(labels) {
|
|
4577
|
-
return `method="${labels.method}",path="${labels.path}",status="${labels.status}"`;
|
|
4578
|
-
}
|
|
4579
|
-
}
|
|
4580
|
-
var prometheusRegistry = new PrometheusRegistry;
|
|
4581
|
-
|
|
4582
|
-
// ../../src/core/http/metricsMiddleware.ts
|
|
4583
|
-
function normalizeMetricPath(pathname) {
|
|
4584
|
-
return pathname.replace(/\/\d+/g, "/:id").replace(/\/[0-9a-f-]{36}/gi, "/:id");
|
|
4585
|
-
}
|
|
4586
|
-
function createMetricsMiddleware() {
|
|
4587
|
-
return async (request, next) => {
|
|
4588
|
-
const startedAt = performance.now();
|
|
4589
|
-
const response = await next();
|
|
4590
|
-
const durationMs = performance.now() - startedAt;
|
|
4591
|
-
const path = normalizeMetricPath(new URL(request.url).pathname);
|
|
4592
|
-
const labels = {
|
|
4593
|
-
method: request.method,
|
|
4594
|
-
path,
|
|
4595
|
-
status: String(response.status)
|
|
4596
|
-
};
|
|
4597
|
-
prometheusRegistry.incrementHttpRequest(labels);
|
|
4598
|
-
prometheusRegistry.observeHttpDuration(labels, durationMs);
|
|
4599
|
-
return response;
|
|
4600
|
-
};
|
|
4601
|
-
}
|
|
4602
|
-
|
|
4603
|
-
// ../../src/core/http/requireAbilityMiddleware.ts
|
|
4604
|
-
function createRequireAbilityMiddleware(abilityChecker) {
|
|
4605
|
-
return (ability) => {
|
|
4606
|
-
return async (_request, next) => {
|
|
4607
|
-
const user = currentAuthUser();
|
|
4608
|
-
try {
|
|
4609
|
-
abilityChecker.requireAbility(user, ability);
|
|
4610
|
-
} catch (error) {
|
|
4611
|
-
if (error instanceof ForbiddenError) {
|
|
4612
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
4613
|
-
}
|
|
4614
|
-
throw error;
|
|
4615
|
-
}
|
|
4616
|
-
return await next();
|
|
4617
|
-
};
|
|
4618
|
-
};
|
|
4619
|
-
}
|
|
4620
|
-
|
|
4621
|
-
// ../../src/core/http/requireAuthMiddleware.ts
|
|
4622
|
-
function createRequireAuthMiddleware(auth) {
|
|
4623
|
-
return async (request, next) => {
|
|
4624
|
-
if (!await auth.check(request)) {
|
|
4625
|
-
const error = new UnauthorizedError;
|
|
4626
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
4627
|
-
}
|
|
4628
|
-
return await next();
|
|
4629
|
-
};
|
|
4630
|
-
}
|
|
4631
|
-
|
|
4632
|
-
// ../../src/core/security/securityEvents.ts
|
|
4633
|
-
function logSecurityEvent2(event, details = {}) {
|
|
4634
|
-
const meta = currentRequestMeta();
|
|
4635
|
-
const user = currentAuthUser();
|
|
4636
|
-
console.log(JSON.stringify({
|
|
4637
|
-
level: "security",
|
|
4638
|
-
event,
|
|
4639
|
-
timestamp: new Date().toISOString(),
|
|
4640
|
-
ip_address: meta.ipAddress ?? null,
|
|
4641
|
-
user_agent: meta.userAgent ?? null,
|
|
4642
|
-
user_id: user?.id ?? null,
|
|
4643
|
-
...details
|
|
4644
|
-
}));
|
|
4645
|
-
}
|
|
4646
|
-
|
|
4647
|
-
// ../../src/core/http/requireGlobalAdminMiddleware.ts
|
|
4648
|
-
function createRequireGlobalAdminMiddleware() {
|
|
4649
|
-
return async (_request, next) => {
|
|
4650
|
-
const user = currentAuthUser();
|
|
4651
|
-
if (!isGlobalAdmin(user)) {
|
|
4652
|
-
logSecurityEvent2("privilege_escalation_blocked", {
|
|
4653
|
-
required_role: "platform_admin",
|
|
4654
|
-
path: new URL(_request.url).pathname
|
|
4655
|
-
});
|
|
4656
|
-
const error = new ForbiddenError("Platform admin access required.");
|
|
4657
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
4658
|
-
}
|
|
4659
|
-
return await next();
|
|
4660
|
-
};
|
|
4661
|
-
}
|
|
4662
|
-
|
|
4663
|
-
// ../../src/core/http/contentNegotiation.ts
|
|
4664
|
-
function requestPrefersJson(request) {
|
|
4665
|
-
if (!request) {
|
|
4666
|
-
return true;
|
|
4667
|
-
}
|
|
4668
|
-
if (request.headers.get("HX-Request") === "true") {
|
|
4669
|
-
return false;
|
|
4670
|
-
}
|
|
4671
|
-
const accept = request.headers.get("accept")?.toLowerCase() ?? "";
|
|
4672
|
-
if (accept.includes("text/html")) {
|
|
4673
|
-
return false;
|
|
4674
|
-
}
|
|
4675
|
-
if (accept.includes("application/json")) {
|
|
4676
|
-
return true;
|
|
4677
|
-
}
|
|
4678
|
-
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
4679
|
-
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
4680
|
-
return false;
|
|
4681
|
-
}
|
|
4682
|
-
const pathname = new URL(request.url).pathname;
|
|
4683
|
-
return pathname.startsWith("/api/");
|
|
4684
|
-
}
|
|
4685
|
-
|
|
4686
|
-
// ../../src/core/http/requireWebAuthMiddleware.ts
|
|
4687
|
-
function createRequireWebAuthMiddleware(auth) {
|
|
4688
|
-
return async (request, next) => {
|
|
4689
|
-
const user = await auth.resolve(request);
|
|
4690
|
-
if (user) {
|
|
4691
|
-
return await next();
|
|
4692
|
-
}
|
|
4693
|
-
if (requestPrefersJson(request)) {
|
|
4694
|
-
throw new UnauthorizedError;
|
|
4695
|
-
}
|
|
4696
|
-
const redirectTarget = encodeURIComponent(new URL(request.url).pathname);
|
|
4697
|
-
return Response.redirect(`/login?redirect=${redirectTarget}`, 302);
|
|
4698
|
-
};
|
|
4699
|
-
}
|
|
4700
|
-
|
|
4701
|
-
// ../../src/core/http/routeMiddleware.ts
|
|
4702
|
-
function withMiddleware(...middleware) {
|
|
4703
|
-
const wrap = composeMiddleware(...middleware);
|
|
4704
|
-
return (handler) => {
|
|
4705
|
-
return wrap(handler);
|
|
4706
|
-
};
|
|
4707
|
-
}
|
|
4708
|
-
|
|
4709
|
-
// ../../src/config/contentSecurityPolicy.ts
|
|
4710
|
-
function strictApiContentSecurityPolicy() {
|
|
4711
|
-
return "default-src 'none'; frame-ancestors 'none'; base-uri 'none'";
|
|
4712
|
-
}
|
|
4713
|
-
function serverHtmxContentSecurityPolicy() {
|
|
4714
|
-
return [
|
|
4715
|
-
"default-src 'self'",
|
|
4716
|
-
"script-src 'self' https://unpkg.com",
|
|
4717
|
-
"style-src 'self'",
|
|
4718
|
-
"connect-src 'self'",
|
|
4719
|
-
"img-src 'self'",
|
|
4720
|
-
"font-src 'self'",
|
|
4721
|
-
"form-action 'self'",
|
|
4722
|
-
"frame-ancestors 'none'",
|
|
4723
|
-
"base-uri 'self'"
|
|
4724
|
-
].join("; ");
|
|
4725
|
-
}
|
|
4726
|
-
function spaContentSecurityPolicy() {
|
|
4727
|
-
return [
|
|
4728
|
-
"default-src 'self'",
|
|
4729
|
-
"script-src 'self'",
|
|
4730
|
-
"style-src 'self'",
|
|
4731
|
-
"connect-src 'self'",
|
|
4732
|
-
"img-src 'self'",
|
|
4733
|
-
"font-src 'self'",
|
|
4734
|
-
"frame-ancestors 'none'",
|
|
4735
|
-
"base-uri 'self'"
|
|
4736
|
-
].join("; ");
|
|
4737
|
-
}
|
|
4738
|
-
function resolveContentSecurityPolicy(response) {
|
|
4739
|
-
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
4740
|
-
if (!contentType.includes("text/html")) {
|
|
4741
|
-
return strictApiContentSecurityPolicy();
|
|
4742
|
-
}
|
|
4743
|
-
const frontendMode = (process.env.FRONTEND_MODE ?? "api").trim();
|
|
4744
|
-
if (frontendMode === "server-htmx") {
|
|
4745
|
-
return serverHtmxContentSecurityPolicy();
|
|
4746
|
-
}
|
|
4747
|
-
if (frontendMode === "spa-react") {
|
|
4748
|
-
return spaContentSecurityPolicy();
|
|
4749
|
-
}
|
|
4750
|
-
return strictApiContentSecurityPolicy();
|
|
4751
|
-
}
|
|
4752
|
-
|
|
4753
|
-
// ../../src/core/http/securityHeadersMiddleware.ts
|
|
4754
|
-
function createSecurityHeadersMiddleware() {
|
|
4755
|
-
return async (_request, next) => {
|
|
4756
|
-
const response = await next();
|
|
4757
|
-
const headers = new Headers(response.headers);
|
|
4758
|
-
headers.set("X-Content-Type-Options", "nosniff");
|
|
4759
|
-
headers.set("X-Frame-Options", "DENY");
|
|
4760
|
-
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
4761
|
-
headers.set("X-XSS-Protection", "0");
|
|
4762
|
-
headers.set("Content-Security-Policy", resolveContentSecurityPolicy(response));
|
|
4763
|
-
if (appConfig.env === "production") {
|
|
4764
|
-
headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
|
4765
|
-
}
|
|
4766
|
-
return new Response(response.body, {
|
|
4767
|
-
status: response.status,
|
|
4768
|
-
statusText: response.statusText,
|
|
4769
|
-
headers
|
|
4770
|
-
});
|
|
4771
|
-
};
|
|
4772
|
-
}
|
|
4773
|
-
|
|
4774
|
-
// ../../src/core/http/throttleMiddleware.ts
|
|
4775
|
-
var {RedisClient: RedisClient4 } = globalThis.Bun;
|
|
4776
|
-
|
|
4777
|
-
// ../../src/core/tenant/tenantContext.ts
|
|
4778
|
-
import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
|
|
4779
|
-
var tenantContext = new AsyncLocalStorage5;
|
|
4780
|
-
function runWithTenant(tenant, callback) {
|
|
4781
|
-
return tenantContext.run(tenant, callback);
|
|
4782
|
-
}
|
|
4783
|
-
function currentTenant() {
|
|
4784
|
-
return tenantContext.getStore() ?? null;
|
|
4785
|
-
}
|
|
4786
|
-
function rateLimitMultiplierForPlan(plan) {
|
|
4787
|
-
switch (plan) {
|
|
4788
|
-
case "enterprise":
|
|
4789
|
-
return 4;
|
|
4790
|
-
case "pro":
|
|
4791
|
-
return 2;
|
|
4792
|
-
default:
|
|
4793
|
-
return 1;
|
|
4794
|
-
}
|
|
4795
|
-
}
|
|
4796
|
-
|
|
4797
|
-
// ../../src/core/http/throttleMiddleware.ts
|
|
4798
|
-
function resolveThrottleIdentity(request) {
|
|
4799
|
-
const user = currentAuthUser();
|
|
4800
|
-
if (user?.tokenId !== undefined) {
|
|
4801
|
-
return `token:${user.tokenId}`;
|
|
4802
|
-
}
|
|
4803
|
-
if (user) {
|
|
4804
|
-
return `user:${user.id}`;
|
|
4805
|
-
}
|
|
4806
|
-
return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
|
|
4807
|
-
}
|
|
4808
|
-
function createThrottleMiddleware(options) {
|
|
4809
|
-
const client = new RedisClient4(options.redisUrl);
|
|
4810
|
-
const prefix = options.keyPrefix ?? "workhub:throttle:";
|
|
4811
|
-
return async (request, next) => {
|
|
4812
|
-
const identity = resolveThrottleIdentity(request);
|
|
4813
|
-
const path = new URL(request.url).pathname;
|
|
4814
|
-
const throttleKey = `${prefix}${identity}:${path}`;
|
|
4815
|
-
const attempts = Number(await client.incr(throttleKey));
|
|
4816
|
-
if (attempts === 1) {
|
|
4817
|
-
await client.expire(throttleKey, options.decaySeconds);
|
|
4818
|
-
}
|
|
4819
|
-
const maxAttempts = options.maxAttempts * rateLimitMultiplierForPlan(currentTenant()?.plan ?? "free");
|
|
4820
|
-
if (attempts > maxAttempts) {
|
|
4821
|
-
return Response.json({ error: "Too many requests." }, {
|
|
4822
|
-
status: 429,
|
|
4823
|
-
headers: {
|
|
4824
|
-
"retry-after": String(options.decaySeconds)
|
|
4825
|
-
}
|
|
4826
|
-
});
|
|
4827
|
-
}
|
|
4828
|
-
return await next();
|
|
4829
|
-
};
|
|
4830
|
-
}
|
|
4831
|
-
|
|
4832
|
-
// ../../src/core/logging/requestLoggingMiddleware.ts
|
|
4833
|
-
function createRequestLoggingMiddleware() {
|
|
4834
|
-
return async (request, next) => {
|
|
4835
|
-
return await runWithRequestMeta({
|
|
4836
|
-
ipAddress: request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip"),
|
|
4837
|
-
userAgent: request.headers.get("user-agent"),
|
|
4838
|
-
request
|
|
4839
|
-
}, async () => {
|
|
4840
|
-
const startedAt = performance.now();
|
|
4841
|
-
const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
|
|
4842
|
-
const response = await next();
|
|
4843
|
-
const durationMs = Math.round(performance.now() - startedAt);
|
|
4844
|
-
appLogger.info("HTTP request completed", {
|
|
4845
|
-
requestId,
|
|
4846
|
-
method: request.method,
|
|
4847
|
-
path: new URL(request.url).pathname,
|
|
4848
|
-
status: response.status,
|
|
4849
|
-
durationMs
|
|
4850
|
-
});
|
|
4851
|
-
return response;
|
|
4852
|
-
});
|
|
4853
|
-
};
|
|
4854
|
-
}
|
|
4855
|
-
|
|
4856
|
-
// ../../src/core/security/publicReads.ts
|
|
4857
|
-
function isPublicReadsEnabled() {
|
|
4858
|
-
return isFeatureEnabled("publicReads");
|
|
4859
|
-
}
|
|
4860
|
-
|
|
4861
|
-
// ../../src/core/tenant/resolveTenant.ts
|
|
4862
|
-
async function resolveTenant(tenantId) {
|
|
4863
|
-
const rows = await connection_default`
|
|
4864
|
-
SELECT id, slug, plan, region
|
|
4865
|
-
FROM tenant
|
|
4866
|
-
WHERE id = ${tenantId}
|
|
4867
|
-
LIMIT 1
|
|
4868
|
-
`;
|
|
4869
|
-
const row = rows[0];
|
|
4870
|
-
return row ? { id: row.id, slug: row.slug, plan: row.plan, region: row.region ?? "eu" } : null;
|
|
4871
|
-
}
|
|
4872
|
-
|
|
4873
|
-
// ../../src/core/tenant/tenantDatabaseScope.ts
|
|
4874
|
-
async function applyTenantContextToTransaction(transaction, tenantId) {
|
|
4875
|
-
await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
|
|
4876
|
-
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
|
|
4877
|
-
}
|
|
4878
|
-
async function runWithTenantDatabase(tenant, callback) {
|
|
4879
|
-
return await getDatabase().begin(async (transaction) => {
|
|
4880
|
-
await applyTenantContextToTransaction(transaction, tenant.id);
|
|
4881
|
-
return await runWithDatabaseConnection(transaction, async () => {
|
|
4882
|
-
return await runWithTenant(tenant, callback);
|
|
4883
|
-
});
|
|
4884
|
-
});
|
|
4885
|
-
}
|
|
4886
|
-
|
|
4887
|
-
// ../../src/core/tenant/tenantMiddleware.ts
|
|
4888
|
-
var DEFAULT_TENANT = {
|
|
4889
|
-
id: 1,
|
|
4890
|
-
slug: "default",
|
|
4891
|
-
plan: "enterprise",
|
|
4892
|
-
region: "eu"
|
|
4893
|
-
};
|
|
4894
|
-
async function resolveUserTenantId(userId) {
|
|
4895
|
-
return await runWithMigrationBypass(async () => {
|
|
4896
|
-
const rows = await connection_default`
|
|
4897
|
-
SELECT tenant_id
|
|
4898
|
-
FROM users
|
|
4899
|
-
WHERE id = ${userId}
|
|
4900
|
-
LIMIT 1
|
|
4901
|
-
`;
|
|
4902
|
-
return rows[0]?.tenant_id ?? DEFAULT_TENANT.id;
|
|
4903
|
-
});
|
|
4904
|
-
}
|
|
4905
|
-
async function resolveTenantForRequest(request) {
|
|
4906
|
-
const user = currentAuthUser();
|
|
4907
|
-
const headerValue = request.headers.get("x-tenant-id")?.trim();
|
|
4908
|
-
const parsedHeader = headerValue !== undefined && headerValue.length > 0 ? Number.parseInt(headerValue, 10) : Number.NaN;
|
|
4909
|
-
if (user) {
|
|
4910
|
-
const userId = typeof user.id === "number" ? user.id : Number.parseInt(String(user.id), 10);
|
|
4911
|
-
if (Number.isInteger(userId) && userId > 0) {
|
|
4912
|
-
const userTenantId = await resolveUserTenantId(userId);
|
|
4913
|
-
if (!isGlobalAdmin(user)) {
|
|
4914
|
-
if (Number.isInteger(parsedHeader) && parsedHeader > 0 && parsedHeader !== userTenantId) {
|
|
4915
|
-
throw new ForbiddenError("Tenant header does not match your account.");
|
|
4916
|
-
}
|
|
4917
|
-
return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
|
|
4918
|
-
}
|
|
4919
|
-
if (Number.isInteger(parsedHeader) && parsedHeader > 0) {
|
|
4920
|
-
return await resolveTenant(parsedHeader) ?? DEFAULT_TENANT;
|
|
4921
|
-
}
|
|
4922
|
-
return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
|
|
4923
|
-
}
|
|
4924
|
-
}
|
|
4925
|
-
const tenantId = Number.isInteger(parsedHeader) && parsedHeader > 0 ? parsedHeader : DEFAULT_TENANT.id;
|
|
4926
|
-
return await resolveTenant(tenantId) ?? DEFAULT_TENANT;
|
|
4927
|
-
}
|
|
4928
|
-
function createTenantMiddleware() {
|
|
4929
|
-
return async (request, next) => {
|
|
4930
|
-
try {
|
|
4931
|
-
const tenant = await resolveTenantForRequest(request);
|
|
4932
|
-
return await runWithTenantDatabase(tenant, async () => {
|
|
4933
|
-
const response = await next();
|
|
4934
|
-
const headers = new Headers(response.headers);
|
|
4935
|
-
headers.set("x-tenant-id", String(tenant.id));
|
|
4936
|
-
headers.set("x-tenant-region", tenant.region);
|
|
4937
|
-
return new Response(response.body, {
|
|
4938
|
-
status: response.status,
|
|
4939
|
-
statusText: response.statusText,
|
|
4940
|
-
headers
|
|
4941
|
-
});
|
|
4942
|
-
});
|
|
4943
|
-
} catch (error) {
|
|
4944
|
-
if (error instanceof HttpError) {
|
|
4945
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
4946
|
-
}
|
|
4947
|
-
throw error;
|
|
4948
|
-
}
|
|
4949
|
-
};
|
|
4950
|
-
}
|
|
4951
|
-
|
|
4952
|
-
// ../../src/core/tracing/otel.ts
|
|
4953
|
-
import { randomBytes } from "crypto";
|
|
4954
|
-
function randomHex(bytes) {
|
|
4955
|
-
return randomBytes(bytes).toString("hex");
|
|
4956
|
-
}
|
|
4957
|
-
function createSpan(input) {
|
|
4958
|
-
const spanId = randomHex(8);
|
|
4959
|
-
return {
|
|
4960
|
-
traceId: input.traceId,
|
|
4961
|
-
spanId,
|
|
4962
|
-
name: input.name,
|
|
4963
|
-
startTimeUnixNano: String(Math.floor(input.startedAt * 1e6)),
|
|
4964
|
-
endTimeUnixNano: String(Math.floor(input.endedAt * 1e6)),
|
|
4965
|
-
attributes: Object.entries(input.attributes ?? {}).map(([key, value]) => ({
|
|
4966
|
-
key,
|
|
4967
|
-
value: { stringValue: value }
|
|
4968
|
-
})),
|
|
4969
|
-
status: { code: 1 }
|
|
4970
|
-
};
|
|
4971
|
-
}
|
|
4972
|
-
async function exportOtelSpan(span) {
|
|
4973
|
-
const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
|
|
4974
|
-
if (!endpoint) {
|
|
4975
|
-
return;
|
|
4976
|
-
}
|
|
4977
|
-
const serviceName = process.env.OTEL_SERVICE_NAME?.trim() ?? "workhub-api";
|
|
4978
|
-
const url = endpoint.endsWith("/v1/traces") ? endpoint : `${endpoint.replace(/\/$/, "")}/v1/traces`;
|
|
4979
|
-
await fetch(url, {
|
|
4980
|
-
method: "POST",
|
|
4981
|
-
headers: { "content-type": "application/json" },
|
|
4982
|
-
body: JSON.stringify({
|
|
4983
|
-
resourceSpans: [
|
|
4984
|
-
{
|
|
4985
|
-
resource: {
|
|
4986
|
-
attributes: [{ key: "service.name", value: { stringValue: serviceName } }]
|
|
4987
|
-
},
|
|
4988
|
-
scopeSpans: [{ spans: [span] }]
|
|
4989
|
-
}
|
|
4990
|
-
]
|
|
4991
|
-
})
|
|
4992
|
-
});
|
|
4993
|
-
}
|
|
4994
|
-
|
|
4995
|
-
// ../../src/core/tracing/traceContext.ts
|
|
4996
|
-
import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
|
|
4997
|
-
var traceContextStorage = new AsyncLocalStorage6;
|
|
4998
|
-
function runWithTraceContext(context, callback) {
|
|
4999
|
-
return traceContextStorage.run(context, callback);
|
|
5000
|
-
}
|
|
5001
|
-
|
|
5002
|
-
// ../../src/core/tracing/tracingMiddleware.ts
|
|
5003
|
-
function createTracingMiddleware() {
|
|
5004
|
-
return async (request, next) => {
|
|
5005
|
-
const traceId = (request.headers.get("x-trace-id") ?? crypto.randomUUID()).replace(/-/g, "");
|
|
5006
|
-
const spanId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
|
|
5007
|
-
const startedAt = performance.now();
|
|
5008
|
-
const path = new URL(request.url).pathname;
|
|
5009
|
-
return await runWithTraceContext({ traceId, spanId }, async () => {
|
|
5010
|
-
const response = await next();
|
|
5011
|
-
const endedAt = performance.now();
|
|
5012
|
-
const headers = new Headers(response.headers);
|
|
5013
|
-
headers.set("x-trace-id", traceId);
|
|
5014
|
-
headers.set("x-span-id", spanId);
|
|
5015
|
-
headers.set("traceparent", `00-${traceId}-${spanId}-01`);
|
|
5016
|
-
headers.set("server-timing", `app;dur=${(endedAt - startedAt).toFixed(2)}`);
|
|
5017
|
-
exportOtelSpan(createSpan({
|
|
5018
|
-
traceId,
|
|
5019
|
-
name: `${request.method} ${path}`,
|
|
5020
|
-
startedAt,
|
|
5021
|
-
endedAt,
|
|
5022
|
-
attributes: {
|
|
5023
|
-
"http.method": request.method,
|
|
5024
|
-
"http.route": path,
|
|
5025
|
-
"http.status_code": String(response.status)
|
|
5026
|
-
}
|
|
5027
|
-
})).catch(() => {
|
|
5028
|
-
return;
|
|
5029
|
-
});
|
|
5030
|
-
return new Response(response.body, {
|
|
5031
|
-
status: response.status,
|
|
5032
|
-
statusText: response.statusText,
|
|
5033
|
-
headers
|
|
5034
|
-
});
|
|
5035
|
-
});
|
|
5036
|
-
};
|
|
5037
|
-
}
|
|
5038
|
-
|
|
5039
4285
|
// ../../src/bootstrap/httpKernel.ts
|
|
5040
4286
|
class HttpKernel {
|
|
5041
4287
|
dependencies;
|
|
@@ -5431,7 +4677,7 @@ function createWebServer(options) {
|
|
|
5431
4677
|
});
|
|
5432
4678
|
}
|
|
5433
4679
|
// ../../src/bootstrap/web/session.ts
|
|
5434
|
-
import { createHash, randomBytes
|
|
4680
|
+
import { createHash, randomBytes } from "crypto";
|
|
5435
4681
|
class CookieSessionStore {
|
|
5436
4682
|
sql;
|
|
5437
4683
|
secret;
|
|
@@ -5457,7 +4703,7 @@ class CookieSessionStore {
|
|
|
5457
4703
|
return header.includes("Secure") ? header : `${header}; Secure`;
|
|
5458
4704
|
}
|
|
5459
4705
|
async create(user) {
|
|
5460
|
-
const id =
|
|
4706
|
+
const id = randomBytes(32).toString("hex");
|
|
5461
4707
|
const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
|
|
5462
4708
|
await this.sql.unsafe(`INSERT INTO sessions (id, user_id, expires_at) VALUES ($1, $2, $3)`, [
|
|
5463
4709
|
id,
|
|
@@ -5478,7 +4724,8 @@ class CookieSessionStore {
|
|
|
5478
4724
|
if (!sessionId || !signature || signature !== this.sign(sessionId)) {
|
|
5479
4725
|
return null;
|
|
5480
4726
|
}
|
|
5481
|
-
const rows = await this.sql.unsafe(`SELECT s.id, s.user_id, s.expires_at, u.name, u.email, u.learn_subscriber
|
|
4727
|
+
const rows = await this.sql.unsafe(`SELECT s.id, s.user_id, s.expires_at, u.name, u.email, u.learn_subscriber,
|
|
4728
|
+
COALESCE(u.is_admin, false) AS is_admin
|
|
5482
4729
|
FROM sessions s
|
|
5483
4730
|
INNER JOIN users u ON u.id = s.user_id
|
|
5484
4731
|
WHERE s.id = $1 AND s.expires_at > NOW()`, [sessionId]);
|
|
@@ -5489,7 +4736,8 @@ class CookieSessionStore {
|
|
|
5489
4736
|
id: row.user_id,
|
|
5490
4737
|
name: row.name,
|
|
5491
4738
|
email: row.email,
|
|
5492
|
-
learn_subscriber: row.learn_subscriber
|
|
4739
|
+
learn_subscriber: row.learn_subscriber,
|
|
4740
|
+
is_admin: row.is_admin
|
|
5493
4741
|
};
|
|
5494
4742
|
}
|
|
5495
4743
|
sign(value) {
|