@getstrata/core 2.2.40 → 2.2.42

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.
@@ -121,7 +121,8 @@ var init_transactionControl = __esm(() => {
121
121
  });
122
122
 
123
123
  // ../../src/core/http/response.ts
124
- import { toHttpError as toHttpError3 } from "@getstrata/core/errors/http";
124
+ import { toHttpError as toHttpError2 } from "@getstrata/core/errors/http";
125
+ import { logServerError, webErrorResponse } from "@getstrata/core/http/webErrorResponse";
125
126
 
126
127
  // ../../src/core/database/errors.ts
127
128
  import {
@@ -258,302 +259,6 @@ function currentRequestMeta() {
258
259
  };
259
260
  }
260
261
 
261
- // ../../src/core/http/webErrorResponse.ts
262
- import { toHttpError as toHttpError2, ValidationError } from "@getstrata/core/errors/http";
263
- import { appLogger } from "@getstrata/core/logging/logger";
264
-
265
- // ../../src/core/runtime/appEnv.ts
266
- var NON_PRODUCTION_APP_ENVS = new Set(["local", "development", "dev", "test", "testing", "ci"]);
267
- function normalizeEnvValue(value) {
268
- return (value ?? "").trim().toLowerCase();
269
- }
270
- function isProductionEnv(env = process.env) {
271
- const appEnv = normalizeEnvValue(env.APP_ENV);
272
- const nodeEnv = normalizeEnvValue(env.NODE_ENV);
273
- if (appEnv === "production" || nodeEnv === "production") {
274
- return true;
275
- }
276
- if (appEnv === "") {
277
- return false;
278
- }
279
- return !NON_PRODUCTION_APP_ENVS.has(appEnv);
280
- }
281
- function envFlagEnabled(value) {
282
- return value === "true";
283
- }
284
-
285
- // ../../src/core/runtime/frontendMode.ts
286
- var FRONTEND_MODES = ["api", "server-htmx", "spa-react", "hybrid"];
287
- var DEFAULT_SPA_PREFIX = "/app";
288
- var FRONTEND_MODE_PATTERN = new RegExp(`^(${FRONTEND_MODES.join("|")})$`);
289
- function parseFrontendMode(value) {
290
- const mode = (value ?? "api").trim();
291
- if (mode === "server-htmx" || mode === "spa-react" || mode === "hybrid") {
292
- return mode;
293
- }
294
- return "api";
295
- }
296
- function readFrontendMode() {
297
- return parseFrontendMode(process.env.FRONTEND_MODE);
298
- }
299
- function isViewsMode(mode) {
300
- return mode === "server-htmx" || mode === "hybrid";
301
- }
302
- function isSpaMode(mode) {
303
- return mode === "spa-react" || mode === "hybrid";
304
- }
305
- function isViewsEnabled() {
306
- return isViewsMode(readFrontendMode());
307
- }
308
- function isSpaEnabled() {
309
- return isSpaMode(readFrontendMode());
310
- }
311
- function normalizeSpaPrefix(value) {
312
- const raw = (value ?? DEFAULT_SPA_PREFIX).trim() || DEFAULT_SPA_PREFIX;
313
- const withSlash = raw.startsWith("/") ? raw : `/${raw}`;
314
- const trimmed = withSlash.replace(/\/+$/, "");
315
- if (trimmed.length === 0 || trimmed === "/") {
316
- return DEFAULT_SPA_PREFIX;
317
- }
318
- return trimmed;
319
- }
320
- function readSpaPrefix() {
321
- return normalizeSpaPrefix(process.env.SPA_PREFIX);
322
- }
323
-
324
- // ../../src/core/view/webErrorView.ts
325
- import { currentRequestMeta as currentRequestMeta2 } from "@getstrata/core/http/requestMetaContext";
326
-
327
- // ../../src/core/http/safeInternalPath.ts
328
- var NESTED_REDIRECT_KEYS = new Set(["redirect", "next", "return", "returnTo", "return_to"]);
329
- function looksLikeExternalTarget(value) {
330
- const trimmed = value.trim();
331
- if (!trimmed.startsWith("/") || trimmed.startsWith("//") || trimmed.includes("\\")) {
332
- return true;
333
- }
334
- if (trimmed.includes("://") || trimmed.includes(":/") || trimmed.includes(":\\")) {
335
- return true;
336
- }
337
- try {
338
- const decoded = decodeURIComponent(trimmed);
339
- if (decoded.startsWith("//") || decoded.includes("\\") || /https?:/i.test(decoded)) {
340
- return true;
341
- }
342
- } catch {
343
- return true;
344
- }
345
- return false;
346
- }
347
- function sanitizeNestedQuery(search) {
348
- if (!search) {
349
- return "";
350
- }
351
- const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
352
- const next = new URLSearchParams;
353
- for (const [key, value] of params.entries()) {
354
- if (NESTED_REDIRECT_KEYS.has(key)) {
355
- const sanitized = sanitizeInternalPath(value, "/");
356
- next.set(key, sanitized);
357
- continue;
358
- }
359
- if (looksLikeExternalTarget(value) && value.includes("://")) {
360
- continue;
361
- }
362
- next.set(key, value);
363
- }
364
- const serialized = next.toString();
365
- return serialized ? `?${serialized}` : "";
366
- }
367
- function sanitizeInternalPath(raw, fallback = "/") {
368
- const trimmed = raw.trim();
369
- if (looksLikeExternalTarget(trimmed)) {
370
- return fallback;
371
- }
372
- const queryIndex = trimmed.indexOf("?");
373
- const hashIndex = trimmed.indexOf("#");
374
- const end = [queryIndex, hashIndex].filter((index) => index >= 0).sort((a, b) => a - b)[0];
375
- const pathname = end === undefined ? trimmed : trimmed.slice(0, end);
376
- if (looksLikeExternalTarget(pathname)) {
377
- return fallback;
378
- }
379
- const search = queryIndex >= 0 ? trimmed.slice(queryIndex, hashIndex >= 0 ? hashIndex : undefined) : "";
380
- return `${pathname}${sanitizeNestedQuery(search)}`;
381
- }
382
- function safeInternalRedirectPath(request, fallback = "/") {
383
- const url = new URL(request.url);
384
- return sanitizeInternalPath(`${url.pathname}${url.search}`, fallback);
385
- }
386
- function loginRedirectLocation(request) {
387
- return `/login?redirect=${encodeURIComponent(safeInternalRedirectPath(request))}`;
388
- }
389
-
390
- // ../../src/core/view/htmlResponse.ts
391
- function htmlResponse(html, init = {}) {
392
- return new Response(html, {
393
- status: init.status ?? 200,
394
- statusText: init.statusText,
395
- headers: {
396
- "Content-Type": "text/html; charset=utf-8"
397
- }
398
- });
399
- }
400
-
401
- // ../../src/core/view/webErrorView.ts
402
- var configuredErrorView = {};
403
- function escapeHtml(value) {
404
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
405
- }
406
- function errorReturnPath(request) {
407
- if (!request)
408
- return "/";
409
- try {
410
- const current = new URL(request.url);
411
- const previous = new URL(request.headers.get("referer") ?? "");
412
- if (previous.origin !== current.origin || (request.method === "GET" || request.method === "HEAD") && previous.pathname === current.pathname)
413
- return "/";
414
- return sanitizeInternalPath(previous.pathname, "/");
415
- } catch {
416
- return "/";
417
- }
418
- }
419
- function renderKernelErrorChrome(input) {
420
- const title = escapeHtml(input.title);
421
- const message = escapeHtml(input.message);
422
- const errorLines = Object.entries(input.errors ?? {}).flatMap(([field, messages]) => messages.map((item) => `${field}: ${item}`)).map((line) => `<li>${escapeHtml(line)}</li>`).join("");
423
- const details = errorLines ? `<ul class="error-list">${errorLines}</ul>` : "";
424
- const goBack = input.status === 422 ? `<p><a href="${escapeHtml(errorReturnPath(input.request ?? currentRequestMeta2().request))}">Go back</a></p>` : "";
425
- return `<!doctype html>
426
- <html lang="en">
427
- <head>
428
- <meta charset="UTF-8" />
429
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
430
- <title>${title}</title>
431
- <link rel="stylesheet" href="/assets/app.css" />
432
- </head>
433
- <body>
434
- <header class="site-header">
435
- <a class="brand" href="/">Home</a>
436
- </header>
437
- <main class="site-main">
438
- <section class="page-header">
439
- <h1>${title}</h1>
440
- <p>${message}</p>
441
- ${details}
442
- ${goBack}
443
- </section>
444
- </main>
445
- </body>
446
- </html>
447
- `;
448
- }
449
- async function renderWebErrorHtml(input) {
450
- const render = configuredErrorView.render;
451
- if (!render) {
452
- return renderKernelErrorChrome(input);
453
- }
454
- try {
455
- return await render({
456
- ...input,
457
- request: input.request ?? currentRequestMeta2().request
458
- });
459
- } catch {
460
- return renderKernelErrorChrome(input);
461
- }
462
- }
463
- async function htmlErrorResponse(input) {
464
- return htmlResponse(await renderWebErrorHtml(input), { status: input.status });
465
- }
466
-
467
- // ../../src/core/http/contentNegotiation.ts
468
- function requestPrefersJson(request) {
469
- if (!request) {
470
- return true;
471
- }
472
- if (request.headers.get("HX-Request") === "true") {
473
- return false;
474
- }
475
- const pathname = new URL(request.url).pathname;
476
- if (pathname.startsWith("/api/")) {
477
- return true;
478
- }
479
- const accept = request.headers.get("accept")?.toLowerCase() ?? "";
480
- if (accept.includes("text/html")) {
481
- return false;
482
- }
483
- if (accept.includes("application/json")) {
484
- return true;
485
- }
486
- const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
487
- if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
488
- return false;
489
- }
490
- return false;
491
- }
492
-
493
- // ../../src/core/http/webErrorResponse.ts
494
- function normalizeFieldErrors(details) {
495
- if (!details || typeof details !== "object" || Array.isArray(details)) {
496
- return {};
497
- }
498
- const errors = {};
499
- for (const [field, messages] of Object.entries(details)) {
500
- if (Array.isArray(messages)) {
501
- errors[field] = messages.map(String);
502
- continue;
503
- }
504
- if (typeof messages === "string") {
505
- errors[field] = [messages];
506
- }
507
- }
508
- return errors;
509
- }
510
- function errorPageTitle(status, message) {
511
- if (status === 404) {
512
- return "Not Found";
513
- }
514
- if (status === 403) {
515
- return "Forbidden";
516
- }
517
- if (status >= 500) {
518
- return "Server Error";
519
- }
520
- return message;
521
- }
522
- function publicErrorMessage(status, message) {
523
- if (status >= 500 && isProductionEnv()) {
524
- return "Something went wrong.";
525
- }
526
- return message;
527
- }
528
- function logServerError(error, mappedError) {
529
- if (mappedError.status < 500) {
530
- return;
531
- }
532
- appLogger.error("Unhandled request error", {
533
- status: mappedError.status,
534
- error: error instanceof Error ? error.message : String(error),
535
- stack: error instanceof Error ? error.stack : undefined
536
- });
537
- }
538
- async function webErrorResponse(error, request) {
539
- if (currentRequestMeta().errorFormat === "json" || !request || !isViewsEnabled() || requestPrefersJson(request)) {
540
- return null;
541
- }
542
- const mappedError = toHttpError2(error) ?? mapDatabaseError(error);
543
- logServerError(error, mappedError);
544
- if (mappedError.status === 401) {
545
- return Response.redirect(loginRedirectLocation(request), 302);
546
- }
547
- const errors = mappedError instanceof ValidationError || mappedError.name === "ValidationError" ? normalizeFieldErrors(mappedError.details) : undefined;
548
- return htmlErrorResponse({
549
- status: mappedError.status,
550
- title: errorPageTitle(mappedError.status, mappedError.message),
551
- message: publicErrorMessage(mappedError.status, mappedError.message),
552
- errors,
553
- request
554
- });
555
- }
556
-
557
262
  // ../../src/core/http/response.ts
