ccdeck 3.29.10 → 3.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +1 -1
  2. package/bin/agent-dag.js +67 -118
  3. package/bin/cli/help.js +67 -0
  4. package/bin/cli/login-item.js +182 -0
  5. package/bin/cli/one-shot.js +254 -0
  6. package/bin/cli/package.js +39 -0
  7. package/bin/cli/pulse.js +98 -0
  8. package/bin/cli/restart.js +179 -0
  9. package/bin/cli/screen.js +166 -0
  10. package/bin/cli/second-start.js +120 -0
  11. package/bin/cli/startup.js +459 -0
  12. package/bin/cli/uninstall.js +169 -0
  13. package/bin/deck.js +112 -1559
  14. package/dist/web/assets/BrowserWatchModal-DP3YMtEi.js +1 -0
  15. package/dist/web/assets/UsageHistoryModal-CcdPVXAw.js +1 -0
  16. package/dist/web/assets/index-3vS6A0vt.css +1 -0
  17. package/dist/web/assets/index-BVTWHDta.js +298 -0
  18. package/dist/web/index.html +2 -2
  19. package/hook/hook.js +15 -14
  20. package/package.json +1 -1
  21. package/release-notes.json +28 -0
  22. package/src/server/account-projects.mjs +25 -11
  23. package/src/server/account-routes.mjs +254 -0
  24. package/src/server/activity.mjs +3 -3
  25. package/src/server/append-budget.mjs +185 -0
  26. package/src/server/append-failures.mjs +147 -0
  27. package/src/server/args.mjs +37 -5
  28. package/src/server/atomic-write.mjs +300 -0
  29. package/src/server/block-notify.mjs +0 -5
  30. package/src/server/boot-lock.mjs +1 -1
  31. package/src/server/brand.mjs +7 -6
  32. package/src/server/browser-react.mjs +10 -2
  33. package/src/server/browser-watch-decks.mjs +146 -0
  34. package/src/server/browser-watch-feed.mjs +69 -0
  35. package/src/server/browser-watch-log.mjs +173 -0
  36. package/src/server/browser-watch-mtime.mjs +18 -0
  37. package/src/server/browser-watch-relay.mjs +112 -0
  38. package/src/server/browser-watch-routes.mjs +172 -0
  39. package/src/server/browser-watch-seen.mjs +127 -0
  40. package/src/server/browser-watch-store.mjs +14 -143
  41. package/src/server/browser-watch.mjs +278 -626
  42. package/src/server/canonical-path.mjs +150 -0
  43. package/src/server/ccusage-failure.mjs +64 -0
  44. package/src/server/ccusage-install.mjs +459 -0
  45. package/src/server/ccusage-runner.mjs +271 -0
  46. package/src/server/ccusage.mjs +21 -756
  47. package/src/server/claude-accounts.mjs +220 -464
  48. package/src/server/claude-collector.mjs +189 -0
  49. package/src/server/claude-dir.mjs +2 -2
  50. package/src/server/claude-identity.mjs +5 -4
  51. package/src/server/claude-reset-credits.mjs +130 -0
  52. package/src/server/claude-verdicts.mjs +174 -0
  53. package/src/server/clear-route.mjs +191 -0
  54. package/src/server/codex-auth.mjs +4 -4
  55. package/src/server/codex-dir.mjs +52 -8
  56. package/src/server/codex-enrichment.mjs +310 -0
  57. package/src/server/codex-quota.mjs +5 -3
  58. package/src/server/codex-translate.mjs +407 -0
  59. package/src/server/codex-usage.mjs +8 -5
  60. package/src/server/codex-watch.mjs +496 -0
  61. package/src/server/cswap-auto-loop.mjs +342 -0
  62. package/src/server/cswap-auto-readers.mjs +303 -0
  63. package/src/server/cswap-auto.mjs +51 -609
  64. package/src/server/cswap-install.mjs +69 -265
  65. package/src/server/cswap-layout.mjs +242 -0
  66. package/src/server/deck-home.mjs +1 -1
  67. package/src/server/deck-prefs.mjs +51 -184
  68. package/src/server/deck-scope.mjs +52 -0
  69. package/src/server/desktop-update-routes.mjs +75 -0
  70. package/src/server/detach.mjs +78 -0
  71. package/src/server/discovery.mjs +298 -0
  72. package/src/server/event-log.mjs +328 -0
  73. package/src/server/event-pipeline.mjs +235 -0
  74. package/src/server/event-ring.mjs +214 -0
  75. package/src/server/event-routes.mjs +352 -0
  76. package/src/server/event-sink.mjs +26 -0
  77. package/src/server/exec-children.mjs +128 -0
  78. package/src/server/exec-not-found.mjs +213 -0
  79. package/src/server/exec-output.mjs +23 -0
  80. package/src/server/exec-spec.mjs +341 -0
  81. package/src/server/exec.mjs +74 -661
  82. package/src/server/health-route.mjs +42 -0
  83. package/src/server/hook-challenge.mjs +37 -0
  84. package/src/server/http-io.mjs +104 -0
  85. package/src/server/index.mjs +172 -7620
  86. package/src/server/install-layout.mjs +506 -0
  87. package/src/server/installer.mjs +79 -608
  88. package/src/server/jsonl-chunks.mjs +139 -0
  89. package/src/server/lan-accounts.mjs +27 -0
  90. package/src/server/lan-beacon.mjs +457 -0
  91. package/src/server/lan-call.mjs +239 -0
  92. package/src/server/lan-copies.mjs +270 -0
  93. package/src/server/lan-deck.mjs +583 -0
  94. package/src/server/lan-dials.mjs +161 -0
  95. package/src/server/lan-engine.mjs +475 -1024
  96. package/src/server/lan-hearing.mjs +49 -0
  97. package/src/server/lan-inbound.mjs +100 -0
  98. package/src/server/lan-invite-offer.mjs +77 -0
  99. package/src/server/lan-invite.mjs +207 -0
  100. package/src/server/lan-lines.mjs +73 -0
  101. package/src/server/lan-manifest.mjs +74 -0
  102. package/src/server/lan-reach.mjs +82 -69
  103. package/src/server/lan-requests.mjs +249 -0
  104. package/src/server/lan-round-record.mjs +45 -0
  105. package/src/server/lan-round-timer.mjs +62 -0
  106. package/src/server/lan-routes.mjs +143 -0
  107. package/src/server/lan-serve.mjs +112 -0
  108. package/src/server/lan-socket.mjs +243 -925
  109. package/src/server/lan-sync.mjs +58 -1026
  110. package/src/server/lan-tailnet-poll.mjs +57 -0
  111. package/src/server/lan-turns.mjs +80 -0
  112. package/src/server/lan-wire.mjs +620 -0
  113. package/src/server/lifecycle.mjs +421 -0
  114. package/src/server/listen.mjs +110 -0
  115. package/src/server/live-decks.mjs +138 -0
  116. package/src/server/load-average.mjs +32 -0
  117. package/src/server/log-election.mjs +235 -0
  118. package/src/server/log-replay.mjs +374 -0
  119. package/src/server/log-writer.mjs +72 -557
  120. package/src/server/login-service.mjs +3 -2
  121. package/src/server/macmon.mjs +1 -1
  122. package/src/server/memory-files.mjs +163 -0
  123. package/src/server/memory-metrics.mjs +178 -0
  124. package/src/server/metrics-history.mjs +94 -0
  125. package/src/server/metrics-run.mjs +100 -0
  126. package/src/server/music-routes.mjs +134 -0
  127. package/src/server/network-metrics.mjs +5 -3
  128. package/src/server/network-sampler.mjs +256 -0
  129. package/src/server/npm-latest.mjs +385 -0
  130. package/src/server/npm-upgrade.mjs +352 -0
  131. package/src/server/npx.mjs +41 -7
  132. package/src/server/output-watch.mjs +5 -4
  133. package/src/server/pinned-build.mjs +108 -0
  134. package/src/server/prefs-path.mjs +14 -0
  135. package/src/server/prefs-refusal.mjs +77 -0
  136. package/src/server/prefs-routes.mjs +76 -0
  137. package/src/server/prefs-set-aside.mjs +86 -0
  138. package/src/server/prefs-state.mjs +51 -0
  139. package/src/server/process-command.mjs +104 -0
  140. package/src/server/process-list.mjs +547 -0
  141. package/src/server/pulse-line.mjs +132 -0
  142. package/src/server/purge-key.mjs +7 -7
  143. package/src/server/quota-cli.mjs +174 -0
  144. package/src/server/quota-oauth.mjs +190 -0
  145. package/src/server/quota-shape.mjs +229 -0
  146. package/src/server/quota-store-resets.mjs +206 -0
  147. package/src/server/quota.mjs +123 -509
  148. package/src/server/relay-guard.mjs +3 -3
  149. package/src/server/request-gates.mjs +441 -0
  150. package/src/server/restart-note.mjs +180 -0
  151. package/src/server/retire-sound-hook.mjs +3 -3
  152. package/src/server/ring-bounds.mjs +215 -0
  153. package/src/server/running-deck.mjs +1 -1
  154. package/src/server/running-version.mjs +24 -0
  155. package/src/server/self-update.mjs +39 -1371
  156. package/src/server/session-enrichment.mjs +622 -0
  157. package/src/server/session-read-gate.mjs +49 -0
  158. package/src/server/session-tracking.mjs +242 -0
  159. package/src/server/sse-clients.mjs +222 -0
  160. package/src/server/stale-discovery.mjs +30 -0
  161. package/src/server/static-serve.mjs +81 -0
  162. package/src/server/supervisor.mjs +73 -12
  163. package/src/server/system-metrics.mjs +169 -1935
  164. package/src/server/term.mjs +11 -249
  165. package/src/server/thermal-metrics.mjs +520 -0
  166. package/src/server/thermal-sampler.mjs +204 -0
  167. package/src/server/token-redact.mjs +123 -0
  168. package/src/server/transcript-gate.mjs +107 -0
  169. package/src/server/transcript-scan.mjs +563 -0
  170. package/src/server/usage-routes.mjs +93 -0
  171. package/src/server/uv-bootstrap.mjs +1 -1
  172. package/src/server/wordmark.mjs +127 -0
  173. package/dist/web/assets/BrowserWatchModal-CMn28FHb.js +0 -1
  174. package/dist/web/assets/UsageHistoryModal-C-sPhsKT.js +0 -1
  175. package/dist/web/assets/index-C1YFI9f4.js +0 -298
  176. package/dist/web/assets/index-Ck2RAla3.css +0 -1
