heyquilt 0.3.121 → 0.3.122
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RELEASES.md +19 -0
- package/bin/quilt.js +23 -2
- package/package.json +2 -1
- package/src/api/memory-store.js +5 -1
- package/src/api/routes/seam-connect.js +103 -0
- package/src/api/routes/session-migrate.js +36 -0
- package/src/api/server.js +37 -9
- package/src/api/supabase-store.js +10 -0
- package/src/connection.js +4 -1
- package/src/control.js +12 -0
- package/src/history.js +15 -1
- package/src/mcp.js +12 -1
- package/src/quilts/accounts.js +136 -0
- package/src/quilts/affected.js +132 -0
- package/src/quilts/checks.js +308 -0
- package/src/quilts/cli.js +97 -10
- package/src/quilts/client.js +9 -0
- package/src/quilts/describe.js +31 -1
- package/src/quilts/events.js +13 -2
- package/src/quilts/export-remote.js +156 -0
- package/src/quilts/import-follow.js +236 -0
- package/src/quilts/import-git.js +6 -3
- package/src/quilts/import.js +67 -5
- package/src/quilts/index.js +28 -6
- package/src/quilts/keys.js +186 -0
- package/src/quilts/merge.js +16 -1
- package/src/quilts/oauth.js +202 -0
- package/src/quilts/reviews.js +18 -5
- package/src/quilts/routes.js +61 -1
- package/src/quilts/runner.js +95 -0
- package/src/quilts/server.js +144 -0
- package/src/quilts/service.js +41 -1
- package/src/quilts/setup.js +9 -2
- package/src/quilts/store-memory.js +164 -3
- package/src/quilts/store-rpc.js +12 -1
- package/src/quilts/tools.js +39 -11
- package/src/quilts/window.js +2 -0
- package/src/relay-mcp.js +33 -3
- package/src/relay-prune.js +4 -1
- package/src/server.js +317 -14
- package/src/session-follow.js +81 -0
- package/src/session-layout3.js +60 -0
- package/src/session-migrate.js +119 -0
- package/src/session-quilt.js +145 -0
- package/src/session-seam.js +71 -0
- package/src/session.js +74 -2
- package/src/source-live/live-block.js +4 -3
- package/src/source-live/live-square.js +1 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// seam runner (plans/quilt-source-control/13 "Checks"): a ready-made check runner anyone starts on
|
|
2
|
+
// their own machine. It polls the runs Seam asked its member to run (GET /checks?state=queued),
|
|
3
|
+
// checks each run's commit out into a fresh block folder, runs the command there, and reports the
|
|
4
|
+
// verdict (exit 0 green, anything else red) on the run's run_key with a link to the log. A run
|
|
5
|
+
// Seam cancels (check.cancelled: a newer head superseded it) is stopped and not reported.
|
|
6
|
+
import fs from 'node:fs'
|
|
7
|
+
import os from 'node:os'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { spawn } from 'node:child_process'
|
|
10
|
+
import { checkout } from './block.js'
|
|
11
|
+
|
|
12
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
13
|
+
const safe = (key) => key.replace(/[^\w.-]+/g, '_').slice(0, 180)
|
|
14
|
+
|
|
15
|
+
/** The log link a verdict carries: --log-url with {run_key} and {file}, else the log file's own address. */
|
|
16
|
+
export function logLink (template, runKey, file) {
|
|
17
|
+
if (!template) return 'file://' + file
|
|
18
|
+
return template.replaceAll('{run_key}', encodeURIComponent(runKey)).replaceAll('{file}', encodeURIComponent(path.basename(file)))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The environment a run's command gets: what to run and, when Seam knows, which tests the change reaches. */
|
|
22
|
+
export function runEnv (run, base = process.env) {
|
|
23
|
+
const a = run.affected || { confidence: 'all' }
|
|
24
|
+
return {
|
|
25
|
+
...base,
|
|
26
|
+
SEAM_CHECK: run.name, SEAM_TIER: run.tier, SEAM_COMMIT: run.commit, SEAM_RUN_KEY: run.run_key,
|
|
27
|
+
SEAM_LINE: run.line || '', SEAM_SQUARE: run.square || '',
|
|
28
|
+
SEAM_AFFECTED_CONFIDENCE: a.confidence, SEAM_AFFECTED_TESTS: a.confidence === 'all' ? '' : (a.tests || []).join('\n')
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Runs checks for one quilt. { client, quilt, checks: ['ci'], run: 'npm test', dir (where block
|
|
34
|
+
* folders and logs go), api, once (handle what is queued now, then return), intervalMs, logUrl,
|
|
35
|
+
* say(line) }. Returns the runs it finished: [{ run_key, status | 'cancelled', url }].
|
|
36
|
+
*/
|
|
37
|
+
export async function runChecks ({ client, quilt, checks, run: command, dir = path.join(os.tmpdir(), 'seam-runner'), api, once = false, intervalMs = 5000, logUrl, say = () => {}, keep = false }) {
|
|
38
|
+
if (!checks?.length) throw new Error('seam runner needs --check <name>[,<name>]')
|
|
39
|
+
if (!command) throw new Error('seam runner needs --run "<command>"')
|
|
40
|
+
const logs = path.join(dir, 'logs')
|
|
41
|
+
fs.mkdirSync(logs, { recursive: true })
|
|
42
|
+
const done = []
|
|
43
|
+
const seen = new Set()
|
|
44
|
+
for (;;) {
|
|
45
|
+
const queued = []
|
|
46
|
+
for (const name of checks) queued.push(...(await client.checkRuns(quilt, { state: 'queued', name })).runs)
|
|
47
|
+
const todo = queued.filter((r) => !seen.has(r.run_key)).sort((a, b) => a.at - b.at)
|
|
48
|
+
for (const r of todo) {
|
|
49
|
+
seen.add(r.run_key)
|
|
50
|
+
done.push(await runOne(r))
|
|
51
|
+
}
|
|
52
|
+
if (once) return done
|
|
53
|
+
await sleep(intervalMs)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function runOne (r) {
|
|
57
|
+
const work = fs.mkdtempSync(path.join(dir, 'run-'))
|
|
58
|
+
const file = path.join(logs, safe(r.run_key) + '.log')
|
|
59
|
+
const url = logLink(logUrl, r.run_key, file)
|
|
60
|
+
say(`${r.name} (${r.tier}) on ${r.commit.slice(0, 12)}${r.square ? ` in ${r.square}` : ` at ${r.line}`}: running`)
|
|
61
|
+
const out = fs.openSync(file, 'w')
|
|
62
|
+
let status
|
|
63
|
+
try {
|
|
64
|
+
fs.writeSync(out, `seam runner: ${r.name} ${r.tier} ${r.commit} run_key ${r.run_key}\n$ ${command}\n`)
|
|
65
|
+
await checkout(client, { quilt, rev: r.commit, dir: work, api })
|
|
66
|
+
const child = spawn(command, { cwd: work, shell: true, env: runEnv(r), stdio: ['ignore', out, out], detached: process.platform !== 'win32' })
|
|
67
|
+
const exited = new Promise((resolve) => child.on('close', (code) => resolve(code)))
|
|
68
|
+
let cancelled = false
|
|
69
|
+
for (;;) {
|
|
70
|
+
const code = await Promise.race([exited, sleep(Math.min(intervalMs, 2000)).then(() => undefined)])
|
|
71
|
+
if (code !== undefined) { status = code === 0 ? 'green' : 'red'; break }
|
|
72
|
+
const gone = (await client.checkRuns(quilt, { state: 'cancelled', name: r.name, limit: 50 })).runs.some((x) => x.run_key === r.run_key)
|
|
73
|
+
if (gone && !cancelled) {
|
|
74
|
+
cancelled = true
|
|
75
|
+
try { process.kill(process.platform !== 'win32' ? -child.pid : child.pid, 'SIGTERM') } catch { child.kill('SIGTERM') }
|
|
76
|
+
}
|
|
77
|
+
if (cancelled) { await exited; status = 'cancelled'; break }
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
fs.writeSync(out, `\nseam runner: ${err.message}\n`)
|
|
81
|
+
status = 'red'
|
|
82
|
+
} finally {
|
|
83
|
+
fs.writeSync(out, `\nseam runner: ${status}\n`)
|
|
84
|
+
fs.closeSync(out)
|
|
85
|
+
if (!keep) fs.rmSync(work, { recursive: true, force: true })
|
|
86
|
+
}
|
|
87
|
+
if (status !== 'cancelled') {
|
|
88
|
+
const body = { check: r.name, status, run_key: r.run_key, url }
|
|
89
|
+
if (r.tier === 'pre_merge' && r.square) await client.reportSquareCheck(quilt, r.square, { ...body, head: r.commit })
|
|
90
|
+
else await client.reportCheck(quilt, r.line, body)
|
|
91
|
+
}
|
|
92
|
+
say(`${r.name} on ${r.commit.slice(0, 12)}: ${status} (log ${url})`)
|
|
93
|
+
return { run_key: r.run_key, status, url }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Seam's own server: the routes in routes.js on an HTTP port, apart from any other product.
|
|
2
|
+
// Who is calling comes from an identity service over HTTP (SEAM_IDENTITY_URL): Seam sends it the
|
|
3
|
+
// caller's Authorization header and gets back the caller as a quilt member. Quilt's accounts API is
|
|
4
|
+
// one such service (GET /v1/seam/me); any other host can be one by answering the same way.
|
|
5
|
+
//
|
|
6
|
+
// seam serve or node src/quilts/server.js
|
|
7
|
+
// SEAM_AUTH_URL Seam's own accounts: its Supabase project's address; Seam checks the
|
|
8
|
+
// project's access tokens itself (SEAM_AUTH_JWT_SECRET for a shared secret)
|
|
9
|
+
// SEAM_IDENTITY_URL an identity service, for every other token: GET with the caller's
|
|
10
|
+
// Authorization → 200 { member_id, kind, name, tool?, on_behalf_of? }, or 401
|
|
11
|
+
// (one of the two is required)
|
|
12
|
+
// SEAM_PORT / PORT port (default 8080); SEAM_HOST (default 0.0.0.0)
|
|
13
|
+
// SEAM_STORE=supabase rows and packs in Seam's own Supabase project: SEAM_SUPABASE_URL,
|
|
14
|
+
// SEAM_SUPABASE_KEY (service role), SEAM_BUCKET (default quilt-packs);
|
|
15
|
+
// otherwise a local store in SEAM_DATA (default ./seam-data)
|
|
16
|
+
import http from 'node:http'
|
|
17
|
+
import crypto from 'node:crypto'
|
|
18
|
+
import { fileURLToPath } from 'node:url'
|
|
19
|
+
import { quiltRoutes } from './routes.js'
|
|
20
|
+
import { QuiltError } from './errors.js'
|
|
21
|
+
import { quiltsFromEnv } from './setup.js'
|
|
22
|
+
import { supabaseVerifier, seamIdentity } from './accounts.js'
|
|
23
|
+
|
|
24
|
+
const MAX_BODY = 1024 * 1024
|
|
25
|
+
const KINDS = new Set(['person', 'agent'])
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* identify(req) from an identity service over HTTP. A caller it accepts is kept for `ttlMs`
|
|
29
|
+
* (keyed by a hash of the Authorization header, never the header itself); a refusal is not kept.
|
|
30
|
+
*/
|
|
31
|
+
export function remoteIdentity ({ url, fetch = globalThis.fetch, ttlMs = 30_000, now = Date.now, max = 10_000, timeoutMs = 5000 }) {
|
|
32
|
+
if (!url) throw new Error('remoteIdentity needs the identity service url (SEAM_IDENTITY_URL)')
|
|
33
|
+
const cache = new Map() // hash → { member, until }
|
|
34
|
+
return async function identify (req) {
|
|
35
|
+
const auth = String(req.headers?.authorization || '')
|
|
36
|
+
if (!auth) throw new QuiltError(401, 'not_signed_in', 'sign in: send "Authorization: Bearer <token>"')
|
|
37
|
+
const key = crypto.createHash('sha256').update(auth).digest('hex')
|
|
38
|
+
const hit = cache.get(key)
|
|
39
|
+
if (hit && hit.until > now()) return hit.member
|
|
40
|
+
let res
|
|
41
|
+
try {
|
|
42
|
+
res = await fetch(url, { headers: { authorization: auth, accept: 'application/json' }, signal: AbortSignal.timeout(timeoutMs) })
|
|
43
|
+
} catch {
|
|
44
|
+
throw new QuiltError(503, 'identity_unavailable', 'Seam could not reach its sign-in service; try again')
|
|
45
|
+
}
|
|
46
|
+
const data = await res.json().catch(() => null)
|
|
47
|
+
if (res.status === 401 || res.status === 403) throw new QuiltError(401, 'not_signed_in', (data && data.error) || 'this sign-in is unknown or expired')
|
|
48
|
+
const m = res.ok && data && typeof data === 'object' ? data : null
|
|
49
|
+
if (!m || typeof m.member_id !== 'string' || !m.member_id || !KINDS.has(m.kind)) throw new QuiltError(503, 'identity_unavailable', 'Seam\'s sign-in service gave an answer it cannot use; try again')
|
|
50
|
+
const member = { member_id: m.member_id, kind: m.kind, name: String(m.name || m.kind) }
|
|
51
|
+
if (m.tool) member.tool = String(m.tool)
|
|
52
|
+
if (m.on_behalf_of) member.on_behalf_of = String(m.on_behalf_of)
|
|
53
|
+
if (cache.size >= max) cache.delete(cache.keys().next().value)
|
|
54
|
+
cache.set(key, { member, until: now() + ttlMs })
|
|
55
|
+
return member
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readBody (req, max, raw) {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
let size = 0; const chunks = []
|
|
62
|
+
req.on('data', (c) => { size += c.length; if (size > max) { req.destroy(); reject(new QuiltError(413, 'too_large', 'too large')) } else chunks.push(c) })
|
|
63
|
+
req.on('end', () => {
|
|
64
|
+
const buf = Buffer.concat(chunks)
|
|
65
|
+
if (raw) return resolve(buf)
|
|
66
|
+
if (!buf.length) return resolve({})
|
|
67
|
+
if (/^application\/x-www-form-urlencoded/i.test(String(req.headers['content-type'] || ''))) return resolve(Object.fromEntries(new URLSearchParams(buf.toString('utf8'))))
|
|
68
|
+
let body
|
|
69
|
+
try { body = JSON.parse(buf.toString('utf8')) } catch { return reject(new QuiltError(400, 'bad_request', 'bad json')) }
|
|
70
|
+
resolve(body && typeof body === 'object' && !Array.isArray(body) ? body : {})
|
|
71
|
+
})
|
|
72
|
+
req.on('error', reject)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const decode = (s) => { try { return decodeURIComponent(s) } catch { throw new QuiltError(400, 'bad_request', 'bad path') } }
|
|
77
|
+
|
|
78
|
+
/** Seam on a port: { server, url, close }. */
|
|
79
|
+
export async function startSeamServer ({ quilts, identify, port = 0, host = '127.0.0.1', log = () => {} }) {
|
|
80
|
+
if (!quilts) throw new Error('startSeamServer needs quilts (createQuilts)')
|
|
81
|
+
const routes = quiltRoutes({ quilts, identify })
|
|
82
|
+
const server = http.createServer(async (req, res) => {
|
|
83
|
+
const send = (status, data) => {
|
|
84
|
+
res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' })
|
|
85
|
+
res.end(JSON.stringify(data))
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const url = new URL(req.url, 'http://x')
|
|
89
|
+
if (req.method === 'GET' && (url.pathname === '/healthz' || url.pathname === '/')) return send(200, { ok: true, service: 'seam' })
|
|
90
|
+
const route = routes.find(([m, re]) => m === req.method && re.test(url.pathname))
|
|
91
|
+
if (!route) return send(404, { ok: false, error: 'not found', code: 'not_found' })
|
|
92
|
+
const opts = route[3] || {}
|
|
93
|
+
const body = opts.raw ? await readBody(req, opts.maxBody || MAX_BODY, true) : ['POST', 'PUT', 'PATCH'].includes(req.method) ? await readBody(req, opts.maxBody || MAX_BODY, false) : {}
|
|
94
|
+
const out = await route[2](req, body, url.pathname.match(route[1]).slice(1).map(decode))
|
|
95
|
+
if (Array.isArray(out)) send(out[0], out[1])
|
|
96
|
+
else send(200, out)
|
|
97
|
+
} catch (err) {
|
|
98
|
+
if (err instanceof QuiltError) return send(err.status, { ok: false, error: err.message, code: err.code, ...err.extra })
|
|
99
|
+
if (err && Number.isInteger(err.status) && err.status >= 400 && err.status < 500) return send(err.status, { ok: false, error: err.message })
|
|
100
|
+
log(`seam error: ${err?.stack || err?.message || err}`)
|
|
101
|
+
send(500, { ok: false, error: 'internal error', code: 'internal' })
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
await new Promise((resolve) => server.listen(port, host, resolve))
|
|
105
|
+
const at = server.address()
|
|
106
|
+
return { server, url: `http://${at.address.includes(':') ? `[${at.address}]` : at.address}:${at.port}`, close: () => new Promise((resolve) => server.close(resolve)) }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The SEAM_* settings as the store settings setup.js reads (its QUILT_SOURCE_* names still work). */
|
|
110
|
+
export function storeEnv (env) {
|
|
111
|
+
const pick = (seam, old) => env[seam] ?? env[old]
|
|
112
|
+
const out = { QUILT_SOURCE: '1' }
|
|
113
|
+
const map = { QUILT_SOURCE_STORE: pick('SEAM_STORE', 'QUILT_SOURCE_STORE'), QUILT_SOURCE_SUPABASE_URL: pick('SEAM_SUPABASE_URL', 'QUILT_SOURCE_SUPABASE_URL'), QUILT_SOURCE_SUPABASE_KEY: pick('SEAM_SUPABASE_KEY', 'QUILT_SOURCE_SUPABASE_KEY'), QUILT_SOURCE_BUCKET: pick('SEAM_BUCKET', 'QUILT_SOURCE_BUCKET'), QUILT_SOURCE_DATA: env.SEAM_DATA ?? env.QUILT_SOURCE_DATA ?? './seam-data', SUPABASE_URL: env.SUPABASE_URL, SEAM_SITE_URL: env.SEAM_SITE_URL }
|
|
114
|
+
for (const [k, v] of Object.entries(map)) if (v !== undefined) out[k] = v
|
|
115
|
+
return out
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Who is calling, from the settings: Seam's own sign-in (SEAM_AUTH_URL, Seam's Supabase project,
|
|
120
|
+
* with SEAM_AUTH_JWT_SECRET when it still signs with a shared secret) checked here, and/or an
|
|
121
|
+
* identity service over HTTP (SEAM_IDENTITY_URL) for every other token.
|
|
122
|
+
*/
|
|
123
|
+
export function identityFromEnv (env, quilts, { fetch = globalThis.fetch } = {}) {
|
|
124
|
+
const authUrl = env.SEAM_AUTH_URL || ''
|
|
125
|
+
const verify = authUrl ? supabaseVerifier({ authUrl, jwtSecret: env.SEAM_AUTH_JWT_SECRET || null, fetch }) : null
|
|
126
|
+
const fallback = env.SEAM_IDENTITY_URL ? remoteIdentity({ url: env.SEAM_IDENTITY_URL, fetch, ...(env.SEAM_IDENTITY_TTL_MS ? { ttlMs: Number(env.SEAM_IDENTITY_TTL_MS) } : {}) }) : null
|
|
127
|
+
if (!verify && !fallback) throw new Error('seam serve needs a sign-in: SEAM_AUTH_URL (Seam\'s own accounts, its Supabase project) and/or SEAM_IDENTITY_URL (an identity service, e.g. https://api.heyquilt.com/v1/seam/me)')
|
|
128
|
+
return seamIdentity({ quilts, verify, fallback })
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** `seam serve`: the settings from the environment, a port, and a log line. */
|
|
132
|
+
export async function serveFromEnv (env = process.env, { log = console.log } = {}) {
|
|
133
|
+
if (!env.SEAM_AUTH_URL && !env.SEAM_IDENTITY_URL) identityFromEnv(env, null)
|
|
134
|
+
const quilts = await quiltsFromEnv(storeEnv(env), { log })
|
|
135
|
+
const identify = identityFromEnv(env, quilts)
|
|
136
|
+
const s = await startSeamServer({ quilts, identify, port: Number(env.SEAM_PORT || env.PORT || 8080), host: env.SEAM_HOST || '0.0.0.0', log })
|
|
137
|
+
const from = [env.SEAM_AUTH_URL && `Seam accounts (${new URL(env.SEAM_AUTH_URL).host})`, env.SEAM_IDENTITY_URL && new URL(env.SEAM_IDENTITY_URL).host].filter(Boolean).join(' and ')
|
|
138
|
+
log(`seam: listening on ${s.url}, sign-in from ${from}`)
|
|
139
|
+
return s
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
143
|
+
serveFromEnv().catch((err) => { console.error(err.message || err); process.exit(1) })
|
|
144
|
+
}
|
package/src/quilts/service.js
CHANGED
|
@@ -31,7 +31,7 @@ export const shortId = (id, n = 12) => (id ? id.slice(0, n) : null)
|
|
|
31
31
|
|
|
32
32
|
const statusOf = {
|
|
33
33
|
not_found: 404, head_moved: 409, tip_moved: 409, square_merging: 409, square_closed: 409, holder_moved: 409, square_state: 409, exists: 409, slug_taken: 409,
|
|
34
|
-
conflict: 422, line_frozen: 423, line_exists: 409, tag_exists: 409, claim_refused: 412, claim_taken: 409, policy: 428, own_square: 403, parent_open: 409, bad_parent: 422, bad_request: 400, forbidden: 403, not_holder: 403, too_many_files: 422, pack_corrupt: 422, key_exists: 409
|
|
34
|
+
conflict: 422, line_frozen: 423, line_exists: 409, tag_exists: 409, claim_refused: 412, claim_taken: 409, policy: 428, own_square: 403, parent_open: 409, bad_parent: 422, bad_request: 400, forbidden: 403, not_holder: 403, too_many_files: 422, pack_corrupt: 422, key_exists: 409, moved: 409, too_many_agents: 409, not_claimable: 403
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/** A store error as a QuiltError with its wire status. */
|
|
@@ -617,6 +617,7 @@ export function createQuiltService ({ store, objects, now = Date.now }) {
|
|
|
617
617
|
const path = cleanPath(ch.path)
|
|
618
618
|
if (path.split('/')[0] === '.seam') throw new QuiltError(400, 'bad_request', '.seam/ is the block\'s own folder and is never committed')
|
|
619
619
|
if (ch.op === 'delete') { out.push({ path, op: 'delete' }); continue }
|
|
620
|
+
if (ch.op === 'conflict') { out.push(await importedTerm(q, path, ch, blobs)); continue }
|
|
620
621
|
if (ch.op !== 'put') throw new QuiltError(400, 'bad_request', `${path}: op is put or delete`)
|
|
621
622
|
let id = ch.blob
|
|
622
623
|
let size = ch.size
|
|
@@ -650,6 +651,45 @@ export function createQuiltService ({ store, objects, now = Date.now }) {
|
|
|
650
651
|
return { changes: out, blobs, resolved }
|
|
651
652
|
}
|
|
652
653
|
|
|
654
|
+
/**
|
|
655
|
+
* A conflict brought in from another system (16: an open merge of a session that became a
|
|
656
|
+
* quilt): `{ path, op: 'conflict', reason: 'import', base, ours, theirs }`, each side
|
|
657
|
+
* `{ content (base64), from_square?, from_commit?, by? }` or null for a side without the file.
|
|
658
|
+
* Writes a `content` term (`edit_vs_delete` when a side is gone) whose origin says it was
|
|
659
|
+
* imported; from then on it is a term like a merge's (07): materialised, settled by hand.
|
|
660
|
+
*/
|
|
661
|
+
async function importedTerm (q, path, ch, blobs) {
|
|
662
|
+
if (ch.reason !== 'import') throw new QuiltError(400, 'bad_request', `${path}: a commit writes a conflict only with reason 'import'; merges make the others`)
|
|
663
|
+
if (!ch.ours && !ch.theirs) throw new QuiltError(400, 'bad_request', `${path}: a conflict needs at least one side with the file`)
|
|
664
|
+
const side = async (s, which) => {
|
|
665
|
+
if (s == null) return {}
|
|
666
|
+
if (typeof s !== 'object') throw new QuiltError(400, 'bad_request', `${path}: ${which} is { content } or null`)
|
|
667
|
+
const out = {}
|
|
668
|
+
if (s.content != null) {
|
|
669
|
+
const bytes = Buffer.from(String(s.content), 'base64')
|
|
670
|
+
out.blob = blobId(bytes)
|
|
671
|
+
out.mode = 'file'
|
|
672
|
+
blobs.push({ kind: 'blob', id: out.blob, bytes })
|
|
673
|
+
}
|
|
674
|
+
if (s.from_commit != null) {
|
|
675
|
+
if (!isId(s.from_commit) || !(await store.getCommit(q, s.from_commit))) throw new QuiltError(422, 'bad_request', `${path}: ${which}.from_commit is not a commit of this quilt`)
|
|
676
|
+
out.from_commit = s.from_commit
|
|
677
|
+
}
|
|
678
|
+
if (s.from_square != null) {
|
|
679
|
+
const sq = await store.getSquare(String(s.from_square))
|
|
680
|
+
if (!sq || sq.quilt_id !== q) throw new QuiltError(422, 'bad_request', `${path}: ${which}.from_square is not a square of this quilt`)
|
|
681
|
+
out.from_square = sq.id
|
|
682
|
+
}
|
|
683
|
+
if (s.by != null) out.by = String(s.by).slice(0, 200)
|
|
684
|
+
return out
|
|
685
|
+
}
|
|
686
|
+
const [base, ours, theirs] = [await side(ch.base, 'base'), await side(ch.ours, 'ours'), await side(ch.theirs, 'theirs')]
|
|
687
|
+
const term = { kind: ours.blob && theirs.blob ? 'content' : 'edit_vs_delete', adds: [ours, theirs], removes: [base], origin: { reason: 'import' } }
|
|
688
|
+
const id = termId(term)
|
|
689
|
+
blobs.push({ kind: 'term', id, bytes: termBytes(term) })
|
|
690
|
+
return { path, op: 'conflict', term: id }
|
|
691
|
+
}
|
|
692
|
+
|
|
653
693
|
// ---- uploads (04: have?, then one pack of what is missing) ----
|
|
654
694
|
|
|
655
695
|
async function have (me, ref, ids) {
|
package/src/quilts/setup.js
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
// QUILT_SOURCE_SUPABASE_KEY (its service role key); the rows from the
|
|
7
7
|
// *_quilts.sql migration, the packs in its bucket QUILT_SOURCE_BUCKET
|
|
8
8
|
// (default quilt-packs)
|
|
9
|
+
// SEAM_IMPORT_SCRATCH=<dir> where imports from a git remote keep their mirrors (default the temp folder)
|
|
10
|
+
// SEAM_SECRET_<NAME> a credential an import names (`--credential name`): user:token or a token
|
|
9
11
|
import path from 'node:path'
|
|
12
|
+
import os from 'node:os'
|
|
10
13
|
import { createQuilts } from './index.js'
|
|
11
14
|
import { createMemoryQuiltStore } from './store-memory.js'
|
|
12
15
|
import { BlobstoreObjects, diskObjects } from './objects.js'
|
|
@@ -24,11 +27,15 @@ export async function quiltsFromEnv (env, { memory = false, log = () => {} } = {
|
|
|
24
27
|
const store = createSupabaseQuiltStore({ url, serviceKey: key })
|
|
25
28
|
const objects = new BlobstoreObjects(new SupabaseBucket({ url, key, bucket: env.QUILT_SOURCE_BUCKET || 'quilt-packs' }))
|
|
26
29
|
log(`quilt source control: rows and packs in ${new URL(url).host}`)
|
|
27
|
-
|
|
30
|
+
const quilts = createQuilts({ store, objects, events: { log }, siteUrl: env.SEAM_SITE_URL || undefined, importFollow: { scratch: env.SEAM_IMPORT_SCRATCH || path.join(os.tmpdir(), 'seam-imports') } })
|
|
31
|
+
quilts.follower.start()
|
|
32
|
+
return quilts
|
|
28
33
|
}
|
|
29
34
|
const dir = path.resolve(env.QUILT_SOURCE_DATA || './quilt-source-data')
|
|
30
35
|
const store = createMemoryQuiltStore({ file: path.join(dir, 'journal.jsonl') })
|
|
31
36
|
log(`quilt source control: local store in ${dir}${memory ? '' : ' (QUILT_SOURCE_DATA)'}`)
|
|
32
37
|
// The local prototype may POST events to webhooks on this machine; the hosted one may not.
|
|
33
|
-
|
|
38
|
+
const quilts = createQuilts({ store, objects: diskObjects(path.join(dir, 'packs')), events: { allowLocalWebhooks: true, log }, siteUrl: env.SEAM_SITE_URL || undefined, importFollow: { scratch: env.SEAM_IMPORT_SCRATCH || path.join(dir, 'imports'), allowLocal: true } })
|
|
39
|
+
quilts.follower.start()
|
|
40
|
+
return quilts
|
|
34
41
|
}
|
|
@@ -23,9 +23,17 @@ export function createMemoryQuiltStore ({ file = null } = {}) {
|
|
|
23
23
|
reviews: new Map(), // quilt → review rows (13), oldest first
|
|
24
24
|
signingKeys: new Map(), // key id → { id, member, public_key, name, added_at, removed_at }
|
|
25
25
|
signatures: new Map(), // quilt + commit → signature rows
|
|
26
|
+
accounts: new Map(), // account id → { id, email, name, member_id, created_at, seen_at } (accounts.js)
|
|
27
|
+
memberClaims: new Map(), // member id → { member_id, email, name, claimed_by, claimed_at } (members made before Seam accounts)
|
|
28
|
+
agents: new Map(), // agent id → { id, account_id, name, created_at, revoked_at } (keys.js)
|
|
29
|
+
keys: new Map(), // key id → { id, hash, account_id, kind, agent_id, public_key, name, created_at, expires_at, revoked_at }
|
|
30
|
+
oauthClients: new Map(), // client id → { id, account_id, name, redirect_uris, created_at, revoked_at } (oauth.js)
|
|
31
|
+
oauthCodes: new Map(), // code hash → { id, code_hash, client_id, account_id, redirect_uri, challenge, scopes, created_at, expires_at, used_at }
|
|
32
|
+
deviceLinks: new Map(), // link id → { id, code_hash, user_code, public_key, name, platform, state, account_id, created_at, expires_at }
|
|
26
33
|
tokens: new Map(), // token id → { id, quilt_id, name, level, hash, created_by, created_at, revoked_at } (tokens.js)
|
|
27
34
|
lineRules: new Map(), // quilt → Map of line → { line, approvals, checks }
|
|
28
35
|
squareChecks: new Map(), // square → Map of check name → its latest report
|
|
36
|
+
checkRuns: new Map(), // quilt → Map of run_key → a check run (13)
|
|
29
37
|
claims: new Map(), // quilt → Map of claim id → claim (06: rows, not log entries)
|
|
30
38
|
resolutions: new Map(), // quilt → Map of key → remembered resolution (07, D12)
|
|
31
39
|
tags: new Map(), // quilt + name → tag (06: never moved or deleted)
|
|
@@ -240,7 +248,7 @@ export function createMemoryQuiltStore ({ file = null } = {}) {
|
|
|
240
248
|
if (expect && 'holder' in expect && s.holder !== expect.holder) throw Object.assign(new StoreError('holder_moved', `the square is now held by ${s.holder}`), { holder: s.holder })
|
|
241
249
|
if (expect && 'state' in expect && s.state !== expect.state) throw Object.assign(new StoreError('square_state', `the square is ${s.state}`), { state: s.state })
|
|
242
250
|
appendLog(s.quilt_id, { kind: 'square', square: s.id, actor, at, payload: { op, ...patch, ...log } })
|
|
243
|
-
Object.assign(s, patch, { updated_at: at })
|
|
251
|
+
Object.assign(s, patch, { updated_at: at }, patch.state === 'merged' ? { closed_at: at } : {})
|
|
244
252
|
return { ...s }
|
|
245
253
|
},
|
|
246
254
|
|
|
@@ -356,6 +364,95 @@ export function createMemoryQuiltStore ({ file = null } = {}) {
|
|
|
356
364
|
if (!row.removed_at) row.removed_at = at
|
|
357
365
|
return clone(row)
|
|
358
366
|
},
|
|
367
|
+
// A Seam account (accounts.js): made on its first verified call, its email kept current.
|
|
368
|
+
accountFor ({ id, email, name, at }) {
|
|
369
|
+
const row = t.accounts.get(id)
|
|
370
|
+
if (row) { row.email = email; row.seen_at = at; return clone(row) }
|
|
371
|
+
const taken = new Set([...t.accounts.values()].map((a) => a.member_id))
|
|
372
|
+
const c = [...t.memberClaims.values()].filter((x) => x.email === String(email).toLowerCase() && x.member_id.startsWith('person:') && !x.claimed_by && !taken.has(x.member_id)).sort((a, b) => a.member_id < b.member_id ? -1 : 1)[0]
|
|
373
|
+
t.accounts.set(id, { id, email, name, member_id: c ? c.member_id : `person:${id}`, created_at: at, seen_at: at })
|
|
374
|
+
if (c) { c.claimed_by = id; c.claimed_at = at }
|
|
375
|
+
return clone(t.accounts.get(id))
|
|
376
|
+
},
|
|
377
|
+
addMemberClaims ({ claims }) {
|
|
378
|
+
let added = 0
|
|
379
|
+
for (const c of claims) if (!t.memberClaims.has(c.member_id)) { t.memberClaims.set(c.member_id, { member_id: c.member_id, email: String(c.email).toLowerCase(), name: c.name ?? null, claimed_by: null, claimed_at: null }); added++ }
|
|
380
|
+
return { added }
|
|
381
|
+
},
|
|
382
|
+
// Seam's own keys (keys.js): a computer's or an agent's; `replace` revokes its earlier live keys.
|
|
383
|
+
addKey (r) {
|
|
384
|
+
if (r.agent_id) { const a = t.agents.get(r.agent_id); if (!a || a.account_id !== r.account_id || a.revoked_at) throw new StoreError('not_found', 'no such agent') }
|
|
385
|
+
if (r.replace) for (const x of t.keys.values()) if (x.account_id === r.account_id && !x.revoked_at && ((r.agent_id && x.agent_id === r.agent_id) || (r.public_key && x.public_key === r.public_key) || (r.grant_id && x.grant_id === r.grant_id))) x.revoked_at = r.created_at
|
|
386
|
+
const { replace, ...row } = r
|
|
387
|
+
t.keys.set(r.id, { agent_id: null, public_key: null, expires_at: null, client_id: null, grant_id: null, scopes: null, ...row, revoked_at: null })
|
|
388
|
+
return clone(t.keys.get(r.id))
|
|
389
|
+
},
|
|
390
|
+
revokeKey ({ id, at }) {
|
|
391
|
+
const x = t.keys.get(id)
|
|
392
|
+
if (!x) throw new StoreError('not_found', 'no such key')
|
|
393
|
+
if (!x.revoked_at) x.revoked_at = at
|
|
394
|
+
return clone(x)
|
|
395
|
+
},
|
|
396
|
+
createAgent ({ id, account_id, name, created_at, max = 100, claim }) {
|
|
397
|
+
const acct = t.accounts.get(account_id)
|
|
398
|
+
if (!acct) throw new StoreError('not_found', 'no such account')
|
|
399
|
+
if ([...t.agents.values()].filter((a) => a.account_id === account_id && !a.revoked_at).length >= max) throw new StoreError('too_many_agents', 'too many agents; revoke one first')
|
|
400
|
+
if (claim) {
|
|
401
|
+
const c = t.memberClaims.get(claim)
|
|
402
|
+
if (!c || c.email !== acct.email.toLowerCase() || c.claimed_by) throw new StoreError('not_claimable', 'that agent is not one of yours to re-link, or it was re-linked already')
|
|
403
|
+
id = claim.slice(6); c.claimed_by = account_id; c.claimed_at = created_at
|
|
404
|
+
}
|
|
405
|
+
t.agents.set(id, { id, account_id, name, created_at, revoked_at: null })
|
|
406
|
+
return clone(t.agents.get(id))
|
|
407
|
+
},
|
|
408
|
+
revokeAgent ({ id, account_id, at }) {
|
|
409
|
+
const a = t.agents.get(id)
|
|
410
|
+
if (!a || a.account_id !== account_id) throw new StoreError('not_found', 'no such agent')
|
|
411
|
+
if (!a.revoked_at) { a.revoked_at = at; for (const x of t.keys.values()) if (x.agent_id === id && !x.revoked_at) x.revoked_at = at }
|
|
412
|
+
return clone(a)
|
|
413
|
+
},
|
|
414
|
+
// OAuth (oauth.js): an app's client, its one-use codes, and ending a grant's tokens.
|
|
415
|
+
addOAuthClient (r) {
|
|
416
|
+
if (!t.accounts.has(r.account_id)) throw new StoreError('not_found', 'no such account')
|
|
417
|
+
t.oauthClients.set(r.id, { ...r, revoked_at: null })
|
|
418
|
+
return clone(t.oauthClients.get(r.id))
|
|
419
|
+
},
|
|
420
|
+
revokeOAuthClient ({ id, account_id, at }) {
|
|
421
|
+
const c = t.oauthClients.get(id)
|
|
422
|
+
if (!c || c.account_id !== account_id) throw new StoreError('not_found', 'no such app')
|
|
423
|
+
if (!c.revoked_at) { c.revoked_at = at; for (const x of t.keys.values()) if (x.client_id === id && !x.revoked_at) x.revoked_at = at }
|
|
424
|
+
return clone(c)
|
|
425
|
+
},
|
|
426
|
+
addOAuthCode (r) {
|
|
427
|
+
for (const [h, c] of t.oauthCodes) if (c.expires_at < r.created_at - 86400_000) t.oauthCodes.delete(h)
|
|
428
|
+
t.oauthCodes.set(r.code_hash, { ...r, used_at: null })
|
|
429
|
+
return clone(t.oauthCodes.get(r.code_hash))
|
|
430
|
+
},
|
|
431
|
+
takeOAuthCode ({ code_hash: h, at }) {
|
|
432
|
+
const c = t.oauthCodes.get(h)
|
|
433
|
+
if (!c) return null
|
|
434
|
+
const out = { ...clone(c), was_used: c.used_at != null }
|
|
435
|
+
if (!c.used_at) c.used_at = at
|
|
436
|
+
return out
|
|
437
|
+
},
|
|
438
|
+
revokeGrant ({ grant_id: g, at }) {
|
|
439
|
+
let n = 0
|
|
440
|
+
for (const x of t.keys.values()) if (x.grant_id === g && !x.revoked_at) { x.revoked_at = at; n++ }
|
|
441
|
+
return { revoked: n }
|
|
442
|
+
},
|
|
443
|
+
addDeviceLink (r) {
|
|
444
|
+
for (const [id, l] of t.deviceLinks) if (l.expires_at < r.created_at - 86400_000) t.deviceLinks.delete(id)
|
|
445
|
+
if ([...t.deviceLinks.values()].some((l) => l.user_code === r.user_code)) throw new StoreError('exists', 'this code is taken')
|
|
446
|
+
t.deviceLinks.set(r.id, { platform: null, ...r, state: 'pending', account_id: null })
|
|
447
|
+
return clone(t.deviceLinks.get(r.id))
|
|
448
|
+
},
|
|
449
|
+
setDeviceLink ({ id, from, state, account_id }) {
|
|
450
|
+
const l = t.deviceLinks.get(id)
|
|
451
|
+
if (!l || l.state !== from) throw new StoreError('moved', 'this sign-in link changed; start again')
|
|
452
|
+
l.state = state
|
|
453
|
+
if (account_id) l.account_id = account_id
|
|
454
|
+
return clone(l)
|
|
455
|
+
},
|
|
359
456
|
// A quilt token (tokens.js): a row and its membership token:<id>, made and ended together.
|
|
360
457
|
createToken (r) {
|
|
361
458
|
quiltRow(r.quilt_id)
|
|
@@ -392,12 +489,31 @@ export function createMemoryQuiltStore ({ file = null } = {}) {
|
|
|
392
489
|
return clone(r)
|
|
393
490
|
},
|
|
394
491
|
// What a line needs to take a merge (13): a number of approvals.
|
|
395
|
-
setLineRule ({ quilt_id: q, line, approvals, checks = [] }) {
|
|
492
|
+
setLineRule ({ quilt_id: q, line, approvals, checks = [], runners = {}, targeting = {} }) {
|
|
396
493
|
if (!t.lines.has(k(q, line))) throw new StoreError('not_found', `no line ${line}`)
|
|
397
494
|
let m = t.lineRules.get(q); if (!m) { m = new Map(); t.lineRules.set(q, m) }
|
|
398
|
-
m.set(line, { line, approvals, checks: [...checks] })
|
|
495
|
+
m.set(line, { line, approvals, checks: [...checks], runners: clone(runners), targeting: clone(targeting), since_seq: quiltRow(q).head_seq })
|
|
399
496
|
return clone(m.get(line))
|
|
400
497
|
},
|
|
498
|
+
// Check runs (13): added once per run_key (a repeat is the same run); returns the stored rows.
|
|
499
|
+
addCheckRuns ({ quilt_id: q, runs }) {
|
|
500
|
+
quiltRow(q)
|
|
501
|
+
let m = t.checkRuns.get(q); if (!m) { m = new Map(); t.checkRuns.set(q, m) }
|
|
502
|
+
return runs.map((r) => { if (!m.has(r.run_key)) m.set(r.run_key, { ...clone(r), quilt_id: q, affected: r.affected ?? null, url: r.url ?? null, verdict: r.verdict ?? null, by: r.by ?? null, advisory: !!r.advisory, finished_at: r.finished_at ?? null }); return clone(m.get(r.run_key)) })
|
|
503
|
+
},
|
|
504
|
+
// Asks a check's runners again (13): a policy log row the events job turns into check.requested.
|
|
505
|
+
requestCheckRun ({ quilt_id: q, line, square_id: sq, actor, at, payload }) {
|
|
506
|
+
if (!t.lines.has(k(q, line))) throw new StoreError('not_found', `no line ${line}`)
|
|
507
|
+
return { seq: appendLog(q, { kind: 'policy', line, square: sq || null, actor, at, payload: clone(payload) }).seq }
|
|
508
|
+
},
|
|
509
|
+
// A run's state and verdict; `expect_state` refuses when it moved (square_state).
|
|
510
|
+
setCheckRun ({ quilt_id: q, run_key: key, state, verdict, by, url, at, expect_state: expect }) {
|
|
511
|
+
const r = t.checkRuns.get(q)?.get(key)
|
|
512
|
+
if (!r) throw new StoreError('not_found', `no check run ${key}`)
|
|
513
|
+
if (expect && r.state !== expect) throw Object.assign(new StoreError('square_state', `the run is ${r.state}`), { state: r.state })
|
|
514
|
+
Object.assign(r, { state, ...(verdict !== undefined ? { verdict } : {}), ...(by !== undefined ? { by } : {}), ...(url !== undefined ? { url } : {}), ...(state === 'done' || state === 'cancelled' ? { finished_at: at } : {}) })
|
|
515
|
+
return clone(r)
|
|
516
|
+
},
|
|
401
517
|
// A check's latest result on a square (13): one row per (square, check), replaced by each report.
|
|
402
518
|
setSquareCheck ({ quilt_id: q, square_id: sq, name, status, head, by, at }) {
|
|
403
519
|
const row = t.squares.get(sq)
|
|
@@ -436,6 +552,36 @@ export function createMemoryQuiltStore ({ file = null } = {}) {
|
|
|
436
552
|
Object.assign(ln, { frozen: !!frozen, updated_at: at })
|
|
437
553
|
return clone(ln)
|
|
438
554
|
},
|
|
555
|
+
// The quilt's import record (14), in settings.import, with the lines it freezes (freeze: true,
|
|
556
|
+
// reason import_follow) or unfreezes (freeze: false) in the same transaction. Refuses with
|
|
557
|
+
// `conflict` when the record's state is not expect_state, or a line's tip is not expect_tips[line].
|
|
558
|
+
setImport ({ quilt_id: q, import: rec, freeze = null, lines = [], expect_state, expect_tips, actor, at }) {
|
|
559
|
+
const quilt = quiltRow(q)
|
|
560
|
+
const cur = quilt.settings?.import || null
|
|
561
|
+
if (expect_state !== undefined && (cur?.state || null) !== expect_state) throw new StoreError('conflict', `the import is ${cur?.state || 'not started'}`, { state: cur?.state || null })
|
|
562
|
+
for (const [name, tip] of Object.entries(expect_tips || {})) {
|
|
563
|
+
const ln = t.lines.get(k(q, name))
|
|
564
|
+
if ((ln?.tip || null) !== (tip || null)) throw new StoreError('conflict', `${name} moved`, { line: name, tip: ln?.tip || null })
|
|
565
|
+
}
|
|
566
|
+
if (freeze !== null) {
|
|
567
|
+
for (const name of lines) {
|
|
568
|
+
const ln = t.lines.get(k(q, name))
|
|
569
|
+
if (!ln || !!ln.frozen === !!freeze) continue
|
|
570
|
+
appendLog(q, { kind: 'policy', line: name, actor, at, payload: { op: freeze ? 'freeze' : 'unfreeze', reason: 'import_follow' } })
|
|
571
|
+
Object.assign(ln, { frozen: !!freeze, updated_at: at })
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (!cur || cur.state !== rec.state) appendLog(q, { kind: 'policy', actor, at, payload: { op: 'import', id: rec.id, state: rec.state } })
|
|
575
|
+
quilt.settings = { ...(quilt.settings || {}), import: clone(rec) }
|
|
576
|
+
return clone(rec)
|
|
577
|
+
},
|
|
578
|
+
// The quilt's export to a git remote (14), in settings.export; a policy row when `logged`.
|
|
579
|
+
setExport ({ quilt_id: q, export: rec, logged = false, actor, at }) {
|
|
580
|
+
const quilt = quiltRow(q)
|
|
581
|
+
if (logged) appendLog(q, { kind: 'policy', actor, at, payload: { op: 'export', target: rec.target, enabled: !!rec.enabled, state: rec.status?.state || null } })
|
|
582
|
+
quilt.settings = { ...(quilt.settings || {}), export: clone(rec) }
|
|
583
|
+
return clone(rec)
|
|
584
|
+
},
|
|
439
585
|
// The merge window's state on a line (07): current window, maximum, red checks.
|
|
440
586
|
setLineWindow ({ quilt_id: q, line, merge_window: w, merge_window_max: m, red_checks: red }) {
|
|
441
587
|
const ln = t.lines.get(k(q, line))
|
|
@@ -566,11 +712,24 @@ export function createMemoryQuiltStore ({ file = null } = {}) {
|
|
|
566
712
|
async resolutions (q) { return [...(t.resolutions.get(q)?.values() || [])].map(clone) },
|
|
567
713
|
async signingKey (id) { const r = t.signingKeys.get(id); return r ? clone(r) : null },
|
|
568
714
|
async tokens (q) { return [...t.tokens.values()].filter((r) => r.quilt_id === q).sort((a, b) => a.created_at - b.created_at).map(clone) },
|
|
715
|
+
async keyByHash (hash) { const r = [...t.keys.values()].find((x) => x.hash === hash); return r ? clone(r) : null },
|
|
716
|
+
async deviceKey (publicKey) { const r = [...t.keys.values()].filter((x) => x.public_key === publicKey && !x.revoked_at).sort((a, b) => b.created_at - a.created_at)[0]; return r ? clone(r) : null },
|
|
717
|
+
async agentClaims (email) { return { claims: [...t.memberClaims.values()].filter((c) => c.email === String(email).toLowerCase() && c.member_id.startsWith('agent:') && !c.claimed_by).sort((a, b) => a.member_id < b.member_id ? -1 : 1).map((c) => ({ member_id: c.member_id, name: c.name })) } },
|
|
718
|
+
async getAgent (id) { const r = t.agents.get(id); return r ? clone(r) : null },
|
|
719
|
+
async agentsOf (accountId) { return [...t.agents.values()].filter((a) => a.account_id === accountId).sort((a, b) => a.created_at - b.created_at).map(clone) },
|
|
720
|
+
async deviceLink (by) { const r = [...t.deviceLinks.values()].find((l) => (by.code_hash && l.code_hash === by.code_hash) || (by.user_code && l.user_code === by.user_code)); return r ? clone(r) : null },
|
|
721
|
+
async oauthClient (id) { const r = t.oauthClients.get(id); return r ? clone(r) : null },
|
|
722
|
+
async oauthClientsOf (accountId) { return [...t.oauthClients.values()].filter((c) => c.account_id === accountId).sort((a, b) => a.created_at - b.created_at).map(clone) },
|
|
723
|
+
async getAccount (id) { const r = t.accounts.get(id); return r ? clone(r) : null },
|
|
569
724
|
async tokenByHash (hash) { const r = [...t.tokens.values()].find((x) => x.hash === hash); return r ? clone(r) : null },
|
|
570
725
|
async signingKeys (member) { return [...t.signingKeys.values()].filter((r) => r.member === member).sort((a, b) => a.added_at - b.added_at).map(clone) },
|
|
571
726
|
async signaturesOf (q, commits) { return commits.flatMap((c) => t.signatures.get(k(q, c)) || []).map(clone) },
|
|
572
727
|
async reviews (q, sq) { return (t.reviews.get(q) || []).filter((r) => r.square_id === sq).map(clone) },
|
|
573
728
|
async squareChecks (q, sq) { return t.squares.get(sq)?.quilt_id === q ? [...(t.squareChecks.get(sq)?.values() || [])].map(clone) : [] },
|
|
729
|
+
async checkRuns (q, { state, name, tier, line, square_id: sq, limit = 100 } = {}) {
|
|
730
|
+
const all = [...(t.checkRuns.get(q)?.values() || [])].filter((r) => (!state || r.state === state) && (!name || r.name === name) && (!tier || r.tier === tier) && (!line || r.line === line) && (!sq || r.square_id === sq))
|
|
731
|
+
return all.reverse().slice(0, limit).map(clone)
|
|
732
|
+
},
|
|
574
733
|
async lineRules (q) { return [...(t.lineRules.get(q)?.values() || [])].map(clone) },
|
|
575
734
|
async claims (q) { return [...(t.claims.get(q)?.values() || [])].map(clone) },
|
|
576
735
|
async getTag (q, name) { return clone(t.tags.get(k(q, name))) },
|
|
@@ -677,6 +836,8 @@ export function createMemoryQuiltStore ({ file = null } = {}) {
|
|
|
677
836
|
async gitShas (q, shas) { const out = {}; for (const sha of shas) { const id = t.gitShas.get(k(q, sha)); if (id) out[sha] = id } return out },
|
|
678
837
|
async gitShaOf (q, id) { return t.gitOf.get(k(q, id)) || null },
|
|
679
838
|
async getBlock (id) { return clone(t.blocks.get(id)) },
|
|
839
|
+
/** Every import still following git: [{ quilt_id, import }]. */
|
|
840
|
+
async importsFollowing () { const out = []; for (const row of t.quilts.values()) if (row.settings?.import?.state === 'following') out.push({ quilt_id: row.id, import: clone(row.settings.import) }); return out },
|
|
680
841
|
close () { if (journal) { fs.closeSync(journal); journal = null } }
|
|
681
842
|
})
|
|
682
843
|
return store
|
package/src/quilts/store-rpc.js
CHANGED
|
@@ -22,7 +22,7 @@ export function createRpcQuiltStore (rpc) {
|
|
|
22
22
|
createQuilt: 'quilt_create_quilt', setMember: 'quilt_set_member', sealPack: 'quilt_seal_pack', stageFiles: 'quilt_stage_files',
|
|
23
23
|
openSquare: 'quilt_open_square', commit: 'quilt_commit', beginMerge: 'quilt_begin_merge', endMerge: 'quilt_end_merge', merge: 'quilt_merge',
|
|
24
24
|
importCommit: 'quilt_import_commit', setSnapshot: 'quilt_set_snapshot', setSquare: 'quilt_set_square', setGitSha: 'quilt_set_git_sha', remember: 'quilt_remember',
|
|
25
|
-
addEvents: 'quilt_add_events', setWebhook: 'quilt_set_webhook', takeLease: 'quilt_take_lease', openAndCommit: 'quilt_open_and_commit', giveLease: 'quilt_give_lease', addStubs: 'quilt_add_stubs', fillLinePaths: 'quilt_fill_line_paths', setConflicted: 'quilt_set_conflicted', setArchive: 'quilt_set_archive', setLineWindow: 'quilt_set_line_window', createLine: 'quilt_create_line', setLineFrozen: 'quilt_set_line_frozen', createTag: 'quilt_create_tag', setLive: 'quilt_set_live', createClaim: 'quilt_create_claim', deleteClaim: 'quilt_delete_claim', setResolution: 'quilt_set_resolution', deleteResolution: 'quilt_delete_resolution', addReview: 'quilt_add_review', setLineRule: 'quilt_set_line_rule', setSquareCheck: 'quilt_set_square_check', addSigningKey: 'quilt_add_signing_key', removeSigningKey: 'quilt_remove_signing_key', addSignature: 'quilt_add_signature', createToken: 'quilt_create_token', revokeToken: 'quilt_revoke_token'
|
|
25
|
+
addEvents: 'quilt_add_events', setWebhook: 'quilt_set_webhook', takeLease: 'quilt_take_lease', openAndCommit: 'quilt_open_and_commit', giveLease: 'quilt_give_lease', addStubs: 'quilt_add_stubs', fillLinePaths: 'quilt_fill_line_paths', setConflicted: 'quilt_set_conflicted', setArchive: 'quilt_set_archive', setLineWindow: 'quilt_set_line_window', createLine: 'quilt_create_line', setLineFrozen: 'quilt_set_line_frozen', createTag: 'quilt_create_tag', setLive: 'quilt_set_live', createClaim: 'quilt_create_claim', deleteClaim: 'quilt_delete_claim', setResolution: 'quilt_set_resolution', deleteResolution: 'quilt_delete_resolution', addReview: 'quilt_add_review', setLineRule: 'quilt_set_line_rule', setSquareCheck: 'quilt_set_square_check', addSigningKey: 'quilt_add_signing_key', removeSigningKey: 'quilt_remove_signing_key', addSignature: 'quilt_add_signature', createToken: 'quilt_create_token', revokeToken: 'quilt_revoke_token', setImport: 'quilt_set_import', setExport: 'quilt_set_export', addCheckRuns: 'quilt_add_check_runs', setCheckRun: 'quilt_set_check_run', requestCheckRun: 'quilt_request_check_run', accountFor: 'quilt_account_for', addKey: 'quilt_add_key', revokeKey: 'quilt_revoke_key', createAgent: 'quilt_create_agent', revokeAgent: 'quilt_revoke_agent', addDeviceLink: 'quilt_add_device_link', setDeviceLink: 'quilt_set_device_link', addMemberClaims: 'quilt_add_member_claims', addOAuthClient: 'quilt_add_oauth_client', revokeOAuthClient: 'quilt_revoke_oauth_client', addOAuthCode: 'quilt_add_oauth_code', takeOAuthCode: 'quilt_take_oauth_code', revokeGrant: 'quilt_revoke_grant'
|
|
26
26
|
}
|
|
27
27
|
const store = {}
|
|
28
28
|
for (const [name, fn] of Object.entries(writes)) store[name] = (args) => call(fn, args)
|
|
@@ -40,12 +40,22 @@ export function createRpcQuiltStore (rpc) {
|
|
|
40
40
|
claims: (q) => call('quilt_claims_of', { quilt_id: q }),
|
|
41
41
|
tokens: (q) => call('quilt_tokens_of', { quilt_id: q }),
|
|
42
42
|
tokenByHash: (hash) => call('quilt_token_by_hash', { hash }),
|
|
43
|
+
oauthClient: (id) => call('quilt_oauth_client', { id }),
|
|
44
|
+
oauthClientsOf: (accountId) => call('quilt_oauth_clients_of', { account_id: accountId }),
|
|
45
|
+
getAccount: (id) => call('quilt_get_account', { id }),
|
|
46
|
+
keyByHash: (hash) => call('quilt_key_by_hash', { hash }),
|
|
47
|
+
deviceKey: (publicKey) => call('quilt_device_key', { public_key: publicKey }),
|
|
48
|
+
getAgent: (id) => call('quilt_get_agent', { id }),
|
|
49
|
+
agentClaims: (email) => call('quilt_agent_claims', { email }),
|
|
50
|
+
agentsOf: (accountId) => call('quilt_agents_of', { account_id: accountId }),
|
|
51
|
+
deviceLink: (by) => call('quilt_device_link', by),
|
|
43
52
|
signingKey: (id) => call('quilt_signing_key', { id }),
|
|
44
53
|
signingKeys: (member) => call('quilt_signing_keys_of', { member }),
|
|
45
54
|
signaturesOf: (q, commits) => call('quilt_signatures_of', { quilt_id: q, commits }),
|
|
46
55
|
reviews: (q, sq) => call('quilt_reviews_of', { quilt_id: q, square_id: sq }),
|
|
47
56
|
squareChecks: (q, sq) => call('quilt_square_checks_of', { quilt_id: q, square_id: sq }),
|
|
48
57
|
lineRules: (q) => call('quilt_line_rules_of', { quilt_id: q }),
|
|
58
|
+
checkRuns: (q, { state, name, tier, line, square_id: sq, limit = 100 } = {}) => call('quilt_check_runs_of', { quilt_id: q, state, name, tier, line, square_id: sq, limit }),
|
|
49
59
|
resolution: (q, key) => call('quilt_resolution', { quilt_id: q, key }),
|
|
50
60
|
resolutions: (q) => call('quilt_resolutions_of', { quilt_id: q }),
|
|
51
61
|
getSquare: (id) => call('quilt_get_square', { id }),
|
|
@@ -74,6 +84,7 @@ export function createRpcQuiltStore (rpc) {
|
|
|
74
84
|
gitShas: async (q, shas) => (await call('quilt_git_shas', { quilt_id: q, shas })) || {},
|
|
75
85
|
gitShaPrefix: (q, prefix) => call('quilt_git_sha_prefix', { quilt_id: q, prefix }),
|
|
76
86
|
gitShaOf: (q, id) => call('quilt_git_sha_of', { quilt_id: q, commit_id: id }),
|
|
87
|
+
importsFollowing: () => call('quilt_imports_following', {}),
|
|
77
88
|
close () {}
|
|
78
89
|
})
|
|
79
90
|
}
|