@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.
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ /**
3
+ * CoAP Sensor Bridge — maps CoAP resources and RFC 7641 observations to Edge
4
+ * proof inputs. CBOR payloads (content-format 60) are decoded into structured
5
+ * claims; malformed CBOR is not signed as sensor data.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.APPLICATION_CBOR = void 0;
9
+ exports.createCoapSensorBridge = createCoapSensorBridge;
10
+ const cbor_js_1 = require("./cbor.js");
11
+ exports.APPLICATION_CBOR = 60;
12
+ function createCoapSensorBridge(config) {
13
+ const timers = [];
14
+ const observations = [];
15
+ let running = false;
16
+ function claimsFor(binding, data, receivedAt = Date.now()) {
17
+ const claim = {
18
+ sensorId: binding.sensorId,
19
+ path: binding.path.join('/'),
20
+ dataType: binding.dataType ?? 'coap-observation',
21
+ payload: data.payload,
22
+ timestamp: receivedAt,
23
+ };
24
+ if (data.contentFormat !== undefined)
25
+ claim.contentFormat = data.contentFormat;
26
+ if (data.observe !== undefined)
27
+ claim.observe = data.observe;
28
+ if (data.contentFormat === exports.APPLICATION_CBOR) {
29
+ try {
30
+ claim.cbor = (0, cbor_js_1.decodeCbor)(data.payload);
31
+ }
32
+ catch {
33
+ // Do not sign a payload that claims to be CBOR but does not decode.
34
+ return undefined;
35
+ }
36
+ }
37
+ return claim;
38
+ }
39
+ async function emitProof(binding, data, receivedAt) {
40
+ const proof = config.runtime.ports.proof;
41
+ if (!proof)
42
+ return;
43
+ const claim = claimsFor(binding, data, receivedAt);
44
+ if (!claim)
45
+ return;
46
+ await proof.createProof({ subject: `sensor:${binding.sensorId}`, claims: [claim] });
47
+ }
48
+ async function pollBinding(binding) {
49
+ try {
50
+ const gw = config.gateway;
51
+ if (!gw)
52
+ return;
53
+ const result = await gw.get(binding.path, binding.host, binding.port);
54
+ if (result.ok && result.data)
55
+ await emitProof(binding, result.data);
56
+ }
57
+ catch { /* non-fatal */ }
58
+ }
59
+ async function observeBinding(binding) {
60
+ const gw = config.gateway;
61
+ if (!gw)
62
+ return false;
63
+ const result = await gw.observe(binding.path, binding.host, binding.port, (notification) => {
64
+ void emitProof(binding, notification, notification.receivedAt).catch(() => { });
65
+ });
66
+ if (!result.ok || !result.data)
67
+ return false;
68
+ observations.push(result.data);
69
+ return true;
70
+ }
71
+ return {
72
+ async start() {
73
+ if (running)
74
+ return;
75
+ for (const binding of config.bindings) {
76
+ if (binding.observe) {
77
+ const registered = await observeBinding(binding).catch(() => false);
78
+ if (registered)
79
+ continue;
80
+ }
81
+ await pollBinding(binding);
82
+ timers.push(setInterval(() => pollBinding(binding), binding.intervalMs));
83
+ }
84
+ running = true;
85
+ },
86
+ async stop() {
87
+ for (const timer of timers)
88
+ clearInterval(timer);
89
+ timers.length = 0;
90
+ const active = observations.splice(0);
91
+ await Promise.all(active.map((observation) => observation.cancel().catch(() => { })));
92
+ running = false;
93
+ },
94
+ async poll() {
95
+ for (const binding of config.bindings)
96
+ await pollBinding(binding);
97
+ },
98
+ };
99
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * CoAP transport port — injected by the caller.
3
+ *
4
+ * CoAP (RFC 7252) runs over UDP. The caller provides the socket.
5
+ * Messages are confirmable (CON), non-confirmable (NON),
6
+ * acknowledgement (ACK), or reset (RST).
7
+ */
8
+ export interface CoapTransportPort {
9
+ /** Bind to a local port. */
10
+ bind(port: number): Promise<void>;
11
+ /** Close the socket. */
12
+ close(): Promise<void>;
13
+ /** Send a CoAP message to a remote endpoint. */
14
+ send(host: string, port: number, message: Uint8Array): Promise<void>;
15
+ /** Register a handler for inbound CoAP messages. */
16
+ onMessage(handler: (message: Uint8Array, remote: {
17
+ host: string;
18
+ port: number;
19
+ }) => void): () => void;
20
+ /** Register a handler for socket errors. */
21
+ onError(handler: (err: Error) => void): () => void;
22
+ }
23
+ export type CoapMessageType = 'CON' | 'NON' | 'ACK' | 'RST';
24
+ export type CoapMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'FETCH';
25
+ export interface CoapBlockOption {
26
+ /** Block number. */
27
+ num: number;
28
+ /** More blocks follow. */
29
+ more: boolean;
30
+ /** Size exponent: block size is 2^(sizeExponent + 4), 16..1024 bytes. */
31
+ sizeExponent: number;
32
+ }
33
+ export interface CoapMessage {
34
+ /** Message type. */
35
+ type: CoapMessageType;
36
+ /** Request method (only for CON/NON requests). */
37
+ method?: CoapMethod;
38
+ /** Response code (only for ACK responses). */
39
+ responseCode?: string;
40
+ /** Message ID for deduplication. */
41
+ messageId: number;
42
+ /** Token for request/response matching. */
43
+ token: Uint8Array;
44
+ /** URI path (e.g. ["sensors", "temperature"]). */
45
+ path: string[];
46
+ /** Uri-Host option (3). Defaults to the destination address when absent. */
47
+ uriHost?: string;
48
+ /** Uri-Port option (7). Defaults to the destination port when absent. */
49
+ uriPort?: number;
50
+ /** Uri-Query options (15), in order. */
51
+ uriQuery?: string[];
52
+ /** Payload bytes. */
53
+ payload: Uint8Array;
54
+ /** Content-Format option (12), e.g. 0 text/plain, 60 application/cbor. */
55
+ contentFormat?: number;
56
+ /** Accept option (17). */
57
+ accept?: number;
58
+ /** Observe option (6): 0 register, 1 deregister, otherwise notification sequence. */
59
+ observe?: number;
60
+ /** OSCORE option (9) bytes, passed through without interpretation. */
61
+ oscore?: Uint8Array;
62
+ /** Block1 option (27) for blockwise requests. */
63
+ block1?: CoapBlockOption;
64
+ /** Block2 option (23) for blockwise responses. */
65
+ block2?: CoapBlockOption;
66
+ /** Size2 option (28): total response body size hint. */
67
+ size2?: number;
68
+ /** Size1 option (60): total request body size hint. */
69
+ size1?: number;
70
+ /** Remote endpoint. */
71
+ remote: {
72
+ host: string;
73
+ port: number;
74
+ };
75
+ /** Timestamp of receipt. */
76
+ receivedAt: number;
77
+ }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ /**
3
+ * CoAP transport port — injected by the caller.
4
+ *
5
+ * CoAP (RFC 7252) runs over UDP. The caller provides the socket.
6
+ * Messages are confirmable (CON), non-confirmable (NON),
7
+ * acknowledgement (ACK), or reset (RST).
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,30 @@
1
+ /**
2
+ * CoAP wire codec (RFC 7252 §3).
3
+ *
4
+ * Kept independent of the transport so the encoding can be exercised against
5
+ * RFC vectors without a socket. Only Uri-Path (option 11) is emitted; Uri-Host
6
+ * and Uri-Port default to the destination address, so they are not sent.
7
+ */
8
+ import type { CoapMessage } from './transport.js';
9
+ export declare const URI_HOST_OPTION = 3;
10
+ export declare const OBSERVE_OPTION = 6;
11
+ export declare const URI_PORT_OPTION = 7;
12
+ export declare const OSCORE_OPTION = 9;
13
+ export declare const BLOCK2_OPTION = 23;
14
+ export declare const BLOCK1_OPTION = 27;
15
+ export declare const SIZE2_OPTION = 28;
16
+ export declare const SIZE1_OPTION = 60;
17
+ export declare const URI_PATH_OPTION = 11;
18
+ export declare const CONTENT_FORMAT_OPTION = 12;
19
+ export declare const ACCEPT_OPTION = 17;
20
+ export declare const URI_QUERY_OPTION = 15;
21
+ export declare function tokenToKey(token: Uint8Array): string;
22
+ export declare function encodeCoap(msg: CoapMessage): Uint8Array;
23
+ export declare function decodeCoap(raw: Uint8Array, remote: {
24
+ host: string;
25
+ port: number;
26
+ }): CoapMessage;
27
+ export declare function encodeBlock(block: import('./transport.js').CoapBlockOption): number;
28
+ export declare function decodeBlock(value: Uint8Array): import('./transport.js').CoapBlockOption;
29
+ export declare function formatResponseCode(code: number): string;
30
+ export declare function parseResponseCode(code: string): number;
package/dist/codec.js ADDED
@@ -0,0 +1,339 @@
1
+ /**
2
+ * CoAP wire codec (RFC 7252 §3).
3
+ *
4
+ * Kept independent of the transport so the encoding can be exercised against
5
+ * RFC vectors without a socket. Only Uri-Path (option 11) is emitted; Uri-Host
6
+ * and Uri-Port default to the destination address, so they are not sent.
7
+ */
8
+ export const URI_HOST_OPTION = 3;
9
+ export const OBSERVE_OPTION = 6;
10
+ export const URI_PORT_OPTION = 7;
11
+ export const OSCORE_OPTION = 9;
12
+ export const BLOCK2_OPTION = 23;
13
+ export const BLOCK1_OPTION = 27;
14
+ export const SIZE2_OPTION = 28;
15
+ export const SIZE1_OPTION = 60;
16
+ export const URI_PATH_OPTION = 11;
17
+ export const CONTENT_FORMAT_OPTION = 12;
18
+ export const ACCEPT_OPTION = 17;
19
+ export const URI_QUERY_OPTION = 15;
20
+ const PAYLOAD_MARKER = 0xff;
21
+ const MAX_TOKEN_LENGTH = 8;
22
+ const MAX_OPTION_VALUE = 65804; // 269 + 0xffff
23
+ const METHOD_CODES = { GET: 1, POST: 2, PUT: 3, DELETE: 4, FETCH: 5 };
24
+ const CODE_METHODS = { 1: 'GET', 2: 'POST', 3: 'PUT', 4: 'DELETE', 5: 'FETCH' };
25
+ export function tokenToKey(token) {
26
+ return Array.from(token).map((byte) => byte.toString(16).padStart(2, '0')).join('');
27
+ }
28
+ export function encodeCoap(msg) {
29
+ const tokenLen = msg.token.length;
30
+ if (tokenLen > MAX_TOKEN_LENGTH)
31
+ throw new Error('CoAP token exceeds 8 bytes');
32
+ if (msg.messageId < 0 || msg.messageId > 0xffff)
33
+ throw new Error('CoAP message id out of range');
34
+ const entries = [];
35
+ if (msg.uriHost !== undefined)
36
+ entries.push({ number: URI_HOST_OPTION, value: new TextEncoder().encode(msg.uriHost) });
37
+ if (msg.observe !== undefined)
38
+ entries.push({ number: OBSERVE_OPTION, value: uintToBytes(msg.observe) });
39
+ if (msg.uriPort !== undefined)
40
+ entries.push({ number: URI_PORT_OPTION, value: uintToBytes(msg.uriPort) });
41
+ if (msg.oscore)
42
+ entries.push({ number: OSCORE_OPTION, value: msg.oscore });
43
+ for (const segment of msg.path) {
44
+ if (segment.length === 0)
45
+ throw new Error('CoAP Uri-Path segment cannot be empty');
46
+ entries.push({ number: URI_PATH_OPTION, value: new TextEncoder().encode(segment) });
47
+ }
48
+ for (const query of msg.uriQuery ?? []) {
49
+ entries.push({ number: URI_QUERY_OPTION, value: new TextEncoder().encode(query) });
50
+ }
51
+ if (msg.contentFormat !== undefined)
52
+ entries.push({ number: CONTENT_FORMAT_OPTION, value: uintToBytes(msg.contentFormat) });
53
+ if (msg.accept !== undefined)
54
+ entries.push({ number: ACCEPT_OPTION, value: uintToBytes(msg.accept) });
55
+ if (msg.block2)
56
+ entries.push({ number: BLOCK2_OPTION, value: uintToBytes(encodeBlock(msg.block2)) });
57
+ if (msg.block1)
58
+ entries.push({ number: BLOCK1_OPTION, value: uintToBytes(encodeBlock(msg.block1)) });
59
+ if (msg.size2 !== undefined)
60
+ entries.push({ number: SIZE2_OPTION, value: uintToBytes(msg.size2) });
61
+ if (msg.size1 !== undefined)
62
+ entries.push({ number: SIZE1_OPTION, value: uintToBytes(msg.size1) });
63
+ // Stable sort keeps repeated Uri-Path segments in path order.
64
+ entries.sort((a, b) => a.number - b.number);
65
+ const options = [];
66
+ let previousOption = 0;
67
+ for (const entry of entries) {
68
+ const delta = optionDeltaNibble(entry.number - previousOption);
69
+ const length = optionLengthNibble(entry.value.length);
70
+ options.push((delta.nibble << 4) | length.nibble, ...delta.extended, ...length.extended, ...entry.value);
71
+ previousOption = entry.number;
72
+ }
73
+ let code = 0;
74
+ if (msg.type === 'CON' || msg.type === 'NON') {
75
+ if (msg.method) {
76
+ if (METHOD_CODES[msg.method] === undefined)
77
+ throw new Error('CoAP request requires a method');
78
+ code = METHOD_CODES[msg.method];
79
+ }
80
+ else if (msg.responseCode !== undefined) {
81
+ code = parseResponseCode(msg.responseCode);
82
+ }
83
+ else {
84
+ throw new Error('CoAP CON/NON message requires a method or response code');
85
+ }
86
+ }
87
+ else if (msg.responseCode !== undefined) {
88
+ code = parseResponseCode(msg.responseCode);
89
+ }
90
+ const marker = msg.payload.length > 0 ? [PAYLOAD_MARKER] : [];
91
+ const out = new Uint8Array(4 + tokenLen + options.length + marker.length + msg.payload.length);
92
+ out[0] = 0x40 | ((msgTypeCode(msg.type) & 0x03) << 4) | tokenLen;
93
+ out[1] = code;
94
+ out[2] = (msg.messageId >> 8) & 0xff;
95
+ out[3] = msg.messageId & 0xff;
96
+ out.set(msg.token, 4);
97
+ out.set(options, 4 + tokenLen);
98
+ const payloadOffset = 4 + tokenLen + options.length;
99
+ if (marker.length)
100
+ out[payloadOffset] = PAYLOAD_MARKER;
101
+ out.set(msg.payload, payloadOffset + marker.length);
102
+ return out;
103
+ }
104
+ export function decodeCoap(raw, remote) {
105
+ if (raw.length < 4)
106
+ throw new Error('Truncated CoAP header');
107
+ const version = (raw[0] >> 6) & 0x03;
108
+ if (version !== 1)
109
+ throw new Error(`Unsupported CoAP version ${version}`);
110
+ const tokenLen = raw[0] & 0x0f;
111
+ if (tokenLen > MAX_TOKEN_LENGTH)
112
+ throw new Error('Invalid CoAP token length');
113
+ if (raw.length < 4 + tokenLen)
114
+ throw new Error('Truncated CoAP token');
115
+ const type = typeFromCode((raw[0] >> 4) & 0x03);
116
+ const code = raw[1];
117
+ const messageId = ((raw[2] << 8) | raw[3]) & 0xffff;
118
+ const token = raw.slice(4, 4 + tokenLen);
119
+ const path = [];
120
+ const uriQuery = [];
121
+ let uriHost;
122
+ let uriPort;
123
+ let optionNumber = 0;
124
+ let offset = 4 + tokenLen;
125
+ let payload = new Uint8Array(0);
126
+ let contentFormat;
127
+ let accept;
128
+ let observe;
129
+ let oscore;
130
+ let block1;
131
+ let block2;
132
+ let size1;
133
+ let size2;
134
+ const seen = new Set();
135
+ while (offset < raw.length) {
136
+ if (raw[offset] === PAYLOAD_MARKER) {
137
+ offset++;
138
+ if (offset === raw.length)
139
+ throw new Error('CoAP payload marker without payload');
140
+ payload = raw.slice(offset);
141
+ offset = raw.length;
142
+ break;
143
+ }
144
+ const delta = (raw[offset] >> 4) & 0x0f;
145
+ const length = raw[offset] & 0x0f;
146
+ offset++;
147
+ const deltaValue = readExtendedOptionValue(delta, raw, offset);
148
+ offset = deltaValue.offset;
149
+ const lengthValue = readExtendedOptionValue(length, raw, offset);
150
+ offset = lengthValue.offset;
151
+ optionNumber += deltaValue.value;
152
+ if (optionNumber > 0xffff)
153
+ throw new Error('CoAP option number out of range');
154
+ if (offset + lengthValue.value > raw.length)
155
+ throw new Error('Truncated CoAP option value');
156
+ const value = raw.slice(offset, offset + lengthValue.value);
157
+ if (optionNumber !== URI_PATH_OPTION && optionNumber !== URI_QUERY_OPTION && seen.has(optionNumber)) {
158
+ throw new Error(`Duplicate CoAP option ${optionNumber}`);
159
+ }
160
+ if (optionNumber === URI_HOST_OPTION) {
161
+ uriHost = new TextDecoder().decode(value);
162
+ }
163
+ else if (optionNumber === URI_PORT_OPTION) {
164
+ uriPort = uintFromBytes(value);
165
+ }
166
+ else if (optionNumber === URI_PATH_OPTION) {
167
+ const segment = new TextDecoder().decode(value);
168
+ if (segment.length === 0)
169
+ throw new Error('Empty CoAP Uri-Path segment');
170
+ path.push(segment);
171
+ }
172
+ else if (optionNumber === URI_QUERY_OPTION) {
173
+ uriQuery.push(new TextDecoder().decode(value));
174
+ }
175
+ else if (optionNumber === CONTENT_FORMAT_OPTION) {
176
+ contentFormat = uintFromBytes(value);
177
+ }
178
+ else if (optionNumber === ACCEPT_OPTION) {
179
+ accept = uintFromBytes(value);
180
+ }
181
+ else if (optionNumber === OBSERVE_OPTION) {
182
+ observe = uintFromBytes(value);
183
+ }
184
+ else if (optionNumber === OSCORE_OPTION) {
185
+ oscore = value.slice();
186
+ }
187
+ else if (optionNumber === BLOCK1_OPTION) {
188
+ block1 = decodeBlock(value);
189
+ }
190
+ else if (optionNumber === BLOCK2_OPTION) {
191
+ block2 = decodeBlock(value);
192
+ }
193
+ else if (optionNumber === SIZE1_OPTION) {
194
+ size1 = uintFromBytes(value);
195
+ }
196
+ else if (optionNumber === SIZE2_OPTION) {
197
+ size2 = uintFromBytes(value);
198
+ }
199
+ seen.add(optionNumber);
200
+ offset += lengthValue.value;
201
+ }
202
+ const message = {
203
+ type,
204
+ messageId,
205
+ token,
206
+ path,
207
+ payload,
208
+ remote,
209
+ receivedAt: Date.now(),
210
+ };
211
+ if (uriHost !== undefined)
212
+ message.uriHost = uriHost;
213
+ if (uriPort !== undefined)
214
+ message.uriPort = uriPort;
215
+ if (uriQuery.length > 0)
216
+ message.uriQuery = uriQuery;
217
+ if (contentFormat !== undefined)
218
+ message.contentFormat = contentFormat;
219
+ if (accept !== undefined)
220
+ message.accept = accept;
221
+ if (observe !== undefined)
222
+ message.observe = observe;
223
+ if (oscore !== undefined)
224
+ message.oscore = oscore;
225
+ if (block1 !== undefined)
226
+ message.block1 = block1;
227
+ if (block2 !== undefined)
228
+ message.block2 = block2;
229
+ if (size1 !== undefined)
230
+ message.size1 = size1;
231
+ if (size2 !== undefined)
232
+ message.size2 = size2;
233
+ const codeClass = code >> 5;
234
+ if (codeClass === 0) {
235
+ const method = CODE_METHODS[code];
236
+ if (method)
237
+ message.method = method;
238
+ }
239
+ else {
240
+ message.responseCode = formatResponseCode(code);
241
+ }
242
+ return message;
243
+ }
244
+ function msgTypeCode(type) {
245
+ switch (type) {
246
+ case 'CON': return 0;
247
+ case 'NON': return 1;
248
+ case 'ACK': return 2;
249
+ case 'RST': return 3;
250
+ }
251
+ }
252
+ function typeFromCode(code) {
253
+ switch (code & 0x03) {
254
+ case 0: return 'CON';
255
+ case 1: return 'NON';
256
+ case 2: return 'ACK';
257
+ default: return 'RST';
258
+ }
259
+ }
260
+ function optionDeltaNibble(value) {
261
+ if (value <= 12)
262
+ return { nibble: value, extended: [] };
263
+ if (value <= 268)
264
+ return { nibble: 13, extended: [value - 13] };
265
+ return { nibble: 14, extended: [(value - 269) >> 8, (value - 269) & 0xff] };
266
+ }
267
+ function optionLengthNibble(value) {
268
+ if (value > MAX_OPTION_VALUE)
269
+ throw new Error('CoAP option value exceeds 65804 bytes');
270
+ if (value <= 12)
271
+ return { nibble: value, extended: [] };
272
+ if (value <= 268)
273
+ return { nibble: 13, extended: [value - 13] };
274
+ return { nibble: 14, extended: [(value - 269) >> 8, (value - 269) & 0xff] };
275
+ }
276
+ function readExtendedOptionValue(nibble, raw, offset) {
277
+ if (nibble <= 12)
278
+ return { value: nibble, offset };
279
+ if (nibble === 13) {
280
+ if (offset >= raw.length)
281
+ throw new Error('Truncated CoAP option delta/length');
282
+ return { value: raw[offset] + 13, offset: offset + 1 };
283
+ }
284
+ if (nibble === 14) {
285
+ if (offset + 1 >= raw.length)
286
+ throw new Error('Truncated CoAP option delta/length');
287
+ return { value: ((raw[offset] << 8) | raw[offset + 1]) + 269, offset: offset + 2 };
288
+ }
289
+ throw new Error('Invalid CoAP option delta/length nibble');
290
+ }
291
+ export function encodeBlock(block) {
292
+ if (!Number.isInteger(block.num) || block.num < 0 || block.num > 0xfffff)
293
+ throw new Error(`Invalid CoAP block number ${block.num}`);
294
+ if (!Number.isInteger(block.sizeExponent) || block.sizeExponent < 0 || block.sizeExponent > 6) {
295
+ throw new Error(`Invalid CoAP block size exponent ${block.sizeExponent}`);
296
+ }
297
+ return ((block.num & 0xfffff) << 4) | (block.more ? 0x08 : 0) | block.sizeExponent;
298
+ }
299
+ export function decodeBlock(value) {
300
+ const encoded = uintFromBytes(value);
301
+ const sizeExponent = encoded & 0x07;
302
+ if (sizeExponent === 7)
303
+ throw new Error('Reserved CoAP block size exponent');
304
+ return { num: encoded >>> 4, more: (encoded & 0x08) !== 0, sizeExponent };
305
+ }
306
+ function uintToBytes(value) {
307
+ if (!Number.isInteger(value) || value < 0 || value > 0xffffffff) {
308
+ throw new Error(`Invalid CoAP uint option value ${value}`);
309
+ }
310
+ if (value === 0)
311
+ return new Uint8Array(0);
312
+ const bytes = [];
313
+ let remaining = value;
314
+ while (remaining > 0) {
315
+ bytes.unshift(remaining & 0xff);
316
+ remaining = Math.floor(remaining / 256);
317
+ }
318
+ return Uint8Array.from(bytes);
319
+ }
320
+ function uintFromBytes(value) {
321
+ if (value.length > 4)
322
+ throw new Error('CoAP uint option exceeds 4 bytes');
323
+ let result = 0;
324
+ for (const byte of value)
325
+ result = result * 256 + byte;
326
+ return result;
327
+ }
328
+ export function formatResponseCode(code) {
329
+ return `${(code >> 5) & 0x07}.${String(code & 0x1f).padStart(2, '0')}`;
330
+ }
331
+ export function parseResponseCode(code) {
332
+ const match = /^([1-5])\.(\d{2})$/.exec(code);
333
+ if (!match)
334
+ throw new Error(`Invalid CoAP response code '${code}'`);
335
+ const detail = Number(match[2]);
336
+ if (detail > 31)
337
+ throw new Error(`Invalid CoAP response code '${code}'`);
338
+ return (Number(match[1]) << 5) | detail;
339
+ }
package/dist/gateway.d.ts CHANGED
@@ -1,22 +1,76 @@
1
1
  /**
2
2
  * CoAP Edge Gateway — wires a CoapTransportPort into an EdgeRuntime.
3
+ *
4
+ * Requests are CON messages matched to responses by token and remote endpoint
5
+ * (RFC 7252 §5.3). Confirmable requests are retransmitted with exponential
6
+ * backoff until acknowledged or until the request deadline expires. An empty
7
+ * ACK keeps the request pending for a separate response, which is acknowledged
8
+ * before being handed to the caller.
9
+ *
10
+ * RFC 7641 observations keep a token registered after the initial response;
11
+ * later notifications with a fresh 24-bit Observe sequence are delivered to the
12
+ * caller and confirmable notifications are acknowledged.
3
13
  */
