@camstack/addon-provider-velux 0.2.157 → 0.2.158
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 +1618 -334
- package/dist/addon.mjs +1618 -334
- package/package.json +6 -2
package/dist/addon.js
CHANGED
|
@@ -4593,6 +4593,12 @@ var numberProcessor = (schema, ctx, _json, _params) => {
|
|
|
4593
4593
|
var booleanProcessor = (_schema, _ctx, json, _params) => {
|
|
4594
4594
|
json.type = "boolean";
|
|
4595
4595
|
};
|
|
4596
|
+
var bigintProcessor = (_schema, ctx, _json, _params) => {
|
|
4597
|
+
if (ctx.unrepresentable === "throw") throw new Error("BigInt cannot be represented in JSON Schema");
|
|
4598
|
+
};
|
|
4599
|
+
var symbolProcessor = (_schema, ctx, _json, _params) => {
|
|
4600
|
+
if (ctx.unrepresentable === "throw") throw new Error("Symbols cannot be represented in JSON Schema");
|
|
4601
|
+
};
|
|
4596
4602
|
var nullProcessor = (_schema, ctx, json, _params) => {
|
|
4597
4603
|
if (ctx.target === "openapi-3.0") {
|
|
4598
4604
|
json.type = "string";
|
|
@@ -4600,12 +4606,17 @@ var nullProcessor = (_schema, ctx, json, _params) => {
|
|
|
4600
4606
|
json.enum = [null];
|
|
4601
4607
|
} else json.type = "null";
|
|
4602
4608
|
};
|
|
4609
|
+
var undefinedProcessor = (_schema, ctx, _json, _params) => {
|
|
4610
|
+
if (ctx.unrepresentable === "throw") throw new Error("Undefined cannot be represented in JSON Schema");
|
|
4611
|
+
};
|
|
4603
4612
|
var voidProcessor = (_schema, ctx, _json, _params) => {
|
|
4604
4613
|
if (ctx.unrepresentable === "throw") throw new Error("Void cannot be represented in JSON Schema");
|
|
4605
4614
|
};
|
|
4606
4615
|
var neverProcessor = (_schema, _ctx, json, _params) => {
|
|
4607
4616
|
json.not = {};
|
|
4608
4617
|
};
|
|
4618
|
+
var anyProcessor = (_schema, _ctx, _json, _params) => {};
|
|
4619
|
+
var unknownProcessor = (_schema, _ctx, _json, _params) => {};
|
|
4609
4620
|
var dateProcessor = (_schema, ctx, _json, _params) => {
|
|
4610
4621
|
if (ctx.unrepresentable === "throw") throw new Error("Date cannot be represented in JSON Schema");
|
|
4611
4622
|
};
|
|
@@ -4637,12 +4648,53 @@ var literalProcessor = (schema, ctx, json, _params) => {
|
|
|
4637
4648
|
json.enum = vals;
|
|
4638
4649
|
}
|
|
4639
4650
|
};
|
|
4651
|
+
var nanProcessor = (_schema, ctx, _json, _params) => {
|
|
4652
|
+
if (ctx.unrepresentable === "throw") throw new Error("NaN cannot be represented in JSON Schema");
|
|
4653
|
+
};
|
|
4654
|
+
var templateLiteralProcessor = (schema, _ctx, json, _params) => {
|
|
4655
|
+
const _json = json;
|
|
4656
|
+
const pattern = schema._zod.pattern;
|
|
4657
|
+
if (!pattern) throw new Error("Pattern not found in template literal");
|
|
4658
|
+
_json.type = "string";
|
|
4659
|
+
_json.pattern = pattern.source;
|
|
4660
|
+
};
|
|
4661
|
+
var fileProcessor = (schema, _ctx, json, _params) => {
|
|
4662
|
+
const _json = json;
|
|
4663
|
+
const file = {
|
|
4664
|
+
type: "string",
|
|
4665
|
+
format: "binary",
|
|
4666
|
+
contentEncoding: "binary"
|
|
4667
|
+
};
|
|
4668
|
+
const { minimum, maximum, mime } = schema._zod.bag;
|
|
4669
|
+
if (minimum !== void 0) file.minLength = minimum;
|
|
4670
|
+
if (maximum !== void 0) file.maxLength = maximum;
|
|
4671
|
+
if (mime) if (mime.length === 1) {
|
|
4672
|
+
file.contentMediaType = mime[0];
|
|
4673
|
+
Object.assign(_json, file);
|
|
4674
|
+
} else {
|
|
4675
|
+
Object.assign(_json, file);
|
|
4676
|
+
_json.anyOf = mime.map((m) => ({ contentMediaType: m }));
|
|
4677
|
+
}
|
|
4678
|
+
else Object.assign(_json, file);
|
|
4679
|
+
};
|
|
4680
|
+
var successProcessor = (_schema, _ctx, json, _params) => {
|
|
4681
|
+
json.type = "boolean";
|
|
4682
|
+
};
|
|
4640
4683
|
var customProcessor = (_schema, ctx, _json, _params) => {
|
|
4641
4684
|
if (ctx.unrepresentable === "throw") throw new Error("Custom types cannot be represented in JSON Schema");
|
|
4642
4685
|
};
|
|
4686
|
+
var functionProcessor = (_schema, ctx, _json, _params) => {
|
|
4687
|
+
if (ctx.unrepresentable === "throw") throw new Error("Function types cannot be represented in JSON Schema");
|
|
4688
|
+
};
|
|
4643
4689
|
var transformProcessor = (_schema, ctx, _json, _params) => {
|
|
4644
4690
|
if (ctx.unrepresentable === "throw") throw new Error("Transforms cannot be represented in JSON Schema");
|
|
4645
4691
|
};
|
|
4692
|
+
var mapProcessor = (_schema, ctx, _json, _params) => {
|
|
4693
|
+
if (ctx.unrepresentable === "throw") throw new Error("Map cannot be represented in JSON Schema");
|
|
4694
|
+
};
|
|
4695
|
+
var setProcessor = (_schema, ctx, _json, _params) => {
|
|
4696
|
+
if (ctx.unrepresentable === "throw") throw new Error("Set cannot be represented in JSON Schema");
|
|
4697
|
+
};
|
|
4646
4698
|
var arrayProcessor = (schema, ctx, _json, params) => {
|
|
4647
4699
|
const json = _json;
|
|
4648
4700
|
const def = schema._zod.def;
|
|
@@ -4847,6 +4899,12 @@ var readonlyProcessor = (schema, ctx, json, params) => {
|
|
|
4847
4899
|
seen.ref = def.innerType;
|
|
4848
4900
|
json.readOnly = true;
|
|
4849
4901
|
};
|
|
4902
|
+
var promiseProcessor = (schema, ctx, _json, params) => {
|
|
4903
|
+
const def = schema._zod.def;
|
|
4904
|
+
process$1(def.innerType, ctx, params);
|
|
4905
|
+
const seen = ctx.seen.get(schema);
|
|
4906
|
+
seen.ref = def.innerType;
|
|
4907
|
+
};
|
|
4850
4908
|
var optionalProcessor = (schema, ctx, _json, params) => {
|
|
4851
4909
|
const def = schema._zod.def;
|
|
4852
4910
|
process$1(def.innerType, ctx, params);
|
|
@@ -4859,6 +4917,81 @@ var lazyProcessor = (schema, ctx, _json, params) => {
|
|
|
4859
4917
|
const seen = ctx.seen.get(schema);
|
|
4860
4918
|
seen.ref = innerType;
|
|
4861
4919
|
};
|
|
4920
|
+
var allProcessors = {
|
|
4921
|
+
string: stringProcessor,
|
|
4922
|
+
number: numberProcessor,
|
|
4923
|
+
boolean: booleanProcessor,
|
|
4924
|
+
bigint: bigintProcessor,
|
|
4925
|
+
symbol: symbolProcessor,
|
|
4926
|
+
null: nullProcessor,
|
|
4927
|
+
undefined: undefinedProcessor,
|
|
4928
|
+
void: voidProcessor,
|
|
4929
|
+
never: neverProcessor,
|
|
4930
|
+
any: anyProcessor,
|
|
4931
|
+
unknown: unknownProcessor,
|
|
4932
|
+
date: dateProcessor,
|
|
4933
|
+
enum: enumProcessor,
|
|
4934
|
+
literal: literalProcessor,
|
|
4935
|
+
nan: nanProcessor,
|
|
4936
|
+
template_literal: templateLiteralProcessor,
|
|
4937
|
+
file: fileProcessor,
|
|
4938
|
+
success: successProcessor,
|
|
4939
|
+
custom: customProcessor,
|
|
4940
|
+
function: functionProcessor,
|
|
4941
|
+
transform: transformProcessor,
|
|
4942
|
+
map: mapProcessor,
|
|
4943
|
+
set: setProcessor,
|
|
4944
|
+
array: arrayProcessor,
|
|
4945
|
+
object: objectProcessor,
|
|
4946
|
+
union: unionProcessor,
|
|
4947
|
+
intersection: intersectionProcessor,
|
|
4948
|
+
tuple: tupleProcessor,
|
|
4949
|
+
record: recordProcessor,
|
|
4950
|
+
nullable: nullableProcessor,
|
|
4951
|
+
nonoptional: nonoptionalProcessor,
|
|
4952
|
+
default: defaultProcessor,
|
|
4953
|
+
prefault: prefaultProcessor,
|
|
4954
|
+
catch: catchProcessor,
|
|
4955
|
+
pipe: pipeProcessor,
|
|
4956
|
+
readonly: readonlyProcessor,
|
|
4957
|
+
promise: promiseProcessor,
|
|
4958
|
+
optional: optionalProcessor,
|
|
4959
|
+
lazy: lazyProcessor
|
|
4960
|
+
};
|
|
4961
|
+
function toJSONSchema(input, params) {
|
|
4962
|
+
if ("_idmap" in input) {
|
|
4963
|
+
const registry = input;
|
|
4964
|
+
const ctx = initializeContext({
|
|
4965
|
+
...params,
|
|
4966
|
+
processors: allProcessors
|
|
4967
|
+
});
|
|
4968
|
+
const defs = {};
|
|
4969
|
+
for (const entry of registry._idmap.entries()) {
|
|
4970
|
+
const [_, schema] = entry;
|
|
4971
|
+
process$1(schema, ctx);
|
|
4972
|
+
}
|
|
4973
|
+
const schemas = {};
|
|
4974
|
+
ctx.external = {
|
|
4975
|
+
registry,
|
|
4976
|
+
uri: params?.uri,
|
|
4977
|
+
defs
|
|
4978
|
+
};
|
|
4979
|
+
for (const entry of registry._idmap.entries()) {
|
|
4980
|
+
const [key, schema] = entry;
|
|
4981
|
+
extractDefs(ctx, schema);
|
|
4982
|
+
schemas[key] = finalize(ctx, schema);
|
|
4983
|
+
}
|
|
4984
|
+
if (Object.keys(defs).length > 0) schemas.__shared = { [ctx.target === "draft-2020-12" ? "$defs" : "definitions"]: defs };
|
|
4985
|
+
return { schemas };
|
|
4986
|
+
}
|
|
4987
|
+
const ctx = initializeContext({
|
|
4988
|
+
...params,
|
|
4989
|
+
processors: allProcessors
|
|
4990
|
+
});
|
|
4991
|
+
process$1(input, ctx);
|
|
4992
|
+
extractDefs(ctx, input);
|
|
4993
|
+
return finalize(ctx, input);
|
|
4994
|
+
}
|
|
4862
4995
|
//#endregion
|
|
4863
4996
|
//#region ../../node_modules/zod/v4/classic/iso.js
|
|
4864
4997
|
var ZodISODateTime = /*@__PURE__*/ $constructor("ZodISODateTime", (inst, def) => {
|
|
@@ -5883,7 +6016,7 @@ var ZodIssueCode = {
|
|
|
5883
6016
|
var ZodFirstPartyTypeKind;
|
|
5884
6017
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5885
6018
|
//#endregion
|
|
5886
|
-
//#region ../types/dist/sleep-
|
|
6019
|
+
//#region ../types/dist/sleep-D1jjWAr8.mjs
|
|
5887
6020
|
/**
|
|
5888
6021
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5889
6022
|
* window to float samples (D455).
|
|
@@ -6031,9 +6164,16 @@ function isSecretMarker(value) {
|
|
|
6031
6164
|
const ref = value["$secret"];
|
|
6032
6165
|
return typeof ref === "string" && ref.length > 0;
|
|
6033
6166
|
}
|
|
6167
|
+
/** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
|
|
6168
|
+
function isSecretMarkerLike(value) {
|
|
6169
|
+
return isRecord$7(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
|
|
6170
|
+
}
|
|
6034
6171
|
function isSecretClear(value) {
|
|
6035
6172
|
return isRecord$7(value) && value["$secretClear"] === true;
|
|
6036
6173
|
}
|
|
6174
|
+
function isSecretFieldView(value) {
|
|
6175
|
+
return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
|
|
6176
|
+
}
|
|
6037
6177
|
/**
|
|
6038
6178
|
* The UI view of a stored secret field value: a marker becomes "set", inline
|
|
6039
6179
|
* plaintext becomes "set (inline)", nothing becomes `null`. A value never
|
|
@@ -6415,6 +6555,53 @@ function emitReadiness(bus, params) {
|
|
|
6415
6555
|
ts
|
|
6416
6556
|
}));
|
|
6417
6557
|
}
|
|
6558
|
+
/** Why a catalogue entry is not a runnable body of its declared kind. */
|
|
6559
|
+
var AddonMigrationDefinitionError = class extends Error {
|
|
6560
|
+
id;
|
|
6561
|
+
constructor(id, message) {
|
|
6562
|
+
super(`${id}: ${message}`);
|
|
6563
|
+
this.id = id;
|
|
6564
|
+
this.name = "AddonMigrationDefinitionError";
|
|
6565
|
+
}
|
|
6566
|
+
};
|
|
6567
|
+
/**
|
|
6568
|
+
* Bind a catalogue to the addon's ports. Throws {@link AddonMigrationDefinitionError} when
|
|
6569
|
+
* an entry's `bodyKind` does not match the body it carries, or is `sql` (a hub-only
|
|
6570
|
+
* body). A throwing body never escapes `execute`: it is reported as a failed result.
|
|
6571
|
+
*/
|
|
6572
|
+
function defineAddonMigrations(catalogue, ports) {
|
|
6573
|
+
return catalogue.map((entry) => {
|
|
6574
|
+
const { meta } = entry;
|
|
6575
|
+
if ("plan" in entry) {
|
|
6576
|
+
if (meta.bodyKind !== "plan") throw new AddonMigrationDefinitionError(meta.id, `carries a plan body but declares "${meta.bodyKind}"`);
|
|
6577
|
+
return {
|
|
6578
|
+
meta,
|
|
6579
|
+
execute: () => guarded(async () => ({
|
|
6580
|
+
kind: "plan",
|
|
6581
|
+
plan: await entry.plan(ports)
|
|
6582
|
+
}))
|
|
6583
|
+
};
|
|
6584
|
+
}
|
|
6585
|
+
if (meta.bodyKind !== "procedure") throw new AddonMigrationDefinitionError(meta.id, `carries a procedure body but declares "${meta.bodyKind}"`);
|
|
6586
|
+
return {
|
|
6587
|
+
meta,
|
|
6588
|
+
execute: () => guarded(async () => ({
|
|
6589
|
+
kind: "procedure",
|
|
6590
|
+
result: await entry.procedure(ports)
|
|
6591
|
+
}))
|
|
6592
|
+
};
|
|
6593
|
+
});
|
|
6594
|
+
}
|
|
6595
|
+
async function guarded(work) {
|
|
6596
|
+
try {
|
|
6597
|
+
return await work();
|
|
6598
|
+
} catch (err) {
|
|
6599
|
+
return {
|
|
6600
|
+
kind: "failed",
|
|
6601
|
+
error: errMsg(err)
|
|
6602
|
+
};
|
|
6603
|
+
}
|
|
6604
|
+
}
|
|
6418
6605
|
var NOT_RUN = {
|
|
6419
6606
|
declared: false,
|
|
6420
6607
|
snapshot: null,
|
|
@@ -6429,7 +6616,7 @@ var NOT_RUN = {
|
|
|
6429
6616
|
* throws: an unreachable or refusing engine is logged and reported `declared: false`,
|
|
6430
6617
|
* because the hub (and this addon's boot) must not hang on a migration.
|
|
6431
6618
|
*/
|
|
6432
|
-
async function runAddonMigrations(host, migrations, logger) {
|
|
6619
|
+
async function runAddonMigrations(host, migrations, logger, options = {}) {
|
|
6433
6620
|
const first = migrations[0];
|
|
6434
6621
|
if (first === void 0) return NOT_RUN;
|
|
6435
6622
|
const owner = first.meta.owner;
|
|
@@ -6458,7 +6645,9 @@ async function runAddonMigrations(host, migrations, logger) {
|
|
|
6458
6645
|
else if (status === "blocked") blocked.push(id);
|
|
6459
6646
|
else skipped.push(id);
|
|
6460
6647
|
};
|
|
6648
|
+
const selected = options.only === void 0 ? null : new Set(options.only);
|
|
6461
6649
|
for (const step of declaration.steps) {
|
|
6650
|
+
if (selected !== null && !selected.has(step.id)) continue;
|
|
6462
6651
|
const bound = byId.get(step.id);
|
|
6463
6652
|
if (bound === void 0 || step.action === "skip") {
|
|
6464
6653
|
skipped.push(step.id);
|
|
@@ -6636,11 +6825,36 @@ function projectStore(store, perNodeKeys, nodeId) {
|
|
|
6636
6825
|
}
|
|
6637
6826
|
return out;
|
|
6638
6827
|
}
|
|
6828
|
+
/**
|
|
6829
|
+
* The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
|
|
6830
|
+
*
|
|
6831
|
+
* A ref is DETERMINISTIC from where the value lives — the owner addon, the
|
|
6832
|
+
* scope (global, one node, one device, one integration, one row of a business
|
|
6833
|
+
* collection) and the field path inside that scope — so saving the same field
|
|
6834
|
+
* twice updates the same ref, and the ref itself says which settings blob
|
|
6835
|
+
* points at it. It is also the AAD of the value's ciphertext: a ciphertext
|
|
6836
|
+
* copied onto another ref does not decrypt.
|
|
6837
|
+
*
|
|
6838
|
+
* Array items are addressed by ITEM ID, never by index
|
|
6839
|
+
* (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
|
|
6840
|
+
*
|
|
6841
|
+
* Pure and browser-safe: no node builtins, no I/O.
|
|
6842
|
+
*/
|
|
6843
|
+
/** The URI scheme of every keychain ref. */
|
|
6844
|
+
var SECRET_REF_SCHEME = "secret://";
|
|
6639
6845
|
var GLOBAL_SCOPE = { kind: "global" };
|
|
6640
6846
|
/** The scope every addon-wide (not per-node, not per-device) setting lives in. */
|
|
6641
6847
|
function globalSecretScope() {
|
|
6642
6848
|
return GLOBAL_SCOPE;
|
|
6643
6849
|
}
|
|
6850
|
+
/**
|
|
6851
|
+
* An addon id as a ref owner. A hub in-process builtin's context id is
|
|
6852
|
+
* `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
|
|
6853
|
+
* addon running forked.
|
|
6854
|
+
*/
|
|
6855
|
+
function normalizeSecretOwner(addonId) {
|
|
6856
|
+
return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
|
|
6857
|
+
}
|
|
6644
6858
|
function encodeSegment(value) {
|
|
6645
6859
|
return encodeURIComponent(value);
|
|
6646
6860
|
}
|
|
@@ -6651,6 +6865,16 @@ function decodeSegment(value) {
|
|
|
6651
6865
|
return null;
|
|
6652
6866
|
}
|
|
6653
6867
|
}
|
|
6868
|
+
/** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
|
|
6869
|
+
function formatSecretScope(scope) {
|
|
6870
|
+
switch (scope.kind) {
|
|
6871
|
+
case "global": return "global";
|
|
6872
|
+
case "node": return `node/${encodeSegment(scope.nodeId)}`;
|
|
6873
|
+
case "device": return `device/${scope.deviceId}`;
|
|
6874
|
+
case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
|
|
6875
|
+
case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
|
|
6876
|
+
}
|
|
6877
|
+
}
|
|
6654
6878
|
/**
|
|
6655
6879
|
* The field path of a value inside an array item, addressed by the item's id:
|
|
6656
6880
|
* `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
|
|
@@ -6659,6 +6883,13 @@ function decodeSegment(value) {
|
|
|
6659
6883
|
function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
|
|
6660
6884
|
return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
|
|
6661
6885
|
}
|
|
6886
|
+
/** Build a ref. Throws on an empty owner or field path — both are bugs. */
|
|
6887
|
+
function buildSecretRef(parts) {
|
|
6888
|
+
const owner = normalizeSecretOwner(parts.owner);
|
|
6889
|
+
if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
|
|
6890
|
+
if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
|
|
6891
|
+
return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
|
|
6892
|
+
}
|
|
6662
6893
|
function parseScope(segments) {
|
|
6663
6894
|
switch (segments[0]) {
|
|
6664
6895
|
case "global": return {
|
|
@@ -7576,14 +7807,18 @@ var BaseAddon = class {
|
|
|
7576
7807
|
* nothing; the boot continues. A migration that FAILED (or was blocked by one)
|
|
7577
7808
|
* and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
|
|
7578
7809
|
* serve data whose shape it cannot trust; `tolerate` migrations never do.
|
|
7810
|
+
*
|
|
7811
|
+
* ALWAYS pass the whole catalogue; a phase that must run only some of it passes
|
|
7812
|
+
* `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
|
|
7813
|
+
* omitted applied ids "newer").
|
|
7579
7814
|
*/
|
|
7580
|
-
async runMigrations(migrations) {
|
|
7815
|
+
async runMigrations(migrations, options) {
|
|
7581
7816
|
const api = this.ctx.api.migrations;
|
|
7582
7817
|
const report = await runAddonMigrations({
|
|
7583
7818
|
declareOwner: (input) => api.declareOwner.mutate(input),
|
|
7584
7819
|
startMigration: (input) => api.startMigration.mutate(input),
|
|
7585
7820
|
finishMigration: (input) => api.finishMigration.mutate(input)
|
|
7586
|
-
}, migrations, this.ctx.logger);
|
|
7821
|
+
}, migrations, this.ctx.logger, options);
|
|
7587
7822
|
if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
|
|
7588
7823
|
return report;
|
|
7589
7824
|
}
|
|
@@ -8567,6 +8802,45 @@ function normalizeAddonInitResult(result) {
|
|
|
8567
8802
|
if (Array.isArray(result)) return { providers: result };
|
|
8568
8803
|
return result;
|
|
8569
8804
|
}
|
|
8805
|
+
var PREFIX = "records/";
|
|
8806
|
+
/** The base of every records failure. `detail` is the human part of the message. */
|
|
8807
|
+
var RecordsError = class extends Error {
|
|
8808
|
+
code;
|
|
8809
|
+
detail;
|
|
8810
|
+
constructor(code, detail) {
|
|
8811
|
+
super(`${PREFIX}${code}: ${detail}`);
|
|
8812
|
+
this.name = "RecordsError";
|
|
8813
|
+
this.code = code;
|
|
8814
|
+
this.detail = detail;
|
|
8815
|
+
}
|
|
8816
|
+
};
|
|
8817
|
+
/** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
|
|
8818
|
+
var RevConflictError = class extends RecordsError {
|
|
8819
|
+
constructor(detail) {
|
|
8820
|
+
super("rev-conflict", detail);
|
|
8821
|
+
this.name = "RevConflictError";
|
|
8822
|
+
}
|
|
8823
|
+
};
|
|
8824
|
+
/** The keychain could not store or serve a value: explicit, never an empty string. */
|
|
8825
|
+
var RecordSecretUnavailableError = class extends RecordsError {
|
|
8826
|
+
constructor(detail) {
|
|
8827
|
+
super("secret-unavailable", detail);
|
|
8828
|
+
this.name = "RecordSecretUnavailableError";
|
|
8829
|
+
}
|
|
8830
|
+
};
|
|
8831
|
+
var InvalidRecordError = class extends RecordsError {
|
|
8832
|
+
constructor(detail) {
|
|
8833
|
+
super("invalid-record", detail);
|
|
8834
|
+
this.name = "InvalidRecordError";
|
|
8835
|
+
}
|
|
8836
|
+
};
|
|
8837
|
+
/** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
|
|
8838
|
+
var DeclarationRefusedError = class extends RecordsError {
|
|
8839
|
+
constructor(detail) {
|
|
8840
|
+
super("declaration-refused", detail);
|
|
8841
|
+
this.name = "DeclarationRefusedError";
|
|
8842
|
+
}
|
|
8843
|
+
};
|
|
8570
8844
|
/** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
|
|
8571
8845
|
/** The files a kind may live in. `history` is device-history's (D850), never records. */
|
|
8572
8846
|
var RECORD_DATABASES = ["settings", "events"];
|
|
@@ -8582,6 +8856,183 @@ var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
|
|
|
8582
8856
|
var RECORD_MAX_DATA_BYTES = 1024 * 1024;
|
|
8583
8857
|
var RECORD_BATCH_MAX = 1e3;
|
|
8584
8858
|
var RECORD_QUERY_MAX = 1e3;
|
|
8859
|
+
/** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
|
|
8860
|
+
var RECORD_SETTINGS_MAX_ROWS = 5e4;
|
|
8861
|
+
/** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
|
|
8862
|
+
var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
8863
|
+
/**
|
|
8864
|
+
* Secret fields of a record, addressed the way a Zod schema declares them
|
|
8865
|
+
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
8866
|
+
* (`auth.accessToken`, `headers[].value`).
|
|
8867
|
+
*
|
|
8868
|
+
* `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
|
|
8869
|
+
* names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
|
|
8870
|
+
* key releases exactly that key's secret.
|
|
8871
|
+
*
|
|
8872
|
+
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
8873
|
+
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
8874
|
+
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
8875
|
+
* leaf names its element: `headers[X-Api].value`.
|
|
8876
|
+
*
|
|
8877
|
+
* Pure and browser-safe. Nothing here mutates: every transform returns a new value.
|
|
8878
|
+
*/
|
|
8879
|
+
/** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
|
|
8880
|
+
function parseSecretPath(path) {
|
|
8881
|
+
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
8882
|
+
return path.split(".").map((raw) => {
|
|
8883
|
+
const each = raw.endsWith("[]");
|
|
8884
|
+
const map = raw.endsWith("{}");
|
|
8885
|
+
const key = each || map ? raw.slice(0, -2) : raw;
|
|
8886
|
+
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
8887
|
+
return {
|
|
8888
|
+
key,
|
|
8889
|
+
each,
|
|
8890
|
+
map
|
|
8891
|
+
};
|
|
8892
|
+
});
|
|
8893
|
+
}
|
|
8894
|
+
/** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
|
|
8895
|
+
function secretArrayPrefixes(path) {
|
|
8896
|
+
const segments = parseSecretPath(path);
|
|
8897
|
+
const prefixes = [];
|
|
8898
|
+
const walked = [];
|
|
8899
|
+
for (const segment of segments) {
|
|
8900
|
+
walked.push(segment.key);
|
|
8901
|
+
if (segment.each) prefixes.push(walked.join("."));
|
|
8902
|
+
}
|
|
8903
|
+
return prefixes;
|
|
8904
|
+
}
|
|
8905
|
+
function isRecord$4(value) {
|
|
8906
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8907
|
+
}
|
|
8908
|
+
function elementId(element, idKey, declared, prefix) {
|
|
8909
|
+
const id = isRecord$4(element) ? element[idKey] : void 0;
|
|
8910
|
+
if (typeof id !== "string" || id.length === 0) throw new InvalidRecordError(`"${declared}": every element of "${prefix}" needs a non-empty string "${idKey}" to carry a secret`);
|
|
8911
|
+
return id;
|
|
8912
|
+
}
|
|
8913
|
+
/**
|
|
8914
|
+
* Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
|
|
8915
|
+
* visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
|
|
8916
|
+
* result is a new body, `data` is untouched.
|
|
8917
|
+
*/
|
|
8918
|
+
var REMOVE_LEAF = Symbol("remove-leaf");
|
|
8919
|
+
function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
|
|
8920
|
+
if (!isRecord$4(node)) return node;
|
|
8921
|
+
const segment = segments[index];
|
|
8922
|
+
if (segment === void 0) return node;
|
|
8923
|
+
if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
|
|
8924
|
+
const child = node[segment.key];
|
|
8925
|
+
const keys = [...walkedKeys, segment.key];
|
|
8926
|
+
const last = index === segments.length - 1;
|
|
8927
|
+
if (segment.each) {
|
|
8928
|
+
if (!Array.isArray(child)) return node;
|
|
8929
|
+
const prefix = keys.join(".");
|
|
8930
|
+
const idKey = arrayKeys[prefix];
|
|
8931
|
+
if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
|
|
8932
|
+
const mapped = child.map((element) => {
|
|
8933
|
+
const id = elementId(element, idKey, declared, prefix);
|
|
8934
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
|
|
8935
|
+
return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
|
|
8936
|
+
});
|
|
8937
|
+
return {
|
|
8938
|
+
...node,
|
|
8939
|
+
[segment.key]: mapped
|
|
8940
|
+
};
|
|
8941
|
+
}
|
|
8942
|
+
if (segment.map) {
|
|
8943
|
+
if (!isRecord$4(child)) return node;
|
|
8944
|
+
const kept = Object.entries(child).map(([mapKey, value]) => {
|
|
8945
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
|
|
8946
|
+
if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
|
|
8947
|
+
return [mapKey, visit({
|
|
8948
|
+
declared,
|
|
8949
|
+
concrete,
|
|
8950
|
+
value
|
|
8951
|
+
})];
|
|
8952
|
+
}).filter(([, value]) => value !== REMOVE_LEAF);
|
|
8953
|
+
return {
|
|
8954
|
+
...node,
|
|
8955
|
+
[segment.key]: Object.fromEntries(kept)
|
|
8956
|
+
};
|
|
8957
|
+
}
|
|
8958
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
8959
|
+
if (last) {
|
|
8960
|
+
const replacement = visit({
|
|
8961
|
+
declared,
|
|
8962
|
+
concrete,
|
|
8963
|
+
value: child
|
|
8964
|
+
});
|
|
8965
|
+
if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
|
|
8966
|
+
return {
|
|
8967
|
+
...node,
|
|
8968
|
+
[segment.key]: replacement
|
|
8969
|
+
};
|
|
8970
|
+
}
|
|
8971
|
+
return {
|
|
8972
|
+
...node,
|
|
8973
|
+
[segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
|
|
8974
|
+
};
|
|
8975
|
+
}
|
|
8976
|
+
/** Replace/remove every present secret leaf; returns a NEW body. */
|
|
8977
|
+
function mapSecretLeaves(data, paths, arrayKeys, visit) {
|
|
8978
|
+
let current = data;
|
|
8979
|
+
for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
|
|
8980
|
+
return current;
|
|
8981
|
+
}
|
|
8982
|
+
/** Every present secret leaf, read-only. */
|
|
8983
|
+
function collectSecretLeaves(data, paths, arrayKeys) {
|
|
8984
|
+
const found = [];
|
|
8985
|
+
mapSecretLeaves(data, paths, arrayKeys, (leaf) => {
|
|
8986
|
+
found.push(leaf);
|
|
8987
|
+
return leaf.value;
|
|
8988
|
+
});
|
|
8989
|
+
return found;
|
|
8990
|
+
}
|
|
8991
|
+
/**
|
|
8992
|
+
* The keychain collection name a kind's secrets live under. Prefixed so a
|
|
8993
|
+
* record's refs never collide with the refs a legacy collection of the same name
|
|
8994
|
+
* already holds during an adoption.
|
|
8995
|
+
*/
|
|
8996
|
+
function recordSecretCollection(kind) {
|
|
8997
|
+
return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
|
|
8998
|
+
}
|
|
8999
|
+
/** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
|
|
9000
|
+
function recordSecretRef(owner, kind, rowId, concretePath) {
|
|
9001
|
+
return buildSecretRef({
|
|
9002
|
+
owner,
|
|
9003
|
+
scope: {
|
|
9004
|
+
kind: "row",
|
|
9005
|
+
collection: recordSecretCollection(kind),
|
|
9006
|
+
rowId
|
|
9007
|
+
},
|
|
9008
|
+
fieldPath: concretePath
|
|
9009
|
+
});
|
|
9010
|
+
}
|
|
9011
|
+
/**
|
|
9012
|
+
* A stored secret, as an admin read shows it: a VIEW naming the row's own
|
|
9013
|
+
* keychain ref (`set: true`), never the value and no longer the bare redaction
|
|
9014
|
+
* token. The form hands it to the secret field, which reveals the value by ref
|
|
9015
|
+
* through the audited `keychain-admin.reveal`; sent back untouched it keeps the
|
|
9016
|
+
* stored secret (the records door reads a view as KEEP).
|
|
9017
|
+
*
|
|
9018
|
+
* ONE implementation for the entries service and for every addon that serves
|
|
9019
|
+
* its own instances (`RecordIntegrationInstances`).
|
|
9020
|
+
*/
|
|
9021
|
+
function isRecord$3(value) {
|
|
9022
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9023
|
+
}
|
|
9024
|
+
function secretViewsOf(owner, kind, id, decl, data) {
|
|
9025
|
+
if (decl.secretPaths.length === 0) return data;
|
|
9026
|
+
const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
|
|
9027
|
+
if (leaf.value !== "__camstack_redacted__") return leaf.value;
|
|
9028
|
+
return {
|
|
9029
|
+
$secret: recordSecretRef(owner, kind, id, leaf.concrete),
|
|
9030
|
+
v: 1,
|
|
9031
|
+
set: true
|
|
9032
|
+
};
|
|
9033
|
+
});
|
|
9034
|
+
return isRecord$3(shown) ? shown : data;
|
|
9035
|
+
}
|
|
8585
9036
|
/**
|
|
8586
9037
|
* The share-view scope kinds a share-link token can carry. The hub mints
|
|
8587
9038
|
* and validates them (`ShareViewGrantSchema` builds its
|
|
@@ -8654,7 +9105,8 @@ function isZodSecret(schema) {
|
|
|
8654
9105
|
}
|
|
8655
9106
|
/**
|
|
8656
9107
|
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
8657
|
-
* element of this array" (`connection.password`, `rows[].token`)
|
|
9108
|
+
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
9109
|
+
* map" (`applianceSecrets{}`). A declared
|
|
8658
9110
|
* object or array is reported as one path and not descended into — everything
|
|
8659
9111
|
* under it is secret.
|
|
8660
9112
|
*/
|
|
@@ -8665,6 +9117,7 @@ function collectZodSecretPaths(schema, prefix = "") {
|
|
|
8665
9117
|
const def = defOf(layer);
|
|
8666
9118
|
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
8667
9119
|
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
9120
|
+
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
8668
9121
|
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
8669
9122
|
else if (def.type === "intersection") {
|
|
8670
9123
|
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
@@ -8739,6 +9192,126 @@ var RecordKindDeclarationSchema = object({
|
|
|
8739
9192
|
}).strict();
|
|
8740
9193
|
/** The kind names: lower-case, dash separated, short enough for an index name. */
|
|
8741
9194
|
var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
|
|
9195
|
+
/** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
|
|
9196
|
+
function assertValidDeclaration(name, decl) {
|
|
9197
|
+
const refuse = (why) => {
|
|
9198
|
+
throw new DeclarationRefusedError(`kind "${name}": ${why}`);
|
|
9199
|
+
};
|
|
9200
|
+
if (!RecordKindNameSchema.safeParse(name).success) refuse("the name must be lower-case-dashed");
|
|
9201
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9202
|
+
for (const index of decl.indexes) {
|
|
9203
|
+
if (seen.has(index.name)) refuse(`index name "${index.name}" is declared twice`);
|
|
9204
|
+
seen.add(index.name);
|
|
9205
|
+
}
|
|
9206
|
+
const { retention } = decl;
|
|
9207
|
+
const hasSecrets = decl.secretPaths.length > 0;
|
|
9208
|
+
if ("ttlMs" in retention) {
|
|
9209
|
+
if (retention.from === "at" && decl.at !== "required") refuse("a ttl from \"at\" needs at: \"required\"");
|
|
9210
|
+
if (hasSecrets) refuse("a kind with secret fields cannot use engine TTL (its keychain values would be orphaned)");
|
|
9211
|
+
}
|
|
9212
|
+
if ("maxRows" in retention || "maxRowsPerDevice" in retention) {
|
|
9213
|
+
if (retention.maxRowsPerDevice !== void 0 && decl.device !== "required") refuse("maxRowsPerDevice needs device: \"required\"");
|
|
9214
|
+
if (decl.at !== "required") refuse("a row bound trims the oldest \"at\": it needs at: \"required\"");
|
|
9215
|
+
if (hasSecrets) refuse("a kind with secret fields cannot use an engine row bound");
|
|
9216
|
+
const bound = Math.max(retention.maxRows ?? 0, retention.maxRowsPerDevice ?? 0);
|
|
9217
|
+
if (decl.database === "settings" && bound > 5e4) refuse(`a row bound above ${RECORD_SETTINGS_MAX_ROWS} belongs in database: "events" (a big delete on the settings thread stalls every configuration read)`);
|
|
9218
|
+
}
|
|
9219
|
+
if (decl.deviceRemoval === "delete" && decl.device === "none") refuse("deviceRemoval: \"delete\" needs a device: \"required\" or \"optional\"");
|
|
9220
|
+
for (const path of decl.secretPaths) for (const prefix of secretArrayPrefixes(path)) if (decl.secretArrayKeys[prefix] === void 0) refuse(`secret path "${path}" crosses the array "${prefix}": declare its id key in secretArrayKeys`);
|
|
9221
|
+
}
|
|
9222
|
+
function bodyFieldsOf(schema) {
|
|
9223
|
+
return { ...toJSONSchema(schema, {
|
|
9224
|
+
unrepresentable: "any",
|
|
9225
|
+
io: "input"
|
|
9226
|
+
}) };
|
|
9227
|
+
}
|
|
9228
|
+
function isRecordLike(value) {
|
|
9229
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9230
|
+
}
|
|
9231
|
+
/** Does every branch of the body schema carry `field`? (A discriminated union has several.) */
|
|
9232
|
+
function hasNameField(json, field) {
|
|
9233
|
+
const branches = json["oneOf"] ?? json["anyOf"];
|
|
9234
|
+
if (Array.isArray(branches)) return branches.length > 0 && branches.every((b) => isRecordLike(b) && hasNameField(b, field));
|
|
9235
|
+
const properties = json["properties"];
|
|
9236
|
+
return isRecordLike(properties) && Object.prototype.hasOwnProperty.call(properties, field);
|
|
9237
|
+
}
|
|
9238
|
+
/**
|
|
9239
|
+
* Declare the kinds of a package. The keys are the kind names. The result is
|
|
9240
|
+
* what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
|
|
9241
|
+
* types its handle from.
|
|
9242
|
+
*/
|
|
9243
|
+
function defineRecordKinds(specs) {
|
|
9244
|
+
const out = {};
|
|
9245
|
+
for (const [name, spec] of Object.entries(specs)) {
|
|
9246
|
+
const secretPaths = collectZodSecretPaths(spec.schema);
|
|
9247
|
+
const nameField = spec.entry?.nameField ?? "name";
|
|
9248
|
+
const fields = bodyFieldsOf(spec.schema);
|
|
9249
|
+
if (spec.entry !== void 0 && !hasNameField(fields, nameField)) throw new DeclarationRefusedError(`kind "${name}": an entry kind's schema needs a "${nameField}" field`);
|
|
9250
|
+
const parsed = RecordKindDeclarationSchema.parse({
|
|
9251
|
+
database: spec.database,
|
|
9252
|
+
class: spec.class,
|
|
9253
|
+
writeTrigger: spec.writeTrigger,
|
|
9254
|
+
device: spec.device,
|
|
9255
|
+
at: spec.at,
|
|
9256
|
+
indexes: spec.indexes === void 0 ? void 0 : [...spec.indexes],
|
|
9257
|
+
retention: spec.retention,
|
|
9258
|
+
deviceRemoval: spec.deviceRemoval,
|
|
9259
|
+
maxDataBytes: spec.maxDataBytes,
|
|
9260
|
+
secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
|
|
9261
|
+
secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
|
|
9262
|
+
transactFamily: spec.transactFamily,
|
|
9263
|
+
entry: spec.entry === void 0 ? void 0 : {
|
|
9264
|
+
title: spec.entry.title,
|
|
9265
|
+
nameField,
|
|
9266
|
+
fields
|
|
9267
|
+
}
|
|
9268
|
+
});
|
|
9269
|
+
assertValidDeclaration(name, parsed);
|
|
9270
|
+
out[name] = {
|
|
9271
|
+
name,
|
|
9272
|
+
schema: spec.schema,
|
|
9273
|
+
declaration: parsed
|
|
9274
|
+
};
|
|
9275
|
+
}
|
|
9276
|
+
return out;
|
|
9277
|
+
}
|
|
9278
|
+
/**
|
|
9279
|
+
* Entry kinds (D881): every list of user-created entries (notification
|
|
9280
|
+
* targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
|
|
9281
|
+
* kind-specific body, and the admin UI lists/adds/edits/deletes all of them
|
|
9282
|
+
* through the `record-entries` capability with password fields derived from
|
|
9283
|
+
* the schema (`.meta({ secret: true })`).
|
|
9284
|
+
*/
|
|
9285
|
+
/** The body of a plain entry: a name and the fields the kind adds. */
|
|
9286
|
+
function entryBody(shape) {
|
|
9287
|
+
return object({
|
|
9288
|
+
name: string().min(1),
|
|
9289
|
+
...shape
|
|
9290
|
+
});
|
|
9291
|
+
}
|
|
9292
|
+
/**
|
|
9293
|
+
* Declare one entry kind. An entry is configuration: never trimmed by the
|
|
9294
|
+
* engine, never aged, belonging to no device.
|
|
9295
|
+
*/
|
|
9296
|
+
function defineEntryKind(name, spec) {
|
|
9297
|
+
const defined = defineRecordKinds({ [name]: {
|
|
9298
|
+
schema: spec.schema,
|
|
9299
|
+
class: "config",
|
|
9300
|
+
writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
|
|
9301
|
+
retention: { none: "an entry goes only when the operator deletes it" },
|
|
9302
|
+
...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
|
|
9303
|
+
...spec.database === void 0 ? {} : { database: spec.database },
|
|
9304
|
+
...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
|
|
9305
|
+
...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
|
|
9306
|
+
...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
|
|
9307
|
+
entry: {
|
|
9308
|
+
title: spec.title,
|
|
9309
|
+
...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
|
|
9310
|
+
}
|
|
9311
|
+
} })[name];
|
|
9312
|
+
if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
|
|
9313
|
+
return defined;
|
|
9314
|
+
}
|
|
8742
9315
|
object({
|
|
8743
9316
|
field: string().min(1),
|
|
8744
9317
|
on: literal("advance")
|
|
@@ -11418,7 +11991,7 @@ object({
|
|
|
11418
11991
|
params: record(string(), union([string(), number()])).optional()
|
|
11419
11992
|
});
|
|
11420
11993
|
//#endregion
|
|
11421
|
-
//#region ../types/dist/composition-
|
|
11994
|
+
//#region ../types/dist/composition-CRNwcFYY.mjs
|
|
11422
11995
|
/**
|
|
11423
11996
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
11424
11997
|
*
|
|
@@ -12181,38 +12754,6 @@ function validateExpressionSource(src) {
|
|
|
12181
12754
|
return null;
|
|
12182
12755
|
}
|
|
12183
12756
|
/**
|
|
12184
|
-
* One power reading becomes a washer, dryer or dishwasher (D944 helper).
|
|
12185
|
-
*
|
|
12186
|
-
* Standby of the control board is under 5 W; a motor or a heater is far
|
|
12187
|
-
* above that, so 5 W is the line. A door lock or a short pump spike is not
|
|
12188
|
-
* a cycle, so running starts only after 30 s above the line.
|
|
12189
|
-
*
|
|
12190
|
-
* A wash, dry or dishwasher pause (soak, heat-wait, heater off between
|
|
12191
|
-
* bursts) lasts several minutes. Ending the cycle on the first dip would
|
|
12192
|
-
* report "finished" in the middle of the program, so the stop dwell is
|
|
12193
|
-
* 10 minutes on every type. The number is stored per type so one machine
|
|
12194
|
-
* can move later without moving the others.
|
|
12195
|
-
*/
|
|
12196
|
-
var APPLIANCE_TEMPLATE_ID = "appliance";
|
|
12197
|
-
var APPLIANCE_KINDS = [
|
|
12198
|
-
"washer",
|
|
12199
|
-
"dryer",
|
|
12200
|
-
"dishwasher"
|
|
12201
|
-
];
|
|
12202
|
-
DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
|
|
12203
|
-
/**
|
|
12204
|
-
* A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
|
|
12205
|
-
*
|
|
12206
|
-
* The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
|
|
12207
|
-
* (D967), so the heater switches exactly as the plug does. Running is the
|
|
12208
|
-
* plug being on; when a power reading is also named, running is the draw
|
|
12209
|
-
* staying above the line instead (a plug left on with the stove's own
|
|
12210
|
-
* thermostat open is not heating). 50 W is far above any standby and below
|
|
12211
|
-
* the smallest radiator.
|
|
12212
|
-
*/
|
|
12213
|
-
var HEATER_TEMPLATE_ID = "heater";
|
|
12214
|
-
DeviceType.Heater;
|
|
12215
|
-
/**
|
|
12216
12757
|
* Statistics and integrals over a history read (D944).
|
|
12217
12758
|
*
|
|
12218
12759
|
* Pure: the same points always give the same answer, so a restart recomputes
|
|
@@ -12356,34 +12897,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
|
|
|
12356
12897
|
/** Actuator groups only. Readings leave this unset. */
|
|
12357
12898
|
active: ActiveCriterionSchema.optional()
|
|
12358
12899
|
}),
|
|
12359
|
-
object({
|
|
12360
|
-
kind: literal("appliance"),
|
|
12361
|
-
templateId: literal(APPLIANCE_TEMPLATE_ID),
|
|
12362
|
-
appliance: _enum(APPLIANCE_KINDS),
|
|
12363
|
-
thresholdWatts: number().positive(),
|
|
12364
|
-
startDwellMs: number().int().nonnegative(),
|
|
12365
|
-
stopDwellMs: number().int().nonnegative(),
|
|
12366
|
-
member: HelperMemberSchema,
|
|
12367
|
-
/**
|
|
12368
|
-
* The plug that powers the appliance (`switch.on`). Optional: when set, the
|
|
12369
|
-
* composition carries a `power-source` feature, so the hub can cut and restore
|
|
12370
|
-
* that plug (power-source link of a NEW-target composition).
|
|
12371
|
-
*/
|
|
12372
|
-
powerSwitch: HelperMemberSchema.optional(),
|
|
12373
|
-
/** How long a forced restart keeps the plug off; the cap's default when unset. */
|
|
12374
|
-
offSeconds: number().optional()
|
|
12375
|
-
}),
|
|
12376
|
-
object({
|
|
12377
|
-
kind: literal("heater"),
|
|
12378
|
-
templateId: literal(HEATER_TEMPLATE_ID),
|
|
12379
|
-
/** The plug, read and commanded at `switch.on`. */
|
|
12380
|
-
plug: HelperMemberSchema,
|
|
12381
|
-
/** The plug's power reading, when it has one: running is then the draw, not the switch. */
|
|
12382
|
-
meter: HelperMemberSchema.optional(),
|
|
12383
|
-
thresholdWatts: number().positive(),
|
|
12384
|
-
startDwellMs: number().int().nonnegative(),
|
|
12385
|
-
stopDwellMs: number().int().nonnegative()
|
|
12386
|
-
}),
|
|
12387
12900
|
object({
|
|
12388
12901
|
kind: literal("statistic"),
|
|
12389
12902
|
stat: _enum(STATISTIC_STATS),
|
|
@@ -12607,7 +13120,7 @@ var CompositionSchema = object({
|
|
|
12607
13120
|
kind: literal("existing"),
|
|
12608
13121
|
device: CompositionSourceRefSchema
|
|
12609
13122
|
})]),
|
|
12610
|
-
features: array(CompositionFeatureSchema).min(
|
|
13123
|
+
features: array(CompositionFeatureSchema).min(0).max(16),
|
|
12611
13124
|
/**
|
|
12612
13125
|
* Present when the block is a helper template. Absent on a free
|
|
12613
13126
|
* composition (the room example, device 4603). The editor reads this,
|
|
@@ -12825,6 +13338,205 @@ var IntegrationInstanceRefSchema = object({
|
|
|
12825
13338
|
]),
|
|
12826
13339
|
candidateStableId: string().optional()
|
|
12827
13340
|
});
|
|
13341
|
+
/**
|
|
13342
|
+
* `RecordIntegrationInstances` - the `integration-instances` provider over ONE
|
|
13343
|
+
* entry kind, so an addon that holds a connection serves it in ~30 lines.
|
|
13344
|
+
*
|
|
13345
|
+
* Every instance is one record of the addon's own kind, keyed by the
|
|
13346
|
+
* `integrations` registry id. The records door does the secret work: a field the
|
|
13347
|
+
* kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
|
|
13348
|
+
* deleting the row deletes the secret, and one row's value is never another's.
|
|
13349
|
+
*
|
|
13350
|
+
* Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
|
|
13351
|
+
* token or the view the form was given KEEP the stored secret;
|
|
13352
|
+
* `{ $secretClear: true }` clears it; any other string replaces it.
|
|
13353
|
+
*/
|
|
13354
|
+
/** The most instances one addon lists (an integrations page, not a table). */
|
|
13355
|
+
var INSTANCES_LIST_MAX = 1e3;
|
|
13356
|
+
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
13357
|
+
function topLevelSecretKeys(paths) {
|
|
13358
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
|
|
13359
|
+
}
|
|
13360
|
+
/** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
|
|
13361
|
+
function secretMapKeys(paths) {
|
|
13362
|
+
return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
|
|
13363
|
+
}
|
|
13364
|
+
/**
|
|
13365
|
+
* A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
|
|
13366
|
+
* view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
|
|
13367
|
+
* or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
|
|
13368
|
+
*/
|
|
13369
|
+
function secretMapPatch(value) {
|
|
13370
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
13371
|
+
const out = {};
|
|
13372
|
+
for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
|
|
13373
|
+
else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
|
|
13374
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
13375
|
+
}
|
|
13376
|
+
/** Does the kind's body schema carry an `enabled` field? */
|
|
13377
|
+
function declaresEnabled(kind) {
|
|
13378
|
+
const json = toJSONSchema(kind.schema, {
|
|
13379
|
+
unrepresentable: "any",
|
|
13380
|
+
io: "input"
|
|
13381
|
+
});
|
|
13382
|
+
const properties = Reflect.get(json, "properties");
|
|
13383
|
+
return typeof properties === "object" && properties !== null && "enabled" in properties;
|
|
13384
|
+
}
|
|
13385
|
+
var RecordIntegrationInstances = class {
|
|
13386
|
+
records;
|
|
13387
|
+
kind;
|
|
13388
|
+
hooks;
|
|
13389
|
+
handle;
|
|
13390
|
+
secretKeys;
|
|
13391
|
+
secretMaps;
|
|
13392
|
+
nameField;
|
|
13393
|
+
/** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
|
|
13394
|
+
registryOwnsEnabled;
|
|
13395
|
+
constructor(records, kind, hooks) {
|
|
13396
|
+
this.records = records;
|
|
13397
|
+
this.kind = kind;
|
|
13398
|
+
this.hooks = hooks;
|
|
13399
|
+
this.handle = records.kind(kind);
|
|
13400
|
+
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
13401
|
+
this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
|
|
13402
|
+
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
13403
|
+
this.registryOwnsEnabled = hooks.description.role === "devices";
|
|
13404
|
+
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"`);
|
|
13405
|
+
}
|
|
13406
|
+
/**
|
|
13407
|
+
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
13408
|
+
* locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
|
|
13409
|
+
* caller can keep what it had instead of reconfiguring with an empty secret (D49).
|
|
13410
|
+
*/
|
|
13411
|
+
async readResolved(id) {
|
|
13412
|
+
try {
|
|
13413
|
+
const record = await this.handle.getResolved(id);
|
|
13414
|
+
return record === null ? null : {
|
|
13415
|
+
id,
|
|
13416
|
+
body: record.data,
|
|
13417
|
+
enabled: record.data.enabled !== false,
|
|
13418
|
+
secrets: "resolved"
|
|
13419
|
+
};
|
|
13420
|
+
} catch (err) {
|
|
13421
|
+
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
13422
|
+
const stored = await this.handle.get(id);
|
|
13423
|
+
if (stored === null) return null;
|
|
13424
|
+
return {
|
|
13425
|
+
id,
|
|
13426
|
+
body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
|
|
13427
|
+
enabled: stored.data.enabled !== false,
|
|
13428
|
+
secrets: "unavailable"
|
|
13429
|
+
};
|
|
13430
|
+
}
|
|
13431
|
+
}
|
|
13432
|
+
/** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
|
|
13433
|
+
async getResolved(id) {
|
|
13434
|
+
return (await this.readResolved(id))?.body ?? null;
|
|
13435
|
+
}
|
|
13436
|
+
/** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
|
|
13437
|
+
async listResolved() {
|
|
13438
|
+
const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
|
|
13439
|
+
const out = [];
|
|
13440
|
+
for (const record of page.records) {
|
|
13441
|
+
const read = await this.readResolved(record.id);
|
|
13442
|
+
if (read !== null) out.push(read);
|
|
13443
|
+
}
|
|
13444
|
+
return out;
|
|
13445
|
+
}
|
|
13446
|
+
viewOf(record) {
|
|
13447
|
+
const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
|
|
13448
|
+
const rawName = record.data[this.nameField];
|
|
13449
|
+
const kind = this.hooks.kindOf?.(record.data);
|
|
13450
|
+
const health = this.hooks.health?.(record.data, record.id);
|
|
13451
|
+
return {
|
|
13452
|
+
id: record.id,
|
|
13453
|
+
name: typeof rawName === "string" ? rawName : record.id,
|
|
13454
|
+
configured: this.hooks.configured(record.data),
|
|
13455
|
+
...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
|
|
13456
|
+
formValues: shown,
|
|
13457
|
+
...kind !== void 0 ? { kind } : {},
|
|
13458
|
+
...health?.status !== void 0 ? { status: health.status } : {},
|
|
13459
|
+
...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
|
|
13460
|
+
};
|
|
13461
|
+
}
|
|
13462
|
+
describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
|
|
13463
|
+
...this.hooks.description,
|
|
13464
|
+
mintsId: true
|
|
13465
|
+
};
|
|
13466
|
+
instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
|
|
13467
|
+
listInstances = async () => {
|
|
13468
|
+
return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
|
|
13469
|
+
};
|
|
13470
|
+
getInstance = async ({ id }) => {
|
|
13471
|
+
const record = await this.handle.get(id);
|
|
13472
|
+
return record === null ? null : this.viewOf(record);
|
|
13473
|
+
};
|
|
13474
|
+
/** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
|
|
13475
|
+
createInstance = async ({ id, name, kind }) => {
|
|
13476
|
+
const minted = id ?? this.hooks.mintId?.({
|
|
13477
|
+
name,
|
|
13478
|
+
...kind !== void 0 ? { kind } : {}
|
|
13479
|
+
});
|
|
13480
|
+
if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
|
|
13481
|
+
await this.handle.put({
|
|
13482
|
+
id: minted,
|
|
13483
|
+
data: this.hooks.initialBody(name, kind)
|
|
13484
|
+
}, { ifRev: 0 });
|
|
13485
|
+
await this.hooks.onChanged?.(minted, "created");
|
|
13486
|
+
return { id: minted };
|
|
13487
|
+
};
|
|
13488
|
+
updateInstance = async ({ id, name, values }) => {
|
|
13489
|
+
const patch = {};
|
|
13490
|
+
for (const [key, value] of Object.entries(values)) {
|
|
13491
|
+
if (value === void 0) continue;
|
|
13492
|
+
if (this.secretMaps.has(key)) {
|
|
13493
|
+
const entries = secretMapPatch(value);
|
|
13494
|
+
if (entries !== null) patch[key] = entries;
|
|
13495
|
+
} else if (!this.secretKeys.has(key)) patch[key] = value;
|
|
13496
|
+
else if (isSecretClear(value)) patch[key] = null;
|
|
13497
|
+
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13498
|
+
}
|
|
13499
|
+
if (name !== void 0) patch[this.nameField] = name;
|
|
13500
|
+
await this.handle.patch(id, patch);
|
|
13501
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
13502
|
+
return { ok: true };
|
|
13503
|
+
};
|
|
13504
|
+
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13505
|
+
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13506
|
+
if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
|
|
13507
|
+
await this.handle.patch(id, { enabled });
|
|
13508
|
+
await this.hooks.onChanged?.(id, "enabled");
|
|
13509
|
+
return { ok: true };
|
|
13510
|
+
};
|
|
13511
|
+
/** The row and its keychain refs go together (the records door). */
|
|
13512
|
+
deleteInstance = async ({ id }) => {
|
|
13513
|
+
await this.handle.delete(id);
|
|
13514
|
+
await this.hooks.onChanged?.(id, "deleted");
|
|
13515
|
+
return { ok: true };
|
|
13516
|
+
};
|
|
13517
|
+
testInstance = async (input) => {
|
|
13518
|
+
if (this.hooks.test === void 0) return {
|
|
13519
|
+
success: false,
|
|
13520
|
+
status: "unsupported",
|
|
13521
|
+
error: "This integration declares no connection test"
|
|
13522
|
+
};
|
|
13523
|
+
return this.hooks.test(input);
|
|
13524
|
+
};
|
|
13525
|
+
/** The flat provider object to hand to `registerProvider`. */
|
|
13526
|
+
toProvider() {
|
|
13527
|
+
return {
|
|
13528
|
+
describe: this.describe,
|
|
13529
|
+
instanceFormSchema: this.instanceFormSchema,
|
|
13530
|
+
listInstances: this.listInstances,
|
|
13531
|
+
getInstance: this.getInstance,
|
|
13532
|
+
createInstance: this.createInstance,
|
|
13533
|
+
updateInstance: this.updateInstance,
|
|
13534
|
+
setInstanceEnabled: this.setInstanceEnabled,
|
|
13535
|
+
deleteInstance: this.deleteInstance,
|
|
13536
|
+
testInstance: this.testInstance
|
|
13537
|
+
};
|
|
13538
|
+
}
|
|
13539
|
+
};
|
|
12828
13540
|
var EncodeProfileSchema = object({
|
|
12829
13541
|
video: object({
|
|
12830
13542
|
codec: _enum([
|
|
@@ -13860,7 +14572,13 @@ var MigrationLedgerEntrySchema = object({
|
|
|
13860
14572
|
* joined by the engine from the catalogues it knows this boot. Absent when no
|
|
13861
14573
|
* catalogue lists the id (a newer database).
|
|
13862
14574
|
*/
|
|
13863
|
-
meta: MigrationMetaSchema.optional()
|
|
14575
|
+
meta: MigrationMetaSchema.optional(),
|
|
14576
|
+
/**
|
|
14577
|
+
* Set when this is an applied row of an addon owner whose catalogue the answering
|
|
14578
|
+
* process has not loaded (the owner has not declared yet): the row is applied, the
|
|
14579
|
+
* process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
|
|
14580
|
+
*/
|
|
14581
|
+
ownerNotLoaded: boolean().optional()
|
|
13864
14582
|
});
|
|
13865
14583
|
/** A catalogued migration that is not applied, with where it stands. */
|
|
13866
14584
|
var MigrationPendingEntrySchema = object({
|
|
@@ -13894,6 +14612,221 @@ var MigrationOwnerStepSchema = object({
|
|
|
13894
14612
|
state: union([literal("pending"), MigrationStatusSchema])
|
|
13895
14613
|
});
|
|
13896
14614
|
/**
|
|
14615
|
+
* Move an integration's legacy `integration_settings` rows into a typed record of the addon's own
|
|
14616
|
+
* kind (planning#416, the one store). The record is keyed by the `integrations` registry id, so
|
|
14617
|
+
* the integration id, the devices stamped with it and the automations that name it do not change.
|
|
14618
|
+
*
|
|
14619
|
+
* Order, so a crash or a locked keychain is safe at every point:
|
|
14620
|
+
*
|
|
14621
|
+
* 1. Phase A, read only: every row of every integration of the addon is read with its secrets
|
|
14622
|
+
* resolved. One that cannot be read (a locked keychain) throws, naming the integration, before
|
|
14623
|
+
* anything is written; `block-owner` then holds the addon.
|
|
14624
|
+
* 2. Phase B, per integration: the addon's `bodyFrom` builds the body; the record is put with
|
|
14625
|
+
* `ifRev: 0` (an existing record is never overwritten). A record this pass CREATED is read back
|
|
14626
|
+
* and every secret leaf must equal what the rows held, else the record is deleted and the pass
|
|
14627
|
+
* throws; an EXISTING record must hold every secret the rows carry, else the rows are kept and
|
|
14628
|
+
* a warning names the path. Only then are the rows (and their keychain values) cleared.
|
|
14629
|
+
*
|
|
14630
|
+
* Idempotent: an integration whose rows are gone and whose record exists is skipped, so a rerun is
|
|
14631
|
+
* a no-op. A default integration with no rows still gets its record (an empty body, D985).
|
|
14632
|
+
* Never logs or throws a secret value; messages name integration ids and secret PATHS only.
|
|
14633
|
+
*/
|
|
14634
|
+
function secretLeavesOf(body, spec) {
|
|
14635
|
+
const out = /* @__PURE__ */ new Map();
|
|
14636
|
+
for (const leaf of collectSecretLeaves(body, spec.secretPaths, spec.secretArrayKeys ?? {})) if (typeof leaf.value === "string" && leaf.value.length > 0) out.set(leaf.concrete, leaf.value);
|
|
14637
|
+
return out;
|
|
14638
|
+
}
|
|
14639
|
+
function why(err) {
|
|
14640
|
+
return err instanceof Error ? err.message : String(err);
|
|
14641
|
+
}
|
|
14642
|
+
async function moveIntegrationRowsToRecords(ports, spec) {
|
|
14643
|
+
const integrations = await ports.listIntegrations(spec.addonId);
|
|
14644
|
+
if (integrations.length === 0) return {
|
|
14645
|
+
outcome: "already-done",
|
|
14646
|
+
summary: { integrations: 0 }
|
|
14647
|
+
};
|
|
14648
|
+
const rowsOf = /* @__PURE__ */ new Map();
|
|
14649
|
+
for (const integration of integrations) try {
|
|
14650
|
+
rowsOf.set(integration.id, await ports.readRows(integration.id));
|
|
14651
|
+
} catch (err) {
|
|
14652
|
+
throw new Error(`integration '${integration.id}': the legacy settings cannot be read with their secrets (${why(err)}) - nothing moved`, { cause: err });
|
|
14653
|
+
}
|
|
14654
|
+
let created = 0;
|
|
14655
|
+
let cleared = 0;
|
|
14656
|
+
let kept = 0;
|
|
14657
|
+
for (const integration of integrations) {
|
|
14658
|
+
const values = rowsOf.get(integration.id) ?? {};
|
|
14659
|
+
const hasRows = Object.keys(values).length > 0;
|
|
14660
|
+
const body = spec.bodyFrom(values, ports, integration);
|
|
14661
|
+
const put = await ports.putRecord(integration.id, body);
|
|
14662
|
+
if (put === "created") created += 1;
|
|
14663
|
+
if (!hasRows) continue;
|
|
14664
|
+
const wanted = secretLeavesOf(body, spec);
|
|
14665
|
+
const back = await ports.readRecord(integration.id);
|
|
14666
|
+
if (back === null) throw new Error(`integration '${integration.id}': the record is gone after it was put`);
|
|
14667
|
+
const held = secretLeavesOf(back, spec);
|
|
14668
|
+
if (put === "created") {
|
|
14669
|
+
const wrong = [...wanted].filter(([path, value]) => held.get(path) !== value).map(([p]) => p);
|
|
14670
|
+
if (wrong.length > 0) {
|
|
14671
|
+
await ports.deleteRecord(integration.id);
|
|
14672
|
+
throw new Error(`integration '${integration.id}': ${wrong.join(", ")} did not read back from the record - the record was removed and the legacy settings are kept`);
|
|
14673
|
+
}
|
|
14674
|
+
} else {
|
|
14675
|
+
const missing = [...wanted].filter(([path]) => !held.has(path)).map(([p]) => p);
|
|
14676
|
+
if (missing.length > 0) {
|
|
14677
|
+
ports.warn(`integration '${integration.id}': the existing record holds no ${missing.join(", ")}; the legacy settings are kept, not cleared`);
|
|
14678
|
+
kept += 1;
|
|
14679
|
+
continue;
|
|
14680
|
+
}
|
|
14681
|
+
}
|
|
14682
|
+
await ports.clearRows(integration.id);
|
|
14683
|
+
cleared += 1;
|
|
14684
|
+
}
|
|
14685
|
+
return created === 0 && cleared === 0 ? {
|
|
14686
|
+
outcome: "already-done",
|
|
14687
|
+
summary: {
|
|
14688
|
+
integrations: integrations.length,
|
|
14689
|
+
kept
|
|
14690
|
+
}
|
|
14691
|
+
} : {
|
|
14692
|
+
outcome: "done",
|
|
14693
|
+
summary: {
|
|
14694
|
+
integrations: integrations.length,
|
|
14695
|
+
created,
|
|
14696
|
+
cleared,
|
|
14697
|
+
kept
|
|
14698
|
+
}
|
|
14699
|
+
};
|
|
14700
|
+
}
|
|
14701
|
+
/**
|
|
14702
|
+
* The addon-side ports of `moveIntegrationRowsToRecords` (planning#416): an addon runs the move
|
|
14703
|
+
* from its own process, so the legacy rows are reached through the hub (`ctx.api.integrations`:
|
|
14704
|
+
* `list`, `getSettings` - which answers the OWNER its secrets resolved - and `clearSettings`) and
|
|
14705
|
+
* the new record through the addon's own records handle. The hub deletes the rows and their
|
|
14706
|
+
* keychain values; the addon never touches the registry.
|
|
14707
|
+
*/
|
|
14708
|
+
function integrationRowsPortsOverApi(deps) {
|
|
14709
|
+
return {
|
|
14710
|
+
warn: (message) => deps.log.warn(message),
|
|
14711
|
+
listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
|
|
14712
|
+
id: i.id,
|
|
14713
|
+
name: i.name
|
|
14714
|
+
})),
|
|
14715
|
+
readRows: (integrationId) => deps.api.getSettings({ id: integrationId }),
|
|
14716
|
+
putRecord: async (id, body) => {
|
|
14717
|
+
if (await deps.records.get(id) !== null) return "exists";
|
|
14718
|
+
try {
|
|
14719
|
+
await deps.records.put({
|
|
14720
|
+
id,
|
|
14721
|
+
data: body
|
|
14722
|
+
}, { ifRev: 0 });
|
|
14723
|
+
} catch (err) {
|
|
14724
|
+
if (err instanceof RevConflictError) return "exists";
|
|
14725
|
+
throw err;
|
|
14726
|
+
}
|
|
14727
|
+
return "created";
|
|
14728
|
+
},
|
|
14729
|
+
readRecord: async (id) => (await deps.records.getResolved(id))?.data ?? null,
|
|
14730
|
+
deleteRecord: async (id) => {
|
|
14731
|
+
await deps.records.delete(id);
|
|
14732
|
+
},
|
|
14733
|
+
clearRows: async (integrationId) => {
|
|
14734
|
+
await deps.api.clearSettings({ id: integrationId });
|
|
14735
|
+
}
|
|
14736
|
+
};
|
|
14737
|
+
}
|
|
14738
|
+
/**
|
|
14739
|
+
* The rows-to-record migration of a device addon whose integrations hold REAL settings
|
|
14740
|
+
* (planning#416, S-B): every integration of the addon becomes a record of the addon's own kind
|
|
14741
|
+
* under its registry id, the record body built from the legacy `integration_settings` rows, and
|
|
14742
|
+
* the rows and their keychain values are cleared once the record is read back
|
|
14743
|
+
* (`moveIntegrationRowsToRecords`). The addon declares ONE entry kind; this file turns that kind
|
|
14744
|
+
* into the body, the migration and its ports, so an addon states nothing but the kind.
|
|
14745
|
+
*
|
|
14746
|
+
* It runs in the addon process through the hub API, BEFORE the addon registers its
|
|
14747
|
+
* `integration-instances` provider, and nothing reads the old rows afterwards.
|
|
14748
|
+
*/
|
|
14749
|
+
/**
|
|
14750
|
+
* The record body of one integration from its resolved legacy rows. A key the kind has no field
|
|
14751
|
+
* for, or whose value the field refuses, is dropped and named in a warning (keys only, never a
|
|
14752
|
+
* value); a blank value is no value. A secret the keychain could not serve (a marker, not a
|
|
14753
|
+
* string) throws: moving it would lose it, and the rows are still there.
|
|
14754
|
+
*/
|
|
14755
|
+
function integrationBodyFromRows(kind, values, log, integration) {
|
|
14756
|
+
const fields = new Map(Object.entries(kind.schema.shape));
|
|
14757
|
+
const body = { name: integration.name };
|
|
14758
|
+
const dropped = [];
|
|
14759
|
+
for (const [key, value] of Object.entries(values)) {
|
|
14760
|
+
if (isSecretMarkerLike(value)) throw new Error(`integration '${integration.id}': ${key} could not be read from the keychain - nothing moved`);
|
|
14761
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
14762
|
+
const field = key === "name" ? void 0 : fields.get(key);
|
|
14763
|
+
if (field === void 0 || !field.safeParse(value).success) {
|
|
14764
|
+
dropped.push(key);
|
|
14765
|
+
continue;
|
|
14766
|
+
}
|
|
14767
|
+
body[key] = value;
|
|
14768
|
+
}
|
|
14769
|
+
if (dropped.length > 0) log.warn(`integration '${integration.id}': legacy settings the ${kind.name} record has no valid field for are dropped: ${dropped.join(", ")}`);
|
|
14770
|
+
return kind.schema.parse(body);
|
|
14771
|
+
}
|
|
14772
|
+
function integrationRowsToRecordId(addonId, date) {
|
|
14773
|
+
return `${addonId}/${date}-integration-rows-to-record`;
|
|
14774
|
+
}
|
|
14775
|
+
function integrationRowsMigration(spec) {
|
|
14776
|
+
return {
|
|
14777
|
+
meta: {
|
|
14778
|
+
id: integrationRowsToRecordId(spec.addonId, spec.date),
|
|
14779
|
+
owner: spec.addonId,
|
|
14780
|
+
level: "addon-state",
|
|
14781
|
+
seq: 1,
|
|
14782
|
+
dependsOn: [],
|
|
14783
|
+
store: "settings",
|
|
14784
|
+
scope: "cluster",
|
|
14785
|
+
onFailure: "block-owner",
|
|
14786
|
+
bodyKind: "procedure",
|
|
14787
|
+
description: "Move every integration of the addon into a typed instance record keyed by its registry id, with its connection settings and secrets, then clear the legacy integration settings rows",
|
|
14788
|
+
fixtureMark: spec.fixtureMark
|
|
14789
|
+
},
|
|
14790
|
+
procedure: (ports) => moveIntegrationRowsToRecords(ports, {
|
|
14791
|
+
addonId: spec.addonId,
|
|
14792
|
+
secretPaths: spec.kind.declaration.secretPaths,
|
|
14793
|
+
secretArrayKeys: spec.kind.declaration.secretArrayKeys,
|
|
14794
|
+
bodyFrom: (values, log, integration) => integrationBodyFromRows(spec.kind, values, log, integration)
|
|
14795
|
+
})
|
|
14796
|
+
};
|
|
14797
|
+
}
|
|
14798
|
+
/** The hub and records sides of {@link integrationRowsMigration}. */
|
|
14799
|
+
function integrationRowsPortsOver(deps) {
|
|
14800
|
+
return integrationRowsPortsOverApi({
|
|
14801
|
+
api: deps.api,
|
|
14802
|
+
records: deps.records.kind({
|
|
14803
|
+
name: deps.kind.name,
|
|
14804
|
+
declaration: deps.kind.declaration,
|
|
14805
|
+
schema: record(string(), unknown())
|
|
14806
|
+
}),
|
|
14807
|
+
log: { warn: deps.warn }
|
|
14808
|
+
});
|
|
14809
|
+
}
|
|
14810
|
+
function defaultIntegrationApiOver(api) {
|
|
14811
|
+
return {
|
|
14812
|
+
list: () => api.integrations.list.query(),
|
|
14813
|
+
getSettings: (input) => api.integrations.getSettings.query(input),
|
|
14814
|
+
clearSettings: (input) => api.integrations.clearSettings.mutate(input),
|
|
14815
|
+
listDevices: (addonId) => api.deviceManager.listAll.query({
|
|
14816
|
+
addonId,
|
|
14817
|
+
projection: "slim"
|
|
14818
|
+
}),
|
|
14819
|
+
setDeviceIntegrationId: async (deviceId, integrationId) => {
|
|
14820
|
+
await api.deviceManager.setIntegrationId.mutate({
|
|
14821
|
+
deviceId,
|
|
14822
|
+
integrationId
|
|
14823
|
+
});
|
|
14824
|
+
},
|
|
14825
|
+
readDeviceHasConfig: async (deviceId) => Object.keys((await api.deviceManager.getDevice.query({ deviceId }))?.config ?? {}).length > 0,
|
|
14826
|
+
readDeviceIntegrationId: async (deviceId) => (await api.deviceManager.getDevice.query({ deviceId }))?.integrationId ?? null
|
|
14827
|
+
};
|
|
14828
|
+
}
|
|
14829
|
+
/**
|
|
13897
14830
|
* The trainer contract — what the hub hands the cloud trainer and what it
|
|
13898
14831
|
* gets back (cloud training, D805+).
|
|
13899
14832
|
*
|
|
@@ -49378,117 +50311,6 @@ function actuator(input) {
|
|
|
49378
50311
|
};
|
|
49379
50312
|
}
|
|
49380
50313
|
new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
|
|
49381
|
-
var PowerSourceRefusalReasonSchema = _enum([
|
|
49382
|
-
"no-power-source",
|
|
49383
|
-
"switch-missing",
|
|
49384
|
-
"switch-offline",
|
|
49385
|
-
"switch-unknown",
|
|
49386
|
-
"restart-in-progress",
|
|
49387
|
-
"switch-call-failed"
|
|
49388
|
-
]);
|
|
49389
|
-
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
49390
|
-
accepted: literal(false),
|
|
49391
|
-
reason: PowerSourceRefusalReasonSchema,
|
|
49392
|
-
/** One line for the operator, naming the switch. */
|
|
49393
|
-
message: string()
|
|
49394
|
-
})]);
|
|
49395
|
-
var PowerSourceInfoSchema = object({
|
|
49396
|
-
/** False when the device has no power source linked; the rest is then empty. */
|
|
49397
|
-
linked: boolean(),
|
|
49398
|
-
switchDeviceId: number().nullable(),
|
|
49399
|
-
switchName: string().nullable(),
|
|
49400
|
-
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
49401
|
-
switchOn: boolean().nullable(),
|
|
49402
|
-
switchOnline: boolean().nullable(),
|
|
49403
|
-
restartInProgress: boolean(),
|
|
49404
|
-
offSeconds: number().int()
|
|
49405
|
-
});
|
|
49406
|
-
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
49407
|
-
var PowerSourceStateSchema = object({
|
|
49408
|
-
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
49409
|
-
switchOn: boolean().nullable(),
|
|
49410
|
-
offSeconds: number().int(),
|
|
49411
|
-
lastFetchedAt: number()
|
|
49412
|
-
});
|
|
49413
|
-
var POWER_SOURCE_DEVICE_TYPES = [
|
|
49414
|
-
DeviceType.Gateway,
|
|
49415
|
-
DeviceType.Hub,
|
|
49416
|
-
DeviceType.Camera,
|
|
49417
|
-
DeviceType.Sensor,
|
|
49418
|
-
DeviceType.MediaPlayer,
|
|
49419
|
-
DeviceType.Vacuum,
|
|
49420
|
-
DeviceType.Generic,
|
|
49421
|
-
DeviceType.Washer,
|
|
49422
|
-
DeviceType.Dryer,
|
|
49423
|
-
DeviceType.Dishwasher,
|
|
49424
|
-
DeviceType.PetFountain,
|
|
49425
|
-
DeviceType.Fan,
|
|
49426
|
-
DeviceType.Humidifier,
|
|
49427
|
-
DeviceType.Climate,
|
|
49428
|
-
DeviceType.WaterHeater
|
|
49429
|
-
];
|
|
49430
|
-
var powerSourceCustomize = {
|
|
49431
|
-
version: 1,
|
|
49432
|
-
targets: [{
|
|
49433
|
-
id: "switch",
|
|
49434
|
-
cardinality: "scalar",
|
|
49435
|
-
vocabulary: {
|
|
49436
|
-
cap: "power-source",
|
|
49437
|
-
stateField: "switchOn"
|
|
49438
|
-
},
|
|
49439
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
49440
|
-
apply: "add",
|
|
49441
|
-
path: "switchOn",
|
|
49442
|
-
sources: [{
|
|
49443
|
-
kind: "switch",
|
|
49444
|
-
transforms: [{ kind: "identity" }]
|
|
49445
|
-
}],
|
|
49446
|
-
companions: [{
|
|
49447
|
-
kind: "constant",
|
|
49448
|
-
path: "offSeconds",
|
|
49449
|
-
value: 10,
|
|
49450
|
-
when: "add"
|
|
49451
|
-
}, {
|
|
49452
|
-
kind: "clock",
|
|
49453
|
-
path: "lastFetchedAt",
|
|
49454
|
-
when: "always"
|
|
49455
|
-
}],
|
|
49456
|
-
sourceReport: { status: "assignment-status" }
|
|
49457
|
-
}]
|
|
49458
|
-
};
|
|
49459
|
-
var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
|
|
49460
|
-
var powerSourceCapability = {
|
|
49461
|
-
name: "power-source",
|
|
49462
|
-
scope: "device",
|
|
49463
|
-
mode: "singleton",
|
|
49464
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
49465
|
-
methods: {
|
|
49466
|
-
getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }),
|
|
49467
|
-
powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
49468
|
-
kind: "mutation",
|
|
49469
|
-
auth: "admin"
|
|
49470
|
-
}),
|
|
49471
|
-
powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
49472
|
-
kind: "mutation",
|
|
49473
|
-
auth: "admin"
|
|
49474
|
-
}),
|
|
49475
|
-
forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
49476
|
-
kind: "mutation",
|
|
49477
|
-
auth: "admin"
|
|
49478
|
-
})
|
|
49479
|
-
},
|
|
49480
|
-
runtimeState: PowerSourceStateSchema,
|
|
49481
|
-
customize: powerSourceCustomize,
|
|
49482
|
-
/**
|
|
49483
|
-
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
49484
|
-
* its next report; a restored value would outlive the reading it names.
|
|
49485
|
-
*
|
|
49486
|
-
* See `RuntimeStateDurability`. Enforced by
|
|
49487
|
-
* `scripts/check-runtime-state-durability.ts`.
|
|
49488
|
-
*/
|
|
49489
|
-
durability: "session",
|
|
49490
|
-
history: "none"
|
|
49491
|
-
};
|
|
49492
50314
|
DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
|
|
49493
50315
|
DeviceType.Camera;
|
|
49494
50316
|
`${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
|
|
@@ -49516,6 +50338,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
|
|
|
49516
50338
|
}).optional()
|
|
49517
50339
|
})) });
|
|
49518
50340
|
/**
|
|
50341
|
+
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
50342
|
+
* `z.infer`, they do not restate the shape.
|
|
50343
|
+
*/
|
|
50344
|
+
var OverrideSourceSchema = object({
|
|
50345
|
+
addonId: string().min(1),
|
|
50346
|
+
stableId: string().min(1),
|
|
50347
|
+
cap: string().min(1),
|
|
50348
|
+
fieldPath: string().min(1)
|
|
50349
|
+
});
|
|
50350
|
+
var IdentityTransformDraftSchema = object({ kind: literal("identity") });
|
|
50351
|
+
var InvertTransformDraftSchema = object({ kind: literal("invert") });
|
|
50352
|
+
var ThresholdTransformDraftSchema = object({
|
|
50353
|
+
kind: literal("threshold"),
|
|
50354
|
+
comparison: _enum(["gte", "lte"]),
|
|
50355
|
+
threshold: number(),
|
|
50356
|
+
startDwellMs: number().nonnegative(),
|
|
50357
|
+
stopDwellMs: number().nonnegative()
|
|
50358
|
+
});
|
|
50359
|
+
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
50360
|
+
IdentityTransformDraftSchema,
|
|
50361
|
+
InvertTransformDraftSchema,
|
|
50362
|
+
ThresholdTransformDraftSchema,
|
|
50363
|
+
object({
|
|
50364
|
+
kind: literal("linear"),
|
|
50365
|
+
scale: number(),
|
|
50366
|
+
offset: number(),
|
|
50367
|
+
clampMin: number().optional(),
|
|
50368
|
+
clampMax: number().optional()
|
|
50369
|
+
}),
|
|
50370
|
+
object({
|
|
50371
|
+
kind: literal("enum-map"),
|
|
50372
|
+
mapping: record(string(), string().nullable())
|
|
50373
|
+
}),
|
|
50374
|
+
object({ kind: literal("number") })
|
|
50375
|
+
]);
|
|
50376
|
+
var ScalarOverrideDraftSchema = object({
|
|
50377
|
+
kind: literal("scalar"),
|
|
50378
|
+
cap: string().min(1),
|
|
50379
|
+
targetId: string().min(1),
|
|
50380
|
+
source: OverrideSourceSchema,
|
|
50381
|
+
transform: CustomizeTransformDraftSchema
|
|
50382
|
+
});
|
|
50383
|
+
var KeyedArrayOverrideDraftSchema = object({
|
|
50384
|
+
kind: literal("keyedArray"),
|
|
50385
|
+
cap: string().min(1),
|
|
50386
|
+
targetId: string().min(1),
|
|
50387
|
+
key: string().min(1),
|
|
50388
|
+
label: string().min(1),
|
|
50389
|
+
fieldPath: string().min(1),
|
|
50390
|
+
source: OverrideSourceSchema,
|
|
50391
|
+
transform: CustomizeTransformDraftSchema
|
|
50392
|
+
});
|
|
50393
|
+
var PulseOverrideDraftSchema = object({
|
|
50394
|
+
kind: literal("pulse"),
|
|
50395
|
+
cap: string().min(1),
|
|
50396
|
+
targetId: string().min(1),
|
|
50397
|
+
enabled: boolean(),
|
|
50398
|
+
debounceMs: number().nonnegative(),
|
|
50399
|
+
edges: array(_enum([
|
|
50400
|
+
"rising",
|
|
50401
|
+
"button",
|
|
50402
|
+
"native-event"
|
|
50403
|
+
])).min(1).optional(),
|
|
50404
|
+
sources: array(OverrideSourceSchema).min(1)
|
|
50405
|
+
});
|
|
50406
|
+
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
50407
|
+
"in-addition",
|
|
50408
|
+
"confirm",
|
|
50409
|
+
"replace"
|
|
50410
|
+
]).optional() });
|
|
50411
|
+
discriminatedUnion("kind", [
|
|
50412
|
+
ScalarOverrideDraftSchema,
|
|
50413
|
+
KeyedArrayOverrideDraftSchema,
|
|
50414
|
+
PulseOverrideDraftSchema,
|
|
50415
|
+
object({
|
|
50416
|
+
kind: literal("span"),
|
|
50417
|
+
cap: string().min(1),
|
|
50418
|
+
targetId: string().min(1),
|
|
50419
|
+
timeoutMs: number().nonnegative(),
|
|
50420
|
+
cooldownMs: number().nonnegative().optional(),
|
|
50421
|
+
sources: array(SpanSourceDraftSchema)
|
|
50422
|
+
})
|
|
50423
|
+
]);
|
|
50424
|
+
object({
|
|
50425
|
+
targetId: string().min(1),
|
|
50426
|
+
configured: boolean(),
|
|
50427
|
+
source: _enum(["native", "assigned"]),
|
|
50428
|
+
state: _enum([
|
|
50429
|
+
"active",
|
|
50430
|
+
"stale",
|
|
50431
|
+
"released",
|
|
50432
|
+
"unavailable"
|
|
50433
|
+
]),
|
|
50434
|
+
reason: string().nullable()
|
|
50435
|
+
});
|
|
50436
|
+
/**
|
|
50437
|
+
* What a running-threshold proposal says (planning#405). The schema is the
|
|
50438
|
+
* type: the algorithm, the capability method and the UI all read `z.infer`.
|
|
50439
|
+
*
|
|
50440
|
+
* A proposal is read-only advice: it carries the numbers it was derived from
|
|
50441
|
+
* (`evidence`) so the operator can judge it, and an `insufficient` answer names
|
|
50442
|
+
* WHY with a typed reason and the catalog defaults, never an invented number.
|
|
50443
|
+
*/
|
|
50444
|
+
var RunningProposalTierSchema = _enum(["raw", "5m"]);
|
|
50445
|
+
var RunningProposalConfidenceSchema = _enum([
|
|
50446
|
+
"high",
|
|
50447
|
+
"medium",
|
|
50448
|
+
"low"
|
|
50449
|
+
]);
|
|
50450
|
+
var RunningProposalInsufficientReasonSchema = _enum([
|
|
50451
|
+
"no-history",
|
|
50452
|
+
"span-too-short",
|
|
50453
|
+
"truncated",
|
|
50454
|
+
"no-idle-floor",
|
|
50455
|
+
"no-runs",
|
|
50456
|
+
"no-separation"
|
|
50457
|
+
]);
|
|
50458
|
+
var RunningProposalReplaySchema = object({
|
|
50459
|
+
/** Intervals the real dwell expression held true over the window. */
|
|
50460
|
+
intervals: number().int().nonnegative(),
|
|
50461
|
+
/** Intervals outside every run, or a second interval inside one. */
|
|
50462
|
+
extra: number().int().nonnegative(),
|
|
50463
|
+
/** Runs without an interval of their own (none, or merged into a neighbour). */
|
|
50464
|
+
missed: number().int().nonnegative()
|
|
50465
|
+
});
|
|
50466
|
+
var RunningProposalEvidenceSchema = object({
|
|
50467
|
+
tier: RunningProposalTierSchema,
|
|
50468
|
+
spanMs: number().nonnegative(),
|
|
50469
|
+
from: number(),
|
|
50470
|
+
to: number(),
|
|
50471
|
+
points: number().int().nonnegative(),
|
|
50472
|
+
runsFound: number().int().nonnegative(),
|
|
50473
|
+
pausesFound: number().int().nonnegative(),
|
|
50474
|
+
longestPauseMs: number().nonnegative(),
|
|
50475
|
+
idleFloorW: number(),
|
|
50476
|
+
noiseCeilingW: number(),
|
|
50477
|
+
plateauW: number(),
|
|
50478
|
+
replay: RunningProposalReplaySchema
|
|
50479
|
+
});
|
|
50480
|
+
var RunningProposalInsufficientEvidenceSchema = object({
|
|
50481
|
+
spanMs: number().nonnegative(),
|
|
50482
|
+
points: number().int().nonnegative(),
|
|
50483
|
+
runsFound: number().int().nonnegative()
|
|
50484
|
+
});
|
|
50485
|
+
var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
|
|
50486
|
+
state: literal("proposal"),
|
|
50487
|
+
transform: ThresholdTransformDraftSchema,
|
|
50488
|
+
evidence: RunningProposalEvidenceSchema,
|
|
50489
|
+
confidence: RunningProposalConfidenceSchema,
|
|
50490
|
+
/** Why the confidence is not higher; empty when nothing held it back. */
|
|
50491
|
+
notes: array(string())
|
|
50492
|
+
}), object({
|
|
50493
|
+
state: literal("insufficient"),
|
|
50494
|
+
reason: RunningProposalInsufficientReasonSchema,
|
|
50495
|
+
evidence: RunningProposalInsufficientEvidenceSchema,
|
|
50496
|
+
/** The catalog defaults, offered as what stands when nothing better is known. */
|
|
50497
|
+
fallback: ThresholdTransformDraftSchema
|
|
50498
|
+
})]);
|
|
50499
|
+
/**
|
|
49519
50500
|
* The report of a one-shot "move this owner's plaintext secrets into the
|
|
49520
50501
|
* keychain" run (D878). COUNTS only - never a value, never a ref.
|
|
49521
50502
|
*
|
|
@@ -49805,6 +50786,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
|
|
|
49805
50786
|
kind: "mutation",
|
|
49806
50787
|
auth: "admin",
|
|
49807
50788
|
access: "view"
|
|
50789
|
+
}), method(object({
|
|
50790
|
+
deviceId: number().int(),
|
|
50791
|
+
cap: string().min(1),
|
|
50792
|
+
fieldPath: string().min(1),
|
|
50793
|
+
deviceType: _enum(DeviceType).optional()
|
|
50794
|
+
}), RunningThresholdProposalSchema, {
|
|
50795
|
+
auth: "admin",
|
|
50796
|
+
access: "view"
|
|
49808
50797
|
}), method(object({
|
|
49809
50798
|
blockId: string().min(1),
|
|
49810
50799
|
trace: AutomationRunTraceSchema.optional(),
|
|
@@ -50781,7 +51770,27 @@ method(object({
|
|
|
50781
51770
|
* exposes only the COMMON surface: link state, supported device
|
|
50782
51771
|
* kinds (so the UI can filter the device picker), and per-device
|
|
50783
51772
|
* expose/unexpose.
|
|
50784
|
-
|
|
51773
|
+
*
|
|
51774
|
+
* INSTANCES. One export addon can serve several independent targets (a
|
|
51775
|
+
* HomeKit bridge, a Home Assistant broker). `listInstances` names them.
|
|
51776
|
+
* `instanceId` on a READ is optional: absent = the whole addon (linked if any
|
|
51777
|
+
* enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
|
|
51778
|
+
* default instance's); present = that instance; an id the provider does not
|
|
51779
|
+
* serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
|
|
51780
|
+
* never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
|
|
51781
|
+
* change always names the target it changes. A single-instance provider
|
|
51782
|
+
* accepts an absent id on a read or its own id, and refuses any other.
|
|
51783
|
+
*/
|
|
51784
|
+
/** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
|
|
51785
|
+
var InstanceIdSchema$1 = string().min(1);
|
|
51786
|
+
var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
|
|
51787
|
+
/** One independent export target of an addon. */
|
|
51788
|
+
var ExportInstanceSchema = object({
|
|
51789
|
+
instanceId: InstanceIdSchema$1,
|
|
51790
|
+
name: string(),
|
|
51791
|
+
enabled: boolean(),
|
|
51792
|
+
kind: string().optional()
|
|
51793
|
+
});
|
|
50785
51794
|
var LinkStateSchema = _enum([
|
|
50786
51795
|
"unlinked",
|
|
50787
51796
|
"linked",
|
|
@@ -50832,10 +51841,14 @@ var ExposedDeviceSchema = object({
|
|
|
50832
51841
|
capabilities: array(string()).optional()
|
|
50833
51842
|
});
|
|
50834
51843
|
var ExposeInputSchema = object({
|
|
51844
|
+
instanceId: InstanceIdSchema$1,
|
|
50835
51845
|
deviceId: string(),
|
|
50836
51846
|
capabilities: array(string()).optional()
|
|
50837
51847
|
});
|
|
50838
|
-
var UnexposeInputSchema = object({
|
|
51848
|
+
var UnexposeInputSchema = object({
|
|
51849
|
+
instanceId: InstanceIdSchema$1,
|
|
51850
|
+
deviceId: string()
|
|
51851
|
+
});
|
|
50839
51852
|
/**
|
|
50840
51853
|
* What an exporter may expose besides devices (D968). An action and a
|
|
50841
51854
|
* notification target are LIST ENTRIES, never devices (D954): they enter an
|
|
@@ -50864,15 +51877,21 @@ var ExposedEntitySchema = object({
|
|
|
50864
51877
|
entity: ExportEntityRefSchema,
|
|
50865
51878
|
exposedAs: string().optional()
|
|
50866
51879
|
});
|
|
50867
|
-
var ExposeEntityInputSchema = object({
|
|
50868
|
-
|
|
50869
|
-
|
|
51880
|
+
var ExposeEntityInputSchema = object({
|
|
51881
|
+
instanceId: InstanceIdSchema$1,
|
|
51882
|
+
entity: ExportEntityRefSchema
|
|
51883
|
+
});
|
|
51884
|
+
var UnexposeEntityInputSchema = object({
|
|
51885
|
+
instanceId: InstanceIdSchema$1,
|
|
51886
|
+
entity: ExportEntityRefSchema
|
|
51887
|
+
});
|
|
51888
|
+
method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
|
|
50870
51889
|
kind: "mutation",
|
|
50871
51890
|
auth: "admin"
|
|
50872
51891
|
}), method(UnexposeInputSchema, _void(), {
|
|
50873
51892
|
kind: "mutation",
|
|
50874
51893
|
auth: "admin"
|
|
50875
|
-
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(
|
|
51894
|
+
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
|
|
50876
51895
|
kind: "mutation",
|
|
50877
51896
|
auth: "admin"
|
|
50878
51897
|
}), optionalMethod(UnexposeEntityInputSchema, _void(), {
|
|
@@ -54463,15 +55482,15 @@ var FanPresetCaptureResultSchema = object({
|
|
|
54463
55482
|
items: array(FanCaptureItemSchema),
|
|
54464
55483
|
omitted: array(FanCaptureOmissionSchema)
|
|
54465
55484
|
});
|
|
54466
|
-
var deviceInput$
|
|
55485
|
+
var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
|
|
54467
55486
|
DeviceType.Fan;
|
|
54468
|
-
method(deviceInput$
|
|
55487
|
+
method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
|
|
54469
55488
|
kind: "mutation",
|
|
54470
55489
|
auth: "admin"
|
|
54471
|
-
}), method(deviceInput$
|
|
55490
|
+
}), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
|
|
54472
55491
|
kind: "mutation",
|
|
54473
55492
|
auth: "protected"
|
|
54474
|
-
}), method(deviceInput$
|
|
55493
|
+
}), method(deviceInput$2.extend({
|
|
54475
55494
|
fields: array(FanCaptureFieldSchema).optional(),
|
|
54476
55495
|
steps: array(ActionStepSchema).max(20).default([])
|
|
54477
55496
|
}), FanPresetCaptureResultSchema, { auth: "protected" });
|
|
@@ -55354,6 +56373,12 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
55354
56373
|
}), object({ success: boolean() }), {
|
|
55355
56374
|
kind: "mutation",
|
|
55356
56375
|
auth: "admin"
|
|
56376
|
+
}), method(object({ id: string() }), object({
|
|
56377
|
+
rows: number().int().nonnegative(),
|
|
56378
|
+
secrets: number().int().nonnegative()
|
|
56379
|
+
}), {
|
|
56380
|
+
kind: "mutation",
|
|
56381
|
+
auth: "admin"
|
|
55357
56382
|
}), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
|
|
55358
56383
|
addonId: string(),
|
|
55359
56384
|
settings: record(string(), unknown())
|
|
@@ -55382,13 +56407,65 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
55382
56407
|
*/
|
|
55383
56408
|
var ConfigSchemaPassthrough$3 = custom();
|
|
55384
56409
|
var IntegrationInstanceIdSchema = string().min(1);
|
|
56410
|
+
var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
|
|
56411
|
+
/**
|
|
56412
|
+
* One kind of instance a provider can create (an adapter, an account type). The sign-in and
|
|
56413
|
+
* connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
|
|
56414
|
+
* hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
|
|
56415
|
+
* interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
|
|
56416
|
+
* instance id.
|
|
56417
|
+
*/
|
|
56418
|
+
var IntegrationInstanceKindSchema = object({
|
|
56419
|
+
/** Stable id of the kind, sent back as `createInstance.kind`. */
|
|
56420
|
+
id: string().min(1),
|
|
56421
|
+
label: string().min(1),
|
|
56422
|
+
/** The kind signs in through a server-held interactive session. */
|
|
56423
|
+
signIn: object({
|
|
56424
|
+
basePath: string().min(1),
|
|
56425
|
+
methods: array(_enum(["device-code", "proxy"])).optional()
|
|
56426
|
+
}).optional(),
|
|
56427
|
+
/** Relative hub path to start an interactive account link of an instance of this kind. */
|
|
56428
|
+
connectPath: string().min(1).optional(),
|
|
56429
|
+
/** Relative hub path to disconnect that account. */
|
|
56430
|
+
disconnectPath: string().min(1).optional()
|
|
56431
|
+
});
|
|
55385
56432
|
var IntegrationInstanceDescriptionSchema = object({
|
|
56433
|
+
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
56434
|
+
role: _enum([
|
|
56435
|
+
"devices",
|
|
56436
|
+
"services",
|
|
56437
|
+
"both"
|
|
56438
|
+
]),
|
|
56439
|
+
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
56440
|
+
discovery: object({
|
|
56441
|
+
mode: _enum([
|
|
56442
|
+
"none",
|
|
56443
|
+
"lan",
|
|
56444
|
+
"account"
|
|
56445
|
+
]),
|
|
56446
|
+
scopedToInstance: boolean()
|
|
56447
|
+
}).optional(),
|
|
55386
56448
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
55387
|
-
instanceMode:
|
|
56449
|
+
instanceMode: IntegrationInstanceModeSchema,
|
|
55388
56450
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
55389
56451
|
kind: string(),
|
|
55390
56452
|
/** True when `testInstance` really probes the remote. */
|
|
55391
|
-
canTest: boolean()
|
|
56453
|
+
canTest: boolean(),
|
|
56454
|
+
/**
|
|
56455
|
+
* The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
|
|
56456
|
+
* Add flow asks which first and sends it as `createInstance.kind`.
|
|
56457
|
+
*/
|
|
56458
|
+
kinds: array(IntegrationInstanceKindSchema).optional(),
|
|
56459
|
+
/**
|
|
56460
|
+
* The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
|
|
56461
|
+
* devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
|
|
56462
|
+
*/
|
|
56463
|
+
allowRestoreId: boolean().optional(),
|
|
56464
|
+
/**
|
|
56465
|
+
* `createInstance` without an `id` works: the provider mints it. Absent means the caller must
|
|
56466
|
+
* send an id (the UI then mints an opaque one).
|
|
56467
|
+
*/
|
|
56468
|
+
mintsId: boolean().optional()
|
|
55392
56469
|
});
|
|
55393
56470
|
var IntegrationInstanceStatusSchema = object({
|
|
55394
56471
|
state: _enum([
|
|
@@ -55397,7 +56474,9 @@ var IntegrationInstanceStatusSchema = object({
|
|
|
55397
56474
|
"error",
|
|
55398
56475
|
"unknown"
|
|
55399
56476
|
]),
|
|
55400
|
-
detail: string().optional()
|
|
56477
|
+
detail: string().optional(),
|
|
56478
|
+
/** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
|
|
56479
|
+
label: string().optional()
|
|
55401
56480
|
});
|
|
55402
56481
|
var IntegrationInstanceSchema = object({
|
|
55403
56482
|
id: IntegrationInstanceIdSchema,
|
|
@@ -55405,13 +56484,18 @@ var IntegrationInstanceSchema = object({
|
|
|
55405
56484
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
55406
56485
|
configured: boolean(),
|
|
55407
56486
|
/**
|
|
55408
|
-
* The instance's own on/off switch (D996)
|
|
55409
|
-
*
|
|
56487
|
+
* The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
|
|
56488
|
+
* so a row written before the field existed is on). Absent when the `integrations` registry
|
|
56489
|
+
* owns the switch (`describe().role` is `devices`): read it from the registry row.
|
|
55410
56490
|
*/
|
|
55411
|
-
enabled: boolean(),
|
|
56491
|
+
enabled: boolean().optional(),
|
|
56492
|
+
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
56493
|
+
kind: string().optional(),
|
|
55412
56494
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
55413
56495
|
formValues: record(string(), unknown()),
|
|
55414
|
-
status: IntegrationInstanceStatusSchema.optional()
|
|
56496
|
+
status: IntegrationInstanceStatusSchema.optional(),
|
|
56497
|
+
/** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
|
|
56498
|
+
lastCheckedAt: number().optional()
|
|
55415
56499
|
});
|
|
55416
56500
|
var IntegrationInstancePatchSchema = object({
|
|
55417
56501
|
id: IntegrationInstanceIdSchema,
|
|
@@ -55419,31 +56503,57 @@ var IntegrationInstancePatchSchema = object({
|
|
|
55419
56503
|
/** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
|
|
55420
56504
|
values: record(string(), unknown())
|
|
55421
56505
|
});
|
|
55422
|
-
|
|
55423
|
-
|
|
55424
|
-
|
|
55425
|
-
|
|
55426
|
-
|
|
55427
|
-
|
|
55428
|
-
|
|
55429
|
-
|
|
55430
|
-
|
|
55431
|
-
|
|
55432
|
-
|
|
55433
|
-
|
|
55434
|
-
|
|
55435
|
-
|
|
55436
|
-
|
|
55437
|
-
|
|
55438
|
-
|
|
55439
|
-
|
|
55440
|
-
}),
|
|
55441
|
-
|
|
55442
|
-
|
|
55443
|
-
}),
|
|
55444
|
-
|
|
55445
|
-
|
|
55446
|
-
|
|
56506
|
+
var integrationInstancesCapability = {
|
|
56507
|
+
name: "integration-instances",
|
|
56508
|
+
scope: "system",
|
|
56509
|
+
mode: "collection",
|
|
56510
|
+
methods: {
|
|
56511
|
+
describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
|
|
56512
|
+
/** The form; `id: null` is the create form. */
|
|
56513
|
+
instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
|
|
56514
|
+
listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
|
|
56515
|
+
getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
|
|
56516
|
+
/**
|
|
56517
|
+
* Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
|
|
56518
|
+
* without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
|
|
56519
|
+
*/
|
|
56520
|
+
createInstance: method(object({
|
|
56521
|
+
id: IntegrationInstanceIdSchema.optional(),
|
|
56522
|
+
name: string().min(1),
|
|
56523
|
+
kind: string().min(1).optional()
|
|
56524
|
+
}), object({ id: string() }), {
|
|
56525
|
+
kind: "mutation",
|
|
56526
|
+
auth: "admin"
|
|
56527
|
+
}),
|
|
56528
|
+
updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
|
|
56529
|
+
kind: "mutation",
|
|
56530
|
+
auth: "admin"
|
|
56531
|
+
}),
|
|
56532
|
+
/**
|
|
56533
|
+
* Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
|
|
56534
|
+
* form patch (D62, D996). An off instance keeps its record and secrets and does no work.
|
|
56535
|
+
*/
|
|
56536
|
+
setInstanceEnabled: method(object({
|
|
56537
|
+
id: IntegrationInstanceIdSchema,
|
|
56538
|
+
enabled: boolean()
|
|
56539
|
+
}), object({ ok: literal(true) }), {
|
|
56540
|
+
kind: "mutation",
|
|
56541
|
+
auth: "admin"
|
|
56542
|
+
}),
|
|
56543
|
+
/** Delete the record and its keychain refs together. */
|
|
56544
|
+
deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
|
|
56545
|
+
kind: "mutation",
|
|
56546
|
+
auth: "admin"
|
|
56547
|
+
}),
|
|
56548
|
+
testInstance: optionalMethod(object({
|
|
56549
|
+
id: IntegrationInstanceIdSchema.nullable(),
|
|
56550
|
+
values: record(string(), unknown())
|
|
56551
|
+
}), TestConnectionResultSchema$1, {
|
|
56552
|
+
kind: "mutation",
|
|
56553
|
+
auth: "admin"
|
|
56554
|
+
})
|
|
56555
|
+
}
|
|
56556
|
+
};
|
|
55447
56557
|
/**
|
|
55448
56558
|
* Two-way audio / talk-back cap. Clients open a WebRTC audio-only
|
|
55449
56559
|
* session (server-initiated offer pattern, identical to the main
|
|
@@ -57461,6 +58571,11 @@ var LogEntrySchema = object({
|
|
|
57461
58571
|
* flight, and not asking for the next slice ends the scan.
|
|
57462
58572
|
*/
|
|
57463
58573
|
var LogReadQuerySchema = object({
|
|
58574
|
+
/**
|
|
58575
|
+
* Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
|
|
58576
|
+
* the provider's first readable one, so a caller written before instances existed is unchanged.
|
|
58577
|
+
*/
|
|
58578
|
+
instanceId: string().min(1).max(200).optional(),
|
|
57464
58579
|
/** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
|
|
57465
58580
|
requestId: string().min(1).max(64).optional(),
|
|
57466
58581
|
/** The device the line is about (`tags.deviceId`, numeric). */
|
|
@@ -57523,6 +58638,8 @@ var LogReadResultSchema = object({
|
|
|
57523
58638
|
/** True when `cancelRead` ended this slice early. */
|
|
57524
58639
|
cancelled: boolean().optional()
|
|
57525
58640
|
});
|
|
58641
|
+
/** Names one destination instance; absent ⇒ the provider's first readable one. */
|
|
58642
|
+
var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
|
|
57526
58643
|
var LogReadCapabilitiesSchema = object({
|
|
57527
58644
|
/** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
|
|
57528
58645
|
readable: boolean(),
|
|
@@ -57541,7 +58658,7 @@ var LogReadCapabilitiesSchema = object({
|
|
|
57541
58658
|
/** How far back the store reaches, when the provider knows (days). */
|
|
57542
58659
|
retentionDays: number().nullable()
|
|
57543
58660
|
});
|
|
57544
|
-
method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(
|
|
58661
|
+
method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(LogInstanceSelectorSchema, LogReadCapabilitiesSchema, { auth: "admin" }), optionalMethod(LogReadQuerySchema, LogReadResultSchema, { auth: "admin" }), optionalMethod(object({ requestId: string().min(1).max(64) }), object({ cancelled: boolean() }), {
|
|
57545
58662
|
kind: "mutation",
|
|
57546
58663
|
auth: "admin"
|
|
57547
58664
|
});
|
|
@@ -69868,6 +70985,14 @@ var powerMeterCapability = {
|
|
|
69868
70985
|
deviceNative: true,
|
|
69869
70986
|
mode: "singleton",
|
|
69870
70987
|
deviceTypes: POWER_METER_DEVICE_TYPES,
|
|
70988
|
+
deviceConfig: { ui: {
|
|
70989
|
+
kind: "widget",
|
|
70990
|
+
widgetId: "host/power-meter-line",
|
|
70991
|
+
tab: "power",
|
|
70992
|
+
placement: "hero",
|
|
70993
|
+
label: "Power",
|
|
70994
|
+
order: 4
|
|
70995
|
+
} },
|
|
69871
70996
|
methods: {},
|
|
69872
70997
|
status: {
|
|
69873
70998
|
schema: PowerMeterStatusSchema,
|
|
@@ -69927,6 +71052,117 @@ var powerMeterCapability = {
|
|
|
69927
71052
|
volatileStateFields: ["lastFetchedAt"],
|
|
69928
71053
|
customize: powerMeterCustomize
|
|
69929
71054
|
};
|
|
71055
|
+
var PowerSourceRefusalReasonSchema = _enum([
|
|
71056
|
+
"no-power-source",
|
|
71057
|
+
"switch-missing",
|
|
71058
|
+
"switch-offline",
|
|
71059
|
+
"switch-unknown",
|
|
71060
|
+
"restart-in-progress",
|
|
71061
|
+
"switch-call-failed"
|
|
71062
|
+
]);
|
|
71063
|
+
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
71064
|
+
accepted: literal(false),
|
|
71065
|
+
reason: PowerSourceRefusalReasonSchema,
|
|
71066
|
+
/** One line for the operator, naming the switch. */
|
|
71067
|
+
message: string()
|
|
71068
|
+
})]);
|
|
71069
|
+
var PowerSourceInfoSchema = object({
|
|
71070
|
+
/** False when the device has no power source linked; the rest is then empty. */
|
|
71071
|
+
linked: boolean(),
|
|
71072
|
+
switchDeviceId: number().nullable(),
|
|
71073
|
+
switchName: string().nullable(),
|
|
71074
|
+
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
71075
|
+
switchOn: boolean().nullable(),
|
|
71076
|
+
switchOnline: boolean().nullable(),
|
|
71077
|
+
restartInProgress: boolean(),
|
|
71078
|
+
offSeconds: number().int()
|
|
71079
|
+
});
|
|
71080
|
+
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
71081
|
+
var PowerSourceStateSchema = object({
|
|
71082
|
+
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
71083
|
+
switchOn: boolean().nullable(),
|
|
71084
|
+
offSeconds: number().int(),
|
|
71085
|
+
lastFetchedAt: number()
|
|
71086
|
+
});
|
|
71087
|
+
var POWER_SOURCE_DEVICE_TYPES = [
|
|
71088
|
+
DeviceType.Gateway,
|
|
71089
|
+
DeviceType.Hub,
|
|
71090
|
+
DeviceType.Camera,
|
|
71091
|
+
DeviceType.Sensor,
|
|
71092
|
+
DeviceType.MediaPlayer,
|
|
71093
|
+
DeviceType.Vacuum,
|
|
71094
|
+
DeviceType.Generic,
|
|
71095
|
+
DeviceType.Washer,
|
|
71096
|
+
DeviceType.Dryer,
|
|
71097
|
+
DeviceType.Dishwasher,
|
|
71098
|
+
DeviceType.PetFountain,
|
|
71099
|
+
DeviceType.Fan,
|
|
71100
|
+
DeviceType.Humidifier,
|
|
71101
|
+
DeviceType.Climate,
|
|
71102
|
+
DeviceType.WaterHeater
|
|
71103
|
+
];
|
|
71104
|
+
var powerSourceCustomize = {
|
|
71105
|
+
version: 1,
|
|
71106
|
+
targets: [{
|
|
71107
|
+
id: "switch",
|
|
71108
|
+
cardinality: "scalar",
|
|
71109
|
+
vocabulary: {
|
|
71110
|
+
cap: "power-source",
|
|
71111
|
+
stateField: "switchOn"
|
|
71112
|
+
},
|
|
71113
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
71114
|
+
apply: "add",
|
|
71115
|
+
path: "switchOn",
|
|
71116
|
+
sources: [{
|
|
71117
|
+
kind: "switch",
|
|
71118
|
+
transforms: [{ kind: "identity" }]
|
|
71119
|
+
}],
|
|
71120
|
+
companions: [{
|
|
71121
|
+
kind: "constant",
|
|
71122
|
+
path: "offSeconds",
|
|
71123
|
+
value: 10,
|
|
71124
|
+
when: "add"
|
|
71125
|
+
}, {
|
|
71126
|
+
kind: "clock",
|
|
71127
|
+
path: "lastFetchedAt",
|
|
71128
|
+
when: "always"
|
|
71129
|
+
}],
|
|
71130
|
+
sourceReport: { status: "assignment-status" }
|
|
71131
|
+
}]
|
|
71132
|
+
};
|
|
71133
|
+
var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
|
|
71134
|
+
var powerSourceCapability = {
|
|
71135
|
+
name: "power-source",
|
|
71136
|
+
scope: "device",
|
|
71137
|
+
mode: "singleton",
|
|
71138
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
71139
|
+
methods: {
|
|
71140
|
+
getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
|
|
71141
|
+
powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
71142
|
+
kind: "mutation",
|
|
71143
|
+
auth: "admin"
|
|
71144
|
+
}),
|
|
71145
|
+
powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
71146
|
+
kind: "mutation",
|
|
71147
|
+
auth: "admin"
|
|
71148
|
+
}),
|
|
71149
|
+
forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
71150
|
+
kind: "mutation",
|
|
71151
|
+
auth: "admin"
|
|
71152
|
+
})
|
|
71153
|
+
},
|
|
71154
|
+
runtimeState: PowerSourceStateSchema,
|
|
71155
|
+
customize: powerSourceCustomize,
|
|
71156
|
+
/**
|
|
71157
|
+
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
71158
|
+
* its next report; a restored value would outlive the reading it names.
|
|
71159
|
+
*
|
|
71160
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
71161
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
71162
|
+
*/
|
|
71163
|
+
durability: "session",
|
|
71164
|
+
history: "none"
|
|
71165
|
+
};
|
|
69930
71166
|
/**
|
|
69931
71167
|
* Presence cap. Models HA `person.*` and `device_tracker.*` entities
|
|
69932
71168
|
* on `DeviceType.Presence`. Read-only — no setters: presence is
|
|
@@ -79406,6 +80642,25 @@ function buildDescriptor(capName, kind) {
|
|
|
79406
80642
|
* contributes one entry per bound cap present in this map.
|
|
79407
80643
|
*/
|
|
79408
80644
|
var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
|
|
80645
|
+
/**
|
|
80646
|
+
* What a remote key is CALLED and what it LOOKS like, for every role (D963),
|
|
80647
|
+
* shared by the admin UI and the viewer. A role is data on the wire; each
|
|
80648
|
+
* client resolves its name from `label` (English and Italian side by side) and
|
|
80649
|
+
* its glyph from `icon`, a lucide id from the shared icon catalogue — the
|
|
80650
|
+
* server sends an id, a client maps it to its own icon set.
|
|
80651
|
+
*
|
|
80652
|
+
* `legend` is text printed on the key INSTEAD of a glyph (a digit has no
|
|
80653
|
+
* pictogram): a client that can draw text shows it, one that cannot falls back
|
|
80654
|
+
* to `icon`.
|
|
80655
|
+
*/
|
|
80656
|
+
function digit(n) {
|
|
80657
|
+
return {
|
|
80658
|
+
label: t(n, n),
|
|
80659
|
+
icon: "hash",
|
|
80660
|
+
legend: n
|
|
80661
|
+
};
|
|
80662
|
+
}
|
|
80663
|
+
digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
|
|
79409
80664
|
DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
|
|
79410
80665
|
var btn = (idSuffix, row, col, init) => ({
|
|
79411
80666
|
idSuffix,
|
|
@@ -79533,6 +80788,34 @@ discriminatedUnion("kind", [object({
|
|
|
79533
80788
|
kind: literal("pronto"),
|
|
79534
80789
|
code: string()
|
|
79535
80790
|
})]);
|
|
80791
|
+
/**
|
|
80792
|
+
* Propose a running threshold from a device's own power history (planning#405).
|
|
80793
|
+
*
|
|
80794
|
+
* Pure and deterministic: the same series gives the same answer. The series is
|
|
80795
|
+
* a HELD-VALUE step series — a point's value stands until the next point — which
|
|
80796
|
+
* is how device-history stores a gauge, so "time at a level" is exact, not a
|
|
80797
|
+
* count of samples.
|
|
80798
|
+
*
|
|
80799
|
+
* 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
|
|
80800
|
+
* near F. A device that is not near one level most of the time has no floor.
|
|
80801
|
+
* 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
|
|
80802
|
+
* 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
|
|
80803
|
+
* the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
|
|
80804
|
+
* 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
|
|
80805
|
+
* grid a person would type; stop dwell covers the longest pause; start dwell
|
|
80806
|
+
* covers the longest standby blip above T.
|
|
80807
|
+
* 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
|
|
80808
|
+
* run must hold exactly one true interval and nothing may flap outside.
|
|
80809
|
+
*/
|
|
80810
|
+
var SECOND_MS = 1e3;
|
|
80811
|
+
var MINUTE_MS$1 = 60 * SECOND_MS;
|
|
80812
|
+
24 * (60 * MINUTE_MS$1);
|
|
80813
|
+
10 * MINUTE_MS$1;
|
|
80814
|
+
30 * SECOND_MS;
|
|
80815
|
+
60 * SECOND_MS;
|
|
80816
|
+
20 * MINUTE_MS$1;
|
|
80817
|
+
10 * SECOND_MS;
|
|
80818
|
+
10 * SECOND_MS;
|
|
79536
80819
|
DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
|
|
79537
80820
|
SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
|
|
79538
80821
|
String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
|
|
@@ -82638,6 +83921,13 @@ Object.freeze({
|
|
|
82638
83921
|
access: "create",
|
|
82639
83922
|
role: "admin"
|
|
82640
83923
|
},
|
|
83924
|
+
"coreBlocks.proposeRunningThreshold": {
|
|
83925
|
+
capName: "core-blocks",
|
|
83926
|
+
capScope: "system",
|
|
83927
|
+
addonId: null,
|
|
83928
|
+
access: "view",
|
|
83929
|
+
role: "admin"
|
|
83930
|
+
},
|
|
82641
83931
|
"coreBlocks.reportAutomationRun": {
|
|
82642
83932
|
capName: "core-blocks",
|
|
82643
83933
|
capScope: "system",
|
|
@@ -83149,6 +84439,13 @@ Object.freeze({
|
|
|
83149
84439
|
access: "view",
|
|
83150
84440
|
role: "admin"
|
|
83151
84441
|
},
|
|
84442
|
+
"deviceExport.listInstances": {
|
|
84443
|
+
capName: "device-export",
|
|
84444
|
+
capScope: "system",
|
|
84445
|
+
addonId: null,
|
|
84446
|
+
access: "view",
|
|
84447
|
+
role: "admin"
|
|
84448
|
+
},
|
|
83152
84449
|
"deviceExport.listSupportedDeviceKinds": {
|
|
83153
84450
|
capName: "device-export",
|
|
83154
84451
|
capScope: "system",
|
|
@@ -84801,6 +86098,13 @@ Object.freeze({
|
|
|
84801
86098
|
access: "create",
|
|
84802
86099
|
role: "admin"
|
|
84803
86100
|
},
|
|
86101
|
+
"integrations.clearSettings": {
|
|
86102
|
+
capName: "integrations",
|
|
86103
|
+
capScope: "system",
|
|
86104
|
+
addonId: null,
|
|
86105
|
+
access: "delete",
|
|
86106
|
+
role: "admin"
|
|
86107
|
+
},
|
|
84804
86108
|
"integrations.create": {
|
|
84805
86109
|
capName: "integrations",
|
|
84806
86110
|
capScope: "system",
|
|
@@ -92096,6 +93400,11 @@ Object.freeze({
|
|
|
92096
93400
|
form: "single",
|
|
92097
93401
|
optional: false
|
|
92098
93402
|
}],
|
|
93403
|
+
"coreBlocks.proposeRunningThreshold": [{
|
|
93404
|
+
name: "deviceId",
|
|
93405
|
+
form: "single",
|
|
93406
|
+
optional: false
|
|
93407
|
+
}],
|
|
92099
93408
|
"cover.close": [{
|
|
92100
93409
|
name: "deviceId",
|
|
92101
93410
|
form: "single",
|
|
@@ -94759,99 +96068,6 @@ Object.freeze({
|
|
|
94759
96068
|
"network-access": "ingress",
|
|
94760
96069
|
"smtp-provider": "email"
|
|
94761
96070
|
});
|
|
94762
|
-
/**
|
|
94763
|
-
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
94764
|
-
* `z.infer`, they do not restate the shape.
|
|
94765
|
-
*/
|
|
94766
|
-
var OverrideSourceSchema = object({
|
|
94767
|
-
addonId: string().min(1),
|
|
94768
|
-
stableId: string().min(1),
|
|
94769
|
-
cap: string().min(1),
|
|
94770
|
-
fieldPath: string().min(1)
|
|
94771
|
-
});
|
|
94772
|
-
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
94773
|
-
object({ kind: literal("identity") }),
|
|
94774
|
-
object({ kind: literal("invert") }),
|
|
94775
|
-
object({
|
|
94776
|
-
kind: literal("threshold"),
|
|
94777
|
-
comparison: _enum(["gte", "lte"]),
|
|
94778
|
-
threshold: number(),
|
|
94779
|
-
startDwellMs: number().nonnegative(),
|
|
94780
|
-
stopDwellMs: number().nonnegative()
|
|
94781
|
-
}),
|
|
94782
|
-
object({
|
|
94783
|
-
kind: literal("linear"),
|
|
94784
|
-
scale: number(),
|
|
94785
|
-
offset: number(),
|
|
94786
|
-
clampMin: number().optional(),
|
|
94787
|
-
clampMax: number().optional()
|
|
94788
|
-
}),
|
|
94789
|
-
object({
|
|
94790
|
-
kind: literal("enum-map"),
|
|
94791
|
-
mapping: record(string(), string().nullable())
|
|
94792
|
-
}),
|
|
94793
|
-
object({ kind: literal("number") })
|
|
94794
|
-
]);
|
|
94795
|
-
var ScalarOverrideDraftSchema = object({
|
|
94796
|
-
kind: literal("scalar"),
|
|
94797
|
-
cap: string().min(1),
|
|
94798
|
-
targetId: string().min(1),
|
|
94799
|
-
source: OverrideSourceSchema,
|
|
94800
|
-
transform: CustomizeTransformDraftSchema
|
|
94801
|
-
});
|
|
94802
|
-
var KeyedArrayOverrideDraftSchema = object({
|
|
94803
|
-
kind: literal("keyedArray"),
|
|
94804
|
-
cap: string().min(1),
|
|
94805
|
-
targetId: string().min(1),
|
|
94806
|
-
key: string().min(1),
|
|
94807
|
-
label: string().min(1),
|
|
94808
|
-
fieldPath: string().min(1),
|
|
94809
|
-
source: OverrideSourceSchema,
|
|
94810
|
-
transform: CustomizeTransformDraftSchema
|
|
94811
|
-
});
|
|
94812
|
-
var PulseOverrideDraftSchema = object({
|
|
94813
|
-
kind: literal("pulse"),
|
|
94814
|
-
cap: string().min(1),
|
|
94815
|
-
targetId: string().min(1),
|
|
94816
|
-
enabled: boolean(),
|
|
94817
|
-
debounceMs: number().nonnegative(),
|
|
94818
|
-
edges: array(_enum([
|
|
94819
|
-
"rising",
|
|
94820
|
-
"button",
|
|
94821
|
-
"native-event"
|
|
94822
|
-
])).min(1).optional(),
|
|
94823
|
-
sources: array(OverrideSourceSchema).min(1)
|
|
94824
|
-
});
|
|
94825
|
-
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
94826
|
-
"in-addition",
|
|
94827
|
-
"confirm",
|
|
94828
|
-
"replace"
|
|
94829
|
-
]).optional() });
|
|
94830
|
-
discriminatedUnion("kind", [
|
|
94831
|
-
ScalarOverrideDraftSchema,
|
|
94832
|
-
KeyedArrayOverrideDraftSchema,
|
|
94833
|
-
PulseOverrideDraftSchema,
|
|
94834
|
-
object({
|
|
94835
|
-
kind: literal("span"),
|
|
94836
|
-
cap: string().min(1),
|
|
94837
|
-
targetId: string().min(1),
|
|
94838
|
-
timeoutMs: number().nonnegative(),
|
|
94839
|
-
cooldownMs: number().nonnegative().optional(),
|
|
94840
|
-
sources: array(SpanSourceDraftSchema)
|
|
94841
|
-
})
|
|
94842
|
-
]);
|
|
94843
|
-
object({
|
|
94844
|
-
targetId: string().min(1),
|
|
94845
|
-
configured: boolean(),
|
|
94846
|
-
source: _enum(["native", "assigned"]),
|
|
94847
|
-
state: _enum([
|
|
94848
|
-
"active",
|
|
94849
|
-
"stale",
|
|
94850
|
-
"released",
|
|
94851
|
-
"unavailable"
|
|
94852
|
-
]),
|
|
94853
|
-
reason: string().nullable()
|
|
94854
|
-
});
|
|
94855
96071
|
var G711_SCALE_CORRECTION_DB = {
|
|
94856
96072
|
PCMU: 20 * Math.log10(4),
|
|
94857
96073
|
PCMA: 20 * Math.log10(8)
|
|
@@ -96356,6 +97572,20 @@ var VeluxReconciler = class {
|
|
|
96356
97572
|
}
|
|
96357
97573
|
};
|
|
96358
97574
|
//#endregion
|
|
97575
|
+
//#region src/custom-actions.ts
|
|
97576
|
+
/** Custom actions of the Velux addon: the LAN scan behind the integration form's gateway picker. */
|
|
97577
|
+
var veluxActions = defineCustomActions({ discoverGateway: customAction(object({}), object({
|
|
97578
|
+
hosts: array(object({
|
|
97579
|
+
host: string(),
|
|
97580
|
+
name: string(),
|
|
97581
|
+
id: string()
|
|
97582
|
+
})),
|
|
97583
|
+
summary: string()
|
|
97584
|
+
}), {
|
|
97585
|
+
kind: "mutation",
|
|
97586
|
+
auth: "admin"
|
|
97587
|
+
}) });
|
|
97588
|
+
//#endregion
|
|
96359
97589
|
//#region src/config.ts
|
|
96360
97590
|
/**
|
|
96361
97591
|
* Operator-supplied connection to ONE Velux KLF-200 gateway on the local
|
|
@@ -96479,20 +97709,6 @@ function buildConnectionFormSchema() {
|
|
|
96479
97709
|
}] };
|
|
96480
97710
|
}
|
|
96481
97711
|
//#endregion
|
|
96482
|
-
//#region src/custom-actions.ts
|
|
96483
|
-
/** Custom actions of the Velux addon: the LAN scan behind the integration form's gateway picker. */
|
|
96484
|
-
var veluxActions = defineCustomActions({ discoverGateway: customAction(object({}), object({
|
|
96485
|
-
hosts: array(object({
|
|
96486
|
-
host: string(),
|
|
96487
|
-
name: string(),
|
|
96488
|
-
id: string()
|
|
96489
|
-
})),
|
|
96490
|
-
summary: string()
|
|
96491
|
-
}), {
|
|
96492
|
-
kind: "mutation",
|
|
96493
|
-
auth: "admin"
|
|
96494
|
-
}) });
|
|
96495
|
-
//#endregion
|
|
96496
97712
|
//#region src/velux-node-resolver.ts
|
|
96497
97713
|
/**
|
|
96498
97714
|
* Per-integration registry that device classes use to reach the live
|
|
@@ -105814,7 +107030,54 @@ function veluxDiscoverySummary(foundCount, proposedCount) {
|
|
|
105814
107030
|
return `Found ${proposedCount} ${proposedCount === 1 ? "gateway" : "gateways"}.`;
|
|
105815
107031
|
}
|
|
105816
107032
|
//#endregion
|
|
107033
|
+
//#region src/velux-instances.ts
|
|
107034
|
+
/**
|
|
107035
|
+
* Velux integrations as `integration-instances` (planning#416, D1007): one `connections` record
|
|
107036
|
+
* per integration under its registry id, holding the connection to ONE KLF-200 gateway. The
|
|
107037
|
+
* gateway password is declared a secret by type; the records door keeps it in the keychain.
|
|
107038
|
+
*/
|
|
107039
|
+
var veluxConnectionsEntryKind = defineEntryKind("connections", {
|
|
107040
|
+
title: "Velux gateways",
|
|
107041
|
+
schema: entryBody({
|
|
107042
|
+
host: string().optional(),
|
|
107043
|
+
password: string().optional().meta({ secret: true })
|
|
107044
|
+
}),
|
|
107045
|
+
writeTrigger: "an operator adds, edits or removes a Velux KLF-200 gateway integration"
|
|
107046
|
+
});
|
|
107047
|
+
/** Every kind this addon declares, keyed by its name. */
|
|
107048
|
+
var veluxRecordKinds = { connections: veluxConnectionsEntryKind };
|
|
107049
|
+
function buildVeluxInstances(records, deps) {
|
|
107050
|
+
return new RecordIntegrationInstances(records, veluxConnectionsEntryKind, {
|
|
107051
|
+
description: {
|
|
107052
|
+
role: "devices",
|
|
107053
|
+
discovery: {
|
|
107054
|
+
mode: "account",
|
|
107055
|
+
scopedToInstance: true
|
|
107056
|
+
},
|
|
107057
|
+
instanceMode: "multiple",
|
|
107058
|
+
kind: "server",
|
|
107059
|
+
canTest: false
|
|
107060
|
+
},
|
|
107061
|
+
formSchema: () => buildConnectionFormSchema(),
|
|
107062
|
+
initialBody: (name) => ({ name }),
|
|
107063
|
+
configured: (body) => (body.host ?? "").length > 0 && body.password !== void 0,
|
|
107064
|
+
onChanged: () => deps.onChanged()
|
|
107065
|
+
});
|
|
107066
|
+
}
|
|
107067
|
+
/**
|
|
107068
|
+
* The gateway connection of one integration as the addon's runtime reads it: from the record,
|
|
107069
|
+
* never from the legacy rows. Null while the record is absent, incomplete, or its password is
|
|
107070
|
+
* unreadable (the runtime keeps what it had, D49).
|
|
107071
|
+
*/
|
|
107072
|
+
async function veluxConnectionOf(instances, id) {
|
|
107073
|
+
const read = await instances.readResolved(id);
|
|
107074
|
+
if (read === null || read.secrets === "unavailable") return null;
|
|
107075
|
+
return connectionFromSettings(read.body);
|
|
107076
|
+
}
|
|
107077
|
+
//#endregion
|
|
105817
107078
|
//#region src/addon.ts
|
|
107079
|
+
/** The date of the integration-rows-to-record migration (D1007). */
|
|
107080
|
+
var VELUX_MIGRATIONS_DATE = "2026-11-05";
|
|
105818
107081
|
/**
|
|
105819
107082
|
* Velux KLF-200 — `mode: account`, one integration per gateway.
|
|
105820
107083
|
*
|
|
@@ -105837,11 +107100,27 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
105837
107100
|
};
|
|
105838
107101
|
integrationReconciler = null;
|
|
105839
107102
|
clients = null;
|
|
107103
|
+
instances = null;
|
|
105840
107104
|
constructor() {
|
|
105841
107105
|
super({});
|
|
105842
107106
|
}
|
|
105843
107107
|
async onInitialize() {
|
|
105844
107108
|
const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
|
|
107109
|
+
const records = this.ctx.records;
|
|
107110
|
+
if (records === void 0) throw new Error("provider-velux: ctx.records is required (hub-only addon)");
|
|
107111
|
+
await records.declare(veluxRecordKinds);
|
|
107112
|
+
await this.runMigrations(defineAddonMigrations([integrationRowsMigration({
|
|
107113
|
+
addonId: this.addonId,
|
|
107114
|
+
date: VELUX_MIGRATIONS_DATE,
|
|
107115
|
+
kind: veluxConnectionsEntryKind,
|
|
107116
|
+
fixtureMark: "host"
|
|
107117
|
+
})], integrationRowsPortsOver({
|
|
107118
|
+
api: defaultIntegrationApiOver(this.ctx.api),
|
|
107119
|
+
records,
|
|
107120
|
+
kind: veluxConnectionsEntryKind,
|
|
107121
|
+
warn: (message) => this.ctx.logger.warn(message)
|
|
107122
|
+
})));
|
|
107123
|
+
this.instances = buildVeluxInstances(records, { onChanged: () => this.reconcileIntegrations() });
|
|
105845
107124
|
setKlfLibraryLogSink((level, message) => {
|
|
105846
107125
|
this.ctx.logger.debug(`klf-200-api ${level}: ${message}`);
|
|
105847
107126
|
});
|
|
@@ -105867,6 +107146,10 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
105867
107146
|
{
|
|
105868
107147
|
capability: connectionTestCapability,
|
|
105869
107148
|
provider: this.buildConnectionTestProvider()
|
|
107149
|
+
},
|
|
107150
|
+
{
|
|
107151
|
+
capability: integrationInstancesCapability,
|
|
107152
|
+
provider: this.requireInstances().toProvider()
|
|
105870
107153
|
}
|
|
105871
107154
|
],
|
|
105872
107155
|
customActions: veluxActions,
|
|
@@ -105901,9 +107184,6 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
105901
107184
|
setKlfLibraryLogSink(null);
|
|
105902
107185
|
await super.onShutdown();
|
|
105903
107186
|
}
|
|
105904
|
-
globalSettingsSchema() {
|
|
105905
|
-
return buildConnectionFormSchema();
|
|
105906
|
-
}
|
|
105907
107187
|
async supportsManualCreation() {
|
|
105908
107188
|
return false;
|
|
105909
107189
|
}
|
|
@@ -105945,7 +107225,7 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
105945
107225
|
for (const integration of all) {
|
|
105946
107226
|
if (integration.addonId !== this.ctx.id) continue;
|
|
105947
107227
|
try {
|
|
105948
|
-
const connection =
|
|
107228
|
+
const connection = await veluxConnectionOf(this.requireInstances(), integration.id);
|
|
105949
107229
|
if (connection) hosts.push(connection.host);
|
|
105950
107230
|
} catch (err) {
|
|
105951
107231
|
this.ctx.logger.warn("Velux discovery: settings read failed", {
|
|
@@ -105960,6 +107240,10 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
105960
107240
|
return [];
|
|
105961
107241
|
}
|
|
105962
107242
|
}
|
|
107243
|
+
requireInstances() {
|
|
107244
|
+
if (!this.instances) throw new Error("Velux provider not initialised");
|
|
107245
|
+
return this.instances;
|
|
107246
|
+
}
|
|
105963
107247
|
requireClients() {
|
|
105964
107248
|
if (!this.clients) throw new Error("Velux provider not initialised");
|
|
105965
107249
|
return this.clients;
|
|
@@ -105976,7 +107260,7 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
105976
107260
|
},
|
|
105977
107261
|
listIntegrations: () => this.ctx.api.integrations.list.query(),
|
|
105978
107262
|
applySettings: async (integration) => {
|
|
105979
|
-
const connection =
|
|
107263
|
+
const connection = await veluxConnectionOf(this.requireInstances(), integration.id);
|
|
105980
107264
|
if (!connection) return false;
|
|
105981
107265
|
await this.requireClients().upsert(integration.id, integration.name, connection);
|
|
105982
107266
|
return true;
|