@@ -32,6 +32,8 @@ const isFileLike = (file: FileOrFileLike): file is FileLike =>
3232 'blob' in file && 'name' in file ;
3333
3434const JSON_AD_MIME = 'application/ad+json' ;
35+ const ATOMIC_SERVER_VERSION_HEADER = 'X-Atomic-Server-Version' ;
36+ const MIN_DID_AUTH_SERVER_MINOR = 40 ;
3537
3638interface FetchResourceOptions extends ParseOpts {
3739 /**
@@ -65,6 +67,9 @@ interface HTTPResourceResult {
6567/** Contains a `fetch` instance, provides methods to GET and POST several types */
6668export class Client {
6769 private __fetchOverride ?: typeof fetch ;
70+ private warnedDidAuthCompatibilityOrigins = new Set < string > ( ) ;
71+ private supportsDidAuthByOrigin = new Map < string , boolean > ( ) ;
72+ private serverVersionByOrigin = new Map < string , string > ( ) ;
6873
6974 public constructor ( fetchOverride ?: typeof fetch ) {
7075 if ( fetchOverride ) {
@@ -164,19 +169,25 @@ export class Client {
164169
165170 // Sign the request with the actual URL being fetched (not the raw DID
166171 // subject) since the server verifies against the full HTTP URL.
167- if ( signInfo && ! subject . startsWith ( 'https://atomicdata.dev' ) ) {
168- // Cookies only work in browsers for same-origin requests right now
169- // https://github.com/atomicdata-dev/atomic-data-browser/issues/253
170- if ( hasBrowserAPI ( ) && subject . startsWith ( window . location . origin ) ) {
171- if ( ! checkAuthenticationCookie ( ) ) {
172- setCookieAuthentication ( signInfo . serverURL , signInfo . agent ) ;
172+ if ( signInfo ) {
173+ if (
174+ this . shouldSkipDidAuthForLegacyServer ( url , signInfo . agent . subject )
175+ ) {
176+ this . warnDidAuthCompatibility ( url ) ;
177+ } else if ( ! subject . startsWith ( 'https://atomicdata.dev' ) ) {
178+ // Cookies only work in browsers for same-origin requests right now
179+ // https://github.com/atomicdata-dev/atomic-data-browser/issues/253
180+ if ( hasBrowserAPI ( ) && subject . startsWith ( window . location . origin ) ) {
181+ if ( ! checkAuthenticationCookie ( ) ) {
182+ setCookieAuthentication ( signInfo . serverURL , signInfo . agent ) ;
183+ }
184+ } else {
185+ requestHeaders = await signRequest (
186+ url ,
187+ signInfo . agent ,
188+ requestHeaders ,
189+ ) ;
173190 }
174- } else {
175- requestHeaders = await signRequest (
176- url ,
177- signInfo . agent ,
178- requestHeaders ,
179- ) ;
180191 }
181192 }
182193
@@ -191,6 +202,7 @@ export class Client {
191202 method : method ?? 'GET' ,
192203 body : bodyReq ,
193204 } ) ;
205+ this . recordServerVersionFromResponse ( url , response ) ;
194206 const body = await response . text ( ) ;
195207
196208 if ( response . status === 200 ) {
@@ -323,4 +335,112 @@ export class Client {
323335
324336 return fetch ( ...params ) ;
325337 }
338+
339+ /**
340+ * Legacy servers (<0.40) cannot verify DID-based auth for cross-origin
341+ * requests. We default to legacy compatibility unless a custom handshake is
342+ * implemented elsewhere.
343+ */
344+ private shouldSkipDidAuthForLegacyServer (
345+ url : string ,
346+ agentSubject ?: string ,
347+ ) : boolean {
348+ if ( ! agentSubject ?. startsWith ( 'did:ad:' ) ) {
349+ return false ;
350+ }
351+
352+ if ( ! hasBrowserAPI ( ) ) {
353+ return false ;
354+ }
355+
356+ const requestOrigin = this . tryGetOrigin ( url ) ;
357+
358+ if ( ! requestOrigin ) {
359+ return false ;
360+ }
361+
362+ if ( requestOrigin === window . location . origin ) {
363+ return false ;
364+ }
365+
366+ const supportsDidAuth = this . supportsDidAuthByOrigin . get ( requestOrigin ) ;
367+
368+ // Legacy-safe default: if we don't know this origin yet, assume <0.40.
369+ return supportsDidAuth !== true ;
370+ }
371+
372+ private warnDidAuthCompatibility ( url : string ) : void {
373+ if ( ! hasBrowserAPI ( ) ) {
374+ return ;
375+ }
376+
377+ const origin = this . tryGetOrigin ( url ) ;
378+
379+ if ( ! origin ) {
380+ return ;
381+ }
382+
383+ if ( this . warnedDidAuthCompatibilityOrigins . has ( origin ) ) {
384+ return ;
385+ }
386+
387+ const version = this . serverVersionByOrigin . get ( origin ) ;
388+ const reason = version
389+ ? `server version '${ version } ' does not support DID auth`
390+ : `server version unknown (assuming <0.40)` ;
391+
392+ this . warnedDidAuthCompatibilityOrigins . add ( origin ) ;
393+ console . warn (
394+ `[atomic-lib] Skipping signed auth request for DID agent on cross-origin request to '${ origin } ': ${ reason } .` ,
395+ ) ;
396+ }
397+
398+ private recordServerVersionFromResponse (
399+ url : string ,
400+ response : Response ,
401+ ) : void {
402+ const version = response . headers . get ( ATOMIC_SERVER_VERSION_HEADER ) ;
403+
404+ if ( ! version ) {
405+ return ;
406+ }
407+
408+ const origin = this . tryGetOrigin ( url ) ;
409+
410+ if ( ! origin ) {
411+ return ;
412+ }
413+
414+ this . serverVersionByOrigin . set ( origin , version ) ;
415+ this . supportsDidAuthByOrigin . set (
416+ origin ,
417+ this . versionSupportsDidAuth ( version ) ,
418+ ) ;
419+ }
420+
421+ private versionSupportsDidAuth ( version : string ) : boolean {
422+ const match = version . match ( / ^ ( \d + ) \. ( \d + ) (?: \. ( \d + ) ) ? / ) ;
423+
424+ if ( ! match ) {
425+ return false ;
426+ }
427+
428+ const major = Number . parseInt ( match [ 1 ] , 10 ) ;
429+ const minor = Number . parseInt ( match [ 2 ] , 10 ) ;
430+
431+ if ( Number . isNaN ( major ) || Number . isNaN ( minor ) ) {
432+ return false ;
433+ }
434+
435+ return major > 0 || ( major === 0 && minor >= MIN_DID_AUTH_SERVER_MINOR ) ;
436+ }
437+
438+ private tryGetOrigin ( url : string ) : string | undefined {
439+ try {
440+ return new URL ( url , hasBrowserAPI ( ) ? window . location . origin : undefined )
441+ . origin ;
442+ } catch {
443+ return undefined ;
444+ }
445+ }
326446}
0 commit comments