Skip to content
This repository has been archived by the owner on Sep 9, 2024. It is now read-only.

Separate concerns of orbit connection vs hosting. #41

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions example/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
79 changes: 31 additions & 48 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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" };
Expand Down Expand Up @@ -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";
Expand Down
84 changes: 44 additions & 40 deletions src/kepler.ts
Original file line number Diff line number Diff line change
@@ -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. */
Expand All @@ -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<SessionConfig> = {}
): Promise<OrbitConnection | undefined> {
// TODO: support multiple urls for kepler.
const keplerUrl = this.config.hosts[0];
let orbitConnection: OrbitConnection = await startSession(
this.wallet,
config
)
connect = (config: Partial<SessionConfig> = {}): Promise<OrbitConnection> =>
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<HostConfig>): Promise<Response> {
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,
}));
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/kv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Response> {
return await this.invoke({
headers: await this.auth.invocationHeaders("get", key),
Expand Down
47 changes: 6 additions & 41 deletions src/orbit.ts
Original file line number Diff line number Diff line change
@@ -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.
*
Expand All @@ -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,
Expand Down Expand Up @@ -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<Response> => {
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;