@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.
- package/bin/owncast-plugin.js +7 -0
- package/index.d.ts +46 -0
- package/index.js +81 -0
- package/package.json +1 -1
package/bin/owncast-plugin.js
CHANGED
|
@@ -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,
|