@scp-dev/hub 0.2.0 → 0.3.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/dist/index.js CHANGED
@@ -22,7 +22,8 @@ import {
22
22
  unreachable,
23
23
  validHandle,
24
24
  validHarness,
25
- validSelector
25
+ validSelector,
26
+ WAKES
26
27
  } from "@scp-dev/core";
27
28
  var MACHINE_NAME = /^[a-z0-9][a-z0-9-]{0,31}$/;
28
29
  var REMOTE = /^[a-z0-9][a-z0-9._-]{0,63}\/([a-z0-9][a-z0-9._-]{0,62})?$/;
@@ -770,6 +771,21 @@ var FederatedCore = class extends RelayCore {
770
771
  this.audit("forward", from, result.id, { to: result.deliveries.map((d) => d.address), kind, hop: result.hop, thread: result.thread });
771
772
  });
772
773
  }
774
+ /**
775
+ * records that a forwarded message reached mirrored endpoints of other machines, unacknowledged, as a hub's delivery rows would: their
776
+ * replies then join its thread here. For links that deliver machine to machine; a hub bridge never needs it
777
+ */
778
+ recordRemoteDelivery(messageId, endpoints) {
779
+ this.tx(() => {
780
+ const message = one(this.sql, `SELECT expires_at FROM messages WHERE id = ?`, messageId);
781
+ if (!message) return;
782
+ for (const endpoint of endpoints) {
783
+ if (this.machineOf(endpoint) === null) continue;
784
+ if (one(this.sql, `SELECT 1 FROM deliveries WHERE message_id = ? AND endpoint = ?`, messageId, endpoint)) continue;
785
+ this.insertDelivery(messageId, endpoint, message.expires_at);
786
+ }
787
+ });
788
+ }
773
789
  /** the earlier result of a forwarded send with this client_id, or null; a different message under the same client_id is refused */
