@totemsdk/edge-coap 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Totem SDK Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # @totemsdk/edge-coap
2
+
3
+ Edge runtime adapter for CoAP — constrained devices, RFC 7252, UDP transport.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @totemsdk/edge-coap
9
+ ```
10
+
11
+ ## Design
12
+
13
+ This package does **not** import `dgram`, `coap`, or any UDP library. All network behaviour is injected via `CoapTransportPort`. The package handles CoAP framing (CON/NON/ACK/RST, options, tokens) and request/response matching.
14
+
15
+ ## Quick start
16
+
17
+ ```ts
18
+ import { createEdgeRuntime, createEdgeDevice, createCapabilitySet } from '@totemsdk/edge';
19
+ import { createCoapGateway, createCoapSensorBridge } from '@totemsdk/edge-coap';
20
+ import type { CoapTransportPort } from '@totemsdk/edge-coap';
21
+
22
+ // 1. Implement CoapTransportPort
23
+ const transport: CoapTransportPort = {
24
+ async bind(port) { /* bind UDP socket */ },
25
+ async close() { /* close socket */ },
26
+ async send(host, port, message) { /* send UDP datagram */ },
27
+ onMessage(handler) { /* register inbound handler */ return () => {}; },
28
+ onError(handler) { /* register error handler */ return () => {}; },
29
+ };
30
+
31
+ // 2. Wire into Edge runtime
32
+ const runtime = createEdgeRuntime({
33
+ deviceId: 'coap-gateway-01',
34
+ capabilities: createCapabilitySet(['transport:coap', 'proof:create']),
35
+ ports: { /* proof port */ },
36
+ });
37
+
38
+ // 3. Create gateway
39
+ const gateway = createCoapGateway({ runtime, transport, localPort: 5683 });
40
+ await gateway.start();
41
+
42
+ // 4. GET a resource
43
+ const result = await gateway.get(['sensors', 'temperature'], '192.168.1.100', 5683);
44
+ console.log(result.data?.payload);
45
+
46
+ // 5. POST to a resource
47
+ await gateway.post(['actuators', 'valve'], new TextEncoder().encode('open'), '192.168.1.101', 5683);
48
+
49
+ // 6. Or use the sensor bridge for automated polling
50
+ const bridge = createCoapSensorBridge({
51
+ runtime,
52
+ transport,
53
+ gateway,
54
+ bindings: [
55
+ { sensorId: 'temp-1', path: ['sensors', 'temperature'], host: '192.168.1.100', port: 5683, intervalMs: 5000 },
56
+ ],
57
+ });
58
+ await bridge.start();
59
+ ```
60
+
61
+ ## Transport port
62
+
63
+ | Method | Description |
64
+ |--------|-------------|
65
+ | `bind(port)` | Bind UDP socket to local port |
66
+ | `close()` | Close the socket |
67
+ | `send(host, port, message)` | Send CoAP message bytes to remote endpoint |
68
+ | `onMessage(handler)` | Register handler for inbound messages with remote address |
69
+ | `onError(handler)` | Register handler for socket errors |
70
+
71
+ ## Supported methods
72
+
73
+ | Method | Gateway method |
74
+ |--------|---------------|
75
+ | GET | `get(path, host, port)` |
76
+ | POST | `post(path, payload, host, port)` |
77
+
78
+ ## Capabilities
79
+
80
+ - `transport:coap` — required for CoAP transport
81
+
82
+ ## License
83
+
84
+ MIT
@@ -0,0 +1,22 @@
1
+ /**
2
+ * CoAP Edge Gateway — wires a CoapTransportPort into an EdgeRuntime.
3
+ */
4
+ import type { EdgeRuntime, EdgeOperationResult } from '@totemsdk/edge';
5
+ import type { CoapTransportPort } from './transport.js';
6
+ export interface CoapGatewayConfig {
7
+ runtime: EdgeRuntime;
8
+ transport: CoapTransportPort;
9
+ localPort: number;
10
+ }
11
+ export interface CoapGateway {
12
+ start(): Promise<void>;
13
+ stop(): Promise<void>;
14
+ 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
+ }>>;
21
+ }
22
+ export declare function createCoapGateway(config: CoapGatewayConfig): CoapGateway;
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ /**
3
+ * CoAP Edge Gateway — wires a CoapTransportPort into an EdgeRuntime.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createCoapGateway = createCoapGateway;
7
+ function createCoapGateway(config) {
8
+ let status = 'stopped';
9
+ let unsubMsg;
10
+ let unsubErr;
11
+ const pending = new Map();
12
+ let nextId = 0;
13
+ return {
14
+ get status() { return status; },
15
+ async start() {
16
+ if (status === 'running')
17
+ return;
18
+ await config.transport.bind(config.localPort);
19
+ unsubMsg = config.transport.onMessage((raw, remote) => {
20
+ try {
21
+ const msg = decodeCoap(raw, remote);
22
+ if (msg.type === 'ACK' && msg.token.length > 0) {
23
+ const tokenId = new DataView(msg.token.buffer, msg.token.byteOffset, 4).getUint32(0, false);
24
+ const p = pending.get(tokenId);
25
+ if (p) {
26
+ clearTimeout(p.timer);
27
+ pending.delete(tokenId);
28
+ p.resolve(msg.payload);
29
+ }
30
+ }
31
+ }
32
+ catch { /* drop */ }
33
+ });
34
+ unsubErr = config.transport.onError(() => { status = 'error'; });
35
+ status = 'running';
36
+ },
37
+ async stop() {
38
+ unsubMsg?.();
39
+ unsubErr?.();
40
+ for (const [, p] of pending) {
41
+ clearTimeout(p.timer);
42
+ p.reject(new Error('Gateway stopped'));
43
+ }
44
+ pending.clear();
45
+ await config.transport.close();
46
+ status = 'stopped';
47
+ },
48
+ async get(path, host, port) {
49
+ return coapRequest(config.transport, 'GET', path, new Uint8Array(0), host, port, pending, nextId++);
50
+ },
51
+ async post(path, payload, host, port) {
52
+ return coapRequest(config.transport, 'POST', path, payload, host, port, pending, nextId++);
53
+ },
54
+ };
55
+ }
56
+ async function coapRequest(transport, method, path, payload, host, port, pending, messageId, timeoutMs = 10000) {
57
+ const token = new Uint8Array(4);
58
+ new DataView(token.buffer).setUint32(0, messageId, false);
59
+ const frame = encodeCoap({ type: 'CON', method, messageId, token, path, payload, remote: { host, port }, receivedAt: Date.now() });
60
+ await transport.send(host, port, frame);
61
+ return new Promise((resolve) => {
62
+ const timer = setTimeout(() => { pending.delete(messageId); resolve({ ok: false, error: 'CoAP request timed out' }); }, timeoutMs);
63
+ pending.set(messageId, { resolve: (v) => resolve({ ok: true, data: { payload: v } }), reject: (e) => resolve({ ok: false, error: e.message }), timer });
64
+ });
65
+ }
66
+ function encodeCoap(msg) {
67
+ const pathBytes = msg.path.map(p => [11, ...new TextEncoder().encode(p)]).flat();
68
+ const tokenLen = Math.min(msg.token.length, 8);
69
+ const header = new Uint8Array(4 + tokenLen);
70
+ header[0] = 0x40 | (msg.type === 'CON' ? 0 : msg.type === 'NON' ? 0x10 : msg.type === 'ACK' ? 0x20 : 0x30);
71
+ header[0] |= tokenLen;
72
+ header[1] = msg.method === 'GET' ? 1 : msg.method === 'POST' ? 2 : msg.method === 'PUT' ? 3 : 4;
73
+ new DataView(header.buffer).setUint16(2, msg.messageId, false);
74
+ header.set(msg.token.slice(0, tokenLen), 4);
75
+ const options = new Uint8Array(pathBytes.length + 1);
76
+ options.set(new Uint8Array(pathBytes), 0);
77
+ options[pathBytes.length] = 0xFF;
78
+ const result = new Uint8Array(header.length + options.length + msg.payload.length);
79
+ result.set(header);
80
+ result.set(options, header.length);
81
+ result.set(msg.payload, header.length + options.length);
82
+ return result;
83
+ }
84
+ function decodeCoap(raw, remote) {
85
+ const tokenLen = raw[0] & 0x0F;
86
+ const typeCode = (raw[0] >> 4) & 0x03;
87
+ const type = typeCode === 0 ? 'CON' : typeCode === 1 ? 'NON' : typeCode === 2 ? 'ACK' : 'RST';
88
+ const code = raw[1];
89
+ const messageId = new DataView(raw.buffer, raw.byteOffset + 2, 2).getUint16(0, false);
90
+ const token = raw.slice(4, 4 + tokenLen);
91
+ let offset = 4 + tokenLen;
92
+ const path = [];
93
+ while (offset < raw.length && raw[offset] !== 0xFF) {
94
+ const delta = (raw[offset] >> 4) & 0x0F;
95
+ const len = raw[offset] & 0x0F;
96
+ offset++;
97
+ if (delta === 11)
98
+ path.push(new TextDecoder().decode(raw.slice(offset, offset + len)));
99
+ offset += len;
100
+ }
101
+ if (offset < raw.length && raw[offset] === 0xFF)
102
+ offset++;
103
+ const payload = raw.slice(offset);
104
+ return { type, messageId, token, path, payload, remote, receivedAt: Date.now() };
105
+ }
@@ -0,0 +1,5 @@
1
+ export type { CoapTransportPort, CoapMessage, CoapMessageType, CoapMethod } from './transport.js';
2
+ export { createCoapGateway } from './gateway.js';
3
+ export type { CoapGatewayConfig, CoapGateway } from './gateway.js';
4
+ export { createCoapSensorBridge } from './sensor-bridge.js';
5
+ export type { CoapSensorBinding, CoapSensorBridgeConfig, CoapSensorBridge } from './sensor-bridge.js';
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCoapSensorBridge = 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 sensor_bridge_js_1 = require("./sensor-bridge.js");
7
+ Object.defineProperty(exports, "createCoapSensorBridge", { enumerable: true, get: function () { return sensor_bridge_js_1.createCoapSensorBridge; } });
@@ -0,0 +1,26 @@
1
+ /**
2
+ * CoAP Sensor Bridge — maps CoAP resource observations to Edge proof inputs.
3
+ */
4
+ import type { EdgeRuntime } from '@totemsdk/edge';
5
+ import type { CoapTransportPort } from './transport.js';
6
+ import type { CoapGateway } from './gateway.js';
7
+ export interface CoapSensorBinding {
8
+ sensorId: string;
9
+ path: string[];
10
+ host: string;
11
+ port: number;
12
+ intervalMs: number;
13
+ dataType?: string;
14
+ }
15
+ export interface CoapSensorBridgeConfig {
16
+ runtime: EdgeRuntime;
17
+ transport: CoapTransportPort;
18
+ gateway?: CoapGateway;
19
+ bindings: CoapSensorBinding[];
20
+ }
21
+ export interface CoapSensorBridge {
22
+ start(): Promise<void>;
23
+ stop(): Promise<void>;
24
+ poll(): Promise<void>;
25
+ }
26
+ export declare function createCoapSensorBridge(config: CoapSensorBridgeConfig): CoapSensorBridge;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ /**
3
+ * CoAP Sensor Bridge — maps CoAP resource observations to Edge proof inputs.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createCoapSensorBridge = createCoapSensorBridge;
7
+ function createCoapSensorBridge(config) {
8
+ const timers = [];
9
+ let running = false;
10
+ async function pollBinding(binding) {
11
+ try {
12
+ const gw = config.gateway;
13
+ if (!gw)
14
+ return;
15
+ const result = await gw.get(binding.path, binding.host, binding.port);
16
+ if (result.ok && config.runtime.ports.proof) {
17
+ await config.runtime.ports.proof.createProof({
18
+ subject: `sensor:${binding.sensorId}`,
19
+ claims: [{
20
+ sensorId: binding.sensorId,
21
+ path: binding.path.join('/'),
22
+ dataType: binding.dataType ?? 'coap-observation',
23
+ payload: result.data?.payload,
24
+ timestamp: Date.now(),
25
+ }],
26
+ });
27
+ }
28
+ }
29
+ catch { /* non-fatal */ }
30
+ }
31
+ return {
32
+ async start() {
33
+ if (running)
34
+ return;
35
+ for (const b of config.bindings) {
36
+ await pollBinding(b);
37
+ timers.push(setInterval(() => pollBinding(b), b.intervalMs));
38
+ }
39
+ running = true;
40
+ },
41
+ async stop() { for (const t of timers)
42
+ clearInterval(t); timers.length = 0; running = false; },
43
+ async poll() { for (const b of config.bindings)
44
+ await pollBinding(b); },
45
+ };
46
+ }
@@ -0,0 +1,47 @@
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';
25
+ export interface CoapMessage {
26
+ /** Message type. */
27
+ type: CoapMessageType;
28
+ /** Request method (only for CON/NON requests). */
29
+ method?: CoapMethod;
30
+ /** Response code (only for ACK responses). */
31
+ responseCode?: string;
32
+ /** Message ID for deduplication. */
33
+ messageId: number;
34
+ /** Token for request/response matching. */
35
+ token: Uint8Array;
36
+ /** URI path (e.g. ["sensors", "temperature"]). */
37
+ path: string[];
38
+ /** Payload bytes. */
39
+ payload: Uint8Array;
40
+ /** Remote endpoint. */
41
+ remote: {
42
+ host: string;
43
+ port: number;
44
+ };
45
+ /** Timestamp of receipt. */
46
+ receivedAt: number;
47
+ }
@@ -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 });
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@totemsdk/edge-coap",
3
+ "version": "0.1.0",
4
+ "description": "Edge runtime adapter for CoAP — constrained devices, RFC 7252, UDP transport",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "require": "./dist/index.js",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "prepare": "tsc",
17
+ "clean": "rm -rf dist",
18
+ "test": "jest --passWithNoTests"
19
+ },
20
+ "files": ["dist", "README.md", "LICENSE"],
21
+ "dependencies": {
22
+ "@totemsdk/edge": "^0.1.3"
23
+ },
24
+ "devDependencies": {
25
+ "@types/jest": "^30.0.0",
26
+ "@types/node": "^20.0.0",
27
+ "jest": "^30.4.2",
28
+ "ts-jest": "^29.0.0",
29
+ "typescript": "^7.0.2"
30
+ },
31
+ "engines": { "node": ">=18.0.0" },
32
+ "publishConfig": { "access": "public" },
33
+ "license": "MIT",
34
+ "author": "Totem SDK",
35
+ "homepage": "https://totem.ing",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/totem-sdk/totem-sdk.git",
39
+ "directory": "packages/totem-sdk/packages/edge-coap"
40
+ },
41
+ "keywords": ["totem", "totemsdk", "minima", "blockchain", "edge", "coap", "transport"],
42
+ "bugs": { "url": "https://github.com/totem-sdk/totem-sdk/issues" }
43
+ }