558
263
  function jsonResponse(data, init = {}) {
559
264
  return Response.json(data, {
@@ -561,6 +266,11 @@ function jsonResponse(data, init = {}) {
561
266
  headers: init.headers
562
267
  });
563
268
  }
269
+ function privateJsonResponse(data, init = {}) {
270
+ const response = jsonResponse(data, init);
271
+ response.headers.set("Cache-Control", "private, no-store");
272
+ return response;
273
+ }
564
274
  function createdResponse(data, init = {}) {
565
275
  return jsonResponse(data, { ...init, status: init.status ?? 201 });
566
276
  }
@@ -568,12 +278,12 @@ function noContentResponse() {
568
278
  return new Response(null, { status: 204 });
569
279
  }
570
280
  function errorResponse(error) {
571
- const mappedError = toHttpError3(error) ?? mapDatabaseError(error);
281
+ const mappedError = toHttpError2(error) ?? mapDatabaseError(error);
572
282
  logServerError(error, mappedError);
573
283
  return Response.json({
574
284
  error: mappedError.message,
575
285
  ...mappedError.details === undefined ? {} : { details: mappedError.details }
576
- }, { status: mappedError.status });
286
+ }, { status: mappedError.status, headers: { "Cache-Control": "no-store" } });
577
287
  }
