homebridge-navilink 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.
Files changed (82) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +202 -0
  3. package/README.md +161 -0
  4. package/SECURITY.md +73 -0
  5. package/config.schema.json +138 -0
  6. package/dist/api/channel.d.ts +94 -0
  7. package/dist/api/channel.js +334 -0
  8. package/dist/api/http.d.ts +68 -0
  9. package/dist/api/http.js +205 -0
  10. package/dist/api/identity.d.ts +51 -0
  11. package/dist/api/identity.js +80 -0
  12. package/dist/api/index.d.ts +17 -0
  13. package/dist/api/index.js +33 -0
  14. package/dist/api/mqtt-codec.d.ts +181 -0
  15. package/dist/api/mqtt-codec.js +377 -0
  16. package/dist/api/mqtt.d.ts +178 -0
  17. package/dist/api/mqtt.js +450 -0
  18. package/dist/api/protocol.d.ts +212 -0
  19. package/dist/api/protocol.js +286 -0
  20. package/dist/api/rest.d.ts +139 -0
  21. package/dist/api/rest.js +317 -0
  22. package/dist/api/sigv4.d.ts +70 -0
  23. package/dist/api/sigv4.js +123 -0
  24. package/dist/api/topics.d.ts +75 -0
  25. package/dist/api/topics.js +90 -0
  26. package/dist/devices/base-accessory.d.ts +127 -0
  27. package/dist/devices/base-accessory.js +232 -0
  28. package/dist/devices/dhw-accessory.d.ts +62 -0
  29. package/dist/devices/dhw-accessory.js +100 -0
  30. package/dist/devices/fault-accessory.d.ts +31 -0
  31. package/dist/devices/fault-accessory.js +73 -0
  32. package/dist/devices/heating-accessory.d.ts +63 -0
  33. package/dist/devices/heating-accessory.js +128 -0
  34. package/dist/devices/host.d.ts +77 -0
  35. package/dist/devices/host.js +23 -0
  36. package/dist/devices/index.d.ts +17 -0
  37. package/dist/devices/index.js +33 -0
  38. package/dist/devices/power-accessory.d.ts +32 -0
  39. package/dist/devices/power-accessory.js +86 -0
  40. package/dist/devices/probe-accessory.d.ts +45 -0
  41. package/dist/devices/probe-accessory.js +108 -0
  42. package/dist/devices/recirculation-accessory.d.ts +47 -0
  43. package/dist/devices/recirculation-accessory.js +122 -0
  44. package/dist/devices/thermostat-accessory.d.ts +129 -0
  45. package/dist/devices/thermostat-accessory.js +372 -0
  46. package/dist/discovery.d.ts +99 -0
  47. package/dist/discovery.js +423 -0
  48. package/dist/index.d.ts +11 -0
  49. package/dist/index.js +15 -0
  50. package/dist/platform.d.ts +120 -0
  51. package/dist/platform.js +425 -0
  52. package/dist/session.d.ts +243 -0
  53. package/dist/session.js +717 -0
  54. package/dist/settings.d.ts +218 -0
  55. package/dist/settings.js +240 -0
  56. package/dist/types/index.d.ts +265 -0
  57. package/dist/types/index.js +58 -0
  58. package/dist/ui-api.d.ts +27 -0
  59. package/dist/ui-api.js +39 -0
  60. package/dist/utils/context.d.ts +32 -0
  61. package/dist/utils/context.js +74 -0
  62. package/dist/utils/errors.d.ts +69 -0
  63. package/dist/utils/errors.js +118 -0
  64. package/dist/utils/index.d.ts +15 -0
  65. package/dist/utils/index.js +31 -0
  66. package/dist/utils/redact.d.ts +85 -0
  67. package/dist/utils/redact.js +210 -0
  68. package/dist/utils/serial.d.ts +18 -0
  69. package/dist/utils/serial.js +24 -0
  70. package/dist/utils/temperature.d.ts +114 -0
  71. package/dist/utils/temperature.js +150 -0
  72. package/dist/utils/timing.d.ts +68 -0
  73. package/dist/utils/timing.js +91 -0
  74. package/dist/utils/validators.d.ts +96 -0
  75. package/dist/utils/validators.js +353 -0
  76. package/docs/FEATURES.md +80 -0
  77. package/docs/PROTOCOL.md +200 -0
  78. package/docs/README-DETAILED.md +312 -0
  79. package/homebridge-ui/public/index.html +123 -0
  80. package/homebridge-ui/public/index.js +503 -0
  81. package/homebridge-ui/server.js +174 -0
  82. package/package.json +87 -0
