@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.
- package/CHANGELOG.md +12 -0
- package/dist/core/http/signedUrl.d.ts +11 -0
- 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/entries/audit/exportAuditLogs.js +1 -1
- package/dist/entries/http/requireAbilityMiddleware.js +44 -1
- package/dist/entries/http/requireWebAuthMiddleware.js +2 -1
- package/dist/entries/http/securityHeadersMiddleware.js +1 -1
- package/dist/entries/http/signedUrl.js +97 -0
- package/dist/entries/jobs/dispatchWebhookJob.js +14 -12
- package/dist/entries/jobs/exportAuditLogsJob.js +340 -0
- 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/framework/public-api.d.ts +2 -0
- package/dist/index.js +161 -15
- package/package.json +17 -2
|
@@ -1,27 +1,77 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// ../../src/core/mail/
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5
25
|
}
|
|
26
|
+
function decodeBasicEntities(value) {
|
|
27
|
+
return value.replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
-
|
|
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 =
|
|
23
|
-
const preview =
|
|
24
|
-
const footer =
|
|
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/
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5
25
|
}
|
|
26
|
+
function decodeBasicEntities(value) {
|
|
27
|
+
return value.replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
-
|
|
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 =
|
|
23
|
-
const preview =
|
|
24
|
-
const footer =
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
25
|
+
}
|
|
26
|
+
function decodeBasicEntities(value) {
|
|
27
|
+
return value.replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/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 (
|
|
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/
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5819
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
|
+
|
|
5963
|
+
// ../../src/core/mail/markdownMail.ts
|
|
5964
|
+
function escapeHtml3(value) {
|
|
5965
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
-
|
|
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 =
|
|
5837
|
-
const preview =
|
|
5838
|
-
const footer =
|
|
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,
|