@nestor-home/satellite 0.0.0-stage → 0.2.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 (174) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -2
  3. package/dist/body.d.ts +27 -0
  4. package/dist/body.d.ts.map +1 -0
  5. package/dist/body.js +9 -0
  6. package/dist/body.js.map +1 -0
  7. package/dist/browser.d.ts +65 -0
  8. package/dist/browser.d.ts.map +1 -0
  9. package/dist/browser.js +161 -0
  10. package/dist/browser.js.map +1 -0
  11. package/dist/codes.d.ts +38 -0
  12. package/dist/codes.d.ts.map +1 -0
  13. package/dist/codes.js +67 -0
  14. package/dist/codes.js.map +1 -0
  15. package/dist/dispatch.d.ts +20 -0
  16. package/dist/dispatch.d.ts.map +1 -0
  17. package/dist/dispatch.js +67 -0
  18. package/dist/dispatch.js.map +1 -0
  19. package/dist/i18n.d.ts +33 -0
  20. package/dist/i18n.d.ts.map +1 -0
  21. package/dist/i18n.js +61 -0
  22. package/dist/i18n.js.map +1 -0
  23. package/dist/index.d.ts +24 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +21 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/locales.d.ts +10 -0
  28. package/dist/locales.d.ts.map +1 -0
  29. package/dist/locales.js +64 -0
  30. package/dist/locales.js.map +1 -0
  31. package/dist/log.d.ts +10 -0
  32. package/dist/log.d.ts.map +1 -0
  33. package/dist/log.js +10 -0
  34. package/dist/log.js.map +1 -0
  35. package/dist/node/audio.d.ts +83 -0
  36. package/dist/node/audio.d.ts.map +1 -0
  37. package/dist/node/audio.js +168 -0
  38. package/dist/node/audio.js.map +1 -0
  39. package/dist/node/bin.d.ts +3 -0
  40. package/dist/node/bin.d.ts.map +1 -0
  41. package/dist/node/bin.js +4 -0
  42. package/dist/node/bin.js.map +1 -0
  43. package/dist/node/cli.d.ts +39 -0
  44. package/dist/node/cli.d.ts.map +1 -0
  45. package/dist/node/cli.js +183 -0
  46. package/dist/node/cli.js.map +1 -0
  47. package/dist/node/env.d.ts +26 -0
  48. package/dist/node/env.d.ts.map +1 -0
  49. package/dist/node/env.js +41 -0
  50. package/dist/node/env.js.map +1 -0
  51. package/dist/node/index.d.ts +11 -0
  52. package/dist/node/index.d.ts.map +1 -0
  53. package/dist/node/index.js +11 -0
  54. package/dist/node/index.js.map +1 -0
  55. package/dist/node/tokens.d.ts +19 -0
  56. package/dist/node/tokens.d.ts.map +1 -0
  57. package/dist/node/tokens.js +50 -0
  58. package/dist/node/tokens.js.map +1 -0
  59. package/dist/node/voice.d.ts +7 -0
  60. package/dist/node/voice.d.ts.map +1 -0
  61. package/dist/node/voice.js +12 -0
  62. package/dist/node/voice.js.map +1 -0
  63. package/dist/node/webrtc.d.ts +50 -0
  64. package/dist/node/webrtc.d.ts.map +1 -0
  65. package/dist/node/webrtc.js +155 -0
  66. package/dist/node/webrtc.js.map +1 -0
  67. package/dist/pairing.d.ts +181 -0
  68. package/dist/pairing.d.ts.map +1 -0
  69. package/dist/pairing.js +451 -0
  70. package/dist/pairing.js.map +1 -0
  71. package/dist/protocol.d.ts +108 -0
  72. package/dist/protocol.d.ts.map +1 -0
  73. package/dist/protocol.js +136 -0
  74. package/dist/protocol.js.map +1 -0
  75. package/dist/qr/index.d.ts +44 -0
  76. package/dist/qr/index.d.ts.map +1 -0
  77. package/dist/qr/index.js +107 -0
  78. package/dist/qr/index.js.map +1 -0
  79. package/dist/satellite.d.ts +133 -0
  80. package/dist/satellite.d.ts.map +1 -0
  81. package/dist/satellite.js +313 -0
  82. package/dist/satellite.js.map +1 -0
  83. package/dist/tokens.d.ts +72 -0
  84. package/dist/tokens.d.ts.map +1 -0
  85. package/dist/tokens.js +150 -0
  86. package/dist/tokens.js.map +1 -0
  87. package/dist/version.d.ts +4 -0
  88. package/dist/version.d.ts.map +1 -0
  89. package/dist/version.js +4 -0
  90. package/dist/version.js.map +1 -0
  91. package/dist/voice.d.ts +92 -0
  92. package/dist/voice.d.ts.map +1 -0
  93. package/dist/voice.js +220 -0
  94. package/dist/voice.js.map +1 -0
  95. package/dist/w3c.d.ts +88 -0
  96. package/dist/w3c.d.ts.map +1 -0
  97. package/dist/w3c.js +117 -0
  98. package/dist/w3c.js.map +1 -0
  99. package/dist/webrtc.d.ts +56 -0
  100. package/dist/webrtc.d.ts.map +1 -0
  101. package/dist/webrtc.js +11 -0
  102. package/dist/webrtc.js.map +1 -0
  103. package/dist/wire.d.ts +132 -0
  104. package/dist/wire.d.ts.map +1 -0
  105. package/dist/wire.js +217 -0
  106. package/dist/wire.js.map +1 -0
  107. package/package.json +89 -4
  108. package/schemas/ack.json +41 -0
  109. package/schemas/hello.json +41 -0
  110. package/schemas/intent.beat.json +35 -0
  111. package/schemas/intent.behavior.json +46 -0
  112. package/schemas/intent.expression.json +53 -0
  113. package/schemas/intent.identify.json +35 -0
  114. package/schemas/manifest.json +96 -0
  115. package/schemas/observation.person.detected.json +48 -0
  116. package/schemas/observation.speech.activity.json +41 -0
  117. package/schemas/pairing.enrollment.json +26 -0
  118. package/schemas/pairing.error.json +39 -0
  119. package/schemas/pairing.request.json +36 -0
  120. package/schemas/pairing.state.json +40 -0
  121. package/schemas/pairing.ticket.json +35 -0
  122. package/schemas/welcome.json +52 -0
  123. package/src/body.ts +28 -0
  124. package/src/browser.ts +217 -0
  125. package/src/codes.ts +76 -0
  126. package/src/dispatch.ts +67 -0
  127. package/src/i18n.ts +70 -0
  128. package/src/index.ts +116 -0
  129. package/src/locales.ts +88 -0
  130. package/src/log.ts +17 -0
  131. package/src/node/audio.ts +201 -0
  132. package/src/node/bin.ts +4 -0
  133. package/src/node/cli.ts +199 -0
  134. package/src/node/env.ts +54 -0
  135. package/src/node/index.ts +36 -0
  136. package/src/node/tokens.ts +59 -0
  137. package/src/node/voice.ts +16 -0
  138. package/src/node/webrtc.ts +191 -0
  139. package/src/pairing.ts +552 -0
  140. package/src/protocol.ts +208 -0
  141. package/src/qr/index.ts +130 -0
  142. package/src/satellite.ts +400 -0
  143. package/src/tokens.ts +194 -0
  144. package/src/version.ts +3 -0
  145. package/src/voice.ts +259 -0
  146. package/src/w3c.ts +160 -0
  147. package/src/webrtc.ts +62 -0
  148. package/src/wire.ts +347 -0
  149. package/voice/LICENSE +11 -0
  150. package/voice/en/0.wav +0 -0
  151. package/voice/en/1.wav +0 -0
  152. package/voice/en/2.wav +0 -0
  153. package/voice/en/3.wav +0 -0
  154. package/voice/en/4.wav +0 -0
  155. package/voice/en/5.wav +0 -0
  156. package/voice/en/6.wav +0 -0
  157. package/voice/en/7.wav +0 -0
  158. package/voice/en/8.wav +0 -0
  159. package/voice/en/9.wav +0 -0
  160. package/voice/en/i-repeat.wav +0 -0
  161. package/voice/en/pairing-code.wav +0 -0
  162. package/voice/fr/0.wav +0 -0
  163. package/voice/fr/1.wav +0 -0
  164. package/voice/fr/2.wav +0 -0
  165. package/voice/fr/3.wav +0 -0
  166. package/voice/fr/4.wav +0 -0
  167. package/voice/fr/5.wav +0 -0
  168. package/voice/fr/6.wav +0 -0
  169. package/voice/fr/7.wav +0 -0
  170. package/voice/fr/8.wav +0 -0
  171. package/voice/fr/9.wav +0 -0
  172. package/voice/fr/code-d-appairage.wav +0 -0
  173. package/voice/fr/je-repete.wav +0 -0
  174. 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,73 @@
