@camstack/addon-matter-broker 0.2.155 → 0.2.156
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/addon.js +103 -14
- package/dist/addon.mjs +103 -14
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.156] - 2026-11-04
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- The integration states its role (devices, services or both) to the Integrations page
|
|
15
|
+
|
|
10
16
|
## [0.2.155] - 2026-11-04
|
|
11
17
|
|
|
12
18
|
### 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-
|
|
5998
|
+
//#region ../types/dist/sleep-BaWhXZFm.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).
|
|
@@ -8831,6 +8831,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
|
8831
8831
|
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
8832
8832
|
* (`auth.accessToken`, `headers[].value`).
|
|
8833
8833
|
*
|
|
8834
|
+
* `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
|
|
8835
|
+
* names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
|
|
8836
|
+
* key releases exactly that key's secret.
|
|
8837
|
+
*
|
|
8834
8838
|
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
8835
8839
|
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
8836
8840
|
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
@@ -8843,11 +8847,13 @@ function parseSecretPath(path) {
|
|
|
8843
8847
|
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
8844
8848
|
return path.split(".").map((raw) => {
|
|
8845
8849
|
const each = raw.endsWith("[]");
|
|
8846
|
-
const
|
|
8850
|
+
const map = raw.endsWith("{}");
|
|
8851
|
+
const key = each || map ? raw.slice(0, -2) : raw;
|
|
8847
8852
|
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
8848
8853
|
return {
|
|
8849
8854
|
key,
|
|
8850
|
-
each
|
|
8855
|
+
each,
|
|
8856
|
+
map
|
|
8851
8857
|
};
|
|
8852
8858
|
});
|
|
8853
8859
|
}
|
|
@@ -8899,6 +8905,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
|
|
|
8899
8905
|
[segment.key]: mapped
|
|
8900
8906
|
};
|
|
8901
8907
|
}
|
|
8908
|
+
if (segment.map) {
|
|
8909
|
+
if (!isRecord$4$1(child)) return node;
|
|
8910
|
+
const kept = Object.entries(child).map(([mapKey, value]) => {
|
|
8911
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
|
|
8912
|
+
if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
|
|
8913
|
+
return [mapKey, visit({
|
|
8914
|
+
declared,
|
|
8915
|
+
concrete,
|
|
8916
|
+
value
|
|
8917
|
+
})];
|
|
8918
|
+
}).filter(([, value]) => value !== REMOVE_LEAF);
|
|
8919
|
+
return {
|
|
8920
|
+
...node,
|
|
8921
|
+
[segment.key]: Object.fromEntries(kept)
|
|
8922
|
+
};
|
|
8923
|
+
}
|
|
8902
8924
|
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
8903
8925
|
if (last) {
|
|
8904
8926
|
const replacement = visit({
|
|
@@ -9040,7 +9062,8 @@ function isZodSecret(schema) {
|
|
|
9040
9062
|
}
|
|
9041
9063
|
/**
|
|
9042
9064
|
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
9043
|
-
* element of this array" (`connection.password`, `rows[].token`)
|
|
9065
|
+
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
9066
|
+
* map" (`applianceSecrets{}`). A declared
|
|
9044
9067
|
* object or array is reported as one path and not descended into — everything
|
|
9045
9068
|
* under it is secret.
|
|
9046
9069
|
*/
|
|
@@ -9051,6 +9074,7 @@ function collectZodSecretPaths(schema, prefix = "") {
|
|
|
9051
9074
|
const def = defOf(layer);
|
|
9052
9075
|
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
9053
9076
|
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
9077
|
+
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
9054
9078
|
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
9055
9079
|
else if (def.type === "intersection") {
|
|
9056
9080
|
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
@@ -13248,11 +13272,49 @@ var IntegrationInstanceRefSchema = object({
|
|
|
13248
13272
|
]),
|
|
13249
13273
|
candidateStableId: string$2().optional()
|
|
13250
13274
|
});
|
|
13275
|
+
/**
|
|
13276
|
+
* `RecordIntegrationInstances` - the `integration-instances` provider over ONE
|
|
13277
|
+
* entry kind, so an addon that holds a connection serves it in ~30 lines.
|
|
13278
|
+
*
|
|
13279
|
+
* Every instance is one record of the addon's own kind, keyed by the
|
|
13280
|
+
* `integrations` registry id. The records door does the secret work: a field the
|
|
13281
|
+
* kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
|
|
13282
|
+
* deleting the row deletes the secret, and one row's value is never another's.
|
|
13283
|
+
*
|
|
13284
|
+
* Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
|
|
13285
|
+
* token or the view the form was given KEEP the stored secret;
|
|
13286
|
+
* `{ $secretClear: true }` clears it; any other string replaces it.
|
|
13287
|
+
*/
|
|
13251
13288
|
/** The most instances one addon lists (an integrations page, not a table). */
|
|
13252
13289
|
var INSTANCES_LIST_MAX = 1e3;
|
|
13253
13290
|
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
13254
13291
|
function topLevelSecretKeys(paths) {
|
|
13255
|
-
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
|
|
13292
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
|
|
13293
|
+
}
|
|
13294
|
+
/** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
|
|
13295
|
+
function secretMapKeys(paths) {
|
|
13296
|
+
return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
|
|
13297
|
+
}
|
|
13298
|
+
/**
|
|
13299
|
+
* A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
|
|
13300
|
+
* view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
|
|
13301
|
+
* or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
|
|
13302
|
+
*/
|
|
13303
|
+
function secretMapPatch(value) {
|
|
13304
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
13305
|
+
const out = {};
|
|
13306
|
+
for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
|
|
13307
|
+
else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
|
|
13308
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
13309
|
+
}
|
|
13310
|
+
/** Does the kind's body schema carry an `enabled` field? */
|
|
13311
|
+
function declaresEnabled(kind) {
|
|
13312
|
+
const json = toJSONSchema(kind.schema, {
|
|
13313
|
+
unrepresentable: "any",
|
|
13314
|
+
io: "input"
|
|
13315
|
+
});
|
|
13316
|
+
const properties = Reflect.get(json, "properties");
|
|
13317
|
+
return typeof properties === "object" && properties !== null && "enabled" in properties;
|
|
13256
13318
|
}
|
|
13257
13319
|
var RecordIntegrationInstances = class {
|
|
13258
13320
|
records;
|
|
@@ -13260,14 +13322,20 @@ var RecordIntegrationInstances = class {
|
|
|
13260
13322
|
hooks;
|
|
13261
13323
|
handle;
|
|
13262
13324
|
secretKeys;
|
|
13325
|
+
secretMaps;
|
|
13263
13326
|
nameField;
|
|
13327
|
+
/** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
|
|
13328
|
+
registryOwnsEnabled;
|
|
13264
13329
|
constructor(records, kind, hooks) {
|
|
13265
13330
|
this.records = records;
|
|
13266
13331
|
this.kind = kind;
|
|
13267
13332
|
this.hooks = hooks;
|
|
13268
13333
|
this.handle = records.kind(kind);
|
|
13269
13334
|
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
13335
|
+
this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
|
|
13270
13336
|
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
13337
|
+
this.registryOwnsEnabled = hooks.description.role === "devices";
|
|
13338
|
+
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
13339
|
}
|
|
13272
13340
|
/**
|
|
13273
13341
|
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
@@ -13318,7 +13386,7 @@ var RecordIntegrationInstances = class {
|
|
|
13318
13386
|
id: record.id,
|
|
13319
13387
|
name: typeof rawName === "string" ? rawName : record.id,
|
|
13320
13388
|
configured: this.hooks.configured(record.data),
|
|
13321
|
-
enabled: record.data.enabled !== false,
|
|
13389
|
+
...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
|
|
13322
13390
|
formValues: shown,
|
|
13323
13391
|
...kind !== void 0 ? { kind } : {},
|
|
13324
13392
|
...health?.status !== void 0 ? { status: health.status } : {},
|
|
@@ -13348,32 +13416,36 @@ var RecordIntegrationInstances = class {
|
|
|
13348
13416
|
id: minted,
|
|
13349
13417
|
data: this.hooks.initialBody(name, kind)
|
|
13350
13418
|
}, { ifRev: 0 });
|
|
13351
|
-
await this.hooks.onChanged?.(minted);
|
|
13419
|
+
await this.hooks.onChanged?.(minted, "created");
|
|
13352
13420
|
return { id: minted };
|
|
13353
13421
|
};
|
|
13354
13422
|
updateInstance = async ({ id, name, values }) => {
|
|
13355
13423
|
const patch = {};
|
|
13356
13424
|
for (const [key, value] of Object.entries(values)) {
|
|
13357
13425
|
if (value === void 0) continue;
|
|
13358
|
-
if (
|
|
13426
|
+
if (this.secretMaps.has(key)) {
|
|
13427
|
+
const entries = secretMapPatch(value);
|
|
13428
|
+
if (entries !== null) patch[key] = entries;
|
|
13429
|
+
} else if (!this.secretKeys.has(key)) patch[key] = value;
|
|
13359
13430
|
else if (isSecretClear(value)) patch[key] = null;
|
|
13360
13431
|
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13361
13432
|
}
|
|
13362
13433
|
if (name !== void 0) patch[this.nameField] = name;
|
|
13363
13434
|
await this.handle.patch(id, patch);
|
|
13364
|
-
await this.hooks.onChanged?.(id);
|
|
13435
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
13365
13436
|
return { ok: true };
|
|
13366
13437
|
};
|
|
13367
13438
|
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13368
13439
|
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13440
|
+
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
13441
|
await this.handle.patch(id, { enabled });
|
|
13370
|
-
await this.hooks.onChanged?.(id);
|
|
13442
|
+
await this.hooks.onChanged?.(id, "enabled");
|
|
13371
13443
|
return { ok: true };
|
|
13372
13444
|
};
|
|
13373
13445
|
/** The row and its keychain refs go together (the records door). */
|
|
13374
13446
|
deleteInstance = async ({ id }) => {
|
|
13375
13447
|
await this.handle.delete(id);
|
|
13376
|
-
await this.hooks.onChanged?.(id);
|
|
13448
|
+
await this.hooks.onChanged?.(id, "deleted");
|
|
13377
13449
|
return { ok: true };
|
|
13378
13450
|
};
|
|
13379
13451
|
testInstance = async (input) => {
|
|
@@ -56100,6 +56172,21 @@ var IntegrationInstanceKindSchema = object({
|
|
|
56100
56172
|
disconnectPath: string$2().min(1).optional()
|
|
56101
56173
|
});
|
|
56102
56174
|
var IntegrationInstanceDescriptionSchema = object({
|
|
56175
|
+
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
56176
|
+
role: _enum([
|
|
56177
|
+
"devices",
|
|
56178
|
+
"services",
|
|
56179
|
+
"both"
|
|
56180
|
+
]),
|
|
56181
|
+
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
56182
|
+
discovery: object({
|
|
56183
|
+
mode: _enum([
|
|
56184
|
+
"none",
|
|
56185
|
+
"lan",
|
|
56186
|
+
"account"
|
|
56187
|
+
]),
|
|
56188
|
+
scopedToInstance: boolean()
|
|
56189
|
+
}).optional(),
|
|
56103
56190
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
56104
56191
|
instanceMode: IntegrationInstanceModeSchema,
|
|
56105
56192
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
@@ -56139,10 +56226,11 @@ var IntegrationInstanceSchema = object({
|
|
|
56139
56226
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
56140
56227
|
configured: boolean(),
|
|
56141
56228
|
/**
|
|
56142
|
-
* The instance's own on/off switch (D996)
|
|
56143
|
-
*
|
|
56229
|
+
* The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
|
|
56230
|
+
* so a row written before the field existed is on). Absent when the `integrations` registry
|
|
56231
|
+
* owns the switch (`describe().role` is `devices`): read it from the registry row.
|
|
56144
56232
|
*/
|
|
56145
|
-
enabled: boolean(),
|
|
56233
|
+
enabled: boolean().optional(),
|
|
56146
56234
|
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
56147
56235
|
kind: string$2().optional(),
|
|
56148
56236
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
@@ -208592,6 +208680,7 @@ function statusOf(info) {
|
|
|
208592
208680
|
function buildMatterInstances(records, deps) {
|
|
208593
208681
|
return new RecordIntegrationInstances(records, matterBrokersEntryKind, {
|
|
208594
208682
|
description: {
|
|
208683
|
+
role: "both",
|
|
208595
208684
|
instanceMode: "multiple",
|
|
208596
208685
|
kind: "connection",
|
|
208597
208686
|
canTest: true,
|
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-
|
|
5996
|
+
//#region ../types/dist/sleep-BaWhXZFm.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).
|
|
@@ -8829,6 +8829,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
|
8829
8829
|
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
8830
8830
|
* (`auth.accessToken`, `headers[].value`).
|
|
8831
8831
|
*
|
|
8832
|
+
* `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
|
|
8833
|
+
* names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
|
|
8834
|
+
* key releases exactly that key's secret.
|
|
8835
|
+
*
|
|
8832
8836
|
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
8833
8837
|
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
8834
8838
|
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
@@ -8841,11 +8845,13 @@ function parseSecretPath(path) {
|
|
|
8841
8845
|
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
8842
8846
|
return path.split(".").map((raw) => {
|
|
8843
8847
|
const each = raw.endsWith("[]");
|
|
8844
|
-
const
|
|
8848
|
+
const map = raw.endsWith("{}");
|
|
8849
|
+
const key = each || map ? raw.slice(0, -2) : raw;
|
|
8845
8850
|
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
8846
8851
|
return {
|
|
8847
8852
|
key,
|
|
8848
|
-
each
|
|
8853
|
+
each,
|
|
8854
|
+
map
|
|
8849
8855
|
};
|
|
8850
8856
|
});
|
|
8851
8857
|
}
|
|
@@ -8897,6 +8903,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
|
|
|
8897
8903
|
[segment.key]: mapped
|
|
8898
8904
|
};
|
|
8899
8905
|
}
|
|
8906
|
+
if (segment.map) {
|
|
8907
|
+
if (!isRecord$4$1(child)) return node;
|
|
8908
|
+
const kept = Object.entries(child).map(([mapKey, value]) => {
|
|
8909
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
|
|
8910
|
+
if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
|
|
8911
|
+
return [mapKey, visit({
|
|
8912
|
+
declared,
|
|
8913
|
+
concrete,
|
|
8914
|
+
value
|
|
8915
|
+
})];
|
|
8916
|
+
}).filter(([, value]) => value !== REMOVE_LEAF);
|
|
8917
|
+
return {
|
|
8918
|
+
...node,
|
|
8919
|
+
[segment.key]: Object.fromEntries(kept)
|
|
8920
|
+
};
|
|
8921
|
+
}
|
|
8900
8922
|
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
8901
8923
|
if (last) {
|
|
8902
8924
|
const replacement = visit({
|
|
@@ -9038,7 +9060,8 @@ function isZodSecret(schema) {
|
|
|
9038
9060
|
}
|
|
9039
9061
|
/**
|
|
9040
9062
|
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
9041
|
-
* element of this array" (`connection.password`, `rows[].token`)
|
|
9063
|
+
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
9064
|
+
* map" (`applianceSecrets{}`). A declared
|
|
9042
9065
|
* object or array is reported as one path and not descended into — everything
|
|
9043
9066
|
* under it is secret.
|
|
9044
9067
|
*/
|
|
@@ -9049,6 +9072,7 @@ function collectZodSecretPaths(schema, prefix = "") {
|
|
|
9049
9072
|
const def = defOf(layer);
|
|
9050
9073
|
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
9051
9074
|
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
9075
|
+
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
9052
9076
|
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
9053
9077
|
else if (def.type === "intersection") {
|
|
9054
9078
|
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
@@ -13246,11 +13270,49 @@ var IntegrationInstanceRefSchema = object({
|
|
|
13246
13270
|
]),
|
|
13247
13271
|
candidateStableId: string$2().optional()
|
|
13248
13272
|
});
|
|
13273
|
+
/**
|
|
13274
|
+
* `RecordIntegrationInstances` - the `integration-instances` provider over ONE
|
|
13275
|
+
* entry kind, so an addon that holds a connection serves it in ~30 lines.
|
|
13276
|
+
*
|
|
13277
|
+
* Every instance is one record of the addon's own kind, keyed by the
|
|
13278
|
+
* `integrations` registry id. The records door does the secret work: a field the
|
|
13279
|
+
* kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
|
|
13280
|
+
* deleting the row deletes the secret, and one row's value is never another's.
|
|
13281
|
+
*
|
|
13282
|
+
* Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
|
|
13283
|
+
* token or the view the form was given KEEP the stored secret;
|
|
13284
|
+
* `{ $secretClear: true }` clears it; any other string replaces it.
|
|
13285
|
+
*/
|
|
13249
13286
|
/** The most instances one addon lists (an integrations page, not a table). */
|
|
13250
13287
|
var INSTANCES_LIST_MAX = 1e3;
|
|
13251
13288
|
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
13252
13289
|
function topLevelSecretKeys(paths) {
|
|
13253
|
-
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
|
|
13290
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
|
|
13291
|
+
}
|
|
13292
|
+
/** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
|
|
13293
|
+
function secretMapKeys(paths) {
|
|
13294
|
+
return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
|
|
13295
|
+
}
|
|
13296
|
+
/**
|
|
13297
|
+
* A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
|
|
13298
|
+
* view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
|
|
13299
|
+
* or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
|
|
13300
|
+
*/
|
|
13301
|
+
function secretMapPatch(value) {
|
|
13302
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
13303
|
+
const out = {};
|
|
13304
|
+
for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
|
|
13305
|
+
else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
|
|
13306
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
13307
|
+
}
|
|
13308
|
+
/** Does the kind's body schema carry an `enabled` field? */
|
|
13309
|
+
function declaresEnabled(kind) {
|
|
13310
|
+
const json = toJSONSchema(kind.schema, {
|
|
13311
|
+
unrepresentable: "any",
|
|
13312
|
+
io: "input"
|
|
13313
|
+
});
|
|
13314
|
+
const properties = Reflect.get(json, "properties");
|
|
13315
|
+
return typeof properties === "object" && properties !== null && "enabled" in properties;
|
|
13254
13316
|
}
|
|
13255
13317
|
var RecordIntegrationInstances = class {
|
|
13256
13318
|
records;
|
|
@@ -13258,14 +13320,20 @@ var RecordIntegrationInstances = class {
|
|
|
13258
13320
|
hooks;
|
|
13259
13321
|
handle;
|
|
13260
13322
|
secretKeys;
|
|
13323
|
+
secretMaps;
|
|
13261
13324
|
nameField;
|
|
13325
|
+
/** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
|
|
13326
|
+
registryOwnsEnabled;
|
|
13262
13327
|
constructor(records, kind, hooks) {
|
|
13263
13328
|
this.records = records;
|
|
13264
13329
|
this.kind = kind;
|
|
13265
13330
|
this.hooks = hooks;
|
|
13266
13331
|
this.handle = records.kind(kind);
|
|
13267
13332
|
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
13333
|
+
this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
|
|
13268
13334
|
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
13335
|
+
this.registryOwnsEnabled = hooks.description.role === "devices";
|
|
13336
|
+
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
13337
|
}
|
|
13270
13338
|
/**
|
|
13271
13339
|
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
@@ -13316,7 +13384,7 @@ var RecordIntegrationInstances = class {
|
|
|
13316
13384
|
id: record.id,
|
|
13317
13385
|
name: typeof rawName === "string" ? rawName : record.id,
|
|
13318
13386
|
configured: this.hooks.configured(record.data),
|
|
13319
|
-
enabled: record.data.enabled !== false,
|
|
13387
|
+
...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
|
|
13320
13388
|
formValues: shown,
|
|
13321
13389
|
...kind !== void 0 ? { kind } : {},
|
|
13322
13390
|
...health?.status !== void 0 ? { status: health.status } : {},
|
|
@@ -13346,32 +13414,36 @@ var RecordIntegrationInstances = class {
|
|
|
13346
13414
|
id: minted,
|
|
13347
13415
|
data: this.hooks.initialBody(name, kind)
|
|
13348
13416
|
}, { ifRev: 0 });
|
|
13349
|
-
await this.hooks.onChanged?.(minted);
|
|
13417
|
+
await this.hooks.onChanged?.(minted, "created");
|
|
13350
13418
|
return { id: minted };
|
|
13351
13419
|
};
|
|
13352
13420
|
updateInstance = async ({ id, name, values }) => {
|
|
13353
13421
|
const patch = {};
|
|
13354
13422
|
for (const [key, value] of Object.entries(values)) {
|
|
13355
13423
|
if (value === void 0) continue;
|
|
13356
|
-
if (
|
|
13424
|
+
if (this.secretMaps.has(key)) {
|
|
13425
|
+
const entries = secretMapPatch(value);
|
|
13426
|
+
if (entries !== null) patch[key] = entries;
|
|
13427
|
+
} else if (!this.secretKeys.has(key)) patch[key] = value;
|
|
13357
13428
|
else if (isSecretClear(value)) patch[key] = null;
|
|
13358
13429
|
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13359
13430
|
}
|
|
13360
13431
|
if (name !== void 0) patch[this.nameField] = name;
|
|
13361
13432
|
await this.handle.patch(id, patch);
|
|
13362
|
-
await this.hooks.onChanged?.(id);
|
|
13433
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
13363
13434
|
return { ok: true };
|
|
13364
13435
|
};
|
|
13365
13436
|
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13366
13437
|
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13438
|
+
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
13439
|
await this.handle.patch(id, { enabled });
|
|
13368
|
-
await this.hooks.onChanged?.(id);
|
|
13440
|
+
await this.hooks.onChanged?.(id, "enabled");
|
|
13369
13441
|
return { ok: true };
|
|
13370
13442
|
};
|
|
13371
13443
|
/** The row and its keychain refs go together (the records door). */
|
|
13372
13444
|
deleteInstance = async ({ id }) => {
|
|
13373
13445
|
await this.handle.delete(id);
|
|
13374
|
-
await this.hooks.onChanged?.(id);
|
|
13446
|
+
await this.hooks.onChanged?.(id, "deleted");
|
|
13375
13447
|
return { ok: true };
|
|
13376
13448
|
};
|
|
13377
13449
|
testInstance = async (input) => {
|
|
@@ -56098,6 +56170,21 @@ var IntegrationInstanceKindSchema = object({
|
|
|
56098
56170
|
disconnectPath: string$2().min(1).optional()
|
|
56099
56171
|
});
|
|
56100
56172
|
var IntegrationInstanceDescriptionSchema = object({
|
|
56173
|
+
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
56174
|
+
role: _enum([
|
|
56175
|
+
"devices",
|
|
56176
|
+
"services",
|
|
56177
|
+
"both"
|
|
56178
|
+
]),
|
|
56179
|
+
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
56180
|
+
discovery: object({
|
|
56181
|
+
mode: _enum([
|
|
56182
|
+
"none",
|
|
56183
|
+
"lan",
|
|
56184
|
+
"account"
|
|
56185
|
+
]),
|
|
56186
|
+
scopedToInstance: boolean()
|
|
56187
|
+
}).optional(),
|
|
56101
56188
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
56102
56189
|
instanceMode: IntegrationInstanceModeSchema,
|
|
56103
56190
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
@@ -56137,10 +56224,11 @@ var IntegrationInstanceSchema = object({
|
|
|
56137
56224
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
56138
56225
|
configured: boolean(),
|
|
56139
56226
|
/**
|
|
56140
|
-
* The instance's own on/off switch (D996)
|
|
56141
|
-
*
|
|
56227
|
+
* The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
|
|
56228
|
+
* so a row written before the field existed is on). Absent when the `integrations` registry
|
|
56229
|
+
* owns the switch (`describe().role` is `devices`): read it from the registry row.
|
|
56142
56230
|
*/
|
|
56143
|
-
enabled: boolean(),
|
|
56231
|
+
enabled: boolean().optional(),
|
|
56144
56232
|
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
56145
56233
|
kind: string$2().optional(),
|
|
56146
56234
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
@@ -208590,6 +208678,7 @@ function statusOf(info) {
|
|
|
208590
208678
|
function buildMatterInstances(records, deps) {
|
|
208591
208679
|
return new RecordIntegrationInstances(records, matterBrokersEntryKind, {
|
|
208592
208680
|
description: {
|
|
208681
|
+
role: "both",
|
|
208593
208682
|
instanceMode: "multiple",
|
|
208594
208683
|
kind: "connection",
|
|
208595
208684
|
canTest: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-matter-broker",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.156",
|
|
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.
|
|
68
|
+
"server": ">=1.2.762"
|
|
69
69
|
}
|
|
70
70
|
},
|
|
71
71
|
"files": [
|