Files
redefined-designs/backend/src/intake/anthropicClient.ts
T
bermudalambandClaude Opus 5 455c52cabb fix(intake): send the workspace id an identity-linked key requires (#271)
Every draft in QA failed with a 400: "anthropic-workspace-id is required when authenticating with an identity-linked API key". A key issued against a workspace, rather than standing alone, is refused unless the request names the workspace it acts in — and the client was constructed with an API key and nothing else.

Nothing about a key's shape says which kind it is, so no amount of configuration checking would have caught this. Only a real call would, which is exactly what #223's task 8 existed to make.

Sent only when ANTHROPIC_WORKSPACE_ID is set. Plenty of keys need no workspace, and sending an empty header would turn the ordinary case into a different error rather than leaving it working. Both compose files carry it with an empty default so an unset variable cannot fail a deploy, and the cutover doc goes from fifteen interpolated names to sixteen — checked against the file, and every name in the list now matches one in the compose.

The failure handling needed no change and got none. The submission kept its photos, the draft recorded ai_error, and the review queue showed the reason. A model call failing must never lose somebody's consignment, and it did not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 17:02:18 -05:00

57 lines
2.0 KiB
TypeScript

import Anthropic from '@anthropic-ai/sdk';
/**
* The client, or null when there is no key.
*
* Null rather than a throw, because an unconfigured environment is a working
* one: submissions still arrive and wait undrafted. The worker treats null
* exactly as it treats a failed call, which keeps one path rather than two.
*
* Constructed once and cached. The SDK holds a connection pool, and building
* one per submission would be wasteful on a route a stranger can trigger.
*/
let cached: Anthropic | null = null;
let resolved = false;
/**
* The headers a key needs beyond the key itself.
*
* An *identity-linked* key — one issued against a workspace rather than
* standing alone — is refused without an `anthropic-workspace-id` naming the
* workspace the request acts in:
*
* 400 invalid_request_error: anthropic-workspace-id is required when
* authenticating with an identity-linked API key
*
* Nothing about a key's shape says which kind it is, so this cannot be detected
* from configuration — only from a real call, which is what #223's task 8 was
* for and what found it (#271).
*
* Sent only when set. Plenty of keys need no workspace, and sending an empty
* header would turn the ordinary case into a different error.
*/
function workspaceHeaders(): Record<string, string> | undefined {
const workspaceId = process.env.ANTHROPIC_WORKSPACE_ID;
if (workspaceId === undefined || workspaceId.trim() === '') return undefined;
return { 'anthropic-workspace-id': workspaceId.trim() };
}
export function getAnthropicClient(): Anthropic | null {
if (resolved) return cached;
const key = process.env.ANTHROPIC_API_KEY;
cached =
key !== undefined && key.trim() !== ''
? new Anthropic({ apiKey: key, defaultHeaders: workspaceHeaders() })
: null;
resolved = true;
return cached;
}
/** Exposed for tests, which need a fresh decision per case. */
export function resetAnthropicClient(): void {
cached = null;
resolved = false;
}