tina4-nodejs 3.13.134 → 3.13.136
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 +2 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +66 -21
- package/packages/core/dist/index.js +66 -21
- package/packages/core/public/swagger/index.html +1 -1
- package/packages/core/src/devAdmin.ts +46 -14
- package/packages/core/src/push.ts +19 -7
- package/packages/core/src/server.ts +7 -2
- package/packages/orm/dist/index.js +66 -21
- package/packages/swagger/dist/index.js +27 -3
- package/packages/swagger/src/ui.ts +28 -3
- package/types/core/src/devAdmin.d.ts +23 -1
package/CLAUDE.md
CHANGED
|
@@ -13,13 +13,13 @@ Even if the skill text is not currently loaded, these are non-negotiable:
|
|
|
13
13
|
|
|
14
14
|
The full discipline lives in `.claude/skills/tina4-maintainer/SKILL.md`; this block is the always-on floor.
|
|
15
15
|
|
|
16
|
-
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.
|
|
16
|
+
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.136)
|
|
17
17
|
|
|
18
18
|
> This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
|
|
19
19
|
|
|
20
20
|
## What This Project Is
|
|
21
21
|
|
|
22
|
-
Tina4 for Node.js/TypeScript v3.13.
|
|
22
|
+
Tina4 for Node.js/TypeScript v3.13.136 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
|
|
23
23
|
|
|
24
24
|
The philosophy: zero ceremony, batteries included, file system as source of truth.
|
|
25
25
|
|
package/package.json
CHANGED
package/packages/cli/dist/bin.js
CHANGED
|
@@ -35062,6 +35062,13 @@ function toolbarJs() {
|
|
|
35062
35062
|
el.className = 't4-ok';
|
|
35063
35063
|
el.innerHTML = 'Latest: <strong class="t4-ok">v' + latest + '</strong> — You are up to date!';
|
|
35064
35064
|
}
|
|
35065
|
+
// A check that did not happen is not a clean bill of health. The server
|
|
35066
|
+
// sends latest: null when it could not reach the registry, and saying so is
|
|
35067
|
+
// the whole point -- "up to date" here would be a guess dressed as a fact.
|
|
35068
|
+
function couldNotCheck(el, why) {
|
|
35069
|
+
el.className = 't4-err';
|
|
35070
|
+
el.textContent = 'Could not check for updates' + (why ? ' (' + why + ')' : '');
|
|
35071
|
+
}
|
|
35065
35072
|
function checkVersion() {
|
|
35066
35073
|
if (modal.style.display === 'block') { modal.style.display = 'none'; return; }
|
|
35067
35074
|
modal.style.display = 'block';
|
|
@@ -35070,6 +35077,7 @@ function toolbarJs() {
|
|
|
35070
35077
|
el.textContent = 'Checking for updates...';
|
|
35071
35078
|
fetch('/__dev/api/version-check').then(function (r) { return r.json(); }).then(function (d) {
|
|
35072
35079
|
var latest = d.latest, current = d.current;
|
|
35080
|
+
if (!latest) { couldNotCheck(el, d.error); return; }
|
|
35073
35081
|
if (latest === current) { upToDate(el, latest); return; }
|
|
35074
35082
|
var cP = current.split('.').map(Number), lP = latest.split('.').map(Number);
|
|
35075
35083
|
var isNewer = false, i, c, l;
|
|
@@ -36412,21 +36420,28 @@ var init_devAdmin = __esm({
|
|
|
36412
36420
|
};
|
|
36413
36421
|
handleVersionCheck = async (_req, res) => {
|
|
36414
36422
|
const current = TINA4_VERSION;
|
|
36415
|
-
|
|
36423
|
+
const url = process.env.TINA4_VERSION_CHECK_URL || "https://registry.npmjs.org/tina4-nodejs/latest";
|
|
36424
|
+
const failed = (why) => res.json({ current, latest: null, error: why });
|
|
36425
|
+
let data;
|
|
36416
36426
|
try {
|
|
36417
36427
|
const controller = new AbortController();
|
|
36418
|
-
const
|
|
36419
|
-
|
|
36420
|
-
|
|
36421
|
-
|
|
36422
|
-
|
|
36423
|
-
|
|
36424
|
-
|
|
36425
|
-
|
|
36428
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
36429
|
+
try {
|
|
36430
|
+
const resp = await fetch(url, {
|
|
36431
|
+
signal: controller.signal,
|
|
36432
|
+
headers: { "User-Agent": `tina4-nodejs/${current}` }
|
|
36433
|
+
});
|
|
36434
|
+
if (!resp.ok) return failed(`npm registry responded ${resp.status}`);
|
|
36435
|
+
data = await resp.json();
|
|
36436
|
+
} finally {
|
|
36437
|
+
clearTimeout(timer);
|
|
36426
36438
|
}
|
|
36427
|
-
} catch {
|
|
36439
|
+
} catch (exc) {
|
|
36440
|
+
return failed(exc instanceof Error && exc.message ? exc.message : String(exc));
|
|
36428
36441
|
}
|
|
36429
|
-
|
|
36442
|
+
const latest = typeof data.version === "string" ? data.version : "";
|
|
36443
|
+
if (!latest) return failed("npm registry did not report a version");
|
|
36444
|
+
return res.json({ current, latest });
|
|
36430
36445
|
};
|
|
36431
36446
|
handleThoughts = (req2, res) => {
|
|
36432
36447
|
const url = new URL(req2.url ?? "/", "http://localhost");
|
|
@@ -37940,13 +37955,37 @@ function swaggerEnabled() {
|
|
|
37940
37955
|
return ["true", "1", "yes", "on"].includes(raw);
|
|
37941
37956
|
}
|
|
37942
37957
|
function createSwaggerRoutes(getSpec) {
|
|
37958
|
+
const serveUi = async (_req, res) => {
|
|
37959
|
+
res.html(SWAGGER_UI_HTML("/swagger/openapi.json"));
|
|
37960
|
+
};
|
|
37943
37961
|
return [
|
|
37944
37962
|
{
|
|
37945
37963
|
method: "GET",
|
|
37946
37964
|
pattern: "/swagger",
|
|
37947
|
-
handler:
|
|
37948
|
-
|
|
37949
|
-
|
|
37965
|
+
handler: serveUi
|
|
37966
|
+
},
|
|
37967
|
+
{
|
|
37968
|
+
// The trailing-slash form, registered rather than left to fall through.
|
|
37969
|
+
//
|
|
37970
|
+
// Matching "/foo/" against a "/foo" route is opt-in via
|
|
37971
|
+
// TINA4_TRAILING_SLASH_REDIRECT and OFF by default, so /swagger/ missed
|
|
37972
|
+
// this route and was answered by the framework-bundled
|
|
37973
|
+
// public/swagger/index.html instead. That mattered twice over. It used to
|
|
37974
|
+
// be a 200 carrying a permanently empty UI, because the bundled file asked
|
|
37975
|
+
// for an unsubstituted {SWAGGER_ROUTE}/swagger.json -- fixed in that file.
|
|
37976
|
+
// And it is a SECOND Swagger UI implementation: the bundled one hardcodes
|
|
37977
|
+
// cdnjs, while the page this handler renders loads from
|
|
37978
|
+
// TINA4_SWAGGER_UI_CDN, so an air-gapped deployment pointing that at a
|
|
37979
|
+
// local mirror silently kept reaching cdnjs on this one path.
|
|
37980
|
+
//
|
|
37981
|
+
// Registering it keeps the fix inside swagger rather than changing how
|
|
37982
|
+
// every route treats trailing slashes, satisfies the shared contract that
|
|
37983
|
+
// already requires a 200 here, and matches python and ruby, which both
|
|
37984
|
+
// serve /swagger and /swagger/ with no env var set. Excluded from the
|
|
37985
|
+
// generated document by INTERNAL_PREFIXES like /swagger itself.
|
|
37986
|
+
method: "GET",
|
|
37987
|
+
pattern: "/swagger/",
|
|
37988
|
+
handler: serveUi
|
|
37950
37989
|
},
|
|
37951
37990
|
{
|
|
37952
37991
|
method: "GET",
|
|
@@ -39650,7 +39689,7 @@ async function configureSwagger(router, ormDir, modelsDir) {
|
|
|
39650
39689
|
try {
|
|
39651
39690
|
const swagger = await Promise.resolve().then(() => (init_src2(), src_exports2));
|
|
39652
39691
|
enabled = swagger.swaggerEnabled();
|
|
39653
|
-
if (!
|
|
39692
|
+
if (!enabled) {
|
|
39654
39693
|
throw new Error("__swagger_disabled__");
|
|
39655
39694
|
}
|
|
39656
39695
|
let modelDefs = [];
|
|
@@ -44258,6 +44297,9 @@ function decodeBase64Url(value, name) {
|
|
|
44258
44297
|
}
|
|
44259
44298
|
return Buffer.from(value.replace(/-/g, "+").replace(/_/g, "/"), "base64");
|
|
44260
44299
|
}
|
|
44300
|
+
function pad32(value) {
|
|
44301
|
+
return value.length >= 32 ? value : Buffer.concat([Buffer.alloc(32 - value.length), value]);
|
|
44302
|
+
}
|
|
44261
44303
|
function vapidPrivateKey(rawPrivate, rawPublic) {
|
|
44262
44304
|
if (rawPublic.length !== 65 || rawPublic[0] !== 4) throw new PushError("P-256 public keys must be 65-byte uncompressed points");
|
|
44263
44305
|
const x = encodeBase64Url(rawPublic.subarray(1, 33));
|
|
@@ -44266,7 +44308,7 @@ function vapidPrivateKey(rawPrivate, rawPublic) {
|
|
|
44266
44308
|
key: {
|
|
44267
44309
|
kty: "EC",
|
|
44268
44310
|
crv: "P-256",
|
|
44269
|
-
d: encodeBase64Url(rawPrivate),
|
|
44311
|
+
d: encodeBase64Url(pad32(rawPrivate)),
|
|
44270
44312
|
x,
|
|
44271
44313
|
y,
|
|
44272
44314
|
ext: true
|
|
@@ -44360,8 +44402,10 @@ function generateVapidKeys() {
|
|
|
44360
44402
|
const ecdh = createECDH(CURVE);
|
|
44361
44403
|
ecdh.generateKeys();
|
|
44362
44404
|
return {
|
|
44405
|
+
// getPublicKey keeps its 65-byte width; getPrivateKey drops a leading zero
|
|
44406
|
+
// byte, so the scalar must be padded to the fixed field width.
|
|
44363
44407
|
publicKey: encodeBase64Url(ecdh.getPublicKey(void 0, "uncompressed")),
|
|
44364
|
-
privateKey: encodeBase64Url(ecdh.getPrivateKey())
|
|
44408
|
+
privateKey: encodeBase64Url(pad32(ecdh.getPrivateKey()))
|
|
44365
44409
|
};
|
|
44366
44410
|
}
|
|
44367
44411
|
var CURVE, RECORD_SIZE, MAX_PAYLOAD, PushError, Push;
|
|
@@ -44453,10 +44497,11 @@ var init_push = __esm({
|
|
|
44453
44497
|
TTL: String(this.options.ttl ?? 60),
|
|
44454
44498
|
...this.options.urgency ? { Urgency: this.options.urgency } : {}
|
|
44455
44499
|
},
|
|
44456
|
-
// Node's fetch accepts Buffer at runtime
|
|
44457
|
-
//
|
|
44458
|
-
//
|
|
44459
|
-
//
|
|
44500
|
+
// Node's fetch accepts a Buffer at runtime. The typecheck build
|
|
44501
|
+
// (lib ES2022, no DOM) does not know the global `BodyInit` name, and
|
|
44502
|
+
// the DOM declaration narrows it to ArrayBuffer-backed views — a
|
|
44503
|
+
// `Uint8Array` (which a Buffer is) satisfies both, so cast to that and
|
|
44504
|
+
// keep the encrypted bytes intact rather than converting them to text.
|
|
44460
44505
|
body
|
|
44461
44506
|
});
|
|
44462
44507
|
} catch (error2) {
|
|
@@ -35041,6 +35041,13 @@ function toolbarJs() {
|
|
|
35041
35041
|
el.className = 't4-ok';
|
|
35042
35042
|
el.innerHTML = 'Latest: <strong class="t4-ok">v' + latest + '</strong> — You are up to date!';
|
|
35043
35043
|
}
|
|
35044
|
+
// A check that did not happen is not a clean bill of health. The server
|
|
35045
|
+
// sends latest: null when it could not reach the registry, and saying so is
|
|
35046
|
+
// the whole point -- "up to date" here would be a guess dressed as a fact.
|
|
35047
|
+
function couldNotCheck(el, why) {
|
|
35048
|
+
el.className = 't4-err';
|
|
35049
|
+
el.textContent = 'Could not check for updates' + (why ? ' (' + why + ')' : '');
|
|
35050
|
+
}
|
|
35044
35051
|
function checkVersion() {
|
|
35045
35052
|
if (modal.style.display === 'block') { modal.style.display = 'none'; return; }
|
|
35046
35053
|
modal.style.display = 'block';
|
|
@@ -35049,6 +35056,7 @@ function toolbarJs() {
|
|
|
35049
35056
|
el.textContent = 'Checking for updates...';
|
|
35050
35057
|
fetch('/__dev/api/version-check').then(function (r) { return r.json(); }).then(function (d) {
|
|
35051
35058
|
var latest = d.latest, current = d.current;
|
|
35059
|
+
if (!latest) { couldNotCheck(el, d.error); return; }
|
|
35052
35060
|
if (latest === current) { upToDate(el, latest); return; }
|
|
35053
35061
|
var cP = current.split('.').map(Number), lP = latest.split('.').map(Number);
|
|
35054
35062
|
var isNewer = false, i, c, l;
|
|
@@ -36391,21 +36399,28 @@ var init_devAdmin = __esm({
|
|
|
36391
36399
|
};
|
|
36392
36400
|
handleVersionCheck = async (_req, res) => {
|
|
36393
36401
|
const current = TINA4_VERSION;
|
|
36394
|
-
|
|
36402
|
+
const url = process.env.TINA4_VERSION_CHECK_URL || "https://registry.npmjs.org/tina4-nodejs/latest";
|
|
36403
|
+
const failed = (why) => res.json({ current, latest: null, error: why });
|
|
36404
|
+
let data;
|
|
36395
36405
|
try {
|
|
36396
36406
|
const controller = new AbortController();
|
|
36397
|
-
const
|
|
36398
|
-
|
|
36399
|
-
|
|
36400
|
-
|
|
36401
|
-
|
|
36402
|
-
|
|
36403
|
-
|
|
36404
|
-
|
|
36407
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
36408
|
+
try {
|
|
36409
|
+
const resp = await fetch(url, {
|
|
36410
|
+
signal: controller.signal,
|
|
36411
|
+
headers: { "User-Agent": `tina4-nodejs/${current}` }
|
|
36412
|
+
});
|
|
36413
|
+
if (!resp.ok) return failed(`npm registry responded ${resp.status}`);
|
|
36414
|
+
data = await resp.json();
|
|
36415
|
+
} finally {
|
|
36416
|
+
clearTimeout(timer);
|
|
36405
36417
|
}
|
|
36406
|
-
} catch {
|
|
36418
|
+
} catch (exc) {
|
|
36419
|
+
return failed(exc instanceof Error && exc.message ? exc.message : String(exc));
|
|
36407
36420
|
}
|
|
36408
|
-
|
|
36421
|
+
const latest = typeof data.version === "string" ? data.version : "";
|
|
36422
|
+
if (!latest) return failed("npm registry did not report a version");
|
|
36423
|
+
return res.json({ current, latest });
|
|
36409
36424
|
};
|
|
36410
36425
|
handleThoughts = (req2, res) => {
|
|
36411
36426
|
const url = new URL(req2.url ?? "/", "http://localhost");
|
|
@@ -37919,13 +37934,37 @@ function swaggerEnabled() {
|
|
|
37919
37934
|
return ["true", "1", "yes", "on"].includes(raw);
|
|
37920
37935
|
}
|
|
37921
37936
|
function createSwaggerRoutes(getSpec) {
|
|
37937
|
+
const serveUi = async (_req, res) => {
|
|
37938
|
+
res.html(SWAGGER_UI_HTML("/swagger/openapi.json"));
|
|
37939
|
+
};
|
|
37922
37940
|
return [
|
|
37923
37941
|
{
|
|
37924
37942
|
method: "GET",
|
|
37925
37943
|
pattern: "/swagger",
|
|
37926
|
-
handler:
|
|
37927
|
-
|
|
37928
|
-
|
|
37944
|
+
handler: serveUi
|
|
37945
|
+
},
|
|
37946
|
+
{
|
|
37947
|
+
// The trailing-slash form, registered rather than left to fall through.
|
|
37948
|
+
//
|
|
37949
|
+
// Matching "/foo/" against a "/foo" route is opt-in via
|
|
37950
|
+
// TINA4_TRAILING_SLASH_REDIRECT and OFF by default, so /swagger/ missed
|
|
37951
|
+
// this route and was answered by the framework-bundled
|
|
37952
|
+
// public/swagger/index.html instead. That mattered twice over. It used to
|
|
37953
|
+
// be a 200 carrying a permanently empty UI, because the bundled file asked
|
|
37954
|
+
// for an unsubstituted {SWAGGER_ROUTE}/swagger.json -- fixed in that file.
|
|
37955
|
+
// And it is a SECOND Swagger UI implementation: the bundled one hardcodes
|
|
37956
|
+
// cdnjs, while the page this handler renders loads from
|
|
37957
|
+
// TINA4_SWAGGER_UI_CDN, so an air-gapped deployment pointing that at a
|
|
37958
|
+
// local mirror silently kept reaching cdnjs on this one path.
|
|
37959
|
+
//
|
|
37960
|
+
// Registering it keeps the fix inside swagger rather than changing how
|
|
37961
|
+
// every route treats trailing slashes, satisfies the shared contract that
|
|
37962
|
+
// already requires a 200 here, and matches python and ruby, which both
|
|
37963
|
+
// serve /swagger and /swagger/ with no env var set. Excluded from the
|
|
37964
|
+
// generated document by INTERNAL_PREFIXES like /swagger itself.
|
|
37965
|
+
method: "GET",
|
|
37966
|
+
pattern: "/swagger/",
|
|
37967
|
+
handler: serveUi
|
|
37929
37968
|
},
|
|
37930
37969
|
{
|
|
37931
37970
|
method: "GET",
|
|
@@ -39629,7 +39668,7 @@ async function configureSwagger(router, ormDir, modelsDir) {
|
|
|
39629
39668
|
try {
|
|
39630
39669
|
const swagger = await Promise.resolve().then(() => (init_src2(), src_exports2));
|
|
39631
39670
|
enabled = swagger.swaggerEnabled();
|
|
39632
|
-
if (!
|
|
39671
|
+
if (!enabled) {
|
|
39633
39672
|
throw new Error("__swagger_disabled__");
|
|
39634
39673
|
}
|
|
39635
39674
|
let modelDefs = [];
|
|
@@ -44219,6 +44258,9 @@ function decodeBase64Url(value, name) {
|
|
|
44219
44258
|
}
|
|
44220
44259
|
return Buffer.from(value.replace(/-/g, "+").replace(/_/g, "/"), "base64");
|
|
44221
44260
|
}
|
|
44261
|
+
function pad32(value) {
|
|
44262
|
+
return value.length >= 32 ? value : Buffer.concat([Buffer.alloc(32 - value.length), value]);
|
|
44263
|
+
}
|
|
44222
44264
|
function vapidPrivateKey(rawPrivate, rawPublic) {
|
|
44223
44265
|
if (rawPublic.length !== 65 || rawPublic[0] !== 4) throw new PushError("P-256 public keys must be 65-byte uncompressed points");
|
|
44224
44266
|
const x = encodeBase64Url(rawPublic.subarray(1, 33));
|
|
@@ -44227,7 +44269,7 @@ function vapidPrivateKey(rawPrivate, rawPublic) {
|
|
|
44227
44269
|
key: {
|
|
44228
44270
|
kty: "EC",
|
|
44229
44271
|
crv: "P-256",
|
|
44230
|
-
d: encodeBase64Url(rawPrivate),
|
|
44272
|
+
d: encodeBase64Url(pad32(rawPrivate)),
|
|
44231
44273
|
x,
|
|
44232
44274
|
y,
|
|
44233
44275
|
ext: true
|
|
@@ -44321,8 +44363,10 @@ function generateVapidKeys() {
|
|
|
44321
44363
|
const ecdh = createECDH(CURVE);
|
|
44322
44364
|
ecdh.generateKeys();
|
|
44323
44365
|
return {
|
|
44366
|
+
// getPublicKey keeps its 65-byte width; getPrivateKey drops a leading zero
|
|
44367
|
+
// byte, so the scalar must be padded to the fixed field width.
|
|
44324
44368
|
publicKey: encodeBase64Url(ecdh.getPublicKey(void 0, "uncompressed")),
|
|
44325
|
-
privateKey: encodeBase64Url(ecdh.getPrivateKey())
|
|
44369
|
+
privateKey: encodeBase64Url(pad32(ecdh.getPrivateKey()))
|
|
44326
44370
|
};
|
|
44327
44371
|
}
|
|
44328
44372
|
var CURVE, RECORD_SIZE, MAX_PAYLOAD, PushError, Push;
|
|
@@ -44414,10 +44458,11 @@ var init_push = __esm({
|
|
|
44414
44458
|
TTL: String(this.options.ttl ?? 60),
|
|
44415
44459
|
...this.options.urgency ? { Urgency: this.options.urgency } : {}
|
|
44416
44460
|
},
|
|
44417
|
-
// Node's fetch accepts Buffer at runtime
|
|
44418
|
-
//
|
|
44419
|
-
//
|
|
44420
|
-
//
|
|
44461
|
+
// Node's fetch accepts a Buffer at runtime. The typecheck build
|
|
44462
|
+
// (lib ES2022, no DOM) does not know the global `BodyInit` name, and
|
|
44463
|
+
// the DOM declaration narrows it to ArrayBuffer-backed views — a
|
|
44464
|
+
// `Uint8Array` (which a Buffer is) satisfies both, so cast to that and
|
|
44465
|
+
// keep the encrypted bytes intact rather than converting them to text.
|
|
44421
44466
|
body
|
|
44422
44467
|
});
|
|
44423
44468
|
} catch (error2) {
|
|
@@ -2069,24 +2069,48 @@ function handleGalleryDeploy(router: Router): RouteHandler {
|
|
|
2069
2069
|
// Version check — proxy to npm registry to avoid browser CORS errors
|
|
2070
2070
|
// ---------------------------------------------------------------------------
|
|
2071
2071
|
|
|
2072
|
-
|
|
2072
|
+
/**
|
|
2073
|
+
* Version check — a check that did not happen says so.
|
|
2074
|
+
*
|
|
2075
|
+
* This used to fall back to `latest = current` on any failure, and the toolbar
|
|
2076
|
+
* renders that as a green "You are up to date!" — so a developer several
|
|
2077
|
+
* releases behind, on a machine with no route out, was told the opposite of the
|
|
2078
|
+
* truth, and the toolbar's own "Could not check for updates" branch could never
|
|
2079
|
+
* fire because the failure arrived as a success. `latest` is `null` when the
|
|
2080
|
+
* check could not be made, and `error` says why. The registry URL is
|
|
2081
|
+
* `TINA4_VERSION_CHECK_URL` when set (a mirror, or a test's own server), else
|
|
2082
|
+
* npm. Mirrors Python `tina4_python.dev_admin._api_version_check`.
|
|
2083
|
+
*/
|
|
2084
|
+
export const handleVersionCheck: RouteHandler = async (_req, res) => {
|
|
2073
2085
|
const current = TINA4_VERSION;
|
|
2074
|
-
|
|
2086
|
+
const url =
|
|
2087
|
+
process.env.TINA4_VERSION_CHECK_URL ||
|
|
2088
|
+
"https://registry.npmjs.org/tina4-nodejs/latest";
|
|
2089
|
+
const failed = (why: string) => res.json({ current, latest: null, error: why });
|
|
2090
|
+
|
|
2091
|
+
let data: Record<string, unknown>;
|
|
2075
2092
|
try {
|
|
2076
2093
|
const controller = new AbortController();
|
|
2077
|
-
const
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
if (
|
|
2094
|
+
const timer = setTimeout(() => controller.abort(), 5000);
|
|
2095
|
+
try {
|
|
2096
|
+
const resp = await fetch(url, {
|
|
2097
|
+
signal: controller.signal,
|
|
2098
|
+
headers: { "User-Agent": `tina4-nodejs/${current}` },
|
|
2099
|
+
});
|
|
2100
|
+
// Reaching the registry is not the same as a 200 with a body.
|
|
2101
|
+
if (!resp.ok) return failed(`npm registry responded ${resp.status}`);
|
|
2102
|
+
data = (await resp.json()) as Record<string, unknown>;
|
|
2103
|
+
} finally {
|
|
2104
|
+
clearTimeout(timer);
|
|
2085
2105
|
}
|
|
2086
|
-
} catch {
|
|
2087
|
-
//
|
|
2106
|
+
} catch (exc) {
|
|
2107
|
+
// offline, timeout, DNS, unreadable body
|
|
2108
|
+
return failed(exc instanceof Error && exc.message ? exc.message : String(exc));
|
|
2088
2109
|
}
|
|
2089
|
-
|
|
2110
|
+
// An answer we cannot read a version out of is the same lie by another route.
|
|
2111
|
+
const latest = typeof data.version === "string" ? data.version : "";
|
|
2112
|
+
if (!latest) return failed("npm registry did not report a version");
|
|
2113
|
+
return res.json({ current, latest });
|
|
2090
2114
|
};
|
|
2091
2115
|
|
|
2092
2116
|
// ---------------------------------------------------------------------------
|
|
@@ -3019,7 +3043,7 @@ function toolbarCss(): string {
|
|
|
3019
3043
|
* starts when the toolbar's `data-reload` is "1" (reload not suppressed for this
|
|
3020
3044
|
* request/port). Mirrors PHP DevAdmin::toolbarJs().
|
|
3021
3045
|
*/
|
|
3022
|
-
function toolbarJs(): string {
|
|
3046
|
+
export function toolbarJs(): string {
|
|
3023
3047
|
return `(function () {
|
|
3024
3048
|
var bar = document.getElementById('tina4-dev-toolbar');
|
|
3025
3049
|
if (!bar) { return; }
|
|
@@ -3029,6 +3053,13 @@ function toolbarJs(): string {
|
|
|
3029
3053
|
el.className = 't4-ok';
|
|
3030
3054
|
el.innerHTML = 'Latest: <strong class="t4-ok">v' + latest + '</strong> — You are up to date!';
|
|
3031
3055
|
}
|
|
3056
|
+
// A check that did not happen is not a clean bill of health. The server
|
|
3057
|
+
// sends latest: null when it could not reach the registry, and saying so is
|
|
3058
|
+
// the whole point -- "up to date" here would be a guess dressed as a fact.
|
|
3059
|
+
function couldNotCheck(el, why) {
|
|
3060
|
+
el.className = 't4-err';
|
|
3061
|
+
el.textContent = 'Could not check for updates' + (why ? ' (' + why + ')' : '');
|
|
3062
|
+
}
|
|
3032
3063
|
function checkVersion() {
|
|
3033
3064
|
if (modal.style.display === 'block') { modal.style.display = 'none'; return; }
|
|
3034
3065
|
modal.style.display = 'block';
|
|
@@ -3037,6 +3068,7 @@ function toolbarJs(): string {
|
|
|
3037
3068
|
el.textContent = 'Checking for updates...';
|
|
3038
3069
|
fetch('/__dev/api/version-check').then(function (r) { return r.json(); }).then(function (d) {
|
|
3039
3070
|
var latest = d.latest, current = d.current;
|
|
3071
|
+
if (!latest) { couldNotCheck(el, d.error); return; }
|
|
3040
3072
|
if (latest === current) { upToDate(el, latest); return; }
|
|
3041
3073
|
var cP = current.split('.').map(Number), lP = latest.split('.').map(Number);
|
|
3042
3074
|
var isNewer = false, i, c, l;
|
|
@@ -69,6 +69,15 @@ function decodeBase64Url(value: string, name: string): Buffer {
|
|
|
69
69
|
return Buffer.from(value.replace(/-/g, "+").replace(/_/g, "/"), "base64");
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// Left-pad big-endian EC material to the fixed 32-byte P-256 field width.
|
|
73
|
+
// createECDH().getPrivateKey() and other raw EC outputs drop a leading zero
|
|
74
|
+
// byte, so a scalar whose top byte is zero comes back short (~0.5% of keys);
|
|
75
|
+
// the JWK `d` and the stored VAPID key are fixed-width, so a short value is a
|
|
76
|
+
// malformed key.
|
|
77
|
+
function pad32(value: Buffer): Buffer {
|
|
78
|
+
return value.length >= 32 ? value : Buffer.concat([Buffer.alloc(32 - value.length), value]);
|
|
79
|
+
}
|
|
80
|
+
|
|
72
81
|
function vapidPrivateKey(rawPrivate: Buffer, rawPublic: Buffer) {
|
|
73
82
|
if (rawPublic.length !== 65 || rawPublic[0] !== 0x04) throw new PushError("P-256 public keys must be 65-byte uncompressed points");
|
|
74
83
|
const x = encodeBase64Url(rawPublic.subarray(1, 33));
|
|
@@ -77,7 +86,7 @@ function vapidPrivateKey(rawPrivate: Buffer, rawPublic: Buffer) {
|
|
|
77
86
|
key: {
|
|
78
87
|
kty: "EC",
|
|
79
88
|
crv: "P-256",
|
|
80
|
-
d: encodeBase64Url(rawPrivate),
|
|
89
|
+
d: encodeBase64Url(pad32(rawPrivate)),
|
|
81
90
|
x,
|
|
82
91
|
y,
|
|
83
92
|
ext: true,
|
|
@@ -180,8 +189,10 @@ export function generateVapidKeys(): { publicKey: string; privateKey: string } {
|
|
|
180
189
|
const ecdh = createECDH(CURVE);
|
|
181
190
|
ecdh.generateKeys();
|
|
182
191
|
return {
|
|
192
|
+
// getPublicKey keeps its 65-byte width; getPrivateKey drops a leading zero
|
|
193
|
+
// byte, so the scalar must be padded to the fixed field width.
|
|
183
194
|
publicKey: encodeBase64Url(ecdh.getPublicKey(undefined, "uncompressed")),
|
|
184
|
-
privateKey: encodeBase64Url(ecdh.getPrivateKey()),
|
|
195
|
+
privateKey: encodeBase64Url(pad32(ecdh.getPrivateKey())),
|
|
185
196
|
};
|
|
186
197
|
}
|
|
187
198
|
|
|
@@ -259,11 +270,12 @@ export class Push {
|
|
|
259
270
|
TTL: String(this.options.ttl ?? 60),
|
|
260
271
|
...(this.options.urgency ? { Urgency: this.options.urgency } : {}),
|
|
261
272
|
},
|
|
262
|
-
// Node's fetch accepts Buffer at runtime
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
|
|
273
|
+
// Node's fetch accepts a Buffer at runtime. The typecheck build
|
|
274
|
+
// (lib ES2022, no DOM) does not know the global `BodyInit` name, and
|
|
275
|
+
// the DOM declaration narrows it to ArrayBuffer-backed views — a
|
|
276
|
+
// `Uint8Array` (which a Buffer is) satisfies both, so cast to that and
|
|
277
|
+
// keep the encrypted bytes intact rather than converting them to text.
|
|
278
|
+
body: body as unknown as Uint8Array,
|
|
267
279
|
});
|
|
268
280
|
} catch (error) {
|
|
269
281
|
throw new PushError(`Web Push request failed: ${String(error)}`);
|
|
@@ -2598,8 +2598,13 @@ async function configureSwagger(router: Router, ormDir: string, modelsDir: strin
|
|
|
2598
2598
|
// Single source of truth for BOTH the gated routes and the bundled
|
|
2599
2599
|
// public/swagger assets (which static serving would otherwise expose).
|
|
2600
2600
|
enabled = swagger.swaggerEnabled();
|
|
2601
|
-
if (!
|
|
2602
|
-
// Skip the rest of the swagger block when disabled.
|
|
2601
|
+
if (!enabled) {
|
|
2602
|
+
// Skip the rest of the swagger block when disabled. Gate on the LOCAL
|
|
2603
|
+
// `enabled` just read from swaggerEnabled(): the module-level
|
|
2604
|
+
// `swaggerAssetsEnabled` is only assigned from this function's RETURN
|
|
2605
|
+
// (see the call site), so inside here it is still its boot-time false --
|
|
2606
|
+
// reading it skipped the block even when swagger was enabled, so
|
|
2607
|
+
// /swagger/openapi.json 404'd on an enabled server.
|
|
2603
2608
|
throw new Error("__swagger_disabled__");
|
|
2604
2609
|
}
|
|
2605
2610
|
|
|
@@ -23330,6 +23330,13 @@ function toolbarJs() {
|
|
|
23330
23330
|
el.className = 't4-ok';
|
|
23331
23331
|
el.innerHTML = 'Latest: <strong class="t4-ok">v' + latest + '</strong> — You are up to date!';
|
|
23332
23332
|
}
|
|
23333
|
+
// A check that did not happen is not a clean bill of health. The server
|
|
23334
|
+
// sends latest: null when it could not reach the registry, and saying so is
|
|
23335
|
+
// the whole point -- "up to date" here would be a guess dressed as a fact.
|
|
23336
|
+
function couldNotCheck(el, why) {
|
|
23337
|
+
el.className = 't4-err';
|
|
23338
|
+
el.textContent = 'Could not check for updates' + (why ? ' (' + why + ')' : '');
|
|
23339
|
+
}
|
|
23333
23340
|
function checkVersion() {
|
|
23334
23341
|
if (modal.style.display === 'block') { modal.style.display = 'none'; return; }
|
|
23335
23342
|
modal.style.display = 'block';
|
|
@@ -23338,6 +23345,7 @@ function toolbarJs() {
|
|
|
23338
23345
|
el.textContent = 'Checking for updates...';
|
|
23339
23346
|
fetch('/__dev/api/version-check').then(function (r) { return r.json(); }).then(function (d) {
|
|
23340
23347
|
var latest = d.latest, current = d.current;
|
|
23348
|
+
if (!latest) { couldNotCheck(el, d.error); return; }
|
|
23341
23349
|
if (latest === current) { upToDate(el, latest); return; }
|
|
23342
23350
|
var cP = current.split('.').map(Number), lP = latest.split('.').map(Number);
|
|
23343
23351
|
var isNewer = false, i, c, l;
|
|
@@ -24680,21 +24688,28 @@ var init_devAdmin = __esm({
|
|
|
24680
24688
|
};
|
|
24681
24689
|
handleVersionCheck = async (_req, res) => {
|
|
24682
24690
|
const current = TINA4_VERSION;
|
|
24683
|
-
|
|
24691
|
+
const url = process.env.TINA4_VERSION_CHECK_URL || "https://registry.npmjs.org/tina4-nodejs/latest";
|
|
24692
|
+
const failed = (why) => res.json({ current, latest: null, error: why });
|
|
24693
|
+
let data;
|
|
24684
24694
|
try {
|
|
24685
24695
|
const controller = new AbortController();
|
|
24686
|
-
const
|
|
24687
|
-
|
|
24688
|
-
|
|
24689
|
-
|
|
24690
|
-
|
|
24691
|
-
|
|
24692
|
-
|
|
24693
|
-
|
|
24696
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
24697
|
+
try {
|
|
24698
|
+
const resp = await fetch(url, {
|
|
24699
|
+
signal: controller.signal,
|
|
24700
|
+
headers: { "User-Agent": `tina4-nodejs/${current}` }
|
|
24701
|
+
});
|
|
24702
|
+
if (!resp.ok) return failed(`npm registry responded ${resp.status}`);
|
|
24703
|
+
data = await resp.json();
|
|
24704
|
+
} finally {
|
|
24705
|
+
clearTimeout(timer);
|
|
24694
24706
|
}
|
|
24695
|
-
} catch {
|
|
24707
|
+
} catch (exc) {
|
|
24708
|
+
return failed(exc instanceof Error && exc.message ? exc.message : String(exc));
|
|
24696
24709
|
}
|
|
24697
|
-
|
|
24710
|
+
const latest = typeof data.version === "string" ? data.version : "";
|
|
24711
|
+
if (!latest) return failed("npm registry did not report a version");
|
|
24712
|
+
return res.json({ current, latest });
|
|
24698
24713
|
};
|
|
24699
24714
|
handleThoughts = (req2, res) => {
|
|
24700
24715
|
const url = new URL(req2.url ?? "/", "http://localhost");
|
|
@@ -26208,13 +26223,37 @@ function swaggerEnabled() {
|
|
|
26208
26223
|
return ["true", "1", "yes", "on"].includes(raw);
|
|
26209
26224
|
}
|
|
26210
26225
|
function createSwaggerRoutes(getSpec) {
|
|
26226
|
+
const serveUi = async (_req, res) => {
|
|
26227
|
+
res.html(SWAGGER_UI_HTML("/swagger/openapi.json"));
|
|
26228
|
+
};
|
|
26211
26229
|
return [
|
|
26212
26230
|
{
|
|
26213
26231
|
method: "GET",
|
|
26214
26232
|
pattern: "/swagger",
|
|
26215
|
-
handler:
|
|
26216
|
-
|
|
26217
|
-
|
|
26233
|
+
handler: serveUi
|
|
26234
|
+
},
|
|
26235
|
+
{
|
|
26236
|
+
// The trailing-slash form, registered rather than left to fall through.
|
|
26237
|
+
//
|
|
26238
|
+
// Matching "/foo/" against a "/foo" route is opt-in via
|
|
26239
|
+
// TINA4_TRAILING_SLASH_REDIRECT and OFF by default, so /swagger/ missed
|
|
26240
|
+
// this route and was answered by the framework-bundled
|
|
26241
|
+
// public/swagger/index.html instead. That mattered twice over. It used to
|
|
26242
|
+
// be a 200 carrying a permanently empty UI, because the bundled file asked
|
|
26243
|
+
// for an unsubstituted {SWAGGER_ROUTE}/swagger.json -- fixed in that file.
|
|
26244
|
+
// And it is a SECOND Swagger UI implementation: the bundled one hardcodes
|
|
26245
|
+
// cdnjs, while the page this handler renders loads from
|
|
26246
|
+
// TINA4_SWAGGER_UI_CDN, so an air-gapped deployment pointing that at a
|
|
26247
|
+
// local mirror silently kept reaching cdnjs on this one path.
|
|
26248
|
+
//
|
|
26249
|
+
// Registering it keeps the fix inside swagger rather than changing how
|
|
26250
|
+
// every route treats trailing slashes, satisfies the shared contract that
|
|
26251
|
+
// already requires a 200 here, and matches python and ruby, which both
|
|
26252
|
+
// serve /swagger and /swagger/ with no env var set. Excluded from the
|
|
26253
|
+
// generated document by INTERNAL_PREFIXES like /swagger itself.
|
|
26254
|
+
method: "GET",
|
|
26255
|
+
pattern: "/swagger/",
|
|
26256
|
+
handler: serveUi
|
|
26218
26257
|
},
|
|
26219
26258
|
{
|
|
26220
26259
|
method: "GET",
|
|
@@ -27918,7 +27957,7 @@ async function configureSwagger(router, ormDir, modelsDir) {
|
|
|
27918
27957
|
try {
|
|
27919
27958
|
const swagger = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
27920
27959
|
enabled = swagger.swaggerEnabled();
|
|
27921
|
-
if (!
|
|
27960
|
+
if (!enabled) {
|
|
27922
27961
|
throw new Error("__swagger_disabled__");
|
|
27923
27962
|
}
|
|
27924
27963
|
let modelDefs = [];
|
|
@@ -33039,6 +33078,9 @@ function decodeBase64Url(value, name) {
|
|
|
33039
33078
|
}
|
|
33040
33079
|
return Buffer.from(value.replace(/-/g, "+").replace(/_/g, "/"), "base64");
|
|
33041
33080
|
}
|
|
33081
|
+
function pad32(value) {
|
|
33082
|
+
return value.length >= 32 ? value : Buffer.concat([Buffer.alloc(32 - value.length), value]);
|
|
33083
|
+
}
|
|
33042
33084
|
function vapidPrivateKey(rawPrivate, rawPublic) {
|
|
33043
33085
|
if (rawPublic.length !== 65 || rawPublic[0] !== 4) throw new PushError("P-256 public keys must be 65-byte uncompressed points");
|
|
33044
33086
|
const x = encodeBase64Url(rawPublic.subarray(1, 33));
|
|
@@ -33047,7 +33089,7 @@ function vapidPrivateKey(rawPrivate, rawPublic) {
|
|
|
33047
33089
|
key: {
|
|
33048
33090
|
kty: "EC",
|
|
33049
33091
|
crv: "P-256",
|
|
33050
|
-
d: encodeBase64Url(rawPrivate),
|
|
33092
|
+
d: encodeBase64Url(pad32(rawPrivate)),
|
|
33051
33093
|
x,
|
|
33052
33094
|
y,
|
|
33053
33095
|
ext: true
|
|
@@ -33141,8 +33183,10 @@ function generateVapidKeys() {
|
|
|
33141
33183
|
const ecdh = createECDH(CURVE);
|
|
33142
33184
|
ecdh.generateKeys();
|
|
33143
33185
|
return {
|
|
33186
|
+
// getPublicKey keeps its 65-byte width; getPrivateKey drops a leading zero
|
|
33187
|
+
// byte, so the scalar must be padded to the fixed field width.
|
|
33144
33188
|
publicKey: encodeBase64Url(ecdh.getPublicKey(void 0, "uncompressed")),
|
|
33145
|
-
privateKey: encodeBase64Url(ecdh.getPrivateKey())
|
|
33189
|
+
privateKey: encodeBase64Url(pad32(ecdh.getPrivateKey()))
|
|
33146
33190
|
};
|
|
33147
33191
|
}
|
|
33148
33192
|
var CURVE, RECORD_SIZE, MAX_PAYLOAD, PushError, Push;
|
|
@@ -33234,10 +33278,11 @@ var init_push = __esm({
|
|
|
33234
33278
|
TTL: String(this.options.ttl ?? 60),
|
|
33235
33279
|
...this.options.urgency ? { Urgency: this.options.urgency } : {}
|
|
33236
33280
|
},
|
|
33237
|
-
// Node's fetch accepts Buffer at runtime
|
|
33238
|
-
//
|
|
33239
|
-
//
|
|
33240
|
-
//
|
|
33281
|
+
// Node's fetch accepts a Buffer at runtime. The typecheck build
|
|
33282
|
+
// (lib ES2022, no DOM) does not know the global `BodyInit` name, and
|
|
33283
|
+
// the DOM declaration narrows it to ArrayBuffer-backed views — a
|
|
33284
|
+
// `Uint8Array` (which a Buffer is) satisfies both, so cast to that and
|
|
33285
|
+
// keep the encrypted bytes intact rather than converting them to text.
|
|
33241
33286
|
body
|
|
33242
33287
|
});
|
|
33243
33288
|
} catch (error2) {
|
|
@@ -521,13 +521,37 @@ function swaggerEnabled() {
|
|
|
521
521
|
return ["true", "1", "yes", "on"].includes(raw);
|
|
522
522
|
}
|
|
523
523
|
function createSwaggerRoutes(getSpec) {
|
|
524
|
+
const serveUi = async (_req, res) => {
|
|
525
|
+
res.html(SWAGGER_UI_HTML("/swagger/openapi.json"));
|
|
526
|
+
};
|
|
524
527
|
return [
|
|
525
528
|
{
|
|
526
529
|
method: "GET",
|
|
527
530
|
pattern: "/swagger",
|
|
528
|
-
handler:
|
|
529
|
-
|
|
530
|
-
|
|
531
|
+
handler: serveUi
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
// The trailing-slash form, registered rather than left to fall through.
|
|
535
|
+
//
|
|
536
|
+
// Matching "/foo/" against a "/foo" route is opt-in via
|
|
537
|
+
// TINA4_TRAILING_SLASH_REDIRECT and OFF by default, so /swagger/ missed
|
|
538
|
+
// this route and was answered by the framework-bundled
|
|
539
|
+
// public/swagger/index.html instead. That mattered twice over. It used to
|
|
540
|
+
// be a 200 carrying a permanently empty UI, because the bundled file asked
|
|
541
|
+
// for an unsubstituted {SWAGGER_ROUTE}/swagger.json -- fixed in that file.
|
|
542
|
+
// And it is a SECOND Swagger UI implementation: the bundled one hardcodes
|
|
543
|
+
// cdnjs, while the page this handler renders loads from
|
|
544
|
+
// TINA4_SWAGGER_UI_CDN, so an air-gapped deployment pointing that at a
|
|
545
|
+
// local mirror silently kept reaching cdnjs on this one path.
|
|
546
|
+
//
|
|
547
|
+
// Registering it keeps the fix inside swagger rather than changing how
|
|
548
|
+
// every route treats trailing slashes, satisfies the shared contract that
|
|
549
|
+
// already requires a 200 here, and matches python and ruby, which both
|
|
550
|
+
// serve /swagger and /swagger/ with no env var set. Excluded from the
|
|
551
|
+
// generated document by INTERNAL_PREFIXES like /swagger itself.
|
|
552
|
+
method: "GET",
|
|
553
|
+
pattern: "/swagger/",
|
|
554
|
+
handler: serveUi
|
|
531
555
|
},
|
|
532
556
|
{
|
|
533
557
|
method: "GET",
|
|
@@ -56,13 +56,38 @@ export function swaggerEnabled(): boolean {
|
|
|
56
56
|
export function createSwaggerRoutes(
|
|
57
57
|
getSpec: () => unknown
|
|
58
58
|
): RouteDefinition[] {
|
|
59
|
+
const serveUi = async (_req: Tina4Request, res: Tina4Response): Promise<void> => {
|
|
60
|
+
res.html(SWAGGER_UI_HTML("/swagger/openapi.json"));
|
|
61
|
+
};
|
|
62
|
+
|
|
59
63
|
return [
|
|
60
64
|
{
|
|
61
65
|
method: "GET",
|
|
62
66
|
pattern: "/swagger",
|
|
63
|
-
handler:
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
handler: serveUi,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
// The trailing-slash form, registered rather than left to fall through.
|
|
71
|
+
//
|
|
72
|
+
// Matching "/foo/" against a "/foo" route is opt-in via
|
|
73
|
+
// TINA4_TRAILING_SLASH_REDIRECT and OFF by default, so /swagger/ missed
|
|
74
|
+
// this route and was answered by the framework-bundled
|
|
75
|
+
// public/swagger/index.html instead. That mattered twice over. It used to
|
|
76
|
+
// be a 200 carrying a permanently empty UI, because the bundled file asked
|
|
77
|
+
// for an unsubstituted {SWAGGER_ROUTE}/swagger.json -- fixed in that file.
|
|
78
|
+
// And it is a SECOND Swagger UI implementation: the bundled one hardcodes
|
|
79
|
+
// cdnjs, while the page this handler renders loads from
|
|
80
|
+
// TINA4_SWAGGER_UI_CDN, so an air-gapped deployment pointing that at a
|
|
81
|
+
// local mirror silently kept reaching cdnjs on this one path.
|
|
82
|
+
//
|
|
83
|
+
// Registering it keeps the fix inside swagger rather than changing how
|
|
84
|
+
// every route treats trailing slashes, satisfies the shared contract that
|
|
85
|
+
// already requires a 200 here, and matches python and ruby, which both
|
|
86
|
+
// serve /swagger and /swagger/ with no env var set. Excluded from the
|
|
87
|
+
// generated document by INTERNAL_PREFIXES like /swagger itself.
|
|
88
|
+
method: "GET",
|
|
89
|
+
pattern: "/swagger/",
|
|
90
|
+
handler: serveUi,
|
|
66
91
|
},
|
|
67
92
|
{
|
|
68
93
|
method: "GET",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - System info (Node.js version, V8, memory, uptime, platform)
|
|
10
10
|
*/
|
|
11
11
|
import type { Router } from "./router.js";
|
|
12
|
-
import type { Tina4Request } from "./types.js";
|
|
12
|
+
import type { RouteHandler, Tina4Request } from "./types.js";
|
|
13
13
|
/** Safe HTTP methods that never carry a state change — they skip the write gate. */
|
|
14
14
|
export declare const DEV_SAFE_METHODS: Set<string>;
|
|
15
15
|
/**
|
|
@@ -203,6 +203,19 @@ export declare class DevAdmin {
|
|
|
203
203
|
* 4. Fallback `http://127.0.0.1:9145` — matches standalone `tina4 agent`.
|
|
204
204
|
*/
|
|
205
205
|
export declare function supervisorBaseUrl(): string;
|
|
206
|
+
/**
|
|
207
|
+
* Version check — a check that did not happen says so.
|
|
208
|
+
*
|
|
209
|
+
* This used to fall back to `latest = current` on any failure, and the toolbar
|
|
210
|
+
* renders that as a green "You are up to date!" — so a developer several
|
|
211
|
+
* releases behind, on a machine with no route out, was told the opposite of the
|
|
212
|
+
* truth, and the toolbar's own "Could not check for updates" branch could never
|
|
213
|
+
* fire because the failure arrived as a success. `latest` is `null` when the
|
|
214
|
+
* check could not be made, and `error` says why. The registry URL is
|
|
215
|
+
* `TINA4_VERSION_CHECK_URL` when set (a mirror, or a test's own server), else
|
|
216
|
+
* npm. Mirrors Python `tina4_python.dev_admin._api_version_check`.
|
|
217
|
+
*/
|
|
218
|
+
export declare const handleVersionCheck: RouteHandler;
|
|
206
219
|
/**
|
|
207
220
|
* Resolve a CodeMirror-friendly language id from a file path's basename.
|
|
208
221
|
*
|
|
@@ -212,4 +225,13 @@ export declare function supervisorBaseUrl(): string;
|
|
|
212
225
|
* - anything unknown → "text"
|
|
213
226
|
*/
|
|
214
227
|
export declare function devAdminLanguage(rel: string): string;
|
|
228
|
+
/**
|
|
229
|
+
* JS for the injected dev toolbar — the version-check modal, the dashboard
|
|
230
|
+
* overlay, and the WebSocket-primary live reloader. Served as an external
|
|
231
|
+
* script so the toolbar carries no inline handlers or `<script>` and stays
|
|
232
|
+
* CSP-clean. Every interaction is wired via addEventListener. The reloader only
|
|
233
|
+
* starts when the toolbar's `data-reload` is "1" (reload not suppressed for this
|
|
234
|
+
* request/port). Mirrors PHP DevAdmin::toolbarJs().
|
|
235
|
+
*/
|
|
236
|
+
export declare function toolbarJs(): string;
|
|
215
237
|
export {};
|