agalwood/Motrix · error · AppError

PLUGIN_MANIFEST_INVALID

PLUGIN_MANIFEST_INVALID

Error message

plugin.update.builtin_no_package

What it means

Thrown by BuiltinUpdater.stage when entry.package is falsy for a builtin plugin update. Builtin updates are the hot-update path for plugins shipped in the app bundle; they require a signed package descriptor (url/size/sha256/signature). A builtin update entry without a package means the registry listed a new version but supplied no artifact to install, so staging aborts before touching the overlay.

Solutions

  1. Publish the package artifact and signature for the builtin version, then regenerate the registry entry so it includes package { url, size, sha256, signature }.
  2. Skip the update on the client until the registry entry is complete (the running builtin stays at its current version).
  3. Audit the registry publishing pipeline to fail when a builtin entry is missing its package/signature.

Example fix

// before — registry builtin entry with no package
{ "id": "motrix.core", "version": "2.1.0" }

// after — include signed package descriptor
{ "id": "motrix.core", "version": "2.1.0",
  "package": { "url": "https://dl.motrix.app/core/2.1.0.moext", "size": 8192,
    "sha256": "<64 hex>", "signature": "<ed25519 base64>" } }
Defensive patterns

Strategy: validation

Validate before calling

function hasBuiltinPackage(entry: { package?: { url?: unknown; size?: unknown; sha256?: unknown; signature?: unknown } }): boolean {
  const p = entry.package
  return !!p && typeof p.url === 'string' && typeof p.size === 'number' && typeof p.sha256 === 'string' && typeof p.signature === 'string'
}
if (!hasBuiltinPackage(registryEntry)) {
  throw new Error('builtin update entry has no signed package descriptor')
}

Type guard

function isBuiltinUpdateEntry(e: unknown): e is { package: { url: string; size: number; sha256: string; signature: string } } {
  const p = (e as { package?: unknown }).package
  if (typeof p !== 'object' || p === null) return false
  return typeof (p as { url?: unknown }).url === 'string' &&
    typeof (p as { size?: unknown }).size === 'number' &&
    typeof (p as { sha256?: unknown }).sha256 === 'string' &&
    typeof (p as { signature?: unknown }).signature === 'string'
}

Try / catch

try {
  await updater.stage(entry, effective)
} catch (e) {
  if (e instanceof AppError && e.code === ErrorCode.PluginManifestInvalid && e.message === 'plugin.update.builtin_no_package') {
    // registry listed a builtin version with no artifact; skip the update, keep current version
    skipBuiltinUpdate(entry.id)
  } else throw e
}

Prevention

When it happens

Trigger: builtinUpdater.stage(entry, effective) where `entry.package` is undefined/null. The entry came from the registry (RegistryClient) describing a builtin plugin version, but the package blob descriptor was omitted.

Common situations: Builtin update was published to the registry index before the package blob/signature upload completed. Registry file partially regenerated and the package section dropped. Schema drift where the builtin entry shape changed but the producer was not updated.

Related errors


AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12). Data as JSON: /api/errors/7b2a3a38a6d903e4. Report an issue: GitHub.

Appendix: source

Thrown at src/core/plugin/update/builtin-updater.ts:51

interface StagedUpdate {
  pluginId: string
  stagingDir: string
}

const OVERLAY_META = '_overlay.json'

export class BuiltinUpdater {
  private readonly pending = new Map<string, StagedUpdate>()

  constructor(private readonly opts: BuiltinUpdaterOptions) {}

  async stage(
    entry: RegistryPluginDTO,
    effective: PluginManifest
  ): Promise<BuiltinStageResult> {
    const pkg = entry.package
    if (!pkg) {
      throw new AppError(
        ErrorCode.PluginManifestInvalid,
        'plugin.update.builtin_no_package'
      )
    }
    if (!pkg.signature) {
      throw new AppError(
        ErrorCode.PluginManifestInvalid,
        'plugin.update.builtin_no_signature'
      )
    }
    if (!semverGt(entry.version, effective.version)) {
      throw new AppError(
        ErrorCode.PluginManifestInvalid,
        'plugin.update.builtin_not_newer'
      )
    }

    const bytes = await fetchVerifiedPackageBytes(entry, this.opts.fetchImpl)

View on GitHub (pinned to 1a708ee577)