@crosslink/sdk-browser 0.1.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/LICENSE +163 -0
- package/README.md +11 -0
- package/dist/chunk-FKNR6EFT.js +54 -0
- package/dist/chunk-RCHT4DYR.js +221 -0
- package/dist/crosslink.global.js +1123 -0
- package/dist/device-crypto-storage-NEJ3IT2Z.js +42 -0
- package/dist/index.d.ts +1491 -0
- package/dist/index.js +4520 -0
- package/dist/storage-FHFZA2HW.js +10 -0
- package/package.json +37 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1491 @@
|
|
|
1
|
+
import { ConnectionKind, CrosslinkTransport, Logger, ConnectionState, PairedAppRecord, RpcClient, ClientLink, NotificationPayload } from '@crosslink/core';
|
|
2
|
+
export { ConnectionState, LogLevel, LogRecord, Logger, MemoryLogSink, PairedAppRecord, RpcClient, consoleLogger, createLogger, noopLogger } from '@crosslink/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pluggable secure storage. Browsers use localStorage (origin-scoped);
|
|
6
|
+
* tests/embedders inject memory or native secure storage.
|
|
7
|
+
*/
|
|
8
|
+
interface SecureStorage {
|
|
9
|
+
get(key: string): string | null;
|
|
10
|
+
set(key: string, value: string): void;
|
|
11
|
+
delete(key: string): void;
|
|
12
|
+
}
|
|
13
|
+
declare class MemorySecureStorage implements SecureStorage {
|
|
14
|
+
private map;
|
|
15
|
+
get(key: string): string | null;
|
|
16
|
+
set(key: string, value: string): void;
|
|
17
|
+
delete(key: string): void;
|
|
18
|
+
}
|
|
19
|
+
declare class LocalStorageSecureStorage implements SecureStorage {
|
|
20
|
+
private readonly ls;
|
|
21
|
+
constructor(ls: Storage);
|
|
22
|
+
get(key: string): string | null;
|
|
23
|
+
set(key: string, value: string): void;
|
|
24
|
+
delete(key: string): void;
|
|
25
|
+
}
|
|
26
|
+
/** Minimal typed wrapper used by the SDK internals. */
|
|
27
|
+
declare class JsonStore<T> {
|
|
28
|
+
private readonly storage;
|
|
29
|
+
private readonly key;
|
|
30
|
+
constructor(storage: SecureStorage, key: string);
|
|
31
|
+
load(defaults: T): T;
|
|
32
|
+
save(value: T): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Structural subset implemented by both the DOM WebSocket and the `ws`
|
|
37
|
+
* package, so the same adapter runs in browsers and under Node.
|
|
38
|
+
*/
|
|
39
|
+
interface WsLike {
|
|
40
|
+
readyState: number;
|
|
41
|
+
send(data: unknown): unknown;
|
|
42
|
+
close(code?: number, reason?: string): unknown;
|
|
43
|
+
removeEventListener?(type: string, cb: (ev?: unknown) => void): unknown;
|
|
44
|
+
addEventListener(type: "open" | "close" | "error", cb: (ev?: unknown) => void): unknown;
|
|
45
|
+
addEventListener(type: "message", cb: (ev: {
|
|
46
|
+
data: unknown;
|
|
47
|
+
}) => void): unknown;
|
|
48
|
+
binaryType?: string;
|
|
49
|
+
}
|
|
50
|
+
/** Wraps any WsLike socket as a CrosslinkTransport (binary frames only). */
|
|
51
|
+
declare function wsTransport(ws: WsLike, kind: ConnectionKind): CrosslinkTransport;
|
|
52
|
+
|
|
53
|
+
interface PairingConfirmRequest {
|
|
54
|
+
sas: string;
|
|
55
|
+
hostName: string;
|
|
56
|
+
hostFp16: string;
|
|
57
|
+
grantedCaps: string[];
|
|
58
|
+
/** Host-signed device-link session; framework callbacks auto-accept these. */
|
|
59
|
+
link: boolean;
|
|
60
|
+
}
|
|
61
|
+
interface CrosslinkClientOptions {
|
|
62
|
+
storage?: SecureStorage;
|
|
63
|
+
deviceName?: string;
|
|
64
|
+
/** Human confirmation of the SAS + granted capabilities. Defaults to approve. */
|
|
65
|
+
onConfirmPairing?(req: PairingConfirmRequest): boolean | Promise<boolean>;
|
|
66
|
+
onStateChange?(state: ConnectionState, detail?: Record<string, unknown>): void;
|
|
67
|
+
requestTimeoutMs?: number;
|
|
68
|
+
/** Structured log sink. Defaults to a no-op; pass `consoleLogger()` in dev. */
|
|
69
|
+
logger?: Logger;
|
|
70
|
+
/**
|
|
71
|
+
* Shared secret for a private relay that also gates client attach. Most
|
|
72
|
+
* relays do not: knowledge of the 128-bit channel id already gates it, and
|
|
73
|
+
* a browser cannot hold a shared secret safely.
|
|
74
|
+
*/
|
|
75
|
+
relayToken?: string;
|
|
76
|
+
/**
|
|
77
|
+
* WebSocket factory, so tests and non-DOM runtimes can inject their own.
|
|
78
|
+
* Defaults to `globalThis.WebSocket`.
|
|
79
|
+
*/
|
|
80
|
+
webSocket?: (url: string) => WsLike;
|
|
81
|
+
/** `fetch` override, used for presence lookups. */
|
|
82
|
+
fetch?: typeof fetch;
|
|
83
|
+
/**
|
|
84
|
+
* Max time to wait for a WebSocket (signaling or transport) to open before
|
|
85
|
+
* giving up. A dead relay/signaling host can otherwise leave a socket in
|
|
86
|
+
* `CONNECTING` forever with no `open` or `error` event, which shows up as
|
|
87
|
+
* the client hanging on "connecting". Default 10s.
|
|
88
|
+
*/
|
|
89
|
+
dialTimeoutMs?: number;
|
|
90
|
+
/**
|
|
91
|
+
* WebRTC upgrade configuration. When set, the client will automatically
|
|
92
|
+
* try to upgrade a relayed session to a direct WebRTC DataChannel after
|
|
93
|
+
* connecting. The relay remains as a fallback if the upgrade fails.
|
|
94
|
+
*
|
|
95
|
+
* Requires a `createPeer` factory that creates an `RTCPeerConnection`
|
|
96
|
+
* with the desired ICE servers.
|
|
97
|
+
*
|
|
98
|
+
* Example with TURN for CGNAT:
|
|
99
|
+
* ```ts
|
|
100
|
+
* webrtc: {
|
|
101
|
+
* createPeer: () => new RTCPeerConnection({
|
|
102
|
+
* iceServers: [
|
|
103
|
+
* { urls: "stun:stun.l.google.com:19302" },
|
|
104
|
+
* { urls: "turn:your-turn-server.com", username: "user", credential: "pass" }
|
|
105
|
+
* ]
|
|
106
|
+
* })
|
|
107
|
+
* }
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
webrtc?: {
|
|
111
|
+
/**
|
|
112
|
+
* Factory that creates a new RTCPeerConnection for each upgrade attempt.
|
|
113
|
+
* The factory should include any ICE servers (STUN/TURN) in the config.
|
|
114
|
+
*/
|
|
115
|
+
createPeer(): unknown;
|
|
116
|
+
/** Timeout for the SDP exchange. Default 15s. */
|
|
117
|
+
timeoutMs?: number;
|
|
118
|
+
};
|
|
119
|
+
/** Connection preference: controls candidate selection. */
|
|
120
|
+
networkMode?: "auto" | "local-only" | "lan-and-relay";
|
|
121
|
+
/** Hybrid X25519 + ML-KEM-768 session exchange. Default `disabled`. */
|
|
122
|
+
hybridPq?: "disabled" | "preferred" | "required";
|
|
123
|
+
}
|
|
124
|
+
declare class CrosslinkClient {
|
|
125
|
+
private readonly options;
|
|
126
|
+
readonly log: Logger;
|
|
127
|
+
private identity;
|
|
128
|
+
private appStore;
|
|
129
|
+
private hints;
|
|
130
|
+
private link?;
|
|
131
|
+
private readonly storage;
|
|
132
|
+
private deviceCryptoStorage?;
|
|
133
|
+
private readonly stateListeners;
|
|
134
|
+
constructor(options?: CrosslinkClientOptions);
|
|
135
|
+
/**
|
|
136
|
+
* Builds a client whose identity and paired-app records are encrypted at
|
|
137
|
+
* rest with a non-extractable WebCrypto key, rather than sitting in
|
|
138
|
+
* `localStorage` in the clear. Prefer this over `new CrosslinkClient()` in
|
|
139
|
+
* browsers; the constructor stays synchronous for embedders that supply
|
|
140
|
+
* their own storage.
|
|
141
|
+
*/
|
|
142
|
+
static create(options?: Omit<CrosslinkClientOptions, "storage"> & {
|
|
143
|
+
storage?: SecureStorage;
|
|
144
|
+
allowPlaintextFallback?: boolean;
|
|
145
|
+
}): Promise<CrosslinkClient>;
|
|
146
|
+
get deviceId(): string;
|
|
147
|
+
listApps(): PairedAppRecord[];
|
|
148
|
+
forget(appId: string): void;
|
|
149
|
+
/**
|
|
150
|
+
* Runs the full pairing flow against a scanned QR / URI: resolve code via
|
|
151
|
+
* signaling, verify pinned fingerprint, verify challenge signature, confirm
|
|
152
|
+
* SAS, persist the paired-app record.
|
|
153
|
+
*
|
|
154
|
+
* Accepts either a raw `crosslink://pair?…` manifest URI or a hosted
|
|
155
|
+
* bootstrap URL (`https://…/…&pair=<manifest>`), because iOS Safari has no
|
|
156
|
+
* handler for the custom scheme — the phone's camera produces the hosted
|
|
157
|
+
* link and this call transparently unwraps it.
|
|
158
|
+
*/
|
|
159
|
+
pairFromQr(text: string, requestedCaps?: string[], codeOverride?: string): Promise<PairedAppRecord>;
|
|
160
|
+
/**
|
|
161
|
+
* Picks how to carry out the pairing exchange.
|
|
162
|
+
*
|
|
163
|
+
* Direct endpoints are tried first and in QR order: a socket straight to the
|
|
164
|
+
* host is faster, keeps the exchange off any third party, and — crucially —
|
|
165
|
+
* needs no service to be deployed anywhere. Only when every direct endpoint
|
|
166
|
+
* refuses does this fall back to a signaling service, and if there is no
|
|
167
|
+
* signaling endpoint either, the error names every route that was tried
|
|
168
|
+
* rather than blaming a missing signaling URL.
|
|
169
|
+
*/
|
|
170
|
+
private openPairingChannel;
|
|
171
|
+
/**
|
|
172
|
+
* The origin of the page this client is running in, or null off-browser.
|
|
173
|
+
*
|
|
174
|
+
* Used to decide which advertised endpoints the browser will actually permit;
|
|
175
|
+
* a Node or native client has no such restriction and gets null.
|
|
176
|
+
*/
|
|
177
|
+
private pageOrigin;
|
|
178
|
+
/** Connects to a previously paired app; returns the RPC client when online. */
|
|
179
|
+
connect(appId?: string): Promise<RpcClient>;
|
|
180
|
+
/**
|
|
181
|
+
* Attempts to upgrade a relayed session to a direct WebRTC DataChannel.
|
|
182
|
+
* Runs asynchronously — the relayed session stays active regardless.
|
|
183
|
+
*/
|
|
184
|
+
private tryWebrtcUpgrade;
|
|
185
|
+
rpc(): RpcClient;
|
|
186
|
+
/**
|
|
187
|
+
* Mints a single-use device-link continuation URI over the current
|
|
188
|
+
* connection, so this same identity can silently re-establish trust from a
|
|
189
|
+
* fresh, storage-isolated context (e.g. after "Add to Home Screen" on iOS,
|
|
190
|
+
* which does not share IndexedDB/localStorage with the Safari tab that
|
|
191
|
+
* paired). Requires an active, authorized connection.
|
|
192
|
+
*/
|
|
193
|
+
createDeviceLink(): Promise<{
|
|
194
|
+
handoffId: string;
|
|
195
|
+
uri: string;
|
|
196
|
+
expiresAt: number;
|
|
197
|
+
}>;
|
|
198
|
+
/** The live connection, exposed for adapters that upgrade the transport. */
|
|
199
|
+
get connection(): ClientLink | undefined;
|
|
200
|
+
/**
|
|
201
|
+
* Convenience for the iOS / Add-to-Home-Screen flow: accepts the long
|
|
202
|
+
* `https://…#pair=<uri>` link a phone camera produces, unwraps it, and
|
|
203
|
+
* delegates to `pairFromQr`.
|
|
204
|
+
*/
|
|
205
|
+
pairFromBootstrap(bootstrapUrl: string, requestedCaps?: string[], codeOverride?: string): Promise<PairedAppRecord>;
|
|
206
|
+
/**
|
|
207
|
+
* Explicit pairing method taking a target host URI/manifest and entered 9-digit code.
|
|
208
|
+
*/
|
|
209
|
+
pairWithCode(targetUri: string, code: string, requestedCaps?: string[]): Promise<PairedAppRecord>;
|
|
210
|
+
/** True when the identity seed is encrypted at rest. */
|
|
211
|
+
get storageEncrypted(): boolean;
|
|
212
|
+
private ws;
|
|
213
|
+
get state(): ConnectionState;
|
|
214
|
+
/**
|
|
215
|
+
* Subscribes to connection-state changes; returns an unsubscribe function.
|
|
216
|
+
*
|
|
217
|
+
* Framework bindings need this. Without it the only way to observe state is
|
|
218
|
+
* the `onStateChange` constructor option — a single callback fixed at
|
|
219
|
+
* construction, which a React provider cannot use without polling.
|
|
220
|
+
*/
|
|
221
|
+
onStateChange(listener: (state: ConnectionState, detail?: Record<string, unknown>) => void): () => void;
|
|
222
|
+
private publishState;
|
|
223
|
+
close(): void;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* An in-memory `WsLike` pair for tests.
|
|
228
|
+
*
|
|
229
|
+
* `WsLike` is the seam the browser SDK is written against, so a fake that
|
|
230
|
+
* satisfies it lets the pairing flow, transport handling and reconnect logic
|
|
231
|
+
* be exercised without a network, a server, or a DOM.
|
|
232
|
+
*
|
|
233
|
+
* Shipped in the package (rather than kept in a test file) so downstream
|
|
234
|
+
* embedders can test their own integrations against the same seam.
|
|
235
|
+
*/
|
|
236
|
+
|
|
237
|
+
interface MockSocketOptions {
|
|
238
|
+
/** Delay before the socket reports itself open. Default: next microtask. */
|
|
239
|
+
openDelayMs?: number;
|
|
240
|
+
/** Fail the connection instead of opening it. */
|
|
241
|
+
failToOpen?: boolean;
|
|
242
|
+
}
|
|
243
|
+
/** One end of a mock connection. Messages written here surface on its peer. */
|
|
244
|
+
declare class MockSocket implements WsLike {
|
|
245
|
+
readonly url: string;
|
|
246
|
+
readyState: number;
|
|
247
|
+
binaryType: string;
|
|
248
|
+
/** Everything this end has sent, in order. */
|
|
249
|
+
readonly sent: unknown[];
|
|
250
|
+
private readonly listeners;
|
|
251
|
+
private peer?;
|
|
252
|
+
constructor(url: string, options?: MockSocketOptions);
|
|
253
|
+
/** Joins two mock sockets so each one's sends arrive at the other. */
|
|
254
|
+
static pair(urlA?: string, urlB?: string): [MockSocket, MockSocket];
|
|
255
|
+
attach(peer: MockSocket): void;
|
|
256
|
+
addEventListener(type: "open" | "close" | "error", cb: (ev?: unknown) => void): void;
|
|
257
|
+
addEventListener(type: "message", cb: (ev: {
|
|
258
|
+
data: unknown;
|
|
259
|
+
}) => void): void;
|
|
260
|
+
removeEventListener(type: string, cb: (ev?: unknown) => void): void;
|
|
261
|
+
send(data: unknown): void;
|
|
262
|
+
close(code?: number, reason?: string): void;
|
|
263
|
+
/** Simulates a transport-level failure (not a clean close). */
|
|
264
|
+
fail(reason?: string): void;
|
|
265
|
+
/** Pushes a message into this end as though the peer had sent it. */
|
|
266
|
+
deliver(data: unknown): void;
|
|
267
|
+
private remoteClosed;
|
|
268
|
+
private emit;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Encrypted-at-rest storage for browser clients.
|
|
273
|
+
*
|
|
274
|
+
* A browser client's identity seed grants access to every app it has paired
|
|
275
|
+
* with. In `localStorage` that seed is a plain string: readable by any script
|
|
276
|
+
* that gets a foothold on the origin, visible in devtools, and trivially
|
|
277
|
+
* exfiltrated by an XSS payload in one line.
|
|
278
|
+
*
|
|
279
|
+
* `IndexedDbSecureStorage` encrypts every value with AES-256-GCM under a key
|
|
280
|
+
* that is generated **non-extractable** and stored as a live `CryptoKey` in
|
|
281
|
+
* IndexedDB. The browser will hand that key back to this origin's scripts to
|
|
282
|
+
* *use*, but never to read: `crypto.subtle.exportKey` on it rejects. Stolen
|
|
283
|
+
* ciphertext is therefore useless off-origin, and an attacker who can run
|
|
284
|
+
* script on the origin has to stay resident and use the key in place rather
|
|
285
|
+
* than copying the seed out.
|
|
286
|
+
*
|
|
287
|
+
* This is meaningfully stronger than `localStorage` and it is not a vault: a
|
|
288
|
+
* script running on the origin can still ask the key to decrypt. The honest
|
|
289
|
+
* summary is that it removes the copy-and-leave attack, not the code-execution
|
|
290
|
+
* one - see docs/security/threat-model.mdx.
|
|
291
|
+
*
|
|
292
|
+
* The synchronous `SecureStorage` interface the SDK uses internally is served
|
|
293
|
+
* by `hydrate()`, which decrypts everything once into memory and writes back
|
|
294
|
+
* through asynchronously.
|
|
295
|
+
*/
|
|
296
|
+
|
|
297
|
+
/** Async counterpart of {@link SecureStorage}. */
|
|
298
|
+
interface AsyncSecureStorage {
|
|
299
|
+
readonly kind: string;
|
|
300
|
+
/** True when values are encrypted at rest. */
|
|
301
|
+
readonly encrypted: boolean;
|
|
302
|
+
get(key: string): Promise<string | null>;
|
|
303
|
+
set(key: string, value: string): Promise<void>;
|
|
304
|
+
delete(key: string): Promise<void>;
|
|
305
|
+
keys(): Promise<string[]>;
|
|
306
|
+
}
|
|
307
|
+
declare class IndexedDbSecureStorage implements AsyncSecureStorage {
|
|
308
|
+
private readonly db;
|
|
309
|
+
private readonly key;
|
|
310
|
+
readonly kind = "indexeddb-aes-gcm";
|
|
311
|
+
readonly encrypted = true;
|
|
312
|
+
private constructor();
|
|
313
|
+
/**
|
|
314
|
+
* Opens the store, generating the non-extractable master key on first use.
|
|
315
|
+
* Rejects when IndexedDB or WebCrypto is unavailable (private-mode Safari,
|
|
316
|
+
* insecure origins) so the caller can choose its own fallback rather than
|
|
317
|
+
* being silently downgraded to plaintext.
|
|
318
|
+
*/
|
|
319
|
+
static open(): Promise<IndexedDbSecureStorage>;
|
|
320
|
+
get(name: string): Promise<string | null>;
|
|
321
|
+
set(name: string, value: string): Promise<void>;
|
|
322
|
+
delete(name: string): Promise<void>;
|
|
323
|
+
keys(): Promise<string[]>;
|
|
324
|
+
/** Destroys every stored value and the master key. */
|
|
325
|
+
wipe(): Promise<void>;
|
|
326
|
+
}
|
|
327
|
+
/** Adapts an existing synchronous {@link SecureStorage} to the async shape. */
|
|
328
|
+
declare class AsyncStorageAdapter implements AsyncSecureStorage {
|
|
329
|
+
private readonly inner;
|
|
330
|
+
private readonly knownKeys;
|
|
331
|
+
readonly kind: string;
|
|
332
|
+
readonly encrypted = false;
|
|
333
|
+
constructor(inner: SecureStorage, kind?: string, knownKeys?: Set<string>);
|
|
334
|
+
get(key: string): Promise<string | null>;
|
|
335
|
+
set(key: string, value: string): Promise<void>;
|
|
336
|
+
delete(key: string): Promise<void>;
|
|
337
|
+
keys(): Promise<string[]>;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* A synchronous `SecureStorage` view over an async backend.
|
|
341
|
+
*
|
|
342
|
+
* Reads are served from the in-memory cache filled by `hydrate()`. Writes
|
|
343
|
+
* update the cache immediately and are flushed to the backend in order; a
|
|
344
|
+
* failed flush is surfaced through `onWriteError` rather than swallowed, since
|
|
345
|
+
* a silently-unpersisted identity seed means the device loses every pairing on
|
|
346
|
+
* the next reload.
|
|
347
|
+
*/
|
|
348
|
+
declare class HydratedSecureStorage implements SecureStorage {
|
|
349
|
+
private readonly backend;
|
|
350
|
+
private readonly onWriteError?;
|
|
351
|
+
private readonly cache;
|
|
352
|
+
/** serializes writes so a rapid set/delete pair cannot land out of order */
|
|
353
|
+
private flushChain;
|
|
354
|
+
private pendingWrites;
|
|
355
|
+
private constructor();
|
|
356
|
+
static hydrate(backend: AsyncSecureStorage, options?: {
|
|
357
|
+
onWriteError?(err: unknown, key: string): void;
|
|
358
|
+
}): Promise<HydratedSecureStorage>;
|
|
359
|
+
get kind(): string;
|
|
360
|
+
get encrypted(): boolean;
|
|
361
|
+
get(key: string): string | null;
|
|
362
|
+
set(key: string, value: string): void;
|
|
363
|
+
delete(key: string): void;
|
|
364
|
+
/** Resolves once every queued write has reached the backend. */
|
|
365
|
+
flushed(): Promise<void>;
|
|
366
|
+
get pending(): number;
|
|
367
|
+
private enqueue;
|
|
368
|
+
}
|
|
369
|
+
interface CreateSecureStorageOptions {
|
|
370
|
+
/**
|
|
371
|
+
* Fall back to localStorage when IndexedDB/WebCrypto are unavailable.
|
|
372
|
+
* Defaults to true - a client that cannot start at all is worse than one
|
|
373
|
+
* that starts with weaker at-rest protection - but the result reports
|
|
374
|
+
* `encrypted: false` so the application can tell the user.
|
|
375
|
+
*/
|
|
376
|
+
allowPlaintextFallback?: boolean;
|
|
377
|
+
onWriteError?(err: unknown, key: string): void;
|
|
378
|
+
}
|
|
379
|
+
interface SecureStorageResult {
|
|
380
|
+
storage: SecureStorage;
|
|
381
|
+
/** Which backend was selected. */
|
|
382
|
+
kind: string;
|
|
383
|
+
/** False when the fallback was used and values are stored in the clear. */
|
|
384
|
+
encrypted: boolean;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Builds the strongest available client storage: IndexedDB + AES-GCM under a
|
|
388
|
+
* non-extractable key, falling back to localStorage, then to memory.
|
|
389
|
+
*/
|
|
390
|
+
declare function createSecureStorage(options?: CreateSecureStorageOptions): Promise<SecureStorageResult>;
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Client-side signaling peer: resolves a pairing code to a host connection
|
|
394
|
+
* and shuttles opaque pairing blobs. Never sees keys or SAS material.
|
|
395
|
+
*/
|
|
396
|
+
declare class SignalingPeer {
|
|
397
|
+
private readonly ws;
|
|
398
|
+
private queue;
|
|
399
|
+
private resolvers;
|
|
400
|
+
private failure?;
|
|
401
|
+
private failureWaiters;
|
|
402
|
+
private constructor();
|
|
403
|
+
static open(wsFactory: () => WsLike, timeoutMs?: number): Promise<SignalingPeer>;
|
|
404
|
+
/** Resolves a pairing code; returns psid, host connection id, and presence. */
|
|
405
|
+
resolve(code: string): Promise<{
|
|
406
|
+
psid: string;
|
|
407
|
+
hostConn: string;
|
|
408
|
+
app: {
|
|
409
|
+
appId: string;
|
|
410
|
+
name: string;
|
|
411
|
+
fingerprint: string;
|
|
412
|
+
pubEdB64: string;
|
|
413
|
+
pubXB64: string;
|
|
414
|
+
relay?: {
|
|
415
|
+
url: string;
|
|
416
|
+
channel: string;
|
|
417
|
+
};
|
|
418
|
+
lan?: {
|
|
419
|
+
host: string;
|
|
420
|
+
port: number;
|
|
421
|
+
};
|
|
422
|
+
};
|
|
423
|
+
}>;
|
|
424
|
+
/** Sends an opaque blob to a connected peer (host or waiter). */
|
|
425
|
+
sendTo(connId: string, blob: string): void;
|
|
426
|
+
/** Awaits the next blob delivered from `fromConnId`. */
|
|
427
|
+
nextBlob(fromConnId: string, timeoutMs?: number): Promise<string>;
|
|
428
|
+
close(): void;
|
|
429
|
+
private send;
|
|
430
|
+
private fail;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Browser-side notification handler.
|
|
435
|
+
*
|
|
436
|
+
* Subscribes to the Crosslink notification event over an established session
|
|
437
|
+
* and surfaces payloads via the Web Notifications API (or a custom handler).
|
|
438
|
+
* Falls back gracefully when Notifications are unavailable (e.g. SSR, denied
|
|
439
|
+
* permission).
|
|
440
|
+
*/
|
|
441
|
+
|
|
442
|
+
interface NotificationHandlerOptions {
|
|
443
|
+
/** If true, automatically request Notification.permission on construction. */
|
|
444
|
+
autoRequestPermission?: boolean;
|
|
445
|
+
/** Custom handler; when provided, bypasses the Web Notifications API. */
|
|
446
|
+
onNotification?(payload: NotificationPayload): void;
|
|
447
|
+
/** Called when a notification is clicked (Web Notifications click event). */
|
|
448
|
+
onClick?(payload: NotificationPayload): void;
|
|
449
|
+
}
|
|
450
|
+
declare class NotificationHandler {
|
|
451
|
+
private readonly options;
|
|
452
|
+
private unsub?;
|
|
453
|
+
private seen;
|
|
454
|
+
constructor(options?: NotificationHandlerOptions);
|
|
455
|
+
/**
|
|
456
|
+
* Begin listening for notifications over an RPC client.
|
|
457
|
+
* Returns an unsubscribe function.
|
|
458
|
+
*/
|
|
459
|
+
start(rpc: RpcClient): () => void;
|
|
460
|
+
stop(): void;
|
|
461
|
+
private deliver;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Where a pairing card gets its pairing sessions.
|
|
466
|
+
*
|
|
467
|
+
* `PairingCard` is the canonical Crosslink pairing UI and stays that. This
|
|
468
|
+
* module is the other half of the same story: the thing that asks the host for
|
|
469
|
+
* a session, so the card can drive itself instead of every application writing
|
|
470
|
+
* the same fetch-and-refresh loop beside it.
|
|
471
|
+
*
|
|
472
|
+
* The default source talks to the control surface `createCrosslinkServer`
|
|
473
|
+
* exposes, so an application that changes nothing gets the whole flow from
|
|
474
|
+
* `createPairingCard({ target })`. A host the page cannot reach
|
|
475
|
+
* over HTTP — an Electron renderer behind a preload bridge — supplies its own
|
|
476
|
+
* object instead. The source decides where a session comes from, never what
|
|
477
|
+
* the pairing screen looks like.
|
|
478
|
+
*/
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* The application the host is pairing for, as the host declares it.
|
|
482
|
+
*
|
|
483
|
+
* The desktop page does not repeat any of this: `createCrosslinkServer` is
|
|
484
|
+
* already given an `application` block, and the card renders whatever the host
|
|
485
|
+
* reports so the widget stays correct when the application is re-branded in
|
|
486
|
+
* one place.
|
|
487
|
+
*/
|
|
488
|
+
interface PairingApplication {
|
|
489
|
+
id?: string;
|
|
490
|
+
name?: string;
|
|
491
|
+
icon?: string | null;
|
|
492
|
+
accentColor?: string;
|
|
493
|
+
backgroundColor?: string;
|
|
494
|
+
textColor?: string;
|
|
495
|
+
appearance?: "light" | "dark" | "auto";
|
|
496
|
+
}
|
|
497
|
+
/** A pairing session as the host describes it. */
|
|
498
|
+
interface PairingSession {
|
|
499
|
+
/** The nine-digit code shown on screen. */
|
|
500
|
+
code: string;
|
|
501
|
+
/** Epoch ms after which the code stops working. */
|
|
502
|
+
expiresAt: number;
|
|
503
|
+
/** Pre-rendered QR, produced host-side from the bootstrap URL. */
|
|
504
|
+
qrSvg?: string | null;
|
|
505
|
+
/** URL the QR encodes, for the "open on this device" fallback. */
|
|
506
|
+
bootstrapUrl?: string | null;
|
|
507
|
+
/** Raw `crosslink://pair?…` URI, when the host advertises one. */
|
|
508
|
+
uri?: string | null;
|
|
509
|
+
/** Routes this session advertises. */
|
|
510
|
+
endpoints?: PairingCardEndpoint[] | null;
|
|
511
|
+
/** Mode the host actually applied, which may differ from the request. */
|
|
512
|
+
networkMode?: NetworkMode;
|
|
513
|
+
/** Why remote access produced no public route, when it did not. */
|
|
514
|
+
remoteNote?: string | null;
|
|
515
|
+
/** Application identity and palette, so the card needs no local copy. */
|
|
516
|
+
application?: PairingApplication | null;
|
|
517
|
+
}
|
|
518
|
+
/** How a pairing card reaches the host. */
|
|
519
|
+
interface PairingSource {
|
|
520
|
+
/** Mints (or returns) the current pairing session. */
|
|
521
|
+
getSession(mode?: NetworkMode): Promise<PairingSession>;
|
|
522
|
+
/** Applies a network-mode change before the next session is minted. */
|
|
523
|
+
setNetworkMode?(mode: NetworkMode): Promise<void>;
|
|
524
|
+
/**
|
|
525
|
+
* Notifies the card that the displayed session is stale — a device
|
|
526
|
+
* redeemed the code, or one was revoked. Returns an unsubscribe function.
|
|
527
|
+
*/
|
|
528
|
+
subscribe?(listener: (event: PairingSourceEvent) => void): () => void;
|
|
529
|
+
/** Endpoint the connected-devices dialog reads. */
|
|
530
|
+
devicesEndpoint?: string;
|
|
531
|
+
/** Endpoint the devices dialog posts revocations to. */
|
|
532
|
+
revokeEndpoint?: string;
|
|
533
|
+
}
|
|
534
|
+
type PairingSourceEvent = {
|
|
535
|
+
type: "invalidate";
|
|
536
|
+
} | {
|
|
537
|
+
type: "connected";
|
|
538
|
+
deviceId?: string;
|
|
539
|
+
} | {
|
|
540
|
+
type: "disconnected";
|
|
541
|
+
deviceId?: string;
|
|
542
|
+
};
|
|
543
|
+
/** Paths the built-in control surface serves, relative to its base path. */
|
|
544
|
+
declare const CONTROL_ROUTES: {
|
|
545
|
+
readonly pairing: "/pairing";
|
|
546
|
+
readonly networkMode: "/network-mode";
|
|
547
|
+
readonly devices: "/devices";
|
|
548
|
+
readonly revoke: "/revoke";
|
|
549
|
+
readonly events: "/events";
|
|
550
|
+
};
|
|
551
|
+
/**
|
|
552
|
+
* The default source: the HTTP control surface `createCrosslinkServer` exposes.
|
|
553
|
+
*
|
|
554
|
+
* It is bound to a base path rather than an origin because that surface is
|
|
555
|
+
* loopback-only by design — it mints pairing codes and revokes devices, so it
|
|
556
|
+
* must not be reachable from the network the QR is scanned on.
|
|
557
|
+
*/
|
|
558
|
+
declare function createHttpPairingSource(basePath?: string): PairingSource;
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Crosslink brand assets and theming primitives — the single source of truth
|
|
562
|
+
* for the Crosslink identity rendered by every Crosslink-owned screen.
|
|
563
|
+
*
|
|
564
|
+
* The mark is an SVG filled with `currentColor`, so it takes the colour of the
|
|
565
|
+
* Crosslink surface around it. That is how an application accent tints the logo
|
|
566
|
+
* without anybody shipping a recoloured copy: `resolveCrosslinkTheme` derives a
|
|
567
|
+
* logo colour from the accent and then lifts it until it clears the WCAG
|
|
568
|
+
* contrast floor for graphical objects against the surface behind it.
|
|
569
|
+
*
|
|
570
|
+
* Nothing here can remove the mark or the attribution. The helpers always
|
|
571
|
+
* return a logo and always return attribution text; only colour, size and
|
|
572
|
+
* placement are configurable.
|
|
573
|
+
*/
|
|
574
|
+
/** `viewBox` of the Crosslink wordmark. */
|
|
575
|
+
declare const CROSSLINK_LOGO_VIEWBOX = "105 363 1060 222";
|
|
576
|
+
/** Path data of the Crosslink wordmark. Always filled with `currentColor`. */
|
|
577
|
+
declare const CROSSLINK_LOGO_PATH = "M233.73 383.42C254.47 380.94 275.68 386.3 293.3 397.22C298.36 400.36 310.34 407.06 306.92 414.39C305.05 418.41 299.18 424.66 294.5 424.16C290.7 423.75 283.6 416.8 279.92 414.57C271.58 409.53 262.05 406.61 252.45 405.27C210.49 399.39 171.23 433.96 172.83 476.5C174.51 521.08 215.67 550.01 258.38 541.73C267.91 539.88 276.87 535.81 284.85 530.38C288.81 527.68 292.08 522.77 297.47 523.95C299.49 524.39 307.14 531.68 307.85 533.67C308.39 535.19 308.34 536.9 307.92 538.44C307.04 541.58 302.8 544.02 300.34 545.88C285.61 557.02 267.9 563.4 249.5 564.79C194.73 568.93 147.61 523.68 151.02 468.5C152.07 451.5 158.16 436.04 167.48 421.97C173.81 412.42 182.42 403.72 192.26 397.74C205.14 389.9 218.77 385.21 233.73 383.42ZM791.59 387.27C795.36 386.55 802.6 386.03 805.44 389.07C807.56 391.34 807.21 394.63 807.24 397.5C807.3 404.17 807.27 410.83 807.25 417.5C807.15 451.5 807.23 485.5 807.25 519.5C807.26 529.5 807.29 539.5 807.27 549.5C807.26 554.33 807.43 559.18 801.47 559.66C797.97 559.94 791 560.91 788.25 558.23C786.05 556.09 786.66 552.28 786.64 549.5C786.6 542.17 786.66 534.83 786.64 527.5C786.54 494.5 786.59 461.5 786.65 428.5C786.67 418.17 786.7 407.83 786.62 397.5C786.59 393.48 786.61 388.22 791.59 387.27ZM1047.5 490.33C1049.96 490.3 1052.92 490.63 1055.26 489.73C1058.8 488.37 1065.75 479.23 1068.61 476.12C1077.64 466.3 1086.84 456.19 1096.5 447C1100.43 443.26 1119.54 442.69 1123.5 445.8C1123.66 451.35 1115.32 457.13 1111.53 461.04C1099.51 473.43 1087.04 485.6 1075.57 498.5C1077.87 503.34 1082.73 507.17 1086.35 511.17C1095.38 521.13 1104.32 531.19 1113.52 541C1115.75 543.37 1125.96 553.61 1126.66 555.81C1127.01 556.89 1126.75 557.43 1126.8 558.5C1123.25 560.55 1118.53 559.73 1114.5 559.69C1110.76 559.66 1106.1 560.51 1102.63 558.88C1098.64 557 1095.59 552.04 1092.65 548.84C1085.62 541.2 1078.7 533.45 1071.74 525.76C1063.43 516.58 1060.8 509.28 1046.79 512.5C1044.96 522.02 1046.59 534.66 1046.57 544.5C1046.57 548.23 1047.48 553.75 1045.59 557.14C1043.49 560.88 1026.82 562.35 1025.95 554.5C1024.58 542.11 1025.96 527.16 1025.95 514.5C1025.94 486.17 1025.87 457.83 1025.93 429.5C1025.95 418.83 1025.94 408.17 1025.9 397.5C1025.89 393.57 1025.68 388.29 1030.61 387.24C1032.49 386.84 1034.59 387.09 1036.5 387.09C1038.56 387.1 1040.88 386.81 1042.81 387.66C1048.5 390.17 1046.49 406.98 1046.51 412.56C1046.56 430.21 1046.47 447.85 1046.53 465.5C1046.56 473.31 1045.12 482.95 1047.5 490.33ZM844.74 395.34C862.3 390.41 870.21 415.48 853.62 421.22C835.59 427.47 827.09 400.31 844.74 395.34ZM463.67 441.41C471.92 440.68 480.11 442.01 487.94 444.53C494.65 446.69 500.51 450.23 506.18 454.35C510.98 457.84 514.79 462.48 518.32 467.22C546.74 505.4 518.83 560.06 472.5 563.08C464.08 563.63 455.77 562.3 447.8 559.63C440.61 557.23 433.85 553.39 428.12 548.4C387.12 512.7 410.35 446.13 463.67 441.41ZM938.73 441.43C964.07 438.8 988.77 454.5 996.81 478.63C1001.03 491.31 999.84 505.34 999.82 518.5C999.8 528.5 999.67 538.5 999.8 548.5C999.85 552.1 1000.71 557.37 996.66 559.21C994.79 560.06 992.49 559.69 990.5 559.67C987.56 559.65 983.54 560.44 981.08 558.44C978.66 556.48 979.13 553.29 979.15 550.5C979.17 544.5 979.16 538.5 979.16 532.5C979.15 506.82 985.32 474.05 954.7 463.73C950.67 462.38 946.75 461.96 942.5 462.04C938.21 462.13 934.22 463.12 930.27 464.75C909.54 473.31 908.83 490.84 908.83 510.5C908.83 519.5 908.79 528.5 908.78 537.5C908.78 543.14 909.77 549.73 908.6 555.26C907.36 561.17 892.64 561.43 889.51 557.98C887.34 555.6 888.08 551.45 888.05 548.5C887.98 539.83 888.06 531.17 888.07 522.5C888.08 510.07 886.92 496.94 889.34 484.69C894.17 460.27 914.52 443.93 938.73 441.43ZM370.81 444.44C378.29 443.39 385.96 443.92 393.5 443.91C396.8 443.91 400.83 443.49 403.26 446.23C405.44 448.69 404.78 452.49 404.76 455.5C404.75 457.24 404.95 459.11 404.32 460.76C401.95 466.94 387.89 464.88 382.5 464.89C368.82 464.9 356.04 469.36 350.54 483.06C345.38 495.93 347.67 523.81 347.69 538.5C347.7 543.73 349 550.79 347.34 555.78C345.69 560.75 334.22 560.97 330.2 559.33C326.24 557.72 327 552.92 327 549.5C327 540.5 326.98 531.5 327 522.5C327.02 510.28 326.04 497.62 328.16 485.54C331.95 463.92 349.33 447.47 370.81 444.44ZM632.82 448.5C630.99 452.96 625.86 456.37 622.98 460.46C622.01 461.84 620.99 464.11 619.29 464.68C614.01 466.44 601.31 464.28 594.65 465.33C578.59 467.86 566.31 480.49 563.75 496.48C562.92 501.66 563.12 506.77 564.57 511.83C566.29 517.82 569.39 523.27 573.67 527.82C584.18 538.98 597.17 538.99 611.5 539.08C626.5 539.17 640.93 539.33 651.61 527.14C656.22 521.88 659.71 515.44 660.73 508.46C661.28 504.62 660.22 499.75 661.65 496.17C663.77 490.82 677.12 490.22 680.29 494.22C682.78 497.37 681.9 502.8 681.67 506.5C680.91 518.95 675.64 530.73 667.69 540.21C651.84 559.1 633.43 559.88 610.5 559.75C595.85 559.66 582.93 559.16 570.01 551.47C529.05 527.1 536.59 463.2 580.88 447.43C593.12 443.07 605.72 444 618.5 443.95C623.82 443.92 630.4 442.77 632.82 448.5ZM672.17 556.5C672.3 555.53 672.1 554.9 672.51 553.96C673.37 551.95 676.41 550 677.9 548.38C680.43 545.62 682.15 542.33 684.5 539.46C697.35 538.01 711.53 541.69 723.35 534.86C748.05 520.57 750.19 485.59 725.32 470.15C716.44 464.64 706.56 464.92 696.5 464.91C681.72 464.89 667.28 463.66 655.36 473.89C648.93 479.41 644.84 487.35 643.27 495.62C642.39 500.25 644.05 506.55 640.99 510.49C636.17 516.71 623.36 514.7 622.14 506.47C618.6 482.48 637.65 455.23 659.96 447.48C671.89 443.34 684.08 443.97 696.5 443.96C712.53 443.94 727.02 445.31 740.35 455.09C776.51 481.63 768.28 540.25 726.62 556.17C714.11 560.95 700.63 559.64 687.5 559.74C682.23 559.78 675.73 561.11 672.17 556.5ZM843.71 444.37C848.02 443.53 856.53 442.92 858.52 447.96C860.51 453 859.14 467.45 859.13 473.5C859.09 492.83 859.08 512.17 859.13 531.5C859.15 538.17 859.16 544.83 859.1 551.5C859.07 555.16 858.76 558.98 854.37 559.61C850.62 560.16 841.54 561.12 839.26 557.25C836.93 553.29 838.56 539.46 838.55 534.5C838.47 514.17 838.44 493.83 838.52 473.5C838.55 467.55 837.15 452.54 839.22 447.7C839.97 445.93 841.87 444.73 843.71 444.37ZM461.77 462.42C418.77 469.25 416.23 530.16 458.13 541.44C464.19 543.07 470.34 543.03 476.49 541.92C517.97 534.45 519.66 475.76 479.83 463.58C474.06 461.82 467.74 461.46 461.77 462.42Z";
|
|
578
|
+
/** Where the attribution link points. */
|
|
579
|
+
declare const CROSSLINK_REPOSITORY = "https://github.com/jacobpowaza/crosslink";
|
|
580
|
+
/** Attribution wording. Not configurable — every Crosslink screen says this. */
|
|
581
|
+
declare const CROSSLINK_ATTRIBUTION_TEXT = "Powered by";
|
|
582
|
+
declare const CROSSLINK_ATTRIBUTION_LINK_TEXT = "Crosslink";
|
|
583
|
+
/**
|
|
584
|
+
* The whole of what an application may say about how Crosslink-owned screens
|
|
585
|
+
* look. There is intentionally no slot for markup, templates or renderers: an
|
|
586
|
+
* application supplies its name, its icon and a few colours, and Crosslink
|
|
587
|
+
* renders the same pairing, bootstrap, install, offline and revoked screens it
|
|
588
|
+
* renders everywhere else.
|
|
589
|
+
*/
|
|
590
|
+
interface CrosslinkTheme {
|
|
591
|
+
/** Application name shown as the screen's title. */
|
|
592
|
+
appName?: string;
|
|
593
|
+
/** URL (or data URI) of the application icon. */
|
|
594
|
+
appIcon?: string;
|
|
595
|
+
/** Application accent, used for actions and to tint the Crosslink mark. */
|
|
596
|
+
accentColor?: string;
|
|
597
|
+
/** Page background behind Crosslink-owned screens. */
|
|
598
|
+
backgroundColor?: string;
|
|
599
|
+
/** Primary text colour. Derived from the background when omitted. */
|
|
600
|
+
textColor?: string;
|
|
601
|
+
/** Forces a palette instead of deriving one from `backgroundColor`. */
|
|
602
|
+
appearance?: "light" | "dark" | "auto";
|
|
603
|
+
}
|
|
604
|
+
interface ResolvedCrosslinkTheme {
|
|
605
|
+
appName: string;
|
|
606
|
+
appIcon: string | null;
|
|
607
|
+
accentColor: string;
|
|
608
|
+
backgroundColor: string;
|
|
609
|
+
surfaceColor: string;
|
|
610
|
+
textColor: string;
|
|
611
|
+
mutedColor: string;
|
|
612
|
+
dividerColor: string;
|
|
613
|
+
/** Colour the Crosslink mark is drawn in — accent, lifted for contrast. */
|
|
614
|
+
logoColor: string;
|
|
615
|
+
/** Colour the attribution text is drawn in. */
|
|
616
|
+
attributionColor: string;
|
|
617
|
+
appearance: "light" | "dark";
|
|
618
|
+
}
|
|
619
|
+
/** WCAG 2.1 minimum contrast for a graphical object such as the logo. */
|
|
620
|
+
declare const LOGO_MIN_CONTRAST = 3;
|
|
621
|
+
/** WCAG 2.1 minimum contrast for the attribution, which is small body text. */
|
|
622
|
+
declare const ATTRIBUTION_MIN_CONTRAST = 4.5;
|
|
623
|
+
interface Rgb {
|
|
624
|
+
r: number;
|
|
625
|
+
g: number;
|
|
626
|
+
b: number;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Parses `#rgb`, `#rrggbb`, `#rrggbbaa` and `rgb()/rgba()`.
|
|
630
|
+
*
|
|
631
|
+
* Anything else — a named colour, a gradient, a CSS variable — returns null,
|
|
632
|
+
* and callers fall back to a palette default rather than compute a contrast
|
|
633
|
+
* ratio against a colour they could not read.
|
|
634
|
+
*/
|
|
635
|
+
declare function parseColor(input: string | undefined | null): Rgb | null;
|
|
636
|
+
/** WCAG contrast ratio between two colours, 1 (identical) to 21 (max). */
|
|
637
|
+
declare function contrastRatio(a: Rgb, b: Rgb): number;
|
|
638
|
+
/**
|
|
639
|
+
* Resolves the application's branding metadata into the complete token set the
|
|
640
|
+
* Crosslink screens draw with.
|
|
641
|
+
*
|
|
642
|
+
* Every field has a Crosslink default, so an application that configures
|
|
643
|
+
* nothing still gets the standard experience; and every colour that carries
|
|
644
|
+
* brand meaning — the mark, the attribution — is contrast-checked here rather
|
|
645
|
+
* than at each call site.
|
|
646
|
+
*/
|
|
647
|
+
declare function resolveCrosslinkTheme(theme?: CrosslinkTheme): ResolvedCrosslinkTheme;
|
|
648
|
+
interface CrosslinkLogoOptions {
|
|
649
|
+
/** CSS width. A number is treated as pixels. Default `140px`. */
|
|
650
|
+
width?: string | number;
|
|
651
|
+
/** Explicit colour. Defaults to `currentColor` so the surface decides. */
|
|
652
|
+
color?: string;
|
|
653
|
+
/** Extra class on the `<svg>`. */
|
|
654
|
+
className?: string;
|
|
655
|
+
/** Extra inline styles appended after the defaults. */
|
|
656
|
+
style?: string;
|
|
657
|
+
/** Accessible name. Default "Crosslink". */
|
|
658
|
+
title?: string;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Renders the Crosslink wordmark as an SVG string.
|
|
662
|
+
*
|
|
663
|
+
* It is `role="img"` with a `<title>` rather than a decorative graphic: on the
|
|
664
|
+
* offline and revoked screens the mark is the only thing naming what is trying
|
|
665
|
+
* to reconnect, so a screen reader has to reach it.
|
|
666
|
+
*/
|
|
667
|
+
declare function crosslinkLogoSvg(options?: CrosslinkLogoOptions): string;
|
|
668
|
+
/** Builds the wordmark as a live DOM node. */
|
|
669
|
+
declare function createCrosslinkLogo(options?: CrosslinkLogoOptions): HTMLElement;
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Canonical reusable Crosslink Pairing Widget (CrosslinkConnect).
|
|
673
|
+
*
|
|
674
|
+
* Provides a standardized, brand-consistent 3-column pairing card:
|
|
675
|
+
* [ App Logo & Blurb ] | [ Scan QR ] | [ Pairing Code Pills ]
|
|
676
|
+
*
|
|
677
|
+
* The settings cog exposes the host's own `networkMode` values and nothing
|
|
678
|
+
* else. There is deliberately no tunnel-provider setup here: Crosslink needs no
|
|
679
|
+
* ngrok token, no Cloudflare account, and no port forwarding, so a UI that asks
|
|
680
|
+
* for them would be advertising configuration the framework does not require.
|
|
681
|
+
*
|
|
682
|
+
* The card also shows which routes the current QR actually advertises, because
|
|
683
|
+
* "remote" either produced a public endpoint or it did not — the widget never
|
|
684
|
+
* implies reachability the host has not confirmed.
|
|
685
|
+
*
|
|
686
|
+
* Fully customizable via options and CSS custom properties:
|
|
687
|
+
* --cl-bg, --cl-fg, --cl-muted, --cl-divider, --cl-pill, --cl-pill-text,
|
|
688
|
+
* --cl-border, --cl-radius
|
|
689
|
+
*/
|
|
690
|
+
|
|
691
|
+
interface PairingCardTheme {
|
|
692
|
+
bg?: string;
|
|
693
|
+
fg?: string;
|
|
694
|
+
muted?: string;
|
|
695
|
+
divider?: string;
|
|
696
|
+
pill?: string;
|
|
697
|
+
pillText?: string;
|
|
698
|
+
/** CSS border shorthand. Default: `1px solid #000000`. */
|
|
699
|
+
border?: string;
|
|
700
|
+
radius?: string;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* The host's network mode. These are the same four values `createCrosslinkServer`
|
|
704
|
+
* accepts, so what the user picks here can be handed straight back to the host.
|
|
705
|
+
*/
|
|
706
|
+
type NetworkMode = "auto" | "local-only" | "lan-and-relay" | "remote";
|
|
707
|
+
/** Endpoint kinds as advertised in a v2 pairing URI. */
|
|
708
|
+
interface PairingCardEndpoint {
|
|
709
|
+
kind: "lan" | "wan" | "sig" | "relay" | "tunnel";
|
|
710
|
+
url: string;
|
|
711
|
+
}
|
|
712
|
+
interface PairingCardOptions {
|
|
713
|
+
/** Target DOM container element or selector to mount into */
|
|
714
|
+
target?: HTMLElement | string;
|
|
715
|
+
/** Application or framework name (e.g. "Crosslink Notes", "Crosslink Chat") */
|
|
716
|
+
appName?: string;
|
|
717
|
+
/** Informational blurb describing security and app pairing */
|
|
718
|
+
blurb?: string;
|
|
719
|
+
/**
|
|
720
|
+
* URL or inline SVG of the *application's* icon, shown beside the app name.
|
|
721
|
+
*
|
|
722
|
+
* It never replaces the Crosslink mark: the card renders both, so a paired
|
|
723
|
+
* device sees which application it is joining and which framework is
|
|
724
|
+
* securing the connection.
|
|
725
|
+
*/
|
|
726
|
+
appIcon?: string;
|
|
727
|
+
/**
|
|
728
|
+
* Application branding — name, icon, and colours. These are the only visual
|
|
729
|
+
* knobs the card exposes; there is no renderer or markup slot, because every
|
|
730
|
+
* Crosslink application is meant to present the same pairing experience.
|
|
731
|
+
*/
|
|
732
|
+
brand?: CrosslinkTheme;
|
|
733
|
+
/** Initial pairing code (e.g. "938472910") */
|
|
734
|
+
code?: string;
|
|
735
|
+
/** Initial QR code SVG string or image URL */
|
|
736
|
+
qr?: string;
|
|
737
|
+
/** Expiration timestamp (epoch ms) or descriptive string */
|
|
738
|
+
expiresAt?: number | string;
|
|
739
|
+
/** Callback triggered when the user clicks 'Refresh code' */
|
|
740
|
+
onRefresh?: () => void | Promise<void>;
|
|
741
|
+
/** Callback triggered when connection network mode changes */
|
|
742
|
+
onNetworkModeChange?: (mode: NetworkMode) => void | Promise<void>;
|
|
743
|
+
/** Current active network mode */
|
|
744
|
+
networkMode?: NetworkMode;
|
|
745
|
+
/** Security doc hyperlink for Open LAN info knob */
|
|
746
|
+
securityGuideUrl?: string;
|
|
747
|
+
/** LAN only guide hyperlink */
|
|
748
|
+
lanGuideUrl?: string;
|
|
749
|
+
/** Remote-access guide hyperlink */
|
|
750
|
+
remoteGuideUrl?: string;
|
|
751
|
+
/** Custom theme color overrides */
|
|
752
|
+
theme?: PairingCardTheme;
|
|
753
|
+
/** Endpoint URL to fetch paired device info. Default: /api/devices */
|
|
754
|
+
devicesEndpoint?: string;
|
|
755
|
+
/** Endpoint URL to revoke device access. Default: /api/revoke */
|
|
756
|
+
revokeEndpoint?: string;
|
|
757
|
+
/**
|
|
758
|
+
* Where the self-driving card gets pairing sessions.
|
|
759
|
+
*
|
|
760
|
+
* Omit this option for the canonical Crosslink system endpoints at
|
|
761
|
+
* `/__crosslink` on the page's own origin. `true` is retained as a legacy
|
|
762
|
+
* alias for that default; a string names a different base path; an object is
|
|
763
|
+
* a custom transport for a host the page cannot reach over HTTP. With any of them the
|
|
764
|
+
* card mints its own pairing session, renders the QR and code, replaces the
|
|
765
|
+
* code before it expires, mints a fresh one when a device redeems the old
|
|
766
|
+
* one, and applies connection-mode changes — the loop every application used
|
|
767
|
+
* to write beside it.
|
|
768
|
+
*
|
|
769
|
+
* Set `false` to keep the card controlled: `update()` is then the only way
|
|
770
|
+
* state changes, which is what a host that already owns its own pairing loop
|
|
771
|
+
* wants. This explicit opt-out keeps the ordinary integration zero-config.
|
|
772
|
+
*/
|
|
773
|
+
source?: false | true | string | PairingSource;
|
|
774
|
+
/** Seconds of headroom before expiry at which a new code is minted. Default 15. */
|
|
775
|
+
refreshLeadSeconds?: number;
|
|
776
|
+
/** Called after the card mints a session. Self-driving mode only. */
|
|
777
|
+
onSession?: (session: PairingSession) => void;
|
|
778
|
+
/** Called when a paired device connects. Self-driving mode only. */
|
|
779
|
+
onDeviceConnected?: (deviceId?: string) => void;
|
|
780
|
+
/** Called after the card renders a minting failure. Self-driving mode only. */
|
|
781
|
+
onError?: (error: Error) => void;
|
|
782
|
+
/** Initial connection status line shown under the pairing code. */
|
|
783
|
+
status?: string;
|
|
784
|
+
/** Whether to inject default CSS styles into document head. Default true */
|
|
785
|
+
injectStyles?: boolean;
|
|
786
|
+
}
|
|
787
|
+
interface PairingCardState {
|
|
788
|
+
code?: string | null;
|
|
789
|
+
qr?: string | null;
|
|
790
|
+
expiresAt?: number | string | null;
|
|
791
|
+
error?: string | null;
|
|
792
|
+
/** Short stable identifier rendered in the QR slot (for example `CL-P409`). */
|
|
793
|
+
errorCode?: string | null;
|
|
794
|
+
loading?: boolean;
|
|
795
|
+
networkMode?: NetworkMode;
|
|
796
|
+
/** Routes the current QR advertises, straight from `PairingCodeInfo.endpoints`. */
|
|
797
|
+
endpoints?: PairingCardEndpoint[] | null;
|
|
798
|
+
/** Host-side note about remote access, e.g. why no `wan` endpoint exists. */
|
|
799
|
+
remoteNote?: string | null;
|
|
800
|
+
/** Connection status line, e.g. "Waiting for a device to scan". */
|
|
801
|
+
status?: string | null;
|
|
802
|
+
/** True once a paired device is connected; the status line turns positive. */
|
|
803
|
+
connected?: boolean;
|
|
804
|
+
}
|
|
805
|
+
declare function injectPairingCardStyles(): void;
|
|
806
|
+
declare class PairingCard {
|
|
807
|
+
readonly element: HTMLElement;
|
|
808
|
+
private options;
|
|
809
|
+
private logoEl;
|
|
810
|
+
private blurbEl;
|
|
811
|
+
private refreshBtn;
|
|
812
|
+
private qrWrapEl;
|
|
813
|
+
private codePillsEl;
|
|
814
|
+
private hintEl;
|
|
815
|
+
private settingsPopover;
|
|
816
|
+
private routeSummaryEl;
|
|
817
|
+
private modePendingEl;
|
|
818
|
+
private appRowEl;
|
|
819
|
+
private statusEl;
|
|
820
|
+
private brand;
|
|
821
|
+
private currentMode;
|
|
822
|
+
private expiryTimer;
|
|
823
|
+
private source;
|
|
824
|
+
private refreshTimer;
|
|
825
|
+
private unsubscribeSource;
|
|
826
|
+
private destroyed;
|
|
827
|
+
private inFlight;
|
|
828
|
+
private refreshQueued;
|
|
829
|
+
private connectedDevices;
|
|
830
|
+
constructor(options?: PairingCardOptions);
|
|
831
|
+
/**
|
|
832
|
+
* Connects the card to a session source and starts the loop.
|
|
833
|
+
*
|
|
834
|
+
* Called from the constructor when `options.source` is set. Split out so a
|
|
835
|
+
* card built before its transport exists — an Electron renderer waiting for
|
|
836
|
+
* a preload bridge, say — can start driving later without a second class.
|
|
837
|
+
*/
|
|
838
|
+
attachSource(source: true | string | PairingSource): void;
|
|
839
|
+
/**
|
|
840
|
+
* Mints a fresh pairing session and renders it.
|
|
841
|
+
*
|
|
842
|
+
* Concurrent calls collapse onto the one in flight. The expiry timer, the
|
|
843
|
+
* refresh button and a host-side invalidation can all fire inside the same
|
|
844
|
+
* second, and three codes minted back to back would invalidate two of them
|
|
845
|
+
* before anybody could finish scanning.
|
|
846
|
+
*
|
|
847
|
+
* Collapsing cannot simply drop the extra calls, though. A request that
|
|
848
|
+
* arrives mid-flight may be the one that matters — the host reporting that a
|
|
849
|
+
* device just redeemed the code being minted — so it is remembered and run
|
|
850
|
+
* once the current mint settles, leaving the card showing a live code rather
|
|
851
|
+
* than one that is already spent.
|
|
852
|
+
*/
|
|
853
|
+
refresh(): Promise<void>;
|
|
854
|
+
private mintSession;
|
|
855
|
+
/**
|
|
856
|
+
* Takes the application's identity from the host.
|
|
857
|
+
*
|
|
858
|
+
* `createCrosslinkServer` is already configured with an `application` block —
|
|
859
|
+
* name, icon, accent, background — so a page that mounts the card needs to
|
|
860
|
+
* repeat none of it. Options still win where they are given: an application
|
|
861
|
+
* that wants the pairing screen to differ from its host metadata says so
|
|
862
|
+
* explicitly, and nothing here overrides that.
|
|
863
|
+
*/
|
|
864
|
+
private adoptHostApplication;
|
|
865
|
+
private sessionStatusText;
|
|
866
|
+
private scheduleSessionRefresh;
|
|
867
|
+
private clearRefreshTimer;
|
|
868
|
+
private handleSourceEvent;
|
|
869
|
+
private normalizeMode;
|
|
870
|
+
private createSettingsPopover;
|
|
871
|
+
toggleSettings(open?: boolean): void;
|
|
872
|
+
setNetworkMode(mode: NetworkMode): void;
|
|
873
|
+
private applyNetworkMode;
|
|
874
|
+
/**
|
|
875
|
+
* Shows the popover as busy and refuses further changes until it settles.
|
|
876
|
+
*
|
|
877
|
+
* The radios keep their state rather than being re-rendered: the user has
|
|
878
|
+
* already chosen, and the question is only whether the host agrees yet.
|
|
879
|
+
*/
|
|
880
|
+
private setSettingsPending;
|
|
881
|
+
getNetworkMode(): NetworkMode;
|
|
882
|
+
mount(target: HTMLElement | string): this;
|
|
883
|
+
update(state: PairingCardState): this;
|
|
884
|
+
applyTheme(theme?: PairingCardTheme): this;
|
|
885
|
+
setBlurb(html: string): this;
|
|
886
|
+
private syncNetworkMode;
|
|
887
|
+
/**
|
|
888
|
+
* Draws the Crosslink mark. It takes no argument on purpose: the mark is the
|
|
889
|
+
* one element of this card an application cannot swap out, so there is no
|
|
890
|
+
* code path here that renders something else in its place.
|
|
891
|
+
*/
|
|
892
|
+
private renderBrandMark;
|
|
893
|
+
/** The application's own icon and name, beside the framework mark. */
|
|
894
|
+
private renderAppRow;
|
|
895
|
+
private renderStatus;
|
|
896
|
+
/**
|
|
897
|
+
* Applies the application palette to the card's CSS variables.
|
|
898
|
+
*
|
|
899
|
+
* The mark inherits `--cl-logo`. On this card that is the card's own
|
|
900
|
+
* foreground rather than the application accent: the pairing card is a
|
|
901
|
+
* Crosslink surface, and drawing the wordmark in the app's colour reads as
|
|
902
|
+
* the application's logo instead of the framework's. The application accent
|
|
903
|
+
* still drives the card's own accented elements. `resolveCrosslinkTheme`
|
|
904
|
+
* derives the foreground from the background, so it clears contrast on a
|
|
905
|
+
* light card as well as a dark one.
|
|
906
|
+
*/
|
|
907
|
+
private applyBrand;
|
|
908
|
+
/** Re-themes a mounted card; the mark and attribution are unaffected. */
|
|
909
|
+
setBrand(brand: CrosslinkTheme): this;
|
|
910
|
+
/** The palette actually in use, after contrast correction. */
|
|
911
|
+
getBrand(): ResolvedCrosslinkTheme;
|
|
912
|
+
private renderQr;
|
|
913
|
+
private renderCode;
|
|
914
|
+
private renderLoading;
|
|
915
|
+
private renderQrSkeleton;
|
|
916
|
+
private renderCodeSkeleton;
|
|
917
|
+
private renderError;
|
|
918
|
+
private openErrorModal;
|
|
919
|
+
private renderExpiry;
|
|
920
|
+
/**
|
|
921
|
+
* Shows the routes the current QR advertises.
|
|
922
|
+
*
|
|
923
|
+
* Naming them is the honest version of a connectivity indicator: if the host
|
|
924
|
+
* asked for remote access and the router said no, there is simply no `wan`
|
|
925
|
+
* route in the list, and the note says why.
|
|
926
|
+
*/
|
|
927
|
+
private renderRoutes;
|
|
928
|
+
private handleRefresh;
|
|
929
|
+
private openConnectedDevicesModal;
|
|
930
|
+
private renderConnectedDevicesModal;
|
|
931
|
+
destroy(): void;
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Maps whatever a host reports into one of the four supported modes.
|
|
935
|
+
*
|
|
936
|
+
* Older builds persisted names like `open-lan-remote` and `ngrok` in
|
|
937
|
+
* localStorage; those are folded onto their nearest current meaning so an
|
|
938
|
+
* upgraded app does not start with a broken selection.
|
|
939
|
+
*/
|
|
940
|
+
declare function normalizeNetworkMode(mode: string | undefined | null): NetworkMode;
|
|
941
|
+
/** Convenience factory for creating a pairing card */
|
|
942
|
+
declare function createPairingCard(options?: PairingCardOptions): PairingCard;
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* The Crosslink attribution badge shown on Crosslink-owned mobile screens.
|
|
946
|
+
*
|
|
947
|
+
* It renders the mark and the wording together and offers no option that
|
|
948
|
+
* removes either: an application chooses colour, size and placement, and the
|
|
949
|
+
* attribution is what is left. Colour defaults come from the shared brand
|
|
950
|
+
* resolver, so the badge lands on the right side of the WCAG contrast floor
|
|
951
|
+
* against whatever background the application configured.
|
|
952
|
+
*/
|
|
953
|
+
type PoweredByPlacement = "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right" | "inline";
|
|
954
|
+
interface PoweredByCrosslinkOptions {
|
|
955
|
+
/** Container or selector to mount into. Defaults to document.body. */
|
|
956
|
+
target?: HTMLElement | string;
|
|
957
|
+
/** Text before the Crosslink link. */
|
|
958
|
+
text?: string;
|
|
959
|
+
/** Linked product name. */
|
|
960
|
+
linkText?: string;
|
|
961
|
+
/** Optional text after the link. */
|
|
962
|
+
suffix?: string;
|
|
963
|
+
/** CSS color for the attribution. */
|
|
964
|
+
color?: string;
|
|
965
|
+
/** Any CSS font-size value, or a pixel number. */
|
|
966
|
+
size?: string | number;
|
|
967
|
+
/** Screen placement, or `inline` to participate in the target's layout. */
|
|
968
|
+
placement?: PoweredByPlacement;
|
|
969
|
+
/** Distance from the selected screen edges. */
|
|
970
|
+
offset?: string | number;
|
|
971
|
+
/** Optional background behind the attribution. */
|
|
972
|
+
background?: string;
|
|
973
|
+
/** Optional extra class for application-specific styling. */
|
|
974
|
+
className?: string;
|
|
975
|
+
/** Stacking order for fixed placements. */
|
|
976
|
+
zIndex?: number;
|
|
977
|
+
/** Application accent, used to tint the mark. */
|
|
978
|
+
accentColor?: string;
|
|
979
|
+
/** Background the badge sits on, used for the contrast correction. */
|
|
980
|
+
backgroundColor?: string;
|
|
981
|
+
/** Height of the mark. Default scales with `size`. */
|
|
982
|
+
logoWidth?: string | number;
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Small reusable attribution shown by Crosslink mobile bootstrap applications.
|
|
986
|
+
* It is also exported for apps that own their lifecycle UI directly.
|
|
987
|
+
*/
|
|
988
|
+
declare class PoweredByCrosslink {
|
|
989
|
+
readonly element: HTMLElement;
|
|
990
|
+
private options;
|
|
991
|
+
constructor(options?: PoweredByCrosslinkOptions);
|
|
992
|
+
mount(target?: HTMLElement | string): this;
|
|
993
|
+
update(options: Partial<PoweredByCrosslinkOptions>): this;
|
|
994
|
+
destroy(): void;
|
|
995
|
+
private render;
|
|
996
|
+
}
|
|
997
|
+
declare function createPoweredByCrosslink(options?: PoweredByCrosslinkOptions): PoweredByCrosslink;
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Crosslink Offline Shell — Framework-level offline/fail-state system for PWAs.
|
|
1001
|
+
*
|
|
1002
|
+
* This module provides a cached PWA shell that launches when the Crosslink host
|
|
1003
|
+
* is completely unreachable. It handles:
|
|
1004
|
+
* - Offline UI display with developer-customizable branding
|
|
1005
|
+
* - Periodic reconnection attempts with exponential backoff
|
|
1006
|
+
* - Silent cryptographic authentication when host returns
|
|
1007
|
+
* - Automatic transition to developer app on successful auth
|
|
1008
|
+
* - Proper distinction between "host offline" vs "pairing required"
|
|
1009
|
+
*/
|
|
1010
|
+
|
|
1011
|
+
interface OfflineConfig {
|
|
1012
|
+
/** Title shown in offline state. Default: "<AppName> is unavailable" or "Crosslink is inactive" */
|
|
1013
|
+
title?: string;
|
|
1014
|
+
/** Message shown in offline state. Default mentions opening desktop app */
|
|
1015
|
+
message?: string;
|
|
1016
|
+
/** App icon URL for offline screen. Defaults to PWA manifest icon */
|
|
1017
|
+
icon?: string;
|
|
1018
|
+
/** App name for offline screen. Defaults to PWA name */
|
|
1019
|
+
appName?: string;
|
|
1020
|
+
/** Theme color for offline screen */
|
|
1021
|
+
themeColor?: string;
|
|
1022
|
+
/** Background color for offline screen */
|
|
1023
|
+
bgColor?: string;
|
|
1024
|
+
/** Application accent. Tints the Crosslink mark and the retry action. */
|
|
1025
|
+
accentColor?: string;
|
|
1026
|
+
/** Primary text colour. Derived from the background when omitted. */
|
|
1027
|
+
textColor?: string;
|
|
1028
|
+
/** Forces a palette instead of deriving one from the background. */
|
|
1029
|
+
appearance?: "light" | "dark" | "auto";
|
|
1030
|
+
/** Troubleshooting guide shown after the retry action. */
|
|
1031
|
+
debuggingUrl?: string;
|
|
1032
|
+
}
|
|
1033
|
+
type OfflineConnectionState = "connecting" | "connected" | "host-offline" | "authentication-required" | "authentication-failed" | "reconnecting";
|
|
1034
|
+
interface HostReachabilityResult {
|
|
1035
|
+
reachable: boolean;
|
|
1036
|
+
hostInfo?: {
|
|
1037
|
+
relay?: {
|
|
1038
|
+
url: string;
|
|
1039
|
+
channel: string;
|
|
1040
|
+
};
|
|
1041
|
+
lan?: {
|
|
1042
|
+
host: string;
|
|
1043
|
+
port: number;
|
|
1044
|
+
};
|
|
1045
|
+
fingerprint?: string;
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
interface OfflineShellOptions {
|
|
1049
|
+
/** Crosslink client options */
|
|
1050
|
+
clientOptions?: CrosslinkClientOptions;
|
|
1051
|
+
/** Existing or custom client instance */
|
|
1052
|
+
client?: CrosslinkClient;
|
|
1053
|
+
/** Offline UI configuration */
|
|
1054
|
+
offline?: OfflineConfig;
|
|
1055
|
+
/** Called when transitioning to connected state - load the developer app */
|
|
1056
|
+
onConnected: (rpc: RpcClient, client: CrosslinkClient) => Promise<void> | void;
|
|
1057
|
+
/** Called when authentication is required (revoked, unknown device, missing key, etc.) */
|
|
1058
|
+
onAuthRequired: () => Promise<void> | void;
|
|
1059
|
+
/** Called when connection state changes */
|
|
1060
|
+
onStateChange?: (state: OfflineConnectionState, detail?: Record<string, unknown>) => void;
|
|
1061
|
+
/** Minimum time between reconnection attempts (ms). Default: 1000 */
|
|
1062
|
+
minRetryDelay?: number;
|
|
1063
|
+
/** Maximum time between reconnection attempts (ms). Default: 30000 */
|
|
1064
|
+
maxRetryDelay?: number;
|
|
1065
|
+
/** Check host reachability interval when offline (ms). Default: 10000 */
|
|
1066
|
+
reachabilityCheckInterval?: number;
|
|
1067
|
+
/** Service Worker script URL to register. Default: "/sw.js" */
|
|
1068
|
+
serviceWorkerUrl?: string;
|
|
1069
|
+
/** Auto-register service worker in browser. Default: true */
|
|
1070
|
+
autoRegisterServiceWorker?: boolean;
|
|
1071
|
+
/** Auto-mount offline UI when host-offline. Default: true */
|
|
1072
|
+
autoMountOfflineUI?: boolean;
|
|
1073
|
+
/** Custom DOM container for offline UI. Defaults to document.body */
|
|
1074
|
+
container?: HTMLElement;
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Default offline configuration values
|
|
1078
|
+
*/
|
|
1079
|
+
declare const DEFAULT_OFFLINE_CONFIG: Required<OfflineConfig>;
|
|
1080
|
+
declare class CrosslinkOfflineShell {
|
|
1081
|
+
private client;
|
|
1082
|
+
private options;
|
|
1083
|
+
private currentState;
|
|
1084
|
+
private reachabilityTimer;
|
|
1085
|
+
private reconnectTimer;
|
|
1086
|
+
private visibilityHandler;
|
|
1087
|
+
private onlineHandler;
|
|
1088
|
+
private isPageVisible;
|
|
1089
|
+
private attemptCount;
|
|
1090
|
+
private isAttempting;
|
|
1091
|
+
private offlineElement;
|
|
1092
|
+
constructor(options: OfflineShellOptions);
|
|
1093
|
+
/**
|
|
1094
|
+
* Initialize and start the offline shell. This should be called early
|
|
1095
|
+
* in the PWA lifecycle, before attempting any connection.
|
|
1096
|
+
*/
|
|
1097
|
+
start(): Promise<void>;
|
|
1098
|
+
/**
|
|
1099
|
+
* Attempt silent cryptographic authentication with the paired host.
|
|
1100
|
+
* This is the core "trusted device" flow - no user interaction required.
|
|
1101
|
+
*/
|
|
1102
|
+
private attemptSilentAuth;
|
|
1103
|
+
/**
|
|
1104
|
+
* Handle connection state changes from underlying ClientLink.
|
|
1105
|
+
*/
|
|
1106
|
+
private handleClientStateChange;
|
|
1107
|
+
/**
|
|
1108
|
+
* Handle connection errors and determine the correct state.
|
|
1109
|
+
* Critical: distinguish between "host offline" vs "authentication required / revoked".
|
|
1110
|
+
*/
|
|
1111
|
+
private handleConnectionError;
|
|
1112
|
+
/**
|
|
1113
|
+
* Show the host-offline UI and start periodic reconnection attempts.
|
|
1114
|
+
*/
|
|
1115
|
+
private showHostOffline;
|
|
1116
|
+
/**
|
|
1117
|
+
* Schedule a reconnection attempt with exponential backoff and jitter.
|
|
1118
|
+
*/
|
|
1119
|
+
private scheduleReconnect;
|
|
1120
|
+
/**
|
|
1121
|
+
* Immediately trigger a silent reconnection attempt.
|
|
1122
|
+
*/
|
|
1123
|
+
reconnectNow(): Promise<void>;
|
|
1124
|
+
/**
|
|
1125
|
+
* Start periodic reachability checks while in host-offline state.
|
|
1126
|
+
*/
|
|
1127
|
+
private startReachabilityChecks;
|
|
1128
|
+
private cancelReachabilityChecks;
|
|
1129
|
+
/**
|
|
1130
|
+
* Check if the host signaling/relay is reachable.
|
|
1131
|
+
*/
|
|
1132
|
+
checkHostReachability(appId: string): Promise<HostReachabilityResult>;
|
|
1133
|
+
private cancelTimers;
|
|
1134
|
+
private setState;
|
|
1135
|
+
private getHints;
|
|
1136
|
+
private setupVisibilityHandlers;
|
|
1137
|
+
private setupOnlineHandler;
|
|
1138
|
+
/**
|
|
1139
|
+
* Mount the offline UI into the DOM
|
|
1140
|
+
*/
|
|
1141
|
+
mountOfflineUI(): void;
|
|
1142
|
+
/**
|
|
1143
|
+
* Unmount the offline UI from the DOM
|
|
1144
|
+
*/
|
|
1145
|
+
unmountOfflineUI(): void;
|
|
1146
|
+
/**
|
|
1147
|
+
* Update the status text shown in the offline UI
|
|
1148
|
+
*/
|
|
1149
|
+
updateOfflineStatus(text: string): void;
|
|
1150
|
+
/**
|
|
1151
|
+
* Get current connection state
|
|
1152
|
+
*/
|
|
1153
|
+
getState(): OfflineConnectionState;
|
|
1154
|
+
/**
|
|
1155
|
+
* Force a reconnection attempt immediately (e.g. user taps "Retry")
|
|
1156
|
+
*/
|
|
1157
|
+
forceReconnect(): Promise<void>;
|
|
1158
|
+
/**
|
|
1159
|
+
* Clean up resources, event listeners, and timers
|
|
1160
|
+
*/
|
|
1161
|
+
destroy(): void;
|
|
1162
|
+
/**
|
|
1163
|
+
* Get the underlying CrosslinkClient
|
|
1164
|
+
*/
|
|
1165
|
+
getClient(): CrosslinkClient;
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Create the offline shell UI element.
|
|
1169
|
+
* Returns a DOM element that can be mounted into the document.
|
|
1170
|
+
*/
|
|
1171
|
+
declare function createOfflineUI(config: OfflineConfig, onRetry?: () => void): HTMLElement;
|
|
1172
|
+
/**
|
|
1173
|
+
* Update the offline UI status text
|
|
1174
|
+
*/
|
|
1175
|
+
declare function updateOfflineStatus(text: string): void;
|
|
1176
|
+
/**
|
|
1177
|
+
* Remove the offline UI
|
|
1178
|
+
*/
|
|
1179
|
+
declare function removeOfflineUI(): void;
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* What this browser origin actually permits.
|
|
1183
|
+
*
|
|
1184
|
+
* Crosslink's mobile promise — pair once, add to home screen, launch later and
|
|
1185
|
+
* see a Crosslink reconnect screen instead of the browser's error page —
|
|
1186
|
+
* depends on browser capabilities that are gated on the *origin*, not on the
|
|
1187
|
+
* framework. A page served from `http://192.168.1.110:8787` gets none of them:
|
|
1188
|
+
*
|
|
1189
|
+
* - `navigator.serviceWorker.register` rejects outside a secure context, so
|
|
1190
|
+
* there is no cached shell and an offline launch shows the browser's own
|
|
1191
|
+
* "cannot connect to server" page.
|
|
1192
|
+
* - `crypto.subtle` is undefined outside a secure context, so device keys
|
|
1193
|
+
* cannot be wrapped in a non-extractable WebCrypto key and fall back to
|
|
1194
|
+
* plaintext at rest.
|
|
1195
|
+
* - Chromium's install prompt requires https.
|
|
1196
|
+
*
|
|
1197
|
+
* And the inverse trade-off is just as real: from an https origin the browser
|
|
1198
|
+
* refuses `ws://` entirely, so the LAN shortcut that makes Crosslink fast is
|
|
1199
|
+
* unavailable to exactly the origins that can install.
|
|
1200
|
+
*
|
|
1201
|
+
* This module reports those facts rather than hiding them. Everything above is
|
|
1202
|
+
* a browser rule; no amount of framework code changes it, so the honest thing
|
|
1203
|
+
* is to name which parts of the experience a given origin can deliver.
|
|
1204
|
+
*/
|
|
1205
|
+
interface BootstrapEnvironment {
|
|
1206
|
+
/** Origin of the page, or null when not running in a browser. */
|
|
1207
|
+
origin: string | null;
|
|
1208
|
+
/** `window.isSecureContext`. The gate on nearly everything below. */
|
|
1209
|
+
secureContext: boolean;
|
|
1210
|
+
/** A service worker can be registered, so an offline shell can be cached. */
|
|
1211
|
+
serviceWorkerAvailable: boolean;
|
|
1212
|
+
/** `crypto.subtle` exists, so device keys can be non-extractable. */
|
|
1213
|
+
webCryptoAvailable: boolean;
|
|
1214
|
+
/** The page is already running as an installed app. */
|
|
1215
|
+
standalone: boolean;
|
|
1216
|
+
/** An install to the home screen will behave like an app, not a bookmark. */
|
|
1217
|
+
installable: boolean;
|
|
1218
|
+
/** Insecure `ws://` endpoints are unusable from this origin. */
|
|
1219
|
+
insecureTransportBlocked: boolean;
|
|
1220
|
+
/** One plain sentence per capability this origin cannot provide. */
|
|
1221
|
+
limitations: string[];
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Describes the current origin's capabilities.
|
|
1225
|
+
*
|
|
1226
|
+
* Safe to call anywhere: outside a browser every capability reads false and
|
|
1227
|
+
* `origin` is null, which is the correct answer for a Node or native client
|
|
1228
|
+
* rather than a reason to throw.
|
|
1229
|
+
*/
|
|
1230
|
+
declare function describeBootstrapEnvironment(): BootstrapEnvironment;
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Crosslink Mobile Bootstrap & Lifecycle State Machine
|
|
1234
|
+
*
|
|
1235
|
+
* Implements the authoritative framework-level onboarding and authorization flow:
|
|
1236
|
+
* 1. Host Offline -> Prebuilt Offline Screen (auto-reconnect with backoff)
|
|
1237
|
+
* 2. No Valid Credential -> Prebuilt Pairing Screen (9-digit code entry + host validation)
|
|
1238
|
+
* 3. First Successful Pairing -> Prebuilt Add to Home Screen Screen (nudge + Continue in Browser)
|
|
1239
|
+
* 4. Authorized -> Developer Mobile Application mounted & revealed
|
|
1240
|
+
* 5. Revocation -> Device immediately revoked, credentials cleared, returns to Pairing Screen
|
|
1241
|
+
*
|
|
1242
|
+
* The developer application is never exposed before authorization is established.
|
|
1243
|
+
*/
|
|
1244
|
+
|
|
1245
|
+
declare const INSTALL_HANDOFF_QUERY_KEY = "crosslink_install";
|
|
1246
|
+
declare const INSTALL_HANDOFF_COOKIE = "crosslink_install";
|
|
1247
|
+
declare const INSTALL_HANDOFF_CONTEXT_COOKIE = "crosslink_install_context";
|
|
1248
|
+
type MobileBootstrapState = "initializing" | "offline" | "pairing-required" | "pairing" | "add-to-home-screen" | "authorized";
|
|
1249
|
+
interface OnboardingConfig {
|
|
1250
|
+
appName?: string;
|
|
1251
|
+
icon?: string;
|
|
1252
|
+
themeColor?: string;
|
|
1253
|
+
instructions?: string;
|
|
1254
|
+
}
|
|
1255
|
+
interface MobileBootstrapOptions {
|
|
1256
|
+
/** Target DOM container for prebuilt screens. Defaults to document.body */
|
|
1257
|
+
container?: HTMLElement;
|
|
1258
|
+
/** Application metadata */
|
|
1259
|
+
appId?: string;
|
|
1260
|
+
appName?: string;
|
|
1261
|
+
capabilities?: string[];
|
|
1262
|
+
/** Client options or existing client instance */
|
|
1263
|
+
client?: CrosslinkClient;
|
|
1264
|
+
clientOptions?: CrosslinkClientOptions;
|
|
1265
|
+
/** Offline screen branding */
|
|
1266
|
+
offline?: OfflineConfig;
|
|
1267
|
+
/** Add to home screen onboarding branding */
|
|
1268
|
+
onboarding?: OnboardingConfig;
|
|
1269
|
+
/** Called ONLY when the device is authorized and connected */
|
|
1270
|
+
onAuthorized: (rpc: RpcClient, client: CrosslinkClient) => Promise<void> | void;
|
|
1271
|
+
/** Called when authorization is lost (revoked, offline, reset) to hide/unmount developer app */
|
|
1272
|
+
onUnauthorized?: () => Promise<void> | void;
|
|
1273
|
+
/** Called on state machine transitions */
|
|
1274
|
+
onStateChange?: (state: MobileBootstrapState, detail?: Record<string, unknown>) => void;
|
|
1275
|
+
/** Service worker options */
|
|
1276
|
+
autoRegisterServiceWorker?: boolean;
|
|
1277
|
+
serviceWorkerUrl?: string;
|
|
1278
|
+
/** Pairing URI override; otherwise read from `#pair=` / `?pair=` or storage. */
|
|
1279
|
+
pairingUri?: string;
|
|
1280
|
+
/** Styling and placement for the mandatory Crosslink attribution. */
|
|
1281
|
+
poweredBy?: PoweredByCrosslinkOptions;
|
|
1282
|
+
/**
|
|
1283
|
+
* Called once with what this origin can actually deliver.
|
|
1284
|
+
*
|
|
1285
|
+
* Lets a host surface the same truth the bootstrap logs — for example that an
|
|
1286
|
+
* http LAN origin cannot cache the offline screen — rather than leaving it
|
|
1287
|
+
* to be found on a phone.
|
|
1288
|
+
*/
|
|
1289
|
+
onEnvironment?: (environment: BootstrapEnvironment) => void;
|
|
1290
|
+
}
|
|
1291
|
+
/** Check if running as a standalone installed PWA */
|
|
1292
|
+
declare function isStandalone(): boolean;
|
|
1293
|
+
/** Wipe all credentials, pairing tokens, and caches on the phone */
|
|
1294
|
+
declare function resetDeviceStorage(): Promise<void>;
|
|
1295
|
+
declare function injectBootstrapStyles(): void;
|
|
1296
|
+
declare class CrosslinkMobileBootstrap {
|
|
1297
|
+
private client;
|
|
1298
|
+
private options;
|
|
1299
|
+
private state;
|
|
1300
|
+
private currentScreenElement;
|
|
1301
|
+
private reconnectTimer;
|
|
1302
|
+
private reachabilityTimer;
|
|
1303
|
+
private visibilityHandler;
|
|
1304
|
+
private onlineHandler;
|
|
1305
|
+
private isAttempting;
|
|
1306
|
+
private attemptCount;
|
|
1307
|
+
private activeRpc;
|
|
1308
|
+
private targetPairingUri;
|
|
1309
|
+
private poweredBy;
|
|
1310
|
+
private environment;
|
|
1311
|
+
constructor(options: MobileBootstrapOptions);
|
|
1312
|
+
/**
|
|
1313
|
+
* Start the authoritative bootstrap state machine.
|
|
1314
|
+
* This is the single entry point controlling what the mobile device sees.
|
|
1315
|
+
*/
|
|
1316
|
+
start(): Promise<void>;
|
|
1317
|
+
private extractPairingUriFromLocation;
|
|
1318
|
+
private discoverInstallHandoff;
|
|
1319
|
+
private clearInstallState;
|
|
1320
|
+
private getEffectiveAppId;
|
|
1321
|
+
private completedOnboardingApps;
|
|
1322
|
+
private isOnboardingCompleted;
|
|
1323
|
+
private markOnboardingCompleted;
|
|
1324
|
+
private clearOnboarding;
|
|
1325
|
+
/** Prepares the cookie-first Safari -> standalone install boundary. */
|
|
1326
|
+
private prepareDeviceLinkHandoff;
|
|
1327
|
+
/**
|
|
1328
|
+
* Silent cryptographic authentication with stored credentials.
|
|
1329
|
+
*/
|
|
1330
|
+
private attemptSilentAuth;
|
|
1331
|
+
private handleAuthError;
|
|
1332
|
+
private handleClientStateChange;
|
|
1333
|
+
/**
|
|
1334
|
+
* Transition state machine to a new state and mount appropriate UI.
|
|
1335
|
+
*/
|
|
1336
|
+
private transitionTo;
|
|
1337
|
+
private unmountCurrentScreen;
|
|
1338
|
+
private ensurePoweredBy;
|
|
1339
|
+
/**
|
|
1340
|
+
* Handle user submitting pairing code on Pairing Screen (Screen A).
|
|
1341
|
+
*/
|
|
1342
|
+
private handlePairingSubmit;
|
|
1343
|
+
/**
|
|
1344
|
+
* Create prebuilt Pairing Screen UI (Screen A).
|
|
1345
|
+
*/
|
|
1346
|
+
private createPairingScreen;
|
|
1347
|
+
/**
|
|
1348
|
+
* Create prebuilt Add to Home Screen UI (Screen B).
|
|
1349
|
+
*/
|
|
1350
|
+
private createAddToHomeScreen;
|
|
1351
|
+
/**
|
|
1352
|
+
* Prebuilt SAS Confirmation Modal.
|
|
1353
|
+
*/
|
|
1354
|
+
private showSasConfirmation;
|
|
1355
|
+
private scheduleReconnect;
|
|
1356
|
+
forceReconnect(): Promise<void>;
|
|
1357
|
+
private cancelTimers;
|
|
1358
|
+
private setupListeners;
|
|
1359
|
+
/** What this origin permits, once `start()` has probed it. */
|
|
1360
|
+
getEnvironment(): BootstrapEnvironment | null;
|
|
1361
|
+
getState(): MobileBootstrapState;
|
|
1362
|
+
getClient(): CrosslinkClient;
|
|
1363
|
+
destroy(): void;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/**
|
|
1367
|
+
* Crosslink Service Worker Template & Generator
|
|
1368
|
+
*
|
|
1369
|
+
* Implements the framework-level offline shell caching strategy:
|
|
1370
|
+
* - Caches the minimum bootstrap shell (HTML, JS, CSS, manifest, icons)
|
|
1371
|
+
* - Network-first for navigation requests with fallback to cached shell on offline/timeout
|
|
1372
|
+
* - Cache-first with network fallback for static shell assets
|
|
1373
|
+
* - Strictly excludes API, RPC, WebSocket, and pairing/auth endpoints from caching
|
|
1374
|
+
* - Versioned cache namespaces with automatic cleanup of old caches
|
|
1375
|
+
*/
|
|
1376
|
+
interface ServiceWorkerConfig {
|
|
1377
|
+
/** Cache version - increment to invalidate old caches */
|
|
1378
|
+
version: string;
|
|
1379
|
+
/** Assets to precache - the minimum shell for offline launch */
|
|
1380
|
+
precacheAssets: string[];
|
|
1381
|
+
/** Maximum navigation wait before showing the cached reconnect shell. */
|
|
1382
|
+
navigationTimeoutMs?: number;
|
|
1383
|
+
}
|
|
1384
|
+
declare const DEFAULT_SERVICE_WORKER_CONFIG: ServiceWorkerConfig;
|
|
1385
|
+
/**
|
|
1386
|
+
* Generate the standalone Service Worker JavaScript code.
|
|
1387
|
+
*/
|
|
1388
|
+
declare function generateServiceWorker(config?: Partial<ServiceWorkerConfig>): string;
|
|
1389
|
+
/**
|
|
1390
|
+
* Default generated Service Worker script
|
|
1391
|
+
*/
|
|
1392
|
+
declare const DEFAULT_SERVICE_WORKER: string;
|
|
1393
|
+
/**
|
|
1394
|
+
* Create a Service Worker config from PWA config
|
|
1395
|
+
*/
|
|
1396
|
+
declare function createServiceWorkerConfig(pwaConfig: {
|
|
1397
|
+
version?: string;
|
|
1398
|
+
icons?: Array<{
|
|
1399
|
+
src: string;
|
|
1400
|
+
sizes?: string;
|
|
1401
|
+
}>;
|
|
1402
|
+
startUrl?: string;
|
|
1403
|
+
manifestPath?: string;
|
|
1404
|
+
}): ServiceWorkerConfig;
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* The two ways a phone can carry out the pairing exchange with a host.
|
|
1408
|
+
*
|
|
1409
|
+
* The cryptographic protocol is identical either way — claim, challenge, SAS,
|
|
1410
|
+
* complete — so the client speaks to a `PairingChannel` and does not care which
|
|
1411
|
+
* one it got. What differs is only who moves the frames:
|
|
1412
|
+
*
|
|
1413
|
+
* - `DirectPairingChannel` opens a WebSocket straight to the host, using an
|
|
1414
|
+
* endpoint from the QR (`lan~ws://…` on the same Wi-Fi, `wan~ws://…` through
|
|
1415
|
+
* a router port mapping). No service in the middle, nothing to deploy, and
|
|
1416
|
+
* nothing to pay for — this is the zero-infrastructure path.
|
|
1417
|
+
* - `BrokeredPairingChannel` relays frames through a signaling service, for
|
|
1418
|
+
* hosts that are only reachable that way.
|
|
1419
|
+
*
|
|
1420
|
+
* Direct is tried first; brokered is the fallback.
|
|
1421
|
+
*/
|
|
1422
|
+
|
|
1423
|
+
interface PairingHostInfo {
|
|
1424
|
+
appId: string;
|
|
1425
|
+
name: string;
|
|
1426
|
+
fingerprint: string;
|
|
1427
|
+
pubEdB64: string;
|
|
1428
|
+
pubXB64: string;
|
|
1429
|
+
relay?: {
|
|
1430
|
+
url: string;
|
|
1431
|
+
channel: string;
|
|
1432
|
+
};
|
|
1433
|
+
lan?: {
|
|
1434
|
+
host: string;
|
|
1435
|
+
port: number;
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
interface ResolvedPairingSession {
|
|
1439
|
+
psid: string;
|
|
1440
|
+
app: PairingHostInfo;
|
|
1441
|
+
}
|
|
1442
|
+
interface PairingChannel {
|
|
1443
|
+
readonly kind: "direct" | "brokered";
|
|
1444
|
+
/** Turns the scanned 9-digit code into a session id plus host presence. */
|
|
1445
|
+
resolve(code: string): Promise<ResolvedPairingSession>;
|
|
1446
|
+
send(frame: object): void;
|
|
1447
|
+
/** Next frame from the host. Rejects if the channel fails first. */
|
|
1448
|
+
next(): Promise<Record<string, unknown>>;
|
|
1449
|
+
close(): void;
|
|
1450
|
+
}
|
|
1451
|
+
/** Pairing over a WebSocket opened straight to the host. */
|
|
1452
|
+
declare class DirectPairingChannel implements PairingChannel {
|
|
1453
|
+
private readonly ws;
|
|
1454
|
+
readonly kind: "direct";
|
|
1455
|
+
private queue;
|
|
1456
|
+
private waiters;
|
|
1457
|
+
private failure?;
|
|
1458
|
+
private constructor();
|
|
1459
|
+
static open(url: string, wsFactory: (url: string) => WsLike, timeoutMs: number): Promise<DirectPairingChannel>;
|
|
1460
|
+
resolve(code: string): Promise<ResolvedPairingSession>;
|
|
1461
|
+
send(frame: object): void;
|
|
1462
|
+
next(): Promise<Record<string, unknown>>;
|
|
1463
|
+
close(): void;
|
|
1464
|
+
private deliver;
|
|
1465
|
+
private fail;
|
|
1466
|
+
}
|
|
1467
|
+
/** Pairing relayed through a signaling service. */
|
|
1468
|
+
declare class BrokeredPairingChannel implements PairingChannel {
|
|
1469
|
+
private readonly peer;
|
|
1470
|
+
readonly kind: "brokered";
|
|
1471
|
+
private hostConn;
|
|
1472
|
+
constructor(peer: SignalingPeer);
|
|
1473
|
+
resolve(code: string): Promise<ResolvedPairingSession>;
|
|
1474
|
+
send(frame: object): void;
|
|
1475
|
+
next(): Promise<Record<string, unknown>>;
|
|
1476
|
+
close(): void;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
declare function createCrosslinkClient(options?: Omit<CrosslinkClientOptions, "storage"> & {
|
|
1480
|
+
storage?: SecureStorage;
|
|
1481
|
+
}): CrosslinkClient;
|
|
1482
|
+
/**
|
|
1483
|
+
* Preferred browser entry point: identity and paired-app records are encrypted
|
|
1484
|
+
* at rest under a non-extractable WebCrypto key. Use `createCrosslinkClient`
|
|
1485
|
+
* only when you are supplying your own storage or need a synchronous factory.
|
|
1486
|
+
*/
|
|
1487
|
+
declare function createSecureCrosslinkClient(options?: Omit<CrosslinkClientOptions, "storage"> & {
|
|
1488
|
+
allowPlaintextFallback?: boolean;
|
|
1489
|
+
}): Promise<CrosslinkClient>;
|
|
1490
|
+
|
|
1491
|
+
export { ATTRIBUTION_MIN_CONTRAST, type AsyncSecureStorage, AsyncStorageAdapter, type BootstrapEnvironment, BrokeredPairingChannel, CONTROL_ROUTES, CROSSLINK_ATTRIBUTION_LINK_TEXT, CROSSLINK_ATTRIBUTION_TEXT, CROSSLINK_LOGO_PATH, CROSSLINK_LOGO_VIEWBOX, CROSSLINK_REPOSITORY, type CreateSecureStorageOptions, CrosslinkClient, type CrosslinkClientOptions, type CrosslinkLogoOptions, CrosslinkMobileBootstrap, CrosslinkOfflineShell, type CrosslinkTheme, DEFAULT_OFFLINE_CONFIG, DEFAULT_SERVICE_WORKER, DEFAULT_SERVICE_WORKER_CONFIG, DirectPairingChannel, type HostReachabilityResult, HydratedSecureStorage, INSTALL_HANDOFF_CONTEXT_COOKIE, INSTALL_HANDOFF_COOKIE, INSTALL_HANDOFF_QUERY_KEY, IndexedDbSecureStorage, JsonStore, LOGO_MIN_CONTRAST, LocalStorageSecureStorage, MemorySecureStorage, type MobileBootstrapOptions, type MobileBootstrapState, MockSocket, type NetworkMode, NotificationHandler, type NotificationHandlerOptions, type OfflineConfig, type OfflineConnectionState, type OfflineShellOptions, type OnboardingConfig, PairingCard, type PairingCardEndpoint, type PairingCardOptions, type PairingCardState, type PairingCardTheme, type PairingChannel, type PairingConfirmRequest, type PairingHostInfo, type PairingSession, type PairingSource, type PairingSourceEvent, PoweredByCrosslink, type PoweredByCrosslinkOptions, type PoweredByPlacement, type ResolvedCrosslinkTheme, type ResolvedPairingSession, type SecureStorage, type SecureStorageResult, type ServiceWorkerConfig, SignalingPeer, type WsLike, contrastRatio, createCrosslinkClient, createCrosslinkLogo, createHttpPairingSource, createOfflineUI, createPairingCard, createPoweredByCrosslink, createSecureCrosslinkClient, createSecureStorage, createServiceWorkerConfig, crosslinkLogoSvg, describeBootstrapEnvironment, generateServiceWorker, injectBootstrapStyles, injectPairingCardStyles, isStandalone, normalizeNetworkMode, parseColor, removeOfflineUI, resetDeviceStorage, resolveCrosslinkTheme, updateOfflineStatus, wsTransport };
|