@@ -1,6 +1,10 @@
1
- // The sockets. Every decision this makes lives in lan-sync.mjs; what is here is
2
- // the plumbing that decision layer refuses to own — a UDP socket that shouts,
3
- // a TCP listener that answers, and the deadlines around both.
1
+ // The sockets. Every decision this makes lives in lan-sync.mjs and
2
+ // lan-wire.mjs, or for an invite in lan-invite.mjs; what is here is the
3
+ // plumbing that decision layer refuses to own — a UDP socket that shouts, a TCP
4
+ // listener that answers, and the deadlines around both. The shouting half is
5
+ // lan-beacon.mjs and the calling half lan-call.mjs, and the line a dialler and
6
+ // this listener speak is lan-lines.mjs's; this file is the answering, and it
7
+ // re-exports the other two, so what imports them from here still does.
4
8
  //
5
9
  // TWO SOCKETS, AND NEITHER IS THE DECK'S HTTP SERVER. That server binds
6
10
  // 127.0.0.1 and stays there. It has a mutation guard that deliberately trusts a
@@ -8,144 +12,26 @@
8
12
  // correct on loopback, and total exposure the moment the same server answers
9
13
  // the network. So this feature never asks anybody to run `--host`: it opens its
10
14
  // own listener, that listener speaks one protocol and nothing else, and it
11
- // refuses every frame from anybody who has not proved they hold the group
12
- // passphrase.
15
+ // serves no frame to anybody who has not proved they hold the key of a deck
16
+ // somebody here accepted.
13
17
  //
14
18
  // DISCOVERY IS BROADCAST ON A FIXED PORT; THE SYNC LISTENER IS EPHEMERAL and
15
19
  // says its port in the beacon. One fixed port rather than two is one thing to
16
20
  // collide with, one firewall dialog, and one number in a support answer.
17
- //
18
- // A DECK HEARS ITSELF. Measured, not assumed: a broadcast to 255.255.255.255
19
- // comes back to every socket on the sending machine bound to that port, from
20
- // the machine's own LAN address rather than from loopback. That is what makes
21
- // two decks on one machine find each other — which is how this gets tested at
22
- // all — and it is why self-recognition is by fingerprint rather than by
23
- // address.
24
- import dgram from "node:dgram";
25
- import { networkInterfaces } from "node:os";
26
- import { looksLikeTunnel } from "./route-via.mjs";
27
-
28
- /** An IPv4 dotted quad as four numbers, or null for anything that is not one. */
29
- /**
30
- * What each refusal reason means, in a sentence the LAN panel can print.
31
- *
32
- * At module scope so it is one object rather than one per frame, and named so
33
- * the `Object.hasOwn` guard below reads as the rule it is rather than as
34
- * punctuation. The keys are the protocol's, not a user's.
35
- */
36
- const REFUSALS = Object.freeze({
37
- pending: "waiting for the other deck to accept this one",
38
- declined: "that deck said no",
39
- "invite only": "that deck pairs only by invite",
40
- // Sent back to a caller that said it was not asking, so the far end's answer
41
- // is about the caller's own setting — the same words its own round uses.
42
- "not asking": "this deck pairs only by invite",
43
- impostor: "that deck has this one pinned under a different key",
44
- "bad proof": "the other deck refused this one's proof",
45
- });
46
-
47
- function quad(text) {
48
- const parts = String(text ?? "").split(".");
49
- if (parts.length !== 4) return null;
50
- const out = parts.map(p => Number(p));
51
- return out.every(n => Number.isInteger(n) && n >= 0 && n <= 255) ? out : null;
52
- }
53
-
54
- /**
55
- * The address that reaches every host on one interface's own subnet.
56
- *
57
- * `address | ~netmask`, which is the definition. Null for a /32, because a
58
- * point-to-point link — a VPN tunnel, `utun` on macOS — has a directed
59
- * broadcast equal to its own address, and sending a beacon to ourselves down a
60
- * tunnel is a packet nobody wanted.
61
- */
62
- export function directedBroadcast(address, netmask) {
63
- const a = quad(address);
64
- const m = quad(netmask);
65
- if (!a || !m) return null;
66
- if (m.every(o => o === 255)) return null;
67
- return a.map((o, i) => o | (~m[i] & 255)).join(".");
68
- }
69
-
70
- /**
71
- * Where a beacon has to go to be heard on this machine's networks.
72
- *
73
- * The limited broadcast first, because it is the one that works where a router
74
- * or an access point filters the directed form, and because it is what every
75
- * deck before this version sent — a machine that was fine stays fine. Then one
76
- * per interface, which is what a multi-homed host actually needs: see announce.
77
- *
78
- * Loopback and IPv6 are skipped. A deck on `lo0` can only hear itself, and this
79
- * protocol is IPv4 broadcast by construction — there is no such thing as an
80
- * IPv6 broadcast address.
81
- */
82
- export function broadcastTargets(ifaces) {
83
- return broadcastPlan(ifaces).map(p => p.to);
84
- }
85
-
86
- /**
87
- * The same targets, each with the interface whose subnet it is — null for the
88
- * limited broadcast, which is nobody's. What lets a beacon be held back when
89
- * the machine would send it out by some OTHER interface: see leavesByTunnel.
90
- */
91
- export function broadcastPlan(ifaces) {
92
- const out = [{ to: "255.255.255.255", iface: null }];
93
- for (const [name, list] of Object.entries(ifaces ?? {})) {
94
- for (const ni of list ?? []) {
95
- if (!ni || ni.internal) continue;
96
- // Node 18 reports `family` as the string "IPv4"; older shapes used 4.
97
- if (ni.family !== "IPv4" && ni.family !== 4) continue;
98
- const to = directedBroadcast(ni.address, ni.netmask);
99
- if (to && !out.some(p => p.to === to)) out.push({ to, iface: name });
100
- }
101
- }
102
- return out;
103
- }
104
-
105
- /**
106
- * Would this broadcast leave the local network?
107
- *
108
- * A directed broadcast belongs to the interface whose subnet it is, so the
109
- * machine sending it out by any other one means something rerouted the local
110
- * network — a VPN, or a Tailscale exit node without local network access —
111
- * and the beacon would come out on the far end of the tunnel. The limited
112
- * broadcast belongs to none, so it is held back only when it would go into
113
- * something that is plainly a tunnel. Unknown answers nothing: send.
114
- */
115
- export function leavesByTunnel(target, via, isTunnel) {
116
- if (!via) return false;
117
- if (target.iface) return via !== target.iface;
118
- return isTunnel(via);
119
- }
120
21
  import net from "node:net";
121
- import { randomBytes } from "node:crypto";
122
22
  import {
123
- beaconPayload, beaconVerdict, challengeFor, cleanName, ephemeralPair, frameChannel, handshakeTranscript,
124
- hostId, mixesEphemeral, notePeer, proof, proofOk, inviteProof, inviteProofBack, readBeacon, readChallenge,
125
- readEphemeral, readPub, sealsFrames, sessionKey, trustedPeer,
126
- ANNOUNCE_MS, MAX_BEACON_BYTES, MAX_MANIFEST_BYTES,
23
+ challengeFor, cleanName, ephemeralPair, frameChannel, handshakeTranscript, mixesEphemeral, proof, proofOk,
24
+ readChallenge, readEphemeral, readPub, sealsFrames, sessionKey, trustedPeer,
127
25
  } from "./lan-sync.mjs";
128
-
129
- /** The one fixed port in the feature. Out of the deck's HTTP range (4317-4400)
130
- * so a beacon can never be mistaken for a deck's own traffic, and unassigned:
131
- * 45317 reads as "4317, elsewhere", which is what a person tracing this in a
132
- * firewall log needs it to say. */
133
- export const DISCOVERY_PORT = 45_317;
134
-
135
- /** How long a connection has to finish the handshake before it is dropped. A
136
- * handshake is two round trips on a local network — single-digit
137
- * milliseconds — so five seconds is generous for a slow machine and short
138
- * enough that holding sockets open costs an attacker something. */
139
- export const HANDSHAKE_MS = 5_000;
140
-
141
- /** The most one frame may be, and the most a peer may hold open.
142
- *
143
- * Frames are JSON lines. The largest legitimate one is a manifest; the cap is
144
- * an order of magnitude over the biggest real store, and the buffer is
145
- * ABANDONED rather than grown past it — a socket that keeps sending without a
146
- * newline is trying to make this allocate, and the answer is to stop reading
147
- * rather than to read faster. */
148
- export const MAX_FRAME_BYTES = MAX_MANIFEST_BYTES;
26
+ import { inviteProof, inviteProofBack } from "./lan-invite.mjs";
27
+ // The line both halves speak — the reader, the writer, the cap on a frame and
28
+ // the deadline on a handshake — is lan-lines.mjs's. Re-exported, so what
29
+ // imports it from here still does.
30
+ import { frameReader, sendFrame, HANDSHAKE_MS } from "./lan-lines.mjs";
31
+ export { frameReader, sendFrame, HANDSHAKE_MS, MAX_FRAME_BYTES } from "./lan-lines.mjs";
32
+ // The calling half — dialling, proving, and reading a refusal — is
33
+ // lan-call.mjs's. Re-exported, so what dials through this file still does.
34
+ export { connectToPeer } from "./lan-call.mjs";
149
35
 
