tina4-nodejs 3.13.142 → 3.13.144
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
package/packages/cli/dist/bin.js
CHANGED
|
@@ -17431,6 +17431,73 @@ var init_trustedProxy = __esm({
|
|
|
17431
17431
|
}
|
|
17432
17432
|
});
|
|
17433
17433
|
|
|
17434
|
+
// ../core/src/csp.ts
|
|
17435
|
+
import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
|
|
17436
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
17437
|
+
function generateNonce() {
|
|
17438
|
+
return randomBytes4(16).toString("base64");
|
|
17439
|
+
}
|
|
17440
|
+
function runWithCspNonce(nonce, fn) {
|
|
17441
|
+
return nonceStore.run({ nonce }, fn);
|
|
17442
|
+
}
|
|
17443
|
+
function setCurrentNonce(nonce) {
|
|
17444
|
+
const store = nonceStore.getStore();
|
|
17445
|
+
if (store) store.nonce = nonce;
|
|
17446
|
+
else nonceFallback = nonce;
|
|
17447
|
+
}
|
|
17448
|
+
function clearCurrentNonce() {
|
|
17449
|
+
setCurrentNonce(void 0);
|
|
17450
|
+
}
|
|
17451
|
+
function currentCspNonce() {
|
|
17452
|
+
const store = nonceStore.getStore();
|
|
17453
|
+
let value = store ? store.nonce : nonceFallback;
|
|
17454
|
+
if (!value) {
|
|
17455
|
+
value = generateNonce();
|
|
17456
|
+
if (store) store.nonce = value;
|
|
17457
|
+
else nonceFallback = value;
|
|
17458
|
+
}
|
|
17459
|
+
return value;
|
|
17460
|
+
}
|
|
17461
|
+
function cspNonce() {
|
|
17462
|
+
return currentCspNonce();
|
|
17463
|
+
}
|
|
17464
|
+
function injectNonceIntoCsp(csp, nonce) {
|
|
17465
|
+
const token = `'nonce-${nonce}'`;
|
|
17466
|
+
const directives = [];
|
|
17467
|
+
for (const rawPart of csp.split(";")) {
|
|
17468
|
+
const part = rawPart.trim();
|
|
17469
|
+
if (!part) continue;
|
|
17470
|
+
const spaceAt = part.search(/\s/);
|
|
17471
|
+
const name = (spaceAt === -1 ? part : part.slice(0, spaceAt)).toLowerCase();
|
|
17472
|
+
const value = spaceAt === -1 ? "" : part.slice(spaceAt + 1).trim();
|
|
17473
|
+
directives.push([name, value]);
|
|
17474
|
+
}
|
|
17475
|
+
const defaultEntry = directives.find(([name]) => name === "default-src");
|
|
17476
|
+
const defaultValue = defaultEntry && defaultEntry[1] ? defaultEntry[1] : "'self'";
|
|
17477
|
+
for (const directive of ["style-src", "script-src"]) {
|
|
17478
|
+
const existing = directives.find(([name]) => name === directive);
|
|
17479
|
+
if (existing) {
|
|
17480
|
+
if (!existing[1].split(/\s+/).includes(token)) {
|
|
17481
|
+
existing[1] = `${existing[1]} ${token}`.trim();
|
|
17482
|
+
}
|
|
17483
|
+
} else {
|
|
17484
|
+
directives.push([directive, `${defaultValue} ${token}`.trim()]);
|
|
17485
|
+
}
|
|
17486
|
+
}
|
|
17487
|
+
return directives.map(([name, value]) => value ? `${name} ${value}` : name).join("; ");
|
|
17488
|
+
}
|
|
17489
|
+
function resolveCspHeader(nonce) {
|
|
17490
|
+
const csp = process.env.TINA4_CSP || "default-src 'self'";
|
|
17491
|
+
return injectNonceIntoCsp(csp, nonce);
|
|
17492
|
+
}
|
|
17493
|
+
var nonceStore, nonceFallback;
|
|
17494
|
+
var init_csp = __esm({
|
|
17495
|
+
"../core/src/csp.ts"() {
|
|
17496
|
+
"use strict";
|
|
17497
|
+
nonceStore = new AsyncLocalStorage3();
|
|
17498
|
+
}
|
|
17499
|
+
});
|
|
17500
|
+
|
|
17434
17501
|
// ../frond/src/engine.ts
|
|
17435
17502
|
var engine_exports = {};
|
|
17436
17503
|
__export(engine_exports, {
|
|
@@ -17440,9 +17507,10 @@ __export(engine_exports, {
|
|
|
17440
17507
|
expressionFormCache: () => expressionFormCache,
|
|
17441
17508
|
filterChainCache: () => filterChainCache,
|
|
17442
17509
|
pathParseCache: () => pathParseCache,
|
|
17510
|
+
setCspNonceProvider: () => setCspNonceProvider,
|
|
17443
17511
|
setFormTokenSessionId: () => setFormTokenSessionId
|
|
17444
17512
|
});
|
|
17445
|
-
import { createHash as createHash3, createHmac as createHmac3, randomBytes as
|
|
17513
|
+
import { createHash as createHash3, createHmac as createHmac3, randomBytes as randomBytes5 } from "node:crypto";
|
|
17446
17514
|
import { readFileSync as readFileSync6, existsSync as existsSync8, statSync as statSync6, realpathSync } from "node:fs";
|
|
17447
17515
|
import { join as join11, resolve as resolve7, isAbsolute as isAbsolute4, sep as sep3 } from "node:path";
|
|
17448
17516
|
function jsonText(value) {
|
|
@@ -18509,6 +18577,9 @@ function _b64url(data) {
|
|
|
18509
18577
|
function setFormTokenSessionId(sessionId) {
|
|
18510
18578
|
_formTokenSessionId = sessionId || "";
|
|
18511
18579
|
}
|
|
18580
|
+
function setCspNonceProvider(fn) {
|
|
18581
|
+
_cspNonceProvider = fn;
|
|
18582
|
+
}
|
|
18512
18583
|
function _buildFormTokenJwt(descriptor = "") {
|
|
18513
18584
|
const secret = process.env.TINA4_SECRET ?? "";
|
|
18514
18585
|
const secretBytes = Buffer.byteLength(secret, "utf8");
|
|
@@ -18520,7 +18591,7 @@ function _buildFormTokenJwt(descriptor = "") {
|
|
|
18520
18591
|
const ttlMinutes = parseInt(process.env.TINA4_TOKEN_LIMIT || "60", 10);
|
|
18521
18592
|
const header = { alg: "HS256", typ: "JWT" };
|
|
18522
18593
|
const now = Math.floor(Date.now() / 1e3);
|
|
18523
|
-
const payload = { type: "form", nonce:
|
|
18594
|
+
const payload = { type: "form", nonce: randomBytes5(8).toString("hex"), iat: now, exp: now + ttlMinutes * 60 };
|
|
18524
18595
|
if (descriptor) {
|
|
18525
18596
|
if (descriptor.includes("|")) {
|
|
18526
18597
|
const [ctx, ref] = descriptor.split("|", 2);
|
|
@@ -18547,7 +18618,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
18547
18618
|
function _generateFormTokenValue(descriptor = "") {
|
|
18548
18619
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
18549
18620
|
}
|
|
18550
|
-
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;
|
|
18621
|
+
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;
|
|
18551
18622
|
var init_engine = __esm({
|
|
18552
18623
|
"../frond/src/engine.ts"() {
|
|
18553
18624
|
"use strict";
|
|
@@ -18901,6 +18972,7 @@ var init_engine = __esm({
|
|
|
18901
18972
|
js_escape: (v) => new SafeString(jsEscape(v))
|
|
18902
18973
|
};
|
|
18903
18974
|
_formTokenSessionId = "";
|
|
18975
|
+
_cspNonceProvider = () => "";
|
|
18904
18976
|
Frond = class _Frond {
|
|
18905
18977
|
// ── Class-level registries ──────────────────────────────────
|
|
18906
18978
|
// Persist globals, filters, and tests across hot-reloads and at
|
|
@@ -19054,6 +19126,7 @@ var init_engine = __esm({
|
|
|
19054
19126
|
this.globals.formTokenValue = (descriptor) => _generateFormTokenValue(descriptor || "");
|
|
19055
19127
|
this.globals.form_token_value = (descriptor) => _generateFormTokenValue(descriptor || "");
|
|
19056
19128
|
this.globals.dump = (value) => renderDump(value);
|
|
19129
|
+
this.globals.csp_nonce = () => _cspNonceProvider();
|
|
19057
19130
|
for (const [k, v] of _Frond.classFilters) this.filters[k] = v;
|
|
19058
19131
|
for (const [k, v] of _Frond.classGlobals) this.globals[k] = v;
|
|
19059
19132
|
for (const [k, v] of _Frond.classTests) this.tests[k] = v;
|
|
@@ -20317,7 +20390,8 @@ async function getFrond() {
|
|
|
20317
20390
|
const dir = _defaultTemplatesDir ?? nodePath.resolve(process.cwd(), "src/templates");
|
|
20318
20391
|
let engine = _frondCache.get(dir);
|
|
20319
20392
|
if (!engine) {
|
|
20320
|
-
const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
20393
|
+
const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
20394
|
+
setCspNonceProvider2(currentCspNonce);
|
|
20321
20395
|
engine = new Frond2(dir);
|
|
20322
20396
|
_frondCache.set(dir, engine);
|
|
20323
20397
|
}
|
|
@@ -20328,7 +20402,8 @@ async function getFrameworkFrond() {
|
|
|
20328
20402
|
const frameworkDir = nodePath.resolve(nodePath.dirname(import.meta.url.replace("file://", "")), "..", "templates");
|
|
20329
20403
|
if (!_frameworkFrond && fs2.existsSync(frameworkDir)) {
|
|
20330
20404
|
try {
|
|
20331
|
-
const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
20405
|
+
const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
20406
|
+
setCspNonceProvider2(currentCspNonce);
|
|
20332
20407
|
_frameworkFrond = new Frond2(frameworkDir);
|
|
20333
20408
|
} catch {
|
|
20334
20409
|
return null;
|
|
@@ -20428,6 +20503,7 @@ function createResponse(res) {
|
|
|
20428
20503
|
return response;
|
|
20429
20504
|
};
|
|
20430
20505
|
response.raw = res;
|
|
20506
|
+
response.cspNonce = currentCspNonce();
|
|
20431
20507
|
response.json = function(data, status2) {
|
|
20432
20508
|
if (res.headersSent) return response;
|
|
20433
20509
|
if (status2 !== void 0) res.statusCode = status2;
|
|
@@ -20573,7 +20649,8 @@ function createResponse(res) {
|
|
|
20573
20649
|
};
|
|
20574
20650
|
response.render = async function(templateName, data, status2, templateDir) {
|
|
20575
20651
|
try {
|
|
20576
|
-
const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
20652
|
+
const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
20653
|
+
setCspNonceProvider2(currentCspNonce);
|
|
20577
20654
|
const dir = templateDir ?? _defaultTemplatesDir ?? nodePath.resolve(process.cwd(), "src/templates");
|
|
20578
20655
|
let engine = _frondCache.get(dir);
|
|
20579
20656
|
if (!engine) {
|
|
@@ -20691,6 +20768,7 @@ var HEADER_TOKEN, _frondCache, _defaultTemplatesDir, _globalFrond, _frameworkFro
|
|
|
20691
20768
|
var init_response = __esm({
|
|
20692
20769
|
"../core/src/response.ts"() {
|
|
20693
20770
|
"use strict";
|
|
20771
|
+
init_csp();
|
|
20694
20772
|
HEADER_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
20695
20773
|
_frondCache = /* @__PURE__ */ new Map();
|
|
20696
20774
|
_defaultTemplatesDir = null;
|
|
@@ -20902,6 +20980,7 @@ var init_middleware = __esm({
|
|
|
20902
20980
|
init_dotenv();
|
|
20903
20981
|
init_router();
|
|
20904
20982
|
init_trustedProxy();
|
|
20983
|
+
init_csp();
|
|
20905
20984
|
init_response();
|
|
20906
20985
|
MiddlewareChain = class {
|
|
20907
20986
|
middlewares = [];
|
|
@@ -21334,12 +21413,18 @@ var init_middleware = __esm({
|
|
|
21334
21413
|
* overrides, WITHOUT Strict-Transport-Security, which depends on the
|
|
21335
21414
|
* request's scheme. Also what a transport rejection carries (ADR-0068
|
|
21336
21415
|
* section 4), written before the scheme is known.
|
|
21416
|
+
*
|
|
21417
|
+
* Content-Security-Policy always carries a per-response nonce in style-src
|
|
21418
|
+
* AND script-src (ADR-0088), read from the request async-context (minted on
|
|
21419
|
+
* first access), so the framework's own inline content runs under the strict
|
|
21420
|
+
* default without 'unsafe-inline'. The nonce is random per response, so a
|
|
21421
|
+
* caller comparing this header must assert its STRUCTURE, never a byte string.
|
|
21337
21422
|
*/
|
|
21338
21423
|
static canonicalHeaders() {
|
|
21339
21424
|
return {
|
|
21340
21425
|
"X-Frame-Options": process.env.TINA4_FRAME_OPTIONS ?? "SAMEORIGIN",
|
|
21341
21426
|
"X-Content-Type-Options": "nosniff",
|
|
21342
|
-
"Content-Security-Policy":
|
|
21427
|
+
"Content-Security-Policy": resolveCspHeader(currentCspNonce()),
|
|
21343
21428
|
"Referrer-Policy": process.env.TINA4_REFERRER_POLICY ?? "strict-origin-when-cross-origin",
|
|
21344
21429
|
"X-XSS-Protection": "0",
|
|
21345
21430
|
"Permissions-Policy": process.env.TINA4_PERMISSIONS_POLICY ?? "camera=(), microphone=(), geolocation=()"
|
|
@@ -21381,7 +21466,7 @@ var init_middleware = __esm({
|
|
|
21381
21466
|
static warnCspDefaultOnce() {
|
|
21382
21467
|
if (_SecurityHeadersMiddleware.cspDefaultWarned) return;
|
|
21383
21468
|
_SecurityHeadersMiddleware.cspDefaultWarned = true;
|
|
21384
|
-
const message = `TINA4_CSP is not set, so Tina4 is serving the default Content-Security-Policy "default-src 'self'" on every response.
|
|
21469
|
+
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'".`;
|
|
21385
21470
|
try {
|
|
21386
21471
|
Log.warning(message);
|
|
21387
21472
|
} catch {
|
|
@@ -23794,7 +23879,7 @@ __export(session_exports, {
|
|
|
23794
23879
|
sessionCookieName: () => sessionCookieName,
|
|
23795
23880
|
sessionStrictMode: () => sessionStrictMode
|
|
23796
23881
|
});
|
|
23797
|
-
import { createHash as createHash5, randomBytes as
|
|
23882
|
+
import { createHash as createHash5, randomBytes as randomBytes6 } from "node:crypto";
|
|
23798
23883
|
import { existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync8, writeFileSync as writeFileSync8, unlinkSync as unlinkSync5, readdirSync as readdirSync6 } from "node:fs";
|
|
23799
23884
|
import { join as join15 } from "node:path";
|
|
23800
23885
|
function isValidSessionId(sessionId) {
|
|
@@ -23812,6 +23897,20 @@ function resolveBackend(name) {
|
|
|
23812
23897
|
}
|
|
23813
23898
|
return normalised;
|
|
23814
23899
|
}
|
|
23900
|
+
function fingerprint(value) {
|
|
23901
|
+
try {
|
|
23902
|
+
return JSON.stringify(value);
|
|
23903
|
+
} catch {
|
|
23904
|
+
return void 0;
|
|
23905
|
+
}
|
|
23906
|
+
}
|
|
23907
|
+
function fingerprints(data) {
|
|
23908
|
+
const prints = /* @__PURE__ */ new Map();
|
|
23909
|
+
for (const [key, value] of Object.entries(data)) {
|
|
23910
|
+
prints.set(key, fingerprint(value));
|
|
23911
|
+
}
|
|
23912
|
+
return prints;
|
|
23913
|
+
}
|
|
23815
23914
|
function isSecureScheme(forwardedProto, socketEncrypted) {
|
|
23816
23915
|
const forwarded = (forwardedProto ?? "").trim();
|
|
23817
23916
|
if (forwarded !== "") {
|
|
@@ -24009,6 +24108,29 @@ var init_session = __esm({
|
|
|
24009
24108
|
* Lets `start()` tell "no such session" from "the store is unreachable".
|
|
24010
24109
|
*/
|
|
24011
24110
|
lastReadFailed = false;
|
|
24111
|
+
/**
|
|
24112
|
+
* What this request last saw of the stored record, one fingerprint per key:
|
|
24113
|
+
* taken by start() and again by every save. save() compares the live data
|
|
24114
|
+
* against it to find the keys THIS request changed.
|
|
24115
|
+
*/
|
|
24116
|
+
loaded = /* @__PURE__ */ new Map();
|
|
24117
|
+
/**
|
|
24118
|
+
* True when this request is working on a record the store holds: start()
|
|
24119
|
+
* adopted one, or a save has written one since. A save never re-creates such
|
|
24120
|
+
* a record once another request has removed it.
|
|
24121
|
+
*/
|
|
24122
|
+
stored = false;
|
|
24123
|
+
/**
|
|
24124
|
+
* True when clear() ran since the last save: the save then replaces the
|
|
24125
|
+
* stored record instead of merging into it.
|
|
24126
|
+
*/
|
|
24127
|
+
cleared = false;
|
|
24128
|
+
/**
|
|
24129
|
+
* True once this request found its session ended by another request (the
|
|
24130
|
+
* record it loaded is gone). It stays ended for the rest of the request:
|
|
24131
|
+
* regenerate() mints nothing either.
|
|
24132
|
+
*/
|
|
24133
|
+
ended = false;
|
|
24012
24134
|
constructor(backend, config) {
|
|
24013
24135
|
const backendType = resolveBackend(
|
|
24014
24136
|
backend ?? config?.backend ?? process.env.TINA4_SESSION_BACKEND ?? "file"
|
|
@@ -24153,6 +24275,10 @@ var init_session = __esm({
|
|
|
24153
24275
|
const now2 = Math.floor(Date.now() / 1e3);
|
|
24154
24276
|
this.data = { _created: now2, _accessed: now2 };
|
|
24155
24277
|
this.dirty = false;
|
|
24278
|
+
this.stored = false;
|
|
24279
|
+
this.loaded = /* @__PURE__ */ new Map();
|
|
24280
|
+
this.cleared = false;
|
|
24281
|
+
this.ended = false;
|
|
24156
24282
|
return sessionId;
|
|
24157
24283
|
}
|
|
24158
24284
|
if (loaded) {
|
|
@@ -24162,14 +24288,22 @@ var init_session = __esm({
|
|
|
24162
24288
|
this.data._accessed = now2;
|
|
24163
24289
|
this.dirty = false;
|
|
24164
24290
|
this.safeWrite(this.sessionId, this.data, this.ttl);
|
|
24291
|
+
this.stored = true;
|
|
24292
|
+
this.loaded = fingerprints(this.data);
|
|
24293
|
+
this.cleared = false;
|
|
24294
|
+
this.ended = false;
|
|
24165
24295
|
return sessionId;
|
|
24166
24296
|
}
|
|
24167
24297
|
}
|
|
24168
|
-
this.sessionId =
|
|
24298
|
+
this.sessionId = randomBytes6(16).toString("hex");
|
|
24169
24299
|
const now = Math.floor(Date.now() / 1e3);
|
|
24170
24300
|
this.data = { _created: now, _accessed: now };
|
|
24171
24301
|
this.dirty = false;
|
|
24172
24302
|
this.safeWrite(this.sessionId, this.data, this.ttl);
|
|
24303
|
+
this.stored = false;
|
|
24304
|
+
this.loaded = /* @__PURE__ */ new Map();
|
|
24305
|
+
this.cleared = false;
|
|
24306
|
+
this.ended = false;
|
|
24173
24307
|
return this.sessionId;
|
|
24174
24308
|
}
|
|
24175
24309
|
/**
|
|
@@ -24210,9 +24344,7 @@ var init_session = __esm({
|
|
|
24210
24344
|
if (this.sessionId) {
|
|
24211
24345
|
this.safeDestroy(this.sessionId);
|
|
24212
24346
|
}
|
|
24213
|
-
this.
|
|
24214
|
-
this.data = null;
|
|
24215
|
-
this.dirty = false;
|
|
24347
|
+
this.forget();
|
|
24216
24348
|
}
|
|
24217
24349
|
/**
|
|
24218
24350
|
* Get all session data (excluding internal keys).
|
|
@@ -24235,6 +24367,7 @@ var init_session = __esm({
|
|
|
24235
24367
|
const now = Math.floor(Date.now() / 1e3);
|
|
24236
24368
|
this.data = { _created: this.data._created, _accessed: now };
|
|
24237
24369
|
this.dirty = true;
|
|
24370
|
+
this.cleared = true;
|
|
24238
24371
|
this.save();
|
|
24239
24372
|
}
|
|
24240
24373
|
/**
|
|
@@ -24251,17 +24384,36 @@ var init_session = __esm({
|
|
|
24251
24384
|
* session fixation — the pre-auth ID is destroyed and the data is carried
|
|
24252
24385
|
* onto a fresh, unguessable ID. A backend destroy/write failure is logged
|
|
24253
24386
|
* (never silent) but does not throw under the default policy.
|
|
24387
|
+
*
|
|
24388
|
+
* What is carried is the session as it is stored now with this request's
|
|
24389
|
+
* own changes applied, the same merge save() does. If another request ended
|
|
24390
|
+
* the session after this one loaded it (a logout, or a regenerate of its
|
|
24391
|
+
* own), it stays ended: nothing is carried, no id is minted and null is
|
|
24392
|
+
* returned, so no cookie goes out to replace the one that request sent.
|
|
24254
24393
|
*/
|
|
24255
24394
|
regenerate() {
|
|
24395
|
+
if (this.ended) return null;
|
|
24396
|
+
if (this.sessionId && this.stored && this.data) {
|
|
24397
|
+
const current = this.safeRead(this.sessionId);
|
|
24398
|
+
if (!this.lastReadFailed) {
|
|
24399
|
+
if (!current) {
|
|
24400
|
+
this.forget();
|
|
24401
|
+
this.ended = true;
|
|
24402
|
+
return null;
|
|
24403
|
+
}
|
|
24404
|
+
if (!this.cleared) this.data = this.merged(current);
|
|
24405
|
+
}
|
|
24406
|
+
}
|
|
24256
24407
|
const oldId = this.sessionId;
|
|
24257
24408
|
const oldData = this.data;
|
|
24258
24409
|
if (oldId) {
|
|
24259
24410
|
this.safeDestroy(oldId);
|
|
24260
24411
|
}
|
|
24261
|
-
this.sessionId =
|
|
24412
|
+
this.sessionId = randomBytes6(16).toString("hex");
|
|
24262
24413
|
this.data = oldData ?? { _created: Math.floor(Date.now() / 1e3), _accessed: Math.floor(Date.now() / 1e3) };
|
|
24263
24414
|
this.data._accessed = Math.floor(Date.now() / 1e3);
|
|
24264
24415
|
this.dirty = true;
|
|
24416
|
+
this.stored = false;
|
|
24265
24417
|
this.save();
|
|
24266
24418
|
return this.sessionId;
|
|
24267
24419
|
}
|
|
@@ -24337,17 +24489,84 @@ var init_session = __esm({
|
|
|
24337
24489
|
*
|
|
24338
24490
|
* Returns true on a successful persist, false if the backend was unreachable
|
|
24339
24491
|
* (logged). The dirty flag is cleared only on success so a later save()
|
|
24340
|
-
* retries once the backend recovers. A
|
|
24492
|
+
* retries once the backend recovers. A call with no session returns true.
|
|
24493
|
+
*
|
|
24494
|
+
* Other requests on the same session run while this one does, so the save
|
|
24495
|
+
* writes only THIS request's changes, onto the record as it is stored now.
|
|
24496
|
+
* Writing back the whole snapshot start() loaded undid whatever another
|
|
24497
|
+
* request did in between: a logout (destroy, clear, regenerate) or a
|
|
24498
|
+
* privilege change made with set(). A record removed since this request
|
|
24499
|
+
* loaded it is never re-created: the session has ended, for this request
|
|
24500
|
+
* too. A request that has not loaded a stored record (a new session, the new
|
|
24501
|
+
* id regenerate() mints) writes its data whole, as before.
|
|
24502
|
+
*
|
|
24503
|
+
* A session the store already holds is re-written even when this request
|
|
24504
|
+
* changed nothing: that write re-stamps the backend deadline to
|
|
24505
|
+
* now + TINA4_SESSION_TTL, so a session expires after that span of INACTIVITY
|
|
24506
|
+
* rather than that long after its last CHANGE (ADR-0087 — a session's expiry
|
|
24507
|
+
* slides on activity). The re-write is the re-read, merged record, so sliding
|
|
24508
|
+
* the deadline is concurrent-safe: it never clobbers another request's change,
|
|
24509
|
+
* never re-creates an ended record, and TINA4_SESSION_TTL=0 still re-stamps a
|
|
24510
|
+
* never-expires deadline (ADR-0027). A request with no stored session and no
|
|
24511
|
+
* change writes nothing — there is no deadline to move.
|
|
24341
24512
|
*/
|
|
24342
24513
|
save() {
|
|
24343
24514
|
if (!this.sessionId || !this.data) return true;
|
|
24344
|
-
if (!this.dirty) return true;
|
|
24345
|
-
|
|
24515
|
+
if (!this.dirty && !this.stored) return true;
|
|
24516
|
+
let record = this.data;
|
|
24517
|
+
if (this.stored) {
|
|
24518
|
+
const current = this.safeRead(this.sessionId);
|
|
24519
|
+
if (this.lastReadFailed) return false;
|
|
24520
|
+
if (!current) {
|
|
24521
|
+
this.forget();
|
|
24522
|
+
this.ended = true;
|
|
24523
|
+
return true;
|
|
24524
|
+
}
|
|
24525
|
+
if (!this.cleared) record = this.merged(current);
|
|
24526
|
+
}
|
|
24527
|
+
if (this.safeWrite(this.sessionId, record, this.ttl)) {
|
|
24346
24528
|
this.dirty = false;
|
|
24529
|
+
this.stored = true;
|
|
24530
|
+
this.cleared = false;
|
|
24531
|
+
this.loaded = fingerprints(this.data);
|
|
24347
24532
|
return true;
|
|
24348
24533
|
}
|
|
24349
24534
|
return false;
|
|
24350
24535
|
}
|
|
24536
|
+
/**
|
|
24537
|
+
* The stored record with this request's changes applied: every key whose
|
|
24538
|
+
* value differs from what this request loaded or last saved, and the
|
|
24539
|
+
* removal of every key it deleted. Keys it did not touch keep whatever is
|
|
24540
|
+
* stored now.
|
|
24541
|
+
*/
|
|
24542
|
+
merged(current) {
|
|
24543
|
+
const data = this.data;
|
|
24544
|
+
const record = { ...current };
|
|
24545
|
+
for (const [key, value] of Object.entries(data)) {
|
|
24546
|
+
const print = fingerprint(value);
|
|
24547
|
+
if (print === void 0 || !this.loaded.has(key) || this.loaded.get(key) !== print) {
|
|
24548
|
+
record[key] = value;
|
|
24549
|
+
}
|
|
24550
|
+
}
|
|
24551
|
+
for (const key of this.loaded.keys()) {
|
|
24552
|
+
if (!Object.prototype.hasOwnProperty.call(data, key)) {
|
|
24553
|
+
delete record[key];
|
|
24554
|
+
}
|
|
24555
|
+
}
|
|
24556
|
+
return record;
|
|
24557
|
+
}
|
|
24558
|
+
/**
|
|
24559
|
+
* End the session for this request without touching the store: no id and
|
|
24560
|
+
* no data, so nothing more is written and no cookie goes out for it.
|
|
24561
|
+
*/
|
|
24562
|
+
forget() {
|
|
24563
|
+
this.sessionId = null;
|
|
24564
|
+
this.data = null;
|
|
24565
|
+
this.dirty = false;
|
|
24566
|
+
this.stored = false;
|
|
24567
|
+
this.cleared = false;
|
|
24568
|
+
this.loaded = /* @__PURE__ */ new Map();
|
|
24569
|
+
}
|
|
24351
24570
|
};
|
|
24352
24571
|
}
|
|
24353
24572
|
});
|
|
@@ -24716,11 +24935,11 @@ function formatSourceBlock(filename, lineno) {
|
|
|
24716
24935
|
const lines = readSourceLines(filename, lineno);
|
|
24717
24936
|
if (lines.length === 0) return "";
|
|
24718
24937
|
const rows = lines.map(([num, text, isError]) => {
|
|
24719
|
-
const
|
|
24938
|
+
const rowClass = isError ? "eo-line eo-line-err" : "eo-line";
|
|
24720
24939
|
const marker = isError ? "▶" : " ";
|
|
24721
|
-
return `<div
|
|
24940
|
+
return `<div class="${rowClass}"><span class="eo-ln">${num}</span><span class="eo-marker">${marker}</span><span class="eo-code">${esc(text)}</span></div>`;
|
|
24722
24941
|
}).join("\n");
|
|
24723
|
-
return `<div
|
|
24942
|
+
return `<div class="eo-source">` + rows + `</div>`;
|
|
24724
24943
|
}
|
|
24725
24944
|
function formatFrame(frame, capturedAt = 0) {
|
|
24726
24945
|
const source = frame.file && frame.line > 0 ? formatSourceBlock(frame.file, frame.line) : "";
|
|
@@ -24732,23 +24951,23 @@ function formatFrame(frame, capturedAt = 0) {
|
|
|
24732
24951
|
if (mtime > capturedAt + 0.5) {
|
|
24733
24952
|
const d = new Date(mtime * 1e3);
|
|
24734
24953
|
const mtimeIso = `${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}:${String(d.getUTCSeconds()).padStart(2, "0")}`;
|
|
24735
|
-
staleBadge = ` <span
|
|
24954
|
+
staleBadge = ` <span class="eo-stale">FILE MODIFIED @ ${mtimeIso} UTC — source may not match what failed</span>`;
|
|
24736
24955
|
}
|
|
24737
24956
|
} catch {
|
|
24738
24957
|
}
|
|
24739
24958
|
}
|
|
24740
|
-
return `<div
|
|
24959
|
+
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>`;
|
|
24741
24960
|
}
|
|
24742
24961
|
function collapsible(title, content, openByDefault = false) {
|
|
24743
24962
|
const open = openByDefault ? " open" : "";
|
|
24744
|
-
return `<details
|
|
24963
|
+
return `<details class="eo-details"${open}><summary class="eo-summary">${esc(title)}</summary><div class="eo-details-body">${content}</div></details>`;
|
|
24745
24964
|
}
|
|
24746
24965
|
function table(pairs) {
|
|
24747
|
-
if (pairs.length === 0) return `<span
|
|
24966
|
+
if (pairs.length === 0) return `<span class="eo-none">None</span>`;
|
|
24748
24967
|
const rows = pairs.map(
|
|
24749
|
-
([key, val]) => `<tr><td
|
|
24968
|
+
([key, val]) => `<tr><td class="eo-key">${esc(key)}</td><td class="eo-val">${esc(val)}</td></tr>`
|
|
24750
24969
|
).join("");
|
|
24751
|
-
return `<table
|
|
24970
|
+
return `<table class="eo-table">${rows}</table>`;
|
|
24752
24971
|
}
|
|
24753
24972
|
function renderErrorOverlay(error2, request) {
|
|
24754
24973
|
const capturedAt = Date.now() / 1e3;
|
|
@@ -24761,7 +24980,7 @@ function renderErrorOverlay(error2, request) {
|
|
|
24761
24980
|
}
|
|
24762
24981
|
const hidden = frames.length - Math.min(frames.length, MAX_FRAMES);
|
|
24763
24982
|
if (hidden > 0) {
|
|
24764
|
-
framesHtml += `<div
|
|
24983
|
+
framesHtml += `<div class="eo-hidden-frames">… ${hidden} more stack frames hidden (truncated at ${MAX_FRAMES})</div>`;
|
|
24765
24984
|
}
|
|
24766
24985
|
const requestPairs = [];
|
|
24767
24986
|
if (request != null) {
|
|
@@ -24798,37 +25017,72 @@ function renderErrorOverlay(error2, request) {
|
|
|
24798
25017
|
];
|
|
24799
25018
|
const envSection = collapsible("Environment", table(envPairs));
|
|
24800
25019
|
const stackSection = collapsible("Stack Trace", framesHtml, true);
|
|
25020
|
+
const nonce = currentCspNonce();
|
|
24801
25021
|
return `<!DOCTYPE html>
|
|
24802
25022
|
<html lang="en">
|
|
24803
25023
|
<head>
|
|
24804
25024
|
<meta charset="utf-8">
|
|
24805
25025
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
24806
25026
|
<title>Tina4 Error \u2014 ${esc(excType)}</title>
|
|
24807
|
-
<style>
|
|
24808
|
-
|
|
24809
|
-
body{background:${BG};color:${TEXT};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px;line-height:1.5;}
|
|
25027
|
+
<style nonce="${nonce}">
|
|
25028
|
+
${overlayStylesheet()}
|
|
24810
25029
|
</style>
|
|
24811
25030
|
</head>
|
|
24812
25031
|
<body>
|
|
24813
|
-
<div
|
|
24814
|
-
<div
|
|
24815
|
-
<div
|
|
24816
|
-
<span
|
|
24817
|
-
<span
|
|
25032
|
+
<div class="eo-wrap">
|
|
25033
|
+
<div class="eo-header">
|
|
25034
|
+
<div class="eo-badge-row">
|
|
25035
|
+
<span class="eo-badge">Error</span>
|
|
25036
|
+
<span class="eo-sub">Tina4 Debug Overlay</span>
|
|
24818
25037
|
</div>
|
|
24819
|
-
<h1
|
|
24820
|
-
<p
|
|
25038
|
+
<h1 class="eo-type">${esc(excType)}</h1>
|
|
25039
|
+
<p class="eo-msg">${esc(excMsg)}</p>
|
|
24821
25040
|
</div>
|
|
24822
25041
|
${stackSection}
|
|
24823
25042
|
${requestSection}
|
|
24824
25043
|
${envSection}
|
|
24825
|
-
<div
|
|
25044
|
+
<div class="eo-footer">
|
|
24826
25045
|
Tina4 Debug Overlay — This page is only shown in debug mode. Set TINA4_DEBUG=false in production.
|
|
24827
25046
|
</div>
|
|
24828
25047
|
</div>
|
|
24829
25048
|
</body>
|
|
24830
25049
|
</html>`;
|
|
24831
25050
|
}
|
|
25051
|
+
function overlayStylesheet() {
|
|
25052
|
+
return `
|
|
25053
|
+
*{margin:0;padding:0;box-sizing:border-box;}
|
|
25054
|
+
body{background:${BG};color:${TEXT};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px;line-height:1.5;}
|
|
25055
|
+
.eo-wrap{max-width:960px;margin:0 auto;}
|
|
25056
|
+
.eo-header{margin-bottom:24px;}
|
|
25057
|
+
.eo-badge-row{display:flex;align-items:center;gap:12px;margin-bottom:12px;}
|
|
25058
|
+
.eo-badge{background:${RED};color:${BG};padding:4px 12px;border-radius:4px;font-weight:700;font-size:13px;text-transform:uppercase;}
|
|
25059
|
+
.eo-sub{color:${SUBTEXT};font-size:14px;}
|
|
25060
|
+
.eo-type{color:${RED};font-size:28px;font-weight:700;margin-bottom:8px;}
|
|
25061
|
+
.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};}
|
|
25062
|
+
.eo-footer{margin-top:32px;padding-top:16px;border-top:1px solid ${OVERLAY};color:${SUBTEXT};font-size:12px;}
|
|
25063
|
+
.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;}
|
|
25064
|
+
.eo-line{display:flex;padding:1px 0;}
|
|
25065
|
+
.eo-line-err{background:${ERROR_LINE_BG};}
|
|
25066
|
+
.eo-ln{color:${YELLOW};min-width:3.5em;text-align:right;padding-right:1em;user-select:none;}
|
|
25067
|
+
.eo-marker{color:${RED};width:1.2em;user-select:none;}
|
|
25068
|
+
.eo-code{color:${TEXT};white-space:pre-wrap;tab-size:4;}
|
|
25069
|
+
.eo-frame{margin-bottom:16px;}
|
|
25070
|
+
.eo-frame-head{margin-bottom:4px;}
|
|
25071
|
+
.eo-file{color:${BLUE};}
|
|
25072
|
+
.eo-sep{color:${SUBTEXT};}
|
|
25073
|
+
.eo-lineno{color:${YELLOW};}
|
|
25074
|
+
.eo-fn{color:${GREEN};}
|
|
25075
|
+
.eo-stale{background:${PEACH};color:${BG};padding:1px 8px;border-radius:3px;font-size:11px;font-weight:700;margin-left:6px;}
|
|
25076
|
+
.eo-details{margin-top:16px;}
|
|
25077
|
+
.eo-summary{cursor:pointer;color:${LAVENDER};font-weight:600;font-size:15px;padding:8px 0;user-select:none;}
|
|
25078
|
+
.eo-details-body{padding:8px 0;}
|
|
25079
|
+
.eo-table{border-collapse:collapse;width:100%;}
|
|
25080
|
+
.eo-key{color:${PEACH};padding:4px 16px 4px 0;vertical-align:top;white-space:nowrap;}
|
|
25081
|
+
.eo-val{color:${TEXT};padding:4px 0;word-break:break-all;}
|
|
25082
|
+
.eo-none{color:${SUBTEXT};}
|
|
25083
|
+
.eo-hidden-frames{color:${SUBTEXT};padding:8px 0;font-size:13px;}
|
|
25084
|
+
`;
|
|
25085
|
+
}
|
|
24832
25086
|
function isDebugMode() {
|
|
24833
25087
|
return isTruthy(process.env.TINA4_DEBUG);
|
|
24834
25088
|
}
|
|
@@ -24837,6 +25091,7 @@ var init_errorOverlay = __esm({
|
|
|
24837
25091
|
"../core/src/errorOverlay.ts"() {
|
|
24838
25092
|
"use strict";
|
|
24839
25093
|
init_dotenv();
|
|
25094
|
+
init_csp();
|
|
24840
25095
|
MAX_FRAMES = 50;
|
|
24841
25096
|
SENSITIVE_KEY_RE = /password|passwd|secret|token|authorization|cookie|key/i;
|
|
24842
25097
|
REDACTED2 = "[redacted]";
|
|
@@ -26710,7 +26965,11 @@ export default async function (req: Tina4Request, res: Tina4Response) {
|
|
|
26710
26965
|
`{% extends "base.twig" %}
|
|
26711
26966
|
{% block title %}Login{% endblock %}
|
|
26712
26967
|
{% block content %}
|
|
26713
|
-
|
|
26968
|
+
{# No inline style attribute: a nonce lets this <style> block run under the
|
|
26969
|
+
strict default Content-Security-Policy. csp_nonce() is a built-in Frond global
|
|
26970
|
+
that returns this response's nonce (ADR-0088). #}
|
|
26971
|
+
<style nonce="{{ csp_nonce() }}">.auth-form{max-width:400px;margin:0 auto}</style>
|
|
26972
|
+
<div class="container mt-4 auth-form">
|
|
26714
26973
|
<h1>Login</h1>
|
|
26715
26974
|
<form method="post" action="/api/auth/login">
|
|
26716
26975
|
{{ form_token() }}
|
|
@@ -26734,7 +26993,8 @@ export default async function (req: Tina4Request, res: Tina4Response) {
|
|
|
26734
26993
|
`{% extends "base.twig" %}
|
|
26735
26994
|
{% block title %}Register{% endblock %}
|
|
26736
26995
|
{% block content %}
|
|
26737
|
-
<
|
|
26996
|
+
<style nonce="{{ csp_nonce() }}">.auth-form{max-width:400px;margin:0 auto}</style>
|
|
26997
|
+
<div class="container mt-4 auth-form">
|
|
26738
26998
|
<h1>Register</h1>
|
|
26739
26999
|
<form method="post" action="/api/auth/register">
|
|
26740
27000
|
{{ form_token() }}
|
|
@@ -27671,7 +27931,7 @@ import * as path3 from "node:path";
|
|
|
27671
27931
|
import * as os from "node:os";
|
|
27672
27932
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
27673
27933
|
import { createRequire as createRequire8 } from "node:module";
|
|
27674
|
-
import { randomBytes as
|
|
27934
|
+
import { randomBytes as randomBytes7 } from "node:crypto";
|
|
27675
27935
|
function reqSibling(pkg) {
|
|
27676
27936
|
return req(`../../${pkg}/src/index.ts`);
|
|
27677
27937
|
}
|
|
@@ -28993,7 +29253,7 @@ var init_mcp = __esm({
|
|
|
28993
29253
|
// ── Session lifecycle + protocol negotiation ──────────────────
|
|
28994
29254
|
/** Mint a new session id and remember it. Called on `initialize`. */
|
|
28995
29255
|
openSession() {
|
|
28996
|
-
const sid =
|
|
29256
|
+
const sid = randomBytes7(16).toString("hex");
|
|
28997
29257
|
this._sessions.set(sid, Date.now());
|
|
28998
29258
|
return sid;
|
|
28999
29259
|
}
|
|
@@ -36769,8 +37029,8 @@ var init_devAdmin = __esm({
|
|
|
36769
37029
|
* Duplicate errors (same message) increment count and update last_seen.
|
|
36770
37030
|
*/
|
|
36771
37031
|
static capture(errorType, message, traceback = "", file = "", line = 0) {
|
|
36772
|
-
const
|
|
36773
|
-
const existing = this.errors.find((e) => e.fingerprint ===
|
|
37032
|
+
const fingerprint2 = `${errorType}|${message}|${file}|${line}`;
|
|
37033
|
+
const existing = this.errors.find((e) => e.fingerprint === fingerprint2);
|
|
36774
37034
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
36775
37035
|
if (existing) {
|
|
36776
37036
|
existing.count = (existing.count || 1) + 1;
|
|
@@ -36783,7 +37043,7 @@ var init_devAdmin = __esm({
|
|
|
36783
37043
|
message,
|
|
36784
37044
|
stack: traceback || void 0,
|
|
36785
37045
|
resolved: false,
|
|
36786
|
-
...{ fingerprint, error_type: errorType, file, line, count: 1, first_seen: now, last_seen: now }
|
|
37046
|
+
...{ fingerprint: fingerprint2, error_type: errorType, file, line, count: 1, first_seen: now, last_seen: now }
|
|
36787
37047
|
});
|
|
36788
37048
|
if (this.errors.length > this.maxErrors) {
|
|
36789
37049
|
this.errors = this.errors.slice(-this.maxErrors);
|
|
@@ -39462,7 +39722,7 @@ function swaggerEnabled() {
|
|
|
39462
39722
|
}
|
|
39463
39723
|
function createSwaggerRoutes(getSpec) {
|
|
39464
39724
|
const serveUi = async (_req, res) => {
|
|
39465
|
-
res.html(SWAGGER_UI_HTML("/swagger/openapi.json"));
|
|
39725
|
+
res.html(SWAGGER_UI_HTML("/swagger/openapi.json", res.cspNonce));
|
|
39466
39726
|
};
|
|
39467
39727
|
return [
|
|
39468
39728
|
{
|
|
@@ -39506,13 +39766,13 @@ var SWAGGER_UI_HTML;
|
|
|
39506
39766
|
var init_ui = __esm({
|
|
39507
39767
|
"../swagger/src/ui.ts"() {
|
|
39508
39768
|
"use strict";
|
|
39509
|
-
SWAGGER_UI_HTML = (specUrl) => `<!DOCTYPE html>
|
|
39769
|
+
SWAGGER_UI_HTML = (specUrl, nonce) => `<!DOCTYPE html>
|
|
39510
39770
|
<html lang="en">
|
|
39511
39771
|
<head>
|
|
39512
39772
|
<meta charset="UTF-8">
|
|
39513
39773
|
<title>Tina4 API Documentation</title>
|
|
39514
39774
|
<link rel="stylesheet" href="${swaggerUiCdn()}/swagger-ui.css">
|
|
39515
|
-
<style>
|
|
39775
|
+
<style nonce="${nonce}">
|
|
39516
39776
|
body { margin: 0; background: #fafafa; }
|
|
39517
39777
|
.topbar { display: none !important; }
|
|
39518
39778
|
</style>
|
|
@@ -39520,7 +39780,7 @@ var init_ui = __esm({
|
|
|
39520
39780
|
<body>
|
|
39521
39781
|
<div id="swagger-ui"></div>
|
|
39522
39782
|
<script src="${swaggerUiCdn()}/swagger-ui-bundle.js"></script>
|
|
39523
|
-
<script>
|
|
39783
|
+
<script nonce="${nonce}">
|
|
39524
39784
|
SwaggerUIBundle({
|
|
39525
39785
|
url: "${specUrl}",
|
|
39526
39786
|
dom_id: '#swagger-ui',
|
|
@@ -39560,7 +39820,7 @@ __export(sso_exports, {
|
|
|
39560
39820
|
SsoError: () => SsoError,
|
|
39561
39821
|
liveSessionIdentity: () => liveSessionIdentity
|
|
39562
39822
|
});
|
|
39563
|
-
import { createHash as createHash9, randomBytes as
|
|
39823
|
+
import { createHash as createHash9, randomBytes as randomBytes8, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
|
|
39564
39824
|
function expiresAt(tokens) {
|
|
39565
39825
|
const lifetime = Number(tokens.expires_in ?? 0);
|
|
39566
39826
|
return lifetime > 0 ? Math.floor(Date.now() / 1e3) + lifetime : 0;
|
|
@@ -39688,9 +39948,9 @@ var init_sso = __esm({
|
|
|
39688
39948
|
async login(requestOrSession, returnTo = "/") {
|
|
39689
39949
|
const session = this.session(requestOrSession);
|
|
39690
39950
|
if (!session) throw new SsoError("SSO login requires a Tina4 Session");
|
|
39691
|
-
const state =
|
|
39692
|
-
const nonce =
|
|
39693
|
-
const verifier =
|
|
39951
|
+
const state = randomBytes8(32).toString("base64url");
|
|
39952
|
+
const nonce = randomBytes8(32).toString("base64url");
|
|
39953
|
+
const verifier = randomBytes8(64).toString("base64url");
|
|
39694
39954
|
const challenge = createHash9("sha256").update(verifier).digest("base64url");
|
|
39695
39955
|
session.set(_Sso.PENDING_KEY, { state, nonce, verifier, return_to: _Sso.safeReturn(returnTo), created_at: Math.floor(Date.now() / 1e3) });
|
|
39696
39956
|
const metadata = await this.discover();
|
|
@@ -39918,7 +40178,7 @@ var init_docsAutoDiscovery = __esm({
|
|
|
39918
40178
|
});
|
|
39919
40179
|
|
|
39920
40180
|
// ../core/src/server.ts
|
|
39921
|
-
import { randomBytes as
|
|
40181
|
+
import { randomBytes as randomBytes9 } from "node:crypto";
|
|
39922
40182
|
import { resolve as resolve20, dirname as dirname14, join as join32, relative as relative10 } from "node:path";
|
|
39923
40183
|
import { existsSync as existsSync26, readdirSync as readdirSync17, readFileSync as readFileSync23, statSync as statSync19 } from "node:fs";
|
|
39924
40184
|
import { isatty } from "node:tty";
|
|
@@ -40098,7 +40358,8 @@ function isDevMode() {
|
|
|
40098
40358
|
}
|
|
40099
40359
|
async function renderErrorPage(code, data, templatesDir) {
|
|
40100
40360
|
try {
|
|
40101
|
-
const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
40361
|
+
const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
40362
|
+
setCspNonceProvider2(currentCspNonce);
|
|
40102
40363
|
const templateFile = `errors/${code}.twig`;
|
|
40103
40364
|
const getCachedFrond = (dir) => {
|
|
40104
40365
|
let instance = frondCache.get(dir);
|
|
@@ -40214,6 +40475,7 @@ function resolveTemplate(pathname, templatesDir) {
|
|
|
40214
40475
|
}
|
|
40215
40476
|
function renderLandingPage(port = 7148) {
|
|
40216
40477
|
const version = TINA4_VERSION;
|
|
40478
|
+
const nonce = currentCspNonce();
|
|
40217
40479
|
const galleryItems = [
|
|
40218
40480
|
{ id: "rest-api", icon: "🚀", name: "REST API", desc: "A simple JSON API with GET and POST endpoints", accent: "accent-blue", tryUrl: "/api/gallery/hello" },
|
|
40219
40481
|
{ id: "orm", icon: "🗃", name: "ORM", desc: "Product model with CRUD endpoints", accent: "accent-green", tryUrl: "/api/gallery/products" },
|
|
@@ -40228,7 +40490,7 @@ function renderLandingPage(port = 7148) {
|
|
|
40228
40490
|
const isDeployed = deployed[item.id] === true;
|
|
40229
40491
|
const tryBtn = isDeployed ? `<a href="${item.tryUrl}" class="gbtn gbtn-try" target="_blank">Try It</a>` : "";
|
|
40230
40492
|
const viewBtn = isDeployed ? `<a href="${item.tryUrl}" class="gbtn gbtn-view" target="_blank">View</a>` : "";
|
|
40231
|
-
const deployBtn = isDeployed ? `<span class="gbtn gbtn-deployed">Deployed</span>` : `<button class="gbtn gbtn-deploy"
|
|
40493
|
+
const deployBtn = isDeployed ? `<span class="gbtn gbtn-deployed">Deployed</span>` : `<button class="gbtn gbtn-deploy" data-deploy="${item.id}">Deploy</button>`;
|
|
40232
40494
|
return `<div class="gallery-card">
|
|
40233
40495
|
<div class="accent ${item.accent}"></div>
|
|
40234
40496
|
<div class="icon">${item.icon}</div>
|
|
@@ -40243,7 +40505,7 @@ function renderLandingPage(port = 7148) {
|
|
|
40243
40505
|
<meta charset="utf-8">
|
|
40244
40506
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
40245
40507
|
<title>Tina4NodeJs</title>
|
|
40246
|
-
<style>
|
|
40508
|
+
<style nonce="${nonce}">
|
|
40247
40509
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
40248
40510
|
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}
|
|
40249
40511
|
.bg-watermark{position:fixed;bottom:-5%;right:-5%;width:45%;opacity:0.04;pointer-events:none;z-index:0}
|
|
@@ -40282,6 +40544,11 @@ h1{font-size:3rem;font-weight:700;margin-bottom:0.25rem;letter-spacing:-1px}
|
|
|
40282
40544
|
.gbtn-deployed{background:transparent;border:1px solid #22c55e;color:#22c55e;cursor:default;font-size:0.7rem}
|
|
40283
40545
|
@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)}}
|
|
40284
40546
|
.star-wiggle{display:inline-block;transform-origin:center}
|
|
40547
|
+
.gallery-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1rem}
|
|
40548
|
+
.tok-comment{color:#64748b}
|
|
40549
|
+
.tok-kw{color:#c084fc}
|
|
40550
|
+
.tok-str{color:#4ade80}
|
|
40551
|
+
.tok-fn{color:#38bdf8}
|
|
40285
40552
|
</style>
|
|
40286
40553
|
</head>
|
|
40287
40554
|
<body>
|
|
@@ -40307,23 +40574,23 @@ h1{font-size:3rem;font-weight:700;margin-bottom:0.25rem;letter-spacing:-1px}
|
|
|
40307
40574
|
<div class="section">
|
|
40308
40575
|
<div class="card">
|
|
40309
40576
|
<h2>Getting Started</h2>
|
|
40310
|
-
<pre class="code-block"><code><span
|
|
40311
|
-
<span
|
|
40577
|
+
<pre class="code-block"><code><span class="tok-comment">// app.ts</span>
|
|
40578
|
+
<span class="tok-kw">import</span> { startServer, Router } <span class="tok-kw">from</span> <span class="tok-str">"tina4-nodejs"</span>;
|
|
40312
40579
|
|
|
40313
|
-
Router.get(<span
|
|
40314
|
-
<span
|
|
40580
|
+
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>) => {
|
|
40581
|
+
<span class="tok-kw">return</span> res.json({ message: <span class="tok-str">"Hello World!"</span> });
|
|
40315
40582
|
});
|
|
40316
40583
|
|
|
40317
|
-
startServer({ port: 7148 }); <span
|
|
40584
|
+
startServer({ port: 7148 }); <span class="tok-comment">// starts on port 7148</span></code></pre>
|
|
40318
40585
|
</div>
|
|
40319
40586
|
</div>
|
|
40320
40587
|
<div class="gallery">
|
|
40321
40588
|
<h2 id="gallery">What You Can Build</h2>
|
|
40322
|
-
<div
|
|
40589
|
+
<div class="gallery-cards">
|
|
40323
40590
|
${galleryCards}
|
|
40324
40591
|
</div>
|
|
40325
40592
|
</div>
|
|
40326
|
-
<script>
|
|
40593
|
+
<script nonce="${nonce}">
|
|
40327
40594
|
function deployGallery(name) {
|
|
40328
40595
|
if (!confirm('Deploy the "' + name + '" gallery example into your project? This will copy files into src/.')) return;
|
|
40329
40596
|
fetch('/__dev/api/gallery/deploy', {
|
|
@@ -40358,6 +40625,12 @@ function deployGallery(name) {
|
|
|
40358
40625
|
}
|
|
40359
40626
|
setTimeout(doWiggle,3000);
|
|
40360
40627
|
})();
|
|
40628
|
+
// CSP-clean wiring: no inline onclick handlers \u2014 bind every Deploy button from
|
|
40629
|
+
// its data-deploy attribute (a nonce covers script elements, not event-handler
|
|
40630
|
+
// attributes).
|
|
40631
|
+
document.querySelectorAll('.gbtn-deploy[data-deploy]').forEach(function(btn){
|
|
40632
|
+
btn.addEventListener('click', function(){ deployGallery(btn.dataset.deploy); });
|
|
40633
|
+
});
|
|
40361
40634
|
</script>
|
|
40362
40635
|
</body>
|
|
40363
40636
|
</html>`;
|
|
@@ -40574,7 +40847,7 @@ async function renderDispatchError(err, req2, res, templatesDir) {
|
|
|
40574
40847
|
}
|
|
40575
40848
|
}
|
|
40576
40849
|
}
|
|
40577
|
-
const requestId = Log.getRequestId() ??
|
|
40850
|
+
const requestId = Log.getRequestId() ?? randomBytes9(4).toString("hex");
|
|
40578
40851
|
if (wantsJson(req2)) {
|
|
40579
40852
|
const body = negotiatedErrorBody(500, "Internal Server Error", requestId);
|
|
40580
40853
|
res.raw.writeHead(500, { "Content-Type": "application/json" });
|
|
@@ -40645,7 +40918,7 @@ function serveStaticAsset(ctx) {
|
|
|
40645
40918
|
return false;
|
|
40646
40919
|
}
|
|
40647
40920
|
async function serveNotFound(ctx) {
|
|
40648
|
-
const requestId = Log.getRequestId() ??
|
|
40921
|
+
const requestId = Log.getRequestId() ?? randomBytes9(4).toString("hex");
|
|
40649
40922
|
if (wantsJson(ctx.req)) {
|
|
40650
40923
|
const body = negotiatedErrorBody(404, "Not Found", requestId);
|
|
40651
40924
|
ctx.res.raw.writeHead(404, httpReason(404), { "Content-Type": "application/json" });
|
|
@@ -40668,7 +40941,8 @@ async function buildDispatchContext(router, base) {
|
|
|
40668
40941
|
const templatesDir = resolve20(root, "src/templates");
|
|
40669
40942
|
let frondEngine = null;
|
|
40670
40943
|
try {
|
|
40671
|
-
const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
40944
|
+
const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
40945
|
+
setCspNonceProvider2(currentCspNonce);
|
|
40672
40946
|
frondEngine = new Frond2(templatesDir);
|
|
40673
40947
|
} catch {
|
|
40674
40948
|
}
|
|
@@ -40778,9 +41052,13 @@ async function dispatchInner(ctx, rawReq, rawRes, requestId) {
|
|
|
40778
41052
|
}
|
|
40779
41053
|
}
|
|
40780
41054
|
async function runDispatch(ctx, rawReq, rawRes) {
|
|
40781
|
-
const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ??
|
|
41055
|
+
const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ?? randomBytes9(4).toString("hex");
|
|
40782
41056
|
if (!rawRes.headersSent) rawRes.setHeader("x-request-id", requestId);
|
|
40783
|
-
|
|
41057
|
+
const requestNonce = generateNonce();
|
|
41058
|
+
return Log.runWithRequestId(
|
|
41059
|
+
requestId,
|
|
41060
|
+
() => runWithCspNonce(requestNonce, () => dispatchInner(ctx, rawReq, rawRes, requestId))
|
|
41061
|
+
);
|
|
40784
41062
|
}
|
|
40785
41063
|
function loopbackBindHosts(host) {
|
|
40786
41064
|
const normalized = host.trim().replace(/^\[|\]$/g, "").toLowerCase();
|
|
@@ -40907,7 +41185,8 @@ async function startServer(config) {
|
|
|
40907
41185
|
let frondEngine = null;
|
|
40908
41186
|
setDefaultTemplatesDir(templatesDir);
|
|
40909
41187
|
try {
|
|
40910
|
-
const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
41188
|
+
const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
41189
|
+
setCspNonceProvider2(currentCspNonce);
|
|
40911
41190
|
frondEngine = new Frond2(templatesDir);
|
|
40912
41191
|
router.addRoute({
|
|
40913
41192
|
method: "GET",
|
|
@@ -41048,10 +41327,12 @@ async function startServer(config) {
|
|
|
41048
41327
|
}
|
|
41049
41328
|
});
|
|
41050
41329
|
aiServer.on("error", (err) => {
|
|
41051
|
-
if (err
|
|
41330
|
+
if (err?.code === "EADDRINUSE") {
|
|
41052
41331
|
Log.warning(`Test port ${testPort} in use \u2014 skipping`);
|
|
41053
|
-
|
|
41332
|
+
} else {
|
|
41333
|
+
Log.error(`Test port ${testPort} bind failed (${err?.code ?? err}) \u2014 skipping`);
|
|
41054
41334
|
}
|
|
41335
|
+
aiServer = null;
|
|
41055
41336
|
});
|
|
41056
41337
|
aiServer.listen(testPort, host);
|
|
41057
41338
|
}
|
|
@@ -41266,6 +41547,7 @@ var init_server = __esm({
|
|
|
41266
41547
|
init_static();
|
|
41267
41548
|
init_dotenv();
|
|
41268
41549
|
init_errorOverlay();
|
|
41550
|
+
init_csp();
|
|
41269
41551
|
init_health();
|
|
41270
41552
|
init_rateLimiter();
|
|
41271
41553
|
init_logger();
|
|
@@ -41515,7 +41797,7 @@ var init_mqttMessage = __esm({
|
|
|
41515
41797
|
// ../core/src/mqtt.ts
|
|
41516
41798
|
import net2 from "node:net";
|
|
41517
41799
|
import tls from "node:tls";
|
|
41518
|
-
import { randomBytes as
|
|
41800
|
+
import { randomBytes as randomBytes10 } from "node:crypto";
|
|
41519
41801
|
import { existsSync as existsSync27, readFileSync as readFileSync24 } from "node:fs";
|
|
41520
41802
|
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;
|
|
41521
41803
|
var init_mqtt = __esm({
|
|
@@ -41603,7 +41885,7 @@ var init_mqtt = __esm({
|
|
|
41603
41885
|
this.caFile = options.caFile ?? (Env.str("TINA4_MQTT_CA_FILE") || null);
|
|
41604
41886
|
this.tlsVerify = options.tlsVerify ?? Env.bool("TINA4_MQTT_TLS_VERIFY", true);
|
|
41605
41887
|
let cid = options.clientId ?? (Env.str("TINA4_MQTT_CLIENT_ID") || null);
|
|
41606
|
-
if (cid === null || cid === "") cid = "tina4-" +
|
|
41888
|
+
if (cid === null || cid === "") cid = "tina4-" + randomBytes10(8).toString("hex");
|
|
41607
41889
|
this.clientId = cid;
|
|
41608
41890
|
this.keepalive = options.keepalive ?? Env.int("TINA4_MQTT_KEEPALIVE", DEFAULT_KEEPALIVE);
|
|
41609
41891
|
this.cleanSession = options.cleanSession ?? true;
|
|
@@ -42717,7 +42999,7 @@ var init_ssrf = __esm({
|
|
|
42717
42999
|
import http from "node:http";
|
|
42718
43000
|
import https from "node:https";
|
|
42719
43001
|
import { URL as URL2 } from "node:url";
|
|
42720
|
-
import { randomBytes as
|
|
43002
|
+
import { randomBytes as randomBytes11 } from "node:crypto";
|
|
42721
43003
|
import { promises as fsp, createWriteStream } from "node:fs";
|
|
42722
43004
|
import { basename as basename7 } from "node:path";
|
|
42723
43005
|
import { pipeline } from "node:stream/promises";
|
|
@@ -43109,7 +43391,7 @@ var init_api = __esm({
|
|
|
43109
43391
|
return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
|
|
43110
43392
|
}
|
|
43111
43393
|
const partContentType = guessContentType(uploadName);
|
|
43112
|
-
const boundary = "----Tina4Boundary" +
|
|
43394
|
+
const boundary = "----Tina4Boundary" + randomBytes11(16).toString("hex");
|
|
43113
43395
|
const bodyBuffer = buildMultipartBody(boundary, fieldName, uploadName, content, partContentType, extraFields);
|
|
43114
43396
|
const contentType = `multipart/form-data; boundary=${boundary}`;
|
|
43115
43397
|
return this.execute("POST", this.buildUrl(path10), bodyBuffer, contentType, headers);
|
|
@@ -46091,7 +46373,7 @@ import {
|
|
|
46091
46373
|
createHmac as createHmac4,
|
|
46092
46374
|
createPrivateKey,
|
|
46093
46375
|
createSign as createSign2,
|
|
46094
|
-
randomBytes as
|
|
46376
|
+
randomBytes as randomBytes12
|
|
46095
46377
|
} from "node:crypto";
|
|
46096
46378
|
function encodeBase64Url(value) {
|
|
46097
46379
|
return Buffer.from(value).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
@@ -46171,7 +46453,7 @@ function encryptPayload(payload, subscription) {
|
|
|
46171
46453
|
]);
|
|
46172
46454
|
const prkKey = hmac(authSecret, sharedSecret);
|
|
46173
46455
|
const ikm = hkdfExpand(prkKey, keyInfo, 32);
|
|
46174
|
-
const salt =
|
|
46456
|
+
const salt = randomBytes12(16);
|
|
46175
46457
|
const contentPrk = hmac(salt, ikm);
|
|
46176
46458
|
const cek = hkdfExpand(contentPrk, Buffer.from("Content-Encoding: aes128gcm\0", "ascii"), 16);
|
|
46177
46459
|
const nonce = hkdfExpand(contentPrk, Buffer.from("Content-Encoding: nonce\0", "ascii"), 12);
|
|
@@ -48928,6 +49210,7 @@ __export(src_exports3, {
|
|
|
48928
49210
|
cacheStats: () => cacheStats,
|
|
48929
49211
|
checkPassword: () => checkPassword,
|
|
48930
49212
|
clearCache: () => clearCache,
|
|
49213
|
+
clearCurrentNonce: () => clearCurrentNonce,
|
|
48931
49214
|
computeAcceptKey: () => computeAcceptKey,
|
|
48932
49215
|
container: () => container,
|
|
48933
49216
|
cors: () => cors,
|
|
@@ -48939,6 +49222,8 @@ __export(src_exports3, {
|
|
|
48939
49222
|
createMessenger: () => createMessenger,
|
|
48940
49223
|
createRequest: () => createRequest,
|
|
48941
49224
|
createResponse: () => createResponse,
|
|
49225
|
+
cspNonce: () => cspNonce,
|
|
49226
|
+
currentCspNonce: () => currentCspNonce,
|
|
48942
49227
|
decodeRequest: () => decodeRequest,
|
|
48943
49228
|
defaultContext: () => defaultContext,
|
|
48944
49229
|
defaultRouter: () => defaultRouter,
|
|
@@ -48964,6 +49249,7 @@ __export(src_exports3, {
|
|
|
48964
49249
|
feedbackWhitelist: () => feedbackWhitelist,
|
|
48965
49250
|
fts5Supported: () => fts5Supported,
|
|
48966
49251
|
generateContext: () => generateContext,
|
|
49252
|
+
generateNonce: () => generateNonce,
|
|
48967
49253
|
generateVapidKeys: () => generateVapidKeys,
|
|
48968
49254
|
get: () => get,
|
|
48969
49255
|
getDefaultDevServer: () => getDefaultDevServer,
|
|
@@ -48986,6 +49272,7 @@ __export(src_exports3, {
|
|
|
48986
49272
|
httpReason: () => httpReason,
|
|
48987
49273
|
inContainer: () => inContainer,
|
|
48988
49274
|
injectFeedbackWidget: () => injectFeedbackWidget,
|
|
49275
|
+
injectNonceIntoCsp: () => injectNonceIntoCsp,
|
|
48989
49276
|
installAll: () => installAll,
|
|
48990
49277
|
installSelected: () => installSelected,
|
|
48991
49278
|
isBannerSuppressed: () => isBannerSuppressed,
|
|
@@ -49040,6 +49327,7 @@ __export(src_exports3, {
|
|
|
49040
49327
|
resetTrustedProxyCache: () => resetTrustedProxyCache,
|
|
49041
49328
|
resolveAlgorithm: () => resolveAlgorithm,
|
|
49042
49329
|
resolveClientIp: () => resolveClientIp,
|
|
49330
|
+
resolveCspHeader: () => resolveCspHeader,
|
|
49043
49331
|
resolveFrameworkVersion: () => resolveFrameworkVersion,
|
|
49044
49332
|
resolvePortAndHost: () => resolvePortAndHost,
|
|
49045
49333
|
resolveStringMiddleware: () => resolveStringMiddleware,
|
|
@@ -49047,12 +49335,14 @@ __export(src_exports3, {
|
|
|
49047
49335
|
responseCache: () => responseCache,
|
|
49048
49336
|
runAll: () => runAll,
|
|
49049
49337
|
runRouteMiddlewares: () => runRouteMiddlewares,
|
|
49338
|
+
runWithCspNonce: () => runWithCspNonce,
|
|
49050
49339
|
runtimeDir: () => runtimeDir,
|
|
49051
49340
|
saveUpload: () => saveUpload,
|
|
49052
49341
|
schemaFromParams: () => schemaFromParams,
|
|
49053
49342
|
selectablePids: () => selectablePids,
|
|
49054
49343
|
serveWebSocketRoute: () => serveWebSocketRoute,
|
|
49055
49344
|
sessionCookieName: () => sessionCookieName,
|
|
49345
|
+
setCurrentNonce: () => setCurrentNonce,
|
|
49056
49346
|
setDefaultTemplatesDir: () => setDefaultTemplatesDir,
|
|
49057
49347
|
setFrond: () => setFrond,
|
|
49058
49348
|
showMenu: () => showMenu,
|
|
@@ -49119,6 +49409,7 @@ var init_src3 = __esm({
|
|
|
49119
49409
|
init_wsdl();
|
|
49120
49410
|
init_htmlElement();
|
|
49121
49411
|
init_errorOverlay();
|
|
49412
|
+
init_csp();
|
|
49122
49413
|
init_ai();
|
|
49123
49414
|
init_sso();
|
|
49124
49415
|
init_push();
|
|
@@ -49327,6 +49618,15 @@ export default async function (req: Tina4Request, res: Tina4Response): Promise<v
|
|
|
49327
49618
|
<p>The Intelligent Native Application 4ramework</p>
|
|
49328
49619
|
</body>
|
|
49329
49620
|
</html>
|
|
49621
|
+
`
|
|
49622
|
+
);
|
|
49623
|
+
writeFileSync(
|
|
49624
|
+
join(targetDir, "public/style.css"),
|
|
49625
|
+
`body { font-family: -apple-system, sans-serif; max-width: 600px; margin: 80px auto; padding: 0 20px; color: #333; }
|
|
49626
|
+
h1 { font-size: 2.5em; margin-bottom: 0.2em; }
|
|
49627
|
+
p { color: #666; font-size: 1.1em; }
|
|
49628
|
+
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
|
|
49629
|
+
a { color: #2563eb; }
|
|
49330
49630
|
`
|
|
49331
49631
|
);
|
|
49332
49632
|
writeFileSync(
|
|
@@ -49335,13 +49635,7 @@ export default async function (req: Tina4Request, res: Tina4Response): Promise<v
|
|
|
49335
49635
|
<html>
|
|
49336
49636
|
<head>
|
|
49337
49637
|
<title>Tina4</title>
|
|
49338
|
-
<style>
|
|
49339
|
-
body { font-family: -apple-system, sans-serif; max-width: 600px; margin: 80px auto; padding: 0 20px; color: #333; }
|
|
49340
|
-
h1 { font-size: 2.5em; margin-bottom: 0.2em; }
|
|
49341
|
-
p { color: #666; font-size: 1.1em; }
|
|
49342
|
-
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
|
|
49343
|
-
a { color: #2563eb; }
|
|
49344
|
-
</style>
|
|
49638
|
+
<link rel="stylesheet" href="/style.css">
|
|
49345
49639
|
</head>
|
|
49346
49640
|
<body>
|
|
49347
49641
|
<h1>tina4</h1>
|