import { Cubit } from 'atlibs/cubit/index.js' import { policyCall } from 'atlibs/oauth/index.js' import { hostCall, sessionXrpc } from 'atlibs/run-module/host-call.js' import { runModule } from 'atlibs/run-module/index.js' // The marque module's session (records/marque.json): write its at.marque.dns zones, nothing else. export const POLICY = { sessions: { marque: { scope: 'atproto repo:at.marque.dns?action=create&action=update' } }, modules: { marque: { session: 'marque' } }, } const IDLE = { status: 'idle', result: null, error: null } const INITIAL = { session: 'loading', did: null, error: null, domains: IDLE, domain: null, zone: IDLE, change: IDLE } // Page logic. Dependencies come in, so the page wires real ones and a test passes stubs: // ctx a PluginContext (createPluginContext, or createNodeContext on a server) // client Promise of a BrowserOAuthClient // marque Promise of the marque module (localModule or loadModule) export class MarqueCubit extends Cubit { #ctx #client #marque #session constructor({ ctx, client, marque }) { super(INITIAL) this.#ctx = ctx this.#client = client this.#marque = marque } // Restore the last session, or finish a sign-in redirect; signed in, list the account's domains. async init() { try { const signedIn = await (await this.#client).init() this.#session = signedIn?.session this.emit({ session: signedIn ? 'signed-in' : 'signed-out', did: signedIn?.session.did ?? null }) } catch (error) { this.emit({ session: 'error', error: error.message }) } if (this.#session) await this.domains() } async login(handle) { try { await (await this.#client).signIn(handle) } catch (error) { this.emit({ error: error.message }) } } async logout() { await (await this.#client).revoke(this.#session.did).catch(() => undefined) this.#session = undefined this.emit({ ...INITIAL, session: 'signed-out' }) } // Signed out, any account's domains and zones are public reads: pass its DID. async domains(did = this.state.did) { this.emit({ did, domains: { ...IDLE, status: 'busy' }, domain: null, zone: IDLE }) this.emit({ domains: await this.#run('list_domains', { did }) }) } async open(domain) { this.emit({ change: IDLE }) await this.#zone(domain) } async #zone(domain) { this.emit({ domain, zone: { ...IDLE, status: 'busy' } }) this.emit({ zone: await this.#run('list_records', { did: this.state.did, domain }) }) } // entry: create_record, update_record or delete_record; fields as module.json names them. async change(entry, fields) { const { did, domain } = this.state this.emit({ change: { ...IDLE, status: 'busy' } }) const change = await this.#run(entry, { ...fields, did, domain }) this.emit({ change }) if (change.status === 'done') await this.#zone(domain) } // Signed in: the user's OAuth session, held to the marque session's scope. Signed out: public reads only. #call() { const session = this.#session if (!session) return hostCall({ ctx: this.#ctx }) return policyCall(POLICY, 'marque', { marque: hostCall({ ctx: this.#ctx, xrpc: sessionXrpc(session), canHeader: false }) }) } async #run(entry, input) { try { const { result, error } = await runModule(this.#ctx, await this.#marque, entry, input, this.#call()) return error === undefined ? { status: 'done', result, error: null } : { status: 'error', result: null, error } } catch (error) { return { status: 'error', result: null, error: error.message } } } }