@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +1 -1
  3. package/dist/core/http/contentSecurityPolicy.d.ts +27 -0
  4. package/dist/core/http/requestMetaContext.d.ts +1 -0
  5. package/dist/core/http/safeInternalPath.d.ts +4 -0
  6. package/dist/core/http/securedRouteModelBinding.d.ts +2 -0
  7. package/dist/core/http/securityHeadersMiddleware.d.ts +5 -2
  8. package/dist/core/http/signedUrl.d.ts +11 -0
  9. package/dist/core/http/webErrorResponse.d.ts +1 -1
  10. package/dist/core/jobs/exportAuditLogsJob.d.ts +12 -0
  11. package/dist/core/mail/sanitizeMailHtml.d.ts +2 -0
  12. package/dist/core/security/totp.d.ts +7 -1
  13. package/dist/core/view/etaViewEngine.d.ts +3 -2
  14. package/dist/core/view/htmlResponse.d.ts +1 -2
  15. package/dist/core/view/index.d.ts +4 -1
  16. package/dist/core/view/viewEngine.d.ts +1 -0
  17. package/dist/core/view/webErrorView.d.ts +21 -0
  18. package/dist/entries/audit/exportAuditLogs.js +1 -1
  19. package/dist/entries/http/contentSecurityPolicy.js +1 -0
  20. package/dist/entries/http/requireAbilityMiddleware.js +44 -1
  21. package/dist/entries/http/requireWebAuthMiddleware.js +36 -3
  22. package/dist/entries/http/response.js +127 -555
  23. package/dist/entries/http/safeInternalPath.js +38 -0
  24. package/dist/entries/http/securedRouteModelBinding.js +27 -5
  25. package/dist/entries/http/securityHeadersMiddleware.js +34 -61
  26. package/dist/entries/http/signedUrl.js +97 -0
  27. package/dist/entries/http/webErrorResponse.js +126 -554
  28. package/dist/entries/jobs/dispatchWebhookJob.js +14 -12
  29. package/dist/entries/jobs/exportAuditLogsJob.js +340 -0
  30. package/dist/entries/logging/requestLoggingMiddleware.js +2 -1
  31. package/dist/entries/mail/markdownMail.js +62 -12
  32. package/dist/entries/mail/markdownMailable.js +62 -12
  33. package/dist/entries/mail/sanitizeMailHtml.js +60 -0
  34. package/dist/entries/openapi/generator.js +1 -1
  35. package/dist/entries/security/safeFetch.js +1 -1
  36. package/dist/entries/security/totp.js +31 -1
  37. package/dist/entries/view.js +88 -8
  38. package/dist/framework/public-api.d.ts +6 -1
  39. package/dist/index.js +744 -349
  40. package/package.json +27 -2
  41. 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
  };
@@ -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 layoutData = this.resolveLayoutData ? await this.resolveLayoutData() : {};
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
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 currentRequestMeta2 } from "@getstrata/core/http/requestMetaContext";
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 currentRequestMeta() {
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 = currentRequestMeta().csrfToken;
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 ? currentRequestMeta2().flash ?? pullFlash(request) : null;
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";