@camstack/addon-import-alexa 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/icon.svg +9 -5
- package/dist/addon.js +754 -100
- package/dist/addon.mjs +755 -101
- package/dist/index.mjs +1 -1
- package/package.json +4 -1
package/dist/addon.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
+
import { networkInterfaces } from "node:os";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
2
4
|
import { promises } from "node:fs";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
//#region \0rolldown/runtime.js
|
|
@@ -6839,6 +6841,123 @@ var ConvertResultSchema = object({
|
|
|
6839
6841
|
})).readonly()
|
|
6840
6842
|
});
|
|
6841
6843
|
/**
|
|
6844
|
+
* Build an `IAddonRouteProvider` from a list of routes. Implements
|
|
6845
|
+
* both the operator-facing `getRoutes` (returning route descriptors
|
|
6846
|
+
* minus the handlers, which can't cross JSON) and the framework-
|
|
6847
|
+
* private `invoke` method that the hub calls when this provider lives
|
|
6848
|
+
* in a forked worker.
|
|
6849
|
+
*
|
|
6850
|
+
* Co-located addons use the returned `getRoutes` directly because
|
|
6851
|
+
* their handlers don't need to cross any wire. The `invoke` method
|
|
6852
|
+
* is present anyway so the bridge code on the hub is uniform — it
|
|
6853
|
+
* doesn't need to switch on "local vs remote provider" at the call
|
|
6854
|
+
* site.
|
|
6855
|
+
*
|
|
6856
|
+
* Example:
|
|
6857
|
+
* const routes: IAddonHttpRoute[] = [
|
|
6858
|
+
* { method: 'GET', path: '/start', access: 'public', handler: this.handleStart },
|
|
6859
|
+
* ]
|
|
6860
|
+
* return [
|
|
6861
|
+
* {
|
|
6862
|
+
* capability: addonRoutesCapability,
|
|
6863
|
+
* provider: buildAddonRouteProvider('auth-oidc', routes),
|
|
6864
|
+
* },
|
|
6865
|
+
* ]
|
|
6866
|
+
*/
|
|
6867
|
+
function buildAddonRouteProvider(id, routes) {
|
|
6868
|
+
return {
|
|
6869
|
+
id,
|
|
6870
|
+
getRoutes: () => routes,
|
|
6871
|
+
invoke: async (input) => {
|
|
6872
|
+
const match = matchRoute(routes, input.method, input.path);
|
|
6873
|
+
if (!match) return {
|
|
6874
|
+
status: 404,
|
|
6875
|
+
headers: {},
|
|
6876
|
+
redirectUrl: null,
|
|
6877
|
+
body: { error: `No route matches ${input.method} ${input.path}` }
|
|
6878
|
+
};
|
|
6879
|
+
const envelope = {
|
|
6880
|
+
status: 200,
|
|
6881
|
+
headers: {},
|
|
6882
|
+
redirectUrl: null
|
|
6883
|
+
};
|
|
6884
|
+
const reply = buildCapturingReply(envelope);
|
|
6885
|
+
const request = {
|
|
6886
|
+
params: {
|
|
6887
|
+
...input.params,
|
|
6888
|
+
...match.params
|
|
6889
|
+
},
|
|
6890
|
+
query: input.query,
|
|
6891
|
+
body: input.body,
|
|
6892
|
+
headers: input.headers,
|
|
6893
|
+
...input.user ? { user: input.user } : {},
|
|
6894
|
+
...input.scopedToken !== void 0 ? { scopedToken: input.scopedToken } : {}
|
|
6895
|
+
};
|
|
6896
|
+
await match.route.handler(request, reply);
|
|
6897
|
+
return envelope;
|
|
6898
|
+
}
|
|
6899
|
+
};
|
|
6900
|
+
}
|
|
6901
|
+
/**
|
|
6902
|
+
* Pattern matcher: same semantics as `AddonRouteRegistry.matchRoute`
|
|
6903
|
+
* but operating on a flat list and bypassing the `/addon/<id>/` prefix
|
|
6904
|
+
* — the bridge sends the post-prefix path directly so we don't need
|
|
6905
|
+
* to round-trip it through normalization.
|
|
6906
|
+
*/
|
|
6907
|
+
function matchRoute(routes, method, path) {
|
|
6908
|
+
const normalizedMethod = method.toUpperCase();
|
|
6909
|
+
for (const route of routes) {
|
|
6910
|
+
if (route.method !== normalizedMethod) continue;
|
|
6911
|
+
const params = matchPath(route.path, path);
|
|
6912
|
+
if (params !== null) return {
|
|
6913
|
+
route,
|
|
6914
|
+
params
|
|
6915
|
+
};
|
|
6916
|
+
}
|
|
6917
|
+
return null;
|
|
6918
|
+
}
|
|
6919
|
+
function matchPath(pattern, p) {
|
|
6920
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
6921
|
+
const pathParts = p.split("/").filter(Boolean);
|
|
6922
|
+
if (patternParts.length !== pathParts.length) return null;
|
|
6923
|
+
const params = {};
|
|
6924
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
6925
|
+
const a = patternParts[i];
|
|
6926
|
+
const b = pathParts[i];
|
|
6927
|
+
if (a.startsWith(":")) params[a.slice(1)] = b;
|
|
6928
|
+
else if (a !== b) return null;
|
|
6929
|
+
}
|
|
6930
|
+
return params;
|
|
6931
|
+
}
|
|
6932
|
+
function buildCapturingReply(envelope) {
|
|
6933
|
+
const wrapper = {
|
|
6934
|
+
status(code) {
|
|
6935
|
+
envelope.status = code;
|
|
6936
|
+
return wrapper;
|
|
6937
|
+
},
|
|
6938
|
+
code(code) {
|
|
6939
|
+
envelope.status = code;
|
|
6940
|
+
return wrapper;
|
|
6941
|
+
},
|
|
6942
|
+
send(data) {
|
|
6943
|
+
envelope.body = data;
|
|
6944
|
+
},
|
|
6945
|
+
redirect(url) {
|
|
6946
|
+
envelope.redirectUrl = url;
|
|
6947
|
+
if (envelope.status === 200) envelope.status = 302;
|
|
6948
|
+
},
|
|
6949
|
+
header(name, value) {
|
|
6950
|
+
envelope.headers[name.toLowerCase()] = value;
|
|
6951
|
+
return wrapper;
|
|
6952
|
+
},
|
|
6953
|
+
type(mime) {
|
|
6954
|
+
envelope.contentType = mime;
|
|
6955
|
+
return wrapper;
|
|
6956
|
+
}
|
|
6957
|
+
};
|
|
6958
|
+
return wrapper;
|
|
6959
|
+
}
|
|
6960
|
+
/**
|
|
6842
6961
|
* Numeric day-of-week: 0 = Sunday … 6 = Saturday (matches `Date.getDay`).
|
|
6843
6962
|
* Named `RecordingWeekday` to avoid collision with the string-union
|
|
6844
6963
|
* `Weekday` exported from `interfaces/timezones.ts`.
|
|
@@ -15606,7 +15725,24 @@ var InvokeReplyEnvelopeSchema = object({
|
|
|
15606
15725
|
/** Set when the handler called `reply.type(mime)`. */
|
|
15607
15726
|
contentType: string().optional()
|
|
15608
15727
|
});
|
|
15609
|
-
|
|
15728
|
+
var addonRoutesCapability = {
|
|
15729
|
+
name: "addon-routes",
|
|
15730
|
+
scope: "system",
|
|
15731
|
+
mode: "collection",
|
|
15732
|
+
internal: true,
|
|
15733
|
+
methods: {
|
|
15734
|
+
getRoutes: method(_void(), array(AddonHttpRouteSchema)),
|
|
15735
|
+
/**
|
|
15736
|
+
* Cross-process dispatch entry point. Forked addons implement this
|
|
15737
|
+
* (via `buildAddonRouteProvider`) so the hub's Fastify catch-all
|
|
15738
|
+
* can route through Moleculer when the handler lives in a worker.
|
|
15739
|
+
*
|
|
15740
|
+
* Local addons can implement it for free with the same helper;
|
|
15741
|
+
* the hub bypasses the wire on co-located addons.
|
|
15742
|
+
*/
|
|
15743
|
+
invoke: method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" })
|
|
15744
|
+
}
|
|
15745
|
+
};
|
|
15610
15746
|
/**
|
|
15611
15747
|
* shm ring usage stats for a `frameSink: 'shm'` decoder session —
|
|
15612
15748
|
* exposed via `decoder.getShmStats` so downstream consumers can
|
|
@@ -23940,6 +24076,122 @@ object({
|
|
|
23940
24076
|
schemaVersion: literal(1)
|
|
23941
24077
|
});
|
|
23942
24078
|
//#endregion
|
|
24079
|
+
//#region src/config.ts
|
|
24080
|
+
/**
|
|
24081
|
+
* Alexa account connection settings for ONE broker (= one Amazon account).
|
|
24082
|
+
*
|
|
24083
|
+
* v1 uses the unofficial alexa-remote2 cookie / refresh-token flow. The operator
|
|
24084
|
+
* supplies the Amazon domain/region and either a captured cookie blob or a
|
|
24085
|
+
* refresh token (the proxy-login UX captures one of these). State is poll-only,
|
|
24086
|
+
* so a configurable poll interval is included.
|
|
24087
|
+
*/
|
|
24088
|
+
var alexaConnectionSchema = object({
|
|
24089
|
+
/** Amazon domain/region. */
|
|
24090
|
+
amazonDomain: string().min(1).default("amazon.com").describe("Amazon domain (region)"),
|
|
24091
|
+
/**
|
|
24092
|
+
* Captured cookie material — either a raw cookie string OR the full
|
|
24093
|
+
* `cookieData` JSON registration blob produced by alexa-remote2's standalone
|
|
24094
|
+
* proxy login (preferred: it carries the refresh token + lets the session
|
|
24095
|
+
* auto-refresh). This is the primary auth input.
|
|
24096
|
+
*/
|
|
24097
|
+
cookie: string().default("").describe("Captured Alexa cookie / cookieData blob (from proxy login)"),
|
|
24098
|
+
/**
|
|
24099
|
+
* Refresh token. NOTE: a bare refresh token alone is NOT sufficient to init
|
|
24100
|
+
* alexa-remote2 — supply the full cookie / cookieData blob (which embeds it).
|
|
24101
|
+
* Retained for provenance / forward-compat.
|
|
24102
|
+
*/
|
|
24103
|
+
refreshToken: string().default("").describe("Alexa refresh token (must be inside cookie blob)"),
|
|
24104
|
+
/** Device-registration secret some flows need. */
|
|
24105
|
+
macDms: string().default("").describe("macDms device-registration secret (optional)"),
|
|
24106
|
+
/** Poll interval for state sync (Alexa has no clean 3rd-party push). */
|
|
24107
|
+
pollIntervalMs: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(15e3).max(36e5).default(6e4)).describe("State poll interval (ms) — conservative to avoid Amazon throttling")
|
|
24108
|
+
});
|
|
24109
|
+
/** Translate a stored connection into the client's credentials shape. */
|
|
24110
|
+
function connectionToCredentials(conn) {
|
|
24111
|
+
return {
|
|
24112
|
+
amazonDomain: conn.amazonDomain,
|
|
24113
|
+
...conn.cookie.length > 0 ? { cookie: conn.cookie } : {},
|
|
24114
|
+
...conn.refreshToken.length > 0 ? { refreshToken: conn.refreshToken } : {},
|
|
24115
|
+
...conn.macDms.length > 0 ? { macDms: conn.macDms } : {}
|
|
24116
|
+
};
|
|
24117
|
+
}
|
|
24118
|
+
/** A single registered Alexa account ("broker"). */
|
|
24119
|
+
var AlexaBrokerEntrySchema = object({
|
|
24120
|
+
id: string().min(1),
|
|
24121
|
+
name: string().min(1),
|
|
24122
|
+
connection: alexaConnectionSchema,
|
|
24123
|
+
integrationId: string().optional()
|
|
24124
|
+
});
|
|
24125
|
+
/**
|
|
24126
|
+
* Interactive proxy-login settings (addon-global, not per-broker). alexa-remote2
|
|
24127
|
+
* spins a local reverse-proxy to Amazon's login page on `proxyHost:proxyPort`;
|
|
24128
|
+
* the operator opens that URL and logs in. `proxyHost` MUST be the host:port the
|
|
24129
|
+
* operator's browser uses (alexa-cookie2 rewrites Amazon URLs to absolute
|
|
24130
|
+
* `http://<proxyHost>:<proxyPort>/...`), so it is a directly-reachable host —
|
|
24131
|
+
* the hub's LAN IP/hostname — NOT behind the hub's HTTPS data-plane mount.
|
|
24132
|
+
* Empty `proxyHost` ⇒ auto-detect the host's primary non-loopback IPv4 at
|
|
24133
|
+
* login-start time.
|
|
24134
|
+
*/
|
|
24135
|
+
var alexaProxyLoginSchema = object({
|
|
24136
|
+
proxyHost: string().default("").describe("Host/IP the browser uses to reach the login proxy (blank = auto-detect)"),
|
|
24137
|
+
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)")
|
|
24138
|
+
});
|
|
24139
|
+
/** Top-level addon config — broker entries + the proxy-login settings. */
|
|
24140
|
+
var alexaAddonConfigSchema = object({
|
|
24141
|
+
brokers: array(AlexaBrokerEntrySchema).default([]),
|
|
24142
|
+
proxyLogin: alexaProxyLoginSchema.default({
|
|
24143
|
+
proxyHost: "",
|
|
24144
|
+
proxyPort: 3456
|
|
24145
|
+
})
|
|
24146
|
+
});
|
|
24147
|
+
/** Narrow an `unknown` to a shallow plain-object record (cast-free boundary). */
|
|
24148
|
+
function toRecord(value) {
|
|
24149
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) return Object.fromEntries(Object.entries(value));
|
|
24150
|
+
return {};
|
|
24151
|
+
}
|
|
24152
|
+
/** Coerce a loose settings blob through the connection schema, applying defaults. */
|
|
24153
|
+
function settingsToAlexaConnection(settings) {
|
|
24154
|
+
return alexaConnectionSchema.parse(toRecord(settings));
|
|
24155
|
+
}
|
|
24156
|
+
/** Hand-written connection form for the broker/integration creation UI. */
|
|
24157
|
+
function buildConnectionFormSchema() {
|
|
24158
|
+
return { sections: [{
|
|
24159
|
+
id: "alexa-account",
|
|
24160
|
+
title: "Amazon Alexa account",
|
|
24161
|
+
description: "Link an Amazon account via the unofficial Alexa client. Supply a captured cookie OR a refresh token. NOTE: this is an unofficial integration — Amazon can change or break it without notice, and it may violate Amazon ToS. State is polled, not pushed.",
|
|
24162
|
+
columns: 1,
|
|
24163
|
+
fields: [
|
|
24164
|
+
{
|
|
24165
|
+
type: "text",
|
|
24166
|
+
key: "amazonDomain",
|
|
24167
|
+
label: "Amazon domain (region)",
|
|
24168
|
+
placeholder: "amazon.com",
|
|
24169
|
+
default: "amazon.com"
|
|
24170
|
+
},
|
|
24171
|
+
{
|
|
24172
|
+
type: "password",
|
|
24173
|
+
key: "cookie",
|
|
24174
|
+
label: "Cookie blob (from proxy login)",
|
|
24175
|
+
showToggle: true
|
|
24176
|
+
},
|
|
24177
|
+
{
|
|
24178
|
+
type: "password",
|
|
24179
|
+
key: "refreshToken",
|
|
24180
|
+
label: "Refresh token (alternative to cookie)",
|
|
24181
|
+
showToggle: true
|
|
24182
|
+
},
|
|
24183
|
+
{
|
|
24184
|
+
type: "number",
|
|
24185
|
+
key: "pollIntervalMs",
|
|
24186
|
+
label: "State poll interval (ms)",
|
|
24187
|
+
min: 15e3,
|
|
24188
|
+
max: 36e5,
|
|
24189
|
+
default: 6e4
|
|
24190
|
+
}
|
|
24191
|
+
]
|
|
24192
|
+
}] };
|
|
24193
|
+
}
|
|
24194
|
+
//#endregion
|
|
23943
24195
|
//#region ../../node_modules/extend/index.js
|
|
23944
24196
|
var require_extend = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
23945
24197
|
var hasOwn = Object.prototype.hasOwnProperty;
|
|
@@ -62524,7 +62776,7 @@ var require_alexa_cookie = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
62524
62776
|
module.exports = AlexaCookie();
|
|
62525
62777
|
}));
|
|
62526
62778
|
//#endregion
|
|
62527
|
-
//#region src/alexa-
|
|
62779
|
+
//#region src/alexa-proxy-login.ts
|
|
62528
62780
|
var import_alexa_remote = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
62529
62781
|
var https = __require("https");
|
|
62530
62782
|
var querystring = __require("querystring");
|
|
@@ -65143,6 +65395,413 @@ var import_alexa_remote = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin
|
|
|
65143
65395
|
module.exports = AlexaRemote;
|
|
65144
65396
|
})))());
|
|
65145
65397
|
/**
|
|
65398
|
+
* A single interactive proxy-login attempt. One instance = one Amazon login.
|
|
65399
|
+
* Construct → {@link start} → poll {@link getStatus} → on `captured` the
|
|
65400
|
+
* `onCookie` callback has fired with the blob → {@link dispose}.
|
|
65401
|
+
*/
|
|
65402
|
+
var AlexaProxyLoginSession = class {
|
|
65403
|
+
#opts;
|
|
65404
|
+
#logger;
|
|
65405
|
+
#remote = null;
|
|
65406
|
+
#phase = "starting";
|
|
65407
|
+
#error = null;
|
|
65408
|
+
#startedAt = Date.now();
|
|
65409
|
+
#capturedAt = null;
|
|
65410
|
+
#captured = false;
|
|
65411
|
+
constructor(opts) {
|
|
65412
|
+
this.#opts = opts;
|
|
65413
|
+
this.#logger = opts.logger;
|
|
65414
|
+
}
|
|
65415
|
+
/** `http://<proxyOwnIp>:<proxyPort>/` — what the operator opens. */
|
|
65416
|
+
get loginUrl() {
|
|
65417
|
+
return `http://${this.#opts.proxyOwnIp}:${this.#opts.proxyPort}/`;
|
|
65418
|
+
}
|
|
65419
|
+
getStatus() {
|
|
65420
|
+
return {
|
|
65421
|
+
sessionId: this.#opts.sessionId,
|
|
65422
|
+
phase: this.#phase,
|
|
65423
|
+
loginUrl: this.#phase === "awaiting-login" ? this.loginUrl : "",
|
|
65424
|
+
amazonDomain: this.#opts.amazonDomain,
|
|
65425
|
+
error: this.#error,
|
|
65426
|
+
startedAt: this.#startedAt,
|
|
65427
|
+
capturedAt: this.#capturedAt
|
|
65428
|
+
};
|
|
65429
|
+
}
|
|
65430
|
+
/**
|
|
65431
|
+
* Spin up the proxy. Returns once `init` has been kicked off; the proxy then
|
|
65432
|
+
* binds asynchronously. alexa-remote2's `init` callback fires TWICE in proxy
|
|
65433
|
+
* mode (this is by design, not a bug):
|
|
65434
|
+
* 1. when the proxy is LISTENING, with an Error whose message is
|
|
65435
|
+
* "Please open http://… and login" — this is the "proxy ready" signal
|
|
65436
|
+
* (→ `awaiting-login`), NOT a failure;
|
|
65437
|
+
* 2. after a successful Amazon login, alexa-remote2 calls `setCookie` (→ the
|
|
65438
|
+
* `cookie` event, our real capture path) and auto-stops the proxy.
|
|
65439
|
+
* So `#onInitDone` must treat the "Please open" error as readiness and any
|
|
65440
|
+
* OTHER error as a genuine failure.
|
|
65441
|
+
*/
|
|
65442
|
+
async start() {
|
|
65443
|
+
const remote = new import_alexa_remote.default();
|
|
65444
|
+
this.#remote = remote;
|
|
65445
|
+
const amazonPage = this.#opts.amazonDomain.length > 0 ? this.#opts.amazonDomain : "amazon.com";
|
|
65446
|
+
remote.on("cookie", () => this.#onCookieEvent(remote));
|
|
65447
|
+
remote.init({
|
|
65448
|
+
proxyOnly: true,
|
|
65449
|
+
proxyOwnIp: this.#opts.proxyOwnIp,
|
|
65450
|
+
proxyPort: this.#opts.proxyPort,
|
|
65451
|
+
proxyLogLevel: "warn",
|
|
65452
|
+
amazonPage,
|
|
65453
|
+
logger: (...args) => {
|
|
65454
|
+
const line = args.map(String).join(" ");
|
|
65455
|
+
if (SECRET_LINE_RE.test(line)) return;
|
|
65456
|
+
this.#logger.debug("alexa proxy-login", { meta: {
|
|
65457
|
+
sessionId: this.#opts.sessionId,
|
|
65458
|
+
line
|
|
65459
|
+
} });
|
|
65460
|
+
}
|
|
65461
|
+
}, (err) => this.#onInitDone(err));
|
|
65462
|
+
}
|
|
65463
|
+
/** Stop the proxy + the remote (idempotent). Safe to call multiple times. */
|
|
65464
|
+
async dispose() {
|
|
65465
|
+
const remote = this.#remote;
|
|
65466
|
+
this.#remote = null;
|
|
65467
|
+
if (remote === null) return;
|
|
65468
|
+
try {
|
|
65469
|
+
remote.stop();
|
|
65470
|
+
} catch (err) {
|
|
65471
|
+
this.#logger.warn("alexa proxy-login: dispose stop() failed", { meta: {
|
|
65472
|
+
sessionId: this.#opts.sessionId,
|
|
65473
|
+
error: errMsg(err)
|
|
65474
|
+
} });
|
|
65475
|
+
}
|
|
65476
|
+
}
|
|
65477
|
+
/** Mark cancelled + dispose. */
|
|
65478
|
+
async cancel() {
|
|
65479
|
+
if (this.#phase !== "captured") this.#phase = "cancelled";
|
|
65480
|
+
await this.dispose();
|
|
65481
|
+
}
|
|
65482
|
+
#onCookieEvent(remote) {
|
|
65483
|
+
if (this.#captured) return;
|
|
65484
|
+
const cookieData = remote.cookieData;
|
|
65485
|
+
const cookie = remote.cookie;
|
|
65486
|
+
if (cookieData === void 0 || cookieData === null) return;
|
|
65487
|
+
if (typeof cookie !== "string" || cookie.length === 0) return;
|
|
65488
|
+
const blob = isStringOrObject(cookieData) ? cookieData : null;
|
|
65489
|
+
if (blob === null) return;
|
|
65490
|
+
this.#captured = true;
|
|
65491
|
+
this.#capturedAt = Date.now();
|
|
65492
|
+
this.#phase = "captured";
|
|
65493
|
+
this.#logger.info("alexa proxy-login: cookie captured", { meta: { sessionId: this.#opts.sessionId } });
|
|
65494
|
+
try {
|
|
65495
|
+
this.#opts.onCookie(this.#opts.sessionId, {
|
|
65496
|
+
cookieData: blob,
|
|
65497
|
+
cookie
|
|
65498
|
+
});
|
|
65499
|
+
} catch (err) {
|
|
65500
|
+
this.#logger.warn("alexa proxy-login: onCookie handler threw", { meta: {
|
|
65501
|
+
sessionId: this.#opts.sessionId,
|
|
65502
|
+
error: errMsg(err)
|
|
65503
|
+
} });
|
|
65504
|
+
}
|
|
65505
|
+
this.dispose();
|
|
65506
|
+
}
|
|
65507
|
+
#onInitDone(err) {
|
|
65508
|
+
if (this.#captured || this.#phase === "cancelled") return;
|
|
65509
|
+
if (err === void 0) return;
|
|
65510
|
+
if (isProxyReadyError(err)) {
|
|
65511
|
+
if (this.#phase === "starting") {
|
|
65512
|
+
this.#phase = "awaiting-login";
|
|
65513
|
+
this.#logger.info("alexa proxy-login: proxy ready — awaiting Amazon login", { meta: {
|
|
65514
|
+
sessionId: this.#opts.sessionId,
|
|
65515
|
+
loginUrl: this.loginUrl
|
|
65516
|
+
} });
|
|
65517
|
+
}
|
|
65518
|
+
return;
|
|
65519
|
+
}
|
|
65520
|
+
this.#phase = "error";
|
|
65521
|
+
this.#error = errMsg(err);
|
|
65522
|
+
this.#logger.warn("alexa proxy-login: init failed", { meta: {
|
|
65523
|
+
sessionId: this.#opts.sessionId,
|
|
65524
|
+
error: this.#error
|
|
65525
|
+
} });
|
|
65526
|
+
this.dispose();
|
|
65527
|
+
}
|
|
65528
|
+
};
|
|
65529
|
+
/**
|
|
65530
|
+
* Lines emitted by alexa-cookie2 that contain raw credential material
|
|
65531
|
+
* (cookie values, CSRF tokens, bearer tokens). Any log line matching this
|
|
65532
|
+
* pattern is silently dropped — it must never reach the structured logger or
|
|
65533
|
+
* Loki.
|
|
65534
|
+
*
|
|
65535
|
+
* Covers patterns such as:
|
|
65536
|
+
* "Alexa-Cookie: Update Cookie <name>=<value>"
|
|
65537
|
+
* "Result: csrf=…, Cookie=…"
|
|
65538
|
+
* "Set-Cookie: …"
|
|
65539
|
+
*/
|
|
65540
|
+
var SECRET_LINE_RE = /cookie|csrf|token/i;
|
|
65541
|
+
/**
|
|
65542
|
+
* alexa-remote2 signals "proxy ready, go log in" by invoking `init`'s callback
|
|
65543
|
+
* with an Error carrying the "Please open http://…/ with your browser and login"
|
|
65544
|
+
* message (alexa-cookie2 `generateAlexaCookie` listening callback). Detect it so
|
|
65545
|
+
* we treat readiness as success, not failure.
|
|
65546
|
+
*/
|
|
65547
|
+
function isProxyReadyError(err) {
|
|
65548
|
+
const m = errMsg(err).toLowerCase();
|
|
65549
|
+
return m.includes("please open") && m.includes("login");
|
|
65550
|
+
}
|
|
65551
|
+
/** True for the `string | object` forms alexa-remote2 accepts back as `cookie`. */
|
|
65552
|
+
function isStringOrObject(value) {
|
|
65553
|
+
return typeof value === "string" || typeof value === "object" && value !== null;
|
|
65554
|
+
}
|
|
65555
|
+
//#endregion
|
|
65556
|
+
//#region src/alexa-proxy-login-manager.ts
|
|
65557
|
+
var AlexaProxyLoginManager = class {
|
|
65558
|
+
#logger;
|
|
65559
|
+
#getProxyConfig;
|
|
65560
|
+
#session = null;
|
|
65561
|
+
#captured = /* @__PURE__ */ new Map();
|
|
65562
|
+
constructor(deps) {
|
|
65563
|
+
this.#logger = deps.logger;
|
|
65564
|
+
this.#getProxyConfig = deps.getProxyConfig;
|
|
65565
|
+
}
|
|
65566
|
+
/**
|
|
65567
|
+
* Start a fresh proxy-login. Cancels any in-flight session first (single
|
|
65568
|
+
* fixed port). Returns the `loginUrl` for the onboarding UI to open.
|
|
65569
|
+
*/
|
|
65570
|
+
async start(amazonDomain) {
|
|
65571
|
+
await this.cancelActive();
|
|
65572
|
+
const { proxyHost, proxyPort } = this.#getProxyConfig();
|
|
65573
|
+
const host = proxyHost.length > 0 ? proxyHost : detectPrimaryIpv4();
|
|
65574
|
+
if (host.length === 0) throw new Error("alexa proxy-login: could not auto-detect a reachable host IP — set proxyHost in settings");
|
|
65575
|
+
const sessionId = randomBytes(9).toString("base64url");
|
|
65576
|
+
const session = new AlexaProxyLoginSession({
|
|
65577
|
+
sessionId,
|
|
65578
|
+
amazonDomain,
|
|
65579
|
+
proxyOwnIp: host,
|
|
65580
|
+
proxyPort,
|
|
65581
|
+
logger: this.#logger,
|
|
65582
|
+
onCookie: (id, captured) => {
|
|
65583
|
+
this.#captured.set(id, captured);
|
|
65584
|
+
}
|
|
65585
|
+
});
|
|
65586
|
+
this.#session = session;
|
|
65587
|
+
try {
|
|
65588
|
+
await session.start();
|
|
65589
|
+
} catch (err) {
|
|
65590
|
+
this.#session = null;
|
|
65591
|
+
await session.dispose();
|
|
65592
|
+
throw new Error(`alexa proxy-login: failed to start proxy — ${errMsg(err)}`);
|
|
65593
|
+
}
|
|
65594
|
+
return {
|
|
65595
|
+
sessionId,
|
|
65596
|
+
loginUrl: session.loginUrl,
|
|
65597
|
+
amazonDomain
|
|
65598
|
+
};
|
|
65599
|
+
}
|
|
65600
|
+
/** Live status for a session id (or null when it isn't the active one). */
|
|
65601
|
+
status(sessionId) {
|
|
65602
|
+
const session = this.#session;
|
|
65603
|
+
if (session === null) return null;
|
|
65604
|
+
const status = session.getStatus();
|
|
65605
|
+
return status.sessionId === sessionId ? status : null;
|
|
65606
|
+
}
|
|
65607
|
+
/**
|
|
65608
|
+
* Pull (and consume) the captured cookie material for a session. Returns null
|
|
65609
|
+
* when nothing has been captured yet. The caller commits it onto a broker.
|
|
65610
|
+
*/
|
|
65611
|
+
takeCaptured(sessionId) {
|
|
65612
|
+
const captured = this.#captured.get(sessionId) ?? null;
|
|
65613
|
+
if (captured !== null) this.#captured.delete(sessionId);
|
|
65614
|
+
return captured;
|
|
65615
|
+
}
|
|
65616
|
+
/** Cancel a specific session (no-op when it isn't active). */
|
|
65617
|
+
async cancel(sessionId) {
|
|
65618
|
+
const session = this.#session;
|
|
65619
|
+
if (session === null) return;
|
|
65620
|
+
if (session.getStatus().sessionId !== sessionId) return;
|
|
65621
|
+
await this.cancelActive();
|
|
65622
|
+
this.#captured.delete(sessionId);
|
|
65623
|
+
}
|
|
65624
|
+
/** Cancel + dispose whatever session is currently active. */
|
|
65625
|
+
async cancelActive() {
|
|
65626
|
+
const session = this.#session;
|
|
65627
|
+
this.#session = null;
|
|
65628
|
+
if (session === null) return;
|
|
65629
|
+
try {
|
|
65630
|
+
await session.cancel();
|
|
65631
|
+
} catch (err) {
|
|
65632
|
+
this.#logger.warn("alexa proxy-login: cancelActive failed", { meta: { error: errMsg(err) } });
|
|
65633
|
+
}
|
|
65634
|
+
}
|
|
65635
|
+
/** Full teardown on addon shutdown. */
|
|
65636
|
+
async dispose() {
|
|
65637
|
+
await this.cancelActive();
|
|
65638
|
+
this.#captured.clear();
|
|
65639
|
+
}
|
|
65640
|
+
};
|
|
65641
|
+
/**
|
|
65642
|
+
* Best-effort primary non-loopback IPv4 of the host. Used when the operator
|
|
65643
|
+
* leaves `proxyHost` blank. Returns '' when none found (then the operator must
|
|
65644
|
+
* set it explicitly — common on multi-homed/containerised hosts).
|
|
65645
|
+
*/
|
|
65646
|
+
function detectPrimaryIpv4() {
|
|
65647
|
+
const ifaces = networkInterfaces();
|
|
65648
|
+
for (const addrs of Object.values(ifaces)) {
|
|
65649
|
+
if (addrs === void 0) continue;
|
|
65650
|
+
for (const addr of addrs) if (addr.family === "IPv4" && !addr.internal && addr.address.length > 0) return addr.address;
|
|
65651
|
+
}
|
|
65652
|
+
return "";
|
|
65653
|
+
}
|
|
65654
|
+
/**
|
|
65655
|
+
* Serialize the captured `cookieData` blob into the single STRING the broker
|
|
65656
|
+
* connection stores in its `cookie` field. `AlexaRemoteClient` already parses a
|
|
65657
|
+
* JSON-object cookie string back into the richer object form on init, so we
|
|
65658
|
+
* persist the object as JSON; a bare string passes through unchanged.
|
|
65659
|
+
*/
|
|
65660
|
+
function capturedToCookieString(captured) {
|
|
65661
|
+
const { cookieData } = captured;
|
|
65662
|
+
if (typeof cookieData === "string") return cookieData;
|
|
65663
|
+
try {
|
|
65664
|
+
return JSON.stringify(cookieData);
|
|
65665
|
+
} catch {
|
|
65666
|
+
return captured.cookie;
|
|
65667
|
+
}
|
|
65668
|
+
}
|
|
65669
|
+
//#endregion
|
|
65670
|
+
//#region src/alexa-login-routes.ts
|
|
65671
|
+
var StartBodySchema = object({ amazonDomain: _enum([
|
|
65672
|
+
"amazon.com",
|
|
65673
|
+
"amazon.it",
|
|
65674
|
+
"amazon.de",
|
|
65675
|
+
"amazon.co.uk",
|
|
65676
|
+
"amazon.fr",
|
|
65677
|
+
"amazon.es",
|
|
65678
|
+
"amazon.ca",
|
|
65679
|
+
"amazon.com.au",
|
|
65680
|
+
"amazon.co.jp",
|
|
65681
|
+
"amazon.in",
|
|
65682
|
+
"amazon.com.br",
|
|
65683
|
+
"amazon.com.mx"
|
|
65684
|
+
]).default("amazon.com") });
|
|
65685
|
+
var StatusQuerySchema = object({ sessionId: string().min(1) });
|
|
65686
|
+
var CommitBodySchema = object({
|
|
65687
|
+
sessionId: string().min(1),
|
|
65688
|
+
/** Name for a NEW broker (when `brokerId` is omitted). */
|
|
65689
|
+
name: string().min(1).default("Alexa account"),
|
|
65690
|
+
/** Existing broker to RE-LINK; omit to create a fresh broker. */
|
|
65691
|
+
brokerId: string().min(1).optional()
|
|
65692
|
+
});
|
|
65693
|
+
var CancelBodySchema = object({ sessionId: string().min(1) });
|
|
65694
|
+
/** Parse a JSON body that may arrive as an object or a raw string. */
|
|
65695
|
+
function asRecord(body) {
|
|
65696
|
+
if (typeof body === "object" && body !== null && !Array.isArray(body)) return Object.fromEntries(Object.entries(body));
|
|
65697
|
+
if (typeof body === "string" && body.length > 0) try {
|
|
65698
|
+
const parsed = JSON.parse(body);
|
|
65699
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return Object.fromEntries(Object.entries(parsed));
|
|
65700
|
+
} catch {}
|
|
65701
|
+
return {};
|
|
65702
|
+
}
|
|
65703
|
+
function buildAlexaLoginRoutes(deps) {
|
|
65704
|
+
const { logger, proxyLogin, commitCookie } = deps;
|
|
65705
|
+
return [
|
|
65706
|
+
{
|
|
65707
|
+
method: "POST",
|
|
65708
|
+
path: "/login/start",
|
|
65709
|
+
access: "admin",
|
|
65710
|
+
description: "Start the interactive Amazon proxy-login; returns the URL to open",
|
|
65711
|
+
handler: async (req, reply) => {
|
|
65712
|
+
const parsed = StartBodySchema.safeParse(asRecord(req.body));
|
|
65713
|
+
if (!parsed.success) {
|
|
65714
|
+
reply.code(400).send({
|
|
65715
|
+
error: "invalid body",
|
|
65716
|
+
detail: parsed.error.message
|
|
65717
|
+
});
|
|
65718
|
+
return;
|
|
65719
|
+
}
|
|
65720
|
+
try {
|
|
65721
|
+
const result = await proxyLogin.start(parsed.data.amazonDomain);
|
|
65722
|
+
reply.code(200).send(result);
|
|
65723
|
+
} catch (err) {
|
|
65724
|
+
logger.warn("alexa login/start failed", { meta: { error: errMsg(err) } });
|
|
65725
|
+
reply.code(500).send({ error: errMsg(err) });
|
|
65726
|
+
}
|
|
65727
|
+
}
|
|
65728
|
+
},
|
|
65729
|
+
{
|
|
65730
|
+
method: "GET",
|
|
65731
|
+
path: "/login/status",
|
|
65732
|
+
access: "admin",
|
|
65733
|
+
description: "Poll the interactive login session status (no secrets returned)",
|
|
65734
|
+
handler: async (req, reply) => {
|
|
65735
|
+
const parsed = StatusQuerySchema.safeParse(req.query);
|
|
65736
|
+
if (!parsed.success) {
|
|
65737
|
+
reply.code(400).send({ error: "missing sessionId" });
|
|
65738
|
+
return;
|
|
65739
|
+
}
|
|
65740
|
+
const status = proxyLogin.status(parsed.data.sessionId);
|
|
65741
|
+
if (status === null) {
|
|
65742
|
+
reply.code(404).send({ error: "unknown or expired session" });
|
|
65743
|
+
return;
|
|
65744
|
+
}
|
|
65745
|
+
reply.code(200).send(status);
|
|
65746
|
+
}
|
|
65747
|
+
},
|
|
65748
|
+
{
|
|
65749
|
+
method: "POST",
|
|
65750
|
+
path: "/login/commit",
|
|
65751
|
+
access: "admin",
|
|
65752
|
+
description: "Persist the captured cookie onto a new or existing broker",
|
|
65753
|
+
handler: async (req, reply) => {
|
|
65754
|
+
const parsed = CommitBodySchema.safeParse(asRecord(req.body));
|
|
65755
|
+
if (!parsed.success) {
|
|
65756
|
+
reply.code(400).send({
|
|
65757
|
+
error: "invalid body",
|
|
65758
|
+
detail: parsed.error.message
|
|
65759
|
+
});
|
|
65760
|
+
return;
|
|
65761
|
+
}
|
|
65762
|
+
const { sessionId, name, brokerId } = parsed.data;
|
|
65763
|
+
const status = proxyLogin.status(sessionId);
|
|
65764
|
+
const captured = proxyLogin.takeCaptured(sessionId);
|
|
65765
|
+
if (captured === null) {
|
|
65766
|
+
reply.code(409).send({ error: "no captured cookie for this session (login not completed yet?)" });
|
|
65767
|
+
return;
|
|
65768
|
+
}
|
|
65769
|
+
const amazonDomain = status?.amazonDomain ?? "amazon.com";
|
|
65770
|
+
try {
|
|
65771
|
+
const result = await commitCookie({
|
|
65772
|
+
cookie: capturedToCookieString(captured),
|
|
65773
|
+
amazonDomain,
|
|
65774
|
+
name,
|
|
65775
|
+
...brokerId !== void 0 ? { brokerId } : {}
|
|
65776
|
+
});
|
|
65777
|
+
await proxyLogin.cancel(sessionId);
|
|
65778
|
+
reply.code(200).send(result);
|
|
65779
|
+
} catch (err) {
|
|
65780
|
+
logger.warn("alexa login/commit failed", { meta: { error: errMsg(err) } });
|
|
65781
|
+
reply.code(500).send({ error: errMsg(err) });
|
|
65782
|
+
}
|
|
65783
|
+
}
|
|
65784
|
+
},
|
|
65785
|
+
{
|
|
65786
|
+
method: "POST",
|
|
65787
|
+
path: "/login/cancel",
|
|
65788
|
+
access: "admin",
|
|
65789
|
+
description: "Cancel an in-flight interactive login session",
|
|
65790
|
+
handler: async (req, reply) => {
|
|
65791
|
+
const parsed = CancelBodySchema.safeParse(asRecord(req.body));
|
|
65792
|
+
if (!parsed.success) {
|
|
65793
|
+
reply.code(400).send({ error: "missing sessionId" });
|
|
65794
|
+
return;
|
|
65795
|
+
}
|
|
65796
|
+
await proxyLogin.cancel(parsed.data.sessionId);
|
|
65797
|
+
reply.code(200).send({ ok: true });
|
|
65798
|
+
}
|
|
65799
|
+
}
|
|
65800
|
+
];
|
|
65801
|
+
}
|
|
65802
|
+
//#endregion
|
|
65803
|
+
//#region src/alexa-remote-decode.ts
|
|
65804
|
+
/**
|
|
65146
65805
|
* Pure, total decoders translating `alexa-remote2` JSON response bodies into the
|
|
65147
65806
|
* controller-agnostic shapes the addon consumes ({@link AlexaAppliance} /
|
|
65148
65807
|
* {@link AlexaStateValue}). NO `alexa-remote2` import — these run against plain
|
|
@@ -65732,100 +66391,6 @@ var AlexaCommandSink = class {
|
|
|
65732
66391
|
/** The single in-process command sink shared across the addon. */
|
|
65733
66392
|
var alexaCommands = new AlexaCommandSink();
|
|
65734
66393
|
//#endregion
|
|
65735
|
-
//#region src/config.ts
|
|
65736
|
-
/**
|
|
65737
|
-
* Alexa account connection settings for ONE broker (= one Amazon account).
|
|
65738
|
-
*
|
|
65739
|
-
* v1 uses the unofficial alexa-remote2 cookie / refresh-token flow. The operator
|
|
65740
|
-
* supplies the Amazon domain/region and either a captured cookie blob or a
|
|
65741
|
-
* refresh token (the proxy-login UX captures one of these). State is poll-only,
|
|
65742
|
-
* so a configurable poll interval is included.
|
|
65743
|
-
*/
|
|
65744
|
-
var alexaConnectionSchema = object({
|
|
65745
|
-
/** Amazon domain/region. */
|
|
65746
|
-
amazonDomain: string().min(1).default("amazon.com").describe("Amazon domain (region)"),
|
|
65747
|
-
/**
|
|
65748
|
-
* Captured cookie material — either a raw cookie string OR the full
|
|
65749
|
-
* `cookieData` JSON registration blob produced by alexa-remote2's standalone
|
|
65750
|
-
* proxy login (preferred: it carries the refresh token + lets the session
|
|
65751
|
-
* auto-refresh). This is the primary auth input.
|
|
65752
|
-
*/
|
|
65753
|
-
cookie: string().default("").describe("Captured Alexa cookie / cookieData blob (from proxy login)"),
|
|
65754
|
-
/**
|
|
65755
|
-
* Refresh token. NOTE: a bare refresh token alone is NOT sufficient to init
|
|
65756
|
-
* alexa-remote2 — supply the full cookie / cookieData blob (which embeds it).
|
|
65757
|
-
* Retained for provenance / forward-compat.
|
|
65758
|
-
*/
|
|
65759
|
-
refreshToken: string().default("").describe("Alexa refresh token (must be inside cookie blob)"),
|
|
65760
|
-
/** Device-registration secret some flows need. */
|
|
65761
|
-
macDms: string().default("").describe("macDms device-registration secret (optional)"),
|
|
65762
|
-
/** Poll interval for state sync (Alexa has no clean 3rd-party push). */
|
|
65763
|
-
pollIntervalMs: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(15e3).max(36e5).default(6e4)).describe("State poll interval (ms) — conservative to avoid Amazon throttling")
|
|
65764
|
-
});
|
|
65765
|
-
/** Translate a stored connection into the client's credentials shape. */
|
|
65766
|
-
function connectionToCredentials(conn) {
|
|
65767
|
-
return {
|
|
65768
|
-
amazonDomain: conn.amazonDomain,
|
|
65769
|
-
...conn.cookie.length > 0 ? { cookie: conn.cookie } : {},
|
|
65770
|
-
...conn.refreshToken.length > 0 ? { refreshToken: conn.refreshToken } : {},
|
|
65771
|
-
...conn.macDms.length > 0 ? { macDms: conn.macDms } : {}
|
|
65772
|
-
};
|
|
65773
|
-
}
|
|
65774
|
-
/** Top-level addon config — an ordered list of broker entries (default empty). */
|
|
65775
|
-
var alexaAddonConfigSchema = object({ brokers: array(object({
|
|
65776
|
-
id: string().min(1),
|
|
65777
|
-
name: string().min(1),
|
|
65778
|
-
connection: alexaConnectionSchema,
|
|
65779
|
-
integrationId: string().optional()
|
|
65780
|
-
})).default([]) });
|
|
65781
|
-
/** Narrow an `unknown` to a shallow plain-object record (cast-free boundary). */
|
|
65782
|
-
function toRecord(value) {
|
|
65783
|
-
if (typeof value === "object" && value !== null && !Array.isArray(value)) return Object.fromEntries(Object.entries(value));
|
|
65784
|
-
return {};
|
|
65785
|
-
}
|
|
65786
|
-
/** Coerce a loose settings blob through the connection schema, applying defaults. */
|
|
65787
|
-
function settingsToAlexaConnection(settings) {
|
|
65788
|
-
return alexaConnectionSchema.parse(toRecord(settings));
|
|
65789
|
-
}
|
|
65790
|
-
/** Hand-written connection form for the broker/integration creation UI. */
|
|
65791
|
-
function buildConnectionFormSchema() {
|
|
65792
|
-
return { sections: [{
|
|
65793
|
-
id: "alexa-account",
|
|
65794
|
-
title: "Amazon Alexa account",
|
|
65795
|
-
description: "Link an Amazon account via the unofficial Alexa client. Supply a captured cookie OR a refresh token. NOTE: this is an unofficial integration — Amazon can change or break it without notice, and it may violate Amazon ToS. State is polled, not pushed.",
|
|
65796
|
-
columns: 1,
|
|
65797
|
-
fields: [
|
|
65798
|
-
{
|
|
65799
|
-
type: "text",
|
|
65800
|
-
key: "amazonDomain",
|
|
65801
|
-
label: "Amazon domain (region)",
|
|
65802
|
-
placeholder: "amazon.com",
|
|
65803
|
-
default: "amazon.com"
|
|
65804
|
-
},
|
|
65805
|
-
{
|
|
65806
|
-
type: "password",
|
|
65807
|
-
key: "cookie",
|
|
65808
|
-
label: "Cookie blob (from proxy login)",
|
|
65809
|
-
showToggle: true
|
|
65810
|
-
},
|
|
65811
|
-
{
|
|
65812
|
-
type: "password",
|
|
65813
|
-
key: "refreshToken",
|
|
65814
|
-
label: "Refresh token (alternative to cookie)",
|
|
65815
|
-
showToggle: true
|
|
65816
|
-
},
|
|
65817
|
-
{
|
|
65818
|
-
type: "number",
|
|
65819
|
-
key: "pollIntervalMs",
|
|
65820
|
-
label: "State poll interval (ms)",
|
|
65821
|
-
min: 15e3,
|
|
65822
|
-
max: 36e5,
|
|
65823
|
-
default: 6e4
|
|
65824
|
-
}
|
|
65825
|
-
]
|
|
65826
|
-
}] };
|
|
65827
|
-
}
|
|
65828
|
-
//#endregion
|
|
65829
66394
|
//#region src/alexa-integration-manager.ts
|
|
65830
66395
|
/**
|
|
65831
66396
|
* Wraps exactly one {@link AlexaClient} (= one Amazon account). Authenticates on
|
|
@@ -66992,7 +67557,13 @@ function isKnownCap(cap) {
|
|
|
66992
67557
|
//#endregion
|
|
66993
67558
|
//#region src/addon.ts
|
|
66994
67559
|
/** Default config — a fresh install starts with no accounts. */
|
|
66995
|
-
var DEFAULTS = {
|
|
67560
|
+
var DEFAULTS = {
|
|
67561
|
+
brokers: [],
|
|
67562
|
+
proxyLogin: {
|
|
67563
|
+
proxyHost: "",
|
|
67564
|
+
proxyPort: 3456
|
|
67565
|
+
}
|
|
67566
|
+
};
|
|
66996
67567
|
/** Device types an imported Alexa appliance can materialise as. */
|
|
66997
67568
|
var ALEXA_DEVICE_TYPES = [
|
|
66998
67569
|
DeviceType.Switch,
|
|
@@ -67015,15 +67586,21 @@ var ALEXA_DEVICE_TYPES = [
|
|
|
67015
67586
|
*
|
|
67016
67587
|
* The Alexa cloud client is the real alexa-remote2 binding (`AlexaRemoteClient`,
|
|
67017
67588
|
* see `alexa-remote-client.ts`); `StubAlexaClient` remains as a test double.
|
|
67018
|
-
* Auth is cookie / cookieData-blob based
|
|
67019
|
-
*
|
|
67020
|
-
* the addon
|
|
67589
|
+
* Auth is cookie / cookieData-blob based. The preferred onboarding path is the
|
|
67590
|
+
* interactive proxy login (alexa-remote2 proxy auth mode, driven by the
|
|
67591
|
+
* onboarding wizard via the addon-routes control plane): the operator opens a
|
|
67592
|
+
* local proxy URL in their browser, completes the real Amazon OAuth/2FA flow,
|
|
67593
|
+
* and the captured cookie is persisted automatically. Pasting a pre-obtained
|
|
67594
|
+
* cookie is kept as a fallback. Either way the cookie lands in the same
|
|
67595
|
+
* `connection.cookie` field; the session is persisted to the addon `dataDir`
|
|
67596
|
+
* and auto-refreshed.
|
|
67021
67597
|
*/
|
|
67022
67598
|
var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
67023
67599
|
addonId = "import-alexa";
|
|
67024
67600
|
providerName = "Alexa Import";
|
|
67025
67601
|
deviceClasses = Object.fromEntries(ALEXA_DEVICE_TYPES.map((t) => [t, AlexaDevice]));
|
|
67026
67602
|
registry = null;
|
|
67603
|
+
proxyLogin = null;
|
|
67027
67604
|
constructor() {
|
|
67028
67605
|
super({ ...DEFAULTS });
|
|
67029
67606
|
}
|
|
@@ -67054,9 +67631,24 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
67054
67631
|
});
|
|
67055
67632
|
alexaCommands.setDispatcher((brokerId, command) => this.requireRegistry().sendCommand(brokerId, command));
|
|
67056
67633
|
await this.registry.restore(this.config.brokers);
|
|
67634
|
+
this.proxyLogin = new AlexaProxyLoginManager({
|
|
67635
|
+
logger: this.ctx.logger,
|
|
67636
|
+
getProxyConfig: () => {
|
|
67637
|
+
const cfg = alexaProxyLoginSchema.parse(this.config.proxyLogin ?? {});
|
|
67638
|
+
return {
|
|
67639
|
+
proxyHost: cfg.proxyHost,
|
|
67640
|
+
proxyPort: cfg.proxyPort
|
|
67641
|
+
};
|
|
67642
|
+
}
|
|
67643
|
+
});
|
|
67057
67644
|
this.ctx.logger.info("Alexa: import addon initialised", { meta: { brokerCount: this.config.brokers.length } });
|
|
67058
67645
|
await this.reconcileIntegrationsToBrokers();
|
|
67059
67646
|
this.subscribeIntegrationLifecycle();
|
|
67647
|
+
const loginRoutes = buildAlexaLoginRoutes({
|
|
67648
|
+
logger: this.ctx.logger,
|
|
67649
|
+
proxyLogin: this.proxyLogin,
|
|
67650
|
+
commitCookie: (input) => this.commitCapturedCookie(input)
|
|
67651
|
+
});
|
|
67060
67652
|
return [
|
|
67061
67653
|
...regs,
|
|
67062
67654
|
{
|
|
@@ -67066,6 +67658,10 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
67066
67658
|
{
|
|
67067
67659
|
capability: deviceAdoptionCapability,
|
|
67068
67660
|
provider: this.buildAdoptionProvider()
|
|
67661
|
+
},
|
|
67662
|
+
{
|
|
67663
|
+
capability: addonRoutesCapability,
|
|
67664
|
+
provider: buildAddonRouteProvider("import-alexa", loginRoutes)
|
|
67069
67665
|
}
|
|
67070
67666
|
];
|
|
67071
67667
|
}
|
|
@@ -67115,6 +67711,12 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
67115
67711
|
}
|
|
67116
67712
|
async onShutdown() {
|
|
67117
67713
|
alexaCommands.clear();
|
|
67714
|
+
try {
|
|
67715
|
+
await this.proxyLogin?.dispose();
|
|
67716
|
+
} catch (err) {
|
|
67717
|
+
this.ctx.logger.warn("Alexa: proxy-login dispose error", { meta: { error: errMsg(err) } });
|
|
67718
|
+
}
|
|
67719
|
+
this.proxyLogin = null;
|
|
67118
67720
|
try {
|
|
67119
67721
|
await this.registry?.shutdown();
|
|
67120
67722
|
} catch (err) {
|
|
@@ -67191,6 +67793,40 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
67191
67793
|
logger: this.ctx.logger
|
|
67192
67794
|
});
|
|
67193
67795
|
}
|
|
67796
|
+
/**
|
|
67797
|
+
* Persist a cookie captured by the interactive proxy-login onto a broker. With
|
|
67798
|
+
* a `brokerId` the broker is RE-LINKED (credentials swapped, devices kept);
|
|
67799
|
+
* otherwise a NEW broker is created. The captured cookie goes into exactly the
|
|
67800
|
+
* same `connection.cookie` field a pasted cookie would — so the registry /
|
|
67801
|
+
* poll / adoption layers are unchanged.
|
|
67802
|
+
*/
|
|
67803
|
+
async commitCapturedCookie(input) {
|
|
67804
|
+
const reg = this.requireRegistry();
|
|
67805
|
+
if (input.brokerId !== void 0) {
|
|
67806
|
+
const existing = this.config.brokers.find((b) => b.id === input.brokerId);
|
|
67807
|
+
if (existing === void 0) throw new Error(`commitCookie: unknown broker "${input.brokerId}"`);
|
|
67808
|
+
const connection = {
|
|
67809
|
+
...existing.connection,
|
|
67810
|
+
amazonDomain: input.amazonDomain,
|
|
67811
|
+
cookie: input.cookie
|
|
67812
|
+
};
|
|
67813
|
+
const updated = await reg.updateEntry(input.brokerId, connection);
|
|
67814
|
+
await this.updateGlobalSettings({ brokers: this.config.brokers.map((b) => b.id === input.brokerId ? updated : b) });
|
|
67815
|
+
this.ctx.logger.info("Alexa: proxy-login cookie committed to existing broker", { meta: { brokerId: input.brokerId } });
|
|
67816
|
+
return { brokerId: input.brokerId };
|
|
67817
|
+
}
|
|
67818
|
+
const connection = settingsToAlexaConnection({
|
|
67819
|
+
amazonDomain: input.amazonDomain,
|
|
67820
|
+
cookie: input.cookie
|
|
67821
|
+
});
|
|
67822
|
+
const entry = await reg.createEntry(input.name, connection);
|
|
67823
|
+
await this.updateGlobalSettings({ brokers: [...this.config.brokers, entry] });
|
|
67824
|
+
this.ctx.logger.info("Alexa: proxy-login cookie committed to new broker", { meta: {
|
|
67825
|
+
brokerId: entry.id,
|
|
67826
|
+
name: input.name
|
|
67827
|
+
} });
|
|
67828
|
+
return { brokerId: entry.id };
|
|
67829
|
+
}
|
|
67194
67830
|
async cascadeRemoveDevicesForBroker(brokerId) {
|
|
67195
67831
|
const reg = this.ctx.kernel.deviceRegistry;
|
|
67196
67832
|
const devices = this.ctx.kernel.devices;
|
|
@@ -67319,6 +67955,24 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
67319
67955
|
label: "Accounts are managed separately",
|
|
67320
67956
|
content: "This integration links to an Amazon Alexa account broker. Add, edit, or remove accounts from External systems → Brokers, then adopt the imported devices. The integration only stores a reference to its broker. NOTE: this is an unofficial integration that Amazon can change or break without notice."
|
|
67321
67957
|
}]
|
|
67958
|
+
}, {
|
|
67959
|
+
id: "alexa-proxy-login",
|
|
67960
|
+
title: "Interactive login (proxy)",
|
|
67961
|
+
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.",
|
|
67962
|
+
columns: 2,
|
|
67963
|
+
fields: [{
|
|
67964
|
+
type: "text",
|
|
67965
|
+
key: "proxyLogin.proxyHost",
|
|
67966
|
+
label: "Login proxy host/IP (blank = auto-detect)",
|
|
67967
|
+
placeholder: "auto-detect"
|
|
67968
|
+
}, {
|
|
67969
|
+
type: "number",
|
|
67970
|
+
key: "proxyLogin.proxyPort",
|
|
67971
|
+
label: "Login proxy port",
|
|
67972
|
+
min: 1,
|
|
67973
|
+
max: 65535,
|
|
67974
|
+
default: 3456
|
|
67975
|
+
}]
|
|
67322
67976
|
}] });
|
|
67323
67977
|
}
|
|
67324
67978
|
async supportsManualCreation() {
|
|
@@ -67353,4 +68007,4 @@ function capsEqual(a, b) {
|
|
|
67353
68007
|
return true;
|
|
67354
68008
|
}
|
|
67355
68009
|
//#endregion
|
|
67356
|
-
export { AlexaImportAddon,
|
|
68010
|
+
export { AlexaImportAddon, AlexaClientNotWiredError as a, alexaAddonConfigSchema as c, connectionToCredentials as d, settingsToAlexaConnection as f, AlexaAuthError as i, alexaConnectionSchema as l, mappableAppliance as n, StubAlexaClient as o, mapAlexaCapabilitiesToCamStack as r, createAlexaClient as s, buildAlexaApplianceCandidate as t, buildConnectionFormSchema as u };
|