@camstack/addon-import-alexa 0.2.184 → 0.2.186

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 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.186] - 2026-11-05
11
+
12
+ ### Changed
13
+
14
+ - An Alexa integration links to its account through the account record
15
+
16
+ ## [0.2.185] - 2026-11-04
17
+
18
+ ### Changed
19
+
20
+ - 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
21
+ - Alexa broker migrations no longer read as 'skipped-newer' (and no false 'Database is newer' alert) after the addon boots
22
+
10
23
  ## [0.2.184] - 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-BaWhXZFm.mjs
6047
+ //#region ../types/dist/sleep-9TIHFj1w.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
  }
@@ -7982,10 +7988,9 @@ var BaseAddon = class {
7982
7988
  *
7983
7989
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
7984
7990
  * needs its connection fields, the default integration the addon's own sections. It is the
7985
- * SHAPE of the form only — the values live in `integrations.getSettings`, and the write path is
7986
- * unchanged (`integrations.setSettings` for an account integration).
7991
+ * SHAPE of the form only — the values live on the instance record.
7987
7992
  */
7988
- async integrationSettingsSchema(_instance) {
7993
+ async integrationFormSchema(_instance) {
7989
7994
  return null;
7990
7995
  }
7991
7996
  /**
@@ -8031,7 +8036,7 @@ var BaseAddon = class {
8031
8036
  * per-instance schema when it declares one, else its global schema (what they always showed).
8032
8037
  */
8033
8038
  async getIntegrationFormSchema(instance, nodeId) {
8034
- const scoped = await this.integrationSettingsSchema(instance);
8039
+ const scoped = await this.integrationFormSchema(instance);
8035
8040
  if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
8036
8041
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
8037
8042
  }
@@ -9305,6 +9310,7 @@ function defineEntryKind(name, spec) {
9305
9310
  class: "config",
9306
9311
  writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9307
9312
  retention: { none: "an entry goes only when the operator deletes it" },
9313
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9308
9314
  ...spec.database === void 0 ? {} : { database: spec.database },
9309
9315
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9310
9316
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
@@ -11974,7 +11980,7 @@ object({
11974
11980
  params: record(string(), union([string(), number$1()])).optional()
11975
11981
  });
11976
11982
  //#endregion
11977
- //#region ../types/dist/composition-CRNwcFYY.mjs
11983
+ //#region ../types/dist/composition-pOHbTzRB.mjs
11978
11984
  /**
11979
11985
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11980
11986
  *
@@ -14672,7 +14678,13 @@ var MigrationLedgerEntrySchema = object({
14672
14678
  * joined by the engine from the catalogues it knows this boot. Absent when no
14673
14679
  * catalogue lists the id (a newer database).
14674
14680
  */
14675
- 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()
14676
14688
  });
14677
14689
  /** A catalogued migration that is not applied, with where it stands. */
14678
14690
  var MigrationPendingEntrySchema = object({
@@ -50448,6 +50460,9 @@ var RunningProposalEvidenceSchema = object({
50448
50460
  runsFound: number$1().int().nonnegative(),
50449
50461
  pausesFound: number$1().int().nonnegative(),
50450
50462
  longestPauseMs: number$1().nonnegative(),
50463
+ /** Episodes shorter than ~90 s (an anti-crease pulse, a display waking): never a run, never a pause. */
50464
+ pulsesFound: number$1().int().nonnegative(),
50465
+ longestPulseMs: number$1().nonnegative(),
50451
50466
  idleFloorW: number$1(),
50452
50467
  noiseCeilingW: number$1(),
50453
50468
  plateauW: number$1(),
@@ -56293,11 +56308,7 @@ var CreateIntegrationInputSchema = object({
56293
56308
  info: record(string(), unknown()).optional(),
56294
56309
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
56295
56310
  originStableId: string().optional(),
56296
- settings: record(string(), unknown()).optional(),
56297
- /** Keys of `settings` the form's schema declares secret (by type) — kept in
56298
- * the keychain, not the table (D787). */
56299
- secretKeys: array(string()).optional(),
56300
- skipRestart: boolean().optional()
56311
+ settings: record(string(), unknown()).optional()
56301
56312
  });
56302
56313
  var UpdateIntegrationInputSchema = object({
56303
56314
  id: string(),
@@ -56318,16 +56329,6 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
56318
56329
  }), method(object({ id: string() }), DeleteIntegrationResultSchema, {
56319
56330
  kind: "mutation",
56320
56331
  auth: "admin"
56321
- }), method(object({ id: string() }), record(string(), unknown()), { auth: "admin" }), method(object({
56322
- id: string(),
56323
- settings: record(string(), unknown()),
56324
- /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
56325
- secretKeys: array(string()).optional(),
56326
- /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
56327
- skipRestart: boolean().optional()
56328
- }), object({ success: boolean() }), {
56329
- kind: "mutation",
56330
- auth: "admin"
56331
56332
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
56332
56333
  addonId: string(),
56333
56334
  settings: record(string(), unknown())
@@ -59304,6 +59305,27 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
59304
59305
  }), {
59305
59306
  kind: "mutation",
59306
59307
  auth: "admin"
59308
+ }), method(object({ integrationId: string().min(1) }), record(string(), unknown()), { auth: "admin" }), method(object({ integrationId: string().min(1) }), object({
59309
+ rows: number$1().int().nonnegative(),
59310
+ secrets: number$1().int().nonnegative()
59311
+ }), {
59312
+ kind: "mutation",
59313
+ auth: "admin"
59314
+ }), method(object({
59315
+ integrationId: string().min(1),
59316
+ patch: record(string(), unknown())
59317
+ }), object({ written: boolean() }), {
59318
+ kind: "mutation",
59319
+ auth: "admin"
59320
+ }), method(object({
59321
+ integrationId: string().min(1),
59322
+ keys: array(string().min(1))
59323
+ }), object({
59324
+ present: boolean(),
59325
+ removed: record(string(), unknown())
59326
+ }), {
59327
+ kind: "mutation",
59328
+ auth: "admin"
59307
59329
  });
59308
59330
  method(object({
59309
59331
  sourceUrl: string(),
@@ -80630,11 +80652,15 @@ discriminatedUnion("kind", [object({
80630
80652
  * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
80631
80653
  * near F. A device that is not near one level most of the time has no floor.
80632
80654
  * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
80633
- * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
80634
- * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
80655
+ * 3. an episode shorter than `PULSE_MAX_MS` is a PULSE (an anti-crease tumble, a
80656
+ * display waking): it is neither a run nor a pause. Among the remaining
80657
+ * episodes, gaps over `PAUSE_MAX_MS` always separate runs; the rest split in
80658
+ * two by the largest log-scale jump (>= 3x): the short ones are pauses INSIDE
80659
+ * a run (a soak), the long ones separate runs. A stretch under `MIN_RUN_MS`
80660
+ * is no run.
80635
80661
  * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
80636
- * grid a person would type; stop dwell covers the longest pause; start dwell
80637
- * covers the longest standby blip above T.
80662
+ * grid a person would type; stop dwell covers the longest pause and the
80663
+ * kind's floor; start dwell covers the longest pulse and standby blip above T.
80638
80664
  * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
80639
80665
  * run must hold exactly one true interval and nothing may flap outside.
80640
80666
  */
@@ -80645,6 +80671,10 @@ var MINUTE_MS$1 = 60 * SECOND_MS;
80645
80671
  30 * SECOND_MS;
80646
80672
  60 * SECOND_MS;
80647
80673
  20 * MINUTE_MS$1;
80674
+ 90 * SECOND_MS;
80675
+ 30 * MINUTE_MS$1;
80676
+ 10 * MINUTE_MS$1;
80677
+ DeviceType.Dryer, 3 * MINUTE_MS$1, DeviceType.Dishwasher, 3 * MINUTE_MS$1, DeviceType.Washer, 10 * MINUTE_MS$1;
80648
80678
  10 * SECOND_MS;
80649
80679
  10 * SECOND_MS;
80650
80680
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
@@ -85964,13 +85994,6 @@ Object.freeze({
85964
85994
  access: "view",
85965
85995
  role: "admin"
85966
85996
  },
85967
- "integrations.getSettings": {
85968
- capName: "integrations",
85969
- capScope: "system",
85970
- addonId: null,
85971
- access: "view",
85972
- role: "admin"
85973
- },
85974
85997
  "integrations.list": {
85975
85998
  capName: "integrations",
85976
85999
  capScope: "system",
@@ -85978,13 +86001,6 @@ Object.freeze({
85978
86001
  access: "view",
85979
86002
  role: "admin"
85980
86003
  },
85981
- "integrations.setSettings": {
85982
- capName: "integrations",
85983
- capScope: "system",
85984
- addonId: null,
85985
- access: "create",
85986
- role: "admin"
85987
- },
85988
86004
  "integrations.testConnection": {
85989
86005
  capName: "integrations",
85990
86006
  capScope: "system",
@@ -86979,6 +86995,13 @@ Object.freeze({
86979
86995
  access: "view",
86980
86996
  role: "admin"
86981
86997
  },
86998
+ "migrations.clearLegacyIntegrationRows": {
86999
+ capName: "migrations",
87000
+ capScope: "system",
87001
+ addonId: null,
87002
+ access: "delete",
87003
+ role: "admin"
87004
+ },
86982
87005
  "migrations.declareOwner": {
86983
87006
  capName: "migrations",
86984
87007
  capScope: "system",
@@ -86986,6 +87009,13 @@ Object.freeze({
86986
87009
  access: "create",
86987
87010
  role: "admin"
86988
87011
  },
87012
+ "migrations.deleteLegacyIntegrationRowKeys": {
87013
+ capName: "migrations",
87014
+ capScope: "system",
87015
+ addonId: null,
87016
+ access: "delete",
87017
+ role: "admin"
87018
+ },
86989
87019
  "migrations.dryRun": {
86990
87020
  capName: "migrations",
86991
87021
  capScope: "system",
@@ -87021,6 +87051,20 @@ Object.freeze({
87021
87051
  access: "create",
87022
87052
  role: "admin"
87023
87053
  },
87054
+ "migrations.patchLegacyIntegrationRows": {
87055
+ capName: "migrations",
87056
+ capScope: "system",
87057
+ addonId: null,
87058
+ access: "create",
87059
+ role: "admin"
87060
+ },
87061
+ "migrations.readLegacyIntegrationRows": {
87062
+ capName: "migrations",
87063
+ capScope: "system",
87064
+ addonId: null,
87065
+ access: "view",
87066
+ role: "admin"
87067
+ },
87024
87068
  "migrations.restorePreMigration": {
87025
87069
  capName: "migrations",
87026
87070
  capScope: "system",
@@ -141638,10 +141682,8 @@ var AlexaIntegrationManager = class {
141638
141682
  this.#status = "connected";
141639
141683
  this.#lastCheckedAt = Date.now();
141640
141684
  this.#onConnected(this.#id);
141641
- await this.refreshAppliances();
141642
- await this.poll();
141643
- this.#schedulePoll();
141644
141685
  this.#logger.info("AlexaIntegrationManager: started", { tags: { brokerId: this.#id } });
141686
+ this.#firstSync(client);
141645
141687
  } catch (err) {
141646
141688
  if (err instanceof AlexaAuthError) {
141647
141689
  this.#status = "auth-failed";
@@ -141662,6 +141704,24 @@ var AlexaIntegrationManager = class {
141662
141704
  });
141663
141705
  }
141664
141706
  }
141707
+ /** Background first refresh + poll, then the poll loop. Does nothing once `client` is stale. */
141708
+ async #firstSync(client) {
141709
+ let refreshed = false;
141710
+ try {
141711
+ await this.refreshAppliances();
141712
+ refreshed = true;
141713
+ } catch (err) {
141714
+ this.#error = errMsg(err);
141715
+ this.#logger.warn("AlexaIntegrationManager: first appliance refresh failed", {
141716
+ tags: { brokerId: this.#id },
141717
+ meta: { error: errMsg(err) }
141718
+ });
141719
+ }
141720
+ if (this.#client !== client) return;
141721
+ if (refreshed) await this.poll();
141722
+ if (this.#client !== client) return;
141723
+ this.#schedulePoll();
141724
+ }
141665
141725
  /** List the account's appliances + publish them to the resolver. */
141666
141726
  async refreshAppliances() {
141667
141727
  const client = this.#client;
@@ -145116,7 +145176,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
145116
145176
  infoOf: (id) => this.registry?.get(id) ?? null,
145117
145177
  onChanged: (id) => this.syncBrokerRuntime(id)
145118
145178
  });
145119
- await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id === FLATTEN_BROKER_RECORDS_ID), this.migrationPorts(records)));
145179
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, this.migrationPorts(records)), { only: [FLATTEN_BROKER_RECORDS_ID] });
145120
145180
  this.activeBrokers = await this.loadPersistedBrokers();
145121
145181
  this.registry = new AlexaBrokerRegistry(this.ctx.logger, {
145122
145182
  dataDir: this.ctx.dataDir,
@@ -145161,7 +145221,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
145161
145221
  deviceCode: this.deviceCodeLogin,
145162
145222
  commitCookie: (input) => this.commitCapturedCookie(input)
145163
145223
  }), ...speakIconRoutes()];
145164
- await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id !== FLATTEN_BROKER_RECORDS_ID), this.migrationPorts(records)));
145224
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, this.migrationPorts(records)), { only: ADDON_MIGRATIONS.map((m) => m.meta.id).filter((id) => id !== FLATTEN_BROKER_RECORDS_ID) });
145165
145225
  const speak = new AlexaSpeakController({
145166
145226
  logger: this.ctx.logger.child("speak"),
145167
145227
  readConfig: () => this.config,
@@ -145479,18 +145539,10 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
145479
145539
  try {
145480
145540
  const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id);
145481
145541
  const surviving = new Set(mine.map((i) => i.id));
145482
- for (const integration of mine) {
145483
- const settings = await this.ctx.api.integrations.getSettings.query({ id: integration.id });
145484
- const brokerId = typeof settings["brokerId"] === "string" ? settings["brokerId"] : void 0;
145485
- if (brokerId === void 0) continue;
145486
- if (!this.activeBrokers.some((b) => b.id === brokerId)) {
145487
- this.ctx.logger.warn("Alexa integration→broker: linked broker not found", { meta: {
145488
- integrationId: integration.id,
145489
- brokerId
145490
- } });
145491
- continue;
145492
- }
145493
- this.requireRegistry().linkIntegration(integration.id, brokerId);
145542
+ for (const broker of this.activeBrokers) {
145543
+ const integrationId = broker.integrationId;
145544
+ if (integrationId === void 0 || !surviving.has(integrationId)) continue;
145545
+ this.requireRegistry().linkIntegration(integrationId, broker.id);
145494
145546
  }
145495
145547
  const toRemove = this.activeBrokers.filter((b) => b.integrationId !== void 0 && !surviving.has(b.integrationId)).map((b) => b.id);
145496
145548
  if (toRemove.length === 0) return;
@@ -145532,9 +145584,9 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
145532
145584
  },
145533
145585
  logger: this.ctx.logger,
145534
145586
  getBrokerIdForIntegration: async (id) => {
145535
- const brokerId = (await this.ctx.api.integrations.getSettings.query({ id }))["brokerId"];
145536
- if (typeof brokerId !== "string") throw new Error(`integration ${id} has no linked brokerId`);
145537
- return brokerId;
145587
+ const broker = this.activeBrokers.find((entry) => entry.integrationId === id);
145588
+ if (broker === void 0) throw new Error(`integration ${id} has no linked brokerId`);
145589
+ return broker.id;
145538
145590
  },
145539
145591
  listAdoptedAlexa: async () => {
145540
145592
  const reg = this.ctx.kernel.deviceRegistry;
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-BaWhXZFm.mjs
6045
+ //#region ../types/dist/sleep-9TIHFj1w.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
  }
@@ -7980,10 +7986,9 @@ var BaseAddon = class {
7980
7986
  *
7981
7987
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
7982
7988
  * needs its connection fields, the default integration the addon's own sections. It is the
7983
- * SHAPE of the form only — the values live in `integrations.getSettings`, and the write path is
7984
- * unchanged (`integrations.setSettings` for an account integration).
7989
+ * SHAPE of the form only — the values live on the instance record.
7985
7990
  */
7986
- async integrationSettingsSchema(_instance) {
7991
+ async integrationFormSchema(_instance) {
7987
7992
  return null;
7988
7993
  }
7989
7994
  /**
@@ -8029,7 +8034,7 @@ var BaseAddon = class {
8029
8034
  * per-instance schema when it declares one, else its global schema (what they always showed).
8030
8035
  */
8031
8036
  async getIntegrationFormSchema(instance, nodeId) {
8032
- const scoped = await this.integrationSettingsSchema(instance);
8037
+ const scoped = await this.integrationFormSchema(instance);
8033
8038
  if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
8034
8039
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
8035
8040
  }
@@ -9303,6 +9308,7 @@ function defineEntryKind(name, spec) {
9303
9308
  class: "config",
9304
9309
  writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9305
9310
  retention: { none: "an entry goes only when the operator deletes it" },
9311
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9306
9312
  ...spec.database === void 0 ? {} : { database: spec.database },
9307
9313
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9308
9314
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
@@ -11972,7 +11978,7 @@ object({
11972
11978
  params: record(string(), union([string(), number$1()])).optional()
11973
11979
  });
11974
11980
  //#endregion
11975
- //#region ../types/dist/composition-CRNwcFYY.mjs
11981
+ //#region ../types/dist/composition-pOHbTzRB.mjs
11976
11982
  /**
11977
11983
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11978
11984
  *
@@ -14670,7 +14676,13 @@ var MigrationLedgerEntrySchema = object({
14670
14676
  * joined by the engine from the catalogues it knows this boot. Absent when no
14671
14677
  * catalogue lists the id (a newer database).
14672
14678
  */
14673
- 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()
14674
14686
  });
14675
14687
  /** A catalogued migration that is not applied, with where it stands. */
14676
14688
  var MigrationPendingEntrySchema = object({
@@ -50446,6 +50458,9 @@ var RunningProposalEvidenceSchema = object({
50446
50458
  runsFound: number$1().int().nonnegative(),
50447
50459
  pausesFound: number$1().int().nonnegative(),
50448
50460
  longestPauseMs: number$1().nonnegative(),
50461
+ /** Episodes shorter than ~90 s (an anti-crease pulse, a display waking): never a run, never a pause. */
50462
+ pulsesFound: number$1().int().nonnegative(),
50463
+ longestPulseMs: number$1().nonnegative(),
50449
50464
  idleFloorW: number$1(),
50450
50465
  noiseCeilingW: number$1(),
50451
50466
  plateauW: number$1(),
@@ -56291,11 +56306,7 @@ var CreateIntegrationInputSchema = object({
56291
56306
  info: record(string(), unknown()).optional(),
56292
56307
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
56293
56308
  originStableId: string().optional(),
56294
- settings: record(string(), unknown()).optional(),
56295
- /** Keys of `settings` the form's schema declares secret (by type) — kept in
56296
- * the keychain, not the table (D787). */
56297
- secretKeys: array(string()).optional(),
56298
- skipRestart: boolean().optional()
56309
+ settings: record(string(), unknown()).optional()
56299
56310
  });
56300
56311
  var UpdateIntegrationInputSchema = object({
56301
56312
  id: string(),
@@ -56316,16 +56327,6 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
56316
56327
  }), method(object({ id: string() }), DeleteIntegrationResultSchema, {
56317
56328
  kind: "mutation",
56318
56329
  auth: "admin"
56319
- }), method(object({ id: string() }), record(string(), unknown()), { auth: "admin" }), method(object({
56320
- id: string(),
56321
- settings: record(string(), unknown()),
56322
- /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
56323
- secretKeys: array(string()).optional(),
56324
- /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
56325
- skipRestart: boolean().optional()
56326
- }), object({ success: boolean() }), {
56327
- kind: "mutation",
56328
- auth: "admin"
56329
56330
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
56330
56331
  addonId: string(),
56331
56332
  settings: record(string(), unknown())
@@ -59302,6 +59303,27 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
59302
59303
  }), {
59303
59304
  kind: "mutation",
59304
59305
  auth: "admin"
59306
+ }), method(object({ integrationId: string().min(1) }), record(string(), unknown()), { auth: "admin" }), method(object({ integrationId: string().min(1) }), object({
59307
+ rows: number$1().int().nonnegative(),
59308
+ secrets: number$1().int().nonnegative()
59309
+ }), {
59310
+ kind: "mutation",
59311
+ auth: "admin"
59312
+ }), method(object({
59313
+ integrationId: string().min(1),
59314
+ patch: record(string(), unknown())
59315
+ }), object({ written: boolean() }), {
59316
+ kind: "mutation",
59317
+ auth: "admin"
59318
+ }), method(object({
59319
+ integrationId: string().min(1),
59320
+ keys: array(string().min(1))
59321
+ }), object({
59322
+ present: boolean(),
59323
+ removed: record(string(), unknown())
59324
+ }), {
59325
+ kind: "mutation",
59326
+ auth: "admin"
59305
59327
  });
59306
59328
  method(object({
59307
59329
  sourceUrl: string(),
@@ -80628,11 +80650,15 @@ discriminatedUnion("kind", [object({
80628
80650
  * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
80629
80651
  * near F. A device that is not near one level most of the time has no floor.
80630
80652
  * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
80631
- * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
80632
- * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
80653
+ * 3. an episode shorter than `PULSE_MAX_MS` is a PULSE (an anti-crease tumble, a
80654
+ * display waking): it is neither a run nor a pause. Among the remaining
80655
+ * episodes, gaps over `PAUSE_MAX_MS` always separate runs; the rest split in
80656
+ * two by the largest log-scale jump (>= 3x): the short ones are pauses INSIDE
80657
+ * a run (a soak), the long ones separate runs. A stretch under `MIN_RUN_MS`
80658
+ * is no run.
80633
80659
  * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
80634
- * grid a person would type; stop dwell covers the longest pause; start dwell
80635
- * covers the longest standby blip above T.
80660
+ * grid a person would type; stop dwell covers the longest pause and the
80661
+ * kind's floor; start dwell covers the longest pulse and standby blip above T.
80636
80662
  * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
80637
80663
  * run must hold exactly one true interval and nothing may flap outside.
80638
80664
  */
@@ -80643,6 +80669,10 @@ var MINUTE_MS$1 = 60 * SECOND_MS;
80643
80669
  30 * SECOND_MS;
80644
80670
  60 * SECOND_MS;
80645
80671
  20 * MINUTE_MS$1;
80672
+ 90 * SECOND_MS;
80673
+ 30 * MINUTE_MS$1;
80674
+ 10 * MINUTE_MS$1;
80675
+ DeviceType.Dryer, 3 * MINUTE_MS$1, DeviceType.Dishwasher, 3 * MINUTE_MS$1, DeviceType.Washer, 10 * MINUTE_MS$1;
80646
80676
  10 * SECOND_MS;
80647
80677
  10 * SECOND_MS;
80648
80678
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
@@ -85962,13 +85992,6 @@ Object.freeze({
85962
85992
  access: "view",
85963
85993
  role: "admin"
85964
85994
  },
85965
- "integrations.getSettings": {
85966
- capName: "integrations",
85967
- capScope: "system",
85968
- addonId: null,
85969
- access: "view",
85970
- role: "admin"
85971
- },
85972
85995
  "integrations.list": {
85973
85996
  capName: "integrations",
85974
85997
  capScope: "system",
@@ -85976,13 +85999,6 @@ Object.freeze({
85976
85999
  access: "view",
85977
86000
  role: "admin"
85978
86001
  },
85979
- "integrations.setSettings": {
85980
- capName: "integrations",
85981
- capScope: "system",
85982
- addonId: null,
85983
- access: "create",
85984
- role: "admin"
85985
- },
85986
86002
  "integrations.testConnection": {
85987
86003
  capName: "integrations",
85988
86004
  capScope: "system",
@@ -86977,6 +86993,13 @@ Object.freeze({
86977
86993
  access: "view",
86978
86994
  role: "admin"
86979
86995
  },
86996
+ "migrations.clearLegacyIntegrationRows": {
86997
+ capName: "migrations",
86998
+ capScope: "system",
86999
+ addonId: null,
87000
+ access: "delete",
87001
+ role: "admin"
87002
+ },
86980
87003
  "migrations.declareOwner": {
86981
87004
  capName: "migrations",
86982
87005
  capScope: "system",
@@ -86984,6 +87007,13 @@ Object.freeze({
86984
87007
  access: "create",
86985
87008
  role: "admin"
86986
87009
  },
87010
+ "migrations.deleteLegacyIntegrationRowKeys": {
87011
+ capName: "migrations",
87012
+ capScope: "system",
87013
+ addonId: null,
87014
+ access: "delete",
87015
+ role: "admin"
87016
+ },
86987
87017
  "migrations.dryRun": {
86988
87018
  capName: "migrations",
86989
87019
  capScope: "system",
@@ -87019,6 +87049,20 @@ Object.freeze({
87019
87049
  access: "create",
87020
87050
  role: "admin"
87021
87051
  },
87052
+ "migrations.patchLegacyIntegrationRows": {
87053
+ capName: "migrations",
87054
+ capScope: "system",
87055
+ addonId: null,
87056
+ access: "create",
87057
+ role: "admin"
87058
+ },
87059
+ "migrations.readLegacyIntegrationRows": {
87060
+ capName: "migrations",
87061
+ capScope: "system",
87062
+ addonId: null,
87063
+ access: "view",
87064
+ role: "admin"
87065
+ },
87022
87066
  "migrations.restorePreMigration": {
87023
87067
  capName: "migrations",
87024
87068
  capScope: "system",
@@ -141636,10 +141680,8 @@ var AlexaIntegrationManager = class {
141636
141680
  this.#status = "connected";
141637
141681
  this.#lastCheckedAt = Date.now();
141638
141682
  this.#onConnected(this.#id);
141639
- await this.refreshAppliances();
141640
- await this.poll();
141641
- this.#schedulePoll();
141642
141683
  this.#logger.info("AlexaIntegrationManager: started", { tags: { brokerId: this.#id } });
141684
+ this.#firstSync(client);
141643
141685
  } catch (err) {
141644
141686
  if (err instanceof AlexaAuthError) {
141645
141687
  this.#status = "auth-failed";
@@ -141660,6 +141702,24 @@ var AlexaIntegrationManager = class {
141660
141702
  });
141661
141703
  }
141662
141704
  }
141705
+ /** Background first refresh + poll, then the poll loop. Does nothing once `client` is stale. */
141706
+ async #firstSync(client) {
141707
+ let refreshed = false;
141708
+ try {
141709
+ await this.refreshAppliances();
141710
+ refreshed = true;
141711
+ } catch (err) {
141712
+ this.#error = errMsg(err);
141713
+ this.#logger.warn("AlexaIntegrationManager: first appliance refresh failed", {
141714
+ tags: { brokerId: this.#id },
141715
+ meta: { error: errMsg(err) }
141716
+ });
141717
+ }
141718
+ if (this.#client !== client) return;
141719
+ if (refreshed) await this.poll();
141720
+ if (this.#client !== client) return;
141721
+ this.#schedulePoll();
141722
+ }
141663
141723
  /** List the account's appliances + publish them to the resolver. */
141664
141724
  async refreshAppliances() {
141665
141725
  const client = this.#client;
@@ -145114,7 +145174,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
145114
145174
  infoOf: (id) => this.registry?.get(id) ?? null,
145115
145175
  onChanged: (id) => this.syncBrokerRuntime(id)
145116
145176
  });
145117
- await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id === FLATTEN_BROKER_RECORDS_ID), this.migrationPorts(records)));
145177
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, this.migrationPorts(records)), { only: [FLATTEN_BROKER_RECORDS_ID] });
145118
145178
  this.activeBrokers = await this.loadPersistedBrokers();
145119
145179
  this.registry = new AlexaBrokerRegistry(this.ctx.logger, {
145120
145180
  dataDir: this.ctx.dataDir,
@@ -145159,7 +145219,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
145159
145219
  deviceCode: this.deviceCodeLogin,
145160
145220
  commitCookie: (input) => this.commitCapturedCookie(input)
145161
145221
  }), ...speakIconRoutes()];
145162
- await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id !== FLATTEN_BROKER_RECORDS_ID), this.migrationPorts(records)));
145222
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, this.migrationPorts(records)), { only: ADDON_MIGRATIONS.map((m) => m.meta.id).filter((id) => id !== FLATTEN_BROKER_RECORDS_ID) });
145163
145223
  const speak = new AlexaSpeakController({
145164
145224
  logger: this.ctx.logger.child("speak"),
145165
145225
  readConfig: () => this.config,
@@ -145477,18 +145537,10 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
145477
145537
  try {
145478
145538
  const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id);
145479
145539
  const surviving = new Set(mine.map((i) => i.id));
145480
- for (const integration of mine) {
145481
- const settings = await this.ctx.api.integrations.getSettings.query({ id: integration.id });
145482
- const brokerId = typeof settings["brokerId"] === "string" ? settings["brokerId"] : void 0;
145483
- if (brokerId === void 0) continue;
145484
- if (!this.activeBrokers.some((b) => b.id === brokerId)) {
145485
- this.ctx.logger.warn("Alexa integration→broker: linked broker not found", { meta: {
145486
- integrationId: integration.id,
145487
- brokerId
145488
- } });
145489
- continue;
145490
- }
145491
- this.requireRegistry().linkIntegration(integration.id, brokerId);
145540
+ for (const broker of this.activeBrokers) {
145541
+ const integrationId = broker.integrationId;
145542
+ if (integrationId === void 0 || !surviving.has(integrationId)) continue;
145543
+ this.requireRegistry().linkIntegration(integrationId, broker.id);
145492
145544
  }
145493
145545
  const toRemove = this.activeBrokers.filter((b) => b.integrationId !== void 0 && !surviving.has(b.integrationId)).map((b) => b.id);
145494
145546
  if (toRemove.length === 0) return;
@@ -145530,9 +145582,9 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
145530
145582
  },
145531
145583
  logger: this.ctx.logger,
145532
145584
  getBrokerIdForIntegration: async (id) => {
145533
- const brokerId = (await this.ctx.api.integrations.getSettings.query({ id }))["brokerId"];
145534
- if (typeof brokerId !== "string") throw new Error(`integration ${id} has no linked brokerId`);
145535
- return brokerId;
145585
+ const broker = this.activeBrokers.find((entry) => entry.integrationId === id);
145586
+ if (broker === void 0) throw new Error(`integration ${id} has no linked brokerId`);
145587
+ return broker.id;
145536
145588
  },
145537
145589
  listAdoptedAlexa: async () => {
145538
145590
  const reg = this.ctx.kernel.deviceRegistry;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-import-alexa",
3
- "version": "0.2.184",
3
+ "version": "0.2.186",
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.762"
75
+ "server": ">=1.2.770"
76
76
  }
77
77
  },
78
78
  "files": [