a tool for shared writing and social publishing
1import { NextRequest, NextResponse } from "next/server";
2import { AtpAgent, AtUri } from "@atproto/api";
3import { DidResolver } from "@atproto/identity";
4import {
5 PubLeafletDocument,
6 PubLeafletPublication,
7 PubLeafletPagesLinearDocument,
8 PubLeafletPagesCanvas,
9} from "lexicons/api";
10
11const didResolver = new DidResolver({});
12
13export async function GET(request: NextRequest) {
14 try {
15 const atUriString = request.nextUrl.searchParams.get("uri");
16
17 if (!atUriString) {
18 return NextResponse.json(
19 {
20 success: false,
21 error: "Missing uri parameter",
22 },
23 { status: 400 },
24 );
25 }
26
27 const uri = new AtUri(atUriString);
28
29 // Only allow document and publication collections
30 if (
31 uri.collection !== "pub.leaflet.document" &&
32 uri.collection !== "pub.leaflet.publication"
33 ) {
34 return NextResponse.json(
35 {
36 success: false,
37 error:
38 "Unsupported collection type. Must be pub.leaflet.document or pub.leaflet.publication",
39 },
40 { status: 400 },
41 );
42 }
43
44 // Resolve DID to get service endpoint
45 const did = await didResolver.resolve(uri.host);
46 const service = did?.service?.[0];
47
48 if (!service) {
49 return NextResponse.json(
50 {
51 success: false,
52 error: "Could not resolve DID service endpoint",
53 },
54 { status: 404 },
55 );
56 }
57
58 // Fetch the record from AT Protocol
59 const agent = new AtpAgent({ service: service.serviceEndpoint as string });
60
61 let recordResponse;
62 try {
63 recordResponse = await agent.com.atproto.repo.getRecord({
64 repo: uri.host,
65 collection: uri.collection,
66 rkey: uri.rkey,
67 });
68 } catch (e) {
69 return NextResponse.json(
70 {
71 success: false,
72 error: "Record not found",
73 },
74 { status: 404 },
75 );
76 }
77
78 // Validate based on collection type
79 if (uri.collection === "pub.leaflet.document") {
80 const result = PubLeafletDocument.validateRecord(
81 recordResponse.data.value,
82 );
83 if (result.success) {
84 return NextResponse.json({
85 success: true,
86 collection: uri.collection,
87 record: result.value,
88 });
89 } else {
90 // Detailed validation: validate pages and blocks individually
91 const record = recordResponse.data.value as {
92 pages?: Array<{ $type?: string; blocks?: Array<{ block?: unknown }> }>;
93 };
94 const pageErrors: Array<{
95 pageIndex: number;
96 pageType: string;
97 error?: unknown;
98 blockErrors?: Array<{
99 blockIndex: number;
100 blockType: string;
101 error: unknown;
102 block: unknown;
103 }>;
104 }> = [];
105
106 if (record.pages && Array.isArray(record.pages)) {
107 for (let pageIndex = 0; pageIndex < record.pages.length; pageIndex++) {
108 const page = record.pages[pageIndex];
109 const pageType = page?.$type || "unknown";
110
111 // Validate page based on type
112 let pageResult;
113 if (pageType === "pub.leaflet.pages.linearDocument") {
114 pageResult = PubLeafletPagesLinearDocument.validateMain(page);
115 } else if (pageType === "pub.leaflet.pages.canvas") {
116 pageResult = PubLeafletPagesCanvas.validateMain(page);
117 } else {
118 pageErrors.push({
119 pageIndex,
120 pageType,
121 error: `Unknown page type: ${pageType}`,
122 });
123 continue;
124 }
125
126 if (!pageResult.success) {
127 // Page has errors, validate individual blocks
128 const blockErrors: Array<{
129 blockIndex: number;
130 blockType: string;
131 error: unknown;
132 block: unknown;
133 }> = [];
134
135 if (page.blocks && Array.isArray(page.blocks)) {
136 for (
137 let blockIndex = 0;
138 blockIndex < page.blocks.length;
139 blockIndex++
140 ) {
141 const blockWrapper = page.blocks[blockIndex];
142 const blockType =
143 (blockWrapper?.block as { $type?: string })?.$type ||
144 "unknown";
145
146 // Validate block wrapper based on page type
147 let blockResult;
148 if (pageType === "pub.leaflet.pages.linearDocument") {
149 blockResult =
150 PubLeafletPagesLinearDocument.validateBlock(blockWrapper);
151 } else {
152 blockResult =
153 PubLeafletPagesCanvas.validateBlock(blockWrapper);
154 }
155
156 if (!blockResult.success) {
157 blockErrors.push({
158 blockIndex,
159 blockType,
160 error: blockResult.error,
161 block: blockWrapper,
162 });
163 }
164 }
165 }
166
167 pageErrors.push({
168 pageIndex,
169 pageType,
170 error: pageResult.error,
171 blockErrors: blockErrors.length > 0 ? blockErrors : undefined,
172 });
173 }
174 }
175 }
176
177 return NextResponse.json({
178 success: false,
179 collection: uri.collection,
180 error: result.error,
181 pageErrors: pageErrors.length > 0 ? pageErrors : undefined,
182 record: recordResponse.data.value,
183 });
184 }
185 }
186
187 if (uri.collection === "pub.leaflet.publication") {
188 const result = PubLeafletPublication.validateRecord(
189 recordResponse.data.value,
190 );
191 if (result.success) {
192 return NextResponse.json({
193 success: true,
194 collection: uri.collection,
195 record: result.value,
196 });
197 } else {
198 return NextResponse.json({
199 success: false,
200 collection: uri.collection,
201 error: result.error,
202 });
203 }
204 }
205 } catch (error) {
206 console.error("Error validating AT URI:", error);
207 return NextResponse.json(
208 {
209 success: false,
210 error: "Invalid URI or internal error",
211 },
212 { status: 400 },
213 );
214 }
215}