@saadnvd1/agent-os 0.3.697 → 0.3.700

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.
Files changed (88) hide show
  1. package/app/api/bus/send/route.ts +11 -1
  2. package/app/api/bus/wait/route.ts +22 -0
  3. package/app/api/peer/hello/reveal/route.ts +6 -0
  4. package/app/api/peer/hello/route.ts +9 -0
  5. package/app/api/peer/hello/status/route.ts +6 -0
  6. package/app/api/peer/ping/route.ts +10 -0
  7. package/app/api/peers/account/route.ts +9 -0
  8. package/app/api/peers/gate.test.ts +111 -0
  9. package/app/api/peers/link/[id]/confirm/route.ts +27 -0
  10. package/app/api/peers/link/[id]/route.ts +20 -0
  11. package/app/api/peers/link/route.ts +23 -0
  12. package/app/api/peers/pending/[id]/accept/route.ts +31 -0
  13. package/app/api/peers/pending/[id]/decline/route.ts +13 -0
  14. package/app/api/peers/pending/route.ts +7 -0
  15. package/app/api/sessions/route.ts +18 -1
  16. package/app/page.tsx +2 -0
  17. package/components/Hosts/AccountMachines.tsx +180 -0
  18. package/components/Hosts/HostRow.tsx +44 -35
  19. package/components/Hosts/HostsDialog.tsx +49 -37
  20. package/components/Hosts/IncomingLinkSheet.tsx +228 -0
  21. package/components/Hosts/LinkCodeSheet.tsx +186 -0
  22. package/components/Hosts/sheet.ts +3 -0
  23. package/components/SessionList/RowSubtitle.tsx +11 -1
  24. package/components/Welcome/ConnectBlock.tsx +1 -1
  25. package/data/peers/index.ts +2 -0
  26. package/data/peers/keys.ts +6 -0
  27. package/data/peers/queries.ts +141 -0
  28. package/lib/__fixtures__/connect-transport.ts +93 -0
  29. package/lib/__fixtures__/peer-machine.ts +45 -0
  30. package/lib/__fixtures__/peer-rpc.ts +224 -0
  31. package/lib/agents/brief.ts +3 -1
  32. package/lib/bus/format.ts +25 -4
  33. package/lib/bus/index.ts +113 -106
  34. package/lib/bus/resolve.ts +16 -2
  35. package/lib/bus/targets.ts +122 -0
  36. package/lib/bus/waits/kick.ts +12 -0
  37. package/lib/bus/waits/lines.ts +69 -0
  38. package/lib/bus/waits/register.ts +83 -0
  39. package/lib/bus/waits/store.ts +196 -0
  40. package/lib/bus/waits/tick.ts +192 -0
  41. package/lib/connect/control-api.ts +108 -2
  42. package/lib/connect/heartbeat.ts +11 -3
  43. package/lib/connect/serve.ts +2 -0
  44. package/lib/db/migrations/20261020T1258_peer_links.ts +49 -0
  45. package/lib/db/migrations/20261020T1345_peer_nonces.ts +18 -0
  46. package/lib/db/migrations/20261020T1600_bus_waits.ts +35 -0
  47. package/lib/db/types.ts +6 -0
  48. package/lib/done/archive.ts +2 -0
  49. package/lib/hosts/index.ts +28 -7
  50. package/lib/hosts/peer-socket.ts +3 -1
  51. package/lib/hosts/read-capped-bytes.ts +23 -0
  52. package/lib/hosts/remote-api.ts +112 -41
  53. package/lib/orchestrator/describe.ts +13 -0
  54. package/lib/orchestrator/facts.ts +17 -0
  55. package/lib/peer/account.ts +198 -0
  56. package/lib/peer/allowlist.ts +136 -0
  57. package/lib/peer/audit.ts +144 -0
  58. package/lib/peer/cli.ts +24 -0
  59. package/lib/peer/client.ts +188 -0
  60. package/lib/peer/code.ts +3 -0
  61. package/lib/peer/envelope.ts +424 -0
  62. package/lib/peer/hello-route.ts +32 -0
  63. package/lib/peer/hello.ts +346 -0
  64. package/lib/peer/internal.ts +22 -0
  65. package/lib/peer/keys.ts +122 -0
  66. package/lib/peer/link-cli.ts +233 -0
  67. package/lib/peer/links.ts +96 -0
  68. package/lib/peer/machines.ts +72 -0
  69. package/lib/peer/nonce-store.ts +23 -0
  70. package/lib/peer/notify.ts +32 -0
  71. package/lib/peer/pairing-out.ts +382 -0
  72. package/lib/peer/pairing.ts +403 -0
  73. package/lib/peer/pin.ts +55 -0
  74. package/lib/peer/replay.ts +81 -0
  75. package/lib/peer/rpc-intake.ts +258 -0
  76. package/lib/peer/rpc-server.ts +369 -0
  77. package/lib/schedules/when.ts +1 -1
  78. package/lib/security/auth.ts +51 -6
  79. package/lib/security/gate.ts +38 -12
  80. package/lib/security/route-guard.ts +3 -1
  81. package/lib/session-artifacts/peer.ts +12 -1
  82. package/lib/sessions/end.ts +3 -0
  83. package/lib/tasks/contract/authorize.ts +4 -0
  84. package/lib/worktree-sweep/testTemp.ts +2 -0
  85. package/package.json +1 -1
  86. package/scripts/agent-os +2 -0
  87. package/scripts/lib/commands.sh +2 -0
  88. package/server.ts +21 -0
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The only routes a linked machine may call here, by method and pattern.
3
+ * These are the calls the peer code makes today and nothing else. A path is
4
+ * taken only in its plain form: no encoding, no dot segments, no doubled or
5
+ * back slashes, so what is matched is exactly what the route sees. An id
6
+ * segment is [A-Za-z0-9_.-]{1,128}, and each route names its query keys.
7
+ */
8
+
9
+ type Method = "GET" | "POST" | "DELETE";
10
+
11
+ interface PeerRoute {
12
+ method: Method;
13
+ pattern: string;
14
+ query?: readonly string[];
15
+ }
16
+
17
+ const each = (
18
+ method: Method,
19
+ base: string,
20
+ tails: string[],
21
+ query?: string[]
22
+ ): PeerRoute[] =>
23
+ tails.map((t) => ({
24
+ method,
25
+ pattern: `${base}/${t}`,
26
+ ...(query && { query }),
27
+ }));
28
+
29
+ export const PEER_ROUTES: readonly PeerRoute[] = [
30
+ { method: "GET", pattern: "/api/tasks" },
31
+ { method: "POST", pattern: "/api/tasks" },
32
+ { method: "POST", pattern: "/api/tasks/import" },
33
+ { method: "GET", pattern: "/api/tasks/arrived", query: ["from"] },
34
+ { method: "GET", pattern: "/api/tasks/held-replies" },
35
+ { method: "POST", pattern: "/api/tasks/held-replies" },
36
+ ...each("POST", "/api/tasks/:id", [
37
+ "export",
38
+ "approve-plan",
39
+ "hold",
40
+ "message",
41
+ "stop",
42
+ "done",
43
+ "drop",
44
+ "resume",
45
+ "moved",
46
+ "merge",
47
+ ]),
48
+ ...each("GET", "/api/tasks/:id", ["plan", "chat-facts"]),
49
+ { method: "GET", pattern: "/api/tasks/:id/chat-tail", query: ["lines"] },
50
+
51
+ { method: "GET", pattern: "/api/sessions" },
52
+ { method: "GET", pattern: "/api/sessions/status" },
53
+ { method: "POST", pattern: "/api/sessions" },
54
+ { method: "DELETE", pattern: "/api/sessions/:id" },
55
+ ...each("POST", "/api/sessions/:id", ["done", "unarchive", "resurrect"]),
56
+ ...each("GET", "/api/sessions/:id", ["preview", "artifacts", "setup"]),
57
+
58
+ { method: "GET", pattern: "/api/projects" },
59
+ { method: "POST", pattern: "/api/projects/availability" },
60
+ { method: "GET", pattern: "/api/tmux/discover" },
61
+ { method: "GET", pattern: "/api/load" },
62
+
63
+ { method: "GET", pattern: "/api/worktree-sweep" },
64
+ { method: "POST", pattern: "/api/worktree-sweep" },
65
+ { method: "POST", pattern: "/api/worktree-sweep/run" },
66
+
67
+ { method: "POST", pattern: "/api/peer/unlink" },
68
+ { method: "GET", pattern: "/api/peer/ping" },
69
+
70
+ // The work registry's exchange (not built yet: 404 until it is).
71
+ { method: "POST", pattern: "/api/claims/exchange" },
72
+ ];
73
+
74
+ const PLAIN_PATH = /^\/api(\/[A-Za-z0-9_.-]{1,128}){1,6}$/;
75
+ const ID = /^[A-Za-z0-9_.-]{1,128}$/;
76
+ const MAX_QUERY = 4096;
77
+
78
+ export type RouteMatch =
79
+ | { ok: true; route: string; path: string; query: string }
80
+ | { ok: false; reason: "bad_path" | "bad_query" | "not_allowed" };
81
+
82
+ function segmentsMatch(pattern: string[], path: string[]): boolean {
83
+ return (
84
+ pattern.length === path.length &&
85
+ pattern.every((p, i) => (p === ":id" ? ID.test(path[i]) : p === path[i]))
86
+ );
87
+ }
88
+
89
+ function cleanQuery(
90
+ raw: string,
91
+ allowed: readonly string[] = []
92
+ ): string | null {
93
+ if (!raw) return "";
94
+ if (raw.length > MAX_QUERY || /[#\u0000-\u001f\u007f]/.test(raw)) return null;
95
+ const params = new URLSearchParams(raw);
96
+ const keys = [...params.keys()];
97
+ if (new Set(keys).size !== keys.length) return null;
98
+ for (const [k, v] of params) {
99
+ if (!allowed.includes(k) || v.length > 1024) return null;
100
+ if (/[\u0000-\u001f\u007f]/.test(v)) return null;
101
+ }
102
+ return params.toString();
103
+ }
104
+
105
+ /** The route a peer's call matches, with its path and query as dispatched. */
106
+ export function matchPeerRoute(
107
+ method: string,
108
+ path: string,
109
+ query = ""
110
+ ): RouteMatch {
111
+ if (typeof path !== "string" || !PLAIN_PATH.test(path))
112
+ return { ok: false, reason: "bad_path" };
113
+ const parts = path.split("/").slice(1);
114
+ if (parts.some((p) => p === "." || p === ".."))
115
+ return { ok: false, reason: "bad_path" };
116
+ const route = PEER_ROUTES.find(
117
+ (r) =>
118
+ r.method === method && segmentsMatch(r.pattern.split("/").slice(1), parts)
119
+ );
120
+ if (!route) return { ok: false, reason: "not_allowed" };
121
+ const clean = cleanQuery(query, route.query);
122
+ if (clean === null) return { ok: false, reason: "bad_query" };
123
+ return { ok: true, route: route.pattern, path, query: clean };
124
+ }
125
+
126
+ /** For the gate: a request target (path and query) a peer may reach. */
127
+ export function peerTargetAllowed(
128
+ method: string | undefined,
129
+ url: string | undefined
130
+ ) {
131
+ const target = url ?? "";
132
+ const q = target.indexOf("?");
133
+ const path = q < 0 ? target : target.slice(0, q);
134
+ const query = q < 0 ? "" : target.slice(q + 1);
135
+ return matchPeerRoute(method ?? "", path, query).ok;
136
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Peer calls, in and out. Successful reads (the mirror polls every few
3
+ * seconds) fold into one row per route per hour. Failures are capped per
4
+ * minute so a flood can't fill the disk, in two budgets: envelopes refused
5
+ * here before their sender was proven (anyone can send those), and
6
+ * everything else (this machine's own failed calls, a linked machine's
7
+ * denied routes), so a forger's flood never hides the second kind. Rows older than 30 days go, checked at most hourly.
8
+ * Bookkeeping: a failure here never fails the call.
9
+ */
10
+
11
+ import { getDb } from "../db";
12
+
13
+ export type Outcome =
14
+ | "ok"
15
+ | "denied_route"
16
+ | "bad_envelope"
17
+ | "too_large"
18
+ | "too_slow"
19
+ | "busy"
20
+ | "bad_sig"
21
+ | "stale"
22
+ | "replay"
23
+ | "unknown_peer"
24
+ | "suspended";
25
+
26
+ export interface AuditEntry {
27
+ hostId: string | null;
28
+ machineId?: string | null;
29
+ direction: "in" | "out";
30
+ method: string;
31
+ /** The route's template (/api/tasks/:id/merge), never the raw path. */
32
+ route: string;
33
+ status: number;
34
+ ms: number;
35
+ bytes: number;
36
+ outcome: Outcome;
37
+ }
38
+
39
+ const HOUR = 3_600_000;
40
+ const RETAIN_MS = 30 * 24 * HOUR;
41
+ const FAILURES_PER_MINUTE = 30;
42
+
43
+ const state = { prunedAt: 0, minute: 0, forged: 0, other: 0 };
44
+
45
+ /** For tests: forget the prune time and the failure budget. */
46
+ export const resetAuditLimits = () => {
47
+ state.prunedAt = 0;
48
+ state.minute = 0;
49
+ state.forged = 0;
50
+ state.other = 0;
51
+ };
52
+
53
+ // Refusals of an inbound envelope that anyone could have sent: a stranger's,
54
+ // a forgery, a malformed or oversized one, or a captured one sent again.
55
+ const UNPROVEN: ReadonlySet<Outcome> = new Set([
56
+ "bad_envelope",
57
+ "too_large",
58
+ "too_slow",
59
+ "busy",
60
+ "bad_sig",
61
+ "unknown_peer",
62
+ "suspended",
63
+ "stale",
64
+ "replay",
65
+ ]);
66
+
67
+ function underFailureCap(e: AuditEntry, now: number): boolean {
68
+ const minute = Math.floor(now / 60_000);
69
+ if (minute !== state.minute) {
70
+ state.minute = minute;
71
+ state.forged = 0;
72
+ state.other = 0;
73
+ }
74
+ const key =
75
+ e.direction === "in" && UNPROVEN.has(e.outcome) ? "forged" : "other";
76
+ return ++state[key] <= FAILURES_PER_MINUTE;
77
+ }
78
+
79
+ export function recordPeerCall(e: AuditEntry, now = Date.now()): void {
80
+ try {
81
+ if (e.outcome !== "ok" && !underFailureCap(e, now)) return;
82
+ const db = getDb();
83
+ if (now - state.prunedAt > HOUR) {
84
+ state.prunedAt = now;
85
+ db.prepare(`DELETE FROM peer_audit WHERE at < ?`).run(now - RETAIN_MS);
86
+ }
87
+ const machineId = e.machineId?.slice(0, 64) ?? null;
88
+ const fold = e.method === "GET" && e.outcome === "ok" && e.status < 400;
89
+ const bucket = fold ? String(Math.floor(now / HOUR)) : null;
90
+ if (bucket) {
91
+ const changed = db
92
+ .prepare(
93
+ `UPDATE peer_audit SET count = count + 1, at = ?, status = ?, ms = ?,
94
+ bytes = bytes + ?
95
+ WHERE host_id IS ? AND direction = ? AND route = ? AND bucket = ?`
96
+ )
97
+ .run(
98
+ now,
99
+ e.status,
100
+ e.ms,
101
+ e.bytes,
102
+ e.hostId,
103
+ e.direction,
104
+ e.route,
105
+ bucket
106
+ );
107
+ if (changed.changes > 0) return;
108
+ }
109
+ db.prepare(
110
+ `INSERT INTO peer_audit (at, host_id, machine_id, direction, method, route,
111
+ status, ms, bytes, outcome, bucket)
112
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
113
+ ).run(
114
+ now,
115
+ e.hostId,
116
+ machineId,
117
+ e.direction,
118
+ e.method.slice(0, 10),
119
+ e.route.slice(0, 200),
120
+ e.status,
121
+ Math.round(e.ms),
122
+ e.bytes,
123
+ e.outcome,
124
+ bucket
125
+ );
126
+ } catch (err) {
127
+ console.error("peer audit:", err);
128
+ }
129
+ }
130
+
131
+ export interface AuditRow extends AuditEntry {
132
+ at: number;
133
+ count: number;
134
+ }
135
+
136
+ export function peerAudit(hostId: string, limit = 200): AuditRow[] {
137
+ return getDb()
138
+ .prepare(
139
+ `SELECT at, host_id AS hostId, machine_id AS machineId, direction, method,
140
+ route, status, ms, bytes, outcome, count
141
+ FROM peer_audit WHERE host_id = ? ORDER BY at DESC, id DESC LIMIT ?`
142
+ )
143
+ .all(hostId, limit) as AuditRow[];
144
+ }
@@ -0,0 +1,24 @@
1
+ /** Entry for agent-os link / unlink (lib/peer/link-cli.ts). Argv: port, then the command. */
2
+
3
+ import { createInterface } from "readline/promises";
4
+ import { runLinkCli } from "./link-cli";
5
+
6
+ const [port, ...args] = process.argv.slice(2);
7
+ runLinkCli(args, {
8
+ base: `http://127.0.0.1:${Number(port) || 3011}`,
9
+ fetch,
10
+ out: (line) => console.log(line),
11
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
12
+ ask: async (prompt) => {
13
+ if (!process.stdin.isTTY) return null;
14
+ const rl = createInterface({
15
+ input: process.stdin,
16
+ output: process.stdout,
17
+ });
18
+ try {
19
+ return await rl.question(prompt);
20
+ } finally {
21
+ rl.close();
22
+ }
23
+ },
24
+ }).then((code) => process.exit(code));
@@ -0,0 +1,188 @@
1
+ /**
2
+ * hostApi's transport for a machine linked through Connect: each call is a
3
+ * fresh envelope (envelope.ts) to POST https://<its m- name>/api/peer/rpc,
4
+ * and the answer only counts if it is signed by the pinned key and bound to
5
+ * this request's nonce. No token is ever sent or stored.
6
+ */
7
+
8
+ import { matchPeerRoute } from "./allowlist";
9
+ import { recordPeerCall, type AuditEntry, type Outcome } from "./audit";
10
+ import {
11
+ MAX_REQUEST_BODY,
12
+ MAX_RESPONSE_ENVELOPE,
13
+ RPC_PATH,
14
+ openResponse,
15
+ sealRequest,
16
+ } from "./envelope";
17
+ import { boxPublic, currentIdentity, signPublic, type Identity } from "./keys";
18
+ import { notePinResult } from "./links";
19
+ import type { HostLink } from "../hosts/remote-api";
20
+ import { readCappedBytes } from "../hosts/read-capped-bytes";
21
+
22
+ /**
23
+ * A failed peer call. `refused`: the other machine (or this one, before
24
+ * sending) said no, so nothing happened there.
25
+ */
26
+ export class PeerCallError extends Error {
27
+ constructor(
28
+ message: string,
29
+ readonly refused: boolean,
30
+ readonly status?: number
31
+ ) {
32
+ super(message);
33
+ }
34
+ }
35
+
36
+ const DOMAIN = /^[a-z0-9-]+(\.[a-z0-9-]+)+$/;
37
+
38
+ /**
39
+ * Only a Connect machine's own name, so a lying account list can't aim this
40
+ * machine at an internal address. AGENTOS_CONNECT_PEER_DOMAIN is for a
41
+ * staging service.
42
+ */
43
+ export function peerHostAllowed(hostname: string, env = process.env): boolean {
44
+ const configured = env.AGENTOS_CONNECT_PEER_DOMAIN ?? "";
45
+ const domain = DOMAIN.test(configured) ? configured : "runagentos.com";
46
+ const escaped = domain.replace(/\./g, "\\.");
47
+ return new RegExp(`^m-[0-9a-f]{16}\\.${escaped}$`).test(hostname);
48
+ }
49
+
50
+ // The fixed refusals rpc-server.ts sends before anything is decrypted.
51
+ const REFUSALS: Record<string, Outcome> = {
52
+ "bad signature": "bad_sig",
53
+ "bad envelope": "bad_sig",
54
+ stale: "stale",
55
+ replay: "replay",
56
+ busy: "replay",
57
+ "too large": "bad_sig",
58
+ };
59
+
60
+ const HINTS: Record<string, string> = {
61
+ "bad signature":
62
+ "it doesn't know this machine, the link is suspended there, or the keys don't match",
63
+ stale: "the clocks on the two machines differ by more than 2 minutes",
64
+ };
65
+
66
+ export interface ClientDeps {
67
+ identity: () => Identity | null;
68
+ audit: (e: AuditEntry) => void;
69
+ noteResult: (hostId: string, error: string | null) => void;
70
+ }
71
+
72
+ const realDeps: ClientDeps = {
73
+ identity: () => currentIdentity(),
74
+ audit: recordPeerCall,
75
+ noteResult: notePinResult,
76
+ };
77
+
78
+ export async function peerCall(
79
+ link: HostLink,
80
+ target: string,
81
+ opts: { method?: string; body?: unknown; timeout?: number },
82
+ deps: ClientDeps = realDeps
83
+ ): Promise<{ status: number; text: string }> {
84
+ const peer = link.connect;
85
+ const refuse = (msg: string): never => {
86
+ throw new PeerCallError(msg, true);
87
+ };
88
+ if (!peer) return refuse("isn't linked through Connect");
89
+ if (peer.state !== "active")
90
+ return refuse(
91
+ `the link is suspended (${peer.suspendedReason ?? "no reason given"})`
92
+ );
93
+ if (!peerHostAllowed(peer.hostname))
94
+ return refuse("its address isn't a Connect machine's");
95
+ const method = (
96
+ opts.method ?? (opts.body === undefined ? "GET" : "POST")
97
+ ).toUpperCase();
98
+ const q = target.indexOf("?");
99
+ const match = matchPeerRoute(
100
+ method,
101
+ q < 0 ? target : target.slice(0, q),
102
+ q < 0 ? "" : target.slice(q + 1)
103
+ );
104
+ if (!match.ok) return refuse("that call isn't open to linked machines");
105
+ const identity = deps.identity();
106
+ if (!identity)
107
+ return refuse("this machine isn't connected to AgentOS Connect");
108
+ const body =
109
+ opts.body === undefined
110
+ ? Buffer.alloc(0)
111
+ : Buffer.from(JSON.stringify(opts.body));
112
+ if (body.length > MAX_REQUEST_BODY) return refuse("the request is too large");
113
+
114
+ const started = Date.now();
115
+ const audit = (outcome: Outcome, status: number, bytes: number) =>
116
+ deps.audit({
117
+ hostId: link.hostId,
118
+ machineId: peer.machineId,
119
+ direction: "out",
120
+ method,
121
+ route: match.route,
122
+ status,
123
+ ms: Date.now() - started,
124
+ bytes,
125
+ outcome,
126
+ });
127
+ const sealed = sealRequest({
128
+ from: identity.machineId,
129
+ to: peer.machineId,
130
+ signKey: identity.signKey,
131
+ peerBox: boxPublic(peer.boxPub),
132
+ request: {
133
+ method,
134
+ path: match.path,
135
+ query: match.query,
136
+ contentType: body.length ? "application/json" : "",
137
+ body,
138
+ },
139
+ });
140
+ let res: Response;
141
+ let raw: Buffer;
142
+ try {
143
+ res = await fetch(`https://${peer.hostname}${RPC_PATH}`, {
144
+ method: "POST",
145
+ headers: { "Content-Type": "application/octet-stream" },
146
+ body: new Uint8Array(sealed.envelope),
147
+ signal: AbortSignal.timeout(opts.timeout ?? 15000),
148
+ redirect: "error",
149
+ });
150
+ raw = await readCappedBytes(res, MAX_RESPONSE_ENVELOPE);
151
+ } catch (err) {
152
+ const message = err instanceof Error ? err.message : String(err);
153
+ deps.noteResult(link.hostId, message);
154
+ throw new PeerCallError(message, false);
155
+ }
156
+ if (res.status !== 200) {
157
+ const text = raw.subarray(0, 64).toString("utf8").trim();
158
+ const outcome = REFUSALS[text];
159
+ if (outcome && res.status >= 400 && res.status < 500) {
160
+ audit(outcome, res.status, sealed.envelope.length);
161
+ const message = HINTS[text] ? `${text}: ${HINTS[text]}` : text;
162
+ deps.noteResult(link.hostId, message);
163
+ // A refusal is unsigned: anyone on the path could forge one. A read
164
+ // changes nothing either way, but a write may have happened.
165
+ throw new PeerCallError(message, method === "GET", res.status);
166
+ }
167
+ deps.noteResult(link.hostId, `HTTP ${res.status}`);
168
+ throw new PeerCallError(`HTTP ${res.status}`, false, res.status);
169
+ }
170
+ let answer;
171
+ try {
172
+ answer = openResponse({
173
+ envelope: raw,
174
+ nonce: sealed.nonce,
175
+ resKey: sealed.resKey,
176
+ peerSign: signPublic(peer.signPub),
177
+ });
178
+ } catch (err) {
179
+ // It may have run there; the answer just can't be trusted.
180
+ const message = err instanceof Error ? err.message : String(err);
181
+ audit("bad_sig", 200, sealed.envelope.length + raw.length);
182
+ deps.noteResult(link.hostId, message);
183
+ throw new PeerCallError(message, false);
184
+ }
185
+ audit("ok", answer.status, body.length + answer.body.length);
186
+ deps.noteResult(link.hostId, null);
187
+ return { status: answer.status, text: answer.body.toString("utf8") };
188
+ }
@@ -0,0 +1,3 @@
1
+ /** "482913" as people read it. Pure, for the browser too. */
2
+ export const showCode = (code: string) =>
3
+ `${code.slice(0, 3)} ${code.slice(3)}`;