@camstack/addon-import-alexa 0.2.157 → 0.2.159

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 +13 -0
  2. package/dist/{MotionZonesSettings-Z-NRG681.mjs → MotionZonesSettings-DGPV-8dv.mjs} +2 -2
  3. package/dist/{PrivacyMaskSettings-DiS2hPpi.mjs → PrivacyMaskSettings-Cn85uMbr.mjs} +4 -4
  4. package/dist/{SceneMonitorEditor-BYYK_XwY.mjs → SceneMonitorEditor-gIlEt9fl.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-n3jAYuIE.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-BwgFX4MI.mjs} +1 -1
  8. package/dist/addon.js +844 -160
  9. package/dist/addon.mjs +844 -160
  10. package/dist/{hostInit-DWBMKrvf.mjs → hostInit-CGhFmOcB.mjs} +2 -2
  11. package/dist/{player-overlays-5MrLqX2i.mjs → player-overlays-BHCfJpas.mjs} +1 -1
  12. package/dist/remoteEntry.js +1 -1
  13. package/dist/{responsive-CsfyZVWS.mjs → responsive-Dclc7CfC.mjs} +1 -1
  14. package/dist/{scene-monitor-copy-DOuMmSwJ.mjs → scene-monitor-copy-BcAIeCGo.mjs} +1 -1
  15. package/dist/{square-BGn7yRva.mjs → square-LLOXavH6.mjs} +1 -1
  16. package/dist/{trash-2-BiBTPv8Y.mjs → trash-2-P7HFzsfr.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-C0zVjr31.mjs} +1 -1
  18. package/i18n/en.json +9 -2
  19. package/i18n/it.json +9 -2
  20. package/package.json +2 -2
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,448 @@ 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
+ * Where an OAuth round-trip ends: the admin-ui completion route
90642
+ * (`/sso-complete`, the page every popup sign-in already lands on), with the
90643
+ * OUTCOME in the URL fragment.
90644
+ *
90645
+ * The outcome is a status plus a reason CODE from a closed list. It never
90646
+ * carries a token, a code, a client secret or a free-form error message: the
90647
+ * page words each code itself, and an unknown code reads as `unknown` rather
90648
+ * than being echoed. Fragment only, so nothing here reaches a server log.
90649
+ *
90650
+ * Pure and browser-safe.
90651
+ */
90652
+ /** The admin-ui route the hub's OAuth completions land on (relative to its mount). */
90653
+ var OAUTH_LANDING_PATH = "/sso-complete";
90654
+ /**
90655
+ * The landing URL for `outcome`. `origin` is the hub address the flow already
90656
+ * runs on (from the `local-network` palette role, never a literal); `null`
90657
+ * gives a hub-relative URL.
90658
+ */
90659
+ function oauthLandingUrl(origin, outcome) {
90660
+ const params = new URLSearchParams({
90661
+ return: "oauth",
90662
+ status: outcome.status
90663
+ });
90664
+ if (outcome.addonId !== void 0) params.set("addon", outcome.addonId);
90665
+ if (outcome.brokerId !== void 0) params.set("broker", outcome.brokerId);
90666
+ if (outcome.integration !== void 0) params.set("integration", outcome.integration);
90667
+ if (outcome.status === "error") params.set("reason", outcome.reason ?? "unknown");
90668
+ if (outcome.action === "disconnect") params.set("action", "disconnect");
90669
+ return `${origin === null ? "" : origin.replace(/\/+$/, "")}${OAUTH_LANDING_PATH}#${params.toString()}`;
90670
+ }
90671
+ var LOOPBACK_HOSTS = new Set([
90672
+ "127.0.0.1",
90673
+ "localhost",
90674
+ "[::1]"
90675
+ ]);
90676
+ function hostWithoutPort(host) {
90677
+ if (host.startsWith("[")) return host.slice(0, host.indexOf("]") + 1);
90678
+ const colon = host.indexOf(":");
90679
+ return colon === -1 ? host : host.slice(0, colon);
90680
+ }
90681
+ function isLoopbackHost(host) {
90682
+ return LOOPBACK_HOSTS.has(hostWithoutPort(host.toLowerCase()));
90683
+ }
90684
+ /** An allow-list entry is valid when it is a bare host[:port] and `http` only targets loopback. */
90685
+ function isValidUpstream(upstream) {
90686
+ if (upstream.host.length === 0 || !/^[a-z0-9.[\]:-]+$/.test(upstream.host)) return false;
90687
+ return upstream.scheme === "https" || isLoopbackHost(upstream.host);
90688
+ }
90689
+ function findUpstream(route, host) {
90690
+ const wanted = host.toLowerCase();
90691
+ return route.upstreams.find((upstream) => upstream.host === wanted);
90692
+ }
90693
+ /** `<basePath>/<host><pathname><search>` — what the browser is sent to. */
90694
+ function proxyPathFor(route, host, pathname, search = "") {
90695
+ return `${route.basePath}/${host}${pathname.startsWith("/") ? pathname : `/${pathname}`}${search}`;
90696
+ }
90697
+ /** Split `/<attemptId>/<host>/<rest…>[?query]` (the data-plane view of a request). */
90698
+ function parseProxyPath(rawUrl) {
90699
+ const queryAt = rawUrl.indexOf("?");
90700
+ const path = queryAt === -1 ? rawUrl : rawUrl.slice(0, queryAt);
90701
+ const search = queryAt === -1 ? "" : rawUrl.slice(queryAt);
90702
+ const match = /^\/([A-Za-z0-9_-]{8,128})\/([^/]+)(\/.*)?$/.exec(path);
90703
+ if (match === null) return null;
90704
+ const attemptId = match[1];
90705
+ const host = match[2];
90706
+ if (attemptId === void 0 || host === void 0) return null;
90707
+ return {
90708
+ attemptId,
90709
+ host: host.toLowerCase(),
90710
+ pathname: match[3] ?? "/",
90711
+ search
90712
+ };
90713
+ }
90714
+ /** The vendor URL a proxied request targets, or null when the host would not stay `host`. */
90715
+ function upstreamUrlFor(upstream, parsed) {
90716
+ try {
90717
+ const url = new URL(`${upstream.scheme}://${upstream.host}${parsed.pathname}${parsed.search}`);
90718
+ return url.host.toLowerCase() === upstream.host ? url : null;
90719
+ } catch {
90720
+ return null;
90721
+ }
90722
+ }
90723
+ function escapeRegex(value) {
90724
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
90725
+ }
90726
+ /** A Location (or any URL value) to a vendor host becomes a proxy path; anything else is unchanged. */
90727
+ function rewriteLocation(location, currentUpstream, route) {
90728
+ let resolved;
90729
+ try {
90730
+ resolved = new URL(location, `${currentUpstream.scheme}://${currentUpstream.host}/`);
90731
+ } catch {
90732
+ return location;
90733
+ }
90734
+ if (resolved.protocol !== "https:" && resolved.protocol !== "http:") return location;
90735
+ const target = findUpstream(route, resolved.host);
90736
+ if (target === void 0) return location;
90737
+ return `${proxyPathFor(route, target.host, resolved.pathname, resolved.search)}${resolved.hash}`;
90738
+ }
90739
+ /** Characters that may precede a URL that STARTS a value (never `=`, `?`, `&`, `%3D`). */
90740
+ var VALUE_START = String.raw`(?<=^|["'(\s>,\[{])`;
90741
+ /**
90742
+ * Rewrite vendor URLs in a text body: absolute (`https://host/…`), protocol-relative
90743
+ * (`//host/…`) and JSON-escaped (`https:\/\/host\/…`) when they start a value; and, in
90744
+ * HTML/CSS, root-relative references (`href="/x"`, `url(/x)`) which would otherwise
90745
+ * escape the proxy mount and land on the hub.
90746
+ */
90747
+ function rewriteBody(body, kind, currentUpstream, route) {
90748
+ let out = body;
90749
+ for (const upstream of route.upstreams) {
90750
+ const host = escapeRegex(upstream.host);
90751
+ const plain = new RegExp(`${VALUE_START}(?:https?:)?//${host}(?=[/"'?#\\s)\\]},<]|$)`, "g");
90752
+ out = out.replace(plain, `${route.basePath}/${upstream.host}`);
90753
+ const escapedBase = `${route.basePath}/${upstream.host}`.replace(/\//g, "\\/");
90754
+ const escaped = new RegExp(`${VALUE_START}(?:https?:)?\\\\/\\\\/${host}(?=\\\\/|["'?#\\s)\\]},<]|$)`, "g");
90755
+ out = out.replace(escaped, escapedBase);
90756
+ }
90757
+ const current = `${route.basePath}/${currentUpstream.host}/`;
90758
+ const notMounted = `(?!/)(?!${escapeRegex(route.basePath.slice(1))}/)`;
90759
+ if (kind === "html") {
90760
+ const attribute = String.raw`(\b(?:href|src|action|formaction|poster|data-href|data-src)\s*=\s*["'])/`;
90761
+ out = out.replace(new RegExp(`${attribute}${notMounted}`, "gi"), `$1${current}`);
90762
+ }
90763
+ if (kind === "html" || kind === "css") {
90764
+ const cssUrl = String.raw`(url\(\s*["']?)/`;
90765
+ out = out.replace(new RegExp(`${cssUrl}${notMounted}`, "gi"), `$1${current}`);
90766
+ }
90767
+ return out;
90768
+ }
90769
+ /**
90770
+ * A vendor `Set-Cookie` for the browser: no `Domain` (the cookie belongs to the hub
90771
+ * origin), `Path` narrowed to this attempt's prefix, `Secure` kept (the hub is HTTPS).
90772
+ */
90773
+ function rewriteSetCookie(cookie, route) {
90774
+ const kept = cookie.split(";").map((part) => part.trim()).filter((part, index) => {
90775
+ if (index === 0) return true;
90776
+ const name = part.split("=")[0]?.trim().toLowerCase() ?? "";
90777
+ return name !== "domain" && name !== "path";
90778
+ });
90779
+ kept.push(`Path=${route.basePath}/`);
90780
+ return kept.join("; ");
90781
+ }
90782
+ /**
90783
+ * A browser `Referer` that points into this proxy, turned back into the vendor URL it
90784
+ * stands for. A Referer that is not ours is dropped (null): it names the hub.
90785
+ */
90786
+ function rewriteReferer(referer, route) {
90787
+ let url;
90788
+ try {
90789
+ url = new URL(referer, "http://hub.invalid");
90790
+ } catch {
90791
+ return null;
90792
+ }
90793
+ if (!url.pathname.startsWith(`${route.basePath}/`)) return null;
90794
+ const rest = /^\/([^/]+)(\/.*)?$/.exec(url.pathname.slice(route.basePath.length));
90795
+ const host = rest?.[1]?.toLowerCase();
90796
+ if (host === void 0) return null;
90797
+ const upstream = findUpstream(route, host);
90798
+ if (upstream === void 0) return null;
90799
+ return `${upstream.scheme}://${upstream.host}${rest?.[2] ?? "/"}${url.search}`;
90800
+ }
90801
+ /** Cookies the browser holds for the hub itself must never reach a vendor. */
90802
+ function filterForwardedCookies(cookieHeader) {
90803
+ return cookieHeader.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !part.toLowerCase().startsWith("camstack_")).join("; ");
90804
+ }
90805
+ var MAX_REQUEST_BODY_BYTES = 2 * 1024 * 1024;
90806
+ var MAX_TEXT_BODY_BYTES = 4 * 1024 * 1024;
90807
+ /** Request headers that are never forwarded to the vendor. */
90808
+ var DROP_REQUEST_HEADERS = new Set([
90809
+ "host",
90810
+ "connection",
90811
+ "keep-alive",
90812
+ "proxy-authorization",
90813
+ "proxy-connection",
90814
+ "te",
90815
+ "trailer",
90816
+ "transfer-encoding",
90817
+ "upgrade",
90818
+ "content-length",
90819
+ "accept-encoding",
90820
+ "authorization",
90821
+ "cookie",
90822
+ "origin",
90823
+ "referer",
90824
+ "x-camstack-dataplane-secret",
90825
+ "x-forwarded-for",
90826
+ "x-forwarded-host",
90827
+ "x-forwarded-proto",
90828
+ "x-forwarded-prefix",
90829
+ "x-real-ip",
90830
+ "forwarded"
90831
+ ]);
90832
+ /**
90833
+ * Response headers dropped: framing/CSP/HSTS that break a page under another origin, and
90834
+ * the ones that no longer describe the (decoded, possibly rewritten) body.
90835
+ */
90836
+ var DROP_RESPONSE_HEADERS = new Set([
90837
+ "content-security-policy",
90838
+ "content-security-policy-report-only",
90839
+ "x-frame-options",
90840
+ "strict-transport-security",
90841
+ "public-key-pins",
90842
+ "report-to",
90843
+ "nel",
90844
+ "content-encoding",
90845
+ "content-length",
90846
+ "transfer-encoding",
90847
+ "connection",
90848
+ "keep-alive",
90849
+ "set-cookie",
90850
+ "location"
90851
+ ]);
90852
+ function bodyKindOf(contentType) {
90853
+ const type = contentType.toLowerCase();
90854
+ if (type.includes("text/html") || type.includes("application/xhtml")) return "html";
90855
+ if (type.includes("text/css")) return "css";
90856
+ if (type.startsWith("text/") || type.includes("javascript") || type.includes("json") || type.includes("xml")) return "other";
90857
+ return null;
90858
+ }
90859
+ function firstHeader(value) {
90860
+ return Array.isArray(value) ? value[0] : value;
90861
+ }
90862
+ async function readRequestBody(req) {
90863
+ const chunks = [];
90864
+ let size = 0;
90865
+ for await (const chunk of req) {
90866
+ const buf = typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk);
90867
+ size += buf.length;
90868
+ if (size > MAX_REQUEST_BODY_BYTES) return "too-large";
90869
+ chunks.push(buf);
90870
+ }
90871
+ return Buffer.concat(chunks);
90872
+ }
90873
+ function sendPlain(res, status, text) {
90874
+ res.writeHead(status, {
90875
+ "content-type": "text/plain; charset=utf-8",
90876
+ "cache-control": "no-store"
90877
+ });
90878
+ res.end(text);
90879
+ }
90880
+ var VendorLoginProxy = class {
90881
+ #addonId;
90882
+ #prefix;
90883
+ #logger;
90884
+ #now;
90885
+ #fetch;
90886
+ #attempts = /* @__PURE__ */ new Map();
90887
+ constructor(options) {
90888
+ this.#addonId = options.addonId;
90889
+ this.#prefix = options.prefix;
90890
+ this.#logger = options.logger;
90891
+ this.#now = options.now ?? (() => Date.now());
90892
+ this.#fetch = options.fetchFn ?? globalThis.fetch.bind(globalThis);
90893
+ }
90894
+ /** The `DataPlaneHandler` to pass to `ctx.dataPlane.serve({ prefix, access: 'admin', … })`. */
90895
+ handler = (req, res) => this.#handle(req, res);
90896
+ /** Register an attempt. A one-shot capability: refused hosts are rejected up front. */
90897
+ open(spec) {
90898
+ for (const upstream of spec.upstreams) if (!isValidUpstream(upstream)) throw new Error(`vendor login proxy: upstream ${upstream.host} is not allowed`);
90899
+ this.#attempts.set(spec.attemptId, {
90900
+ expiresAtMs: spec.expiresAtMs,
90901
+ upstreams: spec.upstreams,
90902
+ evaluate: async (view) => {
90903
+ const verdict = await spec.terminal(view);
90904
+ if (verdict.kind === "continue") return verdict;
90905
+ return {
90906
+ kind: "done",
90907
+ landing: await spec.onDone(verdict.payload)
90908
+ };
90909
+ }
90910
+ });
90911
+ }
90912
+ close(attemptId) {
90913
+ this.#attempts.delete(attemptId);
90914
+ }
90915
+ isOpen(attemptId) {
90916
+ const attempt = this.#attempts.get(attemptId);
90917
+ return attempt !== void 0 && this.#now() <= attempt.expiresAtMs;
90918
+ }
90919
+ /**
90920
+ * The hub-relative path that opens `vendorUrl` through the proxy for this attempt:
90921
+ * the URL the browser is sent to. Null when the host is not in the attempt's list.
90922
+ */
90923
+ entryPath(attemptId, vendorUrl) {
90924
+ const attempt = this.#attempts.get(attemptId);
90925
+ if (attempt === void 0) return null;
90926
+ const url = new URL(vendorUrl);
90927
+ if (findUpstream({
90928
+ basePath: "",
90929
+ upstreams: attempt.upstreams
90930
+ }, url.host) === void 0) return null;
90931
+ return `/addon/${this.#addonId}/${this.#prefix}/${attemptId}/${url.host}${url.pathname}${url.search}`;
90932
+ }
90933
+ #routeFor(attemptId, attempt, forwardedPrefix) {
90934
+ return {
90935
+ basePath: `${forwardedPrefix}/addon/${this.#addonId}/${this.#prefix}/${attemptId}`,
90936
+ upstreams: attempt.upstreams
90937
+ };
90938
+ }
90939
+ async #handle(req, res) {
90940
+ try {
90941
+ await this.#serve(req, res);
90942
+ } catch (err) {
90943
+ this.#logger.warn("vendor login proxy: request failed", { meta: {
90944
+ addonId: this.#addonId,
90945
+ error: err instanceof Error ? err.name : "unknown"
90946
+ } });
90947
+ if (!res.headersSent) sendPlain(res, 502, "The sign-in page could not be reached.");
90948
+ else res.end();
90949
+ }
90950
+ }
90951
+ async #serve(req, res) {
90952
+ const parsed = parseProxyPath(req.url ?? "/");
90953
+ if (parsed === null) {
90954
+ sendPlain(res, 404, "Not found");
90955
+ return;
90956
+ }
90957
+ const forwardedPrefix = (firstHeader(req.headers["x-forwarded-prefix"]) ?? "").replace(/\/+$/, "");
90958
+ const attempt = this.#attempts.get(parsed.attemptId);
90959
+ if (attempt === void 0 || this.#now() > attempt.expiresAtMs) {
90960
+ if (attempt !== void 0) this.#attempts.delete(parsed.attemptId);
90961
+ this.#logger.info("vendor login proxy: attempt refused", { meta: {
90962
+ addonId: this.#addonId,
90963
+ reason: attempt === void 0 ? "missing" : "expired"
90964
+ } });
90965
+ this.#redirect(res, forwardedPrefix, {
90966
+ status: "error",
90967
+ addonId: this.#addonId,
90968
+ reason: "invalid_state"
90969
+ });
90970
+ return;
90971
+ }
90972
+ const route = this.#routeFor(parsed.attemptId, attempt, forwardedPrefix);
90973
+ const upstream = findUpstream(route, parsed.host);
90974
+ const target = upstream === void 0 ? null : upstreamUrlFor(upstream, parsed);
90975
+ if (upstream === void 0 || target === null) {
90976
+ this.#logger.warn("vendor login proxy: host refused", { meta: {
90977
+ addonId: this.#addonId,
90978
+ host: parsed.host
90979
+ } });
90980
+ sendPlain(res, 404, "Not found");
90981
+ return;
90982
+ }
90983
+ const method = (req.method ?? "GET").toUpperCase();
90984
+ let body;
90985
+ if (method !== "GET" && method !== "HEAD") {
90986
+ const read = await readRequestBody(req);
90987
+ if (read === "too-large") {
90988
+ sendPlain(res, 413, "Request too large");
90989
+ return;
90990
+ }
90991
+ body = read;
90992
+ }
90993
+ const headers = this.#requestHeaders(req, route, upstream);
90994
+ const upstreamRes = await this.#fetch(target, {
90995
+ method,
90996
+ headers,
90997
+ redirect: "manual",
90998
+ ...body !== void 0 && body.length > 0 ? { body: new Uint8Array(body) } : {}
90999
+ });
91000
+ const location = upstreamRes.headers.get("location");
91001
+ const setCookies = upstreamRes.headers.getSetCookie();
91002
+ const kind = bodyKindOf(upstreamRes.headers.get("content-type") ?? "");
91003
+ const declaredLength = Number(upstreamRes.headers.get("content-length") ?? "0");
91004
+ const text = kind !== null && !(declaredLength > MAX_TEXT_BODY_BYTES) && method !== "HEAD" ? await upstreamRes.text() : null;
91005
+ this.#logger.debug("vendor login proxy: upstream answered", { meta: {
91006
+ addonId: this.#addonId,
91007
+ host: upstream.host,
91008
+ status: upstreamRes.status,
91009
+ method
91010
+ } });
91011
+ const settled = await attempt.evaluate({
91012
+ status: upstreamRes.status,
91013
+ url: target.toString(),
91014
+ location,
91015
+ setCookies,
91016
+ bodyText: text
91017
+ });
91018
+ if (settled.kind === "done") {
91019
+ this.#attempts.delete(parsed.attemptId);
91020
+ this.#logger.info("vendor login proxy: sign-in finished", { meta: {
91021
+ addonId: this.#addonId,
91022
+ status: settled.landing.status
91023
+ } });
91024
+ this.#redirect(res, forwardedPrefix, settled.landing);
91025
+ return;
91026
+ }
91027
+ const out = {};
91028
+ upstreamRes.headers.forEach((value, name) => {
91029
+ if (!DROP_RESPONSE_HEADERS.has(name.toLowerCase())) out[name] = value;
91030
+ });
91031
+ if (setCookies.length > 0) out["set-cookie"] = setCookies.map((c) => rewriteSetCookie(c, route));
91032
+ if (location !== null) out["location"] = rewriteLocation(location, upstream, route);
91033
+ out["cache-control"] = "no-store";
91034
+ if (text !== null && kind !== null) {
91035
+ const rewritten = rewriteBody(text, kind, upstream, route);
91036
+ res.writeHead(upstreamRes.status, out);
91037
+ res.end(rewritten);
91038
+ return;
91039
+ }
91040
+ res.writeHead(upstreamRes.status, out);
91041
+ await this.#pipeBody(upstreamRes, res);
91042
+ }
91043
+ #requestHeaders(req, route, upstream) {
91044
+ const headers = {};
91045
+ for (const [name, value] of Object.entries(req.headers)) {
91046
+ const lower = name.toLowerCase();
91047
+ if (DROP_REQUEST_HEADERS.has(lower) || value === void 0) continue;
91048
+ headers[lower] = Array.isArray(value) ? value.join(", ") : value;
91049
+ }
91050
+ const cookie = filterForwardedCookies(firstHeader(req.headers["cookie"]) ?? "");
91051
+ if (cookie.length > 0) headers["cookie"] = cookie;
91052
+ if (req.headers["origin"] !== void 0) headers["origin"] = `${upstream.scheme}://${upstream.host}`;
91053
+ const referer = firstHeader(req.headers["referer"]);
91054
+ if (referer !== void 0) {
91055
+ const back = rewriteReferer(referer, route);
91056
+ if (back !== null) headers["referer"] = back;
91057
+ }
91058
+ return headers;
91059
+ }
91060
+ async #pipeBody(upstreamRes, res) {
91061
+ if (upstreamRes.body === null) {
91062
+ res.end();
91063
+ return;
91064
+ }
91065
+ for await (const chunk of upstreamRes.body) {
91066
+ if (!res.write(chunk)) await new Promise((resolve) => {
91067
+ res.once("drain", resolve);
91068
+ res.once("close", resolve);
91069
+ });
91070
+ if (res.destroyed) return;
91071
+ }
91072
+ res.end();
91073
+ }
91074
+ #redirect(res, forwardedPrefix, outcome) {
91075
+ res.writeHead(302, {
91076
+ location: `${forwardedPrefix}${oauthLandingUrl(null, outcome)}`,
91077
+ "cache-control": "no-store"
91078
+ });
91079
+ res.end();
91080
+ }
91081
+ };
90640
91082
  var MB = 1024 * 1024;
