JuliusBrussee/caveman · error

runtimePolicy publicKey must be a base64-encoded 32-byte…

Error message

runtimePolicy publicKey must be a base64-encoded 32-byte Ed25519 key

What it means

The RuntimePolicy constructor validates options.publicKey by decoding it as base64 and requiring exactly 32 bytes (an Ed25519 public key size). Anything else — invalid base64 or wrong length — throws this configuration error before the client is usable.

Solutions

  1. Base64-encode the raw 32-byte Ed25519 public key (standard alphabet, padding allowed)
  2. Ensure you export the public key, not the private key or seed
  3. Convert hex to raw bytes then base64: Buffer.from(hexKey,'hex').toString('base64')
  4. Convert URL-safe base64 to standard by replacing -/_ with +/

Example fix

// before
new RuntimePolicyEnforcer(cave, { publicKey: "9d61b19deffd5a60ba844af492ec2cc4" }); // hex, 16 bytes
// after
new RuntimePolicyEnforcer(cave, { publicKey: "nWGJne/9WmC6hEr0kuwsxMA5G0zmGToLWM0vB1X0N60=" }); // base64, 32 bytes
Defensive patterns

Strategy: validation

Validate before calling

function isValidPinnedKey(key: string): boolean {
  if (!/^[A-Za-z0-9+/]*={0,2}$/.test(key)) return false;
  const raw = Buffer.from(key, "base64");
  return raw.length === 32;
}
if (!isValidPinnedKey(opts.publicKey)) throw new Error("publicKey must be base64 of a 32-byte Ed25519 public key");

Type guard

function isBase64Of32Bytes(v: unknown): v is string {
  return typeof v === "string" && /^([A-Za-z0-9+/]{43}=?|)$/.test(v) && Buffer.from(v, "base64").length === 32;
}

Try / catch

try {
  const enforcer = new RuntimePolicyEnforcer(cave, { publicKey });
} catch (e) {
  if (/publicKey/.test(e.message)) dieWithConfigHint("Invalid CAVEMAN_POLICY_PUBLIC_KEY: expected base64 32-byte Ed25519 key");
  else throw e;
}

Prevention

When it happens

Trigger: Passing publicKey that is not valid base64, is base64 of a non-32-byte key (e.g. hex string, PEM, 64-byte private key, 65-byte uncompressed point), or a URL-safe base64 variant with -/_ characters.

Common situations: Copying a hex-encoded key instead of base64; pasting the Ed25519 private/seed key (64 bytes) instead of the public key; using a PEM-wrapped key; URL-safe base64 from another tool.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/078f4c0f8423e1f1. Report an issue: GitHub.

Appendix: source

Thrown at packages/sdk/typescript/src/index.ts:1082

 * failure. `decide()` is synchronous, local-only, and never throws — with no
 * bundle it returns the caller's baseline. Mirrors the Python
 * `RuntimePolicyClient`.
 */
export class RuntimePolicyClient {
  private bundle: Record<string, unknown> | null = null;
  private signedBundle = false;
  private pinnedKey: Uint8Array | null = null;
  private killedLocally = false;
  private timer: ReturnType<typeof setInterval> | null = null;
  /** True while a `refresh()` is in flight; the background timer skips rather than pile on. */
  private refreshing = false;
  private readonly killEnv: string;

  constructor(private cave: Cave, options: RuntimePolicyOptions = {}) {
    this.killEnv = options.killEnv ?? "CAVEMAN_POLICY_KILL";
    if (options.publicKey !== undefined) {
      const pin = base64Bytes(options.publicKey);
      if (pin === null || pin.length !== 32) throw new Error("runtimePolicy publicKey must be a base64-encoded 32-byte Ed25519 key");
      this.pinnedKey = pin;
    }
    const seconds = options.autoRefreshSeconds;
    if (typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0) {
      const timer = setInterval(() => {
        // A slow or hung endpoint must not queue refreshes behind each other:
        // one tick while a refresh is already in flight is skipped, not stacked.
        if (this.refreshing) return;
        void this.refresh();
      }, seconds * 1000);
      (timer as unknown as { unref?: () => void }).unref?.();
      this.timer = timer;
    }
  }

  /**
   * Fetch and verify the current bundle (`GET /sdk/v1/runtime-policy`).
   *

View on GitHub (pinned to 3ee70a1026)