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