@oh-my-pi/pi-wire 18.2.3 → 18.2.5

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/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.2.5] - 2026-09-17
6
+
7
+ ### Added
8
+
9
+ - Added the `omp stream` wire contract (`@oh-my-pi/pi-wire/stream`) for pane screen updates, viewer snapshots and chat, channel metadata, and `live.omp.sh` stream routes.
10
+ - Added authentication support for stencil.so streams, including user identity in welcome messages, channel ownership metadata, and explicit unauthorized and forbidden close codes.
11
+
12
+ ### Changed
13
+
14
+ - Restricted channel names to the Stencil-compatible alphanumeric-and-underscore format and derived host channels from authentication tokens rather than URL path segments.
15
+
5
16
  ## [16.3.0] - 2026-07-02
6
17
 
7
18
  ### Breaking Changes
@@ -454,3 +454,4 @@ export type RelayControlToGuest = {
454
454
  t: "room-closed";
455
455
  };
456
456
  export type RelayControlMessage = RelayControlToHost | RelayControlToGuest;
457
+ export * from "./stream.js";
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Wire types for `omp stream`: Twitch-style live screen sharing at
3
+ * `live.omp.sh/<username>`.
4
+ *
5
+ * Independent from collab. A publisher (`omp stream`) sends plaintext JSON
6
+ * screen deltas for one or more panes (one pane per omp session attached in
7
+ * the same working directory); the stream server materializes each pane
8
+ * (viewport + bounded history) so late viewers receive a snapshot without
9
+ * touching the publisher, fans frames out to viewers, and hosts chat.
10
+ *
11
+ * Rows are terminal lines carrying only SGR and OSC 8 escapes; the publisher
12
+ * strips every other sequence, truncates to the pane width, and redacts
13
+ * secrets before a row leaves the session process.
14
+ */
15
+ /** Default stream server; its host route derives the channel from the bearer identity. */
16
+ export declare const DEFAULT_STREAM_URL = "https://live.omp.sh";
17
+ /** Protocol version carried in `hello`/`snapshot`; the server rejects mismatches. */
18
+ export declare const STREAM_PROTO = 1;
19
+ /** Channel names are Stencil usernames: lowercase letters, numbers, and underscores; 3–32 chars. */
20
+ export declare const STREAM_CHANNEL_NAME_RE: RegExp;
21
+ /** Longest accepted stream and pane title. */
22
+ export declare const STREAM_TITLE_MAX = 120;
23
+ /** Longest accepted chat display name. */
24
+ export declare const STREAM_CHAT_NAME_MAX = 24;
25
+ /** Longest accepted chat message. */
26
+ export declare const STREAM_CHAT_TEXT_MAX = 500;
27
+ /** History rows the server retains per pane; older rows fall off the top. */
28
+ export declare const STREAM_HISTORY_LIMIT = 2000;
29
+ /** One terminal row: ANSI text limited to SGR + OSC 8, width-truncated. */
30
+ export type StreamRow = string;
31
+ /**
32
+ * Per-pane screen deltas. `pane` ids are assigned by the publisher and are
33
+ * unique for the lifetime of one host connection.
34
+ *
35
+ * - `history` appends rows committed above the viewport (append-only).
36
+ * - `viewport` replaces the whole live viewport.
37
+ * - `patch` rewrites individual viewport rows; `rows` is the new viewport
38
+ * length (shrinks drop trailing rows, growth fills with empty rows before
39
+ * ops apply).
40
+ * - `reset` clears history and viewport (the session cleared its screen).
41
+ */
42
+ export type StreamPaneFrame = {
43
+ t: "pane-open";
44
+ pane: number;
45
+ title: string;
46
+ cols: number;
47
+ rows: number;
48
+ } | {
49
+ t: "pane-close";
50
+ pane: number;
51
+ } | {
52
+ t: "resize";
53
+ pane: number;
54
+ cols: number;
55
+ rows: number;
56
+ } | {
57
+ t: "history";
58
+ pane: number;
59
+ rows: StreamRow[];
60
+ } | {
61
+ t: "viewport";
62
+ pane: number;
63
+ rows: StreamRow[];
64
+ } | {
65
+ t: "patch";
66
+ pane: number;
67
+ ops: [index: number, row: StreamRow][];
68
+ rows: number;
69
+ } | {
70
+ t: "reset";
71
+ pane: number;
72
+ } | {
73
+ t: "paused";
74
+ pane: number;
75
+ paused: boolean;
76
+ };
77
+ /** Publisher → server. `hello` is the first frame on the socket. */
78
+ export type StreamHostFrame = {
79
+ t: "hello";
80
+ proto: number;
81
+ title: string;
82
+ } | {
83
+ t: "title";
84
+ title: string;
85
+ }
86
+ /** Message typed by the streamer; broadcast with `host: true`. */
87
+ | {
88
+ t: "chat";
89
+ text: string;
90
+ } | StreamPaneFrame;
91
+ export interface StreamChatMessage {
92
+ /** Monotonic per channel-session; viewers use it for de-duplication. */
93
+ id: number;
94
+ name: string;
95
+ text: string;
96
+ /** Unix milliseconds. */
97
+ ts: number;
98
+ /** Set when the streamer sent it. */
99
+ host?: boolean;
100
+ }
101
+ /** Server → publisher. */
102
+ export type StreamServerToHost =
103
+ /** `user` is the stencil.so username the bearer resolved to; shown as the host's chat name. */
104
+ {
105
+ t: "welcome";
106
+ proto: number;
107
+ channel: string;
108
+ url: string;
109
+ user?: string;
110
+ } | {
111
+ t: "viewers";
112
+ n: number;
113
+ } | {
114
+ t: "chat";
115
+ msg: StreamChatMessage;
116
+ } | {
117
+ t: "error";
118
+ message: string;
119
+ };
120
+ /** Directory entry served by `GET /api/channels` and `GET /api/channels/<name>`. */
121
+ export interface StreamChannelInfo {
122
+ name: string;
123
+ title: string;
124
+ live: boolean;
125
+ viewers: number;
126
+ panes: number;
127
+ /** stencil.so username of the channel owner (the first authenticated host). */
128
+ owner?: string;
129
+ /** Unix milliseconds of the current live session; absent when offline. */
130
+ startedAt?: number;
131
+ }
132
+ /** Materialized pane state delivered to a joining viewer. */
133
+ export interface StreamPaneSnapshot {
134
+ id: number;
135
+ title: string;
136
+ cols: number;
137
+ rows: number;
138
+ history: StreamRow[];
139
+ viewport: StreamRow[];
140
+ paused: boolean;
141
+ }
142
+ /**
143
+ * Server → viewer. `snapshot` is the first frame after connect and again
144
+ * whenever the publisher reconnects; `offline` means the publisher left and
145
+ * the viewer should keep the socket open for the next `snapshot`.
146
+ */
147
+ export type StreamServerToViewer = {
148
+ t: "snapshot";
149
+ proto: number;
150
+ channel: StreamChannelInfo;
151
+ panes: StreamPaneSnapshot[];
152
+ chat: StreamChatMessage[];
153
+ } | {
154
+ t: "offline";
155
+ } | {
156
+ t: "title";
157
+ title: string;
158
+ } | {
159
+ t: "viewers";
160
+ n: number;
161
+ } | {
162
+ t: "chat";
163
+ msg: StreamChatMessage;
164
+ } | StreamPaneFrame;
165
+ /** Viewer → server. The display name travels with each message until accounts exist. */
166
+ export type StreamViewerFrame = {
167
+ t: "chat";
168
+ name: string;
169
+ text: string;
170
+ };
171
+ /** WebSocket close codes used by the stream server. */
172
+ export declare const STREAM_CLOSE_HOST_CONFLICT = 4009;
173
+ export declare const STREAM_CLOSE_BAD_CHANNEL = 4004;
174
+ export declare const STREAM_CLOSE_PROTO_MISMATCH = 4010;
175
+ /** Host bearer token missing, expired, or not issued by the stencil.so issuer. */
176
+ export declare const STREAM_CLOSE_UNAUTHORIZED = 4401;
177
+ /** Channel is owned by a different stencil.so account. */
178
+ export declare const STREAM_CLOSE_FORBIDDEN = 4403;
179
+ /** Provider id under which `/login` stores the stencil.so credential; `STENCIL_API_KEY` overrides it. */
180
+ export declare const STREAM_AUTH_PROVIDER = "stencil";
181
+ export declare const STREAM_AUTH_ENV = "STENCIL_API_KEY";
182
+ /** HTTP/WS route layout of the stream server, relative to `DEFAULT_STREAM_URL`. */
183
+ export declare const STREAM_ROUTES: {
184
+ readonly channels: "/api/channels";
185
+ readonly channel: (name: string) => string;
186
+ /** The server derives the host channel from the authenticated username. */
187
+ readonly host: "/ws/host";
188
+ readonly watch: (name: string) => string;
189
+ readonly page: (name: string) => string;
190
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-wire",
4
- "version": "18.2.3",
4
+ "version": "18.2.5",
5
5
  "description": "Shared wire protocol types for Oh My Pi packages",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Stencil Labs, Inc.",
package/src/index.ts CHANGED
@@ -442,3 +442,5 @@ export type RelayControlToHost = { t: "peer-joined" | "peer-left"; peer: number
442
442
  /** Relay → guest control message. */
443
443
  export type RelayControlToGuest = { t: "room-closed" };
444
444
  export type RelayControlMessage = RelayControlToHost | RelayControlToGuest;
445
+
446
+ export * from "./stream";
package/src/stream.ts ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Wire types for `omp stream`: Twitch-style live screen sharing at
3
+ * `live.omp.sh/<username>`.
4
+ *
5
+ * Independent from collab. A publisher (`omp stream`) sends plaintext JSON
6
+ * screen deltas for one or more panes (one pane per omp session attached in
7
+ * the same working directory); the stream server materializes each pane
8
+ * (viewport + bounded history) so late viewers receive a snapshot without
9
+ * touching the publisher, fans frames out to viewers, and hosts chat.
10
+ *
11
+ * Rows are terminal lines carrying only SGR and OSC 8 escapes; the publisher
12
+ * strips every other sequence, truncates to the pane width, and redacts
13
+ * secrets before a row leaves the session process.
14
+ */
15
+
16
+ /** Default stream server; its host route derives the channel from the bearer identity. */
17
+ export const DEFAULT_STREAM_URL = "https://live.omp.sh";
18
+
19
+ /** Protocol version carried in `hello`/`snapshot`; the server rejects mismatches. */
20
+ export const STREAM_PROTO = 1;
21
+
22
+ /** Channel names are Stencil usernames: lowercase letters, numbers, and underscores; 3–32 chars. */
23
+ export const STREAM_CHANNEL_NAME_RE = /^[a-z0-9][a-z0-9_]{2,31}$/;
24
+
25
+ /** Longest accepted stream and pane title. */
26
+ export const STREAM_TITLE_MAX = 120;
27
+ /** Longest accepted chat display name. */
28
+ export const STREAM_CHAT_NAME_MAX = 24;
29
+ /** Longest accepted chat message. */
30
+ export const STREAM_CHAT_TEXT_MAX = 500;
31
+ /** History rows the server retains per pane; older rows fall off the top. */
32
+ export const STREAM_HISTORY_LIMIT = 2000;
33
+
34
+ /** One terminal row: ANSI text limited to SGR + OSC 8, width-truncated. */
35
+ export type StreamRow = string;
36
+
37
+ /**
38
+ * Per-pane screen deltas. `pane` ids are assigned by the publisher and are
39
+ * unique for the lifetime of one host connection.
40
+ *
41
+ * - `history` appends rows committed above the viewport (append-only).
42
+ * - `viewport` replaces the whole live viewport.
43
+ * - `patch` rewrites individual viewport rows; `rows` is the new viewport
44
+ * length (shrinks drop trailing rows, growth fills with empty rows before
45
+ * ops apply).
46
+ * - `reset` clears history and viewport (the session cleared its screen).
47
+ */
48
+ export type StreamPaneFrame =
49
+ | { t: "pane-open"; pane: number; title: string; cols: number; rows: number }
50
+ | { t: "pane-close"; pane: number }
51
+ | { t: "resize"; pane: number; cols: number; rows: number }
52
+ | { t: "history"; pane: number; rows: StreamRow[] }
53
+ | { t: "viewport"; pane: number; rows: StreamRow[] }
54
+ | { t: "patch"; pane: number; ops: [index: number, row: StreamRow][]; rows: number }
55
+ | { t: "reset"; pane: number }
56
+ | { t: "paused"; pane: number; paused: boolean };
57
+
58
+ /** Publisher → server. `hello` is the first frame on the socket. */
59
+ export type StreamHostFrame =
60
+ | { t: "hello"; proto: number; title: string }
61
+ | { t: "title"; title: string }
62
+ /** Message typed by the streamer; broadcast with `host: true`. */
63
+ | { t: "chat"; text: string }
64
+ | StreamPaneFrame;
65
+
66
+ export interface StreamChatMessage {
67
+ /** Monotonic per channel-session; viewers use it for de-duplication. */
68
+ id: number;
69
+ name: string;
70
+ text: string;
71
+ /** Unix milliseconds. */
72
+ ts: number;
73
+ /** Set when the streamer sent it. */
74
+ host?: boolean;
75
+ }
76
+
77
+ /** Server → publisher. */
78
+ export type StreamServerToHost =
79
+ /** `user` is the stencil.so username the bearer resolved to; shown as the host's chat name. */
80
+ | { t: "welcome"; proto: number; channel: string; url: string; user?: string }
81
+ | { t: "viewers"; n: number }
82
+ | { t: "chat"; msg: StreamChatMessage }
83
+ | { t: "error"; message: string };
84
+
85
+ /** Directory entry served by `GET /api/channels` and `GET /api/channels/<name>`. */
86
+ export interface StreamChannelInfo {
87
+ name: string;
88
+ title: string;
89
+ live: boolean;
90
+ viewers: number;
91
+ panes: number;
92
+ /** stencil.so username of the channel owner (the first authenticated host). */
93
+ owner?: string;
94
+ /** Unix milliseconds of the current live session; absent when offline. */
95
+ startedAt?: number;
96
+ }
97
+
98
+ /** Materialized pane state delivered to a joining viewer. */
99
+ export interface StreamPaneSnapshot {
100
+ id: number;
101
+ title: string;
102
+ cols: number;
103
+ rows: number;
104
+ history: StreamRow[];
105
+ viewport: StreamRow[];
106
+ paused: boolean;
107
+ }
108
+
109
+ /**
110
+ * Server → viewer. `snapshot` is the first frame after connect and again
111
+ * whenever the publisher reconnects; `offline` means the publisher left and
112
+ * the viewer should keep the socket open for the next `snapshot`.
113
+ */
114
+ export type StreamServerToViewer =
115
+ | {
116
+ t: "snapshot";
117
+ proto: number;
118
+ channel: StreamChannelInfo;
119
+ panes: StreamPaneSnapshot[];
120
+ chat: StreamChatMessage[];
121
+ }
122
+ | { t: "offline" }
123
+ | { t: "title"; title: string }
124
+ | { t: "viewers"; n: number }
125
+ | { t: "chat"; msg: StreamChatMessage }
126
+ | StreamPaneFrame;
127
+
128
+ /** Viewer → server. The display name travels with each message until accounts exist. */
129
+ export type StreamViewerFrame = { t: "chat"; name: string; text: string };
130
+
131
+ /** WebSocket close codes used by the stream server. */
132
+ export const STREAM_CLOSE_HOST_CONFLICT = 4009;
133
+ export const STREAM_CLOSE_BAD_CHANNEL = 4004;
134
+ export const STREAM_CLOSE_PROTO_MISMATCH = 4010;
135
+ /** Host bearer token missing, expired, or not issued by the stencil.so issuer. */
136
+ export const STREAM_CLOSE_UNAUTHORIZED = 4401;
137
+ /** Channel is owned by a different stencil.so account. */
138
+ export const STREAM_CLOSE_FORBIDDEN = 4403;
139
+
140
+ /** Provider id under which `/login` stores the stencil.so credential; `STENCIL_API_KEY` overrides it. */
141
+ export const STREAM_AUTH_PROVIDER = "stencil";
142
+ export const STREAM_AUTH_ENV = "STENCIL_API_KEY";
143
+
144
+ /** HTTP/WS route layout of the stream server, relative to `DEFAULT_STREAM_URL`. */
145
+ export const STREAM_ROUTES = {
146
+ channels: "/api/channels",
147
+ channel: (name: string) => `/api/channels/${name}`,
148
+ /** The server derives the host channel from the authenticated username. */
149
+ host: "/ws/host",
150
+ watch: (name: string) => `/ws/watch/${name}`,
151
+ page: (name: string) => `/${name}`,
152
+ } as const;