@omirion/orbit-sdk 0.1.1 → 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/README.md +29 -19
- package/dist/index.cjs +235 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +176 -103
- package/dist/index.d.ts +176 -103
- package/dist/index.js +235 -75
- package/dist/index.js.map +1 -1
- package/examples/device-rpc-flow/README.md +43 -0
- package/examples/device-rpc-flow/index.tsx +502 -0
- package/examples/device-rpc-flow/package.json +23 -0
- package/examples/device-rpc-flow/tsconfig.json +14 -0
- package/examples/webhook-listener/README.md +41 -0
- package/examples/webhook-listener/index.ts +110 -0
- package/examples/webhook-listener/package.json +19 -0
- package/examples/webhook-listener/tsconfig.json +13 -0
- package/package.json +2 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* webhook-listener, the smallest useful Orbit webhook receiver.
|
|
3
|
+
*
|
|
4
|
+
* Starts an HTTP server, verifies the HMAC signature of every delivery with
|
|
5
|
+
* `parseWebhookEvent`, ACKs it, and prints what arrived: the event type plus
|
|
6
|
+
* a one-line, type-specific summary.
|
|
7
|
+
*
|
|
8
|
+
* Run with: ORBIT_WEBHOOK_SECRET=whsec_… pnpm start
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createServer } from 'node:http'
|
|
12
|
+
import { parseWebhookEvent, type OrbitWebhookEvent } from '@omirion/orbit-sdk'
|
|
13
|
+
|
|
14
|
+
const secret = process.env.ORBIT_WEBHOOK_SECRET
|
|
15
|
+
if (!secret) {
|
|
16
|
+
console.error('Set ORBIT_WEBHOOK_SECRET to your subscription signing secret (whsec_…).')
|
|
17
|
+
process.exit(1)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const port = Number(process.env.PORT ?? 3900)
|
|
21
|
+
|
|
22
|
+
// ANSI helpers, enough color to scan the feed, no dependency.
|
|
23
|
+
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`
|
|
24
|
+
const bold = (s: string) => `\x1b[1m${s}\x1b[0m`
|
|
25
|
+
const green = (s: string) => `\x1b[32m${s}\x1b[0m`
|
|
26
|
+
const red = (s: string) => `\x1b[31m${s}\x1b[0m`
|
|
27
|
+
const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`
|
|
28
|
+
const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`
|
|
29
|
+
|
|
30
|
+
const TYPE_COLOR: Record<string, (s: string) => string> = {
|
|
31
|
+
'device.online': green,
|
|
32
|
+
'device.offline': red,
|
|
33
|
+
'telemetry.metric_changed': cyan,
|
|
34
|
+
'command.result': yellow,
|
|
35
|
+
'deployment.status_changed': cyan,
|
|
36
|
+
'admission.requested': yellow,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One line that tells you what actually happened, per event type. */
|
|
40
|
+
function summarize(event: OrbitWebhookEvent): string {
|
|
41
|
+
const d = event.data as Record<string, any>
|
|
42
|
+
switch (event.type) {
|
|
43
|
+
case 'device.online':
|
|
44
|
+
case 'device.offline':
|
|
45
|
+
return `device ${d.deviceName ?? d.deviceId}`
|
|
46
|
+
case 'telemetry.metric_changed': {
|
|
47
|
+
const changes = Object.entries(d.changes ?? {})
|
|
48
|
+
.map(([key, c]: [string, any]) => `${key}: ${c.previous} → ${c.current}`)
|
|
49
|
+
.join(', ')
|
|
50
|
+
return `device ${d.deviceId} · ${changes}`
|
|
51
|
+
}
|
|
52
|
+
case 'command.result':
|
|
53
|
+
return `device ${d.deviceId} · ${d.command?.name} → ${d.success ? 'ok' : `failed (exit ${d.exitCode})`}`
|
|
54
|
+
case 'deployment.status_changed':
|
|
55
|
+
return `device ${d.deviceId} · deployment ${d.deploymentId} · ${d.previousStatus ?? '∅'} → ${d.status}`
|
|
56
|
+
case 'admission.requested':
|
|
57
|
+
return `host ${d.hostname ?? 'unknown'} · fingerprint ${d.fingerprint}`
|
|
58
|
+
default:
|
|
59
|
+
return JSON.stringify(d)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const seen = new Set<string>() // event-id dedup, deliveries are at-least-once
|
|
64
|
+
|
|
65
|
+
const server = createServer((req, res) => {
|
|
66
|
+
if (req.method !== 'POST') {
|
|
67
|
+
res.writeHead(405).end()
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const chunks: Buffer[] = []
|
|
72
|
+
req.on('data', (chunk) => chunks.push(chunk))
|
|
73
|
+
req.on('end', async () => {
|
|
74
|
+
// parseWebhookEvent needs the EXACT bytes that were signed, never
|
|
75
|
+
// re-serialize parsed JSON.
|
|
76
|
+
const rawBody = Buffer.concat(chunks).toString('utf8')
|
|
77
|
+
|
|
78
|
+
const event = await parseWebhookEvent({
|
|
79
|
+
secret,
|
|
80
|
+
rawBody,
|
|
81
|
+
signature: String(req.headers['x-orbit-signature'] ?? ''),
|
|
82
|
+
timestamp: String(req.headers['x-orbit-timestamp'] ?? ''),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if (!event) {
|
|
86
|
+
console.log(red('✗ rejected delivery (bad signature or outside the replay window)'))
|
|
87
|
+
res.writeHead(401).end()
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ACK fast (any 2xx within 10 s), process after responding.
|
|
92
|
+
res.writeHead(204).end()
|
|
93
|
+
|
|
94
|
+
if (seen.has(event.id)) {
|
|
95
|
+
console.log(dim(`↻ duplicate delivery of ${event.id}, ignored`))
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
seen.add(event.id)
|
|
99
|
+
|
|
100
|
+
const paint = TYPE_COLOR[event.type] ?? ((s: string) => s)
|
|
101
|
+
console.log(
|
|
102
|
+
`${dim(new Date(event.createdAt).toLocaleTimeString())} ${paint(bold(event.type.padEnd(27)))} ${summarize(event)}`
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
server.listen(port, () => {
|
|
108
|
+
console.log(`Listening for Orbit webhooks on ${bold(`http://localhost:${port}`)}`)
|
|
109
|
+
console.log(dim('Waiting for events, toggle a device, run a command, or deploy firmware…\n'))
|
|
110
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@omirion/example-webhook-listener",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Minimal Orbit webhook receiver: verify each delivery's signature and print the event type.",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "tsx index.ts",
|
|
9
|
+
"typecheck": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@omirion/orbit-sdk": "^0.1.1"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/node": "^22.10.0",
|
|
16
|
+
"tsx": "^4.19.2",
|
|
17
|
+
"typescript": "^5.7.2"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omirion/orbit-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Official TypeScript SDK for the Omirion Orbit public API (/api/v1).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"dist",
|
|
19
|
+
"examples",
|
|
19
20
|
"README.md",
|
|
20
21
|
"LICENSE"
|
|
21
22
|
],
|