@getstrata/core 0.5.60 → 0.5.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +1 -1
- package/dist/core/http/contentSecurityPolicy.d.ts +27 -0
- package/dist/core/http/requestMetaContext.d.ts +1 -0
- package/dist/core/http/safeInternalPath.d.ts +4 -0
- package/dist/core/http/securedRouteModelBinding.d.ts +2 -0
- package/dist/core/http/securityHeadersMiddleware.d.ts +5 -2
- package/dist/core/http/signedUrl.d.ts +11 -0
- package/dist/core/http/webErrorResponse.d.ts +1 -1
- package/dist/core/jobs/exportAuditLogsJob.d.ts +12 -0
- package/dist/core/mail/sanitizeMailHtml.d.ts +2 -0
- package/dist/core/security/totp.d.ts +7 -1
- package/dist/core/view/etaViewEngine.d.ts +3 -2
- package/dist/core/view/htmlResponse.d.ts +1 -2
- package/dist/core/view/index.d.ts +4 -1
- package/dist/core/view/viewEngine.d.ts +1 -0
- package/dist/core/view/webErrorView.d.ts +21 -0
- package/dist/entries/audit/exportAuditLogs.js +1 -1
- package/dist/entries/http/contentSecurityPolicy.js +1 -0
- package/dist/entries/http/requireAbilityMiddleware.js +44 -1
- package/dist/entries/http/requireWebAuthMiddleware.js +36 -3
- package/dist/entries/http/response.js +127 -555
- package/dist/entries/http/safeInternalPath.js +38 -0
- package/dist/entries/http/securedRouteModelBinding.js +27 -5
- package/dist/entries/http/securityHeadersMiddleware.js +34 -61
- package/dist/entries/http/signedUrl.js +97 -0
- package/dist/entries/http/webErrorResponse.js +126 -554
- package/dist/entries/jobs/dispatchWebhookJob.js +14 -12
- package/dist/entries/jobs/exportAuditLogsJob.js +340 -0
- package/dist/entries/logging/requestLoggingMiddleware.js +2 -1
- package/dist/entries/mail/markdownMail.js +62 -12
- package/dist/entries/mail/markdownMailable.js +62 -12
- package/dist/entries/mail/sanitizeMailHtml.js +60 -0
- package/dist/entries/openapi/generator.js +1 -1
- package/dist/entries/security/safeFetch.js +1 -1
- package/dist/entries/security/totp.js +31 -1
- package/dist/entries/view.js +88 -8
- package/dist/framework/public-api.d.ts +6 -1
- package/dist/index.js +744 -349
- package/package.json +27 -2
- package/dist/config/contentSecurityPolicy.d.ts +0 -5
|
@@ -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
|
};
|
package/dist/entries/view.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/core/view/etaViewEngine.ts
|
|
3
3
|
import { join, relative } from "path";
|
|
4
|
+
import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
4
5
|
import { Eta } from "eta";
|
|
5
6
|
|
|
6
7
|
// ../../src/core/view/assertEtaHtmlSource.ts
|
|
@@ -198,7 +199,8 @@ class EtaViewEngine {
|
|
|
198
199
|
}
|
|
199
200
|
async render(name, data = {}, options = {}) {
|
|
200
201
|
const template = name.endsWith(".eta") ? name : `${name}.eta`;
|
|
201
|
-
const
|
|
202
|
+
const request = options.request ?? currentRequestMeta().request;
|
|
203
|
+
const layoutData = this.resolveLayoutData ? await this.resolveLayoutData(request) : {};
|
|
202
204
|
const mergedData = { ...layoutData, ...data };
|
|
203
205
|
const body = await this.eta.renderAsync(template, mergedData);
|
|
204
206
|
const layout = options.layout ?? DEFAULT_LAYOUT;
|
|
@@ -236,9 +238,6 @@ function redirectResponse(location, status = 302) {
|
|
|
236
238
|
}
|
|
237
239
|
});
|
|
238
240
|
}
|
|
239
|
-
function notFoundHtmlResponse(body = "Not Found") {
|
|
240
|
-
return htmlResponse(body, { status: 404 });
|
|
241
|
-
}
|
|
242
241
|
function textResponse(body, init = {}) {
|
|
243
242
|
return new Response(body, {
|
|
244
243
|
status: init.status ?? 200,
|
|
@@ -258,9 +257,84 @@ function xmlResponse(body, init = {}) {
|
|
|
258
257
|
function rssResponse(body, init = {}) {
|
|
259
258
|
return xmlResponse(body, { ...init, contentType: "application/rss+xml" });
|
|
260
259
|
}
|
|
260
|
+
// ../../src/core/view/webErrorView.ts
|
|
261
|
+
import { currentRequestMeta as currentRequestMeta2 } from "@getstrata/core/http/requestMetaContext";
|
|
262
|
+
var configuredErrorView = {};
|
|
263
|
+
function configureWebErrorView(options) {
|
|
264
|
+
configuredErrorView = { ...options };
|
|
265
|
+
}
|
|
266
|
+
function escapeHtml(value) {
|
|
267
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
268
|
+
}
|
|
269
|
+
function renderKernelErrorChrome(input) {
|
|
270
|
+
const title = escapeHtml(input.title);
|
|
271
|
+
const message = escapeHtml(input.message);
|
|
272
|
+
const errorLines = Object.entries(input.errors ?? {}).flatMap(([field, messages]) => messages.map((item) => `${field}: ${item}`)).map((line) => `<li>${escapeHtml(line)}</li>`).join("");
|
|
273
|
+
const details = errorLines ? `<ul class="error-list">${errorLines}</ul>` : "";
|
|
274
|
+
const goBack = input.status === 422 ? `<p><a href="javascript:history.back()">Go back</a></p>` : "";
|
|
275
|
+
return `<!doctype html>
|
|
276
|
+
<html lang="en">
|
|
277
|
+
<head>
|
|
278
|
+
<meta charset="UTF-8" />
|
|
279
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
280
|
+
<title>${title}</title>
|
|
281
|
+
<link rel="stylesheet" href="/assets/app.css" />
|
|
282
|
+
</head>
|
|
283
|
+
<body>
|
|
284
|
+
<header class="site-header">
|
|
285
|
+
<a class="brand" href="/">Home</a>
|
|
286
|
+
</header>
|
|
287
|
+
<main class="site-main">
|
|
288
|
+
<section class="page-header">
|
|
289
|
+
<h1>${title}</h1>
|
|
290
|
+
<p>${message}</p>
|
|
291
|
+
${details}
|
|
292
|
+
${goBack}
|
|
293
|
+
</section>
|
|
294
|
+
</main>
|
|
295
|
+
</body>
|
|
296
|
+
</html>
|
|
297
|
+
`;
|
|
298
|
+
}
|
|
299
|
+
function errorTemplateName(status) {
|
|
300
|
+
if (status === 404) {
|
|
301
|
+
return "errors/not-found";
|
|
302
|
+
}
|
|
303
|
+
if (status === 403) {
|
|
304
|
+
return "errors/forbidden";
|
|
305
|
+
}
|
|
306
|
+
return "errors/error";
|
|
307
|
+
}
|
|
308
|
+
async function renderWebErrorHtml(input) {
|
|
309
|
+
const render = configuredErrorView.render;
|
|
310
|
+
if (!render) {
|
|
311
|
+
return renderKernelErrorChrome(input);
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
return await render({
|
|
315
|
+
...input,
|
|
316
|
+
request: input.request ?? currentRequestMeta2().request
|
|
317
|
+
});
|
|
318
|
+
} catch {
|
|
319
|
+
return renderKernelErrorChrome(input);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async function htmlErrorResponse(input) {
|
|
323
|
+
return htmlResponse(await renderWebErrorHtml(input), { status: input.status });
|
|
324
|
+
}
|
|
325
|
+
async function notFoundHtmlResponse(body) {
|
|
326
|
+
if (body !== undefined) {
|
|
327
|
+
return htmlResponse(body, { status: 404 });
|
|
328
|
+
}
|
|
329
|
+
return htmlErrorResponse({
|
|
330
|
+
status: 404,
|
|
331
|
+
title: "Not Found",
|
|
332
|
+
message: "The page you requested was not found."
|
|
333
|
+
});
|
|
334
|
+
}
|
|
261
335
|
// ../../src/core/view/webLayoutData.ts
|
|
262
336
|
import { currentAuthUser } from "@getstrata/core/auth/authContext";
|
|
263
|
-
import { currentRequestMeta as
|
|
337
|
+
import { currentRequestMeta as currentRequestMeta4 } from "@getstrata/core/http/requestMetaContext";
|
|
264
338
|
|
|
265
339
|
// ../../src/core/contracts/serviceTokens.ts
|
|
266
340
|
var CORE_CONFIG_TOKEN = "core.config";
|
|
@@ -357,7 +431,7 @@ var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext"
|
|
|
357
431
|
function runWithRequestMeta(meta, callback) {
|
|
358
432
|
return requestMetaContext.run(meta, callback);
|
|
359
433
|
}
|
|
360
|
-
function
|
|
434
|
+
function currentRequestMeta3() {
|
|
361
435
|
return requestMetaContext.getStore() ?? {
|
|
362
436
|
ipAddress: null,
|
|
363
437
|
userAgent: null
|
|
@@ -439,7 +513,7 @@ function verifyCsrfToken(request, submittedToken) {
|
|
|
439
513
|
return Bun.CSRF.verify(submittedToken, csrfVerifyOptions());
|
|
440
514
|
}
|
|
441
515
|
function resolveCsrfTokenForRequest(request) {
|
|
442
|
-
const metaToken =
|
|
516
|
+
const metaToken = currentRequestMeta3().csrfToken;
|
|
443
517
|
if (metaToken) {
|
|
444
518
|
return metaToken;
|
|
445
519
|
}
|
|
@@ -584,7 +658,7 @@ async function defaultLoadLayoutUser(container, _request) {
|
|
|
584
658
|
async function resolveWebLayoutData(container, request, options = {}) {
|
|
585
659
|
const resolved = { ...configuredLayoutOptions, ...options };
|
|
586
660
|
const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
|
|
587
|
-
const flash = request ?
|
|
661
|
+
const flash = request ? currentRequestMeta4().flash ?? pullFlash(request) : null;
|
|
588
662
|
const userKey = layoutUserKey(resolved);
|
|
589
663
|
const loadUser = resolved.loadUser ?? defaultLoadLayoutUser;
|
|
590
664
|
const user = await loadUser(container, request);
|
|
@@ -593,17 +667,23 @@ async function resolveWebLayoutData(container, request, options = {}) {
|
|
|
593
667
|
[userKey]: user,
|
|
594
668
|
csrfToken,
|
|
595
669
|
flash,
|
|
670
|
+
cspNonce: currentRequestMeta4().cspNonce ?? "",
|
|
596
671
|
...extra
|
|
597
672
|
};
|
|
598
673
|
}
|
|
599
674
|
export {
|
|
600
675
|
DEFAULT_VIEWS_DIRECTORY,
|
|
601
676
|
EtaViewEngine,
|
|
677
|
+
configureWebErrorView,
|
|
602
678
|
configureWebLayoutData,
|
|
679
|
+
errorTemplateName,
|
|
680
|
+
htmlErrorResponse,
|
|
603
681
|
htmlResponse,
|
|
604
682
|
isHtmxRequest,
|
|
605
683
|
notFoundHtmlResponse,
|
|
606
684
|
redirectResponse,
|
|
685
|
+
renderKernelErrorChrome,
|
|
686
|
+
renderWebErrorHtml,
|
|
607
687
|
resolveWebLayoutData,
|
|
608
688
|
rssResponse,
|
|
609
689
|
textResponse,
|
|
@@ -56,6 +56,8 @@ export { auth, cache, config, events, log, mail, policyGate, queue, storage, } f
|
|
|
56
56
|
export { createBodySizeLimitMiddleware } from "../core/http/bodySizeLimitMiddleware.ts";
|
|
57
57
|
export { readClientIp, trustForwardedFor } from "../core/http/clientIp.ts";
|
|
58
58
|
export { conditionalJsonResponse } from "../core/http/conditionalResponse.ts";
|
|
59
|
+
export type { ContentSecurityPolicyDirectives, ContentSecurityPolicyOptions, } from "../core/http/contentSecurityPolicy.ts";
|
|
60
|
+
export { configureContentSecurityPolicy, generateCspNonce, resolveContentSecurityPolicy, resolveHtmlContentSecurityPolicy, serverHtmxContentSecurityPolicy, spaContentSecurityPolicy, strictApiContentSecurityPolicy, } from "../core/http/contentSecurityPolicy.ts";
|
|
59
61
|
export { readBunRequestCookie, readRequestCookie } from "../core/http/cookies.ts";
|
|
60
62
|
export { createCorsMiddleware } from "../core/http/corsMiddleware.ts";
|
|
61
63
|
export { createCsrfMiddleware } from "../core/http/csrfMiddleware.ts";
|
|
@@ -78,8 +80,10 @@ export { createRequireWebAuthMiddleware } from "../core/http/requireWebAuthMiddl
|
|
|
78
80
|
export { serializeDate, toPaginatedResourceCollection, toResourceCollection, } from "../core/http/resources.ts";
|
|
79
81
|
export { createdResponse, jsonResponse, noContentResponse, withErrorHandling, } from "../core/http/response.ts";
|
|
80
82
|
export type { RouteRequest } from "../core/http/route.ts";
|
|
83
|
+
export { loginRedirectLocation, safeInternalRedirectPath, sanitizeInternalPath, } from "../core/http/safeInternalPath.ts";
|
|
81
84
|
export { createScimThrottleMiddleware } from "../core/http/scimThrottleMiddleware.ts";
|
|
82
85
|
export { createSecurityHeadersMiddleware } from "../core/http/securityHeadersMiddleware.ts";
|
|
86
|
+
export { absoluteTemporarySignedUrl, assertValidSignature, hasValidSignature, signedUrl, temporarySignedUrl, } from "../core/http/signedUrl.ts";
|
|
83
87
|
export { createThrottleMiddleware } from "../core/http/throttleMiddleware.ts";
|
|
84
88
|
export { WebFormRequest } from "../core/http/webFormRequest.ts";
|
|
85
89
|
export { installGracefulShutdownSignals, registerShutdownHandler, runGracefulShutdown, } from "../core/lifecycle/gracefulShutdown.ts";
|
|
@@ -90,6 +94,7 @@ export type { MarkdownMailLayoutOptions, RenderedMarkdownMail } from "../core/ma
|
|
|
90
94
|
export { markdownToHtml, renderMarkdownMail, stripMarkdown, wrapMarkdownMailLayout, } from "../core/mail/markdownMail.ts";
|
|
91
95
|
export type { MarkdownMailableInput } from "../core/mail/markdownMailable.ts";
|
|
92
96
|
export { buildMarkdownMailMessage, sendMarkdownMail } from "../core/mail/markdownMailable.ts";
|
|
97
|
+
export { sanitizeMailHtml } from "../core/mail/sanitizeMailHtml.ts";
|
|
93
98
|
export type { MetricLabels } from "../core/metrics/prometheus.ts";
|
|
94
99
|
export { PrometheusRegistry, prometheusRegistry } from "../core/metrics/prometheus.ts";
|
|
95
100
|
export type { DatabaseNotificationPayload, DatabaseNotificationStore, MailNotificationMessage, Notifiable, NotificationChannelName, } from "../core/notifications/index.ts";
|
|
@@ -119,6 +124,6 @@ export { currentTraceId, runWithTraceContext } from "../core/tracing/traceContex
|
|
|
119
124
|
export { createTracingMiddleware } from "../core/tracing/tracingMiddleware.ts";
|
|
120
125
|
export type { ValidationRule, ValidationSchema } from "../core/validation/rules.ts";
|
|
121
126
|
export { emailRule, maxLength, minLength, required, stringRule, validateObject, } from "../core/validation/rules.ts";
|
|
122
|
-
export { configureWebLayoutData, DEFAULT_VIEWS_DIRECTORY, EtaViewEngine, htmlResponse, isHtmxRequest, notFoundHtmlResponse, redirectResponse, resolveWebLayoutData, rssResponse, textResponse, xmlResponse, } from "../core/view/index.ts";
|
|
127
|
+
export { configureWebErrorView, configureWebLayoutData, DEFAULT_VIEWS_DIRECTORY, EtaViewEngine, errorTemplateName, htmlErrorResponse, htmlResponse, isHtmxRequest, notFoundHtmlResponse, redirectResponse, renderKernelErrorChrome, resolveWebLayoutData, rssResponse, textResponse, xmlResponse, } from "../core/view/index.ts";
|
|
123
128
|
export type { ViewEngine } from "../core/view/viewEngine.ts";
|
|
124
129
|
export type { WebLayoutAuthUser, WebLayoutData, WebLayoutDataOptions, WebLayoutUserKey, } from "../core/view/webLayoutData.ts";
|