Hmbown/CodeWhale · error · Error

invalid draft key

Error message

invalid draft key

What it means

deleteDraft() only deletes keys that round-trip through parseDraftKey(): exactly draft:<type>:<id> where type is in AGENT_DRAFT_TYPES and id matches the strict id pattern. Any other string - a bare id, a wrong prefix, an unknown type - is rejected before kv.delete() runs, protecting the namespace from arbitrary deletions.

Source

Thrown at web/lib/community-agent.ts:316

  } catch {
    return null;
  }
}

export async function listDrafts(kv: KVNamespace | undefined, prefix = "draft:"): Promise<AgentDraft[]> {
  if (!kv) return [];
  const listed = await kv.list({ prefix, limit: 100 });
  const drafts: AgentDraft[] = [];
  for (const k of listed.keys) {
    const draft = await getDraft(kv, k.name);
    if (draft) drafts.push(draft);
  }
  return drafts;
}

export async function deleteDraft(kv: KVNamespace | undefined, key: string): Promise<void> {
  if (!kv) return;
  if (!parseDraftKey(key)) throw new Error("invalid draft key");
  await kv.delete(key);
}

// --- Admin session helpers ---

const SESSION_PREFIX = "session:admin:";
const SESSION_TTL_SEC = 60 * 60 * 24; // 24h

function toBase64Url(bytes: Uint8Array): string {
  let s = "";
  for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
  return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}

export async function safeEqual(a: string, b: string): Promise<boolean> {
  const enc = new TextEncoder();
  const ha = new Uint8Array(await crypto.subtle.digest("SHA-256", enc.encode(a)));
  const hb = new Uint8Array(await crypto.subtle.digest("SHA-256", enc.encode(b)));

View on GitHub (pinned to 8880682c63)

Solutions

  1. Pass the exact key returned by listDrafts()/getDraft(), never a bare id
  2. Build keys only via draftKey(type, id)
  3. Guard with parseDraftKey() and skip/400 on non-draft keys

Example fix

// before
await deleteDraft(kv, id); // id like 'abc123' -> throws

// after
const key = draftKey(type, id); // 'draft:dispatch:abc123'
await deleteDraft(kv, key);
Defensive patterns

Strategy: type-guard

Validate before calling

import { parseDraftKey } from './community-agent';
if (parseDraftKey(key) === null) {
  return new Response('invalid draft key', { status: 400 });
}

Type guard

import { parseDraftKey } from './community-agent';
function isDraftKey(key) {
  return typeof key === 'string' && parseDraftKey(key) !== null;
}
// usage: if (!isDraftKey(key)) continue; // skip foreign KV keys during sweeps

Prevention

When it happens

Trigger: Passing the draft id instead of the full KV key; a key whose type segment is not one of the known draft types; sweeping admin routes that forward arbitrary keys.

Common situations: The front-end sends {id} and the handler forwards it as key; a new draft type was added to constants but not to AGENT_DRAFT_TYPES; maintenance scripts iterating the whole namespace.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/e7da7cd4c33c7f80. Report an issue: GitHub.