90641
91083
  1024 * MB, 3072 * MB;
90642
91084
  //#endregion
@@ -90688,18 +91130,12 @@ var AlexaBrokerEntrySchema = object({
90688
91130
  });
90689
91131
  /**
90690
91132
  * Interactive proxy-login settings (addon-global, not per-broker). alexa-remote2
90691
- * spins a local reverse-proxy to Amazon's login page on `proxyHost:proxyPort`;
90692
- * the operator opens that URL and logs in. `proxyHost` MUST be the host:port the
90693
- * operator's browser uses (alexa-cookie2 rewrites Amazon URLs to absolute
90694
- * `http://<proxyHost>:<proxyPort>/...`), so it is a directly-reachable host —
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.
91133
+ * spins a local reverse-proxy to Amazon's login page on `127.0.0.1:proxyPort`; the
91134
+ * operator never reaches it directly — the hub's login proxy (D955) opens it at
91135
+ * `/addon/import-alexa/login/…` under the hub's own HTTPS origin. The port only has to
91136
+ * be free on the hub's loopback.
90698
91137
  */
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
- });
91138
+ 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
91139
  /**
90704
91140
  * Echo speech settings. Speech signs in as an import broker, so the targets
90705
91141
  * are what lives here. `email`, `password`, `region` and `session` are the OLD
@@ -90746,10 +91182,7 @@ function readSpeakSettings(config) {
90746
91182
  /** Top-level addon config — broker entries + the proxy-login settings. */
