-
Notifications
You must be signed in to change notification settings - Fork 611
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Facilitate authing w/ PDS based on DID doc #1727
Changes from 3 commits
c78eaf5
c2754a1
7e3622b
89f3db2
412171a
deb9efc
bb413b5
0424019
763a3f8
791c285
faba675
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -18,6 +18,7 @@ import { | |||
AtpPersistSessionHandler, | ||||
AtpAgentOpts, | ||||
} from './types' | ||||
import { getPdsEndpoint } from './did/did-doc' | ||||
|
||||
const REFRESH_SESSION = 'com.atproto.server.refreshSession' | ||||
|
||||
|
@@ -30,6 +31,11 @@ export class AtpAgent { | |||
api: AtpServiceClient | ||||
session?: AtpSessionData | ||||
|
||||
/** | ||||
* The PDS URL, driven by the did doc. May be undefined. | ||||
*/ | ||||
pdsUrl: URL | undefined | ||||
|
||||
private _baseClient: AtpBaseClient | ||||
private _persistSession?: AtpPersistSessionHandler | ||||
private _refreshSessionPromise: Promise<void> | undefined | ||||
|
@@ -97,6 +103,7 @@ export class AtpAgent { | |||
email: opts.email, | ||||
emailConfirmed: false, | ||||
} | ||||
this._updateApiEndpoint(res.data.didDoc) | ||||
return res | ||||
} catch (e) { | ||||
this.session = undefined | ||||
|
@@ -129,6 +136,7 @@ export class AtpAgent { | |||
email: res.data.email, | ||||
emailConfirmed: res.data.emailConfirmed, | ||||
} | ||||
this._updateApiEndpoint(res.data.didDoc) | ||||
return res | ||||
} catch (e) { | ||||
this.session = undefined | ||||
|
@@ -157,6 +165,7 @@ export class AtpAgent { | |||
this.session.email = res.data.email | ||||
this.session.handle = res.data.handle | ||||
this.session.emailConfirmed = res.data.emailConfirmed | ||||
this._updateApiEndpoint(res.data.didDoc) | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops, missed this in original review. This endpoint
Suggested change
|
||||
return res | ||||
} catch (e) { | ||||
this.session = undefined | ||||
|
@@ -253,7 +262,7 @@ export class AtpAgent { | |||
} | ||||
|
||||
// send the refresh request | ||||
const url = new URL(this.service.origin) | ||||
const url = new URL((this.pdsUrl || this.service).origin) | ||||
url.pathname = `/xrpc/${REFRESH_SESSION}` | ||||
const res = await AtpAgent.fetch( | ||||
url.toString(), | ||||
|
@@ -277,6 +286,7 @@ export class AtpAgent { | |||
handle: res.body.handle, | ||||
did: res.body.did, | ||||
} | ||||
this._updateApiEndpoint(res.body.didDoc) | ||||
this._persistSession?.('update', this.session) | ||||
} | ||||
// else: other failures should be ignored - the issue will | ||||
|
@@ -311,6 +321,21 @@ export class AtpAgent { | |||
*/ | ||||
createModerationReport: typeof this.api.com.atproto.moderation.createReport = | ||||
(data, opts) => this.api.com.atproto.moderation.createReport(data, opts) | ||||
|
||||
/** | ||||
* Helper to update the pds endpoint dynamically. | ||||
* | ||||
* The session methods (create, resume, refresh) may respond with the user's | ||||
* did document which contains the user's canonical PDS endpoint. That endpoint | ||||
* may differ from the endpoint used to contact the server. We capture that | ||||
* PDS endpoint and update the client to use that given endpoint for future | ||||
* requests. (This helps ensure smooth migrations between PDSes, especially | ||||
* when the PDSes are operated by a single org.) | ||||
*/ | ||||
private _updateApiEndpoint(didDoc: unknown) { | ||||
this.pdsUrl = getPdsEndpoint(didDoc) | ||||
this.api.xrpc.uri = this.pdsUrl || this.service | ||||
} | ||||
} | ||||
|
||||
function isErrorObject(v: unknown): v is ErrorResponseBody { | ||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { z } from 'zod' | ||
|
||
export const verificationMethod = z.object({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmmm we already have something very similar this over in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. or if the client doesn't agree with the dependencies of that package, porting just these types & verification methods over to |
||
id: z.string(), | ||
type: z.string(), | ||
controller: z.string(), | ||
publicKeyMultibase: z.string().optional(), | ||
}) | ||
|
||
export const service = z.object({ | ||
id: z.string(), | ||
type: z.string(), | ||
serviceEndpoint: z.union([z.string(), z.record(z.unknown())]), | ||
}) | ||
|
||
export const didDocument = z.object({ | ||
id: z.string(), | ||
alsoKnownAs: z.array(z.string()).optional(), | ||
verificationMethod: z.array(verificationMethod).optional(), | ||
service: z.array(service).optional(), | ||
}) | ||
|
||
export type DidDocument = z.infer<typeof didDocument> | ||
|
||
export function isValidDidDoc(doc: unknown): doc is DidDocument { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not required, but I think we could take this one step further & move this to common-web as well |
||
return didDocument.safeParse(doc).success | ||
} | ||
|
||
export function getPdsEndpoint(doc: unknown): URL | undefined { | ||
if (isValidDidDoc(doc)) { | ||
const pds = doc.service?.find((s) => s.type === 'AtprotoPersonalDataServer') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in |
||
if (pds && typeof pds.serviceEndpoint === 'string') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: this is currently taking the first PDS service entry and only handling the string value. I don't know what a non-string value would look like. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah that'd be considered a valid json-ld did document, but not supported by atproto rn this is similar to how we validate services in our identity resolver 👍 |
||
try { | ||
return new URL(pds.serviceEndpoint) | ||
} catch { | ||
return undefined | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't an addition to the api because its dependencies already depended on it