tina4-nodejs 3.13.134 → 3.13.135
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 +39 -18
- package/packages/core/dist/index.js +39 -18
- 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 +39 -18
- 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.135)
|
|
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.135 - 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");
|
|
@@ -39650,7 +39665,7 @@ async function configureSwagger(router, ormDir, modelsDir) {
|
|
|
39650
39665
|
try {
|
|
39651
39666
|
const swagger = await Promise.resolve().then(() => (init_src2(), src_exports2));
|
|
39652
39667
|
enabled = swagger.swaggerEnabled();
|
|
39653
|
-
if (!
|
|
39668
|
+
if (!enabled) {
|
|
39654
39669
|
throw new Error("__swagger_disabled__");
|
|
39655
39670
|
}
|
|
39656
39671
|
let modelDefs = [];
|
|
@@ -44258,6 +44273,9 @@ function decodeBase64Url(value, name) {
|
|
|
44258
44273
|
}
|
|
44259
44274
|
return Buffer.from(value.replace(/-/g, "+").replace(/_/g, "/"), "base64");
|
|
44260
44275
|
}
|
|
44276
|
+
function pad32(value) {
|
|
44277
|
+
return value.length >= 32 ? value : Buffer.concat([Buffer.alloc(32 - value.length), value]);
|
|
44278
|
+
}
|
|
44261
44279
|
function vapidPrivateKey(rawPrivate, rawPublic) {
|
|
44262
44280
|
if (rawPublic.length !== 65 || rawPublic[0] !== 4) throw new PushError("P-256 public keys must be 65-byte uncompressed points");
|
|
44263
44281
|
const x = encodeBase64Url(rawPublic.subarray(1, 33));
|
|
@@ -44266,7 +44284,7 @@ function vapidPrivateKey(rawPrivate, rawPublic) {
|
|
|
44266
44284
|
key: {
|
|
44267
44285
|
kty: "EC",
|
|
44268
44286
|
crv: "P-256",
|
|
44269
|
-
d: encodeBase64Url(rawPrivate),
|
|
44287
|
+
d: encodeBase64Url(pad32(rawPrivate)),
|
|
44270
44288
|
x,
|
|
44271
44289
|
y,
|
|
44272
44290
|
ext: true
|
|
@@ -44360,8 +44378,10 @@ function generateVapidKeys() {
|
|
|
44360
44378
|
const ecdh = createECDH(CURVE);
|
|
44361
44379
|
ecdh.generateKeys();
|
|
44362
44380
|
return {
|
|
44381
|
+
// getPublicKey keeps its 65-byte width; getPrivateKey drops a leading zero
|
|
44382
|
+
// byte, so the scalar must be padded to the fixed field width.
|
|
44363
44383
|
publicKey: encodeBase64Url(ecdh.getPublicKey(void 0, "uncompressed")),
|
|
44364
|
-
privateKey: encodeBase64Url(ecdh.getPrivateKey())
|
|
44384
|
+
privateKey: encodeBase64Url(pad32(ecdh.getPrivateKey()))
|
|
44365
44385
|
};
|
|
44366
44386
|
}
|
|
44367
44387
|
var CURVE, RECORD_SIZE, MAX_PAYLOAD, PushError, Push;
|
|
@@ -44453,10 +44473,11 @@ var init_push = __esm({
|
|
|
44453
44473
|
TTL: String(this.options.ttl ?? 60),
|
|
44454
44474
|
...this.options.urgency ? { Urgency: this.options.urgency } : {}
|
|
44455
44475
|
},
|
|
44456
|
-
// Node's fetch accepts Buffer at runtime
|
|
44457
|
-
//
|
|
44458
|
-
//
|
|
44459
|
-
//
|
|
44476
|
+
// Node's fetch accepts a Buffer at runtime. The typecheck build
|
|
44477
|
+
// (lib ES2022, no DOM) does not know the global `BodyInit` name, and
|
|
44478
|
+
// the DOM declaration narrows it to ArrayBuffer-backed views — a
|
|
44479
|
+
// `Uint8Array` (which a Buffer is) satisfies both, so cast to that and
|
|
44480
|
+
// keep the encrypted bytes intact rather than converting them to text.
|
|
44460
44481
|
body
|
|
44461
44482
|
});
|
|
44462
44483
|
} 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");
|
|
@@ -39629,7 +39644,7 @@ async function configureSwagger(router, ormDir, modelsDir) {
|
|
|
39629
39644
|
try {
|
|
39630
39645
|
const swagger = await Promise.resolve().then(() => (init_src2(), src_exports2));
|
|
39631
39646
|
enabled = swagger.swaggerEnabled();
|
|
39632
|
-
if (!
|
|
39647
|
+
if (!enabled) {
|
|
39633
39648
|
throw new Error("__swagger_disabled__");
|
|
39634
39649
|
}
|
|
39635
39650
|
let modelDefs = [];
|
|
@@ -44219,6 +44234,9 @@ function decodeBase64Url(value, name) {
|
|
|
44219
44234
|
}
|
|
44220
44235
|
return Buffer.from(value.replace(/-/g, "+").replace(/_/g, "/"), "base64");
|
|
44221
44236
|
}
|
|
44237
|
+
function pad32(value) {
|
|
44238
|
+
return value.length >= 32 ? value : Buffer.concat([Buffer.alloc(32 - value.length), value]);
|
|
44239
|
+
}
|
|
44222
44240
|
function vapidPrivateKey(rawPrivate, rawPublic) {
|
|
44223
44241
|
if (rawPublic.length !== 65 || rawPublic[0] !== 4) throw new PushError("P-256 public keys must be 65-byte uncompressed points");
|
|
44224
44242
|
const x = encodeBase64Url(rawPublic.subarray(1, 33));
|
|
@@ -44227,7 +44245,7 @@ function vapidPrivateKey(rawPrivate, rawPublic) {
|
|
|
44227
44245
|
key: {
|
|
44228
44246
|
kty: "EC",
|
|
44229
44247
|
crv: "P-256",
|
|
44230
|
-
d: encodeBase64Url(rawPrivate),
|
|
44248
|
+
d: encodeBase64Url(pad32(rawPrivate)),
|
|
44231
44249
|
x,
|
|
44232
44250
|
y,
|
|
44233
44251
|
ext: true
|
|
@@ -44321,8 +44339,10 @@ function generateVapidKeys() {
|
|
|
44321
44339
|
const ecdh = createECDH(CURVE);
|
|
44322
44340
|
ecdh.generateKeys();
|
|
44323
44341
|
return {
|
|
44342
|
+
// getPublicKey keeps its 65-byte width; getPrivateKey drops a leading zero
|
|
44343
|
+
// byte, so the scalar must be padded to the fixed field width.
|
|
44324
44344
|
publicKey: encodeBase64Url(ecdh.getPublicKey(void 0, "uncompressed")),
|
|
44325
|
-
privateKey: encodeBase64Url(ecdh.getPrivateKey())
|
|
44345
|
+
privateKey: encodeBase64Url(pad32(ecdh.getPrivateKey()))
|
|
44326
44346
|
};
|
|
44327
44347
|
}
|
|
44328
44348
|
var CURVE, RECORD_SIZE, MAX_PAYLOAD, PushError, Push;
|
|
@@ -44414,10 +44434,11 @@ var init_push = __esm({
|
|
|
44414
44434
|
TTL: String(this.options.ttl ?? 60),
|
|
44415
44435
|
...this.options.urgency ? { Urgency: this.options.urgency } : {}
|
|
44416
44436
|
},
|
|
44417
|
-
// Node's fetch accepts Buffer at runtime
|
|
44418
|
-
//
|
|
44419
|
-
//
|
|
44420
|
-
//
|
|
44437
|
+
// Node's fetch accepts a Buffer at runtime. The typecheck build
|
|
44438
|
+
// (lib ES2022, no DOM) does not know the global `BodyInit` name, and
|
|
44439
|
+
// the DOM declaration narrows it to ArrayBuffer-backed views — a
|
|
44440
|
+
// `Uint8Array` (which a Buffer is) satisfies both, so cast to that and
|
|
44441
|
+
// keep the encrypted bytes intact rather than converting them to text.
|
|
44421
44442
|
body
|
|
44422
44443
|
});
|
|
44423
44444
|
} 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");
|
|
@@ -27918,7 +27933,7 @@ async function configureSwagger(router, ormDir, modelsDir) {
|
|
|
27918
27933
|
try {
|
|
27919
27934
|
const swagger = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
27920
27935
|
enabled = swagger.swaggerEnabled();
|
|
27921
|
-
if (!
|
|
27936
|
+
if (!enabled) {
|
|
27922
27937
|
throw new Error("__swagger_disabled__");
|
|
27923
27938
|
}
|
|
27924
27939
|
let modelDefs = [];
|
|
@@ -33039,6 +33054,9 @@ function decodeBase64Url(value, name) {
|
|
|
33039
33054
|
}
|
|
33040
33055
|
return Buffer.from(value.replace(/-/g, "+").replace(/_/g, "/"), "base64");
|
|
33041
33056
|
}
|
|
33057
|
+
function pad32(value) {
|
|
33058
|
+
return value.length >= 32 ? value : Buffer.concat([Buffer.alloc(32 - value.length), value]);
|
|
33059
|
+
}
|
|
33042
33060
|
function vapidPrivateKey(rawPrivate, rawPublic) {
|
|
33043
33061
|
if (rawPublic.length !== 65 || rawPublic[0] !== 4) throw new PushError("P-256 public keys must be 65-byte uncompressed points");
|
|
33044
33062
|
const x = encodeBase64Url(rawPublic.subarray(1, 33));
|
|
@@ -33047,7 +33065,7 @@ function vapidPrivateKey(rawPrivate, rawPublic) {
|
|
|
33047
33065
|
key: {
|
|
33048
33066
|
kty: "EC",
|
|
33049
33067
|
crv: "P-256",
|
|
33050
|
-
d: encodeBase64Url(rawPrivate),
|
|
33068
|
+
d: encodeBase64Url(pad32(rawPrivate)),
|
|
33051
33069
|
x,
|
|
33052
33070
|
y,
|
|
33053
33071
|
ext: true
|
|
@@ -33141,8 +33159,10 @@ function generateVapidKeys() {
|
|
|
33141
33159
|
const ecdh = createECDH(CURVE);
|
|
33142
33160
|
ecdh.generateKeys();
|
|
33143
33161
|
return {
|
|
33162
|
+
// getPublicKey keeps its 65-byte width; getPrivateKey drops a leading zero
|
|
33163
|
+
// byte, so the scalar must be padded to the fixed field width.
|
|
33144
33164
|
publicKey: encodeBase64Url(ecdh.getPublicKey(void 0, "uncompressed")),
|
|
33145
|
-
privateKey: encodeBase64Url(ecdh.getPrivateKey())
|
|
33165
|
+
privateKey: encodeBase64Url(pad32(ecdh.getPrivateKey()))
|
|
33146
33166
|
};
|
|
33147
33167
|
}
|
|
33148
33168
|
var CURVE, RECORD_SIZE, MAX_PAYLOAD, PushError, Push;
|
|
@@ -33234,10 +33254,11 @@ var init_push = __esm({
|
|
|
33234
33254
|
TTL: String(this.options.ttl ?? 60),
|
|
33235
33255
|
...this.options.urgency ? { Urgency: this.options.urgency } : {}
|
|
33236
33256
|
},
|
|
33237
|
-
// Node's fetch accepts Buffer at runtime
|
|
33238
|
-
//
|
|
33239
|
-
//
|
|
33240
|
-
//
|
|
33257
|
+
// Node's fetch accepts a Buffer at runtime. The typecheck build
|
|
33258
|
+
// (lib ES2022, no DOM) does not know the global `BodyInit` name, and
|
|
33259
|
+
// the DOM declaration narrows it to ArrayBuffer-backed views — a
|
|
33260
|
+
// `Uint8Array` (which a Buffer is) satisfies both, so cast to that and
|
|
33261
|
+
// keep the encrypted bytes intact rather than converting them to text.
|
|
33241
33262
|
body
|
|
33242
33263
|
});
|
|
33243
33264
|
} catch (error2) {
|
|
@@ -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 {};
|