@camstack/addon-import-alexa 0.2.186 → 0.2.187
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/addon.js +209 -37
- package/dist/addon.mjs +209 -37
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.187] - 2026-11-05
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- The integration-settings drop no longer refuses on an Alexa account; its legacy broker link is carried onto the broker record first
|
|
15
|
+
|
|
10
16
|
## [0.2.186] - 2026-11-05
|
|
11
17
|
|
|
12
18
|
### Changed
|
package/dist/addon.js
CHANGED
|
@@ -8841,6 +8841,13 @@ var RecordsError = class extends Error {
|
|
|
8841
8841
|
this.detail = detail;
|
|
8842
8842
|
}
|
|
8843
8843
|
};
|
|
8844
|
+
/** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
|
|
8845
|
+
var RevConflictError = class extends RecordsError {
|
|
8846
|
+
constructor(detail) {
|
|
8847
|
+
super("rev-conflict", detail);
|
|
8848
|
+
this.name = "RevConflictError";
|
|
8849
|
+
}
|
|
8850
|
+
};
|
|
8844
8851
|
/** The keychain could not store or serve a value: explicit, never an empty string. */
|
|
8845
8852
|
var RecordSecretUnavailableError = class extends RecordsError {
|
|
8846
8853
|
constructor(detail) {
|
|
@@ -14770,6 +14777,72 @@ function flattenPortsOver(handle) {
|
|
|
14770
14777
|
};
|
|
14771
14778
|
}
|
|
14772
14779
|
/**
|
|
14780
|
+
* The addon-side ports of `moveIntegrationRowsToRecords` (planning#416). An addon runs the move
|
|
14781
|
+
* from its own process, so the leftover rows are reached through the migration-only door
|
|
14782
|
+
* (`migrations.readLegacyIntegrationRows` / `clearLegacyIntegrationRows`) on the hub's connection.
|
|
14783
|
+
* The door returns markers. This process is the owner, so it resolves and releases them with
|
|
14784
|
+
* `resolveSecretMarkers` — the same call the old settings door used — and only then asks the
|
|
14785
|
+
* door to delete the rows. The addon's own record kind is its records handle.
|
|
14786
|
+
*/
|
|
14787
|
+
async function resolvedRows(stored, secrets) {
|
|
14788
|
+
if (secrets === null) return stored;
|
|
14789
|
+
const { value, unresolved } = await resolveSecretMarkers(stored, secrets);
|
|
14790
|
+
if (unresolved.length > 0) throw new Error(`${unresolved.length} secret(s) cannot be read from the keychain`);
|
|
14791
|
+
return value;
|
|
14792
|
+
}
|
|
14793
|
+
function integrationRowsPortsOverApi(deps) {
|
|
14794
|
+
const secrets = deps.secrets ?? null;
|
|
14795
|
+
return {
|
|
14796
|
+
warn: (message) => deps.log.warn(message),
|
|
14797
|
+
listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
|
|
14798
|
+
id: i.id,
|
|
14799
|
+
name: i.name
|
|
14800
|
+
})),
|
|
14801
|
+
readRows: async (integrationId) => resolvedRows(await deps.api.readLegacyIntegrationRows({ integrationId }), secrets),
|
|
14802
|
+
putRecord: async (id, body) => {
|
|
14803
|
+
if (await deps.records.get(id) !== null) return "exists";
|
|
14804
|
+
try {
|
|
14805
|
+
await deps.records.put({
|
|
14806
|
+
id,
|
|
14807
|
+
data: body
|
|
14808
|
+
}, { ifRev: 0 });
|
|
14809
|
+
} catch (err) {
|
|
14810
|
+
if (err instanceof RevConflictError) return "exists";
|
|
14811
|
+
throw err;
|
|
14812
|
+
}
|
|
14813
|
+
return "created";
|
|
14814
|
+
},
|
|
14815
|
+
readRecord: async (id) => (await deps.records.getResolved(id))?.data ?? null,
|
|
14816
|
+
deleteRecord: async (id) => {
|
|
14817
|
+
await deps.records.delete(id);
|
|
14818
|
+
},
|
|
14819
|
+
clearRows: async (integrationId) => {
|
|
14820
|
+
const refs = collectSecretMarkerRefs(await deps.api.readLegacyIntegrationRows({ integrationId }));
|
|
14821
|
+
if (refs.length > 0 && secrets !== null) await secrets.delete(refs);
|
|
14822
|
+
await deps.api.clearLegacyIntegrationRows({ integrationId });
|
|
14823
|
+
}
|
|
14824
|
+
};
|
|
14825
|
+
}
|
|
14826
|
+
function defaultIntegrationApiOver(api) {
|
|
14827
|
+
return {
|
|
14828
|
+
list: () => api.integrations.list.query(),
|
|
14829
|
+
readLegacyIntegrationRows: (input) => api.migrations.readLegacyIntegrationRows.query(input),
|
|
14830
|
+
clearLegacyIntegrationRows: (input) => api.migrations.clearLegacyIntegrationRows.mutate(input),
|
|
14831
|
+
listDevices: (addonId) => api.deviceManager.listAll.query({
|
|
14832
|
+
addonId,
|
|
14833
|
+
projection: "slim"
|
|
14834
|
+
}),
|
|
14835
|
+
setDeviceIntegrationId: async (deviceId, integrationId) => {
|
|
14836
|
+
await api.deviceManager.setIntegrationId.mutate({
|
|
14837
|
+
deviceId,
|
|
14838
|
+
integrationId
|
|
14839
|
+
});
|
|
14840
|
+
},
|
|
14841
|
+
readDeviceHasConfig: async (deviceId) => Object.keys((await api.deviceManager.getDevice.query({ deviceId }))?.config ?? {}).length > 0,
|
|
14842
|
+
readDeviceIntegrationId: async (deviceId) => (await api.deviceManager.getDevice.query({ deviceId }))?.integrationId ?? null
|
|
14843
|
+
};
|
|
14844
|
+
}
|
|
14845
|
+
/**
|
|
14773
14846
|
* The trainer contract — what the hub hands the cloud trainer and what it
|
|
14774
14847
|
* gets back (cloud training, D805+).
|
|
14775
14848
|
*
|
|
@@ -140596,7 +140669,7 @@ function isStringOrObject(value) {
|
|
|
140596
140669
|
//#endregion
|
|
140597
140670
|
//#region src/alexa-proxy-login-manager.ts
|
|
140598
140671
|
/** The addon id the result page names to return to. */
|
|
140599
|
-
var ADDON_ID = "import-alexa";
|
|
140672
|
+
var ADDON_ID$1 = "import-alexa";
|
|
140600
140673
|
/** An attempt lives as long as a sign-in attempt does elsewhere (10 min). */
|
|
140601
140674
|
var ATTEMPT_TTL_MS = 600 * 1e3;
|
|
140602
140675
|
/** How long the browser waits on the Amazon page for alexa-cookie2 to finish registering. */
|
|
@@ -140681,16 +140754,16 @@ var AlexaProxyLoginManager = class {
|
|
|
140681
140754
|
this.#logger.warn("alexa proxy-login: Amazon ended the login without an authorization code", { meta: { sessionId } });
|
|
140682
140755
|
return {
|
|
140683
140756
|
status: "error",
|
|
140684
|
-
addonId: ADDON_ID,
|
|
140757
|
+
addonId: ADDON_ID$1,
|
|
140685
140758
|
reason: "no_authorization_code"
|
|
140686
140759
|
};
|
|
140687
140760
|
}
|
|
140688
140761
|
return await this.#session?.whenCaptured(this.#captureWaitMs) === true ? {
|
|
140689
140762
|
status: "success",
|
|
140690
|
-
addonId: ADDON_ID
|
|
140763
|
+
addonId: ADDON_ID$1
|
|
140691
140764
|
} : {
|
|
140692
140765
|
status: "error",
|
|
140693
|
-
addonId: ADDON_ID,
|
|
140766
|
+
addonId: ADDON_ID$1,
|
|
140694
140767
|
reason: "exchange_failed"
|
|
140695
140768
|
};
|
|
140696
140769
|
}
|
|
@@ -144966,6 +145039,79 @@ var AlexaSpeakController = class {
|
|
|
144966
145039
|
}
|
|
144967
145040
|
};
|
|
144968
145041
|
//#endregion
|
|
145042
|
+
//#region src/migrations/integration-rows-to-record.ts
|
|
145043
|
+
/**
|
|
145044
|
+
* The Alexa integration's last legacy row, `brokerId` (the integration to broker link), moves onto
|
|
145045
|
+
* the broker record (planning#416): the runtime reads `broker.integrationId`, so the row is only
|
|
145046
|
+
* stamped across when the broker has no link yet, then every legacy row of the integration is
|
|
145047
|
+
* cleared (the door `integration_settings` is dropped through accepts an addon only once this
|
|
145048
|
+
* ran). Nothing is overwritten: a broker that already names an integration, or a `brokerId` that
|
|
145049
|
+
* names no broker, is left as it is and said so in a warning (ids only, never a value).
|
|
145050
|
+
* An empty door is a no-op, so a rerun is too.
|
|
145051
|
+
*/
|
|
145052
|
+
var INTEGRATION_ROWS_TO_RECORD_ID = "import-alexa/2026-11-05-integration-rows-to-record";
|
|
145053
|
+
var ADDON_ID = "import-alexa";
|
|
145054
|
+
async function moveAlexaIntegrationRows(ports) {
|
|
145055
|
+
const integrations = await ports.listIntegrations(ADDON_ID);
|
|
145056
|
+
let stamped = 0;
|
|
145057
|
+
let cleared = 0;
|
|
145058
|
+
for (const integration of integrations) {
|
|
145059
|
+
const rows = await ports.readIntegrationRows(integration.id);
|
|
145060
|
+
if (Object.keys(rows).length === 0) continue;
|
|
145061
|
+
const brokerId = rows["brokerId"];
|
|
145062
|
+
if (typeof brokerId === "string" && brokerId !== "") {
|
|
145063
|
+
const broker = await ports.readBrokerLink(brokerId);
|
|
145064
|
+
if (broker === null) ports.warn(`integration '${integration.id}': brokerId '${brokerId}' names no broker record; nothing linked`);
|
|
145065
|
+
else if (broker.integrationId !== void 0) ports.warn(`integration '${integration.id}': broker '${brokerId}' already names integration '${broker.integrationId}'; the link is not changed`);
|
|
145066
|
+
else {
|
|
145067
|
+
await ports.stampBrokerLink(brokerId, integration.id);
|
|
145068
|
+
if ((await ports.readBrokerLink(brokerId))?.integrationId !== integration.id) throw new Error(`integration '${integration.id}': the link did not read back from broker '${brokerId}' - the legacy settings are kept`);
|
|
145069
|
+
stamped += 1;
|
|
145070
|
+
}
|
|
145071
|
+
}
|
|
145072
|
+
await ports.clearIntegrationRows(integration.id);
|
|
145073
|
+
cleared += 1;
|
|
145074
|
+
}
|
|
145075
|
+
return cleared === 0 ? {
|
|
145076
|
+
outcome: "already-done",
|
|
145077
|
+
summary: { integrations: integrations.length }
|
|
145078
|
+
} : {
|
|
145079
|
+
outcome: "done",
|
|
145080
|
+
summary: {
|
|
145081
|
+
integrations: integrations.length,
|
|
145082
|
+
stamped,
|
|
145083
|
+
cleared
|
|
145084
|
+
}
|
|
145085
|
+
};
|
|
145086
|
+
}
|
|
145087
|
+
/** The hub side (the registry list, the migration-only door) and the broker records side. */
|
|
145088
|
+
function alexaIntegrationRowsPorts(deps) {
|
|
145089
|
+
const brokers = deps.records.kind(alexaBrokersEntryKind);
|
|
145090
|
+
const rows = integrationRowsPortsOverApi({
|
|
145091
|
+
api: deps.api,
|
|
145092
|
+
secrets: deps.secrets,
|
|
145093
|
+
records: deps.records.kind({
|
|
145094
|
+
name: alexaBrokersEntryKind.name,
|
|
145095
|
+
declaration: alexaBrokersEntryKind.declaration,
|
|
145096
|
+
schema: record(string(), unknown())
|
|
145097
|
+
}),
|
|
145098
|
+
log: { warn: deps.warn }
|
|
145099
|
+
});
|
|
145100
|
+
return {
|
|
145101
|
+
warn: deps.warn,
|
|
145102
|
+
listIntegrations: rows.listIntegrations,
|
|
145103
|
+
readIntegrationRows: rows.readRows,
|
|
145104
|
+
clearIntegrationRows: rows.clearRows,
|
|
145105
|
+
readBrokerLink: async (id) => {
|
|
145106
|
+
const row = await brokers.get(id);
|
|
145107
|
+
return row === null ? null : { integrationId: row.data.integrationId };
|
|
145108
|
+
},
|
|
145109
|
+
stampBrokerLink: async (id, integrationId) => {
|
|
145110
|
+
await brokers.patch(id, { integrationId });
|
|
145111
|
+
}
|
|
145112
|
+
};
|
|
145113
|
+
}
|
|
145114
|
+
//#endregion
|
|
144969
145115
|
//#region src/migrations/flatten-broker-records.ts
|
|
144970
145116
|
/**
|
|
144971
145117
|
* The Alexa broker records keep their account flat (planning#408 P3): the nested `connection`
|
|
@@ -145028,37 +145174,56 @@ async function stripEchoTargetVolumeAndMode(ports) {
|
|
|
145028
145174
|
//#endregion
|
|
145029
145175
|
//#region src/migrations/index.ts
|
|
145030
145176
|
/** The `import-alexa` catalogue (D987): offered to the engine when the addon initialises. */
|
|
145031
|
-
var ADDON_MIGRATIONS = [
|
|
145032
|
-
|
|
145033
|
-
|
|
145034
|
-
|
|
145035
|
-
|
|
145036
|
-
|
|
145037
|
-
|
|
145038
|
-
|
|
145039
|
-
|
|
145040
|
-
|
|
145041
|
-
|
|
145042
|
-
|
|
145043
|
-
|
|
145044
|
-
|
|
145045
|
-
|
|
145046
|
-
|
|
145047
|
-
|
|
145048
|
-
|
|
145049
|
-
|
|
145050
|
-
|
|
145051
|
-
|
|
145052
|
-
|
|
145053
|
-
|
|
145054
|
-
|
|
145055
|
-
|
|
145056
|
-
|
|
145057
|
-
|
|
145058
|
-
|
|
145059
|
-
|
|
145060
|
-
|
|
145061
|
-
}
|
|
145177
|
+
var ADDON_MIGRATIONS = [
|
|
145178
|
+
{
|
|
145179
|
+
meta: {
|
|
145180
|
+
id: "import-alexa/2026-11-01-strip-echo-target-volume-and-mode",
|
|
145181
|
+
owner: "import-alexa",
|
|
145182
|
+
level: "addon-state",
|
|
145183
|
+
seq: 1,
|
|
145184
|
+
dependsOn: [],
|
|
145185
|
+
store: "settings",
|
|
145186
|
+
scope: "cluster",
|
|
145187
|
+
onFailure: "tolerate",
|
|
145188
|
+
bodyKind: "procedure",
|
|
145189
|
+
description: "Remove the retired volume and mode from every stored Echo target",
|
|
145190
|
+
fixtureMark: "the Echo id and the name the migration does not know"
|
|
145191
|
+
},
|
|
145192
|
+
procedure: stripEchoTargetVolumeAndMode
|
|
145193
|
+
},
|
|
145194
|
+
{
|
|
145195
|
+
meta: {
|
|
145196
|
+
id: FLATTEN_BROKER_RECORDS_ID,
|
|
145197
|
+
owner: "import-alexa",
|
|
145198
|
+
level: "addon-state",
|
|
145199
|
+
seq: 2,
|
|
145200
|
+
dependsOn: [],
|
|
145201
|
+
store: "settings",
|
|
145202
|
+
scope: "cluster",
|
|
145203
|
+
onFailure: "block-owner",
|
|
145204
|
+
bodyKind: "procedure",
|
|
145205
|
+
description: "Move each Alexa account from a nested connection object to flat top-level fields under the same id, reading every secret back first",
|
|
145206
|
+
fixtureMark: "the integration link and the amazon domain of each account"
|
|
145207
|
+
},
|
|
145208
|
+
procedure: flattenBrokerRecords
|
|
145209
|
+
},
|
|
145210
|
+
{
|
|
145211
|
+
meta: {
|
|
145212
|
+
id: INTEGRATION_ROWS_TO_RECORD_ID,
|
|
145213
|
+
owner: "import-alexa",
|
|
145214
|
+
level: "addon-state",
|
|
145215
|
+
seq: 3,
|
|
145216
|
+
dependsOn: [FLATTEN_BROKER_RECORDS_ID],
|
|
145217
|
+
store: "settings",
|
|
145218
|
+
scope: "cluster",
|
|
145219
|
+
onFailure: "block-owner",
|
|
145220
|
+
bodyKind: "procedure",
|
|
145221
|
+
description: "Stamp the integration link from the legacy brokerId row onto its broker record when the broker has none, then clear the legacy integration settings rows",
|
|
145222
|
+
fixtureMark: "the name of the broker and the integration it already names"
|
|
145223
|
+
},
|
|
145224
|
+
procedure: moveAlexaIntegrationRows
|
|
145225
|
+
}
|
|
145226
|
+
];
|
|
145062
145227
|
//#endregion
|
|
145063
145228
|
//#region src/speak/icon-routes.ts
|
|
145064
145229
|
var ALEXA_ICON_SVG = "<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"#00CAFF\"><title>Amazon Alexa</title><path d=\"M12 0C5.37 0 0 5.37 0 12C0 18.09 4.53 23.11 10.4 23.9V21.5A1.59 1.59 0 0 0 9.32 19.97A8.41 8.41 0 0 1 3.6 11.8A8.37 8.37 0 0 1 12.09 3.6A8.4 8.4 0 0 1 20.4 12.31L20.39 12.38A8.68 8.68 0 0 1 20.36 12.76C20.36 12.83 20.35 12.9 20.34 12.96C20.34 13.04 20.33 13.12 20.32 13.19L20.3 13.29C19.27 20.07 10.45 23.87 10.4 23.9C10.92 23.97 11.46 24 12 24C18.63 24 24 18.63 24 12S18.63 0 12 0Z\"/></svg>";
|
|
@@ -145270,13 +145435,20 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
145270
145435
|
] };
|
|
145271
145436
|
}
|
|
145272
145437
|
migrationPorts(records) {
|
|
145273
|
-
|
|
145438
|
+
const secrets = this.ctx.secrets;
|
|
145439
|
+
if (secrets === void 0) throw new Error("import-alexa: ctx.secrets is required");
|
|
145274
145440
|
return {
|
|
145275
145441
|
readRows: () => Array.isArray(this.config.echoTargets) ? this.config.echoTargets : [],
|
|
145276
145442
|
writeRows: async (rows) => {
|
|
145277
145443
|
await this.updateGlobalSettings({ echoTargets: [...readEchoTargets({ echoTargets: rows })] });
|
|
145278
145444
|
},
|
|
145279
|
-
...flattenPortsOver(records.kind(alexaBrokersEntryKind))
|
|
145445
|
+
...flattenPortsOver(records.kind(alexaBrokersEntryKind)),
|
|
145446
|
+
...alexaIntegrationRowsPorts({
|
|
145447
|
+
api: defaultIntegrationApiOver(this.ctx.api),
|
|
145448
|
+
records,
|
|
145449
|
+
secrets,
|
|
145450
|
+
warn: (message) => this.ctx.logger.warn(message)
|
|
145451
|
+
})
|
|
145280
145452
|
};
|
|
145281
145453
|
}
|
|
145282
145454
|
/**
|
package/dist/addon.mjs
CHANGED
|
@@ -8839,6 +8839,13 @@ var RecordsError = class extends Error {
|
|
|
8839
8839
|
this.detail = detail;
|
|
8840
8840
|
}
|
|
8841
8841
|
};
|
|
8842
|
+
/** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
|
|
8843
|
+
var RevConflictError = class extends RecordsError {
|
|
8844
|
+
constructor(detail) {
|
|
8845
|
+
super("rev-conflict", detail);
|
|
8846
|
+
this.name = "RevConflictError";
|
|
8847
|
+
}
|
|
8848
|
+
};
|
|
8842
8849
|
/** The keychain could not store or serve a value: explicit, never an empty string. */
|
|
8843
8850
|
var RecordSecretUnavailableError = class extends RecordsError {
|
|
8844
8851
|
constructor(detail) {
|
|
@@ -14768,6 +14775,72 @@ function flattenPortsOver(handle) {
|
|
|
14768
14775
|
};
|
|
14769
14776
|
}
|
|
14770
14777
|
/**
|
|
14778
|
+
* The addon-side ports of `moveIntegrationRowsToRecords` (planning#416). An addon runs the move
|
|
14779
|
+
* from its own process, so the leftover rows are reached through the migration-only door
|
|
14780
|
+
* (`migrations.readLegacyIntegrationRows` / `clearLegacyIntegrationRows`) on the hub's connection.
|
|
14781
|
+
* The door returns markers. This process is the owner, so it resolves and releases them with
|
|
14782
|
+
* `resolveSecretMarkers` — the same call the old settings door used — and only then asks the
|
|
14783
|
+
* door to delete the rows. The addon's own record kind is its records handle.
|
|
14784
|
+
*/
|
|
14785
|
+
async function resolvedRows(stored, secrets) {
|
|
14786
|
+
if (secrets === null) return stored;
|
|
14787
|
+
const { value, unresolved } = await resolveSecretMarkers(stored, secrets);
|
|
14788
|
+
if (unresolved.length > 0) throw new Error(`${unresolved.length} secret(s) cannot be read from the keychain`);
|
|
14789
|
+
return value;
|
|
14790
|
+
}
|
|
14791
|
+
function integrationRowsPortsOverApi(deps) {
|
|
14792
|
+
const secrets = deps.secrets ?? null;
|
|
14793
|
+
return {
|
|
14794
|
+
warn: (message) => deps.log.warn(message),
|
|
14795
|
+
listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
|
|
14796
|
+
id: i.id,
|
|
14797
|
+
name: i.name
|
|
14798
|
+
})),
|
|
14799
|
+
readRows: async (integrationId) => resolvedRows(await deps.api.readLegacyIntegrationRows({ integrationId }), secrets),
|
|
14800
|
+
putRecord: async (id, body) => {
|
|
14801
|
+
if (await deps.records.get(id) !== null) return "exists";
|
|
14802
|
+
try {
|
|
14803
|
+
await deps.records.put({
|
|
14804
|
+
id,
|
|
14805
|
+
data: body
|
|
14806
|
+
}, { ifRev: 0 });
|
|
14807
|
+
} catch (err) {
|
|
14808
|
+
if (err instanceof RevConflictError) return "exists";
|
|
14809
|
+
throw err;
|
|
14810
|
+
}
|
|
14811
|
+
return "created";
|
|
14812
|
+
},
|
|
14813
|
+
readRecord: async (id) => (await deps.records.getResolved(id))?.data ?? null,
|
|
14814
|
+
deleteRecord: async (id) => {
|
|
14815
|
+
await deps.records.delete(id);
|
|
14816
|
+
},
|
|
14817
|
+
clearRows: async (integrationId) => {
|
|
14818
|
+
const refs = collectSecretMarkerRefs(await deps.api.readLegacyIntegrationRows({ integrationId }));
|
|
14819
|
+
if (refs.length > 0 && secrets !== null) await secrets.delete(refs);
|
|
14820
|
+
await deps.api.clearLegacyIntegrationRows({ integrationId });
|
|
14821
|
+
}
|
|
14822
|
+
};
|
|
14823
|
+
}
|
|
14824
|
+
function defaultIntegrationApiOver(api) {
|
|
14825
|
+
return {
|
|
14826
|
+
list: () => api.integrations.list.query(),
|
|
14827
|
+
readLegacyIntegrationRows: (input) => api.migrations.readLegacyIntegrationRows.query(input),
|
|
14828
|
+
clearLegacyIntegrationRows: (input) => api.migrations.clearLegacyIntegrationRows.mutate(input),
|
|
14829
|
+
listDevices: (addonId) => api.deviceManager.listAll.query({
|
|
14830
|
+
addonId,
|
|
14831
|
+
projection: "slim"
|
|
14832
|
+
}),
|
|
14833
|
+
setDeviceIntegrationId: async (deviceId, integrationId) => {
|
|
14834
|
+
await api.deviceManager.setIntegrationId.mutate({
|
|
14835
|
+
deviceId,
|
|
14836
|
+
integrationId
|
|
14837
|
+
});
|
|
14838
|
+
},
|
|
14839
|
+
readDeviceHasConfig: async (deviceId) => Object.keys((await api.deviceManager.getDevice.query({ deviceId }))?.config ?? {}).length > 0,
|
|
14840
|
+
readDeviceIntegrationId: async (deviceId) => (await api.deviceManager.getDevice.query({ deviceId }))?.integrationId ?? null
|
|
14841
|
+
};
|
|
14842
|
+
}
|
|
14843
|
+
/**
|
|
14771
14844
|
* The trainer contract — what the hub hands the cloud trainer and what it
|
|
14772
14845
|
* gets back (cloud training, D805+).
|
|
14773
14846
|
*
|
|
@@ -140594,7 +140667,7 @@ function isStringOrObject(value) {
|
|
|
140594
140667
|
//#endregion
|
|
140595
140668
|
//#region src/alexa-proxy-login-manager.ts
|
|
140596
140669
|
/** The addon id the result page names to return to. */
|
|
140597
|
-
var ADDON_ID = "import-alexa";
|
|
140670
|
+
var ADDON_ID$1 = "import-alexa";
|
|
140598
140671
|
/** An attempt lives as long as a sign-in attempt does elsewhere (10 min). */
|
|
140599
140672
|
var ATTEMPT_TTL_MS = 600 * 1e3;
|
|
140600
140673
|
/** How long the browser waits on the Amazon page for alexa-cookie2 to finish registering. */
|
|
@@ -140679,16 +140752,16 @@ var AlexaProxyLoginManager = class {
|
|
|
140679
140752
|
this.#logger.warn("alexa proxy-login: Amazon ended the login without an authorization code", { meta: { sessionId } });
|
|
140680
140753
|
return {
|
|
140681
140754
|
status: "error",
|
|
140682
|
-
addonId: ADDON_ID,
|
|
140755
|
+
addonId: ADDON_ID$1,
|
|
140683
140756
|
reason: "no_authorization_code"
|
|
140684
140757
|
};
|
|
140685
140758
|
}
|
|
140686
140759
|
return await this.#session?.whenCaptured(this.#captureWaitMs) === true ? {
|
|
140687
140760
|
status: "success",
|
|
140688
|
-
addonId: ADDON_ID
|
|
140761
|
+
addonId: ADDON_ID$1
|
|
140689
140762
|
} : {
|
|
140690
140763
|
status: "error",
|
|
140691
|
-
addonId: ADDON_ID,
|
|
140764
|
+
addonId: ADDON_ID$1,
|
|
140692
140765
|
reason: "exchange_failed"
|
|
140693
140766
|
};
|
|
140694
140767
|
}
|
|
@@ -144964,6 +145037,79 @@ var AlexaSpeakController = class {
|
|
|
144964
145037
|
}
|
|
144965
145038
|
};
|
|
144966
145039
|
//#endregion
|
|
145040
|
+
//#region src/migrations/integration-rows-to-record.ts
|
|
145041
|
+
/**
|
|
145042
|
+
* The Alexa integration's last legacy row, `brokerId` (the integration to broker link), moves onto
|
|
145043
|
+
* the broker record (planning#416): the runtime reads `broker.integrationId`, so the row is only
|
|
145044
|
+
* stamped across when the broker has no link yet, then every legacy row of the integration is
|
|
145045
|
+
* cleared (the door `integration_settings` is dropped through accepts an addon only once this
|
|
145046
|
+
* ran). Nothing is overwritten: a broker that already names an integration, or a `brokerId` that
|
|
145047
|
+
* names no broker, is left as it is and said so in a warning (ids only, never a value).
|
|
145048
|
+
* An empty door is a no-op, so a rerun is too.
|
|
145049
|
+
*/
|
|
145050
|
+
var INTEGRATION_ROWS_TO_RECORD_ID = "import-alexa/2026-11-05-integration-rows-to-record";
|
|
145051
|
+
var ADDON_ID = "import-alexa";
|
|
145052
|
+
async function moveAlexaIntegrationRows(ports) {
|
|
145053
|
+
const integrations = await ports.listIntegrations(ADDON_ID);
|
|
145054
|
+
let stamped = 0;
|
|
145055
|
+
let cleared = 0;
|
|
145056
|
+
for (const integration of integrations) {
|
|
145057
|
+
const rows = await ports.readIntegrationRows(integration.id);
|
|
145058
|
+
if (Object.keys(rows).length === 0) continue;
|
|
145059
|
+
const brokerId = rows["brokerId"];
|
|
145060
|
+
if (typeof brokerId === "string" && brokerId !== "") {
|
|
145061
|
+
const broker = await ports.readBrokerLink(brokerId);
|
|
145062
|
+
if (broker === null) ports.warn(`integration '${integration.id}': brokerId '${brokerId}' names no broker record; nothing linked`);
|
|
145063
|
+
else if (broker.integrationId !== void 0) ports.warn(`integration '${integration.id}': broker '${brokerId}' already names integration '${broker.integrationId}'; the link is not changed`);
|
|
145064
|
+
else {
|
|
145065
|
+
await ports.stampBrokerLink(brokerId, integration.id);
|
|
145066
|
+
if ((await ports.readBrokerLink(brokerId))?.integrationId !== integration.id) throw new Error(`integration '${integration.id}': the link did not read back from broker '${brokerId}' - the legacy settings are kept`);
|
|
145067
|
+
stamped += 1;
|
|
145068
|
+
}
|
|
145069
|
+
}
|
|
145070
|
+
await ports.clearIntegrationRows(integration.id);
|
|
145071
|
+
cleared += 1;
|
|
145072
|
+
}
|
|
145073
|
+
return cleared === 0 ? {
|
|
145074
|
+
outcome: "already-done",
|
|
145075
|
+
summary: { integrations: integrations.length }
|
|
145076
|
+
} : {
|
|
145077
|
+
outcome: "done",
|
|
145078
|
+
summary: {
|
|
145079
|
+
integrations: integrations.length,
|
|
145080
|
+
stamped,
|
|
145081
|
+
cleared
|
|
145082
|
+
}
|
|
145083
|
+
};
|
|
145084
|
+
}
|
|
145085
|
+
/** The hub side (the registry list, the migration-only door) and the broker records side. */
|
|
145086
|
+
function alexaIntegrationRowsPorts(deps) {
|
|
145087
|
+
const brokers = deps.records.kind(alexaBrokersEntryKind);
|
|
145088
|
+
const rows = integrationRowsPortsOverApi({
|
|
145089
|
+
api: deps.api,
|
|
145090
|
+
secrets: deps.secrets,
|
|
145091
|
+
records: deps.records.kind({
|
|
145092
|
+
name: alexaBrokersEntryKind.name,
|
|
145093
|
+
declaration: alexaBrokersEntryKind.declaration,
|
|
145094
|
+
schema: record(string(), unknown())
|
|
145095
|
+
}),
|
|
145096
|
+
log: { warn: deps.warn }
|
|
145097
|
+
});
|
|
145098
|
+
return {
|
|
145099
|
+
warn: deps.warn,
|
|
145100
|
+
listIntegrations: rows.listIntegrations,
|
|
145101
|
+
readIntegrationRows: rows.readRows,
|
|
145102
|
+
clearIntegrationRows: rows.clearRows,
|
|
145103
|
+
readBrokerLink: async (id) => {
|
|
145104
|
+
const row = await brokers.get(id);
|
|
145105
|
+
return row === null ? null : { integrationId: row.data.integrationId };
|
|
145106
|
+
},
|
|
145107
|
+
stampBrokerLink: async (id, integrationId) => {
|
|
145108
|
+
await brokers.patch(id, { integrationId });
|
|
145109
|
+
}
|
|
145110
|
+
};
|
|
145111
|
+
}
|
|
145112
|
+
//#endregion
|
|
144967
145113
|
//#region src/migrations/flatten-broker-records.ts
|
|
144968
145114
|
/**
|
|
144969
145115
|
* The Alexa broker records keep their account flat (planning#408 P3): the nested `connection`
|
|
@@ -145026,37 +145172,56 @@ async function stripEchoTargetVolumeAndMode(ports) {
|
|
|
145026
145172
|
//#endregion
|
|
145027
145173
|
//#region src/migrations/index.ts
|
|
145028
145174
|
/** The `import-alexa` catalogue (D987): offered to the engine when the addon initialises. */
|
|
145029
|
-
var ADDON_MIGRATIONS = [
|
|
145030
|
-
|
|
145031
|
-
|
|
145032
|
-
|
|
145033
|
-
|
|
145034
|
-
|
|
145035
|
-
|
|
145036
|
-
|
|
145037
|
-
|
|
145038
|
-
|
|
145039
|
-
|
|
145040
|
-
|
|
145041
|
-
|
|
145042
|
-
|
|
145043
|
-
|
|
145044
|
-
|
|
145045
|
-
|
|
145046
|
-
|
|
145047
|
-
|
|
145048
|
-
|
|
145049
|
-
|
|
145050
|
-
|
|
145051
|
-
|
|
145052
|
-
|
|
145053
|
-
|
|
145054
|
-
|
|
145055
|
-
|
|
145056
|
-
|
|
145057
|
-
|
|
145058
|
-
|
|
145059
|
-
}
|
|
145175
|
+
var ADDON_MIGRATIONS = [
|
|
145176
|
+
{
|
|
145177
|
+
meta: {
|
|
145178
|
+
id: "import-alexa/2026-11-01-strip-echo-target-volume-and-mode",
|
|
145179
|
+
owner: "import-alexa",
|
|
145180
|
+
level: "addon-state",
|
|
145181
|
+
seq: 1,
|
|
145182
|
+
dependsOn: [],
|
|
145183
|
+
store: "settings",
|
|
145184
|
+
scope: "cluster",
|
|
145185
|
+
onFailure: "tolerate",
|
|
145186
|
+
bodyKind: "procedure",
|
|
145187
|
+
description: "Remove the retired volume and mode from every stored Echo target",
|
|
145188
|
+
fixtureMark: "the Echo id and the name the migration does not know"
|
|
145189
|
+
},
|
|
145190
|
+
procedure: stripEchoTargetVolumeAndMode
|
|
145191
|
+
},
|
|
145192
|
+
{
|
|
145193
|
+
meta: {
|
|
145194
|
+
id: FLATTEN_BROKER_RECORDS_ID,
|
|
145195
|
+
owner: "import-alexa",
|
|
145196
|
+
level: "addon-state",
|
|
145197
|
+
seq: 2,
|
|
145198
|
+
dependsOn: [],
|
|
145199
|
+
store: "settings",
|
|
145200
|
+
scope: "cluster",
|
|
145201
|
+
onFailure: "block-owner",
|
|
145202
|
+
bodyKind: "procedure",
|
|
145203
|
+
description: "Move each Alexa account from a nested connection object to flat top-level fields under the same id, reading every secret back first",
|
|
145204
|
+
fixtureMark: "the integration link and the amazon domain of each account"
|
|
145205
|
+
},
|
|
145206
|
+
procedure: flattenBrokerRecords
|
|
145207
|
+
},
|
|
145208
|
+
{
|
|
145209
|
+
meta: {
|
|
145210
|
+
id: INTEGRATION_ROWS_TO_RECORD_ID,
|
|
145211
|
+
owner: "import-alexa",
|
|
145212
|
+
level: "addon-state",
|
|
145213
|
+
seq: 3,
|
|
145214
|
+
dependsOn: [FLATTEN_BROKER_RECORDS_ID],
|
|
145215
|
+
store: "settings",
|
|
145216
|
+
scope: "cluster",
|
|
145217
|
+
onFailure: "block-owner",
|
|
145218
|
+
bodyKind: "procedure",
|
|
145219
|
+
description: "Stamp the integration link from the legacy brokerId row onto its broker record when the broker has none, then clear the legacy integration settings rows",
|
|
145220
|
+
fixtureMark: "the name of the broker and the integration it already names"
|
|
145221
|
+
},
|
|
145222
|
+
procedure: moveAlexaIntegrationRows
|
|
145223
|
+
}
|
|
145224
|
+
];
|
|
145060
145225
|
//#endregion
|
|
145061
145226
|
//#region src/speak/icon-routes.ts
|
|
145062
145227
|
var ALEXA_ICON_SVG = "<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"#00CAFF\"><title>Amazon Alexa</title><path d=\"M12 0C5.37 0 0 5.37 0 12C0 18.09 4.53 23.11 10.4 23.9V21.5A1.59 1.59 0 0 0 9.32 19.97A8.41 8.41 0 0 1 3.6 11.8A8.37 8.37 0 0 1 12.09 3.6A8.4 8.4 0 0 1 20.4 12.31L20.39 12.38A8.68 8.68 0 0 1 20.36 12.76C20.36 12.83 20.35 12.9 20.34 12.96C20.34 13.04 20.33 13.12 20.32 13.19L20.3 13.29C19.27 20.07 10.45 23.87 10.4 23.9C10.92 23.97 11.46 24 12 24C18.63 24 24 18.63 24 12S18.63 0 12 0Z\"/></svg>";
|
|
@@ -145268,13 +145433,20 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
145268
145433
|
] };
|
|
145269
145434
|
}
|
|
145270
145435
|
migrationPorts(records) {
|
|
145271
|
-
|
|
145436
|
+
const secrets = this.ctx.secrets;
|
|
145437
|
+
if (secrets === void 0) throw new Error("import-alexa: ctx.secrets is required");
|
|
145272
145438
|
return {
|
|
145273
145439
|
readRows: () => Array.isArray(this.config.echoTargets) ? this.config.echoTargets : [],
|
|
145274
145440
|
writeRows: async (rows) => {
|
|
145275
145441
|
await this.updateGlobalSettings({ echoTargets: [...readEchoTargets({ echoTargets: rows })] });
|
|
145276
145442
|
},
|
|
145277
|
-
...flattenPortsOver(records.kind(alexaBrokersEntryKind))
|
|
145443
|
+
...flattenPortsOver(records.kind(alexaBrokersEntryKind)),
|
|
145444
|
+
...alexaIntegrationRowsPorts({
|
|
145445
|
+
api: defaultIntegrationApiOver(this.ctx.api),
|
|
145446
|
+
records,
|
|
145447
|
+
secrets,
|
|
145448
|
+
warn: (message) => this.ctx.logger.warn(message)
|
|
145449
|
+
})
|
|
145278
145450
|
};
|
|
145279
145451
|
}
|
|
145280
145452
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-import-alexa",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.187",
|
|
4
4
|
"description": "Alexa device-import provider for CamStack — imports the smart-home devices in a user's Alexa account via the unofficial alexa-remote2 cookie/token client (the inverse of the Alexa exporter)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|