AT-based link agregator. Mirror of https://github.com/likeandscribe/frontpage

Validate record via automatic lexicon discovery (#219)

* Validate record via automatic lexicon discovery

* Add loading and error states

* WIP

* Use lpm for resolution

* Link authority and better keys

* Fix lints

* Allow navigating to lex: uris

* Render blobs properly

* Bump lpm

* Bump lpm

authored by tom.sherman.is and committed by

GitHub e11b9ef4 3960add0

+511 -138
+1
.npmrc
··· 1 + @jsr:registry=https://npm.jsr.io
+321 -52
packages/atproto-browser/app/at/[identifier]/[collection]/[rkey]/page.tsx
··· 1 1 import { JSONType, JSONValue } from "@/app/at/_lib/atproto-json"; 2 - import { resolveNsid, resolveIdentity } from "@/lib/atproto-server"; 2 + import { resolveNsidAuthority, resolveIdentity } from "@/lib/atproto-server"; 3 + import Link from "@/lib/link"; 3 4 import { getHandle, getKey, getPds } from "@atproto/identity"; 5 + import { 6 + InvalidLexiconError, 7 + LexValue, 8 + LexiconDefNotFoundError, 9 + LexiconDoc, 10 + Lexicons, 11 + lexiconDoc, 12 + } from "@atproto/lexicon"; 13 + import { AtpBaseClient, ComAtprotoRepoGetRecord } from "@atproto/api"; 4 14 import { verifyRecords } from "@atproto/repo"; 5 - import { cache, Suspense } from "react"; 15 + import React, { cache, Fragment, ReactNode, Suspense } from "react"; 6 16 import { ErrorBoundary } from "react-error-boundary"; 7 17 import { z } from "zod"; 18 + import { AtUri, InvalidNsidError, NSID } from "@atproto/syntax"; 19 + import { resolveNSIDs } from "@lpm/core"; 20 + import { getAtUriPath } from "@/lib/util"; 8 21 9 22 export default async function RkeyPage(props: { 10 23 params: Promise<{ ··· 35 48 ); 36 49 37 50 if (!getRecordResult.success) { 38 - return ( 39 - <div> 40 - 🚨 Failed to fetch record:{" "} 41 - {getRecordResult.knownError ?? getRecordResult.error} 42 - </div> 43 - ); 51 + return <div>🚨 Failed to fetch record: {getRecordResult.error}</div>; 44 52 } 45 53 46 54 return ( ··· 77 85 {params.collection === "com.atproto.lexicon.schema" ? ( 78 86 <ErrorBoundary fallback={<div>❌ Error verifying lexicon</div>}> 79 87 <Suspense fallback={<div>Verifying lexicon...</div>}> 80 - <LexiconVerification 88 + <LexiconDefinitionVerification 81 89 did={identityResult.didDocument.id} 82 90 nsid={params.rkey} 83 91 /> 84 92 </Suspense> 85 93 </ErrorBoundary> 86 94 ) : null} 95 + 96 + <ErrorBoundary 97 + fallback={ 98 + <details> 99 + <summary>❌ Error validating lexicon</summary> 100 + <div>An unknown error occurred</div> 101 + </details> 102 + } 103 + > 104 + <Suspense 105 + fallback={ 106 + <details aria-busy> 107 + <summary>🤔 Validating against lexicon...</summary> 108 + </details> 109 + } 110 + > 111 + <RecordValidation 112 + did={didDocument.id} 113 + collection={params.collection} 114 + rkey={params.rkey} 115 + /> 116 + </Suspense> 117 + </ErrorBoundary> 118 + 87 119 <JSONValue data={getRecordResult.record.value} repo={didDocument.id} /> 88 120 <small> 89 121 <a href={getRecordResult.url} rel="ugc"> ··· 155 187 !compareJson( 156 188 // Converting to plain object because some values are classes (eg. CIDs) 157 189 JSON.parse(JSON.stringify(claim.record)), 158 - recordResult.record?.value, 190 + JSON.parse(JSON.stringify(recordResult.record?.value)), 159 191 ) 160 192 ) { 161 193 return ( ··· 175 207 return <span title="Valid record">🔒</span>; 176 208 } 177 209 178 - const KNOWN_GET_RECORD_ERRORS = ["RecordNotFound", "InvalidRequest"] as const; 179 - 180 210 type GetRecordResult = 181 211 | { 182 212 success: true; 183 213 url: string; 184 214 record: { 185 215 uri: string; 186 - cid: string; 187 - value: JSONType; 216 + cid?: string; 217 + value: LexValue; 188 218 }; 189 219 } 190 220 | { 191 221 success: false; 192 - knownError: (typeof KNOWN_GET_RECORD_ERRORS)[number] | null; 193 222 error: string; 194 223 }; 195 224 ··· 214 243 throw new Error("No PDS found for DID"); 215 244 } 216 245 217 - const getRecordUrl = new URL(`${pds}/xrpc/com.atproto.repo.getRecord`); 218 - getRecordUrl.searchParams.set("repo", didDocument.id); 219 - getRecordUrl.searchParams.set("collection", collection); 220 - getRecordUrl.searchParams.set("rkey", rkey); 246 + const atpClient = new AtpBaseClient((url, init) => 247 + fetch(new URL(url, pds), init), 248 + ); 221 249 222 - const response = await fetch(getRecordUrl, { 223 - method: "GET", 224 - headers: { 225 - "Content-Type": "application/json", 226 - }, 227 - }); 250 + let response; 228 251 229 - if (!response.ok) { 230 - const parsed = GetRecordFailure.parse(await response.json()); 231 - const knownError = 232 - KNOWN_GET_RECORD_ERRORS.find((e) => e === parsed.error) ?? null; 252 + try { 253 + response = await atpClient.com.atproto.repo.getRecord({ 254 + repo: didDocument.id, 255 + collection, 256 + rkey, 257 + }); 258 + } catch (e) { 259 + if (e instanceof ComAtprotoRepoGetRecord.RecordNotFoundError) { 260 + return { 261 + success: false as const, 262 + error: "RecordNotFound", 263 + }; 264 + } 233 265 234 - return { 235 - success: false as const, 236 - knownError, 237 - error: `${response.statusText}. URL: ${getRecordUrl.toString()}`, 238 - }; 266 + throw e; 239 267 } 240 - 241 - const record = RecordValueSchema.parse(await response.json()); 242 268 243 269 return { 244 270 success: true as const, 245 - record, 246 - url: getRecordUrl.toString(), 271 + record: response.data, 272 + url: `${pds}/xrpc/com.atproto.repo.getRecord?repo=${didDocument.id}&collection=${collection}&rkey=${rkey}`, 247 273 }; 248 274 }, 249 275 ); ··· 259 285 ]), 260 286 ); 261 287 262 - const RecordValueSchema = z.object({ 263 - uri: z.string(), 264 - cid: z.string(), 265 - value: JsonTypeSchema, 266 - }); 267 - 268 - const GetRecordFailure = z.object({ 269 - error: z.string(), 270 - message: z.string().optional(), 271 - }); 272 - 273 288 /** 274 289 * Compare objects deeply, ignoring key order. 275 290 */ ··· 310 325 }); 311 326 } 312 327 313 - async function LexiconVerification({ 328 + async function LexiconDefinitionVerification({ 314 329 nsid: nsidStr, 315 330 did, 316 331 }: { 317 332 nsid: string; 318 333 did: string; 319 334 }) { 320 - const result = await resolveNsid(did, nsidStr); 335 + let nsid; 336 + try { 337 + nsid = NSID.parse(nsidStr); 338 + } catch (e) { 339 + if (e instanceof InvalidNsidError) { 340 + return <div>❌ Invalid lexicon: {e.message}</div>; 341 + } else { 342 + throw e; 343 + } 344 + } 345 + const result = await resolveNsidAuthority(nsid); 346 + 321 347 return ( 322 348 <div style={{ marginTop: "1em" }}> 323 349 {result.success 324 - ? "✅ Verified lexicon" 350 + ? result.authorityDid === did 351 + ? "✅ Verified lexicon" 352 + : `❌ Unverified lexicon: ${result.authorityDid}` 325 353 : `❌ Unverified lexicon: ${result.error}`} 326 354 </div> 327 355 ); 328 356 } 357 + 358 + async function RecordValidation({ 359 + did, 360 + collection, 361 + rkey, 362 + }: { 363 + did: string; 364 + collection: string; 365 + rkey: string; 366 + }) { 367 + const successfulSteps: ReactNode[] = []; 368 + 369 + let nsid; 370 + try { 371 + nsid = NSID.parse(collection); 372 + } catch (e) { 373 + if (e instanceof InvalidNsidError) { 374 + return ( 375 + <RecordValidationResult error={e.message} steps={successfulSteps} /> 376 + ); 377 + } else { 378 + throw e; 379 + } 380 + } 381 + 382 + const nsidAuthorityResult = await resolveNsidAuthority(nsid); 383 + 384 + if (!nsidAuthorityResult.success) { 385 + return ( 386 + <RecordValidationResult 387 + error={nsidAuthorityResult.error} 388 + steps={successfulSteps} 389 + /> 390 + ); 391 + } 392 + 393 + successfulSteps.push( 394 + <li> 395 + Fetched authority:{" "} 396 + <Link href={getAtUriPath({ host: nsidAuthorityResult.authorityDid })}> 397 + {nsidAuthorityResult.authorityDid} 398 + </Link> 399 + </li>, 400 + ); 401 + 402 + const lexiconRecordResult = await getRecord( 403 + nsidAuthorityResult.authorityDid, 404 + "com.atproto.lexicon.schema", 405 + nsid.toString(), 406 + ); 407 + 408 + if (!lexiconRecordResult.success) { 409 + return ( 410 + <details> 411 + <summary>❌ Record not validated</summary> 412 + <pre>{lexiconRecordResult.error}</pre> 413 + </details> 414 + ); 415 + } 416 + 417 + successfulSteps.push( 418 + <li> 419 + Fetched lexicon doc{" "} 420 + <Link href={`/at?u=${lexiconRecordResult.record.uri}`}> 421 + {lexiconRecordResult.record.uri} 422 + </Link> 423 + </li>, 424 + ); 425 + 426 + const schemaResult = lexiconDoc.safeParse( 427 + omit(lexiconRecordResult.record.value as Record<string, unknown>, [ 428 + "$type", 429 + ]), 430 + ); 431 + 432 + if (!schemaResult.success) { 433 + return ( 434 + <RecordValidationResult 435 + error={ 436 + <> 437 + Failed to parse lexicon doc: <pre>{schemaResult.error.message}</pre> 438 + </> 439 + } 440 + steps={successfulSteps} 441 + /> 442 + ); 443 + } 444 + 445 + const resolvedLexicon = await resolveLexiconDocs( 446 + nsidAuthorityResult.authorityDid, 447 + nsid, 448 + ); 449 + 450 + successfulSteps.push( 451 + <li> 452 + Resolved {resolvedLexicon.successes.length} docs:{" "} 453 + <ul> 454 + {resolvedLexicon.successes.map((resolution) => ( 455 + <li key={resolution.uri.toString()}> 456 + <Link href={getAtUriPath(resolution.uri)}> 457 + {resolution.uri.rkey} 458 + </Link> 459 + </li> 460 + ))} 461 + </ul> 462 + </li>, 463 + ); 464 + 465 + if (resolvedLexicon.errors.length > 0) { 466 + return ( 467 + <RecordValidationResult 468 + error={ 469 + <ul> 470 + {resolvedLexicon.errors.map((error) => ( 471 + <li key={error.nsid.toString()}> 472 + <pre> 473 + {error.nsid.toString()}: {error.error} 474 + </pre> 475 + </li> 476 + ))} 477 + </ul> 478 + } 479 + steps={successfulSteps} 480 + /> 481 + ); 482 + } 483 + 484 + const lexicons = new Lexicons(resolvedLexicon.successes.map((r) => r.doc)); 485 + 486 + const recordResult = await getRecord(did, collection, rkey); 487 + if (!recordResult.success) { 488 + // This should never happen, as we should have already fetched the record 489 + throw new Error(recordResult.error); 490 + } 491 + 492 + const record = recordResult.record; 493 + 494 + let validationResult; 495 + try { 496 + validationResult = lexicons.validate(collection, record.value); 497 + } catch (e) { 498 + return ( 499 + <RecordValidationResult 500 + error={ 501 + e instanceof InvalidLexiconError || 502 + e instanceof LexiconDefNotFoundError 503 + ? `Error validating record: ${e.message}` 504 + : "Unknown error occurred calling validate()" 505 + } 506 + steps={successfulSteps} 507 + /> 508 + ); 509 + } 510 + 511 + if (!validationResult.success) { 512 + return ( 513 + <RecordValidationResult 514 + error={validationResult.error.message} 515 + steps={successfulSteps} 516 + /> 517 + ); 518 + } 519 + 520 + return <RecordValidationResult steps={successfulSteps} />; 521 + } 522 + 523 + function RecordValidationResult({ 524 + error, 525 + steps, 526 + }: { 527 + error?: ReactNode; 528 + steps: ReactNode[]; 529 + }) { 530 + return ( 531 + <details> 532 + <summary> 533 + {error ? "❌ Record not validated" : "✅ Record validated"} 534 + </summary> 535 + <ul> 536 + {steps.map((step, i) => ( 537 + <Fragment key={i}>{step}</Fragment> 538 + ))} 539 + {error ? <li>{error}</li> : null} 540 + </ul> 541 + </details> 542 + ); 543 + } 544 + 545 + function omit( 546 + obj: Record<string, unknown>, 547 + keys: string[], 548 + ): Record<string, unknown> { 549 + const copy = { ...obj }; 550 + for (const key of keys) { 551 + delete copy[key]; 552 + } 553 + return copy; 554 + } 555 + 556 + async function resolveLexiconDocs( 557 + did: string, 558 + nsid: NSID, 559 + ): Promise<{ 560 + successes: Array<{ 561 + doc: LexiconDoc; 562 + uri: AtUri; 563 + }>; 564 + errors: { 565 + nsid: NSID; 566 + error: string; 567 + }[]; 568 + }> { 569 + const resolutions = []; 570 + for await (const resolution of resolveNSIDs([nsid.toString()])) { 571 + resolutions.push(resolution); 572 + } 573 + 574 + const successes = resolutions.filter((resolution) => resolution.success); 575 + const errors = resolutions 576 + .filter((resolution) => !resolution.success) 577 + .map((resolution) => ({ 578 + error: resolution.errorCode, 579 + nsid: resolution.nsid, 580 + })); 581 + 582 + const mainResolution = resolutions.find( 583 + (resolution) => 584 + resolution.success && 585 + resolution.nsid.toString() === nsid.toString() && 586 + resolution.uri.host === did, 587 + ); 588 + 589 + if (!mainResolution) { 590 + throw new Error("Main resolution not found"); 591 + } 592 + 593 + return { 594 + successes, 595 + errors, 596 + }; 597 + }
+56 -45
packages/atproto-browser/app/at/_lib/atproto-json.tsx
··· 1 1 import { isDid } from "@atproto/did"; 2 2 import Link from "@/lib/link"; 3 - import { AtBlob } from "../../../lib/at-blob"; 4 3 import { getAtUriPath } from "@/lib/util"; 5 4 import { AtUri } from "@atproto/syntax"; 6 5 import { VideoEmbed } from "./video-embed"; 7 6 import { ErrorBoundary } from "react-error-boundary"; 8 7 import { VideoEmbedWrapper } from "./video-embed-client"; 8 + import { BlobRef, LexValue } from "@atproto/lexicon"; 9 9 10 10 function naiveAtUriCheck(atUri: string) { 11 11 if (!atUri.startsWith("at://")) { ··· 91 91 data: { [x: string]: JSONType }; 92 92 repo: string; 93 93 }) { 94 - const parseBlobResult = AtBlob.safeParse(data); 95 - 96 - const rawObj = ( 94 + return ( 97 95 <dl> 98 96 {Object.entries(data).map(([key, value]) => ( 99 97 <div key={key} style={{ display: "flex", gap: 10 }}> ··· 113 111 ))} 114 112 </dl> 115 113 ); 116 - 117 - if ( 118 - parseBlobResult.success && 119 - parseBlobResult.data.mimeType.startsWith("image/") 120 - ) { 121 - return ( 122 - <> 123 - {/* eslint-disable-next-line @next/next/no-img-element */} 124 - <img 125 - src={`https://cdn.bsky.app/img/feed_thumbnail/plain/${repo}/${parseBlobResult.data.ref.$link}@jpeg`} 126 - alt="" 127 - width={200} 128 - /> 129 - <details> 130 - <summary>View blob content</summary> 131 - {rawObj} 132 - </details> 133 - </> 134 - ); 135 - } 136 - 137 - if ( 138 - parseBlobResult.success && 139 - parseBlobResult.data.mimeType === "video/mp4" 140 - ) { 141 - return ( 142 - <> 143 - <ErrorBoundary fallback={<VideoEmbedWrapper />}> 144 - <VideoEmbed cid={parseBlobResult.data.ref.$link} did={repo} /> 145 - </ErrorBoundary> 146 - <details> 147 - <summary>View blob content</summary> 148 - {rawObj} 149 - </details> 150 - </> 151 - ); 152 - } 153 - 154 - return rawObj; 155 114 } 156 115 157 116 function JSONArray({ data, repo }: { data: JSONType[]; repo: string }) { ··· 167 126 ); 168 127 } 169 128 170 - export function JSONValue({ data, repo }: { data: JSONType; repo: string }) { 129 + export function JSONValue({ data, repo }: { data: LexValue; repo: string }) { 171 130 if (typeof data === "string") { 172 131 return <JSONString data={data} />; 173 132 } ··· 183 142 if (Array.isArray(data)) { 184 143 return <JSONArray data={data} repo={repo} />; 185 144 } 186 - return <JSONObject data={data} repo={repo} />; 145 + 146 + if (data instanceof BlobRef) { 147 + const blobContent = ( 148 + <JSONObject 149 + data={ 150 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 151 + data.toJSON() as any 152 + } 153 + repo={repo} 154 + /> 155 + ); 156 + 157 + if (data.mimeType.startsWith("image/")) { 158 + return ( 159 + <> 160 + {/* eslint-disable-next-line @next/next/no-img-element */} 161 + <img 162 + src={`https://cdn.bsky.app/img/feed_thumbnail/plain/${repo}/${data.ref}@jpeg`} 163 + alt="" 164 + width={200} 165 + /> 166 + <details> 167 + <summary>View blob content</summary> 168 + {blobContent} 169 + </details> 170 + </> 171 + ); 172 + } 173 + 174 + if (data.mimeType === "video/mp4") { 175 + return ( 176 + <> 177 + <ErrorBoundary fallback={<VideoEmbedWrapper />}> 178 + <VideoEmbed cid={data.ref.toString()} did={repo} /> 179 + </ErrorBoundary> 180 + <details> 181 + <summary>View blob content</summary> 182 + {blobContent} 183 + </details> 184 + </> 185 + ); 186 + } 187 + } 188 + 189 + return ( 190 + <JSONObject 191 + data={ 192 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 193 + data as any 194 + } 195 + repo={repo} 196 + /> 197 + ); 187 198 } 188 199 189 200 export type JSONType =
-10
packages/atproto-browser/lib/at-blob.ts
··· 1 - import { z } from "zod"; 2 - 3 - export const AtBlob = z.object({ 4 - $type: z.literal("blob"), 5 - ref: z.object({ 6 - $link: z.string(), 7 - }), 8 - mimeType: z.string(), 9 - size: z.number(), 10 - });
+13 -22
packages/atproto-browser/lib/atproto-server.ts
··· 7 7 } from "@atproto/identity"; 8 8 import { cache } from "react"; 9 9 import { unstable_cache as nextCache } from "next/cache"; 10 - import { isValidHandle, NSID, InvalidNsidError } from "@atproto/syntax"; 10 + import { isValidHandle, NSID } from "@atproto/syntax"; 11 11 import { isDid } from "@atproto/did"; 12 12 import { domainToASCII } from "url"; 13 13 import { resolveTxt } from "node:dns/promises"; ··· 102 102 }; 103 103 } 104 104 105 - export async function resolveNsid( 106 - did: string, 107 - nsidStr: string, 108 - ): Promise<{ success: false; error: string } | { success: true }> { 109 - let nsid; 110 - try { 111 - nsid = NSID.parse(nsidStr); 112 - } catch (e) { 113 - if (e instanceof InvalidNsidError) { 114 - return { success: false, error: e.message }; 115 - } else { 116 - throw e; 117 - } 118 - } 119 - 105 + export async function resolveNsidAuthority( 106 + nsid: NSID, 107 + ): Promise< 108 + { success: false; error: string } | { success: true; authorityDid: string } 109 + > { 120 110 const domainParts = nsid.segments.slice().reverse(); 121 111 const authority = "_lexicon." + domainParts.slice(1).join("."); 122 112 123 113 try { 124 114 const record = (await resolveTxt(authority))[0]?.join(""); 125 - if (`did=${did}` !== record) { 115 + const did = record?.split("=")[1]; 116 + if (!did) { 126 117 return { 127 118 success: false, 128 - error: "invalid", 129 - }; 130 - } else { 131 - return { 132 - success: true, 119 + error: "not found", 133 120 }; 134 121 } 122 + return { 123 + success: true, 124 + authorityDid: did, 125 + }; 135 126 } catch (e) { 136 127 const errorMsg = 137 128 isObject(e) && "code" in e && typeof e.code === "string"
+10
packages/atproto-browser/lib/navigation.test.ts
··· 121 121 error: "No AT URI found in http://example.com", 122 122 }); 123 123 }); 124 + 125 + test("lex input with invalid NSID", async () => { 126 + // Numbers in NSID are not allowed 127 + expect(await navigateAtUri("lex:com.example.invalid1")).toEqual({ 128 + error: "Invalid NSID: com.example.invalid1", 129 + }); 130 + }); 131 + 132 + test.todo("lex input with valid resolvable NSID"); 133 + test.todo("lex input with valid unresolvable NSID");
+33 -7
packages/atproto-browser/lib/navigation.ts
··· 1 1 import "server-only"; 2 2 3 3 import { getAtUriPath } from "./util"; 4 - import { isValidHandle } from "@atproto/syntax"; 4 + import { isValidHandle, NSID } from "@atproto/syntax"; 5 5 import { redirect } from "next/navigation"; 6 6 import { parse as parseHtml } from "node-html-parser"; 7 7 import { parse as parseLinkHeader } from "http-link-header"; 8 8 import { domainToASCII } from "url"; 9 9 import { isDid } from "@atproto/did"; 10 + import { getRegistry as getLexiconRegistry } from "@lpm/core"; 10 11 11 12 export async function navigateAtUri(input: string) { 12 13 // Remove all zero-width characters and weird control codes from the input ··· 32 33 sanitizedInput.startsWith("http://") || 33 34 sanitizedInput.startsWith("https://") 34 35 ? await getAtUriFromHttp(sanitizedInput) 35 - : parseUri( 36 - // Add at:// to start if it's missing 37 - sanitizedInput.startsWith("at://") 38 - ? sanitizedInput 39 - : `at://${sanitizedInput}`, 40 - ); 36 + : isLexUri(sanitizedInput) 37 + ? await getAtUriFromLexUri(sanitizedInput) 38 + : parseUri(sanitizedInput); 41 39 42 40 if ("error" in result) { 43 41 return result; ··· 168 166 // We check for the punycode encoded version of the handle but always return the preserved input so that we can display the original handle 169 167 return handle; 170 168 } 169 + 170 + async function getAtUriFromLexUri( 171 + uri: `lex:${string}`, 172 + ): Promise<UriParseResult> { 173 + const nsidStr = uri.slice(4); 174 + if (!NSID.isValid(nsidStr)) { 175 + return { 176 + error: `Invalid NSID: ${nsidStr}`, 177 + }; 178 + } 179 + const nsid = NSID.parse(uri.slice(4)); 180 + const registry = getLexiconRegistry(); 181 + const node = registry.get(nsid); 182 + const resolution = await node.resolve(); 183 + if (!resolution.success) { 184 + return { 185 + error: `Could not resolve NSID(${nsidStr}): ${resolution.errorCode}`, 186 + }; 187 + } 188 + 189 + return { 190 + uri: resolution.uri, 191 + }; 192 + } 193 + 194 + function isLexUri(uri: string): uri is `lex:${string}` { 195 + return uri.startsWith("lex:"); 196 + }
+3
packages/atproto-browser/package.json
··· 11 11 "test": "vitest" 12 12 }, 13 13 "dependencies": { 14 + "@atproto/api": "^0.14.5", 14 15 "@atproto/did": "^0.1.5", 15 16 "@atproto/identity": "^0.4.6", 17 + "@atproto/lexicon": "^0.4.2", 16 18 "@atproto/repo": "^0.7.0", 17 19 "@atproto/syntax": "^0.3.3", 20 + "@lpm/core": "npm:@jsr/lpm__core@^0.2.9", 18 21 "hls.js": "^1.5.15", 19 22 "http-link-header": "^1.1.3", 20 23 "next": "catalog:",
+74 -2
pnpm-lock.yaml
··· 32 32 33 33 packages/atproto-browser: 34 34 dependencies: 35 + '@atproto/api': 36 + specifier: ^0.14.5 37 + version: 0.14.5 35 38 '@atproto/did': 36 39 specifier: ^0.1.5 37 40 version: 0.1.5 38 41 '@atproto/identity': 39 42 specifier: ^0.4.6 40 43 version: 0.4.6 44 + '@atproto/lexicon': 45 + specifier: ^0.4.2 46 + version: 0.4.7 41 47 '@atproto/repo': 42 48 specifier: ^0.7.0 43 49 version: 0.7.0 44 50 '@atproto/syntax': 45 51 specifier: ^0.3.3 46 52 version: 0.3.3 53 + '@lpm/core': 54 + specifier: npm:@jsr/lpm__core@^0.2.9 55 + version: '@jsr/lpm__core@0.2.9' 47 56 hls.js: 48 57 specifier: ^1.5.15 49 58 version: 1.5.15 ··· 418 427 resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} 419 428 engines: {node: '>=6.0.0'} 420 429 430 + '@atproto/api@0.14.5': 431 + resolution: {integrity: sha512-UnMOJWtZepMhWRnpL7VASp9R2lPLFjQpSSIOOBIqqhprTUot1aIxyFmz1idI58bSl6SZVA7BWRH+i8XyXSKlNg==} 432 + 433 + '@atproto/api@0.14.7': 434 + resolution: {integrity: sha512-YG2kvAtsgtajLlLrorYuHcxGgepG0c/RUB2/iJyBnwKjGqDLG8joOETf38JSNiGzs6NJbNKa9NHG6BQKourxBA==} 435 + 421 436 '@atproto/common-web@0.3.1': 422 437 resolution: {integrity: sha512-N7wiTnus5vAr+lT//0y8m/FaHHLJ9LpGuEwkwDAeV3LCiPif4m/FS8x/QOYrx1PdZQwKso95RAPzCGWQBH5j6Q==} 423 438 ··· 470 485 471 486 '@atproto/xrpc@0.6.3': 472 487 resolution: {integrity: sha512-S3tRvOdA9amPkKLll3rc4vphlDitLrkN5TwWh5Tu/jzk7mnobVVE3akYgICV9XCNHKjWM+IAPxFFI2qi+VW6nQ==} 488 + 489 + '@atproto/xrpc@0.6.9': 490 + resolution: {integrity: sha512-vQGA7++DYMNaHx3C7vEjT+2X6hYYLG7JNbBnDLWu0km1/1KYXgRkAz4h+FfYqg1mvzvIorHU7DAs5wevkJDDlw==} 473 491 474 492 '@babel/code-frame@7.24.7': 475 493 resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} ··· 1277 1295 '@jridgewell/trace-mapping@0.3.25': 1278 1296 resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 1279 1297 1298 + '@jsr/lpm__core@0.2.9': 1299 + resolution: {integrity: sha512-K3Wrv70eBSGvOEIPNNAYtbGnUocGsiYlAU6gx6D0O5pTT0+aPe92Rvibuaf53cgVXWmrGnDtuQ6KL7Iq9bWkzg==, tarball: https://npm.jsr.io/~/11/@jsr/lpm__core/0.2.9.tgz} 1300 + 1280 1301 '@libsql/client@0.14.0': 1281 1302 resolution: {integrity: sha512-/9HEKfn6fwXB5aTEEoMeFh4CtG0ZzbncBb1e++OCdVpgKZ/xyMsIVYXm0w7Pv4RUel803vE6LwniB3PqD72R0Q==} 1282 1303 ··· 1345 1366 1346 1367 '@microsoft/tsdoc@0.14.2': 1347 1368 resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} 1369 + 1370 + '@needle-di/core@0.11.2': 1371 + resolution: {integrity: sha512-QwvlSTfpobIGGEmO/iMjMcL9lPf53cSV8mQVTzMGO4IkcyOQEK3nn7d6fkUOaaefwyiO4HGLEMl5VDaslnUuUg==} 1348 1372 1349 1373 '@neon-rs/load@0.0.4': 1350 1374 resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} ··· 2410 2434 available-typed-arrays@1.0.7: 2411 2435 resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} 2412 2436 engines: {node: '>= 0.4'} 2437 + 2438 + await-lock@2.2.2: 2439 + resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} 2413 2440 2414 2441 axe-core@4.10.1: 2415 2442 resolution: {integrity: sha512-qPC9o+kD8Tir0lzNGLeghbOrWMr3ZJpaRlCIb6Uobt/7N4FiEDvqUMnxzCHRHmg8vOg14kr5gVNyScRmbMaJ9g==} ··· 4730 4757 resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} 4731 4758 engines: {node: '>=14.0.0'} 4732 4759 4760 + tlds@1.255.0: 4761 + resolution: {integrity: sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw==} 4762 + hasBin: true 4763 + 4733 4764 to-fast-properties@2.0.0: 4734 4765 resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} 4735 4766 engines: {node: '>=4'} ··· 5157 5188 '@jridgewell/gen-mapping': 0.3.5 5158 5189 '@jridgewell/trace-mapping': 0.3.25 5159 5190 5191 + '@atproto/api@0.14.5': 5192 + dependencies: 5193 + '@atproto/common-web': 0.4.0 5194 + '@atproto/lexicon': 0.4.7 5195 + '@atproto/syntax': 0.3.3 5196 + '@atproto/xrpc': 0.6.9 5197 + await-lock: 2.2.2 5198 + multiformats: 9.9.0 5199 + tlds: 1.255.0 5200 + zod: 3.23.8 5201 + 5202 + '@atproto/api@0.14.7': 5203 + dependencies: 5204 + '@atproto/common-web': 0.4.0 5205 + '@atproto/lexicon': 0.4.7 5206 + '@atproto/syntax': 0.3.3 5207 + '@atproto/xrpc': 0.6.9 5208 + await-lock: 2.2.2 5209 + multiformats: 9.9.0 5210 + tlds: 1.255.0 5211 + zod: 3.23.8 5212 + 5160 5213 '@atproto/common-web@0.3.1': 5161 5214 dependencies: 5162 5215 graphemer: 1.4.0 ··· 5202 5255 5203 5256 '@atproto/lex-cli@0.5.1': 5204 5257 dependencies: 5205 - '@atproto/lexicon': 0.4.2 5258 + '@atproto/lexicon': 0.4.7 5206 5259 '@atproto/syntax': 0.3.1 5207 5260 chalk: 4.1.2 5208 5261 commander: 9.5.0 ··· 5252 5305 5253 5306 '@atproto/xrpc@0.6.3': 5254 5307 dependencies: 5255 - '@atproto/lexicon': 0.4.2 5308 + '@atproto/lexicon': 0.4.7 5309 + zod: 3.23.8 5310 + 5311 + '@atproto/xrpc@0.6.9': 5312 + dependencies: 5313 + '@atproto/lexicon': 0.4.7 5256 5314 zod: 3.23.8 5257 5315 5258 5316 '@babel/code-frame@7.24.7': ··· 5908 5966 '@jridgewell/resolve-uri': 3.1.2 5909 5967 '@jridgewell/sourcemap-codec': 1.5.0 5910 5968 5969 + '@jsr/lpm__core@0.2.9': 5970 + dependencies: 5971 + '@atproto/api': 0.14.7 5972 + '@atproto/identity': 0.4.6 5973 + '@atproto/lexicon': 0.4.7 5974 + '@atproto/syntax': 0.3.3 5975 + '@needle-di/core': 0.11.2 5976 + 5911 5977 '@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.3)': 5912 5978 dependencies: 5913 5979 '@libsql/core': 0.14.0 ··· 5978 6044 resolve: 1.19.0 5979 6045 5980 6046 '@microsoft/tsdoc@0.14.2': {} 6047 + 6048 + '@needle-di/core@0.11.2': {} 5981 6049 5982 6050 '@neon-rs/load@0.0.4': {} 5983 6051 ··· 7165 7233 available-typed-arrays@1.0.7: 7166 7234 dependencies: 7167 7235 possible-typed-array-names: 1.0.0 7236 + 7237 + await-lock@2.2.2: {} 7168 7238 7169 7239 axe-core@4.10.1: {} 7170 7240 ··· 9888 9958 tinyrainbow@1.2.0: {} 9889 9959 9890 9960 tinyspy@3.0.2: {} 9961 + 9962 + tlds@1.255.0: {} 9891 9963 9892 9964 to-fast-properties@2.0.0: {} 9893 9965