@getstrata/core 0.5.58 → 0.5.60
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/CHANGELOG.md +21 -0
- package/README.md +3 -3
- package/dist/bootstrap/config.d.ts +1 -1
- package/dist/core/auth/sessionCookie.d.ts +2 -1
- package/dist/core/contracts/serviceTokens.d.ts +10 -1
- package/dist/core/database/bindConnection.d.ts +1 -3
- package/dist/core/database/bunSql.d.ts +13 -0
- package/dist/core/http/csrfToken.d.ts +2 -1
- package/dist/core/http/memoryThrottleMiddleware.d.ts +2 -1
- package/dist/core/view/htmlResponse.d.ts +13 -1
- package/dist/core/view/index.d.ts +3 -3
- package/dist/core/view/webLayoutData.d.ts +15 -4
- package/dist/entries/auth/sessionCookie.js +8 -4
- package/dist/entries/auth/sessionGuard.js +44 -5
- package/dist/entries/contracts/serviceTokens.js +43 -1
- package/dist/entries/database/bunSql.js +1 -0
- package/dist/entries/http/csrfMiddleware.js +6 -3
- package/dist/entries/http/csrfToken.js +7 -3
- package/dist/entries/http/memoryThrottleMiddleware.js +10 -2
- package/dist/entries/http/response.js +110 -26
- package/dist/entries/http/webErrorResponse.js +110 -26
- package/dist/entries/queue/createAppQueue.js +2 -0
- package/dist/entries/queue/jobRunner.js +2 -0
- package/dist/entries/queue/publicQueue.js +2 -0
- package/dist/entries/queue/queueMetrics.js +2 -0
- package/dist/entries/queue/redisQueue.js +2 -0
- package/dist/entries/view.js +117 -27
- package/dist/framework/public-api.d.ts +5 -4
- package/dist/index.js +139 -38
- package/package.json +10 -4
|
@@ -297,6 +297,9 @@ class EtaViewEngine {
|
|
|
297
297
|
}
|
|
298
298
|
}
|
|
299
299
|
// ../../src/core/view/htmlResponse.ts
|
|
300
|
+
function withCharset(contentType) {
|
|
301
|
+
return contentType.includes("charset=") ? contentType : `${contentType}; charset=utf-8`;
|
|
302
|
+
}
|
|
300
303
|
function htmlResponse(html, init = {}) {
|
|
301
304
|
return new Response(html, {
|
|
302
305
|
status: init.status ?? 200,
|
|
@@ -309,6 +312,36 @@ function htmlResponse(html, init = {}) {
|
|
|
309
312
|
function isHtmxRequest(request) {
|
|
310
313
|
return request.headers.get("HX-Request") === "true";
|
|
311
314
|
}
|
|
315
|
+
function redirectResponse(location, status = 302) {
|
|
316
|
+
return new Response(null, {
|
|
317
|
+
status,
|
|
318
|
+
headers: {
|
|
319
|
+
Location: location
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
function notFoundHtmlResponse(body = "Not Found") {
|
|
324
|
+
return htmlResponse(body, { status: 404 });
|
|
325
|
+
}
|
|
326
|
+
function textResponse(body, init = {}) {
|
|
327
|
+
return new Response(body, {
|
|
328
|
+
status: init.status ?? 200,
|
|
329
|
+
headers: {
|
|
330
|
+
"Content-Type": "text/plain; charset=utf-8"
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
function xmlResponse(body, init = {}) {
|
|
335
|
+
return new Response(body, {
|
|
336
|
+
status: init.status ?? 200,
|
|
337
|
+
headers: {
|
|
338
|
+
"Content-Type": withCharset(init.contentType ?? "application/xml")
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
function rssResponse(body, init = {}) {
|
|
343
|
+
return xmlResponse(body, { ...init, contentType: "application/rss+xml" });
|
|
344
|
+
}
|
|
312
345
|
// ../../src/core/view/webLayoutData.ts
|
|
313
346
|
import { currentAuthUser } from "@getstrata/core/auth/authContext";
|
|
314
347
|
import { currentRequestMeta as currentRequestMeta2 } from "@getstrata/core/http/requestMetaContext";
|
|
@@ -321,6 +354,42 @@ var CORE_EVENT_BUS_TOKEN = "core.eventBus";
|
|
|
321
354
|
var CORE_POLICY_GATE_TOKEN = "core.policyGate";
|
|
322
355
|
var CORE_AUTH_TOKEN = "core.auth";
|
|
323
356
|
var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
|
|
357
|
+
var CORE_ABILITY_CHECKER_TOKEN = "core.abilityChecker";
|
|
358
|
+
var CORE_AUTH_USER_DIRECTORY_TOKEN = "core.authUserDirectory";
|
|
359
|
+
function isAbilityChecker(value) {
|
|
360
|
+
if (!value || typeof value !== "object") {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
const candidate = value;
|
|
364
|
+
return typeof candidate.tokenCan === "function" && typeof candidate.requireAbility === "function";
|
|
365
|
+
}
|
|
366
|
+
function isAuthUserDirectory(value) {
|
|
367
|
+
if (!value || typeof value !== "object") {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
return typeof value.findByIdOrThrow === "function";
|
|
371
|
+
}
|
|
372
|
+
function resolveAbilityChecker(container) {
|
|
373
|
+
if (container.has(CORE_ABILITY_CHECKER_TOKEN)) {
|
|
374
|
+
return container.resolve(CORE_ABILITY_CHECKER_TOKEN);
|
|
375
|
+
}
|
|
376
|
+
return container.resolve(CORE_TOKEN_SERVICE_TOKEN);
|
|
377
|
+
}
|
|
378
|
+
function resolveAuthUserDirectory(container) {
|
|
379
|
+
if (container.has(CORE_AUTH_USER_DIRECTORY_TOKEN)) {
|
|
380
|
+
const directory = container.resolve(CORE_AUTH_USER_DIRECTORY_TOKEN);
|
|
381
|
+
if (isAuthUserDirectory(directory)) {
|
|
382
|
+
return directory;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (container.has(CORE_TOKEN_SERVICE_TOKEN)) {
|
|
386
|
+
const tokenService = container.resolve(CORE_TOKEN_SERVICE_TOKEN);
|
|
387
|
+
if (isAuthUserDirectory(tokenService)) {
|
|
388
|
+
return tokenService;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
324
393
|
|
|
325
394
|
// ../../src/core/http/csrfToken.ts
|
|
326
395
|
import { timingSafeEqual } from "crypto";
|
|
@@ -382,6 +451,9 @@ function currentRequestMeta() {
|
|
|
382
451
|
// ../../src/core/http/csrfToken.ts
|
|
383
452
|
var CSRF_COOKIE = "workhub_csrf";
|
|
384
453
|
var CSRF_TTL_MS = 60 * 60 * 1000;
|
|
454
|
+
function csrfCookieName() {
|
|
455
|
+
return process.env.CSRF_COOKIE_NAME?.trim() || CSRF_COOKIE;
|
|
456
|
+
}
|
|
385
457
|
function resolveCsrfSecret() {
|
|
386
458
|
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
|
|
387
459
|
}
|
|
@@ -401,11 +473,11 @@ function createCsrfTokenCookie() {
|
|
|
401
473
|
const secure = process.env.APP_ENV === "production" ? "; Secure" : "";
|
|
402
474
|
return {
|
|
403
475
|
token,
|
|
404
|
-
cookie: `${
|
|
476
|
+
cookie: `${csrfCookieName()}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}${secure}`
|
|
405
477
|
};
|
|
406
478
|
}
|
|
407
479
|
function resolveCsrfToken(request) {
|
|
408
|
-
const cookieValue = readRequestCookie(request,
|
|
480
|
+
const cookieValue = readRequestCookie(request, csrfCookieName());
|
|
409
481
|
if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
|
|
410
482
|
return { token: cookieValue };
|
|
411
483
|
}
|
|
@@ -441,7 +513,7 @@ function verifyCsrfToken(request, submittedToken) {
|
|
|
441
513
|
if (!submittedToken) {
|
|
442
514
|
return false;
|
|
443
515
|
}
|
|
444
|
-
const cookieValue = readRequestCookie(request,
|
|
516
|
+
const cookieValue = readRequestCookie(request, csrfCookieName());
|
|
445
517
|
if (!cookieValue) {
|
|
446
518
|
return false;
|
|
447
519
|
}
|
|
@@ -558,44 +630,56 @@ function withFlashClear(response) {
|
|
|
558
630
|
}
|
|
559
631
|
|
|
560
632
|
// ../../src/core/view/webLayoutData.ts
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
633
|
+
var configuredLayoutOptions = {};
|
|
634
|
+
function configureWebLayoutData(options) {
|
|
635
|
+
configuredLayoutOptions = { ...options };
|
|
636
|
+
}
|
|
637
|
+
function layoutUserKey(options) {
|
|
638
|
+
return options.userKey ?? "authUser";
|
|
639
|
+
}
|
|
640
|
+
async function defaultLoadLayoutUser(container, _request) {
|
|
564
641
|
const authUser = currentAuthUser();
|
|
565
642
|
if (!authUser) {
|
|
566
|
-
return
|
|
643
|
+
return null;
|
|
567
644
|
}
|
|
568
645
|
const userId = Number(authUser.id);
|
|
569
646
|
if (!Number.isInteger(userId) || userId <= 0) {
|
|
570
|
-
return
|
|
647
|
+
return null;
|
|
571
648
|
}
|
|
572
|
-
|
|
649
|
+
const directory = resolveAuthUserDirectory(container);
|
|
650
|
+
if (!directory) {
|
|
573
651
|
return {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
role: authUser.role ?? "member"
|
|
578
|
-
},
|
|
579
|
-
csrfToken,
|
|
580
|
-
flash
|
|
652
|
+
id: userId,
|
|
653
|
+
email: "",
|
|
654
|
+
role: authUser.role ?? "member"
|
|
581
655
|
};
|
|
582
656
|
}
|
|
583
|
-
const tokenService = container.resolve(CORE_TOKEN_SERVICE_TOKEN);
|
|
584
657
|
try {
|
|
585
|
-
const user = await
|
|
658
|
+
const user = await directory.findByIdOrThrow(userId);
|
|
586
659
|
return {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
role: authUser.role ?? user.role ?? "member"
|
|
591
|
-
},
|
|
592
|
-
csrfToken,
|
|
593
|
-
flash
|
|
660
|
+
id: userId,
|
|
661
|
+
email: user.email ?? "",
|
|
662
|
+
role: authUser.role ?? user.role ?? "member"
|
|
594
663
|
};
|
|
595
664
|
} catch {
|
|
596
|
-
return
|
|
665
|
+
return null;
|
|
597
666
|
}
|
|
598
667
|
}
|
|
668
|
+
async function resolveWebLayoutData(container, request, options = {}) {
|
|
669
|
+
const resolved = { ...configuredLayoutOptions, ...options };
|
|
670
|
+
const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
|
|
671
|
+
const flash = request ? currentRequestMeta2().flash ?? pullFlash(request) : null;
|
|
672
|
+
const userKey = layoutUserKey(resolved);
|
|
673
|
+
const loadUser = resolved.loadUser ?? defaultLoadLayoutUser;
|
|
674
|
+
const user = await loadUser(container, request);
|
|
675
|
+
const extra = typeof resolved.extra === "function" ? await resolved.extra(user) : resolved.extra ?? {};
|
|
676
|
+
return {
|
|
677
|
+
[userKey]: user,
|
|
678
|
+
csrfToken,
|
|
679
|
+
flash,
|
|
680
|
+
...extra
|
|
681
|
+
};
|
|
682
|
+
}
|
|
599
683
|
// ../../src/core/http/contentNegotiation.ts
|
|
600
684
|
function requestPrefersJson(request) {
|
|
601
685
|
if (!request) {
|
|
@@ -294,6 +294,9 @@ class EtaViewEngine {
|
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
296
|
// ../../src/core/view/htmlResponse.ts
|
|
297
|
+
function withCharset(contentType) {
|
|
298
|
+
return contentType.includes("charset=") ? contentType : `${contentType}; charset=utf-8`;
|
|
299
|
+
}
|
|
297
300
|
function htmlResponse(html, init = {}) {
|
|
298
301
|
return new Response(html, {
|
|
299
302
|
status: init.status ?? 200,
|
|
@@ -306,6 +309,36 @@ function htmlResponse(html, init = {}) {
|
|
|
306
309
|
function isHtmxRequest(request) {
|
|
307
310
|
return request.headers.get("HX-Request") === "true";
|
|
308
311
|
}
|
|
312
|
+
function redirectResponse(location, status = 302) {
|
|
313
|
+
return new Response(null, {
|
|
314
|
+
status,
|
|
315
|
+
headers: {
|
|
316
|
+
Location: location
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
function notFoundHtmlResponse(body = "Not Found") {
|
|
321
|
+
return htmlResponse(body, { status: 404 });
|
|
322
|
+
}
|
|
323
|
+
function textResponse(body, init = {}) {
|
|
324
|
+
return new Response(body, {
|
|
325
|
+
status: init.status ?? 200,
|
|
326
|
+
headers: {
|
|
327
|
+
"Content-Type": "text/plain; charset=utf-8"
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
function xmlResponse(body, init = {}) {
|
|
332
|
+
return new Response(body, {
|
|
333
|
+
status: init.status ?? 200,
|
|
334
|
+
headers: {
|
|
335
|
+
"Content-Type": withCharset(init.contentType ?? "application/xml")
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
function rssResponse(body, init = {}) {
|
|
340
|
+
return xmlResponse(body, { ...init, contentType: "application/rss+xml" });
|
|
341
|
+
}
|
|
309
342
|
// ../../src/core/view/webLayoutData.ts
|
|
310
343
|
import { currentAuthUser } from "@getstrata/core/auth/authContext";
|
|
311
344
|
import { currentRequestMeta as currentRequestMeta2 } from "@getstrata/core/http/requestMetaContext";
|
|
@@ -318,6 +351,42 @@ var CORE_EVENT_BUS_TOKEN = "core.eventBus";
|
|
|
318
351
|
var CORE_POLICY_GATE_TOKEN = "core.policyGate";
|
|
319
352
|
var CORE_AUTH_TOKEN = "core.auth";
|
|
320
353
|
var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
|
|
354
|
+
var CORE_ABILITY_CHECKER_TOKEN = "core.abilityChecker";
|
|
355
|
+
var CORE_AUTH_USER_DIRECTORY_TOKEN = "core.authUserDirectory";
|
|
356
|
+
function isAbilityChecker(value) {
|
|
357
|
+
if (!value || typeof value !== "object") {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
const candidate = value;
|
|
361
|
+
return typeof candidate.tokenCan === "function" && typeof candidate.requireAbility === "function";
|
|
362
|
+
}
|
|
363
|
+
function isAuthUserDirectory(value) {
|
|
364
|
+
if (!value || typeof value !== "object") {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
return typeof value.findByIdOrThrow === "function";
|
|
368
|
+
}
|
|
369
|
+
function resolveAbilityChecker(container) {
|
|
370
|
+
if (container.has(CORE_ABILITY_CHECKER_TOKEN)) {
|
|
371
|
+
return container.resolve(CORE_ABILITY_CHECKER_TOKEN);
|
|
372
|
+
}
|
|
373
|
+
return container.resolve(CORE_TOKEN_SERVICE_TOKEN);
|
|
374
|
+
}
|
|
375
|
+
function resolveAuthUserDirectory(container) {
|
|
376
|
+
if (container.has(CORE_AUTH_USER_DIRECTORY_TOKEN)) {
|
|
377
|
+
const directory = container.resolve(CORE_AUTH_USER_DIRECTORY_TOKEN);
|
|
378
|
+
if (isAuthUserDirectory(directory)) {
|
|
379
|
+
return directory;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (container.has(CORE_TOKEN_SERVICE_TOKEN)) {
|
|
383
|
+
const tokenService = container.resolve(CORE_TOKEN_SERVICE_TOKEN);
|
|
384
|
+
if (isAuthUserDirectory(tokenService)) {
|
|
385
|
+
return tokenService;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
321
390
|
|
|
322
391
|
// ../../src/core/http/csrfToken.ts
|
|
323
392
|
import { timingSafeEqual } from "crypto";
|
|
@@ -379,6 +448,9 @@ function currentRequestMeta() {
|
|
|
379
448
|
// ../../src/core/http/csrfToken.ts
|
|
380
449
|
var CSRF_COOKIE = "workhub_csrf";
|
|
381
450
|
var CSRF_TTL_MS = 60 * 60 * 1000;
|
|
451
|
+
function csrfCookieName() {
|
|
452
|
+
return process.env.CSRF_COOKIE_NAME?.trim() || CSRF_COOKIE;
|
|
453
|
+
}
|
|
382
454
|
function resolveCsrfSecret() {
|
|
383
455
|
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
|
|
384
456
|
}
|
|
@@ -398,11 +470,11 @@ function createCsrfTokenCookie() {
|
|
|
398
470
|
const secure = process.env.APP_ENV === "production" ? "; Secure" : "";
|
|
399
471
|
return {
|
|
400
472
|
token,
|
|
401
|
-
cookie: `${
|
|
473
|
+
cookie: `${csrfCookieName()}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}${secure}`
|
|
402
474
|
};
|
|
403
475
|
}
|
|
404
476
|
function resolveCsrfToken(request) {
|
|
405
|
-
const cookieValue = readRequestCookie(request,
|
|
477
|
+
const cookieValue = readRequestCookie(request, csrfCookieName());
|
|
406
478
|
if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
|
|
407
479
|
return { token: cookieValue };
|
|
408
480
|
}
|
|
@@ -438,7 +510,7 @@ function verifyCsrfToken(request, submittedToken) {
|
|
|
438
510
|
if (!submittedToken) {
|
|
439
511
|
return false;
|
|
440
512
|
}
|
|
441
|
-
const cookieValue = readRequestCookie(request,
|
|
513
|
+
const cookieValue = readRequestCookie(request, csrfCookieName());
|
|
442
514
|
if (!cookieValue) {
|
|
443
515
|
return false;
|
|
444
516
|
}
|
|
@@ -555,44 +627,56 @@ function withFlashClear(response) {
|
|
|
555
627
|
}
|
|
556
628
|
|
|
557
629
|
// ../../src/core/view/webLayoutData.ts
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
630
|
+
var configuredLayoutOptions = {};
|
|
631
|
+
function configureWebLayoutData(options) {
|
|
632
|
+
configuredLayoutOptions = { ...options };
|
|
633
|
+
}
|
|
634
|
+
function layoutUserKey(options) {
|
|
635
|
+
return options.userKey ?? "authUser";
|
|
636
|
+
}
|
|
637
|
+
async function defaultLoadLayoutUser(container, _request) {
|
|
561
638
|
const authUser = currentAuthUser();
|
|
562
639
|
if (!authUser) {
|
|
563
|
-
return
|
|
640
|
+
return null;
|
|
564
641
|
}
|
|
565
642
|
const userId = Number(authUser.id);
|
|
566
643
|
if (!Number.isInteger(userId) || userId <= 0) {
|
|
567
|
-
return
|
|
644
|
+
return null;
|
|
568
645
|
}
|
|
569
|
-
|
|
646
|
+
const directory = resolveAuthUserDirectory(container);
|
|
647
|
+
if (!directory) {
|
|
570
648
|
return {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
role: authUser.role ?? "member"
|
|
575
|
-
},
|
|
576
|
-
csrfToken,
|
|
577
|
-
flash
|
|
649
|
+
id: userId,
|
|
650
|
+
email: "",
|
|
651
|
+
role: authUser.role ?? "member"
|
|
578
652
|
};
|
|
579
653
|
}
|
|
580
|
-
const tokenService = container.resolve(CORE_TOKEN_SERVICE_TOKEN);
|
|
581
654
|
try {
|
|
582
|
-
const user = await
|
|
655
|
+
const user = await directory.findByIdOrThrow(userId);
|
|
583
656
|
return {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
role: authUser.role ?? user.role ?? "member"
|
|
588
|
-
},
|
|
589
|
-
csrfToken,
|
|
590
|
-
flash
|
|
657
|
+
id: userId,
|
|
658
|
+
email: user.email ?? "",
|
|
659
|
+
role: authUser.role ?? user.role ?? "member"
|
|
591
660
|
};
|
|
592
661
|
} catch {
|
|
593
|
-
return
|
|
662
|
+
return null;
|
|
594
663
|
}
|
|
595
664
|
}
|
|
665
|
+
async function resolveWebLayoutData(container, request, options = {}) {
|
|
666
|
+
const resolved = { ...configuredLayoutOptions, ...options };
|
|
667
|
+
const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
|
|
668
|
+
const flash = request ? currentRequestMeta2().flash ?? pullFlash(request) : null;
|
|
669
|
+
const userKey = layoutUserKey(resolved);
|
|
670
|
+
const loadUser = resolved.loadUser ?? defaultLoadLayoutUser;
|
|
671
|
+
const user = await loadUser(container, request);
|
|
672
|
+
const extra = typeof resolved.extra === "function" ? await resolved.extra(user) : resolved.extra ?? {};
|
|
673
|
+
return {
|
|
674
|
+
[userKey]: user,
|
|
675
|
+
csrfToken,
|
|
676
|
+
flash,
|
|
677
|
+
...extra
|
|
678
|
+
};
|
|
679
|
+
}
|
|
596
680
|
// ../../src/core/http/contentNegotiation.ts
|
|
597
681
|
function requestPrefersJson(request) {
|
|
598
682
|
if (!request) {
|
|
@@ -102,7 +102,9 @@ var jobRegistry = readSharedJobRegistry();
|
|
|
102
102
|
|
|
103
103
|
// ../../src/bootstrap/config.ts
|
|
104
104
|
import {
|
|
105
|
+
CORE_ABILITY_CHECKER_TOKEN,
|
|
105
106
|
CORE_AUTH_TOKEN,
|
|
107
|
+
CORE_AUTH_USER_DIRECTORY_TOKEN,
|
|
106
108
|
CORE_CACHE_TOKEN,
|
|
107
109
|
CORE_CONFIG_TOKEN,
|
|
108
110
|
CORE_EVENT_BUS_TOKEN,
|
|
@@ -102,7 +102,9 @@ var jobRegistry = readSharedJobRegistry();
|
|
|
102
102
|
|
|
103
103
|
// ../../src/bootstrap/config.ts
|
|
104
104
|
import {
|
|
105
|
+
CORE_ABILITY_CHECKER_TOKEN,
|
|
105
106
|
CORE_AUTH_TOKEN,
|
|
107
|
+
CORE_AUTH_USER_DIRECTORY_TOKEN,
|
|
106
108
|
CORE_CACHE_TOKEN,
|
|
107
109
|
CORE_CONFIG_TOKEN,
|
|
108
110
|
CORE_EVENT_BUS_TOKEN,
|
package/dist/entries/view.js
CHANGED
|
@@ -213,6 +213,9 @@ class EtaViewEngine {
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
// ../../src/core/view/htmlResponse.ts
|
|
216
|
+
function withCharset(contentType) {
|
|
217
|
+
return contentType.includes("charset=") ? contentType : `${contentType}; charset=utf-8`;
|
|
218
|
+
}
|
|
216
219
|
function htmlResponse(html, init = {}) {
|
|
217
220
|
return new Response(html, {
|
|
218
221
|
status: init.status ?? 200,
|
|
@@ -225,6 +228,36 @@ function htmlResponse(html, init = {}) {
|
|
|
225
228
|
function isHtmxRequest(request) {
|
|
226
229
|
return request.headers.get("HX-Request") === "true";
|
|
227
230
|
}
|
|
231
|
+
function redirectResponse(location, status = 302) {
|
|
232
|
+
return new Response(null, {
|
|
233
|
+
status,
|
|
234
|
+
headers: {
|
|
235
|
+
Location: location
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function notFoundHtmlResponse(body = "Not Found") {
|
|
240
|
+
return htmlResponse(body, { status: 404 });
|
|
241
|
+
}
|
|
242
|
+
function textResponse(body, init = {}) {
|
|
243
|
+
return new Response(body, {
|
|
244
|
+
status: init.status ?? 200,
|
|
245
|
+
headers: {
|
|
246
|
+
"Content-Type": "text/plain; charset=utf-8"
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
function xmlResponse(body, init = {}) {
|
|
251
|
+
return new Response(body, {
|
|
252
|
+
status: init.status ?? 200,
|
|
253
|
+
headers: {
|
|
254
|
+
"Content-Type": withCharset(init.contentType ?? "application/xml")
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
function rssResponse(body, init = {}) {
|
|
259
|
+
return xmlResponse(body, { ...init, contentType: "application/rss+xml" });
|
|
260
|
+
}
|
|
228
261
|
// ../../src/core/view/webLayoutData.ts
|
|
229
262
|
import { currentAuthUser } from "@getstrata/core/auth/authContext";
|
|
230
263
|
import { currentRequestMeta as currentRequestMeta2 } from "@getstrata/core/http/requestMetaContext";
|
|
@@ -237,6 +270,42 @@ var CORE_EVENT_BUS_TOKEN = "core.eventBus";
|
|
|
237
270
|
var CORE_POLICY_GATE_TOKEN = "core.policyGate";
|
|
238
271
|
var CORE_AUTH_TOKEN = "core.auth";
|
|
239
272
|
var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
|
|
273
|
+
var CORE_ABILITY_CHECKER_TOKEN = "core.abilityChecker";
|
|
274
|
+
var CORE_AUTH_USER_DIRECTORY_TOKEN = "core.authUserDirectory";
|
|
275
|
+
function isAbilityChecker(value) {
|
|
276
|
+
if (!value || typeof value !== "object") {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
const candidate = value;
|
|
280
|
+
return typeof candidate.tokenCan === "function" && typeof candidate.requireAbility === "function";
|
|
281
|
+
}
|
|
282
|
+
function isAuthUserDirectory(value) {
|
|
283
|
+
if (!value || typeof value !== "object") {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
return typeof value.findByIdOrThrow === "function";
|
|
287
|
+
}
|
|
288
|
+
function resolveAbilityChecker(container) {
|
|
289
|
+
if (container.has(CORE_ABILITY_CHECKER_TOKEN)) {
|
|
290
|
+
return container.resolve(CORE_ABILITY_CHECKER_TOKEN);
|
|
291
|
+
}
|
|
292
|
+
return container.resolve(CORE_TOKEN_SERVICE_TOKEN);
|
|
293
|
+
}
|
|
294
|
+
function resolveAuthUserDirectory(container) {
|
|
295
|
+
if (container.has(CORE_AUTH_USER_DIRECTORY_TOKEN)) {
|
|
296
|
+
const directory = container.resolve(CORE_AUTH_USER_DIRECTORY_TOKEN);
|
|
297
|
+
if (isAuthUserDirectory(directory)) {
|
|
298
|
+
return directory;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (container.has(CORE_TOKEN_SERVICE_TOKEN)) {
|
|
302
|
+
const tokenService = container.resolve(CORE_TOKEN_SERVICE_TOKEN);
|
|
303
|
+
if (isAuthUserDirectory(tokenService)) {
|
|
304
|
+
return tokenService;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
240
309
|
|
|
241
310
|
// ../../src/core/http/csrfToken.ts
|
|
242
311
|
import { timingSafeEqual } from "crypto";
|
|
@@ -298,6 +367,9 @@ function currentRequestMeta() {
|
|
|
298
367
|
// ../../src/core/http/csrfToken.ts
|
|
299
368
|
var CSRF_COOKIE = "workhub_csrf";
|
|
300
369
|
var CSRF_TTL_MS = 60 * 60 * 1000;
|
|
370
|
+
function csrfCookieName() {
|
|
371
|
+
return process.env.CSRF_COOKIE_NAME?.trim() || CSRF_COOKIE;
|
|
372
|
+
}
|
|
301
373
|
function resolveCsrfSecret() {
|
|
302
374
|
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
|
|
303
375
|
}
|
|
@@ -317,11 +389,11 @@ function createCsrfTokenCookie() {
|
|
|
317
389
|
const secure = process.env.APP_ENV === "production" ? "; Secure" : "";
|
|
318
390
|
return {
|
|
319
391
|
token,
|
|
320
|
-
cookie: `${
|
|
392
|
+
cookie: `${csrfCookieName()}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}${secure}`
|
|
321
393
|
};
|
|
322
394
|
}
|
|
323
395
|
function resolveCsrfToken(request) {
|
|
324
|
-
const cookieValue = readRequestCookie(request,
|
|
396
|
+
const cookieValue = readRequestCookie(request, csrfCookieName());
|
|
325
397
|
if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
|
|
326
398
|
return { token: cookieValue };
|
|
327
399
|
}
|
|
@@ -357,7 +429,7 @@ function verifyCsrfToken(request, submittedToken) {
|
|
|
357
429
|
if (!submittedToken) {
|
|
358
430
|
return false;
|
|
359
431
|
}
|
|
360
|
-
const cookieValue = readRequestCookie(request,
|
|
432
|
+
const cookieValue = readRequestCookie(request, csrfCookieName());
|
|
361
433
|
if (!cookieValue) {
|
|
362
434
|
return false;
|
|
363
435
|
}
|
|
@@ -474,48 +546,66 @@ function withFlashClear(response) {
|
|
|
474
546
|
}
|
|
475
547
|
|
|
476
548
|
// ../../src/core/view/webLayoutData.ts
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
549
|
+
var configuredLayoutOptions = {};
|
|
550
|
+
function configureWebLayoutData(options) {
|
|
551
|
+
configuredLayoutOptions = { ...options };
|
|
552
|
+
}
|
|
553
|
+
function layoutUserKey(options) {
|
|
554
|
+
return options.userKey ?? "authUser";
|
|
555
|
+
}
|
|
556
|
+
async function defaultLoadLayoutUser(container, _request) {
|
|
480
557
|
const authUser = currentAuthUser();
|
|
481
558
|
if (!authUser) {
|
|
482
|
-
return
|
|
559
|
+
return null;
|
|
483
560
|
}
|
|
484
561
|
const userId = Number(authUser.id);
|
|
485
562
|
if (!Number.isInteger(userId) || userId <= 0) {
|
|
486
|
-
return
|
|
563
|
+
return null;
|
|
487
564
|
}
|
|
488
|
-
|
|
565
|
+
const directory = resolveAuthUserDirectory(container);
|
|
566
|
+
if (!directory) {
|
|
489
567
|
return {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
role: authUser.role ?? "member"
|
|
494
|
-
},
|
|
495
|
-
csrfToken,
|
|
496
|
-
flash
|
|
568
|
+
id: userId,
|
|
569
|
+
email: "",
|
|
570
|
+
role: authUser.role ?? "member"
|
|
497
571
|
};
|
|
498
572
|
}
|
|
499
|
-
const tokenService = container.resolve(CORE_TOKEN_SERVICE_TOKEN);
|
|
500
573
|
try {
|
|
501
|
-
const user = await
|
|
574
|
+
const user = await directory.findByIdOrThrow(userId);
|
|
502
575
|
return {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
role: authUser.role ?? user.role ?? "member"
|
|
507
|
-
},
|
|
508
|
-
csrfToken,
|
|
509
|
-
flash
|
|
576
|
+
id: userId,
|
|
577
|
+
email: user.email ?? "",
|
|
578
|
+
role: authUser.role ?? user.role ?? "member"
|
|
510
579
|
};
|
|
511
580
|
} catch {
|
|
512
|
-
return
|
|
581
|
+
return null;
|
|
513
582
|
}
|
|
514
583
|
}
|
|
584
|
+
async function resolveWebLayoutData(container, request, options = {}) {
|
|
585
|
+
const resolved = { ...configuredLayoutOptions, ...options };
|
|
586
|
+
const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
|
|
587
|
+
const flash = request ? currentRequestMeta2().flash ?? pullFlash(request) : null;
|
|
588
|
+
const userKey = layoutUserKey(resolved);
|
|
589
|
+
const loadUser = resolved.loadUser ?? defaultLoadLayoutUser;
|
|
590
|
+
const user = await loadUser(container, request);
|
|
591
|
+
const extra = typeof resolved.extra === "function" ? await resolved.extra(user) : resolved.extra ?? {};
|
|
592
|
+
return {
|
|
593
|
+
[userKey]: user,
|
|
594
|
+
csrfToken,
|
|
595
|
+
flash,
|
|
596
|
+
...extra
|
|
597
|
+
};
|
|
598
|
+
}
|
|
515
599
|
export {
|
|
516
600
|
DEFAULT_VIEWS_DIRECTORY,
|
|
517
601
|
EtaViewEngine,
|
|
602
|
+
configureWebLayoutData,
|
|
518
603
|
htmlResponse,
|
|
519
604
|
isHtmxRequest,
|
|
520
|
-
|
|
605
|
+
notFoundHtmlResponse,
|
|
606
|
+
redirectResponse,
|
|
607
|
+
resolveWebLayoutData,
|
|
608
|
+
rssResponse,
|
|
609
|
+
textResponse,
|
|
610
|
+
xmlResponse
|
|
521
611
|
};
|