@getstrata/core 0.5.60 → 0.5.62
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 +22 -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/signedUrl.d.ts +11 -0
- package/dist/core/http/webErrorResponse.d.ts +1 -1
- package/dist/core/jobs/exportAuditLogsJob.d.ts +12 -0
- package/dist/core/mail/sanitizeMailHtml.d.ts +2 -0
- package/dist/core/security/totp.d.ts +7 -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/audit/exportAuditLogs.js +1 -1
- package/dist/entries/http/contentSecurityPolicy.js +1 -0
- package/dist/entries/http/requireAbilityMiddleware.js +44 -1
- package/dist/entries/http/requireWebAuthMiddleware.js +36 -3
- 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 +34 -61
- package/dist/entries/http/signedUrl.js +97 -0
- package/dist/entries/http/webErrorResponse.js +126 -554
- package/dist/entries/jobs/dispatchWebhookJob.js +14 -12
- package/dist/entries/jobs/exportAuditLogsJob.js +340 -0
- package/dist/entries/logging/requestLoggingMiddleware.js +2 -1
- package/dist/entries/mail/markdownMail.js +62 -12
- package/dist/entries/mail/markdownMailable.js +62 -12
- package/dist/entries/mail/sanitizeMailHtml.js +60 -0
- package/dist/entries/openapi/generator.js +1 -1
- package/dist/entries/security/safeFetch.js +1 -1
- package/dist/entries/security/totp.js +31 -1
- package/dist/entries/view.js +88 -8
- package/dist/framework/public-api.d.ts +6 -1
- package/dist/index.js +744 -349
- package/package.json +27 -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;
|
|
@@ -5535,12 +5540,15 @@ function createMetricsMiddleware() {
|
|
|
5535
5540
|
// ../../src/core/http/requireAbilityMiddleware.ts
|
|
5536
5541
|
function createRequireAbilityMiddleware(abilityChecker) {
|
|
5537
5542
|
return (ability) => {
|
|
5538
|
-
return async (
|
|
5543
|
+
return async (request, next) => {
|
|
5539
5544
|
const user = currentAuthUser();
|
|
5540
5545
|
try {
|
|
5541
5546
|
abilityChecker.requireAbility(user, ability);
|
|
5542
5547
|
} catch (error) {
|
|
5543
5548
|
if (error instanceof ForbiddenError) {
|
|
5549
|
+
if (isViewsEnabled() && !requestPrefersJson(request)) {
|
|
5550
|
+
throw error;
|
|
5551
|
+
}
|
|
5544
5552
|
return Response.json({ error: error.message }, { status: error.status });
|
|
5545
5553
|
}
|
|
5546
5554
|
throw error;
|
|
@@ -5584,13 +5592,12 @@ function createRequireWebAuthMiddleware(auth2) {
|
|
|
5584
5592
|
return async (request, next) => {
|
|
5585
5593
|
const user = await auth2.resolve(request);
|
|
5586
5594
|
if (user) {
|
|
5587
|
-
return await next();
|
|
5595
|
+
return await runWithAuthUser(user, () => next());
|
|
5588
5596
|
}
|
|
5589
5597
|
if (requestPrefersJson(request)) {
|
|
5590
5598
|
throw new UnauthorizedError;
|
|
5591
5599
|
}
|
|
5592
|
-
|
|
5593
|
-
return Response.redirect(`/login?redirect=${redirectTarget}`, 302);
|
|
5600
|
+
return Response.redirect(loginRedirectLocation(request), 302);
|
|
5594
5601
|
};
|
|
5595
5602
|
}
|
|
5596
5603
|
// ../../src/core/http/scimThrottleMiddleware.ts
|
|
@@ -5636,76 +5643,127 @@ function createScimThrottleMiddleware(options) {
|
|
|
5636
5643
|
}
|
|
5637
5644
|
// ../../src/config/app.ts
|
|
5638
5645
|
var appConfig = {
|
|
5639
|
-
name: "WorkHub",
|
|
5646
|
+
name: process.env.APP_NAME?.trim() || "WorkHub",
|
|
5640
5647
|
env: process.env.APP_ENV ?? "local",
|
|
5641
5648
|
debug: (process.env.APP_DEBUG ?? "true") !== "false",
|
|
5642
5649
|
url: process.env.APP_URL ?? "http://localhost:3000",
|
|
5643
5650
|
apiPrefix: process.env.API_PREFIX ?? "/api/v1"
|
|
5644
5651
|
};
|
|
5645
5652
|
|
|
5646
|
-
// ../../src/
|
|
5647
|
-
function
|
|
5648
|
-
return
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
}
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5653
|
+
// ../../src/core/http/securityHeadersMiddleware.ts
|
|
5654
|
+
function createSecurityHeadersMiddleware(options = {}) {
|
|
5655
|
+
return async (request, next) => {
|
|
5656
|
+
const nonce = generateCspNonce();
|
|
5657
|
+
const existing = currentRequestMeta();
|
|
5658
|
+
return await runWithRequestMeta({
|
|
5659
|
+
...existing,
|
|
5660
|
+
request: existing.request ?? request,
|
|
5661
|
+
cspNonce: nonce
|
|
5662
|
+
}, async () => {
|
|
5663
|
+
const response = await next();
|
|
5664
|
+
const headers = new Headers(response.headers);
|
|
5665
|
+
headers.set("X-Content-Type-Options", "nosniff");
|
|
5666
|
+
headers.set("X-Frame-Options", "DENY");
|
|
5667
|
+
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
5668
|
+
headers.set("X-XSS-Protection", "0");
|
|
5669
|
+
headers.set("Content-Security-Policy", resolveContentSecurityPolicy(response, { ...options, nonce }));
|
|
5670
|
+
if (appConfig.env === "production") {
|
|
5671
|
+
headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
|
5672
|
+
}
|
|
5673
|
+
return new Response(response.body, {
|
|
5674
|
+
status: response.status,
|
|
5675
|
+
statusText: response.statusText,
|
|
5676
|
+
headers
|
|
5677
|
+
});
|
|
5678
|
+
});
|
|
5679
|
+
};
|
|
5680
|
+
}
|
|
5681
|
+
// ../../src/core/http/signedUrl.ts
|
|
5682
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
5683
|
+
function resolveSignedUrlSecret() {
|
|
5684
|
+
return process.env.SIGNED_URL_SECRET?.trim() || process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-signed-url-secret";
|
|
5685
|
+
}
|
|
5686
|
+
function resolveSignedUrlOrigin() {
|
|
5687
|
+
return (process.env.APP_URL ?? "http://localhost:3000").replace(/\/$/, "");
|
|
5688
|
+
}
|
|
5689
|
+
function normalizeSignedPath(path) {
|
|
5690
|
+
const trimmed = path.trim();
|
|
5691
|
+
if (!trimmed.startsWith("/") || trimmed.startsWith("//") || trimmed.includes("\\")) {
|
|
5692
|
+
throw new Error("Signed URLs must use a same-origin absolute path.");
|
|
5679
5693
|
}
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
return serverHtmxContentSecurityPolicy();
|
|
5694
|
+
if (trimmed.includes("://")) {
|
|
5695
|
+
throw new Error("Signed URLs must use a same-origin absolute path.");
|
|
5683
5696
|
}
|
|
5684
|
-
|
|
5685
|
-
|
|
5697
|
+
return trimmed;
|
|
5698
|
+
}
|
|
5699
|
+
function sortedQueryString(params) {
|
|
5700
|
+
const entries = [...params.entries()].filter(([key]) => key !== "signature").sort(([left], [right]) => left.localeCompare(right));
|
|
5701
|
+
return new URLSearchParams(entries).toString();
|
|
5702
|
+
}
|
|
5703
|
+
function signCanonicalPayload(path, query) {
|
|
5704
|
+
return createHmac2("sha256", resolveSignedUrlSecret()).update(`${path}
|
|
5705
|
+
${query}`).digest("hex");
|
|
5706
|
+
}
|
|
5707
|
+
function buildSignedSearchParams(path, query = {}, expiresAt) {
|
|
5708
|
+
const params = new URLSearchParams;
|
|
5709
|
+
for (const [key, value] of Object.entries(query)) {
|
|
5710
|
+
if (key === "signature" || key === "expires") {
|
|
5711
|
+
continue;
|
|
5712
|
+
}
|
|
5713
|
+
params.set(key, String(value));
|
|
5714
|
+
}
|
|
5715
|
+
if (expiresAt !== undefined) {
|
|
5716
|
+
params.set("expires", String(expiresAt));
|
|
5686
5717
|
}
|
|
5687
|
-
|
|
5718
|
+
params.set("signature", signCanonicalPayload(path, sortedQueryString(params)));
|
|
5719
|
+
return params;
|
|
5688
5720
|
}
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
return
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5721
|
+
function signedUrl(path, query = {}) {
|
|
5722
|
+
const normalizedPath = normalizeSignedPath(path);
|
|
5723
|
+
const params = buildSignedSearchParams(normalizedPath, query);
|
|
5724
|
+
return `${normalizedPath}?${params.toString()}`;
|
|
5725
|
+
}
|
|
5726
|
+
function temporarySignedUrl(path, expiresInSeconds, query = {}) {
|
|
5727
|
+
if (!Number.isInteger(expiresInSeconds) || expiresInSeconds <= 0) {
|
|
5728
|
+
throw new Error("Signed URL expiry must be a positive integer number of seconds.");
|
|
5729
|
+
}
|
|
5730
|
+
const normalizedPath = normalizeSignedPath(path);
|
|
5731
|
+
const expiresAt = Math.floor(Date.now() / 1000) + expiresInSeconds;
|
|
5732
|
+
const params = buildSignedSearchParams(normalizedPath, query, expiresAt);
|
|
5733
|
+
return `${normalizedPath}?${params.toString()}`;
|
|
5734
|
+
}
|
|
5735
|
+
function absoluteTemporarySignedUrl(path, expiresInSeconds, query = {}, origin = resolveSignedUrlOrigin()) {
|
|
5736
|
+
return `${origin.replace(/\/$/, "")}${temporarySignedUrl(path, expiresInSeconds, query)}`;
|
|
5737
|
+
}
|
|
5738
|
+
function readSignedRequestUrl(input) {
|
|
5739
|
+
if (input instanceof URL) {
|
|
5740
|
+
return input;
|
|
5741
|
+
}
|
|
5742
|
+
if (typeof input === "string") {
|
|
5743
|
+
return new URL(input, resolveSignedUrlOrigin());
|
|
5744
|
+
}
|
|
5745
|
+
return new URL(input.url);
|
|
5746
|
+
}
|
|
5747
|
+
function hasValidSignature(input) {
|
|
5748
|
+
const url = readSignedRequestUrl(input);
|
|
5749
|
+
const signature = url.searchParams.get("signature");
|
|
5750
|
+
if (!signature) {
|
|
5751
|
+
return false;
|
|
5752
|
+
}
|
|
5753
|
+
const expires = url.searchParams.get("expires");
|
|
5754
|
+
if (expires) {
|
|
5755
|
+
const expiresAt = Number.parseInt(expires, 10);
|
|
5756
|
+
if (!Number.isInteger(expiresAt) || expiresAt <= Math.floor(Date.now() / 1000)) {
|
|
5757
|
+
return false;
|
|
5702
5758
|
}
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5759
|
+
}
|
|
5760
|
+
const expected = signCanonicalPayload(url.pathname, sortedQueryString(url.searchParams));
|
|
5761
|
+
return timingSafeCompareString(signature, expected);
|
|
5762
|
+
}
|
|
5763
|
+
function assertValidSignature(input) {
|
|
5764
|
+
if (!hasValidSignature(input)) {
|
|
5765
|
+
throw new ForbiddenError("Invalid or expired signed URL.");
|
|
5766
|
+
}
|
|
5709
5767
|
}
|
|
5710
5768
|
// ../../src/core/http/throttleMiddleware.ts
|
|
5711
5769
|
var {RedisClient: RedisClient4 } = globalThis.Bun;
|
|
@@ -5825,6 +5883,7 @@ function installGracefulShutdownSignals(signals = ["SIGINT", "SIGTERM"]) {
|
|
|
5825
5883
|
function createRequestLoggingMiddleware() {
|
|
5826
5884
|
return async (request, next) => {
|
|
5827
5885
|
return await runWithRequestMeta({
|
|
5886
|
+
...currentRequestMeta(),
|
|
5828
5887
|
ipAddress: readClientIp(request) ?? null,
|
|
5829
5888
|
userAgent: request.headers.get("user-agent"),
|
|
5830
5889
|
request
|
|
@@ -5844,8 +5903,65 @@ function createRequestLoggingMiddleware() {
|
|
|
5844
5903
|
});
|
|
5845
5904
|
};
|
|
5846
5905
|
}
|
|
5906
|
+
// ../../src/core/mail/sanitizeMailHtml.ts
|
|
5907
|
+
var ALLOWED_TAGS = new Set([
|
|
5908
|
+
"a",
|
|
5909
|
+
"blockquote",
|
|
5910
|
+
"br",
|
|
5911
|
+
"code",
|
|
5912
|
+
"em",
|
|
5913
|
+
"h1",
|
|
5914
|
+
"h2",
|
|
5915
|
+
"h3",
|
|
5916
|
+
"h4",
|
|
5917
|
+
"h5",
|
|
5918
|
+
"h6",
|
|
5919
|
+
"hr",
|
|
5920
|
+
"li",
|
|
5921
|
+
"ol",
|
|
5922
|
+
"p",
|
|
5923
|
+
"pre",
|
|
5924
|
+
"strong",
|
|
5925
|
+
"ul"
|
|
5926
|
+
]);
|
|
5927
|
+
function escapeHtml2(value) {
|
|
5928
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5929
|
+
}
|
|
5930
|
+
function decodeBasicEntities(value) {
|
|
5931
|
+
return value.replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
5932
|
+
}
|
|
5933
|
+
function isSafeHref(value) {
|
|
5934
|
+
return /^(https?:|mailto:)/i.test(value.trim());
|
|
5935
|
+
}
|
|
5936
|
+
function sanitizeAttributes(tagName, rawAttributes) {
|
|
5937
|
+
if (tagName !== "a") {
|
|
5938
|
+
return "";
|
|
5939
|
+
}
|
|
5940
|
+
const hrefMatch = rawAttributes.match(/\bhref\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/i);
|
|
5941
|
+
const href = decodeBasicEntities(hrefMatch?.[2] ?? hrefMatch?.[3] ?? hrefMatch?.[4] ?? "");
|
|
5942
|
+
if (!href || !isSafeHref(href)) {
|
|
5943
|
+
return "";
|
|
5944
|
+
}
|
|
5945
|
+
return ` href="${escapeHtml2(href)}"`;
|
|
5946
|
+
}
|
|
5947
|
+
function sanitizeMailHtml(html) {
|
|
5948
|
+
return html.replace(/<\/?([a-zA-Z][a-zA-Z0-9]*)\b([^>]*)>/g, (match, tagName, rawAttributes) => {
|
|
5949
|
+
const name = tagName.toLowerCase();
|
|
5950
|
+
if (!ALLOWED_TAGS.has(name)) {
|
|
5951
|
+
return "";
|
|
5952
|
+
}
|
|
5953
|
+
if (match.startsWith("</")) {
|
|
5954
|
+
return `</${name}>`;
|
|
5955
|
+
}
|
|
5956
|
+
if (match.endsWith("/>") || name === "br" || name === "hr") {
|
|
5957
|
+
return `<${name}>`;
|
|
5958
|
+
}
|
|
5959
|
+
return `<${name}${sanitizeAttributes(name, rawAttributes)}>`;
|
|
5960
|
+
});
|
|
5961
|
+
}
|
|
5962
|
+
|
|
5847
5963
|
// ../../src/core/mail/markdownMail.ts
|
|
5848
|
-
function
|
|
5964
|
+
function escapeHtml3(value) {
|
|
5849
5965
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5850
5966
|
}
|
|
5851
5967
|
function stripMarkdown(markdown) {
|
|
@@ -5854,19 +5970,12 @@ function stripMarkdown(markdown) {
|
|
|
5854
5970
|
`).trim();
|
|
5855
5971
|
}
|
|
5856
5972
|
function markdownToHtml(markdown) {
|
|
5857
|
-
|
|
5858
|
-
return escaped.replace(/^### (.+)$/gm, "<h3>$1</h3>").replace(/^## (.+)$/gm, "<h2>$1</h2>").replace(/^# (.+)$/gm, "<h1>$1</h1>").replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>").replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>').replace(/^[-*]\s+(.+)$/gm, "<li>$1</li>").replace(/(<li>[\s\S]*?<\/li>\n?)+/g, (block) => `<ul>${block}</ul>`).replace(/```(\w+)?\n([\s\S]*?)```/g, "<pre><code>$2</code></pre>").split(/\n\n+/).map((block) => {
|
|
5859
|
-
if (block.startsWith("<")) {
|
|
5860
|
-
return block;
|
|
5861
|
-
}
|
|
5862
|
-
return `<p>${block.replace(/\n/g, " ")}</p>`;
|
|
5863
|
-
}).join(`
|
|
5864
|
-
`);
|
|
5973
|
+
return sanitizeMailHtml(Bun.markdown.html(markdown));
|
|
5865
5974
|
}
|
|
5866
5975
|
function wrapMarkdownMailLayout(bodyHtml, options = {}) {
|
|
5867
|
-
const title =
|
|
5868
|
-
const preview =
|
|
5869
|
-
const footer =
|
|
5976
|
+
const title = escapeHtml3(options.title ?? "GetStrata");
|
|
5977
|
+
const preview = escapeHtml3(options.preview ?? "");
|
|
5978
|
+
const footer = escapeHtml3(options.footer ?? "Sent by GetStrata");
|
|
5870
5979
|
return `<!DOCTYPE html>
|
|
5871
5980
|
<html lang="en">
|
|
5872
5981
|
<head>
|
|
@@ -6689,6 +6798,272 @@ function validateObject(payload, schema) {
|
|
|
6689
6798
|
}
|
|
6690
6799
|
return output;
|
|
6691
6800
|
}
|
|
6801
|
+
// ../../src/core/view/etaViewEngine.ts
|
|
6802
|
+
import { join as join4, relative } from "path";
|
|
6803
|
+
import { Eta } from "eta";
|
|
6804
|
+
|
|
6805
|
+
// ../../src/core/view/assertEtaHtmlSource.ts
|
|
6806
|
+
var HTML_TAGS = new Set([
|
|
6807
|
+
"a",
|
|
6808
|
+
"abbr",
|
|
6809
|
+
"address",
|
|
6810
|
+
"article",
|
|
6811
|
+
"aside",
|
|
6812
|
+
"audio",
|
|
6813
|
+
"b",
|
|
6814
|
+
"blockquote",
|
|
6815
|
+
"body",
|
|
6816
|
+
"button",
|
|
6817
|
+
"canvas",
|
|
6818
|
+
"caption",
|
|
6819
|
+
"cite",
|
|
6820
|
+
"code",
|
|
6821
|
+
"col",
|
|
6822
|
+
"colgroup",
|
|
6823
|
+
"data",
|
|
6824
|
+
"datalist",
|
|
6825
|
+
"dd",
|
|
6826
|
+
"del",
|
|
6827
|
+
"details",
|
|
6828
|
+
"dfn",
|
|
6829
|
+
"dialog",
|
|
6830
|
+
"div",
|
|
6831
|
+
"dl",
|
|
6832
|
+
"dt",
|
|
6833
|
+
"em",
|
|
6834
|
+
"fieldset",
|
|
6835
|
+
"figcaption",
|
|
6836
|
+
"figure",
|
|
6837
|
+
"footer",
|
|
6838
|
+
"form",
|
|
6839
|
+
"h1",
|
|
6840
|
+
"h2",
|
|
6841
|
+
"h3",
|
|
6842
|
+
"h4",
|
|
6843
|
+
"h5",
|
|
6844
|
+
"h6",
|
|
6845
|
+
"header",
|
|
6846
|
+
"hgroup",
|
|
6847
|
+
"hr",
|
|
6848
|
+
"html",
|
|
6849
|
+
"i",
|
|
6850
|
+
"iframe",
|
|
6851
|
+
"img",
|
|
6852
|
+
"input",
|
|
6853
|
+
"ins",
|
|
6854
|
+
"kbd",
|
|
6855
|
+
"label",
|
|
6856
|
+
"legend",
|
|
6857
|
+
"li",
|
|
6858
|
+
"main",
|
|
6859
|
+
"map",
|
|
6860
|
+
"mark",
|
|
6861
|
+
"menu",
|
|
6862
|
+
"meter",
|
|
6863
|
+
"nav",
|
|
6864
|
+
"object",
|
|
6865
|
+
"ol",
|
|
6866
|
+
"optgroup",
|
|
6867
|
+
"option",
|
|
6868
|
+
"output",
|
|
6869
|
+
"p",
|
|
6870
|
+
"picture",
|
|
6871
|
+
"pre",
|
|
6872
|
+
"progress",
|
|
6873
|
+
"q",
|
|
6874
|
+
"rp",
|
|
6875
|
+
"rt",
|
|
6876
|
+
"ruby",
|
|
6877
|
+
"s",
|
|
6878
|
+
"samp",
|
|
6879
|
+
"script",
|
|
6880
|
+
"search",
|
|
6881
|
+
"section",
|
|
6882
|
+
"select",
|
|
6883
|
+
"slot",
|
|
6884
|
+
"small",
|
|
6885
|
+
"source",
|
|
6886
|
+
"span",
|
|
6887
|
+
"strong",
|
|
6888
|
+
"style",
|
|
6889
|
+
"sub",
|
|
6890
|
+
"summary",
|
|
6891
|
+
"sup",
|
|
6892
|
+
"table",
|
|
6893
|
+
"tbody",
|
|
6894
|
+
"td",
|
|
6895
|
+
"template",
|
|
6896
|
+
"textarea",
|
|
6897
|
+
"tfoot",
|
|
6898
|
+
"th",
|
|
6899
|
+
"thead",
|
|
6900
|
+
"time",
|
|
6901
|
+
"title",
|
|
6902
|
+
"tr",
|
|
6903
|
+
"track",
|
|
6904
|
+
"u",
|
|
6905
|
+
"ul",
|
|
6906
|
+
"var",
|
|
6907
|
+
"video",
|
|
6908
|
+
"wbr"
|
|
6909
|
+
]);
|
|
6910
|
+
var TAG_PATTERN = "([a-z][a-z0-9]*)";
|
|
6911
|
+
var CLASS_OR_ID_PATTERN = "[.#][\\w-]+";
|
|
6912
|
+
var CLASS_ID_SHORTHAND = new RegExp(`^${TAG_PATTERN}(?:${CLASS_OR_ID_PATTERN})+`);
|
|
6913
|
+
var ATTRIBUTE_SHORTHAND = new RegExp(`^${TAG_PATTERN}(?:${CLASS_OR_ID_PATTERN})*\\s+[\\w:-]+=`);
|
|
6914
|
+
var TAG_SHORTHAND = new RegExp(`^${TAG_PATTERN}(?:>|\\s+\\S|$)`);
|
|
6915
|
+
function htmlTagName(match) {
|
|
6916
|
+
const tag = match?.[1];
|
|
6917
|
+
return tag && HTML_TAGS.has(tag) ? tag : undefined;
|
|
6918
|
+
}
|
|
6919
|
+
function describePugLikeLine(line) {
|
|
6920
|
+
if (/^\|(?:\s|$)/.test(line)) {
|
|
6921
|
+
return "piped text";
|
|
6922
|
+
}
|
|
6923
|
+
if (htmlTagName(line.match(CLASS_ID_SHORTHAND))) {
|
|
6924
|
+
return "class/id shorthand";
|
|
6925
|
+
}
|
|
6926
|
+
if (htmlTagName(line.match(ATTRIBUTE_SHORTHAND))) {
|
|
6927
|
+
return "attribute shorthand";
|
|
6928
|
+
}
|
|
6929
|
+
if (htmlTagName(line.match(TAG_SHORTHAND))) {
|
|
6930
|
+
return "tag shorthand";
|
|
6931
|
+
}
|
|
6932
|
+
return;
|
|
6933
|
+
}
|
|
6934
|
+
function updateSkipBlock(lower, skipBlock) {
|
|
6935
|
+
if (skipBlock) {
|
|
6936
|
+
if (skipBlock === "comment" && lower.includes("-->")) {
|
|
6937
|
+
return null;
|
|
6938
|
+
}
|
|
6939
|
+
if (skipBlock !== "comment" && lower.includes(`</${skipBlock}`)) {
|
|
6940
|
+
return null;
|
|
6941
|
+
}
|
|
6942
|
+
return skipBlock;
|
|
6943
|
+
}
|
|
6944
|
+
if (lower.startsWith("<!--") && !lower.includes("-->")) {
|
|
6945
|
+
return "comment";
|
|
6946
|
+
}
|
|
6947
|
+
if (lower.startsWith("<script") && !lower.includes("</script")) {
|
|
6948
|
+
return "script";
|
|
6949
|
+
}
|
|
6950
|
+
if (lower.startsWith("<style") && !lower.includes("</style")) {
|
|
6951
|
+
return "style";
|
|
6952
|
+
}
|
|
6953
|
+
return null;
|
|
6954
|
+
}
|
|
6955
|
+
function assertEtaHtmlSource(templateName, source) {
|
|
6956
|
+
let skipBlock = null;
|
|
6957
|
+
for (const rawLine of source.split(/\r?\n/)) {
|
|
6958
|
+
const trimmed = rawLine.trim();
|
|
6959
|
+
if (!trimmed) {
|
|
6960
|
+
continue;
|
|
6961
|
+
}
|
|
6962
|
+
const lower = trimmed.toLowerCase();
|
|
6963
|
+
const nextSkipBlock = updateSkipBlock(lower, skipBlock);
|
|
6964
|
+
if (skipBlock || nextSkipBlock) {
|
|
6965
|
+
skipBlock = nextSkipBlock;
|
|
6966
|
+
continue;
|
|
6967
|
+
}
|
|
6968
|
+
if (trimmed.startsWith("<") || trimmed.startsWith("<%")) {
|
|
6969
|
+
continue;
|
|
6970
|
+
}
|
|
6971
|
+
const kind = describePugLikeLine(trimmed);
|
|
6972
|
+
if (kind) {
|
|
6973
|
+
throw new Error(`Eta view "${templateName}" contains Pug-like markup (${kind}: ${trimmed}). Eta views must be HTML + <% %> tags, not Pug.`);
|
|
6974
|
+
}
|
|
6975
|
+
}
|
|
6976
|
+
}
|
|
6977
|
+
|
|
6978
|
+
// ../../src/core/view/etaViewEngine.ts
|
|
6979
|
+
var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
|
|
6980
|
+
var DEFAULT_LAYOUT = "layouts/app.eta";
|
|
6981
|
+
|
|
6982
|
+
class EtaViewEngine {
|
|
6983
|
+
eta;
|
|
6984
|
+
resolveLayoutData;
|
|
6985
|
+
constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
|
|
6986
|
+
this.eta = new Eta({
|
|
6987
|
+
views: viewsDirectory,
|
|
6988
|
+
autoTrim: false
|
|
6989
|
+
});
|
|
6990
|
+
this.resolveLayoutData = resolveLayoutData;
|
|
6991
|
+
const readFile2 = this.eta.readFile?.bind(this.eta);
|
|
6992
|
+
this.eta.readFile = (path) => {
|
|
6993
|
+
const source = readFile2 ? readFile2(path) : "";
|
|
6994
|
+
assertEtaHtmlSource(relative(viewsDirectory, path) || path, source);
|
|
6995
|
+
return source;
|
|
6996
|
+
};
|
|
6997
|
+
}
|
|
6998
|
+
async render(name, data = {}, options = {}) {
|
|
6999
|
+
const template = name.endsWith(".eta") ? name : `${name}.eta`;
|
|
7000
|
+
const request = options.request ?? currentRequestMeta().request;
|
|
7001
|
+
const layoutData = this.resolveLayoutData ? await this.resolveLayoutData(request) : {};
|
|
7002
|
+
const mergedData = { ...layoutData, ...data };
|
|
7003
|
+
const body = await this.eta.renderAsync(template, mergedData);
|
|
7004
|
+
const layout = options.layout ?? DEFAULT_LAYOUT;
|
|
7005
|
+
if (layout === false) {
|
|
7006
|
+
return body;
|
|
7007
|
+
}
|
|
7008
|
+
const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
|
|
7009
|
+
return await this.eta.renderAsync(layoutTemplate, {
|
|
7010
|
+
...mergedData,
|
|
7011
|
+
body
|
|
7012
|
+
});
|
|
7013
|
+
}
|
|
7014
|
+
}
|
|
7015
|
+
// ../../src/core/view/webLayoutData.ts
|
|
7016
|
+
var configuredLayoutOptions = {};
|
|
7017
|
+
function configureWebLayoutData(options) {
|
|
7018
|
+
configuredLayoutOptions = { ...options };
|
|
7019
|
+
}
|
|
7020
|
+
function layoutUserKey(options) {
|
|
7021
|
+
return options.userKey ?? "authUser";
|
|
7022
|
+
}
|
|
7023
|
+
async function defaultLoadLayoutUser(container, _request) {
|
|
7024
|
+
const authUser = currentAuthUser();
|
|
7025
|
+
if (!authUser) {
|
|
7026
|
+
return null;
|
|
7027
|
+
}
|
|
7028
|
+
const userId = Number(authUser.id);
|
|
7029
|
+
if (!Number.isInteger(userId) || userId <= 0) {
|
|
7030
|
+
return null;
|
|
7031
|
+
}
|
|
7032
|
+
const directory = resolveAuthUserDirectory(container);
|
|
7033
|
+
if (!directory) {
|
|
7034
|
+
return {
|
|
7035
|
+
id: userId,
|
|
7036
|
+
email: "",
|
|
7037
|
+
role: authUser.role ?? "member"
|
|
7038
|
+
};
|
|
7039
|
+
}
|
|
7040
|
+
try {
|
|
7041
|
+
const user = await directory.findByIdOrThrow(userId);
|
|
7042
|
+
return {
|
|
7043
|
+
id: userId,
|
|
7044
|
+
email: user.email ?? "",
|
|
7045
|
+
role: authUser.role ?? user.role ?? "member"
|
|
7046
|
+
};
|
|
7047
|
+
} catch {
|
|
7048
|
+
return null;
|
|
7049
|
+
}
|
|
7050
|
+
}
|
|
7051
|
+
async function resolveWebLayoutData(container, request, options = {}) {
|
|
7052
|
+
const resolved = { ...configuredLayoutOptions, ...options };
|
|
7053
|
+
const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
|
|
7054
|
+
const flash = request ? currentRequestMeta().flash ?? pullFlash(request) : null;
|
|
7055
|
+
const userKey = layoutUserKey(resolved);
|
|
7056
|
+
const loadUser = resolved.loadUser ?? defaultLoadLayoutUser;
|
|
7057
|
+
const user = await loadUser(container, request);
|
|
7058
|
+
const extra = typeof resolved.extra === "function" ? await resolved.extra(user) : resolved.extra ?? {};
|
|
7059
|
+
return {
|
|
7060
|
+
[userKey]: user,
|
|
7061
|
+
csrfToken,
|
|
7062
|
+
flash,
|
|
7063
|
+
cspNonce: currentRequestMeta().cspNonce ?? "",
|
|
7064
|
+
...extra
|
|
7065
|
+
};
|
|
7066
|
+
}
|
|
6692
7067
|
export {
|
|
6693
7068
|
AdminResourceRegistry,
|
|
6694
7069
|
ApiTokenGuard,
|
|
@@ -6747,6 +7122,7 @@ export {
|
|
|
6747
7122
|
ValidationError,
|
|
6748
7123
|
WebFormRequest,
|
|
6749
7124
|
WhereBuilder,
|
|
7125
|
+
absoluteTemporarySignedUrl,
|
|
6750
7126
|
appSchedule,
|
|
6751
7127
|
appendOrganizationScope,
|
|
6752
7128
|
appendProjectScope,
|
|
@@ -6756,6 +7132,7 @@ export {
|
|
|
6756
7132
|
assertIfMatch,
|
|
6757
7133
|
assertOrganizationReadable,
|
|
6758
7134
|
assertResourceInCurrentTenant,
|
|
7135
|
+
assertValidSignature,
|
|
6759
7136
|
auditChecksum,
|
|
6760
7137
|
auth,
|
|
6761
7138
|
authContext,
|
|
@@ -6773,7 +7150,9 @@ export {
|
|
|
6773
7150
|
composeMiddleware,
|
|
6774
7151
|
conditionalJsonResponse,
|
|
6775
7152
|
config,
|
|
7153
|
+
configureContentSecurityPolicy,
|
|
6776
7154
|
configureMembershipLookup,
|
|
7155
|
+
configureWebErrorView,
|
|
6777
7156
|
configureWebLayoutData,
|
|
6778
7157
|
createAuthMiddleware,
|
|
6779
7158
|
createAuthorizeMiddleware,
|
|
@@ -6822,12 +7201,14 @@ export {
|
|
|
6822
7201
|
dehydrateValue,
|
|
6823
7202
|
emailRule,
|
|
6824
7203
|
emptyPaginateResult,
|
|
7204
|
+
errorTemplateName,
|
|
6825
7205
|
etagFromResource,
|
|
6826
7206
|
eventBus,
|
|
6827
7207
|
events,
|
|
6828
7208
|
filterMassAssignable,
|
|
6829
7209
|
formatAdminValue,
|
|
6830
7210
|
freshDatabase,
|
|
7211
|
+
generateCspNonce,
|
|
6831
7212
|
getActiveDatabaseConnection,
|
|
6832
7213
|
getBoundDatabaseConnection,
|
|
6833
7214
|
getDefaultDatabasePool,
|
|
@@ -6839,6 +7220,8 @@ export {
|
|
|
6839
7220
|
hasMinimumOrgRole2 as hasMinimumOrgRole,
|
|
6840
7221
|
hasOne,
|
|
6841
7222
|
hasOrgMembership,
|
|
7223
|
+
hasValidSignature,
|
|
7224
|
+
htmlErrorResponse,
|
|
6842
7225
|
htmlResponse,
|
|
6843
7226
|
hydrateValue,
|
|
6844
7227
|
indexBelongsToManyRelation,
|
|
@@ -6862,6 +7245,7 @@ export {
|
|
|
6862
7245
|
loadSeedersFromDirectory,
|
|
6863
7246
|
log,
|
|
6864
7247
|
logSecurityEvent,
|
|
7248
|
+
loginRedirectLocation,
|
|
6865
7249
|
mail,
|
|
6866
7250
|
mailer,
|
|
6867
7251
|
markdownToHtml,
|
|
@@ -6893,6 +7277,7 @@ export {
|
|
|
6893
7277
|
registerDefaultDatabasePool,
|
|
6894
7278
|
registerModelRepository,
|
|
6895
7279
|
registerShutdownHandler,
|
|
7280
|
+
renderKernelErrorChrome,
|
|
6896
7281
|
renderMarkdownMail,
|
|
6897
7282
|
repositoryConnection,
|
|
6898
7283
|
requestIdMiddleware,
|
|
@@ -6908,9 +7293,11 @@ export {
|
|
|
6908
7293
|
resolveApplicationLogger,
|
|
6909
7294
|
resolveApplicationPolicyGate,
|
|
6910
7295
|
resolveApplicationQueue,
|
|
7296
|
+
resolveContentSecurityPolicy,
|
|
6911
7297
|
resolveCsrfToken,
|
|
6912
7298
|
resolveCsrfTokenForRequest,
|
|
6913
7299
|
resolveDatabaseDriver,
|
|
7300
|
+
resolveHtmlContentSecurityPolicy,
|
|
6914
7301
|
resolveMembershipService,
|
|
6915
7302
|
resolveOrganizationScope,
|
|
6916
7303
|
resolveRepositoryConnection,
|
|
@@ -6931,15 +7318,23 @@ export {
|
|
|
6931
7318
|
runWithTenant,
|
|
6932
7319
|
runWithTenantDatabase,
|
|
6933
7320
|
runWithTraceContext,
|
|
7321
|
+
safeInternalRedirectPath,
|
|
7322
|
+
sanitizeInternalPath,
|
|
7323
|
+
sanitizeMailHtml,
|
|
6934
7324
|
scopedOrganizationIds,
|
|
6935
7325
|
securedBindRouteModel,
|
|
6936
7326
|
securedBindRouteModelByKey,
|
|
6937
7327
|
sendMarkdownMail,
|
|
6938
7328
|
serializeDate,
|
|
7329
|
+
serverHtmxContentSecurityPolicy,
|
|
6939
7330
|
setActiveApplicationContext,
|
|
7331
|
+
signedUrl,
|
|
7332
|
+
spaContentSecurityPolicy,
|
|
6940
7333
|
storageFacade as storage,
|
|
7334
|
+
strictApiContentSecurityPolicy,
|
|
6941
7335
|
stringRule,
|
|
6942
7336
|
stripMarkdown,
|
|
7337
|
+
temporarySignedUrl,
|
|
6943
7338
|
textResponse,
|
|
6944
7339
|
toPaginatedResourceCollection,
|
|
6945
7340
|
toResourceCollection,
|