@scp-dev/hub 0.5.1 → 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 CHANGED
@@ -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,17 @@ 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;
165
178
  /** a live mirrored row shows the presence its machine mirrored, not one its last_seen decays to here */
166
179
  protected view(row: EndpointRow, shown?: Set<string>): EndpointView;
167
180
  protected address(row: EndpointRow): string;
@@ -375,6 +388,8 @@ declare class FederatedCore extends RelayCore {
375
388
  private placer;
376
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 */
377
390
  private scopeColumns;
391
+ /** whether a link of `row` under `key` closes a cycle in what this relay mirrors */
392
+ private loops;
378
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 */
379
394
  private localCaps;
380
395
  /** caps with this core's row ids named as `ep:<handle>`, as they cross machines */
package/dist/index.js CHANGED
@@ -223,6 +223,57 @@ var FederatedCore = class extends RelayCore {
223
223
  localRow(row) {
224
224
  return !row.remote;
225
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
+ }
226
277
  /** a live mirrored row shows the presence its machine mirrored, not one its last_seen decays to here */
227
278
  view(row, shown) {
228
279
  const view = super.view(row, shown);
@@ -376,6 +427,7 @@ var FederatedCore = class extends RelayCore {
376
427
  const old = !before && view.was !== null ? this.byRemote(`${machine}/${view.was}`) : void 0;
377
428
  if (old && old.token_hash === null) {
378
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}`);
379
431
  before = this.byId(old.id);
380
432
  renamed.set(old.id, view.was);
381
433
  }
@@ -459,6 +511,7 @@ var FederatedCore = class extends RelayCore {
459
511
  tree: root?.handle ?? null,
460
512
  parent_ep: parent?.handle ?? null,
461
513
  depth: row.depth ?? 0,
514
+ parent_addr: row.parent_addr ?? null,
462
515
  rules: Object.keys(rules).length ? rules : null,
463
516
  caps: wire,
464
517
  title: row.title ?? null,
@@ -1294,9 +1347,7 @@ var FederatedCore = class extends RelayCore {
1294
1347
  const was = this.view(before);
1295
1348
  const presence = was.presence !== view.presence;
1296
1349
  const status = before.status_state !== after.status_state || before.status_note !== after.status_note;
1297
- const other = ["kind", "harness", "model", "cwd", "labels", "title", "wake", "tree", "parent_ep", "depth", "rules", "caps", "incarnation"].some(
1298
- (field) => before[field] !== after[field]
1299
- );
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]);
1300
1351
  return presence || status || other ? { view, added: false, presence, status } : null;
1301
1352
  }
1302
1353
  remoteView(row) {
@@ -1312,6 +1363,7 @@ var FederatedCore = class extends RelayCore {
1312
1363
  tree: row.depth === null ? null : row.tree ? handle(row.tree) : remote ? handlePart(remote) : row.handle,
1313
1364
  parent_ep: handle(row.parent_ep),
1314
1365
  depth: row.depth,
1366
+ parent_addr: row.parent_addr ?? null,
1315
1367
  rules: row.rules ? JSON.parse(row.rules) : null,
1316
1368
  caps: row.caps ? this.wireCaps(JSON.parse(row.caps)) : null,
1317
1369
  incarnation: row.incarnation ?? null
@@ -1357,8 +1409,10 @@ var FederatedCore = class extends RelayCore {
1357
1409
  caps = merged.caps.length || Object.keys(merged.allow).length ? JSON.stringify(merged) : null;
1358
1410
  }
1359
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;
1360
1413
  return {
1361
1414
  ...place,
1415
+ parent_addr: linked !== null && (place.parent_ep !== null || this.loops(row, linked)) ? null : linked,
1362
1416
  rules: rules === void 0 ? row.rules : rules && (rules.in || rules.out) ? JSON.stringify(rules) : null,
1363
1417
  caps,
1364
1418
  title: scope.title !== void 0 ? scope.title : fresh ? null : row.title,
@@ -1366,6 +1420,12 @@ var FederatedCore = class extends RelayCore {
1366
1420
  incarnation: scope.incarnation ?? row.incarnation
1367
1421
  };
1368
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
+ }
1369
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 */
1370
1430
  localCaps(machine, caps) {
1371
1431
  const ref = (value) => {
@@ -1488,6 +1548,12 @@ function parseScope(view) {
1488
1548
  if (view.depth !== null && (!Number.isInteger(view.depth) || view.depth < 0 || view.depth > 1e3)) throw bad("depth");
1489
1549
  out.depth = view.depth ?? null;
1490
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
+ }
1491
1557
  if (given("rules")) {
1492
1558
  if (view.rules !== null && !isRecord(view.rules)) throw bad("rules");
1493
1559
  const rules = view.rules;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scp-dev/hub",
3
- "version": "0.5.1",
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.1.6"
32
+ "@scp-dev/core": "0.2.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "tsup": "^8.5.1"
package/src/federation.ts CHANGED
@@ -106,6 +106,8 @@ export interface MirrorScope {
106
106
  parent_ep?: string | null
107
107
  /** 0 for a root; null when not known */
108
108
  depth?: number | null
109
+ /** a parent on another machine of the account, `<machine>/<handle>`; null for none */
110
+ parent_addr?: string | null
109
111
  rules?: MirrorRules | null
110
112
  caps?: MirrorCaps | null
111
113
  /**
@@ -147,6 +149,7 @@ interface ParsedScope {
147
149
  tree?: string | null
148
150
  parent?: string | null
149
151
  depth?: number | null
152
+ parentAddr?: string | null
150
153
  rules?: MirrorRules | null
151
154
  caps?: ScopeCaps | null
152
155
  title?: string | null
@@ -449,6 +452,64 @@ export class FederatedCore extends RelayCore {
449
452
  return !(row as FederatedRow).remote
450
453
  }
451
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
+
452
513
  /** a live mirrored row shows the presence its machine mirrored, not one its last_seen decays to here */
453
514
  protected override view(row: EndpointRow, shown?: Set<string>): EndpointView {
454
515
  const view = super.view(row, shown)
@@ -613,6 +674,7 @@ export class FederatedCore extends RelayCore {
613
674
  const old = !before && view.was !== null ? this.byRemote(`${machine}/${view.was}`) : undefined
614
675
  if (old && old.token_hash === null) {
615
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}`)
616
678
  before = this.byId(old.id) as FederatedRow
617
679
  renamed.set(old.id, view.was!)
618
680
  }
@@ -697,6 +759,7 @@ export class FederatedCore extends RelayCore {
697
759
  tree: root?.handle ?? null,
698
760
  parent_ep: parent?.handle ?? null,
699
761
  depth: row.depth ?? 0,
762
+ parent_addr: row.parent_addr ?? null,
700
763
  rules: Object.keys(rules).length ? rules : null,
701
764
  caps: wire,
702
765
  title: row.title ?? null,
@@ -1627,9 +1690,9 @@ export class FederatedCore extends RelayCore {
1627
1690
  const was = this.view(before)
1628
1691
  const presence = was.presence !== view.presence
1629
1692
  const status = before.status_state !== after.status_state || before.status_note !== after.status_note
1630
- const other = (["kind", "harness", "model", "cwd", "labels", "title", "wake", "tree", "parent_ep", "depth", "rules", "caps", "incarnation"] as const).some(
1631
- (field) => before[field] !== (after as FederatedRow)[field],
1632
- )
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])
1633
1696
  return presence || status || other ? { view, added: false, presence, status } : null
1634
1697
  }
1635
1698
 
@@ -1646,6 +1709,7 @@ export class FederatedCore extends RelayCore {
1646
1709
  tree: row.depth === null ? null : row.tree ? handle(row.tree) : remote ? handlePart(remote) : row.handle,
1647
1710
  parent_ep: handle(row.parent_ep),
1648
1711
  depth: row.depth,
1712
+ parent_addr: row.parent_addr ?? null,
1649
1713
  rules: row.rules ? (JSON.parse(row.rules) as MirrorRules) : null,
1650
1714
  caps: row.caps ? this.wireCaps(JSON.parse(row.caps) as ScopeCaps) : null,
1651
1715
  incarnation: (row as FederatedRow).incarnation ?? null,
@@ -1695,8 +1759,10 @@ export class FederatedCore extends RelayCore {
1695
1759
  caps = merged.caps.length || Object.keys(merged.allow).length ? JSON.stringify(merged) : null
1696
1760
  }
1697
1761
  const rules = scope.rules !== undefined ? scope.rules : fresh ? null : undefined
1762
+ const linked = scope.parentAddr !== undefined ? scope.parentAddr : fresh ? null : row.parent_addr
1698
1763
  return {
1699
1764
  ...place,
1765
+ parent_addr: linked !== null && (place.parent_ep !== null || this.loops(row, linked)) ? null : linked,
1700
1766
  rules: rules === undefined ? row.rules : rules && (rules.in || rules.out) ? JSON.stringify(rules) : null,
1701
1767
  caps,
1702
1768
  title: scope.title !== undefined ? scope.title : fresh ? null : row.title,
@@ -1705,6 +1771,13 @@ export class FederatedCore extends RelayCore {
1705
1771
  }
1706
1772
  }
1707
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
+
1708
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 */
1709
1782
  private localCaps(machine: string, caps: ScopeCaps): ScopeCaps {
1710
1783
  const ref = (value: string) => {
@@ -1849,6 +1922,12 @@ function parseScope(view: Partial<MirrorView>): ParsedScope {
1849
1922
  if (view.depth !== null && (!Number.isInteger(view.depth) || (view.depth as number) < 0 || (view.depth as number) > 1000)) throw bad("depth")
1850
1923
  out.depth = view.depth ?? null
1851
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
+ }
1852
1931
  if (given("rules")) {
1853
1932
  if (view.rules !== null && !isRecord(view.rules)) throw bad("rules")
1854
1933
  const rules = view.rules as Record<string, unknown> | null