~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs-marque

0
0
main
98 lines · 3.4 KB Raw History
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 }
		}
	}
}