578
288
  function withErrorHandling(handler) {
579
289
  return async (...args) => {
@@ -620,6 +330,7 @@ export {
620
330
  errorResponse,
621
331
  jsonResponse,
622
332
  noContentResponse,
333
+ privateJsonResponse,
623
334
  withErrorHandling,
624
335
  withJsonErrorHandling
625
336
  };
@@ -457,7 +457,9 @@ async function renderWebErrorHtml(input) {
457
457
  }
458
458
  }
459
459
  async function htmlErrorResponse(input) {
460
- return htmlResponse(await renderWebErrorHtml(input), { status: input.status });
460
+ const response = htmlResponse(await renderWebErrorHtml(input), { status: input.status });
461
+ response.headers.set("Cache-Control", "no-store");
462
+ return response;
461
463
  }
462
464
 
463
465
  // ../../src/core/http/contentNegotiation.ts
@@ -342,7 +342,9 @@ async function renderWebErrorHtml(input) {
342
342
  }
343
343
  }
344
344
  async function htmlErrorResponse(input) {
345
- return htmlResponse(await renderWebErrorHtml(input), { status: input.status });
345
+ const response = htmlResponse(await renderWebErrorHtml(input), { status: input.status });
346
+ response.headers.set("Cache-Control", "no-store");
347
+ return response;
346
348
  }
