@byokit/pair 0.0.0-stage → 0.9.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.
@@ -0,0 +1,177 @@
1
+ import { type KeyPair } from './channel.ts';
2
+ import type { Role } from './host.ts';
3
+ import { type LinkStream } from './stream.ts';
4
+ /** What a device keeps (in secure storage: it holds the device's secret key). `nextSecretKey` is only there while
5
+ * a rekey is under way. */
6
+ export type DeviceGrant = {
7
+ v: 1;
8
+ secretKey: string;
9
+ nextSecretKey?: string;
10
+ pendingUntil?: number;
11
+ host: string;
12
+ hostName: string;
13
+ meta?: unknown;
14
+ urls: string[];
15
+ device: {
16
+ id: string;
17
+ name: string;
18
+ role: Role;
19
+ };
20
+ };
21
+ export type DeviceStore = {
22
+ save(g: DeviceGrant): void | Promise<void>;
23
+ clear(): void | Promise<void>;
24
+ };
25
+ export type LinkStatus = 'connecting' | 'online' | 'offline' | 'refused' | 'removed';
26
+ type WebSocketLike = {
27
+ send(data: string | Uint8Array): void;
28
+ close(code?: number, reason?: string): void;
29
+ binaryType?: string;
30
+ onopen: any;
31
+ onmessage: any;
32
+ onclose: any;
33
+ onerror: any;
34
+ };
35
+ /** `resolve` runs before each connection and returns the address to dial: e.g. open an SSH tunnel to the host and
36
+ * return `ws://127.0.0.1:<port>/…`. The grant keeps the original address. */
37
+ export type Dial = {
38
+ WebSocket?: new (url: string) => WebSocketLike;
39
+ timeoutMs?: number;
40
+ resolve?: (url: string) => string | Promise<string>;
41
+ };
42
+ /** Every way the link can fail, in words a person can act on. */
43
+ export declare const LINK_WORDS: {
44
+ readonly unreachable: "Can't reach your computer. Check it's on and connected.";
45
+ readonly 'wrong-host': "Something other than your computer answered, so this device didn't connect.";
46
+ readonly 'wrong-code': "That code didn't match. Check it, or show a new one on your computer.";
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
+ readonly 'not-paired': "Your computer doesn't know this device any more. Pair it again.";
51
+ readonly removed: "This device was removed on your computer.";
52
+ readonly 'view-only': "This device can watch but not make changes.";
53
+ readonly timeout: "Your computer didn't answer in time.";
54
+ readonly failed: "Your computer couldn't do that.";
55
+ readonly busy: "Your computer is still catching up. Try again in a moment.";
56
+ readonly stopped: "This link is stopped. Try connecting again.";
57
+ readonly 'not-supported': "The app on your computer can't do this yet. Update it there.";
58
+ readonly ended: "This device's access has run out. Pair it again on your computer.";
59
+ readonly 'not-allowed': "This device isn't allowed to do that.";
60
+ readonly 'too-late': "That reached your computer too late, so it wasn't done.";
61
+ readonly unsupported: "Your computer needs an update for that.";
62
+ };
63
+ export type LinkProblem = keyof typeof LINK_WORDS;
64
+ /** A failure with a plain `message` and a `code` for the app. `sealed` means the host said it inside the encrypted
65
+ * channel, so it is really the host speaking; anything else could come from whoever is on the network. */
66
+ export declare class LinkError extends Error {
67
+ code: LinkProblem;
68
+ sealed: boolean;
69
+ constructor(code: LinkProblem, sealed?: boolean);
70
+ }
71
+ export declare class PublicLinkError extends LinkError {
72
+ constructor(message: string);
73
+ }
74
+ type PairOptions = Dial & {
75
+ name: string;
76
+ onWords: (w: string) => void;
77
+ key?: KeyPair;
78
+ };
79
+ /** The grant a scanned code will become, to keep in secure storage *before* pairing. If the app dies while the person
80
+ * at the host decides, a `DeviceLink` made from it later connects once the host has said yes (or learns it said no),
81
+ * instead of leaving the host holding a grant for a key nobody has. Pass its key to `pairWithOffer`, or let
82
+ * `pairWithOffer`'s `onPending` hand it over. A compact offer carries no host key: it needs `host`, the key the code
83
+ * handshake authenticated (`onPending` passes it). For a version 1 offer, a different `host` is refused. */
84
+ export declare function pendingGrant(scanned: string, o: {
85
+ name: string;
86
+ key?: KeyPair;
87
+ host?: string;
88
+ }): DeviceGrant;
89
+ type OfferOptions = PairOptions & {
90
+ /** Keep this pending grant before the host can approve this device (see `pendingGrant`). A compact offer calls it
91
+ * once the host proved it holds the code, before this device's key and name reach it; a throw stops pairing there. */
92
+ onPending?: (g: DeviceGrant) => void | Promise<void>;
93
+ };
94
+ /** A scanned QR (or opened pairing link) in, a grant out, once the person at the host says yes. `onWords` gets the
95
+ * two words to show while they decide. Tries each address in the code until one answers. A compact offer pairs
96
+ * like a typed code over the packed addresses: same single use, life, words and approval; the host key it pins is
97
+ * the one the code handshake authenticated, handed to `onPending` before the host learns this device. */
98
+ export declare function pairWithOffer(scanned: string, o: OfferOptions): Promise<DeviceGrant>;
99
+ /** A typed code in, a grant out. `Host.shortCode` also pins the machine key before sharing the device identity.
100
+ * `url` is an untrusted address, e.g. from relay `findHost`; only the full code authenticates the host. */
101
+ export declare function pairWithCode(url: string, typed: string, o: PairOptions): Promise<DeviceGrant>;
102
+ export type LinkOptions = Dial & {
103
+ store?: DeviceStore;
104
+ onEvent?: (e: unknown) => void;
105
+ onStatus?: (s: LinkStatus) => void;
106
+ onError?: (e: unknown) => void;
107
+ /** How often to check a quiet connection is still there (default 20 s); it is dropped after two silent rounds. */
108
+ pingMs?: number;
109
+ /** Requests waiting at once before new ones are refused as busy (default 1000, what a host keeps per device). */
110
+ maxPending?: number;
111
+ };
112
+ export type RequestOptions = {
113
+ /** Give up waiting after this long. The host may still have done it; a retry is a new request. */
114
+ timeoutMs?: number;
115
+ /** A clock time (ms) after which the host must not start it. */
116
+ notValidAfter?: number;
117
+ };
118
+ /** The device's live link: connects, reconnects forever with backoff, resends unanswered requests after a reconnect. */
119
+ export declare class DeviceLink {
120
+ grant: DeviceGrant;
121
+ status: LinkStatus;
122
+ private o;
123
+ private conn;
124
+ private streams;
125
+ private features;
126
+ private pending;
127
+ private n;
128
+ private tries;
129
+ private stopped;
130
+ private wake;
131
+ private connecting;
132
+ private readonly session;
133
+ private fresh;
134
+ private ack;
135
+ private received;
136
+ private storing;
137
+ private waiting;
138
+ private heard;
139
+ private pingId;
140
+ constructor(grant: DeviceGrant, o?: LinkOptions);
141
+ private set;
142
+ private report;
143
+ private persist;
144
+ private receivedReply;
145
+ private connect;
146
+ private attempt;
147
+ private lost;
148
+ /** A half-open socket (the phone changed networks) looks alive forever; a ping it never answers shows it isn't.
149
+ * A tick that fires long after it was due means this device's own timers were frozen (an app in the background,
150
+ * a hidden tab), not that the host went quiet: no ping went out, so none came back. */
151
+ private ping;
152
+ private sendPending;
153
+ private settle;
154
+ private again;
155
+ /** The socket is gone: so are its streams. */
156
+ private drop;
157
+ private message;
158
+ private removed;
159
+ /** Asks the host to do `op`. Waits through reconnects (or until `timeoutMs`); resolves only with the host's answer. */
160
+ request(op: string, args?: unknown, o?: RequestOptions): Promise<unknown>;
161
+ /** Opens a duplex stream that the host's `stream` handler takes: a terminal pane, a tunnelled connection, a call.
162
+ * Only while `online`. A stream ends when its connection drops (`onEnd('unreachable')`); open it again on the next
163
+ * `online`. Host permission refusals reject; errors from the app handler end an opened stream via `onEnd`. */
164
+ stream(op: string, args?: unknown): Promise<LinkStream>;
165
+ /** Another address to try, e.g. one found on the home network. A wrong host there simply fails its handshake. */
166
+ addUrl(url: string): void;
167
+ private answer;
168
+ /** Moves this device to a fresh key. The new key is stored before the host hears of it, and the old one keeps working
169
+ * until the new one has connected, so a crash at any point leaves a key that works. */
170
+ rekey(): Promise<void>;
171
+ /** Asks the computer to forget this device, then forgets it locally once the computer confirms. */
172
+ unpair(): Promise<void>;
173
+ /** After `refused` or `offline`: try again now. */
174
+ retry(): void;
175
+ stop(): void;
176
+ }
177
+ export {};