@byokit/pair 0.10.1 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +3 -1
- package/dist/device.d.ts +3 -0
- package/dist/device.js +9 -3
- package/dist/host.d.ts +5 -2
- package/dist/host.js +10 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.10.3 (2026-10-17)
|
|
6
|
+
|
|
7
|
+
- FIX: A pairing code that runs out while the computer's person has not answered now tells the device "That pairing code has run out" instead of "Your computer said no to this device". `confirm` gets a second argument, `{ signal }`, that aborts when the code runs out so the computer's prompt can close; a yes that comes later grants nothing.
|
|
8
|
+
|
|
9
|
+
## 0.10.2 (2026-10-17)
|
|
10
|
+
|
|
11
|
+
- FIX: a busy host that answers a ping late no longer has its live link dropped. `DeviceLink` now counts any pong up to the latest ping, a reply to one of its own waiting requests, or a frame on one of its own streams as hearing from the host, and counts silence from its first ping rather than from the connect, so a host that answers even that first ping late stays online. A host that goes silent is still dropped after two silent ping rounds (`2 × pingMs`, checked once a round). Host events and broadcasts alone still do not keep a link up: they show only that the host can talk, not that it hears the device, so only an answer to something the device sent proves the round trip.
|
|
12
|
+
|
|
5
13
|
## 0.10.1 (2026-10-16)
|
|
6
14
|
|
|
7
15
|
- FIX: the browser pairing test opens the pairing page over the test browser's own private CDP pipe once Chromium answers there, not from the launch command line. On a slow first start Chromium's network service quits after 15 s unanswered and took the launch page's request with it, so the test silently sat out its 180 s deadline; the page now loads after Chromium restarts the service, and a page Chromium cannot open fails at once with Chromium's own navigation error.
|
package/README.md
CHANGED
|
@@ -498,7 +498,9 @@ late approval and expiry never create a grant. Malicious routing can still deny
|
|
|
498
498
|
a code by forwarding a live attempt. Anyone who photographs the full code can race to pair with
|
|
499
499
|
the real host, so **both screens still show the same two words and host approval defaults to No**.
|
|
500
500
|
Only an explicit affirmative response after matching words should return `true` from `confirm`.
|
|
501
|
-
The kit has no approval UI; exceptions
|
|
501
|
+
The kit has no approval UI; exceptions decline. A confirmation still pending when the code runs out is refused as
|
|
502
|
+
`expired` (the device says the code ran out), and `confirm`'s `signal` aborts then so the prompt can close; a yes
|
|
503
|
+
that arrives later grants nothing.
|
|
502
504
|
|
|
503
505
|
For relay discovery, display the separate six-character `RelayClient.code()` lookup alongside
|
|
504
506
|
the full `host.shortCode()` pairing code. The device calls `findHost(relay, lookup)` from
|
package/dist/device.d.ts
CHANGED
|
@@ -150,6 +150,9 @@ export declare class DeviceLink {
|
|
|
150
150
|
* A tick that fires long after it was due means this device's own timers were frozen (an app in the background,
|
|
151
151
|
* a hidden tab), not that the host went quiet: no ping went out, so none came back. */
|
|
152
152
|
private ping;
|
|
153
|
+
/** Whether `m` answers something this device sent: any pong so far (a busy host answers late), a reply to a waiting
|
|
154
|
+
* request, or a frame on one of its streams. Only those prove the host hears this device; its events alone do not. */
|
|
155
|
+
private answers;
|
|
153
156
|
private sendPending;
|
|
154
157
|
private settle;
|
|
155
158
|
private again;
|
package/dist/device.js
CHANGED
|
@@ -320,7 +320,7 @@ export class DeviceLink {
|
|
|
320
320
|
let active;
|
|
321
321
|
const l = await dial(url, keyPairFrom(unb64url(secret)), { key: unb64url(this.grant.host) }, { t: 'auth', session: this.session, ack: this.ack, fresh: this.fresh }, this.o, {
|
|
322
322
|
message: (m) => { if (this.conn === active) {
|
|
323
|
-
if (
|
|
323
|
+
if (this.answers(m))
|
|
324
324
|
this.heard = Date.now();
|
|
325
325
|
this.message(m);
|
|
326
326
|
} },
|
|
@@ -396,8 +396,9 @@ export class DeviceLink {
|
|
|
396
396
|
if (this.conn !== l)
|
|
397
397
|
return;
|
|
398
398
|
const now = Date.now();
|
|
399
|
-
// Count the silence from now; a socket that is really dead still misses the pings that follow.
|
|
400
|
-
|
|
399
|
+
// Count the silence from now; a socket that is really dead still misses the pings that follow. Before the first
|
|
400
|
+
// ping nothing was asked, so a host that answers it late is not silent either.
|
|
401
|
+
if (now - due > every / 2 || !this.pingId)
|
|
401
402
|
this.heard = now;
|
|
402
403
|
else if (now - this.heard > 2 * every) {
|
|
403
404
|
l.close();
|
|
@@ -412,6 +413,11 @@ export class DeviceLink {
|
|
|
412
413
|
};
|
|
413
414
|
later(every, tick);
|
|
414
415
|
}
|
|
416
|
+
/** Whether `m` answers something this device sent: any pong so far (a busy host answers late), a reply to a waiting
|
|
417
|
+
* request, or a frame on one of its streams. Only those prove the host hears this device; its events alone do not. */
|
|
418
|
+
answers(m) {
|
|
419
|
+
return (m.t === 'pong' && m.n <= this.pingId) || (m.t === 'res' && this.pending.has(m.id)) || !!this.streams?.has(m.s);
|
|
420
|
+
}
|
|
415
421
|
sendPending(l, id, p) {
|
|
416
422
|
try {
|
|
417
423
|
l.send(p.msg);
|
package/dist/host.d.ts
CHANGED
|
@@ -64,8 +64,11 @@ export type HostOptions = {
|
|
|
64
64
|
name: string;
|
|
65
65
|
/** Answers a device's request. Runs on the host, with the host's credentials; the device only gets the answer. */
|
|
66
66
|
handle: (req: LinkRequest, device: Grant) => unknown;
|
|
67
|
-
/** Asks the person at the host whether this device may join; nothing is stored until it says yes.
|
|
68
|
-
|
|
67
|
+
/** Asks the person at the host whether this device may join; nothing is stored until it says yes. `signal` aborts
|
|
68
|
+
* when the pairing code runs out, so the app can close its prompt; a yes that comes later grants nothing. */
|
|
69
|
+
confirm: (p: PairRequest, o: {
|
|
70
|
+
signal: AbortSignal;
|
|
71
|
+
}) => boolean | Promise<boolean>;
|
|
69
72
|
/** Takes a stream a device opened with `link.stream(op, args)` (a terminal pane, a tunnelled connection, a call):
|
|
70
73
|
* set `onData` and `onEnd`, `write`, `end`. The stream opens before this handler runs; a thrown
|
|
71
74
|
* `PublicLinkError` ends it with a message for the device, while other errors go to `onError` and end it with
|
package/dist/host.js
CHANGED
|
@@ -434,14 +434,22 @@ export class Host {
|
|
|
434
434
|
clearTimeout(timer);
|
|
435
435
|
const req = { name: cleanName(name, 'Device'), role: p.role, words: hs.words, how,
|
|
436
436
|
...(p.kind === undefined ? {} : { kind: p.kind }), ...(p.lifetime ? { lifetime: p.lifetime } : {}), ...(p.meta === undefined ? {} : { meta: p.meta }) };
|
|
437
|
+
const ask = new AbortController();
|
|
438
|
+
let runOut;
|
|
437
439
|
const yes = await Promise.race([
|
|
438
|
-
Promise.resolve().then(() => this.opts.confirm(req)).catch(() => false),
|
|
439
|
-
new Promise((r) => later(this.pairMs, () => r(
|
|
440
|
+
Promise.resolve().then(() => this.opts.confirm(req, { signal: ask.signal })).catch(() => false),
|
|
441
|
+
new Promise((r) => { runOut = later(this.pairMs, () => { ask.abort(); r('expired'); }); }),
|
|
440
442
|
]);
|
|
443
|
+
clearTimeout(runOut);
|
|
441
444
|
// A device that said it kept a pending grant resumes from it after being killed while the person decided, so
|
|
442
445
|
// its yes outlives the socket; any other gone device could never use the grant, so it gets none.
|
|
443
446
|
if (gone && kept !== 1)
|
|
444
447
|
return;
|
|
448
|
+
if (yes === 'expired') {
|
|
449
|
+
if (!gone)
|
|
450
|
+
refuse('expired');
|
|
451
|
+
return;
|
|
452
|
+
} // a yes that comes later is ignored: no grant
|
|
445
453
|
if (!yes) {
|
|
446
454
|
if (!gone)
|
|
447
455
|
refuse('declined');
|
package/package.json
CHANGED