@scp-dev/hub 0.0.0-stage → 0.1.1
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/LICENSE +201 -0
- package/README.md +41 -2
- package/dist/index.d.ts +617 -0
- package/dist/index.js +2878 -0
- package/package.json +39 -4
- package/src/federation.ts +1817 -0
- package/src/hub.ts +1619 -0
- package/src/index.ts +26 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
import { Envelope, RelayCore, CoreOptions, EndpointRow, PolicySubject, ScopeSubject, ScopeNode, ScopeDirection, Admission, MessageRow, EndpointView, ScopeCap, ScopeEntry, Claim, SendParams, SendResult, EndpointKind, Presence, StatusState, Sql, CrossAccountLink, XDelivered, XListed, ActivityRow, XRelease } from '@scp-dev/core';
|
|
2
|
+
|
|
3
|
+
type RelayAuth = Envelope["from"]["auth"];
|
|
4
|
+
/** a machine name: one relay host of an account */
|
|
5
|
+
declare const MACHINE_NAME: RegExp;
|
|
6
|
+
interface FederatedOptions extends CoreOptions {
|
|
7
|
+
/** this host's own machine name; addresses naming it stay local */
|
|
8
|
+
machine?: string;
|
|
9
|
+
/** other names of the local user, such as the account handle once known */
|
|
10
|
+
aliases?: string[];
|
|
11
|
+
/** `from.auth` of senders whose row records none (default local-uid) */
|
|
12
|
+
auth?: RelayAuth;
|
|
13
|
+
/** at most this many live endpoints, mirrored and claimed together */
|
|
14
|
+
endpointCap?: number;
|
|
15
|
+
/** at most this many refusal audit rows a minute for each refusal code; the next one written counts those left out */
|
|
16
|
+
refusalsPerMinute?: number;
|
|
17
|
+
/** the hosted relay's cross-account tables: messages from other accounts in xinbox, in one seq space with deliveries */
|
|
18
|
+
xinbox?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** own rules as they cross machines: selector lists per direction */
|
|
21
|
+
interface MirrorRules {
|
|
22
|
+
in?: string[];
|
|
23
|
+
out?: string[];
|
|
24
|
+
}
|
|
25
|
+
/** 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 */
|
|
26
|
+
interface MirrorCaps {
|
|
27
|
+
caps: ScopeCap[];
|
|
28
|
+
allow?: Record<string, ScopeEntry[]>;
|
|
29
|
+
}
|
|
30
|
+
/** 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 */
|
|
31
|
+
interface MirrorScope {
|
|
32
|
+
/** the root's handle on the same machine (a root's own) */
|
|
33
|
+
tree?: string | null;
|
|
34
|
+
/** the parent's handle on the same machine; null for a root */
|
|
35
|
+
parent_ep?: string | null;
|
|
36
|
+
/** 0 for a root; null when not known */
|
|
37
|
+
depth?: number | null;
|
|
38
|
+
rules?: MirrorRules | null;
|
|
39
|
+
caps?: MirrorCaps | null;
|
|
40
|
+
/**
|
|
41
|
+
* changes when its machine released the endpoint and another took the handle, a new spawn took it over, or its caps loosened: the caps
|
|
42
|
+
* seen before lapse. The part before the first "." names the holder: a change there also drops what a hub queued for the endpoint
|
|
43
|
+
*/
|
|
44
|
+
incarnation?: string | null;
|
|
45
|
+
}
|
|
46
|
+
/** an endpoint as another machine reports it: EndpointView-like, only the address's last segment is used as the handle */
|
|
47
|
+
interface MirrorView extends MirrorScope {
|
|
48
|
+
address: string;
|
|
49
|
+
kind: EndpointKind;
|
|
50
|
+
harness?: string | null;
|
|
51
|
+
model?: string | null;
|
|
52
|
+
cwd?: string | null;
|
|
53
|
+
labels?: Record<string, string>;
|
|
54
|
+
status?: {
|
|
55
|
+
state: StatusState;
|
|
56
|
+
note: string;
|
|
57
|
+
} | null;
|
|
58
|
+
presence?: Presence;
|
|
59
|
+
last_seen?: string | null;
|
|
60
|
+
title?: string | null;
|
|
61
|
+
}
|
|
62
|
+
interface RemoteView extends Omit<EndpointView, "depth">, MirrorScope {
|
|
63
|
+
machine: string;
|
|
64
|
+
}
|
|
65
|
+
/** a row of xinbox: a message from another account, its envelope as JSON */
|
|
66
|
+
interface XInboxRow {
|
|
67
|
+
seq: number;
|
|
68
|
+
id: string;
|
|
69
|
+
from_account: string;
|
|
70
|
+
client_id: string;
|
|
71
|
+
endpoint: string;
|
|
72
|
+
/** the sender's address in its account */
|
|
73
|
+
sender: string;
|
|
74
|
+
message: string;
|
|
75
|
+
thread: string;
|
|
76
|
+
grant: string | null;
|
|
77
|
+
expires_at: number;
|
|
78
|
+
/** when it was received */
|
|
79
|
+
at: number;
|
|
80
|
+
}
|
|
81
|
+
/** a mirrored endpoint of this account as the cross-account checks see it */
|
|
82
|
+
interface XEndpoint {
|
|
83
|
+
id: string;
|
|
84
|
+
/** `<machine>/<handle>`, handle empty for a machine's human */
|
|
85
|
+
remote: string;
|
|
86
|
+
machine: string;
|
|
87
|
+
handle: string;
|
|
88
|
+
kind: EndpointKind;
|
|
89
|
+
depth: number | null;
|
|
90
|
+
harness: string | null;
|
|
91
|
+
presence: Presence;
|
|
92
|
+
}
|
|
93
|
+
interface MirrorChange {
|
|
94
|
+
view: RemoteView;
|
|
95
|
+
added: boolean;
|
|
96
|
+
presence: boolean;
|
|
97
|
+
status: boolean;
|
|
98
|
+
}
|
|
99
|
+
interface Delivery {
|
|
100
|
+
seq: number;
|
|
101
|
+
/** `<machine>/<handle>` of the recipient, or null for a row of no machine */
|
|
102
|
+
endpoint: string | null;
|
|
103
|
+
/** the recipient's endpoint id */
|
|
104
|
+
recipient: string;
|
|
105
|
+
/** whether the recipient is a row its machine's bridge mirrors, not one a direct client claimed */
|
|
106
|
+
mirrored: boolean;
|
|
107
|
+
message: Envelope;
|
|
108
|
+
}
|
|
109
|
+
/** RelayCore with endpoints of other machines: qualified addresses, mirrored and proxy rows, injection and forwarding records */
|
|
110
|
+
declare class FederatedCore extends RelayCore {
|
|
111
|
+
readonly machine: string | null;
|
|
112
|
+
private aliases;
|
|
113
|
+
private scope;
|
|
114
|
+
private readonly defaultAuth;
|
|
115
|
+
private readonly endpointCap;
|
|
116
|
+
private readonly refusalsPerMinute;
|
|
117
|
+
private readonly refusals;
|
|
118
|
+
private readonly xinbox;
|
|
119
|
+
private xfrom;
|
|
120
|
+
constructor(sql: ConstructorParameters<typeof RelayCore>[0], options: FederatedOptions);
|
|
121
|
+
migrate(): void;
|
|
122
|
+
/** with xinbox, a delivery takes the next seq past xinbox's too, so a bridge's replay and ack cursor stay one space */
|
|
123
|
+
protected insertDelivery(messageId: string, endpoint: string, expiresAt: number): number;
|
|
124
|
+
setAliases(aliases: string[]): void;
|
|
125
|
+
/** runs `fn` with unqualified handles resolving on `machine`: a direct client's own namespace on the hub */
|
|
126
|
+
within<T>(machine: string | null, fn: () => T): T;
|
|
127
|
+
/** the hub-side address (`<machine>/<handle>`) an address names, another account's as it is, or null when it is on this machine */
|
|
128
|
+
route(address: unknown): string | null;
|
|
129
|
+
/** whether an address names an endpoint on another machine or of another account; malformed addresses are not remote */
|
|
130
|
+
isRemote(address: unknown): boolean;
|
|
131
|
+
/** the local endpoint id a hub delivery for `<machine>/<handle>` is for */
|
|
132
|
+
localTarget(target: unknown): string;
|
|
133
|
+
/** the live row of `<machine>/<handle>`, for a bridge acting on its own machine's endpoints */
|
|
134
|
+
remoteEndpoint(machine: string, handle: unknown): string;
|
|
135
|
+
/** the machine an endpoint row belongs to, or null for a row of this host */
|
|
136
|
+
machineOf(endpoint: string): string | null;
|
|
137
|
+
/** `<account>/<machine>/<handle>` of another account (handle empty for its machine's human, `me`), or null for any other address */
|
|
138
|
+
foreignAccount(address: unknown): {
|
|
139
|
+
account: string;
|
|
140
|
+
machine: string;
|
|
141
|
+
handle: string;
|
|
142
|
+
} | null;
|
|
143
|
+
private isLocalUser;
|
|
144
|
+
/** whether an endpoint of another account `part` ever reached this relay, so `<part>/me` is that account's human */
|
|
145
|
+
private knownAccount;
|
|
146
|
+
private locate;
|
|
147
|
+
protected resolve(address: unknown): EndpointRow;
|
|
148
|
+
protected addressUsers(): string[];
|
|
149
|
+
protected localRow(row: EndpointRow): boolean;
|
|
150
|
+
protected address(row: EndpointRow): string;
|
|
151
|
+
protected subject(row: EndpointRow): PolicySubject;
|
|
152
|
+
/** a stand-in for another account's sender is no endpoint of this relay: list, peers and snapshots leave it out */
|
|
153
|
+
protected listable(row: EndpointRow): boolean;
|
|
154
|
+
/** an address of another account (a sender's stand-in, or a target with none) is checked as that account's endpoint */
|
|
155
|
+
protected policySubject(address: string): PolicySubject;
|
|
156
|
+
/**
|
|
157
|
+
* 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
|
|
158
|
+
* selector; its mirrored ancestors are kept so a cap widened by one of them is clipped as on its machine
|
|
159
|
+
*/
|
|
160
|
+
protected scopeSubject(row: EndpointRow): ScopeSubject;
|
|
161
|
+
/** 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 */
|
|
162
|
+
protected scopeNode(row: EndpointRow): ScopeNode;
|
|
163
|
+
/**
|
|
164
|
+
* throws not_found, as send() does, when the scope refuses a local endpoint's message to an address on another machine, a reply
|
|
165
|
+
* (`replyTo`, its parent) to a message it got from that address passing as a solicited one; to another account, the cross-account rule on
|
|
166
|
+
* the sender's out lists, unless it replies to a message it got from exactly that address
|
|
167
|
+
*/
|
|
168
|
+
scopeCheck(from: string, to: unknown, replyTo?: string): void;
|
|
169
|
+
/** whether direction `dir` of `endpoint` admits an endpoint of another account: only an `acct:` selector in each list, or no list at all */
|
|
170
|
+
crossAdmission(endpoint: string, dir: ScopeDirection, peer: ScopeNode | {
|
|
171
|
+
account: string;
|
|
172
|
+
machine: string;
|
|
173
|
+
handle: string;
|
|
174
|
+
}): Admission;
|
|
175
|
+
protected envelope(row: MessageRow, seq?: number, recipient?: string): Envelope;
|
|
176
|
+
/**
|
|
177
|
+
* upserts a machine's endpoints from its views, never over a direct client's claimed row; `full` marks its mirrored rows missing from the
|
|
178
|
+
* 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
|
|
179
|
+
* (a new `incarnation`). A row is written only when something in it changed. `seen` gathers the remotes of a full mirror sent in several
|
|
180
|
+
* frames: each frame adds its own, and the closing `full` one counts every remote in it as listed.
|
|
181
|
+
*/
|
|
182
|
+
mirror(machine: string, views: unknown, options?: {
|
|
183
|
+
full?: boolean;
|
|
184
|
+
auth?: RelayAuth;
|
|
185
|
+
seen?: Set<string>;
|
|
186
|
+
}): MirrorChange[];
|
|
187
|
+
/**
|
|
188
|
+
* a local agent's place, rules and caps as its machine's bridge mirrors them (MirrorScope), with its title; nothing for another row. Caps
|
|
189
|
+
* 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
|
|
190
|
+
*/
|
|
191
|
+
mirrorScope(endpoint: string): MirrorScope & {
|
|
192
|
+
title?: string | null;
|
|
193
|
+
};
|
|
194
|
+
private scopeEpoch;
|
|
195
|
+
/**
|
|
196
|
+
* a direct client's claim of `<machine>/<handle>`, with claim()'s takeover and suffix rules, and its `exact`, `scope`, `allow`, `in` and
|
|
197
|
+
* `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
|
|
198
|
+
* row of the same harness no process or session holds, with no new token, and is refused as claim() refuses one otherwise
|
|
199
|
+
*/
|
|
200
|
+
claimOn(machine: string, claim: Claim, auth?: RelayAuth): {
|
|
201
|
+
endpoint: EndpointView;
|
|
202
|
+
token?: string;
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* resume() for a direct client of `machine`, its labels replaced by what `relabel` makes of them, held to the label limits: a token of
|
|
206
|
+
* another machine's endpoint is refused
|
|
207
|
+
*/
|
|
208
|
+
resumeOn(machine: string, token: string, relabel?: (labels: Record<string, string>) => unknown): EndpointView;
|
|
209
|
+
/** every live row of other machines (or of one machine), as views with their machine */
|
|
210
|
+
remoteViews(filter?: {
|
|
211
|
+
except?: string;
|
|
212
|
+
machine?: string;
|
|
213
|
+
}): RemoteView[];
|
|
214
|
+
remoteRowsOf(machine: string): RemoteView[];
|
|
215
|
+
/** a bridge went away: its mirrored rows show offline */
|
|
216
|
+
markOffline(machine: string): MirrorChange[];
|
|
217
|
+
/**
|
|
218
|
+
* a released machine: its rows are retired like a gone handle and can't be resolved or resumed again; with `queue`,
|
|
219
|
+
* their unread deliveries go too, and each message they leave delivered to no one, unless it is a request or has
|
|
220
|
+
* replies, which askers still look up
|
|
221
|
+
*/
|
|
222
|
+
dropMachine(machine: string, options?: {
|
|
223
|
+
queue?: boolean;
|
|
224
|
+
}): number;
|
|
225
|
+
/**
|
|
226
|
+
* hands a machine's direct-client rows with no connection (or only `endpoint`) to its bridge: their resume tokens are
|
|
227
|
+
* dropped, so the bridge mirrors them, and their unread deliveries get fresh seqs past any bridge's replay cursor;
|
|
228
|
+
* returns those deliveries
|
|
229
|
+
*/
|
|
230
|
+
adoptClaimed(machine: string, endpoint?: string): {
|
|
231
|
+
seq: number;
|
|
232
|
+
endpoint: string;
|
|
233
|
+
}[];
|
|
234
|
+
/** the hop a message from this endpoint would get from what it received lately */
|
|
235
|
+
causalHop(endpoint: string): number;
|
|
236
|
+
/** records that an endpoint received a message of `hop`, as reading its inbox does */
|
|
237
|
+
markReceived(endpoint: string, hop: number): void;
|
|
238
|
+
/**
|
|
239
|
+
* 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
|
|
240
|
+
* proxy row; relay notices always pass. A scope refusal throws not_found, as send() does; a redelivery of an admitted message skips it.
|
|
241
|
+
*/
|
|
242
|
+
inboundRefusal(endpoint: string, message: Envelope): string | null;
|
|
243
|
+
/**
|
|
244
|
+
* a message the hub delivered to a local endpoint, stored under the hub's id from a proxy of its sender; idempotent.
|
|
245
|
+
* Throws inbox_full past the inbox cap, which a redelivery, the first reply to the endpoint's own message (as in
|
|
246
|
+
* send()) and relay notices skip.
|
|
247
|
+
*/
|
|
248
|
+
admit(endpoint: string, message: Envelope): {
|
|
249
|
+
seq: number;
|
|
250
|
+
duplicate: boolean;
|
|
251
|
+
};
|
|
252
|
+
/** 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 */
|
|
253
|
+
inject(endpoint: string, options?: Parameters<RelayCore["inject"]>[1]): ReturnType<RelayCore["inject"]>;
|
|
254
|
+
/** offer() for a local endpoint's channel; another machine's row is offered nothing */
|
|
255
|
+
offer(endpoint: string, options?: Parameters<RelayCore["offer"]>[1]): ReturnType<RelayCore["offer"]>;
|
|
256
|
+
/** stopHeld() for a local endpoint; another machine's row has no hooks */
|
|
257
|
+
stopHeld(endpoint: string, options?: Parameters<RelayCore["stopHeld"]>[1]): boolean;
|
|
258
|
+
/** the endpoint's unacked count when a new delivery from `sender` would pass the inbox cap, else null */
|
|
259
|
+
private inboxFull;
|
|
260
|
+
/** 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 */
|
|
261
|
+
recordForward(from: string, params: SendParams, result: SendResult, origin?: {
|
|
262
|
+
tty?: boolean;
|
|
263
|
+
}): void;
|
|
264
|
+
/** the earlier result of a forwarded send with this client_id, or null; a different message under the same client_id is refused */
|
|
265
|
+
forwarded(from: string, params: SendParams): SendResult | null;
|
|
266
|
+
/** one delivery as the hub pushes it, or null when it is gone, acked or expired */
|
|
267
|
+
delivery(seq: number): Delivery | null;
|
|
268
|
+
/** the unacked deliveries to `machine`'s mirrored rows after `after` (xinbox rows after `xafter`), oldest first */
|
|
269
|
+
pending(machine: string, after: number, limit?: number, xafter?: number): Delivery[];
|
|
270
|
+
/**
|
|
271
|
+
* acks deliveries of `machine`'s mirrored rows only; each counts as received for the causal hop. A seq of xinbox moves its endpoint's
|
|
272
|
+
* cursor instead: one row per endpoint for the whole batch, no audit
|
|
273
|
+
*/
|
|
274
|
+
ackOn(machine: string, seqs: unknown): number;
|
|
275
|
+
/** a delivery `machine`'s local policy refused: acked, and a status notice from the relay to the sender */
|
|
276
|
+
rejectOn(machine: string, seq: unknown, code: unknown): {
|
|
277
|
+
notice: {
|
|
278
|
+
seq: number;
|
|
279
|
+
endpoint: string;
|
|
280
|
+
} | null;
|
|
281
|
+
};
|
|
282
|
+
protected audit(event: string, endpoint: string | null, ref: string | null, detail: Record<string, unknown>): void;
|
|
283
|
+
/** an audit row from outside the core (hub events: machine binds, releases, revocations) */
|
|
284
|
+
record(event: string, endpoint: string | null, ref: string | null, detail: Record<string, unknown>): void;
|
|
285
|
+
/**
|
|
286
|
+
* drops what expired, and with xinbox the rows received over XINBOX_KEEP_MS ago that expired or were acked that long ago, keeping
|
|
287
|
+
* the highest seq they held as the floor of the next one
|
|
288
|
+
*/
|
|
289
|
+
sweep(): void;
|
|
290
|
+
/** the mirrored row of `<machine>/<handle>` (a bridge's, never a direct client's), or null */
|
|
291
|
+
xendpoint(remote: string): XEndpoint | null;
|
|
292
|
+
/** the mirrored row an endpoint id is, or null for anything else */
|
|
293
|
+
xsender(endpoint: string): XEndpoint | null;
|
|
294
|
+
/** every mirrored row, for discovery */
|
|
295
|
+
xendpoints(): XEndpoint[];
|
|
296
|
+
/** the xinbox row `id` delivered to `<machine>/<handle>`, or null */
|
|
297
|
+
xreceived(endpoint: string, id: string): XInboxRow | null;
|
|
298
|
+
/** the earlier row of a message from `fromAccount` under `clientId`, or null */
|
|
299
|
+
xduplicate(fromAccount: string, clientId: string): {
|
|
300
|
+
id: string;
|
|
301
|
+
acked: boolean;
|
|
302
|
+
} | null;
|
|
303
|
+
/** what `fromAccount` sent here: from `sender` to `endpoint` in the last minute and hour (each with its oldest), and since `since` */
|
|
304
|
+
xcounts(fromAccount: string, sender: string, endpoint: string, since: number): {
|
|
305
|
+
minute: number;
|
|
306
|
+
minuteAt: number | null;
|
|
307
|
+
hour: number;
|
|
308
|
+
hourAt: number | null;
|
|
309
|
+
day: number;
|
|
310
|
+
};
|
|
311
|
+
/** an endpoint's unread messages: its unacked, unexpired xinbox rows and deliveries */
|
|
312
|
+
xunread(endpoint: XEndpoint): number;
|
|
313
|
+
/** the accounts whose xinbox rows may be read out now; null (unknown) reads none */
|
|
314
|
+
xsenders(accounts: Set<string> | null): void;
|
|
315
|
+
/** deletes the xinbox rows of every account but `keep`; how many it deleted */
|
|
316
|
+
xprune(keep: Set<string>): number;
|
|
317
|
+
xthreadCount(endpoint: string, thread: string): number;
|
|
318
|
+
/** one xinbox row under the next seq; null when a row of the same sender and client_id is there already */
|
|
319
|
+
xinsert(row: Omit<XInboxRow, "seq">): number | null;
|
|
320
|
+
/** a value of the cross-account state, written once */
|
|
321
|
+
xstate(key: string, init: () => string): string;
|
|
322
|
+
private xview;
|
|
323
|
+
/** unacked, unexpired xinbox rows of mirrored endpoints from the accounts xsenders() named, as deliveries, by `where` over `x` */
|
|
324
|
+
private xinboxPage;
|
|
325
|
+
/** the endpoint of an unacked xinbox seq of `machine`, or null */
|
|
326
|
+
private ownXinbox;
|
|
327
|
+
/**
|
|
328
|
+
* 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
|
|
329
|
+
* a row it does not name stays unacked and replays. How many rows it moved over
|
|
330
|
+
*/
|
|
331
|
+
private advanceCursor;
|
|
332
|
+
/** when the holder part of a mirrored agent's incarnation moved, drops its xinbox rows and deliveries: they were the earlier holder's */
|
|
333
|
+
private dropEarlierHolder;
|
|
334
|
+
/** keeps the highest xinbox seq as the floor of the next one, before rows are deleted */
|
|
335
|
+
private xfloor;
|
|
336
|
+
private ownDelivery;
|
|
337
|
+
private noticeTo;
|
|
338
|
+
private forwardHash;
|
|
339
|
+
/** 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 */
|
|
340
|
+
private checkForeignIn;
|
|
341
|
+
/** the stand-in row of another account's sender: `@<account>/<machine>/<handle>`, or `@<account>/me` for its human at a terminal */
|
|
342
|
+
private foreignProxy;
|
|
343
|
+
private proxyFor;
|
|
344
|
+
private checkEnvelope;
|
|
345
|
+
private parseView;
|
|
346
|
+
private diff;
|
|
347
|
+
private remoteView;
|
|
348
|
+
/**
|
|
349
|
+
* 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
|
|
350
|
+
* parent, which must be an agent this machine mirrors; anything else (a parent on another machine or not mirrored, a cycle, a root
|
|
351
|
+
* claiming depth) is unknown. Undefined when the view says nothing about its place.
|
|
352
|
+
*/
|
|
353
|
+
private placer;
|
|
354
|
+
/** 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 */
|
|
355
|
+
private scopeColumns;
|
|
356
|
+
/** 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 */
|
|
357
|
+
private localCaps;
|
|
358
|
+
/** caps with this core's row ids named as `ep:<handle>`, as they cross machines */
|
|
359
|
+
private wireCaps;
|
|
360
|
+
private insertRemote;
|
|
361
|
+
private retire;
|
|
362
|
+
/** a new proxy row, within the endpoint cap */
|
|
363
|
+
private insertProxy;
|
|
364
|
+
private checkCap;
|
|
365
|
+
private checkMachine;
|
|
366
|
+
private byRemote;
|
|
367
|
+
/** a judge status notice to `endpoint` from the relay; its delivery seq */
|
|
368
|
+
judgeNoticeTo(endpoint: string, text: string): number;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
type HubAuth = "account" | "account+bridge";
|
|
372
|
+
type HubRole = "owner" | "admin" | "member";
|
|
373
|
+
/** what the host proved about the token, plus its privilege */
|
|
374
|
+
interface HubGrant {
|
|
375
|
+
tokenId: string;
|
|
376
|
+
userId: string;
|
|
377
|
+
actorId: string | null;
|
|
378
|
+
expiresAt: number;
|
|
379
|
+
/** a scoped token, not a full login; a grant that doesn't say false counts as scoped */
|
|
380
|
+
scoped: boolean;
|
|
381
|
+
/** the actor's org role; null on a personal account */
|
|
382
|
+
role: HubRole | null;
|
|
383
|
+
/** labels set on every endpoint the token claims or mirrors, over the client's own for the same keys */
|
|
384
|
+
labels?: Record<string, string>;
|
|
385
|
+
}
|
|
386
|
+
/** per-socket state; JSON-serialisable, so the host can persist it with the socket */
|
|
387
|
+
interface HubConn {
|
|
388
|
+
id: string;
|
|
389
|
+
grant: HubGrant;
|
|
390
|
+
role: "new" | "client" | "bridge" | "observer";
|
|
391
|
+
/** the machine a bridge or agent client is bound to; a human's or observer's namespace for unqualified handles */
|
|
392
|
+
machine: string | null;
|
|
393
|
+
/** the hub endpoint id a client connection acts as */
|
|
394
|
+
endpoint: string | null;
|
|
395
|
+
watching: boolean;
|
|
396
|
+
subscribed: boolean;
|
|
397
|
+
/** whether a human client said a terminal is attached */
|
|
398
|
+
tty?: boolean;
|
|
399
|
+
}
|
|
400
|
+
/** a bound machine, or a revoked token's name kept as a tombstone */
|
|
401
|
+
interface MachineRow {
|
|
402
|
+
name: string;
|
|
403
|
+
kind: string;
|
|
404
|
+
online: boolean;
|
|
405
|
+
bridge: boolean;
|
|
406
|
+
created_at: string | null;
|
|
407
|
+
last_seen: string | null;
|
|
408
|
+
}
|
|
409
|
+
interface HubSocket {
|
|
410
|
+
conn: HubConn;
|
|
411
|
+
send(text: string): void;
|
|
412
|
+
close(code: number, reason: string): void;
|
|
413
|
+
}
|
|
414
|
+
interface HubDeps {
|
|
415
|
+
sql: Sql;
|
|
416
|
+
/** synchronous sha256 hex */
|
|
417
|
+
hash: (value: string) => string;
|
|
418
|
+
/** runs fn in one synchronous storage transaction */
|
|
419
|
+
transaction?: <T>(fn: () => T) => T;
|
|
420
|
+
now?: () => number;
|
|
421
|
+
/** every live socket of this account (rebuilt from persisted state when the host restarts) */
|
|
422
|
+
sockets(): HubSocket[];
|
|
423
|
+
/** persist socket.conn after the hub changed it */
|
|
424
|
+
save(socket: HubSocket): void;
|
|
425
|
+
/** structured log line, e.g. relay_action; never bodies or tokens */
|
|
426
|
+
log?: (event: string, fields: Record<string, unknown>) => void;
|
|
427
|
+
/** overrides of {@link HUB_LIMITS}; `Infinity` disables a cap, count or byte limit, durations stay finite */
|
|
428
|
+
limits?: Partial<HubLimits>;
|
|
429
|
+
/** the link to other accounts' relays; without it an address of another account is not found */
|
|
430
|
+
crossAccount?: CrossAccountLink;
|
|
431
|
+
}
|
|
432
|
+
declare const HUB_LIMITS: {
|
|
433
|
+
readonly machines: 16;
|
|
434
|
+
readonly endpoints: 512;
|
|
435
|
+
readonly sockets: 64;
|
|
436
|
+
readonly frameBytes: 262144;
|
|
437
|
+
readonly callsPerMinute: 120;
|
|
438
|
+
readonly bridgeCallsPerMinute: 600;
|
|
439
|
+
readonly accountCallsPerMinute: 1200;
|
|
440
|
+
readonly acksPerMinute: 1200;
|
|
441
|
+
readonly accountAcksPerMinute: 4800;
|
|
442
|
+
readonly storedBytes: number;
|
|
443
|
+
readonly clientMachineIdleS: number;
|
|
444
|
+
readonly idlePurgeS: number;
|
|
445
|
+
readonly httpWaitS: 25;
|
|
446
|
+
readonly revalidateMs: 300000;
|
|
447
|
+
readonly deniedS: 3600;
|
|
448
|
+
readonly refusalsPerMinute: 10;
|
|
449
|
+
};
|
|
450
|
+
type HubLimits = {
|
|
451
|
+
[K in keyof typeof HUB_LIMITS]: number;
|
|
452
|
+
};
|
|
453
|
+
/** the hosted relay of one account: machines, bridges, direct clients, push and replay over a FederatedCore; pure, sockets are injected */
|
|
454
|
+
declare class RelayHub {
|
|
455
|
+
private readonly deps;
|
|
456
|
+
private core;
|
|
457
|
+
private account;
|
|
458
|
+
private readonly now;
|
|
459
|
+
private readonly waiters;
|
|
460
|
+
private readonly buckets;
|
|
461
|
+
private readonly transient;
|
|
462
|
+
private readonly gone;
|
|
463
|
+
/** per machine, the full mirror its bridge is sending in several frames; lost on eviction, which only skips that sync's offline sweep */
|
|
464
|
+
private readonly syncs;
|
|
465
|
+
private stored;
|
|
466
|
+
private denied;
|
|
467
|
+
private xcounters;
|
|
468
|
+
private xkey;
|
|
469
|
+
private readonly limits;
|
|
470
|
+
constructor(deps: HubDeps);
|
|
471
|
+
/** idempotent: migrates and records the account handle (the address user part, e.g. "ada"); call before handling each request */
|
|
472
|
+
init(account: {
|
|
473
|
+
user: string;
|
|
474
|
+
}): void;
|
|
475
|
+
/** forgets the core and in-memory state after the object's storage was deleted; the next init migrates afresh */
|
|
476
|
+
reset(): void;
|
|
477
|
+
/** a fresh connection state for an accepted socket */
|
|
478
|
+
static connection(grant: HubGrant): HubConn;
|
|
479
|
+
/** a socket would exceed the `sockets` limit: the host refuses it (429) instead of accepting */
|
|
480
|
+
full(): boolean;
|
|
481
|
+
/** one JSON-RPC text frame from a socket; replies and pushes go out through socket.send */
|
|
482
|
+
message(socket: HubSocket, text: string): Promise<void>;
|
|
483
|
+
/** the socket closed (any reason) */
|
|
484
|
+
closed(socket: HubSocket): void;
|
|
485
|
+
/** POST /v1/relay/rpc: a JSON-RPC request or batch run in order on a short-lived connection; returns the response(s) */
|
|
486
|
+
batch(grant: HubGrant, body: unknown): Promise<unknown>;
|
|
487
|
+
/**
|
|
488
|
+
* closes every socket (and in-flight /rpc call) whose grant matches with 1008, audits it, and unbinds the machines
|
|
489
|
+
* the matching grants bound behind a tombstone of their member, keeping their endpoint rows and queued messages;
|
|
490
|
+
* returns how many closed
|
|
491
|
+
*/
|
|
492
|
+
revoke(match: {
|
|
493
|
+
tokenId?: string;
|
|
494
|
+
userId?: string;
|
|
495
|
+
actorId?: string;
|
|
496
|
+
}): number;
|
|
497
|
+
/**
|
|
498
|
+
* the host's alarm: revalidates each socket's grant and sweeps; returns the next alarm time in ms, or null. A failed
|
|
499
|
+
* check closes with 1008, an expired grant with 4001 (reconnect for a new one). A check that throws keeps the
|
|
500
|
+
* sockets and retries in 30 s; once a token's checks kept throwing for the `revalidateMs` limit its sockets close
|
|
501
|
+
* with the retryable 1013.
|
|
502
|
+
*/
|
|
503
|
+
alarm(validate: (grant: HubGrant) => Promise<boolean>): Promise<number | null>;
|
|
504
|
+
/** when the next alarm should fire (earliest grant expiry, revalidation or its retry after an error, sweep, retention), or null when the hub holds nothing */
|
|
505
|
+
nextAlarm(): number | null;
|
|
506
|
+
/** the account held no machines, tombstones, sockets or queued messages for the `idlePurgeS` limit: the object may delete its storage */
|
|
507
|
+
purgeDue(): boolean;
|
|
508
|
+
/** while the hub holds rows: the earliest unacked delivery's expiry, at most a day and at least a minute away */
|
|
509
|
+
private retentionAlarm;
|
|
510
|
+
/** no machine bound or tombstoned, no live socket and nothing queued */
|
|
511
|
+
private idle;
|
|
512
|
+
private get relay();
|
|
513
|
+
private handle;
|
|
514
|
+
/**
|
|
515
|
+
* one call from the token's bucket for the connection's role (a bridge, or a bridge hello, has its own, larger one)
|
|
516
|
+
* and from the account's; acks (one token per seq) and rejects spend from separate ack buckets of the token and the
|
|
517
|
+
* account instead, so they never starve calls, and an activity event is refused once either call bucket is below a
|
|
518
|
+
* quarter, so sends keep the rest
|
|
519
|
+
*/
|
|
520
|
+
private spend;
|
|
521
|
+
private bucket;
|
|
522
|
+
private dispatch;
|
|
523
|
+
private hello;
|
|
524
|
+
/** refuses a machine bound to another token, a revoked token's machine to anyone but its member or an admin, or one past the machine cap; returns whether this token already holds it */
|
|
525
|
+
private bindable;
|
|
526
|
+
/** binds the machine to the token once its hello succeeded; `held` is what bindable() returned */
|
|
527
|
+
private bind;
|
|
528
|
+
private clientMethod;
|
|
529
|
+
private bridgeMethod;
|
|
530
|
+
/**
|
|
531
|
+
* A bridge's forward to another account's endpoint, writing no row here: shape, the contact, the sender on this account's allowlist,
|
|
532
|
+
* its scope (or the reply grant of the message it answers), the caps, the scrubber, the egress gate, then the peer relay. Anything
|
|
533
|
+
* unresolvable is not_found, as for a missing endpoint.
|
|
534
|
+
*/
|
|
535
|
+
private xsend;
|
|
536
|
+
/**
|
|
537
|
+
* A message from the account `fromAccountId`'s relay (the host's internal route, never a client): the contact, the target on this
|
|
538
|
+
* account's allowlist, its scope (or the reply grant), shape, a retry's earlier answer, the scrubber, the ingress gate, then one xinbox
|
|
539
|
+
* row pushed to the target's bridge. Anything unresolvable is not_found, as for a missing endpoint.
|
|
540
|
+
*/
|
|
541
|
+
xreceive(fromAccountId: string, raw: unknown, judged?: boolean): Promise<XDelivered>;
|
|
542
|
+
/** this account's endpoints the account `fromAccountId` may reach under its contact's allowlist: address, harness and presence only */
|
|
543
|
+
xlist(fromAccountId: string): Promise<XListed[]>;
|
|
544
|
+
/**
|
|
545
|
+
* throws when one more message from `fromAccountId` would pass a cap of this side: the pair a minute and an hour and the contact a
|
|
546
|
+
* day (the lower of this account's limits for the contact and the sender's declared ones), the thread, the account's stored bytes or
|
|
547
|
+
* the target's unread messages; counted from xinbox itself, so a wake keeps them
|
|
548
|
+
*/
|
|
549
|
+
private xadmit;
|
|
550
|
+
/**
|
|
551
|
+
* deletes the queued messages of every account that is no longer an active contact allowed to message this one, after the
|
|
552
|
+
* contacts changed; nothing when the contacts cannot be loaded
|
|
553
|
+
*/
|
|
554
|
+
xprune(): Promise<void>;
|
|
555
|
+
/** the accounts whose messages this account takes now: active contacts with dir in or both; null when they cannot be loaded */
|
|
556
|
+
private xinbound;
|
|
557
|
+
/** what every contact this account may message lets it reach */
|
|
558
|
+
private contactList;
|
|
559
|
+
private delivered;
|
|
560
|
+
/** the body's bytes when the account can hold them; refuses a send past the `storedBytes` limit */
|
|
561
|
+
private room;
|
|
562
|
+
/** bytes of message bodies held, and of the messages from other accounts */
|
|
563
|
+
private storedNow;
|
|
564
|
+
private stash;
|
|
565
|
+
/** wakes direct waiters and pushes a delivery for a mirrored row to the subscribed bridge of its machine */
|
|
566
|
+
private route;
|
|
567
|
+
/** a machine's endpoint changes: `endpoints` to the other bridges, presence and status to watchers */
|
|
568
|
+
private announce;
|
|
569
|
+
/** a direct client's endpoint changed: the other bridges mirror it */
|
|
570
|
+
private announceEndpoint;
|
|
571
|
+
private toBridges;
|
|
572
|
+
private fleet;
|
|
573
|
+
private write;
|
|
574
|
+
private save;
|
|
575
|
+
private waitMs;
|
|
576
|
+
private wake;
|
|
577
|
+
private waitFor;
|
|
578
|
+
private awaitReply;
|
|
579
|
+
private kick;
|
|
580
|
+
private release;
|
|
581
|
+
/** the machine a bridge or agent client connection holds; a human's or observer's machine is only a namespace */
|
|
582
|
+
private boundMachine;
|
|
583
|
+
/** machines a live connection holds */
|
|
584
|
+
private liveMachines;
|
|
585
|
+
/** bindings, then names a revoke left behind (kind `revoked`) */
|
|
586
|
+
/** a read-only view of the account for a dashboard: its machines, listed endpoints and latest activity */
|
|
587
|
+
overview(activity?: number): {
|
|
588
|
+
machines: MachineRow[];
|
|
589
|
+
endpoints: EndpointView[];
|
|
590
|
+
activity: ActivityRow[];
|
|
591
|
+
};
|
|
592
|
+
private machineList;
|
|
593
|
+
/** only the token the machine is bound to, a full login of the member who bound it, or an admin may release it (or a revoked token's name) */
|
|
594
|
+
private releaseMachine;
|
|
595
|
+
/** releases client bindings with no live connection, idle for the `clientMachineIdleS` limit, with nothing queued and no unanswered ask open; bridge bindings never expire */
|
|
596
|
+
private expireMachines;
|
|
597
|
+
/** drops the tombstones past their age with nothing queued for their machine, and retires the rows and xinbox of each name still unbound */
|
|
598
|
+
private forgetTombstones;
|
|
599
|
+
/** the hub_state keys (`denied:<tokenId>`) of tokens revoked lately, to when; loaded once per object lifetime */
|
|
600
|
+
private deniedGrants;
|
|
601
|
+
/** a token revoked lately is refused even when the host accepted its grant before the revoke reached the object */
|
|
602
|
+
private refuseDenied;
|
|
603
|
+
private tx;
|
|
604
|
+
private state;
|
|
605
|
+
private setState;
|
|
606
|
+
/**
|
|
607
|
+
* a held cross-account message its account's human released (the host claimed the hold first): the rest of that side's path, with
|
|
608
|
+
* every check run again and only the gate skipped. Egress sends as the stored sender with the time left, under the hold id its sender
|
|
609
|
+
* got, and with the message's own client id, so the peer answers both a retried release and the sender's retry of the original as the
|
|
610
|
+
* duplicate. Ingress runs xreceive again
|
|
611
|
+
*/
|
|
612
|
+
xrelease(hold: XRelease): Promise<SendResult | XDelivered>;
|
|
613
|
+
/** a status message from the relay to one of this account's endpoints (a held message's sender), pushed to its bridge */
|
|
614
|
+
xnotice(endpoint: string, text: string): void;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export { type Delivery, FederatedCore, type FederatedOptions, HUB_LIMITS, type HubAuth, type HubConn, type HubDeps, type HubGrant, type HubLimits, type HubRole, type HubSocket, MACHINE_NAME, type MachineRow, type MirrorCaps, type MirrorChange, type MirrorRules, type MirrorScope, type MirrorView, type RelayAuth, RelayHub, type RemoteView, type XEndpoint };
|