@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.
package/dist/host.d.ts ADDED
@@ -0,0 +1,214 @@
1
+ import { type KeyPair } from './channel.ts';
2
+ import { type LinkStream } from './stream.ts';
3
+ export type Role = 'control' | 'view';
4
+ /** One paired device. `key` is its static public key (base64url); `kind` is the app's own label (e.g. `browser`,
5
+ * `peer`) for per-kind caps; `expires` ends its access (absent: until removed); `meta` is the app's own. */
6
+ export type Grant = {
7
+ id: string;
8
+ key: string;
9
+ name: string;
10
+ role: Role;
11
+ created: number;
12
+ lastSeen?: number;
13
+ kind?: string;
14
+ expires?: number;
15
+ nextKey?: string;
16
+ meta?: unknown;
17
+ };
18
+ export type GrantStore = {
19
+ load(): Grant[] | Promise<Grant[]>;
20
+ save(grants: Grant[]): void | Promise<void>;
21
+ /** Notify after durable changes, including changes made by other processes. Returns an unsubscribe function.
22
+ * The host serializes reloads with its own writes; notifications need not contain grants. */
23
+ subscribe?: (changed: () => void) => () => void;
24
+ };
25
+ /** Keeps completed answers across host restarts. Store `{ op, args, reply }` as passed to `put` unchanged;
26
+ * `args` is JSON.stringify of the request arguments (or null). A missing answer after a crash may run again.
27
+ * `drop` without keys drops every answer for that device. */
28
+ export type AnswerStore = {
29
+ get(device: string, key: string): unknown | Promise<unknown>;
30
+ put(device: string, key: string, answer: object): void | Promise<void>;
31
+ drop(device: string, keys?: string[]): void | Promise<void>;
32
+ };
33
+ export type LinkRequest = {
34
+ op: string;
35
+ args?: unknown;
36
+ key?: string;
37
+ };
38
+ /** What the person at the host is asked to approve. `words` are on the device's screen too. */
39
+ export type PairRequest = {
40
+ name: string;
41
+ role: Role;
42
+ words: string;
43
+ how: 'scan' | 'code';
44
+ kind?: string;
45
+ lifetime?: number;
46
+ meta?: unknown;
47
+ };
48
+ /** Anything shaped like a browser WebSocket or a `ws` one. */
49
+ export type Socket = {
50
+ send(data: string | Uint8Array): void;
51
+ close(code?: number, reason?: string): void;
52
+ addEventListener(type: string, fn: (e: any) => void): void;
53
+ };
54
+ /** How a grant is made: its role, and optionally its kind, how long its access lasts (ms) and the app's own data. */
55
+ export type GrantTerms = {
56
+ role: Role;
57
+ kind?: string;
58
+ lifetime?: number;
59
+ meta?: unknown;
60
+ };
61
+ export type HostOptions = {
62
+ keys: KeyPair;
63
+ /** The name devices show for this computer. */
64
+ name: string;
65
+ /** Answers a device's request. Runs on the host, with the host's credentials; the device only gets the answer. */
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
+ confirm: (p: PairRequest) => boolean | Promise<boolean>;
69
+ /** Takes a stream a device opened with `link.stream(op, args)` (a terminal pane, a tunnelled connection, a call):
70
+ * set `onData` and `onEnd`, `write`, `end`. The stream opens before this handler runs; a thrown
71
+ * `PublicLinkError` ends it with a message for the device, while other errors go to `onError` and end it with
72
+ * `failed`. `device` is the authenticated grant. Without this, devices are told this host has no streams. */
73
+ stream?: (s: LinkStream, req: LinkRequest, device: Grant) => void | Promise<void>;
74
+ /** Decides every request or stream before the app handles it. When absent, control devices may do anything
75
+ * and view-only devices only what `canView` allows. */
76
+ allow?: (req: LinkRequest, device: Grant) => boolean | Promise<boolean>;
77
+ /** Which requests and streams a view-only device may open when there is no `allow`. Default: none. */
78
+ canView?: (req: LinkRequest) => boolean;
79
+ /** First authenticated socket up / last socket down per grant. Exceptions go to onError. */
80
+ onConnection?: (grant: Grant, online: boolean) => void;
81
+ /** After durable removal, expiry or authority change; never a socket drop. Called even for offline grants.
82
+ * Access/streams are already invalidated. Removal awaits returned privacy cleanup; failure never restores access. */
83
+ onGrantRemoved?: (grant: Grant, why: 'revoked' | 'expired' | 'changed') => void | Promise<void>;
84
+ /** Opt-in app data sent inside sealed ready, on pairing and reconnect. Never implicitly exposes Grant.meta. */
85
+ deviceMeta?: (grant: Grant) => unknown;
86
+ onError?: (error: unknown) => void;
87
+ grants?: GrantStore;
88
+ answers?: AnswerStore;
89
+ maxDevices?: number;
90
+ /** At most this many devices of each kind, e.g. `{ peer: 16 }`. */
91
+ caps?: Record<string, number>;
92
+ /** New handshakes allowed per minute, in all and per `peer` given to `accept`. Default 300 and 30. */
93
+ handshakes?: {
94
+ perMinute?: number;
95
+ perPeer?: number;
96
+ };
97
+ /** How long a pairing code, and a person's answer, may take. At most (and by default) five minutes. */
98
+ pairMs?: number;
99
+ /** The clock, for tests. */
100
+ now?: () => number;
101
+ };
102
+ /** What a 0.3 host understands beyond 0.1, told to devices in `ready`. */
103
+ export declare const FEATURES: string[];
104
+ export declare class Host {
105
+ readonly keys: KeyPair;
106
+ /** This host's address on a relay. */
107
+ readonly id: string;
108
+ private opts;
109
+ private grants;
110
+ private store?;
111
+ private tickets;
112
+ private codes;
113
+ private tries;
114
+ private live;
115
+ private expiryTimers;
116
+ private answered;
117
+ private answerWork;
118
+ private changes;
119
+ private unsubscribe?;
120
+ private stopped;
121
+ private recent;
122
+ private constructor();
123
+ static open(opts: HostOptions): Promise<Host>;
124
+ private now;
125
+ private get pairMs();
126
+ private ended;
127
+ private currentGrant;
128
+ private admitGrant;
129
+ private change;
130
+ private save;
131
+ private report;
132
+ private answerTask;
133
+ private drop;
134
+ private presence;
135
+ private addLive;
136
+ private removeLive;
137
+ private watchExpiry;
138
+ private endLive;
139
+ /** Every grant change goes through here, one at a time: store first, then memory, then the live sockets. */
140
+ private transition;
141
+ /** Re-read the durable authority without writing it back. Removed/changed grants close their sockets.
142
+ * A failed read leaves memory untouched and rejects; subscribed failures also reach onError. */
143
+ reload(): Promise<void>;
144
+ private pending;
145
+ /** A QR's text for one device. `urls` are where a device can reach this host (direct `ws://`, or a relay's
146
+ * `wss://…/link/v1/<id>`); `base` makes it a link a browser can open. Single use, gone after `pairMs`. */
147
+ offer(o: GrantTerms & {
148
+ urls: string[];
149
+ base?: string;
150
+ }): {
151
+ text: string;
152
+ expires: number;
153
+ };
154
+ /** A QR-sized offer: the same secret a typed code carries (same single use, life, words and approval, through
155
+ * one code entry), plus the addresses to try, packed binary. No browser-link form: links keep `offer`. */
156
+ compactOffer(o: GrantTerms & {
157
+ urls: string[];
158
+ }): {
159
+ text: string;
160
+ code: string;
161
+ expires: number;
162
+ };
163
+ /** A code a person types on the device instead of scanning. Single use, gone after `pairMs`. */
164
+ code(o: GrantTerms): {
165
+ code: string;
166
+ expires: number;
167
+ };
168
+ /** A 57-character typed offer that pins this machine's key. Use the whole code with `pairWithCode`;
169
+ * addresses come separately (e.g. relay `findHost`). Same approval, single use and expiry as `code`. */
170
+ shortCode(o: GrantTerms): {
171
+ code: string;
172
+ expires: number;
173
+ };
174
+ /** Withdraws every open QR and code. */
175
+ stopPairing(): void;
176
+ devices(): (Grant & {
177
+ online: boolean;
178
+ })[];
179
+ /** Adds a device this app already trusts some other way: for moving devices paired under an older protocol onto
180
+ * this one without pairing again. Only call it with a key that arrived over an authenticated channel. */
181
+ enrol(g: GrantTerms & {
182
+ key: Uint8Array;
183
+ name: string;
184
+ }): Promise<Grant>;
185
+ setMeta(id: string, meta: unknown): Promise<void>;
186
+ /** Removes a device: its grant goes, its open connections close, and its key is refused from now on. */
187
+ revoke(id: string, why?: 'revoked' | 'ended'): Promise<void>;
188
+ /** An event for every connected device (or those `to` picks). */
189
+ broadcast(e: unknown, to?: (g: Grant) => boolean): void;
190
+ /** One WebSocket, straight from a device. `peer` (e.g. its IP address) lets the host slow down one noisy source. */
191
+ accept(ws: Socket, o?: {
192
+ peer?: string;
193
+ }): void;
194
+ /** One WebSocket to a relay that carries many devices' connections. Each relay message is `{c, f}` (connection id,
195
+ * one frame, exactly as a device sent it or should get it) or `{c, end: code}`. The relay sees only that. */
196
+ relay(ws: Socket): void;
197
+ close(): void;
198
+ private sealed;
199
+ private out;
200
+ /** What the device hears when the app's stream code throws: a `PublicLinkError`'s message, else `failed`. */
201
+ private reason;
202
+ private take;
203
+ private wrong;
204
+ /** A sliding one-minute window, in all and per peer; checked before any key agreement is spent on a stranger. */
205
+ private admit;
206
+ private checkLifetime;
207
+ private grant;
208
+ private connection;
209
+ private openStream;
210
+ private acknowledge;
211
+ private request;
212
+ private admitRequest;
213
+ private run;
214
+ }