import { Cubit } from 'atlibs/cubit/index.js' import { runModule } from 'atlibs/run-module/index.js' // Everything the page knows, in one place. Components render slices of it. // session: "loading" | "signed-out" | "signed-in" | "error" // keys.status: "idle" | "loading" | "loaded" | "error" // create.status: "idle" | "busy" | "done" | "error" const INITIAL = { session: "loading", handle: null, error: null, login: { busy: false, error: null }, keys: { status: "idle", services: [], error: null }, create: { status: "idle", error: null, uri: null }, }; // Page logic: the OAuth session, and the fedproxy wasm module's actions run as that session. // Dependencies come in so the page wires real ones and a test can pass stubs: // client Promise of a BrowserOAuthClient // makeAgent session => @atproto/api Agent // module Promise of { module: Uint8Array, calls: string[] }, the fedproxy wasm and its allowed calls // ctx PluginContext providing ctx.wasm export class FedproxyCubit extends Cubit { #client; #makeAgent; #module; #ctx; #agent; constructor({ client, makeAgent, module, ctx }) { super(INITIAL); this.#client = client; this.#makeAgent = makeAgent; this.#module = module; this.#ctx = ctx; } // Restore the last session, or finish a login redirect. async init() { try { const result = await (await this.#client).init(); if (!result) return this.emit({ session: "signed-out" }); this.#agent = this.#makeAgent(result.session); const res = await this.#agent.com.atproto.server.getSession(); if (!res.success) throw new Error(JSON.stringify(res)); this.emit({ session: "signed-in", handle: res.data.handle }); } catch (error) { return this.emit({ session: "error", error: `An error occurred: ${error.message}` }); } await this.listKeys(); } // Navigates away to the authorization server; only returns if that fails. async login(identifier) { this.emit({ login: { busy: true, error: null } }); try { await (await this.#client).signIn(identifier, { signal: new AbortController().signal }); } catch (error) { this.emit({ login: { busy: false, error: `Login error: ${error.message}` } }); } } async logout() { await (await this.#client).revoke(this.#agent.did).catch(() => undefined); this.#agent = undefined; this.emit({ ...INITIAL, session: "signed-out" }); } async listKeys() { this.emit({ keys: { ...this.state.keys, status: "loading", error: null } }); try { const { services } = await this.#run("list_keys"); this.emit({ keys: { status: "loaded", services, error: null } }); } catch (error) { this.emit({ keys: { ...this.state.keys, status: "error", error: error.message } }); } } // The module validates the key and writes the record. async createKey({ name, service, key }) { this.emit({ create: { status: "busy", error: null, uri: this.state.create.uri } }); let created; try { created = await this.#run("create_key", { name, service, key }); } catch (error) { return this.emit({ create: { status: "error", error: error.message, uri: this.state.create.uri } }); } await this.listKeys(); this.emit({ create: { status: "done", error: null, uri: created.uri } }); } async #run(entry, input = {}) { const agent = this.#agent; const xrpc = async (nsid, { params, input }) => (await agent.call(nsid, params, input)).data; const ran = await runModule(this.#ctx, await this.#module, entry, { ...input, did: agent.did, handle: this.state.handle }, xrpc, { provides: [] }); if (ran.error !== undefined) throw new Error(ran.error); return ran.result; } }