@getstrata/core 0.5.59 → 0.5.61

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