@owncast/plugin-sdk 0.5.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,6 +364,14 @@ export interface TickEvent {
312
364
  }
313
365
 
314
366
  export interface PluginDef {
367
+ /** Declarative chat commands with aliases, moderator gates, and per-user
368
+ * cooldowns. Command messages also remain available to onChatMessage. */
369
+ commands?: Record<string, CommandDefinition>;
370
+ /** Command prefix for the `commands` table. Default "!". */
371
+ commandPrefix?: string;
372
+ /** Match command names case-sensitively. Default false. */
373
+ commandsCaseSensitive?: boolean;
374
+
315
375
  /** Notification handler for chat messages. Fire-and-forget. */
316
376
  onChatMessage?(msg: ChatMessage): void | Promise<void>;
317
377
 
@@ -320,9 +380,9 @@ export interface PluginDef {
320
380
  filterChatMessage?(msg: ChatMessage): FilterResult;
321
381
 
322
382
  /** User connected to chat. */
323
- onChatUserJoined?(user: ChatUser): void | Promise<void>;
383
+ onChatUserJoined?(user: User): void | Promise<void>;
324
384
  /** User disconnected from chat. */
325
- onChatUserParted?(user: ChatUser): void | Promise<void>;
385
+ onChatUserParted?(user: User): void | Promise<void>;
326
386
  /** User changed their display name. */
327
387
  onChatUserRenamed?(change: ChatUserRename): void | Promise<void>;
328
388
  /** A chat message was hidden or restored by a moderator. */
@@ -346,15 +406,20 @@ export interface PluginDef {
346
406
  * in unix milliseconds. Defining this opts the plugin into the tick. */
347
407
  onTick?(event: TickEvent): void | Promise<void>;
348
408
 
349
- /** 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`. */
350
413
  onFediverseFollow?(event: FediverseEngagement): void | Promise<void>;
351
- /** Someone on the fediverse liked a streamer post / federated stream announcement. */
352
- onFediverseLike?(event: FediverseEngagement): void | Promise<void>;
353
- /** Someone on the fediverse boosted (reposted) a streamer post. */
354
- onFediverseRepost?(event: FediverseEngagement): void | Promise<void>;
355
- /** 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`. */
356
421
  onFediverseMention?(post: FediverseInboundPost): void | Promise<void>;
357
- /** Someone replied to one of the streamer's federated posts. */
422
+ /** Someone replied to one of the streamer's federated posts. Requires `fediverse.inbound`. */
358
423
  onFediverseReply?(post: FediverseInboundPost): void | Promise<void>;
359
424
 
360
425
  /** HTTP request handler. Called for any path under /plugins/<name>/ that
@@ -362,6 +427,15 @@ export interface PluginDef {
362
427
  * on `req.authenticated` yourself. Requires `http.serve` permission. */
363
428
  onHttpRequest?(req: IncomingHttpRequest): OutgoingHttpResponse;
364
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
+
365
439
  /** Render HTML for a dynamic tab. Called by the host when the tab was
366
440
  * declared in the manifest without a static `content` file. Return the
367
441
  * full HTML string to inline as the tab body. `req.user` is the viewer's
@@ -374,6 +448,28 @@ export interface PluginDef {
374
448
  * `req.user` is the viewer's chat identity when available. */
375
449
  onPageContent?(req: ContentRequest): string;
376
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
+
377
473
  /** Handlers for plugin-emitted custom events. The key is the event type
378
474
  * string (e.g. "announcement.broadcast"). Notifications only, to filter
379
475
  * custom events, additional API will be needed. */
@@ -391,56 +487,46 @@ export interface CommandContext {
391
487
  /** The originating chat message. */
392
488
  msg: ChatMessage;
393
489
  /** The sender (same as `msg.user`). */
394
- user?: ChatUser;
490
+ user?: User;
395
491
  /** The canonical command name that matched (not the alias used). */
396
492
  command: string;
493
+ /** The command name or alias exactly as the sender typed it. */
494
+ invokedAs: string;
397
495
  /** Whitespace-split arguments after the command word. */
398
496
  args: string[];
399
497
  /** The raw argument string (everything after the command word, trimmed). */
400
498
  argString: string;
401
499
  /** Post a public reply as the plugin's chat bot. */
402
500
  reply(text: string): void;
403
- /** 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
404
502
  * connection is unknown. */
405
503
  replyPrivately(text: string): void;
406
504
  }
407
505
 
408
- /** One command in a {@link defineCommands} table. */
506
+ /** One command in a declarative command table. */
409
507
  export interface CommandDefinition {
508
+ /** Short, human-readable summary shown in command listings. */
509
+ description?: string;
510
+ /** Optional usage/example string, e.g. "!latency <0-4>". */
511
+ usage?: string;
410
512
  /** Alternate names that invoke this command. */
411
513
  aliases?: string[];
412
- /** Only allow senders whose scopes include "MODERATOR". */
514
+ /** Dispatch only for senders whose scopes include "MODERATOR". */
413
515
  modOnly?: boolean;
414
- /** Minimum milliseconds between invocations per user (clocked off
415
- * `msg.timestamp`). */
516
+ /** Non-negative integer milliseconds between invocations per user. */
416
517
  cooldownMs?: number;
417
518
  /** Invoked when the command runs. */
418
519
  run(ctx: CommandContext): void;
419
- /** Invoked instead of `run` when a non-moderator calls a `modOnly` command. */
420
- onDenied?(ctx: CommandContext): void;
421
- /** Invoked instead of `run` when the per-user cooldown hasn't elapsed. */
422
- onCooldown?(ctx: CommandContext): void;
423
520
  }
424
521
 
425
- export interface CommandsConfig {
426
- /** Command prefix. Default `"!"`. */
427
- prefix?: string;
428
- /** Match command names case-sensitively. Default false. */
429
- caseSensitive?: boolean;
430
- commands: Record<string, CommandDefinition>;
431
- /** Fallback when a prefixed message matches no command. */
432
- onUnknown?(ctx: CommandContext): void;
433
- /** Default denied/cooldown handlers, used when a command omits its own. */
434
- onDenied?(ctx: CommandContext): void;
435
- onCooldown?(ctx: CommandContext): void;
436
- }
437
-
438
- /** Build a chat-command router (prefix parsing, aliases, per-user cooldowns,
439
- * moderator gating). Feed the returned function a `ChatMessage`; it returns
440
- * true when the message was a command (even if gated), false otherwise. */
441
- export function defineCommands(
442
- config: CommandsConfig,
443
- ): (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
+ }
444
530
 
445
531
  /** Typed wrappers around the Owncast host. Each method throws if the
446
532
  * corresponding permission was not declared in plugin.manifest.json. */
@@ -464,7 +550,7 @@ export const owncast: {
464
550
  * `chat.send`. */
465
551
  replyTo(msg: ChatMessage | number | bigint, text: string): boolean;
466
552
  /** Recent chat history (most recent last). Requires `chat.history`.
467
- * Default limit is 50; pass a smaller number to get fewer. */
553
+ * Default limit is 50. Pass a smaller number to get fewer. */
468
554
  history(limit?: number): ChatMessage[];
469
555
  /** Hide a chat message by ID. Requires `chat.moderate`. */
470
556
  deleteMessage(messageId: string): void;
@@ -479,12 +565,30 @@ export const owncast: {
479
565
  list(): User[];
480
566
  /** Fetch one user by ID. Requires `users.read`. */
481
567
  get(id: string): User | null;
482
- /** Enable/disable a user; reason is optional. Requires `users.moderate`. */
568
+ /** Enable/disable a user, with an optional reason. Requires `users.moderate`. */
483
569
  setEnabled(id: string, enabled: boolean, reason?: string): void;
484
570
  /** Ban an IP address. Requires `users.moderate`. */
485
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;
486
590
  };
487
- /** 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
488
592
  * public URL. Requires `storage.upload`. */
489
593
  storage: {
490
594
  upload(name: string, data: Uint8Array | string): UploadResult | null;
@@ -499,7 +603,7 @@ export const owncast: {
499
603
  readText(path: string): string | null;
500
604
  /** Write bytes or a string, creating parent directories as needed. */
501
605
  write(path: string, data: Uint8Array | string): FsResult;
502
- /** List entry names directly inside dir; missing dir lists as empty. */
606
+ /** List entry names directly inside dir. A missing dir lists as empty. */
503
607
  list(dir: string): string[];
504
608
  /** Remove a single file or empty directory. */
505
609
  delete(path: string): FsResult;
@@ -507,7 +611,7 @@ export const owncast: {
507
611
  exists(path: string): boolean;
508
612
  };
509
613
  /** Post to the fediverse on the streamer's behalf. Requires `fediverse.post`,
510
- * 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
511
615
  * admins should grant it sparingly. */
512
616
  fediverse: {
513
617
  /** Publish a public, text-only post. Returns `{ url }` (currently empty
@@ -536,7 +640,7 @@ export const owncast: {
536
640
  setJSON(key: string, value: unknown): void;
537
641
  };
538
642
  /** Read this plugin's admin-configurable settings, declared under
539
- * `config` in the manifest. Ambient no permission required. */
643
+ * `config` in the manifest. Ambient, so no permission is required. */
540
644
  config: {
541
645
  /** The effective value of a manifest-declared config key (admin override,
542
646
  * else the declared default), parsed to its declared type. Returns
@@ -544,10 +648,10 @@ export const owncast: {
544
648
  * value. */
545
649
  get<T = unknown>(key: string, fallback?: T): T;
546
650
  };
547
- /** Read files the plugin bundled in its own `assets/` directory templates,
651
+ /** Read files the plugin bundled in its own `assets/` directory: templates,
548
652
  * data files, and other bundled resources loaded at request time. Path is
549
- * relative to `assets/` and must not contain `..`. Ambient no permission
550
- * required. */
653
+ * relative to `assets/` and must not contain `..`. Ambient, so no permission
654
+ * is required. */
551
655
  assets: {
552
656
  /** Raw bytes of the file, or `null` if not found. */
553
657
  read(path: string): Uint8Array | null;
@@ -564,30 +668,30 @@ export const owncast: {
564
668
  actions: {
565
669
  /** Append one or more buttons to the plugin's runtime list. Each
566
670
  * entry is validated with the same rules as `manifest.actions`
567
- * (title required; exactly one of `url` or `html`; relative URLs
568
- * 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
569
673
  * rejected). The next viewer `/api/config` request returns
570
674
  * `manifest.actions` ++ the runtime list. */
571
675
  add(actions: ActionButton | ActionButton[]): void;
572
- /** Drop the runtime additions; only `manifest.actions` remain on
676
+ /** Drop the runtime additions, so only `manifest.actions` remain on
573
677
  * the next viewer `/api/config` request. */
574
678
  clear(): void;
575
679
  };
576
680
  sse: {
577
681
  /** Push one Server-Sent-Event to every browser connected to this
578
682
  * plugin's `/plugins/<name>/_sse/<channel>` stream. `event` is the SSE
579
- * event name (`""` → the default "message" event); `data` is sent as-is
580
- * 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
581
685
  * slow client are dropped rather than blocking the plugin. Requires the
582
686
  * `http.sse` permission. */
583
687
  send(channel: string, event: string, data: unknown): void;
584
688
  };
585
- /** 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
586
690
  * call your callback back later (in this instance). No permission required.
587
691
  * Timers do not survive a plugin reload or host restart. */
588
692
  timer: {
589
693
  /** Run `fn` once after ~`ms` milliseconds. Returns an id for `clear()`.
590
- * 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
591
695
  * per-plugin pending-timer cap. */
592
696
  setTimeout(fn: () => void, ms: number): number;
593
697
  /** Run `fn` every ~`ms` milliseconds until `clear()`. The next run is
@@ -614,7 +718,7 @@ export const owncast: {
614
718
  tags(): string[];
615
719
  };
616
720
  /** Read/change video/transcoding configuration. read() requires
617
- * `videoconfig.read`; write() requires `videoconfig.write`. */
721
+ * `videoconfig.read`, and write() requires `videoconfig.write`. */
618
722
  videoConfig: {
619
723
  read(): VideoConfig;
620
724
  write(config: VideoConfigUpdate): void;
@@ -635,7 +739,7 @@ export interface HttpResponse {
635
739
 
636
740
  /** An entry in `manifest.actions`, declares an action button the Owncast
637
741
  * UI surfaces while this plugin is enabled. Mirrors Owncast's existing
638
- * ExternalAction shape; the host merges enabled-plugin buttons with the
742
+ * ExternalAction shape. The host merges enabled-plugin buttons with the
639
743
  * admin-configured list.
640
744
  *
641
745
  * Exactly one of `url` or `html` is required.
@@ -666,10 +770,10 @@ export interface ActionButton {
666
770
 
667
771
  /** `manifest.network`, narrows outbound HTTP scope for plugins that
668
772
  * declare the `network.fetch` permission. Required when that permission
669
- * is granted; the host rejects loads otherwise. */
773
+ * is granted. The host rejects loads otherwise. */
670
774
  export interface NetworkConfig {
671
775
  /** Hostname globs the plugin can reach via `owncast.http.fetch`.
672
- * Bare names match exactly (`"api.discord.com"`); `*` is a wildcard
776
+ * Bare names match exactly (`"api.discord.com"`), and `*` is a wildcard
673
777
  * segment (`"*.weather.com"`). The bare wildcard `"*"` matches any
674
778
  * host but must be written explicitly. */
675
779
  allowedHosts: string[];