@scp-dev/hub 0.5.0 → 0.6.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.
- package/dist/index.d.ts +18 -1
- package/dist/index.js +80 -6
- package/package.json +2 -2
- package/src/federation.ts +93 -3
- package/src/hub.ts +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _scp_dev_core from '@scp-dev/core';
|
|
2
|
-
import { Envelope, RelayCore, CoreOptions, EndpointRow, PolicySubject, ScopeSubject, ScopeNode, ScopeDirection, Admission, MessageRow,
|
|
2
|
+
import { Envelope, RelayCore, CoreOptions, EndpointRow, EndpointView, PolicySubject, ScopeSubject, ScopeNode, ScopeDirection, Admission, MessageRow, ScopeCap, ScopeEntry, Claim, SendParams, SendResult, EndpointKind, Presence, StatusState, Verdict, JudgeModelConfig, JudgeClient, Sql, JudgePolicy, Wake, CrossAccountLink, XDelivered, XListed, ActivityRow, XRelease } from '@scp-dev/core';
|
|
3
3
|
|
|
4
4
|
type RelayAuth = Envelope["from"]["auth"];
|
|
5
5
|
/** a machine name: one relay host of an account */
|
|
@@ -36,6 +36,8 @@ interface MirrorScope {
|
|
|
36
36
|
parent_ep?: string | null;
|
|
37
37
|
/** 0 for a root; null when not known */
|
|
38
38
|
depth?: number | null;
|
|
39
|
+
/** a parent on another machine of the account, `<machine>/<handle>`; null for none */
|
|
40
|
+
parent_addr?: string | null;
|
|
39
41
|
rules?: MirrorRules | null;
|
|
40
42
|
caps?: MirrorCaps | null;
|
|
41
43
|
/**
|
|
@@ -162,6 +164,19 @@ declare class FederatedCore extends RelayCore {
|
|
|
162
164
|
protected resolve(address: unknown): EndpointRow;
|
|
163
165
|
protected addressUsers(): string[];
|
|
164
166
|
protected localRow(row: EndpointRow): boolean;
|
|
167
|
+
/** `<machine>/<handle>` of a row of this account: a mirrored row's remote, a local one's on this machine; null for another account's or without a machine */
|
|
168
|
+
private keyOf;
|
|
169
|
+
/** the row `<machine>/<handle>` names: a local one on this machine, else a mirrored one */
|
|
170
|
+
private byKey;
|
|
171
|
+
/** whether walking up from `start` (local and mirrored parents, then a link across machines) reaches a row whose key or id is in `targets` */
|
|
172
|
+
private reachesUp;
|
|
173
|
+
/** a parent on another machine is a mirrored agent of this account that neither is nor sits under the child or one of its descendants */
|
|
174
|
+
protected linkAcross(row: EndpointRow, parent: EndpointRow): string;
|
|
175
|
+
protected renamedHandle(from: string, to: string): void;
|
|
176
|
+
protected linkedAcross(a: EndpointRow, b: EndpointRow): boolean;
|
|
177
|
+
protected linkedParent(row: EndpointRow, shown?: Set<string>): string | null;
|
|
178
|
+
/** a live mirrored row shows the presence its machine mirrored, not one its last_seen decays to here */
|
|
179
|
+
protected view(row: EndpointRow, shown?: Set<string>): EndpointView;
|
|
165
180
|
protected address(row: EndpointRow): string;
|
|
166
181
|
protected subject(row: EndpointRow): PolicySubject;
|
|
167
182
|
/** a stand-in for another account's sender is no endpoint of this relay: list, peers and snapshots leave it out */
|
|
@@ -373,6 +388,8 @@ declare class FederatedCore extends RelayCore {
|
|
|
373
388
|
private placer;
|
|
374
389
|
/** 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 */
|
|
375
390
|
private scopeColumns;
|
|
391
|
+
/** whether a link of `row` under `key` closes a cycle in what this relay mirrors */
|
|
392
|
+
private loops;
|
|
376
393
|
/** 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 */
|
|
377
394
|
private localCaps;
|
|
378
395
|
/** caps with this core's row ids named as `ep:<handle>`, as they cross machines */
|
package/dist/index.js
CHANGED
|
@@ -90,6 +90,7 @@ var FederatedCore = class extends RelayCore {
|
|
|
90
90
|
if (!columns.includes("incarnation")) this.sql.all(`ALTER TABLE endpoints ADD COLUMN incarnation TEXT`);
|
|
91
91
|
if (!columns.includes("scope_epoch")) this.sql.all(`ALTER TABLE endpoints ADD COLUMN scope_epoch INTEGER`);
|
|
92
92
|
if (!columns.includes("mirrored_caps")) this.sql.all(`ALTER TABLE endpoints ADD COLUMN mirrored_caps TEXT`);
|
|
93
|
+
if (!columns.includes("mirrored_presence")) this.sql.all(`ALTER TABLE endpoints ADD COLUMN mirrored_presence TEXT`);
|
|
93
94
|
this.sql.all(`CREATE UNIQUE INDEX IF NOT EXISTS endpoints_remote ON endpoints (remote) WHERE remote IS NOT NULL`);
|
|
94
95
|
if (this.xinbox) for (const statement of XACCT_MIGRATION) this.sql.all(statement);
|
|
95
96
|
}
|
|
@@ -222,6 +223,63 @@ var FederatedCore = class extends RelayCore {
|
|
|
222
223
|
localRow(row) {
|
|
223
224
|
return !row.remote;
|
|
224
225
|
}
|
|
226
|
+
/** `<machine>/<handle>` of a row of this account: a mirrored row's remote, a local one's on this machine; null for another account's or without a machine */
|
|
227
|
+
keyOf(row) {
|
|
228
|
+
const remote = row.remote;
|
|
229
|
+
if (remote) return isForeign(remote) ? null : remote;
|
|
230
|
+
return this.machine === null ? null : `${this.machine}/${row.handle}`;
|
|
231
|
+
}
|
|
232
|
+
/** the row `<machine>/<handle>` names: a local one on this machine, else a mirrored one */
|
|
233
|
+
byKey(key) {
|
|
234
|
+
if (!REMOTE.test(key)) return void 0;
|
|
235
|
+
const handle = handlePart(key);
|
|
236
|
+
if (machinePart(key) === this.machine) return validHandle(handle) ? this.byHandle(handle) : void 0;
|
|
237
|
+
return this.byRemote(key);
|
|
238
|
+
}
|
|
239
|
+
/** whether walking up from `start` (local and mirrored parents, then a link across machines) reaches a row whose key or id is in `targets` */
|
|
240
|
+
reachesUp(start, targets) {
|
|
241
|
+
const seen = /* @__PURE__ */ new Set();
|
|
242
|
+
let row = start;
|
|
243
|
+
while (row && !seen.has(row.id) && seen.size <= LIMITS.depthMax * 4) {
|
|
244
|
+
seen.add(row.id);
|
|
245
|
+
const key = this.keyOf(row);
|
|
246
|
+
if (targets.has(row.id) || key !== null && targets.has(key)) return true;
|
|
247
|
+
row = row.parent_ep ? this.byId(row.parent_ep) : row.parent_addr ? this.byKey(row.parent_addr) : void 0;
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
/** a parent on another machine is a mirrored agent of this account that neither is nor sits under the child or one of its descendants */
|
|
252
|
+
linkAcross(row, parent) {
|
|
253
|
+
const remote = parent.remote;
|
|
254
|
+
if (!remote || isForeign(remote) || parent.kind !== "agent" || handlePart(remote) === "")
|
|
255
|
+
throw new RelayError("invalid_params", "the parent is an agent endpoint of this account");
|
|
256
|
+
if (row.parent_addr === remote) return remote;
|
|
257
|
+
const below = this.sql.all(`SELECT id FROM endpoints WHERE tree = ?`, row.id).map((r) => r.id);
|
|
258
|
+
const key = this.keyOf(row);
|
|
259
|
+
if (this.reachesUp(parent, /* @__PURE__ */ new Set([row.id, ...below, ...key === null ? [] : [key]])))
|
|
260
|
+
throw new RelayError("scope_refused", `${this.address(parent)} sits under ${this.address(row)}: a session cannot go under its own descendant`);
|
|
261
|
+
return remote;
|
|
262
|
+
}
|
|
263
|
+
renamedHandle(from, to) {
|
|
264
|
+
if (this.machine !== null) this.sql.all(`UPDATE endpoints SET parent_addr = ? WHERE parent_addr = ?`, `${this.machine}/${to}`, `${this.machine}/${from}`);
|
|
265
|
+
}
|
|
266
|
+
linkedAcross(a, b) {
|
|
267
|
+
const [ka, kb] = [this.keyOf(a), this.keyOf(b)];
|
|
268
|
+
return a.parent_addr !== null && a.parent_addr === kb || b.parent_addr !== null && b.parent_addr === ka;
|
|
269
|
+
}
|
|
270
|
+
linkedParent(row, shown) {
|
|
271
|
+
const key = row.parent_addr;
|
|
272
|
+
if (!key) return null;
|
|
273
|
+
const parent = this.byKey(key);
|
|
274
|
+
if (parent) return shown && !shown.has(parent.id) ? null : this.address(parent);
|
|
275
|
+
return shown ? null : this.remoteAddress(key);
|
|
276
|
+
}
|
|
277
|
+
/** a live mirrored row shows the presence its machine mirrored, not one its last_seen decays to here */
|
|
278
|
+
view(row, shown) {
|
|
279
|
+
const view = super.view(row, shown);
|
|
280
|
+
const { remote, mirrored_presence: mirrored } = row;
|
|
281
|
+
return remote && row.connections > 0 && (mirrored === "online" || mirrored === "idle") ? { ...view, presence: mirrored } : view;
|
|
282
|
+
}
|
|
225
283
|
address(row) {
|
|
226
284
|
const remote = row.remote;
|
|
227
285
|
if (remote && isForeign(remote)) return remote.slice(1);
|
|
@@ -369,6 +427,7 @@ var FederatedCore = class extends RelayCore {
|
|
|
369
427
|
const old = !before && view.was !== null ? this.byRemote(`${machine}/${view.was}`) : void 0;
|
|
370
428
|
if (old && old.token_hash === null) {
|
|
371
429
|
this.sql.all(`UPDATE endpoints SET remote = ? WHERE id = ?`, remote, old.id);
|
|
430
|
+
this.sql.all(`UPDATE endpoints SET parent_addr = ? WHERE parent_addr = ?`, remote, `${machine}/${view.was}`);
|
|
372
431
|
before = this.byId(old.id);
|
|
373
432
|
renamed.set(old.id, view.was);
|
|
374
433
|
}
|
|
@@ -391,6 +450,7 @@ var FederatedCore = class extends RelayCore {
|
|
|
391
450
|
status_note: view.status?.note ?? null,
|
|
392
451
|
token_hash: null,
|
|
393
452
|
connections: live ? 1 : 0,
|
|
453
|
+
mirrored_presence: view.presence,
|
|
394
454
|
last_seen: view.lastSeen ?? (live ? now : before?.last_seen ?? null),
|
|
395
455
|
auth: auth ?? row.auth,
|
|
396
456
|
...view.kind === "agent" ? this.scopeColumns(machine, view.scope, row, place(view.handle)) : {}
|
|
@@ -451,6 +511,7 @@ var FederatedCore = class extends RelayCore {
|
|
|
451
511
|
tree: root?.handle ?? null,
|
|
452
512
|
parent_ep: parent?.handle ?? null,
|
|
453
513
|
depth: row.depth ?? 0,
|
|
514
|
+
parent_addr: row.parent_addr ?? null,
|
|
454
515
|
rules: Object.keys(rules).length ? rules : null,
|
|
455
516
|
caps: wire,
|
|
456
517
|
title: row.title ?? null,
|
|
@@ -1286,9 +1347,7 @@ var FederatedCore = class extends RelayCore {
|
|
|
1286
1347
|
const was = this.view(before);
|
|
1287
1348
|
const presence = was.presence !== view.presence;
|
|
1288
1349
|
const status = before.status_state !== after.status_state || before.status_note !== after.status_note;
|
|
1289
|
-
const other = ["kind", "harness", "model", "cwd", "labels", "title", "wake", "tree", "parent_ep", "depth", "rules", "caps", "incarnation"].some(
|
|
1290
|
-
(field) => before[field] !== after[field]
|
|
1291
|
-
);
|
|
1350
|
+
const other = ["kind", "harness", "model", "cwd", "labels", "title", "wake", "tree", "parent_ep", "parent_addr", "depth", "rules", "caps", "incarnation"].some((field) => before[field] !== after[field]);
|
|
1292
1351
|
return presence || status || other ? { view, added: false, presence, status } : null;
|
|
1293
1352
|
}
|
|
1294
1353
|
remoteView(row) {
|
|
@@ -1304,6 +1363,7 @@ var FederatedCore = class extends RelayCore {
|
|
|
1304
1363
|
tree: row.depth === null ? null : row.tree ? handle(row.tree) : remote ? handlePart(remote) : row.handle,
|
|
1305
1364
|
parent_ep: handle(row.parent_ep),
|
|
1306
1365
|
depth: row.depth,
|
|
1366
|
+
parent_addr: row.parent_addr ?? null,
|
|
1307
1367
|
rules: row.rules ? JSON.parse(row.rules) : null,
|
|
1308
1368
|
caps: row.caps ? this.wireCaps(JSON.parse(row.caps)) : null,
|
|
1309
1369
|
incarnation: row.incarnation ?? null
|
|
@@ -1349,8 +1409,10 @@ var FederatedCore = class extends RelayCore {
|
|
|
1349
1409
|
caps = merged.caps.length || Object.keys(merged.allow).length ? JSON.stringify(merged) : null;
|
|
1350
1410
|
}
|
|
1351
1411
|
const rules = scope.rules !== void 0 ? scope.rules : fresh ? null : void 0;
|
|
1412
|
+
const linked = scope.parentAddr !== void 0 ? scope.parentAddr : fresh ? null : row.parent_addr;
|
|
1352
1413
|
return {
|
|
1353
1414
|
...place,
|
|
1415
|
+
parent_addr: linked !== null && (place.parent_ep !== null || this.loops(row, linked)) ? null : linked,
|
|
1354
1416
|
rules: rules === void 0 ? row.rules : rules && (rules.in || rules.out) ? JSON.stringify(rules) : null,
|
|
1355
1417
|
caps,
|
|
1356
1418
|
title: scope.title !== void 0 ? scope.title : fresh ? null : row.title,
|
|
@@ -1358,6 +1420,12 @@ var FederatedCore = class extends RelayCore {
|
|
|
1358
1420
|
incarnation: scope.incarnation ?? row.incarnation
|
|
1359
1421
|
};
|
|
1360
1422
|
}
|
|
1423
|
+
/** whether a link of `row` under `key` closes a cycle in what this relay mirrors */
|
|
1424
|
+
loops(row, key) {
|
|
1425
|
+
const parent = this.byKey(key);
|
|
1426
|
+
const own = this.keyOf(row);
|
|
1427
|
+
return parent !== void 0 && this.reachesUp(parent, /* @__PURE__ */ new Set([row.id, ...own === null ? [] : [own]]));
|
|
1428
|
+
}
|
|
1361
1429
|
/** 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 */
|
|
1362
1430
|
localCaps(machine, caps) {
|
|
1363
1431
|
const ref = (value) => {
|
|
@@ -1480,6 +1548,12 @@ function parseScope(view) {
|
|
|
1480
1548
|
if (view.depth !== null && (!Number.isInteger(view.depth) || view.depth < 0 || view.depth > 1e3)) throw bad("depth");
|
|
1481
1549
|
out.depth = view.depth ?? null;
|
|
1482
1550
|
}
|
|
1551
|
+
if (given("parent_addr")) {
|
|
1552
|
+
const key = view.parent_addr;
|
|
1553
|
+
if (key !== null && (typeof key !== "string" || !REMOTE.test(key) || !MACHINE_NAME.test(machinePart(key)) || !validHandle(handlePart(key))))
|
|
1554
|
+
throw bad("parent_addr");
|
|
1555
|
+
out.parentAddr = key ?? null;
|
|
1556
|
+
}
|
|
1483
1557
|
if (given("rules")) {
|
|
1484
1558
|
if (view.rules !== null && !isRecord(view.rules)) throw bad("rules");
|
|
1485
1559
|
const rules = view.rules;
|
|
@@ -3468,7 +3542,7 @@ var RelayHub = class _RelayHub {
|
|
|
3468
3542
|
return count;
|
|
3469
3543
|
});
|
|
3470
3544
|
this.stored = null;
|
|
3471
|
-
this.toBridges(name, { machine: name, endpoints: [], full: true });
|
|
3545
|
+
this.toBridges(name, { machine: name, endpoints: [], full: true, released: true });
|
|
3472
3546
|
this.dash({ type: "endpoints", at: new Date(this.now()).toISOString(), machine: name });
|
|
3473
3547
|
this.deps.log?.("relay_action", { account: this.account, machine: name, method: "machine_release", code: "ok" });
|
|
3474
3548
|
return { removed: name, endpoints: dropped };
|
|
@@ -3505,7 +3579,7 @@ var RelayHub = class _RelayHub {
|
|
|
3505
3579
|
core.record("machine_expire", null, name, { machine: name, endpoints: count });
|
|
3506
3580
|
return count;
|
|
3507
3581
|
});
|
|
3508
|
-
this.toBridges(name, { machine: name, endpoints: [], full: true });
|
|
3582
|
+
this.toBridges(name, { machine: name, endpoints: [], full: true, released: true });
|
|
3509
3583
|
this.deps.log?.("relay_action", { account: this.account, machine: name, method: "machine_expire", code: "ok", endpoints: dropped });
|
|
3510
3584
|
}
|
|
3511
3585
|
}
|
|
@@ -3523,7 +3597,7 @@ var RelayHub = class _RelayHub {
|
|
|
3523
3597
|
for (const name of unbound) this.relay.record("machine_forget", null, name, { machine: name, endpoints: this.relay.dropMachine(name, { queue: true }) });
|
|
3524
3598
|
return unbound;
|
|
3525
3599
|
});
|
|
3526
|
-
for (const name of forgotten) this.toBridges(name, { machine: name, endpoints: [], full: true });
|
|
3600
|
+
for (const name of forgotten) this.toBridges(name, { machine: name, endpoints: [], full: true, released: true });
|
|
3527
3601
|
}
|
|
3528
3602
|
/** the hub_state keys (`denied:<tokenId>`) of tokens revoked lately, to when; loaded once per object lifetime */
|
|
3529
3603
|
deniedGrants() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scp-dev/hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Swarm Communication Protocol federation and account hub: mirrored endpoints across machines, bridges, direct clients and cross-account delivery.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://scp.dev",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"typecheck": "tsc -p ."
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@scp-dev/core": "0.
|
|
32
|
+
"@scp-dev/core": "0.2.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"tsup": "^8.5.1"
|
package/src/federation.ts
CHANGED
|
@@ -82,6 +82,8 @@ interface FederatedRow extends EndpointRow {
|
|
|
82
82
|
scope_epoch: number | null
|
|
83
83
|
/** this machine's own agent: the wire caps it mirrored last */
|
|
84
84
|
mirrored_caps: string | null
|
|
85
|
+
/** another machine's row: the presence its bridge mirrored last */
|
|
86
|
+
mirrored_presence: Presence | null
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
/** own rules as they cross machines: selector lists per direction */
|
|
@@ -104,6 +106,8 @@ export interface MirrorScope {
|
|
|
104
106
|
parent_ep?: string | null
|
|
105
107
|
/** 0 for a root; null when not known */
|
|
106
108
|
depth?: number | null
|
|
109
|
+
/** a parent on another machine of the account, `<machine>/<handle>`; null for none */
|
|
110
|
+
parent_addr?: string | null
|
|
107
111
|
rules?: MirrorRules | null
|
|
108
112
|
caps?: MirrorCaps | null
|
|
109
113
|
/**
|
|
@@ -145,6 +149,7 @@ interface ParsedScope {
|
|
|
145
149
|
tree?: string | null
|
|
146
150
|
parent?: string | null
|
|
147
151
|
depth?: number | null
|
|
152
|
+
parentAddr?: string | null
|
|
148
153
|
rules?: MirrorRules | null
|
|
149
154
|
caps?: ScopeCaps | null
|
|
150
155
|
title?: string | null
|
|
@@ -290,6 +295,7 @@ export class FederatedCore extends RelayCore {
|
|
|
290
295
|
if (!columns.includes("incarnation")) this.sql.all(`ALTER TABLE endpoints ADD COLUMN incarnation TEXT`)
|
|
291
296
|
if (!columns.includes("scope_epoch")) this.sql.all(`ALTER TABLE endpoints ADD COLUMN scope_epoch INTEGER`)
|
|
292
297
|
if (!columns.includes("mirrored_caps")) this.sql.all(`ALTER TABLE endpoints ADD COLUMN mirrored_caps TEXT`)
|
|
298
|
+
if (!columns.includes("mirrored_presence")) this.sql.all(`ALTER TABLE endpoints ADD COLUMN mirrored_presence TEXT`)
|
|
293
299
|
this.sql.all(`CREATE UNIQUE INDEX IF NOT EXISTS endpoints_remote ON endpoints (remote) WHERE remote IS NOT NULL`)
|
|
294
300
|
if (this.xinbox) for (const statement of XACCT_MIGRATION) this.sql.all(statement)
|
|
295
301
|
}
|
|
@@ -446,6 +452,71 @@ export class FederatedCore extends RelayCore {
|
|
|
446
452
|
return !(row as FederatedRow).remote
|
|
447
453
|
}
|
|
448
454
|
|
|
455
|
+
/** `<machine>/<handle>` of a row of this account: a mirrored row's remote, a local one's on this machine; null for another account's or without a machine */
|
|
456
|
+
private keyOf(row: EndpointRow): string | null {
|
|
457
|
+
const remote = (row as FederatedRow).remote
|
|
458
|
+
if (remote) return isForeign(remote) ? null : remote
|
|
459
|
+
return this.machine === null ? null : `${this.machine}/${row.handle}`
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** the row `<machine>/<handle>` names: a local one on this machine, else a mirrored one */
|
|
463
|
+
private byKey(key: string): EndpointRow | undefined {
|
|
464
|
+
if (!REMOTE.test(key)) return undefined
|
|
465
|
+
const handle = handlePart(key)
|
|
466
|
+
if (machinePart(key) === this.machine) return validHandle(handle) ? this.byHandle(handle) : undefined
|
|
467
|
+
return this.byRemote(key)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** whether walking up from `start` (local and mirrored parents, then a link across machines) reaches a row whose key or id is in `targets` */
|
|
471
|
+
private reachesUp(start: EndpointRow, targets: Set<string>): boolean {
|
|
472
|
+
const seen = new Set<string>()
|
|
473
|
+
let row: EndpointRow | undefined = start
|
|
474
|
+
while (row && !seen.has(row.id) && seen.size <= LIMITS.depthMax * 4) {
|
|
475
|
+
seen.add(row.id)
|
|
476
|
+
const key = this.keyOf(row)
|
|
477
|
+
if (targets.has(row.id) || (key !== null && targets.has(key))) return true
|
|
478
|
+
row = row.parent_ep ? this.byId(row.parent_ep) : row.parent_addr ? this.byKey(row.parent_addr) : undefined
|
|
479
|
+
}
|
|
480
|
+
return false
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** a parent on another machine is a mirrored agent of this account that neither is nor sits under the child or one of its descendants */
|
|
484
|
+
protected override linkAcross(row: EndpointRow, parent: EndpointRow): string {
|
|
485
|
+
const remote = (parent as FederatedRow).remote
|
|
486
|
+
if (!remote || isForeign(remote) || parent.kind !== "agent" || handlePart(remote) === "")
|
|
487
|
+
throw new RelayError("invalid_params", "the parent is an agent endpoint of this account")
|
|
488
|
+
if (row.parent_addr === remote) return remote
|
|
489
|
+
const below = this.sql.all<{ id: string }>(`SELECT id FROM endpoints WHERE tree = ?`, row.id).map((r) => r.id)
|
|
490
|
+
const key = this.keyOf(row)
|
|
491
|
+
if (this.reachesUp(parent, new Set([row.id, ...below, ...(key === null ? [] : [key])])))
|
|
492
|
+
throw new RelayError("scope_refused", `${this.address(parent)} sits under ${this.address(row)}: a session cannot go under its own descendant`)
|
|
493
|
+
return remote
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
protected override renamedHandle(from: string, to: string): void {
|
|
497
|
+
if (this.machine !== null) this.sql.all(`UPDATE endpoints SET parent_addr = ? WHERE parent_addr = ?`, `${this.machine}/${to}`, `${this.machine}/${from}`)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
protected override linkedAcross(a: EndpointRow, b: EndpointRow): boolean {
|
|
501
|
+
const [ka, kb] = [this.keyOf(a), this.keyOf(b)]
|
|
502
|
+
return (a.parent_addr !== null && a.parent_addr === kb) || (b.parent_addr !== null && b.parent_addr === ka)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
protected override linkedParent(row: EndpointRow, shown?: Set<string>): string | null {
|
|
506
|
+
const key = row.parent_addr
|
|
507
|
+
if (!key) return null
|
|
508
|
+
const parent = this.byKey(key)
|
|
509
|
+
if (parent) return shown && !shown.has(parent.id) ? null : this.address(parent)
|
|
510
|
+
return shown ? null : this.remoteAddress(key)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** a live mirrored row shows the presence its machine mirrored, not one its last_seen decays to here */
|
|
514
|
+
protected override view(row: EndpointRow, shown?: Set<string>): EndpointView {
|
|
515
|
+
const view = super.view(row, shown)
|
|
516
|
+
const { remote, mirrored_presence: mirrored } = row as FederatedRow
|
|
517
|
+
return remote && row.connections > 0 && (mirrored === "online" || mirrored === "idle") ? { ...view, presence: mirrored } : view
|
|
518
|
+
}
|
|
519
|
+
|
|
449
520
|
protected override address(row: EndpointRow): string {
|
|
450
521
|
const remote = (row as FederatedRow).remote
|
|
451
522
|
if (remote && isForeign(remote)) return remote.slice(1)
|
|
@@ -603,6 +674,7 @@ export class FederatedCore extends RelayCore {
|
|
|
603
674
|
const old = !before && view.was !== null ? this.byRemote(`${machine}/${view.was}`) : undefined
|
|
604
675
|
if (old && old.token_hash === null) {
|
|
605
676
|
this.sql.all(`UPDATE endpoints SET remote = ? WHERE id = ?`, remote, old.id)
|
|
677
|
+
this.sql.all(`UPDATE endpoints SET parent_addr = ? WHERE parent_addr = ?`, remote, `${machine}/${view.was}`)
|
|
606
678
|
before = this.byId(old.id) as FederatedRow
|
|
607
679
|
renamed.set(old.id, view.was!)
|
|
608
680
|
}
|
|
@@ -625,6 +697,7 @@ export class FederatedCore extends RelayCore {
|
|
|
625
697
|
status_note: view.status?.note ?? null,
|
|
626
698
|
token_hash: null,
|
|
627
699
|
connections: live ? 1 : 0,
|
|
700
|
+
mirrored_presence: view.presence,
|
|
628
701
|
last_seen: view.lastSeen ?? (live ? now : (before?.last_seen ?? null)),
|
|
629
702
|
auth: auth ?? row.auth,
|
|
630
703
|
...(view.kind === "agent" ? this.scopeColumns(machine, view.scope, row, place(view.handle)) : {}),
|
|
@@ -686,6 +759,7 @@ export class FederatedCore extends RelayCore {
|
|
|
686
759
|
tree: root?.handle ?? null,
|
|
687
760
|
parent_ep: parent?.handle ?? null,
|
|
688
761
|
depth: row.depth ?? 0,
|
|
762
|
+
parent_addr: row.parent_addr ?? null,
|
|
689
763
|
rules: Object.keys(rules).length ? rules : null,
|
|
690
764
|
caps: wire,
|
|
691
765
|
title: row.title ?? null,
|
|
@@ -1616,9 +1690,9 @@ export class FederatedCore extends RelayCore {
|
|
|
1616
1690
|
const was = this.view(before)
|
|
1617
1691
|
const presence = was.presence !== view.presence
|
|
1618
1692
|
const status = before.status_state !== after.status_state || before.status_note !== after.status_note
|
|
1619
|
-
const other = (
|
|
1620
|
-
|
|
1621
|
-
)
|
|
1693
|
+
const other = (
|
|
1694
|
+
["kind", "harness", "model", "cwd", "labels", "title", "wake", "tree", "parent_ep", "parent_addr", "depth", "rules", "caps", "incarnation"] as const
|
|
1695
|
+
).some((field) => before[field] !== (after as FederatedRow)[field])
|
|
1622
1696
|
return presence || status || other ? { view, added: false, presence, status } : null
|
|
1623
1697
|
}
|
|
1624
1698
|
|
|
@@ -1635,6 +1709,7 @@ export class FederatedCore extends RelayCore {
|
|
|
1635
1709
|
tree: row.depth === null ? null : row.tree ? handle(row.tree) : remote ? handlePart(remote) : row.handle,
|
|
1636
1710
|
parent_ep: handle(row.parent_ep),
|
|
1637
1711
|
depth: row.depth,
|
|
1712
|
+
parent_addr: row.parent_addr ?? null,
|
|
1638
1713
|
rules: row.rules ? (JSON.parse(row.rules) as MirrorRules) : null,
|
|
1639
1714
|
caps: row.caps ? this.wireCaps(JSON.parse(row.caps) as ScopeCaps) : null,
|
|
1640
1715
|
incarnation: (row as FederatedRow).incarnation ?? null,
|
|
@@ -1684,8 +1759,10 @@ export class FederatedCore extends RelayCore {
|
|
|
1684
1759
|
caps = merged.caps.length || Object.keys(merged.allow).length ? JSON.stringify(merged) : null
|
|
1685
1760
|
}
|
|
1686
1761
|
const rules = scope.rules !== undefined ? scope.rules : fresh ? null : undefined
|
|
1762
|
+
const linked = scope.parentAddr !== undefined ? scope.parentAddr : fresh ? null : row.parent_addr
|
|
1687
1763
|
return {
|
|
1688
1764
|
...place,
|
|
1765
|
+
parent_addr: linked !== null && (place.parent_ep !== null || this.loops(row, linked)) ? null : linked,
|
|
1689
1766
|
rules: rules === undefined ? row.rules : rules && (rules.in || rules.out) ? JSON.stringify(rules) : null,
|
|
1690
1767
|
caps,
|
|
1691
1768
|
title: scope.title !== undefined ? scope.title : fresh ? null : row.title,
|
|
@@ -1694,6 +1771,13 @@ export class FederatedCore extends RelayCore {
|
|
|
1694
1771
|
}
|
|
1695
1772
|
}
|
|
1696
1773
|
|
|
1774
|
+
/** whether a link of `row` under `key` closes a cycle in what this relay mirrors */
|
|
1775
|
+
private loops(row: EndpointRow, key: string): boolean {
|
|
1776
|
+
const parent = this.byKey(key)
|
|
1777
|
+
const own = this.keyOf(row)
|
|
1778
|
+
return parent !== undefined && this.reachesUp(parent, new Set([row.id, ...(own === null ? [] : [own])]))
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1697
1781
|
/** 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 */
|
|
1698
1782
|
private localCaps(machine: string, caps: ScopeCaps): ScopeCaps {
|
|
1699
1783
|
const ref = (value: string) => {
|
|
@@ -1838,6 +1922,12 @@ function parseScope(view: Partial<MirrorView>): ParsedScope {
|
|
|
1838
1922
|
if (view.depth !== null && (!Number.isInteger(view.depth) || (view.depth as number) < 0 || (view.depth as number) > 1000)) throw bad("depth")
|
|
1839
1923
|
out.depth = view.depth ?? null
|
|
1840
1924
|
}
|
|
1925
|
+
if (given("parent_addr")) {
|
|
1926
|
+
const key = view.parent_addr
|
|
1927
|
+
if (key !== null && (typeof key !== "string" || !REMOTE.test(key) || !MACHINE_NAME.test(machinePart(key)) || !validHandle(handlePart(key))))
|
|
1928
|
+
throw bad("parent_addr")
|
|
1929
|
+
out.parentAddr = key ?? null
|
|
1930
|
+
}
|
|
1841
1931
|
if (given("rules")) {
|
|
1842
1932
|
if (view.rules !== null && !isRecord(view.rules)) throw bad("rules")
|
|
1843
1933
|
const rules = view.rules as Record<string, unknown> | null
|
package/src/hub.ts
CHANGED
|
@@ -1774,7 +1774,7 @@ export class RelayHub {
|
|
|
1774
1774
|
return count
|
|
1775
1775
|
})
|
|
1776
1776
|
this.stored = null
|
|
1777
|
-
this.toBridges(name, { machine: name, endpoints: [], full: true })
|
|
1777
|
+
this.toBridges(name, { machine: name, endpoints: [], full: true, released: true })
|
|
1778
1778
|
this.dash({ type: "endpoints", at: new Date(this.now()).toISOString(), machine: name })
|
|
1779
1779
|
this.deps.log?.("relay_action", { account: this.account, machine: name, method: "machine_release", code: "ok" })
|
|
1780
1780
|
return { removed: name, endpoints: dropped }
|
|
@@ -1812,7 +1812,7 @@ export class RelayHub {
|
|
|
1812
1812
|
core.record("machine_expire", null, name, { machine: name, endpoints: count })
|
|
1813
1813
|
return count
|
|
1814
1814
|
})
|
|
1815
|
-
this.toBridges(name, { machine: name, endpoints: [], full: true })
|
|
1815
|
+
this.toBridges(name, { machine: name, endpoints: [], full: true, released: true })
|
|
1816
1816
|
this.deps.log?.("relay_action", { account: this.account, machine: name, method: "machine_expire", code: "ok", endpoints: dropped })
|
|
1817
1817
|
}
|
|
1818
1818
|
}
|
|
@@ -1831,7 +1831,7 @@ export class RelayHub {
|
|
|
1831
1831
|
for (const name of unbound) this.relay.record("machine_forget", null, name, { machine: name, endpoints: this.relay.dropMachine(name, { queue: true }) })
|
|
1832
1832
|
return unbound
|
|
1833
1833
|
})
|
|
1834
|
-
for (const name of forgotten) this.toBridges(name, { machine: name, endpoints: [], full: true })
|
|
1834
|
+
for (const name of forgotten) this.toBridges(name, { machine: name, endpoints: [], full: true, released: true })
|
|
1835
1835
|
}
|
|
1836
1836
|
|
|
1837
1837
|
/** the hub_state keys (`denied:<tokenId>`) of tokens revoked lately, to when; loaded once per object lifetime */
|