@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.
@@ -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
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "noEmit": true,
10
+ "types": ["node"]
11
+ },
12
+ "include": ["index.ts"]
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omirion/orbit-sdk",
3
- "version": "0.1.1",
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
  ],