1
- # Temporary Holding Version
1
+ # @nestor-home/satellite
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ TypeScript SDK to write **satellites** for [Nestor](https://nestorhome.dev), in browsers and in
4
+ Node: devices (a tab on a wall tablet, a Raspberry Pi speaker, a robot) that pair with a Nestor
5
+ server, stream audio over WebRTC, carry out its intents and report what they observe, following the
6
+ `nestor.satellite/1` protocol. It does not depend on the Nestor server.
7
+
8
+ **Documentation:** [nestorhome.dev/docs/sdks/typescript](https://nestorhome.dev/docs/sdks/typescript/)
9
+ (Node, pairing options, QR codes, the command, conformance).
10
+
11
+ ```bash
12
+ pnpm add @nestor-home/satellite
13
+ pnpm add @roamhq/wrtc # in Node only
14
+ ```
15
+
16
+ A tab as a satellite:
17
+
18
+ ```ts
19
+ import {
20
+ AUDIO_CAPTURE,
21
+ AUDIO_PLAYBACK,
22
+ browserWebRtc,
23
+ IDENTIFY,
24
+ LocalStorageTokenStore,
25
+ Manifest,
26
+ Satellite,
27
+ } from "@nestor-home/satellite";
28
+
29
+ const satellite = new Satellite({
30
+ core: location.origin,
31
+ manifest: Manifest.of(
32
+ { id: "tablette-cuisine", model: "browser", room: "cuisine" },
33
+ AUDIO_CAPTURE,
34
+ AUDIO_PLAYBACK,
35
+ IDENTIFY,
36
+ ),
37
+ tokens: new LocalStorageTokenStore(),
38
+ webrtc: browserWebRtc(),
39
+ pairing: {
40
+ // the device code to type in Nestor; `code` is null once it is no longer needed
41
+ display: (code, text) => {
42
+ document.querySelector("#code")!.textContent = code ?? "";
43
+ document.querySelector("#status")!.textContent = text;
44
+ },
45
+ },
46
+ body: {
47
+ behavior: (behavior) => (document.body.dataset.behavior = behavior),
48
+ identify: () => document.body.animate([{ opacity: 0.2 }, { opacity: 1 }], { duration: 500, iterations: 6 }),
49
+ },
50
+ });
51
+ button.onclick = () => satellite.run(); // a click: browsers only play sound after a gesture
52
+ ```
53
+
54
+ In Node, the machine's microphone and speaker, with nothing to write:
55
+
56
+ ```bash
57
+ NESTOR_PAIRING_URI='nestor+http://nestor.local:7860/?code=K7QF2M9X' npx nestor-satellite run --room salon
58
+ ```
59
+
60
+ The recorded voice that says device codes is shared under CC BY 4.0: see
61
+ [`voice/LICENSE`](voice/LICENSE).
62
+
63
+ ## Development
64
+
65
+ ```bash
66
+ pnpm install
67
+ pnpm lint && pnpm typecheck && pnpm test
68
+ pnpm conformance # the published server's conformance suite (uvx; version in .nestor-version)
69
+ ```
70
+
71
+ ## License
72
+
73
+ 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
@@ -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"}
@@ -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":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,QAAQ,EAAe,WAAW,EAAE,MAAM,UAAU,CAAC;AAE9D,OAAO,EAAoB,OAAO,EAAoD,MAAM,UAAU,CAAC;AA4BvG,MAAM,kBAAkB,GAAG,EAAE,gBAAgB,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC;AAErG,MAAM,UAAU,aAAa,CAAC,UAAgC,EAAE;IAC9D,OAAO,IAAI,aAAa,CAAC,OAAO,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,aAAa;IAOY;IANpB,OAAO,CAAsB;IACrB,MAAM,CAAS;IACxB,UAAU,GAAoB,IAAI,CAAC;IACnC,OAAO,GAA4B,IAAI,CAAC;IACxC,QAAQ,GAAG,KAAK,CAAC;IAEzB,YAA6B,OAA6B;QAA7B,YAAO,GAAP,OAAO,CAAsB;QACxD,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,WAAW,CAAC;QAC5C,IAAI,CAAC,OAAO,GAAG,CAAC,OAAO,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,eAAe,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,SAAS,CAAC;IACpG,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,KAAiB;QAC1B,MAAM,KAAK,GAAG,OAAO,EAAE,CAAC;QACxB,IAAI,KAAK,CAAC,iBAAiB,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;QAC7G,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;QAC/B,IAAI,KAAK,CAAC,QAAQ;YAAE,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACpG,IAAI,CAAC,KAAK,CAAC,OAAO;YAAE,OAAO;QAC3B,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,EAAE,YAAY,CAAC;QAC9C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CAAC,6FAA6F,CAAC,CAAC;QACjH,CAAC;QACD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,kBAAkB,EAAE,CAAC,CAAC;YACpG,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;QACvD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,8DAA8D,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACnG,CAAC;QACD,IAAI,IAAI,CAAC,UAAU,KAAK,IAAI;YAAE,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;IAC1G,CAAC;IAED,UAAU,CAAC,OAAoB;QAC7B,MAAM,UAAU,GAAG,OAAO,EAAE,CAAC,iBAAiB,CAAC;QAC/C,IAAI,UAAU,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;QAChG,OAAO,IAAI,OAAO,CAAC,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IAC7D,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC;QACxB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACvB,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI;YAAE,IAAI,CAAC,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAC3D,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAED,QAAQ,CAAC,KAAuD,EAAE,OAAmB;QACnF,OAAO,EAAE,CAAC;QACV,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC7B,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO,GAAG,EAAE,GAAE,CAAC,CAAC;QACxD,MAAM,MAAM,GAAG,OAAO,EAAE,CAAC,WAAW,CAAC;QACrC,OAAO,CAAC,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACpF,OAAO,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YACtC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,wEAAwE,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC9G,CAAC,CAAC,CAAC;QACH,OAAO,GAAG,EAAE;YACV,IAAI,OAAO,CAAC,SAAS,KAAK,IAAI;gBAAE,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;QAC3D,CAAC,CAAC;IACJ,CAAC;CACF;AAED,SAAS,OAAO;IACd,OAAO,UAA0B,CAAC;AACpC,CAAC;AAaD;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,UAAsC,EAAE;IACpE,MAAM,EAAE,eAAe,EAAE,wBAAwB,EAAE,SAAS,EAAE,GAAG,UAA2B,CAAC;IAC7F,IAAI,eAAe,KAAK,SAAS,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC/E,OAAO,CAAC,IAAI,EAAE,EAAE;QACd,MAAM,SAAS,GAAG,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS;YAAE,SAAS,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QAC9D,eAAe,CAAC,MAAM,EAAE,CAAC,CAAC,wDAAwD;QAClF,eAAe,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACnC,CAAC,CAAC;AACJ,CAAC;AAqBD;;;GAGG;AACH,SAAS,QAAQ,CAAC,IAAY;IAC5B,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACvC,OAAO,IAAI,KAAK,SAAS;QACvB,CAAC,CAAC,IAAI,GAAG,CAAC,YAAY,IAAI,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;QAC9C,CAAC,CAAC,IAAI,GAAG,CAAC,YAAY,MAAM,IAAI,IAAI,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC7D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAC7B,UAAgH,EAAE;IAElH,MAAM,OAAO,GAAI,UAA4D,CAAC,YAAY,CAAC;IAC3F,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,IAAI,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,IAAI,OAAO,EAAE,CAAC,CAAC;IAC5F,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC3C,MAAM,IAAI,GACR,OAAO,CAAC,IAAI;QACZ,CAAC,KAAK,EAAE,IAAY,EAAE,EAAE;YACtB,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;YAC7C,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,UAAU,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;YACtE,OAAO,IAAI,UAAU,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC;QACtD,CAAC,CAAC,CAAC;IACL,IAAI,OAAO,GAA4B,IAAI,CAAC;IAC5C,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,MAAM,OAAO,GAAG,IAAI,GAAG,EAAsD,CAAC;IAC9E,MAAM,UAAU,GAAG,MAAM,CAAC,CAAC,2DAA2D;IACtF,OAAO;QACL,UAAU;QACV,IAAI;QACJ,KAAK,CAAC,GAAG;YACP,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;gBACrB,OAAO,GAAG,MAAM,EAAE,CAAC;gBACnB,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YACxC,CAAC;YACD,MAAM,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;YAC/D,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAC5C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE;gBAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC;YACvE,MAAM,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;YAChC,MAAM,MAAM,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;YAC5C,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC;YACvB,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;YACpC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;YAC3C,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACnB,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC;YACxB,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACpB,MAAM,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAChD,CAAC;QACD,KAAK;YACH,KAAK,MAAM,MAAM,IAAI,OAAO;gBAAE,MAAM,CAAC,IAAI,EAAE,CAAC;YAC5C,OAAO,CAAC,KAAK,EAAE,CAAC;YAChB,IAAI,GAAG,CAAC,CAAC;QACX,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The codes of `nestor.satellite/1` 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/1` 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"}
@@ -0,0 +1,67 @@
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 { describe, quietLogger } from "./log.js";
7
+ import { ack, parse, signalling } from "./wire.js";
8
+ export class Dispatcher {
9
+ body;
10
+ reply;
11
+ logger;
12
+ welcome = null;
13
+ constructor(body, reply, logger = quietLogger) {
14
+ this.body = body;
15
+ this.reply = reply;
16
+ this.logger = logger;
17
+ }
18
+ handle(raw) {
19
+ let data;
20
+ let received;
21
+ try {
22
+ data = JSON.parse(raw);
23
+ const transport = signalling(data);
24
+ if (transport !== null)
25
+ return transport.hungUp ? "hung-up" : "nothing";
26
+ received = parse(data);
27
+ }
28
+ catch (error) {
29
+ this.logger.warn(`ignoring an off-contract message from the core: ${describe(error)}`);
30
+ return "nothing";
31
+ }
32
+ this.call(() => this.body.heartbeat?.());
33
+ const message = received.message;
34
+ if (message === null)
35
+ return "nothing"; // a newer message type: nothing to acknowledge
36
+ if (message.type === "welcome") {
37
+ this.welcome = message;
38
+ return "welcomed";
39
+ }
40
+ switch (message.type) {
41
+ case "intent.behavior":
42
+ this.call(() => this.body.behavior?.(message.behavior));
43
+ break;
44
+ case "intent.expression":
45
+ this.call(() => this.body.expression?.(message.expression, message.intensity));
46
+ break;
47
+ case "intent.beat":
48
+ this.call(() => this.body.beat?.());
49
+ break;
50
+ case "intent.identify":
51
+ this.call(() => this.body.identify?.());
52
+ break;
53
+ }
54
+ this.reply(ack(received.id)); // received, whatever the body made of it
55
+ return "nothing";
56
+ }
57
+ /** A failing body is the driver's bug: it is logged, never allowed to break the link. */
58
+ call(action) {
59
+ try {
60
+ action();
61
+ }
62
+ catch (error) {
63
+ this.logger.warn(`the body failed: ${describe(error)}`);
64
+ }
65
+ }
66
+ }
67
+ //# sourceMappingURL=dispatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,QAAQ,EAAe,WAAW,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,EAAmB,GAAG,EAAE,KAAK,EAAE,UAAU,EAAgB,MAAM,WAAW,CAAC;AAIlF,MAAM,OAAO,UAAU;IAIF;IACA;IACA;IALnB,OAAO,GAAmB,IAAI,CAAC;IAE/B,YACmB,IAAU,EACV,KAAoC,EACpC,SAAiB,WAAW;QAF5B,SAAI,GAAJ,IAAI,CAAM;QACV,UAAK,GAAL,KAAK,CAA+B;QACpC,WAAM,GAAN,MAAM,CAAsB;IAC5C,CAAC;IAEJ,MAAM,CAAC,GAAW;QAChB,IAAI,IAAa,CAAC;QAClB,IAAI,QAAkC,CAAC;QACvC,IAAI,CAAC;YACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACvB,MAAM,SAAS,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;YACnC,IAAI,SAAS,KAAK,IAAI;gBAAE,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;YACxE,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,mDAAmD,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACvF,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QACzC,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC;QACjC,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO,SAAS,CAAC,CAAC,+CAA+C;QACvF,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC/B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;YACvB,OAAO,UAAU,CAAC;QACpB,CAAC;QACD,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;YACrB,KAAK,iBAAiB;gBACpB,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;gBACxD,MAAM;YACR,KAAK,mBAAmB;gBACtB,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;gBAC/E,MAAM;YACR,KAAK,aAAa;gBAChB,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;gBACpC,MAAM;YACR,KAAK,iBAAiB;gBACpB,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;gBACxC,MAAM;QACV,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,yCAAyC;QACvE,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,yFAAyF;IACjF,IAAI,CAAC,MAAkB;QAC7B,IAAI,CAAC;YACH,MAAM,EAAE,CAAC;QACX,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,oBAAoB,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC1D,CAAC;IACH,CAAC;CACF"}
package/dist/i18n.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * What a satellite says to people (its device code, a refused enrollment code, why it cannot pair),
3
+ * in their language. The catalogues are Fluent (`./locales.ts`); English is the fallback.
4
+ *
5
+ * The locale comes from the `locale` option, else the platform (`navigator.languages` in browsers
6
+ * and in Node), else English. Node's CLI reads `LC_ALL`, `LC_MESSAGES` and `LANG` itself.
7
+ */
8
+ import { type MessageId } from "./locales.js";
9
+ export type { MessageId } from "./locales.js";
10
+ declare const CATALOGUES: {
11
+ readonly en: "\ndevice-code-log = Pairing code: { $code }. Type it in Nestor to add this device.\ndevice-code-display = Type this code in Nestor to add this device.\n# The bundled voice has a clip for each piece of text around the digits (nestor-satellite-python's\n# scripts/voice.py records them): record them again after changing this sentence.\ndevice-code-speech = Pairing code: { $digits }. I repeat: { $digits }.\nheadless-hint = This device has no screen and no voice, so the easiest way to pair it is an enrollment code. Add a satellite in Nestor, then set NESTOR_PAIRING_URI.\nenrollment-sent = Sending the enrollment code { $code } to Nestor.\nenrollment-refused = { $reason ->\n [expired] The enrollment code { $code } has expired.\n [used] The enrollment code { $code } was already used.\n [cancelled] The enrollment code { $code } was cancelled.\n *[unknown] Nestor does not know the enrollment code { $code }.\n}\nenrollment-fallback = Asking for a pairing code instead.\nenrollment-already-refused = The enrollment code { $code } was already refused. Make a new one in Nestor.\nenrollment-invalid = This is not an enrollment code. One has 8 letters and digits, like K7QF-2M9X.\nenrollment-not-pending = This device no longer waits for an enrollment code.\npaired = Paired with Nestor.\npairing-expired = The pairing code expired. Asking for a new one.\nidentify = Nestor asks this device to show itself.\ntoo-many-attempts = Too many wrong codes were tried. Trying again in { $seconds } s.\ntoo-many-pending = Too many devices are waiting to be paired. Trying again in { $seconds } s.\nprotocol-unsupported = This Nestor speaks another version of the satellite protocol. Update this device or Nestor.\ninvalid-request = Nestor refused this device's pairing request. Update this device or Nestor.\ntoken-refused = Nestor no longer knows this device. Pairing it again.\nlink-failed = The link to Nestor failed ({ $error }). Trying again.\ncamera-failed = The camera did not open, so this device cannot scan a QR code ({ $error }).\n";
12
+ readonly fr: "\ndevice-code-log = Code d'appairage : { $code }. Saisissez-le dans Nestor pour ajouter cet appareil.\ndevice-code-display = Saisissez ce code dans Nestor pour ajouter cet appareil.\n# La voix fournie a un extrait pour chaque texte autour des chiffres (scripts/voice.py de\n# nestor-satellite-python les enregistre) : les réenregistrer après avoir changé cette phrase.\ndevice-code-speech = Code d'appairage : { $digits }. Je répète : { $digits }.\nheadless-hint = Cet appareil n'a ni écran ni voix : le plus simple est de l'appairer avec un code d'enrôlement. Ajoutez un satellite dans Nestor, puis renseignez NESTOR_PAIRING_URI.\nenrollment-sent = Envoi du code d'enrôlement { $code } à Nestor.\nenrollment-refused = { $reason ->\n [expired] Le code d'enrôlement { $code } a expiré.\n [used] Le code d'enrôlement { $code } a déjà servi.\n [cancelled] Le code d'enrôlement { $code } a été annulé.\n *[unknown] Nestor ne connaît pas le code d'enrôlement { $code }.\n}\nenrollment-fallback = Je demande un code d'appairage à la place.\nenrollment-already-refused = Le code d'enrôlement { $code } a déjà été refusé. Créez-en un nouveau dans Nestor.\nenrollment-invalid = Ce n'est pas un code d'enrôlement. Il compte 8 lettres et chiffres, comme K7QF-2M9X.\nenrollment-not-pending = Cet appareil n'attend plus de code d'enrôlement.\npaired = Appairé avec Nestor.\npairing-expired = Le code d'appairage a expiré. J'en demande un nouveau.\nidentify = Nestor demande à cet appareil de se signaler.\ntoo-many-attempts = Trop de codes erronés ont été essayés. Nouvel essai dans { $seconds } s.\ntoo-many-pending = Trop d'appareils attendent d'être appairés. Nouvel essai dans { $seconds } s.\nprotocol-unsupported = Ce Nestor parle une autre version du protocole des satellites. Mettez à jour cet appareil ou Nestor.\ninvalid-request = Nestor a refusé la demande d'appairage de cet appareil. Mettez à jour cet appareil ou Nestor.\ntoken-refused = Nestor ne connaît plus cet appareil. Nouvel appairage.\nlink-failed = La liaison avec Nestor a échoué ({ $error }). Nouvel essai.\ncamera-failed = La caméra ne s'est pas ouverte : cet appareil ne peut pas lire de QR code ({ $error }).\n";
13
+ };
14
+ export type Locale = keyof typeof CATALOGUES;
15
+ export declare const LOCALES: readonly Locale[];
16
+ export type MessageArgs = Readonly<Record<string, string | number>>;
17
+ /**
18
+ * The first supported locale among `requested` (BCP 47 tags such as `fr-FR`, or POSIX ones such as
19
+ * `fr_FR.UTF-8`), else English.
20
+ */
21
+ export declare function negotiateLocale(requested: readonly (string | null | undefined)[]): Locale;
22
+ /** The platform's preferred languages, when it says (browsers, Node 21+). */
23
+ export declare function platformLanguages(): readonly string[];
24
+ export declare class Messages {
25
+ readonly locale: Locale;
26
+ private readonly bundle;
27
+ private readonly fallback;
28
+ /** `locale`: a tag or a list of preferred tags; by default the platform's languages. */
29
+ constructor(locale?: string | readonly string[]);
30
+ /** The sentence `id` with its arguments, in this locale (in English when this locale lacks it). */
31
+ format(id: MessageId, args?: MessageArgs): string;
32
+ }
33
+ //# sourceMappingURL=i18n.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAEtD,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,QAAA,MAAM,UAAU;;;CAAsB,CAAC;AACvC,MAAM,MAAM,MAAM,GAAG,MAAM,OAAO,UAAU,CAAC;AAC7C,eAAO,MAAM,OAAO,EAAE,SAAS,MAAM,EAAuD,CAAC;AAE7F,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC;AAEpE;;;GAGG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,GAAG,MAAM,CAUzF;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,IAAI,SAAS,MAAM,EAAE,CAGrD;AAED,qBAAa,QAAQ;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAe;IAExC,wFAAwF;gBAC5E,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE;IAO/C,mGAAmG;IACnG,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,GAAE,WAAgB,GAAG,MAAM;CAOtD"}