Skip to content

feat(plugins): ✨ daily update checks and security advisories from public JSON manifests - #881

Merged
aXenDeveloper merged 4 commits into
canaryfrom
claude/vitnode-plugin-updates-stage-2-rlpibi
Oct 17, 2026
Merged

aXenDeveloper merged 4 commits into
canaryfrom
claude/vitnode-plugin-updates-stage-2-rlpibi

Conversation

@aXenDeveloper

Copy link
Copy Markdown
Owner

Stage 2 of plugin management. Core and every plugin that declares
vitnode.updates.url in its package.json are checked once a day (and on
demand) against public update manifests, never npm. Results appear in
AdminCP → System → Plugins; nothing is installed or changed.

  • Versioned Zod contract for update manifests (stable/rc/canary channels,
    releases, advisories), shared by the server and the release tooling
  • updates carried through manifest.gen.ts and the frontend inventory
  • Server-only checker: one fetch per distinct source, HTTPS-only with SSRF
    guards on every hop and at connect time, timeout, streamed size limit,
    bounded concurrency, ETag/Last-Modified with 304 reuse
  • SemVer evaluation of updates, advisories (prereleases included), core
    compatibility and an older compatible target; API and web app versions
    evaluated separately
  • Persistence in core_update_sources / core_update_results with failure
    isolation, preserved findings and stale detection after redeploys
  • Atomic database lease shared by the daily cron job and the manual
    POST /admin/plugins/updates/check (plugins.can_check_updates), with
    expiry, owner-only release and a manual cooldown
  • AdminCP core card, update/compatibility/failure states, red security
    warnings, frontend inventory notice and a Check for updates button
  • Release workflow publishes updates.json to the update-manifest branch
    after npm publication; advisories come from security/advisories.json
  • Documentation under Development → Updates & Security

Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01412WzAxDRk5MbUe4C2S2N1

…lic JSON manifests

Stage 2 of plugin management. Core and every plugin that declares
`vitnode.updates.url` in its package.json are checked once a day (and on
demand) against public update manifests, never npm. Results appear in
AdminCP → System → Plugins; nothing is installed or changed.

- Versioned Zod contract for update manifests (stable/rc/canary channels,
  releases, advisories), shared by the server and the release tooling
- `updates` carried through manifest.gen.ts and the frontend inventory
- Server-only checker: one fetch per distinct source, HTTPS-only with SSRF
  guards on every hop and at connect time, timeout, streamed size limit,
  bounded concurrency, ETag/Last-Modified with 304 reuse
- SemVer evaluation of updates, advisories (prereleases included), core
  compatibility and an older compatible target; API and web app versions
  evaluated separately
- Persistence in core_update_sources / core_update_results with failure
  isolation, preserved findings and stale detection after redeploys
- Atomic database lease shared by the daily cron job and the manual
  `POST /admin/plugins/updates/check` (plugins.can_check_updates), with
  expiry, owner-only release and a manual cooldown
- AdminCP core card, update/compatibility/failure states, red security
  warnings, frontend inventory notice and a Check for updates button
- Release workflow publishes updates.json to the `update-manifest` branch
  after npm publication; advisories come from security/advisories.json
- Documentation under Development → Updates & Security

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01412WzAxDRk5MbUe4C2S2N1
…eed merging

Regression tests for the update checker: manifest contract, SemVer and
channel evaluation, prerelease advisory matching, API/frontend mismatch,
compatibility, safe fetching, shared sources, failure preservation, 304
reuse, stale results, the database lease and cooldown, route permissions,
release-manifest merging and the catalog's update states. Generator tests
now expect the `updates` field.

Also only accept a 304 when a conditional header was actually sent, and
fix two swapped comments on UpdateStatus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01412WzAxDRk5MbUe4C2S2N1
@github-actions github-actions Bot added the 💡 Feature A new feature label Oct 15, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-17T08:48:05.014198Z 6ffbf7a New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

React Doctor found 5 new issues in 4 files · 5 warnings · score 89 / 100 (Great) · 0 fixed · vs canary

