{"record":{"id":"ad4213011f1e4901","repo":"agalwood/Motrix","slug":"plugin-lifecycle-secrets-seed-missing","errorCode":"plugin.lifecycle.secrets_seed_missing","errorMessage":"plugin \"${this.opts.pluginId}\" config key \"${key}\" is secret but no decryptSecret function was injected","messagePattern":"plugin \"(.+?)\" config key \"(.+?)\" is secret but no decryptSecret function was injected","errorType":"exception","errorClass":"ConfigError","httpStatus":null,"severity":"error","filePath":"src/core/plugin/capabilities/config.ts","lineNumber":91,"sourceCode":"   * If the resolved value is a string and the key is secret, it is decrypted\n   * via `decryptSecret`. If no decryptSecret is injected for a stored secret\n   * string, throws ConfigError('plugin.lifecycle.secrets_seed_missing', ...).\n   */\n  async get(key: string): Promise<unknown> {\n    const stored = this.opts.readValues()\n    const storedValue = Object.hasOwn(stored, key) ? stored[key] : undefined\n\n    const resolved =\n      storedValue !== undefined ? storedValue : this.opts.schemaDefaults[key]\n\n    // Only decrypt if the resolved value is a string and the key is secret.\n    if (\n      resolved !== undefined &&\n      typeof resolved === 'string' &&\n      this.opts.secretFields.has(key)\n    ) {\n      if (!this.opts.decryptSecret) {\n        throw new ConfigError(\n          'plugin.lifecycle.secrets_seed_missing',\n          `plugin \"${this.opts.pluginId}\" config key \"${key}\" is secret but no decryptSecret function was injected`\n        )\n      }\n      return this.opts.decryptSecret(resolved)\n    }\n\n    return resolved\n  }\n\n  /**\n   * Returns the stored value verbatim — does NOT fall back to schema defaults\n   * and does NOT decrypt. Intended for the renderer's \"is this overridden?\" check.\n   */\n  async getRaw(key: string): Promise<unknown> {\n    const stored = this.opts.readValues()\n    return Object.hasOwn(stored, key) ? stored[key] : undefined\n  }","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/capabilities/config.ts#L73-L109","documentation":"Thrown by ConfigCapabilityHost's single-key getter when a config key is in `secretFields`, the resolved value is a non-empty string (stored value or schema default), but no `decryptSecret` function was supplied to the capability. The capability refuses to return ciphertext as plaintext, so a secret field without a decryption seed is treated as a misconfigured host. Code is `plugin.lifecycle.secrets_seed_missing`.","triggerScenarios":"Calling `config.get('apiToken')` where `apiToken` is declared secret in the schema and has a value, but the host built the Config capability without injecting `decryptSecret`. Also happens if the host secrets pipeline (e.g. a KMS/seed provider) was disabled in the current environment.","commonSituations":"Local dev environment where the secrets decryptor was never wired; CI runs with a stripped-down host config; a plugin manifest marks a field secret but the host hasn't provisioned the decryption seed for this plugin.","solutions":["Inject a decryptSecret function into the Config capability options at host construction.","If the field should not actually be secret, remove it from secretFields in the plugin's config schema.","In dev/test, supply a no-op decryptor (returns input unchanged) only when values are already plaintext — never do this in production.","Verify the secrets seed/bootstrap runs before plugin activate() and that the decryptor is bound per-plugin, not globally absent."],"exampleFix":"// before\nconst config = new ConfigCapabilityHost({\n  pluginId: 'p', secretFields: new Set(['apiToken']),\n  readValues: () => ({ apiToken: cipherText }),\n  // decryptSecret missing\n})\nawait config.get('apiToken') // throws\n\n// after\nconst config = new ConfigCapabilityHost({\n  pluginId: 'p', secretFields: new Set(['apiToken']),\n  readValues: () => ({ apiToken: cipherText }),\n  decryptSecret: (ct) => kms.decrypt(ct),\n})","handlingStrategy":"validation","validationCode":"function assertDecryptor(opts: ConfigCapabilityHostOptions): void {\n  if (opts.secretFields && opts.secretFields.size > 0 && !opts.decryptSecret) {\n    throw new Error('decryptSecret required because secretFields is non-empty')\n  }\n}","typeGuard":"function isSecretsSeedMissing(e: unknown): e is ConfigError {\n  return e instanceof Error && (e as ConfigError).code === 'plugin.lifecycle.secrets_seed_missing'\n}","tryCatchPattern":"try {\n  await config.get(key)\n} catch (e) {\n  if (isSecretsSeedMissing(e)) {\n    // host misconfiguration: fail loudly in dev, alert ops in prod\n  } else throw e\n}","preventionTips":["Treat secretFields as requiring decryptSecret by construction — assert at host build.","Never ship plaintext values in schemaDefaults for secret fields.","In tests, supply a deterministic decryptor stub rather than omitting it."],"tags":["plugin","config","secrets","lifecycle","capabilities"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}