90747
91183
  var alexaAddonConfigSchema = object({
90748
91184
  brokers: array(AlexaBrokerEntrySchema).default([]),
90749
- proxyLogin: alexaProxyLoginSchema.default({
90750
- proxyHost: "",
90751
- proxyPort: 3456
90752
- }),
91185
+ proxyLogin: alexaProxyLoginSchema.default({ proxyPort: 3456 }),
90753
91186
  speak: alexaSpeakSettingsSchema.default({
90754
91187
  email: "",
90755
91188
  password: "",
@@ -128426,7 +128859,7 @@ var require_fix_request_body = /* @__PURE__ */ __commonJSMin(((exports) => {
128426
128859
  * Fix proxied body if bodyParser is involved.
128427
128860
  */
128428
128861
  function fixRequestBody(proxyReq, req) {
128429
- if (req.readableLength !== 0) return;
128862
+ if (!req.readableEnded) return;
128430
128863
  const requestBody = req.body;
128431
128864
  if (!requestBody) return;
128432
128865
  const contentType = proxyReq.getHeader("Content-Type");
@@ -128521,6 +128954,12 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
128521
128954
  var path$2 = require("path");
128522
128955
  var crypto$1 = require("crypto");
128523
128956
  var FORMERDATA_STORE_VERSION = 4;
128957
+ var ALEXA_APP_VERSION = "2.2.651540.0";
128958
+ var EXTRA_AMAZON_PROXY_HOSTS = [
128959
+ "amazon.com",
128960
+ "www.amazon.com",
128961
+ "alexa.amazon.com"
128962
+ ];
128524
128963
  function addCookies(Cookie, headers) {
128525
128964
  if (!headers || !headers["set-cookie"]) return Cookie;
128526
128965
  const cookies = cookieTools.parse(Cookie);
@@ -128536,6 +128975,15 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
128536
128975
  Cookie = Cookie.replace(/[; ]*$/, "");
128537
128976
  return Cookie;
128538
128977
  }
128978
+ function isAmazonCookieName(name) {
128979
+ return /^(session-id|session-id-time|session-token|ubid-.+|lc-.+|x-.+|at-.+|sess-at-.+|frc|map-md|csrf|sid|csm-hit|i18n-prefs|sp-cdn|skin)$/.test(name);
128980
+ }
128981
+ function sanitizeAmazonCookie(cookie) {
128982
+ const cookies = cookieTools.parse(cookie || "");
128983
+ let sanitizedCookie = "";
128984
+ for (const name of Object.keys(cookies)) if (isAmazonCookieName(name)) sanitizedCookie += `${name}=${cookies[name]}; `;
128985
+ return sanitizedCookie.replace(/[; ]*$/, "");
128986
+ }
128539
128987
  function customStringify(v, func, intent) {
128540
128988
  const cache = /* @__PURE__ */ new Map();
128541
128989
  return JSON.stringify(v, function(key, value) {
@@ -128549,6 +128997,29 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
128549
128997
  }
128550
128998
  function initAmazonProxy(_options, callbackCookie, callbackListening) {
128551
128999
  const initialCookies = {};
129000
+ const proxyBase = () => `http://${_options.proxyOwnIp}:${_options.proxyPort}/`;
129001
+ const defaultAmazonHost = `www.${_options.baseAmazonPage}`;
129002
+ const amazonProxyHosts = [];
129003
+ function escapeRegex(data) {
129004
+ return data.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
129005
+ }
129006
+ function addAmazonProxyHost(host) {
129007
+ if (host && !amazonProxyHosts.includes(host)) amazonProxyHosts.push(host);
129008
+ }
129009
+ addAmazonProxyHost(defaultAmazonHost);
129010
+ addAmazonProxyHost(`alexa.${_options.baseAmazonPage}`);
129011
+ addAmazonProxyHost(_options.baseAmazonPage);
129012
+ EXTRA_AMAZON_PROXY_HOSTS.forEach(addAmazonProxyHost);
129013
+ function amazonHostFromProxyPath(url) {
129014
+ const match = (url || "").match(/^\/([^/?#]+)(?:[/?#]|$)/);
129015
+ if (match && amazonProxyHosts.includes(match[1])) return match[1];
129016
+ return null;
129017
+ }
129018
+ function amazonHostFromProxyUrl(url) {
129019
+ if (!url) return null;
129020
+ for (const host of amazonProxyHosts) if (url === `${proxyBase()}${host}` || url.startsWith(`${proxyBase()}${host}/`)) return host;
129021
+ return null;
129022
+ }
128552
129023
  const formerDataStorePath = _options.formerDataStorePath || path$2.join(__dirname, "formerDataStore.json");
128553
129024
  let formerDataStoreValid = false;
128554
129025
  if (!_options.formerRegistrationData) try {
@@ -128572,7 +129043,7 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
128572
129043
  _options.logger && _options.logger("Proxy Init: reuse frc from former data");
128573
129044
  initialCookies.frc = _options.formerRegistrationData.frc;
128574
129045
  }
128575
- if (!_options.formerRegistrationData || !_options.formerRegistrationData["map-md"]) initialCookies["map-md"] = Buffer.from("{\"device_user_dictionary\":[],\"device_registration_data\":{\"software_version\":\"1\"},\"app_identifier\":{\"app_version\":\"2.2.485407\",\"bundle_id\":\"com.amazon.echo\"}}").toString("base64");
129046
+ if (!_options.formerRegistrationData || !_options.formerRegistrationData["map-md"]) initialCookies["map-md"] = Buffer.from(`{"device_user_dictionary":[],"device_registration_data":{"software_version":"1"},"app_identifier":{"app_version":"${ALEXA_APP_VERSION}","bundle_id":"com.amazon.echo"}}`).toString("base64");
128576
129047
  else {
128577
129048
  _options.logger && _options.logger("Proxy Init: reuse map-md from former data");
128578
129049
  initialCookies["map-md"] = _options.formerRegistrationData["map-md"];
@@ -128605,11 +129076,49 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
128605
129076
  const code_verifier = base64URLEncode(crypto$1.randomBytes(32));
128606
129077
  const code_challenge = base64URLEncode(sha256(code_verifier));
128607
129078
  let proxyCookies = "";
129079
+ let returnedInitUrl;
129080
+ function buildInitialUrl() {
129081
+ return `https://www.${_options.baseAmazonPage}/ap/signin?openid.return_to=https%3A%2F%2Fwww.${_options.baseAmazonPage}%2Fap%2Fmaplanding&openid.assoc_handle=amzn_dp_project_dee_ios${_options.baseAmazonPageHandle}&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&pageId=amzn_dp_project_dee_ios${_options.baseAmazonPageHandle}&accountStatusPolicy=P1&openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.mode=checkid_setup&openid.ns.oa2=http%3A%2F%2Fwww.${_options.baseAmazonPage}%2Fap%2Fext%2Foauth%2F2&openid.oa2.client_id=device%3A${deviceId}&openid.ns.pape=http%3A%2F%2Fspecs.openid.net%2Fextensions%2Fpape%2F1.0&openid.oa2.response_type=code&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&openid.pape.max_auth_age=0&openid.oa2.scope=device_auth_access&openid.oa2.code_challenge_method=S256&openid.oa2.code_challenge=${code_challenge}&language=${_options.amazonPageProxyLanguage}`;
129082
+ }
129083
+ function fixEmbeddedProxyUrls(pathAndQuery) {
129084
+ const qIndex = pathAndQuery.indexOf("?");
129085
+ if (qIndex === -1) return pathAndQuery;
129086
+ const p = pathAndQuery.slice(0, qIndex);
129087
+ const qs = pathAndQuery.slice(qIndex + 1);
129088
+ let params;
129089
+ try {
129090
+ params = new URLSearchParams(qs);
129091
+ } catch (_e) {
129092
+ return pathAndQuery;
129093
+ }
129094
+ let changed = false;
129095
+ for (const key of params.keys()) {
129096
+ const value = params.get(key);
129097
+ const fixed = replaceHostsBack(value);
129098
+ if (fixed !== value) {
129099
+ params.set(key, fixed);
129100
+ changed = true;
129101
+ }
129102
+ }
129103
+ if (changed) _options.logger && _options.logger(`Alexa-Cookie: Fixed embedded proxy URL(s) in outgoing request query`);
129104
+ return changed ? `${p}?${params.toString()}` : pathAndQuery;
129105
+ }
129106
+ function rewriteProxyPath(url, req) {
129107
+ let result;
129108
+ const urlHost = amazonHostFromProxyPath(url);
129109
+ if (urlHost) result = url.replace(new RegExp(`^/${escapeRegex(urlHost)}`), "") || "/";
129110
+ else if (req && req.headers && req.headers.host === `${_options.proxyOwnIp}:${_options.proxyPort}` && url === "/") {
129111
+ const initialUrl = returnedInitUrl || buildInitialUrl();
129112
+ const parsed = new URL(initialUrl);
129113
+ result = `${parsed.pathname}${parsed.search}`;
129114
+ } else result = url;
129115
+ return fixEmbeddedProxyUrls(result);
129116
+ }
128608
129117
  const optionsAlexa = {
128609
129118
  target: `https://alexa.${_options.baseAmazonPage}`,
128610
129119
  changeOrigin: true,
128611
129120
  ws: false,
128612
- pathRewrite: {},
129121
+ pathRewrite: rewriteProxyPath,
128613
129122
  router,
128614
129123
  hostRewrite: true,
128615
129124
  followRedirects: false,
@@ -128617,17 +129126,10 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
128617
129126
  onError,
128618
129127
  onProxyRes,
128619
129128
  onProxyReq,
128620
- headers: {
128621
- "user-agent": "AppleWebKit PitanguiBridge/2.2.485407.0-[HARDWARE=iPhone10_4][SOFTWARE=15.5][DEVICE=iPhone]",
128622
- "accept-language": _options.acceptLanguage,
128623
- "authority": `www.${_options.baseAmazonPage}`
128624
- },
128625
- cookieDomainRewrite: { "*": "" }
129129
+ headers: { "accept-language": _options.acceptLanguage },
129130
+ cookieDomainRewrite: { "*": "" },
129131
+ cookiePathRewrite: { "*": "/" }
128626
129132
  };
128627
- optionsAlexa.pathRewrite[`^/www.${_options.baseAmazonPage}`] = "";
128628
- optionsAlexa.pathRewrite[`^/alexa.${_options.baseAmazonPage}`] = "";
128629
- optionsAlexa.cookieDomainRewrite[`.${_options.baseAmazonPage}`] = _options.proxyOwnIp;
128630
- optionsAlexa.cookieDomainRewrite[_options.baseAmazonPage] = _options.proxyOwnIp;
128631
129133
  if (_options.logger) optionsAlexa.logProvider = function logProvider() {
128632
129134
  return {
128633
129135
  log: _options.logger.log || _options.logger,
@@ -128637,22 +129139,20 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
128637
129139
  error: _options.logger.error || _options.logger
128638
129140
  };
128639
129141
  };
128640
- let returnedInitUrl;
128641
129142
  function router(req) {
128642
129143
  const url = req.originalUrl || req.url;
128643
129144
  _options.logger && _options.logger(`Router: ${url} / ${req.method} / ${JSON.stringify(req.headers)}`);
128644
129145
  if (req.headers.host === `${_options.proxyOwnIp}:${_options.proxyPort}`) {
128645
- if (url.startsWith(`/www.${_options.baseAmazonPage}/`)) return `https://www.${_options.baseAmazonPage}`;
128646
- else if (url.startsWith(`/alexa.${_options.baseAmazonPage}/`)) return `https://alexa.${_options.baseAmazonPage}`;
128647
- else if (req.headers.referer) {
128648
- if (req.headers.referer.startsWith(`http://${_options.proxyOwnIp}:${_options.proxyPort}/www.${_options.baseAmazonPage}/`)) return `https://www.${_options.baseAmazonPage}`;
128649
- else if (req.headers.referer.startsWith(`http://${_options.proxyOwnIp}:${_options.proxyPort}/alexa.${_options.baseAmazonPage}/`)) return `https://alexa.${_options.baseAmazonPage}`;
128650
- }
129146
+ const urlHost = amazonHostFromProxyPath(url);
129147
+ if (urlHost) return `https://${urlHost}`;
129148
+ const refererHost = amazonHostFromProxyUrl(req.headers.referer);
129149
+ if (refererHost) return `https://${refererHost}`;
128651
129150
  if (url === "/") {
128652
- returnedInitUrl = `https://www.${_options.baseAmazonPage}/ap/signin?openid.return_to=https%3A%2F%2Fwww.${_options.baseAmazonPage}%2Fap%2Fmaplanding&openid.assoc_handle=amzn_dp_project_dee_ios${_options.baseAmazonPageHandle}&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&pageId=amzn_dp_project_dee_ios${_options.baseAmazonPageHandle}&accountStatusPolicy=P1&openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.mode=checkid_setup&openid.ns.oa2=http%3A%2F%2Fwww.${_options.baseAmazonPage}%2Fap%2Fext%2Foauth%2F2&openid.oa2.client_id=device%3A${deviceId}&openid.ns.pape=http%3A%2F%2Fspecs.openid.net%2Fextensions%2Fpape%2F1.0&openid.oa2.response_type=code&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&openid.pape.max_auth_age=0&openid.oa2.scope=device_auth_access&openid.oa2.code_challenge_method=S256&openid.oa2.code_challenge=${code_challenge}&language=${_options.amazonPageProxyLanguage}`;
129151
+ returnedInitUrl = buildInitialUrl();
128653
129152
  _options.logger && _options.logger(`Alexa-Cookie: Initial Page Request: ${returnedInitUrl}`);
128654
- return returnedInitUrl;
128655
- } else return `https://www.${_options.baseAmazonPage}`;
129153
+ const parsedInitUrl = new URL(returnedInitUrl);
129154
+ return `${parsedInitUrl.protocol}//${parsedInitUrl.host}`;
129155
+ } else return `https://${defaultAmazonHost}`;
128656
129156
  }
128657
129157
  return `https://alexa.${_options.baseAmazonPage}`;
128658
129158
  }
@@ -128664,21 +129164,34 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
128664
129164
  } catch (err) {}
128665
129165
  }
128666
129166
  function replaceHosts(data) {
128667
- const amazonRegex = new RegExp(`https?://www.${_options.baseAmazonPage}:?[0-9]*/`.replace(/\./g, "\\."), "g");
128668
- const alexaRegex = new RegExp(`https?://alexa.${_options.baseAmazonPage}:?[0-9]*/`.replace(/\./g, "\\."), "g");
128669
129167
  data = data.replace(/&#x2F;/g, "/");
128670
- data = data.replace(amazonRegex, `http://${_options.proxyOwnIp}:${_options.proxyPort}/www.${_options.baseAmazonPage}/`);
128671
- data = data.replace(alexaRegex, `http://${_options.proxyOwnIp}:${_options.proxyPort}/alexa.${_options.baseAmazonPage}/`);
129168
+ for (const host of amazonProxyHosts) {
129169
+ const hostRegex = new RegExp(`https?://${escapeRegex(host)}:?[0-9]*/`, "g");
129170
+ data = data.replace(hostRegex, `${proxyBase()}${host}/`);
129171
+ }
128672
129172
  return data;
128673
129173
  }
128674
129174
  function replaceHostsBack(data) {
128675
- const amazonRegex = new RegExp(`http://${_options.proxyOwnIp}:${_options.proxyPort}/www.${_options.baseAmazonPage}/`.replace(/\./g, "\\."), "g");
128676
- const alexaRegex = new RegExp(`http://${_options.proxyOwnIp}:${_options.proxyPort}/alexa.${_options.baseAmazonPage}/`.replace(/\./g, "\\."), "g");
128677
- data = data.replace(amazonRegex, `https://www.${_options.baseAmazonPage}/`);
128678
- data = data.replace(alexaRegex, `https://alexa.${_options.baseAmazonPage}/`);
128679
- if (data === `http://${_options.proxyOwnIp}:${_options.proxyPort}/`) data = returnedInitUrl;
129175
+ const base = proxyBase();
129176
+ for (const host of amazonProxyHosts) {
129177
+ const hostRegex = new RegExp(`${escapeRegex(base)}${escapeRegex(host)}/`, "g");
129178
+ data = data.replace(hostRegex, `https://${host}/`);
129179
+ }
129180
+ if (data === base) data = returnedInitUrl;
129181
+ else if (data.startsWith(base)) data = `https://${defaultAmazonHost}/${data.slice(base.length)}`;
128680
129182
  return data;
128681
129183
  }
129184
+ function parseQueryParams(data) {
129185
+ const paramStart = data && data.indexOf("?");
129186
+ if (paramStart === -1 || paramStart === void 0) return {};
129187
+ return querystring$2.parse(data.substr(paramStart + 1));
129188
+ }
129189
+ function isProxySuccessUrl(data) {
129190
+ if (!data) return false;
129191
+ if (data.includes("/spa/index.html")) return true;
129192
+ if (!data.includes("/ap/maplanding?")) return false;
129193
+ return !!parseQueryParams(data)["openid.oa2.authorization_code"];
129194
+ }
128682
129195
  function onProxyReq(proxyReq, req) {
128683
129196
  const url = req.originalUrl || req.url;
128684
129197
  if (url.endsWith(".ico") || url.endsWith(".js") || url.endsWith(".ttf") || url.endsWith(".svg") || url.endsWith(".png") || url.endsWith(".appcache")) return;
@@ -128705,14 +129218,26 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
128705
129218
  }
128706
129219
  }
128707
129220
  if (typeof proxyReq.getHeader === "function" && proxyReq.getHeader("origin") !== `https://${proxyReq.getHeader("host")}`) {
128708
- proxyReq.setHeader("origin", `https://www.${_options.baseAmazonPage}`);
128709
- _options.logger && _options.logger("Alexa-Cookie: Modify headers: Delete Origin");
129221
+ proxyReq.setHeader("origin", `https://${proxyReq.getHeader("host") || defaultAmazonHost}`);
129222
+ _options.logger && _options.logger("Alexa-Cookie: Modify headers: Changed Origin");
128710
129223
  modified = true;
128711
129224
  }
128712
- let postBody = "";
128713
- req.on("data", (chunk) => {
128714
- postBody += chunk.toString();
128715
- });
129225
+ if (req.body && typeof req.body === "object") {
129226
+ let bodyChanged = false;
129227
+ for (const key of Object.keys(req.body)) {
129228
+ const value = req.body[key];
129229
+ if (typeof value !== "string") continue;
129230
+ const fixed = replaceHostsBack(value);
129231
+ if (fixed !== value) {
129232
+ req.body[key] = fixed;
129233
+ bodyChanged = true;
129234
+ }
129235
+ }
129236
+ if (bodyChanged) _options.logger && _options.logger("Alexa-Cookie: Fixed embedded proxy URL(s) in outgoing POST body");
129237
+ const bodyData = querystring$2.stringify(req.body);
129238
+ proxyReq.setHeader("Content-Length", Buffer.byteLength(bodyData));
129239
+ proxyReq.write(bodyData);
129240
+ }
128716
129241
  }
128717
129242
  _options.proxyLogLevel === "debug" && _options.logger && _options.logger(`Alexa-Cookie: Proxy-Request: (modified:${modified})${customStringify(proxyReq, null, 2)}`);
128718
129243
  }
@@ -128730,17 +129255,22 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
128730
129255
  proxyCookies = addCookies(proxyCookies, proxyRes.headers);
128731
129256
  }
128732
129257
  _options.logger && _options.logger(`Alexa-Cookie: Cookies handled: ${JSON.stringify(proxyCookies)}`);
128733
- if (proxyRes.socket && proxyRes.socket._host === `www.${_options.baseAmazonPage}` && proxyRes.socket.parser.outgoing && proxyRes.socket.parser.outgoing.method === "GET" && proxyRes.socket.parser.outgoing.path.startsWith("/ap/maplanding") || proxyRes.socket && proxyRes.socket.parser.outgoing && proxyRes.socket.parser.outgoing.getHeader("location") && proxyRes.socket.parser.outgoing.getHeader("location").includes("/ap/maplanding?") || proxyRes.headers.location && (proxyRes.headers.location.includes("/ap/maplanding?") || proxyRes.headers.location.includes("/spa/index.html"))) {
129258
+ const outgoing = proxyRes.socket && proxyRes.socket.parser && proxyRes.socket.parser.outgoing;
129259
+ const successUrl = [
129260
+ proxyRes.headers.location,
129261
+ outgoing && outgoing.method === "GET" && outgoing.path,
129262
+ outgoing && outgoing.getHeader && outgoing.getHeader("location")
129263
+ ].find(isProxySuccessUrl);
129264
+ if (successUrl) {
128734
129265
  _options.logger && _options.logger("Alexa-Cookie: Proxy detected SUCCESS!!");
128735
- const paramStart = proxyRes.headers.location.indexOf("?");
128736
- const queryParams = querystring$2.parse(proxyRes.headers.location.substr(paramStart + 1));
129266
+ const queryParams = parseQueryParams(successUrl);
128737
129267
  proxyRes.statusCode = 302;
128738
129268
  proxyRes.headers.location = `http://${_options.proxyOwnIp}:${_options.proxyPort}/cookie-success`;
128739
129269
  delete proxyRes.headers.referer;
128740
129270
  _options.logger && _options.logger(`Alexa-Cookie: Proxy catched cookie: ${proxyCookies}`);
128741
129271
  _options.logger && _options.logger(`Alexa-Cookie: Proxy catched parameters: ${JSON.stringify(queryParams)}`);
128742
129272
  callbackCookie && callbackCookie(null, {
128743
- "loginCookie": proxyCookies,
129273
+ "loginCookie": sanitizeAmazonCookie(proxyCookies),
128744
129274
  "authorization_code": queryParams["openid.oa2.authorization_code"],
128745
129275
  "frc": initialCookies.frc,
128746
129276
  "map-md": initialCookies["map-md"],
@@ -128752,7 +129282,7 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
128752
129282
  if (proxyRes.headers.location) {
128753
129283
  _options.logger && _options.logger(`Redirect: Original Location ----> ${proxyRes.headers.location}`);
128754
129284
  proxyRes.headers.location = replaceHosts(proxyRes.headers.location);
128755
- if (reqestHost && proxyRes.headers.location.startsWith("/")) proxyRes.headers.location = `http://${_options.proxyOwnIp}:${_options.proxyPort}/${reqestHost}${proxyRes.headers.location}`;
129285
+ if (reqestHost && amazonProxyHosts.includes(reqestHost) && proxyRes.headers.location.startsWith("/")) proxyRes.headers.location = `http://${_options.proxyOwnIp}:${_options.proxyPort}/${reqestHost}${proxyRes.headers.location}`;
128756
129286
  _options.logger && _options.logger(`Redirect: Final Location ----> ${proxyRes.headers.location}`);
128757
129287
  return;
128758
129288
  }
@@ -128770,8 +129300,21 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
128770
129300
  return body;
128771
129301
  });
128772
129302
  }
129303
+ function bufferUrlencodedBody(req, res, next) {
129304
+ const contentType = req.headers["content-type"] || "";
129305
+ if (req.method !== "POST" || contentType.indexOf("application/x-www-form-urlencoded") === -1) return next();
129306
+ let raw = "";
129307
+ req.on("data", (chunk) => {
129308
+ raw += chunk;
129309
+ });
129310
+ req.on("end", () => {
129311
+ req.body = querystring$2.parse(raw);
129312
+ next();
129313
+ });
129314
+ }
128773
129315
  const myProxy = proxy("!/cookie-success", optionsAlexa);
128774
129316
  const app = express();
129317
+ app.use(bufferUrlencodedBody);
128775
129318
  app.use(myProxy);
128776
129319
  app.get("/cookie-success", function(req, res) {
128777
129320
  res.send(_options.proxyCloseWindowHTML);
@@ -128781,7 +129324,8 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
128781
129324
  _options.proxyPort = void 0;
128782
129325
  }
128783
129326
  const server = app.listen(_options.proxyPort, _options.proxyListenBind, function() {
128784
- _options.logger && _options.logger(`Alexa-Cookie: Proxy-Server listening on port ${server.address().port}`);
129327
+ _options.proxyPort = this.address().port;
129328
+ _options.logger && _options.logger(`Alexa-Cookie: Proxy-Server listening on port ${_options.proxyPort}`);
128785
129329
  callbackListening && callbackListening(server);
128786
129330
  callbackListening = null;
128787
129331
  }).on("error", (err) => {
@@ -128827,6 +129371,13 @@ var require_alexa_cookie = /* @__PURE__ */ __commonJSMin(((exports, module) => {
128827
129371
  let proxyServer;
128828
129372
  let _options;
128829
129373
  let Cookie = "";
129374
+ const isAmazonCookieName = (name) => /^(session-id|session-id-time|session-token|ubid-.+|lc-.+|x-.+|at-.+|sess-at-.+|frc|map-md|csrf|sid|csm-hit|i18n-prefs|sp-cdn|skin)$/.test(name);
129375
+ const sanitizeAmazonCookie = (cookie) => {
129376
+ const cookies = cookieTools.parse(cookie || "");
129377
+ let sanitizedCookie = "";
129378
+ for (const name of Object.keys(cookies)) if (isAmazonCookieName(name)) sanitizedCookie += `${name}=${cookies[name]}; `;
129379
+ return sanitizedCookie.replace(/[; ]*$/, "");
129380
+ };
128830
129381
  const addCookies = (Cookie, headers) => {
128831
129382
  if (!headers || !headers["set-cookie"]) return Cookie;
128832
129383
  const cookies = cookieTools.parse(Cookie || "");
@@ -128914,7 +129465,7 @@ var require_alexa_cookie = /* @__PURE__ */ __commonJSMin(((exports, module) => {
128914
129465
  if (!_options.baseAmazonPageHandle && _options.baseAmazonPageHandle !== "") {
128915
129466
  const amazonDomain = _options.baseAmazonPage.substr(_options.baseAmazonPage.lastIndexOf(".") + 1);
128916
129467
  if (amazonDomain === "jp") _options.baseAmazonPageHandle = `_${amazonDomain}`;
128917
- else if (amazonDomain !== "com") _options.baseAmazonPageHandle = "";
129468
+ else if (amazonDomain !== "com") _options.baseAmazonPageHandle = `_${amazonDomain}`;
128918
129469
  else _options.baseAmazonPageHandle = "";
128919
129470
  }
128920
129471
  if (!_options.userAgent) if (os$1.platform() === "win32") _options.userAgent = defaultUserAgent;
@@ -128941,7 +129492,7 @@ var require_alexa_cookie = /* @__PURE__ */ __commonJSMin(((exports, module) => {
128941
129492
  if (_options.formerRegistrationData) _options.proxyOnly = true;
128942
129493
  };
128943
129494
  const getCSRFFromCookies = (cookie, _options, callback) => {
128944
- const csrfUrls = csrfOptions;
129495
+ const csrfUrls = [...csrfOptions];
128945
129496
  function csrfTry() {
128946
129497
  const path = csrfUrls.shift();
128947
129498
  const options = {
@@ -129104,6 +129655,7 @@ var require_alexa_cookie = /* @__PURE__ */ __commonJSMin(((exports, module) => {
129104
129655
  const handleTokenRegistration = (_options, loginData, callback) => {
129105
129656
  _options.logger && _options.logger(`Handle token registration Start: ${JSON.stringify(loginData)}`);
129106
129657
  loginData.deviceAppName = _options.deviceAppName;
129658
+ loginData.loginCookie = sanitizeAmazonCookie(loginData.loginCookie);
129107
129659
  let deviceSerial;
129108
129660
  if (!_options.formerRegistrationData || !_options.formerRegistrationData.deviceSerial) {
129109
129661
  const deviceSerialBuffer = Buffer.alloc(16);
@@ -129191,6 +129743,7 @@ var require_alexa_cookie = /* @__PURE__ */ __commonJSMin(((exports, module) => {
129191
129743
  Cookie = addCookies(Cookie, response.headers);
129192
129744
  loginData.refreshToken = body.response.success.tokens.bearer.refresh_token;
129193
129745
  const accessToken = body.response.success.tokens.bearer.access_token;
129746
+ loginData.accessToken = accessToken;
129194
129747
  loginData.tokenDate = Date.now();
129195
129748
  loginData.macDms = body.response.success.tokens.mac_dms;
129196
129749
  if (body.response.success.tokens.website_cookies && Array.isArray(body.response.success.tokens.website_cookies)) {
@@ -129217,29 +129770,43 @@ var require_alexa_cookie = /* @__PURE__ */ __commonJSMin(((exports, module) => {
129217
129770
  _options.logger && _options.logger("Alexa-Cookie: Get User data");
129218
129771
  _options.logger && _options.logger(JSON.stringify(options));
129219
129772
  request(options, (error, response, body) => {
129220
- if (!error) {
129773
+ if (!error) if (!response || response.statusCode < 200 || response.statusCode >= 300 || !body) {
129774
+ _options.logger && _options.logger(`Get User data Response: status=${response && response.statusCode}, body=${JSON.stringify(body)}`);
129775
+ if (!_options.amazonPage) {
129776
+ callback && callback(/* @__PURE__ */ new Error(`Could not get user data from Amazon: HTTP ${response && response.statusCode}`), null);
129777
+ return;
129778
+ }
129779
+ loginData.amazonPage = _options.amazonPage;
129780
+ } else {
129221
129781
  try {
129222
129782
  if (typeof body !== "object") body = JSON.parse(body);
129223
129783
  } catch (err) {
129224
129784
  _options.logger && _options.logger(`Get User data Response: ${JSON.stringify(body)}`);
129225
- callback && callback(err, null);
129226
- return;
129785
+ if (!_options.amazonPage) {
129786
+ callback && callback(err, null);
129787
+ return;
129788
+ }
129789
+ loginData.amazonPage = _options.amazonPage;
129227
129790
  }
129228
129791
  _options.logger && _options.logger(`Get User data Response: ${JSON.stringify(body)}`);
129229
129792
  Cookie = addCookies(Cookie, response.headers);
129230
- if (body.marketPlaceDomainName) {
129793
+ if (body && body.marketPlaceDomainName) {
129231
129794
  const pos = body.marketPlaceDomainName.indexOf(".");
129232
129795
  if (pos !== -1) _options.amazonPage = body.marketPlaceDomainName.substr(pos + 1);
129233
129796
  }
129234
129797
  loginData.amazonPage = _options.amazonPage;
129235
- } else if (error && (!_options || !_options.amazonPage)) {
129798
+ }
129799
+ else if (error && (!_options || !_options.amazonPage)) {
129236
129800
  callback && callback(error, null);
129237
129801
  return;
129238
129802
  } else if (error && (!_options.formerRegistrationData || !_options.formerRegistrationData.amazonPage) && _options.amazonPage) _options.logger && _options.logger(`Continue with externally set amazonPage: ${_options.amazonPage}`);
129239
129803
  else if (error) _options.logger && _options.logger("Ignore error while getting user data and amazonPage because previously set amazonPage is available");
129240
129804
  loginData.loginCookie = Cookie;
129241
129805
  getLocalCookies(loginData.amazonPage, loginData.refreshToken, (err, localCookie) => {
129242
- if (err) callback && callback(err, null);
129806
+ if (err) {
129807
+ callback && callback(err, null);
129808
+ return;
129809
+ }
129243
129810
  loginData.localCookie = localCookie;
129244
129811
  getCSRFFromCookies(loginData.localCookie, _options, (err, resData) => {
129245
129812
  if (err) {
@@ -129248,7 +129815,6 @@ var require_alexa_cookie = /* @__PURE__ */ __commonJSMin(((exports, module) => {
129248
129815
  }
129249
129816
  loginData.localCookie = resData.cookie;
129250
129817
  loginData.csrf = resData.csrf;
129251
- delete loginData.accessToken;
129252
129818
  delete loginData.authorization_code;
129253
129819
  delete loginData.verifier;
129254
129820
  loginData.dataVersion = 2;
@@ -129351,6 +129917,39 @@ var require_alexa_cookie = /* @__PURE__ */ __commonJSMin(((exports, module) => {
129351
129917
  callback && callback(null, localCookie);
129352
129918
  });
129353
129919
  };
129920
+ const finishCookieRefresh = (loginData, callback) => {
129921
+ _options.logger && _options.logger("Alexa-Cookie: Skip App registration during refresh and update local cookies");
129922
+ const updateLocalCookies = () => {
129923
+ const amazonPage = loginData.amazonPage || _options.amazonPage;
129924
+ getLocalCookies(amazonPage, loginData.refreshToken, (err, localCookie) => {
129925
+ if (err) {
129926
+ callback && callback(err, null);
129927
+ return;
129928
+ }
129929
+ loginData.localCookie = localCookie;
129930
+ getCSRFFromCookies(loginData.localCookie, _options, (err, resData) => {
129931
+ if (err) {
129932
+ callback && callback(/* @__PURE__ */ new Error(`Error getting csrf for ${amazonPage}`), null);
129933
+ return;
129934
+ }
129935
+ loginData.localCookie = resData.cookie;
129936
+ loginData.csrf = resData.csrf;
129937
+ loginData.amazonPage = amazonPage;
129938
+ loginData.tokenDate = Date.now();
129939
+ delete loginData.authorization_code;
129940
+ delete loginData.verifier;
129941
+ loginData.dataVersion = 2;
129942
+ _options.logger && _options.logger("Alexa-Cookie: Refresh finished with updated cookies and csrf");
129943
+ callback && callback(null, loginData);
129944
+ });
129945
+ });
129946
+ };
129947
+ if (loginData.accessToken) {
129948
+ registerTokenCapabilities(loginData.accessToken, updateLocalCookies);
129949
+ return;
129950
+ }
129951
+ updateLocalCookies();
129952
+ };
129354
129953
  this.refreshAlexaCookie = (__options, callback) => {
129355
129954
  if (!__options || !__options.formerRegistrationData || !__options.formerRegistrationData.loginCookie || !__options.formerRegistrationData.refreshToken) {
129356
129955
  callback && callback(/* @__PURE__ */ new Error("No former registration data provided for Cookie Refresh"), null);
@@ -129417,13 +130016,16 @@ var require_alexa_cookie = /* @__PURE__ */ __commonJSMin(((exports, module) => {
129417
130016
  _options.formerRegistrationData.loginCookie = addCookies(Cookie, response.headers);
129418
130017
  _options.formerRegistrationData.accessToken = body.access_token;
129419
130018
  getLocalCookies(_options.baseAmazonPage, _options.formerRegistrationData.refreshToken, (err, comCookie) => {
129420
- if (err) callback && callback(err, null);
130019
+ if (err) {
130020
+ callback && callback(err, null);
130021
+ return;
130022
+ }
129421
130023
  const initCookies = cookieTools.parse(_options.formerRegistrationData.loginCookie);
129422
130024
  let newCookie = `frc=${initCookies.frc}; `;
129423
130025
  newCookie += `map-md=${initCookies["map-md"]}; `;
129424
130026
  newCookie += comCookie;
129425
- _options.formerRegistrationData.loginCookie = newCookie;
129426
- handleTokenRegistration(_options, _options.formerRegistrationData, callback);
130027
+ _options.formerRegistrationData.loginCookie = sanitizeAmazonCookie(newCookie);
130028
+ finishCookieRefresh(_options.formerRegistrationData, callback);
129427
130029
  });
129428
130030
  });
129429
130031
  };
@@ -130018,10 +130620,20 @@ var import_alexa_remote = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin
130018
130620
  listItemId: payload.listItemId
130019
130621
  });
130020
130622
  return;
130623
+ case "NotifyNowPlayingUpdated":
130624
+ const nowPlaying = payload?.update?.update?.nowPlayingData ?? null;
130625
+ this.emit("ws-now-playing-updated", {
130626
+ customerId: payload?.customerId ?? null,
130627
+ taskSessionId: payload?.update?.taskSessionId ?? null,
130628
+ cause: payload?.update?.update?.cause ?? null,
130629
+ mediaId: nowPlaying?.mediaId ?? null,
130630
+ playerState: nowPlaying?.playerState ?? null,
130631
+ nowPlayingData: nowPlaying
130632
+ });
130633
+ return;
130021
130634
  case "PUSH_MICROPHONE_STATE":
130022
130635
  case "PUSH_DELETE_DOPPLER_ACTIVITIES":
130023
130636
  case "PUSH_DEVICE_SETUP_STATE_CHANGE":
130024
- case "NotifyNowPlayingUpdated":
130025
130637
  case "NotifyMediaSessionsUpdated": return;
130026
130638
  }
130027
130639
  this.emit("ws-unknown-command", command, payload);
@@ -132068,7 +132680,7 @@ var import_alexa_remote = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin
132068
132680
  */
132069
132681
  var ALEXA_API_USER_AGENT_POSTFIX = `AlexaRemote/${{
132070
132682
  name: "alexa-remote2",
132071
- version: "8.0.4",
132683
+ version: "8.1.1",
132072
132684
  description: "Remote Control for amazon echo devices",
132073
132685
  author: {
132074
132686
  "name": "Apollon77",
@@ -132097,16 +132709,16 @@ var ALEXA_API_USER_AGENT_POSTFIX = `AlexaRemote/${{
132097
132709
  "layla.amazon.de"
132098
132710
  ],
132099
132711
  dependencies: {
132100
- "alexa-cookie2": "^5.0.3",
132712
+ "alexa-cookie2": "^5.0.6",
132101
132713
  "https": "^1.0.0",
132102
132714
  "querystring": "^0.2.1",
132103
- "ws": "^8.18.3",
132715
+ "ws": "^8.21.0",
132104
132716
  "extend": "^3.0.2",
132105
- "uuid": "^11.1.0"
132717
+ "uuid": "^11.1.1"
132106
132718
  },
132107
132719
  devDependencies: {
132108
- "@alcalzone/release-script": "^5.0.0",
132109
- "@alcalzone/release-script-plugin-license": "^4.0.0",
132720
+ "@alcalzone/release-script": "^5.2.1",
132721
+ "@alcalzone/release-script-plugin-license": "^5.2.0",
132110
132722
  "eslint": "^8.57.1"
132111
132723
  },
132112
132724
  scripts: {
@@ -132126,6 +132738,31 @@ var ALEXA_API_USER_AGENT_POSTFIX = `AlexaRemote/${{
132126
132738
  //#endregion
132127
132739
  //#region src/alexa-proxy-login.ts
132128
132740
  /**
132741
+ * INTERACTIVE Amazon login via `alexa-remote2`'s built-in proxy auth mode, behind the
132742
+ * hub's vendor login proxy (D955).
132743
+ *
132744
+ * alexa-remote2 (through `alexa-cookie2`) spins a local Express reverse-proxy to
132745
+ * Amazon's real login page and captures the resulting cookie + the richer
132746
+ * `cookieData` registration blob (it embeds the refresh token, so the session can
132747
+ * auto-refresh forever). That listener is bound to LOOPBACK only
132748
+ * (`proxyOwnIp` = `proxyListenBind` = 127.0.0.1): nothing outside this process's host can
132749
+ * reach it. The operator's browser reaches it through the hub's data plane instead
132750
+ * (`VendorLoginProxy`, `/addon/<id>/login/<attempt>/127.0.0.1:<port>/…`), which is
132751
+ * HTTPS under the same origin as the admin page. alexa-cookie2 keeps doing the Amazon
132752
+ * specific work (login cookies, device registration, the `/ax/claim` and CSRF steps)
132753
+ * and the hub proxy only rewrites the loopback-origin URLs it emits into the mount.
132754
+ *
132755
+ * Why not the second proxy replacing alexa-cookie2's: the capture needs the PKCE
132756
+ * verifier, the device id, the initial cookies and the app registration alexa-cookie2
132757
+ * already performs; re-implementing them would add JS-built-URL failure modes.
132758
+ *
132759
+ * The captured `cookieData` is handed to {@link AlexaProxyLoginSessionOptions.onCookie}
132760
+ * — the addon persists it on the broker entry, exactly where a pasted cookie
132761
+ * goes, so the rest of the addon (broker / poll / adoption) is unchanged.
132762
+ */
132763
+ /** The only address the alexa-cookie2 listener binds and advertises. */
132764
+ var ALEXA_PROXY_LOOPBACK_HOST = "127.0.0.1";
132765
+ /**
132129
132766
  * A single interactive proxy-login attempt. One instance = one Amazon login.
132130
132767
  * Construct → {@link start} → poll {@link getStatus} → on `captured` the
132131
132768
  * `onCookie` callback has fired with the blob → {@link dispose}.
@@ -132139,13 +132776,37 @@ var AlexaProxyLoginSession = class {
132139
132776
  #startedAt = Date.now();
132140
132777
  #capturedAt = null;
132141
132778
  #captured = false;
132779
+ #waiters = /* @__PURE__ */ new Set();
132142
132780
  constructor(opts) {
132143
132781
  this.#opts = opts;
132144
132782
  this.#logger = opts.logger;
132145
132783
  }
132146
- /** `http://<proxyOwnIp>:<proxyPort>/` — what the operator opens. */
132784
+ /** What the operator opens: the hub-relative entry of the login proxy. */
132147
132785
  get loginUrl() {
132148
- return `http://${this.#opts.proxyOwnIp}:${this.#opts.proxyPort}/`;
132786
+ return this.#opts.loginUrl;
132787
+ }
132788
+ /** `host:port` of the loopback listener — the one upstream the hub proxy may reach. */
132789
+ get upstreamHost() {
132790
+ return `${ALEXA_PROXY_LOOPBACK_HOST}:${this.#opts.proxyPort}`;
132791
+ }
132792
+ /**
132793
+ * Resolves true once the cookie material is in hand, false after `timeoutMs`.
132794
+ * The Amazon page ends on `/cookie-success` BEFORE alexa-cookie2 finished registering
132795
+ * the app, so the hub waits here before it sends the browser to the result page.
132796
+ */
132797
+ whenCaptured(timeoutMs) {
132798
+ if (this.#captured) return Promise.resolve(true);
132799
+ return new Promise((resolve) => {
132800
+ const timer = setTimeout(() => {
132801
+ this.#waiters.delete(settle);
132802
+ resolve(false);
132803
+ }, timeoutMs);
132804
+ const settle = () => {
132805
+ clearTimeout(timer);
132806
+ resolve(true);
132807
+ };
132808
+ this.#waiters.add(settle);
132809
+ });
132149
132810
  }
132150
132811
  getStatus() {
132151
132812
  return {
@@ -132177,7 +132838,8 @@ var AlexaProxyLoginSession = class {
132177
132838
  remote.on("cookie", () => this.#onCookieEvent(remote));
132178
132839
  remote.init({
132179
132840
  proxyOnly: true,
132180
- proxyOwnIp: this.#opts.proxyOwnIp,
132841
+ proxyOwnIp: ALEXA_PROXY_LOOPBACK_HOST,
132842
+ proxyListenBind: ALEXA_PROXY_LOOPBACK_HOST,
132181
132843
  proxyPort: this.#opts.proxyPort,
132182
132844
  proxyLogLevel: "warn",
132183
132845
  amazonPage,
@@ -132223,6 +132885,8 @@ var AlexaProxyLoginSession = class {
132223
132885
  this.#captured = true;
132224
132886
  this.#capturedAt = Date.now();
132225
132887
  this.#phase = "captured";
132888
+ for (const settle of this.#waiters) settle();
132889
+ this.#waiters.clear();
132226
132890
  this.#logger.info("alexa proxy-login: cookie captured", { meta: { sessionId: this.#opts.sessionId } });
132227
132891
  try {
132228
132892
  this.#opts.onCookie(this.#opts.sessionId, {
@@ -132243,10 +132907,7 @@ var AlexaProxyLoginSession = class {
132243
132907
  if (isProxyReadyError(err)) {
132244
132908
  if (this.#phase === "starting") {
132245
132909
  this.#phase = "awaiting-login";
132246
- this.#logger.info("alexa proxy-login: proxy ready — awaiting Amazon login", { meta: {
132247
- sessionId: this.#opts.sessionId,
132248
- loginUrl: this.loginUrl
132249
- } });
132910
+ this.#logger.info("alexa proxy-login: proxy ready — awaiting Amazon login", { meta: { sessionId: this.#opts.sessionId } });
132250
132911
  }
132251
132912
  return;
132252
132913
  }
@@ -132304,62 +132965,44 @@ function isStringOrObject(value) {
132304
132965
  return typeof value === "string" || typeof value === "object" && value !== null;
132305
132966
  }
132306
132967
  //#endregion
132307
- //#region src/alexa-proxy-host.ts
132308
- /** Typed refusal: names WHY no proxy host could be chosen. */
132309
- var AlexaProxyHostError = class extends Error {
132310
- reason;
132311
- constructor(reason, message, options) {
132312
- super(message, options);
132313
- this.name = "AlexaProxyHostError";
132314
- this.reason = reason;
132315
- }
132316
- };
132317
- var PRIVATE_IPV4 = /^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|169\.254\.)/;
132318
- var IPV4_LITERAL = /^\d{1,3}(\.\d{1,3}){3}$/;
132319
- /** A host a browser on the hub's network reaches directly: private IPv4, `.local`, or a bare name. */
132320
- function isLanHost(host) {
132321
- if (host.length === 0) return false;
132322
- if (IPV4_LITERAL.test(host)) return PRIVATE_IPV4.test(host);
132323
- if (host.includes(":")) return false;
132324
- return host.endsWith(".local") || !host.includes(".");
132325
- }
132326
- function lanHostOf(address) {
132968
+ //#region src/alexa-proxy-login-manager.ts
132969
+ /** The data-plane prefix the hub login proxy is served on. */
132970
+ var ALEXA_LOGIN_PROXY_PREFIX = "login";
132971
+ /** The addon id the result page names to return to. */
132972
+ var ADDON_ID = "import-alexa";
132973
+ /** An attempt lives as long as a sign-in attempt does elsewhere (10 min). */
132974
+ var ATTEMPT_TTL_MS = 600 * 1e3;
132975
+ /** How long the browser waits on the Amazon page for alexa-cookie2 to finish registering. */
132976
+ var CAPTURE_WAIT_MS = 6e4;
132977
+ /**
132978
+ * Terminal condition: alexa-cookie2 turns Amazon's final redirect into a 302 to its own
132979
+ * `/cookie-success` page once it holds the login cookie. Matching on the path of the raw
132980
+ * `Location` keeps this independent of the page's HTML.
132981
+ */
132982
+ function cookieSuccessTerminal(view) {
132983
+ if (view.location === null) return { kind: "continue" };
132327
132984
  try {
132328
- const host = new URL(address).hostname;
132329
- return isLanHost(host) && host !== "localhost" ? host : null;
132985
+ return new URL(view.location, `http://127.0.0.1`).pathname === "/cookie-success" ? {
132986
+ kind: "done",
132987
+ payload: true
132988
+ } : { kind: "continue" };
132330
132989
  } catch {
132331
- return null;
132990
+ return { kind: "continue" };
132332
132991
  }
132333
132992
  }
132334
- async function resolveProxyHost(input) {
132335
- const override = input.override.trim();
132336
- if (override.length > 0) return override;
132337
- let addresses;
132338
- try {
132339
- addresses = await input.readAppConnections();
132340
- } catch (err) {
132341
- input.logger.warn("alexa proxy-login: hub addresses unavailable from local-network", { meta: { error: errMsg(err) } });
132342
- throw new AlexaProxyHostError("palette-unavailable", "alexa proxy-login: the hub address list is unavailable — set proxyHost in settings", { cause: err });
132343
- }
132344
- for (const address of addresses) {
132345
- const host = lanHostOf(address);
132346
- if (host !== null) return host;
132347
- }
132348
- input.logger.warn("alexa proxy-login: no LAN address among the hub appConnections", { meta: { addresses: [...addresses] } });
132349
- throw new AlexaProxyHostError("no-lan-address", "alexa proxy-login: the hub has no LAN address the browser can open directly — set proxyHost in settings");
132350
- }
132351
- //#endregion
132352
- //#region src/alexa-proxy-login-manager.ts
132353
132993
  var AlexaProxyLoginManager = class {
132354
132994
  #logger;
132355
132995
  #getProxyConfig;
132356
- #getAppConnectionAddresses;
132996
+ #getLoginProxy;
132997
+ #captureWaitMs;
132357
132998
  #session = null;
132358
132999
  #captured = /* @__PURE__ */ new Map();
133000
+ #attemptCloser = null;
132359
133001
  constructor(deps) {
132360
133002
  this.#logger = deps.logger;
132361
133003
  this.#getProxyConfig = deps.getProxyConfig;
132362
- this.#getAppConnectionAddresses = deps.getAppConnectionAddresses;
133004
+ this.#getLoginProxy = deps.getLoginProxy;
133005
+ this.#captureWaitMs = deps.captureWaitMs ?? CAPTURE_WAIT_MS;
132363
133006
  }
132364
133007
  /**
132365
133008
  * Start a fresh proxy-login. Cancels any in-flight session first (single
@@ -132367,18 +133010,41 @@ var AlexaProxyLoginManager = class {
132367
133010
  */
132368
133011
  async start(amazonDomain) {
132369
133012
  await this.cancelActive();
132370
- const { proxyHost, proxyPort } = this.#getProxyConfig();
132371
- const host = await resolveProxyHost({
132372
- override: proxyHost,
132373
- readAppConnections: this.#getAppConnectionAddresses,
132374
- logger: this.#logger
132375
- });
133013
+ const loginProxy = this.#getLoginProxy();
133014
+ if (loginProxy === null) throw new Error("alexa proxy-login: the hub login proxy is not available on this hub");
133015
+ const { proxyPort } = this.#getProxyConfig();
133016
+ const upstreamHost = `${ALEXA_PROXY_LOOPBACK_HOST}:${proxyPort}`;
132376
133017
  const sessionId = (0, node_crypto.randomBytes)(9).toString("base64url");
133018
+ loginProxy.open({
133019
+ attemptId: sessionId,
133020
+ expiresAtMs: Date.now() + ATTEMPT_TTL_MS,
133021
+ upstreams: [{
133022
+ host: upstreamHost,
133023
+ scheme: "http"
133024
+ }],
133025
+ terminal: cookieSuccessTerminal,
133026
+ onDone: async () => {
133027
+ return await this.#session?.whenCaptured(this.#captureWaitMs) === true ? {
133028
+ status: "success",
133029
+ addonId: ADDON_ID
133030
+ } : {
133031
+ status: "error",
133032
+ addonId: ADDON_ID,
133033
+ reason: "exchange_failed"
133034
+ };
133035
+ }
133036
+ });
133037
+ const loginUrl = loginProxy.entryPath(sessionId, `http://${upstreamHost}/`);
133038
+ if (loginUrl === null) {
133039
+ loginProxy.close(sessionId);
133040
+ throw new Error("alexa proxy-login: the hub login proxy refused the login address");
133041
+ }
133042
+ this.#attemptCloser = () => loginProxy.close(sessionId);
132377
133043
  const session = new AlexaProxyLoginSession({
132378
133044
  sessionId,
132379
133045
  amazonDomain,
132380
- proxyOwnIp: host,
132381
133046
  proxyPort,
133047
+ loginUrl,
132382
133048
  logger: this.#logger,
132383
133049
  onCookie: (id, captured) => {
132384
133050
  this.#captured.set(id, captured);
@@ -132389,6 +133055,7 @@ var AlexaProxyLoginManager = class {
132389
133055
  await session.start();
132390
133056
  } catch (err) {
132391
133057
  this.#session = null;
133058
+ this.#closeAttempt();
132392
133059
  await session.dispose();
132393
133060
  throw new Error(`alexa proxy-login: failed to start proxy — ${errMsg(err)}`, { cause: err });
132394
133061
  }
@@ -132422,10 +133089,16 @@ var AlexaProxyLoginManager = class {
132422
133089
  await this.cancelActive();
132423
133090
  this.#captured.delete(sessionId);
132424
133091
  }
133092
+ #closeAttempt() {
133093
+ const close = this.#attemptCloser;
133094
+ this.#attemptCloser = null;
133095
+ close?.();
133096
+ }
132425
133097
  /** Cancel + dispose whatever session is currently active. */
132426
133098
  async cancelActive() {
132427
133099
  const session = this.#session;
132428
133100
  this.#session = null;
133101
+ this.#closeAttempt();
132429
133102
  if (session === null) return;
132430
133103
  try {
132431
133104
  await session.cancel();
@@ -136040,10 +136713,7 @@ var SPEAK_PAGE = {
136040
136713
  /** Default config — a fresh install starts with no accounts. */
136041
136714
  var DEFAULTS = {
136042
136715
  brokers: [],
136043
- proxyLogin: {
136044
- proxyHost: "",
136045
- proxyPort: 3456
136046
- },
136716
+ proxyLogin: { proxyPort: 3456 },
136047
136717
  speak: {
136048
136718
  email: "",
136049
136719
  password: "",
@@ -136089,6 +136759,8 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
136089
136759
  deviceClasses = Object.fromEntries(ALEXA_DEVICE_TYPES.map((t) => [t, AlexaDevice]));
136090
136760
  registry = null;
136091
136761
  proxyLogin = null;
136762
+ loginPlane = null;
136763
+ loginProxy = null;
136092
136764
  brokerStore = null;
136093
136765
  activeBrokers = [];
136094
136766
  speak = null;
@@ -136128,16 +136800,13 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
136128
136800
  });
136129
136801
  alexaCommands.setDispatcher((brokerId, command) => this.requireRegistry().sendCommand(brokerId, command));
136130
136802
  await this.registry.restore(this.activeBrokers);
136803
+ await this.serveLoginProxy();
136131
136804
  this.proxyLogin = new AlexaProxyLoginManager({
136132
136805
  logger: this.ctx.logger,
136133
136806
  getProxyConfig: () => {
136134
- const cfg = alexaProxyLoginSchema.parse(this.config.proxyLogin ?? {});
136135
- return {
136136
- proxyHost: cfg.proxyHost,
136137
- proxyPort: cfg.proxyPort
136138
- };
136807
+ return { proxyPort: alexaProxyLoginSchema.parse(this.config.proxyLogin ?? {}).proxyPort };
136139
136808
  },
136140
- getAppConnectionAddresses: async () => (await this.ctx.api.localNetwork.getAddresses.query()).roles.appConnections.resolved
136809
+ getLoginProxy: () => this.loginProxy
136141
136810
  });
136142
136811
  this.ctx.logger.info("Alexa: import addon initialised", { meta: { brokerCount: this.activeBrokers.length } });
136143
136812
  await this.reconcileIntegrationsToBrokers();
@@ -136269,6 +136938,24 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
136269
136938
  }
136270
136939
  this.speak?.brokersChanged();
136271
136940
  }
136941
+ /** The hub's HTTPS face for the Amazon login (D955): admin session only. */
136942
+ async serveLoginProxy() {
136943
+ const proxy = new VendorLoginProxy({
136944
+ addonId: this.ctx.id,
136945
+ prefix: ALEXA_LOGIN_PROXY_PREFIX,
136946
+ logger: this.ctx.logger
136947
+ });
136948
+ try {
136949
+ this.loginPlane = await this.ctx.dataPlane?.serve({
136950
+ prefix: "login",
136951
+ access: "admin",
136952
+ handler: proxy.handler
136953
+ }) ?? null;
136954
+ if (this.loginPlane !== null) this.loginProxy = proxy;
136955
+ } catch (err) {
136956
+ this.ctx.logger.warn("Alexa: login proxy failed to serve — interactive sign-in is unavailable", { meta: { error: errMsg(err) } });
136957
+ }
136958
+ }
136272
136959
  async onShutdown() {
136273
136960
  this.speak?.stop();
136274
136961
  this.speak = null;
@@ -136279,6 +136966,9 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
136279
136966
  this.ctx.logger.warn("Alexa: proxy-login dispose error", { meta: { error: errMsg(err) } });
136280
136967
  }
136281
136968
  this.proxyLogin = null;
136969
+ await this.loginPlane?.dispose().catch(() => void 0);
136970
+ this.loginPlane = null;
136971
+ this.loginProxy = null;
136282
136972
  try {
136283
136973
  await this.registry?.shutdown();
136284
136974
  } catch (err) {
@@ -136524,19 +137214,13 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
136524
137214
  id: "alexa-proxy-login",
136525
137215
  title: "Interactive login (proxy)",
136526
137216
  titleKey: "import-alexa:globalSettings.s_alexa-proxy-login.title",
136527
- 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.",
137217
+ 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.",
136528
137218
  descriptionKey: "import-alexa:globalSettings.s_alexa-proxy-login.description",
136529
137219
  columns: 2,
136530
137220
  fields: [{
136531
- type: "text",
136532
- key: "proxyLogin.proxyHost",
136533
- label: "Login proxy host/IP (blank = auto-detect)",
136534
- labelKey: "import-alexa:globalSettings.s_alexa-proxy-login.proxyLogin_proxyHost.label",
136535
- placeholder: "auto-detect"
136536
- }, {
136537
137221
  type: "number",
136538
137222
  key: "proxyLogin.proxyPort",
136539
- label: "Login proxy port",
137223
+ label: "Internal login port (loopback)",
136540
137224
  labelKey: "import-alexa:globalSettings.s_alexa-proxy-login.proxyLogin_proxyPort.label",
136541
137225
  min: 1,
136542
137226
  max: 65535,