@camstack/addon-import-alexa 0.2.183 → 0.2.184
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.184] - 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.183] - 2026-11-04
|
|
11
17
|
|
|
12
18
|
### Changed
|
package/dist/addon.js
CHANGED
|
@@ -6044,7 +6044,7 @@ function number(params) {
|
|
|
6044
6044
|
return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
|
|
6045
6045
|
}
|
|
6046
6046
|
//#endregion
|
|
6047
|
-
//#region ../types/dist/sleep-
|
|
6047
|
+
//#region ../types/dist/sleep-BaWhXZFm.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).
|
|
@@ -8880,6 +8880,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
|
8880
8880
|
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
8881
8881
|
* (`auth.accessToken`, `headers[].value`).
|
|
8882
8882
|
*
|
|
8883
|
+
* `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
|
|
8884
|
+
* names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
|
|
8885
|
+
* key releases exactly that key's secret.
|
|
8886
|
+
*
|
|
8883
8887
|
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
8884
8888
|
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
8885
8889
|
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
@@ -8892,11 +8896,13 @@ function parseSecretPath(path) {
|
|
|
8892
8896
|
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
8893
8897
|
return path.split(".").map((raw) => {
|
|
8894
8898
|
const each = raw.endsWith("[]");
|
|
8895
|
-
const
|
|
8899
|
+
const map = raw.endsWith("{}");
|
|
8900
|
+
const key = each || map ? raw.slice(0, -2) : raw;
|
|
8896
8901
|
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
8897
8902
|
return {
|
|
8898
8903
|
key,
|
|
8899
|
-
each
|
|
8904
|
+
each,
|
|
8905
|
+
map
|
|
8900
8906
|
};
|
|
8901
8907
|
});
|
|
8902
8908
|
}
|
|
@@ -8948,6 +8954,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
|
|
|
8948
8954
|
[segment.key]: mapped
|
|
8949
8955
|
};
|
|
8950
8956
|
}
|
|
8957
|
+
if (segment.map) {
|
|
8958
|
+
if (!isRecord$4$1(child)) return node;
|
|
8959
|
+
const kept = Object.entries(child).map(([mapKey, value]) => {
|
|
8960
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
|
|
8961
|
+
if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
|
|
8962
|
+
return [mapKey, visit({
|
|
8963
|
+
declared,
|
|
8964
|
+
concrete,
|
|
8965
|
+
value
|
|
8966
|
+
})];
|
|
8967
|
+
}).filter(([, value]) => value !== REMOVE_LEAF);
|
|
8968
|
+
return {
|
|
8969
|
+
...node,
|
|
8970
|
+
[segment.key]: Object.fromEntries(kept)
|
|
8971
|
+
};
|
|
8972
|
+
}
|
|
8951
8973
|
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
8952
8974
|
if (last) {
|
|
8953
8975
|
const replacement = visit({
|
|
@@ -9089,7 +9111,8 @@ function isZodSecret(schema) {
|
|
|
9089
9111
|
}
|
|
9090
9112
|
/**
|
|
9091
9113
|
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
9092
|
-
* element of this array" (`connection.password`, `rows[].token`)
|
|
9114
|
+
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
9115
|
+
* map" (`applianceSecrets{}`). A declared
|
|
9093
9116
|
* object or array is reported as one path and not descended into — everything
|
|
9094
9117
|
* under it is secret.
|
|
9095
9118
|
*/
|
|
@@ -9100,6 +9123,7 @@ function collectZodSecretPaths(schema, prefix = "") {
|
|
|
9100
9123
|
const def = defOf(layer);
|
|
9101
9124
|
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
9102
9125
|
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
9126
|
+
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
9103
9127
|
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
9104
9128
|
else if (def.type === "intersection") {
|
|
9105
9129
|
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
@@ -13414,11 +13438,49 @@ function buildCapturingReply(envelope) {
|
|
|
13414
13438
|
};
|
|
13415
13439
|
return wrapper;
|
|
13416
13440
|
}
|
|
13441
|
+
/**
|
|
13442
|
+
* `RecordIntegrationInstances` - the `integration-instances` provider over ONE
|
|
13443
|
+
* entry kind, so an addon that holds a connection serves it in ~30 lines.
|
|
13444
|
+
*
|
|
13445
|
+
* Every instance is one record of the addon's own kind, keyed by the
|
|
13446
|
+
* `integrations` registry id. The records door does the secret work: a field the
|
|
13447
|
+
* kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
|
|
13448
|
+
* deleting the row deletes the secret, and one row's value is never another's.
|
|
13449
|
+
*
|
|
13450
|
+
* Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
|
|
13451
|
+
* token or the view the form was given KEEP the stored secret;
|
|
13452
|
+
* `{ $secretClear: true }` clears it; any other string replaces it.
|
|
13453
|
+
*/
|
|
13417
13454
|
/** The most instances one addon lists (an integrations page, not a table). */
|
|
13418
13455
|
var INSTANCES_LIST_MAX = 1e3;
|
|
13419
13456
|
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
13420
13457
|
function topLevelSecretKeys(paths) {
|
|
13421
|
-
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
|
|
13458
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
|
|
13459
|
+
}
|
|
13460
|
+
/** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
|
|
13461
|
+
function secretMapKeys(paths) {
|
|
13462
|
+
return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
|
|
13463
|
+
}
|
|
13464
|
+
/**
|
|
13465
|
+
* A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
|
|
13466
|
+
* view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
|
|
13467
|
+
* or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
|
|
13468
|
+
*/
|
|
13469
|
+
function secretMapPatch(value) {
|
|
13470
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
13471
|
+
const out = {};
|
|
13472
|
+
for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
|
|
13473
|
+
else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
|
|
13474
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
13475
|
+
}
|
|
13476
|
+
/** Does the kind's body schema carry an `enabled` field? */
|
|
13477
|
+
function declaresEnabled(kind) {
|
|
13478
|
+
const json = toJSONSchema(kind.schema, {
|
|
13479
|
+
unrepresentable: "any",
|
|
13480
|
+
io: "input"
|
|
13481
|
+
});
|
|
13482
|
+
const properties = Reflect.get(json, "properties");
|
|
13483
|
+
return typeof properties === "object" && properties !== null && "enabled" in properties;
|
|
13422
13484
|
}
|
|
13423
13485
|
var RecordIntegrationInstances = class {
|
|
13424
13486
|
records;
|
|
@@ -13426,14 +13488,20 @@ var RecordIntegrationInstances = class {
|
|
|
13426
13488
|
hooks;
|
|
13427
13489
|
handle;
|
|
13428
13490
|
secretKeys;
|
|
13491
|
+
secretMaps;
|
|
13429
13492
|
nameField;
|
|
13493
|
+
/** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
|
|
13494
|
+
registryOwnsEnabled;
|
|
13430
13495
|
constructor(records, kind, hooks) {
|
|
13431
13496
|
this.records = records;
|
|
13432
13497
|
this.kind = kind;
|
|
13433
13498
|
this.hooks = hooks;
|
|
13434
13499
|
this.handle = records.kind(kind);
|
|
13435
13500
|
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
13501
|
+
this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
|
|
13436
13502
|
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
13503
|
+
this.registryOwnsEnabled = hooks.description.role === "devices";
|
|
13504
|
+
if (this.registryOwnsEnabled && declaresEnabled(kind)) throw new Error(`kind "${kind.name}": the integrations registry owns the on/off switch of a role "devices" addon, so the body must not declare "enabled"`);
|
|
13437
13505
|
}
|
|
13438
13506
|
/**
|
|
13439
13507
|
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
@@ -13484,7 +13552,7 @@ var RecordIntegrationInstances = class {
|
|
|
13484
13552
|
id: record.id,
|
|
13485
13553
|
name: typeof rawName === "string" ? rawName : record.id,
|
|
13486
13554
|
configured: this.hooks.configured(record.data),
|
|
13487
|
-
enabled: record.data.enabled !== false,
|
|
13555
|
+
...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
|
|
13488
13556
|
formValues: shown,
|
|
13489
13557
|
...kind !== void 0 ? { kind } : {},
|
|
13490
13558
|
...health?.status !== void 0 ? { status: health.status } : {},
|
|
@@ -13514,32 +13582,36 @@ var RecordIntegrationInstances = class {
|
|
|
13514
13582
|
id: minted,
|
|
13515
13583
|
data: this.hooks.initialBody(name, kind)
|
|
13516
13584
|
}, { ifRev: 0 });
|
|
13517
|
-
await this.hooks.onChanged?.(minted);
|
|
13585
|
+
await this.hooks.onChanged?.(minted, "created");
|
|
13518
13586
|
return { id: minted };
|
|
13519
13587
|
};
|
|
13520
13588
|
updateInstance = async ({ id, name, values }) => {
|
|
13521
13589
|
const patch = {};
|
|
13522
13590
|
for (const [key, value] of Object.entries(values)) {
|
|
13523
13591
|
if (value === void 0) continue;
|
|
13524
|
-
if (
|
|
13592
|
+
if (this.secretMaps.has(key)) {
|
|
13593
|
+
const entries = secretMapPatch(value);
|
|
13594
|
+
if (entries !== null) patch[key] = entries;
|
|
13595
|
+
} else if (!this.secretKeys.has(key)) patch[key] = value;
|
|
13525
13596
|
else if (isSecretClear(value)) patch[key] = null;
|
|
13526
13597
|
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13527
13598
|
}
|
|
13528
13599
|
if (name !== void 0) patch[this.nameField] = name;
|
|
13529
13600
|
await this.handle.patch(id, patch);
|
|
13530
|
-
await this.hooks.onChanged?.(id);
|
|
13601
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
13531
13602
|
return { ok: true };
|
|
13532
13603
|
};
|
|
13533
13604
|
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13534
13605
|
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13606
|
+
if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
|
|
13535
13607
|
await this.handle.patch(id, { enabled });
|
|
13536
|
-
await this.hooks.onChanged?.(id);
|
|
13608
|
+
await this.hooks.onChanged?.(id, "enabled");
|
|
13537
13609
|
return { ok: true };
|
|
13538
13610
|
};
|
|
13539
13611
|
/** The row and its keychain refs go together (the records door). */
|
|
13540
13612
|
deleteInstance = async ({ id }) => {
|
|
13541
13613
|
await this.handle.delete(id);
|
|
13542
|
-
await this.hooks.onChanged?.(id);
|
|
13614
|
+
await this.hooks.onChanged?.(id, "deleted");
|
|
13543
13615
|
return { ok: true };
|
|
13544
13616
|
};
|
|
13545
13617
|
testInstance = async (input) => {
|
|
@@ -56307,6 +56379,21 @@ var IntegrationInstanceKindSchema = object({
|
|
|
56307
56379
|
disconnectPath: string().min(1).optional()
|
|
56308
56380
|
});
|
|
56309
56381
|
var IntegrationInstanceDescriptionSchema = object({
|
|
56382
|
+
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
56383
|
+
role: _enum([
|
|
56384
|
+
"devices",
|
|
56385
|
+
"services",
|
|
56386
|
+
"both"
|
|
56387
|
+
]),
|
|
56388
|
+
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
56389
|
+
discovery: object({
|
|
56390
|
+
mode: _enum([
|
|
56391
|
+
"none",
|
|
56392
|
+
"lan",
|
|
56393
|
+
"account"
|
|
56394
|
+
]),
|
|
56395
|
+
scopedToInstance: boolean()
|
|
56396
|
+
}).optional(),
|
|
56310
56397
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
56311
56398
|
instanceMode: IntegrationInstanceModeSchema,
|
|
56312
56399
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
@@ -56346,10 +56433,11 @@ var IntegrationInstanceSchema = object({
|
|
|
56346
56433
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
56347
56434
|
configured: boolean(),
|
|
56348
56435
|
/**
|
|
56349
|
-
* The instance's own on/off switch (D996)
|
|
56350
|
-
*
|
|
56436
|
+
* The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
|
|
56437
|
+
* so a row written before the field existed is on). Absent when the `integrations` registry
|
|
56438
|
+
* owns the switch (`describe().role` is `devices`): read it from the registry row.
|
|
56351
56439
|
*/
|
|
56352
|
-
enabled: boolean(),
|
|
56440
|
+
enabled: boolean().optional(),
|
|
56353
56441
|
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
56354
56442
|
kind: string().optional(),
|
|
56355
56443
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
@@ -143315,6 +143403,7 @@ function statusOf(info, configured) {
|
|
|
143315
143403
|
function buildAlexaInstances(records, deps) {
|
|
143316
143404
|
return new RecordIntegrationInstances(records, alexaBrokersEntryKind, {
|
|
143317
143405
|
description: {
|
|
143406
|
+
role: "both",
|
|
143318
143407
|
instanceMode: "multiple",
|
|
143319
143408
|
kind: "account",
|
|
143320
143409
|
canTest: true,
|
package/dist/addon.mjs
CHANGED
|
@@ -6042,7 +6042,7 @@ function number(params) {
|
|
|
6042
6042
|
return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
|
|
6043
6043
|
}
|
|
6044
6044
|
//#endregion
|
|
6045
|
-
//#region ../types/dist/sleep-
|
|
6045
|
+
//#region ../types/dist/sleep-BaWhXZFm.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).
|
|
@@ -8878,6 +8878,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
|
8878
8878
|
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
8879
8879
|
* (`auth.accessToken`, `headers[].value`).
|
|
8880
8880
|
*
|
|
8881
|
+
* `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
|
|
8882
|
+
* names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
|
|
8883
|
+
* key releases exactly that key's secret.
|
|
8884
|
+
*
|
|
8881
8885
|
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
8882
8886
|
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
8883
8887
|
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
@@ -8890,11 +8894,13 @@ function parseSecretPath(path) {
|
|
|
8890
8894
|
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
8891
8895
|
return path.split(".").map((raw) => {
|
|
8892
8896
|
const each = raw.endsWith("[]");
|
|
8893
|
-
const
|
|
8897
|
+
const map = raw.endsWith("{}");
|
|
8898
|
+
const key = each || map ? raw.slice(0, -2) : raw;
|
|
8894
8899
|
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
8895
8900
|
return {
|
|
8896
8901
|
key,
|
|
8897
|
-
each
|
|
8902
|
+
each,
|
|
8903
|
+
map
|
|
8898
8904
|
};
|
|
8899
8905
|
});
|
|
8900
8906
|
}
|
|
@@ -8946,6 +8952,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
|
|
|
8946
8952
|
[segment.key]: mapped
|
|
8947
8953
|
};
|
|
8948
8954
|
}
|
|
8955
|
+
if (segment.map) {
|
|
8956
|
+
if (!isRecord$4$1(child)) return node;
|
|
8957
|
+
const kept = Object.entries(child).map(([mapKey, value]) => {
|
|
8958
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
|
|
8959
|
+
if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
|
|
8960
|
+
return [mapKey, visit({
|
|
8961
|
+
declared,
|
|
8962
|
+
concrete,
|
|
8963
|
+
value
|
|
8964
|
+
})];
|
|
8965
|
+
}).filter(([, value]) => value !== REMOVE_LEAF);
|
|
8966
|
+
return {
|
|
8967
|
+
...node,
|
|
8968
|
+
[segment.key]: Object.fromEntries(kept)
|
|
8969
|
+
};
|
|
8970
|
+
}
|
|
8949
8971
|
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
8950
8972
|
if (last) {
|
|
8951
8973
|
const replacement = visit({
|
|
@@ -9087,7 +9109,8 @@ function isZodSecret(schema) {
|
|
|
9087
9109
|
}
|
|
9088
9110
|
/**
|
|
9089
9111
|
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
9090
|
-
* element of this array" (`connection.password`, `rows[].token`)
|
|
9112
|
+
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
9113
|
+
* map" (`applianceSecrets{}`). A declared
|
|
9091
9114
|
* object or array is reported as one path and not descended into — everything
|
|
9092
9115
|
* under it is secret.
|
|
9093
9116
|
*/
|
|
@@ -9098,6 +9121,7 @@ function collectZodSecretPaths(schema, prefix = "") {
|
|
|
9098
9121
|
const def = defOf(layer);
|
|
9099
9122
|
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
9100
9123
|
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
9124
|
+
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
9101
9125
|
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
9102
9126
|
else if (def.type === "intersection") {
|
|
9103
9127
|
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
@@ -13412,11 +13436,49 @@ function buildCapturingReply(envelope) {
|
|
|
13412
13436
|
};
|
|
13413
13437
|
return wrapper;
|
|
13414
13438
|
}
|
|
13439
|
+
/**
|
|
13440
|
+
* `RecordIntegrationInstances` - the `integration-instances` provider over ONE
|
|
13441
|
+
* entry kind, so an addon that holds a connection serves it in ~30 lines.
|
|
13442
|
+
*
|
|
13443
|
+
* Every instance is one record of the addon's own kind, keyed by the
|
|
13444
|
+
* `integrations` registry id. The records door does the secret work: a field the
|
|
13445
|
+
* kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
|
|
13446
|
+
* deleting the row deletes the secret, and one row's value is never another's.
|
|
13447
|
+
*
|
|
13448
|
+
* Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
|
|
13449
|
+
* token or the view the form was given KEEP the stored secret;
|
|
13450
|
+
* `{ $secretClear: true }` clears it; any other string replaces it.
|
|
13451
|
+
*/
|
|
13415
13452
|
/** The most instances one addon lists (an integrations page, not a table). */
|
|
13416
13453
|
var INSTANCES_LIST_MAX = 1e3;
|
|
13417
13454
|
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
13418
13455
|
function topLevelSecretKeys(paths) {
|
|
13419
|
-
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
|
|
13456
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
|
|
13457
|
+
}
|
|
13458
|
+
/** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
|
|
13459
|
+
function secretMapKeys(paths) {
|
|
13460
|
+
return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
|
|
13461
|
+
}
|
|
13462
|
+
/**
|
|
13463
|
+
* A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
|
|
13464
|
+
* view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
|
|
13465
|
+
* or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
|
|
13466
|
+
*/
|
|
13467
|
+
function secretMapPatch(value) {
|
|
13468
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
13469
|
+
const out = {};
|
|
13470
|
+
for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
|
|
13471
|
+
else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
|
|
13472
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
13473
|
+
}
|
|
13474
|
+
/** Does the kind's body schema carry an `enabled` field? */
|
|
13475
|
+
function declaresEnabled(kind) {
|
|
13476
|
+
const json = toJSONSchema(kind.schema, {
|
|
13477
|
+
unrepresentable: "any",
|
|
13478
|
+
io: "input"
|
|
13479
|
+
});
|
|
13480
|
+
const properties = Reflect.get(json, "properties");
|
|
13481
|
+
return typeof properties === "object" && properties !== null && "enabled" in properties;
|
|
13420
13482
|
}
|
|
13421
13483
|
var RecordIntegrationInstances = class {
|
|
13422
13484
|
records;
|
|
@@ -13424,14 +13486,20 @@ var RecordIntegrationInstances = class {
|
|
|
13424
13486
|
hooks;
|
|
13425
13487
|
handle;
|
|
13426
13488
|
secretKeys;
|
|
13489
|
+
secretMaps;
|
|
13427
13490
|
nameField;
|
|
13491
|
+
/** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
|
|
13492
|
+
registryOwnsEnabled;
|
|
13428
13493
|
constructor(records, kind, hooks) {
|
|
13429
13494
|
this.records = records;
|
|
13430
13495
|
this.kind = kind;
|
|
13431
13496
|
this.hooks = hooks;
|
|
13432
13497
|
this.handle = records.kind(kind);
|
|
13433
13498
|
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
13499
|
+
this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
|
|
13434
13500
|
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
13501
|
+
this.registryOwnsEnabled = hooks.description.role === "devices";
|
|
13502
|
+
if (this.registryOwnsEnabled && declaresEnabled(kind)) throw new Error(`kind "${kind.name}": the integrations registry owns the on/off switch of a role "devices" addon, so the body must not declare "enabled"`);
|
|
13435
13503
|
}
|
|
13436
13504
|
/**
|
|
13437
13505
|
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
@@ -13482,7 +13550,7 @@ var RecordIntegrationInstances = class {
|
|
|
13482
13550
|
id: record.id,
|
|
13483
13551
|
name: typeof rawName === "string" ? rawName : record.id,
|
|
13484
13552
|
configured: this.hooks.configured(record.data),
|
|
13485
|
-
enabled: record.data.enabled !== false,
|
|
13553
|
+
...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
|
|
13486
13554
|
formValues: shown,
|
|
13487
13555
|
...kind !== void 0 ? { kind } : {},
|
|
13488
13556
|
...health?.status !== void 0 ? { status: health.status } : {},
|
|
@@ -13512,32 +13580,36 @@ var RecordIntegrationInstances = class {
|
|
|
13512
13580
|
id: minted,
|
|
13513
13581
|
data: this.hooks.initialBody(name, kind)
|
|
13514
13582
|
}, { ifRev: 0 });
|
|
13515
|
-
await this.hooks.onChanged?.(minted);
|
|
13583
|
+
await this.hooks.onChanged?.(minted, "created");
|
|
13516
13584
|
return { id: minted };
|
|
13517
13585
|
};
|
|
13518
13586
|
updateInstance = async ({ id, name, values }) => {
|
|
13519
13587
|
const patch = {};
|
|
13520
13588
|
for (const [key, value] of Object.entries(values)) {
|
|
13521
13589
|
if (value === void 0) continue;
|
|
13522
|
-
if (
|
|
13590
|
+
if (this.secretMaps.has(key)) {
|
|
13591
|
+
const entries = secretMapPatch(value);
|
|
13592
|
+
if (entries !== null) patch[key] = entries;
|
|
13593
|
+
} else if (!this.secretKeys.has(key)) patch[key] = value;
|
|
13523
13594
|
else if (isSecretClear(value)) patch[key] = null;
|
|
13524
13595
|
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13525
13596
|
}
|
|
13526
13597
|
if (name !== void 0) patch[this.nameField] = name;
|
|
13527
13598
|
await this.handle.patch(id, patch);
|
|
13528
|
-
await this.hooks.onChanged?.(id);
|
|
13599
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
13529
13600
|
return { ok: true };
|
|
13530
13601
|
};
|
|
13531
13602
|
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13532
13603
|
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13604
|
+
if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
|
|
13533
13605
|
await this.handle.patch(id, { enabled });
|
|
13534
|
-
await this.hooks.onChanged?.(id);
|
|
13606
|
+
await this.hooks.onChanged?.(id, "enabled");
|
|
13535
13607
|
return { ok: true };
|
|
13536
13608
|
};
|
|
13537
13609
|
/** The row and its keychain refs go together (the records door). */
|
|
13538
13610
|
deleteInstance = async ({ id }) => {
|
|
13539
13611
|
await this.handle.delete(id);
|
|
13540
|
-
await this.hooks.onChanged?.(id);
|
|
13612
|
+
await this.hooks.onChanged?.(id, "deleted");
|
|
13541
13613
|
return { ok: true };
|
|
13542
13614
|
};
|
|
13543
13615
|
testInstance = async (input) => {
|
|
@@ -56305,6 +56377,21 @@ var IntegrationInstanceKindSchema = object({
|
|
|
56305
56377
|
disconnectPath: string().min(1).optional()
|
|
56306
56378
|
});
|
|
56307
56379
|
var IntegrationInstanceDescriptionSchema = object({
|
|
56380
|
+
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
56381
|
+
role: _enum([
|
|
56382
|
+
"devices",
|
|
56383
|
+
"services",
|
|
56384
|
+
"both"
|
|
56385
|
+
]),
|
|
56386
|
+
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
56387
|
+
discovery: object({
|
|
56388
|
+
mode: _enum([
|
|
56389
|
+
"none",
|
|
56390
|
+
"lan",
|
|
56391
|
+
"account"
|
|
56392
|
+
]),
|
|
56393
|
+
scopedToInstance: boolean()
|
|
56394
|
+
}).optional(),
|
|
56308
56395
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
56309
56396
|
instanceMode: IntegrationInstanceModeSchema,
|
|
56310
56397
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
@@ -56344,10 +56431,11 @@ var IntegrationInstanceSchema = object({
|
|
|
56344
56431
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
56345
56432
|
configured: boolean(),
|
|
56346
56433
|
/**
|
|
56347
|
-
* The instance's own on/off switch (D996)
|
|
56348
|
-
*
|
|
56434
|
+
* The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
|
|
56435
|
+
* so a row written before the field existed is on). Absent when the `integrations` registry
|
|
56436
|
+
* owns the switch (`describe().role` is `devices`): read it from the registry row.
|
|
56349
56437
|
*/
|
|
56350
|
-
enabled: boolean(),
|
|
56438
|
+
enabled: boolean().optional(),
|
|
56351
56439
|
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
56352
56440
|
kind: string().optional(),
|
|
56353
56441
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
@@ -143313,6 +143401,7 @@ function statusOf(info, configured) {
|
|
|
143313
143401
|
function buildAlexaInstances(records, deps) {
|
|
143314
143402
|
return new RecordIntegrationInstances(records, alexaBrokersEntryKind, {
|
|
143315
143403
|
description: {
|
|
143404
|
+
role: "both",
|
|
143316
143405
|
instanceMode: "multiple",
|
|
143317
143406
|
kind: "account",
|
|
143318
143407
|
canTest: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-import-alexa",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.184",
|
|
4
4
|
"description": "Alexa device-import provider for CamStack — imports the smart-home devices in a user's Alexa account via the unofficial alexa-remote2 cookie/token client (the inverse of the Alexa exporter)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
}
|
|
73
73
|
],
|
|
74
74
|
"requires": {
|
|
75
|
-
"server": ">=1.2.
|
|
75
|
+
"server": ">=1.2.762"
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
78
|
"files": [
|