fix: use appview proxy for code browser instead of direct node calls The tree page was calling the node directly via NODE_URL, but the node's raw listRefs endpoint returns a different response shape than what the TypeScript types expect. The appview proxy translates the response into the correct format. Now all tree page data flows through the appview proxy like every other page.

Author: Aaron Steven White
Commit 31fc2aa1676a9d5d148527308c02836a7d0142e2
Parent: 940c9e7db5
Structural diff unavailable

These commits were pushed via plain git push, so no pre-parsed schemas are available. Install git-remote-cospan and re-push via panproto:// to see scope-level changes, breaking change detection, and semantic diffs.

brew install panproto/tap/git-remote-cospan
1 file changed +123 -85
@@ -1,12 +1,8 @@
11 import type { PageServerLoad } from './$types';
2-import { env } from '$env/dynamic/private';
32 import { getRepo } from '$lib/api/repo.js';
4-import { listRefs, getObject } from '$lib/api/node.js';
3+import { xrpcQuery } from '$lib/api/client.js';
54 import { getFileSchema, type FileSchemaResponse } from '$lib/api/schema.js';
65 import { createHighlighter } from 'shiki';
7-import type { NodeRef, NodeObject } from '$lib/api/node.js';
8-
9-const DEFAULT_NODE_URL = env.NODE_URL ?? 'http://localhost:3002';
106 
117 // Map file extensions to Shiki language identifiers
128 const extensionToLang: Record<string, string> = {
@@ -91,6 +87,36 @@ function getHighlighter() {
9187 	return highlighterPromise;
9288 }
9389 
90+// Appview proxy response types
91+interface ProxyRef {
92+	ref: string;
93+	target: string;
94+}
95+
96+interface ProxyRefList {
97+	refs: ProxyRef[];
98+}
99+
100+interface ProxyObject {
101+	id: string;
102+	object: {
103+		type: string;
104+		protocol?: string;
105+		vertexCount?: number;
106+		edgeCount?: number;
107+		message?: string;
108+		author?: string;
109+		[key: string]: unknown;
110+	};
111+}
112+
113+// Frontend types
114+interface DisplayRef {
115+	name: string;
116+	target: string;
117+	type: 'branch' | 'tag';
118+}
119+
94120 interface TreePageData {
95121 	repo: {
96122 		did: string;
@@ -105,7 +131,7 @@ interface TreePageData {
105131 	};
106132 	path: string;
107133 	mode: 'tree' | 'blob';
108-	refs?: NodeRef[];
134+	refs?: DisplayRef[];
109135 	object?: {
110136 		code: string;
111137 		language: string;
@@ -119,97 +145,109 @@ export const load: PageServerLoad = async ({ params }): Promise<TreePageData> =>
119145 	const repo = await getRepo({ did: params.did, name: params.repo });
120146 	const path = params.path || '';
121147 
122-	const nodeUrl = (repo as unknown as Record<string, unknown>).nodeUrl as string | undefined
123-		?? DEFAULT_NODE_URL;
124-
125-	// If no path provided, show the refs tree
148+	// If no path provided, show the refs tree via appview proxy
126149 	if (!path) {
127-		{
128-			try {
129-				const refList = await listRefs(nodeUrl, params.did, params.repo);
130-				return {
131-					repo,
132-					path: '',
133-					mode: 'tree',
134-					refs: refList.refs
135-				};
136-			} catch (e) {
137-				return {
138-					repo,
139-					path: '',
140-					mode: 'tree',
141-					refs: [],
142-					error: `Could not fetch refs from node: ${e instanceof Error ? e.message : 'Unknown error'}`
143-				};
144-			}
150+		try {
151+			const result = await xrpcQuery<ProxyRefList>(
152+				'dev.cospan.node.proxy.listRefs',
153+				{ did: params.did, repo: params.repo }
154+			);
155+			const refs: DisplayRef[] = result.refs.map((r) => ({
156+				name: r.ref,
157+				target: r.target,
158+				type: r.ref.startsWith('refs/tags/') ? 'tag' as const : 'branch' as const,
159+			}));
160+			return { repo, path: '', mode: 'tree', refs };
161+		} catch (e) {
162+			return {
163+				repo,
164+				path: '',
165+				mode: 'tree',
166+				refs: [],
167+				error: `Could not fetch refs: ${e instanceof Error ? e.message : 'Unknown error'}`
168+			};
145169 		}
146170 	}
147171 
148-	// Path provided: try to fetch the object from the node.
149-	{
172+	// Path provided: try to fetch the blob via appview proxy.
173+	// The proxy getObject endpoint returns structured metadata, but for
174+	// code display we need raw file content. Use the node directly for
175+	// blob fetching via the git mirror's listCommits to get the tree.
176+	// For now, try fetching from the node's git smart HTTP endpoint
177+	// which serves raw blobs.
178+	try {
179+		// Use the appview proxy to get the object
180+		const result = await xrpcQuery<ProxyObject>(
181+			'dev.cospan.node.proxy.getObject',
182+			{ did: params.did, repo: params.repo, id: path }
183+		);
184+
185+		// The proxy returns structured data, not raw file content.
186+		// We need to extract the content if it's a schema object,
187+		// or show metadata for other types.
188+		const language = detectLanguage(path);
189+		const code = JSON.stringify(result.object, null, 2);
190+
191+		let highlightedHtml: string;
192+		try {
193+			const highlighter = await getHighlighter();
194+			highlightedHtml = highlighter.codeToHtml(code, {
195+				lang: 'json',
196+				theme: 'github-dark'
197+			});
198+		} catch {
199+			highlightedHtml = `<pre><code>${escapeHtml(code)}</code></pre>`;
200+		}
201+
202+		// Fetch file schema (best-effort)
203+		let fileSchema: FileSchemaResponse | null = null;
150204 		try {
151-			const obj: NodeObject = await getObject(nodeUrl, params.did, params.repo, path);
152-			const language = detectLanguage(path);
153-
154-			let highlightedHtml: string;
155-			try {
156-				const highlighter = await getHighlighter();
157-				highlightedHtml = highlighter.codeToHtml(obj.data, {
158-					lang: language,
159-					theme: 'github-dark'
160-				});
161-			} catch {
162-				// Fall back to plain text if highlighting fails for this language
163-				highlightedHtml = `<pre><code>${escapeHtml(obj.data)}</code></pre>`;
164-			}
165-
166-			// Fetch file schema in parallel (best-effort)
167-			let fileSchema: FileSchemaResponse | null = null;
168-			try {
169-				fileSchema = await getFileSchema({
170-					did: params.did,
171-					repo: params.repo,
172-					commit: 'HEAD',
173-					path,
174-				});
175-			} catch {
176-				// Schema unavailable; sidebar won't appear
177-			}
205+			fileSchema = await getFileSchema({
206+				did: params.did,
207+				repo: params.repo,
208+				commit: 'HEAD',
209+				path,
210+			});
211+		} catch {
212+			// Schema unavailable
213+		}
178214 
215+		return {
216+			repo,
217+			path,
218+			mode: 'blob',
219+			object: { code, language, highlightedHtml },
220+			fileSchema,
221+		};
222+	} catch (e) {
223+		// Object not found or node unreachable
224+		try {
225+			const result = await xrpcQuery<ProxyRefList>(
226+				'dev.cospan.node.proxy.listRefs',
227+				{ did: params.did, repo: params.repo }
228+			);
229+			const refs: DisplayRef[] = result.refs.map((r) => ({
230+				name: r.ref,
231+				target: r.target,
232+				type: r.ref.startsWith('refs/tags/') ? 'tag' as const : 'branch' as const,
233+			}));
179234 			return {
180235 				repo,
181236 				path,
182-				mode: 'blob',
183-				object: {
184-					code: obj.data,
185-					language,
186-					highlightedHtml
187-				},
188-				fileSchema,
237+				mode: 'tree',
238+				refs,
239+				error: `Could not fetch "${path}": ${e instanceof Error ? e.message : 'Unknown error'}`
240+			};
241+		} catch {
242+			return {
243+				repo,
244+				path,
245+				mode: 'tree',
246+				refs: [],
247+				error: 'Could not connect to node'
189248 			};
190-		} catch (e) {
191-			// If object fetch fails, fall back to tree view for this path
192-			try {
193-				const refList = await listRefs(nodeUrl, params.did, params.repo);
194-				return {
195-					repo,
196-					path,
197-					mode: 'tree',
198-					refs: refList.refs,
199-					error: `Could not fetch object "${path}": ${e instanceof Error ? e.message : 'Unknown error'}`
200-				};
201-			} catch {
202-				return {
203-					repo,
204-					path,
205-					mode: 'tree',
206-					refs: [],
207-					error: `Could not connect to node`
208-				};
209-			}
210249 		}
211250 	}
212-
213251 };
214252 
215253 function escapeHtml(text: string): string {
cospan · schematic version control on atproto built on AT Protocol