the claude code sourcemaps leaked march 31
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 174 lines 5.8 kB view raw
1/** 2 * Upload BriefTool attachments to private_api so web viewers can preview them. 3 * 4 * When the repl bridge is active, attachment paths are meaningless to a web 5 * viewer (they're on Claude's machine). We upload to /api/oauth/file_upload — 6 * the same store MessageComposer/SpaceMessage render from — and stash the 7 * returned file_uuid alongside the path. Web resolves file_uuid → preview; 8 * desktop/local try path first. 9 * 10 * Best-effort: any failure (no token, bridge off, network error, 4xx) logs 11 * debug and returns undefined. The attachment still carries {path, size, 12 * isImage}, so local-terminal and same-machine-desktop render unaffected. 13 */ 14 15import { feature } from 'bun:bundle' 16import axios from 'axios' 17import { randomUUID } from 'crypto' 18import { readFile } from 'fs/promises' 19import { basename, extname } from 'path' 20import { z } from 'zod/v4' 21 22import { 23 getBridgeAccessToken, 24 getBridgeBaseUrlOverride, 25} from '../../bridge/bridgeConfig.js' 26import { getOauthConfig } from '../../constants/oauth.js' 27import { logForDebugging } from '../../utils/debug.js' 28import { lazySchema } from '../../utils/lazySchema.js' 29import { jsonStringify } from '../../utils/slowOperations.js' 30 31// Matches the private_api backend limit 32const MAX_UPLOAD_BYTES = 30 * 1024 * 1024 33 34const UPLOAD_TIMEOUT_MS = 30_000 35 36// Backend dispatches on mime: image/* → upload_image_wrapped (writes 37// PREVIEW/THUMBNAIL, no ORIGINAL), everything else → upload_generic_file 38// (ORIGINAL only, no preview). Only whitelist raster formats the 39// transcoder reliably handles — svg/bmp/ico risk a 400, and pdf routes 40// to upload_pdf_file_wrapped which also skips ORIGINAL. Dispatch 41// viewers use /preview for images and /contents for everything else, 42// so images go image/* and the rest go octet-stream. 43const MIME_BY_EXT: Record<string, string> = { 44 '.png': 'image/png', 45 '.jpg': 'image/jpeg', 46 '.jpeg': 'image/jpeg', 47 '.gif': 'image/gif', 48 '.webp': 'image/webp', 49} 50 51function guessMimeType(filename: string): string { 52 const ext = extname(filename).toLowerCase() 53 return MIME_BY_EXT[ext] ?? 'application/octet-stream' 54} 55 56function debug(msg: string): void { 57 logForDebugging(`[brief:upload] ${msg}`) 58} 59 60/** 61 * Base URL for uploads. Must match the host the token is valid for. 62 * 63 * Subprocess hosts (cowork) pass ANTHROPIC_BASE_URL alongside 64 * CLAUDE_CODE_OAUTH_TOKEN — prefer that since getOauthConfig() only 65 * returns staging when USE_STAGING_OAUTH is set, which such hosts don't 66 * set. Without this a staging token hits api.anthropic.com → 401 → silent 67 * skip → web viewer sees inert cards with no file_uuid. 68 */ 69function getBridgeBaseUrl(): string { 70 return ( 71 getBridgeBaseUrlOverride() ?? 72 process.env.ANTHROPIC_BASE_URL ?? 73 getOauthConfig().BASE_API_URL 74 ) 75} 76 77// /api/oauth/file_upload returns one of ChatMessage{Image,Blob,Document}FileSchema. 78// All share file_uuid; that's the only field we need. 79const uploadResponseSchema = lazySchema(() => 80 z.object({ file_uuid: z.string() }), 81) 82 83export type BriefUploadContext = { 84 replBridgeEnabled: boolean 85 signal?: AbortSignal 86} 87 88/** 89 * Upload a single attachment. Returns file_uuid on success, undefined otherwise. 90 * Every early-return is intentional graceful degradation. 91 */ 92export async function uploadBriefAttachment( 93 fullPath: string, 94 size: number, 95 ctx: BriefUploadContext, 96): Promise<string | undefined> { 97 // Positive pattern so bun:bundle eliminates the entire body from 98 // non-BRIDGE_MODE builds (negative `if (!feature(...)) return` does not). 99 if (feature('BRIDGE_MODE')) { 100 if (!ctx.replBridgeEnabled) return undefined 101 102 if (size > MAX_UPLOAD_BYTES) { 103 debug(`skip ${fullPath}: ${size} bytes exceeds ${MAX_UPLOAD_BYTES} limit`) 104 return undefined 105 } 106 107 const token = getBridgeAccessToken() 108 if (!token) { 109 debug('skip: no oauth token') 110 return undefined 111 } 112 113 let content: Buffer 114 try { 115 content = await readFile(fullPath) 116 } catch (e) { 117 debug(`read failed for ${fullPath}: ${e}`) 118 return undefined 119 } 120 121 const baseUrl = getBridgeBaseUrl() 122 const url = `${baseUrl}/api/oauth/file_upload` 123 const filename = basename(fullPath) 124 const mimeType = guessMimeType(filename) 125 const boundary = `----FormBoundary${randomUUID()}` 126 127 // Manual multipart — same pattern as filesApi.ts. The oauth endpoint takes 128 // a single "file" part (no "purpose" field like the public Files API). 129 const body = Buffer.concat([ 130 Buffer.from( 131 `--${boundary}\r\n` + 132 `Content-Disposition: form-data; name="file"; filename="${filename}"\r\n` + 133 `Content-Type: ${mimeType}\r\n\r\n`, 134 ), 135 content, 136 Buffer.from(`\r\n--${boundary}--\r\n`), 137 ]) 138 139 try { 140 const response = await axios.post(url, body, { 141 headers: { 142 Authorization: `Bearer ${token}`, 143 'Content-Type': `multipart/form-data; boundary=${boundary}`, 144 'Content-Length': body.length.toString(), 145 }, 146 timeout: UPLOAD_TIMEOUT_MS, 147 signal: ctx.signal, 148 validateStatus: () => true, 149 }) 150 151 if (response.status !== 201) { 152 debug( 153 `upload failed for ${fullPath}: status=${response.status} body=${jsonStringify(response.data).slice(0, 200)}`, 154 ) 155 return undefined 156 } 157 158 const parsed = uploadResponseSchema().safeParse(response.data) 159 if (!parsed.success) { 160 debug( 161 `unexpected response shape for ${fullPath}: ${parsed.error.message}`, 162 ) 163 return undefined 164 } 165 166 debug(`uploaded ${fullPath}${parsed.data.file_uuid} (${size} bytes)`) 167 return parsed.data.file_uuid 168 } catch (e) { 169 debug(`upload threw for ${fullPath}: ${e}`) 170 return undefined 171 } 172 } 173 return undefined 174}