-
Notifications
You must be signed in to change notification settings - Fork 23
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
feat: move validation flow to a Durable Object to make it ⏩ fast ⏩ fast ⏩ fast ⏩ #449
Changes from 11 commits
dae84aa
5f0aa9a
2f34b29
8ad6936
297442a
569bb88
02c6643
330c8ac
98dc6d8
44e3599
ed348e2
db4638d
0a6f5c8
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 |
---|---|---|
@@ -0,0 +1,131 @@ | ||
import { stringToDelegation } from '@web3-storage/access/encoding' | ||
|
||
/** | ||
* | ||
* @template {import('@ucanto/interface').Capabilities} [T=import('@ucanto/interface').Capabilities] | ||
* @param {import('../bindings').DurableObjectNamespace} spaceVerifiers | ||
* @param {string} space | ||
* @param {import('@web3-storage/access/src/types').EncodedDelegation<T>} ucan | ||
*/ | ||
export async function sendDelegationToSpaceVerifier( | ||
spaceVerifiers, | ||
space, | ||
ucan | ||
) { | ||
const durableObjectID = spaceVerifiers.idFromName(space) | ||
const durableObject = spaceVerifiers.get(durableObjectID) | ||
// hostname is totally ignored by the durable object but must be set so set it to example.com | ||
const response = await durableObject.fetch('https://example.com/delegation', { | ||
method: 'PUT', | ||
body: ucan, | ||
}) | ||
if (response.status === 400) { | ||
throw new Error(response.statusText) | ||
} | ||
} | ||
|
||
/** | ||
* @template {import('@ucanto/interface').Capabilities} [T=import('@ucanto/interface').Capabilities] | ||
* @param {WebSocket} server | ||
* @param {import('@web3-storage/access/src/types').EncodedDelegation<T>} ucan | ||
*/ | ||
function sendDelegation(server, ucan) { | ||
server.send( | ||
JSON.stringify({ | ||
type: 'delegation', | ||
delegation: ucan, | ||
}) | ||
) | ||
server.close() | ||
} | ||
|
||
/** | ||
* SpaceVerifier | ||
*/ | ||
export class SpaceVerifier { | ||
/** | ||
* @param {import('../bindings').DurableObjectState} state | ||
*/ | ||
constructor(state) { | ||
this.state = state | ||
// `blockConcurrencyWhile()` ensures no requests are delivered until | ||
// initialization completes. | ||
this.state.blockConcurrencyWhile(async () => { | ||
this.ucan = await this.state.storage.get('ucan') | ||
}) | ||
} | ||
|
||
cleanupServer() { | ||
this.server = undefined | ||
} | ||
|
||
async cleanupUCAN() { | ||
this.ucan = undefined | ||
await this.state.storage.put('ucan', '') | ||
} | ||
|
||
/** | ||
* @param {Request} req | ||
*/ | ||
async fetch(req) { | ||
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. I would expect to see an authorization check somewhere to make we don't give the ucan to someone who shouldn't have it? If someone else tries to connects first before the client we expect, what would happen? (It's been a long day, but I read it like that would deny service to the desired user) I wonder if (long term) maybe this should be like It seems like keying this off of space and only allowing one server/ucan at a time will prevent more than one of these flows happening at a time for a space - not really a problem for 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.
hm, I would probably handle this further up the stack, but also I'm not sure we do this with the existing implementation and if we do I think we're still doing that for this flow? tbh I'm not entirely sure whether we need to do additional auth here or not, but I'm not sure what that would look like concretely - do you have some sort of signature verification in mind? 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.
I think this is exactly the direction we should go with the new stuff coming in yep 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.
yep - keying off Agent will be the way to go for the path that handles the 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. I think invocation CID might be better yet as agent in theory might start multiple authorization flows or maybe we'll want to deliver other messages over websocket. 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. hm I don't totally follow, but seems reasonable as long as both sides of the equation (ie, the code calling registerSpace and the code handling the request from the email link) can both calculate the invocation CID |
||
const path = new URL(req.url).pathname | ||
if (req.method === 'GET' && path.startsWith('/validate-ws/')) { | ||
const upgradeHeader = req.headers.get('Upgrade') | ||
if (!upgradeHeader || upgradeHeader !== 'websocket') { | ||
return new Response('Expected Upgrade: websocket', { status: 426 }) | ||
} | ||
if (this.server) { | ||
return new Response('Websocket already connected for this space.', { | ||
status: 409, | ||
}) | ||
} | ||
const [client, server] = Object.values(new WebSocketPair()) | ||
// @ts-ignore | ||
server.accept() | ||
// if the user has already verified and set this.ucan here, send them the delegation | ||
|
||
if (this.ucan) { | ||
travis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
sendDelegation( | ||
server, | ||
/** @type {import('@web3-storage/access/src/types').EncodedDelegation} */ ( | ||
this.ucan | ||
) | ||
) | ||
await this.cleanupUCAN() | ||
} else { | ||
this.server = server | ||
} | ||
return new Response(undefined, { | ||
status: 101, | ||
webSocket: client, | ||
}) | ||
} else if (req.method === 'PUT' && path === '/delegation') { | ||
const ucan = await req.text() | ||
const delegation = stringToDelegation(ucan) | ||
|
||
// it's only important to check expiration here - if we successfully validate before expiration | ||
// here and a user connects to the websocket later after expiration we should still send the delegation | ||
if (Date.now() < delegation.expiration * 1000) { | ||
if (this.server) { | ||
sendDelegation(this.server, ucan) | ||
this.cleanupServer() | ||
} else { | ||
await this.state.storage.put('ucan', ucan) | ||
this.ucan = ucan | ||
} | ||
return new Response(undefined, { | ||
status: 200, | ||
}) | ||
} else { | ||
this.server?.close() | ||
return new Response('Delegation expired', { | ||
status: 400, | ||
}) | ||
} | ||
} else { | ||
return new Response("SpaceVerifier can't handle this request", { | ||
status: 404, | ||
}) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
/** | ||
* @param {import('@web3-storage/worker-utils/router').ParsedRequest} req | ||
* @param {import('../bindings.js').RouteContext} env | ||
*/ | ||
export async function validateWSDID(req, env) { | ||
const durableObjectID = env.spaceVerifiers.idFromName(req.params.did) | ||
const durableObject = env.spaceVerifiers.get(durableObjectID) | ||
/** @type {import('../bindings.js').WorkerResponse} */ | ||
const response = await durableObject.fetch(req) | ||
// wrap the response because it's not possible to set headers on the response we get back from the durable object | ||
travis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return new Response(response.body, { | ||
status: response.status, | ||
statusText: response.statusText, | ||
headers: response.headers, | ||
webSocket: response.webSocket, | ||
}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,16 +35,16 @@ export function voucherClaimProvider(ctx) { | |
.delegate() | ||
|
||
const encoded = delegationToString(inv) | ||
// For testing | ||
if (ctx.config.ENV === 'test') { | ||
return encoded | ||
} | ||
|
||
const url = `${ctx.url.protocol}//${ctx.url.host}/validate-email?ucan=${encoded}` | ||
|
||
await ctx.email.sendValidation({ | ||
to: capability.nb.identity.replace('mailto:', ''), | ||
url, | ||
}) | ||
|
||
// For testing | ||
if (ctx.config.ENV === 'test') { | ||
return encoded | ||
} | ||
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. Nit: Is this really necessary now hat we can provide debug 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. ah good question and I think no - I had originally been planning to rework the tests to get rid of this, but paused on that because we'll probably end up porting all of this over to the new session stuff, and it will make sense to rework tests and generally clean all of this up at that point. |
||
}) | ||
} |
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.
Does body need to be a string or can we pass binary data ? If we could pass binary it might be a better option.
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.
ah good question, I suspect binary would work? noted and a good change for the next version of this I think