1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import { BrowserOAuthClient } from '@atproto/oauth-client-browser'
import { pageClientId, requestScope } from 'atlibs/oauth/index.js'
import { createPluginContext } from 'atlibs/plugin-context/index.js'
import { localModule } from 'atlibs/run-module/index.js'
import { NAMES, loadPolicy, runAction } from './cocore-page.js'
// Wiring only: one sign-in with the union of the modules' scopes, a form per action from module.json,
// each run held to its own module's session by runAction.
const base = new URL('./', import.meta.url)
const ctx = createPluginContext({ id: 'cocore' })
const modules = Object.fromEntries(NAMES.map((name) => [name, localModule(new URL(`modules/${name}.wasm`, base), new URL(`modules/${name}.json`, base))]))
const policy = await loadPolicy(base)
const clientId = await pageClientId(requestScope(policy, NAMES))
const client = await BrowserOAuthClient.load({ clientId, handleResolver: 'https://bsky.social' })
const session = (await client.init().catch((e) => (alert(e.message), undefined)))?.session
const $ = (selector) => document.querySelector(selector)
$('#login').hidden = Boolean(session)
$('#signed-in').hidden = !session
if (session) $('#who').textContent = session.did
$('#login').onsubmit = async (e) => {
e.preventDefault()
await client.signIn(new FormData(e.target).get('handle')).catch((error) => ($('#login-error').textContent = error.message))
}
$('#logout').onclick = async () => {
await client.revoke(session.did).catch(() => undefined)
location.reload()
}
function build(tag, props = {}, children = []) {
const node = Object.assign(document.createElement(tag), props)
node.append(...children)
return node
}
for (const name of NAMES) {
const spec = await modules[name] // name, description, actions, calls...
const section = build('details', {}, [build('summary', { textContent: `${spec.name}: ${spec.description}` })])
for (const action of spec.actions) {
const out = build('pre', { className: 'out' })
const fields = (action.fields ?? []).map(({ key, label }) => build('label', { textContent: label }, [key === 'prompt' || key === 'bio' ? build('textarea', { name: key, rows: 3 }) : build('input', { name: key })]))
const form = build('form', {}, [build('h4', { textContent: action.label }), build('small', { textContent: action.description ?? '' }), ...fields, build('button', { textContent: action.label }), out])
form.onsubmit = async (e) => {
e.preventDefault()
const button = form.querySelector('button')
button.setAttribute('aria-busy', 'true')
out.textContent = ''
try {
const input = Object.fromEntries([...new FormData(form)].filter(([, v]) => String(v).trim() !== ''))
out.textContent = JSON.stringify(await runAction({ ctx, policy, modules, session }, name, action.entry, input), null, 2)
} catch (error) {
out.textContent = error.message
} finally {
button.removeAttribute('aria-busy')
}
}
section.append(form)
}
$('#actions').append(section)
}