-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: move validation flow to a Durable Object to make it ⏩ fast ⏩ fa…
…st ⏩ fast ⏩ (#449) Introduce a [Cloudflare Durable Objects](https://developers.cloudflare.com/workers/runtime-apis/durable-objects/)-based space verification workflow. Rather than stashing a UCAN in KV and polling inside the Websocket handler until eventual consistency makes it available to the registration process in the CLI or w3console, we use a Durable Object to create a direct connection between the Websocket and the HTTP POST that provides the delegation. It feels most natural to create a Durable Object per space and forward the Websocket request on to the Durable Object and let it handle it entirely. This necessitates adding the space DID information to the HTTP path, which means adding a new endpoint and will require clients to be updated in the wild before we get rid of the KV flow entirely. We should remove KV-based verification once this has rolled out to all of our users. TODO - [x] write tests - [ ] make sure error handling makes sense --------- Co-authored-by: Benjamin Goering <171782+gobengo@users.noreply.github.com>
- Loading branch information
Showing
13 changed files
with
278 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
131 changes: 131 additions & 0 deletions
131
packages/access-api/src/durable-objects/space-verifier.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | ||
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) { | ||
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, | ||
}) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
return new Response(response.body, { | ||
status: response.status, | ||
statusText: response.statusText, | ||
headers: response.headers, | ||
webSocket: response.webSocket, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.