@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/README.md +6 -6
- package/bin/owncast-plugin.js +39 -37
- package/index.d.ts +172 -89
- package/index.js +212 -279
- package/package.json +2 -1
- package/scripts/postinstall.js +5 -5
- package/slug.js +26 -0
- package/testing.js +15 -48
package/index.js
CHANGED
|
@@ -3,20 +3,18 @@
|
|
|
3
3
|
// Authors define typed handlers (onChatMessage, filterChatMessage, ...) plus
|
|
4
4
|
// an `on: { [customEvent]: handler }` object for plugin-emitted events. The
|
|
5
5
|
// SDK derives the manifest's subscriptions from which handlers are present
|
|
6
|
-
// and returns them via register()
|
|
6
|
+
// and returns them via register(). Authors don't maintain a duplicate list.
|
|
7
7
|
|
|
8
8
|
let registered = null;
|
|
9
9
|
|
|
10
|
-
// Command
|
|
11
|
-
// register() so it can build a unified `!help` across all plugins. One entry
|
|
12
|
-
// per command: { name, prefix, description, usage, aliases, modOnly }.
|
|
10
|
+
// Command registrations used for matching, dispatch, and the unified `!help`.
|
|
13
11
|
const commandManifest = [];
|
|
14
12
|
|
|
15
13
|
// Host-driven timers. The sandbox has no setTimeout, so owncast.timer.* asks
|
|
16
14
|
// the host to schedule a callback and call back via the internal "timer.fire"
|
|
17
15
|
// event. The author's callback stays here in the long-lived instance, keyed by
|
|
18
16
|
// a guest-allocated id the host echoes back. State persists across calls
|
|
19
|
-
// because the plugin instance is reused
|
|
17
|
+
// because the plugin instance is reused. Timers are dropped on reload.
|
|
20
18
|
let nextTimerId = 1;
|
|
21
19
|
const timerCallbacks = new Map(); // id -> { fn, repeat }
|
|
22
20
|
|
|
@@ -43,13 +41,22 @@ const Events = Object.freeze({
|
|
|
43
41
|
// Once-a-second tick for periodic work (opt in by defining onTick)
|
|
44
42
|
Tick: "tick",
|
|
45
43
|
// Fediverse, engagement (metadata only) + inbound posts (with content)
|
|
44
|
+
FediverseActivity: "fediverse.activity",
|
|
46
45
|
FediverseFollow: "fediverse.follow",
|
|
47
46
|
FediverseLike: "fediverse.like",
|
|
48
47
|
FediverseRepost: "fediverse.repost",
|
|
48
|
+
FediverseQuote: "fediverse.quote",
|
|
49
49
|
FediverseMention: "fediverse.mention",
|
|
50
50
|
FediverseReply: "fediverse.reply",
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
const InternalEvents = Object.freeze({
|
|
54
|
+
ChatCommand: "chat.command",
|
|
55
|
+
TimerFire: "timer.fire",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const DefaultCommandPrefix = "!";
|
|
59
|
+
|
|
53
60
|
const Permissions = Object.freeze({
|
|
54
61
|
ChatSend: "chat.send",
|
|
55
62
|
ChatHistory: "chat.history",
|
|
@@ -65,7 +72,10 @@ const Permissions = Object.freeze({
|
|
|
65
72
|
NotificationsSend: "notifications.send",
|
|
66
73
|
UsersRead: "users.read",
|
|
67
74
|
UsersModerate: "users.moderate",
|
|
75
|
+
UsersRegister: "users.register",
|
|
76
|
+
AuthGate: "auth.gate",
|
|
68
77
|
FediversePost: "fediverse.post",
|
|
78
|
+
FediverseInbound: "fediverse.inbound",
|
|
69
79
|
HttpSSE: "http.sse",
|
|
70
80
|
VideoConfigRead: "videoconfig.read",
|
|
71
81
|
VideoConfigWrite: "videoconfig.write",
|
|
@@ -84,6 +94,30 @@ const filter = Object.freeze({
|
|
|
84
94
|
},
|
|
85
95
|
});
|
|
86
96
|
|
|
97
|
+
// Verdict helpers for onAuthCheck (the optional re-validation hook the host runs
|
|
98
|
+
// on a viewer's page load). `ok` keeps the session, `refresh` keeps it and
|
|
99
|
+
// extends the cookie (optional `ttl` seconds), and `deny` ends it and bounces
|
|
100
|
+
// the viewer back to the login screen.
|
|
101
|
+
const authCheck = Object.freeze({
|
|
102
|
+
ok() {
|
|
103
|
+
return { action: "ok" };
|
|
104
|
+
},
|
|
105
|
+
refresh(opts) {
|
|
106
|
+
return { action: "refresh", ...(opts || {}) };
|
|
107
|
+
},
|
|
108
|
+
deny(reason) {
|
|
109
|
+
return { action: "deny", reason: reason || "" };
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// dispatchAuthCheck routes the host's re-validation call to the author's
|
|
114
|
+
// onAuthCheck handler. No handler → always "ok" (the hook is optional, and a
|
|
115
|
+
// plugin that doesn't implement it simply never revokes mid-session).
|
|
116
|
+
function dispatchAuthCheck(req) {
|
|
117
|
+
if (!registered || !isFn(registered.onAuthCheck)) return { action: "ok" };
|
|
118
|
+
return registered.onAuthCheck(req) || { action: "ok" };
|
|
119
|
+
}
|
|
120
|
+
|
|
87
121
|
// Distinguishes notification handlers from filter handlers in the HANDLERS
|
|
88
122
|
// map below. Internal, not part of the public API.
|
|
89
123
|
const HandlerKind = Object.freeze({
|
|
@@ -126,6 +160,11 @@ const HANDLERS = Object.freeze({
|
|
|
126
160
|
onSseDisconnect: { event: Events.SseDisconnect, kind: HandlerKind.Notify },
|
|
127
161
|
// Once-a-second tick
|
|
128
162
|
onTick: { event: Events.Tick, kind: HandlerKind.Notify },
|
|
163
|
+
// Verified inbound ActivityPub activity (raw JSON object)
|
|
164
|
+
onFediverse: {
|
|
165
|
+
event: Events.FediverseActivity,
|
|
166
|
+
kind: HandlerKind.Notify,
|
|
167
|
+
},
|
|
129
168
|
// Fediverse engagement (actor + target metadata)
|
|
130
169
|
onFediverseFollow: {
|
|
131
170
|
event: Events.FediverseFollow,
|
|
@@ -136,6 +175,10 @@ const HANDLERS = Object.freeze({
|
|
|
136
175
|
event: Events.FediverseRepost,
|
|
137
176
|
kind: HandlerKind.Notify,
|
|
138
177
|
},
|
|
178
|
+
onFediverseQuote: {
|
|
179
|
+
event: Events.FediverseQuote,
|
|
180
|
+
kind: HandlerKind.Notify,
|
|
181
|
+
},
|
|
139
182
|
// Fediverse inbound posts (with content)
|
|
140
183
|
onFediverseMention: {
|
|
141
184
|
event: Events.FediverseMention,
|
|
@@ -144,156 +187,70 @@ const HANDLERS = Object.freeze({
|
|
|
144
187
|
onFediverseReply: { event: Events.FediverseReply, kind: HandlerKind.Notify },
|
|
145
188
|
});
|
|
146
189
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const JsType = Object.freeze({
|
|
150
|
-
Function: "function",
|
|
151
|
-
Object: "object",
|
|
152
|
-
});
|
|
153
|
-
const isFn = (x) => typeof x === JsType.Function;
|
|
154
|
-
const isObj = (x) => x !== null && typeof x === JsType.Object;
|
|
190
|
+
const isFn = (x) => typeof x === "function";
|
|
191
|
+
const isObj = (x) => x !== null && typeof x === "object";
|
|
155
192
|
|
|
156
193
|
function definePlugin(def) {
|
|
157
|
-
// `commands` is declarative sugar: give definePlugin a command table (and an
|
|
158
|
-
// optional commandPrefix) and the SDK wires the chat subscription for you —
|
|
159
|
-
// no onChatMessage needed. It expands into an onChatMessage handler here, so
|
|
160
|
-
// subscription derivation and dispatch treat it like any chat handler. If you
|
|
161
|
-
// also pass onChatMessage, the command router runs first, then your handler.
|
|
162
|
-
// For advanced composition (e.g. dropping command messages from chat via a
|
|
163
|
-
// filter), use the lower-level defineCommands() router directly instead.
|
|
164
|
-
if (def && isObj(def.commands)) {
|
|
165
|
-
const router = defineCommands({
|
|
166
|
-
prefix: def.commandPrefix,
|
|
167
|
-
caseSensitive: def.commandsCaseSensitive,
|
|
168
|
-
commands: def.commands,
|
|
169
|
-
onUnknown: def.onUnknownCommand,
|
|
170
|
-
});
|
|
171
|
-
const userHandler = def.onChatMessage;
|
|
172
|
-
def.onChatMessage = isFn(userHandler)
|
|
173
|
-
? (msg) => {
|
|
174
|
-
router(msg);
|
|
175
|
-
userHandler(msg);
|
|
176
|
-
}
|
|
177
|
-
: router;
|
|
178
|
-
}
|
|
179
194
|
registered = def;
|
|
180
|
-
|
|
181
|
-
|
|
195
|
+
commandManifest.length = 0;
|
|
196
|
+
if (!def || !isObj(def.commands)) return def;
|
|
182
197
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
// top-level onCooldown(ctx) / onDenied(ctx), and a top-level onUnknown(ctx).
|
|
207
|
-
function defineCommands(config) {
|
|
208
|
-
config = config || {};
|
|
209
|
-
const prefix = config.prefix || "!";
|
|
210
|
-
const caseSensitive = !!config.caseSensitive;
|
|
211
|
-
const norm = (s) => (caseSensitive ? s : s.toLowerCase());
|
|
212
|
-
|
|
213
|
-
// Resolve every name and alias to its canonical command definition, and
|
|
214
|
-
// record metadata so the host can build a unified `!help` (see
|
|
215
|
-
// describeCommands). Metadata is reported via register(); it never affects
|
|
216
|
-
// routing.
|
|
217
|
-
const table = new Map();
|
|
218
|
-
const defs = config.commands || {};
|
|
219
|
-
for (const name of Object.keys(defs)) {
|
|
220
|
-
const def = defs[name];
|
|
221
|
-
table.set(norm(name), { name, def });
|
|
222
|
-
for (const alias of def.aliases || []) table.set(norm(alias), { name, def });
|
|
198
|
+
const prefix =
|
|
199
|
+
def.commandPrefix == null ? DefaultCommandPrefix : def.commandPrefix;
|
|
200
|
+
if (typeof prefix !== "string" || prefix.length === 0) {
|
|
201
|
+
throw new TypeError("commandPrefix must be a non-empty string");
|
|
202
|
+
}
|
|
203
|
+
const caseSensitive = !!def.commandsCaseSensitive;
|
|
204
|
+
for (const name of Object.keys(def.commands)) {
|
|
205
|
+
const command = def.commands[name];
|
|
206
|
+
if (!isObj(command)) {
|
|
207
|
+
throw new TypeError(`command "${name}" must be an object`);
|
|
208
|
+
}
|
|
209
|
+
const aliases = command.aliases == null ? [] : command.aliases;
|
|
210
|
+
if (
|
|
211
|
+
!Array.isArray(aliases) ||
|
|
212
|
+
!aliases.every((alias) => typeof alias === "string")
|
|
213
|
+
) {
|
|
214
|
+
throw new TypeError(`command "${name}" aliases must be an array of strings`);
|
|
215
|
+
}
|
|
216
|
+
const cooldownMs =
|
|
217
|
+
command.cooldownMs == null ? 0 : command.cooldownMs;
|
|
218
|
+
if (!Number.isSafeInteger(cooldownMs) || cooldownMs < 0) {
|
|
219
|
+
throw new TypeError(`command "${name}" cooldownMs must be a non-negative integer`);
|
|
220
|
+
}
|
|
223
221
|
commandManifest.push({
|
|
224
222
|
name,
|
|
225
223
|
prefix,
|
|
226
|
-
description:
|
|
227
|
-
usage:
|
|
228
|
-
aliases
|
|
229
|
-
modOnly: !!
|
|
224
|
+
description: command.description || "",
|
|
225
|
+
usage: command.usage || "",
|
|
226
|
+
aliases,
|
|
227
|
+
modOnly: !!command.modOnly,
|
|
228
|
+
caseSensitive,
|
|
229
|
+
cooldownMs,
|
|
230
230
|
});
|
|
231
231
|
}
|
|
232
|
+
return def;
|
|
233
|
+
}
|
|
232
234
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (!body.startsWith(prefix)) return false;
|
|
239
|
-
const rest = body.slice(prefix.length);
|
|
240
|
-
const parts = rest.split(/\s+/).filter(Boolean);
|
|
241
|
-
if (parts.length === 0) return false;
|
|
242
|
-
const called = parts[0];
|
|
243
|
-
const args = parts.slice(1);
|
|
244
|
-
const argString = rest.slice(called.length).trim();
|
|
245
|
-
|
|
246
|
-
const ctx = {
|
|
247
|
-
msg,
|
|
248
|
-
user: msg && msg.user,
|
|
249
|
-
command: norm(called),
|
|
250
|
-
args,
|
|
251
|
-
argString,
|
|
252
|
-
reply: (text) => owncast.chat.send(text),
|
|
253
|
-
replyPrivately: (text) => {
|
|
254
|
-
if (!owncast.chat.replyTo(msg, text)) owncast.chat.send(text);
|
|
255
|
-
},
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
const entry = table.get(norm(called));
|
|
259
|
-
if (!entry) {
|
|
260
|
-
if (isFn(config.onUnknown)) config.onUnknown(ctx);
|
|
261
|
-
return false;
|
|
262
|
-
}
|
|
263
|
-
const { name, def } = entry;
|
|
264
|
-
ctx.command = name;
|
|
265
|
-
|
|
266
|
-
// Moderator gating: the sender's scopes must include MODERATOR.
|
|
267
|
-
if (def.modOnly) {
|
|
268
|
-
const scopes = (msg.user && msg.user.scopes) || [];
|
|
269
|
-
if (!scopes.includes("MODERATOR")) {
|
|
270
|
-
if (isFn(def.onDenied)) def.onDenied(ctx);
|
|
271
|
-
else if (isFn(config.onDenied)) config.onDenied(ctx);
|
|
272
|
-
return true; // matched a command, but the caller wasn't allowed
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Per-user cooldown, clocked off msg.timestamp so it's deterministic in
|
|
277
|
-
// tests and independent of any sandbox clock quirks.
|
|
278
|
-
const cooldownMs = def.cooldownMs || 0;
|
|
279
|
-
if (cooldownMs > 0) {
|
|
280
|
-
const userId =
|
|
281
|
-
(msg.user && msg.user.id) ||
|
|
282
|
-
(msg.clientId != null ? `c${msg.clientId}` : "anon");
|
|
283
|
-
const key = `${name}${userId}`;
|
|
284
|
-
const now = msg.timestamp ? new Date(msg.timestamp).getTime() : 0;
|
|
285
|
-
const prev = lastRun.get(key);
|
|
286
|
-
if (now && prev && now - prev < cooldownMs) {
|
|
287
|
-
if (isFn(def.onCooldown)) def.onCooldown(ctx);
|
|
288
|
-
else if (isFn(config.onCooldown)) config.onCooldown(ctx);
|
|
289
|
-
return true;
|
|
290
|
-
}
|
|
291
|
-
if (now) lastRun.set(key, now);
|
|
292
|
-
}
|
|
235
|
+
function dispatchCommand(event) {
|
|
236
|
+
if (!isObj(event)) return;
|
|
237
|
+
if (!registered || !isObj(registered.commands)) return;
|
|
238
|
+
const command = registered.commands[event.command];
|
|
239
|
+
if (!command || !isFn(command.run)) return;
|
|
293
240
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
241
|
+
const msg = event.message;
|
|
242
|
+
command.run({
|
|
243
|
+
msg,
|
|
244
|
+
user: msg && msg.user,
|
|
245
|
+
command: event.command,
|
|
246
|
+
invokedAs: event.invokedAs || event.command,
|
|
247
|
+
args: event.args || [],
|
|
248
|
+
argString: event.argString || "",
|
|
249
|
+
reply: (text) => owncast.chat.send(text),
|
|
250
|
+
replyPrivately: (text) => {
|
|
251
|
+
if (!owncast.chat.replyTo(msg, text)) owncast.chat.send(text);
|
|
252
|
+
},
|
|
253
|
+
});
|
|
297
254
|
}
|
|
298
255
|
|
|
299
256
|
// Used by the build-generated entry to compute subscriptions for register().
|
|
@@ -324,9 +281,8 @@ function describeSubscriptions() {
|
|
|
324
281
|
return { notify, filter: filterSubs };
|
|
325
282
|
}
|
|
326
283
|
|
|
327
|
-
// Used by the build-generated entry to report
|
|
328
|
-
//
|
|
329
|
-
// declares no commands.
|
|
284
|
+
// Used by the build-generated entry to report command registrations to the
|
|
285
|
+
// host for matching, dispatch, and the unified `!help`.
|
|
330
286
|
function describeCommands() {
|
|
331
287
|
return commandManifest;
|
|
332
288
|
}
|
|
@@ -336,7 +292,7 @@ function dispatchEvent(envelope) {
|
|
|
336
292
|
// Internal: a host-scheduled timer elapsed. Run the author's callback,
|
|
337
293
|
// dropping one-shot entries first so a throw still cleans up. Not routed to
|
|
338
294
|
// user handlers or the `on` map.
|
|
339
|
-
if (eventType ===
|
|
295
|
+
if (eventType === InternalEvents.TimerFire) {
|
|
340
296
|
const id = payload && payload.id;
|
|
341
297
|
const entry = timerCallbacks.get(id);
|
|
342
298
|
if (entry) {
|
|
@@ -345,6 +301,10 @@ function dispatchEvent(envelope) {
|
|
|
345
301
|
}
|
|
346
302
|
return;
|
|
347
303
|
}
|
|
304
|
+
if (eventType === InternalEvents.ChatCommand) {
|
|
305
|
+
dispatchCommand(payload);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
348
308
|
if (!registered) return;
|
|
349
309
|
for (const [method, info] of Object.entries(HANDLERS)) {
|
|
350
310
|
if (
|
|
@@ -395,7 +355,7 @@ function dispatchHttp(request) {
|
|
|
395
355
|
// host runtime captures), so a plugin author running `owncast-plugin
|
|
396
356
|
// serve` or hitting the host's logs sees exactly which permission to
|
|
397
357
|
// add to their manifest. apiName is the SDK call the author wrote
|
|
398
|
-
// (e.g. "owncast.actions.set")
|
|
358
|
+
// (e.g. "owncast.actions.set"). perm is the manifest permission string.
|
|
399
359
|
function permError(apiName, perm) {
|
|
400
360
|
const msg = `${apiName} requires the '${perm}' permission. Add it to your plugin.manifest.json's "permissions" array.`;
|
|
401
361
|
console.error(`[owncast-plugin] ${msg}`);
|
|
@@ -423,50 +383,45 @@ function scheduleTimer(fn, ms, repeat) {
|
|
|
423
383
|
return id;
|
|
424
384
|
}
|
|
425
385
|
|
|
386
|
+
// hostFns returns the host import table, throwing an actionable error if the
|
|
387
|
+
// named function wasn't granted (the plugin's manifest is missing its
|
|
388
|
+
// permission). This is the per-call guard every owncast.* method used to inline.
|
|
389
|
+
function hostFns(name, perm) {
|
|
390
|
+
const fns = Host.getFunctions();
|
|
391
|
+
if (!fns[name]) throw new Error(`permission '${perm}' not granted`);
|
|
392
|
+
return fns;
|
|
393
|
+
}
|
|
394
|
+
|
|
426
395
|
const owncast = {
|
|
427
396
|
chat: {
|
|
428
397
|
send(text) {
|
|
429
|
-
const fns =
|
|
430
|
-
if (!fns.owncast_send_chat)
|
|
431
|
-
throw new Error(`permission '${Permissions.ChatSend}' not granted`);
|
|
398
|
+
const fns = hostFns("owncast_send_chat", Permissions.ChatSend);
|
|
432
399
|
fns.owncast_send_chat(Memory.fromString(text).offset);
|
|
433
400
|
},
|
|
434
401
|
sendAction(text) {
|
|
435
|
-
const fns =
|
|
436
|
-
if (!fns.owncast_send_chat_action)
|
|
437
|
-
throw new Error(`permission '${Permissions.ChatSend}' not granted`);
|
|
402
|
+
const fns = hostFns("owncast_send_chat_action", Permissions.ChatSend);
|
|
438
403
|
fns.owncast_send_chat_action(Memory.fromString(text).offset);
|
|
439
404
|
},
|
|
440
405
|
system(body) {
|
|
441
|
-
const fns =
|
|
442
|
-
if (!fns.owncast_send_chat_system)
|
|
443
|
-
throw new Error(`permission '${Permissions.ChatSend}' not granted`);
|
|
406
|
+
const fns = hostFns("owncast_send_chat_system", Permissions.ChatSend);
|
|
444
407
|
fns.owncast_send_chat_system(Memory.fromString(body).offset);
|
|
445
408
|
},
|
|
446
409
|
history(limit) {
|
|
447
|
-
const fns =
|
|
448
|
-
if (!fns.owncast_chat_history)
|
|
449
|
-
throw new Error(`permission '${Permissions.ChatHistory}' not granted`);
|
|
410
|
+
const fns = hostFns("owncast_chat_history", Permissions.ChatHistory);
|
|
450
411
|
const offset = fns.owncast_chat_history(limit || 0);
|
|
451
412
|
if (offset == 0) return [];
|
|
452
413
|
return JSON.parse(Memory.find(offset).readString());
|
|
453
414
|
},
|
|
454
415
|
deleteMessage(messageId) {
|
|
455
|
-
const fns =
|
|
456
|
-
if (!fns.owncast_delete_message)
|
|
457
|
-
throw new Error(`permission '${Permissions.ChatModerate}' not granted`);
|
|
416
|
+
const fns = hostFns("owncast_delete_message", Permissions.ChatModerate);
|
|
458
417
|
fns.owncast_delete_message(Memory.fromString(String(messageId)).offset);
|
|
459
418
|
},
|
|
460
419
|
kick(clientId) {
|
|
461
|
-
const fns =
|
|
462
|
-
if (!fns.owncast_kick_client)
|
|
463
|
-
throw new Error(`permission '${Permissions.ChatModerate}' not granted`);
|
|
420
|
+
const fns = hostFns("owncast_kick_client", Permissions.ChatModerate);
|
|
464
421
|
fns.owncast_kick_client(BigInt(clientId));
|
|
465
422
|
},
|
|
466
423
|
sendTo(clientId, text) {
|
|
467
|
-
const fns =
|
|
468
|
-
if (!fns.owncast_send_chat_to)
|
|
469
|
-
throw new Error(`permission '${Permissions.ChatSend}' not granted`);
|
|
424
|
+
const fns = hostFns("owncast_send_chat_to", Permissions.ChatSend);
|
|
470
425
|
fns.owncast_send_chat_to(
|
|
471
426
|
BigInt(clientId),
|
|
472
427
|
Memory.fromString(text).offset,
|
|
@@ -487,9 +442,7 @@ const owncast = {
|
|
|
487
442
|
return true;
|
|
488
443
|
},
|
|
489
444
|
clients() {
|
|
490
|
-
const fns =
|
|
491
|
-
if (!fns.owncast_chat_clients)
|
|
492
|
-
throw new Error(`permission '${Permissions.ChatHistory}' not granted`);
|
|
445
|
+
const fns = hostFns("owncast_chat_clients", Permissions.ChatHistory);
|
|
493
446
|
const offset = fns.owncast_chat_clients();
|
|
494
447
|
if (offset == 0) return [];
|
|
495
448
|
return JSON.parse(Memory.find(offset).readString());
|
|
@@ -497,27 +450,19 @@ const owncast = {
|
|
|
497
450
|
},
|
|
498
451
|
users: {
|
|
499
452
|
list() {
|
|
500
|
-
const fns =
|
|
501
|
-
if (!fns.owncast_users_list)
|
|
502
|
-
throw new Error(`permission '${Permissions.UsersRead}' not granted`);
|
|
453
|
+
const fns = hostFns("owncast_users_list", Permissions.UsersRead);
|
|
503
454
|
const offset = fns.owncast_users_list();
|
|
504
455
|
if (offset == 0) return [];
|
|
505
456
|
return JSON.parse(Memory.find(offset).readString());
|
|
506
457
|
},
|
|
507
458
|
get(id) {
|
|
508
|
-
const fns =
|
|
509
|
-
if (!fns.owncast_user_get)
|
|
510
|
-
throw new Error(`permission '${Permissions.UsersRead}' not granted`);
|
|
459
|
+
const fns = hostFns("owncast_user_get", Permissions.UsersRead);
|
|
511
460
|
const offset = fns.owncast_user_get(Memory.fromString(id).offset);
|
|
512
461
|
if (offset == 0) return null;
|
|
513
462
|
return JSON.parse(Memory.find(offset).readString());
|
|
514
463
|
},
|
|
515
464
|
setEnabled(id, enabled, reason) {
|
|
516
|
-
const fns =
|
|
517
|
-
if (!fns.owncast_user_set_enabled)
|
|
518
|
-
throw new Error(
|
|
519
|
-
`permission '${Permissions.UsersModerate}' not granted`,
|
|
520
|
-
);
|
|
465
|
+
const fns = hostFns("owncast_user_set_enabled", Permissions.UsersModerate);
|
|
521
466
|
fns.owncast_user_set_enabled(
|
|
522
467
|
Memory.fromString(id).offset,
|
|
523
468
|
enabled ? 1 : 0,
|
|
@@ -525,21 +470,54 @@ const owncast = {
|
|
|
525
470
|
);
|
|
526
471
|
},
|
|
527
472
|
banIP(ip) {
|
|
528
|
-
const fns =
|
|
529
|
-
if (!fns.owncast_ban_ip)
|
|
530
|
-
throw new Error(
|
|
531
|
-
`permission '${Permissions.UsersModerate}' not granted`,
|
|
532
|
-
);
|
|
473
|
+
const fns = hostFns("owncast_ban_ip", Permissions.UsersModerate);
|
|
533
474
|
fns.owncast_ban_ip(Memory.fromString(ip).offset);
|
|
534
475
|
},
|
|
476
|
+
// Find-or-create an authenticated Owncast user for an external identity
|
|
477
|
+
// (e.g. a provider account). `authId` is the stable provider-scoped id. The
|
|
478
|
+
// host namespaces it by this plugin's slug so it can't collide with or spoof
|
|
479
|
+
// another plugin's users. Optionally seeds displayName and scopes. Returns
|
|
480
|
+
// { userId }. Throws on host error. Requires `users.register`.
|
|
481
|
+
register(opts) {
|
|
482
|
+
const fns = hostFns("owncast_users_register", Permissions.UsersRegister);
|
|
483
|
+
const req =
|
|
484
|
+
typeof opts === "string" ? { authId: opts } : opts || {};
|
|
485
|
+
const offset = fns.owncast_users_register(
|
|
486
|
+
Memory.fromString(JSON.stringify(req)).offset,
|
|
487
|
+
);
|
|
488
|
+
if (offset == 0) throw new Error("users.register failed");
|
|
489
|
+
const result = JSON.parse(Memory.find(offset).readString());
|
|
490
|
+
if (result.error) throw new Error(result.error);
|
|
491
|
+
return result; // { userId }
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
// Viewer-authentication gate. Only a plugin holding `auth.gate` (and enabled by
|
|
495
|
+
// an admin) can issue sessions, and these are valid only inside onHttpRequest,
|
|
496
|
+
// where the host attaches/clears the signed session cookie on the response.
|
|
497
|
+
auth: {
|
|
498
|
+
// Issue a gate session for an already-registered user (see users.register).
|
|
499
|
+
// `ttl` is optional seconds, and 0/omitted uses the host default. Throws on
|
|
500
|
+
// host error. Requires `auth.gate`.
|
|
501
|
+
grantSession(opts) {
|
|
502
|
+
const fns = hostFns("owncast_auth_grant_session", Permissions.AuthGate);
|
|
503
|
+
const req = typeof opts === "string" ? { userId: opts } : opts || {};
|
|
504
|
+
const offset = fns.owncast_auth_grant_session(
|
|
505
|
+
Memory.fromString(JSON.stringify(req)).offset,
|
|
506
|
+
);
|
|
507
|
+
if (offset == 0) throw new Error("auth.grantSession failed");
|
|
508
|
+
const result = JSON.parse(Memory.find(offset).readString());
|
|
509
|
+
if (result.error) throw new Error(result.error);
|
|
510
|
+
},
|
|
511
|
+
// Clear the current viewer's gate session (logout). The plugin still owns the
|
|
512
|
+
// response/redirect. Requires `auth.gate`.
|
|
513
|
+
endSession() {
|
|
514
|
+
const fns = hostFns("owncast_auth_end_session", Permissions.AuthGate);
|
|
515
|
+
fns.owncast_auth_end_session();
|
|
516
|
+
},
|
|
535
517
|
},
|
|
536
518
|
storage: {
|
|
537
519
|
upload(name, data) {
|
|
538
|
-
const fns =
|
|
539
|
-
if (!fns.owncast_storage_upload)
|
|
540
|
-
throw new Error(
|
|
541
|
-
`permission '${Permissions.StorageUpload}' not granted`,
|
|
542
|
-
);
|
|
520
|
+
const fns = hostFns("owncast_storage_upload", Permissions.StorageUpload);
|
|
543
521
|
const dataMem =
|
|
544
522
|
data instanceof Uint8Array
|
|
545
523
|
? Memory.fromBuffer(
|
|
@@ -565,9 +543,7 @@ const owncast = {
|
|
|
565
543
|
// Read a file's raw bytes. Returns a Uint8Array, or null if the file
|
|
566
544
|
// doesn't exist (or can't be read).
|
|
567
545
|
read(path) {
|
|
568
|
-
const fns =
|
|
569
|
-
if (!fns.owncast_fs_read)
|
|
570
|
-
throw new Error(`permission '${Permissions.StorageFS}' not granted`);
|
|
546
|
+
const fns = hostFns("owncast_fs_read", Permissions.StorageFS);
|
|
571
547
|
const offset = fns.owncast_fs_read(Memory.fromString(path).offset);
|
|
572
548
|
if (offset == 0) return null;
|
|
573
549
|
return new Uint8Array(Memory.find(offset).readBytes());
|
|
@@ -575,9 +551,7 @@ const owncast = {
|
|
|
575
551
|
// Read a file as UTF-8 text. Returns a string, or null if the file
|
|
576
552
|
// doesn't exist. (The Extism boundary decodes the bytes as UTF-8.)
|
|
577
553
|
readText(path) {
|
|
578
|
-
const fns =
|
|
579
|
-
if (!fns.owncast_fs_read)
|
|
580
|
-
throw new Error(`permission '${Permissions.StorageFS}' not granted`);
|
|
554
|
+
const fns = hostFns("owncast_fs_read", Permissions.StorageFS);
|
|
581
555
|
const offset = fns.owncast_fs_read(Memory.fromString(path).offset);
|
|
582
556
|
if (offset == 0) return null;
|
|
583
557
|
return Memory.find(offset).readString();
|
|
@@ -585,9 +559,7 @@ const owncast = {
|
|
|
585
559
|
// Write bytes (Uint8Array) or a string to a file, creating parent
|
|
586
560
|
// directories as needed. Returns { ok, error? }.
|
|
587
561
|
write(path, data) {
|
|
588
|
-
const fns =
|
|
589
|
-
if (!fns.owncast_fs_write)
|
|
590
|
-
throw new Error(`permission '${Permissions.StorageFS}' not granted`);
|
|
562
|
+
const fns = hostFns("owncast_fs_write", Permissions.StorageFS);
|
|
591
563
|
const dataMem =
|
|
592
564
|
data instanceof Uint8Array
|
|
593
565
|
? Memory.fromBuffer(
|
|
@@ -607,27 +579,21 @@ const owncast = {
|
|
|
607
579
|
// List the entry names (files and subdirectories) directly inside dir.
|
|
608
580
|
// A missing directory lists as empty. Returns string[].
|
|
609
581
|
list(dir) {
|
|
610
|
-
const fns =
|
|
611
|
-
if (!fns.owncast_fs_list)
|
|
612
|
-
throw new Error(`permission '${Permissions.StorageFS}' not granted`);
|
|
582
|
+
const fns = hostFns("owncast_fs_list", Permissions.StorageFS);
|
|
613
583
|
const offset = fns.owncast_fs_list(Memory.fromString(dir || "").offset);
|
|
614
584
|
if (offset == 0) return [];
|
|
615
585
|
return JSON.parse(Memory.find(offset).readString());
|
|
616
586
|
},
|
|
617
587
|
// Remove a single file or empty directory. Returns { ok, error? }.
|
|
618
588
|
delete(path) {
|
|
619
|
-
const fns =
|
|
620
|
-
if (!fns.owncast_fs_delete)
|
|
621
|
-
throw new Error(`permission '${Permissions.StorageFS}' not granted`);
|
|
589
|
+
const fns = hostFns("owncast_fs_delete", Permissions.StorageFS);
|
|
622
590
|
const offset = fns.owncast_fs_delete(Memory.fromString(path).offset);
|
|
623
591
|
if (offset == 0) return { ok: false, error: "delete failed" };
|
|
624
592
|
return JSON.parse(Memory.find(offset).readString());
|
|
625
593
|
},
|
|
626
594
|
// Report whether a path exists inside the sandbox. Returns boolean.
|
|
627
595
|
exists(path) {
|
|
628
|
-
const fns =
|
|
629
|
-
if (!fns.owncast_fs_exists)
|
|
630
|
-
throw new Error(`permission '${Permissions.StorageFS}' not granted`);
|
|
596
|
+
const fns = hostFns("owncast_fs_exists", Permissions.StorageFS);
|
|
631
597
|
return fns.owncast_fs_exists(Memory.fromString(path).offset) === 1;
|
|
632
598
|
},
|
|
633
599
|
},
|
|
@@ -636,11 +602,7 @@ const owncast = {
|
|
|
636
602
|
* behalf. Returns { url } on success, null on failure (rate-limited,
|
|
637
603
|
* disabled by admin, etc.). Requires `fediverse.post`. */
|
|
638
604
|
post(text) {
|
|
639
|
-
const fns =
|
|
640
|
-
if (!fns.owncast_fediverse_post)
|
|
641
|
-
throw new Error(
|
|
642
|
-
`permission '${Permissions.FediversePost}' not granted`,
|
|
643
|
-
);
|
|
605
|
+
const fns = hostFns("owncast_fediverse_post", Permissions.FediversePost);
|
|
644
606
|
const offset = fns.owncast_fediverse_post(Memory.fromString(text).offset);
|
|
645
607
|
if (offset == 0) return null;
|
|
646
608
|
return JSON.parse(Memory.find(offset).readString());
|
|
@@ -648,30 +610,18 @@ const owncast = {
|
|
|
648
610
|
},
|
|
649
611
|
notifications: {
|
|
650
612
|
discord(text) {
|
|
651
|
-
const fns =
|
|
652
|
-
if (!fns.owncast_notify_discord)
|
|
653
|
-
throw new Error(
|
|
654
|
-
`permission '${Permissions.NotificationsSend}' not granted`,
|
|
655
|
-
);
|
|
613
|
+
const fns = hostFns("owncast_notify_discord", Permissions.NotificationsSend);
|
|
656
614
|
fns.owncast_notify_discord(Memory.fromString(text).offset);
|
|
657
615
|
},
|
|
658
616
|
browserPush(payload) {
|
|
659
|
-
const fns =
|
|
660
|
-
if (!fns.owncast_notify_browser_push)
|
|
661
|
-
throw new Error(
|
|
662
|
-
`permission '${Permissions.NotificationsSend}' not granted`,
|
|
663
|
-
);
|
|
617
|
+
const fns = hostFns("owncast_notify_browser_push", Permissions.NotificationsSend);
|
|
664
618
|
const obj = typeof payload === "string" ? { title: payload } : payload;
|
|
665
619
|
fns.owncast_notify_browser_push(
|
|
666
620
|
Memory.fromString(JSON.stringify(obj)).offset,
|
|
667
621
|
);
|
|
668
622
|
},
|
|
669
623
|
fediverse(payload) {
|
|
670
|
-
const fns =
|
|
671
|
-
if (!fns.owncast_notify_fediverse)
|
|
672
|
-
throw new Error(
|
|
673
|
-
`permission '${Permissions.NotificationsSend}' not granted`,
|
|
674
|
-
);
|
|
624
|
+
const fns = hostFns("owncast_notify_fediverse", Permissions.NotificationsSend);
|
|
675
625
|
fns.owncast_notify_fediverse(
|
|
676
626
|
Memory.fromString(JSON.stringify(payload)).offset,
|
|
677
627
|
);
|
|
@@ -679,17 +629,13 @@ const owncast = {
|
|
|
679
629
|
},
|
|
680
630
|
stream: {
|
|
681
631
|
current() {
|
|
682
|
-
const fns =
|
|
683
|
-
if (!fns.owncast_stream_current)
|
|
684
|
-
throw new Error(`permission '${Permissions.ServerRead}' not granted`);
|
|
632
|
+
const fns = hostFns("owncast_stream_current", Permissions.ServerRead);
|
|
685
633
|
const offset = fns.owncast_stream_current();
|
|
686
634
|
if (offset == 0) return { online: false, viewers: 0 };
|
|
687
635
|
return JSON.parse(Memory.find(offset).readString());
|
|
688
636
|
},
|
|
689
637
|
broadcaster() {
|
|
690
|
-
const fns =
|
|
691
|
-
if (!fns.owncast_stream_broadcaster)
|
|
692
|
-
throw new Error(`permission '${Permissions.ServerRead}' not granted`);
|
|
638
|
+
const fns = hostFns("owncast_stream_broadcaster", Permissions.ServerRead);
|
|
693
639
|
const offset = fns.owncast_stream_broadcaster();
|
|
694
640
|
if (offset == 0) return {};
|
|
695
641
|
return JSON.parse(Memory.find(offset).readString());
|
|
@@ -697,41 +643,31 @@ const owncast = {
|
|
|
697
643
|
},
|
|
698
644
|
server: {
|
|
699
645
|
info() {
|
|
700
|
-
const fns =
|
|
701
|
-
if (!fns.owncast_server_info)
|
|
702
|
-
throw new Error(`permission '${Permissions.ServerRead}' not granted`);
|
|
646
|
+
const fns = hostFns("owncast_server_info", Permissions.ServerRead);
|
|
703
647
|
const offset = fns.owncast_server_info();
|
|
704
648
|
if (offset == 0) return {};
|
|
705
649
|
return JSON.parse(Memory.find(offset).readString());
|
|
706
650
|
},
|
|
707
651
|
socials() {
|
|
708
|
-
const fns =
|
|
709
|
-
if (!fns.owncast_server_socials)
|
|
710
|
-
throw new Error(`permission '${Permissions.ServerRead}' not granted`);
|
|
652
|
+
const fns = hostFns("owncast_server_socials", Permissions.ServerRead);
|
|
711
653
|
const offset = fns.owncast_server_socials();
|
|
712
654
|
if (offset == 0) return [];
|
|
713
655
|
return JSON.parse(Memory.find(offset).readString());
|
|
714
656
|
},
|
|
715
657
|
emotes() {
|
|
716
|
-
const fns =
|
|
717
|
-
if (!fns.owncast_server_emotes)
|
|
718
|
-
throw new Error(`permission '${Permissions.ServerRead}' not granted`);
|
|
658
|
+
const fns = hostFns("owncast_server_emotes", Permissions.ServerRead);
|
|
719
659
|
const offset = fns.owncast_server_emotes();
|
|
720
660
|
if (offset == 0) return [];
|
|
721
661
|
return JSON.parse(Memory.find(offset).readString());
|
|
722
662
|
},
|
|
723
663
|
federation() {
|
|
724
|
-
const fns =
|
|
725
|
-
if (!fns.owncast_server_federation)
|
|
726
|
-
throw new Error(`permission '${Permissions.ServerRead}' not granted`);
|
|
664
|
+
const fns = hostFns("owncast_server_federation", Permissions.ServerRead);
|
|
727
665
|
const offset = fns.owncast_server_federation();
|
|
728
666
|
if (offset == 0) return { enabled: false };
|
|
729
667
|
return JSON.parse(Memory.find(offset).readString());
|
|
730
668
|
},
|
|
731
669
|
tags() {
|
|
732
|
-
const fns =
|
|
733
|
-
if (!fns.owncast_server_tags)
|
|
734
|
-
throw new Error(`permission '${Permissions.ServerRead}' not granted`);
|
|
670
|
+
const fns = hostFns("owncast_server_tags", Permissions.ServerRead);
|
|
735
671
|
const offset = fns.owncast_server_tags();
|
|
736
672
|
if (offset == 0) return [];
|
|
737
673
|
return JSON.parse(Memory.find(offset).readString());
|
|
@@ -741,24 +677,16 @@ const owncast = {
|
|
|
741
677
|
/** Read the current video/transcoding config: { latencyLevel, codec,
|
|
742
678
|
* variants }. Requires `videoconfig.read`. */
|
|
743
679
|
read() {
|
|
744
|
-
const fns =
|
|
745
|
-
if (!fns.owncast_video_config_read)
|
|
746
|
-
throw new Error(
|
|
747
|
-
`permission '${Permissions.VideoConfigRead}' not granted`,
|
|
748
|
-
);
|
|
680
|
+
const fns = hostFns("owncast_video_config_read", Permissions.VideoConfigRead);
|
|
749
681
|
const offset = fns.owncast_video_config_read();
|
|
750
682
|
if (offset == 0) return { latencyLevel: 0, codec: "", variants: [] };
|
|
751
683
|
return JSON.parse(Memory.find(offset).readString());
|
|
752
684
|
},
|
|
753
685
|
/** Apply a partial video config change. Pass any of { latencyLevel, codec,
|
|
754
|
-
* variants }
|
|
686
|
+
* variants }, where omitted fields are left unchanged. Throws if the host
|
|
755
687
|
* rejects the config. Requires `videoconfig.write`. */
|
|
756
688
|
write(config) {
|
|
757
|
-
const fns =
|
|
758
|
-
if (!fns.owncast_video_config_write)
|
|
759
|
-
throw new Error(
|
|
760
|
-
`permission '${Permissions.VideoConfigWrite}' not granted`,
|
|
761
|
-
);
|
|
689
|
+
const fns = hostFns("owncast_video_config_write", Permissions.VideoConfigWrite);
|
|
762
690
|
const offset = fns.owncast_video_config_write(
|
|
763
691
|
Memory.fromString(JSON.stringify(config || {})).offset,
|
|
764
692
|
);
|
|
@@ -770,17 +698,13 @@ const owncast = {
|
|
|
770
698
|
},
|
|
771
699
|
kv: {
|
|
772
700
|
get(key) {
|
|
773
|
-
const fns =
|
|
774
|
-
if (!fns.owncast_kv_get)
|
|
775
|
-
throw new Error(`permission '${Permissions.StorageKV}' not granted`);
|
|
701
|
+
const fns = hostFns("owncast_kv_get", Permissions.StorageKV);
|
|
776
702
|
const offset = fns.owncast_kv_get(Memory.fromString(key).offset);
|
|
777
703
|
if (offset == 0) return null;
|
|
778
704
|
return Memory.find(offset).readString();
|
|
779
705
|
},
|
|
780
706
|
set(key, value) {
|
|
781
|
-
const fns =
|
|
782
|
-
if (!fns.owncast_kv_set)
|
|
783
|
-
throw new Error(`permission '${Permissions.StorageKV}' not granted`);
|
|
707
|
+
const fns = hostFns("owncast_kv_set", Permissions.StorageKV);
|
|
784
708
|
fns.owncast_kv_set(
|
|
785
709
|
Memory.fromString(key).offset,
|
|
786
710
|
Memory.fromString(String(value)).offset,
|
|
@@ -837,9 +761,7 @@ const owncast = {
|
|
|
837
761
|
},
|
|
838
762
|
events: {
|
|
839
763
|
emit(eventType, payload) {
|
|
840
|
-
const fns =
|
|
841
|
-
if (!fns.owncast_emit_event)
|
|
842
|
-
throw new Error(`permission '${Permissions.EventsEmit}' not granted`);
|
|
764
|
+
const fns = hostFns("owncast_emit_event", Permissions.EventsEmit);
|
|
843
765
|
fns.owncast_emit_event(
|
|
844
766
|
Memory.fromString(eventType).offset,
|
|
845
767
|
Memory.fromString(JSON.stringify(payload)).offset,
|
|
@@ -875,15 +797,13 @@ const owncast = {
|
|
|
875
797
|
// send(channel, event, data) pushes one Server-Sent-Event to every
|
|
876
798
|
// browser connected to this plugin's /plugins/<name>/_sse/<channel>
|
|
877
799
|
// stream. `event` is the SSE event name (browser side:
|
|
878
|
-
// source.addEventListener(event, ...))
|
|
800
|
+
// source.addEventListener(event, ...)). Pass "" for the default
|
|
879
801
|
// "message" event. `data` is sent as-is if it's a string, otherwise
|
|
880
802
|
// JSON-stringified. Fire-and-forget: returns immediately, and frames to
|
|
881
803
|
// a slow client are dropped rather than blocking the plugin. Requires
|
|
882
804
|
// the 'http.sse' permission.
|
|
883
805
|
send(channel, event, data) {
|
|
884
|
-
const fns =
|
|
885
|
-
if (!fns.owncast_sse_send)
|
|
886
|
-
throw new Error(`permission '${Permissions.HttpSSE}' not granted`);
|
|
806
|
+
const fns = hostFns("owncast_sse_send", Permissions.HttpSSE);
|
|
887
807
|
const payload = typeof data === "string" ? data : JSON.stringify(data);
|
|
888
808
|
fns.owncast_sse_send(
|
|
889
809
|
Memory.fromString(channel || "").offset,
|
|
@@ -895,7 +815,7 @@ const owncast = {
|
|
|
895
815
|
timer: {
|
|
896
816
|
// setTimeout(fn, ms) runs fn once after ~ms milliseconds. setInterval
|
|
897
817
|
// repeats until clear(id). The host drives the schedule (the sandbox has
|
|
898
|
-
// no setTimeout)
|
|
818
|
+
// no setTimeout). Your callback runs in this instance when it fires.
|
|
899
819
|
// Returns an id for clear(). Very small delays are clamped up by the host,
|
|
900
820
|
// and there's a per-plugin cap on pending timers (throws past it).
|
|
901
821
|
// Note: timers are in-memory and do not survive a plugin reload or a host
|
|
@@ -945,11 +865,21 @@ function dispatchPageContent(req) {
|
|
|
945
865
|
return registered.onPageContent(req) || "";
|
|
946
866
|
}
|
|
947
867
|
|
|
868
|
+
function dispatchPageStyles() {
|
|
869
|
+
if (!registered || !isFn(registered.onPageStyles)) return "";
|
|
870
|
+
return registered.onPageStyles() || "";
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function dispatchPageScripts() {
|
|
874
|
+
if (!registered || !isFn(registered.onPageScripts)) return "";
|
|
875
|
+
return registered.onPageScripts() || "";
|
|
876
|
+
}
|
|
877
|
+
|
|
948
878
|
module.exports = {
|
|
949
879
|
definePlugin,
|
|
950
|
-
defineCommands,
|
|
951
880
|
owncast,
|
|
952
881
|
filter,
|
|
882
|
+
authCheck,
|
|
953
883
|
FilterAction,
|
|
954
884
|
Events,
|
|
955
885
|
Permissions,
|
|
@@ -958,6 +888,9 @@ module.exports = {
|
|
|
958
888
|
dispatchEvent,
|
|
959
889
|
dispatchFilter,
|
|
960
890
|
dispatchHttp,
|
|
891
|
+
dispatchAuthCheck,
|
|
961
892
|
dispatchTabContent,
|
|
962
893
|
dispatchPageContent,
|
|
894
|
+
dispatchPageStyles,
|
|
895
|
+
dispatchPageScripts,
|
|
963
896
|
};
|