@sirena-lwm2m/coap 0.8.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 (157) hide show
  1. package/LICENSE +17 -0
  2. package/README.md +78 -0
  3. package/dist/api/exchange-context.d.ts +34 -0
  4. package/dist/api/exchange-context.d.ts.map +1 -0
  5. package/dist/api/exchange-context.js +2 -0
  6. package/dist/api/exchange-context.js.map +1 -0
  7. package/dist/api/headers.d.ts +10 -0
  8. package/dist/api/headers.d.ts.map +1 -0
  9. package/dist/api/headers.js +69 -0
  10. package/dist/api/headers.js.map +1 -0
  11. package/dist/api/index.d.ts +5 -0
  12. package/dist/api/index.d.ts.map +1 -0
  13. package/dist/api/index.js +4 -0
  14. package/dist/api/index.js.map +1 -0
  15. package/dist/api/request.d.ts +11 -0
  16. package/dist/api/request.d.ts.map +1 -0
  17. package/dist/api/request.js +49 -0
  18. package/dist/api/request.js.map +1 -0
  19. package/dist/api/response.d.ts +11 -0
  20. package/dist/api/response.d.ts.map +1 -0
  21. package/dist/api/response.js +19 -0
  22. package/dist/api/response.js.map +1 -0
  23. package/dist/blockwise/block1-assembler.d.ts +26 -0
  24. package/dist/blockwise/block1-assembler.d.ts.map +1 -0
  25. package/dist/blockwise/block1-assembler.js +94 -0
  26. package/dist/blockwise/block1-assembler.js.map +1 -0
  27. package/dist/blockwise/block2-chunker.d.ts +23 -0
  28. package/dist/blockwise/block2-chunker.d.ts.map +1 -0
  29. package/dist/blockwise/block2-chunker.js +99 -0
  30. package/dist/blockwise/block2-chunker.js.map +1 -0
  31. package/dist/blockwise/index.d.ts +8 -0
  32. package/dist/blockwise/index.d.ts.map +1 -0
  33. package/dist/blockwise/index.js +5 -0
  34. package/dist/blockwise/index.js.map +1 -0
  35. package/dist/blockwise/middleware.d.ts +7 -0
  36. package/dist/blockwise/middleware.d.ts.map +1 -0
  37. package/dist/blockwise/middleware.js +120 -0
  38. package/dist/blockwise/middleware.js.map +1 -0
  39. package/dist/blockwise/option.d.ts +12 -0
  40. package/dist/blockwise/option.d.ts.map +1 -0
  41. package/dist/blockwise/option.js +55 -0
  42. package/dist/blockwise/option.js.map +1 -0
  43. package/dist/index.d.ts +10 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +6 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/message/index.d.ts +21 -0
  48. package/dist/message/index.d.ts.map +1 -0
  49. package/dist/message/index.js +79 -0
  50. package/dist/message/index.js.map +1 -0
  51. package/dist/observability.d.ts +44 -0
  52. package/dist/observability.d.ts.map +1 -0
  53. package/dist/observability.js +71 -0
  54. package/dist/observability.js.map +1 -0
  55. package/dist/observe-notification.d.ts +21 -0
  56. package/dist/observe-notification.d.ts.map +1 -0
  57. package/dist/observe-notification.js +87 -0
  58. package/dist/observe-notification.js.map +1 -0
  59. package/dist/observe.d.ts +10 -0
  60. package/dist/observe.d.ts.map +1 -0
  61. package/dist/observe.js +3 -0
  62. package/dist/observe.js.map +1 -0
  63. package/dist/option-parser/index.d.ts +30 -0
  64. package/dist/option-parser/index.d.ts.map +1 -0
  65. package/dist/option-parser/index.js +125 -0
  66. package/dist/option-parser/index.js.map +1 -0
  67. package/dist/router/app.d.ts +8 -0
  68. package/dist/router/app.d.ts.map +1 -0
  69. package/dist/router/app.js +25 -0
  70. package/dist/router/app.js.map +1 -0
  71. package/dist/router/content-format.d.ts +7 -0
  72. package/dist/router/content-format.d.ts.map +1 -0
  73. package/dist/router/content-format.js +23 -0
  74. package/dist/router/content-format.js.map +1 -0
  75. package/dist/router/index.d.ts +9 -0
  76. package/dist/router/index.d.ts.map +1 -0
  77. package/dist/router/index.js +5 -0
  78. package/dist/router/index.js.map +1 -0
  79. package/dist/router/radix.d.ts +16 -0
  80. package/dist/router/radix.d.ts.map +1 -0
  81. package/dist/router/radix.js +69 -0
  82. package/dist/router/radix.js.map +1 -0
  83. package/dist/router/router.d.ts +38 -0
  84. package/dist/router/router.d.ts.map +1 -0
  85. package/dist/router/router.js +111 -0
  86. package/dist/router/router.js.map +1 -0
  87. package/dist/router.d.ts +2 -0
  88. package/dist/router.d.ts.map +1 -0
  89. package/dist/router.js +2 -0
  90. package/dist/router.js.map +1 -0
  91. package/dist/security.d.ts +3 -0
  92. package/dist/security.d.ts.map +1 -0
  93. package/dist/security.js +2 -0
  94. package/dist/security.js.map +1 -0
  95. package/dist/store/index.d.ts +26 -0
  96. package/dist/store/index.d.ts.map +1 -0
  97. package/dist/store/index.js +68 -0
  98. package/dist/store/index.js.map +1 -0
  99. package/dist/store/sqlite-store.d.ts +10 -0
  100. package/dist/store/sqlite-store.d.ts.map +1 -0
  101. package/dist/store/sqlite-store.js +75 -0
  102. package/dist/store/sqlite-store.js.map +1 -0
  103. package/dist/store.d.ts +5 -0
  104. package/dist/store.d.ts.map +1 -0
  105. package/dist/store.js +3 -0
  106. package/dist/store.js.map +1 -0
  107. package/dist/transport/dtls.d.ts +38 -0
  108. package/dist/transport/dtls.d.ts.map +1 -0
  109. package/dist/transport/dtls.js +151 -0
  110. package/dist/transport/dtls.js.map +1 -0
  111. package/dist/transport/events.d.ts +34 -0
  112. package/dist/transport/events.d.ts.map +1 -0
  113. package/dist/transport/events.js +2 -0
  114. package/dist/transport/events.js.map +1 -0
  115. package/dist/transport/index.d.ts +10 -0
  116. package/dist/transport/index.d.ts.map +1 -0
  117. package/dist/transport/index.js +5 -0
  118. package/dist/transport/index.js.map +1 -0
  119. package/dist/transport/tcp-connection.d.ts +18 -0
  120. package/dist/transport/tcp-connection.d.ts.map +1 -0
  121. package/dist/transport/tcp-connection.js +72 -0
  122. package/dist/transport/tcp-connection.js.map +1 -0
  123. package/dist/transport/tcp-frame.d.ts +17 -0
  124. package/dist/transport/tcp-frame.d.ts.map +1 -0
  125. package/dist/transport/tcp-frame.js +112 -0
  126. package/dist/transport/tcp-frame.js.map +1 -0
  127. package/dist/transport/tcp-stream.d.ts +21 -0
  128. package/dist/transport/tcp-stream.d.ts.map +1 -0
  129. package/dist/transport/tcp-stream.js +52 -0
  130. package/dist/transport/tcp-stream.js.map +1 -0
  131. package/dist/transport/udp.d.ts +97 -0
  132. package/dist/transport/udp.d.ts.map +1 -0
  133. package/dist/transport/udp.js +994 -0
  134. package/dist/transport/udp.js.map +1 -0
  135. package/dist/transport.d.ts +3 -0
  136. package/dist/transport.d.ts.map +1 -0
  137. package/dist/transport.js +2 -0
  138. package/dist/transport.js.map +1 -0
  139. package/docs/api-reference.md +536 -0
  140. package/docs/blockwise.md +117 -0
  141. package/docs/observability.md +206 -0
  142. package/docs/observe.md +203 -0
  143. package/docs/releasing.md +106 -0
  144. package/docs/router.md +170 -0
  145. package/docs/store.md +212 -0
  146. package/docs/tcp-transport.md +89 -0
  147. package/docs/udp-transport.md +81 -0
  148. package/examples/slice-1.ts +12 -0
  149. package/examples/slice-2.ts +96 -0
  150. package/examples/slice-3-psk.ts +70 -0
  151. package/examples/slice-3-x509.ts +85 -0
  152. package/examples/slice-4.ts +45 -0
  153. package/examples/slice-5-firmware.ts +207 -0
  154. package/examples/slice-6-temperature.ts +163 -0
  155. package/examples/slice-7-file-store.ts +92 -0
  156. package/examples/slice-7-otel.ts +123 -0
  157. package/package.json +73 -0
