@camstack/addon-import-alexa 0.2.158 → 0.2.160
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/CHANGELOG.md +12 -0
- package/dist/{MotionZonesSettings-Z-NRG681.mjs → MotionZonesSettings-TwgbETGR.mjs} +2 -2
- package/dist/{PrivacyMaskSettings-DiS2hPpi.mjs → PrivacyMaskSettings-BoNkDD5o.mjs} +4 -4
- package/dist/{SceneMonitorEditor-BYYK_XwY.mjs → SceneMonitorEditor-4TxlnJK0.mjs} +3 -3
- package/dist/_stub.js +9 -9
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_import_alexa_page-B4G6E4Vs.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_import_alexa_page-CZPqws-b.mjs} +3 -3
- package/dist/{_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DCV5s91A.mjs → _virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Bj2TfHO6.mjs} +1 -1
- package/dist/addon.js +608 -97
- package/dist/addon.mjs +608 -97
- package/dist/{hostInit-DWBMKrvf.mjs → hostInit-Datq9hsz.mjs} +2 -2
- package/dist/{player-overlays-5MrLqX2i.mjs → player-overlays-VaUrkkRt.mjs} +1 -1
- package/dist/remoteEntry.js +1 -1
- package/dist/{responsive-CsfyZVWS.mjs → responsive-D9B1kcZz.mjs} +1 -1
- package/dist/{scene-monitor-copy-DOuMmSwJ.mjs → scene-monitor-copy-CunyuMVK.mjs} +1 -1
- package/dist/{square-BGn7yRva.mjs → square-CuVo4SNm.mjs} +1 -1
- package/dist/{trash-2-BiBTPv8Y.mjs → trash-2-Bjze04-b.mjs} +1 -1
- package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_import_alexa_page__remoteEntry_js-BniKFfAU.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_import_alexa_page__remoteEntry_js-BY9vB6pT.mjs} +1 -1
- package/i18n/en.json +9 -2
- package/i18n/it.json +9 -2
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -990,7 +990,7 @@ var propertyKeyTypes = /* @__PURE__*/ new Set([
|
|
|
990
990
|
"number",
|
|
991
991
|
"symbol"
|
|
992
992
|
]);
|
|
993
|
-
function escapeRegex(str) {
|
|
993
|
+
function escapeRegex$1(str) {
|
|
994
994
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
995
995
|
}
|
|
996
996
|
function clone(inst, def, params) {
|
|
@@ -1720,7 +1720,7 @@ var $ZodCheckUpperCase = /*@__PURE__*/ $constructor("$ZodCheckUpperCase", (inst,
|
|
|
1720
1720
|
});
|
|
1721
1721
|
var $ZodCheckIncludes = /*@__PURE__*/ $constructor("$ZodCheckIncludes", (inst, def) => {
|
|
1722
1722
|
$ZodCheck.init(inst, def);
|
|
1723
|
-
const escapedRegex = escapeRegex(def.includes);
|
|
1723
|
+
const escapedRegex = escapeRegex$1(def.includes);
|
|
1724
1724
|
const pattern = new RegExp(typeof def.position === "number" ? `^.{${def.position}}${escapedRegex}` : escapedRegex);
|
|
1725
1725
|
def.pattern = pattern;
|
|
1726
1726
|
inst._zod.onattach.push((inst) => {
|
|
@@ -1743,7 +1743,7 @@ var $ZodCheckIncludes = /*@__PURE__*/ $constructor("$ZodCheckIncludes", (inst, d
|
|
|
1743
1743
|
});
|
|
1744
1744
|
var $ZodCheckStartsWith = /*@__PURE__*/ $constructor("$ZodCheckStartsWith", (inst, def) => {
|
|
1745
1745
|
$ZodCheck.init(inst, def);
|
|
1746
|
-
const pattern = new RegExp(`^${escapeRegex(def.prefix)}.*`);
|
|
1746
|
+
const pattern = new RegExp(`^${escapeRegex$1(def.prefix)}.*`);
|
|
1747
1747
|
def.pattern ?? (def.pattern = pattern);
|
|
1748
1748
|
inst._zod.onattach.push((inst) => {
|
|
1749
1749
|
const bag = inst._zod.bag;
|
|
@@ -1765,7 +1765,7 @@ var $ZodCheckStartsWith = /*@__PURE__*/ $constructor("$ZodCheckStartsWith", (ins
|
|
|
1765
1765
|
});
|
|
1766
1766
|
var $ZodCheckEndsWith = /*@__PURE__*/ $constructor("$ZodCheckEndsWith", (inst, def) => {
|
|
1767
1767
|
$ZodCheck.init(inst, def);
|
|
1768
|
-
const pattern = new RegExp(`.*${escapeRegex(def.suffix)}$`);
|
|
1768
|
+
const pattern = new RegExp(`.*${escapeRegex$1(def.suffix)}$`);
|
|
1769
1769
|
def.pattern ?? (def.pattern = pattern);
|
|
1770
1770
|
inst._zod.onattach.push((inst) => {
|
|
1771
1771
|
const bag = inst._zod.bag;
|
|
@@ -3001,7 +3001,7 @@ var $ZodEnum = /*@__PURE__*/ $constructor("$ZodEnum", (inst, def) => {
|
|
|
3001
3001
|
const values = getEnumValues(def.entries);
|
|
3002
3002
|
const valuesSet = new Set(values);
|
|
3003
3003
|
inst._zod.values = valuesSet;
|
|
3004
|
-
inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === "string" ? escapeRegex(o) : o.toString()).join("|")})$`);
|
|
3004
|
+
inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === "string" ? escapeRegex$1(o) : o.toString()).join("|")})$`);
|
|
3005
3005
|
inst._zod.parse = (payload, _ctx) => {
|
|
3006
3006
|
const input = payload.value;
|
|
3007
3007
|
if (valuesSet.has(input)) return payload;
|
|
@@ -3019,7 +3019,7 @@ var $ZodLiteral = /*@__PURE__*/ $constructor("$ZodLiteral", (inst, def) => {
|
|
|
3019
3019
|
if (def.values.length === 0) throw new Error("Cannot create literal schema with no valid values");
|
|
3020
3020
|
const values = new Set(def.values);
|
|
3021
3021
|
inst._zod.values = values;
|
|
3022
|
-
inst._zod.pattern = new RegExp(`^(${def.values.map((o) => typeof o === "string" ? escapeRegex(o) : o ? escapeRegex(o.toString()) : String(o)).join("|")})$`);
|
|
3022
|
+
inst._zod.pattern = new RegExp(`^(${def.values.map((o) => typeof o === "string" ? escapeRegex$1(o) : o ? escapeRegex$1(o.toString()) : String(o)).join("|")})$`);
|
|
3023
3023
|
inst._zod.parse = (payload, _ctx) => {
|
|
3024
3024
|
const input = payload.value;
|
|
3025
3025
|
if (values.has(input)) return payload;
|
|
@@ -90637,6 +90637,457 @@ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
|
|
|
90637
90637
|
DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
|
|
90638
90638
|
DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
|
|
90639
90639
|
DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
90640
|
+
/**
|
|
90641
|
+
* The data-plane prefix every `VendorLoginProxy` is served under
|
|
90642
|
+
* (`/addon/<addonId>/vendor-login/<attemptId>/<vendorHost>/…`).
|
|
90643
|
+
*
|
|
90644
|
+
* The hub consults an addon's data-plane prefixes BEFORE its HTTP routes, so a
|
|
90645
|
+
* prefix equal to the first segment of a route the addon serves swallows that
|
|
90646
|
+
* route. A sign-in addon serves `/login/start|status|commit|cancel`; the proxy
|
|
90647
|
+
* therefore lives on a segment no addon route uses. One constant, so the entry
|
|
90648
|
+
* path, the mount and the docs cannot drift apart.
|
|
90649
|
+
*/
|
|
90650
|
+
var VENDOR_LOGIN_PROXY_PREFIX = "vendor-login";
|
|
90651
|
+
/**
|
|
90652
|
+
* Where an OAuth round-trip ends: the admin-ui completion route
|
|
90653
|
+
* (`/sso-complete`, the page every popup sign-in already lands on), with the
|
|
90654
|
+
* OUTCOME in the URL fragment.
|
|
90655
|
+
*
|
|
90656
|
+
* The outcome is a status plus a reason CODE from a closed list. It never
|
|
90657
|
+
* carries a token, a code, a client secret or a free-form error message: the
|
|
90658
|
+
* page words each code itself, and an unknown code reads as `unknown` rather
|
|
90659
|
+
* than being echoed. Fragment only, so nothing here reaches a server log.
|
|
90660
|
+
*
|
|
90661
|
+
* Pure and browser-safe.
|
|
90662
|
+
*/
|
|
90663
|
+
/** The admin-ui route the hub's OAuth completions land on (relative to its mount). */
|
|
90664
|
+
var OAUTH_LANDING_PATH = "/sso-complete";
|
|
90665
|
+
/**
|
|
90666
|
+
* The landing URL for `outcome`. `origin` is the hub address the flow already
|
|
90667
|
+
* runs on (from the `local-network` palette role, never a literal); `null`
|
|
90668
|
+
* gives a hub-relative URL.
|
|
90669
|
+
*/
|
|
90670
|
+
function oauthLandingUrl(origin, outcome) {
|
|
90671
|
+
const params = new URLSearchParams({
|
|
90672
|
+
return: "oauth",
|
|
90673
|
+
status: outcome.status
|
|
90674
|
+
});
|
|
90675
|
+
if (outcome.addonId !== void 0) params.set("addon", outcome.addonId);
|
|
90676
|
+
if (outcome.brokerId !== void 0) params.set("broker", outcome.brokerId);
|
|
90677
|
+
if (outcome.integration !== void 0) params.set("integration", outcome.integration);
|
|
90678
|
+
if (outcome.status === "error") params.set("reason", outcome.reason ?? "unknown");
|
|
90679
|
+
if (outcome.action === "disconnect") params.set("action", "disconnect");
|
|
90680
|
+
return `${origin === null ? "" : origin.replace(/\/+$/, "")}${OAUTH_LANDING_PATH}#${params.toString()}`;
|
|
90681
|
+
}
|
|
90682
|
+
var LOOPBACK_HOSTS = new Set([
|
|
90683
|
+
"127.0.0.1",
|
|
90684
|
+
"localhost",
|
|
90685
|
+
"[::1]"
|
|
90686
|
+
]);
|
|
90687
|
+
function hostWithoutPort(host) {
|
|
90688
|
+
if (host.startsWith("[")) return host.slice(0, host.indexOf("]") + 1);
|
|
90689
|
+
const colon = host.indexOf(":");
|
|
90690
|
+
return colon === -1 ? host : host.slice(0, colon);
|
|
90691
|
+
}
|
|
90692
|
+
function isLoopbackHost(host) {
|
|
90693
|
+
return LOOPBACK_HOSTS.has(hostWithoutPort(host.toLowerCase()));
|
|
90694
|
+
}
|
|
90695
|
+
/** An allow-list entry is valid when it is a bare host[:port] and `http` only targets loopback. */
|
|
90696
|
+
function isValidUpstream(upstream) {
|
|
90697
|
+
if (upstream.host.length === 0 || !/^[a-z0-9.[\]:-]+$/.test(upstream.host)) return false;
|
|
90698
|
+
return upstream.scheme === "https" || isLoopbackHost(upstream.host);
|
|
90699
|
+
}
|
|
90700
|
+
function findUpstream(route, host) {
|
|
90701
|
+
const wanted = host.toLowerCase();
|
|
90702
|
+
return route.upstreams.find((upstream) => upstream.host === wanted);
|
|
90703
|
+
}
|
|
90704
|
+
/** `<basePath>/<host><pathname><search>` — what the browser is sent to. */
|
|
90705
|
+
function proxyPathFor(route, host, pathname, search = "") {
|
|
90706
|
+
return `${route.basePath}/${host}${pathname.startsWith("/") ? pathname : `/${pathname}`}${search}`;
|
|
90707
|
+
}
|
|
90708
|
+
/** Split `/<attemptId>/<host>/<rest…>[?query]` (the data-plane view of a request). */
|
|
90709
|
+
function parseProxyPath(rawUrl) {
|
|
90710
|
+
const queryAt = rawUrl.indexOf("?");
|
|
90711
|
+
const path = queryAt === -1 ? rawUrl : rawUrl.slice(0, queryAt);
|
|
90712
|
+
const search = queryAt === -1 ? "" : rawUrl.slice(queryAt);
|
|
90713
|
+
const match = /^\/([A-Za-z0-9_-]{8,128})\/([^/]+)(\/.*)?$/.exec(path);
|
|
90714
|
+
if (match === null) return null;
|
|
90715
|
+
const attemptId = match[1];
|
|
90716
|
+
const host = match[2];
|
|
90717
|
+
if (attemptId === void 0 || host === void 0) return null;
|
|
90718
|
+
return {
|
|
90719
|
+
attemptId,
|
|
90720
|
+
host: host.toLowerCase(),
|
|
90721
|
+
pathname: match[3] ?? "/",
|
|
90722
|
+
search
|
|
90723
|
+
};
|
|
90724
|
+
}
|
|
90725
|
+
/** The vendor URL a proxied request targets, or null when the host would not stay `host`. */
|
|
90726
|
+
function upstreamUrlFor(upstream, parsed) {
|
|
90727
|
+
try {
|
|
90728
|
+
const url = new URL(`${upstream.scheme}://${upstream.host}${parsed.pathname}${parsed.search}`);
|
|
90729
|
+
return url.host.toLowerCase() === upstream.host ? url : null;
|
|
90730
|
+
} catch {
|
|
90731
|
+
return null;
|
|
90732
|
+
}
|
|
90733
|
+
}
|
|
90734
|
+
function escapeRegex(value) {
|
|
90735
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
90736
|
+
}
|
|
90737
|
+
/** A Location (or any URL value) to a vendor host becomes a proxy path; anything else is unchanged. */
|
|
90738
|
+
function rewriteLocation(location, currentUpstream, route) {
|
|
90739
|
+
let resolved;
|
|
90740
|
+
try {
|
|
90741
|
+
resolved = new URL(location, `${currentUpstream.scheme}://${currentUpstream.host}/`);
|
|
90742
|
+
} catch {
|
|
90743
|
+
return location;
|
|
90744
|
+
}
|
|
90745
|
+
if (resolved.protocol !== "https:" && resolved.protocol !== "http:") return location;
|
|
90746
|
+
const target = findUpstream(route, resolved.host);
|
|
90747
|
+
if (target === void 0) return location;
|
|
90748
|
+
return `${proxyPathFor(route, target.host, resolved.pathname, resolved.search)}${resolved.hash}`;
|
|
90749
|
+
}
|
|
90750
|
+
/** Characters that may precede a URL that STARTS a value (never `=`, `?`, `&`, `%3D`). */
|
|
90751
|
+
var VALUE_START = String.raw`(?<=^|["'(\s>,\[{])`;
|
|
90752
|
+
/**
|
|
90753
|
+
* Rewrite vendor URLs in a text body: absolute (`https://host/…`), protocol-relative
|
|
90754
|
+
* (`//host/…`) and JSON-escaped (`https:\/\/host\/…`) when they start a value; and, in
|
|
90755
|
+
* HTML/CSS, root-relative references (`href="/x"`, `url(/x)`) which would otherwise
|
|
90756
|
+
* escape the proxy mount and land on the hub.
|
|
90757
|
+
*/
|
|
90758
|
+
function rewriteBody(body, kind, currentUpstream, route) {
|
|
90759
|
+
let out = body;
|
|
90760
|
+
for (const upstream of route.upstreams) {
|
|
90761
|
+
const host = escapeRegex(upstream.host);
|
|
90762
|
+
const plain = new RegExp(`${VALUE_START}(?:https?:)?//${host}(?=[/"'?#\\s)\\]},<]|$)`, "g");
|
|
90763
|
+
out = out.replace(plain, `${route.basePath}/${upstream.host}`);
|
|
90764
|
+
const escapedBase = `${route.basePath}/${upstream.host}`.replace(/\//g, "\\/");
|
|
90765
|
+
const escaped = new RegExp(`${VALUE_START}(?:https?:)?\\\\/\\\\/${host}(?=\\\\/|["'?#\\s)\\]},<]|$)`, "g");
|
|
90766
|
+
out = out.replace(escaped, escapedBase);
|
|
90767
|
+
}
|
|
90768
|
+
const current = `${route.basePath}/${currentUpstream.host}/`;
|
|
90769
|
+
const notMounted = `(?!/)(?!${escapeRegex(route.basePath.slice(1))}/)`;
|
|
90770
|
+
if (kind === "html") {
|
|
90771
|
+
const attribute = String.raw`(\b(?:href|src|action|formaction|poster|data-href|data-src)\s*=\s*["'])/`;
|
|
90772
|
+
out = out.replace(new RegExp(`${attribute}${notMounted}`, "gi"), `$1${current}`);
|
|
90773
|
+
}
|
|
90774
|
+
if (kind === "html" || kind === "css") {
|
|
90775
|
+
const cssUrl = String.raw`(url\(\s*["']?)/`;
|
|
90776
|
+
out = out.replace(new RegExp(`${cssUrl}${notMounted}`, "gi"), `$1${current}`);
|
|
90777
|
+
}
|
|
90778
|
+
return out;
|
|
90779
|
+
}
|
|
90780
|
+
/**
|
|
90781
|
+
* A vendor `Set-Cookie` for the browser: no `Domain` (the cookie belongs to the hub
|
|
90782
|
+
* origin), `Path` narrowed to this attempt's prefix, `Secure` kept (the hub is HTTPS).
|
|
90783
|
+
*/
|
|
90784
|
+
function rewriteSetCookie(cookie, route) {
|
|
90785
|
+
const kept = cookie.split(";").map((part) => part.trim()).filter((part, index) => {
|
|
90786
|
+
if (index === 0) return true;
|
|
90787
|
+
const name = part.split("=")[0]?.trim().toLowerCase() ?? "";
|
|
90788
|
+
return name !== "domain" && name !== "path";
|
|
90789
|
+
});
|
|
90790
|
+
kept.push(`Path=${route.basePath}/`);
|
|
90791
|
+
return kept.join("; ");
|
|
90792
|
+
}
|
|
90793
|
+
/**
|
|
90794
|
+
* A browser `Referer` that points into this proxy, turned back into the vendor URL it
|
|
90795
|
+
* stands for. A Referer that is not ours is dropped (null): it names the hub.
|
|
90796
|
+
*/
|
|
90797
|
+
function rewriteReferer(referer, route) {
|
|
90798
|
+
let url;
|
|
90799
|
+
try {
|
|
90800
|
+
url = new URL(referer, "http://hub.invalid");
|
|
90801
|
+
} catch {
|
|
90802
|
+
return null;
|
|
90803
|
+
}
|
|
90804
|
+
if (!url.pathname.startsWith(`${route.basePath}/`)) return null;
|
|
90805
|
+
const rest = /^\/([^/]+)(\/.*)?$/.exec(url.pathname.slice(route.basePath.length));
|
|
90806
|
+
const host = rest?.[1]?.toLowerCase();
|
|
90807
|
+
if (host === void 0) return null;
|
|
90808
|
+
const upstream = findUpstream(route, host);
|
|
90809
|
+
if (upstream === void 0) return null;
|
|
90810
|
+
return `${upstream.scheme}://${upstream.host}${rest?.[2] ?? "/"}${url.search}`;
|
|
90811
|
+
}
|
|
90812
|
+
/** Cookies the browser holds for the hub itself must never reach a vendor. */
|
|
90813
|
+
function filterForwardedCookies(cookieHeader) {
|
|
90814
|
+
return cookieHeader.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !part.toLowerCase().startsWith("camstack_")).join("; ");
|
|
90815
|
+
}
|
|
90816
|
+
var MAX_REQUEST_BODY_BYTES = 2 * 1024 * 1024;
|
|
90817
|
+
var MAX_TEXT_BODY_BYTES = 4 * 1024 * 1024;
|
|
90818
|
+
/** Request headers that are never forwarded to the vendor. */
|
|
90819
|
+
var DROP_REQUEST_HEADERS = new Set([
|
|
90820
|
+
"host",
|
|
90821
|
+
"connection",
|
|
90822
|
+
"keep-alive",
|
|
90823
|
+
"proxy-authorization",
|
|
90824
|
+
"proxy-connection",
|
|
90825
|
+
"te",
|
|
90826
|
+
"trailer",
|
|
90827
|
+
"transfer-encoding",
|
|
90828
|
+
"upgrade",
|
|
90829
|
+
"content-length",
|
|
90830
|
+
"accept-encoding",
|
|
90831
|
+
"authorization",
|
|
90832
|
+
"cookie",
|
|
90833
|
+
"origin",
|
|
90834
|
+
"referer",
|
|
90835
|
+
"x-camstack-dataplane-secret",
|
|
90836
|
+
"x-forwarded-for",
|
|
90837
|
+
"x-forwarded-host",
|
|
90838
|
+
"x-forwarded-proto",
|
|
90839
|
+
"x-forwarded-prefix",
|
|
90840
|
+
"x-real-ip",
|
|
90841
|
+
"forwarded"
|
|
90842
|
+
]);
|
|
90843
|
+
/**
|
|
90844
|
+
* Response headers dropped: framing/CSP/HSTS that break a page under another origin, and
|
|
90845
|
+
* the ones that no longer describe the (decoded, possibly rewritten) body.
|
|
90846
|
+
*/
|
|
90847
|
+
var DROP_RESPONSE_HEADERS = new Set([
|
|
90848
|
+
"content-security-policy",
|
|
90849
|
+
"content-security-policy-report-only",
|
|
90850
|
+
"x-frame-options",
|
|
90851
|
+
"strict-transport-security",
|
|
90852
|
+
"public-key-pins",
|
|
90853
|
+
"report-to",
|
|
90854
|
+
"nel",
|
|
90855
|
+
"content-encoding",
|
|
90856
|
+
"content-length",
|
|
90857
|
+
"transfer-encoding",
|
|
90858
|
+
"connection",
|
|
90859
|
+
"keep-alive",
|
|
90860
|
+
"set-cookie",
|
|
90861
|
+
"location"
|
|
90862
|
+
]);
|
|
90863
|
+
function bodyKindOf(contentType) {
|
|
90864
|
+
const type = contentType.toLowerCase();
|
|
90865
|
+
if (type.includes("text/html") || type.includes("application/xhtml")) return "html";
|
|
90866
|
+
if (type.includes("text/css")) return "css";
|
|
90867
|
+
if (type.startsWith("text/") || type.includes("javascript") || type.includes("json") || type.includes("xml")) return "other";
|
|
90868
|
+
return null;
|
|
90869
|
+
}
|
|
90870
|
+
function firstHeader(value) {
|
|
90871
|
+
return Array.isArray(value) ? value[0] : value;
|
|
90872
|
+
}
|
|
90873
|
+
async function readRequestBody(req) {
|
|
90874
|
+
const chunks = [];
|
|
90875
|
+
let size = 0;
|
|
90876
|
+
for await (const chunk of req) {
|
|
90877
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk);
|
|
90878
|
+
size += buf.length;
|
|
90879
|
+
if (size > MAX_REQUEST_BODY_BYTES) return "too-large";
|
|
90880
|
+
chunks.push(buf);
|
|
90881
|
+
}
|
|
90882
|
+
return Buffer.concat(chunks);
|
|
90883
|
+
}
|
|
90884
|
+
function sendPlain(res, status, text) {
|
|
90885
|
+
res.writeHead(status, {
|
|
90886
|
+
"content-type": "text/plain; charset=utf-8",
|
|
90887
|
+
"cache-control": "no-store"
|
|
90888
|
+
});
|
|
90889
|
+
res.end(text);
|
|
90890
|
+
}
|
|
90891
|
+
var VendorLoginProxy = class {
|
|
90892
|
+
#addonId;
|
|
90893
|
+
#logger;
|
|
90894
|
+
#now;
|
|
90895
|
+
#fetch;
|
|
90896
|
+
#attempts = /* @__PURE__ */ new Map();
|
|
90897
|
+
constructor(options) {
|
|
90898
|
+
this.#addonId = options.addonId;
|
|
90899
|
+
this.#logger = options.logger;
|
|
90900
|
+
this.#now = options.now ?? (() => Date.now());
|
|
90901
|
+
this.#fetch = options.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
90902
|
+
}
|
|
90903
|
+
/** The `DataPlaneHandler` to pass to `ctx.dataPlane.serve({ prefix: VENDOR_LOGIN_PROXY_PREFIX, access: 'admin', … })`. */
|
|
90904
|
+
handler = (req, res) => this.#handle(req, res);
|
|
90905
|
+
/** Register an attempt. A one-shot capability: refused hosts are rejected up front. */
|
|
90906
|
+
open(spec) {
|
|
90907
|
+
for (const upstream of spec.upstreams) if (!isValidUpstream(upstream)) throw new Error(`vendor login proxy: upstream ${upstream.host} is not allowed`);
|
|
90908
|
+
this.#attempts.set(spec.attemptId, {
|
|
90909
|
+
expiresAtMs: spec.expiresAtMs,
|
|
90910
|
+
upstreams: spec.upstreams,
|
|
90911
|
+
evaluate: async (view) => {
|
|
90912
|
+
const verdict = await spec.terminal(view);
|
|
90913
|
+
if (verdict.kind === "continue") return verdict;
|
|
90914
|
+
return {
|
|
90915
|
+
kind: "done",
|
|
90916
|
+
landing: await spec.onDone(verdict.payload)
|
|
90917
|
+
};
|
|
90918
|
+
}
|
|
90919
|
+
});
|
|
90920
|
+
}
|
|
90921
|
+
close(attemptId) {
|
|
90922
|
+
this.#attempts.delete(attemptId);
|
|
90923
|
+
}
|
|
90924
|
+
isOpen(attemptId) {
|
|
90925
|
+
const attempt = this.#attempts.get(attemptId);
|
|
90926
|
+
return attempt !== void 0 && this.#now() <= attempt.expiresAtMs;
|
|
90927
|
+
}
|
|
90928
|
+
/**
|
|
90929
|
+
* The hub-relative path that opens `vendorUrl` through the proxy for this attempt:
|
|
90930
|
+
* the URL the browser is sent to. Null when the host is not in the attempt's list.
|
|
90931
|
+
*/
|
|
90932
|
+
entryPath(attemptId, vendorUrl) {
|
|
90933
|
+
const attempt = this.#attempts.get(attemptId);
|
|
90934
|
+
if (attempt === void 0) return null;
|
|
90935
|
+
const url = new URL(vendorUrl);
|
|
90936
|
+
if (findUpstream({
|
|
90937
|
+
basePath: "",
|
|
90938
|
+
upstreams: attempt.upstreams
|
|
90939
|
+
}, url.host) === void 0) return null;
|
|
90940
|
+
return `/addon/${this.#addonId}/${VENDOR_LOGIN_PROXY_PREFIX}/${attemptId}/${url.host}${url.pathname}${url.search}`;
|
|
90941
|
+
}
|
|
90942
|
+
#routeFor(attemptId, attempt, forwardedPrefix) {
|
|
90943
|
+
return {
|
|
90944
|
+
basePath: `${forwardedPrefix}/addon/${this.#addonId}/${VENDOR_LOGIN_PROXY_PREFIX}/${attemptId}`,
|
|
90945
|
+
upstreams: attempt.upstreams
|
|
90946
|
+
};
|
|
90947
|
+
}
|
|
90948
|
+
async #handle(req, res) {
|
|
90949
|
+
try {
|
|
90950
|
+
await this.#serve(req, res);
|
|
90951
|
+
} catch (err) {
|
|
90952
|
+
this.#logger.warn("vendor login proxy: request failed", { meta: {
|
|
90953
|
+
addonId: this.#addonId,
|
|
90954
|
+
error: err instanceof Error ? err.name : "unknown"
|
|
90955
|
+
} });
|
|
90956
|
+
if (!res.headersSent) sendPlain(res, 502, "The sign-in page could not be reached.");
|
|
90957
|
+
else res.end();
|
|
90958
|
+
}
|
|
90959
|
+
}
|
|
90960
|
+
async #serve(req, res) {
|
|
90961
|
+
const parsed = parseProxyPath(req.url ?? "/");
|
|
90962
|
+
if (parsed === null) {
|
|
90963
|
+
sendPlain(res, 404, "Not found");
|
|
90964
|
+
return;
|
|
90965
|
+
}
|
|
90966
|
+
const forwardedPrefix = (firstHeader(req.headers["x-forwarded-prefix"]) ?? "").replace(/\/+$/, "");
|
|
90967
|
+
const attempt = this.#attempts.get(parsed.attemptId);
|
|
90968
|
+
if (attempt === void 0 || this.#now() > attempt.expiresAtMs) {
|
|
90969
|
+
if (attempt !== void 0) this.#attempts.delete(parsed.attemptId);
|
|
90970
|
+
this.#logger.info("vendor login proxy: attempt refused", { meta: {
|
|
90971
|
+
addonId: this.#addonId,
|
|
90972
|
+
reason: attempt === void 0 ? "missing" : "expired"
|
|
90973
|
+
} });
|
|
90974
|
+
this.#redirect(res, forwardedPrefix, {
|
|
90975
|
+
status: "error",
|
|
90976
|
+
addonId: this.#addonId,
|
|
90977
|
+
reason: "invalid_state"
|
|
90978
|
+
});
|
|
90979
|
+
return;
|
|
90980
|
+
}
|
|
90981
|
+
const route = this.#routeFor(parsed.attemptId, attempt, forwardedPrefix);
|
|
90982
|
+
const upstream = findUpstream(route, parsed.host);
|
|
90983
|
+
const target = upstream === void 0 ? null : upstreamUrlFor(upstream, parsed);
|
|
90984
|
+
if (upstream === void 0 || target === null) {
|
|
90985
|
+
this.#logger.warn("vendor login proxy: host refused", { meta: {
|
|
90986
|
+
addonId: this.#addonId,
|
|
90987
|
+
host: parsed.host
|
|
90988
|
+
} });
|
|
90989
|
+
sendPlain(res, 404, "Not found");
|
|
90990
|
+
return;
|
|
90991
|
+
}
|
|
90992
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
90993
|
+
let body;
|
|
90994
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
90995
|
+
const read = await readRequestBody(req);
|
|
90996
|
+
if (read === "too-large") {
|
|
90997
|
+
sendPlain(res, 413, "Request too large");
|
|
90998
|
+
return;
|
|
90999
|
+
}
|
|
91000
|
+
body = read;
|
|
91001
|
+
}
|
|
91002
|
+
const headers = this.#requestHeaders(req, route, upstream);
|
|
91003
|
+
const upstreamRes = await this.#fetch(target, {
|
|
91004
|
+
method,
|
|
91005
|
+
headers,
|
|
91006
|
+
redirect: "manual",
|
|
91007
|
+
...body !== void 0 && body.length > 0 ? { body: new Uint8Array(body) } : {}
|
|
91008
|
+
});
|
|
91009
|
+
const location = upstreamRes.headers.get("location");
|
|
91010
|
+
const setCookies = upstreamRes.headers.getSetCookie();
|
|
91011
|
+
const kind = bodyKindOf(upstreamRes.headers.get("content-type") ?? "");
|
|
91012
|
+
const declaredLength = Number(upstreamRes.headers.get("content-length") ?? "0");
|
|
91013
|
+
const text = kind !== null && !(declaredLength > MAX_TEXT_BODY_BYTES) && method !== "HEAD" ? await upstreamRes.text() : null;
|
|
91014
|
+
this.#logger.debug("vendor login proxy: upstream answered", { meta: {
|
|
91015
|
+
addonId: this.#addonId,
|
|
91016
|
+
host: upstream.host,
|
|
91017
|
+
status: upstreamRes.status,
|
|
91018
|
+
method
|
|
91019
|
+
} });
|
|
91020
|
+
const settled = await attempt.evaluate({
|
|
91021
|
+
status: upstreamRes.status,
|
|
91022
|
+
url: target.toString(),
|
|
91023
|
+
location,
|
|
91024
|
+
setCookies,
|
|
91025
|
+
bodyText: text
|
|
91026
|
+
});
|
|
91027
|
+
if (settled.kind === "done") {
|
|
91028
|
+
this.#attempts.delete(parsed.attemptId);
|
|
91029
|
+
this.#logger.info("vendor login proxy: sign-in finished", { meta: {
|
|
91030
|
+
addonId: this.#addonId,
|
|
91031
|
+
status: settled.landing.status
|
|
91032
|
+
} });
|
|
91033
|
+
this.#redirect(res, forwardedPrefix, settled.landing);
|
|
91034
|
+
return;
|
|
91035
|
+
}
|
|
91036
|
+
const out = {};
|
|
91037
|
+
upstreamRes.headers.forEach((value, name) => {
|
|
91038
|
+
if (!DROP_RESPONSE_HEADERS.has(name.toLowerCase())) out[name] = value;
|
|
91039
|
+
});
|
|
91040
|
+
if (setCookies.length > 0) out["set-cookie"] = setCookies.map((c) => rewriteSetCookie(c, route));
|
|
91041
|
+
if (location !== null) out["location"] = rewriteLocation(location, upstream, route);
|
|
91042
|
+
out["cache-control"] = "no-store";
|
|
91043
|
+
if (text !== null && kind !== null) {
|
|
91044
|
+
const rewritten = rewriteBody(text, kind, upstream, route);
|
|
91045
|
+
res.writeHead(upstreamRes.status, out);
|
|
91046
|
+
res.end(rewritten);
|
|
91047
|
+
return;
|
|
91048
|
+
}
|
|
91049
|
+
res.writeHead(upstreamRes.status, out);
|
|
91050
|
+
await this.#pipeBody(upstreamRes, res);
|
|
91051
|
+
}
|
|
91052
|
+
#requestHeaders(req, route, upstream) {
|
|
91053
|
+
const headers = {};
|
|
91054
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
91055
|
+
const lower = name.toLowerCase();
|
|
91056
|
+
if (DROP_REQUEST_HEADERS.has(lower) || value === void 0) continue;
|
|
91057
|
+
headers[lower] = Array.isArray(value) ? value.join(", ") : value;
|
|
91058
|
+
}
|
|
91059
|
+
const cookie = filterForwardedCookies(firstHeader(req.headers["cookie"]) ?? "");
|
|
91060
|
+
if (cookie.length > 0) headers["cookie"] = cookie;
|
|
91061
|
+
if (req.headers["origin"] !== void 0) headers["origin"] = `${upstream.scheme}://${upstream.host}`;
|
|
91062
|
+
const referer = firstHeader(req.headers["referer"]);
|
|
91063
|
+
if (referer !== void 0) {
|
|
91064
|
+
const back = rewriteReferer(referer, route);
|
|
91065
|
+
if (back !== null) headers["referer"] = back;
|
|
91066
|
+
}
|
|
91067
|
+
return headers;
|
|
91068
|
+
}
|
|
91069
|
+
async #pipeBody(upstreamRes, res) {
|
|
91070
|
+
if (upstreamRes.body === null) {
|
|
91071
|
+
res.end();
|
|
91072
|
+
return;
|
|
91073
|
+
}
|
|
91074
|
+
for await (const chunk of upstreamRes.body) {
|
|
91075
|
+
if (!res.write(chunk)) await new Promise((resolve) => {
|
|
91076
|
+
res.once("drain", resolve);
|
|
91077
|
+
res.once("close", resolve);
|
|
91078
|
+
});
|
|
91079
|
+
if (res.destroyed) return;
|
|
91080
|
+
}
|
|
91081
|
+
res.end();
|
|
91082
|
+
}
|
|
91083
|
+
#redirect(res, forwardedPrefix, outcome) {
|
|
91084
|
+
res.writeHead(302, {
|
|
91085
|
+
location: `${forwardedPrefix}${oauthLandingUrl(null, outcome)}`,
|
|
91086
|
+
"cache-control": "no-store"
|
|
91087
|
+
});
|
|
91088
|
+
res.end();
|
|
91089
|
+
}
|
|
91090
|
+
};
|
|
90640
91091
|
var MB = 1024 * 1024;
|
|
90641
91092
|
1024 * MB, 3072 * MB;
|
|
90642
91093
|
//#endregion
|
|
@@ -90688,18 +91139,12 @@ var AlexaBrokerEntrySchema = object({
|
|
|
90688
91139
|
});
|
|
90689
91140
|
/**
|
|
90690
91141
|
* Interactive proxy-login settings (addon-global, not per-broker). alexa-remote2
|
|
90691
|
-
* spins a local reverse-proxy to Amazon's login page on `
|
|
90692
|
-
*
|
|
90693
|
-
*
|
|
90694
|
-
*
|
|
90695
|
-
* the hub's LAN IP/hostname — NOT behind the hub's HTTPS data-plane mount.
|
|
90696
|
-
* Empty `proxyHost` ⇒ the first LAN address of the hub's `appConnections`
|
|
90697
|
-
* role (local-network palette) at login-start time.
|
|
91142
|
+
* spins a local reverse-proxy to Amazon's login page on `127.0.0.1:proxyPort`; the
|
|
91143
|
+
* operator never reaches it directly — the hub's login proxy (D955) opens it at
|
|
91144
|
+
* `/addon/import-alexa/vendor-login/…` under the hub's own HTTPS origin. The port only has to
|
|
91145
|
+
* be free on the hub's loopback.
|
|
90698
91146
|
*/
|
|
90699
|
-
var alexaProxyLoginSchema = object({
|
|
90700
|
-
proxyHost: string().default("").describe("Host/IP the browser uses to reach the login proxy (blank = the hub LAN address)"),
|
|
90701
|
-
proxyPort: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(1).max(65535).default(3456)).describe("Port the local Amazon-login proxy listens on (must be reachable)")
|
|
90702
|
-
});
|
|
91147
|
+
var alexaProxyLoginSchema = object({ proxyPort: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(1).max(65535).default(3456)).describe("Loopback port the local Amazon-login proxy listens on (never exposed)") });
|
|
90703
91148
|
/**
|
|
90704
91149
|
* Echo speech settings. Speech signs in as an import broker, so the targets
|
|
90705
91150
|
* are what lives here. `email`, `password`, `region` and `session` are the OLD
|
|
@@ -90746,10 +91191,7 @@ function readSpeakSettings(config) {
|
|
|
90746
91191
|
/** Top-level addon config — broker entries + the proxy-login settings. */
|
|
90747
91192
|
var alexaAddonConfigSchema = object({
|
|
90748
91193
|
brokers: array(AlexaBrokerEntrySchema).default([]),
|
|
90749
|
-
proxyLogin: alexaProxyLoginSchema.default({
|
|
90750
|
-
proxyHost: "",
|
|
90751
|
-
proxyPort: 3456
|
|
90752
|
-
}),
|
|
91194
|
+
proxyLogin: alexaProxyLoginSchema.default({ proxyPort: 3456 }),
|
|
90753
91195
|
speak: alexaSpeakSettingsSchema.default({
|
|
90754
91196
|
email: "",
|
|
90755
91197
|
password: "",
|
|
@@ -132305,6 +132747,31 @@ var ALEXA_API_USER_AGENT_POSTFIX = `AlexaRemote/${{
|
|
|
132305
132747
|
//#endregion
|
|
132306
132748
|
//#region src/alexa-proxy-login.ts
|
|
132307
132749
|
/**
|
|
132750
|
+
* INTERACTIVE Amazon login via `alexa-remote2`'s built-in proxy auth mode, behind the
|
|
132751
|
+
* hub's vendor login proxy (D955).
|
|
132752
|
+
*
|
|
132753
|
+
* alexa-remote2 (through `alexa-cookie2`) spins a local Express reverse-proxy to
|
|
132754
|
+
* Amazon's real login page and captures the resulting cookie + the richer
|
|
132755
|
+
* `cookieData` registration blob (it embeds the refresh token, so the session can
|
|
132756
|
+
* auto-refresh forever). That listener is bound to LOOPBACK only
|
|
132757
|
+
* (`proxyOwnIp` = `proxyListenBind` = 127.0.0.1): nothing outside this process's host can
|
|
132758
|
+
* reach it. The operator's browser reaches it through the hub's data plane instead
|
|
132759
|
+
* (`VendorLoginProxy`, `/addon/<id>/vendor-login/<attempt>/127.0.0.1:<port>/…`), which is
|
|
132760
|
+
* HTTPS under the same origin as the admin page. alexa-cookie2 keeps doing the Amazon
|
|
132761
|
+
* specific work (login cookies, device registration, the `/ax/claim` and CSRF steps)
|
|
132762
|
+
* and the hub proxy only rewrites the loopback-origin URLs it emits into the mount.
|
|
132763
|
+
*
|
|
132764
|
+
* Why not the second proxy replacing alexa-cookie2's: the capture needs the PKCE
|
|
132765
|
+
* verifier, the device id, the initial cookies and the app registration alexa-cookie2
|
|
132766
|
+
* already performs; re-implementing them would add JS-built-URL failure modes.
|
|
132767
|
+
*
|
|
132768
|
+
* The captured `cookieData` is handed to {@link AlexaProxyLoginSessionOptions.onCookie}
|
|
132769
|
+
* — the addon persists it on the broker entry, exactly where a pasted cookie
|
|
132770
|
+
* goes, so the rest of the addon (broker / poll / adoption) is unchanged.
|
|
132771
|
+
*/
|
|
132772
|
+
/** The only address the alexa-cookie2 listener binds and advertises. */
|
|
132773
|
+
var ALEXA_PROXY_LOOPBACK_HOST = "127.0.0.1";
|
|
132774
|
+
/**
|
|
132308
132775
|
* A single interactive proxy-login attempt. One instance = one Amazon login.
|
|
132309
132776
|
* Construct → {@link start} → poll {@link getStatus} → on `captured` the
|
|
132310
132777
|
* `onCookie` callback has fired with the blob → {@link dispose}.
|
|
@@ -132318,13 +132785,37 @@ var AlexaProxyLoginSession = class {
|
|
|
132318
132785
|
#startedAt = Date.now();
|
|
132319
132786
|
#capturedAt = null;
|
|
132320
132787
|
#captured = false;
|
|
132788
|
+
#waiters = /* @__PURE__ */ new Set();
|
|
132321
132789
|
constructor(opts) {
|
|
132322
132790
|
this.#opts = opts;
|
|
132323
132791
|
this.#logger = opts.logger;
|
|
132324
132792
|
}
|
|
132325
|
-
/**
|
|
132793
|
+
/** What the operator opens: the hub-relative entry of the login proxy. */
|
|
132326
132794
|
get loginUrl() {
|
|
132327
|
-
return
|
|
132795
|
+
return this.#opts.loginUrl;
|
|
132796
|
+
}
|
|
132797
|
+
/** `host:port` of the loopback listener — the one upstream the hub proxy may reach. */
|
|
132798
|
+
get upstreamHost() {
|
|
132799
|
+
return `${ALEXA_PROXY_LOOPBACK_HOST}:${this.#opts.proxyPort}`;
|
|
132800
|
+
}
|
|
132801
|
+
/**
|
|
132802
|
+
* Resolves true once the cookie material is in hand, false after `timeoutMs`.
|
|
132803
|
+
* The Amazon page ends on `/cookie-success` BEFORE alexa-cookie2 finished registering
|
|
132804
|
+
* the app, so the hub waits here before it sends the browser to the result page.
|
|
132805
|
+
*/
|
|
132806
|
+
whenCaptured(timeoutMs) {
|
|
132807
|
+
if (this.#captured) return Promise.resolve(true);
|
|
132808
|
+
return new Promise((resolve) => {
|
|
132809
|
+
const timer = setTimeout(() => {
|
|
132810
|
+
this.#waiters.delete(settle);
|
|
132811
|
+
resolve(false);
|
|
132812
|
+
}, timeoutMs);
|
|
132813
|
+
const settle = () => {
|
|
132814
|
+
clearTimeout(timer);
|
|
132815
|
+
resolve(true);
|
|
132816
|
+
};
|
|
132817
|
+
this.#waiters.add(settle);
|
|
132818
|
+
});
|
|
132328
132819
|
}
|
|
132329
132820
|
getStatus() {
|
|
132330
132821
|
return {
|
|
@@ -132356,7 +132847,8 @@ var AlexaProxyLoginSession = class {
|
|
|
132356
132847
|
remote.on("cookie", () => this.#onCookieEvent(remote));
|
|
132357
132848
|
remote.init({
|
|
132358
132849
|
proxyOnly: true,
|
|
132359
|
-
proxyOwnIp:
|
|
132850
|
+
proxyOwnIp: ALEXA_PROXY_LOOPBACK_HOST,
|
|
132851
|
+
proxyListenBind: ALEXA_PROXY_LOOPBACK_HOST,
|
|
132360
132852
|
proxyPort: this.#opts.proxyPort,
|
|
132361
132853
|
proxyLogLevel: "warn",
|
|
132362
132854
|
amazonPage,
|
|
@@ -132402,6 +132894,8 @@ var AlexaProxyLoginSession = class {
|
|
|
132402
132894
|
this.#captured = true;
|
|
132403
132895
|
this.#capturedAt = Date.now();
|
|
132404
132896
|
this.#phase = "captured";
|
|
132897
|
+
for (const settle of this.#waiters) settle();
|
|
132898
|
+
this.#waiters.clear();
|
|
132405
132899
|
this.#logger.info("alexa proxy-login: cookie captured", { meta: { sessionId: this.#opts.sessionId } });
|
|
132406
132900
|
try {
|
|
132407
132901
|
this.#opts.onCookie(this.#opts.sessionId, {
|
|
@@ -132422,10 +132916,7 @@ var AlexaProxyLoginSession = class {
|
|
|
132422
132916
|
if (isProxyReadyError(err)) {
|
|
132423
132917
|
if (this.#phase === "starting") {
|
|
132424
132918
|
this.#phase = "awaiting-login";
|
|
132425
|
-
this.#logger.info("alexa proxy-login: proxy ready — awaiting Amazon login", { meta: {
|
|
132426
|
-
sessionId: this.#opts.sessionId,
|
|
132427
|
-
loginUrl: this.loginUrl
|
|
132428
|
-
} });
|
|
132919
|
+
this.#logger.info("alexa proxy-login: proxy ready — awaiting Amazon login", { meta: { sessionId: this.#opts.sessionId } });
|
|
132429
132920
|
}
|
|
132430
132921
|
return;
|
|
132431
132922
|
}
|
|
@@ -132483,62 +132974,42 @@ function isStringOrObject(value) {
|
|
|
132483
132974
|
return typeof value === "string" || typeof value === "object" && value !== null;
|
|
132484
132975
|
}
|
|
132485
132976
|
//#endregion
|
|
132486
|
-
//#region src/alexa-proxy-
|
|
132487
|
-
/**
|
|
132488
|
-
var
|
|
132489
|
-
|
|
132490
|
-
|
|
132491
|
-
|
|
132492
|
-
|
|
132493
|
-
|
|
132494
|
-
|
|
132495
|
-
|
|
132496
|
-
|
|
132497
|
-
|
|
132498
|
-
|
|
132499
|
-
|
|
132500
|
-
if (host.length === 0) return false;
|
|
132501
|
-
if (IPV4_LITERAL.test(host)) return PRIVATE_IPV4.test(host);
|
|
132502
|
-
if (host.includes(":")) return false;
|
|
132503
|
-
return host.endsWith(".local") || !host.includes(".");
|
|
132504
|
-
}
|
|
132505
|
-
function lanHostOf(address) {
|
|
132977
|
+
//#region src/alexa-proxy-login-manager.ts
|
|
132978
|
+
/** The addon id the result page names to return to. */
|
|
132979
|
+
var ADDON_ID = "import-alexa";
|
|
132980
|
+
/** An attempt lives as long as a sign-in attempt does elsewhere (10 min). */
|
|
132981
|
+
var ATTEMPT_TTL_MS = 600 * 1e3;
|
|
132982
|
+
/** How long the browser waits on the Amazon page for alexa-cookie2 to finish registering. */
|
|
132983
|
+
var CAPTURE_WAIT_MS = 6e4;
|
|
132984
|
+
/**
|
|
132985
|
+
* Terminal condition: alexa-cookie2 turns Amazon's final redirect into a 302 to its own
|
|
132986
|
+
* `/cookie-success` page once it holds the login cookie. Matching on the path of the raw
|
|
132987
|
+
* `Location` keeps this independent of the page's HTML.
|
|
132988
|
+
*/
|
|
132989
|
+
function cookieSuccessTerminal(view) {
|
|
132990
|
+
if (view.location === null) return { kind: "continue" };
|
|
132506
132991
|
try {
|
|
132507
|
-
|
|
132508
|
-
|
|
132992
|
+
return new URL(view.location, `http://127.0.0.1`).pathname === "/cookie-success" ? {
|
|
132993
|
+
kind: "done",
|
|
132994
|
+
payload: true
|
|
132995
|
+
} : { kind: "continue" };
|
|
132509
132996
|
} catch {
|
|
132510
|
-
return
|
|
132511
|
-
}
|
|
132512
|
-
}
|
|
132513
|
-
async function resolveProxyHost(input) {
|
|
132514
|
-
const override = input.override.trim();
|
|
132515
|
-
if (override.length > 0) return override;
|
|
132516
|
-
let addresses;
|
|
132517
|
-
try {
|
|
132518
|
-
addresses = await input.readAppConnections();
|
|
132519
|
-
} catch (err) {
|
|
132520
|
-
input.logger.warn("alexa proxy-login: hub addresses unavailable from local-network", { meta: { error: errMsg(err) } });
|
|
132521
|
-
throw new AlexaProxyHostError("palette-unavailable", "alexa proxy-login: the hub address list is unavailable — set proxyHost in settings", { cause: err });
|
|
132997
|
+
return { kind: "continue" };
|
|
132522
132998
|
}
|
|
132523
|
-
for (const address of addresses) {
|
|
132524
|
-
const host = lanHostOf(address);
|
|
132525
|
-
if (host !== null) return host;
|
|
132526
|
-
}
|
|
132527
|
-
input.logger.warn("alexa proxy-login: no LAN address among the hub appConnections", { meta: { addresses: [...addresses] } });
|
|
132528
|
-
throw new AlexaProxyHostError("no-lan-address", "alexa proxy-login: the hub has no LAN address the browser can open directly — set proxyHost in settings");
|
|
132529
132999
|
}
|
|
132530
|
-
//#endregion
|
|
132531
|
-
//#region src/alexa-proxy-login-manager.ts
|
|
132532
133000
|
var AlexaProxyLoginManager = class {
|
|
132533
133001
|
#logger;
|
|
132534
133002
|
#getProxyConfig;
|
|
132535
|
-
#
|
|
133003
|
+
#getLoginProxy;
|
|
133004
|
+
#captureWaitMs;
|
|
132536
133005
|
#session = null;
|
|
132537
133006
|
#captured = /* @__PURE__ */ new Map();
|
|
133007
|
+
#attemptCloser = null;
|
|
132538
133008
|
constructor(deps) {
|
|
132539
133009
|
this.#logger = deps.logger;
|
|
132540
133010
|
this.#getProxyConfig = deps.getProxyConfig;
|
|
132541
|
-
this.#
|
|
133011
|
+
this.#getLoginProxy = deps.getLoginProxy;
|
|
133012
|
+
this.#captureWaitMs = deps.captureWaitMs ?? CAPTURE_WAIT_MS;
|
|
132542
133013
|
}
|
|
132543
133014
|
/**
|
|
132544
133015
|
* Start a fresh proxy-login. Cancels any in-flight session first (single
|
|
@@ -132546,18 +133017,41 @@ var AlexaProxyLoginManager = class {
|
|
|
132546
133017
|
*/
|
|
132547
133018
|
async start(amazonDomain) {
|
|
132548
133019
|
await this.cancelActive();
|
|
132549
|
-
const
|
|
132550
|
-
|
|
132551
|
-
|
|
132552
|
-
|
|
132553
|
-
logger: this.#logger
|
|
132554
|
-
});
|
|
133020
|
+
const loginProxy = this.#getLoginProxy();
|
|
133021
|
+
if (loginProxy === null) throw new Error("alexa proxy-login: the hub login proxy is not available on this hub");
|
|
133022
|
+
const { proxyPort } = this.#getProxyConfig();
|
|
133023
|
+
const upstreamHost = `${ALEXA_PROXY_LOOPBACK_HOST}:${proxyPort}`;
|
|
132555
133024
|
const sessionId = (0, node_crypto.randomBytes)(9).toString("base64url");
|
|
133025
|
+
loginProxy.open({
|
|
133026
|
+
attemptId: sessionId,
|
|
133027
|
+
expiresAtMs: Date.now() + ATTEMPT_TTL_MS,
|
|
133028
|
+
upstreams: [{
|
|
133029
|
+
host: upstreamHost,
|
|
133030
|
+
scheme: "http"
|
|
133031
|
+
}],
|
|
133032
|
+
terminal: cookieSuccessTerminal,
|
|
133033
|
+
onDone: async () => {
|
|
133034
|
+
return await this.#session?.whenCaptured(this.#captureWaitMs) === true ? {
|
|
133035
|
+
status: "success",
|
|
133036
|
+
addonId: ADDON_ID
|
|
133037
|
+
} : {
|
|
133038
|
+
status: "error",
|
|
133039
|
+
addonId: ADDON_ID,
|
|
133040
|
+
reason: "exchange_failed"
|
|
133041
|
+
};
|
|
133042
|
+
}
|
|
133043
|
+
});
|
|
133044
|
+
const loginUrl = loginProxy.entryPath(sessionId, `http://${upstreamHost}/`);
|
|
133045
|
+
if (loginUrl === null) {
|
|
133046
|
+
loginProxy.close(sessionId);
|
|
133047
|
+
throw new Error("alexa proxy-login: the hub login proxy refused the login address");
|
|
133048
|
+
}
|
|
133049
|
+
this.#attemptCloser = () => loginProxy.close(sessionId);
|
|
132556
133050
|
const session = new AlexaProxyLoginSession({
|
|
132557
133051
|
sessionId,
|
|
132558
133052
|
amazonDomain,
|
|
132559
|
-
proxyOwnIp: host,
|
|
132560
133053
|
proxyPort,
|
|
133054
|
+
loginUrl,
|
|
132561
133055
|
logger: this.#logger,
|
|
132562
133056
|
onCookie: (id, captured) => {
|
|
132563
133057
|
this.#captured.set(id, captured);
|
|
@@ -132568,6 +133062,7 @@ var AlexaProxyLoginManager = class {
|
|
|
132568
133062
|
await session.start();
|
|
132569
133063
|
} catch (err) {
|
|
132570
133064
|
this.#session = null;
|
|
133065
|
+
this.#closeAttempt();
|
|
132571
133066
|
await session.dispose();
|
|
132572
133067
|
throw new Error(`alexa proxy-login: failed to start proxy — ${errMsg(err)}`, { cause: err });
|
|
132573
133068
|
}
|
|
@@ -132601,10 +133096,16 @@ var AlexaProxyLoginManager = class {
|
|
|
132601
133096
|
await this.cancelActive();
|
|
132602
133097
|
this.#captured.delete(sessionId);
|
|
132603
133098
|
}
|
|
133099
|
+
#closeAttempt() {
|
|
133100
|
+
const close = this.#attemptCloser;
|
|
133101
|
+
this.#attemptCloser = null;
|
|
133102
|
+
close?.();
|
|
133103
|
+
}
|
|
132604
133104
|
/** Cancel + dispose whatever session is currently active. */
|
|
132605
133105
|
async cancelActive() {
|
|
132606
133106
|
const session = this.#session;
|
|
132607
133107
|
this.#session = null;
|
|
133108
|
+
this.#closeAttempt();
|
|
132608
133109
|
if (session === null) return;
|
|
132609
133110
|
try {
|
|
132610
133111
|
await session.cancel();
|
|
@@ -136219,10 +136720,7 @@ var SPEAK_PAGE = {
|
|
|
136219
136720
|
/** Default config — a fresh install starts with no accounts. */
|
|
136220
136721
|
var DEFAULTS = {
|
|
136221
136722
|
brokers: [],
|
|
136222
|
-
proxyLogin: {
|
|
136223
|
-
proxyHost: "",
|
|
136224
|
-
proxyPort: 3456
|
|
136225
|
-
},
|
|
136723
|
+
proxyLogin: { proxyPort: 3456 },
|
|
136226
136724
|
speak: {
|
|
136227
136725
|
email: "",
|
|
136228
136726
|
password: "",
|
|
@@ -136268,6 +136766,8 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
136268
136766
|
deviceClasses = Object.fromEntries(ALEXA_DEVICE_TYPES.map((t) => [t, AlexaDevice]));
|
|
136269
136767
|
registry = null;
|
|
136270
136768
|
proxyLogin = null;
|
|
136769
|
+
loginPlane = null;
|
|
136770
|
+
loginProxy = null;
|
|
136271
136771
|
brokerStore = null;
|
|
136272
136772
|
activeBrokers = [];
|
|
136273
136773
|
speak = null;
|
|
@@ -136307,16 +136807,13 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
136307
136807
|
});
|
|
136308
136808
|
alexaCommands.setDispatcher((brokerId, command) => this.requireRegistry().sendCommand(brokerId, command));
|
|
136309
136809
|
await this.registry.restore(this.activeBrokers);
|
|
136810
|
+
await this.serveLoginProxy();
|
|
136310
136811
|
this.proxyLogin = new AlexaProxyLoginManager({
|
|
136311
136812
|
logger: this.ctx.logger,
|
|
136312
136813
|
getProxyConfig: () => {
|
|
136313
|
-
|
|
136314
|
-
return {
|
|
136315
|
-
proxyHost: cfg.proxyHost,
|
|
136316
|
-
proxyPort: cfg.proxyPort
|
|
136317
|
-
};
|
|
136814
|
+
return { proxyPort: alexaProxyLoginSchema.parse(this.config.proxyLogin ?? {}).proxyPort };
|
|
136318
136815
|
},
|
|
136319
|
-
|
|
136816
|
+
getLoginProxy: () => this.loginProxy
|
|
136320
136817
|
});
|
|
136321
136818
|
this.ctx.logger.info("Alexa: import addon initialised", { meta: { brokerCount: this.activeBrokers.length } });
|
|
136322
136819
|
await this.reconcileIntegrationsToBrokers();
|
|
@@ -136448,6 +136945,23 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
136448
136945
|
}
|
|
136449
136946
|
this.speak?.brokersChanged();
|
|
136450
136947
|
}
|
|
136948
|
+
/** The hub's HTTPS face for the Amazon login (D955): admin session only. */
|
|
136949
|
+
async serveLoginProxy() {
|
|
136950
|
+
const proxy = new VendorLoginProxy({
|
|
136951
|
+
addonId: this.ctx.id,
|
|
136952
|
+
logger: this.ctx.logger
|
|
136953
|
+
});
|
|
136954
|
+
try {
|
|
136955
|
+
this.loginPlane = await this.ctx.dataPlane?.serve({
|
|
136956
|
+
prefix: "vendor-login",
|
|
136957
|
+
access: "admin",
|
|
136958
|
+
handler: proxy.handler
|
|
136959
|
+
}) ?? null;
|
|
136960
|
+
if (this.loginPlane !== null) this.loginProxy = proxy;
|
|
136961
|
+
} catch (err) {
|
|
136962
|
+
this.ctx.logger.warn("Alexa: login proxy failed to serve — interactive sign-in is unavailable", { meta: { error: errMsg(err) } });
|
|
136963
|
+
}
|
|
136964
|
+
}
|
|
136451
136965
|
async onShutdown() {
|
|
136452
136966
|
this.speak?.stop();
|
|
136453
136967
|
this.speak = null;
|
|
@@ -136458,6 +136972,9 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
136458
136972
|
this.ctx.logger.warn("Alexa: proxy-login dispose error", { meta: { error: errMsg(err) } });
|
|
136459
136973
|
}
|
|
136460
136974
|
this.proxyLogin = null;
|
|
136975
|
+
await this.loginPlane?.dispose().catch(() => void 0);
|
|
136976
|
+
this.loginPlane = null;
|
|
136977
|
+
this.loginProxy = null;
|
|
136461
136978
|
try {
|
|
136462
136979
|
await this.registry?.shutdown();
|
|
136463
136980
|
} catch (err) {
|
|
@@ -136703,19 +137220,13 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
136703
137220
|
id: "alexa-proxy-login",
|
|
136704
137221
|
title: "Interactive login (proxy)",
|
|
136705
137222
|
titleKey: "import-alexa:globalSettings.s_alexa-proxy-login.title",
|
|
136706
|
-
description: "The \"
|
|
137223
|
+
description: "The \"Sign in\" button in onboarding opens the real Amazon login page through this hub (same address and HTTPS as this page) and captures the cookie after you complete it (incl. 2FA). The port below is only used on the hub itself and is never exposed.",
|
|
136707
137224
|
descriptionKey: "import-alexa:globalSettings.s_alexa-proxy-login.description",
|
|
136708
137225
|
columns: 2,
|
|
136709
137226
|
fields: [{
|
|
136710
|
-
type: "text",
|
|
136711
|
-
key: "proxyLogin.proxyHost",
|
|
136712
|
-
label: "Login proxy host/IP (blank = auto-detect)",
|
|
136713
|
-
labelKey: "import-alexa:globalSettings.s_alexa-proxy-login.proxyLogin_proxyHost.label",
|
|
136714
|
-
placeholder: "auto-detect"
|
|
136715
|
-
}, {
|
|
136716
137227
|
type: "number",
|
|
136717
137228
|
key: "proxyLogin.proxyPort",
|
|
136718
|
-
label: "
|
|
137229
|
+
label: "Internal login port (loopback)",
|
|
136719
137230
|
labelKey: "import-alexa:globalSettings.s_alexa-proxy-login.proxyLogin_proxyPort.label",
|
|
136720
137231
|
min: 1,
|
|
136721
137232
|
max: 65535,
|