150
36
  /** Concurrent connections from all peers together. Small on purpose: the real
151
37
  * number is one per peer per minute, and anything above this is either a bug
@@ -198,414 +84,6 @@ export const MAX_SOCKETS_PER_HOST = 4;
198
84
  */
199
85
  export const IDLE_MS = 30_000;
200
86
 
201
- /** The shortest gap between two "I am here too" replies to a stranger. Long
202
- * enough that a burst of decks starting together cannot make a storm, short
203
- * enough that starting two decks by hand feels instant. */
204
- const REPLY_COOLDOWN_MS = 2_000;
205
-
206
- /**
207
- * The shouting half.
208
- *
209
- * `reuseAddr` is not a convenience: without it a second deck on the same
210
- * machine cannot bind the discovery port at all, and two decks on one machine
211
- * is both a real setup and the only way this gets tested here.
212
- *
213
- * Errors are reported, never thrown. A machine with no route, a firewall that
214
- * refuses the bind, an interface that comes and goes with a VPN — none of them
215
- * is a reason for the deck to fall over, and all of them are reasons for the
216
- * panel to be able to say what happened.
217
- */
218
- export function createBeacon({
219
- port, name, fp, onPeer, onStranger, onError, onIdClash, now = Date.now,
220
- /** The decks somebody has accepted, read fresh each packet so an accept takes
221
- * effect immediately rather than at the next restart. */
222
- trusted = () => [],
223
- // Injected so the suite can drive this with a socket it controls. CI runners
224
- // are not a network: GitHub's have no broadcast domain worth the name, and a
225
- // test that quietly skipped there would be a test that stopped testing
226
- // without saying so. The REAL socket is exercised by hand, two decks on one
227
- // machine, which works because a broadcast comes back to its own host.
228
- createSocket = opts => dgram.createSocket(opts),
229
- // Injected for the same reason, and read PER ANNOUNCE rather than once: a
230
- // laptop that joins a network, or brings a VPN up, grows an interface without
231
- // restarting the deck, and a list captured at start would announce to the
232
- // addresses it had at breakfast.
233
- ifaces = () => networkInterfaces(),
234
- /**
235
- * Addresses to send the beacon to one by one, beside the broadcast — the
236
- * owner's machines on Tailscale, whose tunnel carries no broadcast at all.
237
- * Read per announce like `ifaces`, because the tailnet list is re-read while
238
- * the deck runs and a machine that just came online belongs in the next one.
239
- */
240
- unicast = () => [],
241
- /**
242
- * Which way a packet from this address came: "lan", "tailscale", or null for
243
- * one to ignore entirely. Null is what a tailnet packet gets while this
244
- * deck's Tailscale switch is off — the sockets hear it either way, because
245
- * they bind every interface, and off has to mean the deck does not act on it.
246
- */
247
- routeFor = () => "lan",
248
- /** How long to wait before trying the discovery port again while another
249
- * program holds it, and who to tell when hearing stops or comes back. */
250
- rebindMs = 30_000,
251
- onHearing,
252
- /**
253
- * Where the machine would send each broadcast — see createRouteCheck in
254
- * route-via.mjs. Absent, every broadcast goes, as before; the suite's
255
- * beacons have none.
256
- */
257
- routes = null,
258
- } = {}) {
259
- // Randomised per process. Two beacons from one fingerprint with different
260
- // instance ids mean the deck restarted between them, which is the signal to
261
- // drop any session held for it rather than resume into a process that is gone.
262
- const instance = randomBytes(8).toString("hex");
263
- /** Which computer this is, as opposed to which process or which key. Derived
264
- * once per beacon rather than per packet: it cannot change while a process
265
- * is running, and hashing a hostname thirty seconds apart forever is work
266
- * nobody asked for. See hostId. */
267
- const host = hostId();
268
- const peers = new Map();
269
- let sock = null;
270
- /**
271
- * The socket beacons LEAVE by, on a port of its own. See start.
272
- *
273
- * Null until it is bound, and whenever it could not be — the listening
274
- * socket then sends as it always did, because a beacon from the wrong port is
275
- * still a beacon and none at all is a deck nobody finds.
276
- */
277
- let out = null;
278
- let timer = null;
279
- /**
280
- * Whether this deck can HEAR — whether it holds the discovery port.
281
- *
282
- * SEPARATE FROM RUNNING, because a deck that cannot bind 45317 is still most
283
- * of a deck. It announces from its own socket, so every other deck still
284
- * finds it, asks it and dials it; what it has lost is hearing them announce.
285
- * Stopping the whole feature over that — which is what this did — took a
286
- * working sync away from somebody whose Tailscale exit node happened to be
287
- * holding the port. It keeps trying for the port instead, and takes it back
288
- * the moment it is free.
289
- */
290
- let hearing = false;
291
- let deafError = null;
292
- /** Whether the last announce held back every broadcast because the local
293
- * network goes through a tunnel here — see leavesByTunnel. */
294
- let tunneled = false;
295
- let told = null;
296
- let rebind = null;
297
- let stopped = true;
298
- // Socket binds finish asynchronously. A stopped start must not attach its
299
- // late socket to a subsequent start or leave an outbound socket open.
300
- let startGeneration = 0;
301
- /** When this deck last answered a deck it had not heard, so answering cannot
302
- * become a storm, and which decks it has already answered — without the
303
- * second, a deck that is never accepted is answered again on every packet
304
- * for as long as both are running. */
305
- let repliedAt = 0;
306
- const answered = new Set();
307
-
308
- const payload = () => Buffer.from(JSON.stringify(beaconPayload({ name, fp, port, instance, host })));
309
-
310
- const announce = (also = []) => {
311
- if (!sock && !out) return;
312
- const announcedIn = startGeneration;
313
- // EVERY BROADCAST ADDRESS THIS MACHINE HAS, not one.
314
- //
315
- // This used to send only to 255.255.255.255, on the argument that the
316
- // limited broadcast "needs nothing" while a subnet-directed one needs the
317
- // netmask of whichever interface the packet leaves by. The argument is
318
- // sound and the machine disagreed with it. Measured on a Mac with Wi-Fi and
319
- // a VPN tunnel up, from a bare node process with no deck involved:
320
- //
321
- // send to 255.255.255.255 -> EHOSTUNREACH
322
- // send to 192.168.1.255 -> sent ok
323
- //
324
- // On a multi-homed host the limited broadcast has no single interface to
325
- // leave by, and macOS refuses it rather than choosing. The deck went on
326
- // announcing into nothing for as long as that machine was up: it could still
327
- // HEAR colleagues, because receiving is per-port and not per-address, so the
328
- // symptom was one-sided and looked like everybody else's problem.
329
- //
330
- // So the limited form stays — it is the one that works where a directed
331
- // broadcast is filtered, and it is what Syncthing sends — and every
332
- // interface's own directed broadcast goes out beside it. A duplicate packet
333
- // costs one datagram; a missing one costs the whole feature.
334
- const plan = broadcastPlan(ifaces());
335
- // Asked behind the send, and read from what was last answered: the first
336
- // beacon after a start goes as it always did, and the ones after it know.
337
- void routes?.want?.(plan.map(p => p.to));
338
- const kept = routes ? plan.filter(p => !leavesByTunnel(p, routes.via(p.to), looksLikeTunnel)) : plan;
339
- tunneled = kept.length === 0;
340
- const targets = kept.map(p => p.to);
341
- const from = out ?? sock;
342
- let left = targets.length;
343
- const failed = [];
344
- for (const to of targets) {
345
- from.send(payload(), DISCOVERY_PORT, to, err => {
346
- // A send may finish after this socket has closed and a new LAN
347
- // session has started. Its result says nothing about the new session.
348
- if (stopped || announcedIn !== startGeneration) return;
349
- if (err) failed.push(`${to} (${err.code ?? err.message})`);
350
- // REPORTED ONLY WHEN EVERY ONE FAILED. One address being unreachable is
351
- // the ordinary state of a machine with a VPN up, and a panel that said
352
- // so every thirty seconds would be crying wolf about a working deck.
353
- // No address working at all is a deck nobody can discover, which is
354
- // exactly what the panel is for.
355
- if (--left === 0 && failed.length === targets.length) {
356
- onError?.("announce", new Error(`no broadcast address worked — ${failed.join(", ")}`));
357
- }
358
- });
359
- }
360
- // AND ONE PACKET PER TAILNET MACHINE. Not part of the verdict above: a node
361
- // that has gone to sleep since the list was read is the ordinary state of a
362
- // laptop, and it says nothing about whether this deck can be discovered.
363
- let direct = [];
364
- try { direct = [...(unicast() ?? []), ...also]; } catch { direct = [...also]; }
365
- for (const to of new Set(direct)) {
366
- if (typeof to !== "string" || !to || targets.includes(to)) continue;
367
- from.send(payload(), DISCOVERY_PORT, to, () => {});
368
- }
369
- };
370
-
371
- const onMessage = (msg, rinfo) => {
372
- // A queued UDP callback can run after close(). It belongs to the stopped
373
- // discovery session and must not repopulate peers or invite strangers.
374
- if (stopped) return;
375
- // Everything about whether to care lives in lan-sync.mjs. This hands it
376
- // the bytes and the address and does what it is told.
377
- if (msg.length > MAX_BEACON_BYTES) return;
378
- let via = "lan";
379
- try { via = routeFor(rinfo.address); } catch { via = "lan"; }
380
- if (!via) return;
381
- const beacon = readBeacon(msg);
382
- const verdict = beaconVerdict(beacon, { selfFp: fp, selfInstance: instance, selfHost: host, trusted: trusted() });
383
- // ANSWER A DECK WE HAVE NEVER HEARD, once, WHOEVER IT IS — and that last
384
- // part is the change. It used to answer only a deck already in the group,
385
- // which was fine when a group existed. Now the first thing a new deck has
386
- // to become is a row on somebody's screen, and it cannot become one if
387
- // this deck never tells it that it exists.
388
- //
389
- // Measured on two real decks before any of this: deck 1 saw deck 2 the
390
- // instant it started and deck 2 saw nobody, because deck 1's own
391
- // immediate announce went out before deck 2 was listening. Thirty seconds
392
- // of an empty list is how a working feature reads as broken.
393
- //
394
- // At most once every few seconds, because the obvious version is a shout
395
- // storm: two decks answering each other's answers forever. A new pair
396
- // converges in two extra packets.
397
- const newToUs = beacon && verdict !== "self" && verdict !== "id-clash" && verdict !== "unreadable"
398
- && !peers.has(beacon.fp) && !answered.has(beacon.fp);
399
- if (newToUs && now() - repliedAt > REPLY_COOLDOWN_MS) {
400
- repliedAt = now();
401
- answered.add(beacon.fp);
402
- // A deck that reached this one over the tailnet is answered there too:
403
- // a broadcast never gets back down its tunnel.
404
- announce(via === "tailscale" ? [rinfo.address] : []);
405
- }
406
- if (verdict !== "peer") {
407
- // Another deck is using this one's key — see beaconVerdict. Reported
408
- // rather than fixed here: this file carries packets, and choosing a new
409
- // identity for the deck belongs to whoever stores it.
410
- if (verdict === "id-clash") onIdClash?.();
411
- // A DECK NOBODY HAS ACCEPTED. It is not refused and not silently
412
- // dropped: it is a name and an address on the same network, which is a
413
- // row somebody can accept. Nothing is asked of it and nothing is
414
- // offered to it until they do.
415
- if (verdict === "stranger") {
416
- onStranger?.({
417
- fp: beacon.fp, name: beacon.name, addr: rinfo.address, port: beacon.port,
418
- // Carried through so the list can show one row per machine rather
419
- // than one per key that machine has ever held.
420
- host: beacon.host, at: now(), via,
421
- });
422
- }
423
- return;
424
- }
425
- const noted = notePeer(peers, beacon, rinfo.address, now(), via);
426
- if (noted.changed || noted.restarted) onPeer?.(noted);
427
- };
428
-
429
- /** Tell whoever asked, once per change rather than once per try. */
430
- const tell = now => {
431
- if (told === now) return;
432
- told = now;
433
- onHearing?.(now);
434
- };
435
-
436
- /** One try at the discovery port: the listening socket, or the error that
437
- * kept it. A bind that fails never calls back; it arrives as an error. */
438
- const listen = (startedIn) => new Promise(resolve => {
439
- let s;
440
- try { s = createSocket({ type: "udp4", reuseAddr: true }); } catch (err) { resolve({ err }); return; }
441
- let bound = false;
442
- s.on("error", err => {
443
- if (bound) {
444
- if (!stopped && startedIn === startGeneration) onError?.("socket", err);
445
- return;
446
- }
447
- try { s.close(); } catch { /* never opened */ }
448
- resolve({ err });
449
- });
450
- // close() can leave a message callback queued. After restart, stopped is
451
- // false again, so the socket's own generation must also be checked.
452
- s.on("message", (msg, rinfo) => {
453
- if (startedIn !== startGeneration) return;
454
- onMessage(msg, rinfo);
455
- });
456
- s.bind(DISCOVERY_PORT, "0.0.0.0", () => {
457
- bound = true;
458
- try { s.setBroadcast(true); } catch (err) { onError?.("broadcast", err); }
459
- resolve({ sock: s });
460
- });
461
- });
462
-
463
- const tryListen = async (startedIn) => {
464
- if (stopped || startedIn !== startGeneration) return;
465
- const got = await listen(startedIn);
466
- if (stopped || startedIn !== startGeneration) { try { got.sock?.close(); } catch { /* gone */ } return; }
467
- if (got.sock) {
468
- sock = got.sock;
469
- hearing = true;
470
- deafError = null;
471
- tell(true);
472
- return;
473
- }
474
- hearing = false;
475
- deafError = got.err;
476
- tell(false);
477
- rebind = setTimeout(() => { rebind = null; void tryListen(startedIn); }, rebindMs);
478
- rebind.unref?.();
479
- };
480
-
481
- /**
482
- * The socket beacons leave by, bound to whatever port the OS gives it.
483
- *
484
- * SENT FROM A PORT OF ITS OWN, NOT FROM 45317. Nothing that hears a beacon
485
- * reads the port it came from — the reply goes to 45317 whatever the source
486
- * — and sending from the discovery port gave that port away. Measured on a
487
- * Mac that is a Tailscale exit node: a deck elsewhere on the tailnet routed a
488
- * beacon through it, and Tailscale's forwarder binds its end of every UDP
489
- * flow to the CLIENT's source port (netstack.go, forwardUDP), idling it out
490
- * after two minutes. A beacon every thirty seconds never idles, so the Mac's
491
- * own deck could never bind 45317 again. From an ephemeral port, the
492
- * forwarder takes an ephemeral port.
493
- */
494
- const openOut = () => new Promise(resolve => {
495
- let o;
496
- try { o = createSocket({ type: "udp4" }); } catch { resolve(null); return; }
497
- let opened = false;
498
- o.on("error", err => {
499
- if (opened) { onError?.("socket", err); return; }
500
- opened = true;
501
- try { o.close(); } catch { /* never opened */ }
502
- resolve(null);
503
- });
504
- o.bind(0, "0.0.0.0", () => {
505
- if (opened) return;
506
- opened = true;
507
- try { o.setBroadcast(true); } catch (err) { onError?.("broadcast", err); }
508
- resolve(o);
509
- });
510
- });
511
-
512
- const start = async () => {
513
- const startedIn = ++startGeneration;
514
- stopped = false;
515
- told = null;
516
- await tryListen(startedIn);
517
- if (stopped || startedIn !== startGeneration) return;
518
- const opened = await openOut();
519
- if (stopped || startedIn !== startGeneration) {
520
- try { opened?.close(); } catch { /* already closed */ }
521
- return;
522
- }
523
- out = opened;
524
- // Immediately, not on the next tick. Syncthing's rule: a deck that just
525
- // came up should appear now rather than up to thirty seconds later, which
526
- // is the difference between "it works" and "it seems broken" for anybody
527
- // who starts two decks and watches.
528
- announce();
529
- timer = setInterval(announce, ANNOUNCE_MS);
530
- timer.unref?.();
531
- };
532
-
533
- return {
534
- start,
535
- announce,
536
- peers,
537
- /** Whether this deck holds the discovery port, and why not when it does
538
- * not. */
539
- hearing: () => hearing,
540
- deafError: () => deafError,
541
- tunneled: () => tunneled,
542
- stop() {
543
- stopped = true;
544
- startGeneration++;
545
- if (timer) clearInterval(timer);
546
- timer = null;
547
- if (rebind) clearTimeout(rebind);
548
- rebind = null;
549
- hearing = false;
550
- try { sock?.close(); } catch { /* already closed */ }
551
- sock = null;
552
- try { out?.close(); } catch { /* already closed */ }
553
- out = null;
554
- },
555
- };
556
- }
557
-
558
- /**
559
- * Read newline-delimited JSON off a socket, refusing to be made to allocate.
560
- *
561
- * The cap is on the UNTERMINATED buffer rather than on a frame that arrived,
562
- * which is the distinction that matters: a peer that sends a megabyte with no
563
- * newline in it is not sending a large frame, it is sending nothing at all,
564
- * expensively. Past the cap this stops reading and hands the caller a refusal
565
- * — it does not keep buffering in the hope a newline turns up.
566
- */
567
- export function frameReader(onFrame, onRefuse, max = MAX_FRAME_BYTES) {
568
- let buf = "";
569
- let dead = false;
570
- return chunk => {
571
- if (dead) return;
572
- buf += chunk;
573
- if (buf.length > max) { dead = true; buf = ""; onRefuse("frame too large"); return; }
574
- let i;
575
- while ((i = buf.indexOf("\n")) !== -1) {
576
- const line = buf.slice(0, i);
577
- buf = buf.slice(i + 1);
578
- if (!line.trim()) continue;
579
- let msg;
580
- try { msg = JSON.parse(line); }
581
- catch { dead = true; buf = ""; onRefuse("not json"); return; }
582
- // `typeof [] === "object"`, so an array walks straight past the obvious
583
- // check and reaches a handler that reads `msg.t` off it — undefined, and
584
- // then whatever that handler does with a frame that has no type. A frame
585
- // is a record; anything else is refused.
586
- if (!msg || typeof msg !== "object" || Array.isArray(msg)) {
587
- dead = true; buf = ""; onRefuse("not an object"); return;
588
- }
589
- // A HANDLER THAT THROWS ENDS ITS CONNECTION, NOT THE PROCESS. This runs
590
- // inside the socket's `data` event, where nothing above catches, so a
591
- // throw from a frame handler took the whole deck down — and every frame
592
- // read here has come off the network before anybody is trusted. The
593
- // handlers are meant never to throw; this is what holds when one does.
594
- // The error goes on with the refusal, so the next one that hides here is
595
- // logged with its stack rather than as two words.
596
- try { onFrame(msg); }
597
- catch (err) { dead = true; buf = ""; onRefuse("bad frame", err); return; }
598
- if (dead) return;
599
- }
600
- };
601
- }
602
-
603
- /** One line out. Kept in one place so nothing forgets the newline the reader
604
- * above is waiting for. */
605
- export function sendFrame(sock, obj) {
606
- try { sock.write(`${JSON.stringify(obj)}\n`); } catch { /* peer went away */ }
607
- }
608
-
609
87
  /**
610
88
  * The answering half.
611
89
  *
@@ -644,8 +122,14 @@ export function createSyncServer({
644
122
  * on arrival rather than queued behind a press. */
