tina4-nodejs 3.13.142 → 3.13.143
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/CLAUDE.md +28 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +379 -85
- package/packages/cli/src/commands/generate.ts +7 -2
- package/packages/cli/src/commands/init.ts +18 -8
- package/packages/core/dist/index.js +377 -78
- package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +27 -11
- package/packages/core/gallery/queue/src/routes/gallery/queue/get.ts +20 -7
- package/packages/core/gallery/templates/src/templates/gallery_page.twig +34 -15
- package/packages/core/src/csp.ts +138 -0
- package/packages/core/src/errorOverlay.ts +80 -37
- package/packages/core/src/index.ts +10 -0
- package/packages/core/src/middleware.ts +20 -6
- package/packages/core/src/response.ts +14 -3
- package/packages/core/src/server.ts +52 -15
- package/packages/core/src/session.ts +154 -7
- package/packages/core/src/sso.ts +1 -1
- package/packages/core/src/types.ts +8 -0
- package/packages/core/templates/errors/302.twig +1 -1
- package/packages/core/templates/errors/401.twig +2 -2
- package/packages/core/templates/errors/403.twig +1 -1
- package/packages/core/templates/errors/404.twig +1 -1
- package/packages/core/templates/errors/500.twig +1 -1
- package/packages/core/templates/errors/502.twig +2 -2
- package/packages/core/templates/errors/503.twig +2 -2
- package/packages/core/templates/errors/base.twig +4 -1
- package/packages/frond/dist/index.js +2 -0
- package/packages/frond/src/engine.ts +23 -0
- package/packages/orm/dist/index.js +378 -87
- package/packages/swagger/dist/index.js +4 -4
- package/packages/swagger/src/ui.ts +7 -4
- package/types/core/src/csp.d.ts +42 -0
- package/types/core/src/index.d.ts +1 -0
- package/types/core/src/middleware.d.ts +6 -0
- package/types/core/src/session.d.ts +62 -2
- package/types/core/src/types.d.ts +8 -0
- package/types/frond/src/engine.d.ts +7 -0
|
@@ -5406,6 +5406,73 @@ var init_trustedProxy = __esm({
|
|
|
5406
5406
|
}
|
|
5407
5407
|
});
|
|
5408
5408
|
|
|
5409
|
+
// ../core/src/csp.ts
|
|
5410
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
5411
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
5412
|
+
function generateNonce() {
|
|
5413
|
+
return randomBytes2(16).toString("base64");
|
|
5414
|
+
}
|
|
5415
|
+
function runWithCspNonce(nonce, fn) {
|
|
5416
|
+
return nonceStore.run({ nonce }, fn);
|
|
5417
|
+
}
|
|
5418
|
+
function setCurrentNonce(nonce) {
|
|
5419
|
+
const store = nonceStore.getStore();
|
|
5420
|
+
if (store) store.nonce = nonce;
|
|
5421
|
+
else nonceFallback = nonce;
|
|
5422
|
+
}
|
|
5423
|
+
function clearCurrentNonce() {
|
|
5424
|
+
setCurrentNonce(void 0);
|
|
5425
|
+
}
|
|
5426
|
+
function currentCspNonce() {
|
|
5427
|
+
const store = nonceStore.getStore();
|
|
5428
|
+
let value = store ? store.nonce : nonceFallback;
|
|
5429
|
+
if (!value) {
|
|
5430
|
+
value = generateNonce();
|
|
5431
|
+
if (store) store.nonce = value;
|
|
5432
|
+
else nonceFallback = value;
|
|
5433
|
+
}
|
|
5434
|
+
return value;
|
|
5435
|
+
}
|
|
5436
|
+
function cspNonce() {
|
|
5437
|
+
return currentCspNonce();
|
|
5438
|
+
}
|
|
5439
|
+
function injectNonceIntoCsp(csp, nonce) {
|
|
5440
|
+
const token = `'nonce-${nonce}'`;
|
|
5441
|
+
const directives = [];
|
|
5442
|
+
for (const rawPart of csp.split(";")) {
|
|
5443
|
+
const part = rawPart.trim();
|
|
5444
|
+
if (!part) continue;
|
|
5445
|
+
const spaceAt = part.search(/\s/);
|
|
5446
|
+
const name = (spaceAt === -1 ? part : part.slice(0, spaceAt)).toLowerCase();
|
|
5447
|
+
const value = spaceAt === -1 ? "" : part.slice(spaceAt + 1).trim();
|
|
5448
|
+
directives.push([name, value]);
|
|
5449
|
+
}
|
|
5450
|
+
const defaultEntry = directives.find(([name]) => name === "default-src");
|
|
5451
|
+
const defaultValue = defaultEntry && defaultEntry[1] ? defaultEntry[1] : "'self'";
|
|
5452
|
+
for (const directive of ["style-src", "script-src"]) {
|
|
5453
|
+
const existing = directives.find(([name]) => name === directive);
|
|
5454
|
+
if (existing) {
|
|
5455
|
+
if (!existing[1].split(/\s+/).includes(token)) {
|
|
5456
|
+
existing[1] = `${existing[1]} ${token}`.trim();
|
|
5457
|
+
}
|
|
5458
|
+
} else {
|
|
5459
|
+
directives.push([directive, `${defaultValue} ${token}`.trim()]);
|
|
5460
|
+
}
|
|
5461
|
+
}
|
|
5462
|
+
return directives.map(([name, value]) => value ? `${name} ${value}` : name).join("; ");
|
|
5463
|
+
}
|
|
5464
|
+
function resolveCspHeader(nonce) {
|
|
5465
|
+
const csp = process.env.TINA4_CSP || "default-src 'self'";
|
|
5466
|
+
return injectNonceIntoCsp(csp, nonce);
|
|
5467
|
+
}
|
|
5468
|
+
var nonceStore, nonceFallback;
|
|
5469
|
+
var init_csp = __esm({
|
|
5470
|
+
"../core/src/csp.ts"() {
|
|
5471
|
+
"use strict";
|
|
5472
|
+
nonceStore = new AsyncLocalStorage2();
|
|
5473
|
+
}
|
|
5474
|
+
});
|
|
5475
|
+
|
|
5409
5476
|
// ../frond/src/engine.ts
|
|
5410
5477
|
var engine_exports = {};
|
|
5411
5478
|
__export(engine_exports, {
|
|
@@ -5415,9 +5482,10 @@ __export(engine_exports, {
|
|
|
5415
5482
|
expressionFormCache: () => expressionFormCache,
|
|
5416
5483
|
filterChainCache: () => filterChainCache,
|
|
5417
5484
|
pathParseCache: () => pathParseCache,
|
|
5485
|
+
setCspNonceProvider: () => setCspNonceProvider,
|
|
5418
5486
|
setFormTokenSessionId: () => setFormTokenSessionId
|
|
5419
5487
|
});
|
|
5420
|
-
import { createHash as createHash3, createHmac as createHmac2, randomBytes as
|
|
5488
|
+
import { createHash as createHash3, createHmac as createHmac2, randomBytes as randomBytes3 } from "node:crypto";
|
|
5421
5489
|
import { readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync4, realpathSync } from "node:fs";
|
|
5422
5490
|
import { join as join5, resolve as resolve2, isAbsolute as isAbsolute2, sep } from "node:path";
|
|
5423
5491
|
function jsonText(value) {
|
|
@@ -6484,6 +6552,9 @@ function _b64url(data) {
|
|
|
6484
6552
|
function setFormTokenSessionId(sessionId) {
|
|
6485
6553
|
_formTokenSessionId = sessionId || "";
|
|
6486
6554
|
}
|
|
6555
|
+
function setCspNonceProvider(fn) {
|
|
6556
|
+
_cspNonceProvider = fn;
|
|
6557
|
+
}
|
|
6487
6558
|
function _buildFormTokenJwt(descriptor = "") {
|
|
6488
6559
|
const secret = process.env.TINA4_SECRET ?? "";
|
|
6489
6560
|
const secretBytes = Buffer.byteLength(secret, "utf8");
|
|
@@ -6495,7 +6566,7 @@ function _buildFormTokenJwt(descriptor = "") {
|
|
|
6495
6566
|
const ttlMinutes = parseInt(process.env.TINA4_TOKEN_LIMIT || "60", 10);
|
|
6496
6567
|
const header = { alg: "HS256", typ: "JWT" };
|
|
6497
6568
|
const now = Math.floor(Date.now() / 1e3);
|
|
6498
|
-
const payload = { type: "form", nonce:
|
|
6569
|
+
const payload = { type: "form", nonce: randomBytes3(8).toString("hex"), iat: now, exp: now + ttlMinutes * 60 };
|
|
6499
6570
|
if (descriptor) {
|
|
6500
6571
|
if (descriptor.includes("|")) {
|
|
6501
6572
|
const [ctx, ref] = descriptor.split("|", 2);
|
|
@@ -6522,7 +6593,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
6522
6593
|
function _generateFormTokenValue(descriptor = "") {
|
|
6523
6594
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
6524
6595
|
}
|
|
6525
|
-
var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FAST_FILTERS, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, EXTENDS_RE, EXTENDS_RE_GLOBAL, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, MEMO_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, EXPR_NOT_MATCHED, ARITHMETIC_OPERATIONS, EXPR_EVALUATORS, expressionFormCache, VarRef, JS_SAFE_RE, MEDIA_TYPE_RE, SANDBOX_EXPR_KEYWORDS, SANDBOX_IDENT_RE, SANDBOX_STRING_RE, UNSAFE_KEYS, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
6596
|
+
var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FAST_FILTERS, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, EXTENDS_RE, EXTENDS_RE_GLOBAL, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, MEMO_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, EXPR_NOT_MATCHED, ARITHMETIC_OPERATIONS, EXPR_EVALUATORS, expressionFormCache, VarRef, JS_SAFE_RE, MEDIA_TYPE_RE, SANDBOX_EXPR_KEYWORDS, SANDBOX_IDENT_RE, SANDBOX_STRING_RE, UNSAFE_KEYS, BUILTIN_FILTERS, _formTokenSessionId, _cspNonceProvider, Frond;
|
|
6526
6597
|
var init_engine = __esm({
|
|
6527
6598
|
"../frond/src/engine.ts"() {
|
|
6528
6599
|
"use strict";
|
|
@@ -6876,6 +6947,7 @@ var init_engine = __esm({
|
|
|
6876
6947
|
js_escape: (v) => new SafeString(jsEscape(v))
|
|
6877
6948
|
};
|
|
6878
6949
|
_formTokenSessionId = "";
|
|
6950
|
+
_cspNonceProvider = () => "";
|
|
6879
6951
|
Frond = class _Frond {
|
|
6880
6952
|
// ── Class-level registries ──────────────────────────────────
|
|
6881
6953
|
// Persist globals, filters, and tests across hot-reloads and at
|
|
@@ -7029,6 +7101,7 @@ var init_engine = __esm({
|
|
|
7029
7101
|
this.globals.formTokenValue = (descriptor) => _generateFormTokenValue(descriptor || "");
|
|
7030
7102
|
this.globals.form_token_value = (descriptor) => _generateFormTokenValue(descriptor || "");
|
|
7031
7103
|
this.globals.dump = (value) => renderDump(value);
|
|
7104
|
+
this.globals.csp_nonce = () => _cspNonceProvider();
|
|
7032
7105
|
for (const [k, v] of _Frond.classFilters) this.filters[k] = v;
|
|
7033
7106
|
for (const [k, v] of _Frond.classGlobals) this.globals[k] = v;
|
|
7034
7107
|
for (const [k, v] of _Frond.classTests) this.tests[k] = v;
|
|
@@ -8292,7 +8365,8 @@ async function getFrond() {
|
|
|
8292
8365
|
const dir = _defaultTemplatesDir ?? nodePath.resolve(process.cwd(), "src/templates");
|
|
8293
8366
|
let engine = _frondCache.get(dir);
|
|
8294
8367
|
if (!engine) {
|
|
8295
|
-
const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
8368
|
+
const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
8369
|
+
setCspNonceProvider2(currentCspNonce);
|
|
8296
8370
|
engine = new Frond2(dir);
|
|
8297
8371
|
_frondCache.set(dir, engine);
|
|
8298
8372
|
}
|
|
@@ -8303,7 +8377,8 @@ async function getFrameworkFrond() {
|
|
|
8303
8377
|
const frameworkDir = nodePath.resolve(nodePath.dirname(import.meta.url.replace("file://", "")), "..", "templates");
|
|
8304
8378
|
if (!_frameworkFrond && fs2.existsSync(frameworkDir)) {
|
|
8305
8379
|
try {
|
|
8306
|
-
const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
8380
|
+
const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
8381
|
+
setCspNonceProvider2(currentCspNonce);
|
|
8307
8382
|
_frameworkFrond = new Frond2(frameworkDir);
|
|
8308
8383
|
} catch {
|
|
8309
8384
|
return null;
|
|
@@ -8403,6 +8478,7 @@ function createResponse(res) {
|
|
|
8403
8478
|
return response;
|
|
8404
8479
|
};
|
|
8405
8480
|
response.raw = res;
|
|
8481
|
+
response.cspNonce = currentCspNonce();
|
|
8406
8482
|
response.json = function(data, status2) {
|
|
8407
8483
|
if (res.headersSent) return response;
|
|
8408
8484
|
if (status2 !== void 0) res.statusCode = status2;
|
|
@@ -8548,7 +8624,8 @@ function createResponse(res) {
|
|
|
8548
8624
|
};
|
|
8549
8625
|
response.render = async function(templateName, data, status2, templateDir) {
|
|
8550
8626
|
try {
|
|
8551
|
-
const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
8627
|
+
const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
8628
|
+
setCspNonceProvider2(currentCspNonce);
|
|
8552
8629
|
const dir = templateDir ?? _defaultTemplatesDir ?? nodePath.resolve(process.cwd(), "src/templates");
|
|
8553
8630
|
let engine = _frondCache.get(dir);
|
|
8554
8631
|
if (!engine) {
|
|
@@ -8666,6 +8743,7 @@ var HEADER_TOKEN, _frondCache, _defaultTemplatesDir, _globalFrond, _frameworkFro
|
|
|
8666
8743
|
var init_response = __esm({
|
|
8667
8744
|
"../core/src/response.ts"() {
|
|
8668
8745
|
"use strict";
|
|
8746
|
+
init_csp();
|
|
8669
8747
|
HEADER_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
8670
8748
|
_frondCache = /* @__PURE__ */ new Map();
|
|
8671
8749
|
_defaultTemplatesDir = null;
|
|
@@ -8877,6 +8955,7 @@ var init_middleware = __esm({
|
|
|
8877
8955
|
init_dotenv();
|
|
8878
8956
|
init_router();
|
|
8879
8957
|
init_trustedProxy();
|
|
8958
|
+
init_csp();
|
|
8880
8959
|
init_response();
|
|
8881
8960
|
MiddlewareChain = class {
|
|
8882
8961
|
middlewares = [];
|
|
@@ -9309,12 +9388,18 @@ var init_middleware = __esm({
|
|
|
9309
9388
|
* overrides, WITHOUT Strict-Transport-Security, which depends on the
|
|
9310
9389
|
* request's scheme. Also what a transport rejection carries (ADR-0068
|
|
9311
9390
|
* section 4), written before the scheme is known.
|
|
9391
|
+
*
|
|
9392
|
+
* Content-Security-Policy always carries a per-response nonce in style-src
|
|
9393
|
+
* AND script-src (ADR-0088), read from the request async-context (minted on
|
|
9394
|
+
* first access), so the framework's own inline content runs under the strict
|
|
9395
|
+
* default without 'unsafe-inline'. The nonce is random per response, so a
|
|
9396
|
+
* caller comparing this header must assert its STRUCTURE, never a byte string.
|
|
9312
9397
|
*/
|
|
9313
9398
|
static canonicalHeaders() {
|
|
9314
9399
|
return {
|
|
9315
9400
|
"X-Frame-Options": process.env.TINA4_FRAME_OPTIONS ?? "SAMEORIGIN",
|
|
9316
9401
|
"X-Content-Type-Options": "nosniff",
|
|
9317
|
-
"Content-Security-Policy":
|
|
9402
|
+
"Content-Security-Policy": resolveCspHeader(currentCspNonce()),
|
|
9318
9403
|
"Referrer-Policy": process.env.TINA4_REFERRER_POLICY ?? "strict-origin-when-cross-origin",
|
|
9319
9404
|
"X-XSS-Protection": "0",
|
|
9320
9405
|
"Permissions-Policy": process.env.TINA4_PERMISSIONS_POLICY ?? "camera=(), microphone=(), geolocation=()"
|
|
@@ -9356,7 +9441,7 @@ var init_middleware = __esm({
|
|
|
9356
9441
|
static warnCspDefaultOnce() {
|
|
9357
9442
|
if (_SecurityHeadersMiddleware.cspDefaultWarned) return;
|
|
9358
9443
|
_SecurityHeadersMiddleware.cspDefaultWarned = true;
|
|
9359
|
-
const message = `TINA4_CSP is not set, so Tina4 is serving the default Content-Security-Policy "default-src 'self'" on every response.
|
|
9444
|
+
const message = `TINA4_CSP is not set, so Tina4 is serving the default Content-Security-Policy "default-src 'self'" on every response. The framework injects a per-response nonce into style-src and script-src, so its own inline <style>/<script> (and your templates using the csp_nonce() Frond global) work under this policy. The default still blocks cross-origin fonts/scripts/CDNs, data: URIs, and cross-origin WebSocket/XHR (e.g. a separate API or LiveKit host). If your app uses any of these, set TINA4_CSP to a policy that allows them (see https://tina4.com); to silence this notice without changing behaviour, set TINA4_CSP="default-src 'self'".`;
|
|
9360
9445
|
try {
|
|
9361
9446
|
Log.warning(message);
|
|
9362
9447
|
} catch {
|
|
@@ -11749,7 +11834,7 @@ __export(session_exports, {
|
|
|
11749
11834
|
sessionCookieName: () => sessionCookieName,
|
|
11750
11835
|
sessionStrictMode: () => sessionStrictMode
|
|
11751
11836
|
});
|
|
11752
|
-
import { createHash as createHash5, randomBytes as
|
|
11837
|
+
import { createHash as createHash5, randomBytes as randomBytes4 } from "node:crypto";
|
|
11753
11838
|
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync as unlinkSync4, readdirSync as readdirSync3 } from "node:fs";
|
|
11754
11839
|
import { join as join9 } from "node:path";
|
|
11755
11840
|
function isValidSessionId(sessionId) {
|
|
@@ -11767,6 +11852,20 @@ function resolveBackend(name) {
|
|
|
11767
11852
|
}
|
|
11768
11853
|
return normalised;
|
|
11769
11854
|
}
|
|
11855
|
+
function fingerprint(value) {
|
|
11856
|
+
try {
|
|
11857
|
+
return JSON.stringify(value);
|
|
11858
|
+
} catch {
|
|
11859
|
+
return void 0;
|
|
11860
|
+
}
|
|
11861
|
+
}
|
|
11862
|
+
function fingerprints(data) {
|
|
11863
|
+
const prints = /* @__PURE__ */ new Map();
|
|
11864
|
+
for (const [key, value] of Object.entries(data)) {
|
|
11865
|
+
prints.set(key, fingerprint(value));
|
|
11866
|
+
}
|
|
11867
|
+
return prints;
|
|
11868
|
+
}
|
|
11770
11869
|
function isSecureScheme(forwardedProto, socketEncrypted) {
|
|
11771
11870
|
const forwarded = (forwardedProto ?? "").trim();
|
|
11772
11871
|
if (forwarded !== "") {
|
|
@@ -11964,6 +12063,29 @@ var init_session = __esm({
|
|
|
11964
12063
|
* Lets `start()` tell "no such session" from "the store is unreachable".
|
|
11965
12064
|
*/
|
|
11966
12065
|
lastReadFailed = false;
|
|
12066
|
+
/**
|
|
12067
|
+
* What this request last saw of the stored record, one fingerprint per key:
|
|
12068
|
+
* taken by start() and again by every save. save() compares the live data
|
|
12069
|
+
* against it to find the keys THIS request changed.
|
|
12070
|
+
*/
|
|
12071
|
+
loaded = /* @__PURE__ */ new Map();
|
|
12072
|
+
/**
|
|
12073
|
+
* True when this request is working on a record the store holds: start()
|
|
12074
|
+
* adopted one, or a save has written one since. A save never re-creates such
|
|
12075
|
+
* a record once another request has removed it.
|
|
12076
|
+
*/
|
|
12077
|
+
stored = false;
|
|
12078
|
+
/**
|
|
12079
|
+
* True when clear() ran since the last save: the save then replaces the
|
|
12080
|
+
* stored record instead of merging into it.
|
|
12081
|
+
*/
|
|
12082
|
+
cleared = false;
|
|
12083
|
+
/**
|
|
12084
|
+
* True once this request found its session ended by another request (the
|
|
12085
|
+
* record it loaded is gone). It stays ended for the rest of the request:
|
|
12086
|
+
* regenerate() mints nothing either.
|
|
12087
|
+
*/
|
|
12088
|
+
ended = false;
|
|
11967
12089
|
constructor(backend, config) {
|
|
11968
12090
|
const backendType = resolveBackend(
|
|
11969
12091
|
backend ?? config?.backend ?? process.env.TINA4_SESSION_BACKEND ?? "file"
|
|
@@ -12108,6 +12230,10 @@ var init_session = __esm({
|
|
|
12108
12230
|
const now2 = Math.floor(Date.now() / 1e3);
|
|
12109
12231
|
this.data = { _created: now2, _accessed: now2 };
|
|
12110
12232
|
this.dirty = false;
|
|
12233
|
+
this.stored = false;
|
|
12234
|
+
this.loaded = /* @__PURE__ */ new Map();
|
|
12235
|
+
this.cleared = false;
|
|
12236
|
+
this.ended = false;
|
|
12111
12237
|
return sessionId;
|
|
12112
12238
|
}
|
|
12113
12239
|
if (loaded) {
|
|
@@ -12117,14 +12243,22 @@ var init_session = __esm({
|
|
|
12117
12243
|
this.data._accessed = now2;
|
|
12118
12244
|
this.dirty = false;
|
|
12119
12245
|
this.safeWrite(this.sessionId, this.data, this.ttl);
|
|
12246
|
+
this.stored = true;
|
|
12247
|
+
this.loaded = fingerprints(this.data);
|
|
12248
|
+
this.cleared = false;
|
|
12249
|
+
this.ended = false;
|
|
12120
12250
|
return sessionId;
|
|
12121
12251
|
}
|
|
12122
12252
|
}
|
|
12123
|
-
this.sessionId =
|
|
12253
|
+
this.sessionId = randomBytes4(16).toString("hex");
|
|
12124
12254
|
const now = Math.floor(Date.now() / 1e3);
|
|
12125
12255
|
this.data = { _created: now, _accessed: now };
|
|
12126
12256
|
this.dirty = false;
|
|
12127
12257
|
this.safeWrite(this.sessionId, this.data, this.ttl);
|
|
12258
|
+
this.stored = false;
|
|
12259
|
+
this.loaded = /* @__PURE__ */ new Map();
|
|
12260
|
+
this.cleared = false;
|
|
12261
|
+
this.ended = false;
|
|
12128
12262
|
return this.sessionId;
|
|
12129
12263
|
}
|
|
12130
12264
|
/**
|
|
@@ -12165,9 +12299,7 @@ var init_session = __esm({
|
|
|
12165
12299
|
if (this.sessionId) {
|
|
12166
12300
|
this.safeDestroy(this.sessionId);
|
|
12167
12301
|
}
|
|
12168
|
-
this.
|
|
12169
|
-
this.data = null;
|
|
12170
|
-
this.dirty = false;
|
|
12302
|
+
this.forget();
|
|
12171
12303
|
}
|
|
12172
12304
|
/**
|
|
12173
12305
|
* Get all session data (excluding internal keys).
|
|
@@ -12190,6 +12322,7 @@ var init_session = __esm({
|
|
|
12190
12322
|
const now = Math.floor(Date.now() / 1e3);
|
|
12191
12323
|
this.data = { _created: this.data._created, _accessed: now };
|
|
12192
12324
|
this.dirty = true;
|
|
12325
|
+
this.cleared = true;
|
|
12193
12326
|
this.save();
|
|
12194
12327
|
}
|
|
12195
12328
|
/**
|
|
@@ -12206,17 +12339,36 @@ var init_session = __esm({
|
|
|
12206
12339
|
* session fixation — the pre-auth ID is destroyed and the data is carried
|
|
12207
12340
|
* onto a fresh, unguessable ID. A backend destroy/write failure is logged
|
|
12208
12341
|
* (never silent) but does not throw under the default policy.
|
|
12342
|
+
*
|
|
12343
|
+
* What is carried is the session as it is stored now with this request's
|
|
12344
|
+
* own changes applied, the same merge save() does. If another request ended
|
|
12345
|
+
* the session after this one loaded it (a logout, or a regenerate of its
|
|
12346
|
+
* own), it stays ended: nothing is carried, no id is minted and null is
|
|
12347
|
+
* returned, so no cookie goes out to replace the one that request sent.
|
|
12209
12348
|
*/
|
|
12210
12349
|
regenerate() {
|
|
12350
|
+
if (this.ended) return null;
|
|
12351
|
+
if (this.sessionId && this.stored && this.data) {
|
|
12352
|
+
const current = this.safeRead(this.sessionId);
|
|
12353
|
+
if (!this.lastReadFailed) {
|
|
12354
|
+
if (!current) {
|
|
12355
|
+
this.forget();
|
|
12356
|
+
this.ended = true;
|
|
12357
|
+
return null;
|
|
12358
|
+
}
|
|
12359
|
+
if (!this.cleared) this.data = this.merged(current);
|
|
12360
|
+
}
|
|
12361
|
+
}
|
|
12211
12362
|
const oldId = this.sessionId;
|
|
12212
12363
|
const oldData = this.data;
|
|
12213
12364
|
if (oldId) {
|
|
12214
12365
|
this.safeDestroy(oldId);
|
|
12215
12366
|
}
|
|
12216
|
-
this.sessionId =
|
|
12367
|
+
this.sessionId = randomBytes4(16).toString("hex");
|
|
12217
12368
|
this.data = oldData ?? { _created: Math.floor(Date.now() / 1e3), _accessed: Math.floor(Date.now() / 1e3) };
|
|
12218
12369
|
this.data._accessed = Math.floor(Date.now() / 1e3);
|
|
12219
12370
|
this.dirty = true;
|
|
12371
|
+
this.stored = false;
|
|
12220
12372
|
this.save();
|
|
12221
12373
|
return this.sessionId;
|
|
12222
12374
|
}
|
|
@@ -12292,17 +12444,84 @@ var init_session = __esm({
|
|
|
12292
12444
|
*
|
|
12293
12445
|
* Returns true on a successful persist, false if the backend was unreachable
|
|
12294
12446
|
* (logged). The dirty flag is cleared only on success so a later save()
|
|
12295
|
-
* retries once the backend recovers. A
|
|
12447
|
+
* retries once the backend recovers. A call with no session returns true.
|
|
12448
|
+
*
|
|
12449
|
+
* Other requests on the same session run while this one does, so the save
|
|
12450
|
+
* writes only THIS request's changes, onto the record as it is stored now.
|
|
12451
|
+
* Writing back the whole snapshot start() loaded undid whatever another
|
|
12452
|
+
* request did in between: a logout (destroy, clear, regenerate) or a
|
|
12453
|
+
* privilege change made with set(). A record removed since this request
|
|
12454
|
+
* loaded it is never re-created: the session has ended, for this request
|
|
12455
|
+
* too. A request that has not loaded a stored record (a new session, the new
|
|
12456
|
+
* id regenerate() mints) writes its data whole, as before.
|
|
12457
|
+
*
|
|
12458
|
+
* A session the store already holds is re-written even when this request
|
|
12459
|
+
* changed nothing: that write re-stamps the backend deadline to
|
|
12460
|
+
* now + TINA4_SESSION_TTL, so a session expires after that span of INACTIVITY
|
|
12461
|
+
* rather than that long after its last CHANGE (ADR-0087 — a session's expiry
|
|
12462
|
+
* slides on activity). The re-write is the re-read, merged record, so sliding
|
|
12463
|
+
* the deadline is concurrent-safe: it never clobbers another request's change,
|
|
12464
|
+
* never re-creates an ended record, and TINA4_SESSION_TTL=0 still re-stamps a
|
|
12465
|
+
* never-expires deadline (ADR-0027). A request with no stored session and no
|
|
12466
|
+
* change writes nothing — there is no deadline to move.
|
|
12296
12467
|
*/
|
|
12297
12468
|
save() {
|
|
12298
12469
|
if (!this.sessionId || !this.data) return true;
|
|
12299
|
-
if (!this.dirty) return true;
|
|
12300
|
-
|
|
12470
|
+
if (!this.dirty && !this.stored) return true;
|
|
12471
|
+
let record = this.data;
|
|
12472
|
+
if (this.stored) {
|
|
12473
|
+
const current = this.safeRead(this.sessionId);
|
|
12474
|
+
if (this.lastReadFailed) return false;
|
|
12475
|
+
if (!current) {
|
|
12476
|
+
this.forget();
|
|
12477
|
+
this.ended = true;
|
|
12478
|
+
return true;
|
|
12479
|
+
}
|
|
12480
|
+
if (!this.cleared) record = this.merged(current);
|
|
12481
|
+
}
|
|
12482
|
+
if (this.safeWrite(this.sessionId, record, this.ttl)) {
|
|
12301
12483
|
this.dirty = false;
|
|
12484
|
+
this.stored = true;
|
|
12485
|
+
this.cleared = false;
|
|
12486
|
+
this.loaded = fingerprints(this.data);
|
|
12302
12487
|
return true;
|
|
12303
12488
|
}
|
|
12304
12489
|
return false;
|
|
12305
12490
|
}
|
|
12491
|
+
/**
|
|
12492
|
+
* The stored record with this request's changes applied: every key whose
|
|
12493
|
+
* value differs from what this request loaded or last saved, and the
|
|
12494
|
+
* removal of every key it deleted. Keys it did not touch keep whatever is
|
|
12495
|
+
* stored now.
|
|
12496
|
+
*/
|
|
12497
|
+
merged(current) {
|
|
12498
|
+
const data = this.data;
|
|
12499
|
+
const record = { ...current };
|
|
12500
|
+
for (const [key, value] of Object.entries(data)) {
|
|
12501
|
+
const print = fingerprint(value);
|
|
12502
|
+
if (print === void 0 || !this.loaded.has(key) || this.loaded.get(key) !== print) {
|
|
12503
|
+
record[key] = value;
|
|
12504
|
+
}
|
|
12505
|
+
}
|
|
12506
|
+
for (const key of this.loaded.keys()) {
|
|
12507
|
+
if (!Object.prototype.hasOwnProperty.call(data, key)) {
|
|
12508
|
+
delete record[key];
|
|
12509
|
+
}
|
|
12510
|
+
}
|
|
12511
|
+
return record;
|
|
12512
|
+
}
|
|
12513
|
+
/**
|
|
12514
|
+
* End the session for this request without touching the store: no id and
|
|
12515
|
+
* no data, so nothing more is written and no cookie goes out for it.
|
|
12516
|
+
*/
|
|
12517
|
+
forget() {
|
|
12518
|
+
this.sessionId = null;
|
|
12519
|
+
this.data = null;
|
|
12520
|
+
this.dirty = false;
|
|
12521
|
+
this.stored = false;
|
|
12522
|
+
this.cleared = false;
|
|
12523
|
+
this.loaded = /* @__PURE__ */ new Map();
|
|
12524
|
+
}
|
|
12306
12525
|
};
|
|
12307
12526
|
}
|
|
12308
12527
|
});
|
|
@@ -12671,11 +12890,11 @@ function formatSourceBlock(filename, lineno) {
|
|
|
12671
12890
|
const lines = readSourceLines(filename, lineno);
|
|
12672
12891
|
if (lines.length === 0) return "";
|
|
12673
12892
|
const rows = lines.map(([num, text, isError]) => {
|
|
12674
|
-
const
|
|
12893
|
+
const rowClass = isError ? "eo-line eo-line-err" : "eo-line";
|
|
12675
12894
|
const marker = isError ? "▶" : " ";
|
|
12676
|
-
return `<div
|
|
12895
|
+
return `<div class="${rowClass}"><span class="eo-ln">${num}</span><span class="eo-marker">${marker}</span><span class="eo-code">${esc(text)}</span></div>`;
|
|
12677
12896
|
}).join("\n");
|
|
12678
|
-
return `<div
|
|
12897
|
+
return `<div class="eo-source">` + rows + `</div>`;
|
|
12679
12898
|
}
|
|
12680
12899
|
function formatFrame(frame, capturedAt = 0) {
|
|
12681
12900
|
const source = frame.file && frame.line > 0 ? formatSourceBlock(frame.file, frame.line) : "";
|
|
@@ -12687,23 +12906,23 @@ function formatFrame(frame, capturedAt = 0) {
|
|
|
12687
12906
|
if (mtime > capturedAt + 0.5) {
|
|
12688
12907
|
const d = new Date(mtime * 1e3);
|
|
12689
12908
|
const mtimeIso = `${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}:${String(d.getUTCSeconds()).padStart(2, "0")}`;
|
|
12690
|
-
staleBadge = ` <span
|
|
12909
|
+
staleBadge = ` <span class="eo-stale">FILE MODIFIED @ ${mtimeIso} UTC — source may not match what failed</span>`;
|
|
12691
12910
|
}
|
|
12692
12911
|
} catch {
|
|
12693
12912
|
}
|
|
12694
12913
|
}
|
|
12695
|
-
return `<div
|
|
12914
|
+
return `<div class="eo-frame"><div class="eo-frame-head"><span class="eo-file">${esc(frame.file)}</span><span class="eo-sep"> : </span><span class="eo-lineno">${frame.line}</span><span class="eo-sep"> in </span><span class="eo-fn">${esc(frame.func)}</span>` + staleBadge + `</div>` + source + `</div>`;
|
|
12696
12915
|
}
|
|
12697
12916
|
function collapsible(title, content, openByDefault = false) {
|
|
12698
12917
|
const open = openByDefault ? " open" : "";
|
|
12699
|
-
return `<details
|
|
12918
|
+
return `<details class="eo-details"${open}><summary class="eo-summary">${esc(title)}</summary><div class="eo-details-body">${content}</div></details>`;
|
|
12700
12919
|
}
|
|
12701
12920
|
function table(pairs) {
|
|
12702
|
-
if (pairs.length === 0) return `<span
|
|
12921
|
+
if (pairs.length === 0) return `<span class="eo-none">None</span>`;
|
|
12703
12922
|
const rows = pairs.map(
|
|
12704
|
-
([key, val]) => `<tr><td
|
|
12923
|
+
([key, val]) => `<tr><td class="eo-key">${esc(key)}</td><td class="eo-val">${esc(val)}</td></tr>`
|
|
12705
12924
|
).join("");
|
|
12706
|
-
return `<table
|
|
12925
|
+
return `<table class="eo-table">${rows}</table>`;
|
|
12707
12926
|
}
|
|
12708
12927
|
function renderErrorOverlay(error2, request) {
|
|
12709
12928
|
const capturedAt = Date.now() / 1e3;
|
|
@@ -12716,7 +12935,7 @@ function renderErrorOverlay(error2, request) {
|
|
|
12716
12935
|
}
|
|
12717
12936
|
const hidden = frames.length - Math.min(frames.length, MAX_FRAMES);
|
|
12718
12937
|
if (hidden > 0) {
|
|
12719
|
-
framesHtml += `<div
|
|
12938
|
+
framesHtml += `<div class="eo-hidden-frames">… ${hidden} more stack frames hidden (truncated at ${MAX_FRAMES})</div>`;
|
|
12720
12939
|
}
|
|
12721
12940
|
const requestPairs = [];
|
|
12722
12941
|
if (request != null) {
|
|
@@ -12753,37 +12972,72 @@ function renderErrorOverlay(error2, request) {
|
|
|
12753
12972
|
];
|
|
12754
12973
|
const envSection = collapsible("Environment", table(envPairs));
|
|
12755
12974
|
const stackSection = collapsible("Stack Trace", framesHtml, true);
|
|
12975
|
+
const nonce = currentCspNonce();
|
|
12756
12976
|
return `<!DOCTYPE html>
|
|
12757
12977
|
<html lang="en">
|
|
12758
12978
|
<head>
|
|
12759
12979
|
<meta charset="utf-8">
|
|
12760
12980
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
12761
12981
|
<title>Tina4 Error \u2014 ${esc(excType)}</title>
|
|
12762
|
-
<style>
|
|
12763
|
-
|
|
12764
|
-
body{background:${BG};color:${TEXT};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px;line-height:1.5;}
|
|
12982
|
+
<style nonce="${nonce}">
|
|
12983
|
+
${overlayStylesheet()}
|
|
12765
12984
|
</style>
|
|
12766
12985
|
</head>
|
|
12767
12986
|
<body>
|
|
12768
|
-
<div
|
|
12769
|
-
<div
|
|
12770
|
-
<div
|
|
12771
|
-
<span
|
|
12772
|
-
<span
|
|
12987
|
+
<div class="eo-wrap">
|
|
12988
|
+
<div class="eo-header">
|
|
12989
|
+
<div class="eo-badge-row">
|
|
12990
|
+
<span class="eo-badge">Error</span>
|
|
12991
|
+
<span class="eo-sub">Tina4 Debug Overlay</span>
|
|
12773
12992
|
</div>
|
|
12774
|
-
<h1
|
|
12775
|
-
<p
|
|
12993
|
+
<h1 class="eo-type">${esc(excType)}</h1>
|
|
12994
|
+
<p class="eo-msg">${esc(excMsg)}</p>
|
|
12776
12995
|
</div>
|
|
12777
12996
|
${stackSection}
|
|
12778
12997
|
${requestSection}
|
|
12779
12998
|
${envSection}
|
|
12780
|
-
<div
|
|
12999
|
+
<div class="eo-footer">
|
|
12781
13000
|
Tina4 Debug Overlay — This page is only shown in debug mode. Set TINA4_DEBUG=false in production.
|
|
12782
13001
|
</div>
|
|
12783
13002
|
</div>
|
|
12784
13003
|
</body>
|
|
12785
13004
|
</html>`;
|
|
12786
13005
|
}
|
|
13006
|
+
function overlayStylesheet() {
|
|
13007
|
+
return `
|
|
13008
|
+
*{margin:0;padding:0;box-sizing:border-box;}
|
|
13009
|
+
body{background:${BG};color:${TEXT};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px;line-height:1.5;}
|
|
13010
|
+
.eo-wrap{max-width:960px;margin:0 auto;}
|
|
13011
|
+
.eo-header{margin-bottom:24px;}
|
|
13012
|
+
.eo-badge-row{display:flex;align-items:center;gap:12px;margin-bottom:12px;}
|
|
13013
|
+
.eo-badge{background:${RED};color:${BG};padding:4px 12px;border-radius:4px;font-weight:700;font-size:13px;text-transform:uppercase;}
|
|
13014
|
+
.eo-sub{color:${SUBTEXT};font-size:14px;}
|
|
13015
|
+
.eo-type{color:${RED};font-size:28px;font-weight:700;margin-bottom:8px;}
|
|
13016
|
+
.eo-msg{color:${TEXT};font-size:18px;font-family:'SF Mono','Fira Code','Consolas',monospace;background:${SURFACE};padding:12px 16px;border-radius:6px;border-left:4px solid ${RED};}
|
|
13017
|
+
.eo-footer{margin-top:32px;padding-top:16px;border-top:1px solid ${OVERLAY};color:${SUBTEXT};font-size:12px;}
|
|
13018
|
+
.eo-source{background:${SURFACE};border-radius:6px;padding:12px;overflow-x:auto;font-family:'SF Mono','Fira Code','Consolas',monospace;font-size:13px;line-height:1.6;}
|
|
13019
|
+
.eo-line{display:flex;padding:1px 0;}
|
|
13020
|
+
.eo-line-err{background:${ERROR_LINE_BG};}
|
|
13021
|
+
.eo-ln{color:${YELLOW};min-width:3.5em;text-align:right;padding-right:1em;user-select:none;}
|
|
13022
|
+
.eo-marker{color:${RED};width:1.2em;user-select:none;}
|
|
13023
|
+
.eo-code{color:${TEXT};white-space:pre-wrap;tab-size:4;}
|
|
13024
|
+
.eo-frame{margin-bottom:16px;}
|
|
13025
|
+
.eo-frame-head{margin-bottom:4px;}
|
|
13026
|
+
.eo-file{color:${BLUE};}
|
|
13027
|
+
.eo-sep{color:${SUBTEXT};}
|
|
13028
|
+
.eo-lineno{color:${YELLOW};}
|
|
13029
|
+
.eo-fn{color:${GREEN};}
|
|
13030
|
+
.eo-stale{background:${PEACH};color:${BG};padding:1px 8px;border-radius:3px;font-size:11px;font-weight:700;margin-left:6px;}
|
|
13031
|
+
.eo-details{margin-top:16px;}
|
|
13032
|
+
.eo-summary{cursor:pointer;color:${LAVENDER};font-weight:600;font-size:15px;padding:8px 0;user-select:none;}
|
|
13033
|
+
.eo-details-body{padding:8px 0;}
|
|
13034
|
+
.eo-table{border-collapse:collapse;width:100%;}
|
|
13035
|
+
.eo-key{color:${PEACH};padding:4px 16px 4px 0;vertical-align:top;white-space:nowrap;}
|
|
13036
|
+
.eo-val{color:${TEXT};padding:4px 0;word-break:break-all;}
|
|
13037
|
+
.eo-none{color:${SUBTEXT};}
|
|
13038
|
+
.eo-hidden-frames{color:${SUBTEXT};padding:8px 0;font-size:13px;}
|
|
13039
|
+
`;
|
|
13040
|
+
}
|
|
12787
13041
|
function isDebugMode() {
|
|
12788
13042
|
return isTruthy(process.env.TINA4_DEBUG);
|
|
12789
13043
|
}
|
|
@@ -12792,6 +13046,7 @@ var init_errorOverlay = __esm({
|
|
|
12792
13046
|
"../core/src/errorOverlay.ts"() {
|
|
12793
13047
|
"use strict";
|
|
12794
13048
|
init_dotenv();
|
|
13049
|
+
init_csp();
|
|
12795
13050
|
MAX_FRAMES = 50;
|
|
12796
13051
|
SENSITIVE_KEY_RE = /password|passwd|secret|token|authorization|cookie|key/i;
|
|
12797
13052
|
REDACTED2 = "[redacted]";
|
|
@@ -14642,7 +14897,11 @@ export default async function (req: Tina4Request, res: Tina4Response) {
|
|
|
14642
14897
|
`{% extends "base.twig" %}
|
|
14643
14898
|
{% block title %}Login{% endblock %}
|
|
14644
14899
|
{% block content %}
|
|
14645
|
-
|
|
14900
|
+
{# No inline style attribute: a nonce lets this <style> block run under the
|
|
14901
|
+
strict default Content-Security-Policy. csp_nonce() is a built-in Frond global
|
|
14902
|
+
that returns this response's nonce (ADR-0088). #}
|
|
14903
|
+
<style nonce="{{ csp_nonce() }}">.auth-form{max-width:400px;margin:0 auto}</style>
|
|
14904
|
+
<div class="container mt-4 auth-form">
|
|
14646
14905
|
<h1>Login</h1>
|
|
14647
14906
|
<form method="post" action="/api/auth/login">
|
|
14648
14907
|
{{ form_token() }}
|
|
@@ -14666,7 +14925,8 @@ export default async function (req: Tina4Request, res: Tina4Response) {
|
|
|
14666
14925
|
`{% extends "base.twig" %}
|
|
14667
14926
|
{% block title %}Register{% endblock %}
|
|
14668
14927
|
{% block content %}
|
|
14669
|
-
<
|
|
14928
|
+
<style nonce="{{ csp_nonce() }}">.auth-form{max-width:400px;margin:0 auto}</style>
|
|
14929
|
+
<div class="container mt-4 auth-form">
|
|
14670
14930
|
<h1>Register</h1>
|
|
14671
14931
|
<form method="post" action="/api/auth/register">
|
|
14672
14932
|
{{ form_token() }}
|
|
@@ -15603,7 +15863,7 @@ import * as path3 from "node:path";
|
|
|
15603
15863
|
import * as os from "node:os";
|
|
15604
15864
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
15605
15865
|
import { createRequire as createRequire2 } from "node:module";
|
|
15606
|
-
import { randomBytes as
|
|
15866
|
+
import { randomBytes as randomBytes5 } from "node:crypto";
|
|
15607
15867
|
function reqSibling(pkg) {
|
|
15608
15868
|
return req(`../../${pkg}/src/index.ts`);
|
|
15609
15869
|
}
|
|
@@ -16925,7 +17185,7 @@ var init_mcp = __esm({
|
|
|
16925
17185
|
// ── Session lifecycle + protocol negotiation ──────────────────
|
|
16926
17186
|
/** Mint a new session id and remember it. Called on `initialize`. */
|
|
16927
17187
|
openSession() {
|
|
16928
|
-
const sid =
|
|
17188
|
+
const sid = randomBytes5(16).toString("hex");
|
|
16929
17189
|
this._sessions.set(sid, Date.now());
|
|
16930
17190
|
return sid;
|
|
16931
17191
|
}
|
|
@@ -24701,8 +24961,8 @@ var init_devAdmin = __esm({
|
|
|
24701
24961
|
* Duplicate errors (same message) increment count and update last_seen.
|
|
24702
24962
|
*/
|
|
24703
24963
|
static capture(errorType, message, traceback = "", file = "", line = 0) {
|
|
24704
|
-
const
|
|
24705
|
-
const existing = this.errors.find((e) => e.fingerprint ===
|
|
24964
|
+
const fingerprint2 = `${errorType}|${message}|${file}|${line}`;
|
|
24965
|
+
const existing = this.errors.find((e) => e.fingerprint === fingerprint2);
|
|
24706
24966
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
24707
24967
|
if (existing) {
|
|
24708
24968
|
existing.count = (existing.count || 1) + 1;
|
|
@@ -24715,7 +24975,7 @@ var init_devAdmin = __esm({
|
|
|
24715
24975
|
message,
|
|
24716
24976
|
stack: traceback || void 0,
|
|
24717
24977
|
resolved: false,
|
|
24718
|
-
...{ fingerprint, error_type: errorType, file, line, count: 1, first_seen: now, last_seen: now }
|
|
24978
|
+
...{ fingerprint: fingerprint2, error_type: errorType, file, line, count: 1, first_seen: now, last_seen: now }
|
|
24719
24979
|
});
|
|
24720
24980
|
if (this.errors.length > this.maxErrors) {
|
|
24721
24981
|
this.errors = this.errors.slice(-this.maxErrors);
|
|
@@ -27394,7 +27654,7 @@ function swaggerEnabled() {
|
|
|
27394
27654
|
}
|
|
27395
27655
|
function createSwaggerRoutes(getSpec) {
|
|
27396
27656
|
const serveUi = async (_req, res) => {
|
|
27397
|
-
res.html(SWAGGER_UI_HTML("/swagger/openapi.json"));
|
|
27657
|
+
res.html(SWAGGER_UI_HTML("/swagger/openapi.json", res.cspNonce));
|
|
27398
27658
|
};
|
|
27399
27659
|
return [
|
|
27400
27660
|
{
|
|
@@ -27438,13 +27698,13 @@ var SWAGGER_UI_HTML;
|
|
|
27438
27698
|
var init_ui = __esm({
|
|
27439
27699
|
"../swagger/src/ui.ts"() {
|
|
27440
27700
|
"use strict";
|
|
27441
|
-
SWAGGER_UI_HTML = (specUrl) => `<!DOCTYPE html>
|
|
27701
|
+
SWAGGER_UI_HTML = (specUrl, nonce) => `<!DOCTYPE html>
|
|
27442
27702
|
<html lang="en">
|
|
27443
27703
|
<head>
|
|
27444
27704
|
<meta charset="UTF-8">
|
|
27445
27705
|
<title>Tina4 API Documentation</title>
|
|
27446
27706
|
<link rel="stylesheet" href="${swaggerUiCdn()}/swagger-ui.css">
|
|
27447
|
-
<style>
|
|
27707
|
+
<style nonce="${nonce}">
|
|
27448
27708
|
body { margin: 0; background: #fafafa; }
|
|
27449
27709
|
.topbar { display: none !important; }
|
|
27450
27710
|
</style>
|
|
@@ -27452,7 +27712,7 @@ var init_ui = __esm({
|
|
|
27452
27712
|
<body>
|
|
27453
27713
|
<div id="swagger-ui"></div>
|
|
27454
27714
|
<script src="${swaggerUiCdn()}/swagger-ui-bundle.js"></script>
|
|
27455
|
-
<script>
|
|
27715
|
+
<script nonce="${nonce}">
|
|
27456
27716
|
SwaggerUIBundle({
|
|
27457
27717
|
url: "${specUrl}",
|
|
27458
27718
|
dom_id: '#swagger-ui',
|
|
@@ -27492,7 +27752,7 @@ __export(sso_exports, {
|
|
|
27492
27752
|
SsoError: () => SsoError,
|
|
27493
27753
|
liveSessionIdentity: () => liveSessionIdentity
|
|
27494
27754
|
});
|
|
27495
|
-
import { createHash as createHash9, randomBytes as
|
|
27755
|
+
import { createHash as createHash9, randomBytes as randomBytes6, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
|
|
27496
27756
|
function expiresAt(tokens) {
|
|
27497
27757
|
const lifetime = Number(tokens.expires_in ?? 0);
|
|
27498
27758
|
return lifetime > 0 ? Math.floor(Date.now() / 1e3) + lifetime : 0;
|
|
@@ -27620,9 +27880,9 @@ var init_sso = __esm({
|
|
|
27620
27880
|
async login(requestOrSession, returnTo = "/") {
|
|
27621
27881
|
const session = this.session(requestOrSession);
|
|
27622
27882
|
if (!session) throw new SsoError("SSO login requires a Tina4 Session");
|
|
27623
|
-
const state =
|
|
27624
|
-
const nonce =
|
|
27625
|
-
const verifier =
|
|
27883
|
+
const state = randomBytes6(32).toString("base64url");
|
|
27884
|
+
const nonce = randomBytes6(32).toString("base64url");
|
|
27885
|
+
const verifier = randomBytes6(64).toString("base64url");
|
|
27626
27886
|
const challenge = createHash9("sha256").update(verifier).digest("base64url");
|
|
27627
27887
|
session.set(_Sso.PENDING_KEY, { state, nonce, verifier, return_to: _Sso.safeReturn(returnTo), created_at: Math.floor(Date.now() / 1e3) });
|
|
27628
27888
|
const metadata = await this.discover();
|
|
@@ -27850,7 +28110,7 @@ var init_docsAutoDiscovery = __esm({
|
|
|
27850
28110
|
});
|
|
27851
28111
|
|
|
27852
28112
|
// ../core/src/server.ts
|
|
27853
|
-
import { randomBytes as
|
|
28113
|
+
import { randomBytes as randomBytes7 } from "node:crypto";
|
|
27854
28114
|
import { resolve as resolve15, dirname as dirname11, join as join25, relative as relative10 } from "node:path";
|
|
27855
28115
|
import { existsSync as existsSync23, readdirSync as readdirSync14, readFileSync as readFileSync21, statSync as statSync16 } from "node:fs";
|
|
27856
28116
|
import { isatty } from "node:tty";
|
|
@@ -28030,7 +28290,8 @@ function isDevMode() {
|
|
|
28030
28290
|
}
|
|
28031
28291
|
async function renderErrorPage(code, data, templatesDir) {
|
|
28032
28292
|
try {
|
|
28033
|
-
const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
28293
|
+
const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
28294
|
+
setCspNonceProvider2(currentCspNonce);
|
|
28034
28295
|
const templateFile = `errors/${code}.twig`;
|
|
28035
28296
|
const getCachedFrond = (dir) => {
|
|
28036
28297
|
let instance = frondCache.get(dir);
|
|
@@ -28146,6 +28407,7 @@ function resolveTemplate(pathname, templatesDir) {
|
|
|
28146
28407
|
}
|
|
28147
28408
|
function renderLandingPage(port = 7148) {
|
|
28148
28409
|
const version = TINA4_VERSION;
|
|
28410
|
+
const nonce = currentCspNonce();
|
|
28149
28411
|
const galleryItems = [
|
|
28150
28412
|
{ id: "rest-api", icon: "🚀", name: "REST API", desc: "A simple JSON API with GET and POST endpoints", accent: "accent-blue", tryUrl: "/api/gallery/hello" },
|
|
28151
28413
|
{ id: "orm", icon: "🗃", name: "ORM", desc: "Product model with CRUD endpoints", accent: "accent-green", tryUrl: "/api/gallery/products" },
|
|
@@ -28160,7 +28422,7 @@ function renderLandingPage(port = 7148) {
|
|
|
28160
28422
|
const isDeployed = deployed[item.id] === true;
|
|
28161
28423
|
const tryBtn = isDeployed ? `<a href="${item.tryUrl}" class="gbtn gbtn-try" target="_blank">Try It</a>` : "";
|
|
28162
28424
|
const viewBtn = isDeployed ? `<a href="${item.tryUrl}" class="gbtn gbtn-view" target="_blank">View</a>` : "";
|
|
28163
|
-
const deployBtn = isDeployed ? `<span class="gbtn gbtn-deployed">Deployed</span>` : `<button class="gbtn gbtn-deploy"
|
|
28425
|
+
const deployBtn = isDeployed ? `<span class="gbtn gbtn-deployed">Deployed</span>` : `<button class="gbtn gbtn-deploy" data-deploy="${item.id}">Deploy</button>`;
|
|
28164
28426
|
return `<div class="gallery-card">
|
|
28165
28427
|
<div class="accent ${item.accent}"></div>
|
|
28166
28428
|
<div class="icon">${item.icon}</div>
|
|
@@ -28175,7 +28437,7 @@ function renderLandingPage(port = 7148) {
|
|
|
28175
28437
|
<meta charset="utf-8">
|
|
28176
28438
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
28177
28439
|
<title>Tina4NodeJs</title>
|
|
28178
|
-
<style>
|
|
28440
|
+
<style nonce="${nonce}">
|
|
28179
28441
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
28180
28442
|
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0f172a;color:#e2e8f0;min-height:100vh;display:flex;flex-direction:column;align-items:center;position:relative}
|
|
28181
28443
|
.bg-watermark{position:fixed;bottom:-5%;right:-5%;width:45%;opacity:0.04;pointer-events:none;z-index:0}
|
|
@@ -28214,6 +28476,11 @@ h1{font-size:3rem;font-weight:700;margin-bottom:0.25rem;letter-spacing:-1px}
|
|
|
28214
28476
|
.gbtn-deployed{background:transparent;border:1px solid #22c55e;color:#22c55e;cursor:default;font-size:0.7rem}
|
|
28215
28477
|
@keyframes wiggle{0%{transform:rotate(0deg)}15%{transform:rotate(14deg)}30%{transform:rotate(-10deg)}45%{transform:rotate(8deg)}60%{transform:rotate(-4deg)}75%{transform:rotate(2deg)}100%{transform:rotate(0deg)}}
|
|
28216
28478
|
.star-wiggle{display:inline-block;transform-origin:center}
|
|
28479
|
+
.gallery-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1rem}
|
|
28480
|
+
.tok-comment{color:#64748b}
|
|
28481
|
+
.tok-kw{color:#c084fc}
|
|
28482
|
+
.tok-str{color:#4ade80}
|
|
28483
|
+
.tok-fn{color:#38bdf8}
|
|
28217
28484
|
</style>
|
|
28218
28485
|
</head>
|
|
28219
28486
|
<body>
|
|
@@ -28239,23 +28506,23 @@ h1{font-size:3rem;font-weight:700;margin-bottom:0.25rem;letter-spacing:-1px}
|
|
|
28239
28506
|
<div class="section">
|
|
28240
28507
|
<div class="card">
|
|
28241
28508
|
<h2>Getting Started</h2>
|
|
28242
|
-
<pre class="code-block"><code><span
|
|
28243
|
-
<span
|
|
28509
|
+
<pre class="code-block"><code><span class="tok-comment">// app.ts</span>
|
|
28510
|
+
<span class="tok-kw">import</span> { startServer, Router } <span class="tok-kw">from</span> <span class="tok-str">"tina4-nodejs"</span>;
|
|
28244
28511
|
|
|
28245
|
-
Router.get(<span
|
|
28246
|
-
<span
|
|
28512
|
+
Router.get(<span class="tok-str">"/hello"</span>, <span class="tok-kw">async</span> (<span class="tok-fn">req</span>, <span class="tok-fn">res</span>) => {
|
|
28513
|
+
<span class="tok-kw">return</span> res.json({ message: <span class="tok-str">"Hello World!"</span> });
|
|
28247
28514
|
});
|
|
28248
28515
|
|
|
28249
|
-
startServer({ port: 7148 }); <span
|
|
28516
|
+
startServer({ port: 7148 }); <span class="tok-comment">// starts on port 7148</span></code></pre>
|
|
28250
28517
|
</div>
|
|
28251
28518
|
</div>
|
|
28252
28519
|
<div class="gallery">
|
|
28253
28520
|
<h2 id="gallery">What You Can Build</h2>
|
|
28254
|
-
<div
|
|
28521
|
+
<div class="gallery-cards">
|
|
28255
28522
|
${galleryCards}
|
|
28256
28523
|
</div>
|
|
28257
28524
|
</div>
|
|
28258
|
-
<script>
|
|
28525
|
+
<script nonce="${nonce}">
|
|
28259
28526
|
function deployGallery(name) {
|
|
28260
28527
|
if (!confirm('Deploy the "' + name + '" gallery example into your project? This will copy files into src/.')) return;
|
|
28261
28528
|
fetch('/__dev/api/gallery/deploy', {
|
|
@@ -28290,6 +28557,12 @@ function deployGallery(name) {
|
|
|
28290
28557
|
}
|
|
28291
28558
|
setTimeout(doWiggle,3000);
|
|
28292
28559
|
})();
|
|
28560
|
+
// CSP-clean wiring: no inline onclick handlers \u2014 bind every Deploy button from
|
|
28561
|
+
// its data-deploy attribute (a nonce covers script elements, not event-handler
|
|
28562
|
+
// attributes).
|
|
28563
|
+
document.querySelectorAll('.gbtn-deploy[data-deploy]').forEach(function(btn){
|
|
28564
|
+
btn.addEventListener('click', function(){ deployGallery(btn.dataset.deploy); });
|
|
28565
|
+
});
|
|
28293
28566
|
</script>
|
|
28294
28567
|
</body>
|
|
28295
28568
|
</html>`;
|
|
@@ -28506,7 +28779,7 @@ async function renderDispatchError(err, req2, res, templatesDir) {
|
|
|
28506
28779
|
}
|
|
28507
28780
|
}
|
|
28508
28781
|
}
|
|
28509
|
-
const requestId = Log.getRequestId() ??
|
|
28782
|
+
const requestId = Log.getRequestId() ?? randomBytes7(4).toString("hex");
|
|
28510
28783
|
if (wantsJson(req2)) {
|
|
28511
28784
|
const body = negotiatedErrorBody(500, "Internal Server Error", requestId);
|
|
28512
28785
|
res.raw.writeHead(500, { "Content-Type": "application/json" });
|
|
@@ -28577,7 +28850,7 @@ function serveStaticAsset(ctx) {
|
|
|
28577
28850
|
return false;
|
|
28578
28851
|
}
|
|
28579
28852
|
async function serveNotFound(ctx) {
|
|
28580
|
-
const requestId = Log.getRequestId() ??
|
|
28853
|
+
const requestId = Log.getRequestId() ?? randomBytes7(4).toString("hex");
|
|
28581
28854
|
if (wantsJson(ctx.req)) {
|
|
28582
28855
|
const body = negotiatedErrorBody(404, "Not Found", requestId);
|
|
28583
28856
|
ctx.res.raw.writeHead(404, httpReason(404), { "Content-Type": "application/json" });
|
|
@@ -28600,7 +28873,8 @@ async function buildDispatchContext(router, base) {
|
|
|
28600
28873
|
const templatesDir = resolve15(root, "src/templates");
|
|
28601
28874
|
let frondEngine = null;
|
|
28602
28875
|
try {
|
|
28603
|
-
const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
28876
|
+
const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
28877
|
+
setCspNonceProvider2(currentCspNonce);
|
|
28604
28878
|
frondEngine = new Frond2(templatesDir);
|
|
28605
28879
|
} catch {
|
|
28606
28880
|
}
|
|
@@ -28710,9 +28984,13 @@ async function dispatchInner(ctx, rawReq, rawRes, requestId) {
|
|
|
28710
28984
|
}
|
|
28711
28985
|
}
|
|
28712
28986
|
async function runDispatch(ctx, rawReq, rawRes) {
|
|
28713
|
-
const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ??
|
|
28987
|
+
const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ?? randomBytes7(4).toString("hex");
|
|
28714
28988
|
if (!rawRes.headersSent) rawRes.setHeader("x-request-id", requestId);
|
|
28715
|
-
|
|
28989
|
+
const requestNonce = generateNonce();
|
|
28990
|
+
return Log.runWithRequestId(
|
|
28991
|
+
requestId,
|
|
28992
|
+
() => runWithCspNonce(requestNonce, () => dispatchInner(ctx, rawReq, rawRes, requestId))
|
|
28993
|
+
);
|
|
28716
28994
|
}
|
|
28717
28995
|
function loopbackBindHosts(host) {
|
|
28718
28996
|
const normalized = host.trim().replace(/^\[|\]$/g, "").toLowerCase();
|
|
@@ -28839,7 +29117,8 @@ async function startServer(config) {
|
|
|
28839
29117
|
let frondEngine = null;
|
|
28840
29118
|
setDefaultTemplatesDir(templatesDir);
|
|
28841
29119
|
try {
|
|
28842
|
-
const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
29120
|
+
const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
29121
|
+
setCspNonceProvider2(currentCspNonce);
|
|
28843
29122
|
frondEngine = new Frond2(templatesDir);
|
|
28844
29123
|
router.addRoute({
|
|
28845
29124
|
method: "GET",
|
|
@@ -28980,10 +29259,12 @@ async function startServer(config) {
|
|
|
28980
29259
|
}
|
|
28981
29260
|
});
|
|
28982
29261
|
aiServer.on("error", (err) => {
|
|
28983
|
-
if (err
|
|
29262
|
+
if (err?.code === "EADDRINUSE") {
|
|
28984
29263
|
Log.warning(`Test port ${testPort} in use \u2014 skipping`);
|
|
28985
|
-
|
|
29264
|
+
} else {
|
|
29265
|
+
Log.error(`Test port ${testPort} bind failed (${err?.code ?? err}) \u2014 skipping`);
|
|
28986
29266
|
}
|
|
29267
|
+
aiServer = null;
|
|
28987
29268
|
});
|
|
28988
29269
|
aiServer.listen(testPort, host);
|
|
28989
29270
|
}
|
|
@@ -29198,6 +29479,7 @@ var init_server = __esm({
|
|
|
29198
29479
|
init_static();
|
|
29199
29480
|
init_dotenv();
|
|
29200
29481
|
init_errorOverlay();
|
|
29482
|
+
init_csp();
|
|
29201
29483
|
init_health();
|
|
29202
29484
|
init_rateLimiter();
|
|
29203
29485
|
init_logger();
|
|
@@ -29978,7 +30260,7 @@ var init_mqttMessage = __esm({
|
|
|
29978
30260
|
// ../core/src/mqtt.ts
|
|
29979
30261
|
import net2 from "node:net";
|
|
29980
30262
|
import tls from "node:tls";
|
|
29981
|
-
import { randomBytes as
|
|
30263
|
+
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
29982
30264
|
import { existsSync as existsSync25, readFileSync as readFileSync22 } from "node:fs";
|
|
29983
30265
|
var MqttError, MqttTimeoutError, CONNECT, CONNACK, PUBLISH, PUBACK, SUBSCRIBE, SUBACK, PINGREQ, PINGRESP, DISCONNECT, PROTOCOL_LEVEL, DEFAULT_PORT, DEFAULT_TLS_PORT, DEFAULT_URL, DEFAULT_KEEPALIVE, SUBSCRIPTION_REFUSED, MAX_REMAINING_LENGTH, QOS2_REFUSED_MESSAGE, CONNACK_RETURN_CODES, Mqtt;
|
|
29984
30266
|
var init_mqtt = __esm({
|
|
@@ -30066,7 +30348,7 @@ var init_mqtt = __esm({
|
|
|
30066
30348
|
this.caFile = options.caFile ?? (Env.str("TINA4_MQTT_CA_FILE") || null);
|
|
30067
30349
|
this.tlsVerify = options.tlsVerify ?? Env.bool("TINA4_MQTT_TLS_VERIFY", true);
|
|
30068
30350
|
let cid = options.clientId ?? (Env.str("TINA4_MQTT_CLIENT_ID") || null);
|
|
30069
|
-
if (cid === null || cid === "") cid = "tina4-" +
|
|
30351
|
+
if (cid === null || cid === "") cid = "tina4-" + randomBytes8(8).toString("hex");
|
|
30070
30352
|
this.clientId = cid;
|
|
30071
30353
|
this.keepalive = options.keepalive ?? Env.int("TINA4_MQTT_KEEPALIVE", DEFAULT_KEEPALIVE);
|
|
30072
30354
|
this.cleanSession = options.cleanSession ?? true;
|
|
@@ -31180,7 +31462,7 @@ var init_ssrf = __esm({
|
|
|
31180
31462
|
import http from "node:http";
|
|
31181
31463
|
import https from "node:https";
|
|
31182
31464
|
import { URL as URL2 } from "node:url";
|
|
31183
|
-
import { randomBytes as
|
|
31465
|
+
import { randomBytes as randomBytes9 } from "node:crypto";
|
|
31184
31466
|
import { promises as fsp, createWriteStream } from "node:fs";
|
|
31185
31467
|
import { basename as basename6 } from "node:path";
|
|
31186
31468
|
import { pipeline } from "node:stream/promises";
|
|
@@ -31572,7 +31854,7 @@ var init_api = __esm({
|
|
|
31572
31854
|
return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
|
|
31573
31855
|
}
|
|
31574
31856
|
const partContentType = guessContentType(uploadName);
|
|
31575
|
-
const boundary = "----Tina4Boundary" +
|
|
31857
|
+
const boundary = "----Tina4Boundary" + randomBytes9(16).toString("hex");
|
|
31576
31858
|
const bodyBuffer = buildMultipartBody(boundary, fieldName, uploadName, content, partContentType, extraFields);
|
|
31577
31859
|
const contentType = `multipart/form-data; boundary=${boundary}`;
|
|
31578
31860
|
return this.execute("POST", this.buildUrl(path10), bodyBuffer, contentType, headers);
|
|
@@ -34536,7 +34818,7 @@ import {
|
|
|
34536
34818
|
createHmac as createHmac3,
|
|
34537
34819
|
createPrivateKey,
|
|
34538
34820
|
createSign as createSign2,
|
|
34539
|
-
randomBytes as
|
|
34821
|
+
randomBytes as randomBytes10
|
|
34540
34822
|
} from "node:crypto";
|
|
34541
34823
|
function encodeBase64Url(value) {
|
|
34542
34824
|
return Buffer.from(value).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
@@ -34616,7 +34898,7 @@ function encryptPayload(payload, subscription) {
|
|
|
34616
34898
|
]);
|
|
34617
34899
|
const prkKey = hmac(authSecret, sharedSecret);
|
|
34618
34900
|
const ikm = hkdfExpand(prkKey, keyInfo, 32);
|
|
34619
|
-
const salt =
|
|
34901
|
+
const salt = randomBytes10(16);
|
|
34620
34902
|
const contentPrk = hmac(salt, ikm);
|
|
34621
34903
|
const cek = hkdfExpand(contentPrk, Buffer.from("Content-Encoding: aes128gcm\0", "ascii"), 16);
|
|
34622
34904
|
const nonce = hkdfExpand(contentPrk, Buffer.from("Content-Encoding: nonce\0", "ascii"), 12);
|
|
@@ -37373,6 +37655,7 @@ __export(src_exports2, {
|
|
|
37373
37655
|
cacheStats: () => cacheStats,
|
|
37374
37656
|
checkPassword: () => checkPassword,
|
|
37375
37657
|
clearCache: () => clearCache,
|
|
37658
|
+
clearCurrentNonce: () => clearCurrentNonce,
|
|
37376
37659
|
computeAcceptKey: () => computeAcceptKey,
|
|
37377
37660
|
container: () => container,
|
|
37378
37661
|
cors: () => cors,
|
|
@@ -37384,6 +37667,8 @@ __export(src_exports2, {
|
|
|
37384
37667
|
createMessenger: () => createMessenger,
|
|
37385
37668
|
createRequest: () => createRequest,
|
|
37386
37669
|
createResponse: () => createResponse,
|
|
37670
|
+
cspNonce: () => cspNonce,
|
|
37671
|
+
currentCspNonce: () => currentCspNonce,
|
|
37387
37672
|
decodeRequest: () => decodeRequest,
|
|
37388
37673
|
defaultContext: () => defaultContext,
|
|
37389
37674
|
defaultRouter: () => defaultRouter,
|
|
@@ -37409,6 +37694,7 @@ __export(src_exports2, {
|
|
|
37409
37694
|
feedbackWhitelist: () => feedbackWhitelist,
|
|
37410
37695
|
fts5Supported: () => fts5Supported,
|
|
37411
37696
|
generateContext: () => generateContext,
|
|
37697
|
+
generateNonce: () => generateNonce,
|
|
37412
37698
|
generateVapidKeys: () => generateVapidKeys,
|
|
37413
37699
|
get: () => get,
|
|
37414
37700
|
getDefaultDevServer: () => getDefaultDevServer,
|
|
@@ -37431,6 +37717,7 @@ __export(src_exports2, {
|
|
|
37431
37717
|
httpReason: () => httpReason,
|
|
37432
37718
|
inContainer: () => inContainer,
|
|
37433
37719
|
injectFeedbackWidget: () => injectFeedbackWidget,
|
|
37720
|
+
injectNonceIntoCsp: () => injectNonceIntoCsp,
|
|
37434
37721
|
installAll: () => installAll,
|
|
37435
37722
|
installSelected: () => installSelected,
|
|
37436
37723
|
isBannerSuppressed: () => isBannerSuppressed,
|
|
@@ -37485,6 +37772,7 @@ __export(src_exports2, {
|
|
|
37485
37772
|
resetTrustedProxyCache: () => resetTrustedProxyCache,
|
|
37486
37773
|
resolveAlgorithm: () => resolveAlgorithm,
|
|
37487
37774
|
resolveClientIp: () => resolveClientIp,
|
|
37775
|
+
resolveCspHeader: () => resolveCspHeader,
|
|
37488
37776
|
resolveFrameworkVersion: () => resolveFrameworkVersion,
|
|
37489
37777
|
resolvePortAndHost: () => resolvePortAndHost,
|
|
37490
37778
|
resolveStringMiddleware: () => resolveStringMiddleware,
|
|
@@ -37492,12 +37780,14 @@ __export(src_exports2, {
|
|
|
37492
37780
|
responseCache: () => responseCache,
|
|
37493
37781
|
runAll: () => runAll,
|
|
37494
37782
|
runRouteMiddlewares: () => runRouteMiddlewares,
|
|
37783
|
+
runWithCspNonce: () => runWithCspNonce,
|
|
37495
37784
|
runtimeDir: () => runtimeDir,
|
|
37496
37785
|
saveUpload: () => saveUpload,
|
|
37497
37786
|
schemaFromParams: () => schemaFromParams,
|
|
37498
37787
|
selectablePids: () => selectablePids,
|
|
37499
37788
|
serveWebSocketRoute: () => serveWebSocketRoute,
|
|
37500
37789
|
sessionCookieName: () => sessionCookieName,
|
|
37790
|
+
setCurrentNonce: () => setCurrentNonce,
|
|
37501
37791
|
setDefaultTemplatesDir: () => setDefaultTemplatesDir,
|
|
37502
37792
|
setFrond: () => setFrond,
|
|
37503
37793
|
showMenu: () => showMenu,
|
|
@@ -37564,6 +37854,7 @@ var init_src2 = __esm({
|
|
|
37564
37854
|
init_wsdl();
|
|
37565
37855
|
init_htmlElement();
|
|
37566
37856
|
init_errorOverlay();
|
|
37857
|
+
init_csp();
|
|
37567
37858
|
init_ai();
|
|
37568
37859
|
init_sso();
|
|
37569
37860
|
init_push();
|
|
@@ -41730,7 +42021,7 @@ __export(database_exports, {
|
|
|
41730
42021
|
stripTrailingSemicolons: () => stripTrailingSemicolons,
|
|
41731
42022
|
wrapWithCache: () => wrapWithCache
|
|
41732
42023
|
});
|
|
41733
|
-
import { AsyncLocalStorage as
|
|
42024
|
+
import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
|
|
41734
42025
|
function stripTrailingSemicolons(sql) {
|
|
41735
42026
|
if (!sql) return sql;
|
|
41736
42027
|
let stripped = sql.replace(/\s+$/, "");
|
|
@@ -42144,9 +42435,9 @@ var init_database = __esm({
|
|
|
42144
42435
|
leases = /* @__PURE__ */ new Map();
|
|
42145
42436
|
discardedSlots = /* @__PURE__ */ new Set();
|
|
42146
42437
|
repairs = /* @__PURE__ */ new Map();
|
|
42147
|
-
leaseOwner = new
|
|
42148
|
-
operationStore = new
|
|
42149
|
-
txStore = new
|
|
42438
|
+
leaseOwner = new AsyncLocalStorage3();
|
|
42439
|
+
operationStore = new AsyncLocalStorage3();
|
|
42440
|
+
txStore = new AsyncLocalStorage3();
|
|
42150
42441
|
/**
|
|
42151
42442
|
* Create a Database wrapping an existing adapter.
|
|
42152
42443
|
* For creating a Database from a URL, use the async static factories:
|
|
@@ -46897,7 +47188,7 @@ var init_seeder = __esm({
|
|
|
46897
47188
|
|
|
46898
47189
|
// src/docstore.ts
|
|
46899
47190
|
import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
|
|
46900
|
-
import { randomBytes as
|
|
47191
|
+
import { randomBytes as randomBytes11 } from "node:crypto";
|
|
46901
47192
|
import { mkdirSync as mkdirSync20 } from "node:fs";
|
|
46902
47193
|
import { dirname as dirname14, isAbsolute as isAbsolute6, join as join32 } from "node:path";
|
|
46903
47194
|
function iso(d) {
|
|
@@ -47254,8 +47545,8 @@ var init_docstore = __esm({
|
|
|
47254
47545
|
OID_RE = /^[0-9a-fA-F]{24}$/;
|
|
47255
47546
|
ISO_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/;
|
|
47256
47547
|
ObjectId = class _ObjectId {
|
|
47257
|
-
static _counter =
|
|
47258
|
-
static _process =
|
|
47548
|
+
static _counter = randomBytes11(3).readUIntBE(0, 3);
|
|
47549
|
+
static _process = randomBytes11(5);
|
|
47259
47550
|
_bytes;
|
|
47260
47551
|
constructor(oid) {
|
|
47261
47552
|
if (oid === void 0 || oid === null) {
|
|
@@ -48452,7 +48743,7 @@ var init_attachment = __esm({
|
|
|
48452
48743
|
});
|
|
48453
48744
|
|
|
48454
48745
|
// src/realtime/storage.ts
|
|
48455
|
-
import { randomBytes as
|
|
48746
|
+
import { randomBytes as randomBytes12 } from "node:crypto";
|
|
48456
48747
|
import { mkdirSync as mkdirSync21, readFileSync as readFileSync26, writeFileSync as writeFileSync18, unlinkSync as unlinkSync8, statSync as statSync19 } from "node:fs";
|
|
48457
48748
|
import { resolve as resolve20, sep as sep8 } from "node:path";
|
|
48458
48749
|
import { createRequire as createRequire8 } from "node:module";
|
|
@@ -48472,7 +48763,7 @@ function storageKey(filename = "") {
|
|
|
48472
48763
|
const clean = raw.replace(UNSAFE, "").slice(0, 12);
|
|
48473
48764
|
if (clean) ext = `.${clean}`;
|
|
48474
48765
|
}
|
|
48475
|
-
return `${
|
|
48766
|
+
return `${randomBytes12(16).toString("hex")}${ext}`;
|
|
48476
48767
|
}
|
|
48477
48768
|
function selectStorage(storage) {
|
|
48478
48769
|
if (storage) return storage;
|