@owncast/plugin-sdk 0.4.0 → 0.4.1

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.
@@ -398,6 +398,13 @@ function generateInterface(manifest) {
398
398
  if (perms.has("storage.upload")) {
399
399
  imports.push("owncast_storage_upload(namePtr: PTR, dataPtr: PTR): PTR");
400
400
  }
401
+ if (perms.has("storage.fs")) {
402
+ imports.push("owncast_fs_read(pathPtr: PTR): PTR");
403
+ imports.push("owncast_fs_write(pathPtr: PTR, dataPtr: PTR): PTR");
404
+ imports.push("owncast_fs_list(dirPtr: PTR): PTR");
405
+ imports.push("owncast_fs_delete(pathPtr: PTR): PTR");
406
+ imports.push("owncast_fs_exists(pathPtr: PTR): I32");
407
+ }
401
408
  if (perms.has("fediverse.post")) {
402
409
  imports.push("owncast_fediverse_post(textPtr: PTR): PTR");
403
410
  }
package/index.d.ts CHANGED
@@ -115,6 +115,8 @@ export const Events: {
115
115
  readonly StreamStarted: "stream.started";
116
116
  readonly StreamStopped: "stream.stopped";
117
117
  readonly StreamTitleChanged: "stream.title.changed";
118
+ readonly SseConnect: "sse.connect";
119
+ readonly SseDisconnect: "sse.disconnect";
118
120
  readonly FediverseFollow: "fediverse.follow";
119
121
  readonly FediverseLike: "fediverse.like";
120
122
  readonly FediverseRepost: "fediverse.repost";
@@ -161,6 +163,7 @@ export const Permissions: {
161
163
  readonly ChatFilter: "chat.filter";
162
164
  readonly StorageKV: "storage.kv";
163
165
  readonly StorageUpload: "storage.upload";
166
+ readonly StorageFS: "storage.fs";
164
167
  readonly EventsEmit: "events.emit";
165
168
  readonly NetworkFetch: "network.fetch";
166
169
  readonly HttpServe: "http.serve";
@@ -228,6 +231,13 @@ export interface UploadResult {
228
231
  url: string;
229
232
  }
230
233
 
234
+ /** Result of a mutating owncast.fs call (write/delete). `ok` is false and
235
+ * `error` is set when the host rejected the operation. */
236
+ export interface FsResult {
237
+ ok: boolean;
238
+ error?: string;
239
+ }
240
+
231
241
  export const filter: {
232
242
  pass(): FilterResult;
233
243
  modify(payload: any): FilterResult;
@@ -256,6 +266,18 @@ export interface OutgoingHttpResponse {
256
266
  body?: string;
257
267
  }
258
268
 
269
+ /** Payload for the sse.connect / sse.disconnect events. Fired when a browser
270
+ * opens or closes one of the plugin's `/plugins/<name>/_sse/<channel>`
271
+ * streams, so the plugin can track who is connected. `connectionId` is unique
272
+ * per connection for the life of the host process, so a disconnect can be
273
+ * paired with its connect and the same user counted across several tabs.
274
+ * `user` is present only when the connection carried a chat identity. */
275
+ export interface SSEConnectionEvent {
276
+ channel: string;
277
+ connectionId: number;
278
+ user?: ChatUser;
279
+ }
280
+
259
281
  export interface PluginDef {
260
282
  /** Notification handler for chat messages. Fire-and-forget. */
261
283
  onChatMessage?(msg: ChatMessage): void | Promise<void>;
@@ -280,6 +302,13 @@ export interface PluginDef {
280
302
  /** Stream title was updated. */
281
303
  onStreamTitleChanged?(change: StreamTitleChange): void | Promise<void>;
282
304
 
305
+ /** A browser opened one of this plugin's SSE streams. Use it to track who
306
+ * is connected. Requires the `http.sse` permission. */
307
+ onSseConnect?(event: SSEConnectionEvent): void | Promise<void>;
308
+ /** A browser closed one of this plugin's SSE streams (same connectionId as
309
+ * the matching onSseConnect). Requires the `http.sse` permission. */
310
+ onSseDisconnect?(event: SSEConnectionEvent): void | Promise<void>;
311
+
283
312
  /** Someone on the fediverse followed the streamer's account. */
284
313
  onFediverseFollow?(event: FediverseEngagement): void | Promise<void>;
285
314
  /** Someone on the fediverse liked a streamer post / federated stream announcement. */
@@ -349,6 +378,23 @@ export const owncast: {
349
378
  storage: {
350
379
  upload(name: string, data: Uint8Array | string): UploadResult | null;
351
380
  };
381
+ /** Private, sandboxed filesystem under data/plugin-data/<slug>/. The bytes
382
+ * stay server-side (never served over HTTP) and the host confines every
383
+ * path to this plugin's own directory. All methods require `storage.fs`. */
384
+ fs: {
385
+ /** Read a file's raw bytes, or null if it doesn't exist. */
386
+ read(path: string): Uint8Array | null;
387
+ /** Read a file as UTF-8 text, or null if it doesn't exist. */
388
+ readText(path: string): string | null;
389
+ /** Write bytes or a string, creating parent directories as needed. */
390
+ write(path: string, data: Uint8Array | string): FsResult;
391
+ /** List entry names directly inside dir; missing dir lists as empty. */
392
+ list(dir: string): string[];
393
+ /** Remove a single file or empty directory. */
394
+ delete(path: string): FsResult;
395
+ /** Report whether a path exists inside the sandbox. */
396
+ exists(path: string): boolean;
397
+ };
352
398
  /** Post to the fediverse on the streamer's behalf. Requires `fediverse.post`,
353
399
  * which is high-trust (posts go out under the streamer's own handle);
354
400
  * admins should grant it sparingly. */
package/index.js CHANGED
@@ -24,6 +24,9 @@ const Events = Object.freeze({
24
24
  StreamStarted: "stream.started",
25
25
  StreamStopped: "stream.stopped",
26
26
  StreamTitleChanged: "stream.title.changed",
27
+ // SSE connection lifecycle (who connected to / left a plugin's stream)
28
+ SseConnect: "sse.connect",
29
+ SseDisconnect: "sse.disconnect",
27
30
  // Fediverse, engagement (metadata only) + inbound posts (with content)
28
31
  FediverseFollow: "fediverse.follow",
29
32
  FediverseLike: "fediverse.like",
@@ -39,6 +42,7 @@ const Permissions = Object.freeze({
39
42
  ChatFilter: "chat.filter",
40
43
  StorageKV: "storage.kv",
41
44
  StorageUpload: "storage.upload",
45
+ StorageFS: "storage.fs",
42
46
  EventsEmit: "events.emit",
43
47
  NetworkFetch: "network.fetch",
44
48
  HttpServe: "http.serve",
@@ -102,6 +106,9 @@ const HANDLERS = Object.freeze({
102
106
  event: Events.StreamTitleChanged,
103
107
  kind: HandlerKind.Notify,
104
108
  },
109
+ // SSE connection lifecycle
110
+ onSseConnect: { event: Events.SseConnect, kind: HandlerKind.Notify },
111
+ onSseDisconnect: { event: Events.SseDisconnect, kind: HandlerKind.Notify },
105
112
  // Fediverse engagement (actor + target metadata)
106
113
  onFediverseFollow: {
107
114
  event: Events.FediverseFollow,
@@ -341,6 +348,80 @@ const owncast = {
341
348
  return JSON.parse(Memory.find(offset).readString());
342
349
  },
343
350
  },
351
+ // Private, sandboxed filesystem under data/plugin-data/<slug>/. Unlike
352
+ // storage.upload (which publishes browser-accessible files), these bytes
353
+ // stay server-side. The host confines every path to this plugin's own
354
+ // directory. All methods require the 'storage.fs' permission.
355
+ fs: {
356
+ // Read a file's raw bytes. Returns a Uint8Array, or null if the file
357
+ // doesn't exist (or can't be read).
358
+ read(path) {
359
+ const fns = Host.getFunctions();
360
+ if (!fns.owncast_fs_read)
361
+ throw new Error(`permission '${Permissions.StorageFS}' not granted`);
362
+ const offset = fns.owncast_fs_read(Memory.fromString(path).offset);
363
+ if (offset == 0) return null;
364
+ return new Uint8Array(Memory.find(offset).readBytes());
365
+ },
366
+ // Read a file as UTF-8 text. Returns a string, or null if the file
367
+ // doesn't exist. (The Extism boundary decodes the bytes as UTF-8.)
368
+ readText(path) {
369
+ const fns = Host.getFunctions();
370
+ if (!fns.owncast_fs_read)
371
+ throw new Error(`permission '${Permissions.StorageFS}' not granted`);
372
+ const offset = fns.owncast_fs_read(Memory.fromString(path).offset);
373
+ if (offset == 0) return null;
374
+ return Memory.find(offset).readString();
375
+ },
376
+ // Write bytes (Uint8Array) or a string to a file, creating parent
377
+ // directories as needed. Returns { ok, error? }.
378
+ write(path, data) {
379
+ const fns = Host.getFunctions();
380
+ if (!fns.owncast_fs_write)
381
+ throw new Error(`permission '${Permissions.StorageFS}' not granted`);
382
+ const dataMem =
383
+ data instanceof Uint8Array
384
+ ? Memory.fromBuffer(
385
+ data.buffer.slice(
386
+ data.byteOffset,
387
+ data.byteOffset + data.byteLength,
388
+ ),
389
+ )
390
+ : Memory.fromString(String(data));
391
+ const offset = fns.owncast_fs_write(
392
+ Memory.fromString(path).offset,
393
+ dataMem.offset,
394
+ );
395
+ if (offset == 0) return { ok: false, error: "write failed" };
396
+ return JSON.parse(Memory.find(offset).readString());
397
+ },
398
+ // List the entry names (files and subdirectories) directly inside dir.
399
+ // A missing directory lists as empty. Returns string[].
400
+ list(dir) {
401
+ const fns = Host.getFunctions();
402
+ if (!fns.owncast_fs_list)
403
+ throw new Error(`permission '${Permissions.StorageFS}' not granted`);
404
+ const offset = fns.owncast_fs_list(Memory.fromString(dir || "").offset);
405
+ if (offset == 0) return [];
406
+ return JSON.parse(Memory.find(offset).readString());
407
+ },
408
+ // Remove a single file or empty directory. Returns { ok, error? }.
409
+ delete(path) {
410
+ const fns = Host.getFunctions();
411
+ if (!fns.owncast_fs_delete)
412
+ throw new Error(`permission '${Permissions.StorageFS}' not granted`);
413
+ const offset = fns.owncast_fs_delete(Memory.fromString(path).offset);
414
+ if (offset == 0) return { ok: false, error: "delete failed" };
415
+ return JSON.parse(Memory.find(offset).readString());
416
+ },
417
+ // Report whether a path exists inside the sandbox. Returns boolean.
418
+ exists(path) {
419
+ const fns = Host.getFunctions();
420
+ if (!fns.owncast_fs_exists)
421
+ throw new Error(`permission '${Permissions.StorageFS}' not granted`);
422
+ return fns.owncast_fs_exists(Memory.fromString(path).offset) === 1;
423
+ },
424
+ },
344
425
  fediverse: {
345
426
  /** Publish a public text-only post to the fediverse on the streamer's
346
427
  * behalf. Returns { url } on success, null on failure (rate-limited,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owncast/plugin-sdk",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "SDK for authoring Owncast plugins in JavaScript",
5
5
  "license": "MIT",
6
6
  "author": "Owncast",