@owncast/plugin-sdk 0.6.0 → 0.10.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/index.d.ts CHANGED
@@ -1,34 +1,25 @@
1
1
  /**
2
2
  * Built-in chat message payload (`chat.message.received` and the chat filter).
3
3
  *
4
- * `user` carries the full sender identity use `user.id` for stable per-user
4
+ * `user` carries the full sender identity. Use `user.id` for stable per-user
5
5
  * state and `user.scopes` (e.g. `"MODERATOR"`) for reliable, non-spoofable
6
6
  * moderation gating rather than matching on the display name. `clientId`
7
- * identifies the originating connection; pass it to `owncast.chat.sendTo` (or
7
+ * identifies the originating connection. Pass it to `owncast.chat.sendTo` (or
8
8
  * `owncast.chat.replyTo(msg, …)`) to whisper a reply back to the sender.
9
9
  *
10
10
  * `user` is undefined for the rare message with no associated account.
11
11
  */
12
12
  export interface ChatMessage {
13
13
  id: string;
14
- user?: ChatUser;
14
+ user?: User;
15
15
  clientId?: number;
16
16
  body: string;
17
17
  timestamp: string;
18
18
  }
19
19
 
20
- /** A chat user, payload of join/part/rename events. */
21
- export interface ChatUser {
22
- id: string;
23
- displayName: string;
24
- isBot?: boolean;
25
- isAuthenticated?: boolean;
26
- scopes?: string[];
27
- }
28
-
29
20
  /** Payload of `chat.user.renamed`, the same user changing their name. */
30
21
  export interface ChatUserRename {
31
- user: ChatUser;
22
+ user: User;
32
23
  previousName: string;
33
24
  }
34
25
 