645
123
  invite = () => null,
646
124
  /** One was used. The caller stores the pairing and retires the invite: a
647
- * token that pairs twice is a token worth stealing twice. */
125
+ * token that pairs twice is a token worth stealing twice. Called for EVERY
126
+ * proof that holds, a deck already paired included — see auth (#1137). */
648
127
  onInviteUsed,
128
+ /** A caller brought a proof of the invite this deck is offering, and it did
129
+ * not hold. The caller counts it against the invite and puts the invite
130
+ * away after a few — see MAX_WRONG_PROOFS. Told the caller's address and
131
+ * who it says it is, which is what the record of it is for. */
132
+ onWrongInvite,
649
133
  /** A connection arrived, before anything about it is known — called with the
650
134
  * address it came from and nothing else.
651
135
  *
@@ -666,6 +150,11 @@ export function createSyncServer({
666
150
  * case that slept for the real value would be thirty seconds of CI per run
667
151
  * and would still only be checking a timer. */
668
152
  idleMs = IDLE_MS,
153
+ /** How long a connection has to finish the handshake — see HANDSHAKE_MS,
154
+ * which is what the deck runs on. A parameter for idleMs' reason: so the
155
+ * suite can watch a silent caller dropped in a few hundred milliseconds
156
+ * rather than sitting through the real five seconds of it (#994). */
157
+ handshakeMs = HANDSHAKE_MS,
669
158
  /** Whether this deck seals every frame after the handshake with a peer that
670
159
  * says it does too — see frameChannel. On in every real deck. False makes
671
160
  * this listener announce nothing and seal nothing, which is the wire every
@@ -732,7 +221,7 @@ export function createSyncServer({
732
221
  // Carrying this deck's marks — that it seals, and that it mixes a key pair
733
222
  // of its own into the key — inside the one field the handshake already
734
223
  // binds. See "THE ANNOUNCEMENT RIDES INSIDE THE CHALLENGE" and EPHEMERAL
735
- // in lan-sync.mjs.
224
+ // in lan-wire.mjs.
736
225
  const myChallenge = challengeFor({ seals: sealFrames, ephemeral });
737
226
  let theirChallenge = null;
738
227
  /** What the key was derived over, kept so the invite proofs below are made
@@ -757,7 +246,7 @@ export function createSyncServer({
757
246
  // Armed before the first byte is read, and cleared only by a completed
758
247
  // handshake. A socket that connects and says nothing is the cheapest
759
248
  // possible way to hold a resource, so it is also the first one closed.
760
- const deadline = setTimeout(() => { if (!authed) sock.destroy(); }, HANDSHAKE_MS);
249
+ const deadline = setTimeout(() => { if (!authed) sock.destroy(); }, handshakeMs);
761
250
  deadline.unref?.();
762
251
 
763
252
  const done = () => { clearTimeout(deadline); live.delete(sock); };
@@ -799,6 +288,213 @@ export function createSyncServer({
799
288
  setTimeout(bye, 250).unref?.();
800
289
  };
801
290
 
291
+ /** Step 1 of the four, answered: a caller's hello, checked, and this
292
+ * end's challenge back — with the connection's key derived first. */
293
+ const hello = msg => {
294
+ // A string of letters, digits, `.`, `_` and `-` — a `|` inside it
295
+ // would let the transcript come out the same on both ends while each
296
+ // read a different challenge. See readChallenge.
297
+ if (theirChallenge || !readChallenge(msg.challenge)) return refuse("bad hello");
298
+ const them = readPub(msg.pub);
299
+ // The fingerprint is a hash of the key, so a hello whose two halves
300
+ // disagree is not a deck with a stale field, it is somebody trying to
301
+ // be announced as one deck and prove they are another.
302
+ if (!them || them.fp !== msg.fp) return refuse("bad hello");
303
+ theirChallenge = msg.challenge;
304
+ peerFp = them.fp;
305
+ peerPub = them.pub;
306
+ // THROUGH cleanName, like the beacon and the invite. This path — the
307
+ // handshake — was the one that skipped it, and it is the one that
308
+ // feeds the pairing prompt and cfg.trusted, which lan-deck.mjs writes to
309
+ // prefs.json. So the only bound on the name an operator reads before
310
+ // pressing Accept was the 128 KB frame cap.
311
+ //
312
+ // cleanName caps at MAX_NAME (40) and collapses \s+, which includes
313
+ // U+00A0 — and 200 non-breaking spaces neither collapse in HTML nor
314
+ // offer a break opportunity, so a name of "Alice's laptop at
315
+ // 192.168.1.10 wants to pair" + padding pushed the REAL address out
316
+ // of a fixed 288px column and left a complete, plausible sentence.
317
+ peerName = cleanName(msg.name, "");
318
+ peerPort = Number.isInteger(msg.port) && msg.port > 0 && msg.port < 65_536 ? msg.port : null;
319
+ peerNoAsk = msg.ask === false;
320
+ // MIXED WHEN BOTH CHALLENGES SAY SO, and only then: the two strings
321
+ // the proofs bind decide it, never whether a field turned up. Once
322
+ // both say so, a hello with no usable key is refused rather than
323
+ // answered the old way, because answering the old way is the
324
+ // downgrade. See EPHEMERAL in lan-wire.mjs.
325
+ //
326
+ // A KEY OFFERED IS A KEY ANSWERED, whatever the challenges say, so
327
+ // what this end sends depends only on what it was sent. When the two
328
+ // do not mix, ours goes out and is never used. That happens only
329
+ // between two decks of this version whose challenges somebody edited
330
+ // on the way, and there it takes the dialler on to a proof this end
331
+ // refuses by name — `bad proof`, as for every other edit to a
332
+ // challenge — rather than stopping it one message short. Made here
333
+ // and not when the socket opened, so a connection that never says
334
+ // hello costs what it always did.
335
+ const theirEphemeral = readEphemeral(msg.epk);
336
+ const mixing = mixesEphemeral(myChallenge) && mixesEphemeral(theirChallenge);
337
+ if (mixing && !theirEphemeral) return refuse("bad hello");
338
+ let mine = theirEphemeral && mixesEphemeral(myChallenge) ? ephemeralPair() : null;
339
+ const epk = mine?.pub;
340
+ transcript = mixing
341
+ ? handshakeTranscript(peerFp, fp, theirChallenge, myChallenge, theirEphemeral, epk)
342
+ : handshakeTranscript(peerFp, fp, theirChallenge, myChallenge);
343
+ // A key X25519 cannot use throws in here, and nothing above this
344
+ // handler catches it — see readPub for what that throw used to do.
345
+ try {
346
+ key = sessionKey(secret, peerPub, transcript,
347
+ mixing ? { role: "listener", priv: mine.priv, peer: theirEphemeral } : null);
348
+ } catch {
349
+ return refuse("bad hello");
350
+ } finally {
351
+ // The private half, let go before a byte of the reply is written.
352
+ mine = null;
353
+ }
354
+ sendFrame(sock, { t: "challenge", fp, pub, name, challenge: myChallenge, ...(epk ? { epk } : {}) });
355
+ };
356
+
357
+ /**
358
+ * Step 4, and the only way `authed` turns true: sealed from here when both
359
+ * challenges said so, the handshake deadline off, and this deck's own proof
360
+ * sent — with the invite's proof back when the caller held one. `ok` goes
361
+ * out in the clear; the frames after it are the sealed kind.
362
+ */
363
+ const welcome = back => {
364
+ authed = true;
365
+ chan = sealedChannel();
366
+ clearTimeout(deadline);
367
+ sendFrame(sock, {
368
+ t: "ok", fp, name,
369
+ proof: proof(key, {
370
+ challenge: myChallenge, peerChallenge: theirChallenge,
371
+ fromFp: fp, toFp: peerFp, direction: "reply",
372
+ }),
373
+ ...back,
374
+ });
375
+ };
376
+
377
+ /** Step 3: the caller's proof, and then who it turns out to be — a deck
378
+ * somebody here accepted, one holding this deck's invite, or a refusal
379
+ * that says which. */
380
+ const auth = msg => {
381
+ // A key, too: a challenge on record says a `hello` arrived, not that one
382
+ // was accepted. Unreachable after the guard at the top of the frame
383
+ // handler, and stated anyway, because it is the rule `proof` below
384
+ // depends on.
385
+ if (msg.t !== "auth" || !theirChallenge || !key) return refuse("expected auth");
386
+ const want = proof(key, {
387
+ challenge: theirChallenge, peerChallenge: myChallenge,
388
+ fromFp: peerFp, toFp: fp, direction: "hello",
389
+ });
390
+ // A recording of a previous exchange fails here, because `myChallenge`
391
+ // was made when this socket opened and has never been sent before.
392
+ if (!proofOk(want, msg.proof)) return refuse("bad proof");
393
+
394
+ // WHO IS THIS, and it is the only question left. The handshake proves
395
+ // they hold the key they claimed; the trusted list says whether anybody
396
+ // here ever agreed to talk to it.
397
+ const known = trustedPeer(trusted(), peerFp);
398
+ if (known && known.pub !== peerPub) {
399
+ // The fingerprint we pinned, presented with a different key. 48 bits
400
+ // is far past accident, so this is somebody wearing a paired deck's
401
+ // name — refused loudly rather than quietly re-pinned.
402
+ return refuse("impostor");
403
+ }
404
+ // AN INVITE THIS DECK HANDED OUT, PRESENTED BACK. Whoever is calling
405
+ // holds a token the owner of this machine copied and sent, which is the
406
+ // same decision the accept button is — made earlier, and made once.
407
+ //
408
+ // The proof is over the transcript, so it is worth nothing to somebody
409
+ // who recorded an earlier exchange, and the code itself never travels.
410
+ //
411
+ // ASKED BEFORE "DO I KNOW THIS DECK", because the two questions are
412
+ // independent and the answer to this one is owed to the caller either
413
+ // way. A caller that sent a code is waiting to be shown one back, and a
414
+ // deck it has ALREADY paired with is not exempt from that — somebody
415
+ // pasting a token into a deck that happens to be paired already would
416
+ // otherwise be told the minter does not hold its own invite.
417
+ // `offer` rather than `live`, which is what this used to be called: the
418
+ // socket table one scope out is also `live`, and widening this block
419
+ // widened the shadow with it.
420
+ const offer = invite();
421
+ const brought = typeof msg.invite === "string";
422
+ // Over the transcript the key came from — the ephemeral keys in it
423
+ // when the two mix — which is the string the dialler proved over too.
424
+ const heldInvite = !!offer && brought && proofOk(inviteProof(offer.code, transcript), msg.invite);
425
+ // A PROOF THAT DID NOT HOLD, against an invite that is live (#1137).
426
+ // Counted against that invite, which is put away after a few, and
427
+ // refused by name rather than read as a caller that brought nothing:
428
+ // that used to make a wrong token a pairing request here, and on the
429
+ // caller's screen "waiting for them to say yes" about a question
430
+ // nobody was going to be asked. With no invite live there is nothing
431
+ // to count against, and the caller is answered as it always was.
432
+ if (offer && brought && !heldInvite) {
433
+ onWrongInvite?.({ fp: peerFp, name: peerName, addr: from(sock) });
434
+ return refuse("wrong invite");
435
+ }
436
+ // AND THE CODE, BACK. The session proof says "I hold the private half
437
+ // of the key I just showed you", which anything with a socket can say.
438
+ // This says "I am the deck whose owner minted that token", which only
439
+ // the minter can — and a caller joining on an invite has no pin to
440
+ // check against, so it is the only thing standing between an invite
441
+ // address and whoever else is reachable there.
442
+ const back = heldInvite ? { inviteProof: inviteProofBack(offer.code, transcript) } : {};
443
+ // SPENT BY ANY PROOF THAT HOLDS, known deck or not (#1137). This was
444
+ // asked only on the stranger's path below, so a deck already on the list
445
+ // was shown the proof back and the token stayed live for the rest of its
446
+ // ten minutes — and "already on the list" includes a deck whose first
447
+ // pairing dropped out after this very proof, which is exactly the deck
448
+ // that comes back with a fresh invite. Whether the caller was known
449
+ // decides nothing about whether the token has been used; it has. So
450
+ // there is nothing to press, either: the deck is pinned here.
451
+ if (heldInvite) {
452
+ onInviteUsed?.({ fp: peerFp, pub: peerPub, name: peerName, port: peerPort, addr: from(sock) });
453
+ welcome(back);
454
+ return;
455
+ }
456
+ if (!known) {
457
+ // A DECK THIS ONE'S OWNER ALREADY ANSWERED, and the answer was no.
458
+ // It is not asked again here, and — the half a held refusal cannot
459
+ // do — the deck that asked is told, so its own panel can stop saying
460
+ // "waiting" about a question that has been answered.
461
+ if (declined(peerFp)) return refuse("declined");
462
+ // AN INVITE-ONLY DECK, and this caller holds no live invite of it (a
463
+ // caller that did was paired above, and one that got it wrong was
464
+ // refused). Answered rather than queued: the engine records no
465
+ // request in this mode, so "pending" would leave the other deck's
466
+ // panel saying "waiting for them to say yes" about a question nobody
467
+ // here will ever see. After "declined", which is the more specific
468
+ // answer about this one deck.
469
+ if (inviteOnly()) return refuse("invite only");
470
+ // A CALLER THAT IS NOT ASKING. An invite-only deck still dials the
471
+ // addresses it already had, because an invite-paired deck is one of
472
+ // them; one that turns out not to know it must not become a request
473
+ // here — with the accept switch on, that request would have pinned a
474
+ // deck whose owner said it pairs only by invite.
475
+ if (peerNoAsk) return refuse("not asking");
476
+
477
+ // A REAL DECK WE HAVE NOT MET. It finished a handshake, so it is not
478
+ // a port scan, and it told us a name and an address a person can
479
+ // recognise. That is a row with an accept on it, and nothing else
480
+ // happens until somebody presses it.
481
+ onPending?.({
482
+ fp: peerFp, pub: peerPub, name: peerName,
483
+ addr: from(sock),
484
+ // Where it LISTENS, from the hello — not this socket's remote port,
485
+ // which is ephemeral. This is what lets an accept dial back.
486
+ port: peerPort,
487
+ });
488
+ return refuse("pending");
489
+ }
490
+
491
+ // And ours, so the caller knows it reached the deck it pinned rather
492
+ // than something standing in the way of one. No invite on this path: a
493
+ // caller that held one was answered above, and a deck already on this
494
+ // list that holds no live one is simply a paired deck calling.
495
+ welcome({});
496
+ };
497
+
802
498
  sock.on("data", frameReader(msg => {
803
499
  // NOTHING AFTER A REFUSAL. The reader hands over every frame in a chunk,
804
500
  // and `refuse` destroys the socket without stopping it. So a `hello`
@@ -827,188 +523,8 @@ export function createSyncServer({
827
523
  // they buy is a connection key that is gone once both ends have let go
828
524
  // of the private halves, whoever takes either long-term key later —
829
525
  // see sessionKey.
830
- if (msg.t === "hello") {
831
- // A string of letters, digits, `.`, `_` and `-` — a `|` inside it
832
- // would let the transcript come out the same on both ends while each
833
- // read a different challenge. See readChallenge.
834
- if (theirChallenge || !readChallenge(msg.challenge)) return refuse("bad hello");
835
- const them = readPub(msg.pub);
836
- // The fingerprint is a hash of the key, so a hello whose two halves
837
- // disagree is not a deck with a stale field, it is somebody trying to
838
- // be announced as one deck and prove they are another.
839
- if (!them || them.fp !== msg.fp) return refuse("bad hello");
840
- theirChallenge = msg.challenge;
841
- peerFp = them.fp;
842
- peerPub = them.pub;
843
- // THROUGH cleanName, like the beacon and the invite. This path — the
844
- // handshake — was the one that skipped it, and it is the one that
845
- // feeds the pairing prompt and cfg.trusted, which index.mjs writes to
846
- // prefs.json. So the only bound on the name an operator reads before
847
- // pressing Accept was the 128 KB frame cap.
848
- //
849
- // cleanName caps at MAX_NAME (40) and collapses \s+, which includes
850
- // U+00A0 — and 200 non-breaking spaces neither collapse in HTML nor
851
- // offer a break opportunity, so a name of "Alice's laptop at
852
- // 192.168.1.10 wants to pair" + padding pushed the REAL address out
853
- // of a fixed 288px column and left a complete, plausible sentence.
854
- peerName = cleanName(msg.name, "");
855
- peerPort = Number.isInteger(msg.port) && msg.port > 0 && msg.port < 65_536 ? msg.port : null;
856
- peerNoAsk = msg.ask === false;
857
- // MIXED WHEN BOTH CHALLENGES SAY SO, and only then: the two strings
858
- // the proofs bind decide it, never whether a field turned up. Once
859
- // both say so, a hello with no usable key is refused rather than
860
- // answered the old way, because answering the old way is the
861
- // downgrade. See EPHEMERAL in lan-sync.mjs.
862
- //
863
- // A KEY OFFERED IS A KEY ANSWERED, whatever the challenges say, so
864
- // what this end sends depends only on what it was sent. When the two
865
- // do not mix, ours goes out and is never used. That happens only
866
- // between two decks of this version whose challenges somebody edited
867
- // on the way, and there it takes the dialler on to a proof this end
868
- // refuses by name — `bad proof`, as for every other edit to a
869
- // challenge — rather than stopping it one message short. Made here
870
- // and not when the socket opened, so a connection that never says
871
- // hello costs what it always did.
872
- const theirEphemeral = readEphemeral(msg.epk);
873
- const mixing = mixesEphemeral(myChallenge) && mixesEphemeral(theirChallenge);
874
- if (mixing && !theirEphemeral) return refuse("bad hello");
875
- let mine = theirEphemeral && mixesEphemeral(myChallenge) ? ephemeralPair() : null;
876
- const epk = mine?.pub;
877
- transcript = mixing
878
- ? handshakeTranscript(peerFp, fp, theirChallenge, myChallenge, theirEphemeral, epk)
879
- : handshakeTranscript(peerFp, fp, theirChallenge, myChallenge);
880
- // A key X25519 cannot use throws in here, and nothing above this
881
- // handler catches it — see readPub for what that throw used to do.
882
- try {
883
- key = sessionKey(secret, peerPub, transcript,
884
- mixing ? { role: "listener", priv: mine.priv, peer: theirEphemeral } : null);
885
- } catch {
886
- return refuse("bad hello");
887
- } finally {
888
- // The private half, let go before a byte of the reply is written.
889
- mine = null;
890
- }
891
- sendFrame(sock, { t: "challenge", fp, pub, name, challenge: myChallenge, ...(epk ? { epk } : {}) });
892
- return;
893
- }
894
- // A key, too: a challenge on record says a `hello` arrived, not that one
895
- // was accepted. Unreachable after the guard at the top of this handler,
896
- // and stated anyway, because it is the rule `proof` below depends on.
897
- if (msg.t !== "auth" || !theirChallenge || !key) return refuse("expected auth");
898
- const want = proof(key, {
899
- challenge: theirChallenge, peerChallenge: myChallenge,
900
- fromFp: peerFp, toFp: fp, direction: "hello",
901
- });
902
- // A recording of a previous exchange fails here, because `myChallenge`
903
- // was made when this socket opened and has never been sent before.
904
- if (!proofOk(want, msg.proof)) return refuse("bad proof");
905
-
906
- // WHO IS THIS, and it is the only question left. The handshake proves
907
- // they hold the key they claimed; the trusted list says whether anybody
908
- // here ever agreed to talk to it.
909
- const known = trustedPeer(trusted(), peerFp);
910
- if (known && known.pub !== peerPub) {
911
- // The fingerprint we pinned, presented with a different key. 48 bits
912
- // is far past accident, so this is somebody wearing a paired deck's
913
- // name — refused loudly rather than quietly re-pinned.
914
- return refuse("impostor");
915
- }
916
- // AN INVITE THIS DECK HANDED OUT, PRESENTED BACK. Whoever is calling
917
- // holds a token the owner of this machine copied and sent, which is the
918
- // same decision the accept button is — made earlier, and made once.
919
- //
920
- // The proof is over the transcript, so it is worth nothing to somebody
921
- // who recorded an earlier exchange, and the code itself never travels.
922
- //
923
- // ASKED BEFORE "DO I KNOW THIS DECK", because the two questions are
924
- // independent and the answer to this one is owed to the caller either
925
- // way. A caller that sent a code is waiting to be shown one back, and a
926
- // deck it has ALREADY paired with is not exempt from that — somebody
927
- // pasting a token into a deck that happens to be paired already would
928
- // otherwise be told the minter does not hold its own invite.
929
- // `offer` rather than `live`, which is what this used to be called: the
930
- // socket table one scope out is also `live`, and widening this block
931
- // widened the shadow with it.
932
- const offer = invite();
933
- // Over the transcript the key came from — the ephemeral keys in it
934
- // when the two mix — which is the string the dialler proved over too.
935
- const heldInvite = !!offer && typeof msg.invite === "string"
936
- && proofOk(inviteProof(offer.code, transcript), msg.invite);
937
- // AND THE CODE, BACK. The session proof says "I hold the private half
938
- // of the key I just showed you", which anything with a socket can say.
939
- // This says "I am the deck whose owner minted that token", which only
940
- // the minter can — and a caller joining on an invite has no pin to
941
- // check against, so it is the only thing standing between an invite
942
- // address and whoever else is reachable there.
943
- const back = heldInvite ? { inviteProof: inviteProofBack(offer.code, transcript) } : {};
944
- if (!known) {
945
- if (heldInvite) {
946
- // So there is nothing to press: the deck is pinned here.
947
- onInviteUsed?.({ fp: peerFp, pub: peerPub, name: peerName, port: peerPort,
948
- addr: sock.remoteAddress?.replace(/^::ffff:/, "") ?? "" });
949
- authed = true;
950
- chan = sealedChannel();
951
- clearTimeout(deadline);
952
- sendFrame(sock, {
953
- t: "ok", fp, name,
954
- proof: proof(key, {
955
- challenge: myChallenge, peerChallenge: theirChallenge,
956
- fromFp: fp, toFp: peerFp, direction: "reply",
957
- }),
958
- ...back,
959
- });
960
- return;
961
- }
962
- // A DECK THIS ONE'S OWNER ALREADY ANSWERED, and the answer was no.
963
- // It is not asked again here, and — the half a held refusal cannot
964
- // do — the deck that asked is told, so its own panel can stop saying
965
- // "waiting" about a question that has been answered.
966
- if (declined(peerFp)) return refuse("declined");
967
- // AN INVITE-ONLY DECK, and this caller brought none (a caller that
968
- // did was paired above). Answered rather than queued: the engine
969
- // records no request in this mode, so "pending" would leave the other
970
- // deck's panel saying "waiting for them to say yes" about a question
971
- // nobody here will ever see. After "declined", which is the more
972
- // specific answer about this one deck.
973
- if (inviteOnly()) return refuse("invite only");
974
- // A CALLER THAT IS NOT ASKING. An invite-only deck still dials the
975
- // addresses it already had, because an invite-paired deck is one of
976
- // them; one that turns out not to know it must not become a request
977
- // here — with the accept switch on, that request would have pinned a
978
- // deck whose owner said it pairs only by invite.
979
- if (peerNoAsk) return refuse("not asking");
980
-
981
- // A REAL DECK WE HAVE NOT MET. It finished a handshake, so it is not
982
- // a port scan, and it told us a name and an address a person can
983
- // recognise. That is a row with an accept on it, and nothing else
984
- // happens until somebody presses it.
985
- onPending?.({
986
- fp: peerFp, pub: peerPub, name: peerName,
987
- addr: sock.remoteAddress?.replace(/^::ffff:/, "") ?? "",
988
- // Where it LISTENS, from the hello — not this socket's remote port,
989
- // which is ephemeral. This is what lets an accept dial back.
990
- port: peerPort,
991
- });
992
- return refuse("pending");
993
- }
994
-
995
- authed = true;
996
- chan = sealedChannel();
997
- clearTimeout(deadline);
998
- // And ours, so the caller knows it reached the deck it pinned rather
999
- // than something standing in the way of one. Plus the invite, when one
1000
- // was presented and held: a deck already on this list is not a reason
1001
- // to leave a caller's question unanswered. Nothing is retired on this
1002
- // path — nobody was paired, because they already were.
1003
- sendFrame(sock, {
1004
- t: "ok", fp, name,
1005
- proof: proof(key, {
1006
- challenge: myChallenge, peerChallenge: theirChallenge,
1007
- fromFp: fp, toFp: peerFp, direction: "reply",
1008
- }),
1009
- ...back,
1010
- });
1011
- return;
526
+ if (msg.t === "hello") return hello(msg);
527
+ return auth(msg);
1012
528
  }
1013
529
  // SEALED FROM HERE WHEN BOTH ENDS SAID SO, AND ONLY SEALED. A frame that
1014
530
  // does not open is refused and the socket goes with it — see
@@ -1105,201 +621,3 @@ export function createSyncServer({
1105
621
  },
1106
622
  };
1107
623
  }
1108
-
1109
- /**
1110
- * The calling half: connect, prove, be proved to, then talk.
1111
- *
1112
- * BOTH SIDES PROVE, and the second half is the one that is easy to skip. A
1113
- * handshake where only the caller proves itself stops a stranger reading a
1114
- * manifest and stops nothing else — a stranger can still stand up a listener on
1115
- * the announced port, wait for a real deck to dial it, and be handed whatever
1116
- * that deck was going to say. So this checks the reply with the same care the
1117
- * server checks the hello, and gives up if it does not hold.
1118
- */
1119
- export function connectToPeer({
1120
- host, port, fp, pub, secret, name, myPort = null, code = null, timeoutMs = HANDSHAKE_MS,
1121
- /** False when this deck is reaching an address it already had WITHOUT asking
1122
- * to pair — an invite-only deck's round. Sent only when false, so every other
1123
- * hello is byte-for-byte what it was, and a deck that predates the field
1124
- * reads past it the way it reads past `epk`. */
1125
- ask = true,
1126
- /** The public key we pinned for this deck the first time, or null for a deck
1127
- * we are meeting — an address somebody typed. */
1128
- expectPub = null,
1129
- /** Whether the deck that minted `code` said it will prove it holds one too.
1130
- * From the token's own `pb`, which is the only way to tell a deck that will
1131
- * not from a deck that cannot — see mintInvite. False leaves this path
1132
- * exactly as it shipped, for a token minted by an older deck. */
1133
- inviteProvesBack = false,
1134
- /** Whether this deck seals every frame after the handshake with a deck that
1135
- * says it does too — see frameChannel. False announces nothing and seals
1136
- * nothing, exactly as every deck before #810 dials; only the suite asks. */
1137
- sealFrames = true,
1138
- /** Whether this deck mixes a key pair made for the connection into its key,
1139
- * with a deck that says it does too — see sessionKey. False dials exactly
1140
- * as a deck of #810's version does; only the suite asks. */
1141
- ephemeral = true,
1142
- }) {
1143
- return new Promise((resolve, reject) => {
1144
- const myChallenge = challengeFor({ seals: sealFrames, ephemeral });
1145
- // Made before the other deck is heard from, because the public half goes
1146
- // in the hello. Let go of once the key is derived — unused, when the deck
1147
- // that answers turns out not to mix — and on every way out below.
1148
- let mine = mixesEphemeral(myChallenge) ? ephemeralPair() : null;
1149
- const myEpk = mine?.pub;
1150
- const sock = net.createConnection({ host, port });
1151
- sock.setEncoding("utf8");
1152
- let settled = false;
1153
- const fail = err => {
1154
- if (settled) return;
1155
- settled = true;
1156
- mine = null;
1157
- sock.destroy();
1158
- reject(err instanceof Error ? err : new Error(String(err)));
1159
- };
1160
- const timer = setTimeout(() => fail(new Error("handshake timed out")), timeoutMs);
1161
- timer.unref?.();
1162
-
1163
- sock.on("error", fail);
1164
- sock.on("close", () => fail(new Error("peer closed the connection")));
1165
- sock.on("connect", () => {
1166
- // No proof in the hello: the caller cannot cover a challenge it has not
1167
- // been given, and a proof over an empty one would be a proof that means
1168
- // nothing. It goes in message three.
1169
- // `port` is where WE listen, which is not the port this socket came from
1170
- // — that one is ephemeral and useless to dial. Without it a deck can
1171
- // accept an incoming request and still have no way to reach back, so the
1172
- // pairing is mutual on paper and one-way in fact.
1173
- // `epk` is this connection's key pair, offered: a deck from before #1120
1174
- // reads past a field it does not know, and one of this version answers.
1175
- sendFrame(sock, {
1176
- t: "hello", fp, pub, name, port: myPort, challenge: myChallenge, ...(myEpk ? { epk: myEpk } : {}),
1177
- ...(ask === false ? { ask: false } : {}),
1178
- });
1179
- });
1180
-
1181
- let theirChallenge = null;
1182
- let theirFp = null;
1183
- let theirPub = null;
1184
- let key = null;
1185
- /** What the key was derived over; the invite proofs are made over it too. */
1186
- let transcript = null;
1187
- sock.on("data", frameReader(msg => {
1188
- if (settled) return;
1189
- // A deck that heard us and said no. Each reason is a different problem
1190
- // with a different fix, and until this frame existed they were all one
1191
- // silent close that read as a firewall. See REFUSALS.
1192
- if (msg.t === "no") {
1193
- // `Object.hasOwn`, and here more than anywhere: `msg.why` is a field in
1194
- // a frame written by the OTHER MACHINE, which is the case admin-failure
1195
- // states the rule for (#474). Every member of Object.prototype answers
1196
- // a plain bracket read with an inherited value that is neither nullish
1197
- // nor falsy, so `?? "the other deck refused this handshake"` never
1198
- // fired for one — `{...}["constructor"]` is the Object function itself,
1199
- // and `new Error(Object).message` is the string "function Object() {
1200
- // [native code] }". lan-engine files that as `lastRound.error` and the
1201
- // LAN panel prints it verbatim, so a peer chose what appeared in the
1202
- // user's interface. Asking whether the map has a ROW is the question
1203
- // this read was always trying to ask; no real reason moves.
1204
- return fail(new Error(Object.hasOwn(REFUSALS, msg.why) ? REFUSALS[msg.why]
1205
- : "the other deck refused this handshake"));
1206
- }
1207
- if (msg.t === "challenge") {
1208
- if (theirFp || !readChallenge(msg.challenge)) return fail(new Error("bad challenge"));
1209
- const them = readPub(msg.pub);
1210
- if (!them || them.fp !== msg.fp) return fail(new Error("bad challenge"));
1211
- // THE PIN, CHECKED BEFORE ANYTHING ELSE. A deck we have paired with is
1212
- // this key and no other; a key that does not match is not a peer whose
1213
- // details changed, it is a different machine at the same address.
1214
- if (expectPub && expectPub !== them.pub) {
1215
- return fail(new Error("a different deck is answering at that address"));
1216
- }
1217
- theirChallenge = msg.challenge;
1218
- theirFp = them.fp;
1219
- theirPub = them.pub;
1220
- // Mixed when both challenges say so, as at the listener, and then the
1221
- // listener's key is required: a deck of this version that says it
1222
- // mixes and sends no key is not an older deck, it is a deck whose key
1223
- // was taken out on the way. Refused, and never read the old way.
1224
- const mixing = mixesEphemeral(myChallenge) && mixesEphemeral(theirChallenge);
1225
- const theirEphemeral = mixing ? readEphemeral(msg.epk) : null;
1226
- if (mixing && !theirEphemeral) return fail(new Error("bad challenge"));
1227
- transcript = mixing
1228
- ? handshakeTranscript(fp, theirFp, myChallenge, theirChallenge, myEpk, theirEphemeral)
1229
- : handshakeTranscript(fp, theirFp, myChallenge, theirChallenge);
1230
- try {
1231
- key = sessionKey(secret, theirPub, transcript,
1232
- mixing ? { role: "caller", priv: mine.priv, peer: theirEphemeral } : null);
1233
- } catch {
1234
- return fail(new Error("bad challenge"));
1235
- } finally {
1236
- // Used or not, the private half goes now.
1237
- mine = null;
1238
- }
1239
- sendFrame(sock, {
1240
- t: "auth",
1241
- proof: proof(key, {
1242
- challenge: myChallenge, peerChallenge: theirChallenge,
1243
- fromFp: fp, toFp: theirFp, direction: "hello",
1244
- }),
1245
- // Only when joining on an invite. Sent in the same frame as the
1246
- // session proof so a deck that holds a token is paired in one round
1247
- // trip rather than being queued behind somebody else's press.
1248
- ...(code ? { invite: inviteProof(code, transcript) } : {}),
1249
- });
1250
- return;
1251
- }
1252
- // The same deck that gave us the challenge, or nothing: a reply naming a
1253
- // different fingerprint is a second party in the middle of this.
1254
- //
1255
- // AND A CHALLENGE FIRST. `theirFp` is null until one is accepted, so an
1256
- // `ok` sent as the very first frame with `fp: null` matched it — null is
1257
- // null — and walked into `proof` with no key, which throws. Before #1146
1258
- // that throw ended the deck, and this end dials whatever address a beacon
1259
- // announces; since #1146 the reader catches it as "bad frame". Neither is
1260
- // the answer: an `ok` before a challenge is out of order, and says so.
1261
- if (msg.t !== "ok" || !theirFp || !key || msg.fp !== theirFp) return fail(new Error("expected ok"));
1262
- const want = proof(key, {
1263
- challenge: theirChallenge, peerChallenge: myChallenge,
1264
- fromFp: theirFp, toFp: fp, direction: "reply",
1265
- });
1266
- if (!proofOk(want, msg.proof)) return fail(new Error("that deck could not prove its own key"));
1267
- // AND THAT IT IS THE DECK THE INVITE NAMED. The proof above is about a
1268
- // key the responder chose a moment ago; this one is about a code it had
1269
- // to have been given. `join` has no pin to pass, so `expectPub` above is
1270
- // null on this path and this is the only check that distinguishes the
1271
- // deck whose owner minted the token from whatever else is reachable at
1272
- // one of the ten addresses the token happens to carry.
1273
- //
1274
- // The caller giving up here is not the end of the attempt: `join` walks
1275
- // the rest of the list, so a deck answering at a stale or borrowed
1276
- // address costs one failed address instead of winning the whole token.
1277
- if (code && inviteProvesBack) {
1278
- const back = inviteProofBack(code, transcript);
1279
- if (!proofOk(back, msg.inviteProof)) {
1280
- return fail(new Error("that deck does not hold the invite"));
1281
- }
1282
- }
1283
- // Sealed from here when both challenges said so. The listener decided
1284
- // the same at the same moment from the same two strings, and the proof
1285
- // just checked is what says it saw the same two. See frameChannel.
1286
- const chan = sealsFrames(myChallenge) && sealsFrames(theirChallenge) ? frameChannel(key, "caller") : null;
1287
- settled = true;
1288
- clearTimeout(timer);
1289
- sock.removeAllListeners("close");
1290
- resolve({
1291
- sock, key, sealed: !!chan,
1292
- peerFp: theirFp, peerPub: theirPub, peerName: cleanName(msg.name, ""),
1293
- send: obj => sendFrame(sock, chan ? chan.wrap(obj) : obj),
1294
- /** What a frame from the other end says — or null on a sealed
1295
- * connection when it does not open, which ends the connection: every
1296
- * frame after it would fail too. On a connection to a deck from before
1297
- * #810, a plain frame passes through as it arrived. */
1298
- read: frame => (chan ? chan.unwrap(frame) : frame),
1299
- });
1300
- // A refusal from the reader, with the thrown error as its cause when there
1301
- // was one: the message is the one it always was, and whatever logs the
1302
- // rejection can reach what actually threw.
1303
- }, (why, cause) => fail(cause ? new Error(why, { cause }) : new Error(why))));
1304
- });
1305
- }