@byokit/pair 0.10.0 → 0.10.2
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 +9 -0
- package/dist/channel.js +1 -1
- package/dist/check.js +1 -1
- package/dist/device.d.ts +12 -9
- package/dist/device.js +12 -6
- package/dist/host.js +5 -5
- package/dist/index.js +8 -8
- package/dist/migrate.js +1 -1
- package/dist/node.js +1 -1
- package/dist/pairing.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.10.2 (2026-10-17)
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
## 0.10.1 (2026-10-16)
|
|
10
|
+
|
|
11
|
+
- 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.
|
|
12
|
+
- FIX: pairing and setup checks split "can't reach the computer" from "the computer is slow": a socket that never opens within the timeout reports `unreachable` ("Check the phone and computer are on the same network, and that no firewall is blocking the connection."), while an open socket with no answer reports the new `slow` code ("Your computer is slow to answer. Wait a moment and try again."). Consumers relay the codes unchanged.
|
|
13
|
+
|
|
5
14
|
## 0.10.0 (2026-10-12)
|
|
6
15
|
|
|
7
16
|
- DOCS: the "Checking reachability before pairing" section now shows both sides: setup self-checks its own advertised URLs with `check()` before printing the tick, and the phone re-checks the scanned offer before consent. The probe still spends nothing, so a checked offer pairs afterwards.
|
package/dist/channel.js
CHANGED
|
@@ -14,7 +14,7 @@ import Noise from 'noise-handshake';
|
|
|
14
14
|
import { chacha20poly1305 } from '@noble/ciphers/chacha.js';
|
|
15
15
|
import dh from 'noise-handshake/dh.js';
|
|
16
16
|
import sodium from 'sodium-universal';
|
|
17
|
-
import { CONFIRM_WORDS } from
|
|
17
|
+
import { CONFIRM_WORDS } from './confirm-words.js';
|
|
18
18
|
/** Mixed into every handshake, so a v2 of this protocol can never be confused with v1. */
|
|
19
19
|
export const PROLOGUE = 'byokit-link-v1';
|
|
20
20
|
const CHUNK = 60_000; // a Noise message is at most 65535 bytes; larger payloads go in pieces
|
package/dist/check.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// A per-URL reachability probe for onboarding: before pairing, the phone checks the offer's URLs (and the host
|
|
2
2
|
// checks its own advertised URL) without claiming or burning the one-time offer. No platform APIs: the same code
|
|
3
3
|
// runs in Node, browsers and React Native on the platform WebSocket, like `dial` in device.ts.
|
|
4
|
-
import { LINK_WORDS } from
|
|
4
|
+
import { LINK_WORDS } from './device.js';
|
|
5
5
|
/** The first frame a probe sends. The host answers with LINK_PROBE_OK and closes; it touches no ticket, code,
|
|
6
6
|
// grant or counter, so a checked offer still pairs afterwards. Relayed devices speak bare frames, so the same
|
|
7
7
|
// exchange works through a relay with no relay change. */
|
package/dist/device.d.ts
CHANGED
|
@@ -44,22 +44,22 @@ export declare const LINK_WORDS: {
|
|
|
44
44
|
readonly unreachable: "Can't reach your computer. Check the phone and computer are on the same network, and that no firewall is blocking the connection.";
|
|
45
45
|
readonly 'wrong-host': "Something other than your computer answered, so this device didn't connect.";
|
|
46
46
|
readonly 'wrong-code': "That code didn't match. Check it, or show a new one on your computer.";
|
|
47
|
-
readonly expired:
|
|
48
|
-
readonly declined:
|
|
49
|
-
readonly full:
|
|
47
|
+
readonly expired: 'That pairing code has run out. Show a new one on your computer.';
|
|
48
|
+
readonly declined: 'Your computer said no to this device.';
|
|
49
|
+
readonly full: 'Your computer has all the devices it allows. Remove one there first.';
|
|
50
50
|
readonly 'not-paired': "Your computer doesn't know this device any more. Pair it again.";
|
|
51
|
-
readonly removed:
|
|
52
|
-
readonly 'view-only':
|
|
51
|
+
readonly removed: 'This device was removed on your computer.';
|
|
52
|
+
readonly 'view-only': 'This device can watch but not make changes.';
|
|
53
53
|
readonly timeout: "Your computer didn't answer in time.";
|
|
54
|
-
readonly slow:
|
|
54
|
+
readonly slow: 'Your computer is slow to answer. Wait a moment and try again.';
|
|
55
55
|
readonly failed: "Your computer couldn't do that.";
|
|
56
|
-
readonly busy:
|
|
57
|
-
readonly stopped:
|
|
56
|
+
readonly busy: 'Your computer is still catching up. Try again in a moment.';
|
|
57
|
+
readonly stopped: 'This link is stopped. Try connecting again.';
|
|
58
58
|
readonly 'not-supported': "The app on your computer can't do this yet. Update it there.";
|
|
59
59
|
readonly ended: "This device's access has run out. Pair it again on your computer.";
|
|
60
60
|
readonly 'not-allowed': "This device isn't allowed to do that.";
|
|
61
61
|
readonly 'too-late': "That reached your computer too late, so it wasn't done.";
|
|
62
|
-
readonly unsupported:
|
|
62
|
+
readonly unsupported: 'Your computer needs an update for that.';
|
|
63
63
|
};
|
|
64
64
|
export type LinkProblem = keyof typeof LINK_WORDS;
|
|
65
65
|
/** A failure with a plain `message` and a `code` for the app. `sealed` means the host said it inside the encrypted
|
|
@@ -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
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// the host's credentials. It pairs once (scan or typed code), then keeps one socket open, reconnecting on its own, and
|
|
3
3
|
// its requests survive a reconnect: each carries a key, so a retried tap runs once. Uses only the platform's
|
|
4
4
|
// WebSocket, so the same code runs in browsers, React Native and Node.
|
|
5
|
-
import { Handshake, b64, b64url, keyPair, keyPairFrom, random, unb64url } from
|
|
6
|
-
import { cleanName, codeKey, decodeCompactOffer, parseCode, parseOffer, parseV1Offer, shortKey, COMPACT_TAG } from
|
|
7
|
-
import { Streams } from
|
|
5
|
+
import { Handshake, b64, b64url, keyPair, keyPairFrom, random, unb64url } from './channel.js';
|
|
6
|
+
import { cleanName, codeKey, decodeCompactOffer, parseCode, parseOffer, parseV1Offer, shortKey, COMPACT_TAG } from './pairing.js';
|
|
7
|
+
import { Streams } from './stream.js';
|
|
8
8
|
/** Every way the link can fail, in words a person can act on. */
|
|
9
9
|
export const LINK_WORDS = {
|
|
10
10
|
unreachable: "Can't reach your computer. Check the phone and computer are on the same network, and that no firewall is blocking the connection.",
|
|
@@ -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.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// The host: the computer that owns the credentials. It pairs devices (scanned or typed code, then a person says yes),
|
|
2
2
|
// keeps one durable grant per device, answers their requests, and removes them. It never listens on anything itself:
|
|
3
3
|
// the app hands it WebSockets (`accept`), or one relay socket that carries many devices (`relay`).
|
|
4
|
-
import { Handshake, b64, b64url, firstFrame, hostId, messageBytes, random, unb64url } from
|
|
5
|
-
import { bindCode, cleanName, codeKey, encodeCompactOffer, newCode, normalizeCode, offerText } from
|
|
6
|
-
import { PublicLinkError } from
|
|
7
|
-
import { LINK_PROBE, LINK_PROBE_OK } from
|
|
8
|
-
import { MAX_STREAMS, Streams } from
|
|
4
|
+
import { Handshake, b64, b64url, firstFrame, hostId, messageBytes, random, unb64url } from './channel.js';
|
|
5
|
+
import { bindCode, cleanName, codeKey, encodeCompactOffer, newCode, normalizeCode, offerText } from './pairing.js';
|
|
6
|
+
import { PublicLinkError } from './device.js';
|
|
7
|
+
import { LINK_PROBE, LINK_PROBE_OK } from './check.js';
|
|
8
|
+
import { MAX_STREAMS, Streams } from './stream.js';
|
|
9
9
|
const HANDSHAKE_MS = 15_000;
|
|
10
10
|
const MAX_TRIES = 5; // wrong codes before every open code is withdrawn
|
|
11
11
|
const MAX_ANSWERS = 1000;
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export { hostId, keyPair, keyPairFrom, b64url, unb64url } from
|
|
2
|
-
export { Host, } from
|
|
3
|
-
export { DeviceLink, LINK_WORDS, LinkError, PublicLinkError, pairWithCode, pairWithOffer, pendingGrant, } from
|
|
4
|
-
export { cleanName, decodeCompactOffer, decodeOffer, encodeCompactOffer, encodeOffer, normalizeCode, offerText, parseOffer, parseV1Offer, COMPACT_TAG } from
|
|
5
|
-
export { LinkStream, NDJSON_LINE_TOO_LONG, NDJSON_MAX_LINE, WINDOW, ndjson } from
|
|
6
|
-
export { check, LINK_PROBE, LINK_PROBE_OK } from
|
|
7
|
-
export { browserDeviceStore, browserDeviceStores, secureDeviceStore, secureDeviceStores } from
|
|
8
|
-
export { migrateGrant, GrantMigrationError } from
|
|
1
|
+
export { hostId, keyPair, keyPairFrom, b64url, unb64url } from './channel.js';
|
|
2
|
+
export { Host, } from './host.js';
|
|
3
|
+
export { DeviceLink, LINK_WORDS, LinkError, PublicLinkError, pairWithCode, pairWithOffer, pendingGrant, } from './device.js';
|
|
4
|
+
export { cleanName, decodeCompactOffer, decodeOffer, encodeCompactOffer, encodeOffer, normalizeCode, offerText, parseOffer, parseV1Offer, COMPACT_TAG } from './pairing.js';
|
|
5
|
+
export { LinkStream, NDJSON_LINE_TOO_LONG, NDJSON_MAX_LINE, WINDOW, ndjson } from './stream.js';
|
|
6
|
+
export { check, LINK_PROBE, LINK_PROBE_OK } from './check.js';
|
|
7
|
+
export { browserDeviceStore, browserDeviceStores, secureDeviceStore, secureDeviceStores } from './stores.js';
|
|
8
|
+
export { migrateGrant, GrantMigrationError } from './migrate.js';
|
package/dist/migrate.js
CHANGED
package/dist/node.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Node only (`@byokit/pair/node`): host key and device grant files. Kept out of the main entry for browsers and React Native.
|
|
2
2
|
import { closeSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, unwatchFile, watchFile, writeFileSync, writeSync } from 'node:fs';
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
|
-
import { b64url, keyPair, keyPairFrom, random, unb64url } from
|
|
4
|
+
import { b64url, keyPair, keyPairFrom, random, unb64url } from './channel.js';
|
|
5
5
|
/** The host's key pair, kept in `path` (0600, in a 0700 folder, written atomically). The first call makes it.
|
|
6
6
|
* A file that exists but can't be read as a key is never replaced: a new key would silently cut off every paired
|
|
7
7
|
* device, so it throws and a person decides. */
|
package/dist/pairing.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// What pairing puts in front of a person: the QR's text (or a link that carries it), and the typed code.
|
|
2
2
|
import b4a from 'b4a';
|
|
3
|
-
import { b64url, hash, random, unb64url } from
|
|
3
|
+
import { b64url, hash, random, unb64url } from './channel.js';
|
|
4
4
|
const TAG = 'byokit-link:1:';
|
|
5
5
|
const UNSAFE = /[\u0000-\u001f\u007f\u200e\u200f\u202a-\u202e\u2066-\u2069]/gu; // control and direction-flipping characters
|
|
6
6
|
/** A device or host name as a person should see it: no hidden characters, at most 60 of them. */
|
package/package.json
CHANGED