@@ -36,7 +27,7 @@ export interface ChatUserRename {
36
27
  export interface ChatMessageModeration {
37
28
  messageId: string;
38
29
  visible: boolean;
39
- moderator?: ChatUser;
30
+ moderator?: User;
40
31
  }
41
32
 
42
33
  /** Stream-lifecycle payloads. */
@@ -129,9 +120,11 @@ export const Events: {
129
120
  readonly SseConnect: "sse.connect";
130
121
  readonly SseDisconnect: "sse.disconnect";
131
122
  readonly Tick: "tick";
123
+ readonly FediverseActivity: "fediverse.activity";
132
124
  readonly FediverseFollow: "fediverse.follow";
133
125
  readonly FediverseLike: "fediverse.like";
134
126
  readonly FediverseRepost: "fediverse.repost";
127
+ readonly FediverseQuote: "fediverse.quote";
135
128
  readonly FediverseMention: "fediverse.mention";
136
129
  readonly FediverseReply: "fediverse.reply";
137
130
  };
@@ -146,10 +139,14 @@ export interface FediverseActor {
146
139
 
147
140
  export interface FediverseEngagement {
148
141
  actor: FediverseActor;
149
- /** For likes and reposts: the target object URL. Not set for follows. */
142
+ /** For likes, reposts, and quotes: the target object URL. Not set for follows. */
150
143
  target?: { url: string };
151
144
  }
152
145
 
146
+ export interface FediverseTargetedEngagement extends FediverseEngagement {
147
+ target: { url: string };
148
+ }
149
+
153
150
  /** Inbound fediverse post, a mention or reply that contains content the
154
151
  * plugin can act on. Carries both the rendered content (which has the
155
152
  * source instance's HTML) and a plain-text version (HTML stripped). */
@@ -183,13 +180,40 @@ export const Permissions: {
183
180
  readonly NotificationsSend: "notifications.send";
184
181
  readonly UsersRead: "users.read";
185
182
  readonly UsersModerate: "users.moderate";
183
+ readonly UsersRegister: "users.register";
184
+ readonly AuthGate: "auth.gate";
186
185
  readonly FediversePost: "fediverse.post";
186
+ readonly FediverseInbound: "fediverse.inbound";
187
187
  readonly HttpSSE: "http.sse";
188
188
  readonly VideoConfigRead: "videoconfig.read";
189
189
  readonly VideoConfigWrite: "videoconfig.write";
190
190
  readonly UIModify: "ui.modify";
191
191
  };
192
192
 
193
+ /** Request for `owncast.users.register`. */
194
+ export interface UserRegisterRequest {
195
+ /** Stable, provider-scoped external identity (e.g. `"github:583231"`). The
196
+ * host namespaces it by the calling plugin's slug. */
197
+ authId: string;
198
+ /** Optional display name to seed on the user. */
199
+ displayName?: string;
200
+ /** Optional scopes to grant the user (e.g. `["MODERATOR"]`). */
201
+ scopes?: string[];
202
+ }
203
+
204
+ /** Result of `owncast.users.register`: the resolved Owncast user ID. */
205
+ export interface UserRegisterResult {
206
+ userId: string;
207
+ }
208
+
209
+ /** Request for `owncast.auth.grantSession`. */
210
+ export interface GrantSessionRequest {
211
+ /** The Owncast user ID returned by `owncast.users.register`. */
212
+ userId: string;
213
+ /** Optional session lifetime in seconds. 0/omitted uses the host default. */
214
+ ttl?: number;
215
+ }
216
+
193
217
  export interface BrowserPushPayload {
194
218
  title: string;
195
219
  body?: string;
@@ -221,10 +245,14 @@ export interface FederationInfo {
221
245
  isPrivate?: boolean;
222
246
  }
223
247
 
224
- /** A user record from owncast.users.list() / .get(). */
248
+ /** A user. The sender identity carried by every chat payload
249
+ * (chat.message.received, join/part/rename, moderation) and the record
250
+ * returned by owncast.users.list() / .get(). `displayColor` is an index into
251
+ * the instance's configured user-color palette, not a literal color. */
225
252
  export interface User {
226
253
  id: string;
227
254
  displayName: string;
255
+ displayColor: number;
228
256
  previousNames?: string[];
229
257
  createdAt?: string;
230
258
  disabledAt?: string; // ISO-8601 if banned, omitted otherwise
@@ -262,6 +290,30 @@ export const filter: {
262
290
  drop(reason?: string): FilterResult;
263
291
  };
264
292
 
293
+ /** Request passed to `onAuthCheck`: the host-resolved identity of the viewer
294
+ * whose session is being re-validated (same `user` shape `onHttpRequest`
295
+ * receives, and the plugin never re-resolves it). */
296
+ export interface AuthCheckRequest {
297
+ user: User;
298
+ }
299
+
300
+ /** Verdict returned from `onAuthCheck`:
301
+ * - `ok` keep the session as-is
302
+ * - `refresh` keep it and re-issue the cookie (optionally with a new `ttl`
303
+ * in seconds) for sliding-expiry
304
+ * - `deny` end the session and bounce the viewer back to the login screen */
305
+ export type AuthCheckResult =
306
+ | { action: "ok" }
307
+ | { action: "refresh"; ttl?: number }
308
+ | { action: "deny"; reason?: string };
309
+
310
+ /** Verdict helpers for `onAuthCheck`. */
311
+ export const authCheck: {
312
+ ok(): AuthCheckResult;
313
+ refresh(opts?: { ttl?: number }): AuthCheckResult;
314
+ deny(reason?: string): AuthCheckResult;
315
+ };
316
+
265
317
  /** Incoming HTTP request, paths are relative to the plugin's namespace
266
318
  * (i.e. the leading /plugins/<name>/ has been stripped). */
267
319
  export interface IncomingHttpRequest {
@@ -275,7 +327,7 @@ export interface IncomingHttpRequest {
275
327
  authenticated: boolean;
276
328
  /** Identity of the user that made the request, when it came with a
277
329
  * user-token. Undefined for anonymous or admin-only requests. */
278
- user?: ChatUser;
330
+ user?: User;
279
331
  }
280
332
 
281
333
  export interface OutgoingHttpResponse {
@@ -290,7 +342,7 @@ export interface ContentRequest {
290
342
  slug: string;
291
343
  /** The viewing user's chat identity, when available. Undefined for
292
344
  * anonymous viewers or when the host cannot resolve an identity. */
293
- user?: ChatUser;
345
+ user?: User;
294
346
  }
295
347
 
296
348
  /** Payload for the sse.connect / sse.disconnect events. Fired when a browser
@@ -302,7 +354,7 @@ export interface ContentRequest {
302
354
  export interface SSEConnectionEvent {
303
355
  channel: string;
304
356
  connectionId: number;
305
- user?: ChatUser;
357
+ user?: User;
306
358
  }
307
359
 
308
360
  /** Payload for the once-a-second tick event (onTick). `now` is the host
@@ -312,21 +364,13 @@ export interface TickEvent {
312
364
  }
313
365
 
314
366
  export interface PluginDef {
315
- /** Declarative chat-command table. When set, the SDK wires the chat
316
- * subscription and prefix parsing for you no onChatMessage needed. Maps
317
- * canonical command name → definition (run/description/usage/aliases/
318
- * modOnly/cooldownMs/...); see {@link CommandDefinition}. For advanced
319
- * composition (e.g. dropping command messages via a filter) use the
320
- * lower-level {@link defineCommands} router instead. If you also provide
321
- * onChatMessage, the router runs first and then onChatMessage runs for every
322
- * message. */
367
+ /** Declarative chat commands with aliases, moderator gates, and per-user
368
+ * cooldowns. Command messages also remain available to onChatMessage. */
323
369
  commands?: Record<string, CommandDefinition>;
324
370
  /** Command prefix for the `commands` table. Default "!". */
325
371
  commandPrefix?: string;
326
372
  /** Match command names case-sensitively. Default false. */
327
373
  commandsCaseSensitive?: boolean;
328
- /** Called when a prefixed message matched no command in `commands`. */
329
- onUnknownCommand?(ctx: CommandContext): void;
330
374
 
331
375
  /** Notification handler for chat messages. Fire-and-forget. */
332
376
  onChatMessage?(msg: ChatMessage): void | Promise<void>;
@@ -336,9 +380,9 @@ export interface PluginDef {
336
380
  filterChatMessage?(msg: ChatMessage): FilterResult;
337
381
 
338
382
  /** User connected to chat. */
339
- onChatUserJoined?(user: ChatUser): void | Promise<void>;
383
+ onChatUserJoined?(user: User): void | Promise<void>;
340
384
  /** User disconnected from chat. */
341
- onChatUserParted?(user: ChatUser): void | Promise<void>;
385
+ onChatUserParted?(user: User): void | Promise<void>;
342
386
  /** User changed their display name. */
343
387
  onChatUserRenamed?(change: ChatUserRename): void | Promise<void>;
344
388
  /** A chat message was hidden or restored by a moderator. */
@@ -362,15 +406,20 @@ export interface PluginDef {
362
406
  * in unix milliseconds. Defining this opts the plugin into the tick. */
363
407
  onTick?(event: TickEvent): void | Promise<void>;
364
408
 
365
- /** Someone on the fediverse followed the streamer's account. */
409
+ /** A verified inbound ActivityPub activity as its raw JSON object. Requires `fediverse.inbound`. */
410
+ onFediverse?(activity: Record<string, unknown>): void | Promise<void>;
411
+
412
+ /** Someone on the fediverse followed the streamer's account. Requires `fediverse.inbound`. */
366
413
  onFediverseFollow?(event: FediverseEngagement): void | Promise<void>;
367
- /** Someone on the fediverse liked a streamer post / federated stream announcement. */
368
- onFediverseLike?(event: FediverseEngagement): void | Promise<void>;
369
- /** Someone on the fediverse boosted (reposted) a streamer post. */
370
- onFediverseRepost?(event: FediverseEngagement): void | Promise<void>;
371
- /** Someone @-mentioned the streamer in a public post. */
414
+ /** Someone on the fediverse liked a streamer post / federated stream announcement. Requires `fediverse.inbound`. */
415
+ onFediverseLike?(event: FediverseTargetedEngagement): void | Promise<void>;
416
+ /** Someone on the fediverse boosted (reposted) a streamer post. Requires `fediverse.inbound`. */
417
+ onFediverseRepost?(event: FediverseTargetedEngagement): void | Promise<void>;
418
+ /** Someone on the fediverse quoted a locally authored post. `target.url` identifies that quoted post. Requires `fediverse.inbound`. */
419
+ onFediverseQuote?(event: FediverseTargetedEngagement): void | Promise<void>;
420
+ /** Someone @-mentioned the streamer in a public post. Requires `fediverse.inbound`. */
372
421
  onFediverseMention?(post: FediverseInboundPost): void | Promise<void>;
373
- /** Someone replied to one of the streamer's federated posts. */
422
+ /** Someone replied to one of the streamer's federated posts. Requires `fediverse.inbound`. */
374
423
  onFediverseReply?(post: FediverseInboundPost): void | Promise<void>;
375
424
 
376
425
  /** HTTP request handler. Called for any path under /plugins/<name>/ that
@@ -378,6 +427,15 @@ export interface PluginDef {
378
427
  * on `req.authenticated` yourself. Requires `http.serve` permission. */
379
428
  onHttpRequest?(req: IncomingHttpRequest): OutgoingHttpResponse;
380
429
 
430
+ /** Re-validate a viewer's gate session on page load. Only meaningful for the
431
+ * active `auth.gate` plugin: the host calls it on the viewer's `/` request
432
+ * with the resolved `req.user`, and acts on the verdict: `ok` to continue,
433
+ * `refresh` to extend the session, `deny` to revoke it and force re-login.
434
+ * Optional. Without it a granted session lasts until its cookie
435
+ * expires (no mid-session revocation). This is the revocation hook: return
436
+ * `deny` for users your provider has banned/deleted. Requires `auth.gate`. */
437
+ onAuthCheck?(req: AuthCheckRequest): AuthCheckResult;
438
+
381
439
  /** Render HTML for a dynamic tab. Called by the host when the tab was
382
440
  * declared in the manifest without a static `content` file. Return the
383
441
  * full HTML string to inline as the tab body. `req.user` is the viewer's
@@ -390,6 +448,28 @@ export interface PluginDef {
390
448
  * `req.user` is the viewer's chat identity when available. */
391
449
  onPageContent?(req: ContentRequest): string;
392
450
 
451
+ /** Return CSS to inline into the viewer page at request time, the dynamic
452
+ * counterpart to `manifest.styles`, applied to the whole UI. Called once
453
+ * per `/api/config` for any plugin holding `ui.modify`. No manifest field
454
+ * is needed, just export this handler. Return nothing (a bare `return`, or
455
+ * `""`) to contribute nothing. The output is appended after any static
456
+ * `manifest.styles` files, so returning only the active override wins
457
+ * within your plugin's own styles. Plugin styles sit below the admin's
458
+ * appearance settings, so an admin's explicit colors override yours.
459
+ * Global (no per-viewer argument) so `/api/config` stays cacheable.
460
+ * Requires `ui.modify`. */
461
+ onPageStyles?(): string | null | void;
462
+
463
+ /** Return JavaScript to append to the viewer page at request time, the
464
+ * dynamic counterpart to `manifest.scripts`. Called once per `/api/config`
465
+ * for any plugin holding `ui.modify`. The host wraps each plugin's script
466
+ * (static and dynamic) in a try/catch so a runtime error can't break other
467
+ * plugins, but it runs in the shared viewer `window`: wrap your code in an
468
+ * IIFE to avoid global collisions, and escape any untrusted strings you
469
+ * embed. Return nothing (a bare `return`, or `""`) to contribute nothing.
470
+ * Requires `ui.modify`. */
471
+ onPageScripts?(): string | null | void;
472
+
393
473
  /** Handlers for plugin-emitted custom events. The key is the event type
394
474
  * string (e.g. "announcement.broadcast"). Notifications only, to filter
395
475
  * custom events, additional API will be needed. */
@@ -407,61 +487,46 @@ export interface CommandContext {
407
487
  /** The originating chat message. */
408
488
  msg: ChatMessage;
409
489
  /** The sender (same as `msg.user`). */
410
- user?: ChatUser;
490
+ user?: User;
411
491
  /** The canonical command name that matched (not the alias used). */
412
492
  command: string;
493
+ /** The command name or alias exactly as the sender typed it. */
494
+ invokedAs: string;
413
495
  /** Whitespace-split arguments after the command word. */
414
496
  args: string[];
415
497
  /** The raw argument string (everything after the command word, trimmed). */
416
498
  argString: string;
417
499
  /** Post a public reply as the plugin's chat bot. */
418
500
  reply(text: string): void;
419
- /** Whisper a reply to the sender; falls back to a public post if their
501
+ /** Whisper a reply to the sender, falling back to a public post if their
420
502
  * connection is unknown. */
421
503
  replyPrivately(text: string): void;
422
504
  }
423
505
 
424
- /** One command in a {@link defineCommands} table. */
506
+ /** One command in a declarative command table. */
425
507
  export interface CommandDefinition {
426
- /** Short, human-readable summary of what the command does. Surfaced in
427
- * command listings (e.g. a future `!help`); ignored by the router itself. */
508
+ /** Short, human-readable summary shown in command listings. */
428
509
  description?: string;
429
510
  /** Optional usage/example string, e.g. "!latency <0-4>". */
430
511
  usage?: string;
431
512
  /** Alternate names that invoke this command. */
432
513
  aliases?: string[];
433
- /** Only allow senders whose scopes include "MODERATOR". */
514
+ /** Dispatch only for senders whose scopes include "MODERATOR". */
434
515
  modOnly?: boolean;
435
- /** Minimum milliseconds between invocations per user (clocked off
436
- * `msg.timestamp`). */
516
+ /** Non-negative integer milliseconds between invocations per user. */
437
517
  cooldownMs?: number;
438
518
  /** Invoked when the command runs. */
439
519
  run(ctx: CommandContext): void;
440
- /** Invoked instead of `run` when a non-moderator calls a `modOnly` command. */
441
- onDenied?(ctx: CommandContext): void;
442
- /** Invoked instead of `run` when the per-user cooldown hasn't elapsed. */
443
- onCooldown?(ctx: CommandContext): void;
444
520
  }
445
521
 
446
- export interface CommandsConfig {
447
- /** Command prefix. Default `"!"`. */
448
- prefix?: string;
449
- /** Match command names case-sensitively. Default false. */
450
- caseSensitive?: boolean;
451
- commands: Record<string, CommandDefinition>;
452
- /** Fallback when a prefixed message matches no command. */
453
- onUnknown?(ctx: CommandContext): void;
454
- /** Default denied/cooldown handlers, used when a command omits its own. */
455
- onDenied?(ctx: CommandContext): void;
456
- onCooldown?(ctx: CommandContext): void;
457
- }
458
-
459
- /** Build a chat-command router (prefix parsing, aliases, per-user cooldowns,
460
- * moderator gating). Feed the returned function a `ChatMessage`; it returns
461
- * true when the message was a command (even if gated), false otherwise. */
462
- export function defineCommands(
463
- config: CommandsConfig,
464
- ): (msg: ChatMessage) => boolean;
522
+ /** Internal payload for a matched command declaration. */
523
+ export interface CommandEvent {
524
+ message: ChatMessage;
525
+ command: string;
526
+ invokedAs: string;
527
+ args: string[];
528
+ argString: string;
529
+ }
465
530
 
466
531
  /** Typed wrappers around the Owncast host. Each method throws if the
467
532
  * corresponding permission was not declared in plugin.manifest.json. */
@@ -485,7 +550,7 @@ export const owncast: {
485
550
  * `chat.send`. */
486
551
  replyTo(msg: ChatMessage | number | bigint, text: string): boolean;
487
552
  /** Recent chat history (most recent last). Requires `chat.history`.
488
- * Default limit is 50; pass a smaller number to get fewer. */
553
+ * Default limit is 50. Pass a smaller number to get fewer. */
489
554
  history(limit?: number): ChatMessage[];
490
555
  /** Hide a chat message by ID. Requires `chat.moderate`. */
491
556
  deleteMessage(messageId: string): void;
@@ -500,12 +565,30 @@ export const owncast: {
500
565
  list(): User[];
501
566
  /** Fetch one user by ID. Requires `users.read`. */
502
567
  get(id: string): User | null;
503
- /** Enable/disable a user; reason is optional. Requires `users.moderate`. */
568
+ /** Enable/disable a user, with an optional reason. Requires `users.moderate`. */
504
569
  setEnabled(id: string, enabled: boolean, reason?: string): void;
505
570
  /** Ban an IP address. Requires `users.moderate`. */
506
571
  banIP(ip: string): void;
572
+ /** Find-or-create an authenticated user for an external identity. `authId`
573
+ * is the stable provider-scoped id (e.g. `"github:583231"`). The host
574
+ * namespaces it by this plugin's slug so plugins can't collide on or spoof
575
+ * each other's users. Returns `{ userId }`. Throws on host error.
576
+ * Requires `users.register`. */
577
+ register(opts: UserRegisterRequest | string): UserRegisterResult;
578
+ };
579
+ /** Viewer-authentication gate. Only a plugin holding `auth.gate` (and enabled
580
+ * by an admin) can issue sessions, and only inside `onHttpRequest`, where the
581
+ * host attaches or clears the signed session cookie on the response. */
582
+ auth: {
583
+ /** Issue a gate session for an already-registered user (see
584
+ * `users.register`). `ttl` is optional seconds (0/omitted = host default).
585
+ * Throws on host error. Requires `auth.gate`. */
586
+ grantSession(opts: GrantSessionRequest | string): void;
587
+ /** Clear the current viewer's gate session (logout). The plugin still owns
588
+ * the response/redirect. Requires `auth.gate`. */
589
+ endSession(): void;
507
590
  };
508
- /** Upload bytes to Owncast's storage backend (local or S3); returns a
591
+ /** Upload bytes to Owncast's storage backend (local or S3). Returns a
509
592
  * public URL. Requires `storage.upload`. */
510
593
  storage: {
511
594
  upload(name: string, data: Uint8Array | string): UploadResult | null;
@@ -520,7 +603,7 @@ export const owncast: {
520
603
  readText(path: string): string | null;
521
604
  /** Write bytes or a string, creating parent directories as needed. */
522
605
  write(path: string, data: Uint8Array | string): FsResult;
523
- /** List entry names directly inside dir; missing dir lists as empty. */
606
+ /** List entry names directly inside dir. A missing dir lists as empty. */
524
607
  list(dir: string): string[];
525
608
  /** Remove a single file or empty directory. */
526
609
  delete(path: string): FsResult;
@@ -528,7 +611,7 @@ export const owncast: {
528
611
  exists(path: string): boolean;
529
612
  };
530
613
  /** Post to the fediverse on the streamer's behalf. Requires `fediverse.post`,
531
- * which is high-trust (posts go out under the streamer's own handle);
614
+ * which is high-trust (posts go out under the streamer's own handle), so
532
615
  * admins should grant it sparingly. */
533
616
  fediverse: {
534
617
  /** Publish a public, text-only post. Returns `{ url }` (currently empty
@@ -557,7 +640,7 @@ export const owncast: {
557
640
  setJSON(key: string, value: unknown): void;
558
641
  };
559
642
  /** Read this plugin's admin-configurable settings, declared under
560
- * `config` in the manifest. Ambient no permission required. */
643
+ * `config` in the manifest. Ambient, so no permission is required. */
561
644
  config: {
562
645
  /** The effective value of a manifest-declared config key (admin override,
563
646
  * else the declared default), parsed to its declared type. Returns
@@ -565,10 +648,10 @@ export const owncast: {
565
648
  * value. */
566
649
  get<T = unknown>(key: string, fallback?: T): T;
567
650
  };
568
- /** Read files the plugin bundled in its own `assets/` directory templates,
651
+ /** Read files the plugin bundled in its own `assets/` directory: templates,
569
652
  * data files, and other bundled resources loaded at request time. Path is
570
- * relative to `assets/` and must not contain `..`. Ambient no permission
571
- * required. */
653
+ * relative to `assets/` and must not contain `..`. Ambient, so no permission
654
+ * is required. */
572
655
  assets: {
573
656
  /** Raw bytes of the file, or `null` if not found. */
574
657
  read(path: string): Uint8Array | null;
@@ -585,30 +668,30 @@ export const owncast: {
585
668
  actions: {
586
669
  /** Append one or more buttons to the plugin's runtime list. Each
587
670
  * entry is validated with the same rules as `manifest.actions`
588
- * (title required; exactly one of `url` or `html`; relative URLs
589
- * rewritten into this plugin's namespace; cross-plugin URLs
671
+ * (title required, exactly one of `url` or `html`, relative URLs
672
+ * rewritten into this plugin's namespace, cross-plugin URLs
590
673
  * rejected). The next viewer `/api/config` request returns
591
674
  * `manifest.actions` ++ the runtime list. */
592
675
  add(actions: ActionButton | ActionButton[]): void;
593
- /** Drop the runtime additions; only `manifest.actions` remain on
676
+ /** Drop the runtime additions, so only `manifest.actions` remain on
594
677
  * the next viewer `/api/config` request. */
595
678
  clear(): void;
596
679
  };
597
680
  sse: {
598
681
  /** Push one Server-Sent-Event to every browser connected to this
599
682
  * plugin's `/plugins/<name>/_sse/<channel>` stream. `event` is the SSE
600
- * event name (`""` → the default "message" event); `data` is sent as-is
601
- * if a string, otherwise JSON-stringified. Fire-and-forget; frames to a
683
+ * event name (`""` → the default "message" event). `data` is sent as-is
684
+ * if a string, otherwise JSON-stringified. Fire-and-forget, and frames to a
602
685
  * slow client are dropped rather than blocking the plugin. Requires the
603
686
  * `http.sse` permission. */
604
687
  send(channel: string, event: string, data: unknown): void;
605
688
  };
606
- /** Host-driven timers. The sandbox has no setTimeout; these ask the host to
689
+ /** Host-driven timers. The sandbox has no setTimeout, so these ask the host to
607
690
  * call your callback back later (in this instance). No permission required.
608
691
  * Timers do not survive a plugin reload or host restart. */
609
692
  timer: {
610
693
  /** Run `fn` once after ~`ms` milliseconds. Returns an id for `clear()`.
611
- * Very small delays are clamped up by the host; throws past the
694
+ * Very small delays are clamped up by the host. Throws past the
612
695
  * per-plugin pending-timer cap. */
613
696
  setTimeout(fn: () => void, ms: number): number;
614
697
  /** Run `fn` every ~`ms` milliseconds until `clear()`. The next run is
@@ -635,7 +718,7 @@ export const owncast: {
635
718
  tags(): string[];
636
719
  };
637
720
  /** Read/change video/transcoding configuration. read() requires
638
- * `videoconfig.read`; write() requires `videoconfig.write`. */
721
+ * `videoconfig.read`, and write() requires `videoconfig.write`. */
639
722
  videoConfig: {
640
723
  read(): VideoConfig;
641
724
  write(config: VideoConfigUpdate): void;
@@ -656,7 +739,7 @@ export interface HttpResponse {
656
739
 
657
740
  /** An entry in `manifest.actions`, declares an action button the Owncast
658
741
  * UI surfaces while this plugin is enabled. Mirrors Owncast's existing
659
- * ExternalAction shape; the host merges enabled-plugin buttons with the
742
+ * ExternalAction shape. The host merges enabled-plugin buttons with the
660
743
  * admin-configured list.
661
744
  *
662
745
  * Exactly one of `url` or `html` is required.
@@ -687,10 +770,10 @@ export interface ActionButton {
687
770
 
688
771
  /** `manifest.network`, narrows outbound HTTP scope for plugins that
689
772
  * declare the `network.fetch` permission. Required when that permission
690
- * is granted; the host rejects loads otherwise. */
773
+ * is granted. The host rejects loads otherwise. */
691
774
  export interface NetworkConfig {
692
775
  /** Hostname globs the plugin can reach via `owncast.http.fetch`.
693
- * Bare names match exactly (`"api.discord.com"`); `*` is a wildcard
776
+ * Bare names match exactly (`"api.discord.com"`), and `*` is a wildcard
694
777
  * segment (`"*.weather.com"`). The bare wildcard `"*"` matches any
695
778
  * host but must be written explicitly. */
696
779
  allowedHosts: string[];