@getstrata/core 0.5.60 → 0.5.61
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 +10 -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/webErrorResponse.d.ts +1 -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/http/contentSecurityPolicy.js +1 -0
- package/dist/entries/http/requireWebAuthMiddleware.js +34 -2
- 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 +33 -60
- package/dist/entries/http/webErrorResponse.js +126 -554
- package/dist/entries/logging/requestLoggingMiddleware.js +2 -1
- package/dist/entries/view.js +88 -8
- package/dist/framework/public-api.d.ts +4 -1
- package/dist/index.js +589 -340
- package/package.json +12 -2
- package/dist/config/contentSecurityPolicy.d.ts +0 -5
|
@@ -80,219 +80,9 @@ async function withDatabaseErrorHandling(operation) {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
// ../../src/core/view/
|
|
84
|
-
import {
|
|
85
|
-
import { Eta } from "eta";
|
|
83
|
+
// ../../src/core/view/webErrorView.ts
|
|
84
|
+
import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
86
85
|
|
|
87
|
-
// ../../src/core/view/assertEtaHtmlSource.ts
|
|
88
|
-
var HTML_TAGS = new Set([
|
|
89
|
-
"a",
|
|
90
|
-
"abbr",
|
|
91
|
-
"address",
|
|
92
|
-
"article",
|
|
93
|
-
"aside",
|
|
94
|
-
"audio",
|
|
95
|
-
"b",
|
|
96
|
-
"blockquote",
|
|
97
|
-
"body",
|
|
98
|
-
"button",
|
|
99
|
-
"canvas",
|
|
100
|
-
"caption",
|
|
101
|
-
"cite",
|
|
102
|
-
"code",
|
|
103
|
-
"col",
|
|
104
|
-
"colgroup",
|
|
105
|
-
"data",
|
|
106
|
-
"datalist",
|
|
107
|
-
"dd",
|
|
108
|
-
"del",
|
|
109
|
-
"details",
|
|
110
|
-
"dfn",
|
|
111
|
-
"dialog",
|
|
112
|
-
"div",
|
|
113
|
-
"dl",
|
|
114
|
-
"dt",
|
|
115
|
-
"em",
|
|
116
|
-
"fieldset",
|
|
117
|
-
"figcaption",
|
|
118
|
-
"figure",
|
|
119
|
-
"footer",
|
|
120
|
-
"form",
|
|
121
|
-
"h1",
|
|
122
|
-
"h2",
|
|
123
|
-
"h3",
|
|
124
|
-
"h4",
|
|
125
|
-
"h5",
|
|
126
|
-
"h6",
|
|
127
|
-
"header",
|
|
128
|
-
"hgroup",
|
|
129
|
-
"hr",
|
|
130
|
-
"html",
|
|
131
|
-
"i",
|
|
132
|
-
"iframe",
|
|
133
|
-
"img",
|
|
134
|
-
"input",
|
|
135
|
-
"ins",
|
|
136
|
-
"kbd",
|
|
137
|
-
"label",
|
|
138
|
-
"legend",
|
|
139
|
-
"li",
|
|
140
|
-
"main",
|
|
141
|
-
"map",
|
|
142
|
-
"mark",
|
|
143
|
-
"menu",
|
|
144
|
-
"meter",
|
|
145
|
-
"nav",
|
|
146
|
-
"object",
|
|
147
|
-
"ol",
|
|
148
|
-
"optgroup",
|
|
149
|
-
"option",
|
|
150
|
-
"output",
|
|
151
|
-
"p",
|
|
152
|
-
"picture",
|
|
153
|
-
"pre",
|
|
154
|
-
"progress",
|
|
155
|
-
"q",
|
|
156
|
-
"rp",
|
|
157
|
-
"rt",
|
|
158
|
-
"ruby",
|
|
159
|
-
"s",
|
|
160
|
-
"samp",
|
|
161
|
-
"script",
|
|
162
|
-
"search",
|
|
163
|
-
"section",
|
|
164
|
-
"select",
|
|
165
|
-
"slot",
|
|
166
|
-
"small",
|
|
167
|
-
"source",
|
|
168
|
-
"span",
|
|
169
|
-
"strong",
|
|
170
|
-
"style",
|
|
171
|
-
"sub",
|
|
172
|
-
"summary",
|
|
173
|
-
"sup",
|
|
174
|
-
"table",
|
|
175
|
-
"tbody",
|
|
176
|
-
"td",
|
|
177
|
-
"template",
|
|
178
|
-
"textarea",
|
|
179
|
-
"tfoot",
|
|
180
|
-
"th",
|
|
181
|
-
"thead",
|
|
182
|
-
"time",
|
|
183
|
-
"title",
|
|
184
|
-
"tr",
|
|
185
|
-
"track",
|
|
186
|
-
"u",
|
|
187
|
-
"ul",
|
|
188
|
-
"var",
|
|
189
|
-
"video",
|
|
190
|
-
"wbr"
|
|
191
|
-
]);
|
|
192
|
-
var TAG_PATTERN = "([a-z][a-z0-9]*)";
|
|
193
|
-
var CLASS_OR_ID_PATTERN = "[.#][\\w-]+";
|
|
194
|
-
var CLASS_ID_SHORTHAND = new RegExp(`^${TAG_PATTERN}(?:${CLASS_OR_ID_PATTERN})+`);
|
|
195
|
-
var ATTRIBUTE_SHORTHAND = new RegExp(`^${TAG_PATTERN}(?:${CLASS_OR_ID_PATTERN})*\\s+[\\w:-]+=`);
|
|
196
|
-
var TAG_SHORTHAND = new RegExp(`^${TAG_PATTERN}(?:>|\\s+\\S|$)`);
|
|
197
|
-
function htmlTagName(match) {
|
|
198
|
-
const tag = match?.[1];
|
|
199
|
-
return tag && HTML_TAGS.has(tag) ? tag : undefined;
|
|
200
|
-
}
|
|
201
|
-
function describePugLikeLine(line) {
|
|
202
|
-
if (/^\|(?:\s|$)/.test(line)) {
|
|
203
|
-
return "piped text";
|
|
204
|
-
}
|
|
205
|
-
if (htmlTagName(line.match(CLASS_ID_SHORTHAND))) {
|
|
206
|
-
return "class/id shorthand";
|
|
207
|
-
}
|
|
208
|
-
if (htmlTagName(line.match(ATTRIBUTE_SHORTHAND))) {
|
|
209
|
-
return "attribute shorthand";
|
|
210
|
-
}
|
|
211
|
-
if (htmlTagName(line.match(TAG_SHORTHAND))) {
|
|
212
|
-
return "tag shorthand";
|
|
213
|
-
}
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
function updateSkipBlock(lower, skipBlock) {
|
|
217
|
-
if (skipBlock) {
|
|
218
|
-
if (skipBlock === "comment" && lower.includes("-->")) {
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
if (skipBlock !== "comment" && lower.includes(`</${skipBlock}`)) {
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
return skipBlock;
|
|
225
|
-
}
|
|
226
|
-
if (lower.startsWith("<!--") && !lower.includes("-->")) {
|
|
227
|
-
return "comment";
|
|
228
|
-
}
|
|
229
|
-
if (lower.startsWith("<script") && !lower.includes("</script")) {
|
|
230
|
-
return "script";
|
|
231
|
-
}
|
|
232
|
-
if (lower.startsWith("<style") && !lower.includes("</style")) {
|
|
233
|
-
return "style";
|
|
234
|
-
}
|
|
235
|
-
return null;
|
|
236
|
-
}
|
|
237
|
-
function assertEtaHtmlSource(templateName, source) {
|
|
238
|
-
let skipBlock = null;
|
|
239
|
-
for (const rawLine of source.split(/\r?\n/)) {
|
|
240
|
-
const trimmed = rawLine.trim();
|
|
241
|
-
if (!trimmed) {
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
const lower = trimmed.toLowerCase();
|
|
245
|
-
const nextSkipBlock = updateSkipBlock(lower, skipBlock);
|
|
246
|
-
if (skipBlock || nextSkipBlock) {
|
|
247
|
-
skipBlock = nextSkipBlock;
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
if (trimmed.startsWith("<") || trimmed.startsWith("<%")) {
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
const kind = describePugLikeLine(trimmed);
|
|
254
|
-
if (kind) {
|
|
255
|
-
throw new Error(`Eta view "${templateName}" contains Pug-like markup (${kind}: ${trimmed}). Eta views must be HTML + <% %> tags, not Pug.`);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// ../../src/core/view/etaViewEngine.ts
|
|
261
|
-
var DEFAULT_VIEWS_DIRECTORY = join(process.cwd(), "resources/views");
|
|
262
|
-
var DEFAULT_LAYOUT = "layouts/app.eta";
|
|
263
|
-
|
|
264
|
-
class EtaViewEngine {
|
|
265
|
-
eta;
|
|
266
|
-
resolveLayoutData;
|
|
267
|
-
constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
|
|
268
|
-
this.eta = new Eta({
|
|
269
|
-
views: viewsDirectory,
|
|
270
|
-
autoTrim: false
|
|
271
|
-
});
|
|
272
|
-
this.resolveLayoutData = resolveLayoutData;
|
|
273
|
-
const readFile = this.eta.readFile?.bind(this.eta);
|
|
274
|
-
this.eta.readFile = (path) => {
|
|
275
|
-
const source = readFile ? readFile(path) : "";
|
|
276
|
-
assertEtaHtmlSource(relative(viewsDirectory, path) || path, source);
|
|
277
|
-
return source;
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
async render(name, data = {}, options = {}) {
|
|
281
|
-
const template = name.endsWith(".eta") ? name : `${name}.eta`;
|
|
282
|
-
const layoutData = this.resolveLayoutData ? await this.resolveLayoutData() : {};
|
|
283
|
-
const mergedData = { ...layoutData, ...data };
|
|
284
|
-
const body = await this.eta.renderAsync(template, mergedData);
|
|
285
|
-
const layout = options.layout ?? DEFAULT_LAYOUT;
|
|
286
|
-
if (layout === false) {
|
|
287
|
-
return body;
|
|
288
|
-
}
|
|
289
|
-
const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
|
|
290
|
-
return await this.eta.renderAsync(layoutTemplate, {
|
|
291
|
-
...mergedData,
|
|
292
|
-
body
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
86
|
// ../../src/core/view/htmlResponse.ts
|
|
297
87
|
function withCharset(contentType) {
|
|
298
88
|
return contentType.includes("charset=") ? contentType : `${contentType}; charset=utf-8`;
|
|
@@ -317,9 +107,6 @@ function redirectResponse(location, status = 302) {
|
|
|
317
107
|
}
|
|
318
108
|
});
|
|
319
109
|
}
|
|
320
|
-
function notFoundHtmlResponse(body = "Not Found") {
|
|
321
|
-
return htmlResponse(body, { status: 404 });
|
|
322
|
-
}
|
|
323
110
|
function textResponse(body, init = {}) {
|
|
324
111
|
return new Response(body, {
|
|
325
112
|
status: init.status ?? 200,
|
|
@@ -339,344 +126,82 @@ function xmlResponse(body, init = {}) {
|
|
|
339
126
|
function rssResponse(body, init = {}) {
|
|
340
127
|
return xmlResponse(body, { ...init, contentType: "application/rss+xml" });
|
|
341
128
|
}
|
|
342
|
-
// ../../src/core/view/webLayoutData.ts
|
|
343
|
-
import { currentAuthUser } from "@getstrata/core/auth/authContext";
|
|
344
|
-
import { currentRequestMeta as currentRequestMeta2 } from "@getstrata/core/http/requestMetaContext";
|
|
345
129
|
|
|
346
|
-
// ../../src/core/
|
|
347
|
-
var
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
if (cookies && typeof cookies.get === "function") {
|
|
398
|
-
const value = cookies.get(name);
|
|
399
|
-
if (value) {
|
|
400
|
-
return value;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
const header = request.headers.get("cookie");
|
|
404
|
-
if (!header) {
|
|
405
|
-
return null;
|
|
406
|
-
}
|
|
407
|
-
for (const part of header.split(";")) {
|
|
408
|
-
const idx = part.indexOf("=");
|
|
409
|
-
if (idx === -1)
|
|
410
|
-
continue;
|
|
411
|
-
const cookieName = part.slice(0, idx).trim();
|
|
412
|
-
if (cookieName !== name)
|
|
413
|
-
continue;
|
|
414
|
-
return decodeURIComponent(part.slice(idx + 1).trim());
|
|
415
|
-
}
|
|
416
|
-
return null;
|
|
417
|
-
}
|
|
418
|
-
function readBunRequestCookie(request, name) {
|
|
419
|
-
return request.cookies.get(name) ?? readRequestCookie(request, name);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// ../../src/core/runtime/asyncContextStore.ts
|
|
423
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
424
|
-
function createAsyncContextStore(key) {
|
|
425
|
-
const symbol = Symbol.for(key);
|
|
426
|
-
const globalRecord = globalThis;
|
|
427
|
-
const existing = globalRecord[symbol];
|
|
428
|
-
if (existing) {
|
|
429
|
-
return existing;
|
|
430
|
-
}
|
|
431
|
-
const store = new AsyncLocalStorage;
|
|
432
|
-
globalRecord[symbol] = store;
|
|
433
|
-
return store;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// ../../src/core/http/requestMetaContext.ts
|
|
437
|
-
var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
|
|
438
|
-
function runWithRequestMeta(meta, callback) {
|
|
439
|
-
return requestMetaContext.run(meta, callback);
|
|
440
|
-
}
|
|
441
|
-
function currentRequestMeta() {
|
|
442
|
-
return requestMetaContext.getStore() ?? {
|
|
443
|
-
ipAddress: null,
|
|
444
|
-
userAgent: null
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// ../../src/core/http/csrfToken.ts
|
|
449
|
-
var CSRF_COOKIE = "workhub_csrf";
|
|
450
|
-
var CSRF_TTL_MS = 60 * 60 * 1000;
|
|
451
|
-
function csrfCookieName() {
|
|
452
|
-
return process.env.CSRF_COOKIE_NAME?.trim() || CSRF_COOKIE;
|
|
453
|
-
}
|
|
454
|
-
function resolveCsrfSecret() {
|
|
455
|
-
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
|
|
456
|
-
}
|
|
457
|
-
function csrfVerifyOptions() {
|
|
458
|
-
return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
|
|
459
|
-
}
|
|
460
|
-
function tokensMatch(left, right) {
|
|
461
|
-
const leftBuffer = Buffer.from(left);
|
|
462
|
-
const rightBuffer = Buffer.from(right);
|
|
463
|
-
if (leftBuffer.length !== rightBuffer.length) {
|
|
464
|
-
return false;
|
|
465
|
-
}
|
|
466
|
-
return timingSafeEqual(leftBuffer, rightBuffer);
|
|
467
|
-
}
|
|
468
|
-
function createCsrfTokenCookie() {
|
|
469
|
-
const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
|
|
470
|
-
const secure = process.env.APP_ENV === "production" ? "; Secure" : "";
|
|
471
|
-
return {
|
|
472
|
-
token,
|
|
473
|
-
cookie: `${csrfCookieName()}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}${secure}`
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
function resolveCsrfToken(request) {
|
|
477
|
-
const cookieValue = readRequestCookie(request, csrfCookieName());
|
|
478
|
-
if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
|
|
479
|
-
return { token: cookieValue };
|
|
480
|
-
}
|
|
481
|
-
return createCsrfTokenCookie();
|
|
482
|
-
}
|
|
483
|
-
function readSubmittedCsrfToken(request) {
|
|
484
|
-
const headerToken = request.headers.get("x-csrf-token")?.trim();
|
|
485
|
-
if (headerToken) {
|
|
486
|
-
return headerToken;
|
|
487
|
-
}
|
|
488
|
-
return null;
|
|
489
|
-
}
|
|
490
|
-
async function readSubmittedCsrfTokenFromBody(request) {
|
|
491
|
-
const headerToken = readSubmittedCsrfToken(request);
|
|
492
|
-
if (headerToken) {
|
|
493
|
-
return headerToken;
|
|
494
|
-
}
|
|
495
|
-
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
496
|
-
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
497
|
-
const formData = await request.clone().formData();
|
|
498
|
-
const field = formData.get("_token");
|
|
499
|
-
if (typeof field === "string" && field.trim().length > 0) {
|
|
500
|
-
return field.trim();
|
|
501
|
-
}
|
|
502
|
-
const legacyField = formData.get("_csrf");
|
|
503
|
-
if (typeof legacyField === "string" && legacyField.trim().length > 0) {
|
|
504
|
-
return legacyField.trim();
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
return null;
|
|
508
|
-
}
|
|
509
|
-
function verifyCsrfToken(request, submittedToken) {
|
|
510
|
-
if (!submittedToken) {
|
|
511
|
-
return false;
|
|
512
|
-
}
|
|
513
|
-
const cookieValue = readRequestCookie(request, csrfCookieName());
|
|
514
|
-
if (!cookieValue) {
|
|
515
|
-
return false;
|
|
516
|
-
}
|
|
517
|
-
if (!tokensMatch(submittedToken, cookieValue)) {
|
|
518
|
-
return false;
|
|
519
|
-
}
|
|
520
|
-
return Bun.CSRF.verify(submittedToken, csrfVerifyOptions());
|
|
521
|
-
}
|
|
522
|
-
function resolveCsrfTokenForRequest(request) {
|
|
523
|
-
const metaToken = currentRequestMeta().csrfToken;
|
|
524
|
-
if (metaToken) {
|
|
525
|
-
return metaToken;
|
|
526
|
-
}
|
|
527
|
-
return resolveCsrfToken(request).token;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// ../../src/core/http/flashSession.ts
|
|
531
|
-
import { createHmac, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
532
|
-
var FLASH_COOKIE = "workhub_flash";
|
|
533
|
-
var FLASH_TTL_MS = 60 * 1000;
|
|
534
|
-
function resolveFlashSecret() {
|
|
535
|
-
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
|
|
536
|
-
}
|
|
537
|
-
function signFlashPayload(payload, issuedAt) {
|
|
538
|
-
const signature = createHmac("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
|
|
539
|
-
return `${payload}.${issuedAt}.${signature}`;
|
|
540
|
-
}
|
|
541
|
-
function readFlashCookie(request) {
|
|
542
|
-
const cookieHeader = request.headers.get("cookie");
|
|
543
|
-
if (!cookieHeader) {
|
|
544
|
-
return null;
|
|
545
|
-
}
|
|
546
|
-
for (const part of cookieHeader.split(";")) {
|
|
547
|
-
const [name, ...rest] = part.trim().split("=");
|
|
548
|
-
if (name === FLASH_COOKIE) {
|
|
549
|
-
return decodeURIComponent(rest.join("="));
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
return null;
|
|
553
|
-
}
|
|
554
|
-
function parseFlashCookie(cookieValue) {
|
|
555
|
-
const parts = cookieValue.split(".");
|
|
556
|
-
if (parts.length < 3) {
|
|
557
|
-
return null;
|
|
558
|
-
}
|
|
559
|
-
const signature = parts.pop();
|
|
560
|
-
const issuedAtRaw = parts.pop();
|
|
561
|
-
const payload = parts.join(".");
|
|
562
|
-
if (!signature || !issuedAtRaw || !payload) {
|
|
563
|
-
return null;
|
|
564
|
-
}
|
|
565
|
-
const issuedAt = Number.parseInt(issuedAtRaw, 10);
|
|
566
|
-
if (!Number.isFinite(issuedAt) || Date.now() - issuedAt > FLASH_TTL_MS) {
|
|
567
|
-
return null;
|
|
568
|
-
}
|
|
569
|
-
const expectedSignature = signFlashPayload(payload, issuedAt).split(".").pop();
|
|
570
|
-
if (!expectedSignature) {
|
|
571
|
-
return null;
|
|
572
|
-
}
|
|
573
|
-
const expectedBuffer = Buffer.from(expectedSignature);
|
|
574
|
-
const actualBuffer = Buffer.from(signature);
|
|
575
|
-
if (expectedBuffer.length !== actualBuffer.length) {
|
|
576
|
-
return null;
|
|
577
|
-
}
|
|
578
|
-
if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
|
|
579
|
-
return null;
|
|
130
|
+
// ../../src/core/view/webErrorView.ts
|
|
131
|
+
var configuredErrorView = {};
|
|
132
|
+
function configureWebErrorView(options) {
|
|
133
|
+
configuredErrorView = { ...options };
|
|
134
|
+
}
|
|
135
|
+
function escapeHtml(value) {
|
|
136
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
137
|
+
}
|
|
138
|
+
function renderKernelErrorChrome(input) {
|
|
139
|
+
const title = escapeHtml(input.title);
|
|
140
|
+
const message = escapeHtml(input.message);
|
|
141
|
+
const errorLines = Object.entries(input.errors ?? {}).flatMap(([field, messages]) => messages.map((item) => `${field}: ${item}`)).map((line) => `<li>${escapeHtml(line)}</li>`).join("");
|
|
142
|
+
const details = errorLines ? `<ul class="error-list">${errorLines}</ul>` : "";
|
|
143
|
+
const goBack = input.status === 422 ? `<p><a href="javascript:history.back()">Go back</a></p>` : "";
|
|
144
|
+
return `<!doctype html>
|
|
145
|
+
<html lang="en">
|
|
146
|
+
<head>
|
|
147
|
+
<meta charset="UTF-8" />
|
|
148
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
149
|
+
<title>${title}</title>
|
|
150
|
+
<link rel="stylesheet" href="/assets/app.css" />
|
|
151
|
+
</head>
|
|
152
|
+
<body>
|
|
153
|
+
<header class="site-header">
|
|
154
|
+
<a class="brand" href="/">Home</a>
|
|
155
|
+
</header>
|
|
156
|
+
<main class="site-main">
|
|
157
|
+
<section class="page-header">
|
|
158
|
+
<h1>${title}</h1>
|
|
159
|
+
<p>${message}</p>
|
|
160
|
+
${details}
|
|
161
|
+
${goBack}
|
|
162
|
+
</section>
|
|
163
|
+
</main>
|
|
164
|
+
</body>
|
|
165
|
+
</html>
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
168
|
+
function errorTemplateName(status) {
|
|
169
|
+
if (status === 404) {
|
|
170
|
+
return "errors/not-found";
|
|
171
|
+
}
|
|
172
|
+
if (status === 403) {
|
|
173
|
+
return "errors/forbidden";
|
|
174
|
+
}
|
|
175
|
+
return "errors/error";
|
|
176
|
+
}
|
|
177
|
+
async function renderWebErrorHtml(input) {
|
|
178
|
+
const render = configuredErrorView.render;
|
|
179
|
+
if (!render) {
|
|
180
|
+
return renderKernelErrorChrome(input);
|
|
580
181
|
}
|
|
581
182
|
try {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
}
|
|
586
|
-
if (parsed.level !== "success" && parsed.level !== "error" && parsed.level !== "info") {
|
|
587
|
-
return null;
|
|
588
|
-
}
|
|
589
|
-
return parsed;
|
|
183
|
+
return await render({
|
|
184
|
+
...input,
|
|
185
|
+
request: input.request ?? currentRequestMeta().request
|
|
186
|
+
});
|
|
590
187
|
} catch {
|
|
591
|
-
return
|
|
188
|
+
return renderKernelErrorChrome(input);
|
|
592
189
|
}
|
|
593
190
|
}
|
|
594
|
-
function
|
|
595
|
-
|
|
596
|
-
const issuedAt = Date.now();
|
|
597
|
-
const value = signFlashPayload(payload, issuedAt);
|
|
598
|
-
return `${FLASH_COOKIE}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=60`;
|
|
191
|
+
async function htmlErrorResponse(input) {
|
|
192
|
+
return htmlResponse(await renderWebErrorHtml(input), { status: input.status });
|
|
599
193
|
}
|
|
600
|
-
function
|
|
601
|
-
|
|
602
|
-
}
|
|
603
|
-
function pullFlash(request) {
|
|
604
|
-
const cookieValue = readFlashCookie(request);
|
|
605
|
-
if (!cookieValue) {
|
|
606
|
-
return null;
|
|
194
|
+
async function notFoundHtmlResponse(body) {
|
|
195
|
+
if (body !== undefined) {
|
|
196
|
+
return htmlResponse(body, { status: 404 });
|
|
607
197
|
}
|
|
608
|
-
return
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
headers.append("set-cookie", createFlashCookie(message));
|
|
613
|
-
return new Response(response.body, {
|
|
614
|
-
status: response.status,
|
|
615
|
-
statusText: response.statusText,
|
|
616
|
-
headers
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
function withFlashClear(response) {
|
|
620
|
-
const headers = new Headers(response.headers);
|
|
621
|
-
headers.append("set-cookie", clearFlashCookie());
|
|
622
|
-
return new Response(response.body, {
|
|
623
|
-
status: response.status,
|
|
624
|
-
statusText: response.statusText,
|
|
625
|
-
headers
|
|
198
|
+
return htmlErrorResponse({
|
|
199
|
+
status: 404,
|
|
200
|
+
title: "Not Found",
|
|
201
|
+
message: "The page you requested was not found."
|
|
626
202
|
});
|
|
627
203
|
}
|
|
628
204
|
|
|
629
|
-
// ../../src/core/view/webLayoutData.ts
|
|
630
|
-
var configuredLayoutOptions = {};
|
|
631
|
-
function configureWebLayoutData(options) {
|
|
632
|
-
configuredLayoutOptions = { ...options };
|
|
633
|
-
}
|
|
634
|
-
function layoutUserKey(options) {
|
|
635
|
-
return options.userKey ?? "authUser";
|
|
636
|
-
}
|
|
637
|
-
async function defaultLoadLayoutUser(container, _request) {
|
|
638
|
-
const authUser = currentAuthUser();
|
|
639
|
-
if (!authUser) {
|
|
640
|
-
return null;
|
|
641
|
-
}
|
|
642
|
-
const userId = Number(authUser.id);
|
|
643
|
-
if (!Number.isInteger(userId) || userId <= 0) {
|
|
644
|
-
return null;
|
|
645
|
-
}
|
|
646
|
-
const directory = resolveAuthUserDirectory(container);
|
|
647
|
-
if (!directory) {
|
|
648
|
-
return {
|
|
649
|
-
id: userId,
|
|
650
|
-
email: "",
|
|
651
|
-
role: authUser.role ?? "member"
|
|
652
|
-
};
|
|
653
|
-
}
|
|
654
|
-
try {
|
|
655
|
-
const user = await directory.findByIdOrThrow(userId);
|
|
656
|
-
return {
|
|
657
|
-
id: userId,
|
|
658
|
-
email: user.email ?? "",
|
|
659
|
-
role: authUser.role ?? user.role ?? "member"
|
|
660
|
-
};
|
|
661
|
-
} catch {
|
|
662
|
-
return null;
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
async function resolveWebLayoutData(container, request, options = {}) {
|
|
666
|
-
const resolved = { ...configuredLayoutOptions, ...options };
|
|
667
|
-
const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
|
|
668
|
-
const flash = request ? currentRequestMeta2().flash ?? pullFlash(request) : null;
|
|
669
|
-
const userKey = layoutUserKey(resolved);
|
|
670
|
-
const loadUser = resolved.loadUser ?? defaultLoadLayoutUser;
|
|
671
|
-
const user = await loadUser(container, request);
|
|
672
|
-
const extra = typeof resolved.extra === "function" ? await resolved.extra(user) : resolved.extra ?? {};
|
|
673
|
-
return {
|
|
674
|
-
[userKey]: user,
|
|
675
|
-
csrfToken,
|
|
676
|
-
flash,
|
|
677
|
-
...extra
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
205
|
// ../../src/core/http/contentNegotiation.ts
|
|
681
206
|
function requestPrefersJson(request) {
|
|
682
207
|
if (!request) {
|
|
@@ -700,6 +225,39 @@ function requestPrefersJson(request) {
|
|
|
700
225
|
return pathname.startsWith("/api/");
|
|
701
226
|
}
|
|
702
227
|
|
|
228
|
+
// ../../src/core/http/safeInternalPath.ts
|
|
229
|
+
function looksLikeExternalTarget(value) {
|
|
230
|
+
const trimmed = value.trim();
|
|
231
|
+
if (!trimmed.startsWith("/") || trimmed.startsWith("//") || trimmed.includes("\\")) {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
if (trimmed.includes("://") || trimmed.includes(":/") || trimmed.includes(":\\")) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const decoded = decodeURIComponent(trimmed);
|
|
239
|
+
if (decoded.startsWith("//") || decoded.includes("\\") || /https?:/i.test(decoded)) {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
function sanitizeInternalPath(raw, fallback = "/") {
|
|
248
|
+
if (looksLikeExternalTarget(raw)) {
|
|
249
|
+
return fallback;
|
|
250
|
+
}
|
|
251
|
+
return raw;
|
|
252
|
+
}
|
|
253
|
+
function safeInternalRedirectPath(request, fallback = "/") {
|
|
254
|
+
const url = new URL(request.url);
|
|
255
|
+
return sanitizeInternalPath(`${url.pathname}${url.search}`, fallback);
|
|
256
|
+
}
|
|
257
|
+
function loginRedirectLocation(request) {
|
|
258
|
+
return `/login?redirect=${encodeURIComponent(safeInternalRedirectPath(request))}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
703
261
|
// ../../src/core/http/webErrorResponse.ts
|
|
704
262
|
function normalizeFieldErrors(details) {
|
|
705
263
|
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
|
@@ -717,23 +275,37 @@ function normalizeFieldErrors(details) {
|
|
|
717
275
|
}
|
|
718
276
|
return errors;
|
|
719
277
|
}
|
|
720
|
-
function
|
|
278
|
+
function errorPageTitle(status, message) {
|
|
279
|
+
if (status === 404) {
|
|
280
|
+
return "Not Found";
|
|
281
|
+
}
|
|
282
|
+
if (status === 403) {
|
|
283
|
+
return "Forbidden";
|
|
284
|
+
}
|
|
285
|
+
if (status >= 500) {
|
|
286
|
+
return "Server Error";
|
|
287
|
+
}
|
|
288
|
+
return message;
|
|
289
|
+
}
|
|
290
|
+
function publicErrorMessage(status, message) {
|
|
291
|
+
if (status >= 500 && false) {}
|
|
292
|
+
return message;
|
|
293
|
+
}
|
|
294
|
+
async function webErrorResponse(error, request) {
|
|
721
295
|
if (!request || !isViewsEnabled() || requestPrefersJson(request)) {
|
|
722
296
|
return null;
|
|
723
297
|
}
|
|
724
298
|
const mappedError = error instanceof HttpError2 ? error : mapDatabaseError(error);
|
|
725
299
|
if (mappedError instanceof UnauthorizedError) {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
return htmlResponse(`<section class="page-header"><h1>${mappedError.message}</h1></section>`, {
|
|
736
|
-
status: mappedError.status
|
|
300
|
+
return Response.redirect(loginRedirectLocation(request), 302);
|
|
301
|
+
}
|
|
302
|
+
const errors = mappedError instanceof ValidationError ? normalizeFieldErrors(mappedError.details) : undefined;
|
|
303
|
+
return htmlErrorResponse({
|
|
304
|
+
status: mappedError.status,
|
|
305
|
+
title: errorPageTitle(mappedError.status, mappedError.message),
|
|
306
|
+
message: publicErrorMessage(mappedError.status, mappedError.message),
|
|
307
|
+
errors,
|
|
308
|
+
request
|
|
737
309
|
});
|
|
738
310
|
}
|
|
739
311
|
export {
|