5 warnings

scripts/update-manifest/merge.ts

  • ⚠️ L96 JSON parse/stringify deep clone no-json-parse-stringify-clone

src/api/lib/updates/check.ts

  • ⚠️ L249 await inside a loop async-await-in-loop

src/views/admin/views/core/debug/health/health.ts

  • ⚠️ L239 array.find() inside a loop js-index-maps

src/views/admin/views/core/system/plugins/plugin-update-status.tsx

  • ⚠️ L168 React function has high control-flow complexity no-high-complexity-react-function
  • ⚠️ L277 React function has high control-flow complexity no-high-complexity-react-function

Reviewed by React Doctor for commit 6ffbf7a. See inline comments for fixes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7edc42f58e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +244 to +245
await assertFetchableUrl(url, resolveHost);
if (signal.aborted) throw new UpdateSourceError("timeout");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the timeout while resolving update hosts

When DNS resolution is delayed or the system resolver stops responding, timeoutMs does not bound the update fetch because assertFetchableUrl awaits resolveHost before the abort signal is consulted or passed to the transport. A single affected source can therefore leave the manual endpoint and the entire scheduled check pending indefinitely rather than failing after the documented 10 seconds; race the resolution step against the timeout or otherwise make it abortable.

Useful? React with 👍 / 👎.

Comment on lines +12 to +13
/** Longer than a check can take: every source has a 10 s timeout and they run four at a time. */
export const UPDATE_CHECK_LEASE_MS = 5 * 60_000;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the lease valid for the full source queue

For installations with more than 120 distinct update sources that each approach the 10-second timeout, four-way concurrency makes a check exceed this fixed five-minute lease. Another API instance or manual request can then acquire the expired lease while the first check is still writing results and pruning rows, violating the one-check-at-a-time guarantee and causing duplicate traffic and interleaved database state; renew the lease during the run or size it from the queued work.

Useful? React with 👍 / 👎.

- Introduced `DebugNav` component for navigation within the debug section.
- Created `DebugOverview` component to summarize system health and issues.
- Added various sections including `SecurityUpdateAlert`, `VitnodeSection`, `PluginsSection`, and `ServicesSection` to provide detailed insights into system status.
- Implemented reusable components like `FrameRow`, `CheckRow`, and `FactRow` for consistent UI structure.
- Established tone indicators for health checks with `ToneIcon`, `ToneDot`, and `ToneBadge`.
- Initialized `security-updates.json` for future security update management.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4821f22e5e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


