@owncast/plugin-sdk 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/owncast-plugin.js +15 -1
- package/index.d.ts +31 -0
- package/index.js +32 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +92 -36
package/bin/owncast-plugin.js
CHANGED
|
@@ -202,7 +202,17 @@ function on_http_request() {
|
|
|
202
202
|
Host.outputString(JSON.stringify(response));
|
|
203
203
|
return 0;
|
|
204
204
|
}
|
|
205
|
-
|
|
205
|
+
function on_tab_content() {
|
|
206
|
+
const req = JSON.parse(Host.inputString());
|
|
207
|
+
Host.outputString(sdk.dispatchTabContent(req));
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
function on_page_content() {
|
|
211
|
+
const req = JSON.parse(Host.inputString());
|
|
212
|
+
Host.outputString(sdk.dispatchPageContent(req));
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
module.exports = { register, on_event, on_filter, on_http_request, on_tab_content, on_page_content };
|
|
206
216
|
`;
|
|
207
217
|
fs.writeFileSync(synthEntry, entrySrc);
|
|
208
218
|
|
|
@@ -362,6 +372,8 @@ function generateInterface(manifest) {
|
|
|
362
372
|
"on_event(): I32",
|
|
363
373
|
"on_filter(): I32",
|
|
364
374
|
"on_http_request(): I32",
|
|
375
|
+
"on_tab_content(): I32",
|
|
376
|
+
"on_page_content(): I32",
|
|
365
377
|
];
|
|
366
378
|
|
|
367
379
|
const perms = new Set(manifest.permissions || []);
|
|
@@ -373,6 +385,8 @@ function generateInterface(manifest) {
|
|
|
373
385
|
// Config is ambient too: a plugin reading its own manifest-declared config
|
|
374
386
|
// (admin override falling back to the declared default) needs no permission.
|
|
375
387
|
imports.push("owncast_config_get(keyPtr: PTR): PTR");
|
|
388
|
+
// Asset reading is ambient: a plugin reads only files it shipped itself.
|
|
389
|
+
imports.push("owncast_asset_read(pathPtr: PTR): PTR");
|
|
376
390
|
if (perms.has("chat.send")) {
|
|
377
391
|
imports.push("owncast_send_chat(textPtr: PTR): void");
|
|
378
392
|
imports.push("owncast_send_chat_action(textPtr: PTR): void");
|
package/index.d.ts
CHANGED
|
@@ -284,6 +284,15 @@ export interface OutgoingHttpResponse {
|
|
|
284
284
|
body?: string;
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
+
/** Request context passed to `onTabContent` and `onPageContent` handlers. */
|
|
288
|
+
export interface ContentRequest {
|
|
289
|
+
/** The tab or page-content slot's slug, as declared in the manifest. */
|
|
290
|
+
slug: string;
|
|
291
|
+
/** The viewing user's chat identity, when available. Undefined for
|
|
292
|
+
* anonymous viewers or when the host cannot resolve an identity. */
|
|
293
|
+
user?: ChatUser;
|
|
294
|
+
}
|
|
295
|
+
|
|
287
296
|
/** Payload for the sse.connect / sse.disconnect events. Fired when a browser
|
|
288
297
|
* opens or closes one of the plugin's `/plugins/<name>/_sse/<channel>`
|
|
289
298
|
* streams, so the plugin can track who is connected. `connectionId` is unique
|
|
@@ -353,6 +362,18 @@ export interface PluginDef {
|
|
|
353
362
|
* on `req.authenticated` yourself. Requires `http.serve` permission. */
|
|
354
363
|
onHttpRequest?(req: IncomingHttpRequest): OutgoingHttpResponse;
|
|
355
364
|
|
|
365
|
+
/** Render HTML for a dynamic tab. Called by the host when the tab was
|
|
366
|
+
* declared in the manifest without a static `content` file. Return the
|
|
367
|
+
* full HTML string to inline as the tab body. `req.user` is the viewer's
|
|
368
|
+
* chat identity when available, undefined for anonymous viewers. */
|
|
369
|
+
onTabContent?(req: ContentRequest): string;
|
|
370
|
+
|
|
371
|
+
/** Render HTML for the plugin's dynamic extraPageContent slot. Called by
|
|
372
|
+
* the host when extraPageContent was declared without a static `content`
|
|
373
|
+
* file. Return the full HTML string to inline into the viewer page.
|
|
374
|
+
* `req.user` is the viewer's chat identity when available. */
|
|
375
|
+
onPageContent?(req: ContentRequest): string;
|
|
376
|
+
|
|
356
377
|
/** Handlers for plugin-emitted custom events. The key is the event type
|
|
357
378
|
* string (e.g. "announcement.broadcast"). Notifications only, to filter
|
|
358
379
|
* custom events, additional API will be needed. */
|
|
@@ -523,6 +544,16 @@ export const owncast: {
|
|
|
523
544
|
* value. */
|
|
524
545
|
get<T = unknown>(key: string, fallback?: T): T;
|
|
525
546
|
};
|
|
547
|
+
/** Read files the plugin bundled in its own `assets/` directory — templates,
|
|
548
|
+
* 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. */
|
|
551
|
+
assets: {
|
|
552
|
+
/** Raw bytes of the file, or `null` if not found. */
|
|
553
|
+
read(path: string): Uint8Array | null;
|
|
554
|
+
/** File contents as a UTF-8 string, or `null` if not found. */
|
|
555
|
+
readText(path: string): string | null;
|
|
556
|
+
};
|
|
526
557
|
events: {
|
|
527
558
|
emit(eventType: string, payload: unknown): void;
|
|
528
559
|
};
|
package/index.js
CHANGED
|
@@ -770,6 +770,26 @@ const owncast = {
|
|
|
770
770
|
return JSON.parse(Memory.find(offset).readString());
|
|
771
771
|
},
|
|
772
772
|
},
|
|
773
|
+
// Read files the plugin shipped in its own assets/ directory. Useful for
|
|
774
|
+
// templates, data files, and other bundled resources that need to be read
|
|
775
|
+
// at request time. Path is relative to assets/ and must not contain "..".
|
|
776
|
+
// Ambient — no permission required.
|
|
777
|
+
assets: {
|
|
778
|
+
// Returns a Uint8Array of the file's raw bytes, or null if not found.
|
|
779
|
+
read(path) {
|
|
780
|
+
const fns = Host.getFunctions();
|
|
781
|
+
const offset = fns.owncast_asset_read(Memory.fromString(path).offset);
|
|
782
|
+
if (offset == 0) return null;
|
|
783
|
+
return new Uint8Array(Memory.find(offset).readBytes());
|
|
784
|
+
},
|
|
785
|
+
// Returns the file contents as a UTF-8 string, or null if not found.
|
|
786
|
+
readText(path) {
|
|
787
|
+
const fns = Host.getFunctions();
|
|
788
|
+
const offset = fns.owncast_asset_read(Memory.fromString(path).offset);
|
|
789
|
+
if (offset == 0) return null;
|
|
790
|
+
return Memory.find(offset).readString();
|
|
791
|
+
},
|
|
792
|
+
},
|
|
773
793
|
events: {
|
|
774
794
|
emit(eventType, payload) {
|
|
775
795
|
const fns = Host.getFunctions();
|
|
@@ -870,6 +890,16 @@ const owncast = {
|
|
|
870
890
|
},
|
|
871
891
|
};
|
|
872
892
|
|
|
893
|
+
function dispatchTabContent(req) {
|
|
894
|
+
if (!registered || !isFn(registered.onTabContent)) return "";
|
|
895
|
+
return registered.onTabContent(req) || "";
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function dispatchPageContent(req) {
|
|
899
|
+
if (!registered || !isFn(registered.onPageContent)) return "";
|
|
900
|
+
return registered.onPageContent(req) || "";
|
|
901
|
+
}
|
|
902
|
+
|
|
873
903
|
module.exports = {
|
|
874
904
|
definePlugin,
|
|
875
905
|
defineCommands,
|
|
@@ -882,4 +912,6 @@ module.exports = {
|
|
|
882
912
|
dispatchEvent,
|
|
883
913
|
dispatchFilter,
|
|
884
914
|
dispatchHttp,
|
|
915
|
+
dispatchTabContent,
|
|
916
|
+
dispatchPageContent,
|
|
885
917
|
};
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -21,21 +21,71 @@ const EXTISM_JS_VERSION = "v1.6.0";
|
|
|
21
21
|
const BINARYEN_VERSION = "version_119";
|
|
22
22
|
const HOST_BINARIES_REPO = "owncast/plugin-sdk";
|
|
23
23
|
|
|
24
|
-
// The host binaries (owncast-plugin-test/serve)
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
24
|
+
// The host binaries (owncast-plugin-test/serve) implement the host-function
|
|
25
|
+
// contract that the bundled JS runtime imports. That contract is additive
|
|
26
|
+
// within a major version — host functions are only ever added, never removed or
|
|
27
|
+
// renamed (a removal is a breaking change that requires a major bump) — so the
|
|
28
|
+
// NEWEST published binary is compatible with every plugin runtime. We therefore
|
|
29
|
+
// fetch the latest release tag rather than deriving one from the npm version.
|
|
30
|
+
//
|
|
31
|
+
// This keeps the binary in lockstep with `@owncast/plugin-sdk@^x` (which npm
|
|
32
|
+
// already floats to the newest compatible runtime) and fixes the old "zero the
|
|
33
|
+
// patch" guess: that fetched v<major>.<minor>.0, which 404'd on JS-only patches
|
|
34
|
+
// and — when a host change shipped in a patch (e.g. timer support in 0.4.2) —
|
|
35
|
+
// fetched a binary too old to satisfy the runtime's imports, breaking
|
|
36
|
+
// `npm test`.
|
|
37
|
+
//
|
|
38
|
+
// Override with OWNCAST_PLUGIN_HOST_BINARIES_VERSION (with or without a leading
|
|
39
|
+
// "v") to pin a specific tag, e.g. in CI or when bisecting.
|
|
40
|
+
function latestReleaseTag() {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
https
|
|
43
|
+
.get(
|
|
44
|
+
`https://api.github.com/repos/${HOST_BINARIES_REPO}/releases/latest`,
|
|
45
|
+
{
|
|
46
|
+
headers: {
|
|
47
|
+
"User-Agent": "owncast-plugin-sdk-postinstall",
|
|
48
|
+
Accept: "application/vnd.github+json",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
(res) => {
|
|
52
|
+
if (res.statusCode !== 200) {
|
|
53
|
+
res.resume();
|
|
54
|
+
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
55
|
+
}
|
|
56
|
+
let body = "";
|
|
57
|
+
res.on("data", (c) => (body += c));
|
|
58
|
+
res.on("end", () => {
|
|
59
|
+
try {
|
|
60
|
+
const tag = JSON.parse(body).tag_name;
|
|
61
|
+
if (!tag) return reject(new Error("no tag_name in response"));
|
|
62
|
+
resolve(tag);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
reject(err);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
.on("error", reject);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function resolveHostBinariesVersion() {
|
|
32
74
|
const override = process.env.OWNCAST_PLUGIN_HOST_BINARIES_VERSION;
|
|
33
|
-
if (override) return override.replace(/^v
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
75
|
+
if (override) return override.replace(/^v/i, "");
|
|
76
|
+
try {
|
|
77
|
+
return (await latestReleaseTag()).replace(/^v/i, "");
|
|
78
|
+
} catch (e) {
|
|
79
|
+
// Offline or API error: best-effort fall back to this package's own
|
|
80
|
+
// version. The download below 404-skips gracefully if no such release.
|
|
81
|
+
const pkg = require("../package.json").version;
|
|
82
|
+
console.warn(
|
|
83
|
+
`[plugin-sdk] could not resolve latest host-binary release ` +
|
|
84
|
+
`(${e.message}); falling back to v${pkg}`,
|
|
85
|
+
);
|
|
86
|
+
return pkg;
|
|
87
|
+
}
|
|
37
88
|
}
|
|
38
|
-
const HOST_BINARIES_VERSION = hostBinariesVersion();
|
|
39
89
|
|
|
40
90
|
const platform = process.platform;
|
|
41
91
|
const arch = process.arch;
|
|
@@ -71,7 +121,7 @@ function binaryenURL() {
|
|
|
71
121
|
return `https://github.com/WebAssembly/binaryen/releases/download/${BINARYEN_VERSION}/${file}`;
|
|
72
122
|
}
|
|
73
123
|
|
|
74
|
-
function hostBinaryURL(name) {
|
|
124
|
+
function hostBinaryURL(name, version) {
|
|
75
125
|
// Per-platform asset naming matches Go's GOOS-GOARCH convention so the
|
|
76
126
|
// release CI can `go build` once per matrix entry without renaming.
|
|
77
127
|
const map = {
|
|
@@ -81,7 +131,7 @@ function hostBinaryURL(name) {
|
|
|
81
131
|
"darwin-arm64": "darwin-arm64",
|
|
82
132
|
};
|
|
83
133
|
const suffix = map[platformKey()];
|
|
84
|
-
return `https://github.com/${HOST_BINARIES_REPO}/releases/download/v${
|
|
134
|
+
return `https://github.com/${HOST_BINARIES_REPO}/releases/download/v${version}/${name}-${suffix}`;
|
|
85
135
|
}
|
|
86
136
|
|
|
87
137
|
function download(url, dest) {
|
|
@@ -143,27 +193,33 @@ async function main() {
|
|
|
143
193
|
// if the release doesn't exist yet (dev environments running against a
|
|
144
194
|
// not-yet-released SDK version can substitute their own via
|
|
145
195
|
// tools/bootstrap.sh).
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
196
|
+
const hostBinaries = ["owncast-plugin-test", "owncast-plugin-serve"];
|
|
197
|
+
const missing = hostBinaries.filter(
|
|
198
|
+
(b) => !fs.existsSync(path.join(cacheDir, b)),
|
|
199
|
+
);
|
|
200
|
+
if (missing.length) {
|
|
201
|
+
// Resolve the version only when something needs downloading, so a repeat
|
|
202
|
+
// install with a populated cache never hits the network.
|
|
203
|
+
const version = await resolveHostBinariesVersion();
|
|
204
|
+
for (const binary of missing) {
|
|
205
|
+
const dest = path.join(cacheDir, binary);
|
|
206
|
+
const gz = dest + ".gz";
|
|
207
|
+
try {
|
|
208
|
+
console.log(`[plugin-sdk] downloading ${binary} v${version}...`);
|
|
209
|
+
await download(hostBinaryURL(binary, version) + ".gz", gz);
|
|
210
|
+
fs.writeFileSync(dest, zlib.gunzipSync(fs.readFileSync(gz)));
|
|
211
|
+
fs.chmodSync(dest, 0o755);
|
|
212
|
+
fs.unlinkSync(gz);
|
|
213
|
+
} catch (e) {
|
|
214
|
+
// 404 is expected before the first release; other errors get a soft
|
|
215
|
+
// warning so the user sees them but the install still succeeds.
|
|
216
|
+
console.warn(
|
|
217
|
+
`[plugin-sdk] could not fetch ${binary}: ${e.message}\n` +
|
|
218
|
+
` Build locally via tools/bootstrap.sh, or use the latest GitHub release.`,
|
|
219
|
+
);
|
|
220
|
+
// Make sure no partial files are left behind.
|
|
221
|
+
for (const p of [gz, dest]) if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
222
|
+
}
|
|
167
223
|
}
|
|
168
224
|
}
|
|
169
225
|
|