@camstack/addon-matter-broker 0.2.155 → 0.2.157

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,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.157] - 2026-11-05
11
+
12
+ ### Changed
13
+
14
+ - A Matter integration links to its fabric through the fabric record
15
+
16
+ ## [0.2.156] - 2026-11-04
17
+
18
+ ### Changed
19
+
20
+ - The integration states its role (devices, services or both) to the Integrations page
21
+
10
22
  ## [0.2.155] - 2026-11-04
11
23
 
12
24
  ### Changed
package/dist/addon.js CHANGED
@@ -5995,7 +5995,7 @@ var ZodIssueCode = {
5995
5995
  var ZodFirstPartyTypeKind;
5996
5996
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5997
5997
  //#endregion
5998
- //#region ../types/dist/sleep-BkzQdpzF.mjs
5998
+ //#region ../types/dist/sleep-9TIHFj1w.mjs
5999
5999
  /**
6000
6000
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6001
6001
  * window to float samples (D455).
@@ -6595,7 +6595,7 @@ var NOT_RUN = {
6595
6595
  * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6596
6596
  * because the hub (and this addon's boot) must not hang on a migration.
6597
6597
  */
6598
- async function runAddonMigrations(host, migrations, logger) {
6598
+ async function runAddonMigrations(host, migrations, logger, options = {}) {
6599
6599
  const first = migrations[0];
6600
6600
  if (first === void 0) return NOT_RUN;
6601
6601
  const owner = first.meta.owner;
@@ -6624,7 +6624,9 @@ async function runAddonMigrations(host, migrations, logger) {
6624
6624
  else if (status === "blocked") blocked.push(id);
6625
6625
  else skipped.push(id);
6626
6626
  };
6627
+ const selected = options.only === void 0 ? null : new Set(options.only);
6627
6628
  for (const step of declaration.steps) {
6629
+ if (selected !== null && !selected.has(step.id)) continue;
6628
6630
  const bound = byId.get(step.id);
6629
6631
  if (bound === void 0 || step.action === "skip") {
6630
6632
  skipped.push(step.id);
@@ -7784,14 +7786,18 @@ var BaseAddon = class {
7784
7786
  * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7785
7787
  * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7786
7788
  * serve data whose shape it cannot trust; `tolerate` migrations never do.
7789
+ *
7790
+ * ALWAYS pass the whole catalogue; a phase that must run only some of it passes
7791
+ * `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
7792
+ * omitted applied ids "newer").
7787
7793
  */
7788
- async runMigrations(migrations) {
7794
+ async runMigrations(migrations, options) {
7789
7795
  const api = this.ctx.api.migrations;
7790
7796
  const report = await runAddonMigrations({
7791
7797
  declareOwner: (input) => api.declareOwner.mutate(input),
7792
7798
  startMigration: (input) => api.startMigration.mutate(input),
7793
7799
  finishMigration: (input) => api.finishMigration.mutate(input)
7794
- }, migrations, this.ctx.logger);
7800
+ }, migrations, this.ctx.logger, options);
7795
7801
  if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
7796
7802
  return report;
7797
7803
  }
@@ -7933,10 +7939,9 @@ var BaseAddon = class {
7933
7939
  *
7934
7940
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
7935
7941
  * needs its connection fields, the default integration the addon's own sections. It is the
7936
- * SHAPE of the form only — the values live in `integrations.getSettings`, and the write path is
7937
- * unchanged (`integrations.setSettings` for an account integration).
7942
+ * SHAPE of the form only — the values live on the instance record.
7938
7943
  */
7939
- async integrationSettingsSchema(_instance) {
7944
+ async integrationFormSchema(_instance) {
7940
7945
  return null;
7941
7946
  }
7942
7947
  /**
@@ -7982,7 +7987,7 @@ var BaseAddon = class {
7982
7987
  * per-instance schema when it declares one, else its global schema (what they always showed).
7983
7988
  */
7984
7989
  async getIntegrationFormSchema(instance, nodeId) {
7985
- const scoped = await this.integrationSettingsSchema(instance);
7990
+ const scoped = await this.integrationFormSchema(instance);
7986
7991
  if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
7987
7992
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
7988
7993
  }
@@ -8831,6 +8836,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8831
8836
  * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8832
8837
  * (`auth.accessToken`, `headers[].value`).
8833
8838
  *
8839
+ * `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
8840
+ * names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
8841
+ * key releases exactly that key's secret.
8842
+ *
8834
8843
  * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8835
8844
  * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8836
8845
  * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
@@ -8843,11 +8852,13 @@ function parseSecretPath(path) {
8843
8852
  if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8844
8853
  return path.split(".").map((raw) => {
8845
8854
  const each = raw.endsWith("[]");
8846
- const key = each ? raw.slice(0, -2) : raw;
8855
+ const map = raw.endsWith("{}");
8856
+ const key = each || map ? raw.slice(0, -2) : raw;
8847
8857
  if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8848
8858
  return {
8849
8859
  key,
8850
- each
8860
+ each,
8861
+ map
8851
8862
  };
8852
8863
  });
8853
8864
  }
@@ -8899,6 +8910,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
8899
8910
  [segment.key]: mapped
8900
8911
  };
8901
8912
  }
8913
+ if (segment.map) {
8914
+ if (!isRecord$4$1(child)) return node;
8915
+ const kept = Object.entries(child).map(([mapKey, value]) => {
8916
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
8917
+ if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
8918
+ return [mapKey, visit({
8919
+ declared,
8920
+ concrete,
8921
+ value
8922
+ })];
8923
+ }).filter(([, value]) => value !== REMOVE_LEAF);
8924
+ return {
8925
+ ...node,
8926
+ [segment.key]: Object.fromEntries(kept)
8927
+ };
8928
+ }
8902
8929
  const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8903
8930
  if (last) {
8904
8931
  const replacement = visit({
@@ -9040,7 +9067,8 @@ function isZodSecret(schema) {
9040
9067
  }
9041
9068
  /**
9042
9069
  * Every declared secret path in a config schema: dotted keys, `[]` for "each
9043
- * element of this array" (`connection.password`, `rows[].token`). A declared
9070
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9071
+ * map" (`applianceSecrets{}`). A declared
9044
9072
  * object or array is reported as one path and not descended into — everything
9045
9073
  * under it is secret.
9046
9074
  */
@@ -9051,6 +9079,7 @@ function collectZodSecretPaths(schema, prefix = "") {
9051
9079
  const def = defOf(layer);
9052
9080
  if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
9053
9081
  else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9082
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
9054
9083
  else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
9055
9084
  else if (def.type === "intersection") {
9056
9085
  if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
@@ -9232,6 +9261,7 @@ function defineEntryKind(name, spec) {
9232
9261
  class: "config",
9233
9262
  writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9234
9263
  retention: { none: "an entry goes only when the operator deletes it" },
9264
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9235
9265
  ...spec.database === void 0 ? {} : { database: spec.database },
9236
9266
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9237
9267
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
@@ -11901,7 +11931,7 @@ object({
11901
11931
  params: record(string$2(), union([string$2(), number()])).optional()
11902
11932
  });
11903
11933
  //#endregion
11904
- //#region ../types/dist/composition-CRNwcFYY.mjs
11934
+ //#region ../types/dist/composition-pOHbTzRB.mjs
11905
11935
  /**
11906
11936
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11907
11937
  *
@@ -13248,11 +13278,49 @@ var IntegrationInstanceRefSchema = object({
13248
13278
  ]),
13249
13279
  candidateStableId: string$2().optional()
13250
13280
  });
13281
+ /**
13282
+ * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
13283
+ * entry kind, so an addon that holds a connection serves it in ~30 lines.
13284
+ *
13285
+ * Every instance is one record of the addon's own kind, keyed by the
13286
+ * `integrations` registry id. The records door does the secret work: a field the
13287
+ * kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
13288
+ * deleting the row deletes the secret, and one row's value is never another's.
13289
+ *
13290
+ * Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
13291
+ * token or the view the form was given KEEP the stored secret;
13292
+ * `{ $secretClear: true }` clears it; any other string replaces it.
13293
+ */
13251
13294
  /** The most instances one addon lists (an integrations page, not a table). */
13252
13295
  var INSTANCES_LIST_MAX = 1e3;
13253
13296
  /** The keys of the kind's secret fields that sit at the top level of the body. */
13254
13297
  function topLevelSecretKeys(paths) {
13255
- return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
13298
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
13299
+ }
13300
+ /** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
13301
+ function secretMapKeys(paths) {
13302
+ return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
13303
+ }
13304
+ /**
13305
+ * A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
13306
+ * view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
13307
+ * or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
13308
+ */
13309
+ function secretMapPatch(value) {
13310
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
13311
+ const out = {};
13312
+ for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
13313
+ else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
13314
+ return Object.keys(out).length > 0 ? out : null;
13315
+ }
13316
+ /** Does the kind's body schema carry an `enabled` field? */
13317
+ function declaresEnabled(kind) {
13318
+ const json = toJSONSchema(kind.schema, {
13319
+ unrepresentable: "any",
13320
+ io: "input"
13321
+ });
13322
+ const properties = Reflect.get(json, "properties");
13323
+ return typeof properties === "object" && properties !== null && "enabled" in properties;
13256
13324
  }
13257
13325
  var RecordIntegrationInstances = class {
13258
13326
  records;
@@ -13260,14 +13328,20 @@ var RecordIntegrationInstances = class {
13260
13328
  hooks;
13261
13329
  handle;
13262
13330
  secretKeys;
13331
+ secretMaps;
13263
13332
  nameField;
13333
+ /** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
13334
+ registryOwnsEnabled;
13264
13335
  constructor(records, kind, hooks) {
13265
13336
  this.records = records;
13266
13337
  this.kind = kind;
13267
13338
  this.hooks = hooks;
13268
13339
  this.handle = records.kind(kind);
13269
13340
  this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13341
+ this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
13270
13342
  this.nameField = kind.declaration.entry?.nameField ?? "name";
13343
+ this.registryOwnsEnabled = hooks.description.role === "devices";
13344
+ 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"`);
13271
13345
  }
13272
13346
  /**
13273
13347
  * One instance read for the addon's own runtime; null when absent. When the keychain is
@@ -13318,7 +13392,7 @@ var RecordIntegrationInstances = class {
13318
13392
  id: record.id,
13319
13393
  name: typeof rawName === "string" ? rawName : record.id,
13320
13394
  configured: this.hooks.configured(record.data),
13321
- enabled: record.data.enabled !== false,
13395
+ ...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
13322
13396
  formValues: shown,
13323
13397
  ...kind !== void 0 ? { kind } : {},
13324
13398
  ...health?.status !== void 0 ? { status: health.status } : {},
@@ -13348,32 +13422,36 @@ var RecordIntegrationInstances = class {
13348
13422
  id: minted,
13349
13423
  data: this.hooks.initialBody(name, kind)
13350
13424
  }, { ifRev: 0 });
13351
- await this.hooks.onChanged?.(minted);
13425
+ await this.hooks.onChanged?.(minted, "created");
13352
13426
  return { id: minted };
13353
13427
  };
13354
13428
  updateInstance = async ({ id, name, values }) => {
13355
13429
  const patch = {};
13356
13430
  for (const [key, value] of Object.entries(values)) {
13357
13431
  if (value === void 0) continue;
13358
- if (!this.secretKeys.has(key)) patch[key] = value;
13432
+ if (this.secretMaps.has(key)) {
13433
+ const entries = secretMapPatch(value);
13434
+ if (entries !== null) patch[key] = entries;
13435
+ } else if (!this.secretKeys.has(key)) patch[key] = value;
13359
13436
  else if (isSecretClear(value)) patch[key] = null;
13360
13437
  else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13361
13438
  }
13362
13439
  if (name !== void 0) patch[this.nameField] = name;
13363
13440
  await this.handle.patch(id, patch);
13364
- await this.hooks.onChanged?.(id);
13441
+ await this.hooks.onChanged?.(id, "updated");
13365
13442
  return { ok: true };
13366
13443
  };
13367
13444
  /** The switch lives in the record body; the addon is told so it applies it (D996). */
13368
13445
  setInstanceEnabled = async ({ id, enabled }) => {
13446
+ if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
13369
13447
  await this.handle.patch(id, { enabled });
13370
- await this.hooks.onChanged?.(id);
13448
+ await this.hooks.onChanged?.(id, "enabled");
13371
13449
  return { ok: true };
13372
13450
  };
13373
13451
  /** The row and its keychain refs go together (the records door). */
13374
13452
  deleteInstance = async ({ id }) => {
13375
13453
  await this.handle.delete(id);
13376
- await this.hooks.onChanged?.(id);
13454
+ await this.hooks.onChanged?.(id, "deleted");
13377
13455
  return { ok: true };
13378
13456
  };
13379
13457
  testInstance = async (input) => {
@@ -14434,7 +14512,13 @@ var MigrationLedgerEntrySchema = object({
14434
14512
  * joined by the engine from the catalogues it knows this boot. Absent when no
14435
14513
  * catalogue lists the id (a newer database).
14436
14514
  */
14437
- meta: MigrationMetaSchema.optional()
14515
+ meta: MigrationMetaSchema.optional(),
14516
+ /**
14517
+ * Set when this is an applied row of an addon owner whose catalogue the answering
14518
+ * process has not loaded (the owner has not declared yet): the row is applied, the
14519
+ * process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
14520
+ */
14521
+ ownerNotLoaded: boolean().optional()
14438
14522
  });
14439
14523
  /** A catalogued migration that is not applied, with where it stands. */
14440
14524
  var MigrationPendingEntrySchema = object({
@@ -50169,6 +50253,9 @@ var RunningProposalEvidenceSchema = object({
50169
50253
  runsFound: number().int().nonnegative(),
50170
50254
  pausesFound: number().int().nonnegative(),
50171
50255
  longestPauseMs: number().nonnegative(),
50256
+ /** Episodes shorter than ~90 s (an anti-crease pulse, a display waking): never a run, never a pause. */
50257
+ pulsesFound: number().int().nonnegative(),
50258
+ longestPulseMs: number().nonnegative(),
50172
50259
  idleFloorW: number(),
50173
50260
  noiseCeilingW: number(),
50174
50261
  plateauW: number(),
@@ -56014,11 +56101,7 @@ var CreateIntegrationInputSchema = object({
56014
56101
  info: record(string$2(), unknown()).optional(),
56015
56102
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
56016
56103
  originStableId: string$2().optional(),
56017
- settings: record(string$2(), unknown()).optional(),
56018
- /** Keys of `settings` the form's schema declares secret (by type) — kept in
56019
- * the keychain, not the table (D787). */
56020
- secretKeys: array(string$2()).optional(),
56021
- skipRestart: boolean().optional()
56104
+ settings: record(string$2(), unknown()).optional()
56022
56105
  });
56023
56106
  var UpdateIntegrationInputSchema = object({
56024
56107
  id: string$2(),
@@ -56039,16 +56122,6 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string$2
56039
56122
  }), method(object({ id: string$2() }), DeleteIntegrationResultSchema, {
56040
56123
  kind: "mutation",
56041
56124
  auth: "admin"
56042
- }), method(object({ id: string$2() }), record(string$2(), unknown()), { auth: "admin" }), method(object({
56043
- id: string$2(),
56044
- settings: record(string$2(), unknown()),
56045
- /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
56046
- secretKeys: array(string$2()).optional(),
56047
- /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
56048
- skipRestart: boolean().optional()
56049
- }), object({ success: boolean() }), {
56050
- kind: "mutation",
56051
- auth: "admin"
56052
56125
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
56053
56126
  addonId: string$2(),
56054
56127
  settings: record(string$2(), unknown())
@@ -56100,6 +56173,21 @@ var IntegrationInstanceKindSchema = object({
56100
56173
  disconnectPath: string$2().min(1).optional()
56101
56174
  });
56102
56175
  var IntegrationInstanceDescriptionSchema = object({
56176
+ /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
56177
+ role: _enum([
56178
+ "devices",
56179
+ "services",
56180
+ "both"
56181
+ ]),
56182
+ /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
56183
+ discovery: object({
56184
+ mode: _enum([
56185
+ "none",
56186
+ "lan",
56187
+ "account"
56188
+ ]),
56189
+ scopedToInstance: boolean()
56190
+ }).optional(),
56103
56191
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
56104
56192
  instanceMode: IntegrationInstanceModeSchema,
56105
56193
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
@@ -56139,10 +56227,11 @@ var IntegrationInstanceSchema = object({
56139
56227
  /** The addon's own rule (an URL is present, a client id is present...). */
56140
56228
  configured: boolean(),
56141
56229
  /**
56142
- * The instance's own on/off switch (D996): always present, derived from the record body
56143
- * (`enabled !== false`, so a row written before the field existed is on).
56230
+ * The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
56231
+ * so a row written before the field existed is on). Absent when the `integrations` registry
56232
+ * owns the switch (`describe().role` is `devices`): read it from the registry row.
56144
56233
  */
56145
- enabled: boolean(),
56234
+ enabled: boolean().optional(),
56146
56235
  /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
56147
56236
  kind: string$2().optional(),
56148
56237
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
@@ -59009,6 +59098,27 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
59009
59098
  }), {
59010
59099
  kind: "mutation",
59011
59100
  auth: "admin"
59101
+ }), method(object({ integrationId: string$2().min(1) }), record(string$2(), unknown()), { auth: "admin" }), method(object({ integrationId: string$2().min(1) }), object({
59102
+ rows: number().int().nonnegative(),
59103
+ secrets: number().int().nonnegative()
59104
+ }), {
59105
+ kind: "mutation",
59106
+ auth: "admin"
59107
+ }), method(object({
59108
+ integrationId: string$2().min(1),
59109
+ patch: record(string$2(), unknown())
59110
+ }), object({ written: boolean() }), {
59111
+ kind: "mutation",
59112
+ auth: "admin"
59113
+ }), method(object({
59114
+ integrationId: string$2().min(1),
59115
+ keys: array(string$2().min(1))
59116
+ }), object({
59117
+ present: boolean(),
59118
+ removed: record(string$2(), unknown())
59119
+ }), {
59120
+ kind: "mutation",
59121
+ auth: "admin"
59012
59122
  });
59013
59123
  method(object({
59014
59124
  sourceUrl: string$2(),
@@ -80335,11 +80445,15 @@ discriminatedUnion("kind", [object({
80335
80445
  * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
80336
80446
  * near F. A device that is not near one level most of the time has no floor.
80337
80447
  * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
80338
- * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
80339
- * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
80448
+ * 3. an episode shorter than `PULSE_MAX_MS` is a PULSE (an anti-crease tumble, a
80449
+ * display waking): it is neither a run nor a pause. Among the remaining
80450
+ * episodes, gaps over `PAUSE_MAX_MS` always separate runs; the rest split in
80451
+ * two by the largest log-scale jump (>= 3x): the short ones are pauses INSIDE
80452
+ * a run (a soak), the long ones separate runs. A stretch under `MIN_RUN_MS`
80453
+ * is no run.
80340
80454
  * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
80341
- * grid a person would type; stop dwell covers the longest pause; start dwell
80342
- * covers the longest standby blip above T.
80455
+ * grid a person would type; stop dwell covers the longest pause and the
80456
+ * kind's floor; start dwell covers the longest pulse and standby blip above T.
80343
80457
  * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
80344
80458
  * run must hold exactly one true interval and nothing may flap outside.
80345
80459
  */
@@ -80350,6 +80464,10 @@ var MINUTE_MS$1 = 60 * SECOND_MS;
80350
80464
  30 * SECOND_MS;
80351
80465
  60 * SECOND_MS;
80352
80466
  20 * MINUTE_MS$1;
80467
+ 90 * SECOND_MS;
80468
+ 30 * MINUTE_MS$1;
80469
+ 10 * MINUTE_MS$1;
80470
+ DeviceType.Dryer, 3 * MINUTE_MS$1, DeviceType.Dishwasher, 3 * MINUTE_MS$1, DeviceType.Washer, 10 * MINUTE_MS$1;
80353
80471
  10 * SECOND_MS;
80354
80472
  10 * SECOND_MS;
80355
80473
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
@@ -85669,13 +85787,6 @@ Object.freeze({
85669
85787
  access: "view",
85670
85788
  role: "admin"
85671
85789
  },
85672
- "integrations.getSettings": {
85673
- capName: "integrations",
85674
- capScope: "system",
85675
- addonId: null,
85676
- access: "view",
85677
- role: "admin"
85678
- },
85679
85790
  "integrations.list": {
85680
85791
  capName: "integrations",
85681
85792
  capScope: "system",
@@ -85683,13 +85794,6 @@ Object.freeze({
85683
85794
  access: "view",
85684
85795
  role: "admin"
85685
85796
  },
85686
- "integrations.setSettings": {
85687
- capName: "integrations",
85688
- capScope: "system",
85689
- addonId: null,
85690
- access: "create",
85691
- role: "admin"
85692
- },
85693
85797
  "integrations.testConnection": {
85694
85798
  capName: "integrations",
85695
85799
  capScope: "system",
@@ -86684,6 +86788,13 @@ Object.freeze({
86684
86788
  access: "view",
86685
86789
  role: "admin"
86686
86790
  },
86791
+ "migrations.clearLegacyIntegrationRows": {
86792
+ capName: "migrations",
86793
+ capScope: "system",
86794
+ addonId: null,
86795
+ access: "delete",
86796
+ role: "admin"
86797
+ },
86687
86798
  "migrations.declareOwner": {
86688
86799
  capName: "migrations",
86689
86800
  capScope: "system",
@@ -86691,6 +86802,13 @@ Object.freeze({
86691
86802
  access: "create",
86692
86803
  role: "admin"
86693
86804
  },
86805
+ "migrations.deleteLegacyIntegrationRowKeys": {
86806
+ capName: "migrations",
86807
+ capScope: "system",
86808
+ addonId: null,
86809
+ access: "delete",
86810
+ role: "admin"
86811
+ },
86694
86812
  "migrations.dryRun": {
86695
86813
  capName: "migrations",
86696
86814
  capScope: "system",
@@ -86726,6 +86844,20 @@ Object.freeze({
86726
86844
  access: "create",
86727
86845
  role: "admin"
86728
86846
  },
86847
+ "migrations.patchLegacyIntegrationRows": {
86848
+ capName: "migrations",
86849
+ capScope: "system",
86850
+ addonId: null,
86851
+ access: "create",
86852
+ role: "admin"
86853
+ },
86854
+ "migrations.readLegacyIntegrationRows": {
86855
+ capName: "migrations",
86856
+ capScope: "system",
86857
+ addonId: null,
86858
+ access: "view",
86859
+ role: "admin"
86860
+ },
86729
86861
  "migrations.restorePreMigration": {
86730
86862
  capName: "migrations",
86731
86863
  capScope: "system",
@@ -208592,6 +208724,7 @@ function statusOf(info) {
208592
208724
  function buildMatterInstances(records, deps) {
208593
208725
  return new RecordIntegrationInstances(records, matterBrokersEntryKind, {
208594
208726
  description: {
208727
+ role: "both",
208595
208728
  instanceMode: "multiple",
208596
208729
  kind: "connection",
208597
208730
  canTest: true,
@@ -209011,18 +209144,10 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
209011
209144
  try {
209012
209145
  const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id);
209013
209146
  const surviving = new Set(mine.map((i) => i.id));
209014
- for (const integration of mine) {
209015
- const settings = await this.ctx.api.integrations.getSettings.query({ id: integration.id });
209016
- const brokerId = typeof settings["brokerId"] === "string" ? settings["brokerId"] : void 0;
209017
- if (brokerId === void 0) continue;
209018
- if (!this.activeBrokers.some((b) => b.id === brokerId)) {
209019
- this.ctx.logger.warn("Matter integration→broker: linked broker not found", { meta: {
209020
- integrationId: integration.id,
209021
- brokerId
209022
- } });
209023
- continue;
209024
- }
209025
- this.requireRegistry().linkIntegration(integration.id, brokerId);
209147
+ for (const broker of this.activeBrokers) {
209148
+ const integrationId = broker.integrationId;
209149
+ if (integrationId === void 0 || !surviving.has(integrationId)) continue;
209150
+ this.requireRegistry().linkIntegration(integrationId, broker.id);
209026
209151
  }
209027
209152
  const toRemove = this.activeBrokers.filter((b) => b.integrationId !== void 0 && !surviving.has(b.integrationId)).map((b) => b.id);
209028
209153
  if (toRemove.length === 0) return;
@@ -209063,9 +209188,9 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
209063
209188
  },
209064
209189
  logger: this.ctx.logger,
209065
209190
  getBrokerIdForIntegration: async (id) => {
209066
- const brokerId = (await this.ctx.api.integrations.getSettings.query({ id }))["brokerId"];
209067
- if (typeof brokerId !== "string") throw new Error(`integration ${id} has no linked brokerId`);
209068
- return brokerId;
209191
+ const broker = this.activeBrokers.find((entry) => entry.integrationId === id);
209192
+ if (broker === void 0) throw new Error(`integration ${id} has no linked brokerId`);
209193
+ return broker.id;
209069
209194
  },
209070
209195
  listAdoptedMatter: async () => {
209071
209196
  const reg = this.ctx.kernel.deviceRegistry;
package/dist/addon.mjs CHANGED
@@ -5993,7 +5993,7 @@ var ZodIssueCode = {
5993
5993
  var ZodFirstPartyTypeKind;
5994
5994
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5995
5995
  //#endregion
5996
- //#region ../types/dist/sleep-BkzQdpzF.mjs
5996
+ //#region ../types/dist/sleep-9TIHFj1w.mjs
5997
5997
  /**
5998
5998
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5999
5999
  * window to float samples (D455).
@@ -6593,7 +6593,7 @@ var NOT_RUN = {
6593
6593
  * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6594
6594
  * because the hub (and this addon's boot) must not hang on a migration.
6595
6595
  */
6596
- async function runAddonMigrations(host, migrations, logger) {
6596
+ async function runAddonMigrations(host, migrations, logger, options = {}) {
6597
6597
  const first = migrations[0];
6598
6598
  if (first === void 0) return NOT_RUN;
6599
6599
  const owner = first.meta.owner;
@@ -6622,7 +6622,9 @@ async function runAddonMigrations(host, migrations, logger) {
6622
6622
  else if (status === "blocked") blocked.push(id);
6623
6623
  else skipped.push(id);
6624
6624
  };
6625
+ const selected = options.only === void 0 ? null : new Set(options.only);
6625
6626
  for (const step of declaration.steps) {
6627
+ if (selected !== null && !selected.has(step.id)) continue;
6626
6628
  const bound = byId.get(step.id);
6627
6629
  if (bound === void 0 || step.action === "skip") {
6628
6630
  skipped.push(step.id);
@@ -7782,14 +7784,18 @@ var BaseAddon = class {
7782
7784
  * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7783
7785
  * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7784
7786
  * serve data whose shape it cannot trust; `tolerate` migrations never do.
7787
+ *
7788
+ * ALWAYS pass the whole catalogue; a phase that must run only some of it passes
7789
+ * `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
7790
+ * omitted applied ids "newer").
7785
7791
  */
7786
- async runMigrations(migrations) {
7792
+ async runMigrations(migrations, options) {
7787
7793
  const api = this.ctx.api.migrations;
7788
7794
  const report = await runAddonMigrations({
7789
7795
  declareOwner: (input) => api.declareOwner.mutate(input),
7790
7796
  startMigration: (input) => api.startMigration.mutate(input),
7791
7797
  finishMigration: (input) => api.finishMigration.mutate(input)
7792
- }, migrations, this.ctx.logger);
7798
+ }, migrations, this.ctx.logger, options);
7793
7799
  if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
7794
7800
  return report;
7795
7801
  }
@@ -7931,10 +7937,9 @@ var BaseAddon = class {
7931
7937
  *
7932
7938
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
7933
7939
  * needs its connection fields, the default integration the addon's own sections. It is the
7934
- * SHAPE of the form only — the values live in `integrations.getSettings`, and the write path is
7935
- * unchanged (`integrations.setSettings` for an account integration).
7940
+ * SHAPE of the form only — the values live on the instance record.
7936
7941
  */
7937
- async integrationSettingsSchema(_instance) {
7942
+ async integrationFormSchema(_instance) {
7938
7943
  return null;
7939
7944
  }
7940
7945
  /**
@@ -7980,7 +7985,7 @@ var BaseAddon = class {
7980
7985
  * per-instance schema when it declares one, else its global schema (what they always showed).
7981
7986
  */
7982
7987
  async getIntegrationFormSchema(instance, nodeId) {
7983
- const scoped = await this.integrationSettingsSchema(instance);
7988
+ const scoped = await this.integrationFormSchema(instance);
7984
7989
  if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
7985
7990
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
7986
7991
  }
@@ -8829,6 +8834,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8829
8834
  * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8830
8835
  * (`auth.accessToken`, `headers[].value`).
8831
8836
  *
8837
+ * `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
8838
+ * names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
8839
+ * key releases exactly that key's secret.
8840
+ *
8832
8841
  * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8833
8842
  * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8834
8843
  * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
@@ -8841,11 +8850,13 @@ function parseSecretPath(path) {
8841
8850
  if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8842
8851
  return path.split(".").map((raw) => {
8843
8852
  const each = raw.endsWith("[]");
8844
- const key = each ? raw.slice(0, -2) : raw;
8853
+ const map = raw.endsWith("{}");
8854
+ const key = each || map ? raw.slice(0, -2) : raw;
8845
8855
  if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8846
8856
  return {
8847
8857
  key,
8848
- each
8858
+ each,
8859
+ map
8849
8860
  };
8850
8861
  });
8851
8862
  }
@@ -8897,6 +8908,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
8897
8908
  [segment.key]: mapped
8898
8909
  };
8899
8910
  }
8911
+ if (segment.map) {
8912
+ if (!isRecord$4$1(child)) return node;
8913
+ const kept = Object.entries(child).map(([mapKey, value]) => {
8914
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
8915
+ if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
8916
+ return [mapKey, visit({
8917
+ declared,
8918
+ concrete,
8919
+ value
8920
+ })];
8921
+ }).filter(([, value]) => value !== REMOVE_LEAF);
8922
+ return {
8923
+ ...node,
8924
+ [segment.key]: Object.fromEntries(kept)
8925
+ };
8926
+ }
8900
8927
  const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8901
8928
  if (last) {
8902
8929
  const replacement = visit({
@@ -9038,7 +9065,8 @@ function isZodSecret(schema) {
9038
9065
  }
9039
9066
  /**
9040
9067
  * Every declared secret path in a config schema: dotted keys, `[]` for "each
9041
- * element of this array" (`connection.password`, `rows[].token`). A declared
9068
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9069
+ * map" (`applianceSecrets{}`). A declared
9042
9070
  * object or array is reported as one path and not descended into — everything
9043
9071
  * under it is secret.
9044
9072
  */
@@ -9049,6 +9077,7 @@ function collectZodSecretPaths(schema, prefix = "") {
9049
9077
  const def = defOf(layer);
9050
9078
  if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
9051
9079
  else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9080
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
9052
9081
  else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
9053
9082
  else if (def.type === "intersection") {
9054
9083
  if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
@@ -9230,6 +9259,7 @@ function defineEntryKind(name, spec) {
9230
9259
  class: "config",
9231
9260
  writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9232
9261
  retention: { none: "an entry goes only when the operator deletes it" },
9262
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9233
9263
  ...spec.database === void 0 ? {} : { database: spec.database },
9234
9264
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9235
9265
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
@@ -11899,7 +11929,7 @@ object({
11899
11929
  params: record(string$2(), union([string$2(), number()])).optional()
11900
11930
  });
11901
11931
  //#endregion
11902
- //#region ../types/dist/composition-CRNwcFYY.mjs
11932
+ //#region ../types/dist/composition-pOHbTzRB.mjs
11903
11933
  /**
11904
11934
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11905
11935
  *
@@ -13246,11 +13276,49 @@ var IntegrationInstanceRefSchema = object({
13246
13276
  ]),
13247
13277
  candidateStableId: string$2().optional()
13248
13278
  });
13279
+ /**
13280
+ * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
13281
+ * entry kind, so an addon that holds a connection serves it in ~30 lines.
13282
+ *
13283
+ * Every instance is one record of the addon's own kind, keyed by the
13284
+ * `integrations` registry id. The records door does the secret work: a field the
13285
+ * kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
13286
+ * deleting the row deletes the secret, and one row's value is never another's.
13287
+ *
13288
+ * Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
13289
+ * token or the view the form was given KEEP the stored secret;
13290
+ * `{ $secretClear: true }` clears it; any other string replaces it.
13291
+ */
13249
13292
  /** The most instances one addon lists (an integrations page, not a table). */
13250
13293
  var INSTANCES_LIST_MAX = 1e3;
13251
13294
  /** The keys of the kind's secret fields that sit at the top level of the body. */
13252
13295
  function topLevelSecretKeys(paths) {
13253
- return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
13296
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
13297
+ }
13298
+ /** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
13299
+ function secretMapKeys(paths) {
13300
+ return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
13301
+ }
13302
+ /**
13303
+ * A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
13304
+ * view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
13305
+ * or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
13306
+ */
13307
+ function secretMapPatch(value) {
13308
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
13309
+ const out = {};
13310
+ for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
13311
+ else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
13312
+ return Object.keys(out).length > 0 ? out : null;
13313
+ }
13314
+ /** Does the kind's body schema carry an `enabled` field? */
13315
+ function declaresEnabled(kind) {
13316
+ const json = toJSONSchema(kind.schema, {
13317
+ unrepresentable: "any",
13318
+ io: "input"
13319
+ });
13320
+ const properties = Reflect.get(json, "properties");
13321
+ return typeof properties === "object" && properties !== null && "enabled" in properties;
13254
13322
  }
13255
13323
  var RecordIntegrationInstances = class {
13256
13324
  records;
@@ -13258,14 +13326,20 @@ var RecordIntegrationInstances = class {
13258
13326
  hooks;
13259
13327
  handle;
13260
13328
  secretKeys;
13329
+ secretMaps;
13261
13330
  nameField;
13331
+ /** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
13332
+ registryOwnsEnabled;
13262
13333
  constructor(records, kind, hooks) {
13263
13334
  this.records = records;
13264
13335
  this.kind = kind;
13265
13336
  this.hooks = hooks;
13266
13337
  this.handle = records.kind(kind);
13267
13338
  this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13339
+ this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
13268
13340
  this.nameField = kind.declaration.entry?.nameField ?? "name";
13341
+ this.registryOwnsEnabled = hooks.description.role === "devices";
13342
+ 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"`);
13269
13343
  }
13270
13344
  /**
13271
13345
  * One instance read for the addon's own runtime; null when absent. When the keychain is
@@ -13316,7 +13390,7 @@ var RecordIntegrationInstances = class {
13316
13390
  id: record.id,
13317
13391
  name: typeof rawName === "string" ? rawName : record.id,
13318
13392
  configured: this.hooks.configured(record.data),
13319
- enabled: record.data.enabled !== false,
13393
+ ...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
13320
13394
  formValues: shown,
13321
13395
  ...kind !== void 0 ? { kind } : {},
13322
13396
  ...health?.status !== void 0 ? { status: health.status } : {},
@@ -13346,32 +13420,36 @@ var RecordIntegrationInstances = class {
13346
13420
  id: minted,
13347
13421
  data: this.hooks.initialBody(name, kind)
13348
13422
  }, { ifRev: 0 });
13349
- await this.hooks.onChanged?.(minted);
13423
+ await this.hooks.onChanged?.(minted, "created");
13350
13424
  return { id: minted };
13351
13425
  };
13352
13426
  updateInstance = async ({ id, name, values }) => {
13353
13427
  const patch = {};
13354
13428
  for (const [key, value] of Object.entries(values)) {
13355
13429
  if (value === void 0) continue;
13356
- if (!this.secretKeys.has(key)) patch[key] = value;
13430
+ if (this.secretMaps.has(key)) {
13431
+ const entries = secretMapPatch(value);
13432
+ if (entries !== null) patch[key] = entries;
13433
+ } else if (!this.secretKeys.has(key)) patch[key] = value;
13357
13434
  else if (isSecretClear(value)) patch[key] = null;
13358
13435
  else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13359
13436
  }
13360
13437
  if (name !== void 0) patch[this.nameField] = name;
13361
13438
  await this.handle.patch(id, patch);
13362
- await this.hooks.onChanged?.(id);
13439
+ await this.hooks.onChanged?.(id, "updated");
13363
13440
  return { ok: true };
13364
13441
  };
13365
13442
  /** The switch lives in the record body; the addon is told so it applies it (D996). */
13366
13443
  setInstanceEnabled = async ({ id, enabled }) => {
13444
+ if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
13367
13445
  await this.handle.patch(id, { enabled });
13368
- await this.hooks.onChanged?.(id);
13446
+ await this.hooks.onChanged?.(id, "enabled");
13369
13447
  return { ok: true };
13370
13448
  };
13371
13449
  /** The row and its keychain refs go together (the records door). */
13372
13450
  deleteInstance = async ({ id }) => {
13373
13451
  await this.handle.delete(id);
13374
- await this.hooks.onChanged?.(id);
13452
+ await this.hooks.onChanged?.(id, "deleted");
13375
13453
  return { ok: true };
13376
13454
  };
13377
13455
  testInstance = async (input) => {
@@ -14432,7 +14510,13 @@ var MigrationLedgerEntrySchema = object({
14432
14510
  * joined by the engine from the catalogues it knows this boot. Absent when no
14433
14511
  * catalogue lists the id (a newer database).
14434
14512
  */
14435
- meta: MigrationMetaSchema.optional()
14513
+ meta: MigrationMetaSchema.optional(),
14514
+ /**
14515
+ * Set when this is an applied row of an addon owner whose catalogue the answering
14516
+ * process has not loaded (the owner has not declared yet): the row is applied, the
14517
+ * process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
14518
+ */
14519
+ ownerNotLoaded: boolean().optional()
14436
14520
  });
14437
14521
  /** A catalogued migration that is not applied, with where it stands. */
14438
14522
  var MigrationPendingEntrySchema = object({
@@ -50167,6 +50251,9 @@ var RunningProposalEvidenceSchema = object({
50167
50251
  runsFound: number().int().nonnegative(),
50168
50252
  pausesFound: number().int().nonnegative(),
50169
50253
  longestPauseMs: number().nonnegative(),
50254
+ /** Episodes shorter than ~90 s (an anti-crease pulse, a display waking): never a run, never a pause. */
50255
+ pulsesFound: number().int().nonnegative(),
50256
+ longestPulseMs: number().nonnegative(),
50170
50257
  idleFloorW: number(),
50171
50258
  noiseCeilingW: number(),
50172
50259
  plateauW: number(),
@@ -56012,11 +56099,7 @@ var CreateIntegrationInputSchema = object({
56012
56099
  info: record(string$2(), unknown()).optional(),
56013
56100
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
56014
56101
  originStableId: string$2().optional(),
56015
- settings: record(string$2(), unknown()).optional(),
56016
- /** Keys of `settings` the form's schema declares secret (by type) — kept in
56017
- * the keychain, not the table (D787). */
56018
- secretKeys: array(string$2()).optional(),
56019
- skipRestart: boolean().optional()
56102
+ settings: record(string$2(), unknown()).optional()
56020
56103
  });
56021
56104
  var UpdateIntegrationInputSchema = object({
56022
56105
  id: string$2(),
@@ -56037,16 +56120,6 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string$2
56037
56120
  }), method(object({ id: string$2() }), DeleteIntegrationResultSchema, {
56038
56121
  kind: "mutation",
56039
56122
  auth: "admin"
56040
- }), method(object({ id: string$2() }), record(string$2(), unknown()), { auth: "admin" }), method(object({
56041
- id: string$2(),
56042
- settings: record(string$2(), unknown()),
56043
- /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
56044
- secretKeys: array(string$2()).optional(),
56045
- /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
56046
- skipRestart: boolean().optional()
56047
- }), object({ success: boolean() }), {
56048
- kind: "mutation",
56049
- auth: "admin"
56050
56123
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
56051
56124
  addonId: string$2(),
56052
56125
  settings: record(string$2(), unknown())
@@ -56098,6 +56171,21 @@ var IntegrationInstanceKindSchema = object({
56098
56171
  disconnectPath: string$2().min(1).optional()
56099
56172
  });
56100
56173
  var IntegrationInstanceDescriptionSchema = object({
56174
+ /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
56175
+ role: _enum([
56176
+ "devices",
56177
+ "services",
56178
+ "both"
56179
+ ]),
56180
+ /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
56181
+ discovery: object({
56182
+ mode: _enum([
56183
+ "none",
56184
+ "lan",
56185
+ "account"
56186
+ ]),
56187
+ scopedToInstance: boolean()
56188
+ }).optional(),
56101
56189
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
56102
56190
  instanceMode: IntegrationInstanceModeSchema,
56103
56191
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
@@ -56137,10 +56225,11 @@ var IntegrationInstanceSchema = object({
56137
56225
  /** The addon's own rule (an URL is present, a client id is present...). */
56138
56226
  configured: boolean(),
56139
56227
  /**
56140
- * The instance's own on/off switch (D996): always present, derived from the record body
56141
- * (`enabled !== false`, so a row written before the field existed is on).
56228
+ * The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
56229
+ * so a row written before the field existed is on). Absent when the `integrations` registry
56230
+ * owns the switch (`describe().role` is `devices`): read it from the registry row.
56142
56231
  */
56143
- enabled: boolean(),
56232
+ enabled: boolean().optional(),
56144
56233
  /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
56145
56234
  kind: string$2().optional(),
56146
56235
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
@@ -59007,6 +59096,27 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
59007
59096
  }), {
59008
59097
  kind: "mutation",
59009
59098
  auth: "admin"
59099
+ }), method(object({ integrationId: string$2().min(1) }), record(string$2(), unknown()), { auth: "admin" }), method(object({ integrationId: string$2().min(1) }), object({
59100
+ rows: number().int().nonnegative(),
59101
+ secrets: number().int().nonnegative()
59102
+ }), {
59103
+ kind: "mutation",
59104
+ auth: "admin"
59105
+ }), method(object({
59106
+ integrationId: string$2().min(1),
59107
+ patch: record(string$2(), unknown())
59108
+ }), object({ written: boolean() }), {
59109
+ kind: "mutation",
59110
+ auth: "admin"
59111
+ }), method(object({
59112
+ integrationId: string$2().min(1),
59113
+ keys: array(string$2().min(1))
59114
+ }), object({
59115
+ present: boolean(),
59116
+ removed: record(string$2(), unknown())
59117
+ }), {
59118
+ kind: "mutation",
59119
+ auth: "admin"
59010
59120
  });
59011
59121
  method(object({
59012
59122
  sourceUrl: string$2(),
@@ -80333,11 +80443,15 @@ discriminatedUnion("kind", [object({
80333
80443
  * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
80334
80444
  * near F. A device that is not near one level most of the time has no floor.
80335
80445
  * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
80336
- * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
80337
- * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
80446
+ * 3. an episode shorter than `PULSE_MAX_MS` is a PULSE (an anti-crease tumble, a
80447
+ * display waking): it is neither a run nor a pause. Among the remaining
80448
+ * episodes, gaps over `PAUSE_MAX_MS` always separate runs; the rest split in
80449
+ * two by the largest log-scale jump (>= 3x): the short ones are pauses INSIDE
80450
+ * a run (a soak), the long ones separate runs. A stretch under `MIN_RUN_MS`
80451
+ * is no run.
80338
80452
  * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
80339
- * grid a person would type; stop dwell covers the longest pause; start dwell
80340
- * covers the longest standby blip above T.
80453
+ * grid a person would type; stop dwell covers the longest pause and the
80454
+ * kind's floor; start dwell covers the longest pulse and standby blip above T.
80341
80455
  * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
80342
80456
  * run must hold exactly one true interval and nothing may flap outside.
80343
80457
  */
@@ -80348,6 +80462,10 @@ var MINUTE_MS$1 = 60 * SECOND_MS;
80348
80462
  30 * SECOND_MS;
80349
80463
  60 * SECOND_MS;
80350
80464
  20 * MINUTE_MS$1;
80465
+ 90 * SECOND_MS;
80466
+ 30 * MINUTE_MS$1;
80467
+ 10 * MINUTE_MS$1;
80468
+ DeviceType.Dryer, 3 * MINUTE_MS$1, DeviceType.Dishwasher, 3 * MINUTE_MS$1, DeviceType.Washer, 10 * MINUTE_MS$1;
80351
80469
  10 * SECOND_MS;
80352
80470
  10 * SECOND_MS;
80353
80471
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
@@ -85667,13 +85785,6 @@ Object.freeze({
85667
85785
  access: "view",
85668
85786
  role: "admin"
85669
85787
  },
85670
- "integrations.getSettings": {
85671
- capName: "integrations",
85672
- capScope: "system",
85673
- addonId: null,
85674
- access: "view",
85675
- role: "admin"
85676
- },
85677
85788
  "integrations.list": {
85678
85789
  capName: "integrations",
85679
85790
  capScope: "system",
@@ -85681,13 +85792,6 @@ Object.freeze({
85681
85792
  access: "view",
85682
85793
  role: "admin"
85683
85794
  },
85684
- "integrations.setSettings": {
85685
- capName: "integrations",
85686
- capScope: "system",
85687
- addonId: null,
85688
- access: "create",
85689
- role: "admin"
85690
- },
85691
85795
  "integrations.testConnection": {
85692
85796
  capName: "integrations",
85693
85797
  capScope: "system",
@@ -86682,6 +86786,13 @@ Object.freeze({
86682
86786
  access: "view",
86683
86787
  role: "admin"
86684
86788
  },
86789
+ "migrations.clearLegacyIntegrationRows": {
86790
+ capName: "migrations",
86791
+ capScope: "system",
86792
+ addonId: null,
86793
+ access: "delete",
86794
+ role: "admin"
86795
+ },
86685
86796
  "migrations.declareOwner": {
86686
86797
  capName: "migrations",
86687
86798
  capScope: "system",
@@ -86689,6 +86800,13 @@ Object.freeze({
86689
86800
  access: "create",
86690
86801
  role: "admin"
86691
86802
  },
86803
+ "migrations.deleteLegacyIntegrationRowKeys": {
86804
+ capName: "migrations",
86805
+ capScope: "system",
86806
+ addonId: null,
86807
+ access: "delete",
86808
+ role: "admin"
86809
+ },
86692
86810
  "migrations.dryRun": {
86693
86811
  capName: "migrations",
86694
86812
  capScope: "system",
@@ -86724,6 +86842,20 @@ Object.freeze({
86724
86842
  access: "create",
86725
86843
  role: "admin"
86726
86844
  },
86845
+ "migrations.patchLegacyIntegrationRows": {
86846
+ capName: "migrations",
86847
+ capScope: "system",
86848
+ addonId: null,
86849
+ access: "create",
86850
+ role: "admin"
86851
+ },
86852
+ "migrations.readLegacyIntegrationRows": {
86853
+ capName: "migrations",
86854
+ capScope: "system",
86855
+ addonId: null,
86856
+ access: "view",
86857
+ role: "admin"
86858
+ },
86727
86859
  "migrations.restorePreMigration": {
86728
86860
  capName: "migrations",
86729
86861
  capScope: "system",
@@ -208590,6 +208722,7 @@ function statusOf(info) {
208590
208722
  function buildMatterInstances(records, deps) {
208591
208723
  return new RecordIntegrationInstances(records, matterBrokersEntryKind, {
208592
208724
  description: {
208725
+ role: "both",
208593
208726
  instanceMode: "multiple",
208594
208727
  kind: "connection",
208595
208728
  canTest: true,
@@ -209009,18 +209142,10 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
209009
209142
  try {
209010
209143
  const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id);
209011
209144
  const surviving = new Set(mine.map((i) => i.id));
209012
- for (const integration of mine) {
209013
- const settings = await this.ctx.api.integrations.getSettings.query({ id: integration.id });
209014
- const brokerId = typeof settings["brokerId"] === "string" ? settings["brokerId"] : void 0;
209015
- if (brokerId === void 0) continue;
209016
- if (!this.activeBrokers.some((b) => b.id === brokerId)) {
209017
- this.ctx.logger.warn("Matter integration→broker: linked broker not found", { meta: {
209018
- integrationId: integration.id,
209019
- brokerId
209020
- } });
209021
- continue;
209022
- }
209023
- this.requireRegistry().linkIntegration(integration.id, brokerId);
209145
+ for (const broker of this.activeBrokers) {
209146
+ const integrationId = broker.integrationId;
209147
+ if (integrationId === void 0 || !surviving.has(integrationId)) continue;
209148
+ this.requireRegistry().linkIntegration(integrationId, broker.id);
209024
209149
  }
209025
209150
  const toRemove = this.activeBrokers.filter((b) => b.integrationId !== void 0 && !surviving.has(b.integrationId)).map((b) => b.id);
209026
209151
  if (toRemove.length === 0) return;
@@ -209061,9 +209186,9 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
209061
209186
  },
209062
209187
  logger: this.ctx.logger,
209063
209188
  getBrokerIdForIntegration: async (id) => {
209064
- const brokerId = (await this.ctx.api.integrations.getSettings.query({ id }))["brokerId"];
209065
- if (typeof brokerId !== "string") throw new Error(`integration ${id} has no linked brokerId`);
209066
- return brokerId;
209189
+ const broker = this.activeBrokers.find((entry) => entry.integrationId === id);
209190
+ if (broker === void 0) throw new Error(`integration ${id} has no linked brokerId`);
209191
+ return broker.id;
209067
209192
  },
209068
209193
  listAdoptedMatter: async () => {
209069
209194
  const reg = this.ctx.kernel.deviceRegistry;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-matter-broker",
3
- "version": "0.2.155",
3
+ "version": "0.2.157",
4
4
  "description": "Matter broker addon for CamStack — owns a Matter fabric (commissioning + the long-lived controller) via the matter.js controller and brokers commissioned Matter nodes into CamStack",
5
5
  "keywords": [
6
6
  "camstack",
@@ -65,7 +65,7 @@
65
65
  }
66
66
  ],
67
67
  "requires": {
68
- "server": ">=1.2.761"
68
+ "server": ">=1.2.770"
69
69
  }
70
70
  },
71
71
  "files": [