From c6a1b8e1ae182c10834c91624a98fdeef5e98eaf Mon Sep 17 00:00:00 2001 From: Jacob Date: Thu, 2 Jun 2022 18:49:09 +0100 Subject: [PATCH] Separate concerns of orbit connection vs hosting. --- example/src/main.ts | 13 ++++--- src/index.test.ts | 79 +++++++++++++++++------------------------- src/kepler.ts | 84 ++++++++++++++++++++++++--------------------- src/kv.ts | 4 +++ src/orbit.ts | 47 ++++--------------------- 5 files changed, 94 insertions(+), 133 deletions(-) diff --git a/example/src/main.ts b/example/src/main.ts index 7b777ed..f553283 100644 --- a/example/src/main.ts +++ b/example/src/main.ts @@ -24,13 +24,18 @@ let loadOrbit = document.getElementById("loadOrbit") as HTMLDivElement; let loadOrbitBtn = document.getElementById("loadOrbitBtn") as HTMLButtonElement; loadOrbitBtn.onclick = async () => { - kepler = new Kepler(wallet, { hosts: ["http://localhost:8000"] }); - let o = await kepler.orbit(); - if (o === undefined) { + kepler = new Kepler(wallet, { host: "http://localhost:8000" }); + orbitConnection = await kepler.connect(); + + let ok = await orbitConnection + .list() + .then( + async ({ status }) => status !== 404 || (await kepler.hostOrbit()).ok + ); + if (!ok) { console.error("unable to host orbit"); return; } - orbitConnection = o; loadOrbitBtn.innerText = orbitConnection.id(); loadOrbitBtn.disabled = true; diff --git a/src/index.test.ts b/src/index.test.ts index c19b159..24117de 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -2,17 +2,10 @@ import { Kepler, OrbitConnection, Response } from "./"; import Blob from "fetch-blob"; import { Wallet } from "ethers"; import { startSession, Authenticator } from "./authenticator"; -import { hostOrbit } from "./orbit"; const keplerUrl = "http://localhost:8000"; const domain = "example.com"; -function expectDefined(orbit: OrbitConnection | undefined): OrbitConnection { - expect(orbit).not.toBeUndefined(); - // @ts-ignore - return orbit; -} - function expectSuccess(response: Response): Response { expect(response.status).toBe(200); return response; @@ -41,26 +34,20 @@ describe("Authenticator", () => { }); it("host", async () => { - let wallet = newWallet(); - await startSession(wallet, { - expirationTime: "3000-01-01T00:00:00.000Z", - issuedAt: "2022-01-01T00:00:00.000Z", - domain, - }) - .then((session) => hostOrbit(wallet, keplerUrl, session.orbitId, domain)) - .then(expectSuccess); + let kepler = new Kepler(newWallet(), { host: keplerUrl }); + await kepler.hostOrbit({ domain }).then(expectSuccess); }); }); describe("Kepler Client", () => { let orbit: OrbitConnection; - const keplerConfig = { hosts: [keplerUrl] }; + const keplerConfig = { host: keplerUrl }; const orbitConfig = { domain }; beforeAll(async () => { - orbit = await new Kepler(newWallet(), keplerConfig) - .orbit(orbitConfig) - .then(expectDefined); + let kepler = new Kepler(newWallet(), keplerConfig); + await kepler.hostOrbit(orbitConfig).then(expectSuccess); + orbit = await kepler.connect(orbitConfig); }); it("cannot put null value", async () => { @@ -254,38 +241,37 @@ describe("Kepler Client", () => { }); it("undelegated account cannot access a different orbit", async () => { - await new Kepler(newWallet(), keplerConfig) - .orbit({ orbitId: orbit.id(), ...orbitConfig }) - .then(expectDefined) + let kepler = new Kepler(newWallet(), keplerConfig); + await kepler.hostOrbit(orbitConfig).then(expectSuccess); + await kepler + .connect({ orbitId: orbit.id(), ...orbitConfig }) .then((orbit) => orbit.list()) .then(expectUnauthorised); }); it("expired session key cannot be used", async () => { - await new Kepler(newWallet(), keplerConfig) - .orbit({ + let kepler = new Kepler(newWallet(), keplerConfig); + await kepler.hostOrbit(orbitConfig).then(expectSuccess); + await kepler + .connect({ expirationTime: new Date(Date.now() - 1000 * 60 * 60).toISOString(), ...orbitConfig, }) - .then(expectDefined) .then((orbit) => orbit.list()) .then(expectUnauthorised); }); it("only allows properly authorised actions", async () => { const kepler = new Kepler(newWallet(), keplerConfig); - const write = await kepler - .orbit({ - actions: ["put", "del"], - ...orbitConfig, - }) - .then(expectDefined); - const read = await kepler - .orbit({ - actions: ["get", "list"], - ...orbitConfig, - }) - .then(expectDefined); + await kepler.hostOrbit(orbitConfig).then(expectSuccess); + const write = await kepler.connect({ + actions: ["put", "del"], + ...orbitConfig, + }); + const read = await kepler.connect({ + actions: ["get", "list"], + ...orbitConfig, + }); const key = "key"; const json = { hello: "hey" }; @@ -320,17 +306,14 @@ describe("Kepler Client", () => { }); it("there is a one-to-one mapping between wallets and orbits", async () => { - const wallet = newWallet(); - const orbit1 = await new Kepler(wallet, keplerConfig) - .orbit({ - domain: "example1.com", - }) - .then(expectDefined); - const orbit2 = await new Kepler(wallet, keplerConfig) - .orbit({ - domain: "example2.com", - }) - .then(expectDefined); + const kepler = new Kepler(newWallet(), keplerConfig); + await kepler.hostOrbit(orbitConfig).then(expectSuccess); + const orbit1 = await kepler.connect({ + domain: "example1.com", + }); + const orbit2 = await kepler.connect({ + domain: "example2.com", + }); const key = "key"; const value = "value"; diff --git a/src/kepler.ts b/src/kepler.ts index a16291f..a21729a 100644 --- a/src/kepler.ts +++ b/src/kepler.ts @@ -1,18 +1,15 @@ +import wasmPromise, { HostConfig } from "@spruceid/kepler-sdk-wasm"; import { SessionConfig } from "."; import { Authenticator, startSession } from "./authenticator"; -import { hostOrbit, OrbitConnection } from "./orbit"; +import { FetchResponse, OrbitConnection, Response } from "./orbit"; import { WalletProvider } from "./walletProvider"; const fetch_ = typeof fetch === "undefined" ? require("node-fetch") : fetch; /** Configuration for [[Kepler]]. */ export type KeplerOptions = { - /** The Kepler hosts that you wish to connect to. - * - * Currently only a single host is supported, but for future compatibility this property is - * expected to be a list. Only the first host in the list will be used. - */ - hosts: string[]; + /** The Kepler host that you wish to connect to. */ + host: string; }; /** An object for interacting with Kepler instances. */ @@ -25,52 +22,59 @@ export class Kepler { * @param config Optional configuration for Kepler. */ constructor(wallet: WalletProvider, config: KeplerOptions) { - this.config = { - hosts: config.hosts, - }; + this.config = config; this.wallet = wallet; } /** Make a connection to an orbit. * - * This method handles the creation and connection to an orbit in Kepler. This method should - * usually be used without providing any ConnectionOptions: + * This method handles the connection to an orbit in Kepler. This method should + * usually be used without providing any SessionConfig: * ```ts * let orbitConnection = await kepler.orbit(); * ``` - * In this case the orbit ID will be derived from the wallet's address. The wallet will be - * asked to sign a message delegating access to a session key for 1 hour. If the orbit does not - * already exist in the Kepler instance, then the wallet will be asked to sign another message - * to permit the Kepler instance to host the orbit. + * In this case the orbit ID will be derived from the wallet's address. + * + * The wallet will be asked to sign a message delegating access to a session key for 1 hour. * * @param config Optional parameters to configure the orbit connection. - * @returns Returns undefined if the Kepler instance was unable to host the orbit. */ - async orbit( - config: Partial = {} - ): Promise { - // TODO: support multiple urls for kepler. - const keplerUrl = this.config.hosts[0]; - let orbitConnection: OrbitConnection = await startSession( - this.wallet, - config - ) + connect = (config: Partial = {}): Promise => + startSession(this.wallet, config) .then((session) => new Authenticator(session)) - .then((authn) => new OrbitConnection(keplerUrl, authn)); + .then((authn) => new OrbitConnection(this.config.host, authn)); + + async hostOrbit(config?: Partial): Promise { + const wasm = await wasmPromise; + + const address = config?.address ?? (await this.wallet.getAddress()); + const chainId = config?.chainId ?? (await this.wallet.getChainId()); + const config_: HostConfig = { + address, + chainId, + domain: config?.domain ?? window.location.hostname, + issuedAt: config?.issuedAt ?? new Date(Date.now()).toISOString(), + orbitId: config?.orbitId ?? wasm.makeOrbitId(address, chainId), + peerId: + config?.peerId ?? + (await fetch_(this.config.host + "/peer/generate").then( + (res: FetchResponse) => res.text() + )), + }; + + const siwe = wasm.generateHostSIWEMessage(JSON.stringify(config_)); + const signature = await this.wallet.signMessage(siwe); + const hostHeaders = wasm.host(JSON.stringify({ siwe, signature })); - return await orbitConnection.list().then(async ({ status }) => { - if (status === 404) { - console.info("Orbit does not already exist. Creating..."); - let { ok } = await hostOrbit( - this.wallet, - keplerUrl, - orbitConnection.id(), - config.domain - ); - return ok ? orbitConnection : undefined; - } - return orbitConnection; - }); + return fetch_(this.config.host + "/delegate", { + method: "POST", + headers: JSON.parse(hostHeaders), + }).then(({ ok, status, statusText, headers }: FetchResponse) => ({ + ok, + status, + statusText, + headers, + })); } } diff --git a/src/kv.ts b/src/kv.ts index 8976d3e..f959a9f 100644 --- a/src/kv.ts +++ b/src/kv.ts @@ -4,6 +4,10 @@ import { invoke } from "./kepler"; export class KV { constructor(private url: string, private auth: Authenticator) {} + public reconnect(newUrl: string) { + this.url = newUrl; + } + public async get(key: string): Promise { return await this.invoke({ headers: await this.auth.invocationHeaders("get", key), diff --git a/src/orbit.ts b/src/orbit.ts index 0c6ff37..2209ec6 100644 --- a/src/orbit.ts +++ b/src/orbit.ts @@ -1,14 +1,9 @@ -import wasmPromise from "@spruceid/kepler-sdk-wasm"; -import { HostConfig } from "."; import { Authenticator } from "./authenticator"; import { KV } from "./kv"; -import { WalletProvider } from "./walletProvider"; const Blob = typeof window === "undefined" ? require("fetch-blob") : window.Blob; -const fetch_ = typeof fetch === "undefined" ? require("node-fetch") : fetch; - /** * A connection to an orbit in a Kepler instance. * @@ -32,6 +27,11 @@ export class OrbitConnection { return this.orbitId; } + /** Re-use the session to connect to the orbit at a different Kepler host. */ + reconnect(newUrl: string) { + this.kv.reconnect(newUrl); + } + /** Store an object in the connected orbit. * * Supports storing values that are of type string, @@ -251,39 +251,4 @@ export type Response = { data?: any; }; -type FetchResponse = globalThis.Response; - -export const hostOrbit = async ( - wallet: WalletProvider, - keplerUrl: string, - orbitId: string, - domain: string = window.location.hostname -): Promise => { - const wasm = await wasmPromise; - const address = await wallet.getAddress(); - const chainId = await wallet.getChainId(); - const issuedAt = new Date(Date.now()).toISOString(); - const peerId = await fetch_(keplerUrl + "/peer/generate").then( - (res: FetchResponse) => res.text() - ); - const config: HostConfig = { - address, - chainId, - domain, - issuedAt, - orbitId, - peerId, - }; - const siwe = wasm.generateHostSIWEMessage(JSON.stringify(config)); - const signature = await wallet.signMessage(siwe); - const hostHeaders = wasm.host(JSON.stringify({ siwe, signature })); - return fetch_(keplerUrl + "/delegate", { - method: "POST", - headers: JSON.parse(hostHeaders), - }).then(({ ok, status, statusText, headers }: FetchResponse) => ({ - ok, - status, - statusText, - headers, - })); -}; +export type FetchResponse = globalThis.Response;