return {
count: result.securityUpdates.length,
fixedVersion: result.latest?.version ?? securityUpdate.fixed,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the advisory's declared fixed version

When a core advisory has fixed: null but its package still has a normal channel target, result.latest is non-null and this expression presents that target as the fix. In the common no-fix case where the target is the currently installed, still-affected release, the Debug Panel says to update to the same vulnerable version instead of showing “No fixed version has been published”; use securityUpdate.fixed, or only substitute latest after proving it is outside the advisory's affected range.

Useful? React with 👍 / 👎.

Comment on lines +185 to +188
validators = {
etag: response.etag,
lastModified: response.lastModified,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound validators before writing them

A source can return an ETag longer than 512 characters or a Last-Modified value longer than 128 characters, but these values are copied into correspondingly bounded varchar columns without validation. PostgreSQL then rejects the source upsert outside the fetch/parsing try, causing mapLimited and the entire update check to fail instead of recording this one source as failed and continuing with the others; reject or truncate oversized validators before persistence.

Useful? React with 👍 / 👎.

Comment on lines +110 to +112
// A result for another source answers a different question.
if (row?.sourceUrl !== component.sourceUrl || row.result === null) {
return notChecked;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Drop metadata from a previous update source

When a deployed plugin changes its update URL, this branch correctly discards the old result but returns notChecked with the old row's lastAttemptAt and lastError. If the previous URL's last request failed, the catalog therefore reports that the new source failed before it has ever been checked; clear the attempt/error metadata when row.sourceUrl !== component.sourceUrl.

Useful? React with 👍 / 👎.

};

const assertValid = (manifest: UpdateManifest): UpdateManifest => {
const parsed = parseUpdateManifest(JSON.parse(JSON.stringify(manifest)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/no-json-parse-stringify-clone (warning)

JSON.parse(JSON.stringify(x)) deep-clones by re-serializing: it is slow on large objects and silently drops undefined, functions, Date/Map/Set, and cyclic references. Use structuredClone(x).

Fix → Replace JSON.parse(JSON.stringify(value)) with structuredClone(value). It is faster and preserves Dates, Maps, Sets, and cyclic references.

Docs

if (outcome.error !== null) {
// The failure is recorded; what was found before - and the versions it
// was found for - stays exactly as it was.
await db

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/async-await-in-loop (warning)

This for…of loop waits before starting the next iteration. If iterations perform independent asynchronous work, consider bounded concurrency; await syntax alone does not establish a speedup.

Fix → Consider concurrent calls only for independent asynchronous work. Shared queues or synchronous work may not benefit. Preserve resource limits, transaction ordering, and failure/cancellation semantics; observe all callback promises.

Docs

for (const plugin of installed.plugins) {
const problem = pluginProblem(
plugin,
updates?.plugins.find(update => update.id === plugin.id),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/js-index-maps (warning)

This gets slow as your list grows because array.find() runs inside a loop, so build a Map once before the loop for instant lookups

Fix → Build a Map once before the loop instead of calling array.find(...) inside it

Docs

): (typeof CHECK_ERRORS)[number] =>
CHECK_ERRORS.find(entry => entry === error.code) ?? "network";

const ResultLines = ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/no-high-complexity-react-function (warning)

ResultLines has cyclomatic complexity 17, cognitive complexity 17, and maximum nesting depth 2, so its React logic is hard to understand and change. Extract independent branches into components or hooks.

Fix → Extract independent render branches and state logic into focused components or hooks until the control flow is easy to follow.

Docs

* Security findings are rendered apart, by `SecurityUpdates`, and never
* folded into "update available".
*/
export const PluginUpdateStatus = ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/no-high-complexity-react-function (warning)

PluginUpdateStatus has cyclomatic complexity 18, cognitive complexity 15, and maximum nesting depth 2, so its React logic is hard to understand and change. Extract independent branches into components or hooks.

Fix → Extract independent render branches and state logic into focused components or hooks until the control flow is easy to follow.

Docs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6ffbf7ae36

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +30 to +32
href: "/admin/core/debug/cron",
icon: <ClockIcon />,
permission: { module: "cron", permission: "can_view" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate moved search entries with the debug permission

For an administrator who has cron.can_view, queue.can_view, or system.can_view but lacks debug.can_view, these new search-only items are displayed, yet every destination is nested under the debug layout, whose loader rejects that user in tanstack/admin/debug/route.tsx:59 before the child permission is checked. This turns valid search results into links that consistently lead to an access-denied page; expose them only when the parent debug permission is also granted, or stop requiring that extra permission in the parent.

Useful? React with 👍 / 👎.

Comment on lines +190 to +194
return [
check("vitnode-latest", "ok", {
key: "vitnode.up_to_date",
values: { version: installed.core.version },
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Only report the core as current after an up-to-date result

When a successful core check returns package_missing, channel_missing, or unknown_channel, core.state is still checked, so execution reaches this unconditional success result and the Debug Panel claims the installed core is up to date. The same happens for a stale recomputed result that is not an available update. Only emit vitnode.up_to_date when core.result.status === "up_to_date" and the result is not stale; otherwise surface the unresolved or stale state.

Useful? React with 👍 / 👎.

@aXenDeveloper
aXenDeveloper merged commit 59479f9 into canary Oct 17, 2026
8 checks passed
@aXenDeveloper
aXenDeveloper deleted the claude/vitnode-plugin-updates-stage-2-rlpibi branch October 17, 2026 08:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💡 Feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants