@volter/teams 0.2.16 → 0.2.18

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.
@@ -82,3 +82,15 @@ export interface BrowserPane extends ChannelEvents {
82
82
  }
83
83
 
84
84
  export function attachBrowserPane(channel: BrowserChannel, options: { pane: string; mode?: 'observe' | 'control'; cols?: number; rows?: number }): Promise<BrowserPane>;
85
+
86
+ /** This machine through its app door (`supercode app-door on`), with the pairing from `app-door link`. */
87
+ export function openBrowserLocalMachine(target: { door: string; token: string }, options?: ChannelOptions): Promise<BrowserChannel>;
88
+ /** The app door's pairing from a page's address (`#supercode=…`), or null. */
89
+ export function readAppDoorPairing(hash?: string): { door: string; token: string } | null;
90
+ /** A harness SDK transport over a browser channel's `harness` stream. */
91
+ export function browserHarnessTransport(channel: BrowserChannel | (() => BrowserChannel | Promise<BrowserChannel>)): {
92
+ readonly running: boolean;
93
+ start(handlers: { onLine(line: string): void; onExit(code: number | null, reason: string | null): void; onStderr?(text: string): void; onWriteError?(error: Error): void }): Promise<void>;
94
+ write(line: string): void;
95
+ close(): Promise<void>;
96
+ };
@@ -167,6 +167,82 @@ export function openBrowserSession({ origin, teamId, sessionId, token }, { WebSo
167
167
  return connectBrowserChannel(socket, channel, `could not reach session ${sessionId} through ${origin}`, 'the server closed the session channel');
168
168
  }
169
169
 
170
+ /**
171
+ * This machine, through its app door (the daemon's loopback WebSocket for web apps, off until the
172
+ * person turns it on: `supercode app-door on`). `door` and `token` come from the link the
173
+ * person opened the app with (`app-door link`); see readAppDoorPairing.
174
+ */
175
+ export function openBrowserLocalMachine({ door, token }, { WebSocketImpl = globalThis.WebSocket } = {}) {
176
+ const socket = new WebSocketImpl(door, [CHANNEL_PROTOCOL, `bearer.${token}`]);
177
+ const channel = new BrowserChannel(socket);
178
+ return connectBrowserChannel(socket, channel, `could not reach this machine's app door at ${door}: is it on? (supercode app-door status)`, 'the machine closed the app door\'s channel');
179
+ }
180
+
181
+ /**
182
+ * The app door's pairing from a page's address (`#supercode=<base64url JSON { door, token }>`, as
183
+ * `app-door link` writes it), or null. The caller keeps it and removes it from the address.
184
+ */
185
+ export function readAppDoorPairing(hash = globalThis.location?.hash ?? '') {
186
+ const match = /(?:^#|&)supercode=([A-Za-z0-9_-]+)/.exec(hash);
187
+ if (!match) return null;
188
+ try {
189
+ const padded = match[1].replace(/-/g, '+').replace(/_/g, '/');
190
+ const value = JSON.parse(decoder.decode(Uint8Array.from(atob(padded + '='.repeat((4 - (padded.length % 4)) % 4)), (c) => c.charCodeAt(0))));
191
+ // Only this machine's own door: a link naming any other address would hand the person's work to it.
192
+ const door = typeof value?.door === 'string' ? new URL(value.door) : null;
193
+ const local = door && door.protocol === 'ws:' && ['127.0.0.1', 'localhost', '[::1]'].includes(door.hostname);
194
+ return local && typeof value.token === 'string' && value.token ? { door: door.href, token: value.token } : null;
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * A harness SDK transport (`ClientOptions.transport`) over a browser machine channel's `harness`
202
+ * stream: harness.v1 JSON lines to the machine's harness service. `channel` is a channel, or a function
203
+ * that opens one, called again after the stream closes so a restarted daemon costs one reconnect.
204
+ */
205
+ export function browserHarnessTransport(channel) {
206
+ const owned = typeof channel === 'function';
207
+ const source = owned ? channel : () => channel;
208
+ let stream = null;
209
+ let opened = null;
210
+ return {
211
+ get running() { return stream !== null && !stream.closed; },
212
+ async start(handlers) {
213
+ // A channel this transport opened is its own to close when it opens the next.
214
+ if (owned && opened && !opened.closed) opened.close();
215
+ opened = await source();
216
+ const current = await opened.open('harness');
217
+ stream = current;
218
+ const text = new TextDecoder();
219
+ let pending = '';
220
+ current.on('data', (chunk) => {
221
+ if (stream !== current) return; // a replaced stream's late lines are not the client's
222
+ pending += typeof chunk === 'string' ? chunk : text.decode(chunk, { stream: true });
223
+ let end;
224
+ while ((end = pending.indexOf('\n')) >= 0) {
225
+ const line = pending.slice(0, end).replace(/\r$/, '');
226
+ pending = pending.slice(end + 1);
227
+ if (line) handlers.onLine(line);
228
+ }
229
+ });
230
+ // Only the current stream's end is the transport's: an earlier one closing late is not.
231
+ current.on('close', (reason) => { if (stream !== current) return; stream = null; handlers.onExit(null, reason ?? null); });
232
+ },
233
+ write(line) {
234
+ if (!stream || stream.closed) throw new Error('the harness stream is not open');
235
+ stream.write(line.endsWith('\n') ? line : line + '\n');
236
+ },
237
+ async close() {
238
+ stream?.close();
239
+ stream = null;
240
+ if (owned) opened?.close();
241
+ opened = null;
242
+ },
243
+ };
244
+ }
245
+
170
246
  /** A turn was accepted by its stream, but its delivery receipt was not obtained. */
171
247
  export class TurnDeliveryUnconfirmedError extends Error {
172
248
  constructor(message) { super(message); this.name = 'TurnDeliveryUnconfirmedError'; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/teams",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
4
4
  "type": "module",
5
5
  "description": "Volter Teams, the neutral team plane: machine identity and daemon core, the local door and server link, admission from machine grants, product doors, and the Teams server's neutral routes (company RFC 0008).",
6
6
  "license": "MIT",
@@ -11,9 +11,10 @@
11
11
  * `brand(<role>)`; scripts/build-styles.mjs expands each to
12
12
  * `var(--volter-<role>, <brand value>)`, colours as light-dark(). A host
13
13
  * that sets `--volter-*` (the brand's tokens.css) or `--scui-*` governs.
14
- * The scheme follows `--scui-color-scheme`
15
- * (default `light dark`, i.e. the OS), or the host's own `color-scheme`
16
- * when it sets `--scui-color-scheme: inherit`.
14
+ * The scheme follows `--scui-color-scheme` (default `light dark`, i.e.
15
+ * the OS). A host with its own theme sets it to `light` or `dark` with
16
+ * that theme: `inherit` there inherits the custom property itself, not
17
+ * the host's `color-scheme`, and leaves the OS's scheme in force.
17
18
  * 2. Component tokens (`--scui-user-bg`, `--scui-envelope-radius`, ...) are
18
19
  * never declared: each is read with its default as a fallback at the use
19
20
  * site, so the default tracks the primitives wherever they are overridden.