@getstrata/core 0.5.60 → 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 +10 -0
- package/README.md +1 -1
- package/dist/core/http/contentSecurityPolicy.d.ts +27 -0
- 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 +1 -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/http/contentSecurityPolicy.js +1 -0
- package/dist/entries/http/requireWebAuthMiddleware.js +34 -2
- package/dist/entries/http/response.js +127 -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 +126 -554
- package/dist/entries/logging/requestLoggingMiddleware.js +2 -1
- package/dist/entries/view.js +88 -8
- package/dist/framework/public-api.d.ts +4 -1
- package/dist/index.js +589 -340
- package/package.json +12 -2
- package/dist/config/contentSecurityPolicy.d.ts +0 -5
package/dist/index.js
CHANGED
|
@@ -4306,6 +4306,133 @@ function conditionalJsonResponse(request, data, init = {}) {
|
|
|
4306
4306
|
headers
|
|
4307
4307
|
});
|
|
4308
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
|
+
}
|
|
4309
4436
|
// ../../src/core/http/cookies.ts
|
|
4310
4437
|
function readRequestCookie(request, name) {
|
|
4311
4438
|
const cookies = request.cookies;
|
|
@@ -4906,219 +5033,6 @@ function isViewsEnabled() {
|
|
|
4906
5033
|
return readFrontendMode() === "server-htmx";
|
|
4907
5034
|
}
|
|
4908
5035
|
|
|
4909
|
-
// ../../src/core/view/etaViewEngine.ts
|
|
4910
|
-
import { join as join4, relative } from "path";
|
|
4911
|
-
import { Eta } from "eta";
|
|
4912
|
-
|
|
4913
|
-
// ../../src/core/view/assertEtaHtmlSource.ts
|
|
4914
|
-
var HTML_TAGS = new Set([
|
|
4915
|
-
"a",
|
|
4916
|
-
"abbr",
|
|
4917
|
-
"address",
|
|
4918
|
-
"article",
|
|
4919
|
-
"aside",
|
|
4920
|
-
"audio",
|
|
4921
|
-
"b",
|
|
4922
|
-
"blockquote",
|
|
4923
|
-
"body",
|
|
4924
|
-
"button",
|
|
4925
|
-
"canvas",
|
|
4926
|
-
"caption",
|
|
4927
|
-
"cite",
|
|
4928
|
-
"code",
|
|
4929
|
-
"col",
|
|
4930
|
-
"colgroup",
|
|
4931
|
-
"data",
|
|
4932
|
-
"datalist",
|
|
4933
|
-
"dd",
|
|
4934
|
-
"del",
|
|
4935
|
-
"details",
|
|
4936
|
-
"dfn",
|
|
4937
|
-
"dialog",
|
|
4938
|
-
"div",
|
|
4939
|
-
"dl",
|
|
4940
|
-
"dt",
|
|
4941
|
-
"em",
|
|
4942
|
-
"fieldset",
|
|
4943
|
-
"figcaption",
|
|
4944
|
-
"figure",
|
|
4945
|
-
"footer",
|
|
4946
|
-
"form",
|
|
4947
|
-
"h1",
|
|
4948
|
-
"h2",
|
|
4949
|
-
"h3",
|
|
4950
|
-
"h4",
|
|
4951
|
-
"h5",
|
|
4952
|
-
"h6",
|
|
4953
|
-
"header",
|
|
4954
|
-
"hgroup",
|
|
4955
|
-
"hr",
|
|
4956
|
-
"html",
|
|
4957
|
-
"i",
|
|
4958
|
-
"iframe",
|
|
4959
|
-
"img",
|
|
4960
|
-
"input",
|
|
4961
|
-
"ins",
|
|
4962
|
-
"kbd",
|
|
4963
|
-
"label",
|
|
4964
|
-
"legend",
|
|
4965
|
-
"li",
|
|
4966
|
-
"main",
|
|
4967
|
-
"map",
|
|
4968
|
-
"mark",
|
|
4969
|
-
"menu",
|
|
4970
|
-
"meter",
|
|
4971
|
-
"nav",
|
|
4972
|
-
"object",
|
|
4973
|
-
"ol",
|
|
4974
|
-
"optgroup",
|
|
4975
|
-
"option",
|
|
4976
|
-
"output",
|
|
4977
|
-
"p",
|
|
4978
|
-
"picture",
|
|
4979
|
-
"pre",
|
|
4980
|
-
"progress",
|
|
4981
|
-
"q",
|
|
4982
|
-
"rp",
|
|
4983
|
-
"rt",
|
|
4984
|
-
"ruby",
|
|
4985
|
-
"s",
|
|
4986
|
-
"samp",
|
|
4987
|
-
"script",
|
|
4988
|
-
"search",
|
|
4989
|
-
"section",
|
|
4990
|
-
"select",
|
|
4991
|
-
"slot",
|
|
4992
|
-
"small",
|
|
4993
|
-
"source",
|
|
4994
|
-
"span",
|
|
4995
|
-
"strong",
|
|
4996
|
-
"style",
|
|
4997
|
-
"sub",
|
|
4998
|
-
"summary",
|
|
4999
|
-
"sup",
|
|
5000
|
-
"table",
|
|
5001
|
-
"tbody",
|
|
5002
|
-
"td",
|
|
5003
|
-
"template",
|
|
5004
|
-
"textarea",
|
|
5005
|
-
"tfoot",
|
|
5006
|
-
"th",
|
|
5007
|
-
"thead",
|
|
5008
|
-
"time",
|
|
5009
|
-
"title",
|
|
5010
|
-
"tr",
|
|
5011
|
-
"track",
|
|
5012
|
-
"u",
|
|
5013
|
-
"ul",
|
|
5014
|
-
"var",
|
|
5015
|
-
"video",
|
|
5016
|
-
"wbr"
|
|
5017
|
-
]);
|
|
5018
|
-
var TAG_PATTERN = "([a-z][a-z0-9]*)";
|
|
5019
|
-
var CLASS_OR_ID_PATTERN = "[.#][\\w-]+";
|
|
5020
|
-
var CLASS_ID_SHORTHAND = new RegExp(`^${TAG_PATTERN}(?:${CLASS_OR_ID_PATTERN})+`);
|
|
5021
|
-
var ATTRIBUTE_SHORTHAND = new RegExp(`^${TAG_PATTERN}(?:${CLASS_OR_ID_PATTERN})*\\s+[\\w:-]+=`);
|
|
5022
|
-
var TAG_SHORTHAND = new RegExp(`^${TAG_PATTERN}(?:>|\\s+\\S|$)`);
|
|
5023
|
-
function htmlTagName(match) {
|
|
5024
|
-
const tag = match?.[1];
|
|
5025
|
-
return tag && HTML_TAGS.has(tag) ? tag : undefined;
|
|
5026
|
-
}
|
|
5027
|
-
function describePugLikeLine(line) {
|
|
5028
|
-
if (/^\|(?:\s|$)/.test(line)) {
|
|
5029
|
-
return "piped text";
|
|
5030
|
-
}
|
|
5031
|
-
if (htmlTagName(line.match(CLASS_ID_SHORTHAND))) {
|
|
5032
|
-
return "class/id shorthand";
|
|
5033
|
-
}
|
|
5034
|
-
if (htmlTagName(line.match(ATTRIBUTE_SHORTHAND))) {
|
|
5035
|
-
return "attribute shorthand";
|
|
5036
|
-
}
|
|
5037
|
-
if (htmlTagName(line.match(TAG_SHORTHAND))) {
|
|
5038
|
-
return "tag shorthand";
|
|
5039
|
-
}
|
|
5040
|
-
return;
|
|
5041
|
-
}
|
|
5042
|
-
function updateSkipBlock(lower, skipBlock) {
|
|
5043
|
-
if (skipBlock) {
|
|
5044
|
-
if (skipBlock === "comment" && lower.includes("-->")) {
|
|
5045
|
-
return null;
|
|
5046
|
-
}
|
|
5047
|
-
if (skipBlock !== "comment" && lower.includes(`</${skipBlock}`)) {
|
|
5048
|
-
return null;
|
|
5049
|
-
}
|
|
5050
|
-
return skipBlock;
|
|
5051
|
-
}
|
|
5052
|
-
if (lower.startsWith("<!--") && !lower.includes("-->")) {
|
|
5053
|
-
return "comment";
|
|
5054
|
-
}
|
|
5055
|
-
if (lower.startsWith("<script") && !lower.includes("</script")) {
|
|
5056
|
-
return "script";
|
|
5057
|
-
}
|
|
5058
|
-
if (lower.startsWith("<style") && !lower.includes("</style")) {
|
|
5059
|
-
return "style";
|
|
5060
|
-
}
|
|
5061
|
-
return null;
|
|
5062
|
-
}
|
|
5063
|
-
function assertEtaHtmlSource(templateName, source) {
|
|
5064
|
-
let skipBlock = null;
|
|
5065
|
-
for (const rawLine of source.split(/\r?\n/)) {
|
|
5066
|
-
const trimmed = rawLine.trim();
|
|
5067
|
-
if (!trimmed) {
|
|
5068
|
-
continue;
|
|
5069
|
-
}
|
|
5070
|
-
const lower = trimmed.toLowerCase();
|
|
5071
|
-
const nextSkipBlock = updateSkipBlock(lower, skipBlock);
|
|
5072
|
-
if (skipBlock || nextSkipBlock) {
|
|
5073
|
-
skipBlock = nextSkipBlock;
|
|
5074
|
-
continue;
|
|
5075
|
-
}
|
|
5076
|
-
if (trimmed.startsWith("<") || trimmed.startsWith("<%")) {
|
|
5077
|
-
continue;
|
|
5078
|
-
}
|
|
5079
|
-
const kind = describePugLikeLine(trimmed);
|
|
5080
|
-
if (kind) {
|
|
5081
|
-
throw new Error(`Eta view "${templateName}" contains Pug-like markup (${kind}: ${trimmed}). Eta views must be HTML + <% %> tags, not Pug.`);
|
|
5082
|
-
}
|
|
5083
|
-
}
|
|
5084
|
-
}
|
|
5085
|
-
|
|
5086
|
-
// ../../src/core/view/etaViewEngine.ts
|
|
5087
|
-
var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
|
|
5088
|
-
var DEFAULT_LAYOUT = "layouts/app.eta";
|
|
5089
|
-
|
|
5090
|
-
class EtaViewEngine {
|
|
5091
|
-
eta;
|
|
5092
|
-
resolveLayoutData;
|
|
5093
|
-
constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
|
|
5094
|
-
this.eta = new Eta({
|
|
5095
|
-
views: viewsDirectory,
|
|
5096
|
-
autoTrim: false
|
|
5097
|
-
});
|
|
5098
|
-
this.resolveLayoutData = resolveLayoutData;
|
|
5099
|
-
const readFile2 = this.eta.readFile?.bind(this.eta);
|
|
5100
|
-
this.eta.readFile = (path) => {
|
|
5101
|
-
const source = readFile2 ? readFile2(path) : "";
|
|
5102
|
-
assertEtaHtmlSource(relative(viewsDirectory, path) || path, source);
|
|
5103
|
-
return source;
|
|
5104
|
-
};
|
|
5105
|
-
}
|
|
5106
|
-
async render(name, data = {}, options = {}) {
|
|
5107
|
-
const template = name.endsWith(".eta") ? name : `${name}.eta`;
|
|
5108
|
-
const layoutData = this.resolveLayoutData ? await this.resolveLayoutData() : {};
|
|
5109
|
-
const mergedData = { ...layoutData, ...data };
|
|
5110
|
-
const body = await this.eta.renderAsync(template, mergedData);
|
|
5111
|
-
const layout = options.layout ?? DEFAULT_LAYOUT;
|
|
5112
|
-
if (layout === false) {
|
|
5113
|
-
return body;
|
|
5114
|
-
}
|
|
5115
|
-
const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
|
|
5116
|
-
return await this.eta.renderAsync(layoutTemplate, {
|
|
5117
|
-
...mergedData,
|
|
5118
|
-
body
|
|
5119
|
-
});
|
|
5120
|
-
}
|
|
5121
|
-
}
|
|
5122
5036
|
// ../../src/core/view/htmlResponse.ts
|
|
5123
5037
|
function withCharset(contentType) {
|
|
5124
5038
|
return contentType.includes("charset=") ? contentType : `${contentType}; charset=utf-8`;
|
|
@@ -5143,9 +5057,6 @@ function redirectResponse(location, status = 302) {
|
|
|
5143
5057
|
}
|
|
5144
5058
|
});
|
|
5145
5059
|
}
|
|
5146
|
-
function notFoundHtmlResponse(body = "Not Found") {
|
|
5147
|
-
return htmlResponse(body, { status: 404 });
|
|
5148
|
-
}
|
|
5149
5060
|
function textResponse(body, init = {}) {
|
|
5150
5061
|
return new Response(body, {
|
|
5151
5062
|
status: init.status ?? 200,
|
|
@@ -5165,57 +5076,82 @@ function xmlResponse(body, init = {}) {
|
|
|
5165
5076
|
function rssResponse(body, init = {}) {
|
|
5166
5077
|
return xmlResponse(body, { ...init, contentType: "application/rss+xml" });
|
|
5167
5078
|
}
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5079
|
+
|
|
5080
|
+
// ../../src/core/view/webErrorView.ts
|
|
5081
|
+
var configuredErrorView = {};
|
|
5082
|
+
function configureWebErrorView(options) {
|
|
5083
|
+
configuredErrorView = { ...options };
|
|
5172
5084
|
}
|
|
5173
|
-
function
|
|
5174
|
-
return
|
|
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
|
+
`;
|
|
5175
5117
|
}
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
return null;
|
|
5118
|
+
function errorTemplateName(status) {
|
|
5119
|
+
if (status === 404) {
|
|
5120
|
+
return "errors/not-found";
|
|
5180
5121
|
}
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
return null;
|
|
5122
|
+
if (status === 403) {
|
|
5123
|
+
return "errors/forbidden";
|
|
5184
5124
|
}
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
};
|
|
5125
|
+
return "errors/error";
|
|
5126
|
+
}
|
|
5127
|
+
async function renderWebErrorHtml(input) {
|
|
5128
|
+
const render = configuredErrorView.render;
|
|
5129
|
+
if (!render) {
|
|
5130
|
+
return renderKernelErrorChrome(input);
|
|
5192
5131
|
}
|
|
5193
5132
|
try {
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
role: authUser.role ?? user.role ?? "member"
|
|
5199
|
-
};
|
|
5133
|
+
return await render({
|
|
5134
|
+
...input,
|
|
5135
|
+
request: input.request ?? currentRequestMeta().request
|
|
5136
|
+
});
|
|
5200
5137
|
} catch {
|
|
5201
|
-
return
|
|
5138
|
+
return renderKernelErrorChrome(input);
|
|
5202
5139
|
}
|
|
5203
5140
|
}
|
|
5204
|
-
async function
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
...extra
|
|
5217
|
-
};
|
|
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 });
|
|
5147
|
+
}
|
|
5148
|
+
return htmlErrorResponse({
|
|
5149
|
+
status: 404,
|
|
5150
|
+
title: "Not Found",
|
|
5151
|
+
message: "The page you requested was not found."
|
|
5152
|
+
});
|
|
5218
5153
|
}
|
|
5154
|
+
|
|
5219
5155
|
// ../../src/core/http/contentNegotiation.ts
|
|
5220
5156
|
function requestPrefersJson(request) {
|
|
5221
5157
|
if (!request) {
|
|
@@ -5239,6 +5175,39 @@ function requestPrefersJson(request) {
|
|
|
5239
5175
|
return pathname.startsWith("/api/");
|
|
5240
5176
|
}
|
|
5241
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
|
+
|
|
5242
5211
|
// ../../src/core/http/webErrorResponse.ts
|
|
5243
5212
|
function normalizeFieldErrors(details) {
|
|
5244
5213
|
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
|
@@ -5256,23 +5225,37 @@ function normalizeFieldErrors(details) {
|
|
|
5256
5225
|
}
|
|
5257
5226
|
return errors;
|
|
5258
5227
|
}
|
|
5259
|
-
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) {
|
|
5260
5245
|
if (!request || !isViewsEnabled() || requestPrefersJson(request)) {
|
|
5261
5246
|
return null;
|
|
5262
5247
|
}
|
|
5263
5248
|
const mappedError = error instanceof HttpError ? error : mapDatabaseError(error);
|
|
5264
5249
|
if (mappedError instanceof UnauthorizedError) {
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
return htmlResponse(`<section class="page-header"><h1>${mappedError.message}</h1></section>`, {
|
|
5275
|
-
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
|
|
5276
5259
|
});
|
|
5277
5260
|
}
|
|
5278
5261
|
|
|
@@ -5302,7 +5285,7 @@ function withErrorHandling(handler) {
|
|
|
5302
5285
|
return await handler(...args);
|
|
5303
5286
|
} catch (error) {
|
|
5304
5287
|
const request = args.find((arg) => arg instanceof Request);
|
|
5305
|
-
const webResponse = webErrorResponse(error, request);
|
|
5288
|
+
const webResponse = await webErrorResponse(error, request);
|
|
5306
5289
|
if (webResponse) {
|
|
5307
5290
|
return webResponse;
|
|
5308
5291
|
}
|
|
@@ -5329,10 +5312,32 @@ function bindRouteModel(param, resolver, handler) {
|
|
|
5329
5312
|
function isMutatingPolicyAction(action) {
|
|
5330
5313
|
return action === "update" || action === "delete";
|
|
5331
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
|
+
}
|
|
5332
5337
|
function securedBindRouteModel(param, resolver, authorization, handler) {
|
|
5333
5338
|
return async (request) => {
|
|
5334
5339
|
const id = parsePositiveIntParam(String(request.params[param]), String(param));
|
|
5335
|
-
const model = await resolver(id, request);
|
|
5340
|
+
const model = requireResolvedModel(await resolver(id, request));
|
|
5336
5341
|
const gate = resolveApplicationPolicyGate();
|
|
5337
5342
|
const auth2 = resolveApplicationAuth();
|
|
5338
5343
|
const user = currentAuthUser() ?? await auth2.resolve(request);
|
|
@@ -5343,7 +5348,7 @@ function securedBindRouteModel(param, resolver, authorization, handler) {
|
|
|
5343
5348
|
});
|
|
5344
5349
|
}
|
|
5345
5350
|
const response = await handler(request, model);
|
|
5346
|
-
if (isEtagEnabled() && authorization.action === "view") {
|
|
5351
|
+
if (isEtagEnabled() && authorization.action === "view" && shouldApplyViewEtag(response, model, authorization)) {
|
|
5347
5352
|
return applyConditionalGet(request, response, etagFromResource(model));
|
|
5348
5353
|
}
|
|
5349
5354
|
return response;
|
|
@@ -5355,7 +5360,7 @@ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
|
|
|
5355
5360
|
if (!key) {
|
|
5356
5361
|
throw new BadRequestError(`Missing route parameter "${String(param)}".`);
|
|
5357
5362
|
}
|
|
5358
|
-
const model = await resolver(key, request);
|
|
5363
|
+
const model = requireResolvedModel(await resolver(key, request));
|
|
5359
5364
|
const gate = resolveApplicationPolicyGate();
|
|
5360
5365
|
const auth2 = resolveApplicationAuth();
|
|
5361
5366
|
const user = currentAuthUser() ?? await auth2.resolve(request);
|
|
@@ -5366,7 +5371,7 @@ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
|
|
|
5366
5371
|
});
|
|
5367
5372
|
}
|
|
5368
5373
|
const response = await handler(request, model);
|
|
5369
|
-
if (isEtagEnabled() && authorization.action === "view") {
|
|
5374
|
+
if (isEtagEnabled() && authorization.action === "view" && shouldApplyViewEtag(response, model, authorization)) {
|
|
5370
5375
|
return applyConditionalGet(request, response, etagFromResource(model));
|
|
5371
5376
|
}
|
|
5372
5377
|
return response;
|
|
@@ -5589,8 +5594,7 @@ function createRequireWebAuthMiddleware(auth2) {
|
|
|
5589
5594
|
if (requestPrefersJson(request)) {
|
|
5590
5595
|
throw new UnauthorizedError;
|
|
5591
5596
|
}
|
|
5592
|
-
|
|
5593
|
-
return Response.redirect(`/login?redirect=${redirectTarget}`, 302);
|
|
5597
|
+
return Response.redirect(loginRedirectLocation(request), 302);
|
|
5594
5598
|
};
|
|
5595
5599
|
}
|
|
5596
5600
|
// ../../src/core/http/scimThrottleMiddleware.ts
|
|
@@ -5643,67 +5647,31 @@ var appConfig = {
|
|
|
5643
5647
|
apiPrefix: process.env.API_PREFIX ?? "/api/v1"
|
|
5644
5648
|
};
|
|
5645
5649
|
|
|
5646
|
-
// ../../src/config/contentSecurityPolicy.ts
|
|
5647
|
-
function strictApiContentSecurityPolicy() {
|
|
5648
|
-
return "default-src 'none'; frame-ancestors 'none'; base-uri 'none'";
|
|
5649
|
-
}
|
|
5650
|
-
function serverHtmxContentSecurityPolicy() {
|
|
5651
|
-
return [
|
|
5652
|
-
"default-src 'self'",
|
|
5653
|
-
"script-src 'self' https://unpkg.com",
|
|
5654
|
-
"style-src 'self'",
|
|
5655
|
-
"connect-src 'self'",
|
|
5656
|
-
"img-src 'self'",
|
|
5657
|
-
"font-src 'self'",
|
|
5658
|
-
"form-action 'self'",
|
|
5659
|
-
"frame-ancestors 'none'",
|
|
5660
|
-
"base-uri 'self'"
|
|
5661
|
-
].join("; ");
|
|
5662
|
-
}
|
|
5663
|
-
function spaContentSecurityPolicy() {
|
|
5664
|
-
return [
|
|
5665
|
-
"default-src 'self'",
|
|
5666
|
-
"script-src 'self'",
|
|
5667
|
-
"style-src 'self'",
|
|
5668
|
-
"connect-src 'self'",
|
|
5669
|
-
"img-src 'self'",
|
|
5670
|
-
"font-src 'self'",
|
|
5671
|
-
"frame-ancestors 'none'",
|
|
5672
|
-
"base-uri 'self'"
|
|
5673
|
-
].join("; ");
|
|
5674
|
-
}
|
|
5675
|
-
function resolveContentSecurityPolicy(response) {
|
|
5676
|
-
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
5677
|
-
if (!contentType.includes("text/html")) {
|
|
5678
|
-
return strictApiContentSecurityPolicy();
|
|
5679
|
-
}
|
|
5680
|
-
const frontendMode = (process.env.FRONTEND_MODE ?? "api").trim();
|
|
5681
|
-
if (frontendMode === "server-htmx") {
|
|
5682
|
-
return serverHtmxContentSecurityPolicy();
|
|
5683
|
-
}
|
|
5684
|
-
if (frontendMode === "spa-react") {
|
|
5685
|
-
return spaContentSecurityPolicy();
|
|
5686
|
-
}
|
|
5687
|
-
return strictApiContentSecurityPolicy();
|
|
5688
|
-
}
|
|
5689
|
-
|
|
5690
5650
|
// ../../src/core/http/securityHeadersMiddleware.ts
|
|
5691
|
-
function createSecurityHeadersMiddleware() {
|
|
5692
|
-
return async (
|
|
5693
|
-
const
|
|
5694
|
-
const
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
headers
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
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
|
+
});
|
|
5707
5675
|
});
|
|
5708
5676
|
};
|
|
5709
5677
|
}
|
|
@@ -5825,6 +5793,7 @@ function installGracefulShutdownSignals(signals = ["SIGINT", "SIGTERM"]) {
|
|
|
5825
5793
|
function createRequestLoggingMiddleware() {
|
|
5826
5794
|
return async (request, next) => {
|
|
5827
5795
|
return await runWithRequestMeta({
|
|
5796
|
+
...currentRequestMeta(),
|
|
5828
5797
|
ipAddress: readClientIp(request) ?? null,
|
|
5829
5798
|
userAgent: request.headers.get("user-agent"),
|
|
5830
5799
|
request
|
|
@@ -5845,7 +5814,7 @@ function createRequestLoggingMiddleware() {
|
|
|
5845
5814
|
};
|
|
5846
5815
|
}
|
|
5847
5816
|
// ../../src/core/mail/markdownMail.ts
|
|
5848
|
-
function
|
|
5817
|
+
function escapeHtml2(value) {
|
|
5849
5818
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5850
5819
|
}
|
|
5851
5820
|
function stripMarkdown(markdown) {
|
|
@@ -5864,9 +5833,9 @@ function markdownToHtml(markdown) {
|
|
|
5864
5833
|
`);
|
|
5865
5834
|
}
|
|
5866
5835
|
function wrapMarkdownMailLayout(bodyHtml, options = {}) {
|
|
5867
|
-
const title =
|
|
5868
|
-
const preview =
|
|
5869
|
-
const footer =
|
|
5836
|
+
const title = escapeHtml2(options.title ?? "GetStrata");
|
|
5837
|
+
const preview = escapeHtml2(options.preview ?? "");
|
|
5838
|
+
const footer = escapeHtml2(options.footer ?? "Sent by GetStrata");
|
|
5870
5839
|
return `<!DOCTYPE html>
|
|
5871
5840
|
<html lang="en">
|
|
5872
5841
|
<head>
|
|
@@ -6689,6 +6658,272 @@ function validateObject(payload, schema) {
|
|
|
6689
6658
|
}
|
|
6690
6659
|
return output;
|
|
6691
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
|
+
}
|
|
6692
6927
|
export {
|
|
6693
6928
|
AdminResourceRegistry,
|
|
6694
6929
|
ApiTokenGuard,
|
|
@@ -6773,7 +7008,9 @@ export {
|
|
|
6773
7008
|
composeMiddleware,
|
|
6774
7009
|
conditionalJsonResponse,
|
|
6775
7010
|
config,
|
|
7011
|
+
configureContentSecurityPolicy,
|
|
6776
7012
|
configureMembershipLookup,
|
|
7013
|
+
configureWebErrorView,
|
|
6777
7014
|
configureWebLayoutData,
|
|
6778
7015
|
createAuthMiddleware,
|
|
6779
7016
|
createAuthorizeMiddleware,
|
|
@@ -6822,12 +7059,14 @@ export {
|
|
|
6822
7059
|
dehydrateValue,
|
|
6823
7060
|
emailRule,
|
|
6824
7061
|
emptyPaginateResult,
|
|
7062
|
+
errorTemplateName,
|
|
6825
7063
|
etagFromResource,
|
|
6826
7064
|
eventBus,
|
|
6827
7065
|
events,
|
|
6828
7066
|
filterMassAssignable,
|
|
6829
7067
|
formatAdminValue,
|
|
6830
7068
|
freshDatabase,
|
|
7069
|
+
generateCspNonce,
|
|
6831
7070
|
getActiveDatabaseConnection,
|
|
6832
7071
|
getBoundDatabaseConnection,
|
|
6833
7072
|
getDefaultDatabasePool,
|
|
@@ -6839,6 +7078,7 @@ export {
|
|
|
6839
7078
|
hasMinimumOrgRole2 as hasMinimumOrgRole,
|
|
6840
7079
|
hasOne,
|
|
6841
7080
|
hasOrgMembership,
|
|
7081
|
+
htmlErrorResponse,
|
|
6842
7082
|
htmlResponse,
|
|
6843
7083
|
hydrateValue,
|
|
6844
7084
|
indexBelongsToManyRelation,
|
|
@@ -6862,6 +7102,7 @@ export {
|
|
|
6862
7102
|
loadSeedersFromDirectory,
|
|
6863
7103
|
log,
|
|
6864
7104
|
logSecurityEvent,
|
|
7105
|
+
loginRedirectLocation,
|
|
6865
7106
|
mail,
|
|
6866
7107
|
mailer,
|
|
6867
7108
|
markdownToHtml,
|
|
@@ -6893,6 +7134,7 @@ export {
|
|
|
6893
7134
|
registerDefaultDatabasePool,
|
|
6894
7135
|
registerModelRepository,
|
|
6895
7136
|
registerShutdownHandler,
|
|
7137
|
+
renderKernelErrorChrome,
|
|
6896
7138
|
renderMarkdownMail,
|
|
6897
7139
|
repositoryConnection,
|
|
6898
7140
|
requestIdMiddleware,
|
|
@@ -6908,9 +7150,11 @@ export {
|
|
|
6908
7150
|
resolveApplicationLogger,
|
|
6909
7151
|
resolveApplicationPolicyGate,
|
|
6910
7152
|
resolveApplicationQueue,
|
|
7153
|
+
resolveContentSecurityPolicy,
|
|
6911
7154
|
resolveCsrfToken,
|
|
6912
7155
|
resolveCsrfTokenForRequest,
|
|
6913
7156
|
resolveDatabaseDriver,
|
|
7157
|
+
resolveHtmlContentSecurityPolicy,
|
|
6914
7158
|
resolveMembershipService,
|
|
6915
7159
|
resolveOrganizationScope,
|
|
6916
7160
|
resolveRepositoryConnection,
|
|
@@ -6931,13 +7175,18 @@ export {
|
|
|
6931
7175
|
runWithTenant,
|
|
6932
7176
|
runWithTenantDatabase,
|
|
6933
7177
|
runWithTraceContext,
|
|
7178
|
+
safeInternalRedirectPath,
|
|
7179
|
+
sanitizeInternalPath,
|
|
6934
7180
|
scopedOrganizationIds,
|
|
6935
7181
|
securedBindRouteModel,
|
|
6936
7182
|
securedBindRouteModelByKey,
|
|
6937
7183
|
sendMarkdownMail,
|
|
6938
7184
|
serializeDate,
|
|
7185
|
+
serverHtmxContentSecurityPolicy,
|
|
6939
7186
|
setActiveApplicationContext,
|
|
7187
|
+
spaContentSecurityPolicy,
|
|
6940
7188
|
storageFacade as storage,
|
|
7189
|
+
strictApiContentSecurityPolicy,
|
|
6941
7190
|
stringRule,
|
|
6942
7191
|
stripMarkdown,
|
|
6943
7192
|
textResponse,
|