@scp-dev/hub 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,1817 @@
1
+ import {
2
+ type Admission,
3
+ ACCOUNT_HANDLE,
4
+ admits,
5
+ type Claim,
6
+ cleanLabels,
7
+ cleanText,
8
+ type CoreOptions,
9
+ effectiveSelector,
10
+ type EndpointKind,
11
+ type EndpointRow,
12
+ type EndpointView,
13
+ type Envelope,
14
+ findSecret,
15
+ GONE_PREFIX,
16
+ groupCapKey,
17
+ HUMAN_HANDLE,
18
+ LIMITS,
19
+ MESSAGE_KINDS,
20
+ type MessageRow,
21
+ newId,
22
+ one,
23
+ type PolicySubject,
24
+ type Presence,
25
+ randomHex,
26
+ RelayCore,
27
+ RelayError,
28
+ type ScopeCap,
29
+ type ScopeCaps,
30
+ type ScopeDirection,
31
+ type ScopeEntry,
32
+ type ScopeNode,
33
+ type ScopeRules,
34
+ type ScopeSubject,
35
+ type SendParams,
36
+ type SendResult,
37
+ STATUS_STATES,
38
+ type SqlValue,
39
+ type StatusState,
40
+ SYSTEM_HANDLE,
41
+ seqList,
42
+ unreachable,
43
+ validHandle,
44
+ validHarness,
45
+ validSelector,
46
+ } from "@scp-dev/core"
47
+
48
+ export type RelayAuth = Envelope["from"]["auth"]
49
+
50
+ /** a machine name: one relay host of an account */
51
+ export const MACHINE_NAME = /^[a-z0-9][a-z0-9-]{0,31}$/
52
+
53
+ const REMOTE = /^[a-z0-9][a-z0-9._-]{0,63}\/([a-z0-9][a-z0-9._-]{0,62})?$/
54
+ const MESSAGE_ID = /^[a-z]{2,8}_[a-z0-9]{8,64}$/
55
+ const AUTHS: readonly RelayAuth[] = ["local-uid", "account", "account+bridge", "contact"]
56
+ const PRESENCES: readonly Presence[] = ["online", "idle", "offline"]
57
+
58
+ export interface FederatedOptions extends CoreOptions {
59
+ /** this host's own machine name; addresses naming it stay local */
60
+ machine?: string
61
+ /** other names of the local user, such as the account handle once known */
62
+ aliases?: string[]
63
+ /** `from.auth` of senders whose row records none (default local-uid) */
64
+ auth?: RelayAuth
65
+ /** at most this many live endpoints, mirrored and claimed together */
66
+ endpointCap?: number
67
+ /** at most this many refusal audit rows a minute for each refusal code; the next one written counts those left out */
68
+ refusalsPerMinute?: number
69
+ /** the hosted relay's cross-account tables: messages from other accounts in xinbox, in one seq space with deliveries */
70
+ xinbox?: boolean
71
+ }
72
+
73
+ interface FederatedRow extends EndpointRow {
74
+ remote: string | null
75
+ auth: RelayAuth | null
76
+ incarnation: string | null
77
+ /** this machine's own agent: moves on when its caps loosen, so the incarnation it mirrors changes */
78
+ scope_epoch: number | null
79
+ /** this machine's own agent: the wire caps it mirrored last */
80
+ mirrored_caps: string | null
81
+ }
82
+
83
+ /** own rules as they cross machines: selector lists per direction */
84
+ export interface MirrorRules {
85
+ in?: string[]
86
+ out?: string[]
87
+ }
88
+
89
+ /** caps and allowances as they cross machines: `by` and `via` name an endpoint of the same machine as `ep:<handle>`, else human, policy, inherited or gone */
90
+ export interface MirrorCaps {
91
+ caps: ScopeCap[]
92
+ allow?: Record<string, ScopeEntry[]>
93
+ }
94
+
95
+ /** an agent endpoint's place in its machine's tree, its rules and its caps; a field left out is unknown, or unchanged once known */
96
+ export interface MirrorScope {
97
+ /** the root's handle on the same machine (a root's own) */
98
+ tree?: string | null
99
+ /** the parent's handle on the same machine; null for a root */
100
+ parent_ep?: string | null
101
+ /** 0 for a root; null when not known */
102
+ depth?: number | null
103
+ rules?: MirrorRules | null
104
+ caps?: MirrorCaps | null
105
+ /**
106
+ * changes when its machine released the endpoint and another took the handle, a new spawn took it over, or its caps loosened: the caps
107
+ * seen before lapse. The part before the first "." names the holder: a change there also drops what a hub queued for the endpoint
108
+ */
109
+ incarnation?: string | null
110
+ }
111
+
112
+ /** an endpoint as another machine reports it: EndpointView-like, only the address's last segment is used as the handle */
113
+ export interface MirrorView extends MirrorScope {
114
+ address: string
115
+ kind: EndpointKind
116
+ harness?: string | null
117
+ model?: string | null
118
+ cwd?: string | null
119
+ labels?: Record<string, string>
120
+ status?: { state: StatusState; note: string } | null
121
+ presence?: Presence
122
+ last_seen?: string | null
123
+ title?: string | null
124
+ }
125
+
126
+ export interface RemoteView extends Omit<EndpointView, "depth">, MirrorScope {
127
+ machine: string
128
+ }
129
+
130
+ interface Placement {
131
+ tree: string | null
132
+ parent_ep: string | null
133
+ depth: number | null
134
+ }
135
+
136
+ interface ParsedScope {
137
+ tree?: string | null
138
+ parent?: string | null
139
+ depth?: number | null
140
+ rules?: MirrorRules | null
141
+ caps?: ScopeCaps | null
142
+ title?: string | null
143
+ incarnation?: string | null
144
+ }
145
+
146
+ type ParsedView = {
147
+ handle: string
148
+ kind: EndpointKind
149
+ harness: string | null
150
+ model: string | null
151
+ cwd: string | null
152
+ labels: string
153
+ status: { state: StatusState; note: string } | null
154
+ presence: Presence
155
+ lastSeen: number | null
156
+ scope: ParsedScope
157
+ }
158
+
159
+ const UNKNOWN: Placement = { tree: null, parent_ep: null, depth: null }
160
+ const REFS = new Set(["human", "policy", "inherited", "gone"])
161
+ const INCARNATION = /^[A-Za-z0-9._:-]{1,128}$/
162
+ const SCOPE_BYTES = 16 * 1024
163
+
164
+ const XACCT_MIGRATION = [
165
+ `CREATE TABLE IF NOT EXISTS xinbox (seq INTEGER PRIMARY KEY, id TEXT NOT NULL, from_account TEXT NOT NULL, client_id TEXT NOT NULL, endpoint TEXT NOT NULL, sender TEXT NOT NULL, message TEXT NOT NULL, thread TEXT NOT NULL, grant TEXT, expires_at INTEGER NOT NULL, at INTEGER NOT NULL, UNIQUE (from_account, client_id))`,
166
+ `CREATE TABLE IF NOT EXISTS xcursor (endpoint TEXT PRIMARY KEY, acked_seq INTEGER NOT NULL, at INTEGER, old_seq INTEGER, old_at INTEGER) WITHOUT ROWID`,
167
+ `CREATE TABLE IF NOT EXISTS xstate (key TEXT PRIMARY KEY, value TEXT) WITHOUT ROWID`,
168
+ `CREATE TABLE IF NOT EXISTS xcounters (contact TEXT PRIMARY KEY, day INTEGER, sent INTEGER, at INTEGER) WITHOUT ROWID`,
169
+ ]
170
+
171
+ /**
172
+ * how long an xinbox row outlives its receipt, and an acked one its ack (24 to about 48 h, by the cursor's checkpoint): the reply
173
+ * grant, the dedupe of a retry and the recipient's rate counts read it
174
+ */
175
+ const XINBOX_KEEP_MS = 86_400_000
176
+
177
+ /** the highest seq handed out so far: deliveries' and xinbox's, kept past purges by the acked cursors and the sweep's floor */
178
+ const SEQ_TOP = `MAX(COALESCE((SELECT seq FROM sqlite_sequence WHERE name = 'deliveries'), 0), COALESCE((SELECT MAX(seq) FROM xinbox), 0),
179
+ COALESCE((SELECT MAX(acked_seq) FROM xcursor), 0), COALESCE((SELECT CAST(value AS INTEGER) FROM xstate WHERE key = 'seq_floor'), 0))`
180
+
181
+ /** a row of xinbox: a message from another account, its envelope as JSON */
182
+ export interface XInboxRow {
183
+ seq: number
184
+ id: string
185
+ from_account: string
186
+ client_id: string
187
+ endpoint: string
188
+ /** the sender's address in its account */
189
+ sender: string
190
+ message: string
191
+ thread: string
192
+ grant: string | null
193
+ expires_at: number
194
+ /** when it was received */
195
+ at: number
196
+ }
197
+
198
+ /** a mirrored endpoint of this account as the cross-account checks see it */
199
+ export interface XEndpoint {
200
+ id: string
201
+ /** `<machine>/<handle>`, handle empty for a machine's human */
202
+ remote: string
203
+ machine: string
204
+ handle: string
205
+ kind: EndpointKind
206
+ depth: number | null
207
+ harness: string | null
208
+ presence: Presence
209
+ }
210
+
211
+ export interface MirrorChange {
212
+ view: RemoteView
213
+ added: boolean
214
+ presence: boolean
215
+ status: boolean
216
+ }
217
+
218
+ export interface Delivery {
219
+ seq: number
220
+ /** `<machine>/<handle>` of the recipient, or null for a row of no machine */
221
+ endpoint: string | null
222
+ /** the recipient's endpoint id */
223
+ recipient: string
224
+ /** whether the recipient is a row its machine's bridge mirrors, not one a direct client claimed */
225
+ mirrored: boolean
226
+ message: Envelope
227
+ }
228
+
229
+ const machinePart = (remote: string): string => remote.slice(0, remote.indexOf("/"))
230
+ /** a stand-in for another account's sender, `@<account>/...`: never a machine of this account */
231
+ const isForeign = (remote: string): boolean => remote.startsWith("@")
232
+ const foreignOf = (remote: string): string => remote.slice(1, remote.indexOf("/"))
233
+
234
+ /** an endpoint of another account as the scope check sees it: it matches only `acct:` selectors */
235
+ export function foreignNode(peer: { account: string; machine: string; handle: string }): ScopeNode {
236
+ const address = `${peer.account}/${peer.machine}/${peer.handle || "me"}`
237
+ return {
238
+ id: `@${address}`,
239
+ address,
240
+ subject: { handle: peer.handle, harness: null, kind: peer.handle === "" ? "human" : "agent", labels: {}, account: peer.account },
241
+ rules: {},
242
+ caps: { caps: [], allow: {} },
243
+ groups: [],
244
+ policy: [],
245
+ }
246
+ }
247
+ const handlePart = (remote: string): string => remote.slice(remote.indexOf("/") + 1)
248
+
249
+ /** RelayCore with endpoints of other machines: qualified addresses, mirrored and proxy rows, injection and forwarding records */
250
+ export class FederatedCore extends RelayCore {
251
+ readonly machine: string | null
252
+ private aliases: Set<string>
253
+ private scope: string | null = null
254
+ private readonly defaultAuth: RelayAuth
255
+ private readonly endpointCap: number | null
256
+ private readonly refusalsPerMinute: number | null
257
+ private readonly refusals = new Map<string, { minute: number; written: number; suppressed: number }>()
258
+ private readonly xinbox: boolean
259
+ private xfrom: Set<string> | null = null
260
+
261
+ constructor(sql: ConstructorParameters<typeof RelayCore>[0], options: FederatedOptions) {
262
+ super(sql, options)
263
+ this.machine = options.machine ?? null
264
+ this.aliases = new Set(options.aliases ?? [])
265
+ this.defaultAuth = options.auth ?? "local-uid"
266
+ this.endpointCap = options.endpointCap ?? null
267
+ this.refusalsPerMinute = options.refusalsPerMinute ?? null
268
+ this.xinbox = options.xinbox === true
269
+ }
270
+
271
+ override migrate(): void {
272
+ super.migrate()
273
+ const columns = this.sql.all<{ name: string }>(`PRAGMA table_info(endpoints)`).map((c) => c.name)
274
+ if (!columns.includes("remote")) this.sql.all(`ALTER TABLE endpoints ADD COLUMN remote TEXT`)
275
+ if (!columns.includes("auth")) this.sql.all(`ALTER TABLE endpoints ADD COLUMN auth TEXT`)
276
+ if (!columns.includes("incarnation")) this.sql.all(`ALTER TABLE endpoints ADD COLUMN incarnation TEXT`)
277
+ if (!columns.includes("scope_epoch")) this.sql.all(`ALTER TABLE endpoints ADD COLUMN scope_epoch INTEGER`)
278
+ if (!columns.includes("mirrored_caps")) this.sql.all(`ALTER TABLE endpoints ADD COLUMN mirrored_caps TEXT`)
279
+ this.sql.all(`CREATE UNIQUE INDEX IF NOT EXISTS endpoints_remote ON endpoints (remote) WHERE remote IS NOT NULL`)
280
+ if (this.xinbox) for (const statement of XACCT_MIGRATION) this.sql.all(statement)
281
+ }
282
+
283
+ /** with xinbox, a delivery takes the next seq past xinbox's too, so a bridge's replay and ack cursor stay one space */
284
+ protected override insertDelivery(messageId: string, endpoint: string, expiresAt: number): number {
285
+ if (!this.xinbox) return super.insertDelivery(messageId, endpoint, expiresAt)
286
+ return one<{ seq: number }>(
287
+ this.sql,
288
+ `INSERT INTO deliveries (seq, message_id, endpoint, expires_at) VALUES (1 + ${SEQ_TOP}, ?, ?, ?) RETURNING seq`,
289
+ messageId,
290
+ endpoint,
291
+ expiresAt,
292
+ )!.seq
293
+ }
294
+
295
+ setAliases(aliases: string[]): void {
296
+ this.aliases = new Set(aliases)
297
+ }
298
+
299
+ /** runs `fn` with unqualified handles resolving on `machine`: a direct client's own namespace on the hub */
300
+ within<T>(machine: string | null, fn: () => T): T {
301
+ const previous = this.scope
302
+ this.scope = machine
303
+ try {
304
+ return fn()
305
+ } finally {
306
+ this.scope = previous
307
+ }
308
+ }
309
+
310
+ // addresses
311
+
312
+ /** the hub-side address (`<machine>/<handle>`) an address names, another account's as it is, or null when it is on this machine */
313
+ route(address: unknown): string | null {
314
+ if (this.foreignAccount(address)) return String(address).trim()
315
+ const { machine, handle } = this.locate(address)
316
+ return machine === null ? null : `${machine}/${handle}`
317
+ }
318
+
319
+ /** whether an address names an endpoint on another machine or of another account; malformed addresses are not remote */
320
+ isRemote(address: unknown): boolean {
321
+ try {
322
+ return this.route(address) !== null
323
+ } catch {
324
+ return false
325
+ }
326
+ }
327
+
328
+ /** the local endpoint id a hub delivery for `<machine>/<handle>` is for */
329
+ localTarget(target: unknown): string {
330
+ if (typeof target !== "string" || !REMOTE.test(target) || machinePart(target) !== this.machine)
331
+ throw new RelayError("not_found", `no endpoint ${String(target)} on this machine`)
332
+ const handle = handlePart(target)
333
+ const row = handle === "" ? this.byHandle(HUMAN_HANDLE) : validHandle(handle) ? this.byHandle(handle) : undefined
334
+ if (!row) throw new RelayError("not_found", `no endpoint ${target} on this machine`)
335
+ return row.id
336
+ }
337
+
338
+ /** the live row of `<machine>/<handle>`, for a bridge acting on its own machine's endpoints */
339
+ remoteEndpoint(machine: string, handle: unknown): string {
340
+ if (typeof handle !== "string" || handle.includes("/")) throw new RelayError("invalid_params", "from is a handle on the bridge's own machine")
341
+ const row = this.byRemote(`${machine}/${handle}`)
342
+ if (!row) throw new RelayError("not_found", `no endpoint ${machine}/${handle}`)
343
+ return row.id
344
+ }
345
+
346
+ /** the machine an endpoint row belongs to, or null for a row of this host */
347
+ machineOf(endpoint: string): string | null {
348
+ const row = this.byId(endpoint) as FederatedRow | undefined
349
+ return row?.remote ? machinePart(row.remote) : null
350
+ }
351
+
352
+ /** `<account>/<machine>/<handle>` of another account (handle empty for its machine's human, `me`), or null for any other address */
353
+ foreignAccount(address: unknown): { account: string; machine: string; handle: string } | null {
354
+ if (typeof address !== "string") return null
355
+ const parts = address.trim().split("/")
356
+ if (parts.length !== 3) return null
357
+ const [account, machine, handle] = parts as [string, string, string]
358
+ if (this.isLocalUser(account) || !ACCOUNT_HANDLE.test(account) || !MACHINE_NAME.test(machine)) return null
359
+ if (handle !== "me" && handle !== "" && !validHandle(handle)) return null
360
+ return { account, machine, handle: handle === "me" ? "" : handle }
361
+ }
362
+
363
+ private isLocalUser(part: string): boolean {
364
+ return part === "me" || part === this.user || this.aliases.has(part)
365
+ }
366
+
367
+ /** whether an endpoint of another account `part` ever reached this relay, so `<part>/me` is that account's human */
368
+ private knownAccount(part: string): boolean {
369
+ return (
370
+ !this.isLocalUser(part) &&
371
+ ACCOUNT_HANDLE.test(part) &&
372
+ one(this.sql, `SELECT 1 FROM endpoints WHERE substr(remote, 1, ?) = ? LIMIT 1`, part.length + 2, `@${part}/`) !== undefined
373
+ )
374
+ }
375
+
376
+ private locate(address: unknown): { machine: string | null; handle: string } {
377
+ if (typeof address !== "string" || !address.trim()) throw new RelayError("invalid_params", "an address is machine/handle, user/handle, or a handle")
378
+ const text = address.trim()
379
+ const parts = text.split("/")
380
+ let machine: string | null
381
+ let handle: string
382
+ if (parts.length === 1) [machine, handle] = [null, parts[0]!]
383
+ else if (parts.length === 2 && parts[1] === "me" && this.knownAccount(parts[0]!))
384
+ throw new RelayError(
385
+ "invalid_params",
386
+ `${text} is account ${parts[0]}'s human, which takes no messages yet; message one of its endpoints (${parts[0]}/<machine>/<handle>)`,
387
+ )
388
+ else if (parts.length === 2) [machine, handle] = [this.isLocalUser(parts[0]!) ? null : parts[0]!, parts[1]!]
389
+ else if (parts.length === 3 && this.isLocalUser(parts[0]!)) [machine, handle] = [parts[1]!, parts[2]!]
390
+ else throw unreachable(text)
391
+ if (machine !== null && handle === "me") handle = ""
392
+ if (machine === null && this.scope !== null && handle !== "") machine = this.scope
393
+ if (machine !== null && machine === this.machine) machine = null
394
+ return { machine, handle }
395
+ }
396
+
397
+ protected override resolve(address: unknown): EndpointRow {
398
+ const { machine, handle } = this.locate(address)
399
+ const text = String(address).trim()
400
+ if (machine === null) {
401
+ const row = handle === "" ? this.byHandle(HUMAN_HANDLE) : validHandle(handle) ? this.byHandle(handle) : undefined
402
+ if (!row) throw unreachable(text)
403
+ return row
404
+ }
405
+ const row = this.byRemote(`${machine}/${handle}`)
406
+ if (!row) throw unreachable(text)
407
+ return row
408
+ }
409
+
410
+ protected override addressUsers(): string[] {
411
+ return [this.user, "me", ...this.aliases]
412
+ }
413
+
414
+ protected override localRow(row: EndpointRow): boolean {
415
+ return !(row as FederatedRow).remote
416
+ }
417
+
418
+ protected override address(row: EndpointRow): string {
419
+ const remote = (row as FederatedRow).remote
420
+ if (remote && isForeign(remote)) return remote.slice(1)
421
+ return remote ? `${this.user}/${remote}` : super.address(row)
422
+ }
423
+
424
+ protected override subject(row: EndpointRow): PolicySubject {
425
+ const remote = (row as FederatedRow).remote
426
+ if (!remote) return super.subject(row)
427
+ return {
428
+ handle: "",
429
+ harness: row.harness,
430
+ kind: row.kind,
431
+ labels: JSON.parse(row.labels) as Record<string, string>,
432
+ ...(isForeign(remote) ? { account: foreignOf(remote) } : {}),
433
+ }
434
+ }
435
+
436
+ /** a stand-in for another account's sender is no endpoint of this relay: list, peers and snapshots leave it out */
437
+ protected override listable(row: EndpointRow): boolean {
438
+ const remote = (row as FederatedRow).remote
439
+ return !remote || !isForeign(remote)
440
+ }
441
+
442
+ /** an address of another account (a sender's stand-in, or a target with none) is checked as that account's endpoint */
443
+ protected override policySubject(address: string): PolicySubject {
444
+ const proxy = typeof address === "string" ? this.byRemote(`@${address.trim()}`) : undefined
445
+ if (proxy) return this.subject(proxy)
446
+ const foreign = this.foreignAccount(address)
447
+ if (foreign) return foreignNode(foreign).subject
448
+ return super.policySubject(address)
449
+ }
450
+
451
+ /**
452
+ * another machine's row sits in no local tree: a fleet of its own on its machine, so a scoped local endpoint reaches it only by a leaving
453
+ * selector; its mirrored ancestors are kept so a cap widened by one of them is clipped as on its machine
454
+ */
455
+ protected override scopeSubject(row: EndpointRow): ScopeSubject {
456
+ const remote = (row as FederatedRow).remote
457
+ if (!remote) return super.scopeSubject(row)
458
+ const ancestors: string[] = []
459
+ for (let id = row.parent_ep; id && id !== row.id && !ancestors.includes(id) && ancestors.length <= LIMITS.depthMax; id = this.byId(id)?.parent_ep ?? null)
460
+ ancestors.push(id)
461
+ return { id: row.id, tree: row.tree ?? row.id, ancestors, groups: [], rootHandle: "", machine: machinePart(remote), local: false }
462
+ }
463
+
464
+ /** another machine's row has the rules and caps its machine mirrored; only the hub's own human (a full login on the hub) is the account's human */
465
+ protected override scopeNode(row: EndpointRow): ScopeNode {
466
+ const remote = (row as FederatedRow).remote
467
+ if (!remote) return super.scopeNode(row)
468
+ if (isForeign(remote))
469
+ return { id: row.id, address: this.address(row), subject: this.subject(row), rules: {}, caps: { caps: [], allow: {} }, groups: [], policy: [] }
470
+ const subject = row.kind === "system" ? this.subject(row) : { ...this.subject(row), scope: this.scopeSubject(row) }
471
+ const human = row.kind === "human" && handlePart(remote) === "" && this.isLocalUser(machinePart(remote))
472
+ const caps = row.caps ? (JSON.parse(row.caps) as ScopeCaps) : { caps: [], allow: {} }
473
+ return {
474
+ id: row.id,
475
+ address: this.address(row),
476
+ subject,
477
+ human,
478
+ rules: row.rules ? (JSON.parse(row.rules) as ScopeRules) : {},
479
+ caps: { caps: caps.caps, allow: caps.allow ?? {} },
480
+ groups: [],
481
+ policy: [],
482
+ }
483
+ }
484
+
485
+ /**
486
+ * throws not_found, as send() does, when the scope refuses a local endpoint's message to an address on another machine, a reply
487
+ * (`replyTo`, its parent) to a message it got from that address passing as a solicited one; to another account, the cross-account rule on
488
+ * the sender's out lists, unless it replies to a message it got from exactly that address
489
+ */
490
+ scopeCheck(from: string, to: unknown, replyTo?: string): void {
491
+ const foreign = this.foreignAccount(to)
492
+ if (!foreign) {
493
+ const parent = replyTo === undefined ? undefined : one<MessageRow>(this.sql, `SELECT * FROM messages WHERE id = ?`, String(replyTo))
494
+ this.checkScope(this.mustEndpoint(from), this.resolve(to), String(to).trim(), parent)
495
+ return
496
+ }
497
+ const sender = this.mustEndpoint(from)
498
+ const text = String(to).trim()
499
+ if (replyTo !== undefined) {
500
+ const parent = one<{ from_ep: string }>(
501
+ this.sql,
502
+ `SELECT m.from_ep AS from_ep FROM messages m JOIN deliveries d ON d.message_id = m.id WHERE m.id = ? AND d.endpoint = ?`,
503
+ replyTo,
504
+ from,
505
+ )
506
+ const origin = parent ? this.byId(parent.from_ep) : undefined
507
+ if (origin && this.address(origin) === text) return
508
+ }
509
+ const admission = this.crossAdmission(from, "out", foreignNode(foreign))
510
+ if (admission.ok) return
511
+ this.audit("scope_refused", from, null, { from: this.address(sender), to: text, dir: "out", reason: admission.why })
512
+ throw new RelayError("not_found", `no endpoint ${text}`)
513
+ }
514
+
515
+ /** whether direction `dir` of `endpoint` admits an endpoint of another account: only an `acct:` selector in each list, or no list at all */
516
+ crossAdmission(endpoint: string, dir: ScopeDirection, peer: ScopeNode | { account: string; machine: string; handle: string }): Admission {
517
+ const world = this.scopeWorld()
518
+ const node = world.node(endpoint)
519
+ if (!node) return { ok: false, why: "the endpoint is gone" }
520
+ return admits(world, node, dir, "id" in peer ? peer : foreignNode(peer))
521
+ }
522
+
523
+ protected override envelope(row: MessageRow, seq?: number, recipient?: string): Envelope {
524
+ const envelope = super.envelope(row, seq, recipient)
525
+ const sender = this.byId(row.from_ep) as FederatedRow | undefined
526
+ if (sender?.remote && isForeign(sender.remote)) {
527
+ const account = foreignOf(sender.remote)
528
+ const parts = sender.remote.slice(1).split("/")
529
+ const { rel: _, ...from } = envelope.from
530
+ envelope.from = {
531
+ ...from,
532
+ user: account,
533
+ auth: "contact",
534
+ account,
535
+ rel: `contact:${account}`,
536
+ ...(parts.length === 3 ? { machine: parts[1]! } : {}),
537
+ ...(sender.kind === "human" ? { human: true as const } : {}),
538
+ }
539
+ return envelope
540
+ }
541
+ const machine = sender?.remote ? machinePart(sender.remote) : null
542
+ const other = machine !== null && !this.isLocalUser(machine)
543
+ const human = other && sender?.kind === "human" && row.sender_tty === 1
544
+ envelope.from = { ...envelope.from, auth: sender?.auth ?? this.defaultAuth, ...(other ? { machine } : {}), ...(human ? { human: true as const } : {}) }
545
+ return envelope
546
+ }
547
+
548
+ // mirrored and claimed rows
549
+
550
+ /**
551
+ * upserts a machine's endpoints from its views, never over a direct client's claimed row; `full` marks its mirrored rows missing from the
552
+ * list offline. An agent's place is worked out here from its mirrored parent, and a cap seen once stays until the endpoint is released
553
+ * (a new `incarnation`). A row is written only when something in it changed. `seen` gathers the remotes of a full mirror sent in several
554
+ * frames: each frame adds its own, and the closing `full` one counts every remote in it as listed.
555
+ */
556
+ mirror(machine: string, views: unknown, options: { full?: boolean; auth?: RelayAuth; seen?: Set<string> } = {}): MirrorChange[] {
557
+ this.checkMachine(machine)
558
+ if (!Array.isArray(views)) throw new RelayError("invalid_params", "endpoints must be a list")
559
+ if (views.length > (this.endpointCap ?? 4096)) throw new RelayError("too_large", `at most ${this.endpointCap ?? 4096} endpoints`)
560
+ const parsed = [...new Map(views.map((view) => this.parseView(view)).map((view) => [view.handle, view])).values()]
561
+ const auth = options.auth ?? null
562
+ return this.tx(() => {
563
+ const now = this.now()
564
+ const changes: MirrorChange[] = []
565
+ const seen = options.seen ?? new Set<string>()
566
+ const batch: { view: ParsedView; id: string; before: FederatedRow | undefined }[] = []
567
+ for (const view of parsed) {
568
+ const remote = `${machine}/${view.handle}`
569
+ seen.add(remote)
570
+ const before = this.byRemote(remote)
571
+ if (before && before.token_hash !== null) continue
572
+ if (!before) this.checkCap()
573
+ batch.push({ view, id: before?.id ?? this.insertRemote(remote, view.kind, view.harness, auth, now), before })
574
+ }
575
+ const place = this.placer(machine, batch)
576
+ for (const { view, id, before } of batch) {
577
+ const row = before ?? (this.byId(id) as FederatedRow)
578
+ if (before && view.kind === "agent") this.dropEarlierHolder(before, view.scope.incarnation)
579
+ const live = view.presence !== "offline"
580
+ const next: Partial<FederatedRow> = {
581
+ kind: view.kind,
582
+ harness: view.harness,
583
+ model: view.model,
584
+ cwd: view.cwd,
585
+ labels: view.labels,
586
+ status_state: view.status?.state ?? null,
587
+ status_note: view.status?.note ?? null,
588
+ token_hash: null,
589
+ connections: live ? 1 : 0,
590
+ last_seen: view.lastSeen ?? (live ? now : (before?.last_seen ?? null)),
591
+ auth: auth ?? row.auth,
592
+ ...(view.kind === "agent" ? this.scopeColumns(machine, view.scope, row, place(view.handle)) : {}),
593
+ }
594
+ const columns = Object.entries(next).filter(([column, value]) => row[column as keyof FederatedRow] !== value)
595
+ if (!columns.length) continue
596
+ this.sql.all(
597
+ `UPDATE endpoints SET ${columns.map(([column]) => `${column} = ?`).join(", ")} WHERE id = ?`,
598
+ ...columns.map(([, value]) => value as SqlValue),
599
+ id,
600
+ )
601
+ const change = this.diff(before, this.byId(id)!)
602
+ if (change) changes.push(change)
603
+ }
604
+ if (options.full) {
605
+ const missing = this.sql.all<FederatedRow>(
606
+ `SELECT * FROM endpoints WHERE substr(remote, 1, ?) = ? AND token_hash IS NULL AND connections > 0`,
607
+ machine.length + 1,
608
+ `${machine}/`,
609
+ )
610
+ for (const row of missing.filter((r) => !seen.has(r.remote!))) {
611
+ this.sql.all(`UPDATE endpoints SET connections = 0, last_seen = ? WHERE id = ?`, now, row.id)
612
+ changes.push({ view: this.remoteView(this.byId(row.id)!), added: false, presence: true, status: false })
613
+ }
614
+ }
615
+ return changes
616
+ })
617
+ }
618
+
619
+ /**
620
+ * a local agent's place, rules and caps as its machine's bridge mirrors them (MirrorScope), with its title; nothing for another row. Caps
621
+ * looser than the ones mirrored last move the endpoint's scope epoch and so its incarnation: a hub keeps the caps it has seen until that changes
622
+ */
623
+ mirrorScope(endpoint: string): MirrorScope & { title?: string | null } {
624
+ const row = this.mustEndpoint(endpoint)
625
+ if (row.kind !== "agent" || !this.localRow(row)) return {}
626
+ const node = this.scopeNode(row)
627
+ const root = row.tree ? this.byId(row.tree) : row
628
+ const parent = row.parent_ep ? this.byId(row.parent_ep) : undefined
629
+ const caps: ScopeCap[] = [...node.caps.caps]
630
+ for (const group of node.groups) {
631
+ for (const dir of ["in", "out"] as const) {
632
+ const rules = group[dir]
633
+ if (!rules) continue
634
+ const list = { source: "", by: group.owner, group: group.name, entries: [] }
635
+ const sels = [...rules, ...(node.caps.allow[groupCapKey(group.name, dir)] ?? [])].map((entry) => ({ ...entry, sel: effectiveSelector(list, entry) }))
636
+ caps.push({ id: groupCapKey(group.name, dir), dir, by: group.owner, sels })
637
+ }
638
+ }
639
+ for (const cap of node.policy) caps.push({ id: `policy:${cap.match}:${cap.dir}`, dir: cap.dir, by: "policy", sels: cap.sels })
640
+ const allow = Object.fromEntries(Object.entries(node.caps.allow).filter(([key]) => key.startsWith("inherited:")))
641
+ const rules: MirrorRules = { ...(node.rules.in ? { in: node.rules.in } : {}), ...(node.rules.out ? { out: node.rules.out } : {}) }
642
+ const wire = caps.length || Object.keys(allow).length ? this.wireCaps({ caps, allow }) : null
643
+ const epoch = this.scopeEpoch(row as FederatedRow, wire)
644
+ return {
645
+ tree: root?.handle ?? null,
646
+ parent_ep: parent?.handle ?? null,
647
+ depth: row.depth ?? 0,
648
+ rules: Object.keys(rules).length ? rules : null,
649
+ caps: wire,
650
+ title: row.title ?? null,
651
+ incarnation: `${this.hash(row.spawned_at ? `${row.id}@${row.spawned_at}` : row.id).slice(0, 24)}.${epoch}`,
652
+ }
653
+ }
654
+
655
+ private scopeEpoch(row: FederatedRow, caps: MirrorCaps | null): number {
656
+ const epoch = row.scope_epoch ?? 0
657
+ const text = caps ? JSON.stringify(caps) : null
658
+ if (text === row.mirrored_caps) return epoch
659
+ const seen = row.mirrored_caps ? (JSON.parse(row.mirrored_caps) as MirrorCaps) : null
660
+ const next = seen && loosens(seen, caps ?? { caps: [] }) ? epoch + 1 : epoch
661
+ this.sql.all(`UPDATE endpoints SET scope_epoch = ?, mirrored_caps = ? WHERE id = ?`, next || null, text, row.id)
662
+ return next
663
+ }
664
+
665
+ /**
666
+ * a direct client's claim of `<machine>/<handle>`, with claim()'s takeover and suffix rules, and its `exact`, `scope`, `allow`, `in` and
667
+ * `out`: a new row takes the claim's own rules, a taken-over one keeps its own. An exact claim is never suffixed: it joins a live claimed
668
+ * row of the same harness no process or session holds, with no new token, and is refused as claim() refuses one otherwise
669
+ */
670
+ claimOn(machine: string, claim: Claim, auth: RelayAuth = "account"): { endpoint: EndpointView; token?: string } {
671
+ this.checkMachine(machine)
672
+ const harness = cleanText(claim.harness, "harness", 32) ?? "cli"
673
+ if (!validHarness(harness)) throw new RelayError("invalid_params", `harness ${JSON.stringify(harness)} is not a valid name`)
674
+ const wanted = cleanText(claim.handle, "handle", 63) ?? harness
675
+ if (!validHandle(wanted)) throw new RelayError("invalid_params", `handle ${JSON.stringify(wanted)} is not valid: lowercase letters, digits, . _ -`)
676
+ const fields = {
677
+ model: cleanText(claim.model, "model", 128),
678
+ cwd: cleanText(claim.cwd, "cwd", 1024),
679
+ parent: cleanText(claim.parent, "parent", 128),
680
+ labels: JSON.stringify(cleanLabels(claim.labels)),
681
+ }
682
+ const leaked = [wanted, fields.model, fields.cwd, fields.parent, fields.labels].map((v) => (v ? findSecret(v) : null)).find(Boolean)
683
+ if (leaked) throw new RelayError("secret_refused", `the claim looks like it carries a credential (${leaked})`)
684
+ const rules = this.claimRules(claim)
685
+ const exact = claim.exact === true
686
+ return this.tx(() => {
687
+ const now = this.now()
688
+ let row: FederatedRow | undefined
689
+ let created = false
690
+ let joined = false
691
+ for (let n = 1; n < (exact ? 2 : 1000) && !row; n++) {
692
+ const handle = n === 1 ? wanted : `${wanted.slice(0, 58)}-${n}`
693
+ const held = this.byRemote(`${machine}/${handle}`)
694
+ if (!held) {
695
+ this.checkCap()
696
+ row = this.byId(this.insertRemote(`${machine}/${handle}`, "agent", harness, auth, now)) as FederatedRow
697
+ created = true
698
+ } else if (held.kind === "agent" && held.connections === 0 && held.harness === harness) {
699
+ row = held
700
+ } else if (
701
+ exact &&
702
+ held.kind === "agent" &&
703
+ held.token_hash !== null &&
704
+ held.process === null &&
705
+ held.session === null &&
706
+ held.parent_ep === null &&
707
+ held.harness === harness
708
+ ) {
709
+ row = held
710
+ joined = true
711
+ } else if (held.kind === "agent" && held.connections === 0 && (held.last_seen ?? held.created_at) < now - LIMITS.handleBoundS * 1000) {
712
+ this.retire(held.id)
713
+ n--
714
+ }
715
+ }
716
+ if (!row) {
717
+ if (exact) throw this.exactRefusal(this.byRemote(`${machine}/${wanted}`)!, wanted, harness, now)
718
+ throw new RelayError("invalid_params", `no free handle for ${wanted} on ${machine}`)
719
+ }
720
+ if (created) {
721
+ const own = this.boundRules(rules)
722
+ if (Object.keys(own).length) this.sql.all(`UPDATE endpoints SET rules = ? WHERE id = ?`, JSON.stringify(own), row.id)
723
+ for (const dir of ["in", "out"] as const) this.needGroups(this.byId(row.id)!, rules[dir])
724
+ }
725
+ const token = joined ? undefined : `kre_${randomHex(32)}`
726
+ this.sql.all(
727
+ `UPDATE endpoints SET harness = ?, model = ?, cwd = ?, parent = ?, labels = ?, token_hash = ${token ? "?" : "token_hash"}, auth = ?, connections = connections + 1, last_seen = ? WHERE id = ?`,
728
+ harness,
729
+ fields.model,
730
+ fields.cwd,
731
+ fields.parent,
732
+ fields.labels,
733
+ ...(token ? [this.hash(token)] : []),
734
+ auth,
735
+ now,
736
+ row.id,
737
+ )
738
+ this.audit("claim", row.id, null, { handle: row.remote, harness, ...(joined ? { joined: true } : {}) })
739
+ return { endpoint: this.view(this.byId(row.id)!), ...(token ? { token } : {}) }
740
+ })
741
+ }
742
+
743
+ /**
744
+ * resume() for a direct client of `machine`, its labels replaced by what `relabel` makes of them, held to the label limits: a token of
745
+ * another machine's endpoint is refused
746
+ */
747
+ resumeOn(machine: string, token: string, relabel?: (labels: Record<string, string>) => unknown): EndpointView {
748
+ const row = typeof token === "string" ? one<FederatedRow>(this.sql, `SELECT * FROM endpoints WHERE token_hash = ?`, this.hash(token)) : undefined
749
+ if (!row?.remote || machinePart(row.remote) !== machine) throw new RelayError("unauthorized", "unknown resume token")
750
+ if (!relabel) return this.resume(token)
751
+ const merged = JSON.stringify(cleanLabels(relabel(JSON.parse(row.labels) as Record<string, string>)))
752
+ return this.tx(() => {
753
+ if (merged !== row.labels) this.sql.all(`UPDATE endpoints SET labels = ? WHERE id = ?`, merged, row.id)
754
+ return this.resume(token)
755
+ })
756
+ }
757
+
758
+ /** every live row of other machines (or of one machine), as views with their machine */
759
+ remoteViews(filter: { except?: string; machine?: string } = {}): RemoteView[] {
760
+ return this.sql
761
+ .all<FederatedRow>(`SELECT * FROM endpoints WHERE remote IS NOT NULL ORDER BY remote`)
762
+ .filter((row) => (!filter.except || machinePart(row.remote!) !== filter.except) && (!filter.machine || machinePart(row.remote!) === filter.machine))
763
+ .map((row) => this.remoteView(row))
764
+ }
765
+
766
+ remoteRowsOf(machine: string): RemoteView[] {
767
+ return this.remoteViews({ machine })
768
+ }
769
+
770
+ /** a bridge went away: its mirrored rows show offline */
771
+ markOffline(machine: string): MirrorChange[] {
772
+ const rows = this.sql.all<{ id: string }>(
773
+ `UPDATE endpoints SET connections = 0, last_seen = ? WHERE substr(remote, 1, ?) = ? AND token_hash IS NULL AND connections > 0 RETURNING id`,
774
+ this.now(),
775
+ machine.length + 1,
776
+ `${machine}/`,
777
+ )
778
+ return rows.map((row) => ({ view: this.remoteView(this.byId(row.id)!), added: false, presence: true, status: false }))
779
+ }
780
+
781
+ /**
782
+ * a released machine: its rows are retired like a gone handle and can't be resolved or resumed again; with `queue`,
783
+ * their unread deliveries go too, and each message they leave delivered to no one, unless it is a request or has
784
+ * replies, which askers still look up
785
+ */
786
+ dropMachine(machine: string, options: { queue?: boolean } = {}): number {
787
+ const rows = this.sql.all<{ id: string }>(
788
+ `UPDATE endpoints SET handle = ? || id, remote = NULL, token_hash = NULL, connections = 0 WHERE substr(remote, 1, ?) = ? RETURNING id`,
789
+ GONE_PREFIX,
790
+ machine.length + 1,
791
+ `${machine}/`,
792
+ )
793
+ if (options.queue && this.xinbox) {
794
+ this.xfloor()
795
+ this.sql.all(`DELETE FROM xinbox WHERE substr(endpoint, 1, ?) = ?`, machine.length + 1, `${machine}/`)
796
+ }
797
+ if (options.queue) {
798
+ for (const { id } of rows) {
799
+ for (const { message_id } of this.sql.all<{ message_id: string }>(
800
+ `DELETE FROM deliveries WHERE endpoint = ? AND acked_at IS NULL RETURNING message_id`,
801
+ id,
802
+ )) {
803
+ this.sql.all(
804
+ `DELETE FROM messages WHERE id = ? AND kind != 'request' AND NOT EXISTS (SELECT 1 FROM deliveries WHERE message_id = ?) AND NOT EXISTS (SELECT 1 FROM messages WHERE reply_to = ?)`,
805
+ message_id,
806
+ message_id,
807
+ message_id,
808
+ )
809
+ }
810
+ }
811
+ }
812
+ return rows.length
813
+ }
814
+
815
+ /**
816
+ * hands a machine's direct-client rows with no connection (or only `endpoint`) to its bridge: their resume tokens are
817
+ * dropped, so the bridge mirrors them, and their unread deliveries get fresh seqs past any bridge's replay cursor;
818
+ * returns those deliveries
819
+ */
820
+ adoptClaimed(machine: string, endpoint?: string): { seq: number; endpoint: string }[] {
821
+ return this.tx(() => {
822
+ const now = this.now()
823
+ const rows = this.sql.all<{ id: string }>(
824
+ `UPDATE endpoints SET token_hash = NULL WHERE substr(remote, 1, ?) = ? AND token_hash IS NOT NULL AND connections = 0 ${endpoint ? "AND id = ?" : ""} RETURNING id`,
825
+ ...[machine.length + 1, `${machine}/`, ...(endpoint ? [endpoint] : [])],
826
+ )
827
+ const adopted: { seq: number; endpoint: string }[] = []
828
+ for (const { id } of rows) {
829
+ const unread = this.sql.all<{ seq: number; message_id: string; expires_at: number }>(
830
+ `DELETE FROM deliveries WHERE endpoint = ? AND acked_at IS NULL AND expires_at > ? RETURNING seq, message_id, expires_at`,
831
+ id,
832
+ now,
833
+ )
834
+ for (const row of unread.sort((a, b) => a.seq - b.seq)) {
835
+ const seq = this.insertDelivery(row.message_id, id, row.expires_at)
836
+ adopted.push({ seq, endpoint: id })
837
+ }
838
+ }
839
+ return adopted
840
+ })
841
+ }
842
+
843
+ // hops
844
+
845
+ /** the hop a message from this endpoint would get from what it received lately */
846
+ causalHop(endpoint: string): number {
847
+ const row = this.mustEndpoint(endpoint)
848
+ return row.last_received_at !== null && row.last_received_at >= this.now() - LIMITS.causalWindowS * 1000 ? (row.last_received_hop ?? 0) + 1 : 0
849
+ }
850
+
851
+ /** records that an endpoint received a message of `hop`, as reading its inbox does */
852
+ markReceived(endpoint: string, hop: number): void {
853
+ const now = this.now()
854
+ this.sql.all(
855
+ `UPDATE endpoints SET last_received_at = ?, last_received_hop = MAX(?, CASE WHEN last_received_at >= ? THEN COALESCE(last_received_hop, 0) ELSE 0 END) WHERE id = ?`,
856
+ now,
857
+ hop,
858
+ now - LIMITS.causalWindowS * 1000,
859
+ endpoint,
860
+ )
861
+ }
862
+
863
+ // messages across machines
864
+
865
+ /**
866
+ * why the local policy refuses a hub delivery to the local `endpoint`, or null; checks the message as admit() does and creates its sender's
867
+ * proxy row; relay notices always pass. A scope refusal throws not_found, as send() does; a redelivery of an admitted message skips it.
868
+ */
869
+ inboundRefusal(endpoint: string, message: Envelope): string | null {
870
+ const recipient = this.mustEndpoint(endpoint)
871
+ this.checkEnvelope(message)
872
+ const sender = this.byId(this.tx(() => this.proxyFor(message.from)))!
873
+ if (sender.kind === "system") return null
874
+ if (!one(this.sql, `SELECT 1 FROM deliveries WHERE message_id = ? AND endpoint = ?`, message.id, endpoint)) {
875
+ const parent =
876
+ message.kind === "reply" && message.reply_to ? one<MessageRow>(this.sql, `SELECT * FROM messages WHERE id = ?`, message.reply_to) : undefined
877
+ const remote = (sender as FederatedRow).remote
878
+ if (remote && isForeign(remote)) this.checkForeignIn(sender, recipient, parent)
879
+ else this.checkScope(sender, recipient, this.address(recipient), parent)
880
+ }
881
+ return this.checkPolicy(this.addressOf(sender.id), this.addressOf(endpoint))
882
+ }
883
+
884
+ /**
885
+ * a message the hub delivered to a local endpoint, stored under the hub's id from a proxy of its sender; idempotent.
886
+ * Throws inbox_full past the inbox cap, which a redelivery, the first reply to the endpoint's own message (as in
887
+ * send()) and relay notices skip.
888
+ */
889
+ admit(endpoint: string, message: Envelope): { seq: number; duplicate: boolean } {
890
+ this.mustEndpoint(endpoint)
891
+ const checked = this.checkEnvelope(message)
892
+ const stored = this.tx((): { seq: number; duplicate: boolean } | { sender: string; unacked: number } => {
893
+ const sender = this.proxyFor(message.from)
894
+ const had = one<{ seq: number }>(this.sql, `SELECT seq FROM deliveries WHERE message_id = ? AND endpoint = ?`, message.id, endpoint)
895
+ if (had) return { seq: had.seq, duplicate: true }
896
+ const unacked = this.inboxFull(endpoint, sender, message)
897
+ if (unacked !== null) return { sender, unacked }
898
+ this.sql.all(
899
+ `INSERT OR IGNORE INTO messages (id, thread, reply_to, kind, from_ep, to_addr, body, mime, data_class, attachments, hop, created_at, expires_at, sender_tty, require_reply)
900
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
901
+ message.id,
902
+ message.thread,
903
+ message.reply_to,
904
+ message.kind,
905
+ sender,
906
+ message.to,
907
+ message.body.text,
908
+ message.body.mime,
909
+ message.data_class,
910
+ JSON.stringify(message.attachments.map((a) => ({ kind: "path", path: a.path }))),
911
+ message.hop,
912
+ checked.createdAt,
913
+ checked.expiresAt,
914
+ message.from.claims.tty === undefined ? null : message.from.claims.tty ? 1 : 0,
915
+ message.require_reply === true && message.kind === "request" ? 1 : null,
916
+ )
917
+ const seq = this.insertDelivery(message.id, endpoint, checked.expiresAt)
918
+ this.audit("inject", endpoint, message.id, { from: message.from.address, hop: message.hop })
919
+ return { seq, duplicate: false }
920
+ })
921
+ if ("unacked" in stored) throw this.refuse(stored.sender, "inbox_full", `${this.addressOf(endpoint)} has ${stored.unacked} unread messages`)
922
+ return stored
923
+ }
924
+
925
+ /** inject() for the hooks of a local endpoint; another machine's row (mirrored, claimed on the hub, or a sender's proxy) has none and gets nothing */
926
+ override inject(endpoint: string, options: Parameters<RelayCore["inject"]>[1] = {}): ReturnType<RelayCore["inject"]> {
927
+ if (this.machineOf(endpoint) !== null) return { messages: [], offered: [], hold: null, more: 0, unread: 0, block: false }
928
+ return super.inject(endpoint, options)
929
+ }
930
+
931
+ /** offer() for a local endpoint's channel; another machine's row is offered nothing */
932
+ override offer(endpoint: string, options: Parameters<RelayCore["offer"]>[1] = {}): ReturnType<RelayCore["offer"]> {
933
+ if (this.machineOf(endpoint) !== null) return { messages: [], hold: null }
934
+ return super.offer(endpoint, options)
935
+ }
936
+
937
+ /** stopHeld() for a local endpoint; another machine's row has no hooks */
938
+ override stopHeld(endpoint: string, options: Parameters<RelayCore["stopHeld"]>[1] = {}): boolean {
939
+ return this.machineOf(endpoint) === null && super.stopHeld(endpoint, options)
940
+ }
941
+
942
+ /** the endpoint's unacked count when a new delivery from `sender` would pass the inbox cap, else null */
943
+ private inboxFull(endpoint: string, sender: string, message: Envelope): number | null {
944
+ if (this.byId(sender)!.kind === "system") return null
945
+ const parent =
946
+ message.kind === "reply" && message.reply_to
947
+ ? one<{ id: string; from_ep: string }>(this.sql, `SELECT id, from_ep FROM messages WHERE id = ?`, message.reply_to)
948
+ : undefined
949
+ const firstReply =
950
+ parent?.from_ep === endpoint &&
951
+ one(this.sql, `SELECT 1 FROM messages WHERE reply_to = ? AND from_ep = ? AND kind = 'reply' AND id != ?`, parent.id, sender, message.id) === undefined
952
+ if (firstReply) return null
953
+ const unacked = one<{ n: number }>(
954
+ this.sql,
955
+ `SELECT COUNT(*) AS n FROM deliveries WHERE endpoint = ? AND acked_at IS NULL AND expires_at > ?`,
956
+ endpoint,
957
+ this.now(),
958
+ )!.n
959
+ return unacked >= LIMITS.inboxMax ? unacked : null
960
+ }
961
+
962
+ /** a send the hub accepted for a local endpoint, stored under the hub's id with no deliveries, so replyTo() and a retried client_id work */
963
+ recordForward(from: string, params: SendParams, result: SendResult, origin: { tty?: boolean } = {}): void {
964
+ const sender = this.mustEndpoint(from)
965
+ const now = this.now()
966
+ const ttl = Number.isInteger(params.ttl_s) ? params.ttl_s! : LIMITS.ttlDefaultS
967
+ const targets = Array.isArray(params.to) ? params.to : [params.to]
968
+ const clientId = cleanText(params.client_id, "client_id", 128)
969
+ const kind = params.kind ?? "message"
970
+ this.tx(() => {
971
+ this.sql.all(
972
+ `INSERT OR IGNORE INTO messages (id, thread, reply_to, kind, from_ep, to_addr, body, mime, data_class, attachments, hop, created_at, expires_at, sender_tty, require_reply)
973
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
974
+ result.id,
975
+ result.thread,
976
+ params.reply_to ?? null,
977
+ kind,
978
+ from,
979
+ targets.map(String).join(","),
980
+ params.body,
981
+ params.mime ?? "text/markdown",
982
+ params.data_class ?? "public",
983
+ JSON.stringify((params.attachments ?? []).map((a) => ({ kind: "path", path: a.path }))),
984
+ result.hop,
985
+ now,
986
+ now + ttl * 1000,
987
+ sender.kind === "human" ? (origin.tty === true ? 1 : 0) : null,
988
+ params.require_reply === true && kind === "request" ? 1 : null,
989
+ )
990
+ if (clientId) {
991
+ const { duplicate: _, ...stored } = result
992
+ this.sql.all(
993
+ `INSERT OR REPLACE INTO idempotency (from_ep, client_id, payload_hash, result, created_at) VALUES (?, ?, ?, ?, ?)`,
994
+ from,
995
+ clientId,
996
+ this.forwardHash(params),
997
+ JSON.stringify(stored),
998
+ now,
999
+ )
1000
+ }
1001
+ if (params.reply_to)
1002
+ this.sql.all(`UPDATE deliveries SET acked_at = ? WHERE message_id = ? AND endpoint = ? AND acked_at IS NULL`, now, params.reply_to, from)
1003
+ this.audit("forward", from, result.id, { to: result.deliveries.map((d) => d.address), kind, hop: result.hop, thread: result.thread })
1004
+ })
1005
+ }
1006
+
1007
+ /** the earlier result of a forwarded send with this client_id, or null; a different message under the same client_id is refused */
1008
+ forwarded(from: string, params: SendParams): SendResult | null {
1009
+ const clientId = cleanText(params.client_id, "client_id", 128)
1010
+ if (!clientId) return null
1011
+ const seen = one<{ result: string; payload_hash: string; created_at: number }>(
1012
+ this.sql,
1013
+ `SELECT result, payload_hash, created_at FROM idempotency WHERE from_ep = ? AND client_id = ?`,
1014
+ from,
1015
+ clientId,
1016
+ )
1017
+ if (!seen || seen.created_at < this.now() - LIMITS.idempotencyS * 1000) return null
1018
+ if (seen.payload_hash !== this.forwardHash(params)) throw new RelayError("invalid_params", `client_id ${clientId} was already used for a different message`)
1019
+ return { ...(JSON.parse(seen.result) as SendResult), duplicate: true }
1020
+ }
1021
+
1022
+ /** one delivery as the hub pushes it, or null when it is gone, acked or expired */
1023
+ delivery(seq: number): Delivery | null {
1024
+ const row = one<MessageRow & { seq: number; recipient: string; remote: string | null; claimed: number }>(
1025
+ this.sql,
1026
+ `SELECT m.*, d.seq AS seq, d.endpoint AS recipient, e.remote AS remote, e.token_hash IS NOT NULL AS claimed FROM deliveries d JOIN messages m ON m.id = d.message_id JOIN endpoints e ON e.id = d.endpoint
1027
+ WHERE d.seq = ? AND d.acked_at IS NULL AND d.expires_at > ?`,
1028
+ seq,
1029
+ this.now(),
1030
+ )
1031
+ if (row)
1032
+ return {
1033
+ seq: row.seq,
1034
+ endpoint: row.remote,
1035
+ recipient: row.recipient,
1036
+ mirrored: row.claimed === 0,
1037
+ message: this.envelope(row, undefined, row.recipient),
1038
+ }
1039
+ return this.xinbox ? (this.xinboxPage(`x.seq = ?`, [seq], 1)[0] ?? null) : null
1040
+ }
1041
+
1042
+ /** the unacked deliveries to `machine`'s mirrored rows after `after` (xinbox rows after `xafter`), oldest first */
1043
+ pending(machine: string, after: number, limit = 200, xafter = after): Delivery[] {
1044
+ const deliveries = this.sql
1045
+ .all<MessageRow & { seq: number; recipient: string; remote: string }>(
1046
+ `SELECT m.*, d.seq AS seq, d.endpoint AS recipient, e.remote AS remote FROM deliveries d JOIN messages m ON m.id = d.message_id JOIN endpoints e ON e.id = d.endpoint
1047
+ WHERE substr(e.remote, 1, ?) = ? AND e.token_hash IS NULL AND d.acked_at IS NULL AND d.expires_at > ? AND d.seq > ? ORDER BY d.seq LIMIT ?`,
1048
+ machine.length + 1,
1049
+ `${machine}/`,
1050
+ this.now(),
1051
+ after,
1052
+ limit,
1053
+ )
1054
+ .map(
1055
+ (row): Delivery => ({
1056
+ seq: row.seq,
1057
+ endpoint: row.remote,
1058
+ recipient: row.recipient,
1059
+ mirrored: true,
1060
+ message: this.envelope(row, undefined, row.recipient),
1061
+ }),
1062
+ )
1063
+ if (!this.xinbox) return deliveries
1064
+ const others = this.xinboxPage(`substr(x.endpoint, 1, ?) = ? AND x.seq > ?`, [machine.length + 1, `${machine}/`, xafter], limit)
1065
+ return others.length ? [...deliveries, ...others].sort((a, b) => a.seq - b.seq).slice(0, limit) : deliveries
1066
+ }
1067
+
1068
+ /**
1069
+ * acks deliveries of `machine`'s mirrored rows only; each counts as received for the causal hop. A seq of xinbox moves its endpoint's
1070
+ * cursor instead: one row per endpoint for the whole batch, no audit
1071
+ */
1072
+ ackOn(machine: string, seqs: unknown): number {
1073
+ let acked = 0
1074
+ const batches = new Map<string, Set<number>>()
1075
+ for (const seq of seqList(seqs)) {
1076
+ const row = this.ownDelivery(machine, seq)
1077
+ if (!row) {
1078
+ const endpoint = this.ownXinbox(machine, seq)
1079
+ if (endpoint !== null) batches.set(endpoint, (batches.get(endpoint) ?? new Set()).add(seq))
1080
+ continue
1081
+ }
1082
+ this.sql.all(`UPDATE deliveries SET acked_at = ? WHERE seq = ?`, this.now(), seq)
1083
+ this.markReceived(row.endpoint, row.hop)
1084
+ acked++
1085
+ }
1086
+ if (acked) this.audit("ack", null, null, { machine, count: acked })
1087
+ for (const [endpoint, batch] of batches) acked += this.advanceCursor(endpoint, batch)
1088
+ return acked
1089
+ }
1090
+
1091
+ /** a delivery `machine`'s local policy refused: acked, and a status notice from the relay to the sender */
1092
+ rejectOn(machine: string, seq: unknown, code: unknown): { notice: { seq: number; endpoint: string } | null } {
1093
+ if (!Number.isInteger(seq)) throw new RelayError("invalid_params", "seq must be an integer")
1094
+ if (typeof code !== "string" || !/^[a-z_]{1,32}$/.test(code)) throw new RelayError("invalid_params", "code is an error code")
1095
+ const row = this.ownDelivery(machine, seq as number)
1096
+ if (!row) {
1097
+ const endpoint = this.ownXinbox(machine, seq as number)
1098
+ if (endpoint === null) throw new RelayError("not_found", `no unacked delivery ${String(seq)} for ${machine}`)
1099
+ this.advanceCursor(endpoint, new Set([seq as number]))
1100
+ return { notice: null }
1101
+ }
1102
+ return this.tx(() => {
1103
+ this.sql.all(`UPDATE deliveries SET acked_at = ? WHERE seq = ?`, this.now(), seq as number)
1104
+ this.audit("reject", row.endpoint, row.message_id, { code, machine })
1105
+ const sender = this.byId(row.from_ep)
1106
+ if (!sender || sender.kind === "system") return { notice: null }
1107
+ const recipient = this.byId(row.endpoint)!
1108
+ const noticeSeq = this.noticeTo(sender.id, `${this.address(recipient)} refused message ${row.message_id} (${code}).`)
1109
+ return { notice: { seq: noticeSeq, endpoint: sender.id } }
1110
+ })
1111
+ }
1112
+
1113
+ protected override audit(event: string, endpoint: string | null, ref: string | null, detail: Record<string, unknown>): void {
1114
+ if (event === "refuse" && this.refusalsPerMinute !== null) {
1115
+ const minute = Math.floor(this.now() / 60_000)
1116
+ const code = String(detail.code)
1117
+ let budget = this.refusals.get(code)
1118
+ if (!budget) {
1119
+ budget = { minute, written: 0, suppressed: 0 }
1120
+ this.refusals.set(code, budget)
1121
+ }
1122
+ if (budget.minute !== minute) Object.assign(budget, { minute, written: 0 })
1123
+ if (budget.written >= this.refusalsPerMinute) {
1124
+ budget.suppressed++
1125
+ return
1126
+ }
1127
+ budget.written++
1128
+ if (budget.suppressed) {
1129
+ detail = { ...detail, suppressed: budget.suppressed }
1130
+ budget.suppressed = 0
1131
+ }
1132
+ }
1133
+ super.audit(event, endpoint, ref, detail)
1134
+ }
1135
+
1136
+ /** an audit row from outside the core (hub events: machine binds, releases, revocations) */
1137
+ record(event: string, endpoint: string | null, ref: string | null, detail: Record<string, unknown>): void {
1138
+ this.audit(event, endpoint, ref, detail)
1139
+ }
1140
+
1141
+ /**
1142
+ * drops what expired, and with xinbox the rows received over XINBOX_KEEP_MS ago that expired or were acked that long ago, keeping
1143
+ * the highest seq they held as the floor of the next one
1144
+ */
1145
+ override sweep(): void {
1146
+ super.sweep()
1147
+ if (!this.xinbox) return
1148
+ this.xfloor()
1149
+ const now = this.now()
1150
+ const settled = now - XINBOX_KEEP_MS
1151
+ this.sql.all(
1152
+ `DELETE FROM xinbox WHERE at <= ? AND (expires_at <= ? OR EXISTS (SELECT 1 FROM xcursor c WHERE c.endpoint = xinbox.endpoint
1153
+ AND ((xinbox.seq <= c.acked_seq AND c.at <= ?) OR (xinbox.seq <= c.old_seq AND c.old_at <= ?))))`,
1154
+ settled,
1155
+ now,
1156
+ settled,
1157
+ settled,
1158
+ )
1159
+ this.sql.all(`DELETE FROM xcursor WHERE endpoint NOT IN (SELECT endpoint FROM xinbox)`)
1160
+ }
1161
+
1162
+ // other accounts (the hosted relay)
1163
+
1164
+ /** the mirrored row of `<machine>/<handle>` (a bridge's, never a direct client's), or null */
1165
+ xendpoint(remote: string): XEndpoint | null {
1166
+ const row = this.byRemote(remote)
1167
+ return row && row.token_hash === null && row.kind !== "system" && !isForeign(remote) ? this.xview(row) : null
1168
+ }
1169
+
1170
+ /** the mirrored row an endpoint id is, or null for anything else */
1171
+ xsender(endpoint: string): XEndpoint | null {
1172
+ const row = this.byId(endpoint) as FederatedRow | undefined
1173
+ return row?.remote && row.token_hash === null && row.kind !== "system" && !isForeign(row.remote) ? this.xview(row) : null
1174
+ }
1175
+
1176
+ /** every mirrored row, for discovery */
1177
+ xendpoints(): XEndpoint[] {
1178
+ return this.sql
1179
+ .all<FederatedRow>(`SELECT * FROM endpoints WHERE remote IS NOT NULL AND token_hash IS NULL AND kind != 'system' ORDER BY remote`)
1180
+ .filter((row) => !isForeign(row.remote!))
1181
+ .map((row) => this.xview(row))
1182
+ }
1183
+
1184
+ /** the xinbox row `id` delivered to `<machine>/<handle>`, or null */
1185
+ xreceived(endpoint: string, id: string): XInboxRow | null {
1186
+ return this.xinbox ? (one<XInboxRow>(this.sql, `SELECT * FROM xinbox WHERE endpoint = ? AND id = ?`, endpoint, id) ?? null) : null
1187
+ }
1188
+
1189
+ /** the earlier row of a message from `fromAccount` under `clientId`, or null */
1190
+ xduplicate(fromAccount: string, clientId: string): { id: string; acked: boolean } | null {
1191
+ const row = one<{ id: string; seq: number; endpoint: string }>(
1192
+ this.sql,
1193
+ `SELECT id, seq, endpoint FROM xinbox WHERE from_account = ? AND client_id = ?`,
1194
+ fromAccount,
1195
+ clientId,
1196
+ )
1197
+ if (!row) return null
1198
+ const cursor = one<{ acked_seq: number }>(this.sql, `SELECT acked_seq FROM xcursor WHERE endpoint = ?`, row.endpoint)?.acked_seq ?? 0
1199
+ return { id: row.id, acked: row.seq <= cursor }
1200
+ }
1201
+
1202
+ /** what `fromAccount` sent here: from `sender` to `endpoint` in the last minute and hour (each with its oldest), and since `since` */
1203
+ xcounts(
1204
+ fromAccount: string,
1205
+ sender: string,
1206
+ endpoint: string,
1207
+ since: number,
1208
+ ): { minute: number; minuteAt: number | null; hour: number; hourAt: number | null; day: number } {
1209
+ const now = this.now()
1210
+ const pair = one<{ minute: number; minute_at: number | null; hour: number; hour_at: number | null }>(
1211
+ this.sql,
1212
+ `SELECT COALESCE(SUM(at > ?), 0) AS minute, MIN(CASE WHEN at > ? THEN at END) AS minute_at, COUNT(*) AS hour, MIN(at) AS hour_at FROM xinbox
1213
+ WHERE from_account = ? AND sender = ? AND endpoint = ? AND at > ?`,
1214
+ now - 60_000,
1215
+ now - 60_000,
1216
+ fromAccount,
1217
+ sender,
1218
+ endpoint,
1219
+ now - 3_600_000,
1220
+ )!
1221
+ const day = one<{ n: number }>(this.sql, `SELECT COUNT(*) AS n FROM xinbox WHERE from_account = ? AND at >= ?`, fromAccount, since)!.n
1222
+ return { minute: pair.minute, minuteAt: pair.minute_at, hour: pair.hour, hourAt: pair.hour_at, day }
1223
+ }
1224
+
1225
+ /** an endpoint's unread messages: its unacked, unexpired xinbox rows and deliveries */
1226
+ xunread(endpoint: XEndpoint): number {
1227
+ const now = this.now()
1228
+ return one<{ n: number }>(
1229
+ this.sql,
1230
+ `SELECT (SELECT COUNT(*) FROM xinbox x WHERE x.endpoint = ? AND x.expires_at > ? AND x.seq > COALESCE((SELECT acked_seq FROM xcursor WHERE endpoint = x.endpoint), 0))
1231
+ + (SELECT COUNT(*) FROM deliveries WHERE endpoint = ? AND acked_at IS NULL AND expires_at > ?) AS n`,
1232
+ endpoint.remote,
1233
+ now,
1234
+ endpoint.id,
1235
+ now,
1236
+ )!.n
1237
+ }
1238
+
1239
+ /** the accounts whose xinbox rows may be read out now; null (unknown) reads none */
1240
+ xsenders(accounts: Set<string> | null): void {
1241
+ this.xfrom = accounts
1242
+ }
1243
+
1244
+ /** deletes the xinbox rows of every account but `keep`; how many it deleted */
1245
+ xprune(keep: Set<string>): number {
1246
+ if (!this.xinbox) return 0
1247
+ const json = JSON.stringify([...keep])
1248
+ if (!one(this.sql, `SELECT 1 FROM xinbox WHERE from_account NOT IN (SELECT value FROM json_each(?)) LIMIT 1`, json)) return 0
1249
+ this.xfloor()
1250
+ return this.sql.all(`DELETE FROM xinbox WHERE from_account NOT IN (SELECT value FROM json_each(?)) RETURNING seq`, json).length
1251
+ }
1252
+
1253
+ xthreadCount(endpoint: string, thread: string): number {
1254
+ return one<{ n: number }>(this.sql, `SELECT COUNT(*) AS n FROM xinbox WHERE endpoint = ? AND thread = ?`, endpoint, thread)!.n
1255
+ }
1256
+
1257
+ /** one xinbox row under the next seq; null when a row of the same sender and client_id is there already */
1258
+ xinsert(row: Omit<XInboxRow, "seq">): number | null {
1259
+ return (
1260
+ one<{ seq: number }>(
1261
+ this.sql,
1262
+ `INSERT INTO xinbox (seq, id, from_account, client_id, endpoint, sender, message, thread, grant, expires_at, at) SELECT 1 + ${SEQ_TOP}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? WHERE true
1263
+ ON CONFLICT (from_account, client_id) DO NOTHING RETURNING seq`,
1264
+ row.id,
1265
+ row.from_account,
1266
+ row.client_id,
1267
+ row.endpoint,
1268
+ row.sender,
1269
+ row.message,
1270
+ row.thread,
1271
+ row.grant,
1272
+ row.expires_at,
1273
+ row.at,
1274
+ )?.seq ?? null
1275
+ )
1276
+ }
1277
+
1278
+ /** a value of the cross-account state, written once */
1279
+ xstate(key: string, init: () => string): string {
1280
+ const found = one<{ value: string }>(this.sql, `SELECT value FROM xstate WHERE key = ?`, key)
1281
+ if (found) return found.value
1282
+ const value = init()
1283
+ this.sql.all(`INSERT INTO xstate (key, value) VALUES (?, ?) ON CONFLICT (key) DO NOTHING`, key, value)
1284
+ return one<{ value: string }>(this.sql, `SELECT value FROM xstate WHERE key = ?`, key)!.value
1285
+ }
1286
+
1287
+ private xview(row: EndpointRow): XEndpoint {
1288
+ const remote = (row as FederatedRow).remote!
1289
+ return {
1290
+ id: row.id,
1291
+ remote,
1292
+ machine: machinePart(remote),
1293
+ handle: handlePart(remote),
1294
+ kind: row.kind,
1295
+ depth: row.kind === "agent" ? row.depth : null,
1296
+ harness: row.harness,
1297
+ presence: this.view(row).presence,
1298
+ }
1299
+ }
1300
+
1301
+ /** unacked, unexpired xinbox rows of mirrored endpoints from the accounts xsenders() named, as deliveries, by `where` over `x` */
1302
+ private xinboxPage(where: string, params: SqlValue[], limit: number): Delivery[] {
1303
+ if (!this.xfrom?.size) return []
1304
+ return this.sql
1305
+ .all<{ seq: number; endpoint: string; message: string; recipient: string }>(
1306
+ `SELECT x.seq AS seq, x.endpoint AS endpoint, x.message AS message, e.id AS recipient FROM xinbox x JOIN endpoints e ON e.remote = x.endpoint AND e.token_hash IS NULL
1307
+ WHERE ${where} AND x.expires_at > ? AND x.from_account IN (SELECT value FROM json_each(?))
1308
+ AND x.seq > COALESCE((SELECT acked_seq FROM xcursor WHERE endpoint = x.endpoint), 0) ORDER BY x.seq LIMIT ?`,
1309
+ ...params,
1310
+ this.now(),
1311
+ JSON.stringify([...this.xfrom]),
1312
+ limit,
1313
+ )
1314
+ .map((row) => ({ seq: row.seq, endpoint: row.endpoint, recipient: row.recipient, mirrored: true, message: JSON.parse(row.message) as Envelope }))
1315
+ }
1316
+
1317
+ /** the endpoint of an unacked xinbox seq of `machine`, or null */
1318
+ private ownXinbox(machine: string, seq: number): string | null {
1319
+ if (!this.xinbox) return null
1320
+ return (
1321
+ one<{ endpoint: string }>(
1322
+ this.sql,
1323
+ `SELECT x.endpoint AS endpoint FROM xinbox x WHERE x.seq = ? AND substr(x.endpoint, 1, ?) = ? AND x.seq > COALESCE((SELECT acked_seq FROM xcursor WHERE endpoint = x.endpoint), 0)`,
1324
+ seq,
1325
+ machine.length + 1,
1326
+ `${machine}/`,
1327
+ )?.endpoint ?? null
1328
+ )
1329
+ }
1330
+
1331
+ /**
1332
+ * moves an endpoint's cursor over the leading run of its unacked, unexpired rows that `batch` names, in one write or none; a seq past
1333
+ * a row it does not name stays unacked and replays. How many rows it moved over
1334
+ */
1335
+ private advanceCursor(endpoint: string, batch: Set<number>): number {
1336
+ const now = this.now()
1337
+ const cursor = one<{ acked_seq: number }>(this.sql, `SELECT acked_seq FROM xcursor WHERE endpoint = ?`, endpoint)?.acked_seq ?? 0
1338
+ let to = cursor
1339
+ let moved = 0
1340
+ for (const { seq } of this.sql.all<{ seq: number }>(
1341
+ `SELECT seq FROM xinbox WHERE endpoint = ? AND seq > ? AND seq <= ? AND expires_at > ? ORDER BY seq`,
1342
+ endpoint,
1343
+ cursor,
1344
+ Math.max(...batch),
1345
+ now,
1346
+ )) {
1347
+ if (!batch.has(seq)) break
1348
+ to = seq
1349
+ moved++
1350
+ }
1351
+ if (to === cursor) return 0
1352
+ // the checkpoint (old_seq, old_at) rolls to the previous position at most once a day, so the sweep can tell rows acked a day ago
1353
+ this.sql.all(
1354
+ `INSERT INTO xcursor (endpoint, acked_seq, at) VALUES (?, ?, ?) ON CONFLICT (endpoint) DO UPDATE SET
1355
+ old_seq = CASE WHEN old_at IS NULL OR excluded.at - old_at >= ? THEN acked_seq ELSE old_seq END,
1356
+ old_at = CASE WHEN old_at IS NULL OR excluded.at - old_at >= ? THEN at ELSE old_at END,
1357
+ acked_seq = excluded.acked_seq, at = excluded.at`,
1358
+ endpoint,
1359
+ to,
1360
+ now,
1361
+ XINBOX_KEEP_MS,
1362
+ XINBOX_KEEP_MS,
1363
+ )
1364
+ return moved
1365
+ }
1366
+
1367
+ /** when the holder part of a mirrored agent's incarnation moved, drops its xinbox rows and deliveries: they were the earlier holder's */
1368
+ private dropEarlierHolder(row: FederatedRow, incarnation: string | null | undefined): void {
1369
+ const holder = (value: string | null | undefined) => (value?.includes(".") ? value.slice(0, value.indexOf(".")) : null)
1370
+ const was = holder(row.incarnation)
1371
+ const next = holder(incarnation)
1372
+ if (was === null || next === null || was === next) return
1373
+ let xinbox = 0
1374
+ if (this.xinbox) {
1375
+ this.xfloor()
1376
+ xinbox = this.sql.all(`DELETE FROM xinbox WHERE endpoint = ? RETURNING seq`, row.remote).length
1377
+ }
1378
+ const deliveries = this.sql.all(`DELETE FROM deliveries WHERE endpoint = ? RETURNING seq`, row.id).length
1379
+ this.audit("mirror_reset", row.id, null, { target: row.remote, xinbox, deliveries })
1380
+ }
1381
+
1382
+ /** keeps the highest xinbox seq as the floor of the next one, before rows are deleted */
1383
+ private xfloor(): void {
1384
+ const top = one<{ n: number | null }>(this.sql, `SELECT MAX(seq) AS n FROM xinbox`)!.n
1385
+ const floor = Number(one<{ value: string }>(this.sql, `SELECT value FROM xstate WHERE key = 'seq_floor'`)?.value ?? 0)
1386
+ if (top !== null && top > floor)
1387
+ this.sql.all(`INSERT INTO xstate (key, value) VALUES ('seq_floor', ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value`, String(top))
1388
+ }
1389
+
1390
+ // internals
1391
+
1392
+ private ownDelivery(machine: string, seq: number): { endpoint: string; message_id: string; from_ep: string; hop: number } | undefined {
1393
+ return one(
1394
+ this.sql,
1395
+ `SELECT d.endpoint AS endpoint, d.message_id AS message_id, m.from_ep AS from_ep, m.hop AS hop FROM deliveries d JOIN messages m ON m.id = d.message_id JOIN endpoints e ON e.id = d.endpoint
1396
+ WHERE d.seq = ? AND d.acked_at IS NULL AND substr(e.remote, 1, ?) = ? AND e.token_hash IS NULL`,
1397
+ seq,
1398
+ machine.length + 1,
1399
+ `${machine}/`,
1400
+ )
1401
+ }
1402
+
1403
+ private noticeTo(endpoint: string, text: string): number {
1404
+ const system = this.byHandle(SYSTEM_HANDLE)!
1405
+ const now = this.now()
1406
+ const id = newId("msg", now)
1407
+ const expires = now + LIMITS.ttlDefaultS * 1000
1408
+ this.sql.all(
1409
+ `INSERT INTO messages (id, thread, reply_to, kind, from_ep, to_addr, body, mime, data_class, attachments, hop, created_at, expires_at)
1410
+ VALUES (?, ?, NULL, 'status', ?, ?, ?, 'text/plain', 'public', '[]', 0, ?, ?)`,
1411
+ id,
1412
+ newId("thr", now),
1413
+ system.id,
1414
+ this.addressOf(endpoint),
1415
+ text,
1416
+ now,
1417
+ expires,
1418
+ )
1419
+ return this.insertDelivery(id, endpoint, expires)
1420
+ }
1421
+
1422
+ private forwardHash(params: SendParams): string {
1423
+ const targets = (Array.isArray(params.to) ? params.to : [params.to]).map(String).sort()
1424
+ return this.hash(
1425
+ JSON.stringify([
1426
+ "forward",
1427
+ targets,
1428
+ params.body,
1429
+ params.kind ?? "message",
1430
+ params.thread ?? null,
1431
+ params.reply_to ?? null,
1432
+ params.data_class ?? "public",
1433
+ (params.attachments ?? []).map((a) => a.path).sort(),
1434
+ params.mime ?? null,
1435
+ params.ttl_s ?? null,
1436
+ ]),
1437
+ )
1438
+ }
1439
+
1440
+ /** a message from another account passes the recipient's in lists by `acct:`, or as a reply to the recipient's own message to that sender */
1441
+ private checkForeignIn(sender: EndpointRow, recipient: EndpointRow, parent: MessageRow | undefined): void {
1442
+ if (parent && parent.from_ep === recipient.id && parent.to_addr.split(",").some((to) => to.trim() === this.address(sender))) return
1443
+ const admission = this.crossAdmission(recipient.id, "in", this.scopeNode(sender))
1444
+ if (admission.ok) return
1445
+ this.audit("scope_refused", sender.id, null, { from: this.address(sender), to: this.address(recipient), dir: "in", reason: admission.why })
1446
+ throw new RelayError("not_found", `no endpoint ${this.address(recipient)}`)
1447
+ }
1448
+
1449
+ /** the stand-in row of another account's sender: `@<account>/<machine>/<handle>`, or `@<account>/me` for its human at a terminal */
1450
+ private foreignProxy(from: Envelope["from"]): string {
1451
+ const bad = () => new RelayError("invalid_params", `the sender ${String(from.address)} is not an endpoint of a contact`)
1452
+ const account = from.account
1453
+ if (typeof account !== "string" || !ACCOUNT_HANDLE.test(account) || this.isLocalUser(account) || from.user !== account) throw bad()
1454
+ let remote: string
1455
+ if (from.human === true) {
1456
+ if (from.address !== `${account}/me`) throw bad()
1457
+ remote = `@${account}/me`
1458
+ } else {
1459
+ const parsed = this.foreignAccount(from.address)
1460
+ if (!parsed || parsed.account !== account) throw bad()
1461
+ remote = `@${account}/${parsed.machine}/${parsed.handle}`
1462
+ }
1463
+ const harness = from.claims.harness && validHarness(from.claims.harness) ? from.claims.harness : null
1464
+ const existing = this.byRemote(remote)
1465
+ const id = existing?.id ?? this.insertProxy(remote, from.human === true ? "human" : "agent", harness, from.auth)
1466
+ if (existing && existing.harness !== harness) this.sql.all(`UPDATE endpoints SET harness = ? WHERE id = ?`, harness, id)
1467
+ return id
1468
+ }
1469
+
1470
+ private proxyFor(from: Envelope["from"]): string {
1471
+ if (from.auth === "contact") return this.foreignProxy(from)
1472
+ const prefix = `${from.user}/`
1473
+ if (!from.address.startsWith(prefix)) throw new RelayError("invalid_params", "the sender's address is not of its user")
1474
+ const rest = from.address.slice(prefix.length)
1475
+ const remote = rest.includes("/") ? rest : `${from.user}/${rest}`
1476
+ if (!REMOTE.test(remote)) throw new RelayError("invalid_params", `the sender's address ${from.address} is not valid`)
1477
+ const handle = handlePart(remote)
1478
+ const kind: EndpointKind = handle === "" ? "human" : rest.includes("/") ? "agent" : "system"
1479
+ const harness = from.claims.harness && validHarness(from.claims.harness) ? from.claims.harness : null
1480
+ const model = cleanText(from.claims.model, "model", 128)
1481
+ const existing = this.byRemote(remote)
1482
+ const id = existing?.id ?? this.insertProxy(remote, kind, harness, from.auth)
1483
+ if (!existing || existing.harness !== harness || existing.model !== model || existing.auth !== from.auth) {
1484
+ this.sql.all(`UPDATE endpoints SET harness = ?, model = ?, auth = ? WHERE id = ?`, harness, model, from.auth, id)
1485
+ }
1486
+ return id
1487
+ }
1488
+
1489
+ private checkEnvelope(message: Envelope): { createdAt: number; expiresAt: number } {
1490
+ const bad = (what: string) => new RelayError("invalid_params", `the message's ${what} is not valid`)
1491
+ if (!message || typeof message !== "object") throw bad("shape")
1492
+ if (typeof message.id !== "string" || !MESSAGE_ID.test(message.id)) throw bad("id")
1493
+ if (typeof message.thread !== "string" || !message.thread || message.thread.length > 128) throw bad("thread")
1494
+ if (message.reply_to !== null && (typeof message.reply_to !== "string" || message.reply_to.length > 128)) throw bad("reply_to")
1495
+ if (!MESSAGE_KINDS.includes(message.kind)) throw bad("kind")
1496
+ if (typeof message.to !== "string" || message.to.length > 4096) throw bad("to")
1497
+ if (!message.body || typeof message.body.text !== "string" || typeof message.body.mime !== "string" || message.body.mime.length > 64) throw bad("body")
1498
+ if (new TextEncoder().encode(message.body.text).length > LIMITS.bodyBytes)
1499
+ throw new RelayError("too_large", `the body is more than ${LIMITS.bodyBytes} bytes`)
1500
+ if (message.data_class !== "public" && message.data_class !== "private") throw bad("data_class")
1501
+ if (
1502
+ !Array.isArray(message.attachments) ||
1503
+ message.attachments.length > LIMITS.attachments ||
1504
+ message.attachments.some((a) => a?.kind !== "path" || typeof a.path !== "string" || !a.path.startsWith("/"))
1505
+ )
1506
+ throw bad("attachments")
1507
+ if (!Number.isInteger(message.hop) || message.hop < 0 || message.hop > LIMITS.hopMax) throw bad("hop")
1508
+ const from = message.from
1509
+ if (!from || typeof from.address !== "string" || typeof from.user !== "string" || !from.claims || !AUTHS.includes(from.auth)) throw bad("sender")
1510
+ if (from.claims.tty !== undefined && typeof from.claims.tty !== "boolean") throw bad("sender")
1511
+ const createdAt = Date.parse(message.created_at)
1512
+ const expiresAt = Date.parse(message.expires_at)
1513
+ if (!Number.isFinite(createdAt) || !Number.isFinite(expiresAt)) throw bad("timestamps")
1514
+ const secret = findSecret(message.body.text)
1515
+ if (secret) throw new RelayError("secret_refused", `the message looks like it carries a credential (${secret})`)
1516
+ return { createdAt, expiresAt }
1517
+ }
1518
+
1519
+ private parseView(value: unknown): ParsedView {
1520
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new RelayError("invalid_params", "an endpoint is an object")
1521
+ const view = value as Partial<MirrorView>
1522
+ if (typeof view.address !== "string") throw new RelayError("invalid_params", "an endpoint needs an address")
1523
+ const handle = view.address.slice(view.address.lastIndexOf("/") + 1)
1524
+ const kind = view.kind
1525
+ if (kind !== "agent" && kind !== "human") throw new RelayError("invalid_params", "an endpoint's kind is agent or human")
1526
+ if (kind === "human" ? handle !== "" : !validHandle(handle))
1527
+ throw new RelayError("invalid_params", `${JSON.stringify(view.address)} is not a valid endpoint address`)
1528
+ const harness = cleanText(view.harness, "harness", 32)
1529
+ if (harness !== null && !validHarness(harness)) throw new RelayError("invalid_params", `harness ${JSON.stringify(harness)} is not a valid name`)
1530
+ let status: { state: StatusState; note: string } | null = null
1531
+ if (view.status) {
1532
+ if (!STATUS_STATES.includes(view.status.state)) throw new RelayError("invalid_params", `state must be one of ${STATUS_STATES.join(", ")}`)
1533
+ status = { state: view.status.state, note: cleanText(view.status.note, "note", LIMITS.noteChars) ?? "" }
1534
+ }
1535
+ const presence = view.presence ?? "offline"
1536
+ if (!PRESENCES.includes(presence)) throw new RelayError("invalid_params", "presence is online, idle or offline")
1537
+ const parsedSeen = typeof view.last_seen === "string" ? Date.parse(view.last_seen) : Number.NaN
1538
+ const out = {
1539
+ handle,
1540
+ kind,
1541
+ harness,
1542
+ model: cleanText(view.model, "model", 128),
1543
+ cwd: cleanText(view.cwd, "cwd", 1024),
1544
+ labels: JSON.stringify(cleanLabels(view.labels)),
1545
+ status,
1546
+ presence,
1547
+ lastSeen: Number.isFinite(parsedSeen) ? parsedSeen : null,
1548
+ scope: kind === "agent" ? parseScope(view) : {},
1549
+ }
1550
+ const leaked = [handle, out.model, out.cwd, out.labels, status?.note, out.scope.title].map((v) => (v ? findSecret(v) : null)).find(Boolean)
1551
+ if (leaked) throw new RelayError("secret_refused", `an endpoint looks like it carries a credential (${leaked})`)
1552
+ return out
1553
+ }
1554
+
1555
+ private diff(before: FederatedRow | undefined, after: EndpointRow): MirrorChange | null {
1556
+ const view = this.remoteView(after)
1557
+ if (!before) return { view, added: true, presence: true, status: true }
1558
+ const was = this.view(before)
1559
+ const presence = was.presence !== view.presence
1560
+ const status = before.status_state !== after.status_state || before.status_note !== after.status_note
1561
+ const other = (["kind", "harness", "model", "cwd", "labels", "title", "tree", "parent_ep", "depth", "rules", "caps", "incarnation"] as const).some(
1562
+ (field) => before[field] !== (after as FederatedRow)[field],
1563
+ )
1564
+ return presence || status || other ? { view, added: false, presence, status } : null
1565
+ }
1566
+
1567
+ private remoteView(row: EndpointRow): RemoteView {
1568
+ const remote = (row as FederatedRow).remote
1569
+ const view: RemoteView = { ...this.view(row), machine: remote ? machinePart(remote) : (this.machine ?? "") }
1570
+ if (row.kind !== "agent") return view
1571
+ const handle = (id: string | null) => {
1572
+ const other = id ? (this.byId(id) as FederatedRow | undefined) : undefined
1573
+ return other?.remote ? handlePart(other.remote) : null
1574
+ }
1575
+ return {
1576
+ ...view,
1577
+ tree: row.depth === null ? null : row.tree ? handle(row.tree) : remote ? handlePart(remote) : row.handle,
1578
+ parent_ep: handle(row.parent_ep),
1579
+ depth: row.depth,
1580
+ rules: row.rules ? (JSON.parse(row.rules) as MirrorRules) : null,
1581
+ caps: row.caps ? this.wireCaps(JSON.parse(row.caps) as ScopeCaps) : null,
1582
+ incarnation: (row as FederatedRow).incarnation ?? null,
1583
+ }
1584
+ }
1585
+
1586
+ /**
1587
+ * the place each agent of a mirror batch gets, worked out here: a root is a view with no parent and depth 0; a child sits one below its
1588
+ * parent, which must be an agent this machine mirrors; anything else (a parent on another machine or not mirrored, a cycle, a root
1589
+ * claiming depth) is unknown. Undefined when the view says nothing about its place.
1590
+ */
1591
+ private placer(machine: string, batch: { view: ParsedView; before: FederatedRow | undefined }[]): (handle: string) => Placement | undefined {
1592
+ const agents = new Map(batch.filter((entry) => entry.view.kind === "agent").map((entry) => [entry.view.handle, entry]))
1593
+ const memo = new Map<string, Placement | undefined>()
1594
+ const place = (handle: string, trail: string[]): Placement | undefined => {
1595
+ if (trail.includes(handle)) return UNKNOWN
1596
+ if (memo.has(handle)) return memo.get(handle)
1597
+ const { parent, depth } = agents.get(handle)!.view.scope
1598
+ let result: Placement | undefined
1599
+ if (parent === undefined && depth === undefined) result = undefined
1600
+ else if (parent === undefined) result = UNKNOWN
1601
+ else if (parent === null) result = depth === 0 ? { tree: null, parent_ep: null, depth: 0 } : UNKNOWN
1602
+ else {
1603
+ const row = validHandle(parent) ? this.byRemote(`${machine}/${parent}`) : undefined
1604
+ if (!row || row.token_hash !== null || row.kind !== "agent") result = UNKNOWN
1605
+ else {
1606
+ const up = agents.has(parent)
1607
+ ? (place(parent, [...trail, handle]) ?? (agents.get(parent)!.before ? stored(agents.get(parent)!.before!) : UNKNOWN))
1608
+ : stored(row)
1609
+ result = up.depth === null || up.depth + 1 > LIMITS.depthMax ? UNKNOWN : { tree: up.tree ?? row.id, parent_ep: row.id, depth: up.depth + 1 }
1610
+ }
1611
+ }
1612
+ if (!trail.length) memo.set(handle, result)
1613
+ return result
1614
+ }
1615
+ return (handle) => place(handle, [])
1616
+ }
1617
+
1618
+ /** an agent row's place, rules, caps, title and incarnation after a view: what it left out stays, unless the endpoint is a new incarnation */
1619
+ private scopeColumns(machine: string, scope: ParsedScope, row: FederatedRow, placement: Placement | undefined): Partial<FederatedRow> {
1620
+ const fresh = scope.incarnation !== undefined && scope.incarnation !== null && row.incarnation !== null && scope.incarnation !== row.incarnation
1621
+ const place = placement ?? (fresh ? UNKNOWN : stored(row))
1622
+ let caps = fresh ? null : row.caps
1623
+ if (scope.caps !== undefined) {
1624
+ const next = this.localCaps(machine, scope.caps ?? { caps: [], allow: {} })
1625
+ const merged = fresh || !row.caps ? next : stickyCaps(JSON.parse(row.caps) as ScopeCaps, next)
1626
+ caps = merged.caps.length || Object.keys(merged.allow).length ? JSON.stringify(merged) : null
1627
+ }
1628
+ const rules = scope.rules !== undefined ? scope.rules : fresh ? null : undefined
1629
+ return {
1630
+ ...place,
1631
+ rules: rules === undefined ? row.rules : rules && (rules.in || rules.out) ? JSON.stringify(rules) : null,
1632
+ caps,
1633
+ title: scope.title !== undefined ? scope.title : fresh ? null : row.title,
1634
+ incarnation: scope.incarnation ?? row.incarnation,
1635
+ }
1636
+ }
1637
+
1638
+ /** wire caps of `machine` with their `ep:<handle>` references as this core's row ids; an endpoint the machine does not mirror is `gone`, which admits nothing */
1639
+ private localCaps(machine: string, caps: ScopeCaps): ScopeCaps {
1640
+ const ref = (value: string) => {
1641
+ if (REFS.has(value)) return value
1642
+ const handle = value.startsWith("ep:") ? value.slice(3) : ""
1643
+ const row = validHandle(handle) ? this.byRemote(`${machine}/${handle}`) : undefined
1644
+ return row && row.token_hash === null && row.kind === "agent" ? row.id : "gone"
1645
+ }
1646
+ return mapRefs(caps, ref)
1647
+ }
1648
+
1649
+ /** caps with this core's row ids named as `ep:<handle>`, as they cross machines */
1650
+ private wireCaps(caps: ScopeCaps): MirrorCaps {
1651
+ const ref = (value: string) => {
1652
+ if (REFS.has(value)) return value
1653
+ const row = this.byId(value) as FederatedRow | undefined
1654
+ if (row?.kind !== "agent") return "gone"
1655
+ return `ep:${row.remote ? handlePart(row.remote) : row.handle}`
1656
+ }
1657
+ const mapped = mapRefs(caps, ref)
1658
+ return { caps: mapped.caps, allow: mapped.allow }
1659
+ }
1660
+
1661
+ private insertRemote(remote: string, kind: EndpointKind | "system", harness: string | null, auth: RelayAuth | null, now: number): string {
1662
+ const id = newId("ep", now)
1663
+ this.sql.all(
1664
+ `INSERT INTO endpoints (id, handle, kind, harness, labels, created_at, remote, auth) VALUES (?, ?, ?, ?, '{}', ?, ?, ?)`,
1665
+ id,
1666
+ `_r_${id}`,
1667
+ kind,
1668
+ harness,
1669
+ now,
1670
+ remote,
1671
+ auth,
1672
+ )
1673
+ return id
1674
+ }
1675
+
1676
+ private retire(id: string): void {
1677
+ this.sql.all(`UPDATE endpoints SET handle = ? || id, remote = NULL, token_hash = NULL, connections = 0 WHERE id = ?`, GONE_PREFIX, id)
1678
+ this.dropPeek(id)
1679
+ }
1680
+
1681
+ /** a new proxy row, within the endpoint cap */
1682
+ private insertProxy(remote: string, kind: EndpointKind | "system", harness: string | null, auth: RelayAuth): string {
1683
+ this.checkCap()
1684
+ return this.insertRemote(remote, kind, harness, auth, this.now())
1685
+ }
1686
+
1687
+ private checkCap(): void {
1688
+ if (this.endpointCap === null) return
1689
+ const live = one<{ n: number }>(
1690
+ this.sql,
1691
+ `SELECT COUNT(*) AS n FROM endpoints WHERE kind != 'system' AND substr(handle, 1, ?) != ?`,
1692
+ GONE_PREFIX.length,
1693
+ GONE_PREFIX,
1694
+ )!.n
1695
+ if (live >= this.endpointCap) throw new RelayError("too_large", `at most ${this.endpointCap} endpoints on this relay`)
1696
+ }
1697
+
1698
+ private checkMachine(machine: string): void {
1699
+ if (typeof machine !== "string" || !MACHINE_NAME.test(machine))
1700
+ throw new RelayError("invalid_params", `machine ${JSON.stringify(machine)} is not a valid name`)
1701
+ }
1702
+
1703
+ private byRemote(remote: string): FederatedRow | undefined {
1704
+ return one<FederatedRow>(this.sql, `SELECT * FROM endpoints WHERE remote = ?`, remote)
1705
+ }
1706
+
1707
+ /** a judge status notice to `endpoint` from the relay; its delivery seq */
1708
+ judgeNoticeTo(endpoint: string, text: string): number {
1709
+ return this.noticeTo(endpoint, text)
1710
+ }
1711
+ }
1712
+
1713
+ const stored = (row: FederatedRow): Placement => ({ tree: row.tree, parent_ep: row.parent_ep, depth: row.depth })
1714
+
1715
+ function mapRefs(caps: ScopeCaps, ref: (value: string) => string): ScopeCaps {
1716
+ const entries = (list: ScopeEntry[]) => list.map((entry) => (entry.via === undefined ? entry : { ...entry, via: ref(entry.via) }))
1717
+ return {
1718
+ caps: caps.caps.map((cap) => ({ ...cap, by: ref(cap.by), sels: entries(cap.sels) })),
1719
+ allow: Object.fromEntries(Object.entries(caps.allow ?? {}).map(([key, list]) => [key, entries(list)])),
1720
+ }
1721
+ }
1722
+
1723
+ /** a later view's caps over the ones seen before: a cap it leaves out stays, and a cap seen before gains only widened (`via`) entries */
1724
+ function stickyCaps(seen: ScopeCaps, next: ScopeCaps): ScopeCaps {
1725
+ const plain = (entry: ScopeEntry) => entry.via === undefined
1726
+ const caps = next.caps.map((cap) => {
1727
+ const old = seen.caps.find((had) => had.id === cap.id)
1728
+ return old
1729
+ ? { ...cap, sels: cap.sels.filter((entry) => !plain(entry) || old.sels.some((had) => plain(had) && had.sel === entry.sel && had.group === entry.group)) }
1730
+ : cap
1731
+ })
1732
+ for (const old of seen.caps) if (!next.caps.some((cap) => cap.id === old.id)) caps.push(old)
1733
+ return { caps, allow: next.allow }
1734
+ }
1735
+
1736
+ /** whether `next` drops a cap of `seen` or gives one of them a plain entry: what {@link stickyCaps} would not take without a new incarnation */
1737
+ function loosens(seen: MirrorCaps, next: MirrorCaps): boolean {
1738
+ const plain = (entry: ScopeEntry) => entry.via === undefined
1739
+ return seen.caps.some((old) => {
1740
+ const cap = next.caps.find((c) => c.id === old.id)
1741
+ return !cap || cap.sels.some((entry) => plain(entry) && !old.sels.some((had) => plain(had) && had.sel === entry.sel && had.group === entry.group))
1742
+ })
1743
+ }
1744
+
1745
+ const bad = (what: string) => new RelayError("invalid_params", `an endpoint's ${what} is not valid`)
1746
+ const isRecord = (value: unknown): value is Record<string, unknown> => !!value && typeof value === "object" && !Array.isArray(value)
1747
+
1748
+ function shortText(value: unknown, what: string, max = 128): string {
1749
+ if (typeof value !== "string" || !value || value.length > max) throw bad(what)
1750
+ return value
1751
+ }
1752
+
1753
+ function parseSelectors(value: unknown, what: string): string[] {
1754
+ if (!Array.isArray(value) || value.length > 64 || value.some((sel) => !validSelector(sel))) throw bad(what)
1755
+ return value as string[]
1756
+ }
1757
+
1758
+ function parseEntries(value: unknown): ScopeEntry[] {
1759
+ if (!Array.isArray(value) || value.length > 64) throw bad("caps")
1760
+ return value.map((entry) => {
1761
+ if (!isRecord(entry) || !validSelector(entry.sel)) throw bad("caps")
1762
+ return {
1763
+ sel: entry.sel as string,
1764
+ ...(entry.via === undefined ? {} : { via: shortText(entry.via, "caps") }),
1765
+ ...(entry.group === undefined ? {} : { group: shortText(entry.group, "caps") }),
1766
+ ...(entry.base === true ? { base: true as const } : {}),
1767
+ ...(entry.member === true ? { member: true as const } : {}),
1768
+ }
1769
+ })
1770
+ }
1771
+
1772
+ /** the scope fields of an agent's view, checked; a field left out stays undefined */
1773
+ function parseScope(view: Partial<MirrorView>): ParsedScope {
1774
+ const out: ParsedScope = {}
1775
+ const given = (key: keyof MirrorView) => key in view && view[key] !== undefined
1776
+ if (given("tree")) out.tree = view.tree === null ? null : shortText(view.tree, "tree")
1777
+ if (given("parent_ep")) out.parent = view.parent_ep === null ? null : shortText(view.parent_ep, "parent_ep")
1778
+ if (given("depth")) {
1779
+ if (view.depth !== null && (!Number.isInteger(view.depth) || (view.depth as number) < 0 || (view.depth as number) > 1000)) throw bad("depth")
1780
+ out.depth = view.depth ?? null
1781
+ }
1782
+ if (given("rules")) {
1783
+ if (view.rules !== null && !isRecord(view.rules)) throw bad("rules")
1784
+ const rules = view.rules as Record<string, unknown> | null
1785
+ out.rules = rules
1786
+ ? {
1787
+ ...(rules.in === undefined ? {} : { in: parseSelectors(rules.in, "rules") }),
1788
+ ...(rules.out === undefined ? {} : { out: parseSelectors(rules.out, "rules") }),
1789
+ }
1790
+ : null
1791
+ }
1792
+ if (given("caps")) {
1793
+ if (view.caps === null) out.caps = null
1794
+ else {
1795
+ const caps = view.caps as unknown
1796
+ if (!isRecord(caps) || !Array.isArray(caps.caps) || caps.caps.length > 64 || (caps.allow !== undefined && !isRecord(caps.allow))) throw bad("caps")
1797
+ const allow = Object.entries((caps.allow ?? {}) as Record<string, unknown>)
1798
+ if (allow.length > 64) throw bad("caps")
1799
+ out.caps = {
1800
+ caps: caps.caps.map((cap: unknown) => {
1801
+ if (!isRecord(cap) || (cap.dir !== "in" && cap.dir !== "out")) throw bad("caps")
1802
+ return { id: shortText(cap.id, "caps"), dir: cap.dir as ScopeDirection, by: shortText(cap.by, "caps"), sels: parseEntries(cap.sels) }
1803
+ }),
1804
+ allow: Object.fromEntries(allow.map(([key, list]) => [shortText(key, "caps"), parseEntries(list)])),
1805
+ }
1806
+ }
1807
+ }
1808
+ if (new TextEncoder().encode(JSON.stringify([out.rules ?? null, out.caps ?? null])).length > SCOPE_BYTES)
1809
+ throw new RelayError("too_large", `an endpoint's rules and caps are more than ${SCOPE_BYTES} bytes`)
1810
+ if (given("title"))
1811
+ out.title = view.title === null ? null : cleanText(typeof view.title === "string" ? view.title.replace(/\s+/g, " ") : view.title, "title", 64) || null
1812
+ if (given("incarnation")) {
1813
+ if (view.incarnation !== null && (typeof view.incarnation !== "string" || !INCARNATION.test(view.incarnation))) throw bad("incarnation")
1814
+ out.incarnation = view.incarnation ?? null
1815
+ }
1816
+ return out
1817
+ }