@@ -0,0 +1,70 @@
1
+ import tls from 'node:tls';
2
+ import { createServer, Response, stringToBytes, encodeMessage, decodeMessage, stringToCode } from '@sirena-lwm2m/coap';
3
+
4
+ const PSK_IDENTITY = 'device-001';
5
+ const PSK_KEY = Buffer.from('s3cr3t');
6
+
7
+ const server = createServer({
8
+ listen: ['coaps://127.0.0.1:5684'],
9
+ security: {
10
+ mode: 'psk',
11
+ psk: (identity) => identity === PSK_IDENTITY ? PSK_KEY : null,
12
+ },
13
+ });
14
+
15
+ server.on('request', async (_req, ctx) => {
16
+ console.log('ctx.peer:', ctx.peer);
17
+ const identity = ctx.peer.mode === 'psk' ? ctx.peer.identity : 'unknown';
18
+ await ctx.respond(Response.ok(stringToBytes(identity)));
19
+ });
20
+
21
+ await server.start();
22
+ console.log('Server listening on:', server.listenerSummary());
23
+
24
+ const port = server.listenerSummary()[0].port;
25
+
26
+ function pskGet(host: string, port: number): Promise<string> {
27
+ const packet = Buffer.from(encodeMessage({
28
+ version: 1,
29
+ type: 'CON',
30
+ token: new Uint8Array([0x01]),
31
+ code: stringToCode('0.01'),
32
+ messageId: 1,
33
+ options: [],
34
+ payload: new Uint8Array(0),
35
+ }));
36
+
37
+ return new Promise((resolve, reject) => {
38
+ const sock = tls.connect({
39
+ host,
40
+ port,
41
+ pskCallback: () => ({ psk: PSK_KEY, identity: PSK_IDENTITY }),
42
+ rejectUnauthorized: false,
43
+ });
44
+
45
+ const timer = setTimeout(() => { sock.destroy(); reject(new Error('timeout')); }, 5000);
46
+
47
+ sock.once('secureConnect', () => {
48
+ sock.write(packet);
49
+ });
50
+
51
+ sock.once('data', (chunk: Buffer) => {
52
+ clearTimeout(timer);
53
+ sock.destroy();
54
+ const decoded = decodeMessage(new Uint8Array(chunk));
55
+ resolve(Buffer.from(decoded.payload).toString('utf8'));
56
+ });
57
+
58
+ sock.once('error', (err) => { clearTimeout(timer); reject(err); });
59
+ });
60
+ }
61
+
62
+ const body = await pskGet('127.0.0.1', port);
63
+ console.log('Response body:', body);
64
+
65
+ if (body !== PSK_IDENTITY) {
66
+ throw new Error(`Expected '${PSK_IDENTITY}', got '${body}'`);
67
+ }
68
+
69
+ await server.stop();
70
+ console.log('Server stopped cleanly.');
@@ -0,0 +1,85 @@
1
+ import tls from 'node:tls';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { createServer, Response, stringToBytes, stringToCode } from '@sirena-lwm2m/coap';
6
+ import { encodeTcpFrame, decodeTcpFrame } from '../dist/transport/index.js';
7
+
8
+ const FIXTURES_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'test', 'fixtures', 'tls');
9
+
10
+ const CA_CERT = fs.readFileSync(path.join(FIXTURES_DIR, 'ca.cert.pem'));
11
+ const SERVER_KEY = fs.readFileSync(path.join(FIXTURES_DIR, 'server.key.pem'));
12
+ const SERVER_CERT = fs.readFileSync(path.join(FIXTURES_DIR, 'server.cert.pem'));
13
+ const CLIENT_KEY = fs.readFileSync(path.join(FIXTURES_DIR, 'client.key.pem'));
14
+ const CLIENT_CERT = fs.readFileSync(path.join(FIXTURES_DIR, 'client.cert.pem'));
15
+
16
+ const server = createServer({
17
+ listen: ['coaps+tcp://127.0.0.1:5685'],
18
+ security: {
19
+ mode: 'x509',
20
+ key: SERVER_KEY,
21
+ cert: SERVER_CERT,
22
+ ca: CA_CERT,
23
+ requestClientCert: true,
24
+ },
25
+ });
26
+
27
+ server.on('request', async (_req, ctx) => {
28
+ console.log('ctx.peer:', ctx.peer);
29
+ await ctx.respond(Response.ok(stringToBytes('authenticated')));
30
+ });
31
+
32
+ await server.start();
33
+ console.log('Server listening on:', server.listenerSummary());
34
+
35
+ const port = server.listenerSummary()[0].port;
36
+
37
+ function x509Get(host: string, port: number): Promise<string> {
38
+ const frame = Buffer.from(encodeTcpFrame({
39
+ code: stringToCode('0.01'),
40
+ token: new Uint8Array([0x01]),
41
+ options: [],
42
+ payload: new Uint8Array(0),
43
+ }));
44
+
45
+ return new Promise((resolve, reject) => {
46
+ const sock = tls.connect({
47
+ host,
48
+ port,
49
+ key: CLIENT_KEY,
50
+ cert: CLIENT_CERT,
51
+ ca: CA_CERT,
52
+ });
53
+
54
+ const timer = setTimeout(() => { sock.destroy(); reject(new Error('timeout')); }, 5000);
55
+
56
+ sock.once('secureConnect', () => {
57
+ sock.write(frame);
58
+ });
59
+
60
+ let buf = Buffer.alloc(0);
61
+ const onData = (chunk: Buffer) => {
62
+ buf = Buffer.concat([buf, chunk]);
63
+ const result = decodeTcpFrame(buf);
64
+ if (result !== null && result.frame.payload.length > 0) {
65
+ clearTimeout(timer);
66
+ sock.removeListener('data', onData);
67
+ sock.destroy();
68
+ resolve(Buffer.from(result.frame.payload).toString('utf8'));
69
+ }
70
+ };
71
+ sock.on('data', onData);
72
+
73
+ sock.once('error', (err) => { clearTimeout(timer); reject(err); });
74
+ });
75
+ }
76
+
77
+ const body = await x509Get('127.0.0.1', port);
78
+ console.log('Response body:', body);
79
+
80
+ if (body !== 'authenticated') {
81
+ throw new Error(`Expected 'authenticated', got '${body}'`);
82
+ }
83
+
84
+ await server.stop();
85
+ console.log('Server stopped cleanly.');
@@ -0,0 +1,45 @@
1
+ import { createServer, stringToCode, Response, stringToBytes } from '@sirena-lwm2m/coap';
2
+ import { createApp } from '@sirena-lwm2m/coap/router';
3
+ import type { Middleware } from '@sirena-lwm2m/coap/router';
4
+
5
+ // Custom option number for Authorization (non-critical, safe to use in demo)
6
+ const OPTION_AUTHORIZATION = 65000;
7
+
8
+ const server = createServer({
9
+ listen: [{ host: '127.0.0.1', port: 5683 }],
10
+ });
11
+
12
+ const router = createApp({ server, discovery: true });
13
+
14
+ // Auth middleware: rejects requests missing the Authorization option with 4.01
15
+ const authMiddleware: Middleware = (req, ctx, next) => {
16
+ const authOpt = ctx.options.find(o => o.number === OPTION_AUTHORIZATION);
17
+ if (authOpt === undefined) {
18
+ ctx.abort(stringToCode('4.01'));
19
+ return;
20
+ }
21
+ return next();
22
+ };
23
+
24
+ router.use(authMiddleware);
25
+
26
+ router.add({
27
+ path: '/3/{iid}',
28
+ method: 'GET',
29
+ handler: (_req, ctx) => {
30
+ ctx.respond(new Response(stringToCode('2.05'), stringToBytes('device-object')));
31
+ },
32
+ });
33
+
34
+ router.add({
35
+ path: '/3/{iid}/{rid}',
36
+ method: 'PUT',
37
+ handler: (_req, ctx) => {
38
+ ctx.respond(new Response(stringToCode('2.04')));
39
+ },
40
+ });
41
+
42
+ await server.start();
43
+ console.log('CoAP server listening on:', server.listenerSummary());
44
+ console.log('Registered routes: GET /3/{iid}, PUT /3/{iid}/{rid}');
45
+ console.log('Discovery enabled at /.well-known/core');
@@ -0,0 +1,207 @@
1
+ import dgram from 'node:dgram';
2
+ import { createServer, stringToCode, Response } from '@sirena-lwm2m/coap';
3
+ import { createApp } from '@sirena-lwm2m/coap/router';
4
+ import { blockwiseMiddleware } from '@sirena-lwm2m/coap/blockwise';
5
+ import { encodeMessage, decodeMessage } from '@sirena-lwm2m/coap';
6
+ import { OptionNumber, uintToBytes } from '@sirena-lwm2m/coap';
7
+ import { encodeBlockOption, decodeBlockOption } from '@sirena-lwm2m/coap/blockwise';
8
+
9
+ const PORT = 5685;
10
+ const HOST = '127.0.0.1';
11
+
12
+ // ── Server ────────────────────────────────────────────────────────────────────
13
+
14
+ const server = createServer({
15
+ listen: [{ host: HOST, port: PORT }],
16
+ });
17
+
18
+ const router = createApp({ server });
19
+
20
+ router.use(blockwiseMiddleware());
21
+
22
+ // PUT /firmware — handler only sees the fully-assembled body
23
+ router.add({
24
+ path: '/firmware',
25
+ method: 'PUT',
26
+ handler: async (req, ctx) => {
27
+ let byteCount = 0;
28
+ if (req.body) {
29
+ const reader = req.body.getReader();
30
+ while (true) {
31
+ const { done, value } = await reader.read();
32
+ if (done) break;
33
+ byteCount += value?.length ?? 0;
34
+ }
35
+ }
36
+ console.log(`firmware bytes received: ${byteCount}`);
37
+ await ctx.respond(new Response(stringToCode('2.04')));
38
+ },
39
+ });
40
+
41
+ // GET /manifest — 500-byte JSON manifest, chunked transparently via Block2
42
+ const MANIFEST = Buffer.from(
43
+ JSON.stringify({ version: '1.2.3', size: 8_388_608, crc32: 'deadbeef' }).padEnd(500, ' '),
44
+ );
45
+
46
+ router.add({
47
+ path: '/manifest',
48
+ method: 'GET',
49
+ handler: async (_req, ctx) => {
50
+ await ctx.respond(new Response(stringToCode('2.05'), MANIFEST));
51
+ },
52
+ });
53
+
54
+ await server.start();
55
+ console.log('CoAP server listening on:', server.listenerSummary());
56
+
57
+ // ── Self-test client ──────────────────────────────────────────────────────────
58
+
59
+ // CON PUT with Block1 option — sends one UDP datagram and awaits ACK/response
60
+ function sendBlock1(
61
+ sock: dgram.Socket,
62
+ token: Uint8Array,
63
+ msgId: number,
64
+ num: number,
65
+ szx: number,
66
+ m: boolean,
67
+ chunk: Uint8Array,
68
+ ): Promise<{ code: number; options: { number: number; value: Uint8Array }[] }> {
69
+ return new Promise((resolve, reject) => {
70
+ const uriPathBytes = new TextEncoder().encode('firmware');
71
+ const packet = encodeMessage({
72
+ version: 1,
73
+ type: 'CON',
74
+ token,
75
+ code: stringToCode('0.03'), // PUT
76
+ messageId: msgId,
77
+ options: [
78
+ { number: OptionNumber.UriPath, value: uriPathBytes },
79
+ { number: OptionNumber.Block1, value: encodeBlockOption({ num, szx, m }) },
80
+ ],
81
+ payload: chunk,
82
+ });
83
+
84
+ const timer = setTimeout(() => reject(new Error(`Block1 NUM=${num} timeout`)), 5000);
85
+
86
+ const onMessage = (msg: Buffer) => {
87
+ const decoded = decodeMessage(msg);
88
+ if (decoded.messageId === msgId) {
89
+ clearTimeout(timer);
90
+ sock.off('message', onMessage);
91
+ resolve({ code: decoded.code, options: decoded.options });
92
+ }
93
+ };
94
+
95
+ sock.on('message', onMessage);
96
+ sock.send(packet, PORT, HOST);
97
+ });
98
+ }
99
+
100
+ // GET with Block2 option — requests a specific block number
101
+ function sendBlock2Get(
102
+ sock: dgram.Socket,
103
+ token: Uint8Array,
104
+ msgId: number,
105
+ path: string,
106
+ num: number,
107
+ szx: number,
108
+ ): Promise<{ code: number; payload: Uint8Array; m: boolean; options: { number: number; value: Uint8Array }[] }> {
109
+ return new Promise((resolve, reject) => {
110
+ const uriPathBytes = new TextEncoder().encode(path);
111
+ const packet = encodeMessage({
112
+ version: 1,
113
+ type: 'CON',
114
+ token,
115
+ code: stringToCode('0.01'), // GET
116
+ messageId: msgId,
117
+ options: [
118
+ { number: OptionNumber.UriPath, value: uriPathBytes },
119
+ { number: OptionNumber.Block2, value: encodeBlockOption({ num, szx, m: false }) },
120
+ ],
121
+ payload: new Uint8Array(0),
122
+ });
123
+
124
+ const timer = setTimeout(() => reject(new Error(`Block2 NUM=${num} timeout`)), 5000);
125
+
126
+ const onMessage = (msg: Buffer) => {
127
+ const decoded = decodeMessage(msg);
128
+ if (decoded.messageId === msgId) {
129
+ clearTimeout(timer);
130
+ sock.off('message', onMessage);
131
+ const block2Opt = decoded.options.find(o => o.number === OptionNumber.Block2);
132
+ const m = block2Opt ? decodeBlockOption(block2Opt.value).m : false;
133
+ resolve({ code: decoded.code, payload: decoded.payload, m, options: decoded.options });
134
+ }
135
+ };
136
+
137
+ sock.on('message', onMessage);
138
+ sock.send(packet, PORT, HOST);
139
+ });
140
+ }
141
+
142
+ const sock = dgram.createSocket('udp4');
143
+ await new Promise<void>((resolve) => sock.bind(0, HOST, resolve));
144
+
145
+ // Test 1: 8 MiB Block1 PUT /firmware
146
+ const FIRMWARE_SIZE = 8 * 1024 * 1024; // 8 MiB
147
+ const firmware = Buffer.alloc(FIRMWARE_SIZE, 0xab);
148
+
149
+ const SZX = 6; // block size = 2^(6+4) = 1024 bytes
150
+ const BLOCK_SIZE = 1024;
151
+ const totalChunks = Math.ceil(FIRMWARE_SIZE / BLOCK_SIZE);
152
+
153
+ const block1Token = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
154
+ let msgId = 1;
155
+
156
+ console.log(`Uploading ${FIRMWARE_SIZE} bytes in ${totalChunks} × ${BLOCK_SIZE}-byte blocks…`);
157
+
158
+ for (let num = 0; num < totalChunks; num++) {
159
+ const start = num * BLOCK_SIZE;
160
+ const end = Math.min(start + BLOCK_SIZE, FIRMWARE_SIZE);
161
+ const chunk = firmware.subarray(start, end);
162
+ const m = num < totalChunks - 1;
163
+
164
+ const resp = await sendBlock1(sock, block1Token, msgId++, num, SZX, m, chunk);
165
+
166
+ if (m) {
167
+ // Expect 2.31 Continue (0x5f)
168
+ if (resp.code !== 0x5f) {
169
+ throw new Error(`Expected 2.31 Continue for chunk ${num}, got 0x${resp.code.toString(16)}`);
170
+ }
171
+ } else {
172
+ // Final chunk — expect 2.04 Changed (0x44)
173
+ if (resp.code !== stringToCode('2.04')) {
174
+ throw new Error(`Expected 2.04 Changed for final chunk, got 0x${resp.code.toString(16)}`);
175
+ }
176
+ console.log('Block1 PUT /firmware: 2.04 Changed ✓');
177
+ }
178
+ }
179
+
180
+ // Test 2: Block2 GET /manifest — reassemble all chunks
181
+ const block2Token = new Uint8Array([0x05, 0x06, 0x07, 0x08]);
182
+ const manifestChunks: Uint8Array[] = [];
183
+ let blockNum = 0;
184
+ const GET_SZX = 5; // 512-byte blocks for manifest
185
+
186
+ while (true) {
187
+ const resp = await sendBlock2Get(sock, block2Token, msgId++, 'manifest', blockNum, GET_SZX);
188
+ if (resp.code !== stringToCode('2.05')) {
189
+ throw new Error(`Expected 2.05 Content for manifest block ${blockNum}, got 0x${resp.code.toString(16)}`);
190
+ }
191
+ manifestChunks.push(resp.payload);
192
+ if (!resp.m) break;
193
+ blockNum++;
194
+ }
195
+
196
+ const manifestBytes = Buffer.concat(manifestChunks);
197
+ if (manifestBytes.length !== MANIFEST.length) {
198
+ throw new Error(`Manifest length mismatch: expected ${MANIFEST.length}, got ${manifestBytes.length}`);
199
+ }
200
+ if (!manifestBytes.equals(MANIFEST)) {
201
+ throw new Error('Manifest content mismatch');
202
+ }
203
+ console.log(`Block2 GET /manifest: ${manifestBytes.length} bytes received, content verified ✓`);
204
+
205
+ sock.close();
206
+ await server.stop();
207
+ console.log('All self-tests passed.');
@@ -0,0 +1,163 @@
1
+ // RFC 7641 Observe — temperature sensor example
2
+ //
3
+ // Server: publishes /temperature as an observable resource (fake sensor, updates every 500 ms).
4
+ // Client: registers via GET+Observe:0, logs 5 notifications, then deregisters via GET+Observe:1.
5
+ //
6
+ // Run: node --experimental-strip-types examples/slice-6-temperature.ts
7
+
8
+ import dgram from 'node:dgram';
9
+ import { createServer, Response } from '@sirena-lwm2m/coap';
10
+ import { createApp } from '@sirena-lwm2m/coap/router';
11
+ import { encodeMessage, decodeMessage, stringToCode } from '@sirena-lwm2m/coap';
12
+ import { OptionNumber, uintToBytes, bytesToUint } from '@sirena-lwm2m/coap';
13
+ import type { Observable, Observer, Unsubscribe } from '@sirena-lwm2m/coap';
14
+
15
+ const PORT = 5686;
16
+ const HOST = '127.0.0.1';
17
+
18
+ // ── Observable temperature subject ───────────────────────────────────────────
19
+
20
+ function makeSubject<T>(): { observable: Observable<T>; next(v: T): void; complete(): void } {
21
+ const subscribers = new Set<Observer<T>>();
22
+ return {
23
+ observable: {
24
+ subscribe(observer: Observer<T>): Unsubscribe {
25
+ subscribers.add(observer);
26
+ return () => { subscribers.delete(observer); };
27
+ },
28
+ },
29
+ next(v: T) {
30
+ for (const obs of subscribers) obs.next(v);
31
+ },
32
+ complete() {
33
+ for (const obs of subscribers) obs.complete?.();
34
+ subscribers.clear();
35
+ },
36
+ };
37
+ }
38
+
39
+ // ── Server ────────────────────────────────────────────────────────────────────
40
+
41
+ const server = createServer({ listen: [{ host: HOST, port: PORT }] });
42
+ const router = createApp({ server });
43
+
44
+ const tempSubject = makeSubject<Uint8Array>();
45
+
46
+ router.add({
47
+ path: '/temperature',
48
+ method: 'GET',
49
+ handler: async (_req, ctx) => {
50
+ const initialReading = encodeTemperature(20.0);
51
+ await ctx.respond(Response.ok(initialReading));
52
+ // ctx.observe() is a no-op for non-Observe GET requests; safe to call unconditionally
53
+ ctx.observe(tempSubject.observable);
54
+ },
55
+ });
56
+
57
+ await server.start();
58
+ console.log('CoAP server listening on', server.listenerSummary());
59
+
60
+ // ── Fake temperature sensor (updates every 500 ms) ────────────────────────────
61
+
62
+ function encodeTemperature(celsius: number): Uint8Array {
63
+ const buf = Buffer.alloc(4);
64
+ buf.writeFloatBE(celsius, 0);
65
+ return buf;
66
+ }
67
+
68
+ function decodeTemperature(payload: Uint8Array): number {
69
+ return Buffer.from(payload).readFloatBE(0);
70
+ }
71
+
72
+ let temp = 20.0;
73
+ const sensorInterval = setInterval(() => {
74
+ temp += (Math.random() - 0.5) * 0.5;
75
+ tempSubject.next(encodeTemperature(temp));
76
+ }, 500);
77
+
78
+ // ── Client ────────────────────────────────────────────────────────────────────
79
+
80
+ const sock = dgram.createSocket('udp4');
81
+ await new Promise<void>((resolve) => sock.bind(0, HOST, resolve));
82
+
83
+ const TOKEN = new Uint8Array([0xca, 0xfe, 0xbe, 0xef]);
84
+ let msgId = 100;
85
+
86
+ function buildObservePacket(deregister: boolean): Uint8Array {
87
+ // Options must be in ascending numeric order per RFC 7252.
88
+ // Observe = 6, UriPath = 11 — Observe goes first.
89
+ return encodeMessage({
90
+ version: 1,
91
+ type: 'CON',
92
+ token: TOKEN,
93
+ code: stringToCode('0.01'), // GET
94
+ messageId: msgId++,
95
+ options: [
96
+ { number: OptionNumber.Observe, value: uintToBytes(deregister ? 1 : 0) },
97
+ { number: OptionNumber.UriPath, value: new TextEncoder().encode('temperature') },
98
+ ],
99
+ payload: new Uint8Array(0),
100
+ });
101
+ }
102
+
103
+ function sendAck(messageId: number): void {
104
+ const ack = encodeMessage({
105
+ version: 1,
106
+ type: 'ACK',
107
+ token: TOKEN,
108
+ code: stringToCode('0.00'), // Empty ACK
109
+ messageId,
110
+ options: [],
111
+ payload: new Uint8Array(0),
112
+ });
113
+ sock.send(ack, PORT, HOST);
114
+ }
115
+
116
+ // Register (GET + Observe:0)
117
+ const registerPacket = buildObservePacket(false);
118
+ sock.send(registerPacket, PORT, HOST);
119
+ console.log('Client: sent Observe registration (GET + Observe:0)');
120
+
121
+ // Collect 5 notifications then deregister
122
+ let notificationCount = 0;
123
+
124
+ await new Promise<void>((resolve, reject) => {
125
+ const timeout = setTimeout(() => reject(new Error('Timed out waiting for 5 notifications')), 15_000);
126
+
127
+ sock.on('message', (msg: Buffer) => {
128
+ const decoded = decodeMessage(msg);
129
+
130
+ // ACK any CON notification so the server can send the next one
131
+ if (decoded.type === 'CON') {
132
+ sendAck(decoded.messageId);
133
+ }
134
+
135
+ const observeOpt = decoded.options.find(o => o.number === OptionNumber.Observe);
136
+ if (observeOpt === undefined) return; // skip non-Observe responses
137
+
138
+ const seq = bytesToUint(observeOpt.value);
139
+ const tempC = decodeTemperature(decoded.payload);
140
+ notificationCount++;
141
+
142
+ console.log(`Notification #${notificationCount}: seq=${seq}, temp=${tempC.toFixed(2)}°C`);
143
+
144
+ if (notificationCount === 5) {
145
+ clearTimeout(timeout);
146
+ resolve();
147
+ }
148
+ });
149
+ });
150
+
151
+ // Deregister (GET + Observe:1)
152
+ const deregisterPacket = buildObservePacket(true);
153
+ sock.send(deregisterPacket, PORT, HOST);
154
+ console.log('Client: sent deregistration (GET + Observe:1)');
155
+
156
+ // Brief pause to allow the server to process the deregistration, then clean up
157
+ await new Promise(r => setTimeout(r, 300));
158
+
159
+ clearInterval(sensorInterval);
160
+ tempSubject.complete();
161
+ sock.close();
162
+ await server.stop();
163
+ console.log('Done — received 5 notifications and deregistered cleanly.');
@@ -0,0 +1,92 @@
1
+ // Slice 7a — SqliteStore example with file-backed dedup store
2
+ //
3
+ // Demonstrates a CoAP server that uses SqliteStore for durable transaction
4
+ // deduplication across restarts. The SQLite database is stored on disk so
5
+ // the server can survive a crash and still correctly dedup retransmissions
6
+ // that arrived before the restart.
7
+ //
8
+ // Run: node --experimental-strip-types examples/slice-7-file-store.ts
9
+
10
+ import * as fs from 'node:fs';
11
+ import * as os from 'node:os';
12
+ import * as path from 'node:path';
13
+ import dgram from 'node:dgram';
14
+ import { createServer, Response, encodeMessage, decodeMessage, stringToCode } from '@sirena-lwm2m/coap';
15
+ import { createSqliteStore } from '@sirena-lwm2m/coap/store';
16
+
17
+ // ── Persistent SQLite store ───────────────────────────────────────────────────
18
+
19
+ const dbDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sirena-coap-'));
20
+ const dbPath = path.join(dbDir, 'dedup.db');
21
+
22
+ console.log(`Using SQLite dedup store at: ${dbPath}`);
23
+
24
+ const store = await createSqliteStore({ path: dbPath });
25
+
26
+ // ── Server ────────────────────────────────────────────────────────────────────
27
+
28
+ const server = createServer({
29
+ listen: ['coap://127.0.0.1:5690'],
30
+ store,
31
+ });
32
+
33
+ let requestCount = 0;
34
+
35
+ server.on('request', async (_req, ctx) => {
36
+ requestCount++;
37
+ console.log(`[server] request #${requestCount} from ${ctx.remote}`);
38
+ await ctx.respond(Response.ok(new TextEncoder().encode(`hello #${requestCount}`)));
39
+ });
40
+
41
+ await server.start();
42
+ console.log('Server started:', server.listenerSummary());
43
+ console.log('Store summary:', server.storeSummary());
44
+
45
+ // ── Client: send same CON twice (dedup demo) ──────────────────────────────────
46
+
47
+ const sock = dgram.createSocket('udp4');
48
+ await new Promise<void>((resolve) => sock.bind(0, '127.0.0.1', resolve));
49
+
50
+ const TOKEN = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
51
+ const MID = 0x1234;
52
+
53
+ const request = encodeMessage({
54
+ version: 1,
55
+ type: 'CON',
56
+ token: TOKEN,
57
+ code: stringToCode('0.01'), // GET
58
+ messageId: MID,
59
+ options: [],
60
+ payload: new Uint8Array(0),
61
+ });
62
+
63
+ const responses: string[] = [];
64
+
65
+ sock.on('message', (msg) => {
66
+ const decoded = decodeMessage(msg);
67
+ responses.push(new TextDecoder().decode(decoded.payload));
68
+ console.log(`[client] received: ${responses[responses.length - 1]}`);
69
+ });
70
+
71
+ // First transmission
72
+ sock.send(request, 5690, '127.0.0.1');
73
+ console.log('[client] sent CON request (first)');
74
+ await new Promise(r => setTimeout(r, 50));
75
+
76
+ // Retransmission of same MID — server should dedup and replay the cached response
77
+ sock.send(request, 5690, '127.0.0.1');
78
+ console.log('[client] retransmitted same CON request (should be deduped)');
79
+ await new Promise(r => setTimeout(r, 50));
80
+
81
+ console.log(`\nResult: ${requestCount} handler invocation(s) for 2 transmissions`);
82
+ console.log(requestCount === 1
83
+ ? '✓ CON retransmission correctly deduped via SqliteStore'
84
+ : '✗ Dedup failed — handler was invoked more than once');
85
+
86
+ // ── Cleanup ───────────────────────────────────────────────────────────────────
87
+
88
+ sock.close();
89
+ store.close();
90
+ await server.stop();
91
+ fs.rmSync(dbDir, { recursive: true, force: true });
92
+ console.log('\nDone.');