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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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 knotless module's session (records/knotless.json's scope; a stage.json has this shape).
export const POLICY = {
sessions: {
knotless: { scope: 'atproto repo:dev.knotless.repo?action=create repo:dev.knotless.pack?action=create repo:dev.knotless.claim?action=create repo:dev.knotless.ref?action=create&action=update blob:application/x-git-packed-objects' },
},
modules: { knotless: { session: 'knotless' } },
}
const IDLE = { status: 'idle', result: null, error: null }
const INITIAL = { session: 'loading', did: null, error: null, run: IDLE }
// Page logic. Dependencies come in, so the page wires real ones and a test passes stubs:
// ctx a PluginContext (createPluginContext)
// client Promise of a BrowserOAuthClient
// knotless Promise of the knotless module (localModule or loadModule)
export class KnotlessCubit extends Cubit {
#ctx
#client
#module
#session
constructor({ ctx, client, knotless }) {
super(INITIAL)
this.#ctx = ctx
this.#client = client
this.#module = knotless
}
// Restore the last session, or finish a sign-in redirect.
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 })
}
}
// Navigates to the user's authorization server; returns only if that fails.
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' })
}
createRepo({ name, description, defaultBranch }) {
return this.#run('create_repo', { name, description, defaultBranch })
}
// changes: a list, or the form's JSON text (the module parses it).
commit({ name, branch, parent, message, author, changes }) {
return this.#run('commit', { name, branch, parent, message, author, changes })
}
// Signed in: the user's OAuth session, held to the module's own scope by policyCall.
async #run(entry, input) {
const session = this.#session
const call = policyCall(POLICY, 'knotless', {
knotless: hostCall({ ctx: this.#ctx, xrpc: sessionXrpc(session), canHeader: false }),
})
this.emit({ run: { ...IDLE, status: 'busy' } })
try {
// ioBytes at its ceiling: a commit past 192 KiB uploads its pack in 4 MB blob parts from one event
const { result, error } = await runModule(this.#ctx, await this.#module, entry, { did: session.did, ...input }, call, { budget: { ioBytes: 16 << 20 } })
this.emit({ run: error === undefined ? { status: 'done', result, error: null } : { status: 'error', result: null, error } })
} catch (error) {
this.emit({ run: { status: 'error', result: null, error: error.message } })
}
}
}