{"record":{"id":"19ebdf1fb68178ff","repo":"agalwood/Motrix","slug":"plugin-lifecycle-secrets-invalid-token","errorCode":"plugin.lifecycle.secrets_invalid_token","errorMessage":"secret token has invalid format: expected prefix \"${PREFIX}\"","messagePattern":"secret token has invalid format: expected prefix \"(.+?)\"","errorType":"error_code","errorClass":"SecretStoreError","httpStatus":null,"severity":"error","filePath":"src/core/plugin/secret-store-libsodium.ts","lineNumber":110,"sourceCode":"    }\n  }\n\n  available(): boolean {\n    return true\n  }\n\n  async encrypt(plaintext: string): Promise<string> {\n    await sodium.ready\n    const nonce = sodium.randombytes_buf(NONCE_BYTES)\n    const ct = sodium.crypto_secretbox_easy(plaintext, nonce, this.key)\n    const nonceb64 = Buffer.from(nonce).toString('base64')\n    const ctb64 = Buffer.from(ct).toString('base64')\n    return `${PREFIX}${nonceb64}:${ctb64}`\n  }\n\n  async decrypt(token: string): Promise<string> {\n    if (!token.startsWith(PREFIX)) {\n      throw new SecretStoreError(\n        'plugin.lifecycle.secrets_invalid_token',\n        `secret token has invalid format: expected prefix \"${PREFIX}\"`\n      )\n    }\n\n    const rest = token.slice(PREFIX.length)\n    const colonIdx = rest.indexOf(':')\n    if (colonIdx === -1) {\n      throw new SecretStoreError(\n        'plugin.lifecycle.secrets_invalid_token',\n        'secret token has invalid format: missing nonce/ciphertext separator'\n      )\n    }\n\n    const nonceb64 = rest.slice(0, colonIdx)\n    const ctb64 = rest.slice(colonIdx + 1)\n\n    if (!nonceb64 || !ctb64) {","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/secret-store-libsodium.ts#L92-L128","documentation":"Thrown by LibsodiumSecretStore.decrypt when the supplied token does not start with the expected PREFIX (`\"box:\"`). Every token this store produces is shaped `box:<base64(nonce)>:<base64(ciphertext)>`, so a missing prefix means the value was not produced by this store — it is plaintext, a legacy `safe:` token from the deprecated Electron SafeStorage backend, or corrupted.","triggerScenarios":"decrypt(token) where `!token.startsWith('box:')`. Fires when reading an old config secret that predates the libsodium store, when the secret field holds raw plaintext, or when a different SecretStore implementation wrote the value.","commonSituations":"Upgrading from the deprecated Electron keychain-backed SafeStorageSecretStore whose tokens used a `safe:` prefix. Manual edit of appSettings JSON replaced an encrypted value with plaintext. Two stores (env-seed vs lockbox) — value encrypted under one key source, decrypted under another runtime with no matching key.","solutions":["If migrating from a legacy store, re-encrypt the plaintext through LibsodiumSecretStore.encrypt once and persist the new `box:` token.","If the field should be plaintext, do not route it through decrypt().","Confirm only LibsodiumSecretStore (or FailingSecretStore) is used to write/read the field across all runtimes."],"exampleFix":"// before — legacy/plaintext value fed to decrypt\nawait store.decrypt('my-api-key')\n\n// after — re-encrypt once, then store the resulting token\nconst token = await store.encrypt('my-api-key')\nawait saveSettings({ apiKey: token })\n// later: await store.decrypt(settings.apiKey)","handlingStrategy":"validation","validationCode":"const BOX_PREFIX = 'box:'\nfunction isLikelyBoxToken(s: unknown): boolean {\n  return typeof s === 'string' && s.startsWith(BOX_PREFIX)\n}\nif (!isLikelyBoxToken(settings.apiKey)) {\n  // value is plaintext or legacy; re-encrypt before calling decrypt\n  settings.apiKey = await store.encrypt(settings.apiKey)\n}","typeGuard":"function isBoxToken(s: unknown): s is string {\n  return typeof s === 'string' && s.startsWith('box:')\n}","tryCatchPattern":"try {\n  await store.decrypt(token)\n} catch (e) {\n  if (e instanceof SecretStoreError && e.code === 'plugin.lifecycle.secrets_invalid_token') {\n    // token was not produced by this store — migrate/re-encrypt, then retry\n  } else throw e\n}","preventionTips":["Always persist only tokens returned by LibsodiumSecretStore.encrypt (prefix 'box:').","On store migrations, re-encrypt legacy/plaintext values once and persist the new token.","Use the same store implementation across all runtimes that read a given secret field."],"tags":["plugin","secrets","libsodium","validation"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}