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 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.134)
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.134 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.13.134",
3
+ "version": "3.13.135",
4
4
  "type": "module",
5
5
  "description": "Tina4 for Node.js/TypeScript - native TypeScript conventions and shared Tina4 contracts",
6
6
  "keywords": [
@@ -35062,6 +35062,13 @@ function toolbarJs() {
35062
35062
  el.className = 't4-ok';
35063
35063
  el.innerHTML = 'Latest: <strong class="t4-ok">v' + latest + '</strong> &mdash; 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
- let latest = current;
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 timeout = setTimeout(() => controller.abort(), 5e3);
36419
- const resp = await fetch("https://registry.npmjs.org/tina4-nodejs/latest", {
36420
- signal: controller.signal
36421
- });
36422
- clearTimeout(timeout);
36423
- if (resp.ok) {
36424
- const data = await resp.json();
36425
- if (typeof data.version === "string") latest = data.version;
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
- res.json({ current, latest });
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 (!swaggerAssetsEnabled) {
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, while the DOM declaration
44457
- // used by the published type build narrows BodyInit to ArrayBuffer
44458
- // backed views. Keep the binary payload intact and make that boundary
44459
- // explicit rather than converting the encrypted bytes to text.
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> &mdash; 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
- let latest = current;
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 timeout = setTimeout(() => controller.abort(), 5e3);
36398
- const resp = await fetch("https://registry.npmjs.org/tina4-nodejs/latest", {
36399
- signal: controller.signal
36400
- });
36401
- clearTimeout(timeout);
36402
- if (resp.ok) {
36403
- const data = await resp.json();
36404
- if (typeof data.version === "string") latest = data.version;
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
- res.json({ current, latest });
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 (!swaggerAssetsEnabled) {
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, while the DOM declaration
44418
- // used by the published type build narrows BodyInit to ArrayBuffer
44419
- // backed views. Keep the binary payload intact and make that boundary
44420
- // explicit rather than converting the encrypted bytes to text.
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
- const handleVersionCheck: RouteHandler = async (_req, res) => {
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
- let latest = current;
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 timeout = setTimeout(() => controller.abort(), 5000);
2078
- const resp = await fetch("https://registry.npmjs.org/tina4-nodejs/latest", {
2079
- signal: controller.signal,
2080
- });
2081
- clearTimeout(timeout);
2082
- if (resp.ok) {
2083
- const data = (await resp.json()) as Record<string, unknown>;
2084
- if (typeof data.version === "string") latest = data.version;
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
- // Offline or timeout return current as latest
2106
+ } catch (exc) {
2107
+ // offline, timeout, DNS, unreadable body
2108
+ return failed(exc instanceof Error && exc.message ? exc.message : String(exc));
2088
2109
  }
2089
- res.json({ current, latest });
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> &mdash; 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, while the DOM declaration
263
- // used by the published type build narrows BodyInit to ArrayBuffer
264
- // backed views. Keep the binary payload intact and make that boundary
265
- // explicit rather than converting the encrypted bytes to text.
266
- body: body as unknown as BodyInit,
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 (!swaggerAssetsEnabled) {
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> &mdash; 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
- let latest = current;
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 timeout = setTimeout(() => controller.abort(), 5e3);
24687
- const resp = await fetch("https://registry.npmjs.org/tina4-nodejs/latest", {
24688
- signal: controller.signal
24689
- });
24690
- clearTimeout(timeout);
24691
- if (resp.ok) {
24692
- const data = await resp.json();
24693
- if (typeof data.version === "string") latest = data.version;
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
- res.json({ current, latest });
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 (!swaggerAssetsEnabled) {
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, while the DOM declaration
33238
- // used by the published type build narrows BodyInit to ArrayBuffer
33239
- // backed views. Keep the binary payload intact and make that boundary
33240
- // explicit rather than converting the encrypted bytes to text.
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 {};