347
349
 
348
350
  // ../../src/core/http/contentNegotiation.ts
@@ -106,7 +106,7 @@ export { createRequireAbilityMiddleware } from "../core/http/requireAbilityMiddl
106
106
  export { createRequireGlobalAdminMiddleware } from "../core/http/requireGlobalAdminMiddleware.ts";
107
107
  export { createRequireWebAuthMiddleware } from "../core/http/requireWebAuthMiddleware.ts";
108
108
  export { JsonResource, ResourceCollection, serializeDate, toPaginatedResourceCollection, toResourceCollection, whenLoaded, } from "../core/http/resources.ts";
109
- export { createdResponse, errorResponse, jsonResponse, noContentResponse, withErrorHandling, } from "../core/http/response.ts";
109
+ export { createdResponse, errorResponse, jsonResponse, noContentResponse, privateJsonResponse, withErrorHandling, } from "../core/http/response.ts";
110
110
  export type { ResourceActions, ResourceMemberHandler, ResourceRouteMap, RouteRequest, } from "../core/http/route.ts";
111
111
  export { getRouteParams, mergeRouteMaps, resourceRoutes } from "../core/http/route.ts";
112
112
  export { loginRedirectLocation, safeInternalRedirectPath, sanitizeInternalPath, } from "../core/http/safeInternalPath.ts";
@@ -171,5 +171,5 @@ export type { ValidationRule, ValidationSchema } from "../core/validation/rules.
171
171
  export { emailRule, maxLength, minLength, required, stringRule, validateObject, } from "../core/validation/rules.ts";
172
172
  export { CORE_VIEW_RENDERER_TOKEN, configureWebErrorView, configureWebLayoutData, createWebLayoutDataResolver, DEFAULT_VIEWS_DIRECTORY, EtaViewEngine, errorTemplateName, htmlErrorResponse, htmlResponse, isHtmxRequest, notFoundHtmlResponse, redirectResponse, renderKernelErrorChrome, renderWebErrorHtml, resolveWebLayoutData, rssResponse, textResponse, ViewRenderer, xmlResponse, } from "../core/view/index.ts";
173
173
  export type { ViewEngine } from "../core/view/viewEngine.ts";
174
- export type { HtmxViewResponseOptions, NamespacedViewRenderer, ViewResponseOptions, } from "../core/view/viewRenderer.ts";
174
+ export type { HtmxViewResponseOptions, NamespacedViewRenderer, ViewNamespaceOptions, ViewResponseOptions, } from "../core/view/viewRenderer.ts";
175
175
  export type { WebLayoutAuthUser, WebLayoutData, WebLayoutDataOptions, WebLayoutUserKey, } from "../core/view/webLayoutData.ts";