774
790
  forwarded(from, params) {
775
791
  const clientId = cleanText(params.client_id, "client_id", 128);
@@ -1268,7 +1284,7 @@ var FederatedCore = class extends RelayCore {
1268
1284
  const was = this.view(before);
1269
1285
  const presence = was.presence !== view.presence;
1270
1286
  const status = before.status_state !== after.status_state || before.status_note !== after.status_note;
1271
- const other = ["kind", "harness", "model", "cwd", "labels", "title", "tree", "parent_ep", "depth", "rules", "caps", "incarnation"].some(
1287
+ const other = ["kind", "harness", "model", "cwd", "labels", "title", "wake", "tree", "parent_ep", "depth", "rules", "caps", "incarnation"].some(
1272
1288
  (field) => before[field] !== after[field]
1273
1289
  );
1274
1290
  return presence || status || other ? { view, added: false, presence, status } : null;
@@ -1336,6 +1352,7 @@ var FederatedCore = class extends RelayCore {
1336
1352
  rules: rules === void 0 ? row.rules : rules && (rules.in || rules.out) ? JSON.stringify(rules) : null,
1337
1353
  caps,
1338
1354
  title: scope.title !== void 0 ? scope.title : fresh ? null : row.title,
1355
+ wake: scope.wake !== void 0 ? scope.wake : fresh ? null : row.wake,
1339
1356
  incarnation: scope.incarnation ?? row.incarnation
1340
1357
  };
1341
1358
  }
@@ -1489,6 +1506,7 @@ function parseScope(view) {
1489
1506
  throw new RelayError("too_large", `an endpoint's rules and caps are more than ${SCOPE_BYTES} bytes`);
1490
1507
  if (given("title"))
1491
1508
  out.title = view.title === null ? null : cleanText(typeof view.title === "string" ? view.title.replace(/\s+/g, " ") : view.title, "title", 64) || null;
1509
+ if (given("wake")) out.wake = typeof view.wake === "string" && view.wake !== "none" && WAKES.includes(view.wake) ? view.wake : null;
1492
1510
  if (given("incarnation")) {
1493
1511
  if (view.incarnation !== null && (typeof view.incarnation !== "string" || !INCARNATION.test(view.incarnation))) throw bad("incarnation");
1494
1512
  out.incarnation = view.incarnation ?? null;
@@ -1504,20 +1522,20 @@ import {
1504
1522
  cleanText as cleanText2,
1505
1523
  dayStart,
1506
1524
  declaredLimits,
1507
- findSecret as findSecret2,
1525
+ findSecret as findSecret3,
1508
1526
  gateRefusal,
1509
1527
  HUMAN_HANDLE as HUMAN_HANDLE2,
1510
1528
  honestSender,
1511
1529
  joinRedacted,
1512
1530
  judgeDetailForAgents,
1513
- LIMITS as LIMITS2,
1531
+ LIMITS as LIMITS3,
1514
1532
  MESSAGE_KINDS as MESSAGE_KINDS2,
1515
- newId as newId2,
1516
- one as one2,
1533
+ newId as newId3,
1534
+ one as one3,
1517
1535
  PREAMBLE,
1518
1536
  PROTOCOL,
1519
1537
  parseXMessage,
1520
- RelayError as RelayError2,
1538
+ RelayError as RelayError3,
1521
1539
  randomHex as randomHex2,
1522
1540
  replyGrant,
1523
1541
  SERVER_FIELDS,
@@ -1528,6 +1546,470 @@ import {
1528
1546
  XLIMITS,
1529
1547
  xlimits
1530
1548
  } from "@scp-dev/core";
1549
+
1550
+ // src/judge-gate.ts
1551
+ import {
1552
+ applyRedactions,
1553
+ DEFAULT_JUDGE_BUDGET,
1554
+ findSecret as findSecret2,
1555
+ HttpJudgeClient,
1556
+ JUDGE_PROMPT_VERSION,
1557
+ judgeBoundary,
1558
+ LIMITS as LIMITS2,
1559
+ mergeVerdicts,
1560
+ newId as newId2,
1561
+ newJudgeStats,
1562
+ one as one2,
1563
+ RelayError as RelayError2,
1564
+ runJudge,
1565
+ sha256Hex,
1566
+ VerdictCache
1567
+ } from "@scp-dev/core";
1568
+ var JUDGE_LIMITS = {
1569
+ endpointPerMinute: LIMITS2.pairPerMinute,
1570
+ endpointPerHour: LIMITS2.pairPerHour,
1571
+ treePerMinute: LIMITS2.pairPerMinute * 2,
1572
+ treePerHour: LIMITS2.pairPerHour * 2,
1573
+ heldPerSender: 100
1574
+ };
1575
+ var iso = (ms) => ms === null ? null : new Date(ms).toISOString();
1576
+ var JudgeGate = class {
1577
+ constructor(o) {
1578
+ this.o = o;
1579
+ this.limits = { ...JUDGE_LIMITS, ...o.limits };
1580
+ o.sql.all(
1581
+ `CREATE TABLE IF NOT EXISTS judge_holds (id TEXT PRIMARY KEY, from_ep TEXT NOT NULL, params TEXT NOT NULL, origin TEXT NOT NULL, verdict TEXT NOT NULL,
1582
+ created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, state TEXT NOT NULL, by TEXT, resolved_at INTEGER)`
1583
+ );
1584
+ o.sql.all(
1585
+ `CREATE TABLE IF NOT EXISTS judge_sent (from_ep TEXT NOT NULL, client_id TEXT NOT NULL, fingerprint TEXT NOT NULL, result TEXT NOT NULL, created_at INTEGER NOT NULL,
1586
+ PRIMARY KEY (from_ep, client_id))`
1587
+ );
1588
+ o.sql.all(`UPDATE judge_holds SET state = 'held' WHERE state = 'releasing'`);
1589
+ }
1590
+ o;
1591
+ stats = newJudgeStats();
1592
+ policy;
1593
+ version = "";
1594
+ client = null;
1595
+ cache = new VerdictCache();
1596
+ shadows = /* @__PURE__ */ new Set();
1597
+ /** judged sends in the last hour per budget: `ep:<id>`, `tree:<root id>` and `*` for the whole broker */
1598
+ spent = /* @__PURE__ */ new Map();
1599
+ limits;
1600
+ /** the policy's judge section; a model client only when its key is readable, else the prefilter alone */
1601
+ configure(policy) {
1602
+ this.policy = policy;
1603
+ this.version = policy ? sha256Hex(JSON.stringify(policy)) : "";
1604
+ this.client = null;
1605
+ const model = policy?.model;
1606
+ if (!policy || policy.mode === "off" || !model) return;
1607
+ const key = this.keyReader(model);
1608
+ if (!key) {
1609
+ this.o.log(
1610
+ `relay broker: the judge model's key (${model.key_env ? `key_env ${model.key_env}` : `key_file ${model.key_file}`}) is not readable; judging with the prefilter only`
1611
+ );
1612
+ return;
1613
+ }
1614
+ this.client = (this.o.client ?? ((m, k) => new HttpJudgeClient({ model: m.id, effort: m.effort, key: k })))(model, key);
1615
+ }
1616
+ keyReader(model) {
1617
+ if (model.key_env) {
1618
+ const name = model.key_env;
1619
+ return this.o.env[name] ? async () => this.o.env[name] ?? "" : null;
1620
+ }
1621
+ return this.o.keyFile?.(model.key_file) ?? null;
1622
+ }
1623
+ /** resolves once every shadow judgement started so far finished */
1624
+ async settled() {
1625
+ await Promise.all([...this.shadows]);
1626
+ }
1627
+ /** a send or ask through the judge; an unjudged one goes straight to `deliver` */
1628
+ async send(from, params, origin, deliver) {
1629
+ const policy = this.policy;
1630
+ const body = params.body;
1631
+ if (!policy || policy.mode === "off" || typeof body !== "string" || new TextEncoder().encode(body).length > LIMITS2.bodyBytes || findSecret2(body))
1632
+ return deliver(from, params, origin);
1633
+ const targets = Array.isArray(params.to) ? params.to : [params.to];
1634
+ if (targets.some((to) => this.o.core.foreignAccount(to) !== null)) {
1635
+ if (policy.mode === "enforce") {
1636
+ this.o.core.auditJudge("judge_skipped", from, null, {
1637
+ side: "local",
1638
+ boundary: "other account",
1639
+ prompt_version: JUDGE_PROMPT_VERSION,
1640
+ categories: [],
1641
+ sha256: sha256Hex(body),
1642
+ verdict: "skipped",
1643
+ reason: "other_account_judged_by_hub",
1644
+ to: targets.map(String)
1645
+ });
1646
+ }
1647
+ return deliver(from, params, origin);
1648
+ }
1649
+ if (policy.mode === "shadow") {
1650
+ const result = await deliver(from, params, origin);
1651
+ if (!result.duplicate) this.shadow(policy, from, params, origin, result.id);
1652
+ return result;
1653
+ }
1654
+ const { groups, tree } = this.boundaries(policy, from, params, origin);
1655
+ if (!groups.size) return deliver(from, params, origin);
1656
+ const ttl = params.ttl_s ?? LIMITS2.ttlDefaultS;
1657
+ if (!Number.isInteger(ttl) || ttl < 1 || ttl > LIMITS2.ttlMaxS) throw new RelayError2("invalid_params", `ttl_s must be 1 to ${LIMITS2.ttlMaxS}`);
1658
+ const seen = params.client_id === void 0 ? void 0 : this.seen(from, params);
1659
+ if (seen) return seen;
1660
+ const waiting = one2(this.o.sql, `SELECT COUNT(*) AS n FROM judge_holds WHERE from_ep = ? AND state IN ('held', 'releasing')`, from).n;
1661
+ if (waiting >= this.limits.heldPerSender)
1662
+ throw new RelayError2("rate_limited", `${waiting} of your messages wait for review by your human; send more once they released or refused some`, 3600);
1663
+ this.spend(policy, from, tree);
1664
+ const verdict = await this.judge(policy, from, body, groups);
1665
+ const detail = this.detail(verdict, body, groups);
1666
+ switch (verdict.verdict) {
1667
+ case "refuse":
1668
+ this.o.core.auditJudge("judge_refuse", from, null, detail);
1669
+ throw new RelayError2(
1670
+ "judge_refused",
1671
+ `the relay judge refused this message (local; ${verdict.categories.join(", ") || "no category"}): ${verdict.reason}`,
1672
+ void 0,
1673
+ {
1674
+ side: "local",
1675
+ categories: verdict.categories,
1676
+ reason: verdict.reason
1677
+ }
1678
+ );
1679
+ case "hold":
1680
+ return this.hold(from, params, origin, verdict, groups, detail);
1681
+ case "redact": {
1682
+ const applied = applyRedactions(body, verdict.redactions);
1683
+ const result = await deliver(from, { ...params, body: applied.text }, origin);
1684
+ const redacted = { n: applied.n, kinds: applied.kinds };
1685
+ this.o.core.auditJudge(verdict.source === "error" ? "judge_skipped" : "judge_redact", from, result.id, { ...detail, redacted });
1686
+ return this.remember(from, params, { ...result, redacted });
1687
+ }
1688
+ case "pass": {
1689
+ const result = await deliver(from, params, origin);
1690
+ if (verdict.source === "error") this.o.core.auditJudge("judge_skipped", from, result.id, detail);
1691
+ return this.remember(from, params, result);
1692
+ }
1693
+ }
1694
+ }
1695
+ /** an earlier judged send or waiting hold under the send's client_id: the same message is answered as before, another one refused as core refuses it */
1696
+ seen(from, params) {
1697
+ const clientId = params.client_id;
1698
+ const held = one2(
1699
+ this.o.sql,
1700
+ `SELECT id, params FROM judge_holds WHERE from_ep = ? AND json_extract(params, '$.client_id') = ? AND state IN ('held', 'releasing')`,
1701
+ from,
1702
+ clientId
1703
+ );
1704
+ const sent = held ? void 0 : one2(
1705
+ this.o.sql,
1706
+ `SELECT fingerprint, result FROM judge_sent WHERE from_ep = ? AND client_id = ? AND created_at > ?`,
1707
+ from,
1708
+ clientId,
1709
+ this.o.now() - LIMITS2.idempotencyS * 1e3
1710
+ );
1711
+ const before = held ? fingerprint(JSON.parse(held.params)) : sent?.fingerprint;
1712
+ if (before === void 0) return void 0;
1713
+ if (before !== fingerprint(params)) throw new RelayError2("invalid_params", `client_id ${clientId} was already used for a different message`);
1714
+ return held ? heldResult(held.id, params) : { ...JSON.parse(sent.result), duplicate: true };
1715
+ }
1716
+ remember(from, params, result) {
1717
+ if (params.client_id !== void 0 && !result.duplicate) {
1718
+ this.o.sql.all(
1719
+ `INSERT OR REPLACE INTO judge_sent (from_ep, client_id, fingerprint, result, created_at) VALUES (?, ?, ?, ?, ?)`,
1720
+ from,
1721
+ params.client_id,
1722
+ fingerprint(params),
1723
+ JSON.stringify(result),
1724
+ this.o.now()
1725
+ );
1726
+ }
1727
+ return result;
1728
+ }
1729
+ /**
1730
+ * a judged send counts before any model call against its sender's, its lineage tree's and the whole broker's budget, since refused and
1731
+ * held sends never reach the send's own limits, and a new handle or a helper process must not bring a fresh budget
1732
+ */
1733
+ spend(policy, from, tree) {
1734
+ const now = this.o.now();
1735
+ const global = policy.budget ?? DEFAULT_JUDGE_BUDGET;
1736
+ const budgets = [
1737
+ [`ep:${from}`, this.limits.endpointPerMinute, this.limits.endpointPerHour, "from this endpoint"],
1738
+ [`tree:${tree}`, this.limits.treePerMinute, this.limits.treePerHour, "from this lineage tree"],
1739
+ ["*", global.per_minute, global.per_hour, "on this broker"]
1740
+ ];
1741
+ const recent = budgets.map(([key]) => (this.spent.get(key) ?? []).filter((at) => at > now - 36e5));
1742
+ budgets.forEach(([key, perMinute, perHour, what], i) => {
1743
+ for (const [limit, windowS] of [
1744
+ [perMinute, 60],
1745
+ [perHour, 3600]
1746
+ ]) {
1747
+ const inWindow = recent[i].filter((at) => at > now - windowS * 1e3);
1748
+ if (inWindow.length >= limit) {
1749
+ this.spent.set(key, recent[i]);
1750
+ throw new RelayError2(
1751
+ "rate_limited",
1752
+ `${limit} judged messages ${what} in ${windowS} s`,
1753
+ Math.max(1, Math.ceil((inWindow[0] + windowS * 1e3 - now) / 1e3))
1754
+ );
1755
+ }
1756
+ }
1757
+ });
1758
+ for (const [i, [key]] of budgets.entries()) this.spent.set(key, [...recent[i], now]);
1759
+ }
1760
+ /** how many budgets are tracked (idle ones are evicted by the sweep) */
1761
+ budgetKeys() {
1762
+ return this.spent.size;
1763
+ }
1764
+ /** the held messages, newest first, without bodies */
1765
+ holds(state) {
1766
+ const rows = state ? this.o.sql.all(`SELECT * FROM judge_holds WHERE state = ? ORDER BY created_at DESC, rowid DESC LIMIT 200`, state) : this.o.sql.all(`SELECT * FROM judge_holds ORDER BY created_at DESC, rowid DESC LIMIT 200`);
1767
+ return rows.map((row) => this.view(row));
1768
+ }
1769
+ /** one hold with its body and the judge's redactions while it waits */
1770
+ show(id) {
1771
+ const row = this.row(id);
1772
+ const params = JSON.parse(row.params);
1773
+ const verdict = JSON.parse(row.verdict);
1774
+ return { ...this.view(row), ...typeof params.body === "string" ? { body: params.body } : {}, redactions: verdict.redactions };
1775
+ }
1776
+ /** delivers a held message as its sender, as stored or redacted: scope, policy and limits are checked again by the send itself */
1777
+ async release(id, options, deliver) {
1778
+ const row = one2(
1779
+ this.o.sql,
1780
+ `UPDATE judge_holds SET state = 'releasing' WHERE id = ? AND state = 'held' AND expires_at > ? RETURNING *`,
1781
+ id,
1782
+ this.o.now()
1783
+ );
1784
+ if (!row) throw new RelayError2("not_found", `no held message ${id}`);
1785
+ const params = JSON.parse(row.params);
1786
+ const verdict = JSON.parse(row.verdict);
1787
+ let sent = params;
1788
+ let redacted;
1789
+ if (options.redacted) {
1790
+ const applied = applyRedactions(params.body, verdict.redactions);
1791
+ if (!applied.n) {
1792
+ this.o.sql.all(`UPDATE judge_holds SET state = 'held' WHERE id = ?`, id);
1793
+ throw new RelayError2("invalid_params", `hold ${id} has no redactions: release it as it is or refuse it`);
1794
+ }
1795
+ sent = { ...params, body: applied.text };
1796
+ redacted = { n: applied.n, kinds: applied.kinds };
1797
+ }
1798
+ let result;
1799
+ try {
1800
+ result = await deliver(row.from_ep, sent, JSON.parse(row.origin));
1801
+ } catch (error) {
1802
+ this.o.sql.all(`UPDATE judge_holds SET state = 'held' WHERE id = ?`, id);
1803
+ throw error;
1804
+ }
1805
+ this.remember(row.from_ep, params, { ...result, ...redacted ? { redacted } : {} });
1806
+ this.resolve(row, "released", options.by);
1807
+ this.o.core.auditJudge("judge_override", row.from_ep, id, {
1808
+ ...this.storedDetail(row, verdict),
1809
+ by: options.by,
1810
+ action: redacted ? "release_redacted" : "release",
1811
+ ...redacted ? { redacted } : {}
1812
+ });
1813
+ this.tell(
1814
+ row.from_ep,
1815
+ `relay: your message held for review as ${id} was released by your human${redacted ? " with redactions" : ""} and delivered as ${result.id}`
1816
+ );
1817
+ return { ...result, ...redacted ? { redacted } : {} };
1818
+ }
1819
+ /** refuses a held message: it is never delivered, and its sender hears the human's reason */
1820
+ refuse(id, reason, by) {
1821
+ this.expire(this.o.now());
1822
+ const row = one2(this.o.sql, `SELECT * FROM judge_holds WHERE id = ? AND state = 'held'`, id);
1823
+ if (!row) throw new RelayError2("not_found", `no held message ${id}`);
1824
+ const verdict = JSON.parse(row.verdict);
1825
+ this.resolve(row, "refused", by);
1826
+ this.o.core.auditJudge("judge_override", row.from_ep, id, { ...this.storedDetail(row, verdict), by, action: "refuse", reason });
1827
+ this.tell(row.from_ep, `relay: your message held for review as ${id} was refused by your human: ${reason}`);
1828
+ return this.view(this.row(id));
1829
+ }
1830
+ /** expires holds past their message TTL (body purged, sender told) and drops resolved ones past the audit window */
1831
+ sweep() {
1832
+ const now = this.o.now();
1833
+ this.expire(now);
1834
+ this.o.sql.all(`DELETE FROM judge_holds WHERE state IN ('released', 'refused', 'expired') AND resolved_at <= ?`, now - LIMITS2.auditKeepS * 1e3);
1835
+ this.o.sql.all(`DELETE FROM judge_sent WHERE created_at <= ?`, now - LIMITS2.idempotencyS * 1e3);
1836
+ for (const [key, times] of this.spent) if (!times.length || times[times.length - 1] <= now - 36e5) this.spent.delete(key);
1837
+ }
1838
+ expire(now) {
1839
+ for (const row of this.o.sql.all(`SELECT * FROM judge_holds WHERE state = 'held' AND expires_at <= ?`, now)) {
1840
+ const verdict = JSON.parse(row.verdict);
1841
+ this.resolve(row, "expired", null);
1842
+ this.o.core.auditJudge("judge_expire", row.from_ep, row.id, this.storedDetail(row, verdict));
1843
+ this.tell(row.from_ep, `relay: your message held for review as ${row.id} expired without review and was not delivered`);
1844
+ }
1845
+ }
1846
+ /** the recipients by the boundary their message crosses; throws as send does for a target the sender cannot reach */
1847
+ boundaries(policy, from, params, origin) {
1848
+ const subjects = () => this.o.core.judgeSubjects(from, params, origin);
1849
+ const { sender, recipients, tree } = this.o.within ? this.o.within(origin, subjects) : subjects();
1850
+ const groups = /* @__PURE__ */ new Map();
1851
+ for (const recipient of recipients) {
1852
+ const label = judgeBoundary(policy, sender, recipient);
1853
+ if (label !== null) groups.set(label, [...groups.get(label) ?? [], recipient.address]);
1854
+ }
1855
+ return { groups, tree };
1856
+ }
1857
+ async judge(policy, from, body, groups) {
1858
+ const verdicts = await Promise.all(
1859
+ [...groups].map(
1860
+ ([boundary, to]) => runJudge({
1861
+ body,
1862
+ side: "local",
1863
+ boundary,
1864
+ configVersion: this.version,
1865
+ client: this.client,
1866
+ cache: this.cache,
1867
+ timeoutMs: policy.timeout_s * 1e3,
1868
+ failMode: policy.fail,
1869
+ ...this.client && policy.model ? { model: policy.model.id } : {},
1870
+ from: this.o.core.addressOf(from),
1871
+ to,
1872
+ now: this.o.now,
1873
+ stats: this.stats
1874
+ })
1875
+ )
1876
+ );
1877
+ return mergeVerdicts(verdicts);
1878
+ }
1879
+ shadow(policy, from, params, origin, ref) {
1880
+ const task = (async () => {
1881
+ try {
1882
+ const { groups, tree } = this.boundaries(policy, from, params, origin);
1883
+ if (!groups.size) return;
1884
+ this.spend(policy, from, tree);
1885
+ const body = params.body;
1886
+ const verdict = await this.judge(policy, from, body, groups);
1887
+ this.o.core.auditJudge("judge_shadow", from, ref, this.detail(verdict, body, groups));
1888
+ } catch (error) {
1889
+ this.o.log(`relay broker: a shadow judgement failed: ${error instanceof Error ? error.message : String(error)}`);
1890
+ }
1891
+ })();
1892
+ this.shadows.add(task);
1893
+ void task.finally(() => this.shadows.delete(task));
1894
+ }
1895
+ hold(from, params, origin, verdict, groups, detail) {
1896
+ const now = this.o.now();
1897
+ const id = newId2("hold", now);
1898
+ const stored2 = { ...params, client_id: params.client_id ?? id };
1899
+ const to = [...groups.values()].flat();
1900
+ const record = { ...verdict, boundary: [...groups.keys()].join(" | "), to };
1901
+ this.o.sql.all(
1902
+ `INSERT INTO judge_holds (id, from_ep, params, origin, verdict, created_at, expires_at, state) VALUES (?, ?, ?, ?, ?, ?, ?, 'held')`,
1903
+ id,
1904
+ from,
1905
+ JSON.stringify(stored2),
1906
+ JSON.stringify(origin),
1907
+ JSON.stringify(record),
1908
+ now,
1909
+ now + (params.ttl_s ?? LIMITS2.ttlDefaultS) * 1e3
1910
+ );
1911
+ this.o.core.auditJudge("judge_hold", from, id, detail);
1912
+ return heldResult(id, params);
1913
+ }
1914
+ detail(verdict, body, groups) {
1915
+ return {
1916
+ side: "local",
1917
+ boundary: [...groups.keys()].join(" | "),
1918
+ prompt_version: verdict.prompt_version,
1919
+ categories: verdict.categories,
1920
+ sha256: sha256Hex(body),
1921
+ verdict: verdict.verdict,
1922
+ source: verdict.source,
1923
+ reason: verdict.reason,
1924
+ to: [...groups.values()].flat(),
1925
+ ...verdict.model ? { model: verdict.model } : {},
1926
+ ...verdict.ms !== void 0 ? { ms: verdict.ms } : {}
1927
+ };
1928
+ }
1929
+ storedDetail(row, verdict) {
1930
+ const params = JSON.parse(row.params);
1931
+ return {
1932
+ side: "local",
1933
+ boundary: verdict.boundary,
1934
+ prompt_version: verdict.prompt_version,
1935
+ categories: verdict.categories,
1936
+ sha256: params.sha256 ?? sha256Hex(params.body ?? ""),
1937
+ verdict: verdict.verdict,
1938
+ to: verdict.to,
1939
+ ...verdict.model ? { model: verdict.model } : {}
1940
+ };
1941
+ }
1942
+ /** marks a hold resolved and purges its body and the judge's quotes of it */
1943
+ resolve(row, state, by) {
1944
+ const { body, ...params } = JSON.parse(row.params);
1945
+ const verdict = JSON.parse(row.verdict);
1946
+ const purged = { ...params, bytes: new TextEncoder().encode(body).length, sha256: sha256Hex(body) };
1947
+ const kinds = { ...verdict, redactions: verdict.redactions.map((r) => ({ kind: r.kind })) };
1948
+ this.o.sql.all(
1949
+ `UPDATE judge_holds SET state = ?, by = ?, resolved_at = ?, params = ?, verdict = ? WHERE id = ?`,
1950
+ state,
1951
+ by,
1952
+ this.o.now(),
1953
+ JSON.stringify(purged),
1954
+ JSON.stringify(kinds),
1955
+ row.id
1956
+ );
1957
+ }
1958
+ tell(endpoint, text) {
1959
+ try {
1960
+ if (this.o.notice) this.o.notice(endpoint, text);
1961
+ else {
1962
+ this.o.core.judgeNotice(endpoint, text);
1963
+ this.o.wake(endpoint);
1964
+ }
1965
+ } catch (error) {
1966
+ if (!(error instanceof RelayError2)) throw error;
1967
+ }
1968
+ }
1969
+ row(id) {
1970
+ const row = one2(this.o.sql, `SELECT * FROM judge_holds WHERE id = ?`, id);
1971
+ if (!row) throw new RelayError2("not_found", `no held message ${id}`);
1972
+ return row;
1973
+ }
1974
+ view(row) {
1975
+ const params = JSON.parse(row.params);
1976
+ const verdict = JSON.parse(row.verdict);
1977
+ return {
1978
+ id: row.id,
1979
+ from: this.o.core.addressOf(row.from_ep),
1980
+ to: verdict.to,
1981
+ state: row.state,
1982
+ created_at: iso(row.created_at),
1983
+ expires_at: iso(row.expires_at),
1984
+ categories: verdict.categories,
1985
+ reason: verdict.reason,
1986
+ boundary: verdict.boundary,
1987
+ source: verdict.source,
1988
+ bytes: params.bytes ?? new TextEncoder().encode(params.body ?? "").length,
1989
+ by: row.by,
1990
+ resolved_at: iso(row.resolved_at)
1991
+ };
1992
+ }
1993
+ };
1994
+ var fingerprint = (p) => sha256Hex(
1995
+ JSON.stringify([
1996
+ [p.to].flat().map(String).sort(),
1997
+ p.body,
1998
+ p.kind ?? "message",
1999
+ p.thread ?? null,
2000
+ p.reply_to ?? null,
2001
+ p.data_class ?? "public",
2002
+ (p.attachments ?? []).map((a) => a?.path).sort(),
2003
+ p.mime ?? null,
2004
+ p.ttl_s ?? LIMITS2.ttlDefaultS,
2005
+ p.require_reply ?? null
2006
+ ])
2007
+ );
2008
+ function heldResult(id, params) {
2009
+ return { id, thread: params.thread ?? "", hop: 0, deliveries: [], held: { hold_id: id, side: "local" } };
2010
+ }
2011
+
2012
+ // src/hub.ts
1531
2013
  var GRAPH_MINUTES = 15;
1532
2014
  var GRAPH_MINUTES_MAX = 60;
1533
2015
  var HUB_LIMITS = {
@@ -1563,9 +2045,9 @@ var HUB_MIGRATION = [
1563
2045
  ];
1564
2046
  var OBSERVER_METHODS = /* @__PURE__ */ new Set(["list", "snapshot", "watch", "log", "machines", "machines_rm"]);
1565
2047
  var CLIENT_METHODS = /* @__PURE__ */ new Set(["whoami", "send", "ask", "ask_wait", "inbox", "ack", "status", "event", ...OBSERVER_METHODS]);
1566
- var iso = (ms) => ms === null ? null : new Date(ms).toISOString();
2048
+ var iso2 = (ms) => ms === null ? null : new Date(ms).toISOString();
1567
2049
  function rpcError(error) {
1568
- if (!(error instanceof RelayError2)) return { code: -32603, message: "internal error", data: { code: "unavailable" } };
2050
+ if (!(error instanceof RelayError3)) return { code: -32603, message: "internal error", data: { code: "unavailable" } };
1569
2051
  const code = error.code === "method_not_found" ? -32601 : error.code === "invalid_params" ? -32602 : -32e3;
1570
2052
  return { code, message: error.message, data: { code: error.code, ...error.retryAfterS ? { retry_after: error.retryAfterS } : {} } };
1571
2053
  }
@@ -1573,16 +2055,16 @@ function mirrorSync(value) {
1573
2055
  if (value === void 0 || value === null) return null;
1574
2056
  const { id, part, parts } = value;
1575
2057
  if (typeof id !== "string" || !id || id.length > 64 || !Number.isInteger(parts) || parts < 1 || parts > 4096 || !Number.isInteger(part) || part < 1 || part > parts) {
1576
- throw new RelayError2("invalid_params", "sync is { id, part, parts } of a full mirror in several frames");
2058
+ throw new RelayError3("invalid_params", "sync is { id, part, parts } of a full mirror in several frames");
1577
2059
  }
1578
2060
  return { id, part, parts };
1579
2061
  }
1580
2062
  function serverFields(params) {
1581
- for (const field of SERVER_FIELDS) if (field in params) throw new RelayError2("invalid_params", `${field} is set by the relay, not the sender`);
2063
+ for (const field of SERVER_FIELDS) if (field in params) throw new RelayError3("invalid_params", `${field} is set by the relay, not the sender`);
1582
2064
  }
1583
2065
  function object(value, what) {
1584
2066
  if (value === void 0) return {};
1585
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new RelayError2("invalid_params", `${what} must be an object`);
2067
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new RelayError3("invalid_params", `${what} must be an object`);
1586
2068
  return value;
1587
2069
  }
1588
2070
  var fullLogin = (grant) => grant.scoped === false;
@@ -1629,6 +2111,8 @@ var RelayHub = class _RelayHub {
1629
2111
  xcounters = null;
1630
2112
  xkey = null;
1631
2113
  limits;
2114
+ judgeConfig = null;
2115
+ gate = null;
1632
2116
  /** idempotent: migrates and records the account handle (the address user part, e.g. "ada"); call before handling each request */
1633
2117
  init(account) {
1634
2118
  if (this.core && this.account === account.user) return;
@@ -1649,6 +2133,7 @@ var RelayHub = class _RelayHub {
1649
2133
  });
1650
2134
  core.migrate();
1651
2135
  this.core = core;
2136
+ this.gate = null;
1652
2137
  this.account = account.user;
1653
2138
  if (this.deps.crossAccount) {
1654
2139
  this.xkey = core.xstate("key", () => randomHex2(32));
@@ -1663,12 +2148,37 @@ var RelayHub = class _RelayHub {
1663
2148
  this.denied = null;
1664
2149
  this.xcounters = null;
1665
2150
  this.xkey = null;
2151
+ this.gate = null;
1666
2152
  this.buckets.clear();
1667
2153
  this.gone.clear();
1668
2154
  }
2155
+ /**
2156
+ * judges this account's own sends (client send and ask, bridge forward) before delivery; sends to other accounts are left to the
2157
+ * cross-account gate. Without a policy, or with mode off, the hub holds no judge and creates no judge tables
2158
+ */
2159
+ configureJudge(policy, options = { env: {} }) {
2160
+ this.gate = null;
2161
+ this.judgeConfig = policy && policy.mode !== "off" ? { policy, options } : null;
2162
+ }
2163
+ /** the held messages, newest first, without bodies */
2164
+ holds(state) {
2165
+ return this.judged().holds(state);
2166
+ }
2167
+ /** one hold with its body while it waits */
2168
+ holdShow(id) {
2169
+ return this.judged().show(id);
2170
+ }
2171
+ /** delivers a held message as its sender, every send check run again */
2172
+ holdRelease(id, options) {
2173
+ return this.judged().release(id, options, this.deliverJudged);
2174
+ }
2175
+ /** refuses a held message: never delivered, its sender told why */
2176
+ holdRefuse(id, reason, by) {
2177
+ return this.judged().refuse(id, reason, by);
2178
+ }
1669
2179
  /** a fresh connection state for an accepted socket */
1670
2180
  static connection(grant) {
1671
- return { id: newId2("conn"), grant, role: "new", machine: null, endpoint: null, watching: false, subscribed: false };
2181
+ return { id: newId3("conn"), grant, role: "new", machine: null, endpoint: null, watching: false, subscribed: false };
1672
2182
  }
1673
2183
  /** a socket would exceed the `sockets` limit: the host refuses it (429) instead of accepting */
1674
2184
  full() {
@@ -1677,7 +2187,7 @@ var RelayHub = class _RelayHub {
1677
2187
  /** one JSON-RPC text frame from a socket; replies and pushes go out through socket.send */
1678
2188
  async message(socket, text) {
1679
2189
  if (new TextEncoder().encode(text).length > this.limits.frameBytes) {
1680
- this.write(socket, { jsonrpc: "2.0", id: null, error: rpcError(new RelayError2("too_large", `a frame is at most ${this.limits.frameBytes} bytes`)) });
2190
+ this.write(socket, { jsonrpc: "2.0", id: null, error: rpcError(new RelayError3("too_large", `a frame is at most ${this.limits.frameBytes} bytes`)) });
1681
2191
  socket.close(1009, "frame too large");
1682
2192
  this.closed(socket);
1683
2193
  return;
@@ -1703,7 +2213,7 @@ var RelayHub = class _RelayHub {
1703
2213
  /** POST /v1/relay/rpc: a JSON-RPC request or batch run in order on a short-lived connection; returns the response(s) */
1704
2214
  async batch(grant, body) {
1705
2215
  if (JSON.stringify(body ?? null).length > this.limits.frameBytes) {
1706
- return { jsonrpc: "2.0", id: null, error: rpcError(new RelayError2("too_large", `a request is at most ${this.limits.frameBytes} bytes`)) };
2216
+ return { jsonrpc: "2.0", id: null, error: rpcError(new RelayError3("too_large", `a request is at most ${this.limits.frameBytes} bytes`)) };
1707
2217
  }
1708
2218
  const requests = Array.isArray(body) ? body : [body];
1709
2219
  if (!requests.length) return { jsonrpc: "2.0", id: null, error: { code: -32600, message: "an empty batch" } };
@@ -1823,6 +2333,7 @@ var RelayHub = class _RelayHub {
1823
2333
  this.gone.clear();
1824
2334
  for (const [key, bucket] of this.buckets) if (bucket.tokens + (now - bucket.at) * bucket.rate / 6e4 >= bucket.rate) this.buckets.delete(key);
1825
2335
  this.xcounters?.prune();
2336
+ this.judgeGate()?.sweep();
1826
2337
  this.setState("last_sweep", now);
1827
2338
  }
1828
2339
  if (this.idle(now)) {
@@ -1850,7 +2361,7 @@ var RelayHub = class _RelayHub {
1850
2361
  swept = now;
1851
2362
  this.setState("last_sweep", swept);
1852
2363
  }
1853
- const retrying = one2(this.deps.sql, `SELECT 1 FROM hub_state WHERE key LIKE ? LIMIT 1`, `${REVALIDATE_ERROR}%`) !== void 0;
2364
+ const retrying = one3(this.deps.sql, `SELECT 1 FROM hub_state WHERE key LIKE ? LIMIT 1`, `${REVALIDATE_ERROR}%`) !== void 0;
1854
2365
  return Math.min(
1855
2366
  validated + (retrying ? REVALIDATE_RETRY_MS : this.limits.revalidateMs),
1856
2367
  swept + SWEEP_MS,
@@ -1866,7 +2377,7 @@ var RelayHub = class _RelayHub {
1866
2377
  /** while the hub holds rows: the earliest unacked delivery's expiry, at most a day and at least a minute away */
1867
2378
  retentionAlarm(now) {
1868
2379
  const sql = this.deps.sql;
1869
- const held = one2(
2380
+ const held = one3(
1870
2381
  sql,
1871
2382
  `SELECT EXISTS (SELECT 1 FROM machines) OR EXISTS (SELECT 1 FROM machine_tombstones) OR EXISTS (SELECT 1 FROM endpoints WHERE handle NOT IN (?, ?))
1872
2383
  OR EXISTS (SELECT 1 FROM messages) OR EXISTS (SELECT 1 FROM deliveries) OR EXISTS (SELECT 1 FROM audit) AS held`,
@@ -1874,13 +2385,13 @@ var RelayHub = class _RelayHub {
1874
2385
  SYSTEM_HANDLE2
1875
2386
  ).held;
1876
2387
  if (!held) return null;
1877
- const expiry = one2(sql, `SELECT MIN(expires_at) AS at FROM deliveries WHERE acked_at IS NULL`).at;
2388
+ const expiry = one3(sql, `SELECT MIN(expires_at) AS at FROM deliveries WHERE acked_at IS NULL`).at;
1878
2389
  return Math.max(now + MIN_ALARM_MS, Math.min(expiry ?? Number.POSITIVE_INFINITY, now + DAY_MS));
1879
2390
  }
1880
2391
  /** no machine bound or tombstoned, no live socket and nothing queued */
1881
2392
  idle(now) {
1882
2393
  if (this.deps.sockets().some((s) => !this.gone.has(s.conn.id))) return false;
1883
- return !one2(
2394
+ return !one3(
1884
2395
  this.deps.sql,
1885
2396
  `SELECT EXISTS (SELECT 1 FROM machines) OR EXISTS (SELECT 1 FROM machine_tombstones) OR EXISTS (SELECT 1 FROM deliveries WHERE acked_at IS NULL AND expires_at > ?) AS busy`,
1886
2397
  now
@@ -1901,7 +2412,7 @@ var RelayHub = class _RelayHub {
1901
2412
  const result = await this.dispatch(socket, method, object(request.params, "params"));
1902
2413
  return request.id === void 0 ? null : { jsonrpc: "2.0", id, result };
1903
2414
  } catch (error) {
1904
- code = error instanceof RelayError2 ? error.code : "internal";
2415
+ code = error instanceof RelayError3 ? error.code : "internal";
1905
2416
  if (code === "internal") this.deps.log?.("relay_internal_error", { account: this.account, method, error: error instanceof Error ? error.name : "error" });
1906
2417
  return request.id === void 0 ? null : { jsonrpc: "2.0", id, error: rpcError(error) };
1907
2418
  } finally {
@@ -1919,7 +2430,7 @@ var RelayHub = class _RelayHub {
1919
2430
  const now = this.now();
1920
2431
  if (method === "ack" && (conn.role === "client" || conn.role === "bridge") || method === "reject" && conn.role === "bridge") {
1921
2432
  const seqs = params?.seqs;
1922
- const cost = method === "ack" && Array.isArray(seqs) ? Math.min(Math.max(seqs.length, 1), LIMITS2.inboxPage) : 1;
2433
+ const cost = method === "ack" && Array.isArray(seqs) ? Math.min(Math.max(seqs.length, 1), LIMITS3.inboxPage) : 1;
1923
2434
  const token = this.bucket(`${conn.grant.tokenId}:ack`, this.limits.acksPerMinute, now);
1924
2435
  const account2 = this.bucket("account:ack", this.limits.accountAcksPerMinute, now);
1925
2436
  for (const [b, scope] of [
@@ -1927,7 +2438,7 @@ var RelayHub = class _RelayHub {
1927
2438
  [account2, "an account"]
1928
2439
  ]) {
1929
2440
  if (b.tokens < cost)
1930
- throw new RelayError2("rate_limited", `at most ${b.rate} acked seqs per minute on ${scope}`, Math.max(1, Math.ceil((cost - b.tokens) * 60 / b.rate)));
2441
+ throw new RelayError3("rate_limited", `at most ${b.rate} acked seqs per minute on ${scope}`, Math.max(1, Math.ceil((cost - b.tokens) * 60 / b.rate)));
1931
2442
  }
1932
2443
  token.tokens -= cost;
1933
2444
  account2.tokens -= cost;
@@ -1940,7 +2451,7 @@ var RelayHub = class _RelayHub {
1940
2451
  if (method === "event") {
1941
2452
  for (const b of [bucket, account]) {
1942
2453
  if (b.tokens < b.rate / 4)
1943
- throw new RelayError2(
2454
+ throw new RelayError3(
1944
2455
  "rate_limited",
1945
2456
  "activity events wait while the token or the account is near its call limit",
1946
2457
  Math.max(1, Math.ceil((b.rate / 4 - b.tokens) * 60 / b.rate))
@@ -1952,14 +2463,14 @@ var RelayHub = class _RelayHub {
1952
2463
  socket.close(1013, "rate limited");
1953
2464
  this.closed(socket);
1954
2465
  }
1955
- throw new RelayError2(
2466
+ throw new RelayError3(
1956
2467
  "rate_limited",
1957
2468
  `at most ${rate} ${bridge ? "bridge " : ""}calls per minute on a token`,
1958
2469
  Math.max(1, Math.ceil((1 - bucket.tokens) * 60 / rate))
1959
2470
  );
1960
2471
  }
1961
2472
  if (account.tokens < 1) {
1962
- throw new RelayError2(
2473
+ throw new RelayError3(
1963
2474
  "rate_limited",
1964
2475
  `at most ${this.limits.accountCallsPerMinute} calls per minute on an account`,
1965
2476
  Math.max(1, Math.ceil((1 - account.tokens) * 60 / account.rate))
@@ -1978,36 +2489,36 @@ var RelayHub = class _RelayHub {
1978
2489
  return bucket;
1979
2490
  }
1980
2491
  async dispatch(socket, method, params) {
1981
- if (this.gone.has(socket.conn.id)) throw new RelayError2("unauthorized", "this connection is closed");
2492
+ if (this.gone.has(socket.conn.id)) throw new RelayError3("unauthorized", "this connection is closed");
1982
2493
  if (method === "hello") return this.hello(socket, params);
1983
- if (socket.conn.role === "new") throw new RelayError2("unauthorized", "call hello first");
2494
+ if (socket.conn.role === "new") throw new RelayError3("unauthorized", "call hello first");
1984
2495
  if (socket.conn.role === "bridge") return this.bridgeMethod(socket, method, params);
1985
2496
  if (socket.conn.role === "observer" && !OBSERVER_METHODS.has(method)) {
1986
- if (CLIENT_METHODS.has(method)) throw new RelayError2("unauthorized", "this connection only observes: say hello as an agent or the human");
1987
- throw new RelayError2("method_not_found", `no method ${method}`);
2497
+ if (CLIENT_METHODS.has(method)) throw new RelayError3("unauthorized", "this connection only observes: say hello as an agent or the human");
2498
+ throw new RelayError3("method_not_found", `no method ${method}`);
1988
2499
  }
1989
2500
  return this.clientMethod(socket, method, params);
1990
2501
  }
1991
2502
  hello(socket, params) {
1992
2503
  const core = this.relay;
1993
2504
  const conn = socket.conn;
1994
- if (conn.role !== "new") throw new RelayError2("invalid_params", "hello was already called on this connection");
1995
- if (params.protocol !== PROTOCOL) throw new RelayError2("invalid_params", `this relay speaks ${PROTOCOL}`);
2505
+ if (conn.role !== "new") throw new RelayError3("invalid_params", "hello was already called on this connection");
2506
+ if (params.protocol !== PROTOCOL) throw new RelayError3("invalid_params", `this relay speaks ${PROTOCOL}`);
1996
2507
  if (params.transient !== void 0)
1997
- throw new RelayError2("invalid_params", "the hub takes no hook connections: hooks report to the local broker, which bridges them");
2508
+ throw new RelayError3("invalid_params", "the hub takes no hook connections: hooks report to the local broker, which bridges them");
1998
2509
  serverFields(params);
1999
2510
  const observe = params.observe === true;
2000
2511
  const named = params.machine;
2001
2512
  if ((!observe || named !== void 0) && (typeof named !== "string" || !MACHINE_NAME.test(named) || named === "me" || named === "local" || named === this.account)) {
2002
- throw new RelayError2("invalid_params", "machine is a name of lowercase letters, digits and -, other than me, local and the account handle");
2513
+ throw new RelayError3("invalid_params", "machine is a name of lowercase letters, digits and -, other than me, local and the account handle");
2003
2514
  }
2004
2515
  const bridge = params.bridge === true;
2005
2516
  const human = params.human === true;
2006
2517
  this.deps.sql.all(`DELETE FROM hub_state WHERE key = 'idle_since'`);
2007
- const info = { account: this.account, capabilities: CAPABILITIES, server_time: new Date(this.now()).toISOString(), limits: { ...LIMITS2, ...this.limits } };
2518
+ const info = { account: this.account, capabilities: CAPABILITIES, server_time: new Date(this.now()).toISOString(), limits: { ...LIMITS3, ...this.limits } };
2008
2519
  if (observe) {
2009
2520
  if (bridge || human || params.claim !== void 0 || params.resume !== void 0)
2010
- throw new RelayError2("invalid_params", "an observer hello has no bridge, human, claim or resume");
2521
+ throw new RelayError3("invalid_params", "an observer hello has no bridge, human, claim or resume");
2011
2522
  conn.role = "observer";
2012
2523
  conn.machine = typeof named === "string" ? named : null;
2013
2524
  this.save(socket);
@@ -2015,11 +2526,11 @@ var RelayHub = class _RelayHub {
2015
2526
  }
2016
2527
  const machine = named;
2017
2528
  const base = { ...info, machine };
2018
- if (bridge && this.transient.has(socket)) throw new RelayError2("invalid_params", "a bridge connects over the WebSocket");
2529
+ if (bridge && this.transient.has(socket)) throw new RelayError3("invalid_params", "a bridge connects over the WebSocket");
2019
2530
  if (human && typeof params.tty !== "boolean")
2020
- throw new RelayError2("invalid_params", "a human hello says whether a terminal is attached (tty: true or false)");
2531
+ throw new RelayError3("invalid_params", "a human hello says whether a terminal is attached (tty: true or false)");
2021
2532
  if (human) {
2022
- if (!fullLogin(conn.grant)) throw new RelayError2("unauthorized", "a scoped token acts as an agent: use --as");
2533
+ if (!fullLogin(conn.grant)) throw new RelayError3("unauthorized", "a scoped token acts as an agent: use --as");
2023
2534
  const endpoint2 = core.claimHuman(params.tty);
2024
2535
  conn.role = "client";
2025
2536
  conn.machine = machine;
@@ -2046,8 +2557,8 @@ var RelayHub = class _RelayHub {
2046
2557
  core.record("bridge", null, machine, { machine });
2047
2558
  return { ...base, endpoints: core.remoteViews({ except: machine }), others: this.othersOf(conn.grant, machine) };
2048
2559
  }
2049
- if (one2(this.deps.sql, `SELECT kind FROM machines WHERE name = ?`, machine)?.kind === "bridge") {
2050
- throw new RelayError2(
2560
+ if (one3(this.deps.sql, `SELECT kind FROM machines WHERE name = ?`, machine)?.kind === "bridge") {
2561
+ throw new RelayError3(
2051
2562
  "invalid_params",
2052
2563
  `machine ${machine} is linked through its broker's bridge; run the command there without --cloud, or pick another --machine`
2053
2564
  );
@@ -2068,23 +2579,23 @@ var RelayHub = class _RelayHub {
2068
2579
  }
2069
2580
  /** 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 */
2070
2581
  bindable(machine, grant) {
2071
- const bound = one2(this.deps.sql, `SELECT token_id FROM machines WHERE name = ?`, machine);
2582
+ const bound = one3(this.deps.sql, `SELECT token_id FROM machines WHERE name = ?`, machine);
2072
2583
  if (bound && bound.token_id !== grant.tokenId) {
2073
2584
  this.relay.record("refuse", null, machine, { code: "unauthorized", machine });
2074
- throw new RelayError2("unauthorized", `machine ${machine} is bound to another token; release it with ${CLI_NAME} machines rm ${machine}`);
2585
+ throw new RelayError3("unauthorized", `machine ${machine} is bound to another token; release it with ${CLI_NAME} machines rm ${machine}`);
2075
2586
  }
2076
2587
  if (bound) return true;
2077
- const tomb = one2(this.deps.sql, `SELECT actor_id FROM machine_tombstones WHERE name = ?`, machine);
2588
+ const tomb = one3(this.deps.sql, `SELECT actor_id FROM machine_tombstones WHERE name = ?`, machine);
2078
2589
  if (tomb && !fullOf(grant, tomb.actor_id) && !accountAdmin(grant)) {
2079
2590
  this.relay.record("refuse", null, machine, { code: "unauthorized", machine });
2080
- throw new RelayError2(
2591
+ throw new RelayError3(
2081
2592
  "unauthorized",
2082
2593
  `machine ${machine} was unbound when its token was revoked; a full login of the same member (or an org owner or admin) can take it back, or ${CLI_NAME} machines rm ${machine} drops its queued messages`
2083
2594
  );
2084
2595
  }
2085
- const count = one2(this.deps.sql, `SELECT COUNT(*) AS n FROM machines`).n;
2596
+ const count = one3(this.deps.sql, `SELECT COUNT(*) AS n FROM machines`).n;
2086
2597
  if (count >= this.limits.machines)
2087
- throw new RelayError2("too_large", `at most ${this.limits.machines} machines per account; release one with ${CLI_NAME} machines rm`);
2598
+ throw new RelayError3("too_large", `at most ${this.limits.machines} machines per account; release one with ${CLI_NAME} machines rm`);
2088
2599
  return false;
2089
2600
  }
2090
2601
  /** binds the machine to the token once its hello succeeded; `held` is what bindable() returned */
@@ -2141,7 +2652,7 @@ var RelayHub = class _RelayHub {
2141
2652
  case "machines_rm":
2142
2653
  return this.releaseMachine(socket, params.name);
2143
2654
  }
2144
- if (!me) throw new RelayError2("unauthorized", "this connection has no endpoint");
2655
+ if (!me) throw new RelayError3("unauthorized", "this connection has no endpoint");
2145
2656
  switch (method) {
2146
2657
  case "whoami":
2147
2658
  return core.whoami(me);
@@ -2151,15 +2662,14 @@ var RelayHub = class _RelayHub {
2151
2662
  const send = { ...params };
2152
2663
  delete send.wait_s;
2153
2664
  if (method === "ask") send.kind = "request";
2154
- const bytes = this.room(me, send.body);
2155
- const result = core.within(conn.machine, () => core.send(me, send, conn.tty === void 0 ? {} : { tty: conn.tty }));
2156
- if (!result.duplicate) this.stash(bytes);
2157
- if (!result.duplicate) this.delivered(me, result, send.kind ?? "message");
2158
- if (method === "send") return result;
2665
+ const origin = conn.tty === void 0 ? {} : { tty: conn.tty };
2666
+ const gate = this.judgeGate();
2667
+ const result = gate ? await gate.send(me, send, { ...origin, machine: conn.machine }, this.deliverJudged) : this.deliverOn(conn.machine, me, send, origin);
2668
+ if (method === "send" || result.held) return result;
2159
2669
  return this.awaitReply(socket, me, result.id, this.waitMs(socket, params.wait_s), result);
2160
2670
  }
2161
2671
  case "ask_wait": {
2162
- if (typeof params.id !== "string") throw new RelayError2("invalid_params", "id is the request's message id");
2672
+ if (typeof params.id !== "string") throw new RelayError3("invalid_params", "id is the request's message id");
2163
2673
  return this.awaitReply(socket, me, params.id, this.waitMs(socket, params.wait_s));
2164
2674
  }
2165
2675
  case "inbox": {
@@ -2187,7 +2697,7 @@ var RelayHub = class _RelayHub {
2187
2697
  return row;
2188
2698
  }
2189
2699
  }
2190
- throw new RelayError2("method_not_found", `no method ${method}`);
2700
+ throw new RelayError3("method_not_found", `no method ${method}`);
2191
2701
  }
2192
2702
  async bridgeMethod(socket, method, params) {
2193
2703
  const core = this.relay;
@@ -2225,20 +2735,19 @@ var RelayHub = class _RelayHub {
2225
2735
  const send = { ...object(params.params, "params") };
2226
2736
  serverFields(send);
2227
2737
  const asked = params.hop_floor ?? 0;
2228
- if (!Number.isInteger(asked) || asked < 0 || asked > 1e3) throw new RelayError2("invalid_params", "hop_floor is a hop count");
2738
+ if (!Number.isInteger(asked) || asked < 0 || asked > 1e3) throw new RelayError3("invalid_params", "hop_floor is a hop count");
2229
2739
  if ([send.to].flat().some((to) => core.foreignAccount(to) !== null)) return this.xsend(sender, send, { tty: params.tty === true, hop: asked });
2230
2740
  delete send.attachments;
2231
- const floor = Math.min(asked, LIMITS2.hopMax + 1);
2741
+ const floor = Math.min(asked, LIMITS3.hopMax + 1);
2232
2742
  if (floor > 0) core.markReceived(sender, floor - 1);
2233
- const bytes = this.room(sender, send.body);
2234
- const result = core.within(machine, () => core.send(sender, send, { tty: params.tty === true }));
2235
- if (!result.duplicate) this.stash(bytes);
2236
- if (!result.duplicate) this.delivered(sender, result, send.kind ?? "message");
2237
- return result;
2743
+ const gate = this.judgeGate();
2744
+ const origin = { tty: params.tty === true };
2745
+ if (gate) return gate.send(sender, send, { ...origin, machine }, this.deliverJudged);
2746
+ return this.deliverOn(machine, sender, send, origin);
2238
2747
  }
2239
2748
  case "subscribe": {
2240
2749
  const after = params.after ?? 0;
2241
- if (!Number.isInteger(after) || after < 0) throw new RelayError2("invalid_params", "after is a seq");
2750
+ if (!Number.isInteger(after) || after < 0) throw new RelayError3("invalid_params", "after is a seq");
2242
2751
  if (this.deps.crossAccount) core.xsenders(await this.xinbound());
2243
2752
  conn.subscribed = true;
2244
2753
  this.save(socket);
@@ -2275,7 +2784,7 @@ var RelayHub = class _RelayHub {
2275
2784
  this.save(socket);
2276
2785
  return core.snapshot({ activity: 20 });
2277
2786
  }
2278
- throw new RelayError2("method_not_found", `no method ${method} on a bridge`);
2787
+ throw new RelayError3("method_not_found", `no method ${method} on a bridge`);
2279
2788
  }
2280
2789
  // other accounts
2281
2790
  /**
@@ -2287,22 +2796,22 @@ var RelayHub = class _RelayHub {
2287
2796
  const core = this.relay;
2288
2797
  const link = this.deps.crossAccount;
2289
2798
  const targets = [send.to].flat();
2290
- const missing = () => new RelayError2("not_found", `no endpoint ${targets.map(String).join(", ")}`);
2291
- if (targets.length !== 1) throw new RelayError2("invalid_params", "a message to another account goes to exactly one endpoint: no lists, groups or tree:");
2799
+ const missing = () => new RelayError3("not_found", `no endpoint ${targets.map(String).join(", ")}`);
2800
+ if (targets.length !== 1) throw new RelayError3("invalid_params", "a message to another account goes to exactly one endpoint: no lists, groups or tree:");
2292
2801
  const target = core.foreignAccount(targets[0]);
2293
2802
  if (!target || !link || !this.xcounters || !this.xkey) throw missing();
2294
- if (typeof send.body !== "string") throw new RelayError2("invalid_params", "body must be a string");
2803
+ if (typeof send.body !== "string") throw new RelayError3("invalid_params", "body must be a string");
2295
2804
  const bytes = new TextEncoder().encode(send.body).length;
2296
- if (bytes > XLIMITS.bodyBytes) throw new RelayError2("too_large", `a message to another account is at most ${XLIMITS.bodyBytes} bytes; this one is ${bytes}`);
2805
+ if (bytes > XLIMITS.bodyBytes) throw new RelayError3("too_large", `a message to another account is at most ${XLIMITS.bodyBytes} bytes; this one is ${bytes}`);
2297
2806
  if (send.attachments !== void 0 && !(Array.isArray(send.attachments) && !send.attachments.length))
2298
- throw new RelayError2("data_class_refused", "attachments stay in this account");
2299
- if (send.data_class === "private") throw new RelayError2("data_class_refused", "a private message stays in this account");
2300
- if (send.data_class !== void 0 && send.data_class !== "public") throw new RelayError2("invalid_params", "data_class must be public or private");
2807
+ throw new RelayError3("data_class_refused", "attachments stay in this account");
2808
+ if (send.data_class === "private") throw new RelayError3("data_class_refused", "a private message stays in this account");
2809
+ if (send.data_class !== void 0 && send.data_class !== "public") throw new RelayError3("invalid_params", "data_class must be public or private");
2301
2810
  const kind = send.kind ?? "message";
2302
- if (!MESSAGE_KINDS2.includes(kind)) throw new RelayError2("invalid_params", `kind must be one of ${MESSAGE_KINDS2.join(", ")}`);
2303
- if (send.reply_to !== void 0 && typeof send.reply_to !== "string") throw new RelayError2("invalid_params", "reply_to is a message id");
2811
+ if (!MESSAGE_KINDS2.includes(kind)) throw new RelayError3("invalid_params", `kind must be one of ${MESSAGE_KINDS2.join(", ")}`);
2812
+ if (send.reply_to !== void 0 && typeof send.reply_to !== "string") throw new RelayError3("invalid_params", "reply_to is a message id");
2304
2813
  if (origin.hop > XLIMITS.hopMax)
2305
- throw new RelayError2(
2814
+ throw new RelayError3(
2306
2815
  "loop_guard",
2307
2816
  `this message would be hop ${origin.hop} of a chain across accounts (limit ${XLIMITS.hopMax}); stop and report to your user`
2308
2817
  );
@@ -2317,16 +2826,16 @@ var RelayHub = class _RelayHub {
2317
2826
  const granted = answered && answered.from.address === address ? parent : null;
2318
2827
  if (!granted && !core.crossAdmission(sender, "out", target).ok) throw missing();
2319
2828
  const now = this.now();
2320
- const thread = granted?.thread ?? cleanText2(send.thread, "thread", 64) ?? newId2("thr", now);
2829
+ const thread = granted?.thread ?? cleanText2(send.thread, "thread", 64) ?? newId3("thr", now);
2321
2830
  const hop = Math.max(origin.hop, granted ? answered.hop + 1 : 0);
2322
2831
  if (hop > XLIMITS.hopMax)
2323
- throw new RelayError2("loop_guard", `this message would be hop ${hop} of a chain across accounts (limit ${XLIMITS.hopMax}); stop and report to your user`);
2832
+ throw new RelayError3("loop_guard", `this message would be hop ${hop} of a chain across accounts (limit ${XLIMITS.hopMax}); stop and report to your user`);
2324
2833
  this.xcounters.check(contact, me.remote, address, thread);
2325
- const secret = findSecret2(send.body);
2326
- if (secret) throw new RelayError2("secret_refused", `the message looks like it carries a credential (${secret}); send a reference instead`);
2327
- const ttl = send.ttl_s ?? LIMITS2.ttlDefaultS;
2328
- if (!Number.isInteger(ttl) || ttl < 1 || ttl > LIMITS2.ttlMaxS)
2329
- throw new RelayError2("invalid_params", `ttl_s must be 1 to ${LIMITS2.ttlMaxS}`);
2834
+ const secret = findSecret3(send.body);
2835
+ if (secret) throw new RelayError3("secret_refused", `the message looks like it carries a credential (${secret}); send a reference instead`);
2836
+ const ttl = send.ttl_s ?? LIMITS3.ttlDefaultS;
2837
+ if (!Number.isInteger(ttl) || ttl < 1 || ttl > LIMITS3.ttlMaxS)
2838
+ throw new RelayError3("invalid_params", `ttl_s must be 1 to ${LIMITS3.ttlMaxS}`);
2330
2839
  const self = link.self;
2331
2840
  const own = `${self.handle}/${me.machine}/${me.handle || "me"}`;
2332
2841
  const human = me.kind === "human" && me.handle === "" && origin.tty;
@@ -2339,7 +2848,7 @@ var RelayHub = class _RelayHub {
2339
2848
  machine: me.machine,
2340
2849
  harness: me.harness
2341
2850
  };
2342
- const id = origin.id ?? newId2("msg", now);
2851
+ const id = origin.id ?? newId3("msg", now);
2343
2852
  const expiresAt = now + ttl * 1e3;
2344
2853
  const declared = declaredLimits(contact.limits);
2345
2854
  const msg = {
@@ -2351,7 +2860,7 @@ var RelayHub = class _RelayHub {
2351
2860
  thread,
2352
2861
  reply_to: replyTo,
2353
2862
  hop,
2354
- client_id: origin.clientId ?? this.deps.hash(`${own}|${address}|${cleanText2(send.client_id, "client_id", 128) ?? newId2("cid", now)}`),
2863
+ client_id: origin.clientId ?? this.deps.hash(`${own}|${address}|${cleanText2(send.client_id, "client_id", 128) ?? newId3("cid", now)}`),
2355
2864
  created_at: now,
2356
2865
  expires_at: expiresAt,
2357
2866
  ...granted?.grant ? { reply_grant: granted.grant, reply_grant_expires_at: granted.expires_at } : {},
@@ -2373,7 +2882,7 @@ var RelayHub = class _RelayHub {
2373
2882
  try {
2374
2883
  delivered = await link.deliver(contact, msg);
2375
2884
  } catch (error) {
2376
- if (!(error instanceof RelayError2) || error.code === "unavailable") refund();
2885
+ if (!(error instanceof RelayError3) || error.code === "unavailable") refund();
2377
2886
  throw error;
2378
2887
  }
2379
2888
  this.fleet({ type: "message", machine: me.machine, id: delivered.id, thread, kind, from: from.address, to: [address], hop });
@@ -2396,7 +2905,7 @@ var RelayHub = class _RelayHub {
2396
2905
  const core = this.relay;
2397
2906
  const link = this.deps.crossAccount;
2398
2907
  const msg = parseXMessage(raw);
2399
- const missing = () => new RelayError2("not_found", `no endpoint ${link?.self.handle ?? this.account}/${msg.to}`);
2908
+ const missing = () => new RelayError3("not_found", `no endpoint ${link?.self.handle ?? this.account}/${msg.to}`);
2400
2909
  if (!link || !this.xcounters || !this.xkey) throw missing();
2401
2910
  const contact = await link.contactById(fromAccountId);
2402
2911
  if (!contact || contact.dir === "out" || contact.peerId !== fromAccountId || msg.from.account !== contact.peerHandle) throw missing();
@@ -2412,11 +2921,11 @@ var RelayHub = class _RelayHub {
2412
2921
  handle: parts.length === 3 && parts[2] !== "me" ? parts[2] : ""
2413
2922
  };
2414
2923
  if (!granted && !core.crossAdmission(target.id, "in", peer).ok) throw missing();
2415
- if (!honestSender(msg.from)) throw new RelayError2("invalid_params", "the sender is not one its relay may name");
2924
+ if (!honestSender(msg.from)) throw new RelayError3("invalid_params", "the sender is not one its relay may name");
2416
2925
  const bytes = new TextEncoder().encode(msg.body).length;
2417
- if (bytes > XLIMITS.bodyBytes) throw new RelayError2("too_large", `a message from another account is at most ${XLIMITS.bodyBytes} bytes`);
2418
- if (msg.hop > XLIMITS.hopMax) throw new RelayError2("loop_guard", `hop ${msg.hop} is past the limit across accounts (${XLIMITS.hopMax})`);
2419
- if (msg.expires_at <= this.now()) throw new RelayError2("invalid_params", "the message expired");
2926
+ if (bytes > XLIMITS.bodyBytes) throw new RelayError3("too_large", `a message from another account is at most ${XLIMITS.bodyBytes} bytes`);
2927
+ if (msg.hop > XLIMITS.hopMax) throw new RelayError3("loop_guard", `hop ${msg.hop} is past the limit across accounts (${XLIMITS.hopMax})`);
2928
+ if (msg.expires_at <= this.now()) throw new RelayError3("invalid_params", "the message expired");
2420
2929
  const earlier = core.xduplicate(fromAccountId, msg.client_id);
2421
2930
  if (earlier) return { id: earlier.id, state: earlier.acked ? "delivered" : "queued", duplicate: true };
2422
2931
  const envelope = {
@@ -2446,8 +2955,8 @@ var RelayHub = class _RelayHub {
2446
2955
  const message = JSON.stringify(envelope);
2447
2956
  const stored2 = new TextEncoder().encode(message).length;
2448
2957
  this.xadmit(fromAccountId, contact, msg, target, stored2);
2449
- const secret = findSecret2(msg.body);
2450
- if (secret) throw new RelayError2("secret_refused", `the message looks like it carries a credential (${secret})`);
2958
+ const secret = findSecret3(msg.body);
2959
+ if (secret) throw new RelayError3("secret_refused", `the message looks like it carries a credential (${secret})`);
2451
2960
  const verdict = judged ? { verdict: "pass" } : await link.gate.ingress(msg, { side: "ingress", account: link.self.handle, peer: contact.peerHandle, peer_id: fromAccountId });
2452
2961
  if (verdict.verdict === "refuse") throw gateRefusal(verdict, "ingress");
2453
2962
  if (verdict.verdict === "hold") return { id: msg.id, state: "queued", held: { hold_id: verdict.hold_id } };
@@ -2513,16 +3022,16 @@ var RelayHub = class _RelayHub {
2513
3022
  const counts = core.xcounts(fromAccountId, msg.from.address, target.remote, dayStart(now));
2514
3023
  const retry = (since, ms) => Math.max(1, Math.ceil(((since ?? now) + ms - now) / 1e3));
2515
3024
  if (counts.minute >= perMinute)
2516
- throw new RelayError2("rate_limited", `${perMinute} messages from ${msg.from.address} to ${msg.to} a minute`, retry(counts.minuteAt, 6e4));
3025
+ throw new RelayError3("rate_limited", `${perMinute} messages from ${msg.from.address} to ${msg.to} a minute`, retry(counts.minuteAt, 6e4));
2517
3026
  if (counts.hour >= perHour)
2518
- throw new RelayError2("rate_limited", `${perHour} messages from ${msg.from.address} to ${msg.to} an hour`, retry(counts.hourAt, 36e5));
2519
- if (counts.day >= perDay) throw new RelayError2("rate_limited", `${perDay} messages from ${contact.peerHandle} today`, untilTomorrow(now));
3027
+ throw new RelayError3("rate_limited", `${perHour} messages from ${msg.from.address} to ${msg.to} an hour`, retry(counts.hourAt, 36e5));
3028
+ if (counts.day >= perDay) throw new RelayError3("rate_limited", `${perDay} messages from ${contact.peerHandle} today`, untilTomorrow(now));
2520
3029
  if (core.xthreadCount(target.remote, msg.thread) >= XLIMITS.threadMax)
2521
- throw new RelayError2("loop_guard", `thread ${msg.thread} reached ${XLIMITS.threadMax} messages across accounts`);
3030
+ throw new RelayError3("loop_guard", `thread ${msg.thread} reached ${XLIMITS.threadMax} messages across accounts`);
2522
3031
  const cap = this.limits.storedBytes;
2523
- if (this.storedNow() + bytes > cap) throw new RelayError2("inbox_full", `the receiving account's relay holds its cap of ${cap} bytes of messages`);
3032
+ if (this.storedNow() + bytes > cap) throw new RelayError3("inbox_full", `the receiving account's relay holds its cap of ${cap} bytes of messages`);
2524
3033
  const unread = core.xunread(target);
2525
- if (unread >= LIMITS2.inboxMax) throw new RelayError2("inbox_full", `${msg.to} has ${unread} unread messages`);
3034
+ if (unread >= LIMITS3.inboxMax) throw new RelayError3("inbox_full", `${msg.to} has ${unread} unread messages`);
2526
3035
  }
2527
3036
  /**
2528
3037
  * deletes the queued messages of every account that is no longer an active contact allowed to message this one, after the
@@ -2561,6 +3070,44 @@ var RelayHub = class _RelayHub {
2561
3070
  }))
2562
3071
  );
2563
3072
  }
3073
+ // judge
3074
+ /** the judge, made on first use once configured; null when it is not */
3075
+ judgeGate() {
3076
+ if (!this.judgeConfig) return null;
3077
+ if (this.gate) return this.gate;
3078
+ const core = this.relay;
3079
+ const { policy, options } = this.judgeConfig;
3080
+ const gate = new JudgeGate({
3081
+ sql: this.deps.sql,
3082
+ core,
3083
+ now: this.now,
3084
+ env: options.env,
3085
+ client: options.client,
3086
+ limits: options.limits,
3087
+ log: (line) => this.deps.log?.("relay_judge", { account: this.account, line }),
3088
+ wake: (endpoint) => this.wake(endpoint),
3089
+ notice: (endpoint, text) => this.xnotice(endpoint, text),
3090
+ within: (origin, fn) => core.within(origin.machine ?? null, fn)
3091
+ });
3092
+ gate.configure(policy);
3093
+ this.gate = gate;
3094
+ return gate;
3095
+ }
3096
+ judged() {
3097
+ const gate = this.judgeGate();
3098
+ if (!gate) throw new RelayError3("not_found", "this relay has no judge");
3099
+ return gate;
3100
+ }
3101
+ /** a send of this account as it runs unjudged: room for the body, the core send on the sender's machine, then push */
3102
+ deliverOn(machine, from, send, origin) {
3103
+ const core = this.relay;
3104
+ const bytes = this.room(from, send.body);
3105
+ const result = core.within(machine, () => core.send(from, send, origin));
3106
+ if (!result.duplicate) this.stash(bytes);
3107
+ if (!result.duplicate) this.delivered(from, result, send.kind ?? "message");
3108
+ return result;
3109
+ }
3110
+ deliverJudged = async (from, send, { machine, ...origin }) => this.deliverOn(machine ?? null, from, send, origin);
2564
3111
  // push and wait
2565
3112
  delivered(from, result, kind) {
2566
3113
  const core = this.relay;
@@ -2583,7 +3130,7 @@ var RelayHub = class _RelayHub {
2583
3130
  const cap = this.limits.storedBytes;
2584
3131
  if (this.storedNow() + bytes > cap) {
2585
3132
  this.relay.record("refuse", sender, null, { code: "too_large" });
2586
- throw new RelayError2(
3133
+ throw new RelayError3(
2587
3134
  "too_large",
2588
3135
  `the account relay holds its cap of ${cap} bytes of message bodies; ack or let messages expire, or release a machine with ${CLI_NAME} machines rm`
2589
3136
  );
@@ -2593,7 +3140,7 @@ var RelayHub = class _RelayHub {
2593
3140
  /** bytes of message bodies held, and of the messages from other accounts */
2594
3141
  storedNow() {
2595
3142
  const xinbox = this.deps.crossAccount ? ` + (SELECT COALESCE(SUM(LENGTH(CAST(message AS BLOB))), 0) FROM xinbox)` : "";
2596
- this.stored ??= one2(this.deps.sql, `SELECT (SELECT COALESCE(SUM(LENGTH(CAST(body AS BLOB))), 0) FROM messages)${xinbox} AS n`).n;
3143
+ this.stored ??= one3(this.deps.sql, `SELECT (SELECT COALESCE(SUM(LENGTH(CAST(body AS BLOB))), 0) FROM messages)${xinbox} AS n`).n;
2597
3144
  return this.stored;
2598
3145
  }
2599
3146
  stash(bytes) {
@@ -2633,7 +3180,7 @@ var RelayHub = class _RelayHub {
2633
3180
  }
2634
3181
  /** `own` tells each bridge whether a bound machine is its member's own device */
2635
3182
  toBridges(machine, params) {
2636
- const bound = one2(this.deps.sql, `SELECT actor_id FROM machines WHERE name = ?`, machine);
3183
+ const bound = one3(this.deps.sql, `SELECT actor_id FROM machines WHERE name = ?`, machine);
2637
3184
  for (const socket of this.deps.sockets()) {
2638
3185
  if (socket.conn.role !== "bridge" || socket.conn.machine === machine) continue;
2639
3186
  const own = bound ? { own: (bound.actor_id ?? null) === (socket.conn.grant.actorId ?? null) } : {};
@@ -2662,8 +3209,8 @@ var RelayHub = class _RelayHub {
2662
3209
  }
2663
3210
  waitMs(socket, value) {
2664
3211
  if (value === void 0) return 0;
2665
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new RelayError2("invalid_params", "wait_s must be a number of seconds");
2666
- return Math.min(value, this.transient.has(socket) ? this.limits.httpWaitS : LIMITS2.waitMaxS) * 1e3;
3212
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new RelayError3("invalid_params", "wait_s must be a number of seconds");
3213
+ return Math.min(value, this.transient.has(socket) ? this.limits.httpWaitS : LIMITS3.waitMaxS) * 1e3;
2667
3214
  }
2668
3215
  wake(endpoint) {
2669
3216
  const set = this.waiters.get(endpoint);
@@ -2719,7 +3266,7 @@ var RelayHub = class _RelayHub {
2719
3266
  const endpoint = conn.endpoint;
2720
3267
  core.disconnect(endpoint);
2721
3268
  const machine = core.machineOf(endpoint);
2722
- if (machine && one2(this.deps.sql, `SELECT kind FROM machines WHERE name = ?`, machine)?.kind === "bridge") {
3269
+ if (machine && one3(this.deps.sql, `SELECT kind FROM machines WHERE name = ?`, machine)?.kind === "bridge") {
2723
3270
  for (const delivery of core.adoptClaimed(machine, endpoint)) this.route(delivery.seq, delivery.endpoint);
2724
3271
  }
2725
3272
  this.wake(endpoint);
@@ -2790,7 +3337,7 @@ var RelayHub = class _RelayHub {
2790
3337
  const traffic = pairs.flatMap((row) => {
2791
3338
  const from = byId.get(row.from_ep);
2792
3339
  const to = byId.get(row.to_ep);
2793
- return from && to && from !== to ? [{ kind: "message", from, to, count: row.n, last_at: iso(row.last) }] : [];
3340
+ return from && to && from !== to ? [{ kind: "message", from, to, count: row.n, last_at: iso2(row.last) }] : [];
2794
3341
  });
2795
3342
  edges.push(...traffic.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to)));
2796
3343
  return { minutes: window, machines: this.machineList(), nodes, edges };
@@ -2802,10 +3349,10 @@ var RelayHub = class _RelayHub {
2802
3349
  kind: row.kind,
2803
3350
  online: sockets.some((s) => this.boundMachine(s.conn) === row.name),
2804
3351
  bridge: sockets.some((s) => s.conn.role === "bridge" && s.conn.machine === row.name),
2805
- created_at: iso(row.created_at),
2806
- last_seen: iso(row.last_seen)
3352
+ created_at: iso2(row.created_at),
3353
+ last_seen: iso2(row.last_seen)
2807
3354
  }));
2808
- const revoked = this.deps.sql.all(`SELECT name, at FROM machine_tombstones WHERE name NOT IN (SELECT name FROM machines) ORDER BY name`).map((row) => ({ name: row.name, kind: "revoked", online: false, bridge: false, created_at: null, last_seen: iso(row.at) }));
3355
+ const revoked = this.deps.sql.all(`SELECT name, at FROM machine_tombstones WHERE name NOT IN (SELECT name FROM machines) ORDER BY name`).map((row) => ({ name: row.name, kind: "revoked", online: false, bridge: false, created_at: null, last_seen: iso2(row.at) }));
2809
3356
  return [...bound, ...revoked];
2810
3357
  }
2811
3358
  /**
@@ -2814,15 +3361,15 @@ var RelayHub = class _RelayHub {
2814
3361
  */
2815
3362
  releaseMachine(caller, name) {
2816
3363
  const core = this.relay;
2817
- if (typeof name !== "string" || !MACHINE_NAME.test(name)) throw new RelayError2("invalid_params", "name is a machine name");
2818
- const bound = one2(this.deps.sql, `SELECT token_id, actor_id FROM machines WHERE name = ?`, name);
2819
- const tomb = bound ? void 0 : one2(this.deps.sql, `SELECT actor_id FROM machine_tombstones WHERE name = ?`, name);
2820
- if (!bound && !tomb) throw new RelayError2("not_found", `no machine ${name}`);
3364
+ if (typeof name !== "string" || !MACHINE_NAME.test(name)) throw new RelayError3("invalid_params", "name is a machine name");
3365
+ const bound = one3(this.deps.sql, `SELECT token_id, actor_id FROM machines WHERE name = ?`, name);
3366
+ const tomb = bound ? void 0 : one3(this.deps.sql, `SELECT actor_id FROM machine_tombstones WHERE name = ?`, name);
3367
+ if (!bound && !tomb) throw new RelayError3("not_found", `no machine ${name}`);
2821
3368
  const grant = caller.conn.grant;
2822
3369
  const member = !!bound && bound.actor_id !== null && grant.actorId === bound.actor_id;
2823
3370
  if (!(bound?.token_id === grant.tokenId || fullOf(grant, (bound ?? tomb).actor_id) || accountAdmin(grant) || member && !this.liveMachines().has(name))) {
2824
3371
  core.record("refuse", null, name, { code: "unauthorized", machine: name });
2825
- throw new RelayError2(
3372
+ throw new RelayError3(
2826
3373
  "unauthorized",
2827
3374
  `only the token machine ${name} is bound to, a full login of the member who bound it, or an org owner or admin can release it${member ? "; any credential of that member can once it is offline" : ""}`
2828
3375
  );
@@ -2852,14 +3399,14 @@ var RelayHub = class _RelayHub {
2852
3399
  );
2853
3400
  for (const { name } of idle) {
2854
3401
  if (live.has(name)) continue;
2855
- const queued = one2(
3402
+ const queued = one3(
2856
3403
  this.deps.sql,
2857
3404
  `SELECT 1 FROM deliveries d JOIN endpoints e ON e.id = d.endpoint WHERE substr(e.remote, 1, ?) = ? AND d.acked_at IS NULL AND d.expires_at > ? LIMIT 1`,
2858
3405
  name.length + 1,
2859
3406
  `${name}/`,
2860
3407
  now
2861
3408
  );
2862
- const asking = one2(
3409
+ const asking = one3(
2863
3410
  this.deps.sql,
2864
3411
  `SELECT 1 FROM messages m JOIN endpoints e ON e.id = m.from_ep WHERE substr(e.remote, 1, ?) = ? AND m.kind = 'request' AND m.expires_at > ?
2865
3412
  AND NOT EXISTS (SELECT 1 FROM messages r WHERE r.reply_to = m.id AND r.kind = 'reply') LIMIT 1`,
@@ -2885,10 +3432,10 @@ var RelayHub = class _RelayHub {
2885
3432
  `DELETE FROM machine_tombstones WHERE at <= ? AND NOT EXISTS (SELECT 1 FROM deliveries d JOIN endpoints e ON e.id = d.endpoint
2886
3433
  WHERE substr(e.remote, 1, length(machine_tombstones.name) + 1) = machine_tombstones.name || '/' AND d.acked_at IS NULL AND d.expires_at > ?)
2887
3434
  RETURNING name`,
2888
- now - (LIMITS2.ttlMaxS + LIMITS2.purgeAfterS) * 1e3,
3435
+ now - (LIMITS3.ttlMaxS + LIMITS3.purgeAfterS) * 1e3,
2889
3436
  now
2890
3437
  );
2891
- const unbound = names.map((row) => row.name).filter((name) => !one2(this.deps.sql, `SELECT 1 FROM machines WHERE name = ?`, name));
3438
+ const unbound = names.map((row) => row.name).filter((name) => !one3(this.deps.sql, `SELECT 1 FROM machines WHERE name = ?`, name));
2892
3439
  for (const name of unbound) this.relay.record("machine_forget", null, name, { machine: name, endpoints: this.relay.dropMachine(name, { queue: true }) });
2893
3440
  return unbound;
2894
3441
  });
@@ -2906,13 +3453,13 @@ var RelayHub = class _RelayHub {
2906
3453
  const at = this.deniedGrants().get(DENIED + socket.conn.grant.tokenId);
2907
3454
  if (at === void 0 || at <= this.now() - this.limits.deniedS * 1e3) return;
2908
3455
  this.kick(socket, "the token was revoked");
2909
- throw new RelayError2("unauthorized", "the token was revoked");
3456
+ throw new RelayError3("unauthorized", "the token was revoked");
2910
3457
  }
2911
3458
  tx(fn) {
2912
3459
  return this.deps.transaction ? this.deps.transaction(fn) : fn();
2913
3460
  }
2914
3461
  state(key) {
2915
- return one2(this.deps.sql, `SELECT value FROM hub_state WHERE key = ?`, key)?.value ?? null;
3462
+ return one3(this.deps.sql, `SELECT value FROM hub_state WHERE key = ?`, key)?.value ?? null;
2916
3463
  }
2917
3464
  setState(key, value) {
2918
3465
  this.deps.sql.all(`INSERT OR REPLACE INTO hub_state (key, value) VALUES (?, ?)`, key, value);
@@ -2927,14 +3474,14 @@ var RelayHub = class _RelayHub {
2927
3474
  if (hold.side === "ingress") return this.xreceive(hold.from_account, hold.message, true);
2928
3475
  const msg = hold.message;
2929
3476
  const left = Math.ceil((msg.expires_at - this.now()) / 1e3);
2930
- if (left < 1) throw new RelayError2("invalid_params", "the held message expired");
3477
+ if (left < 1) throw new RelayError3("invalid_params", "the held message expired");
2931
3478
  const send = {
2932
3479
  to: `${hold.peer}/${msg.to.endsWith("/") ? `${msg.to}me` : msg.to}`,
2933
3480
  body: msg.body,
2934
3481
  kind: msg.kind,
2935
3482
  thread: msg.thread,
2936
3483
  ...msg.reply_to ? { reply_to: msg.reply_to } : {},
2937
- ttl_s: Math.min(left, LIMITS2.ttlMaxS)
3484
+ ttl_s: Math.min(left, LIMITS3.ttlMaxS)
2938
3485
  };
2939
3486
  return this.xsend(hold.sender, send, { tty: msg.from.human, hop: msg.hop, judged: true, clientId: msg.client_id, id: hold.hold_id });
2940
3487
  }
@@ -2948,6 +3495,8 @@ export {
2948
3495
  GRAPH_MINUTES,
2949
3496
  GRAPH_MINUTES_MAX,
2950
3497
  HUB_LIMITS,
3498
+ JUDGE_LIMITS,
3499
+ JudgeGate,
2951
3500
  MACHINE_NAME,
2952
3501
  RelayHub
2953
3502
  };