4
14
  import type { EdgeRuntime, EdgeOperationResult } from '@totemsdk/edge';
5
- import type { CoapTransportPort } from './transport.js';
15
+ import type { CoapTransportPort, CoapBlockOption } from './transport.js';
16
+ import type { CoapSecurityPort } from './security.js';
17
+ export interface CoapGatewayTiming {
18
+ /** Overall request deadline in milliseconds. Default 10_000. */
19
+ requestTimeoutMs?: number;
20
+ /** Initial retransmission interval in milliseconds. Default 2_000. */
21
+ ackTimeoutMs?: number;
22
+ /** Maximum retransmissions before waiting for the deadline. Default 4. */
23
+ maxRetransmits?: number;
24
+ }
6
25
  export interface CoapGatewayConfig {
7
26
  runtime: EdgeRuntime;
8
27
  transport: CoapTransportPort;
9
28
  localPort: number;
29
+ timing?: CoapGatewayTiming;
30
+ /** Optional OSCORE boundary; protected endpoints fail closed without it. */
31
+ security?: CoapSecurityPort;
32
+ /** Transparent RFC 7959 blockwise transfer for large payloads. */
33
+ blockwise?: CoapBlockwiseOptions;
34
+ }
35
+ export interface CoapBlockwiseOptions {
36
+ /** Enable transparent blockwise transfer. Default true. */
37
+ enabled?: boolean;
38
+ /** Preferred block size in bytes (power of two, 16-1024). Default 1024. */
39
+ blockSize?: number;
40
+ /** Maximum assembled response body. Default 1 MiB. */
41
+ maxBodySize?: number;
42
+ }
43
+ export interface CoapResponseData {
44
+ payload: Uint8Array;
45
+ /** Content-Format option value, e.g. 0 text/plain or 60 application/cbor. */
46
+ contentFormat?: number;
47
+ /** Observe option value (notifications only). */
48
+ observe?: number;
49
+ /** Block1 option echoed by the server. */
50
+ block1?: CoapBlockOption;
51
+ /** Block2 option describing this response block. */
52
+ block2?: CoapBlockOption;
53
+ /** Size1 hint from the server. */
54
+ size1?: number;
55
+ /** Size2 hint (total response body size). */
56
+ size2?: number;
57
+ }
58
+ export interface CoapObservationData extends CoapResponseData {
59
+ observe: number;
60
+ receivedAt: number;
61
+ }
62
+ export interface CoapObservation {
63
+ /** Correlation token as lowercase hex. */
64
+ readonly token: string;
65
+ /** Deregister and stop delivering notifications. */
66
+ cancel(): Promise<void>;
10
67
  }
11
68
  export interface CoapGateway {
12
69
  start(): Promise<void>;
13
70
  stop(): Promise<void>;
14
71
  readonly status: 'stopped' | 'running' | 'error';
15
- get(path: string[], host: string, port: number): Promise<EdgeOperationResult<{
16
- payload: Uint8Array;
17
- }>>;
18
- post(path: string[], payload: Uint8Array, host: string, port: number): Promise<EdgeOperationResult<{
19
- payload: Uint8Array;
20
- }>>;
72
+ get(path: string[], host: string, port: number): Promise<EdgeOperationResult<CoapResponseData>>;
73
+ post(path: string[], payload: Uint8Array, host: string, port: number): Promise<EdgeOperationResult<CoapResponseData>>;
74
+ observe(path: string[], host: string, port: number, handler: (notification: CoapObservationData) => void): Promise<EdgeOperationResult<CoapObservation>>;
21
75
  }
22
76
  export declare function createCoapGateway(config: CoapGatewayConfig): CoapGateway;