@corewire/core 0.0.0 → 1.0.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -1
- package/dist/chunk-wE0WeVzl.mjs +22 -0
- package/dist/crypto-CXSIS6si.mjs +3093 -0
- package/dist/crypto.d.mts +111 -0
- package/dist/crypto.mjs +3 -0
- package/dist/discovery.d.mts +110 -0
- package/dist/discovery.mjs +763 -0
- package/dist/encoding-BaLbwFQ0.mjs +1663 -0
- package/dist/encoding.d.mts +411 -0
- package/dist/encoding.mjs +3 -0
- package/dist/errors-DVv1n4fL.mjs +125 -0
- package/dist/index-BJKNwUmq.d.mts +125 -0
- package/dist/index.d.mts +82 -0
- package/dist/index.mjs +5 -0
- package/dist/pairing.d.mts +169 -0
- package/dist/pairing.mjs +2487 -0
- package/dist/reporter-DfM6vPT0.mjs +223 -0
- package/dist/reporter-Dn7n2TXN.d.mts +112 -0
- package/dist/transport-C4BMdD2J.mjs +270 -0
- package/dist/transport.d.mts +2 -0
- package/dist/transport.mjs +3 -0
- package/dist/types-qzabRJJ9.d.mts +5 -0
- package/dist/utils-DY5OKUuy.mjs +8 -0
- package/package.json +84 -18
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
//#region src/reporter.ts
|
|
2
|
+
const ALL_GROUPS = [
|
|
3
|
+
"debug",
|
|
4
|
+
"error",
|
|
5
|
+
"info",
|
|
6
|
+
"net",
|
|
7
|
+
"raw",
|
|
8
|
+
"warn"
|
|
9
|
+
];
|
|
10
|
+
/**
|
|
11
|
+
* Logs under an identifier, usually a device id. The global {@link reporter} decides which groups
|
|
12
|
+
* print.
|
|
13
|
+
*/
|
|
14
|
+
var Logger = class {
|
|
15
|
+
/** The identifier this logger is scoped to. */
|
|
16
|
+
get id() {
|
|
17
|
+
return this.#id;
|
|
18
|
+
}
|
|
19
|
+
/** ANSI-colored label prefix used in log output. */
|
|
20
|
+
get label() {
|
|
21
|
+
return this.#label;
|
|
22
|
+
}
|
|
23
|
+
#id;
|
|
24
|
+
#label;
|
|
25
|
+
#sink;
|
|
26
|
+
/**
|
|
27
|
+
* @param id - Prefix of every line, usually a device id.
|
|
28
|
+
* @param sink - Also receives every record of this logger, next to the global reporter.
|
|
29
|
+
*/
|
|
30
|
+
constructor(id, sink) {
|
|
31
|
+
this.#id = id;
|
|
32
|
+
this.#label = `\u001b[36m[${id}]\u001b[39m`;
|
|
33
|
+
this.#sink = sink;
|
|
34
|
+
}
|
|
35
|
+
/** Logs in the `debug` group, in cyan. */
|
|
36
|
+
debug(...data) {
|
|
37
|
+
this.#write("debug", data);
|
|
38
|
+
}
|
|
39
|
+
/** Logs in the `error` group, in red. */
|
|
40
|
+
error(...data) {
|
|
41
|
+
this.#write("error", data);
|
|
42
|
+
}
|
|
43
|
+
/** Logs in the `info` group, in green. */
|
|
44
|
+
info(...data) {
|
|
45
|
+
this.#write("info", data);
|
|
46
|
+
}
|
|
47
|
+
/** Logs in the `net` group, in yellow. */
|
|
48
|
+
net(...data) {
|
|
49
|
+
this.#write("net", data);
|
|
50
|
+
}
|
|
51
|
+
/** Logs in the `raw` group, in blue; meant for hex dumps and binary protocol data. */
|
|
52
|
+
raw(...data) {
|
|
53
|
+
this.#write("raw", data);
|
|
54
|
+
}
|
|
55
|
+
/** Logs in the `warn` group, in yellow. */
|
|
56
|
+
warn(...data) {
|
|
57
|
+
this.#write("warn", data);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Hands one protocol message to the traffic sink. Does nothing without a sink, so callers
|
|
61
|
+
* that have to build `decoded` first should check {@link Reporter.tapsTraffic}.
|
|
62
|
+
*
|
|
63
|
+
* @param direction - `out` for what we sent, `in` for what the device sent.
|
|
64
|
+
*/
|
|
65
|
+
traffic(protocol, direction, summary, decoded, bytes) {
|
|
66
|
+
reporter.reportTraffic({
|
|
67
|
+
deviceId: this.#id,
|
|
68
|
+
protocol,
|
|
69
|
+
direction,
|
|
70
|
+
summary,
|
|
71
|
+
decoded,
|
|
72
|
+
bytes,
|
|
73
|
+
timestamp: Date.now()
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
#write(group, data) {
|
|
77
|
+
write(group, this.#id, this.#label, data);
|
|
78
|
+
if (!this.#sink) return;
|
|
79
|
+
try {
|
|
80
|
+
this.#sink(group, data);
|
|
81
|
+
} catch {}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Decides which log groups print, and where log records and traffic go. Every {@link Logger}
|
|
86
|
+
* goes through the {@link reporter} singleton.
|
|
87
|
+
*/
|
|
88
|
+
var Reporter = class {
|
|
89
|
+
#enabled = [];
|
|
90
|
+
#sink = null;
|
|
91
|
+
#sinkGroups = ALL_GROUPS;
|
|
92
|
+
#silenceConsole = false;
|
|
93
|
+
#trafficSink = null;
|
|
94
|
+
/** Whether a traffic sink is installed. */
|
|
95
|
+
get tapsTraffic() {
|
|
96
|
+
return this.#trafficSink !== null;
|
|
97
|
+
}
|
|
98
|
+
/** Enables every group except the verbose `raw`. */
|
|
99
|
+
all() {
|
|
100
|
+
this.#enabled = [
|
|
101
|
+
"debug",
|
|
102
|
+
"error",
|
|
103
|
+
"info",
|
|
104
|
+
"net",
|
|
105
|
+
"warn"
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
/** Disables every group. */
|
|
109
|
+
none() {
|
|
110
|
+
this.#enabled = [];
|
|
111
|
+
}
|
|
112
|
+
disable(group) {
|
|
113
|
+
if (this.#enabled.includes(group)) this.#enabled.splice(this.#enabled.indexOf(group), 1);
|
|
114
|
+
}
|
|
115
|
+
enable(group) {
|
|
116
|
+
if (!this.#enabled.includes(group)) this.#enabled.push(group);
|
|
117
|
+
}
|
|
118
|
+
/** Whether a group reaches the console or the sink, so a caller can skip building an expensive message. */
|
|
119
|
+
accepts(group) {
|
|
120
|
+
return this.printsToConsole(group) || this.#sink !== null && this.#sinkGroups.includes(group);
|
|
121
|
+
}
|
|
122
|
+
isEnabled(group) {
|
|
123
|
+
return this.#enabled.includes(group);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Installs a sink that receives one record per logging call, whichever groups print. Replaces
|
|
127
|
+
* a sink that was already installed.
|
|
128
|
+
*/
|
|
129
|
+
setSink(sink, options = {}) {
|
|
130
|
+
this.#sink = sink;
|
|
131
|
+
this.#sinkGroups = options.groups ?? ALL_GROUPS;
|
|
132
|
+
this.#silenceConsole = options.silenceConsole === true;
|
|
133
|
+
}
|
|
134
|
+
/** Removes the installed sink and restores console output. */
|
|
135
|
+
clearSink() {
|
|
136
|
+
this.#sink = null;
|
|
137
|
+
this.#sinkGroups = ALL_GROUPS;
|
|
138
|
+
this.#silenceConsole = false;
|
|
139
|
+
}
|
|
140
|
+
/** Installs a sink that receives every tapped protocol message. Replaces a sink that was already installed. */
|
|
141
|
+
setTrafficSink(sink) {
|
|
142
|
+
this.#trafficSink = sink;
|
|
143
|
+
}
|
|
144
|
+
/** Removes the installed traffic sink. */
|
|
145
|
+
clearTrafficSink() {
|
|
146
|
+
this.#trafficSink = null;
|
|
147
|
+
}
|
|
148
|
+
/** Hands one entry to the traffic sink, swallowing a failing sink like {@link report} does. */
|
|
149
|
+
reportTraffic(entry) {
|
|
150
|
+
if (this.#trafficSink === null) return;
|
|
151
|
+
try {
|
|
152
|
+
this.#trafficSink(entry);
|
|
153
|
+
} catch {}
|
|
154
|
+
}
|
|
155
|
+
/** Whether a group still reaches the console. False while a sink silences the console. */
|
|
156
|
+
printsToConsole(group) {
|
|
157
|
+
return !this.#silenceConsole && this.#enabled.includes(group);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Hands one record to the installed sink. A throwing sink must never take the caller down
|
|
161
|
+
* with it, so this swallows the failure.
|
|
162
|
+
*
|
|
163
|
+
* @param deviceId - The logger identifier, or null when there is none.
|
|
164
|
+
*/
|
|
165
|
+
report(group, deviceId, args) {
|
|
166
|
+
if (this.#sink === null || !this.#sinkGroups.includes(group)) return;
|
|
167
|
+
try {
|
|
168
|
+
this.#sink({
|
|
169
|
+
group,
|
|
170
|
+
deviceId,
|
|
171
|
+
args,
|
|
172
|
+
timestamp: Date.now()
|
|
173
|
+
});
|
|
174
|
+
} catch {}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
/** How each group prints: the console method and the ANSI color of its tag. */
|
|
178
|
+
const CONSOLE = {
|
|
179
|
+
debug: {
|
|
180
|
+
method: "debug",
|
|
181
|
+
color: "36"
|
|
182
|
+
},
|
|
183
|
+
error: {
|
|
184
|
+
method: "error",
|
|
185
|
+
color: "31"
|
|
186
|
+
},
|
|
187
|
+
info: {
|
|
188
|
+
method: "info",
|
|
189
|
+
color: "32"
|
|
190
|
+
},
|
|
191
|
+
net: {
|
|
192
|
+
method: "info",
|
|
193
|
+
color: "33"
|
|
194
|
+
},
|
|
195
|
+
raw: {
|
|
196
|
+
method: "log",
|
|
197
|
+
color: "34"
|
|
198
|
+
},
|
|
199
|
+
warn: {
|
|
200
|
+
method: "warn",
|
|
201
|
+
color: "33"
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* The one path every {@link Logger} method takes. The console prints only the enabled groups; the
|
|
206
|
+
* sink sees every record, whether or not it printed.
|
|
207
|
+
*
|
|
208
|
+
* @param id - The logger identifier, or null when the record has no logger.
|
|
209
|
+
* @param label - ANSI-colored prefix printed before the values.
|
|
210
|
+
*/
|
|
211
|
+
function write(group, id, label, data) {
|
|
212
|
+
reporter.report(group, id, data);
|
|
213
|
+
if (!reporter.printsToConsole(group)) return;
|
|
214
|
+
const { method, color } = CONSOLE[group];
|
|
215
|
+
const tag = `\u001b[${color}m[${group}]\u001b[39m`;
|
|
216
|
+
if (label === null) console[method](tag, ...data);
|
|
217
|
+
else console[method](tag, label, ...data);
|
|
218
|
+
}
|
|
219
|
+
/** The global {@link Reporter} that decides which log groups print. */
|
|
220
|
+
const reporter = new Reporter();
|
|
221
|
+
|
|
222
|
+
//#endregion
|
|
223
|
+
export { reporter as n, Logger as t };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
//#region src/reporter.d.ts
|
|
2
|
+
/** A log group. The global {@link reporter} turns each group on or off. */
|
|
3
|
+
type DebugGroup = "debug" | "error" | "info" | "net" | "raw" | "warn";
|
|
4
|
+
/** One log record, as a {@link ReporterSink} receives it. */
|
|
5
|
+
type ReporterEntry = {
|
|
6
|
+
readonly group: DebugGroup; /** The {@link Logger} identifier, or `null` for output not tied to a logger. */
|
|
7
|
+
readonly deviceId: string | null; /** The values passed to the logging method, unformatted. */
|
|
8
|
+
readonly args: readonly unknown[]; /** Wall-clock milliseconds since the Unix epoch. */
|
|
9
|
+
readonly timestamp: number;
|
|
10
|
+
};
|
|
11
|
+
/** Receives every log record, whether or not its group prints to the console. */
|
|
12
|
+
type ReporterSink = (entry: ReporterEntry) => void;
|
|
13
|
+
/** Options for {@link Reporter.setSink}. */
|
|
14
|
+
type ReporterSinkOptions = {
|
|
15
|
+
/** Stops console output entirely while the sink is installed. Default: false. */readonly silenceConsole?: boolean; /** Limits which groups reach the sink. Default: every group. */
|
|
16
|
+
readonly groups?: readonly DebugGroup[];
|
|
17
|
+
};
|
|
18
|
+
/** The label of the channel a tapped message crossed; each protocol package picks its own. */
|
|
19
|
+
type TrafficProtocol = string;
|
|
20
|
+
type TrafficDirection = "in" | "out";
|
|
21
|
+
/** One protocol message as it crossed the wire, after decryption or before encryption. */
|
|
22
|
+
type TrafficEntry = {
|
|
23
|
+
/** The {@link Logger} identifier of the device the message belongs to. */readonly deviceId: string | null;
|
|
24
|
+
readonly protocol: TrafficProtocol;
|
|
25
|
+
readonly direction: TrafficDirection; /** One line that identifies the message, for example `SETUP /stream cseq=4`. */
|
|
26
|
+
readonly summary: string; /** The decoded message, when the caller has one. */
|
|
27
|
+
readonly decoded?: unknown; /** The plaintext frame. */
|
|
28
|
+
readonly bytes?: Uint8Array; /** Wall-clock milliseconds since the Unix epoch. */
|
|
29
|
+
readonly timestamp: number;
|
|
30
|
+
};
|
|
31
|
+
type TrafficSink = (entry: TrafficEntry) => void;
|
|
32
|
+
/** Receives every record of one {@link Logger}, whichever groups the global reporter prints. */
|
|
33
|
+
type LogSink = (group: DebugGroup, args: readonly unknown[]) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Logs under an identifier, usually a device id. The global {@link reporter} decides which groups
|
|
36
|
+
* print.
|
|
37
|
+
*/
|
|
38
|
+
declare class Logger {
|
|
39
|
+
#private;
|
|
40
|
+
/** The identifier this logger is scoped to. */
|
|
41
|
+
get id(): string;
|
|
42
|
+
/** ANSI-colored label prefix used in log output. */
|
|
43
|
+
get label(): string;
|
|
44
|
+
/**
|
|
45
|
+
* @param id - Prefix of every line, usually a device id.
|
|
46
|
+
* @param sink - Also receives every record of this logger, next to the global reporter.
|
|
47
|
+
*/
|
|
48
|
+
constructor(id: string, sink?: LogSink);
|
|
49
|
+
/** Logs in the `debug` group, in cyan. */
|
|
50
|
+
debug(...data: any[]): void;
|
|
51
|
+
/** Logs in the `error` group, in red. */
|
|
52
|
+
error(...data: any[]): void;
|
|
53
|
+
/** Logs in the `info` group, in green. */
|
|
54
|
+
info(...data: any[]): void;
|
|
55
|
+
/** Logs in the `net` group, in yellow. */
|
|
56
|
+
net(...data: any[]): void;
|
|
57
|
+
/** Logs in the `raw` group, in blue; meant for hex dumps and binary protocol data. */
|
|
58
|
+
raw(...data: any[]): void;
|
|
59
|
+
/** Logs in the `warn` group, in yellow. */
|
|
60
|
+
warn(...data: any[]): void;
|
|
61
|
+
/**
|
|
62
|
+
* Hands one protocol message to the traffic sink. Does nothing without a sink, so callers
|
|
63
|
+
* that have to build `decoded` first should check {@link Reporter.tapsTraffic}.
|
|
64
|
+
*
|
|
65
|
+
* @param direction - `out` for what we sent, `in` for what the device sent.
|
|
66
|
+
*/
|
|
67
|
+
traffic(protocol: TrafficProtocol, direction: TrafficDirection, summary: string, decoded?: unknown, bytes?: Uint8Array): void;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Decides which log groups print, and where log records and traffic go. Every {@link Logger}
|
|
71
|
+
* goes through the {@link reporter} singleton.
|
|
72
|
+
*/
|
|
73
|
+
declare class Reporter {
|
|
74
|
+
#private;
|
|
75
|
+
/** Whether a traffic sink is installed. */
|
|
76
|
+
get tapsTraffic(): boolean;
|
|
77
|
+
/** Enables every group except the verbose `raw`. */
|
|
78
|
+
all(): void;
|
|
79
|
+
/** Disables every group. */
|
|
80
|
+
none(): void;
|
|
81
|
+
disable(group: DebugGroup): void;
|
|
82
|
+
enable(group: DebugGroup): void;
|
|
83
|
+
/** Whether a group reaches the console or the sink, so a caller can skip building an expensive message. */
|
|
84
|
+
accepts(group: DebugGroup): boolean;
|
|
85
|
+
isEnabled(group: DebugGroup): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Installs a sink that receives one record per logging call, whichever groups print. Replaces
|
|
88
|
+
* a sink that was already installed.
|
|
89
|
+
*/
|
|
90
|
+
setSink(sink: ReporterSink, options?: ReporterSinkOptions): void;
|
|
91
|
+
/** Removes the installed sink and restores console output. */
|
|
92
|
+
clearSink(): void;
|
|
93
|
+
/** Installs a sink that receives every tapped protocol message. Replaces a sink that was already installed. */
|
|
94
|
+
setTrafficSink(sink: TrafficSink): void;
|
|
95
|
+
/** Removes the installed traffic sink. */
|
|
96
|
+
clearTrafficSink(): void;
|
|
97
|
+
/** Hands one entry to the traffic sink, swallowing a failing sink like {@link report} does. */
|
|
98
|
+
reportTraffic(entry: TrafficEntry): void;
|
|
99
|
+
/** Whether a group still reaches the console. False while a sink silences the console. */
|
|
100
|
+
printsToConsole(group: DebugGroup): boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Hands one record to the installed sink. A throwing sink must never take the caller down
|
|
103
|
+
* with it, so this swallows the failure.
|
|
104
|
+
*
|
|
105
|
+
* @param deviceId - The logger identifier, or null when there is none.
|
|
106
|
+
*/
|
|
107
|
+
report(group: DebugGroup, deviceId: string | null, args: readonly unknown[]): void;
|
|
108
|
+
}
|
|
109
|
+
/** The global {@link Reporter} that decides which log groups print. */
|
|
110
|
+
declare const reporter: Reporter;
|
|
111
|
+
//#endregion
|
|
112
|
+
export { ReporterEntry as a, TrafficDirection as c, TrafficSink as d, reporter as f, Reporter as i, TrafficEntry as l, LogSink as n, ReporterSink as o, Logger as r, ReporterSinkOptions as s, DebugGroup as t, TrafficProtocol as u };
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { a as ConnectionError, i as ConnectionClosedError, o as ConnectionTimeoutError } from "./errors-DVv1n4fL.mjs";
|
|
2
|
+
import { t as Logger } from "./reporter-DfM6vPT0.mjs";
|
|
3
|
+
import { Socket } from "node:net";
|
|
4
|
+
import { EventEmitter } from "node:events";
|
|
5
|
+
|
|
6
|
+
//#region src/const.ts
|
|
7
|
+
/** Timeout in milliseconds for TCP socket connections during initial connect. Matches Apple's 30s default. */
|
|
8
|
+
const SOCKET_TIMEOUT = 3e4;
|
|
9
|
+
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/transport/connection.ts
|
|
12
|
+
/**
|
|
13
|
+
* One TCP socket to a device, with keep-alive and no-delay. Subclasses extend the event map with
|
|
14
|
+
* protocol events.
|
|
15
|
+
*
|
|
16
|
+
* A connection connects once and ends once. Every way it ends, `disconnect()` and `destroy()`
|
|
17
|
+
* included, emits `closed` with the reason and aborts {@link signal}. Reconnecting takes a new
|
|
18
|
+
* connection, because a fresh socket carries no pairing and no encryption keys. `error` reports
|
|
19
|
+
* problems that do not end the connection by themselves, such as a write after the socket closed.
|
|
20
|
+
*/
|
|
21
|
+
var Connection = class extends EventEmitter {
|
|
22
|
+
get address() {
|
|
23
|
+
return this.#address;
|
|
24
|
+
}
|
|
25
|
+
get context() {
|
|
26
|
+
return this.#context;
|
|
27
|
+
}
|
|
28
|
+
get port() {
|
|
29
|
+
return this.#port;
|
|
30
|
+
}
|
|
31
|
+
get isConnected() {
|
|
32
|
+
return this.#state === "connected";
|
|
33
|
+
}
|
|
34
|
+
/** The local IP address of the socket, or `0.0.0.0` when it is not connected. */
|
|
35
|
+
get localAddress() {
|
|
36
|
+
return this.#socket?.localAddress ?? "0.0.0.0";
|
|
37
|
+
}
|
|
38
|
+
get state() {
|
|
39
|
+
return this.#state;
|
|
40
|
+
}
|
|
41
|
+
/** Aborts with a {@link ConnectionClosedError} when the connection ends. */
|
|
42
|
+
get signal() {
|
|
43
|
+
return this.#controller.signal;
|
|
44
|
+
}
|
|
45
|
+
#address;
|
|
46
|
+
#port;
|
|
47
|
+
#context;
|
|
48
|
+
#controller = new AbortController();
|
|
49
|
+
#connectPromise;
|
|
50
|
+
#debug = false;
|
|
51
|
+
#socket;
|
|
52
|
+
#state = "disconnected";
|
|
53
|
+
constructor(context, address, port) {
|
|
54
|
+
super();
|
|
55
|
+
this.#address = address;
|
|
56
|
+
this.#port = port;
|
|
57
|
+
this.#context = context;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Opens the socket. Returns at once when already connected.
|
|
61
|
+
*
|
|
62
|
+
* @throws {ConnectionTimeoutError} If the socket does not open within the socket timeout.
|
|
63
|
+
* @throws {ConnectionError} If the socket cannot open, or the connection is busy or has ended.
|
|
64
|
+
*/
|
|
65
|
+
async connect() {
|
|
66
|
+
if (this.#state === "connected") return;
|
|
67
|
+
if (this.signal.aborted) throw new ConnectionClosedError("This connection has ended, create a new one.");
|
|
68
|
+
if (this.#state !== "disconnected") throw new ConnectionError("A connection is already being established.");
|
|
69
|
+
this.#state = "connecting";
|
|
70
|
+
const connected = new Promise((resolve, reject) => {
|
|
71
|
+
this.#connectPromise = {
|
|
72
|
+
resolve,
|
|
73
|
+
reject
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
const socket = new Socket();
|
|
77
|
+
this.#socket = socket;
|
|
78
|
+
socket.setNoDelay(true);
|
|
79
|
+
socket.setTimeout(SOCKET_TIMEOUT);
|
|
80
|
+
socket.on("close", () => this.#onClose());
|
|
81
|
+
socket.on("connect", () => this.#onConnect());
|
|
82
|
+
socket.on("data", (data) => this.#onData(data));
|
|
83
|
+
socket.on("error", (error) => this.#onError(error));
|
|
84
|
+
socket.on("timeout", () => this.#onTimeout());
|
|
85
|
+
this.#context.logger.net(`Connecting to ${this.#address}:${this.#port}...`);
|
|
86
|
+
socket.connect({
|
|
87
|
+
host: this.#address,
|
|
88
|
+
port: this.#port
|
|
89
|
+
});
|
|
90
|
+
return connected;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Ends the connection right away without a graceful shutdown. Passing an error ends it with
|
|
94
|
+
* reason `error`, for a subclass that cannot continue after a broken frame.
|
|
95
|
+
*/
|
|
96
|
+
destroy(error) {
|
|
97
|
+
if (this.#state === "disconnected" && !this.#socket) return;
|
|
98
|
+
this.#end(error ? "error" : "local", error);
|
|
99
|
+
}
|
|
100
|
+
/** Ends the socket gracefully and resolves once it is closed. */
|
|
101
|
+
async disconnect() {
|
|
102
|
+
const socket = this.#socket;
|
|
103
|
+
if (!socket || this.signal.aborted) return;
|
|
104
|
+
if (socket.connecting || socket.destroyed) {
|
|
105
|
+
this.#end("local");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const closed = new Promise((resolve) => this.signal.addEventListener("abort", () => resolve(), { once: true }));
|
|
109
|
+
if (this.#state !== "closing") {
|
|
110
|
+
this.#state = "closing";
|
|
111
|
+
socket.end();
|
|
112
|
+
}
|
|
113
|
+
await closed;
|
|
114
|
+
}
|
|
115
|
+
/** Toggles hex and ASCII dumps of incoming data. */
|
|
116
|
+
debug(enabled) {
|
|
117
|
+
this.#debug = enabled;
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
/** Writes to the socket, or emits `error` when the socket is not writable. */
|
|
121
|
+
write(data) {
|
|
122
|
+
if (!this.#socket || this.#state !== "connected" || !this.#socket.writable) {
|
|
123
|
+
this.#reportError(new ConnectionClosedError("Cannot write to a disconnected connection."));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.#socket.write(data, (error) => {
|
|
127
|
+
if (!error || this.signal.aborted) return;
|
|
128
|
+
this.#context.logger.error("Failed to write data to socket.");
|
|
129
|
+
this.#reportError(error);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/** Ends the connection, the one exit for every path. It closes the socket, settles a pending connect, aborts the signal and emits `closed`. */
|
|
133
|
+
#end(reason, error) {
|
|
134
|
+
if (this.signal.aborted) return;
|
|
135
|
+
const socket = this.#socket;
|
|
136
|
+
this.#socket = void 0;
|
|
137
|
+
this.#state = reason === "error" || reason === "timeout" ? "failed" : "disconnected";
|
|
138
|
+
if (socket) {
|
|
139
|
+
socket.removeAllListeners();
|
|
140
|
+
socket.on("error", () => {});
|
|
141
|
+
socket.destroy();
|
|
142
|
+
}
|
|
143
|
+
this.#context.logger.net(`Connection closed (${reason}${error ? `: ${error.message}` : ""}).`);
|
|
144
|
+
this.#controller.abort(new ConnectionClosedError(`Connection closed (${reason}).`));
|
|
145
|
+
if (this.#connectPromise) {
|
|
146
|
+
this.#connectPromise.reject(error ?? new ConnectionClosedError("Connection closed before it was established."));
|
|
147
|
+
this.#connectPromise = void 0;
|
|
148
|
+
}
|
|
149
|
+
this.#emitInternal("closed", reason, error);
|
|
150
|
+
}
|
|
151
|
+
#onClose() {
|
|
152
|
+
this.#end(this.#state === "closing" ? "local" : "remote");
|
|
153
|
+
}
|
|
154
|
+
#onConnect() {
|
|
155
|
+
this.#state = "connected";
|
|
156
|
+
this.#socket.setKeepAlive(true, 1e4);
|
|
157
|
+
this.#socket.setTimeout(0);
|
|
158
|
+
const pending = this.#connectPromise;
|
|
159
|
+
this.#connectPromise = void 0;
|
|
160
|
+
this.#emitInternal("connect");
|
|
161
|
+
pending?.resolve();
|
|
162
|
+
}
|
|
163
|
+
#onData(data) {
|
|
164
|
+
if (this.#debug) {
|
|
165
|
+
const cutoff = Math.min(data.byteLength, 64);
|
|
166
|
+
this.#context.logger.debug(`Received ${data.byteLength} bytes of data.`);
|
|
167
|
+
this.#context.logger.debug(`hex=${data.subarray(0, cutoff).toString("hex")}`);
|
|
168
|
+
this.#context.logger.debug(`ascii=${data.toString("ascii").replace(/[^\x20-\x7E]/g, ".").substring(0, cutoff)}`);
|
|
169
|
+
}
|
|
170
|
+
this.#emitInternal("data", data);
|
|
171
|
+
}
|
|
172
|
+
#onError(error) {
|
|
173
|
+
this.#context.logger.error(`Connection error: ${error.message}`);
|
|
174
|
+
if (this.#state === "connecting") {
|
|
175
|
+
this.#end("error", new ConnectionError(`Cannot connect to ${this.#address}:${this.#port}: ${error.message}`));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
this.#end("error", error);
|
|
179
|
+
}
|
|
180
|
+
/** Fires only while connecting, because the idle timeout goes off once the socket opens. */
|
|
181
|
+
#onTimeout() {
|
|
182
|
+
this.#context.logger.error("Connection timed out.");
|
|
183
|
+
this.#end("timeout", new ConnectionTimeoutError());
|
|
184
|
+
}
|
|
185
|
+
/** Emits `error` when someone listens, because an unhandled `error` event would throw. */
|
|
186
|
+
#reportError(error) {
|
|
187
|
+
if (this.listenerCount("error") > 0) this.#emitInternal("error", error);
|
|
188
|
+
else this.#context.logger.warn("No error handler registered.", this.constructor.name, error.message);
|
|
189
|
+
}
|
|
190
|
+
/** The merged event map makes the inherited emit signature too broad to call directly. */
|
|
191
|
+
#emitInternal(event, ...args) {
|
|
192
|
+
return this.emit(event, ...args);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/transport/context.ts
|
|
198
|
+
/** The name every protocol presents for this client when the identity has none. */
|
|
199
|
+
const DEFAULT_CLIENT_NAME = "CoreWire";
|
|
200
|
+
/**
|
|
201
|
+
* Travels through every protocol layer of one device connection, so all layers log under the
|
|
202
|
+
* same device id and present the same identity.
|
|
203
|
+
*/
|
|
204
|
+
var Context = class {
|
|
205
|
+
/** Identifies the target device, usually by its mDNS host name. */
|
|
206
|
+
get deviceId() {
|
|
207
|
+
return this.#deviceId;
|
|
208
|
+
}
|
|
209
|
+
/** The identity overrides this client presents; a protocol fills the rest with its defaults. */
|
|
210
|
+
get identity() {
|
|
211
|
+
return this.#identity;
|
|
212
|
+
}
|
|
213
|
+
/** Prefixes its output with the device id. */
|
|
214
|
+
get logger() {
|
|
215
|
+
return this.#logger;
|
|
216
|
+
}
|
|
217
|
+
#deviceId;
|
|
218
|
+
#identity;
|
|
219
|
+
#logger;
|
|
220
|
+
/**
|
|
221
|
+
* @param sink - Also receives every log record of this context.
|
|
222
|
+
*/
|
|
223
|
+
constructor(deviceId, identity = {}, sink) {
|
|
224
|
+
this.#deviceId = deviceId;
|
|
225
|
+
this.#identity = { ...identity };
|
|
226
|
+
this.#logger = new Logger(deviceId, sink);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
//#endregion
|
|
231
|
+
//#region src/transport/encryption.ts
|
|
232
|
+
/** Builds the 12-byte ChaCha20-Poly1305 nonce for a message counter. */
|
|
233
|
+
function buildNonce(counter, format) {
|
|
234
|
+
const nonce = Buffer.alloc(12);
|
|
235
|
+
nonce.writeBigUInt64LE(BigInt(counter), format === "counter-at-0" ? 0 : 4);
|
|
236
|
+
return nonce;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Connection that can switch to ChaCha20-Poly1305 once pairing yields keys. Subclasses frame and
|
|
240
|
+
* encrypt with the {@link EncryptionState}, because every protocol frames differently.
|
|
241
|
+
*/
|
|
242
|
+
var EncryptionAwareConnection = class extends Connection {
|
|
243
|
+
get isEncrypted() {
|
|
244
|
+
return !!this.#encryption;
|
|
245
|
+
}
|
|
246
|
+
/** Keys and counters once {@link enableEncryption} ran; for the subclass that frames the messages. */
|
|
247
|
+
get encryption() {
|
|
248
|
+
return this.#encryption;
|
|
249
|
+
}
|
|
250
|
+
#encryption;
|
|
251
|
+
enableEncryption(readKey, writeKey) {
|
|
252
|
+
this.#encryption = new EncryptionState(readKey, writeKey);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
/** Keys and per-direction message counters of one encrypted connection. */
|
|
256
|
+
var EncryptionState = class {
|
|
257
|
+
readKey;
|
|
258
|
+
readCount;
|
|
259
|
+
writeKey;
|
|
260
|
+
writeCount;
|
|
261
|
+
constructor(readKey, writeKey) {
|
|
262
|
+
this.readCount = 0;
|
|
263
|
+
this.readKey = readKey;
|
|
264
|
+
this.writeCount = 0;
|
|
265
|
+
this.writeKey = writeKey;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
//#endregion
|
|
270
|
+
export { DEFAULT_CLIENT_NAME as a, Context as i, EncryptionState as n, Connection as o, buildNonce as r, EncryptionAwareConnection as t };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as Connection, c as Context, i as buildNonce, l as DEFAULT_CLIENT_NAME, n as EncryptionState, o as CloseReason, r as NonceFormat, s as ConnectionState, t as EncryptionAwareConnection, u as DeviceIdentity } from "./index-BJKNwUmq.mjs";
|
|
2
|
+
export { type CloseReason, Connection, type ConnectionState, Context, DEFAULT_CLIENT_NAME, type DeviceIdentity, EncryptionAwareConnection, EncryptionState, type NonceFormat, buildNonce };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { a as DEFAULT_CLIENT_NAME, i as Context, n as EncryptionState, o as Connection, r as buildNonce, t as EncryptionAwareConnection } from "./transport-C4BMdD2J.mjs";
|
|
2
|
+
|
|
3
|
+
export { Connection, Context, DEFAULT_CLIENT_NAME, EncryptionAwareConnection, EncryptionState, buildNonce };
|