@camstack/addon-import-alexa 0.2.183 → 0.2.185
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 +13 -0
- package/dist/addon.js +140 -23
- package/dist/addon.mjs +140 -23
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.185] - 2026-11-04
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Alexa starts without waiting for the first appliance list and state poll, so a large account no longer exceeds the runner init budget during an update
|
|
15
|
+
- Alexa broker migrations no longer read as 'skipped-newer' (and no false 'Database is newer' alert) after the addon boots
|
|
16
|
+
|
|
17
|
+
## [0.2.184] - 2026-11-04
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- The integration states its role (devices, services or both) to the Integrations page
|
|
22
|
+
|
|
10
23
|
## [0.2.183] - 2026-11-04
|
|
11
24
|
|
|
12
25
|
### Changed
|
package/dist/addon.js
CHANGED
|
@@ -6044,7 +6044,7 @@ function number(params) {
|
|
|
6044
6044
|
return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
|
|
6045
6045
|
}
|
|
6046
6046
|
//#endregion
|
|
6047
|
-
//#region ../types/dist/sleep-
|
|
6047
|
+
//#region ../types/dist/sleep-Cv4Zmie9.mjs
|
|
6048
6048
|
/**
|
|
6049
6049
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
6050
6050
|
* window to float samples (D455).
|
|
@@ -6644,7 +6644,7 @@ var NOT_RUN = {
|
|
|
6644
6644
|
* throws: an unreachable or refusing engine is logged and reported `declared: false`,
|
|
6645
6645
|
* because the hub (and this addon's boot) must not hang on a migration.
|
|
6646
6646
|
*/
|
|
6647
|
-
async function runAddonMigrations(host, migrations, logger) {
|
|
6647
|
+
async function runAddonMigrations(host, migrations, logger, options = {}) {
|
|
6648
6648
|
const first = migrations[0];
|
|
6649
6649
|
if (first === void 0) return NOT_RUN;
|
|
6650
6650
|
const owner = first.meta.owner;
|
|
@@ -6673,7 +6673,9 @@ async function runAddonMigrations(host, migrations, logger) {
|
|
|
6673
6673
|
else if (status === "blocked") blocked.push(id);
|
|
6674
6674
|
else skipped.push(id);
|
|
6675
6675
|
};
|
|
6676
|
+
const selected = options.only === void 0 ? null : new Set(options.only);
|
|
6676
6677
|
for (const step of declaration.steps) {
|
|
6678
|
+
if (selected !== null && !selected.has(step.id)) continue;
|
|
6677
6679
|
const bound = byId.get(step.id);
|
|
6678
6680
|
if (bound === void 0 || step.action === "skip") {
|
|
6679
6681
|
skipped.push(step.id);
|
|
@@ -7833,14 +7835,18 @@ var BaseAddon = class {
|
|
|
7833
7835
|
* nothing; the boot continues. A migration that FAILED (or was blocked by one)
|
|
7834
7836
|
* and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
|
|
7835
7837
|
* serve data whose shape it cannot trust; `tolerate` migrations never do.
|
|
7838
|
+
*
|
|
7839
|
+
* ALWAYS pass the whole catalogue; a phase that must run only some of it passes
|
|
7840
|
+
* `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
|
|
7841
|
+
* omitted applied ids "newer").
|
|
7836
7842
|
*/
|
|
7837
|
-
async runMigrations(migrations) {
|
|
7843
|
+
async runMigrations(migrations, options) {
|
|
7838
7844
|
const api = this.ctx.api.migrations;
|
|
7839
7845
|
const report = await runAddonMigrations({
|
|
7840
7846
|
declareOwner: (input) => api.declareOwner.mutate(input),
|
|
7841
7847
|
startMigration: (input) => api.startMigration.mutate(input),
|
|
7842
7848
|
finishMigration: (input) => api.finishMigration.mutate(input)
|
|
7843
|
-
}, migrations, this.ctx.logger);
|
|
7849
|
+
}, migrations, this.ctx.logger, options);
|
|
7844
7850
|
if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
|
|
7845
7851
|
return report;
|
|
7846
7852
|
}
|
|
@@ -8880,6 +8886,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
|
8880
8886
|
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
8881
8887
|
* (`auth.accessToken`, `headers[].value`).
|
|
8882
8888
|
*
|
|
8889
|
+
* `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
|
|
8890
|
+
* names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
|
|
8891
|
+
* key releases exactly that key's secret.
|
|
8892
|
+
*
|
|
8883
8893
|
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
8884
8894
|
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
8885
8895
|
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
@@ -8892,11 +8902,13 @@ function parseSecretPath(path) {
|
|
|
8892
8902
|
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
8893
8903
|
return path.split(".").map((raw) => {
|
|
8894
8904
|
const each = raw.endsWith("[]");
|
|
8895
|
-
const
|
|
8905
|
+
const map = raw.endsWith("{}");
|
|
8906
|
+
const key = each || map ? raw.slice(0, -2) : raw;
|
|
8896
8907
|
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
8897
8908
|
return {
|
|
8898
8909
|
key,
|
|
8899
|
-
each
|
|
8910
|
+
each,
|
|
8911
|
+
map
|
|
8900
8912
|
};
|
|
8901
8913
|
});
|
|
8902
8914
|
}
|
|
@@ -8948,6 +8960,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
|
|
|
8948
8960
|
[segment.key]: mapped
|
|
8949
8961
|
};
|
|
8950
8962
|
}
|
|
8963
|
+
if (segment.map) {
|
|
8964
|
+
if (!isRecord$4$1(child)) return node;
|
|
8965
|
+
const kept = Object.entries(child).map(([mapKey, value]) => {
|
|
8966
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
|
|
8967
|
+
if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
|
|
8968
|
+
return [mapKey, visit({
|
|
8969
|
+
declared,
|
|
8970
|
+
concrete,
|
|
8971
|
+
value
|
|
8972
|
+
})];
|
|
8973
|
+
}).filter(([, value]) => value !== REMOVE_LEAF);
|
|
8974
|
+
return {
|
|
8975
|
+
...node,
|
|
8976
|
+
[segment.key]: Object.fromEntries(kept)
|
|
8977
|
+
};
|
|
8978
|
+
}
|
|
8951
8979
|
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
8952
8980
|
if (last) {
|
|
8953
8981
|
const replacement = visit({
|
|
@@ -9089,7 +9117,8 @@ function isZodSecret(schema) {
|
|
|
9089
9117
|
}
|
|
9090
9118
|
/**
|
|
9091
9119
|
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
9092
|
-
* element of this array" (`connection.password`, `rows[].token`)
|
|
9120
|
+
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
9121
|
+
* map" (`applianceSecrets{}`). A declared
|
|
9093
9122
|
* object or array is reported as one path and not descended into — everything
|
|
9094
9123
|
* under it is secret.
|
|
9095
9124
|
*/
|
|
@@ -9100,6 +9129,7 @@ function collectZodSecretPaths(schema, prefix = "") {
|
|
|
9100
9129
|
const def = defOf(layer);
|
|
9101
9130
|
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
9102
9131
|
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
9132
|
+
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
9103
9133
|
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
9104
9134
|
else if (def.type === "intersection") {
|
|
9105
9135
|
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
@@ -13414,11 +13444,49 @@ function buildCapturingReply(envelope) {
|
|
|
13414
13444
|
};
|
|
13415
13445
|
return wrapper;
|
|
13416
13446
|
}
|
|
13447
|
+
/**
|
|
13448
|
+
* `RecordIntegrationInstances` - the `integration-instances` provider over ONE
|
|
13449
|
+
* entry kind, so an addon that holds a connection serves it in ~30 lines.
|
|
13450
|
+
*
|
|
13451
|
+
* Every instance is one record of the addon's own kind, keyed by the
|
|
13452
|
+
* `integrations` registry id. The records door does the secret work: a field the
|
|
13453
|
+
* kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
|
|
13454
|
+
* deleting the row deletes the secret, and one row's value is never another's.
|
|
13455
|
+
*
|
|
13456
|
+
* Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
|
|
13457
|
+
* token or the view the form was given KEEP the stored secret;
|
|
13458
|
+
* `{ $secretClear: true }` clears it; any other string replaces it.
|
|
13459
|
+
*/
|
|
13417
13460
|
/** The most instances one addon lists (an integrations page, not a table). */
|
|
13418
13461
|
var INSTANCES_LIST_MAX = 1e3;
|
|
13419
13462
|
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
13420
13463
|
function topLevelSecretKeys(paths) {
|
|
13421
|
-
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
|
|
13464
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
|
|
13465
|
+
}
|
|
13466
|
+
/** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
|
|
13467
|
+
function secretMapKeys(paths) {
|
|
13468
|
+
return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
|
|
13469
|
+
}
|
|
13470
|
+
/**
|
|
13471
|
+
* A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
|
|
13472
|
+
* view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
|
|
13473
|
+
* or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
|
|
13474
|
+
*/
|
|
13475
|
+
function secretMapPatch(value) {
|
|
13476
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
13477
|
+
const out = {};
|
|
13478
|
+
for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
|
|
13479
|
+
else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
|
|
13480
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
13481
|
+
}
|
|
13482
|
+
/** Does the kind's body schema carry an `enabled` field? */
|
|
13483
|
+
function declaresEnabled(kind) {
|
|
13484
|
+
const json = toJSONSchema(kind.schema, {
|
|
13485
|
+
unrepresentable: "any",
|
|
13486
|
+
io: "input"
|
|
13487
|
+
});
|
|
13488
|
+
const properties = Reflect.get(json, "properties");
|
|
13489
|
+
return typeof properties === "object" && properties !== null && "enabled" in properties;
|
|
13422
13490
|
}
|
|
13423
13491
|
var RecordIntegrationInstances = class {
|
|
13424
13492
|
records;
|
|
@@ -13426,14 +13494,20 @@ var RecordIntegrationInstances = class {
|
|
|
13426
13494
|
hooks;
|
|
13427
13495
|
handle;
|
|
13428
13496
|
secretKeys;
|
|
13497
|
+
secretMaps;
|
|
13429
13498
|
nameField;
|
|
13499
|
+
/** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
|
|
13500
|
+
registryOwnsEnabled;
|
|
13430
13501
|
constructor(records, kind, hooks) {
|
|
13431
13502
|
this.records = records;
|
|
13432
13503
|
this.kind = kind;
|
|
13433
13504
|
this.hooks = hooks;
|
|
13434
13505
|
this.handle = records.kind(kind);
|
|
13435
13506
|
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
13507
|
+
this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
|
|
13436
13508
|
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
13509
|
+
this.registryOwnsEnabled = hooks.description.role === "devices";
|
|
13510
|
+
if (this.registryOwnsEnabled && declaresEnabled(kind)) throw new Error(`kind "${kind.name}": the integrations registry owns the on/off switch of a role "devices" addon, so the body must not declare "enabled"`);
|
|
13437
13511
|
}
|
|
13438
13512
|
/**
|
|
13439
13513
|
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
@@ -13484,7 +13558,7 @@ var RecordIntegrationInstances = class {
|
|
|
13484
13558
|
id: record.id,
|
|
13485
13559
|
name: typeof rawName === "string" ? rawName : record.id,
|
|
13486
13560
|
configured: this.hooks.configured(record.data),
|
|
13487
|
-
enabled: record.data.enabled !== false,
|
|
13561
|
+
...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
|
|
13488
13562
|
formValues: shown,
|
|
13489
13563
|
...kind !== void 0 ? { kind } : {},
|
|
13490
13564
|
...health?.status !== void 0 ? { status: health.status } : {},
|
|
@@ -13514,32 +13588,36 @@ var RecordIntegrationInstances = class {
|
|
|
13514
13588
|
id: minted,
|
|
13515
13589
|
data: this.hooks.initialBody(name, kind)
|
|
13516
13590
|
}, { ifRev: 0 });
|
|
13517
|
-
await this.hooks.onChanged?.(minted);
|
|
13591
|
+
await this.hooks.onChanged?.(minted, "created");
|
|
13518
13592
|
return { id: minted };
|
|
13519
13593
|
};
|
|
13520
13594
|
updateInstance = async ({ id, name, values }) => {
|
|
13521
13595
|
const patch = {};
|
|
13522
13596
|
for (const [key, value] of Object.entries(values)) {
|
|
13523
13597
|
if (value === void 0) continue;
|
|
13524
|
-
if (
|
|
13598
|
+
if (this.secretMaps.has(key)) {
|
|
13599
|
+
const entries = secretMapPatch(value);
|
|
13600
|
+
if (entries !== null) patch[key] = entries;
|
|
13601
|
+
} else if (!this.secretKeys.has(key)) patch[key] = value;
|
|
13525
13602
|
else if (isSecretClear(value)) patch[key] = null;
|
|
13526
13603
|
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13527
13604
|
}
|
|
13528
13605
|
if (name !== void 0) patch[this.nameField] = name;
|
|
13529
13606
|
await this.handle.patch(id, patch);
|
|
13530
|
-
await this.hooks.onChanged?.(id);
|
|
13607
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
13531
13608
|
return { ok: true };
|
|
13532
13609
|
};
|
|
13533
13610
|
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13534
13611
|
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13612
|
+
if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
|
|
13535
13613
|
await this.handle.patch(id, { enabled });
|
|
13536
|
-
await this.hooks.onChanged?.(id);
|
|
13614
|
+
await this.hooks.onChanged?.(id, "enabled");
|
|
13537
13615
|
return { ok: true };
|
|
13538
13616
|
};
|
|
13539
13617
|
/** The row and its keychain refs go together (the records door). */
|
|
13540
13618
|
deleteInstance = async ({ id }) => {
|
|
13541
13619
|
await this.handle.delete(id);
|
|
13542
|
-
await this.hooks.onChanged?.(id);
|
|
13620
|
+
await this.hooks.onChanged?.(id, "deleted");
|
|
13543
13621
|
return { ok: true };
|
|
13544
13622
|
};
|
|
13545
13623
|
testInstance = async (input) => {
|
|
@@ -14600,7 +14678,13 @@ var MigrationLedgerEntrySchema = object({
|
|
|
14600
14678
|
* joined by the engine from the catalogues it knows this boot. Absent when no
|
|
14601
14679
|
* catalogue lists the id (a newer database).
|
|
14602
14680
|
*/
|
|
14603
|
-
meta: MigrationMetaSchema.optional()
|
|
14681
|
+
meta: MigrationMetaSchema.optional(),
|
|
14682
|
+
/**
|
|
14683
|
+
* Set when this is an applied row of an addon owner whose catalogue the answering
|
|
14684
|
+
* process has not loaded (the owner has not declared yet): the row is applied, the
|
|
14685
|
+
* process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
|
|
14686
|
+
*/
|
|
14687
|
+
ownerNotLoaded: boolean().optional()
|
|
14604
14688
|
});
|
|
14605
14689
|
/** A catalogued migration that is not applied, with where it stands. */
|
|
14606
14690
|
var MigrationPendingEntrySchema = object({
|
|
@@ -56307,6 +56391,21 @@ var IntegrationInstanceKindSchema = object({
|
|
|
56307
56391
|
disconnectPath: string().min(1).optional()
|
|
56308
56392
|
});
|
|
56309
56393
|
var IntegrationInstanceDescriptionSchema = object({
|
|
56394
|
+
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
56395
|
+
role: _enum([
|
|
56396
|
+
"devices",
|
|
56397
|
+
"services",
|
|
56398
|
+
"both"
|
|
56399
|
+
]),
|
|
56400
|
+
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
56401
|
+
discovery: object({
|
|
56402
|
+
mode: _enum([
|
|
56403
|
+
"none",
|
|
56404
|
+
"lan",
|
|
56405
|
+
"account"
|
|
56406
|
+
]),
|
|
56407
|
+
scopedToInstance: boolean()
|
|
56408
|
+
}).optional(),
|
|
56310
56409
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
56311
56410
|
instanceMode: IntegrationInstanceModeSchema,
|
|
56312
56411
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
@@ -56346,10 +56445,11 @@ var IntegrationInstanceSchema = object({
|
|
|
56346
56445
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
56347
56446
|
configured: boolean(),
|
|
56348
56447
|
/**
|
|
56349
|
-
* The instance's own on/off switch (D996)
|
|
56350
|
-
*
|
|
56448
|
+
* The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
|
|
56449
|
+
* so a row written before the field existed is on). Absent when the `integrations` registry
|
|
56450
|
+
* owns the switch (`describe().role` is `devices`): read it from the registry row.
|
|
56351
56451
|
*/
|
|
56352
|
-
enabled: boolean(),
|
|
56452
|
+
enabled: boolean().optional(),
|
|
56353
56453
|
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
56354
56454
|
kind: string().optional(),
|
|
56355
56455
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
@@ -141550,10 +141650,8 @@ var AlexaIntegrationManager = class {
|
|
|
141550
141650
|
this.#status = "connected";
|
|
141551
141651
|
this.#lastCheckedAt = Date.now();
|
|
141552
141652
|
this.#onConnected(this.#id);
|
|
141553
|
-
await this.refreshAppliances();
|
|
141554
|
-
await this.poll();
|
|
141555
|
-
this.#schedulePoll();
|
|
141556
141653
|
this.#logger.info("AlexaIntegrationManager: started", { tags: { brokerId: this.#id } });
|
|
141654
|
+
this.#firstSync(client);
|
|
141557
141655
|
} catch (err) {
|
|
141558
141656
|
if (err instanceof AlexaAuthError) {
|
|
141559
141657
|
this.#status = "auth-failed";
|
|
@@ -141574,6 +141672,24 @@ var AlexaIntegrationManager = class {
|
|
|
141574
141672
|
});
|
|
141575
141673
|
}
|
|
141576
141674
|
}
|
|
141675
|
+
/** Background first refresh + poll, then the poll loop. Does nothing once `client` is stale. */
|
|
141676
|
+
async #firstSync(client) {
|
|
141677
|
+
let refreshed = false;
|
|
141678
|
+
try {
|
|
141679
|
+
await this.refreshAppliances();
|
|
141680
|
+
refreshed = true;
|
|
141681
|
+
} catch (err) {
|
|
141682
|
+
this.#error = errMsg(err);
|
|
141683
|
+
this.#logger.warn("AlexaIntegrationManager: first appliance refresh failed", {
|
|
141684
|
+
tags: { brokerId: this.#id },
|
|
141685
|
+
meta: { error: errMsg(err) }
|
|
141686
|
+
});
|
|
141687
|
+
}
|
|
141688
|
+
if (this.#client !== client) return;
|
|
141689
|
+
if (refreshed) await this.poll();
|
|
141690
|
+
if (this.#client !== client) return;
|
|
141691
|
+
this.#schedulePoll();
|
|
141692
|
+
}
|
|
141577
141693
|
/** List the account's appliances + publish them to the resolver. */
|
|
141578
141694
|
async refreshAppliances() {
|
|
141579
141695
|
const client = this.#client;
|
|
@@ -143315,6 +143431,7 @@ function statusOf(info, configured) {
|
|
|
143315
143431
|
function buildAlexaInstances(records, deps) {
|
|
143316
143432
|
return new RecordIntegrationInstances(records, alexaBrokersEntryKind, {
|
|
143317
143433
|
description: {
|
|
143434
|
+
role: "both",
|
|
143318
143435
|
instanceMode: "multiple",
|
|
143319
143436
|
kind: "account",
|
|
143320
143437
|
canTest: true,
|
|
@@ -145027,7 +145144,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
145027
145144
|
infoOf: (id) => this.registry?.get(id) ?? null,
|
|
145028
145145
|
onChanged: (id) => this.syncBrokerRuntime(id)
|
|
145029
145146
|
});
|
|
145030
|
-
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS
|
|
145147
|
+
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, this.migrationPorts(records)), { only: [FLATTEN_BROKER_RECORDS_ID] });
|
|
145031
145148
|
this.activeBrokers = await this.loadPersistedBrokers();
|
|
145032
145149
|
this.registry = new AlexaBrokerRegistry(this.ctx.logger, {
|
|
145033
145150
|
dataDir: this.ctx.dataDir,
|
|
@@ -145072,7 +145189,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
145072
145189
|
deviceCode: this.deviceCodeLogin,
|
|
145073
145190
|
commitCookie: (input) => this.commitCapturedCookie(input)
|
|
145074
145191
|
}), ...speakIconRoutes()];
|
|
145075
|
-
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.
|
|
145192
|
+
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, this.migrationPorts(records)), { only: ADDON_MIGRATIONS.map((m) => m.meta.id).filter((id) => id !== FLATTEN_BROKER_RECORDS_ID) });
|
|
145076
145193
|
const speak = new AlexaSpeakController({
|
|
145077
145194
|
logger: this.ctx.logger.child("speak"),
|
|
145078
145195
|
readConfig: () => this.config,
|
package/dist/addon.mjs
CHANGED
|
@@ -6042,7 +6042,7 @@ function number(params) {
|
|
|
6042
6042
|
return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
|
|
6043
6043
|
}
|
|
6044
6044
|
//#endregion
|
|
6045
|
-
//#region ../types/dist/sleep-
|
|
6045
|
+
//#region ../types/dist/sleep-Cv4Zmie9.mjs
|
|
6046
6046
|
/**
|
|
6047
6047
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
6048
6048
|
* window to float samples (D455).
|
|
@@ -6642,7 +6642,7 @@ var NOT_RUN = {
|
|
|
6642
6642
|
* throws: an unreachable or refusing engine is logged and reported `declared: false`,
|
|
6643
6643
|
* because the hub (and this addon's boot) must not hang on a migration.
|
|
6644
6644
|
*/
|
|
6645
|
-
async function runAddonMigrations(host, migrations, logger) {
|
|
6645
|
+
async function runAddonMigrations(host, migrations, logger, options = {}) {
|
|
6646
6646
|
const first = migrations[0];
|
|
6647
6647
|
if (first === void 0) return NOT_RUN;
|
|
6648
6648
|
const owner = first.meta.owner;
|
|
@@ -6671,7 +6671,9 @@ async function runAddonMigrations(host, migrations, logger) {
|
|
|
6671
6671
|
else if (status === "blocked") blocked.push(id);
|
|
6672
6672
|
else skipped.push(id);
|
|
6673
6673
|
};
|
|
6674
|
+
const selected = options.only === void 0 ? null : new Set(options.only);
|
|
6674
6675
|
for (const step of declaration.steps) {
|
|
6676
|
+
if (selected !== null && !selected.has(step.id)) continue;
|
|
6675
6677
|
const bound = byId.get(step.id);
|
|
6676
6678
|
if (bound === void 0 || step.action === "skip") {
|
|
6677
6679
|
skipped.push(step.id);
|
|
@@ -7831,14 +7833,18 @@ var BaseAddon = class {
|
|
|
7831
7833
|
* nothing; the boot continues. A migration that FAILED (or was blocked by one)
|
|
7832
7834
|
* and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
|
|
7833
7835
|
* serve data whose shape it cannot trust; `tolerate` migrations never do.
|
|
7836
|
+
*
|
|
7837
|
+
* ALWAYS pass the whole catalogue; a phase that must run only some of it passes
|
|
7838
|
+
* `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
|
|
7839
|
+
* omitted applied ids "newer").
|
|
7834
7840
|
*/
|
|
7835
|
-
async runMigrations(migrations) {
|
|
7841
|
+
async runMigrations(migrations, options) {
|
|
7836
7842
|
const api = this.ctx.api.migrations;
|
|
7837
7843
|
const report = await runAddonMigrations({
|
|
7838
7844
|
declareOwner: (input) => api.declareOwner.mutate(input),
|
|
7839
7845
|
startMigration: (input) => api.startMigration.mutate(input),
|
|
7840
7846
|
finishMigration: (input) => api.finishMigration.mutate(input)
|
|
7841
|
-
}, migrations, this.ctx.logger);
|
|
7847
|
+
}, migrations, this.ctx.logger, options);
|
|
7842
7848
|
if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
|
|
7843
7849
|
return report;
|
|
7844
7850
|
}
|
|
@@ -8878,6 +8884,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
|
8878
8884
|
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
8879
8885
|
* (`auth.accessToken`, `headers[].value`).
|
|
8880
8886
|
*
|
|
8887
|
+
* `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
|
|
8888
|
+
* names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
|
|
8889
|
+
* key releases exactly that key's secret.
|
|
8890
|
+
*
|
|
8881
8891
|
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
8882
8892
|
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
8883
8893
|
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
@@ -8890,11 +8900,13 @@ function parseSecretPath(path) {
|
|
|
8890
8900
|
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
8891
8901
|
return path.split(".").map((raw) => {
|
|
8892
8902
|
const each = raw.endsWith("[]");
|
|
8893
|
-
const
|
|
8903
|
+
const map = raw.endsWith("{}");
|
|
8904
|
+
const key = each || map ? raw.slice(0, -2) : raw;
|
|
8894
8905
|
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
8895
8906
|
return {
|
|
8896
8907
|
key,
|
|
8897
|
-
each
|
|
8908
|
+
each,
|
|
8909
|
+
map
|
|
8898
8910
|
};
|
|
8899
8911
|
});
|
|
8900
8912
|
}
|
|
@@ -8946,6 +8958,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
|
|
|
8946
8958
|
[segment.key]: mapped
|
|
8947
8959
|
};
|
|
8948
8960
|
}
|
|
8961
|
+
if (segment.map) {
|
|
8962
|
+
if (!isRecord$4$1(child)) return node;
|
|
8963
|
+
const kept = Object.entries(child).map(([mapKey, value]) => {
|
|
8964
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
|
|
8965
|
+
if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
|
|
8966
|
+
return [mapKey, visit({
|
|
8967
|
+
declared,
|
|
8968
|
+
concrete,
|
|
8969
|
+
value
|
|
8970
|
+
})];
|
|
8971
|
+
}).filter(([, value]) => value !== REMOVE_LEAF);
|
|
8972
|
+
return {
|
|
8973
|
+
...node,
|
|
8974
|
+
[segment.key]: Object.fromEntries(kept)
|
|
8975
|
+
};
|
|
8976
|
+
}
|
|
8949
8977
|
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
8950
8978
|
if (last) {
|
|
8951
8979
|
const replacement = visit({
|
|
@@ -9087,7 +9115,8 @@ function isZodSecret(schema) {
|
|
|
9087
9115
|
}
|
|
9088
9116
|
/**
|
|
9089
9117
|
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
9090
|
-
* element of this array" (`connection.password`, `rows[].token`)
|
|
9118
|
+
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
9119
|
+
* map" (`applianceSecrets{}`). A declared
|
|
9091
9120
|
* object or array is reported as one path and not descended into — everything
|
|
9092
9121
|
* under it is secret.
|
|
9093
9122
|
*/
|
|
@@ -9098,6 +9127,7 @@ function collectZodSecretPaths(schema, prefix = "") {
|
|
|
9098
9127
|
const def = defOf(layer);
|
|
9099
9128
|
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
9100
9129
|
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
9130
|
+
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
9101
9131
|
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
9102
9132
|
else if (def.type === "intersection") {
|
|
9103
9133
|
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
@@ -13412,11 +13442,49 @@ function buildCapturingReply(envelope) {
|
|
|
13412
13442
|
};
|
|
13413
13443
|
return wrapper;
|
|
13414
13444
|
}
|
|
13445
|
+
/**
|
|
13446
|
+
* `RecordIntegrationInstances` - the `integration-instances` provider over ONE
|
|
13447
|
+
* entry kind, so an addon that holds a connection serves it in ~30 lines.
|
|
13448
|
+
*
|
|
13449
|
+
* Every instance is one record of the addon's own kind, keyed by the
|
|
13450
|
+
* `integrations` registry id. The records door does the secret work: a field the
|
|
13451
|
+
* kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
|
|
13452
|
+
* deleting the row deletes the secret, and one row's value is never another's.
|
|
13453
|
+
*
|
|
13454
|
+
* Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
|
|
13455
|
+
* token or the view the form was given KEEP the stored secret;
|
|
13456
|
+
* `{ $secretClear: true }` clears it; any other string replaces it.
|
|
13457
|
+
*/
|
|
13415
13458
|
/** The most instances one addon lists (an integrations page, not a table). */
|
|
13416
13459
|
var INSTANCES_LIST_MAX = 1e3;
|
|
13417
13460
|
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
13418
13461
|
function topLevelSecretKeys(paths) {
|
|
13419
|
-
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
|
|
13462
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
|
|
13463
|
+
}
|
|
13464
|
+
/** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
|
|
13465
|
+
function secretMapKeys(paths) {
|
|
13466
|
+
return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
|
|
13467
|
+
}
|
|
13468
|
+
/**
|
|
13469
|
+
* A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
|
|
13470
|
+
* view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
|
|
13471
|
+
* or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
|
|
13472
|
+
*/
|
|
13473
|
+
function secretMapPatch(value) {
|
|
13474
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
13475
|
+
const out = {};
|
|
13476
|
+
for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
|
|
13477
|
+
else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
|
|
13478
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
13479
|
+
}
|
|
13480
|
+
/** Does the kind's body schema carry an `enabled` field? */
|
|
13481
|
+
function declaresEnabled(kind) {
|
|
13482
|
+
const json = toJSONSchema(kind.schema, {
|
|
13483
|
+
unrepresentable: "any",
|
|
13484
|
+
io: "input"
|
|
13485
|
+
});
|
|
13486
|
+
const properties = Reflect.get(json, "properties");
|
|
13487
|
+
return typeof properties === "object" && properties !== null && "enabled" in properties;
|
|
13420
13488
|
}
|
|
13421
13489
|
var RecordIntegrationInstances = class {
|
|
13422
13490
|
records;
|
|
@@ -13424,14 +13492,20 @@ var RecordIntegrationInstances = class {
|
|
|
13424
13492
|
hooks;
|
|
13425
13493
|
handle;
|
|
13426
13494
|
secretKeys;
|
|
13495
|
+
secretMaps;
|
|
13427
13496
|
nameField;
|
|
13497
|
+
/** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
|
|
13498
|
+
registryOwnsEnabled;
|
|
13428
13499
|
constructor(records, kind, hooks) {
|
|
13429
13500
|
this.records = records;
|
|
13430
13501
|
this.kind = kind;
|
|
13431
13502
|
this.hooks = hooks;
|
|
13432
13503
|
this.handle = records.kind(kind);
|
|
13433
13504
|
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
13505
|
+
this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
|
|
13434
13506
|
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
13507
|
+
this.registryOwnsEnabled = hooks.description.role === "devices";
|
|
13508
|
+
if (this.registryOwnsEnabled && declaresEnabled(kind)) throw new Error(`kind "${kind.name}": the integrations registry owns the on/off switch of a role "devices" addon, so the body must not declare "enabled"`);
|
|
13435
13509
|
}
|
|
13436
13510
|
/**
|
|
13437
13511
|
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
@@ -13482,7 +13556,7 @@ var RecordIntegrationInstances = class {
|
|
|
13482
13556
|
id: record.id,
|
|
13483
13557
|
name: typeof rawName === "string" ? rawName : record.id,
|
|
13484
13558
|
configured: this.hooks.configured(record.data),
|
|
13485
|
-
enabled: record.data.enabled !== false,
|
|
13559
|
+
...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
|
|
13486
13560
|
formValues: shown,
|
|
13487
13561
|
...kind !== void 0 ? { kind } : {},
|
|
13488
13562
|
...health?.status !== void 0 ? { status: health.status } : {},
|
|
@@ -13512,32 +13586,36 @@ var RecordIntegrationInstances = class {
|
|
|
13512
13586
|
id: minted,
|
|
13513
13587
|
data: this.hooks.initialBody(name, kind)
|
|
13514
13588
|
}, { ifRev: 0 });
|
|
13515
|
-
await this.hooks.onChanged?.(minted);
|
|
13589
|
+
await this.hooks.onChanged?.(minted, "created");
|
|
13516
13590
|
return { id: minted };
|
|
13517
13591
|
};
|
|
13518
13592
|
updateInstance = async ({ id, name, values }) => {
|
|
13519
13593
|
const patch = {};
|
|
13520
13594
|
for (const [key, value] of Object.entries(values)) {
|
|
13521
13595
|
if (value === void 0) continue;
|
|
13522
|
-
if (
|
|
13596
|
+
if (this.secretMaps.has(key)) {
|
|
13597
|
+
const entries = secretMapPatch(value);
|
|
13598
|
+
if (entries !== null) patch[key] = entries;
|
|
13599
|
+
} else if (!this.secretKeys.has(key)) patch[key] = value;
|
|
13523
13600
|
else if (isSecretClear(value)) patch[key] = null;
|
|
13524
13601
|
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13525
13602
|
}
|
|
13526
13603
|
if (name !== void 0) patch[this.nameField] = name;
|
|
13527
13604
|
await this.handle.patch(id, patch);
|
|
13528
|
-
await this.hooks.onChanged?.(id);
|
|
13605
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
13529
13606
|
return { ok: true };
|
|
13530
13607
|
};
|
|
13531
13608
|
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13532
13609
|
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13610
|
+
if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
|
|
13533
13611
|
await this.handle.patch(id, { enabled });
|
|
13534
|
-
await this.hooks.onChanged?.(id);
|
|
13612
|
+
await this.hooks.onChanged?.(id, "enabled");
|
|
13535
13613
|
return { ok: true };
|
|
13536
13614
|
};
|
|
13537
13615
|
/** The row and its keychain refs go together (the records door). */
|
|
13538
13616
|
deleteInstance = async ({ id }) => {
|
|
13539
13617
|
await this.handle.delete(id);
|
|
13540
|
-
await this.hooks.onChanged?.(id);
|
|
13618
|
+
await this.hooks.onChanged?.(id, "deleted");
|
|
13541
13619
|
return { ok: true };
|
|
13542
13620
|
};
|
|
13543
13621
|
testInstance = async (input) => {
|
|
@@ -14598,7 +14676,13 @@ var MigrationLedgerEntrySchema = object({
|
|
|
14598
14676
|
* joined by the engine from the catalogues it knows this boot. Absent when no
|
|
14599
14677
|
* catalogue lists the id (a newer database).
|
|
14600
14678
|
*/
|
|
14601
|
-
meta: MigrationMetaSchema.optional()
|
|
14679
|
+
meta: MigrationMetaSchema.optional(),
|
|
14680
|
+
/**
|
|
14681
|
+
* Set when this is an applied row of an addon owner whose catalogue the answering
|
|
14682
|
+
* process has not loaded (the owner has not declared yet): the row is applied, the
|
|
14683
|
+
* process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
|
|
14684
|
+
*/
|
|
14685
|
+
ownerNotLoaded: boolean().optional()
|
|
14602
14686
|
});
|
|
14603
14687
|
/** A catalogued migration that is not applied, with where it stands. */
|
|
14604
14688
|
var MigrationPendingEntrySchema = object({
|
|
@@ -56305,6 +56389,21 @@ var IntegrationInstanceKindSchema = object({
|
|
|
56305
56389
|
disconnectPath: string().min(1).optional()
|
|
56306
56390
|
});
|
|
56307
56391
|
var IntegrationInstanceDescriptionSchema = object({
|
|
56392
|
+
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
56393
|
+
role: _enum([
|
|
56394
|
+
"devices",
|
|
56395
|
+
"services",
|
|
56396
|
+
"both"
|
|
56397
|
+
]),
|
|
56398
|
+
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
56399
|
+
discovery: object({
|
|
56400
|
+
mode: _enum([
|
|
56401
|
+
"none",
|
|
56402
|
+
"lan",
|
|
56403
|
+
"account"
|
|
56404
|
+
]),
|
|
56405
|
+
scopedToInstance: boolean()
|
|
56406
|
+
}).optional(),
|
|
56308
56407
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
56309
56408
|
instanceMode: IntegrationInstanceModeSchema,
|
|
56310
56409
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
@@ -56344,10 +56443,11 @@ var IntegrationInstanceSchema = object({
|
|
|
56344
56443
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
56345
56444
|
configured: boolean(),
|
|
56346
56445
|
/**
|
|
56347
|
-
* The instance's own on/off switch (D996)
|
|
56348
|
-
*
|
|
56446
|
+
* The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
|
|
56447
|
+
* so a row written before the field existed is on). Absent when the `integrations` registry
|
|
56448
|
+
* owns the switch (`describe().role` is `devices`): read it from the registry row.
|
|
56349
56449
|
*/
|
|
56350
|
-
enabled: boolean(),
|
|
56450
|
+
enabled: boolean().optional(),
|
|
56351
56451
|
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
56352
56452
|
kind: string().optional(),
|
|
56353
56453
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
@@ -141548,10 +141648,8 @@ var AlexaIntegrationManager = class {
|
|
|
141548
141648
|
this.#status = "connected";
|
|
141549
141649
|
this.#lastCheckedAt = Date.now();
|
|
141550
141650
|
this.#onConnected(this.#id);
|
|
141551
|
-
await this.refreshAppliances();
|
|
141552
|
-
await this.poll();
|
|
141553
|
-
this.#schedulePoll();
|
|
141554
141651
|
this.#logger.info("AlexaIntegrationManager: started", { tags: { brokerId: this.#id } });
|
|
141652
|
+
this.#firstSync(client);
|
|
141555
141653
|
} catch (err) {
|
|
141556
141654
|
if (err instanceof AlexaAuthError) {
|
|
141557
141655
|
this.#status = "auth-failed";
|
|
@@ -141572,6 +141670,24 @@ var AlexaIntegrationManager = class {
|
|
|
141572
141670
|
});
|
|
141573
141671
|
}
|
|
141574
141672
|
}
|
|
141673
|
+
/** Background first refresh + poll, then the poll loop. Does nothing once `client` is stale. */
|
|
141674
|
+
async #firstSync(client) {
|
|
141675
|
+
let refreshed = false;
|
|
141676
|
+
try {
|
|
141677
|
+
await this.refreshAppliances();
|
|
141678
|
+
refreshed = true;
|
|
141679
|
+
} catch (err) {
|
|
141680
|
+
this.#error = errMsg(err);
|
|
141681
|
+
this.#logger.warn("AlexaIntegrationManager: first appliance refresh failed", {
|
|
141682
|
+
tags: { brokerId: this.#id },
|
|
141683
|
+
meta: { error: errMsg(err) }
|
|
141684
|
+
});
|
|
141685
|
+
}
|
|
141686
|
+
if (this.#client !== client) return;
|
|
141687
|
+
if (refreshed) await this.poll();
|
|
141688
|
+
if (this.#client !== client) return;
|
|
141689
|
+
this.#schedulePoll();
|
|
141690
|
+
}
|
|
141575
141691
|
/** List the account's appliances + publish them to the resolver. */
|
|
141576
141692
|
async refreshAppliances() {
|
|
141577
141693
|
const client = this.#client;
|
|
@@ -143313,6 +143429,7 @@ function statusOf(info, configured) {
|
|
|
143313
143429
|
function buildAlexaInstances(records, deps) {
|
|
143314
143430
|
return new RecordIntegrationInstances(records, alexaBrokersEntryKind, {
|
|
143315
143431
|
description: {
|
|
143432
|
+
role: "both",
|
|
143316
143433
|
instanceMode: "multiple",
|
|
143317
143434
|
kind: "account",
|
|
143318
143435
|
canTest: true,
|
|
@@ -145025,7 +145142,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
145025
145142
|
infoOf: (id) => this.registry?.get(id) ?? null,
|
|
145026
145143
|
onChanged: (id) => this.syncBrokerRuntime(id)
|
|
145027
145144
|
});
|
|
145028
|
-
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS
|
|
145145
|
+
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, this.migrationPorts(records)), { only: [FLATTEN_BROKER_RECORDS_ID] });
|
|
145029
145146
|
this.activeBrokers = await this.loadPersistedBrokers();
|
|
145030
145147
|
this.registry = new AlexaBrokerRegistry(this.ctx.logger, {
|
|
145031
145148
|
dataDir: this.ctx.dataDir,
|
|
@@ -145070,7 +145187,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
145070
145187
|
deviceCode: this.deviceCodeLogin,
|
|
145071
145188
|
commitCookie: (input) => this.commitCapturedCookie(input)
|
|
145072
145189
|
}), ...speakIconRoutes()];
|
|
145073
|
-
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.
|
|
145190
|
+
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, this.migrationPorts(records)), { only: ADDON_MIGRATIONS.map((m) => m.meta.id).filter((id) => id !== FLATTEN_BROKER_RECORDS_ID) });
|
|
145074
145191
|
const speak = new AlexaSpeakController({
|
|
145075
145192
|
logger: this.ctx.logger.child("speak"),
|
|
145076
145193
|
readConfig: () => this.config,
|
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.185",
|
|
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",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
}
|
|
73
73
|
],
|
|
74
74
|
"requires": {
|
|
75
|
-
"server": ">=1.2.
|
|
75
|
+
"server": ">=1.2.763"
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
78
|
"files": [
|