@@ -0,0 +1,205 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 tbaur
4
+ *
5
+ * Licensed under the Apache License, Version 2.0
6
+ * See LICENSE file for full license text
7
+ *
8
+ * @fileoverview JSON over HTTPS against the NaviLink cloud.
9
+ *
10
+ * Built on `node:https` rather than `fetch` for three reasons, in ascending
11
+ * order of how much they matter.
12
+ *
13
+ * A response size cap. `fetch` gives you a body you have already committed to
14
+ * buffering, or a stream you have to cap by hand; a cap applied as bytes
15
+ * arrive is both simpler and actually enforced. The cloud is not ours and its
16
+ * responses are not bounded by anything we control.
17
+ *
18
+ * Separate connect and total deadlines. `AbortSignal.timeout` expresses one
19
+ * instant, so it cannot say "reach the host quickly, then allow it time to
20
+ * answer". An unreachable cloud should fail startup in seconds, not hold a
21
+ * slot for the whole request budget.
22
+ *
23
+ * Control over what is logged. Everything on this path is a credential: the
24
+ * request body carries the password, and the response body carries a JWT pair
25
+ * and a set of AWS keys. `fetch` errors quote the URL, and a debug logger
26
+ * wrapped round it would eventually quote a body. Here, the only thing that
27
+ * reaches the log is the method and the path.
28
+ */
29
+ var __importDefault = (this && this.__importDefault) || function (mod) {
30
+ return (mod && mod.__esModule) ? mod : { "default": mod };
31
+ };
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.postJson = void 0;
34
+ exports.parseJsonBody = parseJsonBody;
35
+ const node_https_1 = __importDefault(require("node:https"));
36
+ const errors_1 = require("../utils/errors");
37
+ const redact_1 = require("../utils/redact");
38
+ /** Longest body excerpt put into a parse-failure message. */
39
+ const MAX_BODY_EXCERPT = 80;
40
+ /**
41
+ * POST a JSON body and read the response as text.
42
+ *
43
+ * Rejects with {@link ConnectionError} for anything that prevented an answer
44
+ * and {@link ProtocolError} when the answer arrived but was unusable. Parsing
45
+ * is left to the caller: a non-200 from this API still carries a JSON body
46
+ * explaining why, and throwing on the status here would discard it.
47
+ *
48
+ * Redirects are not followed. The cloud does not issue them, and following one
49
+ * would let a compromised or spoofed endpoint move a request that carries the
50
+ * user's password to a host of its choosing.
51
+ */
52
+ const postJson = (url, body, options) => {
53
+ const { connectTimeoutMs, totalTimeoutMs, maxBytes, headers = {}, signal } = options;
54
+ const payload = JSON.stringify(body ?? {});
55
+ return new Promise((resolve, reject) => {
56
+ if (signal?.aborted === true) {
57
+ reject(new errors_1.ConnectionError('request aborted before it started'));
58
+ return;
59
+ }
60
+ let settled = false;
61
+ let onAbort;
62
+ // Held in a container because `cleanup` closes over it before the timer
63
+ // that fills it in can be created: the timer's callback needs `fail`, and
64
+ // `fail` needs `cleanup`.
65
+ const timers = {};
66
+ const request = node_https_1.default.request(url, {
67
+ method: 'POST',
68
+ // A connection per request. There is no long-poll to queue behind here,
69
+ // but sign-in and the device list are a handful of calls per hour and a
70
+ // pooled socket to a cloud endpoint is one more thing holding state
71
+ // across a token refresh.
72
+ agent: false,
73
+ headers: {
74
+ accept: 'application/json',
75
+ 'content-type': 'application/json',
76
+ 'content-length': String(Buffer.byteLength(payload)),
77
+ ...headers,
78
+ },
79
+ }, (response) => {
80
+ const chunks = [];
81
+ let received = 0;
82
+ response.on('data', (chunk) => {
83
+ received += chunk.length;
84
+ if (received > maxBytes) {
85
+ fail(new errors_1.ProtocolError(`response exceeds ${maxBytes} bytes`));
86
+ return;
87
+ }
88
+ chunks.push(chunk);
89
+ });
90
+ response.on('end', () => {
91
+ finish({
92
+ status: response.statusCode ?? 0,
93
+ body: Buffer.concat(chunks).toString('utf8'),
94
+ });
95
+ });
96
+ response.on('error', (error) => {
97
+ fail(new errors_1.ConnectionError('response stream failed', { cause: error }));
98
+ });
99
+ });
100
+ const cleanup = () => {
101
+ if (timers.connect !== undefined) {
102
+ clearTimeout(timers.connect);
103
+ }
104
+ if (timers.total !== undefined) {
105
+ clearTimeout(timers.total);
106
+ }
107
+ if (onAbort !== undefined) {
108
+ signal?.removeEventListener('abort', onAbort);
109
+ }
110
+ };
111
+ const finish = (response) => {
112
+ if (settled) {
113
+ return;
114
+ }
115
+ settled = true;
116
+ cleanup();
117
+ resolve(response);
118
+ };
119
+ const fail = (error) => {
120
+ if (settled) {
121
+ return;
122
+ }
123
+ settled = true;
124
+ cleanup();
125
+ request.destroy();
126
+ reject(error);
127
+ };
128
+ // Our own timer, not `request.setTimeout`. Node defers that call until
129
+ // the socket is already connected, then this module immediately clears
130
+ // it. Without that, a host that blackholes the SYN would sit on the
131
+ // total deadline instead of failing in seconds, which is the whole
132
+ // point of having two clocks.
133
+ timers.connect = setTimeout(() => {
134
+ fail(new errors_1.ConnectionError(`connect timed out after ${connectTimeoutMs}ms`));
135
+ }, connectTimeoutMs);
136
+ request.on('socket', (socket) => {
137
+ const onReady = () => {
138
+ if (timers.connect !== undefined) {
139
+ clearTimeout(timers.connect);
140
+ timers.connect = undefined;
141
+ }
142
+ socket.setNoDelay(true);
143
+ };
144
+ if (socket.connecting) {
145
+ // A TLSSocket has `encrypted`. Waiting on TCP `connect` would clear
146
+ // the timer before the handshake finishes. A plain socket (the test
147
+ // loopback under an `https:` URL) only emits `connect`.
148
+ if ('encrypted' in socket) {
149
+ socket.once('secureConnect', onReady);
150
+ }
151
+ else {
152
+ socket.once('connect', onReady);
153
+ }
154
+ }
155
+ else {
156
+ onReady();
157
+ }
158
+ });
159
+ request.on('error', (error) => {
160
+ // The message is deliberately generic. Node puts the request URL into
161
+ // some socket errors, and while this API's URLs carry no secrets, the
162
+ // rule that nothing from this layer quotes a URL is easier to keep than
163
+ // an exception list.
164
+ fail(new errors_1.ConnectionError('request failed', { cause: error }));
165
+ });
166
+ // Referenced on purpose, and always cleared in `cleanup`: a caller is
167
+ // awaiting this request, so the process must not be free to exit under it.
168
+ timers.total = setTimeout(() => {
169
+ fail(new errors_1.ConnectionError(`request timed out after ${totalTimeoutMs}ms`));
170
+ }, totalTimeoutMs);
171
+ if (signal !== undefined) {
172
+ onAbort = () => {
173
+ fail(new errors_1.ConnectionError('request aborted'));
174
+ };
175
+ signal.addEventListener('abort', onAbort, { once: true });
176
+ }
177
+ request.end(payload);
178
+ });
179
+ };
180
+ exports.postJson = postJson;
181
+ /**
182
+ * Parse a response body as JSON, or explain why it could not be.
183
+ *
184
+ * The excerpt in the failure message is capped and redacted: a gateway error
185
+ * page can be a whole HTML document, and a partial JSON body from this API
186
+ * would otherwise put a token into the log.
187
+ */
188
+ function parseJsonBody(body) {
189
+ if (body.length === 0) {
190
+ return undefined;
191
+ }
192
+ try {
193
+ return JSON.parse(body);
194
+ }
195
+ catch (error) {
196
+ throw new errors_1.ProtocolError(`the cloud returned a body that is not JSON (${bodyExcerpt(body)})`, { cause: error });
197
+ }
198
+ }
199
+ /** A short, redacted look at a body that could not be parsed. */
200
+ function bodyExcerpt(body) {
201
+ const cleaned = (0, redact_1.redactSecrets)(body).replace(/[\u0000-\u001F\u007F]/g, '\uFFFD');
202
+ return cleaned.length > MAX_BODY_EXCERPT
203
+ ? `${cleaned.slice(0, MAX_BODY_EXCERPT)}\u2026`
204
+ : cleaned;
205
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Copyright (c) 2026 tbaur
3
+ *
4
+ * Licensed under the Apache License, Version 2.0
5
+ * See LICENSE file for full license text
6
+ *
7
+ * @fileoverview Stable appliance identity.
8
+ *
9
+ * Accessory identity has to survive everything that legitimately changes about
10
+ * an installation: a new router, a re-paired gateway, a firmware update, a
11
+ * renamed device, a changed account email. What is left is the gateway's MAC
12
+ * address and the channel the appliance sits on, so that is what identity is.
13
+ *
14
+ * The MAC never leaves this layer in the clear. It is the appliance's address
15
+ * in every MQTT topic, so it is a capability as much as an identifier: it goes
16
+ * into a device id and into topic construction, and everywhere else (logs,
17
+ * HomeKit serial numbers, bug reports) it is masked.
18
+ */
19
+ import type { AccessoryKind, ResolvedAccessory } from '../types';
20
+ /**
21
+ * Normalise a MAC to the cloud's own spelling: lower-case hex, no separators.
22
+ *
23
+ * Accepts the colon-separated form as well, because a hand-edited
24
+ * configuration is likely to use it and rejecting that would be a puzzle
25
+ * rather than a help. Returns undefined rather than guessing, so a caller can
26
+ * skip the entry instead of building an unstable identity from a bad value.
27
+ */
28
+ export declare function normalizeMac(value: unknown): string | undefined;
29
+ /** Build a device id from a gateway MAC and a channel number. */
30
+ export declare function makeDeviceId(mac: string, channel: number): string;
31
+ /** The MAC and channel inside a device id, or undefined when it is malformed. */
32
+ export declare function parseDeviceId(value: unknown): {
33
+ mac: string;
34
+ channel: number;
35
+ } | undefined;
36
+ /**
37
+ * The identity of an accessory: what it does, for which appliance.
38
+ *
39
+ * Two accessories with the same key are the same accessory, and one whose key
40
+ * changes is a different accessory that takes the old one's rooms, scenes and
41
+ * automations with it when the old one goes.
42
+ */
43
+ export declare function accessoryIdentityKey(accessory: {
44
+ kind: AccessoryKind;
45
+ deviceId: string;
46
+ }): string;
47
+ /** True when a cached context describes the same accessory as a resolved one. */
48
+ export declare function hasAccessoryIdentity(context: {
49
+ kind?: unknown;
50
+ deviceId?: unknown;
51
+ }, accessory: ResolvedAccessory): boolean;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 tbaur
4
+ *
5
+ * Licensed under the Apache License, Version 2.0
6
+ * See LICENSE file for full license text
7
+ *
8
+ * @fileoverview Stable appliance identity.
9
+ *
10
+ * Accessory identity has to survive everything that legitimately changes about
11
+ * an installation: a new router, a re-paired gateway, a firmware update, a
12
+ * renamed device, a changed account email. What is left is the gateway's MAC
13
+ * address and the channel the appliance sits on, so that is what identity is.
14
+ *
15
+ * The MAC never leaves this layer in the clear. It is the appliance's address
16
+ * in every MQTT topic, so it is a capability as much as an identifier: it goes
17
+ * into a device id and into topic construction, and everywhere else (logs,
18
+ * HomeKit serial numbers, bug reports) it is masked.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.normalizeMac = normalizeMac;
22
+ exports.makeDeviceId = makeDeviceId;
23
+ exports.parseDeviceId = parseDeviceId;
24
+ exports.accessoryIdentityKey = accessoryIdentityKey;
25
+ exports.hasAccessoryIdentity = hasAccessoryIdentity;
26
+ const settings_1 = require("../settings");
27
+ /** Twelve hex digits, which is how the cloud spells a MAC. */
28
+ const BARE_MAC = /^[0-9a-f]{12}$/;
29
+ /**
30
+ * Normalise a MAC to the cloud's own spelling: lower-case hex, no separators.
31
+ *
32
+ * Accepts the colon-separated form as well, because a hand-edited
33
+ * configuration is likely to use it and rejecting that would be a puzzle
34
+ * rather than a help. Returns undefined rather than guessing, so a caller can
35
+ * skip the entry instead of building an unstable identity from a bad value.
36
+ */
37
+ function normalizeMac(value) {
38
+ if (typeof value !== 'string') {
39
+ return undefined;
40
+ }
41
+ const cleaned = value.trim().toLowerCase().replaceAll(/[:-]/g, '');
42
+ return BARE_MAC.test(cleaned) ? cleaned : undefined;
43
+ }
44
+ /** Build a device id from a gateway MAC and a channel number. */
45
+ function makeDeviceId(mac, channel) {
46
+ return `${mac.toLowerCase()}:${channel}`;
47
+ }
48
+ /** The MAC and channel inside a device id, or undefined when it is malformed. */
49
+ function parseDeviceId(value) {
50
+ if (typeof value !== 'string') {
51
+ return undefined;
52
+ }
53
+ const separator = value.lastIndexOf(':');
54
+ if (separator <= 0) {
55
+ return undefined;
56
+ }
57
+ const mac = normalizeMac(value.slice(0, separator));
58
+ const channel = Number(value.slice(separator + 1));
59
+ if (mac === undefined || !Number.isInteger(channel)) {
60
+ return undefined;
61
+ }
62
+ if (channel < settings_1.MIN_CHANNEL || channel > settings_1.MAX_CHANNEL) {
63
+ return undefined;
64
+ }
65
+ return { mac, channel };
66
+ }
67
+ /**
68
+ * The identity of an accessory: what it does, for which appliance.
69
+ *
70
+ * Two accessories with the same key are the same accessory, and one whose key
71
+ * changes is a different accessory that takes the old one's rooms, scenes and
72
+ * automations with it when the old one goes.
73
+ */
74
+ function accessoryIdentityKey(accessory) {
75
+ return `${accessory.deviceId}:${accessory.kind}`;
76
+ }
77
+ /** True when a cached context describes the same accessory as a resolved one. */
78
+ function hasAccessoryIdentity(context, accessory) {
79
+ return context.kind === accessory.kind && context.deviceId === accessory.deviceId;
80
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Copyright (c) 2026 tbaur
3
+ *
4
+ * Licensed under the Apache License, Version 2.0
5
+ * See LICENSE file for full license text
6
+ *
7
+ * @fileoverview API barrel.
8
+ */
9
+ export * from './channel';
10
+ export * from './http';
11
+ export * from './identity';
12
+ export * from './mqtt';
13
+ export * from './mqtt-codec';
14
+ export * from './protocol';
15
+ export * from './rest';
16
+ export * from './sigv4';
17
+ export * from './topics';
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 tbaur
4
+ *
5
+ * Licensed under the Apache License, Version 2.0
6
+ * See LICENSE file for full license text
7
+ *
8
+ * @fileoverview API barrel.
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
22
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
23
+ };
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ __exportStar(require("./channel"), exports);
26
+ __exportStar(require("./http"), exports);
27
+ __exportStar(require("./identity"), exports);
28
+ __exportStar(require("./mqtt"), exports);
29
+ __exportStar(require("./mqtt-codec"), exports);
30
+ __exportStar(require("./protocol"), exports);
31
+ __exportStar(require("./rest"), exports);
32
+ __exportStar(require("./sigv4"), exports);
33
+ __exportStar(require("./topics"), exports);
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Copyright (c) 2026 tbaur
3
+ *
4
+ * Licensed under the Apache License, Version 2.0
5
+ * See LICENSE file for full license text
6
+ *
7
+ * @fileoverview MQTT 3.1.1 packet encoding and decoding, for the subset AWS
8
+ * IoT needs.
9
+ *
10
+ * Writing this rather than taking a dependency was a deliberate call, and the
11
+ * reasoning belongs next to the code it justifies.
12
+ *
13
+ * `mqtt.js` rebuilds the WebSocket path from its own parsed options, which
14
+ * discards the SigV4 query string, so the upgrade is rejected or hangs. The
15
+ * documented way round is `transformWsUrl`, a hook that exists to undo what the
16
+ * library just did. That is a bad sign for the one part of this plugin where a
17
+ * subtle failure means a user's heating silently stops responding.
18
+ *
19
+ * AWS IoT speaks 3.1.1, this plugin needs seven packet types, and the wire
20
+ * format is a fixed header, a variable-length integer and length-prefixed
21
+ * strings. It is a few hundred lines with no I/O in it, which makes it
22
+ * exhaustively testable without a socket. The family this plugin belongs to
23
+ * already hand-rolls a capped XML reader for the same reason. The dependency
24
+ * it replaces brings roughly twenty transitive packages into a plugin that
25
+ * handles a cloud password, which the security posture would rather not carry.
26
+ *
27
+ * What is deliberately absent: QoS 2 (AWS IoT does not support it), retained
28
+ * message handling beyond the flag, topic aliases and MQTT 5 properties. If a
29
+ * future NaviLink protocol needs any of them, add it here with a test rather
30
+ * than reaching back for the library.
31
+ *
32
+ * Section references are to the OASIS MQTT 3.1.1 specification.
33
+ */
34
+ /** Packet type codes (§2.2.1). */
35
+ export declare const PacketType: {
36
+ readonly CONNECT: 1;
37
+ readonly CONNACK: 2;
38
+ readonly PUBLISH: 3;
39
+ readonly PUBACK: 4;
40
+ readonly SUBSCRIBE: 8;
41
+ readonly SUBACK: 9;
42
+ readonly PINGREQ: 12;
43
+ readonly PINGRESP: 13;
44
+ readonly DISCONNECT: 14;
45
+ };
46
+ /**
47
+ * Largest packet accepted from the broker.
48
+ *
49
+ * A status frame is a couple of kilobytes. 256 KiB is generous while keeping a
50
+ * malfunctioning or hostile broker from growing the heap: the remaining-length
51
+ * field can encode 256 MB, and a decoder that trusts it will happily buffer
52
+ * that much before noticing.
53
+ */
54
+ export declare const MAX_PACKET_BYTES = 262144;
55
+ /** A message the broker delivered to us. */
56
+ export interface PublishPacket {
57
+ type: typeof PacketType.PUBLISH;
58
+ topic: string;
59
+ payload: Uint8Array;
60
+ qos: 0 | 1;
61
+ retain: boolean;
62
+ dup: boolean;
63
+ /** Present for QoS 1, which must be acknowledged. */
64
+ packetId?: number;
65
+ }
66
+ /** The broker's answer to our CONNECT. */
67
+ export interface ConnackPacket {
68
+ type: typeof PacketType.CONNACK;
69
+ sessionPresent: boolean;
70
+ returnCode: number;
71
+ }
72
+ /** The broker's acknowledgement of a QoS 1 PUBLISH we sent. */
73
+ export interface PubackPacket {
74
+ type: typeof PacketType.PUBACK;
75
+ packetId: number;
76
+ }
77
+ /** The broker's answer to a SUBSCRIBE. */
78
+ export interface SubackPacket {
79
+ type: typeof PacketType.SUBACK;
80
+ packetId: number;
81
+ /** One per requested topic: the granted QoS, or 0x80 for a refusal. */
82
+ returnCodes: number[];
83
+ }
84
+ /** The broker's answer to a PINGREQ. */
85
+ export interface PingrespPacket {
86
+ type: typeof PacketType.PINGRESP;
87
+ }
88
+ /** Any packet this decoder produces. */
89
+ export type DecodedPacket = ConnackPacket | PublishPacket | PubackPacket | SubackPacket | PingrespPacket;
90
+ /** A topic filter and the QoS to request for it. */
91
+ export interface Subscription {
92
+ topic: string;
93
+ qos: 0 | 1;
94
+ }
95
+ /** A last-will message, published by the broker if we disappear. */
96
+ export interface WillMessage {
97
+ topic: string;
98
+ payload: string;
99
+ qos: 0 | 1;
100
+ retain: boolean;
101
+ }
102
+ /** Everything CONNECT carries. */
103
+ export interface ConnectOptions {
104
+ clientId: string;
105
+ keepaliveSec: number;
106
+ cleanSession: boolean;
107
+ username?: string;
108
+ password?: string;
109
+ will?: WillMessage;
110
+ }
111
+ /** Human-readable text for a CONNACK return code. */
112
+ export declare function describeConnackReturnCode(code: number): string;
113
+ /** True when a SUBACK return code means the subscription was refused (§3.9.3). */
114
+ export declare function isSubscribeFailure(code: number): boolean;
115
+ /**
116
+ * Encode the remaining-length field (§2.2.3).
117
+ *
118
+ * A base-128 varint, little-endian, with the top bit as the continuation flag
119
+ * and a hard limit of four bytes.
120
+ */
121
+ export declare function encodeRemainingLength(length: number): number[];
122
+ /**
123
+ * Build a CONNECT packet (§3.1).
124
+ *
125
+ * AWS IoT authenticates from the SigV4 signature on the WebSocket URL, so the
126
+ * username and password fields are not credentials here. The username is
127
+ * optional. The verified US endpoint accepts a CONNECT without one, so this
128
+ * plugin omits it rather than inventing an SDK marker we have not recorded.
129
+ */
130
+ export declare function encodeConnect(options: ConnectOptions): Uint8Array;
131
+ /**
132
+ * Build a PUBLISH packet (§3.3).
133
+ *
134
+ * `packetId` is required for QoS 1 and forbidden for QoS 0, which the
135
+ * specification states and this enforces: a QoS 1 publish with no identifier
136
+ * cannot be acknowledged, so the caller would wait for a PUBACK that can never
137
+ * be matched.
138
+ */
139
+ export declare function encodePublish(input: {
140
+ topic: string;
141
+ payload: string;
142
+ qos: 0 | 1;
143
+ packetId?: number;
144
+ retain?: boolean;
145
+ }): Uint8Array;
146
+ /** Build a PUBACK, acknowledging a QoS 1 message the broker sent us (§3.4). */
147
+ export declare function encodePuback(packetId: number): Uint8Array;
148
+ /**
149
+ * Build a SUBSCRIBE packet (§3.8).
150
+ *
151
+ * The fixed-header flags are required to be `0b0010`; a broker must treat
152
+ * anything else as a protocol violation and close the connection.
153
+ */
154
+ export declare function encodeSubscribe(packetId: number, subscriptions: readonly Subscription[]): Uint8Array;
155
+ /** Build a PINGREQ (§3.12). */
156
+ export declare function encodePingreq(): Uint8Array;
157
+ /** Build a DISCONNECT (§3.14), which tells the broker not to send our will. */
158
+ export declare function encodeDisconnect(): Uint8Array;
159
+ /**
160
+ * Reassembles packets from a byte stream.
161
+ *
162
+ * Needed because MQTT framing and WebSocket framing are unrelated. One
163
+ * WebSocket message can carry several MQTT packets, one MQTT packet can be
164
+ * split across several WebSocket messages, and a split can fall inside the
165
+ * length field itself. A decoder that assumed one frame is one packet works
166
+ * perfectly on a quiet connection and corrupts under load, which is the worst
167
+ * possible failure schedule.
168
+ */
169
+ export declare class PacketDecoder {
170
+ private buffer;
171
+ /**
172
+ * Add received bytes and return every complete packet now available.
173
+ *
174
+ * Throws {@link ProtocolError} on a malformed stream. The caller is expected
175
+ * to treat that as fatal for the connection: once framing is lost there is no
176
+ * way to resynchronise, because MQTT has no frame delimiter to scan for.
177
+ */
178
+ push(chunk: Uint8Array): DecodedPacket[];
179
+ /** Bytes held pending the rest of a packet. Exposed for tests and diagnostics. */
180
+ get pending(): number;
181
+ }