@nestor-home/satellite 0.0.0-stage → 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 +21 -0
- package/README.md +163 -2
- package/dist/body.d.ts +27 -0
- package/dist/body.d.ts.map +1 -0
- package/dist/body.js +9 -0
- package/dist/body.js.map +1 -0
- package/dist/browser.d.ts +65 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +161 -0
- package/dist/browser.js.map +1 -0
- package/dist/codes.d.ts +38 -0
- package/dist/codes.d.ts.map +1 -0
- package/dist/codes.js +67 -0
- package/dist/codes.js.map +1 -0
- package/dist/dispatch.d.ts +20 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +67 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/i18n.d.ts +33 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +61 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/locales.d.ts +10 -0
- package/dist/locales.d.ts.map +1 -0
- package/dist/locales.js +64 -0
- package/dist/locales.js.map +1 -0
- package/dist/log.d.ts +10 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +10 -0
- package/dist/log.js.map +1 -0
- package/dist/node/audio.d.ts +83 -0
- package/dist/node/audio.d.ts.map +1 -0
- package/dist/node/audio.js +168 -0
- package/dist/node/audio.js.map +1 -0
- package/dist/node/bin.d.ts +3 -0
- package/dist/node/bin.d.ts.map +1 -0
- package/dist/node/bin.js +4 -0
- package/dist/node/bin.js.map +1 -0
- package/dist/node/cli.d.ts +39 -0
- package/dist/node/cli.d.ts.map +1 -0
- package/dist/node/cli.js +183 -0
- package/dist/node/cli.js.map +1 -0
- package/dist/node/env.d.ts +26 -0
- package/dist/node/env.d.ts.map +1 -0
- package/dist/node/env.js +41 -0
- package/dist/node/env.js.map +1 -0
- package/dist/node/index.d.ts +11 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +11 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/tokens.d.ts +19 -0
- package/dist/node/tokens.d.ts.map +1 -0
- package/dist/node/tokens.js +50 -0
- package/dist/node/tokens.js.map +1 -0
- package/dist/node/voice.d.ts +7 -0
- package/dist/node/voice.d.ts.map +1 -0
- package/dist/node/voice.js +12 -0
- package/dist/node/voice.js.map +1 -0
- package/dist/node/webrtc.d.ts +50 -0
- package/dist/node/webrtc.d.ts.map +1 -0
- package/dist/node/webrtc.js +155 -0
- package/dist/node/webrtc.js.map +1 -0
- package/dist/pairing.d.ts +181 -0
- package/dist/pairing.d.ts.map +1 -0
- package/dist/pairing.js +451 -0
- package/dist/pairing.js.map +1 -0
- package/dist/protocol.d.ts +108 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +136 -0
- package/dist/protocol.js.map +1 -0
- package/dist/qr/index.d.ts +44 -0
- package/dist/qr/index.d.ts.map +1 -0
- package/dist/qr/index.js +107 -0
- package/dist/qr/index.js.map +1 -0
- package/dist/satellite.d.ts +133 -0
- package/dist/satellite.d.ts.map +1 -0
- package/dist/satellite.js +313 -0
- package/dist/satellite.js.map +1 -0
- package/dist/tokens.d.ts +72 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +150 -0
- package/dist/tokens.js.map +1 -0
- package/dist/version.d.ts +4 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/dist/voice.d.ts +92 -0
- package/dist/voice.d.ts.map +1 -0
- package/dist/voice.js +220 -0
- package/dist/voice.js.map +1 -0
- package/dist/w3c.d.ts +88 -0
- package/dist/w3c.d.ts.map +1 -0
- package/dist/w3c.js +117 -0
- package/dist/w3c.js.map +1 -0
- package/dist/webrtc.d.ts +56 -0
- package/dist/webrtc.d.ts.map +1 -0
- package/dist/webrtc.js +11 -0
- package/dist/webrtc.js.map +1 -0
- package/dist/wire.d.ts +132 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +217 -0
- package/dist/wire.js.map +1 -0
- package/package.json +89 -4
- package/schemas/ack.json +41 -0
- package/schemas/hello.json +41 -0
- package/schemas/intent.beat.json +35 -0
- package/schemas/intent.behavior.json +46 -0
- package/schemas/intent.expression.json +53 -0
- package/schemas/intent.identify.json +35 -0
- package/schemas/manifest.json +96 -0
- package/schemas/observation.person.detected.json +48 -0
- package/schemas/observation.speech.activity.json +41 -0
- package/schemas/pairing.enrollment.json +26 -0
- package/schemas/pairing.error.json +39 -0
- package/schemas/pairing.request.json +36 -0
- package/schemas/pairing.state.json +40 -0
- package/schemas/pairing.ticket.json +35 -0
- package/schemas/welcome.json +52 -0
- package/src/body.ts +28 -0
- package/src/browser.ts +217 -0
- package/src/codes.ts +76 -0
- package/src/dispatch.ts +67 -0
- package/src/i18n.ts +70 -0
- package/src/index.ts +116 -0
- package/src/locales.ts +88 -0
- package/src/log.ts +17 -0
- package/src/node/audio.ts +201 -0
- package/src/node/bin.ts +4 -0
- package/src/node/cli.ts +199 -0
- package/src/node/env.ts +54 -0
- package/src/node/index.ts +36 -0
- package/src/node/tokens.ts +59 -0
- package/src/node/voice.ts +16 -0
- package/src/node/webrtc.ts +191 -0
- package/src/pairing.ts +552 -0
- package/src/protocol.ts +208 -0
- package/src/qr/index.ts +130 -0
- package/src/satellite.ts +400 -0
- package/src/tokens.ts +194 -0
- package/src/version.ts +3 -0
- package/src/voice.ts +259 -0
- package/src/w3c.ts +160 -0
- package/src/webrtc.ts +62 -0
- package/src/wire.ts +347 -0
- package/voice/LICENSE +11 -0
- package/voice/en/0.wav +0 -0
- package/voice/en/1.wav +0 -0
- package/voice/en/2.wav +0 -0
- package/voice/en/3.wav +0 -0
- package/voice/en/4.wav +0 -0
- package/voice/en/5.wav +0 -0
- package/voice/en/6.wav +0 -0
- package/voice/en/7.wav +0 -0
- package/voice/en/8.wav +0 -0
- package/voice/en/9.wav +0 -0
- package/voice/en/i-repeat.wav +0 -0
- package/voice/en/pairing-code.wav +0 -0
- package/voice/fr/0.wav +0 -0
- package/voice/fr/1.wav +0 -0
- package/voice/fr/2.wav +0 -0
- package/voice/fr/3.wav +0 -0
- package/voice/fr/4.wav +0 -0
- package/voice/fr/5.wav +0 -0
- package/voice/fr/6.wav +0 -0
- package/voice/fr/7.wav +0 -0
- package/voice/fr/8.wav +0 -0
- package/voice/fr/9.wav +0 -0
- package/voice/fr/code-d-appairage.wav +0 -0
- package/voice/fr/je-repete.wav +0 -0
- package/voice/index.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nestor-home
|
|
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
CHANGED
|
@@ -1,3 +1,164 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nestor-home/satellite
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
TypeScript SDK to write **satellites** for [Nestor](https://github.com/nestor-home/nestor), in
|
|
4
|
+
browsers and in Node: devices (a tab on a wall tablet, a Raspberry Pi speaker, a robot) that pair
|
|
5
|
+
with a Nestor core, stream audio over WebRTC, carry out the core's intents and report what they
|
|
6
|
+
observe, following the `nestor.satellite/2` protocol. The SDK does not depend on the Nestor server.
|
|
7
|
+
|
|
8
|
+
## Pairing
|
|
9
|
+
|
|
10
|
+
A person pairs a satellite from Nestor's admin, with one of two proofs:
|
|
11
|
+
|
|
12
|
+
- an **enrollment code** (`K7QF-2M9X`, or the pairing URI `nestor+http://nestor.local:7860/?code=K7QF2M9X`)
|
|
13
|
+
made in the admin and handed to the device: set in its configuration, typed on it, or scanned;
|
|
14
|
+
- a **device code** (6 digits) the device presents, on its screen, aloud or in its log, and the
|
|
15
|
+
person types in the admin.
|
|
16
|
+
|
|
17
|
+
The SDK uses the enrollment code it is given; otherwise it asks for a device code and presents it
|
|
18
|
+
through every output the driver provides, and always in the log. While it waits, `satellite.enroll()`
|
|
19
|
+
still takes an enrollment code; whichever proof the core accepts first wins. A refused enrollment code
|
|
20
|
+
is never sent again, even after a restart: its SHA-256 hash is kept in the token store, never the code.
|
|
21
|
+
|
|
22
|
+
The manifest's `pairing_methods` follow from what the device can do, never declared by hand:
|
|
23
|
+
|
|
24
|
+
| Method | When |
|
|
25
|
+
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
26
|
+
| `enrollment.provisioning` | always |
|
|
27
|
+
| `device_code.display` | `pairing.display` is given |
|
|
28
|
+
| `device_code.speech` | `pairing.speak` is given, or the device has a speaker (`audio.playback`) and no `display`: the SDK then says the code itself, offline, with recorded clips (`pairing.voice: true` to also do it with a screen) |
|
|
29
|
+
| `enrollment.input` | `pairing.accepts: ["enrollment.input"]` |
|
|
30
|
+
| `enrollment.qr` | `pairing.scanner` reads QR codes with a camera (`@nestor-home/satellite/qr`, browsers) |
|
|
31
|
+
|
|
32
|
+
What the device says to people is translated (English and French, Fluent catalogues): `locale`
|
|
33
|
+
option, else the platform's language. The bundled voice has English and French clips; another
|
|
34
|
+
language is said in English.
|
|
35
|
+
|
|
36
|
+
## In Node, headless
|
|
37
|
+
|
|
38
|
+
`pnpm add @nestor-home/satellite @roamhq/wrtc`. Add a satellite in Nestor with an enrollment code, and
|
|
39
|
+
hand its pairing URI to the device:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
NESTOR_PAIRING_URI='nestor+http://nestor.local:7860/?code=K7QF2M9X' npx @nestor-home/satellite run --room salon
|
|
43
|
+
# or
|
|
44
|
+
npx @nestor-home/satellite run --room salon --pairing-uri 'nestor+http://nestor.local:7860/?code=K7QF2M9X'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The URI gives the core's address; with a bare code (`--pairing-code K7QF-2M9X`, or
|
|
48
|
+
`NESTOR_PAIRING_CODE`), give it with `--core`. Without either, the satellite says a device code to
|
|
49
|
+
type in Nestor on its speaker, and logs it (`--output none`: only logs it). The token is kept in `~/.config/nestor-satellite/<id>.json` (mode 600); see
|
|
50
|
+
`nestor-satellite --help`.
|
|
51
|
+
|
|
52
|
+
The same in code:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { AUDIO_CAPTURE, AUDIO_PLAYBACK, EXPRESSION, Manifest, Satellite } from "@nestor-home/satellite";
|
|
56
|
+
import {
|
|
57
|
+
CommandSink,
|
|
58
|
+
CommandSource,
|
|
59
|
+
FileTokenStore,
|
|
60
|
+
languagesFromEnv,
|
|
61
|
+
nodeWebRtc,
|
|
62
|
+
provisioningFromEnv,
|
|
63
|
+
} from "@nestor-home/satellite/node";
|
|
64
|
+
|
|
65
|
+
const provisioning = provisioningFromEnv(); // NESTOR_PAIRING_URI or NESTOR_PAIRING_CODE, if set
|
|
66
|
+
const satellite = new Satellite({
|
|
67
|
+
core: provisioning?.core ?? "http://nestor.local:7860",
|
|
68
|
+
manifest: Manifest.of(
|
|
69
|
+
{ id: "enceinte-salon", model: "raspberry-pi-4", room: "salon" },
|
|
70
|
+
AUDIO_CAPTURE,
|
|
71
|
+
AUDIO_PLAYBACK,
|
|
72
|
+
EXPRESSION,
|
|
73
|
+
),
|
|
74
|
+
tokens: new FileTokenStore("~/.config/salon.json"), // mode 600
|
|
75
|
+
webrtc: nodeWebRtc({
|
|
76
|
+
source: new CommandSource("arecord -q -t raw -f S16_LE -c 1 -r 48000"),
|
|
77
|
+
sink: new CommandSink("aplay -q -t raw -f S16_LE -c 1 -r 48000"),
|
|
78
|
+
}),
|
|
79
|
+
pairing: { enrollment: provisioning?.enrollment }, // no screen: the device code is said on the sink, and logged
|
|
80
|
+
locale: languagesFromEnv(), // LC_ALL, LC_MESSAGES, LANG
|
|
81
|
+
body: { expression: (expression) => console.log("expression:", expression) },
|
|
82
|
+
});
|
|
83
|
+
const stop = new AbortController();
|
|
84
|
+
process.once("SIGINT", () => stop.abort());
|
|
85
|
+
await satellite.run(stop.signal); // pairs the first time, then reconnects forever
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## In a browser, with a screen
|
|
89
|
+
|
|
90
|
+
Served over `https:` or `localhost`, from the core's origin or through a proxy:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import {
|
|
94
|
+
AUDIO_CAPTURE,
|
|
95
|
+
AUDIO_PLAYBACK,
|
|
96
|
+
browserWebRtc,
|
|
97
|
+
IDENTIFY,
|
|
98
|
+
LocalStorageTokenStore,
|
|
99
|
+
Manifest,
|
|
100
|
+
Satellite,
|
|
101
|
+
} from "@nestor-home/satellite";
|
|
102
|
+
import { cameraQrScanner } from "@nestor-home/satellite/qr"; // needs `pnpm add zxing-wasm`
|
|
103
|
+
import wasmUrl from "zxing-wasm/reader/zxing_reader.wasm?url"; // Vite: served by the page, not a CDN
|
|
104
|
+
|
|
105
|
+
const satellite = new Satellite({
|
|
106
|
+
core: location.origin,
|
|
107
|
+
manifest: Manifest.of(
|
|
108
|
+
{ id: "tablette-cuisine", model: "browser", room: "cuisine" },
|
|
109
|
+
AUDIO_CAPTURE,
|
|
110
|
+
AUDIO_PLAYBACK,
|
|
111
|
+
IDENTIFY,
|
|
112
|
+
),
|
|
113
|
+
tokens: new LocalStorageTokenStore(),
|
|
114
|
+
webrtc: browserWebRtc(),
|
|
115
|
+
pairing: {
|
|
116
|
+
// The device code and what to do with it; `code` is null once it is no longer needed.
|
|
117
|
+
display: (code, text) => {
|
|
118
|
+
document.querySelector("#code")!.textContent = code ?? "";
|
|
119
|
+
document.querySelector("#status")!.textContent = text;
|
|
120
|
+
},
|
|
121
|
+
voice: true, // optional: says the code aloud too, with the bundled voice (Web Audio)
|
|
122
|
+
accepts: ["enrollment.input"], // an enrollment code can be typed in the page
|
|
123
|
+
scanner: cameraQrScanner({ wasmUrl }), // optional: reads the pairing URI's QR code with the camera
|
|
124
|
+
},
|
|
125
|
+
body: {
|
|
126
|
+
behavior: (behavior) => (document.body.dataset.behavior = behavior),
|
|
127
|
+
identify: () => document.body.animate([{ opacity: 0.2 }, { opacity: 1 }], { duration: 500, iterations: 6 }),
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
form.onsubmit = async () => console.log(await satellite.enroll(input.value)); // approved, refused…
|
|
131
|
+
button.onclick = () => satellite.run(); // a click: browsers only play sound after a gesture
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
A tab as a satellite: `NESTOR_CORE=http://nestor.local:7860 pnpm example`, then open the page
|
|
135
|
+
(`?pairing_uri=…` pairs it with an enrollment code, `?lang=fr` speaks French, `?speak` says the code,
|
|
136
|
+
`?qr` scans QR codes).
|
|
137
|
+
|
|
138
|
+
QR codes are decoded by [`zxing-wasm`](https://github.com/Sec-ant/zxing-wasm) (ZXing-C++ in
|
|
139
|
+
WebAssembly, MIT, an optional peer dependency; its reader is about 1 MB of WebAssembly, fetched from
|
|
140
|
+
jsDelivr unless `wasmUrl` says where the page serves it). Node has no camera, so a Node satellite never
|
|
141
|
+
declares `enrollment.qr`.
|
|
142
|
+
|
|
143
|
+
## The bundled voice
|
|
144
|
+
|
|
145
|
+
`voice/` holds the clips the SDK says a device code with (16 kHz mono WAV, about 500 KB), identical in
|
|
146
|
+
the Python SDK, which records them with Kokoro-82M. They are shared under CC BY 4.0: see
|
|
147
|
+
[`voice/LICENSE`](voice/LICENSE) for the attribution. A driver with a voice of its own passes
|
|
148
|
+
`pairing.speak`, which gets the whole sentence.
|
|
149
|
+
|
|
150
|
+
In Node, WebRTC is [`@roamhq/wrtc`](https://github.com/WonderInventions/node-webrtc), an optional
|
|
151
|
+
peer dependency: libwebrtc, as in browsers, with Opus and RTCP built in, prebuilt for Linux and
|
|
152
|
+
macOS (x64, arm64) and Windows x64.
|
|
153
|
+
|
|
154
|
+
## Development
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
pnpm install
|
|
158
|
+
pnpm lint && pnpm typecheck && pnpm test
|
|
159
|
+
pnpm conformance # the server's conformance suite; needs ../nestor with its venv
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT
|
package/dist/body.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a driver implements to carry out the core's intents. Every method is optional: implement
|
|
3
|
+
* what the device can do.
|
|
4
|
+
*
|
|
5
|
+
* Calls come from the network loop and must return at once: keep the state, animate elsewhere.
|
|
6
|
+
* Physical safety (limits, watchdogs) belongs to the driver and never depends on the link.
|
|
7
|
+
*/
|
|
8
|
+
import type { Behavior, Expression } from "./protocol.js";
|
|
9
|
+
export interface Body {
|
|
10
|
+
/** The assistant's attitude changed (idle, listening, thinking, speaking): show it until the next. */
|
|
11
|
+
behavior?(behavior: Behavior): void;
|
|
12
|
+
/** Perform an expression now: it arrives as the matching audio is played. Needs `expression.emotion`. */
|
|
13
|
+
expression?(expression: Expression, intensity: number): void;
|
|
14
|
+
/** A word is being played: a light speech accent. Needs `motion.head`. */
|
|
15
|
+
beat?(): void;
|
|
16
|
+
/**
|
|
17
|
+
* Make the device noticed for a few seconds (blink, wiggle, chime, flash the screen), so a person
|
|
18
|
+
* tells it apart from others: asked while it waits to be paired, and once paired
|
|
19
|
+
* (`intent.identify`). A driver that implements it declares `device.identify`.
|
|
20
|
+
*/
|
|
21
|
+
identify?(): void;
|
|
22
|
+
/** The core was just heard from (a message or audio). A watchdog can rest the device when heartbeats stop. */
|
|
23
|
+
heartbeat?(): void;
|
|
24
|
+
/** The link to the core came up (welcomed) or went down. */
|
|
25
|
+
connected?(up: boolean): void;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=body.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"body.d.ts","sourceRoot":"","sources":["../src/body.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE1D,MAAM,WAAW,IAAI;IACnB,sGAAsG;IACtG,QAAQ,CAAC,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IACpC,yGAAyG;IACzG,UAAU,CAAC,CAAC,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7D,0EAA0E;IAC1E,IAAI,CAAC,IAAI,IAAI,CAAC;IACd;;;;OAIG;IACH,QAAQ,CAAC,IAAI,IAAI,CAAC;IAClB,8GAA8G;IAC9G,SAAS,CAAC,IAAI,IAAI,CAAC;IACnB,4DAA4D;IAC5D,SAAS,CAAC,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAC;CAC/B"}
|
package/dist/body.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a driver implements to carry out the core's intents. Every method is optional: implement
|
|
3
|
+
* what the device can do.
|
|
4
|
+
*
|
|
5
|
+
* Calls come from the network loop and must return at once: keep the state, animate elsewhere.
|
|
6
|
+
* Physical safety (limits, watchdogs) belongs to the driver and never depends on the link.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=body.js.map
|
package/dist/body.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"body.js","sourceRoot":"","sources":["../src/body.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser's own WebRTC: `RTCPeerConnection`, the microphone through `getUserMedia`, the core's
|
|
3
|
+
* voice through an `<audio>` element.
|
|
4
|
+
*
|
|
5
|
+
* Browsers only open the microphone in a secure context (`https:` or `http://localhost`), and only
|
|
6
|
+
* play sound after a user gesture: start the satellite from a click.
|
|
7
|
+
*/
|
|
8
|
+
import { type Logger } from "./log.js";
|
|
9
|
+
import type { Speaker } from "./voice.js";
|
|
10
|
+
import type { WebRtc } from "./webrtc.js";
|
|
11
|
+
/** What this adapter needs of an `<audio>` element (an `HTMLAudioElement` fits). */
|
|
12
|
+
export interface AudioElementLike {
|
|
13
|
+
srcObject: unknown;
|
|
14
|
+
play(): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export interface BrowserWebRtcOptions {
|
|
17
|
+
/** Constraints for `getUserMedia({audio})`; echo cancellation, noise suppression and gain control by default. */
|
|
18
|
+
readonly microphone?: Readonly<Record<string, boolean | number | string>>;
|
|
19
|
+
/** Where the core's voice plays (default: a detached `new Audio()`). */
|
|
20
|
+
readonly element?: AudioElementLike;
|
|
21
|
+
/** Where the SDK says the device code itself (default: `webAudioSpeaker()`, `null` for none). */
|
|
22
|
+
readonly speaker?: Speaker | null;
|
|
23
|
+
readonly logger?: Logger;
|
|
24
|
+
}
|
|
25
|
+
export declare function browserWebRtc(options?: BrowserWebRtcOptions): WebRtc;
|
|
26
|
+
/** What `browserSpeech` needs of the Web Speech API (`window.speechSynthesis` fits). */
|
|
27
|
+
export interface SpeechSynthesisLike {
|
|
28
|
+
speak(utterance: unknown): void;
|
|
29
|
+
cancel(): void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The browser's voice (Web Speech API) as a pairing output: `pairing: { speak: browserSpeech({ lang }) }`
|
|
33
|
+
* declares `device_code.speech`. `undefined` when this browser cannot speak, so nothing is declared.
|
|
34
|
+
* `lang` should match the satellite's `locale`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function browserSpeech(options?: {
|
|
37
|
+
readonly lang?: string;
|
|
38
|
+
}): ((text: string) => void) | undefined;
|
|
39
|
+
/** What `webAudioSpeaker` needs of Web Audio (`AudioContext` fits). */
|
|
40
|
+
export interface AudioContextLike {
|
|
41
|
+
readonly currentTime: number;
|
|
42
|
+
readonly destination: unknown;
|
|
43
|
+
resume(): Promise<void>;
|
|
44
|
+
createBuffer(channels: number, length: number, sampleRate: number): {
|
|
45
|
+
copyToChannel(source: Float32Array<ArrayBuffer>, channel: number): void;
|
|
46
|
+
readonly duration: number;
|
|
47
|
+
};
|
|
48
|
+
createBufferSource(): {
|
|
49
|
+
buffer: unknown;
|
|
50
|
+
connect(destination: unknown): unknown;
|
|
51
|
+
start(when: number): void;
|
|
52
|
+
stop(): void;
|
|
53
|
+
onended: (() => void) | null;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The page's speaker through Web Audio, with the bundled voice fetched from the package: the device
|
|
58
|
+
* code sounds the same as on any other satellite, offline. `undefined` when the browser has no Web
|
|
59
|
+
* Audio. Browsers only play sound after a user gesture: start the satellite from a click.
|
|
60
|
+
*/
|
|
61
|
+
export declare function webAudioSpeaker(options?: {
|
|
62
|
+
readonly context?: () => AudioContextLike;
|
|
63
|
+
readonly load?: (path: string) => Promise<Uint8Array>;
|
|
64
|
+
}): Speaker | undefined;
|
|
65
|
+
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAY,KAAK,MAAM,EAAe,MAAM,UAAU,CAAC;AAC9D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,OAAO,KAAK,EAAiC,MAAM,EAAE,MAAM,aAAa,CAAC;AAEzE,oFAAoF;AACpF,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,iHAAiH;IACjH,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC;IAC1E,wEAAwE;IACxE,QAAQ,CAAC,OAAO,CAAC,EAAE,gBAAgB,CAAC;IACpC,iGAAiG;IACjG,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAaD,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAExE;AAoED,wFAAwF;AACxF,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,CAAC;IAChC,MAAM,IAAI,IAAI,CAAC;CAChB;AAOD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE;IAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAS5G;AAED,uEAAuE;AACvE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,YAAY,CACV,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,GACjB;QAAE,aAAa,CAAC,MAAM,EAAE,YAAY,CAAC,WAAW,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1G,kBAAkB,IAAI;QACpB,MAAM,EAAE,OAAO,CAAC;QAChB,OAAO,CAAC,WAAW,EAAE,OAAO,GAAG,OAAO,CAAC;QACvC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAC1B,IAAI,IAAI,IAAI,CAAC;QACb,OAAO,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC;KAC9B,CAAC;CACH;AAaD;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,OAAO,GAAE;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,gBAAgB,CAAC;IAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAA;CAAO,GACjH,OAAO,GAAG,SAAS,CA0CrB"}
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser's own WebRTC: `RTCPeerConnection`, the microphone through `getUserMedia`, the core's
|
|
3
|
+
* voice through an `<audio>` element.
|
|
4
|
+
*
|
|
5
|
+
* Browsers only open the microphone in a secure context (`https:` or `http://localhost`), and only
|
|
6
|
+
* play sound after a user gesture: start the satellite from a click.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, quietLogger } from "./log.js";
|
|
9
|
+
import { W3cPeer } from "./w3c.js";
|
|
10
|
+
const DEFAULT_MICROPHONE = { echoCancellation: true, noiseSuppression: true, autoGainControl: true };
|
|
11
|
+
export function browserWebRtc(options = {}) {
|
|
12
|
+
return new BrowserWebRtc(options);
|
|
13
|
+
}
|
|
14
|
+
class BrowserWebRtc {
|
|
15
|
+
options;
|
|
16
|
+
speaker;
|
|
17
|
+
logger;
|
|
18
|
+
microphone = null;
|
|
19
|
+
element = null;
|
|
20
|
+
playback = false;
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.options = options;
|
|
23
|
+
this.logger = options.logger ?? quietLogger;
|
|
24
|
+
this.speaker = (options.speaker === undefined ? webAudioSpeaker() : options.speaker) ?? undefined;
|
|
25
|
+
}
|
|
26
|
+
async open(needs) {
|
|
27
|
+
const media = globals();
|
|
28
|
+
if (media.RTCPeerConnection === undefined)
|
|
29
|
+
throw new Error("this browser has no WebRTC (RTCPeerConnection)");
|
|
30
|
+
this.playback = needs.playback;
|
|
31
|
+
if (needs.playback)
|
|
32
|
+
this.element = this.options.element ?? (media.Audio ? new media.Audio() : null);
|
|
33
|
+
if (!needs.capture)
|
|
34
|
+
return;
|
|
35
|
+
const devices = media.navigator?.mediaDevices;
|
|
36
|
+
if (devices === undefined) {
|
|
37
|
+
throw new Error("audio.capture is declared but this page cannot open a microphone (needs https or localhost)");
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const stream = await devices.getUserMedia({ audio: this.options.microphone ?? DEFAULT_MICROPHONE });
|
|
41
|
+
this.microphone = stream.getAudioTracks()[0] ?? null;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw new Error(`audio.capture is declared but the microphone did not open: ${describe(error)}`);
|
|
45
|
+
}
|
|
46
|
+
if (this.microphone === null)
|
|
47
|
+
throw new Error("audio.capture is declared but no microphone track came");
|
|
48
|
+
}
|
|
49
|
+
createPeer(options) {
|
|
50
|
+
const Connection = globals().RTCPeerConnection;
|
|
51
|
+
if (Connection === undefined)
|
|
52
|
+
throw new Error("this browser has no WebRTC (RTCPeerConnection)");
|
|
53
|
+
return new W3cPeer(Connection, this, options, this.logger);
|
|
54
|
+
}
|
|
55
|
+
async close() {
|
|
56
|
+
this.microphone?.stop();
|
|
57
|
+
this.microphone = null;
|
|
58
|
+
if (this.element !== null)
|
|
59
|
+
this.element.srcObject = null;
|
|
60
|
+
}
|
|
61
|
+
outgoing() {
|
|
62
|
+
return this.microphone;
|
|
63
|
+
}
|
|
64
|
+
incoming(event, onAudio) {
|
|
65
|
+
onAudio();
|
|
66
|
+
const element = this.element;
|
|
67
|
+
if (!this.playback || element === null)
|
|
68
|
+
return () => { };
|
|
69
|
+
const Stream = globals().MediaStream;
|
|
70
|
+
element.srcObject = event.streams[0] ?? (Stream ? new Stream([event.track]) : null);
|
|
71
|
+
element.play().catch((error) => {
|
|
72
|
+
this.logger.warn(`the core's voice cannot play yet (start the satellite from a click): ${describe(error)}`);
|
|
73
|
+
});
|
|
74
|
+
return () => {
|
|
75
|
+
if (element.srcObject !== null)
|
|
76
|
+
element.srcObject = null;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function globals() {
|
|
81
|
+
return globalThis;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The browser's voice (Web Speech API) as a pairing output: `pairing: { speak: browserSpeech({ lang }) }`
|
|
85
|
+
* declares `device_code.speech`. `undefined` when this browser cannot speak, so nothing is declared.
|
|
86
|
+
* `lang` should match the satellite's `locale`.
|
|
87
|
+
*/
|
|
88
|
+
export function browserSpeech(options = {}) {
|
|
89
|
+
const { speechSynthesis, SpeechSynthesisUtterance: Utterance } = globalThis;
|
|
90
|
+
if (speechSynthesis === undefined || Utterance === undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
return (text) => {
|
|
93
|
+
const utterance = new Utterance(text);
|
|
94
|
+
if (options.lang !== undefined)
|
|
95
|
+
utterance.lang = options.lang;
|
|
96
|
+
speechSynthesis.cancel(); // the latest sentence replaces the one still being said
|
|
97
|
+
speechSynthesis.speak(utterance);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* A file of the bundled voice, in the package next to this module. Written so bundlers find the files:
|
|
102
|
+
* Vite turns each template into the list of what it matches, one folder level per placeholder.
|
|
103
|
+
*/
|
|
104
|
+
function voiceUrl(path) {
|
|
105
|
+
const [folder, file] = path.split("/");
|
|
106
|
+
return file === undefined
|
|
107
|
+
? new URL(`../voice/${path}`, import.meta.url)
|
|
108
|
+
: new URL(`../voice/${folder}/${file}`, import.meta.url);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The page's speaker through Web Audio, with the bundled voice fetched from the package: the device
|
|
112
|
+
* code sounds the same as on any other satellite, offline. `undefined` when the browser has no Web
|
|
113
|
+
* Audio. Browsers only play sound after a user gesture: start the satellite from a click.
|
|
114
|
+
*/
|
|
115
|
+
export function webAudioSpeaker(options = {}) {
|
|
116
|
+
const Context = globalThis.AudioContext;
|
|
117
|
+
const create = options.context ?? (Context === undefined ? undefined : () => new Context());
|
|
118
|
+
if (create === undefined)
|
|
119
|
+
return undefined;
|
|
120
|
+
const load = options.load ??
|
|
121
|
+
(async (path) => {
|
|
122
|
+
const response = await fetch(voiceUrl(path));
|
|
123
|
+
if (!response.ok)
|
|
124
|
+
throw new Error(`${path}: HTTP ${response.status}`);
|
|
125
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
126
|
+
});
|
|
127
|
+
let context = null;
|
|
128
|
+
let next = 0;
|
|
129
|
+
const playing = new Set();
|
|
130
|
+
const sampleRate = 16_000; // the clips' own rate: Web Audio resamples to the device's
|
|
131
|
+
return {
|
|
132
|
+
sampleRate,
|
|
133
|
+
load,
|
|
134
|
+
write(pcm) {
|
|
135
|
+
if (context === null) {
|
|
136
|
+
context = create();
|
|
137
|
+
void context.resume().catch(() => { });
|
|
138
|
+
}
|
|
139
|
+
const buffer = context.createBuffer(1, pcm.length, sampleRate);
|
|
140
|
+
const floats = new Float32Array(pcm.length);
|
|
141
|
+
for (let i = 0; i < pcm.length; i++)
|
|
142
|
+
floats[i] = (pcm[i] ?? 0) / 32768;
|
|
143
|
+
buffer.copyToChannel(floats, 0);
|
|
144
|
+
const source = context.createBufferSource();
|
|
145
|
+
source.buffer = buffer;
|
|
146
|
+
source.connect(context.destination);
|
|
147
|
+
next = Math.max(next, context.currentTime);
|
|
148
|
+
source.start(next);
|
|
149
|
+
next += buffer.duration;
|
|
150
|
+
playing.add(source);
|
|
151
|
+
source.onended = () => playing.delete(source);
|
|
152
|
+
},
|
|
153
|
+
flush() {
|
|
154
|
+
for (const source of playing)
|
|
155
|
+
source.stop();
|
|
156
|
+
playing.clear();
|
|
157
|
+
next = 0;
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
|
package/dist/codes.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The codes of `nestor.satellite/2` pairing, and the pairing URI that carries an enrollment code.
|
|
3
|
+
*
|
|
4
|
+
* Device code: 6 decimal digits the core gives the device, which presents them (screen, voice, log)
|
|
5
|
+
* for a person to type in the admin.
|
|
6
|
+
*
|
|
7
|
+
* Enrollment code: 8 symbols of Crockford's base32 (`K7QF-2M9X`), made in the admin and handed to
|
|
8
|
+
* the device (configured, typed or scanned). Reading one back forgives case, dashes, spaces and the
|
|
9
|
+
* letters Crockford maps to digits (O → 0, I and L → 1).
|
|
10
|
+
*
|
|
11
|
+
* Pairing URI: `nestor+http://nestor.local:7860/?code=K7QF2M9X`, the core's base URL prefixed with
|
|
12
|
+
* `nestor+`, and the enrollment code.
|
|
13
|
+
*/
|
|
14
|
+
/** Crockford's base32: no I, L, O, U. */
|
|
15
|
+
export declare const ENROLLMENT_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
16
|
+
export declare const ENROLLMENT_LENGTH = 8;
|
|
17
|
+
export declare const DEVICE_CODE: RegExp;
|
|
18
|
+
/** The canonical form of a typed or scanned enrollment code (`k7qf-2m9x` → `K7QF2M9X`); `null` when it cannot be one. */
|
|
19
|
+
export declare function normalizeEnrollmentCode(text: string): string | null;
|
|
20
|
+
/** How an enrollment code is shown to people: `K7QF-2M9X`. */
|
|
21
|
+
export declare function groupEnrollmentCode(code: string): string;
|
|
22
|
+
export interface PairingUri {
|
|
23
|
+
/** The core's base URL, e.g. `http://nestor.local:7860`. */
|
|
24
|
+
readonly core: string;
|
|
25
|
+
/** The enrollment code, canonical form. */
|
|
26
|
+
readonly code: string;
|
|
27
|
+
}
|
|
28
|
+
/** The core's base URL and the enrollment code of `nestor+http(s)://host:port/path/?code=…`. */
|
|
29
|
+
export declare function parsePairingUri(uri: string): PairingUri;
|
|
30
|
+
/**
|
|
31
|
+
* What a person handed the device: a pairing URI (`core` is then its core) or a bare enrollment code
|
|
32
|
+
* (`core` is `null`). `null` when it is neither.
|
|
33
|
+
*/
|
|
34
|
+
export declare function readEnrollment(text: string): {
|
|
35
|
+
readonly core: string | null;
|
|
36
|
+
readonly code: string;
|
|
37
|
+
} | null;
|
|
38
|
+
//# sourceMappingURL=codes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codes.d.ts","sourceRoot":"","sources":["../src/codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,yCAAyC;AACzC,eAAO,MAAM,mBAAmB,qCAAqC,CAAC;AACtE,eAAO,MAAM,iBAAiB,IAAI,CAAC;AACnC,eAAO,MAAM,WAAW,QAAY,CAAC;AAIrC,yHAAyH;AACzH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOnE;AAED,8DAA8D;AAC9D,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,MAAM,WAAW,UAAU;IACzB,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,gGAAgG;AAChG,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAavD;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAU3G"}
|
package/dist/codes.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The codes of `nestor.satellite/2` pairing, and the pairing URI that carries an enrollment code.
|
|
3
|
+
*
|
|
4
|
+
* Device code: 6 decimal digits the core gives the device, which presents them (screen, voice, log)
|
|
5
|
+
* for a person to type in the admin.
|
|
6
|
+
*
|
|
7
|
+
* Enrollment code: 8 symbols of Crockford's base32 (`K7QF-2M9X`), made in the admin and handed to
|
|
8
|
+
* the device (configured, typed or scanned). Reading one back forgives case, dashes, spaces and the
|
|
9
|
+
* letters Crockford maps to digits (O → 0, I and L → 1).
|
|
10
|
+
*
|
|
11
|
+
* Pairing URI: `nestor+http://nestor.local:7860/?code=K7QF2M9X`, the core's base URL prefixed with
|
|
12
|
+
* `nestor+`, and the enrollment code.
|
|
13
|
+
*/
|
|
14
|
+
import { ProtocolError } from "./protocol.js";
|
|
15
|
+
/** Crockford's base32: no I, L, O, U. */
|
|
16
|
+
export const ENROLLMENT_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
17
|
+
export const ENROLLMENT_LENGTH = 8;
|
|
18
|
+
export const DEVICE_CODE = /^\d{6}$/;
|
|
19
|
+
const ALIASES = { O: "0", I: "1", L: "1" };
|
|
20
|
+
/** The canonical form of a typed or scanned enrollment code (`k7qf-2m9x` → `K7QF2M9X`); `null` when it cannot be one. */
|
|
21
|
+
export function normalizeEnrollmentCode(text) {
|
|
22
|
+
const code = text
|
|
23
|
+
.replace(/[\s-]/g, "")
|
|
24
|
+
.toUpperCase()
|
|
25
|
+
.replace(/[OIL]/g, (c) => ALIASES[c] ?? c);
|
|
26
|
+
if (code.length !== ENROLLMENT_LENGTH || [...code].some((c) => !ENROLLMENT_ALPHABET.includes(c)))
|
|
27
|
+
return null;
|
|
28
|
+
return code;
|
|
29
|
+
}
|
|
30
|
+
/** How an enrollment code is shown to people: `K7QF-2M9X`. */
|
|
31
|
+
export function groupEnrollmentCode(code) {
|
|
32
|
+
return `${code.slice(0, 4)}-${code.slice(4)}`;
|
|
33
|
+
}
|
|
34
|
+
/** The core's base URL and the enrollment code of `nestor+http(s)://host:port/path/?code=…`. */
|
|
35
|
+
export function parsePairingUri(uri) {
|
|
36
|
+
const invalid = () => new ProtocolError("a pairing URI looks like nestor+http://host:port/?code=K7QF2M9X");
|
|
37
|
+
let url;
|
|
38
|
+
try {
|
|
39
|
+
url = new URL(uri.trim());
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw invalid();
|
|
43
|
+
}
|
|
44
|
+
const scheme = { "nestor+http:": "http", "nestor+https:": "https" }[url.protocol];
|
|
45
|
+
const codes = url.searchParams.getAll("code");
|
|
46
|
+
const code = codes.length === 1 && codes[0] !== undefined ? normalizeEnrollmentCode(codes[0]) : null;
|
|
47
|
+
if (scheme === undefined || url.host === "" || code === null)
|
|
48
|
+
throw invalid();
|
|
49
|
+
return { core: `${scheme}://${url.host}${url.pathname.replace(/\/+$/, "")}`, code };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* What a person handed the device: a pairing URI (`core` is then its core) or a bare enrollment code
|
|
53
|
+
* (`core` is `null`). `null` when it is neither.
|
|
54
|
+
*/
|
|
55
|
+
export function readEnrollment(text) {
|
|
56
|
+
if (/^nestor\+/i.test(text.trim())) {
|
|
57
|
+
try {
|
|
58
|
+
return parsePairingUri(text);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const code = normalizeEnrollmentCode(text);
|
|
65
|
+
return code === null ? null : { core: null, code };
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=codes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codes.js","sourceRoot":"","sources":["../src/codes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the satellite does with each message from the core: hand intents to the body and
|
|
3
|
+
* acknowledge them, take note of the welcome, notice the core hanging up. The connection reports
|
|
4
|
+
* the link up to the body once it is welcomed.
|
|
5
|
+
*/
|
|
6
|
+
import type { Body } from "./body.js";
|
|
7
|
+
import { type Logger } from "./log.js";
|
|
8
|
+
import { type AckMessage, type Welcome } from "./wire.js";
|
|
9
|
+
export type Outcome = "nothing" | "welcomed" | "hung-up";
|
|
10
|
+
export declare class Dispatcher {
|
|
11
|
+
private readonly body;
|
|
12
|
+
private readonly reply;
|
|
13
|
+
private readonly logger;
|
|
14
|
+
welcome: Welcome | null;
|
|
15
|
+
constructor(body: Body, reply: (message: AckMessage) => void, logger?: Logger);
|
|
16
|
+
handle(raw: string): Outcome;
|
|
17
|
+
/** A failing body is the driver's bug: it is logged, never allowed to break the link. */
|
|
18
|
+
private call;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=dispatch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAY,KAAK,MAAM,EAAe,MAAM,UAAU,CAAC;AAC9D,OAAO,EAAE,KAAK,UAAU,EAA0B,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AAElF,MAAM,MAAM,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;AAEzD,qBAAa,UAAU;IAInB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IALzB,OAAO,EAAE,OAAO,GAAG,IAAI,CAAQ;gBAGZ,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,IAAI,EACpC,MAAM,GAAE,MAAoB;IAG/C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAqC5B,yFAAyF;IACzF,OAAO,CAAC,IAAI;CAOb"}
|