@getstrata/core 0.5.61 → 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.
@@ -1,27 +1,77 @@
1
1
  // @bun
2
- // ../../src/core/mail/markdownMail.ts
2
+ // ../../src/core/mail/sanitizeMailHtml.ts
3
+ var ALLOWED_TAGS = new Set([
4
+ "a",
5
+ "blockquote",
6
+ "br",
7
+ "code",
8
+ "em",
9
+ "h1",
10
+ "h2",
11
+ "h3",
12
+ "h4",
13
+ "h5",
14
+ "h6",
15
+ "hr",
16
+ "li",
17
+ "ol",
18
+ "p",
19
+ "pre",
20
+ "strong",
21
+ "ul"
22
+ ]);
3
23
  function escapeHtml(value) {
4
24
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
5
25
  }
26
+ function decodeBasicEntities(value) {
27
+ return value.replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
28
+ }
29
+ function isSafeHref(value) {
30
+ return /^(https?:|mailto:)/i.test(value.trim());
31
+ }
32
+ function sanitizeAttributes(tagName, rawAttributes) {
33
+ if (tagName !== "a") {
34
+ return "";
35
+ }
36
+ const hrefMatch = rawAttributes.match(/\bhref\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/i);
37
+ const href = decodeBasicEntities(hrefMatch?.[2] ?? hrefMatch?.[3] ?? hrefMatch?.[4] ?? "");
38
+ if (!href || !isSafeHref(href)) {
39
+ return "";
40
+ }
41
+ return ` href="${escapeHtml(href)}"`;
42
+ }
43
+ function sanitizeMailHtml(html) {
44
+ return html.replace(/<\/?([a-zA-Z][a-zA-Z0-9]*)\b([^>]*)>/g, (match, tagName, rawAttributes) => {
45
+ const name = tagName.toLowerCase();
46
+ if (!ALLOWED_TAGS.has(name)) {
47
+ return "";
48
+ }
49
+ if (match.startsWith("</")) {
50
+ return `</${name}>`;
51
+ }
52
+ if (match.endsWith("/>") || name === "br" || name === "hr") {
53
+ return `<${name}>`;
54
+ }
55
+ return `<${name}${sanitizeAttributes(name, rawAttributes)}>`;
56
+ });
57
+ }
58
+
59
+ // ../../src/core/mail/markdownMail.ts
60
+ function escapeHtml2(value) {
61
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
62
+ }
6
63
  function stripMarkdown(markdown) {
7
64
  return markdown.replace(/^#{1,6}\s+/gm, "").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/^[-*]\s+/gm, "\u2022 ").replace(/```[\s\S]*?```/g, "").replace(/\n{3,}/g, `
8
65
 
9
66
  `).trim();
10
67
  }
11
68
  function markdownToHtml(markdown) {
12
- const escaped = markdown.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
13
- 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) => {
14
- if (block.startsWith("<")) {
15
- return block;
16
- }
17
- return `<p>${block.replace(/\n/g, " ")}</p>`;
18
- }).join(`
19
- `);
69
+ return sanitizeMailHtml(Bun.markdown.html(markdown));
20
70
  }
21
71
  function wrapMarkdownMailLayout(bodyHtml, options = {}) {
22
- const title = escapeHtml(options.title ?? "GetStrata");
23
- const preview = escapeHtml(options.preview ?? "");
24
- const footer = escapeHtml(options.footer ?? "Sent by GetStrata");
72
+ const title = escapeHtml2(options.title ?? "GetStrata");
73
+ const preview = escapeHtml2(options.preview ?? "");
74
+ const footer = escapeHtml2(options.footer ?? "Sent by GetStrata");
25
75
  return `<!DOCTYPE html>
26
76
  <html lang="en">
27
77
  <head>
@@ -1,27 +1,77 @@
1
1
  // @bun
2
- // ../../src/core/mail/markdownMail.ts
2
+ // ../../src/core/mail/sanitizeMailHtml.ts
3
+ var ALLOWED_TAGS = new Set([
4
+ "a",
5
+ "blockquote",
6
+ "br",
7
+ "code",
8
+ "em",
9
+ "h1",
10
+ "h2",
11
+ "h3",
12
+ "h4",
13
+ "h5",
14
+ "h6",
15
+ "hr",
16
+ "li",
17
+ "ol",
18
+ "p",
19
+ "pre",
20
+ "strong",
21
+ "ul"
22
+ ]);
3
23
  function escapeHtml(value) {
4
24
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
5
25
  }
26
+ function decodeBasicEntities(value) {
27
+ return value.replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
28
+ }
29
+ function isSafeHref(value) {
30
+ return /^(https?:|mailto:)/i.test(value.trim());
31
+ }
32
+ function sanitizeAttributes(tagName, rawAttributes) {
33
+ if (tagName !== "a") {
34
+ return "";
35
+ }
36
+ const hrefMatch = rawAttributes.match(/\bhref\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/i);
37
+ const href = decodeBasicEntities(hrefMatch?.[2] ?? hrefMatch?.[3] ?? hrefMatch?.[4] ?? "");
38
+ if (!href || !isSafeHref(href)) {
39
+ return "";
40
+ }
41
+ return ` href="${escapeHtml(href)}"`;
42
+ }
43
+ function sanitizeMailHtml(html) {
44
+ return html.replace(/<\/?([a-zA-Z][a-zA-Z0-9]*)\b([^>]*)>/g, (match, tagName, rawAttributes) => {
45
+ const name = tagName.toLowerCase();
46
+ if (!ALLOWED_TAGS.has(name)) {
47
+ return "";
48
+ }
49
+ if (match.startsWith("</")) {
50
+ return `</${name}>`;
51
+ }
52
+ if (match.endsWith("/>") || name === "br" || name === "hr") {
53
+ return `<${name}>`;
54
+ }
55
+ return `<${name}${sanitizeAttributes(name, rawAttributes)}>`;
56
+ });
57
+ }
58
+
59
+ // ../../src/core/mail/markdownMail.ts
60
+ function escapeHtml2(value) {
61
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
62
+ }
6
63
  function stripMarkdown(markdown) {
7
64
  return markdown.replace(/^#{1,6}\s+/gm, "").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/^[-*]\s+/gm, "\u2022 ").replace(/```[\s\S]*?```/g, "").replace(/\n{3,}/g, `
8
65
 
9
66
  `).trim();
10
67
  }
11
68
  function markdownToHtml(markdown) {
12
- const escaped = markdown.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
13
- 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) => {
14
- if (block.startsWith("<")) {
15
- return block;
16
- }
17
- return `<p>${block.replace(/\n/g, " ")}</p>`;
18
- }).join(`
19
- `);
69
+ return sanitizeMailHtml(Bun.markdown.html(markdown));
20
70
  }
21
71
  function wrapMarkdownMailLayout(bodyHtml, options = {}) {
22
- const title = escapeHtml(options.title ?? "GetStrata");
23
- const preview = escapeHtml(options.preview ?? "");
24
- const footer = escapeHtml(options.footer ?? "Sent by GetStrata");
72
+ const title = escapeHtml2(options.title ?? "GetStrata");
73
+ const preview = escapeHtml2(options.preview ?? "");
74
+ const footer = escapeHtml2(options.footer ?? "Sent by GetStrata");
25
75
  return `<!DOCTYPE html>
26
76
  <html lang="en">
27
77
  <head>
@@ -0,0 +1,60 @@
1
+ // @bun
2
+ // ../../src/core/mail/sanitizeMailHtml.ts
3
+ var ALLOWED_TAGS = new Set([
4
+ "a",
5
+ "blockquote",
6
+ "br",
7
+ "code",
8
+ "em",
9
+ "h1",
10
+ "h2",
11
+ "h3",
12
+ "h4",
13
+ "h5",
14
+ "h6",
15
+ "hr",
16
+ "li",
17
+ "ol",
18
+ "p",
19
+ "pre",
20
+ "strong",
21
+ "ul"
22
+ ]);
23
+ function escapeHtml(value) {
24
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
25
+ }
26
+ function decodeBasicEntities(value) {
27
+ return value.replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
28
+ }
29
+ function isSafeHref(value) {
30
+ return /^(https?:|mailto:)/i.test(value.trim());
31
+ }
32
+ function sanitizeAttributes(tagName, rawAttributes) {
33
+ if (tagName !== "a") {
34
+ return "";
35
+ }
36
+ const hrefMatch = rawAttributes.match(/\bhref\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/i);
37
+ const href = decodeBasicEntities(hrefMatch?.[2] ?? hrefMatch?.[3] ?? hrefMatch?.[4] ?? "");
38
+ if (!href || !isSafeHref(href)) {
39
+ return "";
40
+ }
41
+ return ` href="${escapeHtml(href)}"`;
42
+ }
43
+ function sanitizeMailHtml(html) {
44
+ return html.replace(/<\/?([a-zA-Z][a-zA-Z0-9]*)\b([^>]*)>/g, (match, tagName, rawAttributes) => {
45
+ const name = tagName.toLowerCase();
46
+ if (!ALLOWED_TAGS.has(name)) {
47
+ return "";
48
+ }
49
+ if (match.startsWith("</")) {
50
+ return `</${name}>`;
51
+ }
52
+ if (match.endsWith("/>") || name === "br" || name === "hr") {
53
+ return `<${name}>`;
54
+ }
55
+ return `<${name}${sanitizeAttributes(name, rawAttributes)}>`;
56
+ });
57
+ }
58
+ export {
59
+ sanitizeMailHtml
60
+ };
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  // ../../src/config/app.ts
3
3
  var appConfig = {
4
- name: "WorkHub",
4
+ name: process.env.APP_NAME?.trim() || "WorkHub",
5
5
  env: process.env.APP_ENV ?? "local",
6
6
  debug: (process.env.APP_DEBUG ?? "true") !== "false",
7
7
  url: process.env.APP_URL ?? "http://localhost:3000",
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  // ../../src/config/app.ts
3
3
  var appConfig = {
4
- name: "WorkHub",
4
+ name: process.env.APP_NAME?.trim() || "WorkHub",
5
5
  env: process.env.APP_ENV ?? "local",
6
6
  debug: (process.env.APP_DEBUG ?? "true") !== "false",
7
7
  url: process.env.APP_URL ?? "http://localhost:3000",
@@ -1,6 +1,6 @@
1
1
  // @bun
2
2
  // ../../src/core/security/totp.ts
3
- import { createHmac } from "crypto";
3
+ import { createHmac, randomBytes } from "crypto";
4
4
  function decodeBase32(input) {
5
5
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
6
6
  const normalized = input.replace(/=+$/u, "").toUpperCase();
@@ -18,6 +18,34 @@ function decodeBase32(input) {
18
18
  }
19
19
  return Buffer.from(bytes);
20
20
  }
21
+ function encodeBase32(bytes) {
22
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
23
+ let bits = "";
24
+ for (const byte of bytes) {
25
+ bits += byte.toString(2).padStart(8, "0");
26
+ }
27
+ let output = "";
28
+ for (let index = 0;index < bits.length; index += 5) {
29
+ const chunk = bits.slice(index, index + 5).padEnd(5, "0");
30
+ output += alphabet[Number.parseInt(chunk, 2)] ?? "";
31
+ }
32
+ return output;
33
+ }
34
+ function generateTotpSecret(byteLength = 20) {
35
+ return encodeBase32(randomBytes(byteLength));
36
+ }
37
+ function buildOtpauthUrl(options) {
38
+ const issuer = options.issuer?.trim() || process.env.APP_NAME?.trim() || "WorkHub";
39
+ const label = `${issuer}:${options.account}`;
40
+ const params = new URLSearchParams({
41
+ secret: options.secret,
42
+ issuer,
43
+ algorithm: "SHA1",
44
+ digits: "6",
45
+ period: "30"
46
+ });
47
+ return `otpauth://totp/${encodeURIComponent(label)}?${params.toString()}`;
48
+ }
21
49
  function generateTotp(secret, counter, digits = 6) {
22
50
  const key = decodeBase32(secret);
23
51
  const buffer = Buffer.alloc(8);
@@ -46,6 +74,8 @@ function verifyTotp(secret, token, window = 1) {
46
74
  return false;
47
75
  }
48
76
  export {
77
+ buildOtpauthUrl,
49
78
  generateTotp,
79
+ generateTotpSecret,
50
80
  verifyTotp
51
81
  };
@@ -83,6 +83,7 @@ export type { RouteRequest } from "../core/http/route.ts";
83
83
  export { loginRedirectLocation, safeInternalRedirectPath, sanitizeInternalPath, } from "../core/http/safeInternalPath.ts";
84
84
  export { createScimThrottleMiddleware } from "../core/http/scimThrottleMiddleware.ts";
85
85
  export { createSecurityHeadersMiddleware } from "../core/http/securityHeadersMiddleware.ts";
86
+ export { absoluteTemporarySignedUrl, assertValidSignature, hasValidSignature, signedUrl, temporarySignedUrl, } from "../core/http/signedUrl.ts";
86
87
  export { createThrottleMiddleware } from "../core/http/throttleMiddleware.ts";
87
88
  export { WebFormRequest } from "../core/http/webFormRequest.ts";
88
89
  export { installGracefulShutdownSignals, registerShutdownHandler, runGracefulShutdown, } from "../core/lifecycle/gracefulShutdown.ts";
@@ -93,6 +94,7 @@ export type { MarkdownMailLayoutOptions, RenderedMarkdownMail } from "../core/ma
93
94
  export { markdownToHtml, renderMarkdownMail, stripMarkdown, wrapMarkdownMailLayout, } from "../core/mail/markdownMail.ts";
94
95
  export type { MarkdownMailableInput } from "../core/mail/markdownMailable.ts";
95
96
  export { buildMarkdownMailMessage, sendMarkdownMail } from "../core/mail/markdownMailable.ts";
97
+ export { sanitizeMailHtml } from "../core/mail/sanitizeMailHtml.ts";
96
98
  export type { MetricLabels } from "../core/metrics/prometheus.ts";
97
99
  export { PrometheusRegistry, prometheusRegistry } from "../core/metrics/prometheus.ts";
98
100
  export type { DatabaseNotificationPayload, DatabaseNotificationStore, MailNotificationMessage, Notifiable, NotificationChannelName, } from "../core/notifications/index.ts";
package/dist/index.js CHANGED
@@ -5540,12 +5540,15 @@ function createMetricsMiddleware() {
5540
5540
  // ../../src/core/http/requireAbilityMiddleware.ts
5541
5541
  function createRequireAbilityMiddleware(abilityChecker) {
5542
5542
  return (ability) => {
5543
- return async (_request, next) => {
5543
+ return async (request, next) => {
5544
5544
  const user = currentAuthUser();
5545
5545
  try {
5546
5546
  abilityChecker.requireAbility(user, ability);
5547
5547
  } catch (error) {
5548
5548
  if (error instanceof ForbiddenError) {
5549
+ if (isViewsEnabled() && !requestPrefersJson(request)) {
5550
+ throw error;
5551
+ }
5549
5552
  return Response.json({ error: error.message }, { status: error.status });
5550
5553
  }
5551
5554
  throw error;
@@ -5589,7 +5592,7 @@ function createRequireWebAuthMiddleware(auth2) {
5589
5592
  return async (request, next) => {
5590
5593
  const user = await auth2.resolve(request);
5591
5594
  if (user) {
5592
- return await next();
5595
+ return await runWithAuthUser(user, () => next());
5593
5596
  }
5594
5597
  if (requestPrefersJson(request)) {
5595
5598
  throw new UnauthorizedError;
@@ -5640,7 +5643,7 @@ function createScimThrottleMiddleware(options) {
5640
5643
  }
5641
5644
  // ../../src/config/app.ts
5642
5645
  var appConfig = {
5643
- name: "WorkHub",
5646
+ name: process.env.APP_NAME?.trim() || "WorkHub",
5644
5647
  env: process.env.APP_ENV ?? "local",
5645
5648
  debug: (process.env.APP_DEBUG ?? "true") !== "false",
5646
5649
  url: process.env.APP_URL ?? "http://localhost:3000",
@@ -5675,6 +5678,93 @@ function createSecurityHeadersMiddleware(options = {}) {
5675
5678
  });
5676
5679
  };
5677
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.");
5693
+ }
5694
+ if (trimmed.includes("://")) {
5695
+ throw new Error("Signed URLs must use a same-origin absolute path.");
5696
+ }
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));
5717
+ }
5718
+ params.set("signature", signCanonicalPayload(path, sortedQueryString(params)));
5719
+ return params;
5720
+ }
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;
5758
+ }
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
+ }
5767
+ }
5678
5768
  // ../../src/core/http/throttleMiddleware.ts
5679
5769
  var {RedisClient: RedisClient4 } = globalThis.Bun;
5680
5770
  function resolveThrottleIdentity(request) {
@@ -5813,29 +5903,79 @@ function createRequestLoggingMiddleware() {
5813
5903
  });
5814
5904
  };
5815
5905
  }
5816
- // ../../src/core/mail/markdownMail.ts
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
+ ]);
5817
5927
  function escapeHtml2(value) {
5818
5928
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
5819
5929
  }
5930
+ function decodeBasicEntities(value) {
5931
+ return value.replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/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
+
5963
+ // ../../src/core/mail/markdownMail.ts
5964
+ function escapeHtml3(value) {
5965
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
5966
+ }
5820
5967
  function stripMarkdown(markdown) {
5821
5968
  return markdown.replace(/^#{1,6}\s+/gm, "").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/^[-*]\s+/gm, "\u2022 ").replace(/```[\s\S]*?```/g, "").replace(/\n{3,}/g, `
5822
5969
 
5823
5970
  `).trim();
5824
5971
  }
5825
5972
  function markdownToHtml(markdown) {
5826
- const escaped = markdown.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
5827
- 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) => {
5828
- if (block.startsWith("<")) {
5829
- return block;
5830
- }
5831
- return `<p>${block.replace(/\n/g, " ")}</p>`;
5832
- }).join(`
5833
- `);
5973
+ return sanitizeMailHtml(Bun.markdown.html(markdown));
5834
5974
  }
5835
5975
  function wrapMarkdownMailLayout(bodyHtml, options = {}) {
5836
- const title = escapeHtml2(options.title ?? "GetStrata");
5837
- const preview = escapeHtml2(options.preview ?? "");
5838
- const footer = escapeHtml2(options.footer ?? "Sent by GetStrata");
5976
+ const title = escapeHtml3(options.title ?? "GetStrata");
5977
+ const preview = escapeHtml3(options.preview ?? "");
5978
+ const footer = escapeHtml3(options.footer ?? "Sent by GetStrata");
5839
5979
  return `<!DOCTYPE html>
5840
5980
  <html lang="en">
5841
5981
  <head>
@@ -6982,6 +7122,7 @@ export {
6982
7122
  ValidationError,
6983
7123
  WebFormRequest,
6984
7124
  WhereBuilder,
7125
+ absoluteTemporarySignedUrl,
6985
7126
  appSchedule,
6986
7127
  appendOrganizationScope,
6987
7128
  appendProjectScope,
@@ -6991,6 +7132,7 @@ export {
6991
7132
  assertIfMatch,
6992
7133
  assertOrganizationReadable,
6993
7134
  assertResourceInCurrentTenant,
7135
+ assertValidSignature,
6994
7136
  auditChecksum,
6995
7137
  auth,
6996
7138
  authContext,
@@ -7078,6 +7220,7 @@ export {
7078
7220
  hasMinimumOrgRole2 as hasMinimumOrgRole,
7079
7221
  hasOne,
7080
7222
  hasOrgMembership,
7223
+ hasValidSignature,
7081
7224
  htmlErrorResponse,
7082
7225
  htmlResponse,
7083
7226
  hydrateValue,
@@ -7177,6 +7320,7 @@ export {
7177
7320
  runWithTraceContext,
7178
7321
  safeInternalRedirectPath,
7179
7322
  sanitizeInternalPath,
7323
+ sanitizeMailHtml,
7180
7324
  scopedOrganizationIds,
7181
7325
  securedBindRouteModel,
7182
7326
  securedBindRouteModelByKey,
@@ -7184,11 +7328,13 @@ export {
7184
7328
  serializeDate,
7185
7329
  serverHtmxContentSecurityPolicy,
7186
7330
  setActiveApplicationContext,
7331
+ signedUrl,
7187
7332
  spaContentSecurityPolicy,
7188
7333
  storageFacade as storage,
7189
7334
  strictApiContentSecurityPolicy,
7190
7335
  stringRule,
7191
7336
  stripMarkdown,
7337
+ temporarySignedUrl,
7192
7338
  textResponse,
7193
7339
  toPaginatedResourceCollection,
7194
7340
  toResourceCollection,