@@ -10,6 +10,7 @@ import {
1010 getFe34Origin ,
1111 haveSameFe34Origin ,
1212 parseIri ,
13+ type RemoteDocument ,
1314} from "@fedify/vocab-runtime" ;
1415import jsonld from "@fedify/vocab-runtime/jsonld" ;
1516import { getLogger } from "@logtape/logtape" ;
@@ -493,6 +494,7 @@ interface ProofMessageDigests {
493494
494495interface ProofMessageDigestCache {
495496 value ?: Promise < ProofMessageDigests > ;
497+ proofContextLoader ?: DocumentLoader ;
496498}
497499
498500function expandContextPropertyIri (
@@ -519,11 +521,12 @@ function expandContextPropertyIri(
519521
520522async function getProofPropertyNames (
521523 jsonLd : Record < string , unknown > ,
524+ documentLoader : DocumentLoader = preloadedOnlyDocumentLoader ,
522525) : Promise < Set < string > > {
523526 const names = new Set ( [ "proof" , SECURITY_PROOF ] ) ;
524527 if ( jsonLd [ "@context" ] == null ) return names ;
525528 try {
526- const options = { documentLoader : preloadedOnlyDocumentLoader } ;
529+ const options = { documentLoader } ;
527530 let activeContext = await jsonld . processContext ( null , null , options ) ;
528531 activeContext = await jsonld . processContext (
529532 activeContext ,
@@ -570,13 +573,16 @@ async function getProofPropertyNames(
570573
571574async function createProofMessageDigests (
572575 jsonLd : unknown ,
576+ proofContextLoader ?: DocumentLoader ,
573577) : Promise < ProofMessageDigests > {
574578 const msg = { ...( jsonLd as Record < string , unknown > ) } ;
575579 // `verifyProof()` promises to ignore existing proofs on the input;
576580 // strip every top-level property that the active JSON-LD context maps to
577581 // the security proof predicate so its bytes are not folded into the JCS
578582 // message digest.
579- for ( const property of await getProofPropertyNames ( msg ) ) {
583+ for (
584+ const property of await getProofPropertyNames ( msg , proofContextLoader )
585+ ) {
580586 delete msg [ property ] ;
581587 }
582588 const encoder = new TextEncoder ( ) ;
@@ -722,7 +728,10 @@ async function verifyProofInternal(
722728 ) ;
723729 } ;
724730 const messageDigests = await (
725- messageDigestCache . value ??= createProofMessageDigests ( jsonLd )
731+ messageDigestCache . value ??= createProofMessageDigests (
732+ jsonLd ,
733+ messageDigestCache . proofContextLoader ,
734+ )
726735 ) ;
727736 if ( await verifyCandidate ( messageDigests . onWire ) ) return publicKey ;
728737 const normalizedDigest = await messageDigests . normalized ( ) ;
@@ -832,18 +841,37 @@ function classifyFep2277CoreType(
832841async function expandPortableObjectRoot (
833842 jsonLd : unknown ,
834843 contextLoader : DocumentLoader | undefined ,
835- ) : Promise < Record < string , unknown > > {
844+ ) : Promise < {
845+ root : Record < string , unknown > ;
846+ proofContextLoader : DocumentLoader ;
847+ } > {
836848 if ( ! isJsonLdNode ( jsonLd ) ) {
837849 throw new TypeError ( "Expected a single JSON-LD object." ) ;
838850 }
851+ const loadedContexts = new Map < string , RemoteDocument > ( ) ;
852+ const loader = getNormalizationContextLoader ( contextLoader ) ;
853+ const recordingLoader : DocumentLoader = async ( url , options ) => {
854+ const remoteDocument = await loader ( url , options ) ;
855+ const key = URL . canParse ( url ) ? new URL ( url ) . href : url ;
856+ loadedContexts . set ( key , structuredClone ( remoteDocument ) ) ;
857+ return remoteDocument ;
858+ } ;
839859 const expanded = await jsonld . expand ( jsonLd , {
840- documentLoader : getNormalizationContextLoader ( contextLoader ) ,
860+ documentLoader : recordingLoader ,
841861 keepFreeFloatingNodes : true ,
842862 } ) ;
843863 if ( expanded . length !== 1 || ! isJsonLdNode ( expanded [ 0 ] ) ) {
844864 throw new TypeError ( "Expected a single JSON-LD object." ) ;
845865 }
846- return expanded [ 0 ] ;
866+ return {
867+ root : expanded [ 0 ] ,
868+ proofContextLoader : async ( url , options ) => {
869+ const key = URL . canParse ( url ) ? new URL ( url ) . href : url ;
870+ const remoteDocument = loadedContexts . get ( key ) ;
871+ if ( remoteDocument != null ) return structuredClone ( remoteDocument ) ;
872+ return await preloadedOnlyDocumentLoader ( url , options ) ;
873+ } ,
874+ } ;
847875}
848876
849877/**
@@ -869,7 +897,10 @@ export async function verifyPortableObjectProof(
869897 jsonLd : unknown ,
870898 options : VerifyPortableObjectProofOptions = { } ,
871899) : Promise < VerifyPortableObjectProofResult > {
872- const root = await expandPortableObjectRoot ( jsonLd , options . contextLoader ) ;
900+ const { root, proofContextLoader } = await expandPortableObjectRoot (
901+ jsonLd ,
902+ options . contextLoader ,
903+ ) ;
873904 const id = root [ "@id" ] ;
874905 if (
875906 typeof id !== "string" ||
@@ -990,7 +1021,7 @@ export async function verifyPortableObjectProof(
9901021 }
9911022
9921023 const keys : Multikey [ ] = [ ] ;
993- const messageDigestCache : ProofMessageDigestCache = { } ;
1024+ const messageDigestCache : ProofMessageDigestCache = { proofContextLoader } ;
9941025 for ( let proofIndex = 0 ; proofIndex < proofs . length ; proofIndex ++ ) {
9951026 const key = await verifyProofWithMessageDigestCache (
9961027 jsonLd ,
0 commit comments