Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
6.3 kB · 175 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176/** * Resolve file_uuid attachments on inbound bridge user messages. * * Web composer uploads via cookie-authed /api/{org}/upload, sends file_uuid * alongside the message. Here we fetch each via GET /api/oauth/files/{uuid}/content * (oauth-authed, same store), write to ~/.claude/uploads/{sessionId}/, and * return @path refs to prepend. Claude's Read tool takes it from there. * * Best-effort: any failure (no token, network, non-2xx, disk) logs debug and * skips that attachment. The message still reaches Claude, just without @path. */
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'import axios from 'axios'import { randomUUID } from 'crypto'import { mkdir, writeFile } from 'fs/promises'import { basename, join } from 'path'import { z } from 'zod/v4'import { getSessionId } from '../bootstrap/state.js'import { logForDebugging } from '../utils/debug.js'import { getClaudeConfigHomeDir } from '../utils/envUtils.js'import { lazySchema } from '../utils/lazySchema.js'import { getBridgeAccessToken, getBridgeBaseUrl } from './bridgeConfig.js'
const DOWNLOAD_TIMEOUT_MS = 30_000
function debug(msg: string): void { logForDebugging(`[bridge:inbound-attach] ${msg}`)}
const attachmentSchema = lazySchema(() => z.object({ file_uuid: z.string(), file_name: z.string(), }),)const attachmentsArraySchema = lazySchema(() => z.array(attachmentSchema()))
export type InboundAttachment = z.infer<ReturnType<typeof attachmentSchema>>
/** Pull file_attachments off a loosely-typed inbound message. */export function extractInboundAttachments(msg: unknown): InboundAttachment[] { if (typeof msg !== 'object' || msg === null || !('file_attachments' in msg)) { return [] } const parsed = attachmentsArraySchema().safeParse(msg.file_attachments) return parsed.success ? parsed.data : []}
/** * Strip path components and keep only filename-safe chars. file_name comes * from the network (web composer), so treat it as untrusted even though the * composer controls it. */function sanitizeFileName(name: string): string { const base = basename(name).replace(/[^a-zA-Z0-9._-]/g, '_') return base || 'attachment'}
function uploadsDir(): string { return join(getClaudeConfigHomeDir(), 'uploads', getSessionId())}
/** * Fetch + write one attachment. Returns the absolute path on success, * undefined on any failure. */async function resolveOne(att: InboundAttachment): Promise<string | undefined> { const token = getBridgeAccessToken() if (!token) { debug('skip: no oauth token') return undefined }
let data: Buffer try { // getOauthConfig() (via getBridgeBaseUrl) throws on a non-allowlisted // CLAUDE_CODE_CUSTOM_OAUTH_URL — keep it inside the try so a bad // FedStart URL degrades to "no @path" instead of crashing print.ts's // reader loop (which has no catch around the await). const url = `${getBridgeBaseUrl()}/api/oauth/files/${encodeURIComponent(att.file_uuid)}/content` const response = await axios.get(url, { headers: { Authorization: `Bearer ${token}` }, responseType: 'arraybuffer', timeout: DOWNLOAD_TIMEOUT_MS, validateStatus: () => true, }) if (response.status !== 200) { debug(`fetch ${att.file_uuid} failed: status=${response.status}`) return undefined } data = Buffer.from(response.data) } catch (e) { debug(`fetch ${att.file_uuid} threw: ${e}`) return undefined }
// uuid-prefix makes collisions impossible across messages and within one // (same filename, different files). 8 chars is enough — this isn't security. const safeName = sanitizeFileName(att.file_name) const prefix = ( att.file_uuid.slice(0, 8) || randomUUID().slice(0, 8) ).replace(/[^a-zA-Z0-9_-]/g, '_') const dir = uploadsDir() const outPath = join(dir, `${prefix}-${safeName}`)
try { await mkdir(dir, { recursive: true }) await writeFile(outPath, data) } catch (e) { debug(`write ${outPath} failed: ${e}`) return undefined }
debug(`resolved ${att.file_uuid} → ${outPath} (${data.length} bytes)`) return outPath}
/** * Resolve all attachments on an inbound message to a prefix string of * @path refs. Empty string if none resolved. */export async function resolveInboundAttachments( attachments: InboundAttachment[],): Promise<string> { if (attachments.length === 0) return '' debug(`resolving ${attachments.length} attachment(s)`) const paths = await Promise.all(attachments.map(resolveOne)) const ok = paths.filter((p): p is string => p !== undefined) if (ok.length === 0) return '' // Quoted form — extractAtMentionedFiles truncates unquoted @refs at the // first space, which breaks any home dir with spaces (/Users/John Smith/). return ok.map(p => `@"${p}"`).join(' ') + ' '}
/** * Prepend @path refs to content, whichever form it's in. * Targets the LAST text block — processUserInputBase reads inputString * from processedBlocks[processedBlocks.length - 1], so putting refs in * block[0] means they're silently ignored for [text, image] content. */export function prependPathRefs( content: string | Array<ContentBlockParam>, prefix: string,): string | Array<ContentBlockParam> { if (!prefix) return content if (typeof content === 'string') return prefix + content const i = content.findLastIndex(b => b.type === 'text') if (i !== -1) { const b = content[i]! if (b.type === 'text') { return [ ...content.slice(0, i), { ...b, text: prefix + b.text }, ...content.slice(i + 1), ] } } // No text block — append one at the end so it's last. return [...content, { type: 'text', text: prefix.trimEnd() }]}
/** * Convenience: extract + resolve + prepend. No-op when the message has no * file_attachments field (fast path — no network, returns same reference). */export async function resolveAndPrepend( msg: unknown, content: string | Array<ContentBlockParam>,): Promise<string | Array<ContentBlockParam>> { const attachments = extractInboundAttachments(msg) if (attachments.length === 0) return content const prefix = await resolveInboundAttachments(attachments) return prependPathRefs(content, prefix)}