@getstrata/core 0.5.59 → 0.5.61
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 +17 -0
- package/README.md +2 -2
- package/dist/core/database/bindConnection.d.ts +1 -3
- package/dist/core/database/bunSql.d.ts +13 -0
- package/dist/core/http/contentSecurityPolicy.d.ts +27 -0
- package/dist/core/http/memoryThrottleMiddleware.d.ts +2 -1
- package/dist/core/http/requestMetaContext.d.ts +1 -0
- package/dist/core/http/safeInternalPath.d.ts +4 -0
- package/dist/core/http/securedRouteModelBinding.d.ts +2 -0
- package/dist/core/http/securityHeadersMiddleware.d.ts +5 -2
- package/dist/core/http/webErrorResponse.d.ts +1 -1
- package/dist/core/view/etaViewEngine.d.ts +3 -2
- package/dist/core/view/htmlResponse.d.ts +5 -2
- package/dist/core/view/index.d.ts +4 -1
- package/dist/core/view/viewEngine.d.ts +1 -0
- package/dist/core/view/webErrorView.d.ts +21 -0
- package/dist/entries/database/bunSql.js +1 -0
- package/dist/entries/http/contentSecurityPolicy.js +1 -0
- package/dist/entries/http/memoryThrottleMiddleware.js +10 -2
- package/dist/entries/http/requireWebAuthMiddleware.js +34 -2
- package/dist/entries/http/response.js +133 -555
- package/dist/entries/http/safeInternalPath.js +38 -0
- package/dist/entries/http/securedRouteModelBinding.js +27 -5
- package/dist/entries/http/securityHeadersMiddleware.js +33 -60
- package/dist/entries/http/webErrorResponse.js +132 -554
- package/dist/entries/logging/requestLoggingMiddleware.js +2 -1
- package/dist/entries/view.js +96 -9
- package/dist/framework/public-api.d.ts +7 -3
- package/dist/index.js +665 -384
- package/package.json +17 -2
- package/dist/config/contentSecurityPolicy.d.ts +0 -5
package/dist/index.js
CHANGED
|
@@ -756,12 +756,6 @@ function getBoundDatabaseConnection() {
|
|
|
756
756
|
function resetBoundDatabaseConnection() {
|
|
757
757
|
boundConnectionHolder.connection = null;
|
|
758
758
|
}
|
|
759
|
-
|
|
760
|
-
// ../../src/core/database/bindConnection.ts
|
|
761
|
-
function bindDatabaseConnection2(connection) {
|
|
762
|
-
bindDatabaseConnection(connection);
|
|
763
|
-
}
|
|
764
|
-
|
|
765
759
|
// ../../src/core/database/connectionContext.ts
|
|
766
760
|
var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
|
|
767
761
|
function runWithDatabaseConnection(connection, callback) {
|
|
@@ -951,7 +945,7 @@ function createScimAuthMiddleware() {
|
|
|
951
945
|
return jsonScimError("SCIM tenant not found.", 401);
|
|
952
946
|
}
|
|
953
947
|
return await runWithTenantDatabase(tenant, async () => {
|
|
954
|
-
|
|
948
|
+
bindDatabaseConnection(getActiveDatabaseConnection(getDefaultDatabasePool()));
|
|
955
949
|
try {
|
|
956
950
|
return await next();
|
|
957
951
|
} finally {
|
|
@@ -2679,6 +2673,25 @@ class BaseRepository {
|
|
|
2679
2673
|
}
|
|
2680
2674
|
}
|
|
2681
2675
|
var baseRepository_default = BaseRepository;
|
|
2676
|
+
// ../../src/core/database/bunSql.ts
|
|
2677
|
+
var {SQL } = globalThis.Bun;
|
|
2678
|
+
function createBunSqlPool(options) {
|
|
2679
|
+
if (!options.url) {
|
|
2680
|
+
throw new Error("DATABASE_URL is not configured. Set url before creating a Bun SQL pool.");
|
|
2681
|
+
}
|
|
2682
|
+
return new SQL({
|
|
2683
|
+
url: options.url,
|
|
2684
|
+
max: options.max ?? 10,
|
|
2685
|
+
idleTimeout: options.idleTimeout ?? 30,
|
|
2686
|
+
maxLifetime: options.maxLifetime ?? 3600,
|
|
2687
|
+
connectionTimeout: options.connectionTimeout ?? 10
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
function bindBunSql(sql) {
|
|
2691
|
+
bindDatabaseConnection(sql);
|
|
2692
|
+
registerDefaultDatabasePool(sql);
|
|
2693
|
+
return sql;
|
|
2694
|
+
}
|
|
2682
2695
|
// ../../src/core/database/connection.ts
|
|
2683
2696
|
function createDatabaseConnection(source) {
|
|
2684
2697
|
return {
|
|
@@ -4293,6 +4306,133 @@ function conditionalJsonResponse(request, data, init = {}) {
|
|
|
4293
4306
|
headers
|
|
4294
4307
|
});
|
|
4295
4308
|
}
|
|
4309
|
+
// ../../src/core/http/contentSecurityPolicy.ts
|
|
4310
|
+
var HTMX_2_0_4_INDICATOR_STYLE_HASH = "'sha256-bsV5JivYxvGywDAZ22EZJKBFip65Ng9xoJVLbBg7bdo='";
|
|
4311
|
+
var API_DIRECTIVES = {
|
|
4312
|
+
"default-src": ["'none'"],
|
|
4313
|
+
"frame-ancestors": ["'none'"],
|
|
4314
|
+
"base-uri": ["'none'"]
|
|
4315
|
+
};
|
|
4316
|
+
var HTMX_HTML_DIRECTIVES = {
|
|
4317
|
+
"default-src": ["'self'"],
|
|
4318
|
+
"script-src": ["'self'", "https://unpkg.com"],
|
|
4319
|
+
"style-src": ["'self'", HTMX_2_0_4_INDICATOR_STYLE_HASH],
|
|
4320
|
+
"connect-src": ["'self'"],
|
|
4321
|
+
"img-src": ["'self'", "data:", "https:"],
|
|
4322
|
+
"font-src": ["'self'"],
|
|
4323
|
+
"media-src": ["'self'", "https:"],
|
|
4324
|
+
"frame-src": [
|
|
4325
|
+
"https://www.youtube.com",
|
|
4326
|
+
"https://www.youtube-nocookie.com",
|
|
4327
|
+
"https://player.vimeo.com"
|
|
4328
|
+
],
|
|
4329
|
+
"form-action": ["'self'"],
|
|
4330
|
+
"frame-ancestors": ["'none'"],
|
|
4331
|
+
"base-uri": ["'self'"]
|
|
4332
|
+
};
|
|
4333
|
+
var SPA_HTML_DIRECTIVES = {
|
|
4334
|
+
"default-src": ["'self'"],
|
|
4335
|
+
"script-src": ["'self'"],
|
|
4336
|
+
"style-src": ["'self'"],
|
|
4337
|
+
"connect-src": ["'self'"],
|
|
4338
|
+
"img-src": ["'self'", "data:", "https:"],
|
|
4339
|
+
"font-src": ["'self'"],
|
|
4340
|
+
"frame-ancestors": ["'none'"],
|
|
4341
|
+
"base-uri": ["'self'"]
|
|
4342
|
+
};
|
|
4343
|
+
var configuredHtmlOptions = {};
|
|
4344
|
+
function generateCspNonce() {
|
|
4345
|
+
const bytes = new Uint8Array(16);
|
|
4346
|
+
crypto.getRandomValues(bytes);
|
|
4347
|
+
return Buffer.from(bytes).toString("base64");
|
|
4348
|
+
}
|
|
4349
|
+
function configureContentSecurityPolicy(options) {
|
|
4350
|
+
configuredHtmlOptions = { ...options };
|
|
4351
|
+
}
|
|
4352
|
+
function cloneDirectives(source) {
|
|
4353
|
+
const copy = {};
|
|
4354
|
+
for (const [directive, values] of Object.entries(source)) {
|
|
4355
|
+
copy[directive] = [...values];
|
|
4356
|
+
}
|
|
4357
|
+
return copy;
|
|
4358
|
+
}
|
|
4359
|
+
function normalizeSources(value) {
|
|
4360
|
+
if (Array.isArray(value)) {
|
|
4361
|
+
return value.flatMap((item) => item.split(/\s+/).filter(Boolean));
|
|
4362
|
+
}
|
|
4363
|
+
return value.split(/\s+/).filter(Boolean);
|
|
4364
|
+
}
|
|
4365
|
+
function mergeDirectives(base, extra) {
|
|
4366
|
+
const merged = cloneDirectives(base);
|
|
4367
|
+
for (const [directive, value] of Object.entries(extra ?? {})) {
|
|
4368
|
+
if (value === false) {
|
|
4369
|
+
delete merged[directive];
|
|
4370
|
+
continue;
|
|
4371
|
+
}
|
|
4372
|
+
const sources = normalizeSources(value);
|
|
4373
|
+
const existing = merged[directive] ?? [];
|
|
4374
|
+
merged[directive] = [...existing, ...sources.filter((source) => !existing.includes(source))];
|
|
4375
|
+
}
|
|
4376
|
+
return merged;
|
|
4377
|
+
}
|
|
4378
|
+
function serializeDirectives(directives) {
|
|
4379
|
+
return Object.entries(directives).map(([directive, values]) => values.length > 0 ? `${directive} ${values.join(" ")}` : directive).join("; ");
|
|
4380
|
+
}
|
|
4381
|
+
function applyNonce(directives, nonce) {
|
|
4382
|
+
if (!nonce) {
|
|
4383
|
+
return directives;
|
|
4384
|
+
}
|
|
4385
|
+
const token = `'nonce-${nonce}'`;
|
|
4386
|
+
const next = cloneDirectives(directives);
|
|
4387
|
+
for (const directive of ["script-src", "style-src"]) {
|
|
4388
|
+
const values = next[directive] ?? ["'self'"];
|
|
4389
|
+
if (!values.includes(token)) {
|
|
4390
|
+
next[directive] = [...values, token];
|
|
4391
|
+
}
|
|
4392
|
+
}
|
|
4393
|
+
return next;
|
|
4394
|
+
}
|
|
4395
|
+
function htmlBaselineDirectives() {
|
|
4396
|
+
const frontendMode = (process.env.FRONTEND_MODE ?? "api").trim();
|
|
4397
|
+
if (frontendMode === "server-htmx") {
|
|
4398
|
+
return cloneDirectives(HTMX_HTML_DIRECTIVES);
|
|
4399
|
+
}
|
|
4400
|
+
if (frontendMode === "spa-react") {
|
|
4401
|
+
return cloneDirectives(SPA_HTML_DIRECTIVES);
|
|
4402
|
+
}
|
|
4403
|
+
return cloneDirectives(API_DIRECTIVES);
|
|
4404
|
+
}
|
|
4405
|
+
function strictApiContentSecurityPolicy() {
|
|
4406
|
+
return serializeDirectives(API_DIRECTIVES);
|
|
4407
|
+
}
|
|
4408
|
+
function serverHtmxContentSecurityPolicy(nonce) {
|
|
4409
|
+
return serializeDirectives(applyNonce(cloneDirectives(HTMX_HTML_DIRECTIVES), nonce));
|
|
4410
|
+
}
|
|
4411
|
+
function spaContentSecurityPolicy(nonce) {
|
|
4412
|
+
return serializeDirectives(applyNonce(cloneDirectives(SPA_HTML_DIRECTIVES), nonce));
|
|
4413
|
+
}
|
|
4414
|
+
function resolveHtmlContentSecurityPolicy(options = {}) {
|
|
4415
|
+
const resolved = {
|
|
4416
|
+
...configuredHtmlOptions,
|
|
4417
|
+
...options,
|
|
4418
|
+
directives: {
|
|
4419
|
+
...configuredHtmlOptions.directives,
|
|
4420
|
+
...options.directives
|
|
4421
|
+
}
|
|
4422
|
+
};
|
|
4423
|
+
const override = resolved.htmlCsp ?? resolved.csp;
|
|
4424
|
+
if (override) {
|
|
4425
|
+
return override;
|
|
4426
|
+
}
|
|
4427
|
+
return serializeDirectives(applyNonce(mergeDirectives(htmlBaselineDirectives(), resolved.directives), options.nonce));
|
|
4428
|
+
}
|
|
4429
|
+
function resolveContentSecurityPolicy(response, options = {}) {
|
|
4430
|
+
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
4431
|
+
if (!contentType.includes("text/html")) {
|
|
4432
|
+
return strictApiContentSecurityPolicy();
|
|
4433
|
+
}
|
|
4434
|
+
return resolveHtmlContentSecurityPolicy(options);
|
|
4435
|
+
}
|
|
4296
4436
|
// ../../src/core/http/cookies.ts
|
|
4297
4437
|
function readRequestCookie(request, name) {
|
|
4298
4438
|
const cookies = request.cookies;
|
|
@@ -4893,310 +5033,125 @@ function isViewsEnabled() {
|
|
|
4893
5033
|
return readFrontendMode() === "server-htmx";
|
|
4894
5034
|
}
|
|
4895
5035
|
|
|
4896
|
-
// ../../src/core/view/
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
// ../../src/core/view/assertEtaHtmlSource.ts
|
|
4901
|
-
var HTML_TAGS = new Set([
|
|
4902
|
-
"a",
|
|
4903
|
-
"abbr",
|
|
4904
|
-
"address",
|
|
4905
|
-
"article",
|
|
4906
|
-
"aside",
|
|
4907
|
-
"audio",
|
|
4908
|
-
"b",
|
|
4909
|
-
"blockquote",
|
|
4910
|
-
"body",
|
|
4911
|
-
"button",
|
|
4912
|
-
"canvas",
|
|
4913
|
-
"caption",
|
|
4914
|
-
"cite",
|
|
4915
|
-
"code",
|
|
4916
|
-
"col",
|
|
4917
|
-
"colgroup",
|
|
4918
|
-
"data",
|
|
4919
|
-
"datalist",
|
|
4920
|
-
"dd",
|
|
4921
|
-
"del",
|
|
4922
|
-
"details",
|
|
4923
|
-
"dfn",
|
|
4924
|
-
"dialog",
|
|
4925
|
-
"div",
|
|
4926
|
-
"dl",
|
|
4927
|
-
"dt",
|
|
4928
|
-
"em",
|
|
4929
|
-
"fieldset",
|
|
4930
|
-
"figcaption",
|
|
4931
|
-
"figure",
|
|
4932
|
-
"footer",
|
|
4933
|
-
"form",
|
|
4934
|
-
"h1",
|
|
4935
|
-
"h2",
|
|
4936
|
-
"h3",
|
|
4937
|
-
"h4",
|
|
4938
|
-
"h5",
|
|
4939
|
-
"h6",
|
|
4940
|
-
"header",
|
|
4941
|
-
"hgroup",
|
|
4942
|
-
"hr",
|
|
4943
|
-
"html",
|
|
4944
|
-
"i",
|
|
4945
|
-
"iframe",
|
|
4946
|
-
"img",
|
|
4947
|
-
"input",
|
|
4948
|
-
"ins",
|
|
4949
|
-
"kbd",
|
|
4950
|
-
"label",
|
|
4951
|
-
"legend",
|
|
4952
|
-
"li",
|
|
4953
|
-
"main",
|
|
4954
|
-
"map",
|
|
4955
|
-
"mark",
|
|
4956
|
-
"menu",
|
|
4957
|
-
"meter",
|
|
4958
|
-
"nav",
|
|
4959
|
-
"object",
|
|
4960
|
-
"ol",
|
|
4961
|
-
"optgroup",
|
|
4962
|
-
"option",
|
|
4963
|
-
"output",
|
|
4964
|
-
"p",
|
|
4965
|
-
"picture",
|
|
4966
|
-
"pre",
|
|
4967
|
-
"progress",
|
|
4968
|
-
"q",
|
|
4969
|
-
"rp",
|
|
4970
|
-
"rt",
|
|
4971
|
-
"ruby",
|
|
4972
|
-
"s",
|
|
4973
|
-
"samp",
|
|
4974
|
-
"script",
|
|
4975
|
-
"search",
|
|
4976
|
-
"section",
|
|
4977
|
-
"select",
|
|
4978
|
-
"slot",
|
|
4979
|
-
"small",
|
|
4980
|
-
"source",
|
|
4981
|
-
"span",
|
|
4982
|
-
"strong",
|
|
4983
|
-
"style",
|
|
4984
|
-
"sub",
|
|
4985
|
-
"summary",
|
|
4986
|
-
"sup",
|
|
4987
|
-
"table",
|
|
4988
|
-
"tbody",
|
|
4989
|
-
"td",
|
|
4990
|
-
"template",
|
|
4991
|
-
"textarea",
|
|
4992
|
-
"tfoot",
|
|
4993
|
-
"th",
|
|
4994
|
-
"thead",
|
|
4995
|
-
"time",
|
|
4996
|
-
"title",
|
|
4997
|
-
"tr",
|
|
4998
|
-
"track",
|
|
4999
|
-
"u",
|
|
5000
|
-
"ul",
|
|
5001
|
-
"var",
|
|
5002
|
-
"video",
|
|
5003
|
-
"wbr"
|
|
5004
|
-
]);
|
|
5005
|
-
var TAG_PATTERN = "([a-z][a-z0-9]*)";
|
|
5006
|
-
var CLASS_OR_ID_PATTERN = "[.#][\\w-]+";
|
|
5007
|
-
var CLASS_ID_SHORTHAND = new RegExp(`^${TAG_PATTERN}(?:${CLASS_OR_ID_PATTERN})+`);
|
|
5008
|
-
var ATTRIBUTE_SHORTHAND = new RegExp(`^${TAG_PATTERN}(?:${CLASS_OR_ID_PATTERN})*\\s+[\\w:-]+=`);
|
|
5009
|
-
var TAG_SHORTHAND = new RegExp(`^${TAG_PATTERN}(?:>|\\s+\\S|$)`);
|
|
5010
|
-
function htmlTagName(match) {
|
|
5011
|
-
const tag = match?.[1];
|
|
5012
|
-
return tag && HTML_TAGS.has(tag) ? tag : undefined;
|
|
5036
|
+
// ../../src/core/view/htmlResponse.ts
|
|
5037
|
+
function withCharset(contentType) {
|
|
5038
|
+
return contentType.includes("charset=") ? contentType : `${contentType}; charset=utf-8`;
|
|
5013
5039
|
}
|
|
5014
|
-
function
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
return "attribute shorthand";
|
|
5023
|
-
}
|
|
5024
|
-
if (htmlTagName(line.match(TAG_SHORTHAND))) {
|
|
5025
|
-
return "tag shorthand";
|
|
5026
|
-
}
|
|
5027
|
-
return;
|
|
5040
|
+
function htmlResponse(html, init = {}) {
|
|
5041
|
+
return new Response(html, {
|
|
5042
|
+
status: init.status ?? 200,
|
|
5043
|
+
statusText: init.statusText,
|
|
5044
|
+
headers: {
|
|
5045
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
5046
|
+
}
|
|
5047
|
+
});
|
|
5028
5048
|
}
|
|
5029
|
-
function
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5049
|
+
function isHtmxRequest(request) {
|
|
5050
|
+
return request.headers.get("HX-Request") === "true";
|
|
5051
|
+
}
|
|
5052
|
+
function redirectResponse(location, status = 302) {
|
|
5053
|
+
return new Response(null, {
|
|
5054
|
+
status,
|
|
5055
|
+
headers: {
|
|
5056
|
+
Location: location
|
|
5033
5057
|
}
|
|
5034
|
-
|
|
5035
|
-
|
|
5058
|
+
});
|
|
5059
|
+
}
|
|
5060
|
+
function textResponse(body, init = {}) {
|
|
5061
|
+
return new Response(body, {
|
|
5062
|
+
status: init.status ?? 200,
|
|
5063
|
+
headers: {
|
|
5064
|
+
"Content-Type": "text/plain; charset=utf-8"
|
|
5036
5065
|
}
|
|
5037
|
-
|
|
5066
|
+
});
|
|
5067
|
+
}
|
|
5068
|
+
function xmlResponse(body, init = {}) {
|
|
5069
|
+
return new Response(body, {
|
|
5070
|
+
status: init.status ?? 200,
|
|
5071
|
+
headers: {
|
|
5072
|
+
"Content-Type": withCharset(init.contentType ?? "application/xml")
|
|
5073
|
+
}
|
|
5074
|
+
});
|
|
5075
|
+
}
|
|
5076
|
+
function rssResponse(body, init = {}) {
|
|
5077
|
+
return xmlResponse(body, { ...init, contentType: "application/rss+xml" });
|
|
5078
|
+
}
|
|
5079
|
+
|
|
5080
|
+
// ../../src/core/view/webErrorView.ts
|
|
5081
|
+
var configuredErrorView = {};
|
|
5082
|
+
function configureWebErrorView(options) {
|
|
5083
|
+
configuredErrorView = { ...options };
|
|
5084
|
+
}
|
|
5085
|
+
function escapeHtml(value) {
|
|
5086
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
5087
|
+
}
|
|
5088
|
+
function renderKernelErrorChrome(input) {
|
|
5089
|
+
const title = escapeHtml(input.title);
|
|
5090
|
+
const message = escapeHtml(input.message);
|
|
5091
|
+
const errorLines = Object.entries(input.errors ?? {}).flatMap(([field, messages]) => messages.map((item) => `${field}: ${item}`)).map((line) => `<li>${escapeHtml(line)}</li>`).join("");
|
|
5092
|
+
const details = errorLines ? `<ul class="error-list">${errorLines}</ul>` : "";
|
|
5093
|
+
const goBack = input.status === 422 ? `<p><a href="javascript:history.back()">Go back</a></p>` : "";
|
|
5094
|
+
return `<!doctype html>
|
|
5095
|
+
<html lang="en">
|
|
5096
|
+
<head>
|
|
5097
|
+
<meta charset="UTF-8" />
|
|
5098
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
5099
|
+
<title>${title}</title>
|
|
5100
|
+
<link rel="stylesheet" href="/assets/app.css" />
|
|
5101
|
+
</head>
|
|
5102
|
+
<body>
|
|
5103
|
+
<header class="site-header">
|
|
5104
|
+
<a class="brand" href="/">Home</a>
|
|
5105
|
+
</header>
|
|
5106
|
+
<main class="site-main">
|
|
5107
|
+
<section class="page-header">
|
|
5108
|
+
<h1>${title}</h1>
|
|
5109
|
+
<p>${message}</p>
|
|
5110
|
+
${details}
|
|
5111
|
+
${goBack}
|
|
5112
|
+
</section>
|
|
5113
|
+
</main>
|
|
5114
|
+
</body>
|
|
5115
|
+
</html>
|
|
5116
|
+
`;
|
|
5117
|
+
}
|
|
5118
|
+
function errorTemplateName(status) {
|
|
5119
|
+
if (status === 404) {
|
|
5120
|
+
return "errors/not-found";
|
|
5038
5121
|
}
|
|
5039
|
-
if (
|
|
5040
|
-
return "
|
|
5122
|
+
if (status === 403) {
|
|
5123
|
+
return "errors/forbidden";
|
|
5041
5124
|
}
|
|
5042
|
-
|
|
5043
|
-
|
|
5125
|
+
return "errors/error";
|
|
5126
|
+
}
|
|
5127
|
+
async function renderWebErrorHtml(input) {
|
|
5128
|
+
const render = configuredErrorView.render;
|
|
5129
|
+
if (!render) {
|
|
5130
|
+
return renderKernelErrorChrome(input);
|
|
5044
5131
|
}
|
|
5045
|
-
|
|
5046
|
-
return
|
|
5132
|
+
try {
|
|
5133
|
+
return await render({
|
|
5134
|
+
...input,
|
|
5135
|
+
request: input.request ?? currentRequestMeta().request
|
|
5136
|
+
});
|
|
5137
|
+
} catch {
|
|
5138
|
+
return renderKernelErrorChrome(input);
|
|
5047
5139
|
}
|
|
5048
|
-
return null;
|
|
5049
5140
|
}
|
|
5050
|
-
function
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
}
|
|
5057
|
-
const lower = trimmed.toLowerCase();
|
|
5058
|
-
const nextSkipBlock = updateSkipBlock(lower, skipBlock);
|
|
5059
|
-
if (skipBlock || nextSkipBlock) {
|
|
5060
|
-
skipBlock = nextSkipBlock;
|
|
5061
|
-
continue;
|
|
5062
|
-
}
|
|
5063
|
-
if (trimmed.startsWith("<") || trimmed.startsWith("<%")) {
|
|
5064
|
-
continue;
|
|
5065
|
-
}
|
|
5066
|
-
const kind = describePugLikeLine(trimmed);
|
|
5067
|
-
if (kind) {
|
|
5068
|
-
throw new Error(`Eta view "${templateName}" contains Pug-like markup (${kind}: ${trimmed}). Eta views must be HTML + <% %> tags, not Pug.`);
|
|
5069
|
-
}
|
|
5141
|
+
async function htmlErrorResponse(input) {
|
|
5142
|
+
return htmlResponse(await renderWebErrorHtml(input), { status: input.status });
|
|
5143
|
+
}
|
|
5144
|
+
async function notFoundHtmlResponse(body) {
|
|
5145
|
+
if (body !== undefined) {
|
|
5146
|
+
return htmlResponse(body, { status: 404 });
|
|
5070
5147
|
}
|
|
5148
|
+
return htmlErrorResponse({
|
|
5149
|
+
status: 404,
|
|
5150
|
+
title: "Not Found",
|
|
5151
|
+
message: "The page you requested was not found."
|
|
5152
|
+
});
|
|
5071
5153
|
}
|
|
5072
5154
|
|
|
5073
|
-
// ../../src/core/view/etaViewEngine.ts
|
|
5074
|
-
var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
|
|
5075
|
-
var DEFAULT_LAYOUT = "layouts/app.eta";
|
|
5076
|
-
|
|
5077
|
-
class EtaViewEngine {
|
|
5078
|
-
eta;
|
|
5079
|
-
resolveLayoutData;
|
|
5080
|
-
constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
|
|
5081
|
-
this.eta = new Eta({
|
|
5082
|
-
views: viewsDirectory,
|
|
5083
|
-
autoTrim: false
|
|
5084
|
-
});
|
|
5085
|
-
this.resolveLayoutData = resolveLayoutData;
|
|
5086
|
-
const readFile2 = this.eta.readFile?.bind(this.eta);
|
|
5087
|
-
this.eta.readFile = (path) => {
|
|
5088
|
-
const source = readFile2 ? readFile2(path) : "";
|
|
5089
|
-
assertEtaHtmlSource(relative(viewsDirectory, path) || path, source);
|
|
5090
|
-
return source;
|
|
5091
|
-
};
|
|
5092
|
-
}
|
|
5093
|
-
async render(name, data = {}, options = {}) {
|
|
5094
|
-
const template = name.endsWith(".eta") ? name : `${name}.eta`;
|
|
5095
|
-
const layoutData = this.resolveLayoutData ? await this.resolveLayoutData() : {};
|
|
5096
|
-
const mergedData = { ...layoutData, ...data };
|
|
5097
|
-
const body = await this.eta.renderAsync(template, mergedData);
|
|
5098
|
-
const layout = options.layout ?? DEFAULT_LAYOUT;
|
|
5099
|
-
if (layout === false) {
|
|
5100
|
-
return body;
|
|
5101
|
-
}
|
|
5102
|
-
const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
|
|
5103
|
-
return await this.eta.renderAsync(layoutTemplate, {
|
|
5104
|
-
...mergedData,
|
|
5105
|
-
body
|
|
5106
|
-
});
|
|
5107
|
-
}
|
|
5108
|
-
}
|
|
5109
|
-
// ../../src/core/view/htmlResponse.ts
|
|
5110
|
-
function htmlResponse(html, init = {}) {
|
|
5111
|
-
return new Response(html, {
|
|
5112
|
-
status: init.status ?? 200,
|
|
5113
|
-
statusText: init.statusText,
|
|
5114
|
-
headers: {
|
|
5115
|
-
"Content-Type": "text/html; charset=utf-8"
|
|
5116
|
-
}
|
|
5117
|
-
});
|
|
5118
|
-
}
|
|
5119
|
-
function isHtmxRequest(request) {
|
|
5120
|
-
return request.headers.get("HX-Request") === "true";
|
|
5121
|
-
}
|
|
5122
|
-
function redirectResponse(location, status = 302) {
|
|
5123
|
-
return new Response(null, {
|
|
5124
|
-
status,
|
|
5125
|
-
headers: {
|
|
5126
|
-
Location: location
|
|
5127
|
-
}
|
|
5128
|
-
});
|
|
5129
|
-
}
|
|
5130
|
-
function notFoundHtmlResponse(body = "Not Found") {
|
|
5131
|
-
return htmlResponse(body, { status: 404 });
|
|
5132
|
-
}
|
|
5133
|
-
function textResponse(body, init = {}) {
|
|
5134
|
-
return new Response(body, {
|
|
5135
|
-
status: init.status ?? 200,
|
|
5136
|
-
headers: {
|
|
5137
|
-
"Content-Type": "text/plain; charset=utf-8"
|
|
5138
|
-
}
|
|
5139
|
-
});
|
|
5140
|
-
}
|
|
5141
|
-
function xmlResponse(body, init = {}) {
|
|
5142
|
-
return new Response(body, {
|
|
5143
|
-
status: init.status ?? 200,
|
|
5144
|
-
headers: {
|
|
5145
|
-
"Content-Type": "application/xml; charset=utf-8"
|
|
5146
|
-
}
|
|
5147
|
-
});
|
|
5148
|
-
}
|
|
5149
|
-
// ../../src/core/view/webLayoutData.ts
|
|
5150
|
-
var configuredLayoutOptions = {};
|
|
5151
|
-
function configureWebLayoutData(options) {
|
|
5152
|
-
configuredLayoutOptions = { ...options };
|
|
5153
|
-
}
|
|
5154
|
-
function layoutUserKey(options) {
|
|
5155
|
-
return options.userKey ?? "authUser";
|
|
5156
|
-
}
|
|
5157
|
-
async function defaultLoadLayoutUser(container, _request) {
|
|
5158
|
-
const authUser = currentAuthUser();
|
|
5159
|
-
if (!authUser) {
|
|
5160
|
-
return null;
|
|
5161
|
-
}
|
|
5162
|
-
const userId = Number(authUser.id);
|
|
5163
|
-
if (!Number.isInteger(userId) || userId <= 0) {
|
|
5164
|
-
return null;
|
|
5165
|
-
}
|
|
5166
|
-
const directory = resolveAuthUserDirectory(container);
|
|
5167
|
-
if (!directory) {
|
|
5168
|
-
return {
|
|
5169
|
-
id: userId,
|
|
5170
|
-
email: "",
|
|
5171
|
-
role: authUser.role ?? "member"
|
|
5172
|
-
};
|
|
5173
|
-
}
|
|
5174
|
-
try {
|
|
5175
|
-
const user = await directory.findByIdOrThrow(userId);
|
|
5176
|
-
return {
|
|
5177
|
-
id: userId,
|
|
5178
|
-
email: user.email ?? "",
|
|
5179
|
-
role: authUser.role ?? user.role ?? "member"
|
|
5180
|
-
};
|
|
5181
|
-
} catch {
|
|
5182
|
-
return null;
|
|
5183
|
-
}
|
|
5184
|
-
}
|
|
5185
|
-
async function resolveWebLayoutData(container, request, options = {}) {
|
|
5186
|
-
const resolved = { ...configuredLayoutOptions, ...options };
|
|
5187
|
-
const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
|
|
5188
|
-
const flash = request ? currentRequestMeta().flash ?? pullFlash(request) : null;
|
|
5189
|
-
const userKey = layoutUserKey(resolved);
|
|
5190
|
-
const loadUser = resolved.loadUser ?? defaultLoadLayoutUser;
|
|
5191
|
-
const user = await loadUser(container, request);
|
|
5192
|
-
const extra = typeof resolved.extra === "function" ? await resolved.extra(user) : resolved.extra ?? {};
|
|
5193
|
-
return {
|
|
5194
|
-
[userKey]: user,
|
|
5195
|
-
csrfToken,
|
|
5196
|
-
flash,
|
|
5197
|
-
...extra
|
|
5198
|
-
};
|
|
5199
|
-
}
|
|
5200
5155
|
// ../../src/core/http/contentNegotiation.ts
|
|
5201
5156
|
function requestPrefersJson(request) {
|
|
5202
5157
|
if (!request) {
|
|
@@ -5220,6 +5175,39 @@ function requestPrefersJson(request) {
|
|
|
5220
5175
|
return pathname.startsWith("/api/");
|
|
5221
5176
|
}
|
|
5222
5177
|
|
|
5178
|
+
// ../../src/core/http/safeInternalPath.ts
|
|
5179
|
+
function looksLikeExternalTarget(value) {
|
|
5180
|
+
const trimmed = value.trim();
|
|
5181
|
+
if (!trimmed.startsWith("/") || trimmed.startsWith("//") || trimmed.includes("\\")) {
|
|
5182
|
+
return true;
|
|
5183
|
+
}
|
|
5184
|
+
if (trimmed.includes("://") || trimmed.includes(":/") || trimmed.includes(":\\")) {
|
|
5185
|
+
return true;
|
|
5186
|
+
}
|
|
5187
|
+
try {
|
|
5188
|
+
const decoded = decodeURIComponent(trimmed);
|
|
5189
|
+
if (decoded.startsWith("//") || decoded.includes("\\") || /https?:/i.test(decoded)) {
|
|
5190
|
+
return true;
|
|
5191
|
+
}
|
|
5192
|
+
} catch {
|
|
5193
|
+
return true;
|
|
5194
|
+
}
|
|
5195
|
+
return false;
|
|
5196
|
+
}
|
|
5197
|
+
function sanitizeInternalPath(raw, fallback = "/") {
|
|
5198
|
+
if (looksLikeExternalTarget(raw)) {
|
|
5199
|
+
return fallback;
|
|
5200
|
+
}
|
|
5201
|
+
return raw;
|
|
5202
|
+
}
|
|
5203
|
+
function safeInternalRedirectPath(request, fallback = "/") {
|
|
5204
|
+
const url = new URL(request.url);
|
|
5205
|
+
return sanitizeInternalPath(`${url.pathname}${url.search}`, fallback);
|
|
5206
|
+
}
|
|
5207
|
+
function loginRedirectLocation(request) {
|
|
5208
|
+
return `/login?redirect=${encodeURIComponent(safeInternalRedirectPath(request))}`;
|
|
5209
|
+
}
|
|
5210
|
+
|
|
5223
5211
|
// ../../src/core/http/webErrorResponse.ts
|
|
5224
5212
|
function normalizeFieldErrors(details) {
|
|
5225
5213
|
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
|
@@ -5237,23 +5225,37 @@ function normalizeFieldErrors(details) {
|
|
|
5237
5225
|
}
|
|
5238
5226
|
return errors;
|
|
5239
5227
|
}
|
|
5240
|
-
function
|
|
5228
|
+
function errorPageTitle(status, message) {
|
|
5229
|
+
if (status === 404) {
|
|
5230
|
+
return "Not Found";
|
|
5231
|
+
}
|
|
5232
|
+
if (status === 403) {
|
|
5233
|
+
return "Forbidden";
|
|
5234
|
+
}
|
|
5235
|
+
if (status >= 500) {
|
|
5236
|
+
return "Server Error";
|
|
5237
|
+
}
|
|
5238
|
+
return message;
|
|
5239
|
+
}
|
|
5240
|
+
function publicErrorMessage(status, message) {
|
|
5241
|
+
if (status >= 500 && false) {}
|
|
5242
|
+
return message;
|
|
5243
|
+
}
|
|
5244
|
+
async function webErrorResponse(error, request) {
|
|
5241
5245
|
if (!request || !isViewsEnabled() || requestPrefersJson(request)) {
|
|
5242
5246
|
return null;
|
|
5243
5247
|
}
|
|
5244
5248
|
const mappedError = error instanceof HttpError ? error : mapDatabaseError(error);
|
|
5245
5249
|
if (mappedError instanceof UnauthorizedError) {
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
return htmlResponse(`<section class="page-header"><h1>${mappedError.message}</h1></section>`, {
|
|
5256
|
-
status: mappedError.status
|
|
5250
|
+
return Response.redirect(loginRedirectLocation(request), 302);
|
|
5251
|
+
}
|
|
5252
|
+
const errors = mappedError instanceof ValidationError ? normalizeFieldErrors(mappedError.details) : undefined;
|
|
5253
|
+
return htmlErrorResponse({
|
|
5254
|
+
status: mappedError.status,
|
|
5255
|
+
title: errorPageTitle(mappedError.status, mappedError.message),
|
|
5256
|
+
message: publicErrorMessage(mappedError.status, mappedError.message),
|
|
5257
|
+
errors,
|
|
5258
|
+
request
|
|
5257
5259
|
});
|
|
5258
5260
|
}
|
|
5259
5261
|
|
|
@@ -5283,7 +5285,7 @@ function withErrorHandling(handler) {
|
|
|
5283
5285
|
return await handler(...args);
|
|
5284
5286
|
} catch (error) {
|
|
5285
5287
|
const request = args.find((arg) => arg instanceof Request);
|
|
5286
|
-
const webResponse = webErrorResponse(error, request);
|
|
5288
|
+
const webResponse = await webErrorResponse(error, request);
|
|
5287
5289
|
if (webResponse) {
|
|
5288
5290
|
return webResponse;
|
|
5289
5291
|
}
|
|
@@ -5310,10 +5312,32 @@ function bindRouteModel(param, resolver, handler) {
|
|
|
5310
5312
|
function isMutatingPolicyAction(action) {
|
|
5311
5313
|
return action === "update" || action === "delete";
|
|
5312
5314
|
}
|
|
5315
|
+
function modelHasIdentity(model) {
|
|
5316
|
+
return !!model && typeof model === "object" && "id" in model && model.id !== undefined && model.id !== null;
|
|
5317
|
+
}
|
|
5318
|
+
function shouldApplyViewEtag(response, model, authorization) {
|
|
5319
|
+
if (authorization.etag === false) {
|
|
5320
|
+
return false;
|
|
5321
|
+
}
|
|
5322
|
+
if (authorization.etag === true) {
|
|
5323
|
+
return true;
|
|
5324
|
+
}
|
|
5325
|
+
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
5326
|
+
if (contentType.includes("text/html")) {
|
|
5327
|
+
return false;
|
|
5328
|
+
}
|
|
5329
|
+
return modelHasIdentity(model);
|
|
5330
|
+
}
|
|
5331
|
+
function requireResolvedModel(model) {
|
|
5332
|
+
if (model == null) {
|
|
5333
|
+
throw new NotFoundError;
|
|
5334
|
+
}
|
|
5335
|
+
return model;
|
|
5336
|
+
}
|
|
5313
5337
|
function securedBindRouteModel(param, resolver, authorization, handler) {
|
|
5314
5338
|
return async (request) => {
|
|
5315
5339
|
const id = parsePositiveIntParam(String(request.params[param]), String(param));
|
|
5316
|
-
const model = await resolver(id, request);
|
|
5340
|
+
const model = requireResolvedModel(await resolver(id, request));
|
|
5317
5341
|
const gate = resolveApplicationPolicyGate();
|
|
5318
5342
|
const auth2 = resolveApplicationAuth();
|
|
5319
5343
|
const user = currentAuthUser() ?? await auth2.resolve(request);
|
|
@@ -5324,7 +5348,7 @@ function securedBindRouteModel(param, resolver, authorization, handler) {
|
|
|
5324
5348
|
});
|
|
5325
5349
|
}
|
|
5326
5350
|
const response = await handler(request, model);
|
|
5327
|
-
if (isEtagEnabled() && authorization.action === "view") {
|
|
5351
|
+
if (isEtagEnabled() && authorization.action === "view" && shouldApplyViewEtag(response, model, authorization)) {
|
|
5328
5352
|
return applyConditionalGet(request, response, etagFromResource(model));
|
|
5329
5353
|
}
|
|
5330
5354
|
return response;
|
|
@@ -5336,7 +5360,7 @@ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
|
|
|
5336
5360
|
if (!key) {
|
|
5337
5361
|
throw new BadRequestError(`Missing route parameter "${String(param)}".`);
|
|
5338
5362
|
}
|
|
5339
|
-
const model = await resolver(key, request);
|
|
5363
|
+
const model = requireResolvedModel(await resolver(key, request));
|
|
5340
5364
|
const gate = resolveApplicationPolicyGate();
|
|
5341
5365
|
const auth2 = resolveApplicationAuth();
|
|
5342
5366
|
const user = currentAuthUser() ?? await auth2.resolve(request);
|
|
@@ -5347,7 +5371,7 @@ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
|
|
|
5347
5371
|
});
|
|
5348
5372
|
}
|
|
5349
5373
|
const response = await handler(request, model);
|
|
5350
|
-
if (isEtagEnabled() && authorization.action === "view") {
|
|
5374
|
+
if (isEtagEnabled() && authorization.action === "view" && shouldApplyViewEtag(response, model, authorization)) {
|
|
5351
5375
|
return applyConditionalGet(request, response, etagFromResource(model));
|
|
5352
5376
|
}
|
|
5353
5377
|
return response;
|
|
@@ -5395,9 +5419,11 @@ function createLoginThrottleMiddleware(options) {
|
|
|
5395
5419
|
};
|
|
5396
5420
|
}
|
|
5397
5421
|
// ../../src/core/http/memoryThrottleMiddleware.ts
|
|
5398
|
-
var
|
|
5422
|
+
var throttleBucketRegistries = new Set;
|
|
5399
5423
|
function createMemoryThrottleMiddleware(options) {
|
|
5400
5424
|
const prefix = options.keyPrefix ?? "workhub:memory-throttle:";
|
|
5425
|
+
const buckets = new Map;
|
|
5426
|
+
throttleBucketRegistries.add(buckets);
|
|
5401
5427
|
return async (request, next) => {
|
|
5402
5428
|
const path = new URL(request.url).pathname;
|
|
5403
5429
|
const identity = readClientIp(request) ?? request.headers.get("authorization")?.slice(0, 32) ?? "unknown";
|
|
@@ -5420,6 +5446,11 @@ function createMemoryThrottleMiddleware(options) {
|
|
|
5420
5446
|
return await next();
|
|
5421
5447
|
};
|
|
5422
5448
|
}
|
|
5449
|
+
function resetMemoryThrottleForTests() {
|
|
5450
|
+
for (const buckets of throttleBucketRegistries) {
|
|
5451
|
+
buckets.clear();
|
|
5452
|
+
}
|
|
5453
|
+
}
|
|
5423
5454
|
// ../../src/core/metrics/prometheus.ts
|
|
5424
5455
|
class PrometheusRegistry {
|
|
5425
5456
|
httpRequestsTotal = new Map;
|
|
@@ -5563,8 +5594,7 @@ function createRequireWebAuthMiddleware(auth2) {
|
|
|
5563
5594
|
if (requestPrefersJson(request)) {
|
|
5564
5595
|
throw new UnauthorizedError;
|
|
5565
5596
|
}
|
|
5566
|
-
|
|
5567
|
-
return Response.redirect(`/login?redirect=${redirectTarget}`, 302);
|
|
5597
|
+
return Response.redirect(loginRedirectLocation(request), 302);
|
|
5568
5598
|
};
|
|
5569
5599
|
}
|
|
5570
5600
|
// ../../src/core/http/scimThrottleMiddleware.ts
|
|
@@ -5617,67 +5647,31 @@ var appConfig = {
|
|
|
5617
5647
|
apiPrefix: process.env.API_PREFIX ?? "/api/v1"
|
|
5618
5648
|
};
|
|
5619
5649
|
|
|
5620
|
-
// ../../src/config/contentSecurityPolicy.ts
|
|
5621
|
-
function strictApiContentSecurityPolicy() {
|
|
5622
|
-
return "default-src 'none'; frame-ancestors 'none'; base-uri 'none'";
|
|
5623
|
-
}
|
|
5624
|
-
function serverHtmxContentSecurityPolicy() {
|
|
5625
|
-
return [
|
|
5626
|
-
"default-src 'self'",
|
|
5627
|
-
"script-src 'self' https://unpkg.com",
|
|
5628
|
-
"style-src 'self'",
|
|
5629
|
-
"connect-src 'self'",
|
|
5630
|
-
"img-src 'self'",
|
|
5631
|
-
"font-src 'self'",
|
|
5632
|
-
"form-action 'self'",
|
|
5633
|
-
"frame-ancestors 'none'",
|
|
5634
|
-
"base-uri 'self'"
|
|
5635
|
-
].join("; ");
|
|
5636
|
-
}
|
|
5637
|
-
function spaContentSecurityPolicy() {
|
|
5638
|
-
return [
|
|
5639
|
-
"default-src 'self'",
|
|
5640
|
-
"script-src 'self'",
|
|
5641
|
-
"style-src 'self'",
|
|
5642
|
-
"connect-src 'self'",
|
|
5643
|
-
"img-src 'self'",
|
|
5644
|
-
"font-src 'self'",
|
|
5645
|
-
"frame-ancestors 'none'",
|
|
5646
|
-
"base-uri 'self'"
|
|
5647
|
-
].join("; ");
|
|
5648
|
-
}
|
|
5649
|
-
function resolveContentSecurityPolicy(response) {
|
|
5650
|
-
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
5651
|
-
if (!contentType.includes("text/html")) {
|
|
5652
|
-
return strictApiContentSecurityPolicy();
|
|
5653
|
-
}
|
|
5654
|
-
const frontendMode = (process.env.FRONTEND_MODE ?? "api").trim();
|
|
5655
|
-
if (frontendMode === "server-htmx") {
|
|
5656
|
-
return serverHtmxContentSecurityPolicy();
|
|
5657
|
-
}
|
|
5658
|
-
if (frontendMode === "spa-react") {
|
|
5659
|
-
return spaContentSecurityPolicy();
|
|
5660
|
-
}
|
|
5661
|
-
return strictApiContentSecurityPolicy();
|
|
5662
|
-
}
|
|
5663
|
-
|
|
5664
5650
|
// ../../src/core/http/securityHeadersMiddleware.ts
|
|
5665
|
-
function createSecurityHeadersMiddleware() {
|
|
5666
|
-
return async (
|
|
5667
|
-
const
|
|
5668
|
-
const
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
headers
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
headers
|
|
5651
|
+
function createSecurityHeadersMiddleware(options = {}) {
|
|
5652
|
+
return async (request, next) => {
|
|
5653
|
+
const nonce = generateCspNonce();
|
|
5654
|
+
const existing = currentRequestMeta();
|
|
5655
|
+
return await runWithRequestMeta({
|
|
5656
|
+
...existing,
|
|
5657
|
+
request: existing.request ?? request,
|
|
5658
|
+
cspNonce: nonce
|
|
5659
|
+
}, async () => {
|
|
5660
|
+
const response = await next();
|
|
5661
|
+
const headers = new Headers(response.headers);
|
|
5662
|
+
headers.set("X-Content-Type-Options", "nosniff");
|
|
5663
|
+
headers.set("X-Frame-Options", "DENY");
|
|
5664
|
+
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
5665
|
+
headers.set("X-XSS-Protection", "0");
|
|
5666
|
+
headers.set("Content-Security-Policy", resolveContentSecurityPolicy(response, { ...options, nonce }));
|
|
5667
|
+
if (appConfig.env === "production") {
|
|
5668
|
+
headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
|
5669
|
+
}
|
|
5670
|
+
return new Response(response.body, {
|
|
5671
|
+
status: response.status,
|
|
5672
|
+
statusText: response.statusText,
|
|
5673
|
+
headers
|
|
5674
|
+
});
|
|
5681
5675
|
});
|
|
5682
5676
|
};
|
|
5683
5677
|
}
|
|
@@ -5799,6 +5793,7 @@ function installGracefulShutdownSignals(signals = ["SIGINT", "SIGTERM"]) {
|
|
|
5799
5793
|
function createRequestLoggingMiddleware() {
|
|
5800
5794
|
return async (request, next) => {
|
|
5801
5795
|
return await runWithRequestMeta({
|
|
5796
|
+
...currentRequestMeta(),
|
|
5802
5797
|
ipAddress: readClientIp(request) ?? null,
|
|
5803
5798
|
userAgent: request.headers.get("user-agent"),
|
|
5804
5799
|
request
|
|
@@ -5819,7 +5814,7 @@ function createRequestLoggingMiddleware() {
|
|
|
5819
5814
|
};
|
|
5820
5815
|
}
|
|
5821
5816
|
// ../../src/core/mail/markdownMail.ts
|
|
5822
|
-
function
|
|
5817
|
+
function escapeHtml2(value) {
|
|
5823
5818
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5824
5819
|
}
|
|
5825
5820
|
function stripMarkdown(markdown) {
|
|
@@ -5838,9 +5833,9 @@ function markdownToHtml(markdown) {
|
|
|
5838
5833
|
`);
|
|
5839
5834
|
}
|
|
5840
5835
|
function wrapMarkdownMailLayout(bodyHtml, options = {}) {
|
|
5841
|
-
const title =
|
|
5842
|
-
const preview =
|
|
5843
|
-
const footer =
|
|
5836
|
+
const title = escapeHtml2(options.title ?? "GetStrata");
|
|
5837
|
+
const preview = escapeHtml2(options.preview ?? "");
|
|
5838
|
+
const footer = escapeHtml2(options.footer ?? "Sent by GetStrata");
|
|
5844
5839
|
return `<!DOCTYPE html>
|
|
5845
5840
|
<html lang="en">
|
|
5846
5841
|
<head>
|
|
@@ -6663,6 +6658,272 @@ function validateObject(payload, schema) {
|
|
|
6663
6658
|
}
|
|
6664
6659
|
return output;
|
|
6665
6660
|
}
|
|
6661
|
+
// ../../src/core/view/etaViewEngine.ts
|
|
6662
|
+
import { join as join4, relative } from "path";
|
|
6663
|
+
import { Eta } from "eta";
|
|
6664
|
+
|
|
6665
|
+
// ../../src/core/view/assertEtaHtmlSource.ts
|
|
6666
|
+
var HTML_TAGS = new Set([
|
|
6667
|
+
"a",
|
|
6668
|
+
"abbr",
|
|
6669
|
+
"address",
|
|
6670
|
+
"article",
|
|
6671
|
+
"aside",
|
|
6672
|
+
"audio",
|
|
6673
|
+
"b",
|
|
6674
|
+
"blockquote",
|
|
6675
|
+
"body",
|
|
6676
|
+
"button",
|
|
6677
|
+
"canvas",
|
|
6678
|
+
"caption",
|
|
6679
|
+
"cite",
|
|
6680
|
+
"code",
|
|
6681
|
+
"col",
|
|
6682
|
+
"colgroup",
|
|
6683
|
+
"data",
|
|
6684
|
+
"datalist",
|
|
6685
|
+
"dd",
|
|
6686
|
+
"del",
|
|
6687
|
+
"details",
|
|
6688
|
+
"dfn",
|
|
6689
|
+
"dialog",
|
|
6690
|
+
"div",
|
|
6691
|
+
"dl",
|
|
6692
|
+
"dt",
|
|
6693
|
+
"em",
|
|
6694
|
+
"fieldset",
|
|
6695
|
+
"figcaption",
|
|
6696
|
+
"figure",
|
|
6697
|
+
"footer",
|
|
6698
|
+
"form",
|
|
6699
|
+
"h1",
|
|
6700
|
+
"h2",
|
|
6701
|
+
"h3",
|
|
6702
|
+
"h4",
|
|
6703
|
+
"h5",
|
|
6704
|
+
"h6",
|
|
6705
|
+
"header",
|
|
6706
|
+
"hgroup",
|
|
6707
|
+
"hr",
|
|
6708
|
+
"html",
|
|
6709
|
+
"i",
|
|
6710
|
+
"iframe",
|
|
6711
|
+
"img",
|
|
6712
|
+
"input",
|
|
6713
|
+
"ins",
|
|
6714
|
+
"kbd",
|
|
6715
|
+
"label",
|
|
6716
|
+
"legend",
|
|
6717
|
+
"li",
|
|
6718
|
+
"main",
|
|
6719
|
+
"map",
|
|
6720
|
+
"mark",
|
|
6721
|
+
"menu",
|
|
6722
|
+
"meter",
|
|
6723
|
+
"nav",
|
|
6724
|
+
"object",
|
|
6725
|
+
"ol",
|
|
6726
|
+
"optgroup",
|
|
6727
|
+
"option",
|
|
6728
|
+
"output",
|
|
6729
|
+
"p",
|
|
6730
|
+
"picture",
|
|
6731
|
+
"pre",
|
|
6732
|
+
"progress",
|
|
6733
|
+
"q",
|
|
6734
|
+
"rp",
|
|
6735
|
+
"rt",
|
|
6736
|
+
"ruby",
|
|
6737
|
+
"s",
|
|
6738
|
+
"samp",
|
|
6739
|
+
"script",
|
|
6740
|
+
"search",
|
|
6741
|
+
"section",
|
|
6742
|
+
"select",
|
|
6743
|
+
"slot",
|
|
6744
|
+
"small",
|
|
6745
|
+
"source",
|
|
6746
|
+
"span",
|
|
6747
|
+
"strong",
|
|
6748
|
+
"style",
|
|
6749
|
+
"sub",
|
|
6750
|
+
"summary",
|
|
6751
|
+
"sup",
|
|
6752
|
+
"table",
|
|
6753
|
+
"tbody",
|
|
6754
|
+
"td",
|
|
6755
|
+
"template",
|
|
6756
|
+
"textarea",
|
|
6757
|
+
"tfoot",
|
|
6758
|
+
"th",
|
|
6759
|
+
"thead",
|
|
6760
|
+
"time",
|
|
6761
|
+
"title",
|
|
6762
|
+
"tr",
|
|
6763
|
+
"track",
|
|
6764
|
+
"u",
|
|
6765
|
+
"ul",
|
|
6766
|
+
"var",
|
|
6767
|
+
"video",
|
|
6768
|
+
"wbr"
|
|
6769
|
+
]);
|
|
6770
|
+
var TAG_PATTERN = "([a-z][a-z0-9]*)";
|
|
6771
|
+
var CLASS_OR_ID_PATTERN = "[.#][\\w-]+";
|
|
6772
|
+
var CLASS_ID_SHORTHAND = new RegExp(`^${TAG_PATTERN}(?:${CLASS_OR_ID_PATTERN})+`);
|
|
6773
|
+
var ATTRIBUTE_SHORTHAND = new RegExp(`^${TAG_PATTERN}(?:${CLASS_OR_ID_PATTERN})*\\s+[\\w:-]+=`);
|
|
6774
|
+
var TAG_SHORTHAND = new RegExp(`^${TAG_PATTERN}(?:>|\\s+\\S|$)`);
|
|
6775
|
+
function htmlTagName(match) {
|
|
6776
|
+
const tag = match?.[1];
|
|
6777
|
+
return tag && HTML_TAGS.has(tag) ? tag : undefined;
|
|
6778
|
+
}
|
|
6779
|
+
function describePugLikeLine(line) {
|
|
6780
|
+
if (/^\|(?:\s|$)/.test(line)) {
|
|
6781
|
+
return "piped text";
|
|
6782
|
+
}
|
|
6783
|
+
if (htmlTagName(line.match(CLASS_ID_SHORTHAND))) {
|
|
6784
|
+
return "class/id shorthand";
|
|
6785
|
+
}
|
|
6786
|
+
if (htmlTagName(line.match(ATTRIBUTE_SHORTHAND))) {
|
|
6787
|
+
return "attribute shorthand";
|
|
6788
|
+
}
|
|
6789
|
+
if (htmlTagName(line.match(TAG_SHORTHAND))) {
|
|
6790
|
+
return "tag shorthand";
|
|
6791
|
+
}
|
|
6792
|
+
return;
|
|
6793
|
+
}
|
|
6794
|
+
function updateSkipBlock(lower, skipBlock) {
|
|
6795
|
+
if (skipBlock) {
|
|
6796
|
+
if (skipBlock === "comment" && lower.includes("-->")) {
|
|
6797
|
+
return null;
|
|
6798
|
+
}
|
|
6799
|
+
if (skipBlock !== "comment" && lower.includes(`</${skipBlock}`)) {
|
|
6800
|
+
return null;
|
|
6801
|
+
}
|
|
6802
|
+
return skipBlock;
|
|
6803
|
+
}
|
|
6804
|
+
if (lower.startsWith("<!--") && !lower.includes("-->")) {
|
|
6805
|
+
return "comment";
|
|
6806
|
+
}
|
|
6807
|
+
if (lower.startsWith("<script") && !lower.includes("</script")) {
|
|
6808
|
+
return "script";
|
|
6809
|
+
}
|
|
6810
|
+
if (lower.startsWith("<style") && !lower.includes("</style")) {
|
|
6811
|
+
return "style";
|
|
6812
|
+
}
|
|
6813
|
+
return null;
|
|
6814
|
+
}
|
|
6815
|
+
function assertEtaHtmlSource(templateName, source) {
|
|
6816
|
+
let skipBlock = null;
|
|
6817
|
+
for (const rawLine of source.split(/\r?\n/)) {
|
|
6818
|
+
const trimmed = rawLine.trim();
|
|
6819
|
+
if (!trimmed) {
|
|
6820
|
+
continue;
|
|
6821
|
+
}
|
|
6822
|
+
const lower = trimmed.toLowerCase();
|
|
6823
|
+
const nextSkipBlock = updateSkipBlock(lower, skipBlock);
|
|
6824
|
+
if (skipBlock || nextSkipBlock) {
|
|
6825
|
+
skipBlock = nextSkipBlock;
|
|
6826
|
+
continue;
|
|
6827
|
+
}
|
|
6828
|
+
if (trimmed.startsWith("<") || trimmed.startsWith("<%")) {
|
|
6829
|
+
continue;
|
|
6830
|
+
}
|
|
6831
|
+
const kind = describePugLikeLine(trimmed);
|
|
6832
|
+
if (kind) {
|
|
6833
|
+
throw new Error(`Eta view "${templateName}" contains Pug-like markup (${kind}: ${trimmed}). Eta views must be HTML + <% %> tags, not Pug.`);
|
|
6834
|
+
}
|
|
6835
|
+
}
|
|
6836
|
+
}
|
|
6837
|
+
|
|
6838
|
+
// ../../src/core/view/etaViewEngine.ts
|
|
6839
|
+
var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
|
|
6840
|
+
var DEFAULT_LAYOUT = "layouts/app.eta";
|
|
6841
|
+
|
|
6842
|
+
class EtaViewEngine {
|
|
6843
|
+
eta;
|
|
6844
|
+
resolveLayoutData;
|
|
6845
|
+
constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
|
|
6846
|
+
this.eta = new Eta({
|
|
6847
|
+
views: viewsDirectory,
|
|
6848
|
+
autoTrim: false
|
|
6849
|
+
});
|
|
6850
|
+
this.resolveLayoutData = resolveLayoutData;
|
|
6851
|
+
const readFile2 = this.eta.readFile?.bind(this.eta);
|
|
6852
|
+
this.eta.readFile = (path) => {
|
|
6853
|
+
const source = readFile2 ? readFile2(path) : "";
|
|
6854
|
+
assertEtaHtmlSource(relative(viewsDirectory, path) || path, source);
|
|
6855
|
+
return source;
|
|
6856
|
+
};
|
|
6857
|
+
}
|
|
6858
|
+
async render(name, data = {}, options = {}) {
|
|
6859
|
+
const template = name.endsWith(".eta") ? name : `${name}.eta`;
|
|
6860
|
+
const request = options.request ?? currentRequestMeta().request;
|
|
6861
|
+
const layoutData = this.resolveLayoutData ? await this.resolveLayoutData(request) : {};
|
|
6862
|
+
const mergedData = { ...layoutData, ...data };
|
|
6863
|
+
const body = await this.eta.renderAsync(template, mergedData);
|
|
6864
|
+
const layout = options.layout ?? DEFAULT_LAYOUT;
|
|
6865
|
+
if (layout === false) {
|
|
6866
|
+
return body;
|
|
6867
|
+
}
|
|
6868
|
+
const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
|
|
6869
|
+
return await this.eta.renderAsync(layoutTemplate, {
|
|
6870
|
+
...mergedData,
|
|
6871
|
+
body
|
|
6872
|
+
});
|
|
6873
|
+
}
|
|
6874
|
+
}
|
|
6875
|
+
// ../../src/core/view/webLayoutData.ts
|
|
6876
|
+
var configuredLayoutOptions = {};
|
|
6877
|
+
function configureWebLayoutData(options) {
|
|
6878
|
+
configuredLayoutOptions = { ...options };
|
|
6879
|
+
}
|
|
6880
|
+
function layoutUserKey(options) {
|
|
6881
|
+
return options.userKey ?? "authUser";
|
|
6882
|
+
}
|
|
6883
|
+
async function defaultLoadLayoutUser(container, _request) {
|
|
6884
|
+
const authUser = currentAuthUser();
|
|
6885
|
+
if (!authUser) {
|
|
6886
|
+
return null;
|
|
6887
|
+
}
|
|
6888
|
+
const userId = Number(authUser.id);
|
|
6889
|
+
if (!Number.isInteger(userId) || userId <= 0) {
|
|
6890
|
+
return null;
|
|
6891
|
+
}
|
|
6892
|
+
const directory = resolveAuthUserDirectory(container);
|
|
6893
|
+
if (!directory) {
|
|
6894
|
+
return {
|
|
6895
|
+
id: userId,
|
|
6896
|
+
email: "",
|
|
6897
|
+
role: authUser.role ?? "member"
|
|
6898
|
+
};
|
|
6899
|
+
}
|
|
6900
|
+
try {
|
|
6901
|
+
const user = await directory.findByIdOrThrow(userId);
|
|
6902
|
+
return {
|
|
6903
|
+
id: userId,
|
|
6904
|
+
email: user.email ?? "",
|
|
6905
|
+
role: authUser.role ?? user.role ?? "member"
|
|
6906
|
+
};
|
|
6907
|
+
} catch {
|
|
6908
|
+
return null;
|
|
6909
|
+
}
|
|
6910
|
+
}
|
|
6911
|
+
async function resolveWebLayoutData(container, request, options = {}) {
|
|
6912
|
+
const resolved = { ...configuredLayoutOptions, ...options };
|
|
6913
|
+
const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
|
|
6914
|
+
const flash = request ? currentRequestMeta().flash ?? pullFlash(request) : null;
|
|
6915
|
+
const userKey = layoutUserKey(resolved);
|
|
6916
|
+
const loadUser = resolved.loadUser ?? defaultLoadLayoutUser;
|
|
6917
|
+
const user = await loadUser(container, request);
|
|
6918
|
+
const extra = typeof resolved.extra === "function" ? await resolved.extra(user) : resolved.extra ?? {};
|
|
6919
|
+
return {
|
|
6920
|
+
[userKey]: user,
|
|
6921
|
+
csrfToken,
|
|
6922
|
+
flash,
|
|
6923
|
+
cspNonce: currentRequestMeta().cspNonce ?? "",
|
|
6924
|
+
...extra
|
|
6925
|
+
};
|
|
6926
|
+
}
|
|
6666
6927
|
export {
|
|
6667
6928
|
AdminResourceRegistry,
|
|
6668
6929
|
ApiTokenGuard,
|
|
@@ -6735,7 +6996,8 @@ export {
|
|
|
6735
6996
|
authContext,
|
|
6736
6997
|
belongsTo,
|
|
6737
6998
|
belongsToMany,
|
|
6738
|
-
|
|
6999
|
+
bindBunSql,
|
|
7000
|
+
bindDatabaseConnection,
|
|
6739
7001
|
bindRouteModel,
|
|
6740
7002
|
buildMarkdownMailMessage,
|
|
6741
7003
|
buildRequestCacheKey,
|
|
@@ -6746,11 +7008,14 @@ export {
|
|
|
6746
7008
|
composeMiddleware,
|
|
6747
7009
|
conditionalJsonResponse,
|
|
6748
7010
|
config,
|
|
7011
|
+
configureContentSecurityPolicy,
|
|
6749
7012
|
configureMembershipLookup,
|
|
7013
|
+
configureWebErrorView,
|
|
6750
7014
|
configureWebLayoutData,
|
|
6751
7015
|
createAuthMiddleware,
|
|
6752
7016
|
createAuthorizeMiddleware,
|
|
6753
7017
|
createBodySizeLimitMiddleware,
|
|
7018
|
+
createBunSqlPool,
|
|
6754
7019
|
createCacheStore,
|
|
6755
7020
|
createCorsMiddleware,
|
|
6756
7021
|
createCsrfMiddleware,
|
|
@@ -6794,13 +7059,16 @@ export {
|
|
|
6794
7059
|
dehydrateValue,
|
|
6795
7060
|
emailRule,
|
|
6796
7061
|
emptyPaginateResult,
|
|
7062
|
+
errorTemplateName,
|
|
6797
7063
|
etagFromResource,
|
|
6798
7064
|
eventBus,
|
|
6799
7065
|
events,
|
|
6800
7066
|
filterMassAssignable,
|
|
6801
7067
|
formatAdminValue,
|
|
6802
7068
|
freshDatabase,
|
|
7069
|
+
generateCspNonce,
|
|
6803
7070
|
getActiveDatabaseConnection,
|
|
7071
|
+
getBoundDatabaseConnection,
|
|
6804
7072
|
getDefaultDatabasePool,
|
|
6805
7073
|
getDefaultDatabaseQuery,
|
|
6806
7074
|
getMigrationStatus,
|
|
@@ -6810,6 +7078,7 @@ export {
|
|
|
6810
7078
|
hasMinimumOrgRole2 as hasMinimumOrgRole,
|
|
6811
7079
|
hasOne,
|
|
6812
7080
|
hasOrgMembership,
|
|
7081
|
+
htmlErrorResponse,
|
|
6813
7082
|
htmlResponse,
|
|
6814
7083
|
hydrateValue,
|
|
6815
7084
|
indexBelongsToManyRelation,
|
|
@@ -6833,6 +7102,7 @@ export {
|
|
|
6833
7102
|
loadSeedersFromDirectory,
|
|
6834
7103
|
log,
|
|
6835
7104
|
logSecurityEvent,
|
|
7105
|
+
loginRedirectLocation,
|
|
6836
7106
|
mail,
|
|
6837
7107
|
mailer,
|
|
6838
7108
|
markdownToHtml,
|
|
@@ -6864,11 +7134,14 @@ export {
|
|
|
6864
7134
|
registerDefaultDatabasePool,
|
|
6865
7135
|
registerModelRepository,
|
|
6866
7136
|
registerShutdownHandler,
|
|
7137
|
+
renderKernelErrorChrome,
|
|
6867
7138
|
renderMarkdownMail,
|
|
6868
7139
|
repositoryConnection,
|
|
6869
7140
|
requestIdMiddleware,
|
|
6870
7141
|
required,
|
|
7142
|
+
resetBoundDatabaseConnection,
|
|
6871
7143
|
resetDefaultStorage,
|
|
7144
|
+
resetMemoryThrottleForTests,
|
|
6872
7145
|
resolveApplicationAuth,
|
|
6873
7146
|
resolveApplicationCache,
|
|
6874
7147
|
resolveApplicationConfig,
|
|
@@ -6877,9 +7150,11 @@ export {
|
|
|
6877
7150
|
resolveApplicationLogger,
|
|
6878
7151
|
resolveApplicationPolicyGate,
|
|
6879
7152
|
resolveApplicationQueue,
|
|
7153
|
+
resolveContentSecurityPolicy,
|
|
6880
7154
|
resolveCsrfToken,
|
|
6881
7155
|
resolveCsrfTokenForRequest,
|
|
6882
7156
|
resolveDatabaseDriver,
|
|
7157
|
+
resolveHtmlContentSecurityPolicy,
|
|
6883
7158
|
resolveMembershipService,
|
|
6884
7159
|
resolveOrganizationScope,
|
|
6885
7160
|
resolveRepositoryConnection,
|
|
@@ -6888,6 +7163,7 @@ export {
|
|
|
6888
7163
|
resolveUserTenantId,
|
|
6889
7164
|
resolveWebLayoutData,
|
|
6890
7165
|
rollbackDatabase,
|
|
7166
|
+
rssResponse,
|
|
6891
7167
|
runDueScheduledTasks,
|
|
6892
7168
|
runGracefulShutdown,
|
|
6893
7169
|
runInTransaction,
|
|
@@ -6899,13 +7175,18 @@ export {
|
|
|
6899
7175
|
runWithTenant,
|
|
6900
7176
|
runWithTenantDatabase,
|
|
6901
7177
|
runWithTraceContext,
|
|
7178
|
+
safeInternalRedirectPath,
|
|
7179
|
+
sanitizeInternalPath,
|
|
6902
7180
|
scopedOrganizationIds,
|
|
6903
7181
|
securedBindRouteModel,
|
|
6904
7182
|
securedBindRouteModelByKey,
|
|
6905
7183
|
sendMarkdownMail,
|
|
6906
7184
|
serializeDate,
|
|
7185
|
+
serverHtmxContentSecurityPolicy,
|
|
6907
7186
|
setActiveApplicationContext,
|
|
7187
|
+
spaContentSecurityPolicy,
|
|
6908
7188
|
storageFacade as storage,
|
|
7189
|
+
strictApiContentSecurityPolicy,
|
|
6909
7190
|
stringRule,
|
|
6910
7191
|
stripMarkdown,
|
|
6911
7192
|
textResponse,
|