@totemsdk/edge-coap 0.1.0 → 0.3.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 +202 -21
- package/NOTICE +60 -0
- package/README.md +201 -1
- package/dist/cbor.d.ts +22 -0
- package/dist/cbor.js +266 -0
- package/dist/cjs/cbor.d.ts +22 -0
- package/dist/cjs/cbor.js +271 -0
- package/dist/cjs/codec.d.ts +30 -0
- package/dist/cjs/codec.js +349 -0
- package/dist/cjs/gateway.d.ts +76 -0
- package/dist/cjs/gateway.js +511 -0
- package/dist/cjs/index.d.ts +13 -0
- package/dist/cjs/index.js +17 -0
- package/dist/cjs/native-transport.d.ts +33 -0
- package/dist/cjs/native-transport.js +183 -0
- package/dist/cjs/oscore.d.ts +32 -0
- package/dist/cjs/oscore.js +562 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/security.d.ts +29 -0
- package/dist/cjs/security.js +19 -0
- package/dist/cjs/sensor-bridge.d.ts +31 -0
- package/dist/cjs/sensor-bridge.js +99 -0
- package/dist/cjs/transport.d.ts +77 -0
- package/dist/cjs/transport.js +9 -0
- package/dist/codec.d.ts +30 -0
- package/dist/codec.js +339 -0
- package/dist/gateway.d.ts +61 -7
- package/dist/gateway.js +470 -67
- package/dist/index.d.ts +11 -3
- package/dist/index.js +6 -7
- package/dist/native-transport.d.ts +33 -0
- package/dist/native-transport.js +146 -0
- package/dist/oscore.d.ts +32 -0
- package/dist/oscore.js +526 -0
- package/dist/security.d.ts +29 -0
- package/dist/security.js +16 -0
- package/dist/sensor-bridge.d.ts +6 -1
- package/dist/sensor-bridge.js +73 -24
- package/dist/transport.d.ts +31 -1
- package/dist/transport.js +1 -2
- package/package.json +55 -26
- package/sidecar.json +34 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CoAP Edge Gateway — wires a CoapTransportPort into an EdgeRuntime.
|
|
4
|
+
*
|
|
5
|
+
* Requests are CON messages matched to responses by token and remote endpoint
|
|
6
|
+
* (RFC 7252 §5.3). Confirmable requests are retransmitted with exponential
|
|
7
|
+
* backoff until acknowledged or until the request deadline expires. An empty
|
|
8
|
+
* ACK keeps the request pending for a separate response, which is acknowledged
|
|
9
|
+
* before being handed to the caller.
|
|
10
|
+
*
|
|
11
|
+
* RFC 7641 observations keep a token registered after the initial response;
|
|
12
|
+
* later notifications with a fresh 24-bit Observe sequence are delivered to the
|
|
13
|
+
* caller and confirmable notifications are acknowledged.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.createCoapGateway = createCoapGateway;
|
|
17
|
+
const codec_js_1 = require("./codec.js");
|
|
18
|
+
const DEFAULT_TIMING = {
|
|
19
|
+
requestTimeoutMs: 10000,
|
|
20
|
+
ackTimeoutMs: 2000,
|
|
21
|
+
maxRetransmits: 4,
|
|
22
|
+
};
|
|
23
|
+
/** RFC 7641 §3.4 freshness window for observation sequence numbers. */
|
|
24
|
+
const OBSERVATION_FRESHNESS_MS = 128000;
|
|
25
|
+
const OBSERVATION_SEQUENCE_MODULO = 16777216;
|
|
26
|
+
const OBSERVATION_HALF_RANGE = 8388608;
|
|
27
|
+
function createCoapGateway(config) {
|
|
28
|
+
let status = 'stopped';
|
|
29
|
+
let unsubMsg;
|
|
30
|
+
let unsubErr;
|
|
31
|
+
const pending = new Map();
|
|
32
|
+
const pendingByMessageId = new Map();
|
|
33
|
+
const observations = new Map();
|
|
34
|
+
let messageId = 0;
|
|
35
|
+
function detach(key, entry) {
|
|
36
|
+
entry.settled = true;
|
|
37
|
+
clearTimeout(entry.deadline);
|
|
38
|
+
if (entry.retransmit)
|
|
39
|
+
clearTimeout(entry.retransmit);
|
|
40
|
+
entry.retransmit = undefined;
|
|
41
|
+
if (pending.get(key) === entry)
|
|
42
|
+
pending.delete(key);
|
|
43
|
+
if (pendingByMessageId.get(entry.messageId) === key)
|
|
44
|
+
pendingByMessageId.delete(entry.messageId);
|
|
45
|
+
}
|
|
46
|
+
function settle(key, entry, error, data) {
|
|
47
|
+
if (entry.settled)
|
|
48
|
+
return;
|
|
49
|
+
detach(key, entry);
|
|
50
|
+
if (error)
|
|
51
|
+
entry.resolve({ ok: false, error: error.message });
|
|
52
|
+
else
|
|
53
|
+
entry.resolve({ ok: true, data: data ?? { payload: new Uint8Array(0) } });
|
|
54
|
+
}
|
|
55
|
+
const context = {
|
|
56
|
+
transport: config.transport,
|
|
57
|
+
security: config.security,
|
|
58
|
+
pending,
|
|
59
|
+
pendingByMessageId,
|
|
60
|
+
observations,
|
|
61
|
+
nextMessageId: () => {
|
|
62
|
+
for (let candidates = 0; candidates <= 0xffff; candidates++) {
|
|
63
|
+
const id = messageId;
|
|
64
|
+
messageId = (messageId + 1) & 0xffff;
|
|
65
|
+
if (!pendingByMessageId.has(id))
|
|
66
|
+
return id;
|
|
67
|
+
}
|
|
68
|
+
throw new Error('No free CoAP message ids');
|
|
69
|
+
},
|
|
70
|
+
timing: {
|
|
71
|
+
requestTimeoutMs: config.timing?.requestTimeoutMs ?? DEFAULT_TIMING.requestTimeoutMs,
|
|
72
|
+
ackTimeoutMs: config.timing?.ackTimeoutMs ?? DEFAULT_TIMING.ackTimeoutMs,
|
|
73
|
+
maxRetransmits: config.timing?.maxRetransmits ?? DEFAULT_TIMING.maxRetransmits,
|
|
74
|
+
},
|
|
75
|
+
settle,
|
|
76
|
+
};
|
|
77
|
+
const blockwise = {
|
|
78
|
+
enabled: config.blockwise?.enabled ?? true,
|
|
79
|
+
blockSize: normalizeBlockSize(config.blockwise?.blockSize ?? 1024),
|
|
80
|
+
maxBodySize: config.blockwise?.maxBodySize ?? 1024 * 1024,
|
|
81
|
+
};
|
|
82
|
+
if (!Number.isInteger(blockwise.maxBodySize) || blockwise.maxBodySize < 16) {
|
|
83
|
+
throw new Error('Invalid blockwise maxBodySize');
|
|
84
|
+
}
|
|
85
|
+
function findPending(remote, tokenKey, msgId) {
|
|
86
|
+
const key = tokenKey ?? (msgId === undefined ? undefined : pendingByMessageId.get(msgId));
|
|
87
|
+
if (!key)
|
|
88
|
+
return undefined;
|
|
89
|
+
const entry = pending.get(key);
|
|
90
|
+
if (!entry)
|
|
91
|
+
return undefined;
|
|
92
|
+
if (entry.host !== remote.host || entry.port !== remote.port)
|
|
93
|
+
return undefined;
|
|
94
|
+
return { key, entry };
|
|
95
|
+
}
|
|
96
|
+
function acknowledge(remote, msgId) {
|
|
97
|
+
const ack = (0, codec_js_1.encodeCoap)({
|
|
98
|
+
type: 'ACK', messageId: msgId, token: new Uint8Array(0), path: [],
|
|
99
|
+
payload: new Uint8Array(0), remote, receivedAt: Date.now(),
|
|
100
|
+
});
|
|
101
|
+
void Promise.resolve(config.transport.send(remote.host, remote.port, ack)).catch(() => { });
|
|
102
|
+
}
|
|
103
|
+
async function cancelObservation(key) {
|
|
104
|
+
const observation = observations.get(key);
|
|
105
|
+
if (!observation)
|
|
106
|
+
return;
|
|
107
|
+
observations.delete(key);
|
|
108
|
+
const id = context.nextMessageId();
|
|
109
|
+
let message = {
|
|
110
|
+
type: 'CON', method: 'GET', messageId: id, token: observation.token, path: observation.path,
|
|
111
|
+
payload: new Uint8Array(0), observe: 1,
|
|
112
|
+
remote: { host: observation.host, port: observation.port }, receivedAt: Date.now(),
|
|
113
|
+
};
|
|
114
|
+
if (config.security?.protects(observation.host, observation.port)) {
|
|
115
|
+
try {
|
|
116
|
+
message = await config.security.protect(message);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
await config.transport.send(observation.host, observation.port, (0, codec_js_1.encodeCoap)(message));
|
|
124
|
+
}
|
|
125
|
+
catch { /* deregistration is best effort */ }
|
|
126
|
+
}
|
|
127
|
+
function registerObservation(key, entry, msg) {
|
|
128
|
+
detach(key, entry);
|
|
129
|
+
const observation = {
|
|
130
|
+
token: entry.frame.slice(4, 4 + (entry.frame[0] & 0x0f)),
|
|
131
|
+
path: [...entry.observation.path],
|
|
132
|
+
host: entry.host,
|
|
133
|
+
port: entry.port,
|
|
134
|
+
handler: entry.observation.handler,
|
|
135
|
+
lastSequence: msg.observe,
|
|
136
|
+
lastReceivedAt: msg.receivedAt,
|
|
137
|
+
};
|
|
138
|
+
observations.set(key, observation);
|
|
139
|
+
entry.resolve({
|
|
140
|
+
ok: true,
|
|
141
|
+
data: { token: key, cancel: () => cancelObservation(key) },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function deliverNotification(key, observation, msg) {
|
|
145
|
+
if (observation.host !== msg.remote.host || observation.port !== msg.remote.port)
|
|
146
|
+
return;
|
|
147
|
+
if (msg.type === 'RST') {
|
|
148
|
+
observations.delete(key);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (msg.type === 'CON')
|
|
152
|
+
acknowledge(msg.remote, msg.messageId);
|
|
153
|
+
const codeClass = Number(msg.responseCode?.split('.')[0] ?? 0);
|
|
154
|
+
if (codeClass !== 2 || msg.observe === undefined) {
|
|
155
|
+
// A terminal error or a response without Observe ends the relation.
|
|
156
|
+
observations.delete(key);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (!isFreshObservation(observation.lastSequence, observation.lastReceivedAt, msg.observe, msg.receivedAt))
|
|
160
|
+
return;
|
|
161
|
+
observation.lastSequence = msg.observe;
|
|
162
|
+
observation.lastReceivedAt = msg.receivedAt;
|
|
163
|
+
status = 'running';
|
|
164
|
+
try {
|
|
165
|
+
observation.handler({
|
|
166
|
+
payload: msg.payload,
|
|
167
|
+
contentFormat: msg.contentFormat,
|
|
168
|
+
observe: msg.observe,
|
|
169
|
+
receivedAt: msg.receivedAt,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
catch { /* consumer failure must not break the transport */ }
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
get status() { return status; },
|
|
176
|
+
async start() {
|
|
177
|
+
if (status === 'running')
|
|
178
|
+
return;
|
|
179
|
+
await config.transport.bind(config.localPort);
|
|
180
|
+
unsubMsg = config.transport.onMessage((raw, remote) => {
|
|
181
|
+
void (async () => {
|
|
182
|
+
let msg;
|
|
183
|
+
try {
|
|
184
|
+
msg = (0, codec_js_1.decodeCoap)(raw, remote);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// Fail closed at the security boundary: protected endpoints must send
|
|
190
|
+
// OSCORE (empty ACK/RST is exempt), and OSCORE from an unconfigured
|
|
191
|
+
// endpoint is never treated as plaintext.
|
|
192
|
+
const control = msg.token.length === 0 && (msg.type === 'ACK' || msg.type === 'RST') && msg.responseCode === undefined;
|
|
193
|
+
const protectedEndpoint = config.security?.protects(remote.host, remote.port) ?? false;
|
|
194
|
+
if (msg.oscore !== undefined) {
|
|
195
|
+
if (!protectedEndpoint || !config.security)
|
|
196
|
+
return;
|
|
197
|
+
try {
|
|
198
|
+
msg = await config.security.unprotect(msg);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else if (protectedEndpoint && !control) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (msg.token.length === 4) {
|
|
208
|
+
const key = (0, codec_js_1.tokenToKey)(msg.token);
|
|
209
|
+
const match = findPending(remote, key);
|
|
210
|
+
if (match) {
|
|
211
|
+
if (msg.type === 'RST') {
|
|
212
|
+
settle(match.key, match.entry, new Error('CoAP reset'));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (msg.type === 'CON')
|
|
216
|
+
acknowledge(msg.remote, msg.messageId);
|
|
217
|
+
const data = { payload: msg.payload };
|
|
218
|
+
if (msg.contentFormat !== undefined)
|
|
219
|
+
data.contentFormat = msg.contentFormat;
|
|
220
|
+
if (msg.observe !== undefined)
|
|
221
|
+
data.observe = msg.observe;
|
|
222
|
+
if (msg.block1 !== undefined)
|
|
223
|
+
data.block1 = msg.block1;
|
|
224
|
+
if (msg.block2 !== undefined)
|
|
225
|
+
data.block2 = msg.block2;
|
|
226
|
+
if (msg.size1 !== undefined)
|
|
227
|
+
data.size1 = msg.size1;
|
|
228
|
+
if (msg.size2 !== undefined)
|
|
229
|
+
data.size2 = msg.size2;
|
|
230
|
+
if (match.entry.observation) {
|
|
231
|
+
if (msg.observe === undefined) {
|
|
232
|
+
settle(match.key, match.entry, new Error('CoAP server did not establish the observation'));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const codeClass = Number(msg.responseCode?.split('.')[0] ?? 0);
|
|
236
|
+
if (codeClass !== 2) {
|
|
237
|
+
settle(match.key, match.entry, new Error(`CoAP error response ${msg.responseCode ?? 'unknown'}`));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
registerObservation(match.key, match.entry, msg);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const codeClass = Number(msg.responseCode?.split('.')[0] ?? 0);
|
|
244
|
+
if (codeClass !== 2) {
|
|
245
|
+
settle(match.key, match.entry, new Error(`CoAP error response ${msg.responseCode ?? 'unknown'}`));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
status = 'running';
|
|
249
|
+
settle(match.key, match.entry, undefined, data);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const observation = observations.get(key);
|
|
253
|
+
if (observation)
|
|
254
|
+
deliverNotification(key, observation, msg);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// Empty ACK/RST messages carry no token and are correlated by message id.
|
|
258
|
+
if (msg.token.length !== 0 || (msg.type !== 'ACK' && msg.type !== 'RST'))
|
|
259
|
+
return;
|
|
260
|
+
const match = findPending(remote, undefined, msg.messageId);
|
|
261
|
+
if (!match)
|
|
262
|
+
return;
|
|
263
|
+
if (msg.type === 'RST') {
|
|
264
|
+
settle(match.key, match.entry, new Error('CoAP reset'));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (msg.responseCode !== undefined)
|
|
268
|
+
return;
|
|
269
|
+
match.entry.acknowledged = true;
|
|
270
|
+
if (match.entry.retransmit)
|
|
271
|
+
clearTimeout(match.entry.retransmit);
|
|
272
|
+
match.entry.retransmit = undefined;
|
|
273
|
+
})().catch(() => { });
|
|
274
|
+
});
|
|
275
|
+
unsubErr = config.transport.onError((err) => {
|
|
276
|
+
status = 'error';
|
|
277
|
+
for (const [key, entry] of [...pending]) {
|
|
278
|
+
settle(key, entry, new Error(`CoAP transport error: ${err.message}`));
|
|
279
|
+
}
|
|
280
|
+
observations.clear();
|
|
281
|
+
});
|
|
282
|
+
status = 'running';
|
|
283
|
+
},
|
|
284
|
+
async stop() {
|
|
285
|
+
unsubMsg?.();
|
|
286
|
+
unsubErr?.();
|
|
287
|
+
unsubMsg = undefined;
|
|
288
|
+
unsubErr = undefined;
|
|
289
|
+
for (const [key, entry] of [...pending])
|
|
290
|
+
settle(key, entry, new Error('Gateway stopped'));
|
|
291
|
+
pending.clear();
|
|
292
|
+
pendingByMessageId.clear();
|
|
293
|
+
observations.clear();
|
|
294
|
+
await config.transport.close();
|
|
295
|
+
status = 'stopped';
|
|
296
|
+
},
|
|
297
|
+
async get(path, host, port) {
|
|
298
|
+
const first = await coapRequest(context, 'GET', path, new Uint8Array(0), host, port);
|
|
299
|
+
return blockwise.enabled
|
|
300
|
+
? followBlock2(context, first, 'GET', path, new Uint8Array(0), host, port, blockwise.maxBodySize)
|
|
301
|
+
: first;
|
|
302
|
+
},
|
|
303
|
+
async post(path, payload, host, port) {
|
|
304
|
+
if (!blockwise.enabled) {
|
|
305
|
+
return coapRequest(context, 'POST', path, payload, host, port);
|
|
306
|
+
}
|
|
307
|
+
const first = payload.length > blockwise.blockSize
|
|
308
|
+
? await uploadBlockwise(context, path, payload, host, port, blockwise.blockSize)
|
|
309
|
+
: await coapRequest(context, 'POST', path, payload, host, port);
|
|
310
|
+
return followBlock2(context, first, 'POST', path, new Uint8Array(0), host, port, blockwise.maxBodySize);
|
|
311
|
+
},
|
|
312
|
+
async observe(path, host, port, handler) {
|
|
313
|
+
return coapRequest(context, 'GET', path, new Uint8Array(0), host, port, {
|
|
314
|
+
observation: { path: [...path], handler },
|
|
315
|
+
});
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function isFreshObservation(previous, receivedAt, next, now) {
|
|
320
|
+
if (now - receivedAt > OBSERVATION_FRESHNESS_MS)
|
|
321
|
+
return true;
|
|
322
|
+
const difference = ((next - previous) % OBSERVATION_SEQUENCE_MODULO + OBSERVATION_SEQUENCE_MODULO) % OBSERVATION_SEQUENCE_MODULO;
|
|
323
|
+
return difference !== 0 && difference < OBSERVATION_HALF_RANGE;
|
|
324
|
+
}
|
|
325
|
+
async function coapRequest(context, method, path, payload, host, port, options = {}) {
|
|
326
|
+
if (!path.length || path.some((segment) => !segment || segment.includes('/'))) {
|
|
327
|
+
return { ok: false, error: 'Invalid CoAP URI path' };
|
|
328
|
+
}
|
|
329
|
+
const { transport, pending, pendingByMessageId, timing, settle } = context;
|
|
330
|
+
let id;
|
|
331
|
+
let token;
|
|
332
|
+
let key;
|
|
333
|
+
let frame;
|
|
334
|
+
try {
|
|
335
|
+
id = context.nextMessageId();
|
|
336
|
+
token = uniqueToken(pending, context.observations);
|
|
337
|
+
key = (0, codec_js_1.tokenToKey)(token);
|
|
338
|
+
let message = {
|
|
339
|
+
type: 'CON', method, messageId: id, token, path, payload,
|
|
340
|
+
observe: options.observation ? 0 : undefined,
|
|
341
|
+
...(options.block1 ? { block1: options.block1 } : {}),
|
|
342
|
+
...(options.block2 ? { block2: options.block2 } : {}),
|
|
343
|
+
...(options.size1 !== undefined ? { size1: options.size1 } : {}),
|
|
344
|
+
...(options.accept !== undefined ? { accept: options.accept } : {}),
|
|
345
|
+
remote: { host, port }, receivedAt: Date.now(),
|
|
346
|
+
};
|
|
347
|
+
if (context.security?.protects(host, port)) {
|
|
348
|
+
try {
|
|
349
|
+
message = await context.security.protect(message);
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
return { ok: false, error: `CoAP protection failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
frame = (0, codec_js_1.encodeCoap)(message);
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
359
|
+
}
|
|
360
|
+
function scheduleRetransmit(entryKey, entry) {
|
|
361
|
+
if (entry.acknowledged || entry.attempts >= context.timing.maxRetransmits)
|
|
362
|
+
return;
|
|
363
|
+
entry.retransmit = setTimeout(() => {
|
|
364
|
+
if (entry.settled || entry.acknowledged)
|
|
365
|
+
return;
|
|
366
|
+
if (pending.get(entryKey) !== entry)
|
|
367
|
+
return;
|
|
368
|
+
entry.attempts++;
|
|
369
|
+
void Promise.resolve(transport.send(entry.host, entry.port, entry.frame)).catch(() => { });
|
|
370
|
+
entry.intervalMs *= 2;
|
|
371
|
+
scheduleRetransmit(entryKey, entry);
|
|
372
|
+
}, entry.intervalMs);
|
|
373
|
+
}
|
|
374
|
+
return new Promise((resolve) => {
|
|
375
|
+
const entry = {
|
|
376
|
+
messageId: id,
|
|
377
|
+
frame, host, port,
|
|
378
|
+
resolve,
|
|
379
|
+
deadline: setTimeout(() => settle(key, entry, new Error('CoAP request timed out')), timing.requestTimeoutMs),
|
|
380
|
+
intervalMs: timing.ackTimeoutMs,
|
|
381
|
+
attempts: 0,
|
|
382
|
+
acknowledged: false,
|
|
383
|
+
settled: false,
|
|
384
|
+
...(options.observation ? { observation: options.observation } : {}),
|
|
385
|
+
};
|
|
386
|
+
pending.set(key, entry);
|
|
387
|
+
pendingByMessageId.set(id, key);
|
|
388
|
+
if (timing.ackTimeoutMs > 0)
|
|
389
|
+
scheduleRetransmit(key, entry);
|
|
390
|
+
void Promise.resolve(transport.send(host, port, frame)).catch((error) => {
|
|
391
|
+
settle(key, entry, error instanceof Error ? error : new Error(String(error)));
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* RFC 7252 §5.3.1 expects client tokens to be hard for an off-path attacker to
|
|
397
|
+
* guess. Use WebCrypto when the host provides it; otherwise mix a per-gateway
|
|
398
|
+
* seed so tokens are not simply sequential message ids.
|
|
399
|
+
*/
|
|
400
|
+
const tokenSeed = Math.floor(Math.random() * 0xffffffff);
|
|
401
|
+
function uniqueToken(pending, observations) {
|
|
402
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
403
|
+
const token = createToken(attempt);
|
|
404
|
+
const key = (0, codec_js_1.tokenToKey)(token);
|
|
405
|
+
if (!pending.has(key) && !observations.has(key))
|
|
406
|
+
return token;
|
|
407
|
+
}
|
|
408
|
+
throw new Error('Unable to allocate a unique CoAP token');
|
|
409
|
+
}
|
|
410
|
+
function createToken(messageId) {
|
|
411
|
+
const token = new Uint8Array(4);
|
|
412
|
+
const webCrypto = globalThis.crypto;
|
|
413
|
+
if (typeof webCrypto?.getRandomValues === 'function') {
|
|
414
|
+
webCrypto.getRandomValues(token);
|
|
415
|
+
return token;
|
|
416
|
+
}
|
|
417
|
+
const value = (tokenSeed ^ Math.imul(messageId + 1, 0x9e3779b1)) >>> 0;
|
|
418
|
+
new DataView(token.buffer).setUint32(0, value, false);
|
|
419
|
+
return token;
|
|
420
|
+
}
|
|
421
|
+
function normalizeBlockSize(size) {
|
|
422
|
+
if (!Number.isInteger(size) || size < 16 || size > 1024 || (size & (size - 1)) !== 0) {
|
|
423
|
+
throw new Error(`Invalid blockwise block size ${size}; expected a power of two from 16 to 1024`);
|
|
424
|
+
}
|
|
425
|
+
return size;
|
|
426
|
+
}
|
|
427
|
+
function blockSizeExponent(size) {
|
|
428
|
+
return Math.log2(size) - 4;
|
|
429
|
+
}
|
|
430
|
+
async function uploadBlockwise(context, path, payload, host, port, blockSize) {
|
|
431
|
+
let sizeExponent = blockSizeExponent(blockSize);
|
|
432
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
433
|
+
const outcome = await uploadPass(context, path, payload, host, port, sizeExponent);
|
|
434
|
+
if (outcome.retryExponent === undefined) {
|
|
435
|
+
return outcome.result ?? { ok: false, error: 'Empty blockwise upload' };
|
|
436
|
+
}
|
|
437
|
+
sizeExponent = outcome.retryExponent;
|
|
438
|
+
}
|
|
439
|
+
return { ok: false, error: 'Blockwise size renegotiation failed' };
|
|
440
|
+
}
|
|
441
|
+
async function uploadPass(context, path, payload, host, port, sizeExponent) {
|
|
442
|
+
const blockSize = 2 ** (sizeExponent + 4);
|
|
443
|
+
let offset = 0;
|
|
444
|
+
let num = 0;
|
|
445
|
+
let last;
|
|
446
|
+
while (offset < payload.length) {
|
|
447
|
+
const chunk = payload.slice(offset, Math.min(offset + blockSize, payload.length));
|
|
448
|
+
const more = offset + chunk.length < payload.length;
|
|
449
|
+
const result = await coapRequest(context, 'POST', path, chunk, host, port, {
|
|
450
|
+
block1: { num, more, sizeExponent },
|
|
451
|
+
size1: payload.length,
|
|
452
|
+
});
|
|
453
|
+
if (!result.ok)
|
|
454
|
+
return { result };
|
|
455
|
+
if (more) {
|
|
456
|
+
const acknowledgement = result.data?.block1;
|
|
457
|
+
if (!acknowledgement)
|
|
458
|
+
return { result: { ok: false, error: 'Server did not acknowledge the blockwise upload' } };
|
|
459
|
+
if (acknowledgement.num !== num)
|
|
460
|
+
return { result: { ok: false, error: `Unexpected Block1 number ${acknowledgement.num}` } };
|
|
461
|
+
if (acknowledgement.sizeExponent < sizeExponent) {
|
|
462
|
+
// The server selected a smaller block size; restart the transfer.
|
|
463
|
+
return { retryExponent: acknowledgement.sizeExponent };
|
|
464
|
+
}
|
|
465
|
+
if (acknowledgement.sizeExponent > sizeExponent) {
|
|
466
|
+
return { result: { ok: false, error: 'Server increased the Block1 size mid-transfer' } };
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
last = result;
|
|
470
|
+
offset += chunk.length;
|
|
471
|
+
num++;
|
|
472
|
+
}
|
|
473
|
+
return { result: last };
|
|
474
|
+
}
|
|
475
|
+
async function followBlock2(context, first, method, path, body, host, port, maxBodySize) {
|
|
476
|
+
if (!first.ok || !first.data?.block2?.more)
|
|
477
|
+
return first;
|
|
478
|
+
const sizeExponent = first.data.block2.sizeExponent;
|
|
479
|
+
let assembled = first.data.payload.slice();
|
|
480
|
+
let expected = first.data.block2.num + 1;
|
|
481
|
+
let current = first.data;
|
|
482
|
+
if (assembled.length > maxBodySize)
|
|
483
|
+
return { ok: false, error: 'Blockwise response exceeds maxBodySize' };
|
|
484
|
+
while (current.block2?.more) {
|
|
485
|
+
const next = await coapRequest(context, method, path, method === 'POST' ? new Uint8Array(0) : body, host, port, {
|
|
486
|
+
block2: { num: expected, more: false, sizeExponent },
|
|
487
|
+
});
|
|
488
|
+
if (!next.ok || !next.data)
|
|
489
|
+
return next;
|
|
490
|
+
if (!next.data.block2)
|
|
491
|
+
return { ok: false, error: 'Blockwise response is missing Block2' };
|
|
492
|
+
if (next.data.block2.num !== expected) {
|
|
493
|
+
return { ok: false, error: `Unexpected Block2 number ${next.data.block2.num}` };
|
|
494
|
+
}
|
|
495
|
+
if (next.data.block2.sizeExponent !== sizeExponent) {
|
|
496
|
+
return { ok: false, error: 'Server changed the Block2 size mid-transfer' };
|
|
497
|
+
}
|
|
498
|
+
assembled = concatBytes(assembled, next.data.payload);
|
|
499
|
+
if (assembled.length > maxBodySize)
|
|
500
|
+
return { ok: false, error: 'Blockwise response exceeds maxBodySize' };
|
|
501
|
+
current = next.data;
|
|
502
|
+
expected++;
|
|
503
|
+
}
|
|
504
|
+
return { ok: true, data: { ...current, payload: assembled } };
|
|
505
|
+
}
|
|
506
|
+
function concatBytes(first, second) {
|
|
507
|
+
const out = new Uint8Array(first.length + second.length);
|
|
508
|
+
out.set(first, 0);
|
|
509
|
+
out.set(second, first.length);
|
|
510
|
+
return out;
|
|
511
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type { CoapTransportPort, CoapMessage, CoapMessageType, CoapMethod, CoapBlockOption } from './transport.js';
|
|
2
|
+
export { createCoapGateway } from './gateway.js';
|
|
3
|
+
export type { CoapGatewayConfig, CoapGateway, CoapResponseData, CoapObservationData, CoapObservation, CoapBlockwiseOptions, } from './gateway.js';
|
|
4
|
+
export { decodeCbor, CborDecodeError } from './cbor.js';
|
|
5
|
+
export type { CborValue, CborDecodeOptions } from './cbor.js';
|
|
6
|
+
export { isOscoreProtected } from './security.js';
|
|
7
|
+
export type { CoapSecurityPort } from './security.js';
|
|
8
|
+
export { createOscoreContext } from './oscore.js';
|
|
9
|
+
export type { OscoreContextConfig } from './oscore.js';
|
|
10
|
+
export { createCoapSensorBridge, APPLICATION_CBOR } from './sensor-bridge.js';
|
|
11
|
+
export type { CoapSensorBinding, CoapSensorBridgeConfig, CoapSensorBridge } from './sensor-bridge.js';
|
|
12
|
+
export { NativeCoapTransport } from './native-transport.js';
|
|
13
|
+
export type { NativeCoapConfig } from './native-transport.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NativeCoapTransport = exports.APPLICATION_CBOR = exports.createCoapSensorBridge = exports.createOscoreContext = exports.isOscoreProtected = exports.CborDecodeError = exports.decodeCbor = exports.createCoapGateway = void 0;
|
|
4
|
+
var gateway_js_1 = require("./gateway.js");
|
|
5
|
+
Object.defineProperty(exports, "createCoapGateway", { enumerable: true, get: function () { return gateway_js_1.createCoapGateway; } });
|
|
6
|
+
var cbor_js_1 = require("./cbor.js");
|
|
7
|
+
Object.defineProperty(exports, "decodeCbor", { enumerable: true, get: function () { return cbor_js_1.decodeCbor; } });
|
|
8
|
+
Object.defineProperty(exports, "CborDecodeError", { enumerable: true, get: function () { return cbor_js_1.CborDecodeError; } });
|
|
9
|
+
var security_js_1 = require("./security.js");
|
|
10
|
+
Object.defineProperty(exports, "isOscoreProtected", { enumerable: true, get: function () { return security_js_1.isOscoreProtected; } });
|
|
11
|
+
var oscore_js_1 = require("./oscore.js");
|
|
12
|
+
Object.defineProperty(exports, "createOscoreContext", { enumerable: true, get: function () { return oscore_js_1.createOscoreContext; } });
|
|
13
|
+
var sensor_bridge_js_1 = require("./sensor-bridge.js");
|
|
14
|
+
Object.defineProperty(exports, "createCoapSensorBridge", { enumerable: true, get: function () { return sensor_bridge_js_1.createCoapSensorBridge; } });
|
|
15
|
+
Object.defineProperty(exports, "APPLICATION_CBOR", { enumerable: true, get: function () { return sensor_bridge_js_1.APPLICATION_CBOR; } });
|
|
16
|
+
var native_transport_js_1 = require("./native-transport.js");
|
|
17
|
+
Object.defineProperty(exports, "NativeCoapTransport", { enumerable: true, get: function () { return native_transport_js_1.NativeCoapTransport; } });
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NativeCoapTransport — connects to the edge-coap Go binary over TCP.
|
|
3
|
+
*
|
|
4
|
+
* The Go binary listens on a local TCP socket (default 127.0.0.1:15003) and
|
|
5
|
+
* accepts newline-delimited JSON send requests. Inbound datagrams are pushed
|
|
6
|
+
* back as `message` events; token correlation stays in the gateway.
|
|
7
|
+
*/
|
|
8
|
+
import type { CoapTransportPort } from './transport.js';
|
|
9
|
+
export interface NativeCoapConfig {
|
|
10
|
+
host?: string;
|
|
11
|
+
port?: number;
|
|
12
|
+
connectTimeoutMs?: number;
|
|
13
|
+
requestTimeoutMs?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare class NativeCoapTransport implements CoapTransportPort {
|
|
16
|
+
private socket;
|
|
17
|
+
private buffer;
|
|
18
|
+
private pending;
|
|
19
|
+
private messageHandlers;
|
|
20
|
+
private errorHandlers;
|
|
21
|
+
private readonly config;
|
|
22
|
+
private closedByUser;
|
|
23
|
+
constructor(config?: NativeCoapConfig);
|
|
24
|
+
private emitError;
|
|
25
|
+
bind(_port: number): Promise<void>;
|
|
26
|
+
close(): Promise<void>;
|
|
27
|
+
send(host: string, port: number, message: Uint8Array): Promise<void>;
|
|
28
|
+
onMessage(handler: (message: Uint8Array, remote: {
|
|
29
|
+
host: string;
|
|
30
|
+
port: number;
|
|
31
|
+
}) => void): () => void;
|
|
32
|
+
onError(handler: (err: Error) => void): () => void;
|
|
33
|
+
}
|