@lumiastream/ui 0.6.1 → 0.6.2
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/dist/index.js +63 -33
- package/dist/se-import.d.ts +2 -2
- package/dist/se-import.js +63 -33
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -4901,6 +4901,29 @@ function listenersToLumiaEvents(listeners, listener) {
|
|
|
4901
4901
|
}
|
|
4902
4902
|
return [...out];
|
|
4903
4903
|
}
|
|
4904
|
+
var SE_FIRST_PARTY_SCRIPT_RX = /^(?:https?:)?\/\/(?:[^/]+\.)?(?:streamelements\.com|lumiastream\.com)\//i;
|
|
4905
|
+
var EXTERNAL_SCRIPT_SRC_RX = /<script\b[^>]*\bsrc\s*=\s*["']([^"']+)["']/gi;
|
|
4906
|
+
var CHAT_COMMAND_VALUE_RX = /^\s*!\S+/;
|
|
4907
|
+
function hasExternalScriptLoader(html, js) {
|
|
4908
|
+
const haystack = `${html ?? ""}
|
|
4909
|
+
${js ?? ""}`;
|
|
4910
|
+
EXTERNAL_SCRIPT_SRC_RX.lastIndex = 0;
|
|
4911
|
+
for (const m of haystack.matchAll(EXTERNAL_SCRIPT_SRC_RX)) {
|
|
4912
|
+
const src = m[1]?.trim();
|
|
4913
|
+
if (!src) continue;
|
|
4914
|
+
if (src.startsWith("/") && !src.startsWith("//")) continue;
|
|
4915
|
+
if (SE_FIRST_PARTY_SCRIPT_RX.test(src)) continue;
|
|
4916
|
+
return true;
|
|
4917
|
+
}
|
|
4918
|
+
return false;
|
|
4919
|
+
}
|
|
4920
|
+
function hasChatCommandFieldValue(fieldData) {
|
|
4921
|
+
if (!fieldData) return false;
|
|
4922
|
+
for (const v of Object.values(fieldData)) {
|
|
4923
|
+
if (typeof v === "string" && CHAT_COMMAND_VALUE_RX.test(v)) return true;
|
|
4924
|
+
}
|
|
4925
|
+
return false;
|
|
4926
|
+
}
|
|
4904
4927
|
function translateConfigs(sourceFields) {
|
|
4905
4928
|
if (!sourceFields) return [];
|
|
4906
4929
|
return Object.entries(sourceFields).filter(([key]) => !RESERVED_FIELD_KEYS.has(key)).map(([key, f]) => {
|
|
@@ -4953,6 +4976,13 @@ function mapCustom(widget, ctx) {
|
|
|
4953
4976
|
widget.listeners,
|
|
4954
4977
|
widget.listener
|
|
4955
4978
|
);
|
|
4979
|
+
const externalLoader = hasExternalScriptLoader(v.html, v.js);
|
|
4980
|
+
const wantsChat = externalLoader || hasChatCommandFieldValue(fieldData);
|
|
4981
|
+
if (externalLoader) {
|
|
4982
|
+
if (!eventsList.includes("alert")) eventsList.push("alert");
|
|
4983
|
+
if (!eventsList.includes("alert.trigger")) eventsList.push("alert.trigger");
|
|
4984
|
+
}
|
|
4985
|
+
if (wantsChat && !eventsList.includes("chat")) eventsList.push("chat");
|
|
4956
4986
|
const data = { ...fieldData ?? {} };
|
|
4957
4987
|
if (eventsList.length > 0) data.events = eventsList;
|
|
4958
4988
|
return buildUnit(
|
|
@@ -7080,7 +7110,7 @@ async function mirrorOneAsset(url, upload, signal, proxyAssetFetch) {
|
|
|
7080
7110
|
const blob = await fetchAssetBlob(url, signal, proxyAssetFetch);
|
|
7081
7111
|
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
7082
7112
|
if (blob.size > MAX_MIRROR_ASSET_BYTES) {
|
|
7083
|
-
throw new Error(`Asset is ${(blob.size / (1024 * 1024)).toFixed(1)} MB
|
|
7113
|
+
throw new Error(`Asset is ${(blob.size / (1024 * 1024)).toFixed(1)} MB, over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
|
|
7084
7114
|
}
|
|
7085
7115
|
const filename = filenameFromURL(url);
|
|
7086
7116
|
const type = blob.type || guessMime(filename);
|
|
@@ -7102,7 +7132,7 @@ async function fetchAssetBlob(url, signal, proxyAssetFetch) {
|
|
|
7102
7132
|
if (!res.ok) throw new Error(`Source returned HTTP ${res.status}`);
|
|
7103
7133
|
const declared = Number(res.headers.get("content-length"));
|
|
7104
7134
|
if (Number.isFinite(declared) && declared > MAX_MIRROR_ASSET_BYTES) {
|
|
7105
|
-
throw new Error(`Asset is ${(declared / (1024 * 1024)).toFixed(1)} MB
|
|
7135
|
+
throw new Error(`Asset is ${(declared / (1024 * 1024)).toFixed(1)} MB, over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
|
|
7106
7136
|
}
|
|
7107
7137
|
return res.blob();
|
|
7108
7138
|
}
|
|
@@ -7342,7 +7372,7 @@ function decodeJwtPayload(jwt) {
|
|
|
7342
7372
|
}
|
|
7343
7373
|
const claims = parsed;
|
|
7344
7374
|
if (typeof claims.channel !== "string" || typeof claims.authToken !== "string") {
|
|
7345
|
-
throw new SEAuthError("invalid-jwt", "Token payload is missing the 'channel' or 'authToken' claim
|
|
7375
|
+
throw new SEAuthError("invalid-jwt", "Token payload is missing the 'channel' or 'authToken' claim. not a StreamElements account token.");
|
|
7346
7376
|
}
|
|
7347
7377
|
if (typeof claims.exp === "number" && claims.exp * 1e3 < Date.now()) {
|
|
7348
7378
|
throw new SEAuthError("expired", "This StreamElements token has expired. Generate a new one on the SE Account page.");
|
|
@@ -7351,7 +7381,7 @@ function decodeJwtPayload(jwt) {
|
|
|
7351
7381
|
}
|
|
7352
7382
|
var SEClient = class _SEClient {
|
|
7353
7383
|
// `jwt` is held alongside the decoded claims so the client can switch auth
|
|
7354
|
-
// modes without re-parsing. Defaults to `bearer-jwt
|
|
7384
|
+
// modes without re-parsing. Defaults to `bearer-jwt`. confirmed against
|
|
7355
7385
|
// the user's diagnostic 2026-05-16: every endpoint the existing 5 helpers
|
|
7356
7386
|
// hit (users/current, channels/me, overlays, bot/commands, plus
|
|
7357
7387
|
// sessions/loyalty/bits leaderboard transitively) returns 200, AND
|
|
@@ -7439,7 +7469,7 @@ var SEClient = class _SEClient {
|
|
|
7439
7469
|
let detail = "";
|
|
7440
7470
|
try {
|
|
7441
7471
|
const body = await res.json();
|
|
7442
|
-
if (body?.message) detail = `
|
|
7472
|
+
if (body?.message) detail = ` - ${body.message}`;
|
|
7443
7473
|
} catch {
|
|
7444
7474
|
}
|
|
7445
7475
|
throw new Error(`StreamElements API returned HTTP ${res.status}${detail}`);
|
|
@@ -7481,7 +7511,7 @@ async function fetchElementWidgetInstancePublic(channelId, widgetInstanceId, pub
|
|
|
7481
7511
|
let detail = "";
|
|
7482
7512
|
try {
|
|
7483
7513
|
const body = JSON.parse(await result.errorBody());
|
|
7484
|
-
if (body?.message) detail = `
|
|
7514
|
+
if (body?.message) detail = ` - ${body.message}`;
|
|
7485
7515
|
} catch {
|
|
7486
7516
|
}
|
|
7487
7517
|
const reason = result.status === 401 || result.status === 403 ? "StreamElements rejected the widget share token. The share link may have been revoked or made private." : result.status === 404 ? "Element widget not found. Double-check the share URL." : `HTTP ${result.status}`;
|
|
@@ -8489,15 +8519,15 @@ var PLACEHOLDER_WIDGET_TYPES = /* @__PURE__ */ new Set([
|
|
|
8489
8519
|
"streamlabels"
|
|
8490
8520
|
]);
|
|
8491
8521
|
var PLACEHOLDER_REASONS = {
|
|
8492
|
-
donation_ticker: "No native Lumia equivalent yet
|
|
8493
|
-
stream_boss: "Lumia has a native streamboss module
|
|
8494
|
-
credits: "Lumia has a native credits module
|
|
8495
|
-
spin_wheel: "Lumia has a native spinwheel module
|
|
8496
|
-
emote_wall: "Lumia has a native emotebox module
|
|
8497
|
-
sub_train: "No native Lumia equivalent yet
|
|
8522
|
+
donation_ticker: "No native Lumia equivalent yet. imported as a placeholder text layer.",
|
|
8523
|
+
stream_boss: "Lumia has a native streamboss module. set it up directly rather than importing the SL config.",
|
|
8524
|
+
credits: "Lumia has a native credits module. set it up directly rather than importing the SL config.",
|
|
8525
|
+
spin_wheel: "Lumia has a native spinwheel module. set it up directly rather than importing the SL config.",
|
|
8526
|
+
emote_wall: "Lumia has a native emotebox module. set it up directly rather than importing the SL config.",
|
|
8527
|
+
sub_train: "No native Lumia equivalent yet. imported as a placeholder text layer.",
|
|
8498
8528
|
streamlabels: "Use Lumia template variables (e.g. {{twitch_last_follower}}) inside a Text module instead of importing the SL streamlabels config."
|
|
8499
8529
|
};
|
|
8500
|
-
var CUSTOM_HTML_REVIEW_REASON = "The Streamlabs widget used custom HTML/CSS/JS that Lumia cannot host. The native Lumia module is the right starting point
|
|
8530
|
+
var CUSTOM_HTML_REVIEW_REASON = "The Streamlabs widget used custom HTML/CSS/JS that Lumia cannot host. The native Lumia module is the right starting point. recreate the styling there. The original SL settings are stashed under module.content.importMeta for reference.";
|
|
8501
8531
|
function stampImportMeta(module, widgetType, role, extra = {}) {
|
|
8502
8532
|
stampImportMetaMerge(module, {
|
|
8503
8533
|
source: "streamlabs",
|
|
@@ -8655,7 +8685,7 @@ function importStreamlabsWidget(response, widgetType, options = {}) {
|
|
|
8655
8685
|
});
|
|
8656
8686
|
reviewItems.push({
|
|
8657
8687
|
moduleId: placeholder.module.id,
|
|
8658
|
-
// SL doesn't have a single "widget" object the way SE does
|
|
8688
|
+
// SL doesn't have a single "widget" object the way SE does. we
|
|
8659
8689
|
// stash the settings snapshot here so the review step can show
|
|
8660
8690
|
// the raw config the user imported. Cast through unknown because
|
|
8661
8691
|
// ReviewItem is currently typed against SEWidget; widening the
|
|
@@ -8673,7 +8703,7 @@ function importStreamlabsWidget(response, widgetType, options = {}) {
|
|
|
8673
8703
|
layers.push(placeholder.layer);
|
|
8674
8704
|
modules[placeholder.layer.id] = placeholder.module;
|
|
8675
8705
|
coverage.notes.push(
|
|
8676
|
-
`Unknown Streamlabs widget type "${widgetType}"
|
|
8706
|
+
`Unknown Streamlabs widget type "${widgetType}". imported as a placeholder.`
|
|
8677
8707
|
);
|
|
8678
8708
|
coverage.mappings.push({
|
|
8679
8709
|
seType: `streamlabs:${widgetType}`,
|
|
@@ -9086,7 +9116,7 @@ function StepHeader({
|
|
|
9086
9116
|
] });
|
|
9087
9117
|
}
|
|
9088
9118
|
var JWT_FEATURES = [
|
|
9089
|
-
"All your overlays
|
|
9119
|
+
"All your overlays: pick one, many, or all",
|
|
9090
9120
|
"Live counters (follower / sub / tip / bits goals)",
|
|
9091
9121
|
"Chat bot commands",
|
|
9092
9122
|
"Loyalty points + watch time",
|
|
@@ -9240,7 +9270,7 @@ function StepModePicker({
|
|
|
9240
9270
|
}
|
|
9241
9271
|
)
|
|
9242
9272
|
] }),
|
|
9243
|
-
caveat: "Requires pasting your StreamElements JWT. Stored only in memory
|
|
9273
|
+
caveat: "Requires pasting your StreamElements JWT. Stored only in memory. Never sent to Lumia.",
|
|
9244
9274
|
onClick: onChooseJwt
|
|
9245
9275
|
}
|
|
9246
9276
|
)
|
|
@@ -9265,7 +9295,7 @@ function StepConnect({
|
|
|
9265
9295
|
{
|
|
9266
9296
|
number: 1,
|
|
9267
9297
|
title: "Connect your StreamElements account",
|
|
9268
|
-
subtitle: "Paste your StreamElements JWT to import overlays, live counters, chat commands, loyalty points, watch time, and recent events. The token is read in your browser only
|
|
9298
|
+
subtitle: "Paste your StreamElements JWT to import overlays, live counters, chat commands, loyalty points, watch time, and recent events. The token is read in your browser only. Lumia never sends it to our servers."
|
|
9269
9299
|
}
|
|
9270
9300
|
),
|
|
9271
9301
|
/* @__PURE__ */ jsx14(
|
|
@@ -11914,15 +11944,15 @@ function importElementOverlay(response, options = {}) {
|
|
|
11914
11944
|
// src/se-import/index.ts
|
|
11915
11945
|
var IMPORT_META_KEY2 = "importMeta";
|
|
11916
11946
|
var REVIEW_REASONS = {
|
|
11917
|
-
"se-widget-bit-boss": "No native Lumia equivalent
|
|
11947
|
+
"se-widget-bit-boss": "No native Lumia equivalent. bit boss fight bar with HP/damage states.",
|
|
11918
11948
|
// `se-widget-hype-cup` intentionally omitted — it now routes to the native
|
|
11919
11949
|
// `tipjar` module (see dispatcher.ts) and lands with `partial` status, so it
|
|
11920
11950
|
// never reaches the placeholder-reason fallback. If a future feature flag
|
|
11921
11951
|
// disables tipjar auto-import, add an entry to FLAG_OFF_REASONS instead.
|
|
11922
|
-
"se-widget-media-share": "No native Lumia equivalent
|
|
11923
|
-
"se-widget-contest": "No native Lumia equivalent
|
|
11924
|
-
"se-widget-giveaway": "No native Lumia equivalent
|
|
11925
|
-
"se-widget-botcounter": "No native Lumia equivalent
|
|
11952
|
+
"se-widget-media-share": "No native Lumia equivalent. viewer-submitted media player.",
|
|
11953
|
+
"se-widget-contest": "No native Lumia equivalent. SE-specific contest widget.",
|
|
11954
|
+
"se-widget-giveaway": "No native Lumia equivalent. SE giveaway service.",
|
|
11955
|
+
"se-widget-botcounter": "No native Lumia equivalent. chatter/message counter.",
|
|
11926
11956
|
"se-widget-top-cheerers-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render cheer rankings.',
|
|
11927
11957
|
"se-widget-top-gifters-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render gifter rankings.',
|
|
11928
11958
|
"se-widget-top-tippers-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render tipper rankings.',
|
|
@@ -11932,7 +11962,7 @@ var REVIEW_REASONS = {
|
|
|
11932
11962
|
// so we can't pre-populate the slideshow items. The streamer adds their
|
|
11933
11963
|
// product images by hand in the slideshow editor. Status = 'partial' (see
|
|
11934
11964
|
// dispatcher.ts), so this string surfaces in the review step.
|
|
11935
|
-
"se-widget-merch-products-rotator": "Imported as an empty slideshow. Lumia doesn't fetch your Fourthwall product catalog yet, so add your product images manually in the slideshow editor
|
|
11965
|
+
"se-widget-merch-products-rotator": "Imported as an empty slideshow. Lumia doesn't fetch your Fourthwall product catalog yet, so add your product images manually in the slideshow editor. the layer is pre-positioned and styled to match your SE widget."
|
|
11936
11966
|
// Seasonal widgets (snow / halloween / halloween-2019 / xmas / valentine / easter
|
|
11937
11967
|
// / world-cup) intentionally omitted — they route directly to image/slideshow/
|
|
11938
11968
|
// video modules now (see dispatcher.ts SEASONAL_IMAGE_TYPES), so they always
|
|
@@ -11940,7 +11970,7 @@ var REVIEW_REASONS = {
|
|
|
11940
11970
|
};
|
|
11941
11971
|
function reasonFor(seType, status, flaggedOff) {
|
|
11942
11972
|
if (flaggedOff && FLAG_OFF_REASONS[seType]) return FLAG_OFF_REASONS[seType];
|
|
11943
|
-
return REVIEW_REASONS[seType] ?? (status === "template" ? "Imported as a generic template stub." : "No native mapping yet
|
|
11973
|
+
return REVIEW_REASONS[seType] ?? (status === "template" ? "Imported as a generic template stub." : "No native mapping yet. imported as a labelled text placeholder.");
|
|
11944
11974
|
}
|
|
11945
11975
|
function buildSEBootstrapDescription(overlayId, sourceUrl) {
|
|
11946
11976
|
const head = overlayId ? `Imported from StreamElements overlay ${overlayId}` : "Imported from StreamElements";
|
|
@@ -11970,7 +12000,7 @@ async function fetchSEBootstrap({
|
|
|
11970
12000
|
}) {
|
|
11971
12001
|
if (!apikey) {
|
|
11972
12002
|
throw new Error(
|
|
11973
|
-
"Missing access token. Use the public preview URL (Overlay menu \u2192 Get URL \u2192 Public link)
|
|
12003
|
+
"Missing access token. Use the public preview URL (Overlay menu \u2192 Get URL \u2192 Public link). editor URLs do not include a token."
|
|
11974
12004
|
);
|
|
11975
12005
|
}
|
|
11976
12006
|
const result = await fetchWithProxyFallback(buildBootstrapUrl(overlayId), {
|
|
@@ -11981,7 +12011,7 @@ async function fetchSEBootstrap({
|
|
|
11981
12011
|
let detail = "";
|
|
11982
12012
|
try {
|
|
11983
12013
|
const body = JSON.parse(await result.errorBody());
|
|
11984
|
-
detail = body?.message ?
|
|
12014
|
+
detail = body?.message ? `. ${body.message}` : "";
|
|
11985
12015
|
} catch {
|
|
11986
12016
|
}
|
|
11987
12017
|
const reason = result.status === 401 || result.status === 403 ? "StreamElements rejected the URL token. The preview link may have been revoked." : result.status === 404 ? "Overlay not found. Double-check the URL." : `HTTP ${result.status}`;
|
|
@@ -12123,17 +12153,17 @@ function importSEBootstrap(bootstrap, options = {}) {
|
|
|
12123
12153
|
}
|
|
12124
12154
|
if (coverage.mappings.some((m) => m.status === "placeholder")) {
|
|
12125
12155
|
coverage.notes.push(
|
|
12126
|
-
"Some widgets imported as placeholder text layers
|
|
12156
|
+
"Some widgets imported as placeholder text layers. see SE/05-widget-to-module-mapping.md for status."
|
|
12127
12157
|
);
|
|
12128
12158
|
}
|
|
12129
12159
|
if (coverage.mappings.some((m) => m.seType === "se-widget-merch-goal")) {
|
|
12130
12160
|
coverage.notes.push(
|
|
12131
|
-
"Merch-goal widgets imported as generic goals
|
|
12161
|
+
"Merch-goal widgets imported as generic goals. see SE/03-missing-variables.md \xA78 for the missing data source."
|
|
12132
12162
|
);
|
|
12133
12163
|
}
|
|
12134
12164
|
if (coverage.mappings.some((m) => m.seType === "se-widget-custom-event-list")) {
|
|
12135
12165
|
coverage.notes.push(
|
|
12136
|
-
"\u26A0 Custom widgets are experimental
|
|
12166
|
+
"\u26A0 Custom widgets are experimental. they run under the SE compatibility shim (Embed.tsx) which handles most SE APIs but may not cover every edge case. Verify each custom layer renders correctly in the editor."
|
|
12137
12167
|
);
|
|
12138
12168
|
}
|
|
12139
12169
|
const overlay = {
|
|
@@ -12278,7 +12308,7 @@ function buildAIPromptForSEWidget(widget, canvas) {
|
|
|
12278
12308
|
const fullVariables = JSON.stringify(variables, null, 2);
|
|
12279
12309
|
const truncated = fullVariables.length > VARIABLES_BUDGET;
|
|
12280
12310
|
const variablesJson = truncated ? `${fullVariables.slice(0, VARIABLES_BUDGET)}
|
|
12281
|
-
\u2026[truncated
|
|
12311
|
+
\u2026[truncated. ${fullVariables.length - VARIABLES_BUDGET} more bytes elided]` : fullVariables;
|
|
12282
12312
|
return [
|
|
12283
12313
|
"Recreate a StreamElements overlay widget as a Lumia Stream custom overlay (HTML + CSS + JS).",
|
|
12284
12314
|
"",
|
|
@@ -12286,14 +12316,14 @@ function buildAIPromptForSEWidget(widget, canvas) {
|
|
|
12286
12316
|
listener ? `Primary event listener: ${listener}` : "",
|
|
12287
12317
|
listeners ? `Subscribes to events: ${listeners.join(", ")}` : "",
|
|
12288
12318
|
`Canvas: ${landing.width} \xD7 ${landing.height}`,
|
|
12289
|
-
truncated ? "Note: the widget config below is truncated
|
|
12319
|
+
truncated ? "Note: the widget config below is truncated. focus on visible/structural fields and ignore tail content you cannot see." : "",
|
|
12290
12320
|
"",
|
|
12291
12321
|
"Widget configuration (JSON):",
|
|
12292
12322
|
"```json",
|
|
12293
12323
|
variablesJson,
|
|
12294
12324
|
"```",
|
|
12295
12325
|
"",
|
|
12296
|
-
"Constraints
|
|
12326
|
+
"Constraints. your output must run inside a Lumia Custom Overlay sandboxed iframe:",
|
|
12297
12327
|
'- Use `window.Overlay.on("alert", handler)` / `window.Overlay.on("chat", handler)` to receive Lumia events.',
|
|
12298
12328
|
"- Lumia template variables use double-brace syntax: {{username}}, {{amount}}, {{message}}, {{currency}}.",
|
|
12299
12329
|
"- The `window.DATA` object holds user-configurable values (mirrors your `configs`).",
|
package/dist/se-import.d.ts
CHANGED
|
@@ -183,7 +183,7 @@ declare function fetchWithProxyFallback(url: string, options?: FetchWithProxyOpt
|
|
|
183
183
|
|
|
184
184
|
/**
|
|
185
185
|
* Distinct importable CDN URLs referenced anywhere inside the overlay's
|
|
186
|
-
* settings. Covers both StreamElements and Streamlabs source hosts
|
|
186
|
+
* settings. Covers both StreamElements and Streamlabs source hosts, see
|
|
187
187
|
* IMPORTABLE_CDN_REGEX above for the full host list.
|
|
188
188
|
*
|
|
189
189
|
* Name kept as `findSEAssetURLs` for back-compat with existing callers; the
|
|
@@ -211,7 +211,7 @@ interface AssetMirrorProgress {
|
|
|
211
211
|
*
|
|
212
212
|
* The host call site is responsible for retries and progress reporting;
|
|
213
213
|
* this function is intentionally single-shot. Pass `signal` (AbortSignal)
|
|
214
|
-
* to abort the source-side `fetch` when the modal closes mid-mirror
|
|
214
|
+
* to abort the source-side `fetch` when the modal closes mid-mirror, the
|
|
215
215
|
* upload itself is consumer-defined and the caller decides whether/how to
|
|
216
216
|
* short-circuit it. The proxy path doesn't take a signal because the
|
|
217
217
|
* host-provided fetch shape varies.
|
package/dist/se-import.js
CHANGED
|
@@ -1821,6 +1821,29 @@ function listenersToLumiaEvents(listeners, listener) {
|
|
|
1821
1821
|
}
|
|
1822
1822
|
return [...out];
|
|
1823
1823
|
}
|
|
1824
|
+
var SE_FIRST_PARTY_SCRIPT_RX = /^(?:https?:)?\/\/(?:[^/]+\.)?(?:streamelements\.com|lumiastream\.com)\//i;
|
|
1825
|
+
var EXTERNAL_SCRIPT_SRC_RX = /<script\b[^>]*\bsrc\s*=\s*["']([^"']+)["']/gi;
|
|
1826
|
+
var CHAT_COMMAND_VALUE_RX = /^\s*!\S+/;
|
|
1827
|
+
function hasExternalScriptLoader(html, js) {
|
|
1828
|
+
const haystack = `${html ?? ""}
|
|
1829
|
+
${js ?? ""}`;
|
|
1830
|
+
EXTERNAL_SCRIPT_SRC_RX.lastIndex = 0;
|
|
1831
|
+
for (const m of haystack.matchAll(EXTERNAL_SCRIPT_SRC_RX)) {
|
|
1832
|
+
const src = m[1]?.trim();
|
|
1833
|
+
if (!src) continue;
|
|
1834
|
+
if (src.startsWith("/") && !src.startsWith("//")) continue;
|
|
1835
|
+
if (SE_FIRST_PARTY_SCRIPT_RX.test(src)) continue;
|
|
1836
|
+
return true;
|
|
1837
|
+
}
|
|
1838
|
+
return false;
|
|
1839
|
+
}
|
|
1840
|
+
function hasChatCommandFieldValue(fieldData) {
|
|
1841
|
+
if (!fieldData) return false;
|
|
1842
|
+
for (const v of Object.values(fieldData)) {
|
|
1843
|
+
if (typeof v === "string" && CHAT_COMMAND_VALUE_RX.test(v)) return true;
|
|
1844
|
+
}
|
|
1845
|
+
return false;
|
|
1846
|
+
}
|
|
1824
1847
|
function translateConfigs(sourceFields) {
|
|
1825
1848
|
if (!sourceFields) return [];
|
|
1826
1849
|
return Object.entries(sourceFields).filter(([key]) => !RESERVED_FIELD_KEYS.has(key)).map(([key, f]) => {
|
|
@@ -1873,6 +1896,13 @@ function mapCustom(widget, ctx) {
|
|
|
1873
1896
|
widget.listeners,
|
|
1874
1897
|
widget.listener
|
|
1875
1898
|
);
|
|
1899
|
+
const externalLoader = hasExternalScriptLoader(v.html, v.js);
|
|
1900
|
+
const wantsChat = externalLoader || hasChatCommandFieldValue(fieldData);
|
|
1901
|
+
if (externalLoader) {
|
|
1902
|
+
if (!eventsList.includes("alert")) eventsList.push("alert");
|
|
1903
|
+
if (!eventsList.includes("alert.trigger")) eventsList.push("alert.trigger");
|
|
1904
|
+
}
|
|
1905
|
+
if (wantsChat && !eventsList.includes("chat")) eventsList.push("chat");
|
|
1876
1906
|
const data = { ...fieldData ?? {} };
|
|
1877
1907
|
if (eventsList.length > 0) data.events = eventsList;
|
|
1878
1908
|
return buildUnit(
|
|
@@ -4000,7 +4030,7 @@ async function mirrorOneAsset(url, upload, signal, proxyAssetFetch) {
|
|
|
4000
4030
|
const blob = await fetchAssetBlob(url, signal, proxyAssetFetch);
|
|
4001
4031
|
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
4002
4032
|
if (blob.size > MAX_MIRROR_ASSET_BYTES) {
|
|
4003
|
-
throw new Error(`Asset is ${(blob.size / (1024 * 1024)).toFixed(1)} MB
|
|
4033
|
+
throw new Error(`Asset is ${(blob.size / (1024 * 1024)).toFixed(1)} MB, over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
|
|
4004
4034
|
}
|
|
4005
4035
|
const filename = filenameFromURL(url);
|
|
4006
4036
|
const type = blob.type || guessMime(filename);
|
|
@@ -4022,7 +4052,7 @@ async function fetchAssetBlob(url, signal, proxyAssetFetch) {
|
|
|
4022
4052
|
if (!res.ok) throw new Error(`Source returned HTTP ${res.status}`);
|
|
4023
4053
|
const declared = Number(res.headers.get("content-length"));
|
|
4024
4054
|
if (Number.isFinite(declared) && declared > MAX_MIRROR_ASSET_BYTES) {
|
|
4025
|
-
throw new Error(`Asset is ${(declared / (1024 * 1024)).toFixed(1)} MB
|
|
4055
|
+
throw new Error(`Asset is ${(declared / (1024 * 1024)).toFixed(1)} MB, over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
|
|
4026
4056
|
}
|
|
4027
4057
|
return res.blob();
|
|
4028
4058
|
}
|
|
@@ -4262,7 +4292,7 @@ function decodeJwtPayload(jwt) {
|
|
|
4262
4292
|
}
|
|
4263
4293
|
const claims = parsed;
|
|
4264
4294
|
if (typeof claims.channel !== "string" || typeof claims.authToken !== "string") {
|
|
4265
|
-
throw new SEAuthError("invalid-jwt", "Token payload is missing the 'channel' or 'authToken' claim
|
|
4295
|
+
throw new SEAuthError("invalid-jwt", "Token payload is missing the 'channel' or 'authToken' claim. not a StreamElements account token.");
|
|
4266
4296
|
}
|
|
4267
4297
|
if (typeof claims.exp === "number" && claims.exp * 1e3 < Date.now()) {
|
|
4268
4298
|
throw new SEAuthError("expired", "This StreamElements token has expired. Generate a new one on the SE Account page.");
|
|
@@ -4271,7 +4301,7 @@ function decodeJwtPayload(jwt) {
|
|
|
4271
4301
|
}
|
|
4272
4302
|
var SEClient = class _SEClient {
|
|
4273
4303
|
// `jwt` is held alongside the decoded claims so the client can switch auth
|
|
4274
|
-
// modes without re-parsing. Defaults to `bearer-jwt
|
|
4304
|
+
// modes without re-parsing. Defaults to `bearer-jwt`. confirmed against
|
|
4275
4305
|
// the user's diagnostic 2026-05-16: every endpoint the existing 5 helpers
|
|
4276
4306
|
// hit (users/current, channels/me, overlays, bot/commands, plus
|
|
4277
4307
|
// sessions/loyalty/bits leaderboard transitively) returns 200, AND
|
|
@@ -4359,7 +4389,7 @@ var SEClient = class _SEClient {
|
|
|
4359
4389
|
let detail = "";
|
|
4360
4390
|
try {
|
|
4361
4391
|
const body = await res.json();
|
|
4362
|
-
if (body?.message) detail = `
|
|
4392
|
+
if (body?.message) detail = ` - ${body.message}`;
|
|
4363
4393
|
} catch {
|
|
4364
4394
|
}
|
|
4365
4395
|
throw new Error(`StreamElements API returned HTTP ${res.status}${detail}`);
|
|
@@ -4401,7 +4431,7 @@ async function fetchElementWidgetInstancePublic(channelId, widgetInstanceId, pub
|
|
|
4401
4431
|
let detail = "";
|
|
4402
4432
|
try {
|
|
4403
4433
|
const body = JSON.parse(await result.errorBody());
|
|
4404
|
-
if (body?.message) detail = `
|
|
4434
|
+
if (body?.message) detail = ` - ${body.message}`;
|
|
4405
4435
|
} catch {
|
|
4406
4436
|
}
|
|
4407
4437
|
const reason = result.status === 401 || result.status === 403 ? "StreamElements rejected the widget share token. The share link may have been revoked or made private." : result.status === 404 ? "Element widget not found. Double-check the share URL." : `HTTP ${result.status}`;
|
|
@@ -7343,15 +7373,15 @@ var PLACEHOLDER_WIDGET_TYPES = /* @__PURE__ */ new Set([
|
|
|
7343
7373
|
"streamlabels"
|
|
7344
7374
|
]);
|
|
7345
7375
|
var PLACEHOLDER_REASONS = {
|
|
7346
|
-
donation_ticker: "No native Lumia equivalent yet
|
|
7347
|
-
stream_boss: "Lumia has a native streamboss module
|
|
7348
|
-
credits: "Lumia has a native credits module
|
|
7349
|
-
spin_wheel: "Lumia has a native spinwheel module
|
|
7350
|
-
emote_wall: "Lumia has a native emotebox module
|
|
7351
|
-
sub_train: "No native Lumia equivalent yet
|
|
7376
|
+
donation_ticker: "No native Lumia equivalent yet. imported as a placeholder text layer.",
|
|
7377
|
+
stream_boss: "Lumia has a native streamboss module. set it up directly rather than importing the SL config.",
|
|
7378
|
+
credits: "Lumia has a native credits module. set it up directly rather than importing the SL config.",
|
|
7379
|
+
spin_wheel: "Lumia has a native spinwheel module. set it up directly rather than importing the SL config.",
|
|
7380
|
+
emote_wall: "Lumia has a native emotebox module. set it up directly rather than importing the SL config.",
|
|
7381
|
+
sub_train: "No native Lumia equivalent yet. imported as a placeholder text layer.",
|
|
7352
7382
|
streamlabels: "Use Lumia template variables (e.g. {{twitch_last_follower}}) inside a Text module instead of importing the SL streamlabels config."
|
|
7353
7383
|
};
|
|
7354
|
-
var CUSTOM_HTML_REVIEW_REASON = "The Streamlabs widget used custom HTML/CSS/JS that Lumia cannot host. The native Lumia module is the right starting point
|
|
7384
|
+
var CUSTOM_HTML_REVIEW_REASON = "The Streamlabs widget used custom HTML/CSS/JS that Lumia cannot host. The native Lumia module is the right starting point. recreate the styling there. The original SL settings are stashed under module.content.importMeta for reference.";
|
|
7355
7385
|
function stampImportMeta(module, widgetType, role, extra = {}) {
|
|
7356
7386
|
stampImportMetaMerge(module, {
|
|
7357
7387
|
source: "streamlabs",
|
|
@@ -7509,7 +7539,7 @@ function importStreamlabsWidget(response, widgetType, options = {}) {
|
|
|
7509
7539
|
});
|
|
7510
7540
|
reviewItems.push({
|
|
7511
7541
|
moduleId: placeholder.module.id,
|
|
7512
|
-
// SL doesn't have a single "widget" object the way SE does
|
|
7542
|
+
// SL doesn't have a single "widget" object the way SE does. we
|
|
7513
7543
|
// stash the settings snapshot here so the review step can show
|
|
7514
7544
|
// the raw config the user imported. Cast through unknown because
|
|
7515
7545
|
// ReviewItem is currently typed against SEWidget; widening the
|
|
@@ -7527,7 +7557,7 @@ function importStreamlabsWidget(response, widgetType, options = {}) {
|
|
|
7527
7557
|
layers.push(placeholder.layer);
|
|
7528
7558
|
modules[placeholder.layer.id] = placeholder.module;
|
|
7529
7559
|
coverage.notes.push(
|
|
7530
|
-
`Unknown Streamlabs widget type "${widgetType}"
|
|
7560
|
+
`Unknown Streamlabs widget type "${widgetType}". imported as a placeholder.`
|
|
7531
7561
|
);
|
|
7532
7562
|
coverage.mappings.push({
|
|
7533
7563
|
seType: `streamlabs:${widgetType}`,
|
|
@@ -7962,7 +7992,7 @@ function StepHeader({
|
|
|
7962
7992
|
] });
|
|
7963
7993
|
}
|
|
7964
7994
|
var JWT_FEATURES = [
|
|
7965
|
-
"All your overlays
|
|
7995
|
+
"All your overlays: pick one, many, or all",
|
|
7966
7996
|
"Live counters (follower / sub / tip / bits goals)",
|
|
7967
7997
|
"Chat bot commands",
|
|
7968
7998
|
"Loyalty points + watch time",
|
|
@@ -8116,7 +8146,7 @@ function StepModePicker({
|
|
|
8116
8146
|
}
|
|
8117
8147
|
)
|
|
8118
8148
|
] }),
|
|
8119
|
-
caveat: "Requires pasting your StreamElements JWT. Stored only in memory
|
|
8149
|
+
caveat: "Requires pasting your StreamElements JWT. Stored only in memory. Never sent to Lumia.",
|
|
8120
8150
|
onClick: onChooseJwt
|
|
8121
8151
|
}
|
|
8122
8152
|
)
|
|
@@ -8141,7 +8171,7 @@ function StepConnect({
|
|
|
8141
8171
|
{
|
|
8142
8172
|
number: 1,
|
|
8143
8173
|
title: "Connect your StreamElements account",
|
|
8144
|
-
subtitle: "Paste your StreamElements JWT to import overlays, live counters, chat commands, loyalty points, watch time, and recent events. The token is read in your browser only
|
|
8174
|
+
subtitle: "Paste your StreamElements JWT to import overlays, live counters, chat commands, loyalty points, watch time, and recent events. The token is read in your browser only. Lumia never sends it to our servers."
|
|
8145
8175
|
}
|
|
8146
8176
|
),
|
|
8147
8177
|
/* @__PURE__ */ jsx14(
|
|
@@ -10790,15 +10820,15 @@ function importElementOverlay(response, options = {}) {
|
|
|
10790
10820
|
// src/se-import/index.ts
|
|
10791
10821
|
var IMPORT_META_KEY2 = "importMeta";
|
|
10792
10822
|
var REVIEW_REASONS = {
|
|
10793
|
-
"se-widget-bit-boss": "No native Lumia equivalent
|
|
10823
|
+
"se-widget-bit-boss": "No native Lumia equivalent. bit boss fight bar with HP/damage states.",
|
|
10794
10824
|
// `se-widget-hype-cup` intentionally omitted — it now routes to the native
|
|
10795
10825
|
// `tipjar` module (see dispatcher.ts) and lands with `partial` status, so it
|
|
10796
10826
|
// never reaches the placeholder-reason fallback. If a future feature flag
|
|
10797
10827
|
// disables tipjar auto-import, add an entry to FLAG_OFF_REASONS instead.
|
|
10798
|
-
"se-widget-media-share": "No native Lumia equivalent
|
|
10799
|
-
"se-widget-contest": "No native Lumia equivalent
|
|
10800
|
-
"se-widget-giveaway": "No native Lumia equivalent
|
|
10801
|
-
"se-widget-botcounter": "No native Lumia equivalent
|
|
10828
|
+
"se-widget-media-share": "No native Lumia equivalent. viewer-submitted media player.",
|
|
10829
|
+
"se-widget-contest": "No native Lumia equivalent. SE-specific contest widget.",
|
|
10830
|
+
"se-widget-giveaway": "No native Lumia equivalent. SE giveaway service.",
|
|
10831
|
+
"se-widget-botcounter": "No native Lumia equivalent. chatter/message counter.",
|
|
10802
10832
|
"se-widget-top-cheerers-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render cheer rankings.',
|
|
10803
10833
|
"se-widget-top-gifters-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render gifter rankings.',
|
|
10804
10834
|
"se-widget-top-tippers-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render tipper rankings.',
|
|
@@ -10808,7 +10838,7 @@ var REVIEW_REASONS = {
|
|
|
10808
10838
|
// so we can't pre-populate the slideshow items. The streamer adds their
|
|
10809
10839
|
// product images by hand in the slideshow editor. Status = 'partial' (see
|
|
10810
10840
|
// dispatcher.ts), so this string surfaces in the review step.
|
|
10811
|
-
"se-widget-merch-products-rotator": "Imported as an empty slideshow. Lumia doesn't fetch your Fourthwall product catalog yet, so add your product images manually in the slideshow editor
|
|
10841
|
+
"se-widget-merch-products-rotator": "Imported as an empty slideshow. Lumia doesn't fetch your Fourthwall product catalog yet, so add your product images manually in the slideshow editor. the layer is pre-positioned and styled to match your SE widget."
|
|
10812
10842
|
// Seasonal widgets (snow / halloween / halloween-2019 / xmas / valentine / easter
|
|
10813
10843
|
// / world-cup) intentionally omitted — they route directly to image/slideshow/
|
|
10814
10844
|
// video modules now (see dispatcher.ts SEASONAL_IMAGE_TYPES), so they always
|
|
@@ -10816,7 +10846,7 @@ var REVIEW_REASONS = {
|
|
|
10816
10846
|
};
|
|
10817
10847
|
function reasonFor(seType, status, flaggedOff) {
|
|
10818
10848
|
if (flaggedOff && FLAG_OFF_REASONS[seType]) return FLAG_OFF_REASONS[seType];
|
|
10819
|
-
return REVIEW_REASONS[seType] ?? (status === "template" ? "Imported as a generic template stub." : "No native mapping yet
|
|
10849
|
+
return REVIEW_REASONS[seType] ?? (status === "template" ? "Imported as a generic template stub." : "No native mapping yet. imported as a labelled text placeholder.");
|
|
10820
10850
|
}
|
|
10821
10851
|
function buildSEBootstrapDescription(overlayId, sourceUrl) {
|
|
10822
10852
|
const head = overlayId ? `Imported from StreamElements overlay ${overlayId}` : "Imported from StreamElements";
|
|
@@ -10846,7 +10876,7 @@ async function fetchSEBootstrap({
|
|
|
10846
10876
|
}) {
|
|
10847
10877
|
if (!apikey) {
|
|
10848
10878
|
throw new Error(
|
|
10849
|
-
"Missing access token. Use the public preview URL (Overlay menu \u2192 Get URL \u2192 Public link)
|
|
10879
|
+
"Missing access token. Use the public preview URL (Overlay menu \u2192 Get URL \u2192 Public link). editor URLs do not include a token."
|
|
10850
10880
|
);
|
|
10851
10881
|
}
|
|
10852
10882
|
const result = await fetchWithProxyFallback(buildBootstrapUrl(overlayId), {
|
|
@@ -10857,7 +10887,7 @@ async function fetchSEBootstrap({
|
|
|
10857
10887
|
let detail = "";
|
|
10858
10888
|
try {
|
|
10859
10889
|
const body = JSON.parse(await result.errorBody());
|
|
10860
|
-
detail = body?.message ?
|
|
10890
|
+
detail = body?.message ? `. ${body.message}` : "";
|
|
10861
10891
|
} catch {
|
|
10862
10892
|
}
|
|
10863
10893
|
const reason = result.status === 401 || result.status === 403 ? "StreamElements rejected the URL token. The preview link may have been revoked." : result.status === 404 ? "Overlay not found. Double-check the URL." : `HTTP ${result.status}`;
|
|
@@ -10999,17 +11029,17 @@ function importSEBootstrap(bootstrap, options = {}) {
|
|
|
10999
11029
|
}
|
|
11000
11030
|
if (coverage.mappings.some((m) => m.status === "placeholder")) {
|
|
11001
11031
|
coverage.notes.push(
|
|
11002
|
-
"Some widgets imported as placeholder text layers
|
|
11032
|
+
"Some widgets imported as placeholder text layers. see SE/05-widget-to-module-mapping.md for status."
|
|
11003
11033
|
);
|
|
11004
11034
|
}
|
|
11005
11035
|
if (coverage.mappings.some((m) => m.seType === "se-widget-merch-goal")) {
|
|
11006
11036
|
coverage.notes.push(
|
|
11007
|
-
"Merch-goal widgets imported as generic goals
|
|
11037
|
+
"Merch-goal widgets imported as generic goals. see SE/03-missing-variables.md \xA78 for the missing data source."
|
|
11008
11038
|
);
|
|
11009
11039
|
}
|
|
11010
11040
|
if (coverage.mappings.some((m) => m.seType === "se-widget-custom-event-list")) {
|
|
11011
11041
|
coverage.notes.push(
|
|
11012
|
-
"\u26A0 Custom widgets are experimental
|
|
11042
|
+
"\u26A0 Custom widgets are experimental. they run under the SE compatibility shim (Embed.tsx) which handles most SE APIs but may not cover every edge case. Verify each custom layer renders correctly in the editor."
|
|
11013
11043
|
);
|
|
11014
11044
|
}
|
|
11015
11045
|
const overlay = {
|
|
@@ -11154,7 +11184,7 @@ function buildAIPromptForSEWidget(widget, canvas) {
|
|
|
11154
11184
|
const fullVariables = JSON.stringify(variables, null, 2);
|
|
11155
11185
|
const truncated = fullVariables.length > VARIABLES_BUDGET;
|
|
11156
11186
|
const variablesJson = truncated ? `${fullVariables.slice(0, VARIABLES_BUDGET)}
|
|
11157
|
-
\u2026[truncated
|
|
11187
|
+
\u2026[truncated. ${fullVariables.length - VARIABLES_BUDGET} more bytes elided]` : fullVariables;
|
|
11158
11188
|
return [
|
|
11159
11189
|
"Recreate a StreamElements overlay widget as a Lumia Stream custom overlay (HTML + CSS + JS).",
|
|
11160
11190
|
"",
|
|
@@ -11162,14 +11192,14 @@ function buildAIPromptForSEWidget(widget, canvas) {
|
|
|
11162
11192
|
listener ? `Primary event listener: ${listener}` : "",
|
|
11163
11193
|
listeners ? `Subscribes to events: ${listeners.join(", ")}` : "",
|
|
11164
11194
|
`Canvas: ${landing.width} \xD7 ${landing.height}`,
|
|
11165
|
-
truncated ? "Note: the widget config below is truncated
|
|
11195
|
+
truncated ? "Note: the widget config below is truncated. focus on visible/structural fields and ignore tail content you cannot see." : "",
|
|
11166
11196
|
"",
|
|
11167
11197
|
"Widget configuration (JSON):",
|
|
11168
11198
|
"```json",
|
|
11169
11199
|
variablesJson,
|
|
11170
11200
|
"```",
|
|
11171
11201
|
"",
|
|
11172
|
-
"Constraints
|
|
11202
|
+
"Constraints. your output must run inside a Lumia Custom Overlay sandboxed iframe:",
|
|
11173
11203
|
'- Use `window.Overlay.on("alert", handler)` / `window.Overlay.on("chat", handler)` to receive Lumia events.',
|
|
11174
11204
|
"- Lumia template variables use double-brace syntax: {{username}}, {{amount}}, {{message}}, {{currency}}.",
|
|
11175
11205
|
"- The `window.DATA` object holds user-configurable values (mirrors your `configs`).",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumiastream/ui",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"author": "Lumia Stream",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"description": "Lumia UI Kit",
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
"vitest": "^4.1.6"
|
|
129
129
|
},
|
|
130
130
|
"dependencies": {
|
|
131
|
-
"@lumiastream/lumia-translations": "1.16.
|
|
131
|
+
"@lumiastream/lumia-translations": "1.16.1",
|
|
132
132
|
"@lumiastream/lumia-types": "^3.3.7-alpha.2",
|
|
133
133
|
"classnames": "^2.5.1",
|
|
134
134
|
"globals": "^17.4.0",
|