@camstack/addon-provider-onvif 1.2.153 → 1.2.154
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 +1653 -251
- package/dist/addon.mjs +1653 -251
- package/package.json +9 -2
package/dist/addon.mjs
CHANGED
|
@@ -4562,6 +4562,12 @@ var numberProcessor = (schema, ctx, _json, _params) => {
|
|
|
4562
4562
|
var booleanProcessor = (_schema, _ctx, json, _params) => {
|
|
4563
4563
|
json.type = "boolean";
|
|
4564
4564
|
};
|
|
4565
|
+
var bigintProcessor = (_schema, ctx, _json, _params) => {
|
|
4566
|
+
if (ctx.unrepresentable === "throw") throw new Error("BigInt cannot be represented in JSON Schema");
|
|
4567
|
+
};
|
|
4568
|
+
var symbolProcessor = (_schema, ctx, _json, _params) => {
|
|
4569
|
+
if (ctx.unrepresentable === "throw") throw new Error("Symbols cannot be represented in JSON Schema");
|
|
4570
|
+
};
|
|
4565
4571
|
var nullProcessor = (_schema, ctx, json, _params) => {
|
|
4566
4572
|
if (ctx.target === "openapi-3.0") {
|
|
4567
4573
|
json.type = "string";
|
|
@@ -4569,12 +4575,17 @@ var nullProcessor = (_schema, ctx, json, _params) => {
|
|
|
4569
4575
|
json.enum = [null];
|
|
4570
4576
|
} else json.type = "null";
|
|
4571
4577
|
};
|
|
4578
|
+
var undefinedProcessor = (_schema, ctx, _json, _params) => {
|
|
4579
|
+
if (ctx.unrepresentable === "throw") throw new Error("Undefined cannot be represented in JSON Schema");
|
|
4580
|
+
};
|
|
4572
4581
|
var voidProcessor = (_schema, ctx, _json, _params) => {
|
|
4573
4582
|
if (ctx.unrepresentable === "throw") throw new Error("Void cannot be represented in JSON Schema");
|
|
4574
4583
|
};
|
|
4575
4584
|
var neverProcessor = (_schema, _ctx, json, _params) => {
|
|
4576
4585
|
json.not = {};
|
|
4577
4586
|
};
|
|
4587
|
+
var anyProcessor = (_schema, _ctx, _json, _params) => {};
|
|
4588
|
+
var unknownProcessor = (_schema, _ctx, _json, _params) => {};
|
|
4578
4589
|
var dateProcessor = (_schema, ctx, _json, _params) => {
|
|
4579
4590
|
if (ctx.unrepresentable === "throw") throw new Error("Date cannot be represented in JSON Schema");
|
|
4580
4591
|
};
|
|
@@ -4606,12 +4617,53 @@ var literalProcessor = (schema, ctx, json, _params) => {
|
|
|
4606
4617
|
json.enum = vals;
|
|
4607
4618
|
}
|
|
4608
4619
|
};
|
|
4620
|
+
var nanProcessor = (_schema, ctx, _json, _params) => {
|
|
4621
|
+
if (ctx.unrepresentable === "throw") throw new Error("NaN cannot be represented in JSON Schema");
|
|
4622
|
+
};
|
|
4623
|
+
var templateLiteralProcessor = (schema, _ctx, json, _params) => {
|
|
4624
|
+
const _json = json;
|
|
4625
|
+
const pattern = schema._zod.pattern;
|
|
4626
|
+
if (!pattern) throw new Error("Pattern not found in template literal");
|
|
4627
|
+
_json.type = "string";
|
|
4628
|
+
_json.pattern = pattern.source;
|
|
4629
|
+
};
|
|
4630
|
+
var fileProcessor = (schema, _ctx, json, _params) => {
|
|
4631
|
+
const _json = json;
|
|
4632
|
+
const file = {
|
|
4633
|
+
type: "string",
|
|
4634
|
+
format: "binary",
|
|
4635
|
+
contentEncoding: "binary"
|
|
4636
|
+
};
|
|
4637
|
+
const { minimum, maximum, mime } = schema._zod.bag;
|
|
4638
|
+
if (minimum !== void 0) file.minLength = minimum;
|
|
4639
|
+
if (maximum !== void 0) file.maxLength = maximum;
|
|
4640
|
+
if (mime) if (mime.length === 1) {
|
|
4641
|
+
file.contentMediaType = mime[0];
|
|
4642
|
+
Object.assign(_json, file);
|
|
4643
|
+
} else {
|
|
4644
|
+
Object.assign(_json, file);
|
|
4645
|
+
_json.anyOf = mime.map((m) => ({ contentMediaType: m }));
|
|
4646
|
+
}
|
|
4647
|
+
else Object.assign(_json, file);
|
|
4648
|
+
};
|
|
4649
|
+
var successProcessor = (_schema, _ctx, json, _params) => {
|
|
4650
|
+
json.type = "boolean";
|
|
4651
|
+
};
|
|
4609
4652
|
var customProcessor = (_schema, ctx, _json, _params) => {
|
|
4610
4653
|
if (ctx.unrepresentable === "throw") throw new Error("Custom types cannot be represented in JSON Schema");
|
|
4611
4654
|
};
|
|
4655
|
+
var functionProcessor = (_schema, ctx, _json, _params) => {
|
|
4656
|
+
if (ctx.unrepresentable === "throw") throw new Error("Function types cannot be represented in JSON Schema");
|
|
4657
|
+
};
|
|
4612
4658
|
var transformProcessor = (_schema, ctx, _json, _params) => {
|
|
4613
4659
|
if (ctx.unrepresentable === "throw") throw new Error("Transforms cannot be represented in JSON Schema");
|
|
4614
4660
|
};
|
|
4661
|
+
var mapProcessor = (_schema, ctx, _json, _params) => {
|
|
4662
|
+
if (ctx.unrepresentable === "throw") throw new Error("Map cannot be represented in JSON Schema");
|
|
4663
|
+
};
|
|
4664
|
+
var setProcessor = (_schema, ctx, _json, _params) => {
|
|
4665
|
+
if (ctx.unrepresentable === "throw") throw new Error("Set cannot be represented in JSON Schema");
|
|
4666
|
+
};
|
|
4615
4667
|
var arrayProcessor = (schema, ctx, _json, params) => {
|
|
4616
4668
|
const json = _json;
|
|
4617
4669
|
const def = schema._zod.def;
|
|
@@ -4816,6 +4868,12 @@ var readonlyProcessor = (schema, ctx, json, params) => {
|
|
|
4816
4868
|
seen.ref = def.innerType;
|
|
4817
4869
|
json.readOnly = true;
|
|
4818
4870
|
};
|
|
4871
|
+
var promiseProcessor = (schema, ctx, _json, params) => {
|
|
4872
|
+
const def = schema._zod.def;
|
|
4873
|
+
process$1(def.innerType, ctx, params);
|
|
4874
|
+
const seen = ctx.seen.get(schema);
|
|
4875
|
+
seen.ref = def.innerType;
|
|
4876
|
+
};
|
|
4819
4877
|
var optionalProcessor = (schema, ctx, _json, params) => {
|
|
4820
4878
|
const def = schema._zod.def;
|
|
4821
4879
|
process$1(def.innerType, ctx, params);
|
|
@@ -4828,6 +4886,81 @@ var lazyProcessor = (schema, ctx, _json, params) => {
|
|
|
4828
4886
|
const seen = ctx.seen.get(schema);
|
|
4829
4887
|
seen.ref = innerType;
|
|
4830
4888
|
};
|
|
4889
|
+
var allProcessors = {
|
|
4890
|
+
string: stringProcessor,
|
|
4891
|
+
number: numberProcessor,
|
|
4892
|
+
boolean: booleanProcessor,
|
|
4893
|
+
bigint: bigintProcessor,
|
|
4894
|
+
symbol: symbolProcessor,
|
|
4895
|
+
null: nullProcessor,
|
|
4896
|
+
undefined: undefinedProcessor,
|
|
4897
|
+
void: voidProcessor,
|
|
4898
|
+
never: neverProcessor,
|
|
4899
|
+
any: anyProcessor,
|
|
4900
|
+
unknown: unknownProcessor,
|
|
4901
|
+
date: dateProcessor,
|
|
4902
|
+
enum: enumProcessor,
|
|
4903
|
+
literal: literalProcessor,
|
|
4904
|
+
nan: nanProcessor,
|
|
4905
|
+
template_literal: templateLiteralProcessor,
|
|
4906
|
+
file: fileProcessor,
|
|
4907
|
+
success: successProcessor,
|
|
4908
|
+
custom: customProcessor,
|
|
4909
|
+
function: functionProcessor,
|
|
4910
|
+
transform: transformProcessor,
|
|
4911
|
+
map: mapProcessor,
|
|
4912
|
+
set: setProcessor,
|
|
4913
|
+
array: arrayProcessor,
|
|
4914
|
+
object: objectProcessor,
|
|
4915
|
+
union: unionProcessor,
|
|
4916
|
+
intersection: intersectionProcessor,
|
|
4917
|
+
tuple: tupleProcessor,
|
|
4918
|
+
record: recordProcessor,
|
|
4919
|
+
nullable: nullableProcessor,
|
|
4920
|
+
nonoptional: nonoptionalProcessor,
|
|
4921
|
+
default: defaultProcessor,
|
|
4922
|
+
prefault: prefaultProcessor,
|
|
4923
|
+
catch: catchProcessor,
|
|
4924
|
+
pipe: pipeProcessor,
|
|
4925
|
+
readonly: readonlyProcessor,
|
|
4926
|
+
promise: promiseProcessor,
|
|
4927
|
+
optional: optionalProcessor,
|
|
4928
|
+
lazy: lazyProcessor
|
|
4929
|
+
};
|
|
4930
|
+
function toJSONSchema(input, params) {
|
|
4931
|
+
if ("_idmap" in input) {
|
|
4932
|
+
const registry = input;
|
|
4933
|
+
const ctx = initializeContext({
|
|
4934
|
+
...params,
|
|
4935
|
+
processors: allProcessors
|
|
4936
|
+
});
|
|
4937
|
+
const defs = {};
|
|
4938
|
+
for (const entry of registry._idmap.entries()) {
|
|
4939
|
+
const [_, schema] = entry;
|
|
4940
|
+
process$1(schema, ctx);
|
|
4941
|
+
}
|
|
4942
|
+
const schemas = {};
|
|
4943
|
+
ctx.external = {
|
|
4944
|
+
registry,
|
|
4945
|
+
uri: params?.uri,
|
|
4946
|
+
defs
|
|
4947
|
+
};
|
|
4948
|
+
for (const entry of registry._idmap.entries()) {
|
|
4949
|
+
const [key, schema] = entry;
|
|
4950
|
+
extractDefs(ctx, schema);
|
|
4951
|
+
schemas[key] = finalize(ctx, schema);
|
|
4952
|
+
}
|
|
4953
|
+
if (Object.keys(defs).length > 0) schemas.__shared = { [ctx.target === "draft-2020-12" ? "$defs" : "definitions"]: defs };
|
|
4954
|
+
return { schemas };
|
|
4955
|
+
}
|
|
4956
|
+
const ctx = initializeContext({
|
|
4957
|
+
...params,
|
|
4958
|
+
processors: allProcessors
|
|
4959
|
+
});
|
|
4960
|
+
process$1(input, ctx);
|
|
4961
|
+
extractDefs(ctx, input);
|
|
4962
|
+
return finalize(ctx, input);
|
|
4963
|
+
}
|
|
4831
4964
|
//#endregion
|
|
4832
4965
|
//#region ../../node_modules/zod/v4/classic/iso.js
|
|
4833
4966
|
var ZodISODateTime = /*@__PURE__*/ $constructor("ZodISODateTime", (inst, def) => {
|
|
@@ -5852,7 +5985,7 @@ var ZodIssueCode = {
|
|
|
5852
5985
|
var ZodFirstPartyTypeKind;
|
|
5853
5986
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5854
5987
|
//#endregion
|
|
5855
|
-
//#region ../types/dist/sleep-
|
|
5988
|
+
//#region ../types/dist/sleep-D1jjWAr8.mjs
|
|
5856
5989
|
/**
|
|
5857
5990
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5858
5991
|
* window to float samples (D455).
|
|
@@ -6000,9 +6133,16 @@ function isSecretMarker(value) {
|
|
|
6000
6133
|
const ref = value["$secret"];
|
|
6001
6134
|
return typeof ref === "string" && ref.length > 0;
|
|
6002
6135
|
}
|
|
6136
|
+
/** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
|
|
6137
|
+
function isSecretMarkerLike(value) {
|
|
6138
|
+
return isRecord$7(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
|
|
6139
|
+
}
|
|
6003
6140
|
function isSecretClear(value) {
|
|
6004
6141
|
return isRecord$7(value) && value["$secretClear"] === true;
|
|
6005
6142
|
}
|
|
6143
|
+
function isSecretFieldView(value) {
|
|
6144
|
+
return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
|
|
6145
|
+
}
|
|
6006
6146
|
/**
|
|
6007
6147
|
* The UI view of a stored secret field value: a marker becomes "set", inline
|
|
6008
6148
|
* plaintext becomes "set (inline)", nothing becomes `null`. A value never
|
|
@@ -6445,7 +6585,7 @@ var NOT_RUN = {
|
|
|
6445
6585
|
* throws: an unreachable or refusing engine is logged and reported `declared: false`,
|
|
6446
6586
|
* because the hub (and this addon's boot) must not hang on a migration.
|
|
6447
6587
|
*/
|
|
6448
|
-
async function runAddonMigrations(host, migrations, logger) {
|
|
6588
|
+
async function runAddonMigrations(host, migrations, logger, options = {}) {
|
|
6449
6589
|
const first = migrations[0];
|
|
6450
6590
|
if (first === void 0) return NOT_RUN;
|
|
6451
6591
|
const owner = first.meta.owner;
|
|
@@ -6474,7 +6614,9 @@ async function runAddonMigrations(host, migrations, logger) {
|
|
|
6474
6614
|
else if (status === "blocked") blocked.push(id);
|
|
6475
6615
|
else skipped.push(id);
|
|
6476
6616
|
};
|
|
6617
|
+
const selected = options.only === void 0 ? null : new Set(options.only);
|
|
6477
6618
|
for (const step of declaration.steps) {
|
|
6619
|
+
if (selected !== null && !selected.has(step.id)) continue;
|
|
6478
6620
|
const bound = byId.get(step.id);
|
|
6479
6621
|
if (bound === void 0 || step.action === "skip") {
|
|
6480
6622
|
skipped.push(step.id);
|
|
@@ -6652,11 +6794,36 @@ function projectStore(store, perNodeKeys, nodeId) {
|
|
|
6652
6794
|
}
|
|
6653
6795
|
return out;
|
|
6654
6796
|
}
|
|
6797
|
+
/**
|
|
6798
|
+
* The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
|
|
6799
|
+
*
|
|
6800
|
+
* A ref is DETERMINISTIC from where the value lives — the owner addon, the
|
|
6801
|
+
* scope (global, one node, one device, one integration, one row of a business
|
|
6802
|
+
* collection) and the field path inside that scope — so saving the same field
|
|
6803
|
+
* twice updates the same ref, and the ref itself says which settings blob
|
|
6804
|
+
* points at it. It is also the AAD of the value's ciphertext: a ciphertext
|
|
6805
|
+
* copied onto another ref does not decrypt.
|
|
6806
|
+
*
|
|
6807
|
+
* Array items are addressed by ITEM ID, never by index
|
|
6808
|
+
* (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
|
|
6809
|
+
*
|
|
6810
|
+
* Pure and browser-safe: no node builtins, no I/O.
|
|
6811
|
+
*/
|
|
6812
|
+
/** The URI scheme of every keychain ref. */
|
|
6813
|
+
var SECRET_REF_SCHEME = "secret://";
|
|
6655
6814
|
var GLOBAL_SCOPE = { kind: "global" };
|
|
6656
6815
|
/** The scope every addon-wide (not per-node, not per-device) setting lives in. */
|
|
6657
6816
|
function globalSecretScope() {
|
|
6658
6817
|
return GLOBAL_SCOPE;
|
|
6659
6818
|
}
|
|
6819
|
+
/**
|
|
6820
|
+
* An addon id as a ref owner. A hub in-process builtin's context id is
|
|
6821
|
+
* `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
|
|
6822
|
+
* addon running forked.
|
|
6823
|
+
*/
|
|
6824
|
+
function normalizeSecretOwner(addonId) {
|
|
6825
|
+
return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
|
|
6826
|
+
}
|
|
6660
6827
|
function encodeSegment(value) {
|
|
6661
6828
|
return encodeURIComponent(value);
|
|
6662
6829
|
}
|
|
@@ -6667,6 +6834,16 @@ function decodeSegment(value) {
|
|
|
6667
6834
|
return null;
|
|
6668
6835
|
}
|
|
6669
6836
|
}
|
|
6837
|
+
/** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
|
|
6838
|
+
function formatSecretScope(scope) {
|
|
6839
|
+
switch (scope.kind) {
|
|
6840
|
+
case "global": return "global";
|
|
6841
|
+
case "node": return `node/${encodeSegment(scope.nodeId)}`;
|
|
6842
|
+
case "device": return `device/${scope.deviceId}`;
|
|
6843
|
+
case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
|
|
6844
|
+
case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
|
|
6845
|
+
}
|
|
6846
|
+
}
|
|
6670
6847
|
/**
|
|
6671
6848
|
* The field path of a value inside an array item, addressed by the item's id:
|
|
6672
6849
|
* `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
|
|
@@ -6675,6 +6852,13 @@ function decodeSegment(value) {
|
|
|
6675
6852
|
function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
|
|
6676
6853
|
return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
|
|
6677
6854
|
}
|
|
6855
|
+
/** Build a ref. Throws on an empty owner or field path — both are bugs. */
|
|
6856
|
+
function buildSecretRef(parts) {
|
|
6857
|
+
const owner = normalizeSecretOwner(parts.owner);
|
|
6858
|
+
if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
|
|
6859
|
+
if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
|
|
6860
|
+
return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
|
|
6861
|
+
}
|
|
6678
6862
|
function parseScope(segments) {
|
|
6679
6863
|
switch (segments[0]) {
|
|
6680
6864
|
case "global": return {
|
|
@@ -7592,14 +7776,18 @@ var BaseAddon = class {
|
|
|
7592
7776
|
* nothing; the boot continues. A migration that FAILED (or was blocked by one)
|
|
7593
7777
|
* and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
|
|
7594
7778
|
* serve data whose shape it cannot trust; `tolerate` migrations never do.
|
|
7779
|
+
*
|
|
7780
|
+
* ALWAYS pass the whole catalogue; a phase that must run only some of it passes
|
|
7781
|
+
* `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
|
|
7782
|
+
* omitted applied ids "newer").
|
|
7595
7783
|
*/
|
|
7596
|
-
async runMigrations(migrations) {
|
|
7784
|
+
async runMigrations(migrations, options) {
|
|
7597
7785
|
const api = this.ctx.api.migrations;
|
|
7598
7786
|
const report = await runAddonMigrations({
|
|
7599
7787
|
declareOwner: (input) => api.declareOwner.mutate(input),
|
|
7600
7788
|
startMigration: (input) => api.startMigration.mutate(input),
|
|
7601
7789
|
finishMigration: (input) => api.finishMigration.mutate(input)
|
|
7602
|
-
}, migrations, this.ctx.logger);
|
|
7790
|
+
}, migrations, this.ctx.logger, options);
|
|
7603
7791
|
if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
|
|
7604
7792
|
return report;
|
|
7605
7793
|
}
|
|
@@ -7940,6 +8128,10 @@ var BaseAddon = class {
|
|
|
7940
8128
|
const stored = keychain ? await this.readAddonStoreWithRetry() : {};
|
|
7941
8129
|
const patch = {};
|
|
7942
8130
|
for (const key of keys) {
|
|
8131
|
+
if (tree.get(key)?.kind === "array") {
|
|
8132
|
+
patch[key] = [];
|
|
8133
|
+
continue;
|
|
8134
|
+
}
|
|
7943
8135
|
const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
|
|
7944
8136
|
patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
|
|
7945
8137
|
}
|
|
@@ -8579,6 +8771,45 @@ function normalizeAddonInitResult(result) {
|
|
|
8579
8771
|
if (Array.isArray(result)) return { providers: result };
|
|
8580
8772
|
return result;
|
|
8581
8773
|
}
|
|
8774
|
+
var PREFIX = "records/";
|
|
8775
|
+
/** The base of every records failure. `detail` is the human part of the message. */
|
|
8776
|
+
var RecordsError = class extends Error {
|
|
8777
|
+
code;
|
|
8778
|
+
detail;
|
|
8779
|
+
constructor(code, detail) {
|
|
8780
|
+
super(`${PREFIX}${code}: ${detail}`);
|
|
8781
|
+
this.name = "RecordsError";
|
|
8782
|
+
this.code = code;
|
|
8783
|
+
this.detail = detail;
|
|
8784
|
+
}
|
|
8785
|
+
};
|
|
8786
|
+
/** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
|
|
8787
|
+
var RevConflictError = class extends RecordsError {
|
|
8788
|
+
constructor(detail) {
|
|
8789
|
+
super("rev-conflict", detail);
|
|
8790
|
+
this.name = "RevConflictError";
|
|
8791
|
+
}
|
|
8792
|
+
};
|
|
8793
|
+
/** The keychain could not store or serve a value: explicit, never an empty string. */
|
|
8794
|
+
var RecordSecretUnavailableError = class extends RecordsError {
|
|
8795
|
+
constructor(detail) {
|
|
8796
|
+
super("secret-unavailable", detail);
|
|
8797
|
+
this.name = "RecordSecretUnavailableError";
|
|
8798
|
+
}
|
|
8799
|
+
};
|
|
8800
|
+
var InvalidRecordError = class extends RecordsError {
|
|
8801
|
+
constructor(detail) {
|
|
8802
|
+
super("invalid-record", detail);
|
|
8803
|
+
this.name = "InvalidRecordError";
|
|
8804
|
+
}
|
|
8805
|
+
};
|
|
8806
|
+
/** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
|
|
8807
|
+
var DeclarationRefusedError = class extends RecordsError {
|
|
8808
|
+
constructor(detail) {
|
|
8809
|
+
super("declaration-refused", detail);
|
|
8810
|
+
this.name = "DeclarationRefusedError";
|
|
8811
|
+
}
|
|
8812
|
+
};
|
|
8582
8813
|
/** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
|
|
8583
8814
|
/** The files a kind may live in. `history` is device-history's (D850), never records. */
|
|
8584
8815
|
var RECORD_DATABASES = ["settings", "events"];
|
|
@@ -8594,6 +8825,183 @@ var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
|
|
|
8594
8825
|
var RECORD_MAX_DATA_BYTES = 1024 * 1024;
|
|
8595
8826
|
var RECORD_BATCH_MAX = 1e3;
|
|
8596
8827
|
var RECORD_QUERY_MAX = 1e3;
|
|
8828
|
+
/** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
|
|
8829
|
+
var RECORD_SETTINGS_MAX_ROWS = 5e4;
|
|
8830
|
+
/** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
|
|
8831
|
+
var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
8832
|
+
/**
|
|
8833
|
+
* Secret fields of a record, addressed the way a Zod schema declares them
|
|
8834
|
+
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
8835
|
+
* (`auth.accessToken`, `headers[].value`).
|
|
8836
|
+
*
|
|
8837
|
+
* `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
|
|
8838
|
+
* names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
|
|
8839
|
+
* key releases exactly that key's secret.
|
|
8840
|
+
*
|
|
8841
|
+
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
8842
|
+
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
8843
|
+
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
8844
|
+
* leaf names its element: `headers[X-Api].value`.
|
|
8845
|
+
*
|
|
8846
|
+
* Pure and browser-safe. Nothing here mutates: every transform returns a new value.
|
|
8847
|
+
*/
|
|
8848
|
+
/** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
|
|
8849
|
+
function parseSecretPath(path) {
|
|
8850
|
+
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
8851
|
+
return path.split(".").map((raw) => {
|
|
8852
|
+
const each = raw.endsWith("[]");
|
|
8853
|
+
const map = raw.endsWith("{}");
|
|
8854
|
+
const key = each || map ? raw.slice(0, -2) : raw;
|
|
8855
|
+
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
8856
|
+
return {
|
|
8857
|
+
key,
|
|
8858
|
+
each,
|
|
8859
|
+
map
|
|
8860
|
+
};
|
|
8861
|
+
});
|
|
8862
|
+
}
|
|
8863
|
+
/** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
|
|
8864
|
+
function secretArrayPrefixes(path) {
|
|
8865
|
+
const segments = parseSecretPath(path);
|
|
8866
|
+
const prefixes = [];
|
|
8867
|
+
const walked = [];
|
|
8868
|
+
for (const segment of segments) {
|
|
8869
|
+
walked.push(segment.key);
|
|
8870
|
+
if (segment.each) prefixes.push(walked.join("."));
|
|
8871
|
+
}
|
|
8872
|
+
return prefixes;
|
|
8873
|
+
}
|
|
8874
|
+
function isRecord$4(value) {
|
|
8875
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8876
|
+
}
|
|
8877
|
+
function elementId(element, idKey, declared, prefix) {
|
|
8878
|
+
const id = isRecord$4(element) ? element[idKey] : void 0;
|
|
8879
|
+
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`);
|
|
8880
|
+
return id;
|
|
8881
|
+
}
|
|
8882
|
+
/**
|
|
8883
|
+
* Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
|
|
8884
|
+
* visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
|
|
8885
|
+
* result is a new body, `data` is untouched.
|
|
8886
|
+
*/
|
|
8887
|
+
var REMOVE_LEAF = Symbol("remove-leaf");
|
|
8888
|
+
function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
|
|
8889
|
+
if (!isRecord$4(node)) return node;
|
|
8890
|
+
const segment = segments[index];
|
|
8891
|
+
if (segment === void 0) return node;
|
|
8892
|
+
if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
|
|
8893
|
+
const child = node[segment.key];
|
|
8894
|
+
const keys = [...walkedKeys, segment.key];
|
|
8895
|
+
const last = index === segments.length - 1;
|
|
8896
|
+
if (segment.each) {
|
|
8897
|
+
if (!Array.isArray(child)) return node;
|
|
8898
|
+
const prefix = keys.join(".");
|
|
8899
|
+
const idKey = arrayKeys[prefix];
|
|
8900
|
+
if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
|
|
8901
|
+
const mapped = child.map((element) => {
|
|
8902
|
+
const id = elementId(element, idKey, declared, prefix);
|
|
8903
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
|
|
8904
|
+
return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
|
|
8905
|
+
});
|
|
8906
|
+
return {
|
|
8907
|
+
...node,
|
|
8908
|
+
[segment.key]: mapped
|
|
8909
|
+
};
|
|
8910
|
+
}
|
|
8911
|
+
if (segment.map) {
|
|
8912
|
+
if (!isRecord$4(child)) return node;
|
|
8913
|
+
const kept = Object.entries(child).map(([mapKey, value]) => {
|
|
8914
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
|
|
8915
|
+
if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
|
|
8916
|
+
return [mapKey, visit({
|
|
8917
|
+
declared,
|
|
8918
|
+
concrete,
|
|
8919
|
+
value
|
|
8920
|
+
})];
|
|
8921
|
+
}).filter(([, value]) => value !== REMOVE_LEAF);
|
|
8922
|
+
return {
|
|
8923
|
+
...node,
|
|
8924
|
+
[segment.key]: Object.fromEntries(kept)
|
|
8925
|
+
};
|
|
8926
|
+
}
|
|
8927
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
8928
|
+
if (last) {
|
|
8929
|
+
const replacement = visit({
|
|
8930
|
+
declared,
|
|
8931
|
+
concrete,
|
|
8932
|
+
value: child
|
|
8933
|
+
});
|
|
8934
|
+
if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
|
|
8935
|
+
return {
|
|
8936
|
+
...node,
|
|
8937
|
+
[segment.key]: replacement
|
|
8938
|
+
};
|
|
8939
|
+
}
|
|
8940
|
+
return {
|
|
8941
|
+
...node,
|
|
8942
|
+
[segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
|
|
8943
|
+
};
|
|
8944
|
+
}
|
|
8945
|
+
/** Replace/remove every present secret leaf; returns a NEW body. */
|
|
8946
|
+
function mapSecretLeaves(data, paths, arrayKeys, visit) {
|
|
8947
|
+
let current = data;
|
|
8948
|
+
for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
|
|
8949
|
+
return current;
|
|
8950
|
+
}
|
|
8951
|
+
/** Every present secret leaf, read-only. */
|
|
8952
|
+
function collectSecretLeaves(data, paths, arrayKeys) {
|
|
8953
|
+
const found = [];
|
|
8954
|
+
mapSecretLeaves(data, paths, arrayKeys, (leaf) => {
|
|
8955
|
+
found.push(leaf);
|
|
8956
|
+
return leaf.value;
|
|
8957
|
+
});
|
|
8958
|
+
return found;
|
|
8959
|
+
}
|
|
8960
|
+
/**
|
|
8961
|
+
* The keychain collection name a kind's secrets live under. Prefixed so a
|
|
8962
|
+
* record's refs never collide with the refs a legacy collection of the same name
|
|
8963
|
+
* already holds during an adoption.
|
|
8964
|
+
*/
|
|
8965
|
+
function recordSecretCollection(kind) {
|
|
8966
|
+
return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
|
|
8967
|
+
}
|
|
8968
|
+
/** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
|
|
8969
|
+
function recordSecretRef(owner, kind, rowId, concretePath) {
|
|
8970
|
+
return buildSecretRef({
|
|
8971
|
+
owner,
|
|
8972
|
+
scope: {
|
|
8973
|
+
kind: "row",
|
|
8974
|
+
collection: recordSecretCollection(kind),
|
|
8975
|
+
rowId
|
|
8976
|
+
},
|
|
8977
|
+
fieldPath: concretePath
|
|
8978
|
+
});
|
|
8979
|
+
}
|
|
8980
|
+
/**
|
|
8981
|
+
* A stored secret, as an admin read shows it: a VIEW naming the row's own
|
|
8982
|
+
* keychain ref (`set: true`), never the value and no longer the bare redaction
|
|
8983
|
+
* token. The form hands it to the secret field, which reveals the value by ref
|
|
8984
|
+
* through the audited `keychain-admin.reveal`; sent back untouched it keeps the
|
|
8985
|
+
* stored secret (the records door reads a view as KEEP).
|
|
8986
|
+
*
|
|
8987
|
+
* ONE implementation for the entries service and for every addon that serves
|
|
8988
|
+
* its own instances (`RecordIntegrationInstances`).
|
|
8989
|
+
*/
|
|
8990
|
+
function isRecord$3(value) {
|
|
8991
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8992
|
+
}
|
|
8993
|
+
function secretViewsOf(owner, kind, id, decl, data) {
|
|
8994
|
+
if (decl.secretPaths.length === 0) return data;
|
|
8995
|
+
const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
|
|
8996
|
+
if (leaf.value !== "__camstack_redacted__") return leaf.value;
|
|
8997
|
+
return {
|
|
8998
|
+
$secret: recordSecretRef(owner, kind, id, leaf.concrete),
|
|
8999
|
+
v: 1,
|
|
9000
|
+
set: true
|
|
9001
|
+
};
|
|
9002
|
+
});
|
|
9003
|
+
return isRecord$3(shown) ? shown : data;
|
|
9004
|
+
}
|
|
8597
9005
|
/**
|
|
8598
9006
|
* The share-view scope kinds a share-link token can carry. The hub mints
|
|
8599
9007
|
* and validates them (`ShareViewGrantSchema` builds its
|
|
@@ -8618,6 +9026,75 @@ var PeerBytesTicketSchema = object({
|
|
|
8618
9026
|
*/
|
|
8619
9027
|
declaredBytes: number().int().nonnegative().nullable()
|
|
8620
9028
|
});
|
|
9029
|
+
function defOf(schema) {
|
|
9030
|
+
const zodBag = Reflect.get(schema, "_zod");
|
|
9031
|
+
if (typeof zodBag !== "object" || zodBag === null) return {};
|
|
9032
|
+
const def = Reflect.get(zodBag, "def");
|
|
9033
|
+
if (typeof def !== "object" || def === null) return {};
|
|
9034
|
+
return def;
|
|
9035
|
+
}
|
|
9036
|
+
function declaredHere(schema) {
|
|
9037
|
+
return globalRegistry.get(schema)?.secret === true;
|
|
9038
|
+
}
|
|
9039
|
+
/** Wrapper kinds whose `innerType` is the field itself. */
|
|
9040
|
+
var WRAPPERS = new Set([
|
|
9041
|
+
"optional",
|
|
9042
|
+
"nullable",
|
|
9043
|
+
"default",
|
|
9044
|
+
"prefault",
|
|
9045
|
+
"catch",
|
|
9046
|
+
"readonly",
|
|
9047
|
+
"nonoptional"
|
|
9048
|
+
]);
|
|
9049
|
+
/**
|
|
9050
|
+
* The schema layers that make up ONE field: the schema itself and every
|
|
9051
|
+
* wrapper below it (optional / default / nullable / readonly / catch), plus
|
|
9052
|
+
* both sides of a pipe (`z.preprocess` puts the real schema on `out`,
|
|
9053
|
+
* `.transform` on `in`).
|
|
9054
|
+
*/
|
|
9055
|
+
function layersOf(schema) {
|
|
9056
|
+
const out = [];
|
|
9057
|
+
const queue = [schema];
|
|
9058
|
+
while (queue.length > 0) {
|
|
9059
|
+
const s = queue.shift();
|
|
9060
|
+
if (!s || out.includes(s)) continue;
|
|
9061
|
+
out.push(s);
|
|
9062
|
+
const def = defOf(s);
|
|
9063
|
+
if (def.type !== void 0 && WRAPPERS.has(def.type) && def.innerType) queue.push(def.innerType);
|
|
9064
|
+
if (def.type === "pipe") {
|
|
9065
|
+
if (def.in) queue.push(def.in);
|
|
9066
|
+
if (def.out) queue.push(def.out);
|
|
9067
|
+
}
|
|
9068
|
+
}
|
|
9069
|
+
return out;
|
|
9070
|
+
}
|
|
9071
|
+
/** True when any layer of this field declares `.meta({ secret: true })`. */
|
|
9072
|
+
function isZodSecret(schema) {
|
|
9073
|
+
return layersOf(schema).some(declaredHere);
|
|
9074
|
+
}
|
|
9075
|
+
/**
|
|
9076
|
+
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
9077
|
+
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
9078
|
+
* map" (`applianceSecrets{}`). A declared
|
|
9079
|
+
* object or array is reported as one path and not descended into — everything
|
|
9080
|
+
* under it is secret.
|
|
9081
|
+
*/
|
|
9082
|
+
function collectZodSecretPaths(schema, prefix = "") {
|
|
9083
|
+
if (isZodSecret(schema)) return prefix.length > 0 ? [prefix] : [];
|
|
9084
|
+
const paths = [];
|
|
9085
|
+
for (const layer of layersOf(schema)) {
|
|
9086
|
+
const def = defOf(layer);
|
|
9087
|
+
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
9088
|
+
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
9089
|
+
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
9090
|
+
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
9091
|
+
else if (def.type === "intersection") {
|
|
9092
|
+
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
9093
|
+
if (def.right) paths.push(...collectZodSecretPaths(def.right, prefix));
|
|
9094
|
+
}
|
|
9095
|
+
}
|
|
9096
|
+
return [...new Set(paths)];
|
|
9097
|
+
}
|
|
8621
9098
|
/**
|
|
8622
9099
|
* Record kinds (D881): the declaration an addon makes of one kind of fact, the
|
|
8623
9100
|
* Zod row schema that describes it, and the helper that turns the two into the
|
|
@@ -8684,6 +9161,126 @@ var RecordKindDeclarationSchema = object({
|
|
|
8684
9161
|
}).strict();
|
|
8685
9162
|
/** The kind names: lower-case, dash separated, short enough for an index name. */
|
|
8686
9163
|
var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
|
|
9164
|
+
/** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
|
|
9165
|
+
function assertValidDeclaration(name, decl) {
|
|
9166
|
+
const refuse = (why) => {
|
|
9167
|
+
throw new DeclarationRefusedError(`kind "${name}": ${why}`);
|
|
9168
|
+
};
|
|
9169
|
+
if (!RecordKindNameSchema.safeParse(name).success) refuse("the name must be lower-case-dashed");
|
|
9170
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9171
|
+
for (const index of decl.indexes) {
|
|
9172
|
+
if (seen.has(index.name)) refuse(`index name "${index.name}" is declared twice`);
|
|
9173
|
+
seen.add(index.name);
|
|
9174
|
+
}
|
|
9175
|
+
const { retention } = decl;
|
|
9176
|
+
const hasSecrets = decl.secretPaths.length > 0;
|
|
9177
|
+
if ("ttlMs" in retention) {
|
|
9178
|
+
if (retention.from === "at" && decl.at !== "required") refuse("a ttl from \"at\" needs at: \"required\"");
|
|
9179
|
+
if (hasSecrets) refuse("a kind with secret fields cannot use engine TTL (its keychain values would be orphaned)");
|
|
9180
|
+
}
|
|
9181
|
+
if ("maxRows" in retention || "maxRowsPerDevice" in retention) {
|
|
9182
|
+
if (retention.maxRowsPerDevice !== void 0 && decl.device !== "required") refuse("maxRowsPerDevice needs device: \"required\"");
|
|
9183
|
+
if (decl.at !== "required") refuse("a row bound trims the oldest \"at\": it needs at: \"required\"");
|
|
9184
|
+
if (hasSecrets) refuse("a kind with secret fields cannot use an engine row bound");
|
|
9185
|
+
const bound = Math.max(retention.maxRows ?? 0, retention.maxRowsPerDevice ?? 0);
|
|
9186
|
+
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)`);
|
|
9187
|
+
}
|
|
9188
|
+
if (decl.deviceRemoval === "delete" && decl.device === "none") refuse("deviceRemoval: \"delete\" needs a device: \"required\" or \"optional\"");
|
|
9189
|
+
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`);
|
|
9190
|
+
}
|
|
9191
|
+
function bodyFieldsOf(schema) {
|
|
9192
|
+
return { ...toJSONSchema(schema, {
|
|
9193
|
+
unrepresentable: "any",
|
|
9194
|
+
io: "input"
|
|
9195
|
+
}) };
|
|
9196
|
+
}
|
|
9197
|
+
function isRecordLike(value) {
|
|
9198
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9199
|
+
}
|
|
9200
|
+
/** Does every branch of the body schema carry `field`? (A discriminated union has several.) */
|
|
9201
|
+
function hasNameField(json, field) {
|
|
9202
|
+
const branches = json["oneOf"] ?? json["anyOf"];
|
|
9203
|
+
if (Array.isArray(branches)) return branches.length > 0 && branches.every((b) => isRecordLike(b) && hasNameField(b, field));
|
|
9204
|
+
const properties = json["properties"];
|
|
9205
|
+
return isRecordLike(properties) && Object.prototype.hasOwnProperty.call(properties, field);
|
|
9206
|
+
}
|
|
9207
|
+
/**
|
|
9208
|
+
* Declare the kinds of a package. The keys are the kind names. The result is
|
|
9209
|
+
* what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
|
|
9210
|
+
* types its handle from.
|
|
9211
|
+
*/
|
|
9212
|
+
function defineRecordKinds(specs) {
|
|
9213
|
+
const out = {};
|
|
9214
|
+
for (const [name, spec] of Object.entries(specs)) {
|
|
9215
|
+
const secretPaths = collectZodSecretPaths(spec.schema);
|
|
9216
|
+
const nameField = spec.entry?.nameField ?? "name";
|
|
9217
|
+
const fields = bodyFieldsOf(spec.schema);
|
|
9218
|
+
if (spec.entry !== void 0 && !hasNameField(fields, nameField)) throw new DeclarationRefusedError(`kind "${name}": an entry kind's schema needs a "${nameField}" field`);
|
|
9219
|
+
const parsed = RecordKindDeclarationSchema.parse({
|
|
9220
|
+
database: spec.database,
|
|
9221
|
+
class: spec.class,
|
|
9222
|
+
writeTrigger: spec.writeTrigger,
|
|
9223
|
+
device: spec.device,
|
|
9224
|
+
at: spec.at,
|
|
9225
|
+
indexes: spec.indexes === void 0 ? void 0 : [...spec.indexes],
|
|
9226
|
+
retention: spec.retention,
|
|
9227
|
+
deviceRemoval: spec.deviceRemoval,
|
|
9228
|
+
maxDataBytes: spec.maxDataBytes,
|
|
9229
|
+
secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
|
|
9230
|
+
secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
|
|
9231
|
+
transactFamily: spec.transactFamily,
|
|
9232
|
+
entry: spec.entry === void 0 ? void 0 : {
|
|
9233
|
+
title: spec.entry.title,
|
|
9234
|
+
nameField,
|
|
9235
|
+
fields
|
|
9236
|
+
}
|
|
9237
|
+
});
|
|
9238
|
+
assertValidDeclaration(name, parsed);
|
|
9239
|
+
out[name] = {
|
|
9240
|
+
name,
|
|
9241
|
+
schema: spec.schema,
|
|
9242
|
+
declaration: parsed
|
|
9243
|
+
};
|
|
9244
|
+
}
|
|
9245
|
+
return out;
|
|
9246
|
+
}
|
|
9247
|
+
/**
|
|
9248
|
+
* Entry kinds (D881): every list of user-created entries (notification
|
|
9249
|
+
* targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
|
|
9250
|
+
* kind-specific body, and the admin UI lists/adds/edits/deletes all of them
|
|
9251
|
+
* through the `record-entries` capability with password fields derived from
|
|
9252
|
+
* the schema (`.meta({ secret: true })`).
|
|
9253
|
+
*/
|
|
9254
|
+
/** The body of a plain entry: a name and the fields the kind adds. */
|
|
9255
|
+
function entryBody(shape) {
|
|
9256
|
+
return object({
|
|
9257
|
+
name: string().min(1),
|
|
9258
|
+
...shape
|
|
9259
|
+
});
|
|
9260
|
+
}
|
|
9261
|
+
/**
|
|
9262
|
+
* Declare one entry kind. An entry is configuration: never trimmed by the
|
|
9263
|
+
* engine, never aged, belonging to no device.
|
|
9264
|
+
*/
|
|
9265
|
+
function defineEntryKind(name, spec) {
|
|
9266
|
+
const defined = defineRecordKinds({ [name]: {
|
|
9267
|
+
schema: spec.schema,
|
|
9268
|
+
class: "config",
|
|
9269
|
+
writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
|
|
9270
|
+
retention: { none: "an entry goes only when the operator deletes it" },
|
|
9271
|
+
...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
|
|
9272
|
+
...spec.database === void 0 ? {} : { database: spec.database },
|
|
9273
|
+
...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
|
|
9274
|
+
...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
|
|
9275
|
+
...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
|
|
9276
|
+
entry: {
|
|
9277
|
+
title: spec.title,
|
|
9278
|
+
...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
|
|
9279
|
+
}
|
|
9280
|
+
} })[name];
|
|
9281
|
+
if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
|
|
9282
|
+
return defined;
|
|
9283
|
+
}
|
|
8687
9284
|
object({
|
|
8688
9285
|
field: string().min(1),
|
|
8689
9286
|
on: literal("advance")
|
|
@@ -11337,7 +11934,7 @@ object({
|
|
|
11337
11934
|
params: record(string(), union([string(), number()])).optional()
|
|
11338
11935
|
});
|
|
11339
11936
|
//#endregion
|
|
11340
|
-
//#region ../types/dist/composition-
|
|
11937
|
+
//#region ../types/dist/composition-CRNwcFYY.mjs
|
|
11341
11938
|
/**
|
|
11342
11939
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
11343
11940
|
*
|
|
@@ -12100,38 +12697,6 @@ function validateExpressionSource(src) {
|
|
|
12100
12697
|
return null;
|
|
12101
12698
|
}
|
|
12102
12699
|
/**
|
|
12103
|
-
* One power reading becomes a washer, dryer or dishwasher (D944 helper).
|
|
12104
|
-
*
|
|
12105
|
-
* Standby of the control board is under 5 W; a motor or a heater is far
|
|
12106
|
-
* above that, so 5 W is the line. A door lock or a short pump spike is not
|
|
12107
|
-
* a cycle, so running starts only after 30 s above the line.
|
|
12108
|
-
*
|
|
12109
|
-
* A wash, dry or dishwasher pause (soak, heat-wait, heater off between
|
|
12110
|
-
* bursts) lasts several minutes. Ending the cycle on the first dip would
|
|
12111
|
-
* report "finished" in the middle of the program, so the stop dwell is
|
|
12112
|
-
* 10 minutes on every type. The number is stored per type so one machine
|
|
12113
|
-
* can move later without moving the others.
|
|
12114
|
-
*/
|
|
12115
|
-
var APPLIANCE_TEMPLATE_ID = "appliance";
|
|
12116
|
-
var APPLIANCE_KINDS = [
|
|
12117
|
-
"washer",
|
|
12118
|
-
"dryer",
|
|
12119
|
-
"dishwasher"
|
|
12120
|
-
];
|
|
12121
|
-
DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
|
|
12122
|
-
/**
|
|
12123
|
-
* A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
|
|
12124
|
-
*
|
|
12125
|
-
* The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
|
|
12126
|
-
* (D967), so the heater switches exactly as the plug does. Running is the
|
|
12127
|
-
* plug being on; when a power reading is also named, running is the draw
|
|
12128
|
-
* staying above the line instead (a plug left on with the stove's own
|
|
12129
|
-
* thermostat open is not heating). 50 W is far above any standby and below
|
|
12130
|
-
* the smallest radiator.
|
|
12131
|
-
*/
|
|
12132
|
-
var HEATER_TEMPLATE_ID = "heater";
|
|
12133
|
-
DeviceType.Heater;
|
|
12134
|
-
/**
|
|
12135
12700
|
* Statistics and integrals over a history read (D944).
|
|
12136
12701
|
*
|
|
12137
12702
|
* Pure: the same points always give the same answer, so a restart recomputes
|
|
@@ -12275,34 +12840,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
|
|
|
12275
12840
|
/** Actuator groups only. Readings leave this unset. */
|
|
12276
12841
|
active: ActiveCriterionSchema.optional()
|
|
12277
12842
|
}),
|
|
12278
|
-
object({
|
|
12279
|
-
kind: literal("appliance"),
|
|
12280
|
-
templateId: literal(APPLIANCE_TEMPLATE_ID),
|
|
12281
|
-
appliance: _enum(APPLIANCE_KINDS),
|
|
12282
|
-
thresholdWatts: number().positive(),
|
|
12283
|
-
startDwellMs: number().int().nonnegative(),
|
|
12284
|
-
stopDwellMs: number().int().nonnegative(),
|
|
12285
|
-
member: HelperMemberSchema,
|
|
12286
|
-
/**
|
|
12287
|
-
* The plug that powers the appliance (`switch.on`). Optional: when set, the
|
|
12288
|
-
* composition carries a `power-source` feature, so the hub can cut and restore
|
|
12289
|
-
* that plug (power-source link of a NEW-target composition).
|
|
12290
|
-
*/
|
|
12291
|
-
powerSwitch: HelperMemberSchema.optional(),
|
|
12292
|
-
/** How long a forced restart keeps the plug off; the cap's default when unset. */
|
|
12293
|
-
offSeconds: number().optional()
|
|
12294
|
-
}),
|
|
12295
|
-
object({
|
|
12296
|
-
kind: literal("heater"),
|
|
12297
|
-
templateId: literal(HEATER_TEMPLATE_ID),
|
|
12298
|
-
/** The plug, read and commanded at `switch.on`. */
|
|
12299
|
-
plug: HelperMemberSchema,
|
|
12300
|
-
/** The plug's power reading, when it has one: running is then the draw, not the switch. */
|
|
12301
|
-
meter: HelperMemberSchema.optional(),
|
|
12302
|
-
thresholdWatts: number().positive(),
|
|
12303
|
-
startDwellMs: number().int().nonnegative(),
|
|
12304
|
-
stopDwellMs: number().int().nonnegative()
|
|
12305
|
-
}),
|
|
12306
12843
|
object({
|
|
12307
12844
|
kind: literal("statistic"),
|
|
12308
12845
|
stat: _enum(STATISTIC_STATS),
|
|
@@ -12526,7 +13063,7 @@ var CompositionSchema = object({
|
|
|
12526
13063
|
kind: literal("existing"),
|
|
12527
13064
|
device: CompositionSourceRefSchema
|
|
12528
13065
|
})]),
|
|
12529
|
-
features: array(CompositionFeatureSchema).min(
|
|
13066
|
+
features: array(CompositionFeatureSchema).min(0).max(16),
|
|
12530
13067
|
/**
|
|
12531
13068
|
* Present when the block is a helper template. Absent on a free
|
|
12532
13069
|
* composition (the room example, device 4603). The editor reads this,
|
|
@@ -12744,6 +13281,205 @@ var IntegrationInstanceRefSchema = object({
|
|
|
12744
13281
|
]),
|
|
12745
13282
|
candidateStableId: string().optional()
|
|
12746
13283
|
});
|
|
13284
|
+
/**
|
|
13285
|
+
* `RecordIntegrationInstances` - the `integration-instances` provider over ONE
|
|
13286
|
+
* entry kind, so an addon that holds a connection serves it in ~30 lines.
|
|
13287
|
+
*
|
|
13288
|
+
* Every instance is one record of the addon's own kind, keyed by the
|
|
13289
|
+
* `integrations` registry id. The records door does the secret work: a field the
|
|
13290
|
+
* kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
|
|
13291
|
+
* deleting the row deletes the secret, and one row's value is never another's.
|
|
13292
|
+
*
|
|
13293
|
+
* Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
|
|
13294
|
+
* token or the view the form was given KEEP the stored secret;
|
|
13295
|
+
* `{ $secretClear: true }` clears it; any other string replaces it.
|
|
13296
|
+
*/
|
|
13297
|
+
/** The most instances one addon lists (an integrations page, not a table). */
|
|
13298
|
+
var INSTANCES_LIST_MAX = 1e3;
|
|
13299
|
+
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
13300
|
+
function topLevelSecretKeys(paths) {
|
|
13301
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
|
|
13302
|
+
}
|
|
13303
|
+
/** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
|
|
13304
|
+
function secretMapKeys(paths) {
|
|
13305
|
+
return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
|
|
13306
|
+
}
|
|
13307
|
+
/**
|
|
13308
|
+
* A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
|
|
13309
|
+
* view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
|
|
13310
|
+
* or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
|
|
13311
|
+
*/
|
|
13312
|
+
function secretMapPatch(value) {
|
|
13313
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
13314
|
+
const out = {};
|
|
13315
|
+
for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
|
|
13316
|
+
else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
|
|
13317
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
13318
|
+
}
|
|
13319
|
+
/** Does the kind's body schema carry an `enabled` field? */
|
|
13320
|
+
function declaresEnabled(kind) {
|
|
13321
|
+
const json = toJSONSchema(kind.schema, {
|
|
13322
|
+
unrepresentable: "any",
|
|
13323
|
+
io: "input"
|
|
13324
|
+
});
|
|
13325
|
+
const properties = Reflect.get(json, "properties");
|
|
13326
|
+
return typeof properties === "object" && properties !== null && "enabled" in properties;
|
|
13327
|
+
}
|
|
13328
|
+
var RecordIntegrationInstances = class {
|
|
13329
|
+
records;
|
|
13330
|
+
kind;
|
|
13331
|
+
hooks;
|
|
13332
|
+
handle;
|
|
13333
|
+
secretKeys;
|
|
13334
|
+
secretMaps;
|
|
13335
|
+
nameField;
|
|
13336
|
+
/** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
|
|
13337
|
+
registryOwnsEnabled;
|
|
13338
|
+
constructor(records, kind, hooks) {
|
|
13339
|
+
this.records = records;
|
|
13340
|
+
this.kind = kind;
|
|
13341
|
+
this.hooks = hooks;
|
|
13342
|
+
this.handle = records.kind(kind);
|
|
13343
|
+
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
13344
|
+
this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
|
|
13345
|
+
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
13346
|
+
this.registryOwnsEnabled = hooks.description.role === "devices";
|
|
13347
|
+
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"`);
|
|
13348
|
+
}
|
|
13349
|
+
/**
|
|
13350
|
+
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
13351
|
+
* locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
|
|
13352
|
+
* caller can keep what it had instead of reconfiguring with an empty secret (D49).
|
|
13353
|
+
*/
|
|
13354
|
+
async readResolved(id) {
|
|
13355
|
+
try {
|
|
13356
|
+
const record = await this.handle.getResolved(id);
|
|
13357
|
+
return record === null ? null : {
|
|
13358
|
+
id,
|
|
13359
|
+
body: record.data,
|
|
13360
|
+
enabled: record.data.enabled !== false,
|
|
13361
|
+
secrets: "resolved"
|
|
13362
|
+
};
|
|
13363
|
+
} catch (err) {
|
|
13364
|
+
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
13365
|
+
const stored = await this.handle.get(id);
|
|
13366
|
+
if (stored === null) return null;
|
|
13367
|
+
return {
|
|
13368
|
+
id,
|
|
13369
|
+
body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
|
|
13370
|
+
enabled: stored.data.enabled !== false,
|
|
13371
|
+
secrets: "unavailable"
|
|
13372
|
+
};
|
|
13373
|
+
}
|
|
13374
|
+
}
|
|
13375
|
+
/** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
|
|
13376
|
+
async getResolved(id) {
|
|
13377
|
+
return (await this.readResolved(id))?.body ?? null;
|
|
13378
|
+
}
|
|
13379
|
+
/** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
|
|
13380
|
+
async listResolved() {
|
|
13381
|
+
const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
|
|
13382
|
+
const out = [];
|
|
13383
|
+
for (const record of page.records) {
|
|
13384
|
+
const read = await this.readResolved(record.id);
|
|
13385
|
+
if (read !== null) out.push(read);
|
|
13386
|
+
}
|
|
13387
|
+
return out;
|
|
13388
|
+
}
|
|
13389
|
+
viewOf(record) {
|
|
13390
|
+
const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
|
|
13391
|
+
const rawName = record.data[this.nameField];
|
|
13392
|
+
const kind = this.hooks.kindOf?.(record.data);
|
|
13393
|
+
const health = this.hooks.health?.(record.data, record.id);
|
|
13394
|
+
return {
|
|
13395
|
+
id: record.id,
|
|
13396
|
+
name: typeof rawName === "string" ? rawName : record.id,
|
|
13397
|
+
configured: this.hooks.configured(record.data),
|
|
13398
|
+
...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
|
|
13399
|
+
formValues: shown,
|
|
13400
|
+
...kind !== void 0 ? { kind } : {},
|
|
13401
|
+
...health?.status !== void 0 ? { status: health.status } : {},
|
|
13402
|
+
...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
|
|
13403
|
+
};
|
|
13404
|
+
}
|
|
13405
|
+
describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
|
|
13406
|
+
...this.hooks.description,
|
|
13407
|
+
mintsId: true
|
|
13408
|
+
};
|
|
13409
|
+
instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
|
|
13410
|
+
listInstances = async () => {
|
|
13411
|
+
return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
|
|
13412
|
+
};
|
|
13413
|
+
getInstance = async ({ id }) => {
|
|
13414
|
+
const record = await this.handle.get(id);
|
|
13415
|
+
return record === null ? null : this.viewOf(record);
|
|
13416
|
+
};
|
|
13417
|
+
/** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
|
|
13418
|
+
createInstance = async ({ id, name, kind }) => {
|
|
13419
|
+
const minted = id ?? this.hooks.mintId?.({
|
|
13420
|
+
name,
|
|
13421
|
+
...kind !== void 0 ? { kind } : {}
|
|
13422
|
+
});
|
|
13423
|
+
if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
|
|
13424
|
+
await this.handle.put({
|
|
13425
|
+
id: minted,
|
|
13426
|
+
data: this.hooks.initialBody(name, kind)
|
|
13427
|
+
}, { ifRev: 0 });
|
|
13428
|
+
await this.hooks.onChanged?.(minted, "created");
|
|
13429
|
+
return { id: minted };
|
|
13430
|
+
};
|
|
13431
|
+
updateInstance = async ({ id, name, values }) => {
|
|
13432
|
+
const patch = {};
|
|
13433
|
+
for (const [key, value] of Object.entries(values)) {
|
|
13434
|
+
if (value === void 0) continue;
|
|
13435
|
+
if (this.secretMaps.has(key)) {
|
|
13436
|
+
const entries = secretMapPatch(value);
|
|
13437
|
+
if (entries !== null) patch[key] = entries;
|
|
13438
|
+
} else if (!this.secretKeys.has(key)) patch[key] = value;
|
|
13439
|
+
else if (isSecretClear(value)) patch[key] = null;
|
|
13440
|
+
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13441
|
+
}
|
|
13442
|
+
if (name !== void 0) patch[this.nameField] = name;
|
|
13443
|
+
await this.handle.patch(id, patch);
|
|
13444
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
13445
|
+
return { ok: true };
|
|
13446
|
+
};
|
|
13447
|
+
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13448
|
+
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13449
|
+
if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
|
|
13450
|
+
await this.handle.patch(id, { enabled });
|
|
13451
|
+
await this.hooks.onChanged?.(id, "enabled");
|
|
13452
|
+
return { ok: true };
|
|
13453
|
+
};
|
|
13454
|
+
/** The row and its keychain refs go together (the records door). */
|
|
13455
|
+
deleteInstance = async ({ id }) => {
|
|
13456
|
+
await this.handle.delete(id);
|
|
13457
|
+
await this.hooks.onChanged?.(id, "deleted");
|
|
13458
|
+
return { ok: true };
|
|
13459
|
+
};
|
|
13460
|
+
testInstance = async (input) => {
|
|
13461
|
+
if (this.hooks.test === void 0) return {
|
|
13462
|
+
success: false,
|
|
13463
|
+
status: "unsupported",
|
|
13464
|
+
error: "This integration declares no connection test"
|
|
13465
|
+
};
|
|
13466
|
+
return this.hooks.test(input);
|
|
13467
|
+
};
|
|
13468
|
+
/** The flat provider object to hand to `registerProvider`. */
|
|
13469
|
+
toProvider() {
|
|
13470
|
+
return {
|
|
13471
|
+
describe: this.describe,
|
|
13472
|
+
instanceFormSchema: this.instanceFormSchema,
|
|
13473
|
+
listInstances: this.listInstances,
|
|
13474
|
+
getInstance: this.getInstance,
|
|
13475
|
+
createInstance: this.createInstance,
|
|
13476
|
+
updateInstance: this.updateInstance,
|
|
13477
|
+
setInstanceEnabled: this.setInstanceEnabled,
|
|
13478
|
+
deleteInstance: this.deleteInstance,
|
|
13479
|
+
testInstance: this.testInstance
|
|
13480
|
+
};
|
|
13481
|
+
}
|
|
13482
|
+
};
|
|
12747
13483
|
var EncodeProfileSchema = object({
|
|
12748
13484
|
video: object({
|
|
12749
13485
|
codec: _enum([
|
|
@@ -13789,7 +14525,13 @@ var MigrationLedgerEntrySchema = object({
|
|
|
13789
14525
|
* joined by the engine from the catalogues it knows this boot. Absent when no
|
|
13790
14526
|
* catalogue lists the id (a newer database).
|
|
13791
14527
|
*/
|
|
13792
|
-
meta: MigrationMetaSchema.optional()
|
|
14528
|
+
meta: MigrationMetaSchema.optional(),
|
|
14529
|
+
/**
|
|
14530
|
+
* Set when this is an applied row of an addon owner whose catalogue the answering
|
|
14531
|
+
* process has not loaded (the owner has not declared yet): the row is applied, the
|
|
14532
|
+
* process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
|
|
14533
|
+
*/
|
|
14534
|
+
ownerNotLoaded: boolean().optional()
|
|
13793
14535
|
});
|
|
13794
14536
|
/** A catalogued migration that is not applied, with where it stands. */
|
|
13795
14537
|
var MigrationPendingEntrySchema = object({
|
|
@@ -13823,6 +14565,339 @@ var MigrationOwnerStepSchema = object({
|
|
|
13823
14565
|
state: union([literal("pending"), MigrationStatusSchema])
|
|
13824
14566
|
});
|
|
13825
14567
|
/**
|
|
14568
|
+
* Move an integration's legacy `integration_settings` rows into a typed record of the addon's own
|
|
14569
|
+
* kind (planning#416, the one store). The record is keyed by the `integrations` registry id, so
|
|
14570
|
+
* the integration id, the devices stamped with it and the automations that name it do not change.
|
|
14571
|
+
*
|
|
14572
|
+
* Order, so a crash or a locked keychain is safe at every point:
|
|
14573
|
+
*
|
|
14574
|
+
* 1. Phase A, read only: every row of every integration of the addon is read with its secrets
|
|
14575
|
+
* resolved. One that cannot be read (a locked keychain) throws, naming the integration, before
|
|
14576
|
+
* anything is written; `block-owner` then holds the addon.
|
|
14577
|
+
* 2. Phase B, per integration: the addon's `bodyFrom` builds the body; the record is put with
|
|
14578
|
+
* `ifRev: 0` (an existing record is never overwritten). A record this pass CREATED is read back
|
|
14579
|
+
* and every secret leaf must equal what the rows held, else the record is deleted and the pass
|
|
14580
|
+
* throws; an EXISTING record must hold every secret the rows carry, else the rows are kept and
|
|
14581
|
+
* a warning names the path. Only then are the rows (and their keychain values) cleared.
|
|
14582
|
+
*
|
|
14583
|
+
* Idempotent: an integration whose rows are gone and whose record exists is skipped, so a rerun is
|
|
14584
|
+
* a no-op. A default integration with no rows still gets its record (an empty body, D985).
|
|
14585
|
+
* Never logs or throws a secret value; messages name integration ids and secret PATHS only.
|
|
14586
|
+
*/
|
|
14587
|
+
function secretLeavesOf(body, spec) {
|
|
14588
|
+
const out = /* @__PURE__ */ new Map();
|
|
14589
|
+
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);
|
|
14590
|
+
return out;
|
|
14591
|
+
}
|
|
14592
|
+
function why(err) {
|
|
14593
|
+
return err instanceof Error ? err.message : String(err);
|
|
14594
|
+
}
|
|
14595
|
+
async function moveIntegrationRowsToRecords(ports, spec) {
|
|
14596
|
+
const integrations = await ports.listIntegrations(spec.addonId);
|
|
14597
|
+
if (integrations.length === 0) return {
|
|
14598
|
+
outcome: "already-done",
|
|
14599
|
+
summary: { integrations: 0 }
|
|
14600
|
+
};
|
|
14601
|
+
const rowsOf = /* @__PURE__ */ new Map();
|
|
14602
|
+
for (const integration of integrations) try {
|
|
14603
|
+
rowsOf.set(integration.id, await ports.readRows(integration.id));
|
|
14604
|
+
} catch (err) {
|
|
14605
|
+
throw new Error(`integration '${integration.id}': the legacy settings cannot be read with their secrets (${why(err)}) - nothing moved`, { cause: err });
|
|
14606
|
+
}
|
|
14607
|
+
let created = 0;
|
|
14608
|
+
let cleared = 0;
|
|
14609
|
+
let kept = 0;
|
|
14610
|
+
for (const integration of integrations) {
|
|
14611
|
+
const values = rowsOf.get(integration.id) ?? {};
|
|
14612
|
+
const hasRows = Object.keys(values).length > 0;
|
|
14613
|
+
const body = spec.bodyFrom(values, ports, integration);
|
|
14614
|
+
const put = await ports.putRecord(integration.id, body);
|
|
14615
|
+
if (put === "created") created += 1;
|
|
14616
|
+
if (!hasRows) continue;
|
|
14617
|
+
const wanted = secretLeavesOf(body, spec);
|
|
14618
|
+
const back = await ports.readRecord(integration.id);
|
|
14619
|
+
if (back === null) throw new Error(`integration '${integration.id}': the record is gone after it was put`);
|
|
14620
|
+
const held = secretLeavesOf(back, spec);
|
|
14621
|
+
if (put === "created") {
|
|
14622
|
+
const wrong = [...wanted].filter(([path, value]) => held.get(path) !== value).map(([p]) => p);
|
|
14623
|
+
if (wrong.length > 0) {
|
|
14624
|
+
await ports.deleteRecord(integration.id);
|
|
14625
|
+
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`);
|
|
14626
|
+
}
|
|
14627
|
+
} else {
|
|
14628
|
+
const missing = [...wanted].filter(([path]) => !held.has(path)).map(([p]) => p);
|
|
14629
|
+
if (missing.length > 0) {
|
|
14630
|
+
ports.warn(`integration '${integration.id}': the existing record holds no ${missing.join(", ")}; the legacy settings are kept, not cleared`);
|
|
14631
|
+
kept += 1;
|
|
14632
|
+
continue;
|
|
14633
|
+
}
|
|
14634
|
+
}
|
|
14635
|
+
await ports.clearRows(integration.id);
|
|
14636
|
+
cleared += 1;
|
|
14637
|
+
}
|
|
14638
|
+
return created === 0 && cleared === 0 ? {
|
|
14639
|
+
outcome: "already-done",
|
|
14640
|
+
summary: {
|
|
14641
|
+
integrations: integrations.length,
|
|
14642
|
+
kept
|
|
14643
|
+
}
|
|
14644
|
+
} : {
|
|
14645
|
+
outcome: "done",
|
|
14646
|
+
summary: {
|
|
14647
|
+
integrations: integrations.length,
|
|
14648
|
+
created,
|
|
14649
|
+
cleared,
|
|
14650
|
+
kept
|
|
14651
|
+
}
|
|
14652
|
+
};
|
|
14653
|
+
}
|
|
14654
|
+
/**
|
|
14655
|
+
* The addon-side ports of `moveIntegrationRowsToRecords` (planning#416): an addon runs the move
|
|
14656
|
+
* from its own process, so the legacy rows are reached through the hub (`ctx.api.integrations`:
|
|
14657
|
+
* `list`, `getSettings` - which answers the OWNER its secrets resolved - and `clearSettings`) and
|
|
14658
|
+
* the new record through the addon's own records handle. The hub deletes the rows and their
|
|
14659
|
+
* keychain values; the addon never touches the registry.
|
|
14660
|
+
*/
|
|
14661
|
+
function integrationRowsPortsOverApi(deps) {
|
|
14662
|
+
return {
|
|
14663
|
+
warn: (message) => deps.log.warn(message),
|
|
14664
|
+
listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
|
|
14665
|
+
id: i.id,
|
|
14666
|
+
name: i.name
|
|
14667
|
+
})),
|
|
14668
|
+
readRows: (integrationId) => deps.api.getSettings({ id: integrationId }),
|
|
14669
|
+
putRecord: async (id, body) => {
|
|
14670
|
+
if (await deps.records.get(id) !== null) return "exists";
|
|
14671
|
+
try {
|
|
14672
|
+
await deps.records.put({
|
|
14673
|
+
id,
|
|
14674
|
+
data: body
|
|
14675
|
+
}, { ifRev: 0 });
|
|
14676
|
+
} catch (err) {
|
|
14677
|
+
if (err instanceof RevConflictError) return "exists";
|
|
14678
|
+
throw err;
|
|
14679
|
+
}
|
|
14680
|
+
return "created";
|
|
14681
|
+
},
|
|
14682
|
+
readRecord: async (id) => (await deps.records.getResolved(id))?.data ?? null,
|
|
14683
|
+
deleteRecord: async (id) => {
|
|
14684
|
+
await deps.records.delete(id);
|
|
14685
|
+
},
|
|
14686
|
+
clearRows: async (integrationId) => {
|
|
14687
|
+
await deps.api.clearSettings({ id: integrationId });
|
|
14688
|
+
}
|
|
14689
|
+
};
|
|
14690
|
+
}
|
|
14691
|
+
function ordinalOf(id) {
|
|
14692
|
+
const digits = id.replace(/\D+/g, "");
|
|
14693
|
+
return digits.length > 0 ? Number(digits) : Number.POSITIVE_INFINITY;
|
|
14694
|
+
}
|
|
14695
|
+
/** The lowest-numbered eligible integration, or null. */
|
|
14696
|
+
function defaultIntegrationOf(integrations, eligible) {
|
|
14697
|
+
return [...eligible === void 0 ? integrations : integrations.filter(eligible)].sort((a, b) => ordinalOf(a.id) - ordinalOf(b.id) || (a.id < b.id ? -1 : 1))[0] ?? null;
|
|
14698
|
+
}
|
|
14699
|
+
function unstamped(device) {
|
|
14700
|
+
return device.integrationId === void 0 || device.integrationId === null || device.integrationId === "";
|
|
14701
|
+
}
|
|
14702
|
+
/**
|
|
14703
|
+
* An allocation reservation is not a device (D376): `allocateDeviceId` writes a row of type
|
|
14704
|
+
* `generic`, named by its stable id, that is not registered and holds no configuration. The hub
|
|
14705
|
+
* listing does not carry the `registered` bit, so the shape picks the candidates and the missing
|
|
14706
|
+
* configuration decides.
|
|
14707
|
+
*/
|
|
14708
|
+
async function isReservation(ports, device) {
|
|
14709
|
+
if (device.type !== "generic" || device.name === void 0 || device.name !== device.stableId) return false;
|
|
14710
|
+
return !await ports.hasConfig(device.id);
|
|
14711
|
+
}
|
|
14712
|
+
async function claimDevicesToDefaultIntegration(ports, spec) {
|
|
14713
|
+
const all = await ports.listDevices(spec.addonId);
|
|
14714
|
+
const byId = new Map(all.map((d) => [d.id, d]));
|
|
14715
|
+
const reservations = /* @__PURE__ */ new Set();
|
|
14716
|
+
for (const device of all) if (unstamped(device) && await isReservation(ports, device)) reservations.add(device.id);
|
|
14717
|
+
const orphans = all.filter((d) => unstamped(d) && !reservations.has(d.id));
|
|
14718
|
+
const summaryBase = { reservations: reservations.size };
|
|
14719
|
+
if (orphans.length === 0) return {
|
|
14720
|
+
outcome: "already-done",
|
|
14721
|
+
summary: {
|
|
14722
|
+
claimed: 0,
|
|
14723
|
+
unresolved: 0,
|
|
14724
|
+
...summaryBase
|
|
14725
|
+
}
|
|
14726
|
+
};
|
|
14727
|
+
const fallback = defaultIntegrationOf(await ports.listIntegrations(spec.addonId), spec.eligible);
|
|
14728
|
+
const plan = /* @__PURE__ */ new Map();
|
|
14729
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
14730
|
+
const failed = /* @__PURE__ */ new Set();
|
|
14731
|
+
const resolve = (device, seen) => {
|
|
14732
|
+
if (!unstamped(device)) return device.integrationId ?? null;
|
|
14733
|
+
const planned = plan.get(device.id);
|
|
14734
|
+
if (planned !== void 0) return planned;
|
|
14735
|
+
if (failed.has(device.id)) return null;
|
|
14736
|
+
let target = null;
|
|
14737
|
+
const parentId = device.parentDeviceId ?? null;
|
|
14738
|
+
if (parentId === null) target = fallback?.id ?? null;
|
|
14739
|
+
else {
|
|
14740
|
+
const parent = byId.get(parentId);
|
|
14741
|
+
if (parent !== void 0 && !reservations.has(parentId) && !seen.has(parentId)) target = resolve(parent, new Set([...seen, device.id]));
|
|
14742
|
+
}
|
|
14743
|
+
if (target === null) failed.add(device.id);
|
|
14744
|
+
else plan.set(device.id, target);
|
|
14745
|
+
return target;
|
|
14746
|
+
};
|
|
14747
|
+
if (orphans.some((d) => (d.parentDeviceId ?? null) === null) && fallback === null) throw new Error(`${orphans.length} device(s) of '${spec.addonId}' carry no integration and the addon has no integration to claim them to`);
|
|
14748
|
+
for (const device of orphans) if (resolve(device, new Set([device.id])) === null) unresolved.add(device.id);
|
|
14749
|
+
for (const id of unresolved) ports.warnDevice(`device ${id} of '${spec.addonId}' carries no integration and its owner chain does not resolve to one: left unstamped`, id);
|
|
14750
|
+
for (const [deviceId, integrationId] of plan) await ports.setIntegrationId(deviceId, integrationId);
|
|
14751
|
+
const wrong = [];
|
|
14752
|
+
for (const [deviceId, integrationId] of plan) if (await ports.readIntegrationId(deviceId) !== integrationId) wrong.push(deviceId);
|
|
14753
|
+
if (wrong.length > 0) throw new Error(`device(s) ${wrong.join(", ")} did not read back their integration after the claim`);
|
|
14754
|
+
const claimed = plan.size;
|
|
14755
|
+
ports.info(`${claimed} device(s) of '${spec.addonId}' claimed to their owner's integration or the default; ${unresolved.size} unresolved, ${reservations.size} reservation(s) skipped`);
|
|
14756
|
+
return {
|
|
14757
|
+
outcome: claimed > 0 ? "done" : "already-done",
|
|
14758
|
+
summary: {
|
|
14759
|
+
claimed,
|
|
14760
|
+
unresolved: unresolved.size,
|
|
14761
|
+
...summaryBase
|
|
14762
|
+
}
|
|
14763
|
+
};
|
|
14764
|
+
}
|
|
14765
|
+
/**
|
|
14766
|
+
* The two migrations of a device addon whose integrations hold NO settings of their own
|
|
14767
|
+
* (planning#416, S-A): every integration becomes a record of the addon's own kind with a name and
|
|
14768
|
+
* nothing else (D985, the default integration is an instance with an empty body), then the devices
|
|
14769
|
+
* that carry no `integrationId` are claimed to the addon's default integration.
|
|
14770
|
+
*
|
|
14771
|
+
* 1. `rows-to-record` (block-owner): `moveIntegrationRowsToRecords` over the hub API. Rows an
|
|
14772
|
+
* addon of this shape does not own (a key nothing reads) are named in a warning, by key, and
|
|
14773
|
+
* dropped with the rows once the record is read back.
|
|
14774
|
+
* 2. `claim-devices` (tolerate): `claimDevicesToDefaultIntegration`; with no integration to claim
|
|
14775
|
+
* to it fails and is retried on the next boot.
|
|
14776
|
+
*
|
|
14777
|
+
* Both run in the addon process through the hub API (`ctx.api`), BEFORE the addon registers its
|
|
14778
|
+
* `integration-instances` provider: while a provider is registered the hub answers `getSettings`
|
|
14779
|
+
* from the record, not from the rows.
|
|
14780
|
+
*/
|
|
14781
|
+
function defaultIntegrationPortsOver(deps) {
|
|
14782
|
+
return {
|
|
14783
|
+
...integrationRowsPortsOverApi({
|
|
14784
|
+
api: deps.api,
|
|
14785
|
+
records: deps.records.kind({
|
|
14786
|
+
name: deps.kind.name,
|
|
14787
|
+
declaration: deps.kind.declaration,
|
|
14788
|
+
schema: record(string(), unknown())
|
|
14789
|
+
}),
|
|
14790
|
+
log: { warn: deps.warn }
|
|
14791
|
+
}),
|
|
14792
|
+
...claimDevicesPortsOver({
|
|
14793
|
+
api: deps.api,
|
|
14794
|
+
info: deps.info,
|
|
14795
|
+
warnDevice: deps.warnDevice
|
|
14796
|
+
})
|
|
14797
|
+
};
|
|
14798
|
+
}
|
|
14799
|
+
/** The hub side of the device claim, over `ctx.api`. */
|
|
14800
|
+
function claimDevicesPortsOver(deps) {
|
|
14801
|
+
return {
|
|
14802
|
+
listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
|
|
14803
|
+
id: i.id,
|
|
14804
|
+
name: i.name,
|
|
14805
|
+
info: i.info ?? {}
|
|
14806
|
+
})),
|
|
14807
|
+
listDevices: (addonId) => deps.api.listDevices(addonId),
|
|
14808
|
+
hasConfig: (deviceId) => deps.api.readDeviceHasConfig(deviceId),
|
|
14809
|
+
setIntegrationId: (deviceId, integrationId) => deps.api.setDeviceIntegrationId(deviceId, integrationId),
|
|
14810
|
+
readIntegrationId: (deviceId) => deps.api.readDeviceIntegrationId(deviceId),
|
|
14811
|
+
info: deps.info,
|
|
14812
|
+
warnDevice: deps.warnDevice
|
|
14813
|
+
};
|
|
14814
|
+
}
|
|
14815
|
+
function defaultIntegrationMigrationIds(addonId, date) {
|
|
14816
|
+
return {
|
|
14817
|
+
rowsToRecord: `${addonId}/${date}-integration-rows-to-record`,
|
|
14818
|
+
claimDevices: `${addonId}/${date}-claim-devices-to-default-integration`
|
|
14819
|
+
};
|
|
14820
|
+
}
|
|
14821
|
+
var FIXTURE_MARK = "a legacy setting key the migration does not know";
|
|
14822
|
+
/**
|
|
14823
|
+
* The device claim alone, for an addon that keeps its integration settings where they are for now
|
|
14824
|
+
* (its own wave moves them) but whose persisted devices must name their default integration.
|
|
14825
|
+
*/
|
|
14826
|
+
function claimDevicesMigration(spec) {
|
|
14827
|
+
return {
|
|
14828
|
+
meta: {
|
|
14829
|
+
id: defaultIntegrationMigrationIds(spec.addonId, spec.date).claimDevices,
|
|
14830
|
+
owner: spec.addonId,
|
|
14831
|
+
level: "addon-state",
|
|
14832
|
+
seq: spec.seq,
|
|
14833
|
+
dependsOn: [...spec.dependsOn],
|
|
14834
|
+
store: "settings",
|
|
14835
|
+
scope: "cluster",
|
|
14836
|
+
onFailure: "tolerate",
|
|
14837
|
+
bodyKind: "procedure",
|
|
14838
|
+
description: "Claim the persisted devices that carry no integration to the addon's default integration, keeping their device and stable ids",
|
|
14839
|
+
fixtureMark: "a device config field the claim does not know"
|
|
14840
|
+
},
|
|
14841
|
+
procedure: (ports) => claimDevicesToDefaultIntegration(ports, {
|
|
14842
|
+
addonId: spec.addonId,
|
|
14843
|
+
...spec.eligible !== void 0 ? { eligible: spec.eligible } : {}
|
|
14844
|
+
})
|
|
14845
|
+
};
|
|
14846
|
+
}
|
|
14847
|
+
function defaultIntegrationMigrations(spec) {
|
|
14848
|
+
const ids = defaultIntegrationMigrationIds(spec.addonId, spec.date);
|
|
14849
|
+
const seq = spec.seq ?? 1;
|
|
14850
|
+
return [{
|
|
14851
|
+
meta: {
|
|
14852
|
+
id: ids.rowsToRecord,
|
|
14853
|
+
owner: spec.addonId,
|
|
14854
|
+
level: "addon-state",
|
|
14855
|
+
seq,
|
|
14856
|
+
dependsOn: [],
|
|
14857
|
+
store: "settings",
|
|
14858
|
+
scope: "cluster",
|
|
14859
|
+
onFailure: "block-owner",
|
|
14860
|
+
bodyKind: "procedure",
|
|
14861
|
+
description: "Move every integration of the addon into a typed instance record keyed by its registry id, then clear the legacy integration settings rows",
|
|
14862
|
+
fixtureMark: FIXTURE_MARK
|
|
14863
|
+
},
|
|
14864
|
+
procedure: (ports) => moveIntegrationRowsToRecords(ports, {
|
|
14865
|
+
addonId: spec.addonId,
|
|
14866
|
+
secretPaths: [],
|
|
14867
|
+
bodyFrom: (values, log, integration) => {
|
|
14868
|
+
const left = Object.keys(values);
|
|
14869
|
+
if (left.length > 0) log.warn(`integration '${integration.id}': legacy settings nothing reads any more are dropped: ${left.join(", ")}`);
|
|
14870
|
+
return { name: integration.name };
|
|
14871
|
+
}
|
|
14872
|
+
})
|
|
14873
|
+
}, claimDevicesMigration({
|
|
14874
|
+
addonId: spec.addonId,
|
|
14875
|
+
date: spec.date,
|
|
14876
|
+
seq: seq + 1,
|
|
14877
|
+
dependsOn: [ids.rowsToRecord],
|
|
14878
|
+
...spec.eligible !== void 0 ? { eligible: spec.eligible } : {}
|
|
14879
|
+
})];
|
|
14880
|
+
}
|
|
14881
|
+
function defaultIntegrationApiOver(api) {
|
|
14882
|
+
return {
|
|
14883
|
+
list: () => api.integrations.list.query(),
|
|
14884
|
+
getSettings: (input) => api.integrations.getSettings.query(input),
|
|
14885
|
+
clearSettings: (input) => api.integrations.clearSettings.mutate(input),
|
|
14886
|
+
listDevices: (addonId) => api.deviceManager.listAll.query({
|
|
14887
|
+
addonId,
|
|
14888
|
+
projection: "slim"
|
|
14889
|
+
}),
|
|
14890
|
+
setDeviceIntegrationId: async (deviceId, integrationId) => {
|
|
14891
|
+
await api.deviceManager.setIntegrationId.mutate({
|
|
14892
|
+
deviceId,
|
|
14893
|
+
integrationId
|
|
14894
|
+
});
|
|
14895
|
+
},
|
|
14896
|
+
readDeviceHasConfig: async (deviceId) => Object.keys((await api.deviceManager.getDevice.query({ deviceId }))?.config ?? {}).length > 0,
|
|
14897
|
+
readDeviceIntegrationId: async (deviceId) => (await api.deviceManager.getDevice.query({ deviceId }))?.integrationId ?? null
|
|
14898
|
+
};
|
|
14899
|
+
}
|
|
14900
|
+
/**
|
|
13826
14901
|
* The trainer contract — what the hub hands the cloud trainer and what it
|
|
13827
14902
|
* gets back (cloud training, D805+).
|
|
13828
14903
|
*
|
|
@@ -48119,49 +49194,6 @@ function actuator(input) {
|
|
|
48119
49194
|
};
|
|
48120
49195
|
}
|
|
48121
49196
|
new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
|
|
48122
|
-
var PowerSourceRefusalReasonSchema = _enum([
|
|
48123
|
-
"no-power-source",
|
|
48124
|
-
"switch-missing",
|
|
48125
|
-
"switch-offline",
|
|
48126
|
-
"switch-unknown",
|
|
48127
|
-
"restart-in-progress",
|
|
48128
|
-
"switch-call-failed"
|
|
48129
|
-
]);
|
|
48130
|
-
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
48131
|
-
accepted: literal(false),
|
|
48132
|
-
reason: PowerSourceRefusalReasonSchema,
|
|
48133
|
-
/** One line for the operator, naming the switch. */
|
|
48134
|
-
message: string()
|
|
48135
|
-
})]);
|
|
48136
|
-
var PowerSourceInfoSchema = object({
|
|
48137
|
-
/** False when the device has no power source linked; the rest is then empty. */
|
|
48138
|
-
linked: boolean(),
|
|
48139
|
-
switchDeviceId: number().nullable(),
|
|
48140
|
-
switchName: string().nullable(),
|
|
48141
|
-
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
48142
|
-
switchOn: boolean().nullable(),
|
|
48143
|
-
switchOnline: boolean().nullable(),
|
|
48144
|
-
restartInProgress: boolean(),
|
|
48145
|
-
offSeconds: number().int()
|
|
48146
|
-
});
|
|
48147
|
-
object({
|
|
48148
|
-
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
48149
|
-
switchOn: boolean().nullable(),
|
|
48150
|
-
offSeconds: number().int(),
|
|
48151
|
-
lastFetchedAt: number()
|
|
48152
|
-
});
|
|
48153
|
-
DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater;
|
|
48154
|
-
var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
|
|
48155
|
-
systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }), systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
48156
|
-
kind: "mutation",
|
|
48157
|
-
auth: "admin"
|
|
48158
|
-
}), systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
48159
|
-
kind: "mutation",
|
|
48160
|
-
auth: "admin"
|
|
48161
|
-
}), systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
48162
|
-
kind: "mutation",
|
|
48163
|
-
auth: "admin"
|
|
48164
|
-
});
|
|
48165
49197
|
DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
|
|
48166
49198
|
DeviceType.Camera;
|
|
48167
49199
|
`${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
|
|
@@ -48189,6 +49221,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
|
|
|
48189
49221
|
}).optional()
|
|
48190
49222
|
})) });
|
|
48191
49223
|
/**
|
|
49224
|
+
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
49225
|
+
* `z.infer`, they do not restate the shape.
|
|
49226
|
+
*/
|
|
49227
|
+
var OverrideSourceSchema = object({
|
|
49228
|
+
addonId: string().min(1),
|
|
49229
|
+
stableId: string().min(1),
|
|
49230
|
+
cap: string().min(1),
|
|
49231
|
+
fieldPath: string().min(1)
|
|
49232
|
+
});
|
|
49233
|
+
var IdentityTransformDraftSchema = object({ kind: literal("identity") });
|
|
49234
|
+
var InvertTransformDraftSchema = object({ kind: literal("invert") });
|
|
49235
|
+
var ThresholdTransformDraftSchema = object({
|
|
49236
|
+
kind: literal("threshold"),
|
|
49237
|
+
comparison: _enum(["gte", "lte"]),
|
|
49238
|
+
threshold: number(),
|
|
49239
|
+
startDwellMs: number().nonnegative(),
|
|
49240
|
+
stopDwellMs: number().nonnegative()
|
|
49241
|
+
});
|
|
49242
|
+
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
49243
|
+
IdentityTransformDraftSchema,
|
|
49244
|
+
InvertTransformDraftSchema,
|
|
49245
|
+
ThresholdTransformDraftSchema,
|
|
49246
|
+
object({
|
|
49247
|
+
kind: literal("linear"),
|
|
49248
|
+
scale: number(),
|
|
49249
|
+
offset: number(),
|
|
49250
|
+
clampMin: number().optional(),
|
|
49251
|
+
clampMax: number().optional()
|
|
49252
|
+
}),
|
|
49253
|
+
object({
|
|
49254
|
+
kind: literal("enum-map"),
|
|
49255
|
+
mapping: record(string(), string().nullable())
|
|
49256
|
+
}),
|
|
49257
|
+
object({ kind: literal("number") })
|
|
49258
|
+
]);
|
|
49259
|
+
var ScalarOverrideDraftSchema = object({
|
|
49260
|
+
kind: literal("scalar"),
|
|
49261
|
+
cap: string().min(1),
|
|
49262
|
+
targetId: string().min(1),
|
|
49263
|
+
source: OverrideSourceSchema,
|
|
49264
|
+
transform: CustomizeTransformDraftSchema
|
|
49265
|
+
});
|
|
49266
|
+
var KeyedArrayOverrideDraftSchema = object({
|
|
49267
|
+
kind: literal("keyedArray"),
|
|
49268
|
+
cap: string().min(1),
|
|
49269
|
+
targetId: string().min(1),
|
|
49270
|
+
key: string().min(1),
|
|
49271
|
+
label: string().min(1),
|
|
49272
|
+
fieldPath: string().min(1),
|
|
49273
|
+
source: OverrideSourceSchema,
|
|
49274
|
+
transform: CustomizeTransformDraftSchema
|
|
49275
|
+
});
|
|
49276
|
+
var PulseOverrideDraftSchema = object({
|
|
49277
|
+
kind: literal("pulse"),
|
|
49278
|
+
cap: string().min(1),
|
|
49279
|
+
targetId: string().min(1),
|
|
49280
|
+
enabled: boolean(),
|
|
49281
|
+
debounceMs: number().nonnegative(),
|
|
49282
|
+
edges: array(_enum([
|
|
49283
|
+
"rising",
|
|
49284
|
+
"button",
|
|
49285
|
+
"native-event"
|
|
49286
|
+
])).min(1).optional(),
|
|
49287
|
+
sources: array(OverrideSourceSchema).min(1)
|
|
49288
|
+
});
|
|
49289
|
+
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
49290
|
+
"in-addition",
|
|
49291
|
+
"confirm",
|
|
49292
|
+
"replace"
|
|
49293
|
+
]).optional() });
|
|
49294
|
+
discriminatedUnion("kind", [
|
|
49295
|
+
ScalarOverrideDraftSchema,
|
|
49296
|
+
KeyedArrayOverrideDraftSchema,
|
|
49297
|
+
PulseOverrideDraftSchema,
|
|
49298
|
+
object({
|
|
49299
|
+
kind: literal("span"),
|
|
49300
|
+
cap: string().min(1),
|
|
49301
|
+
targetId: string().min(1),
|
|
49302
|
+
timeoutMs: number().nonnegative(),
|
|
49303
|
+
cooldownMs: number().nonnegative().optional(),
|
|
49304
|
+
sources: array(SpanSourceDraftSchema)
|
|
49305
|
+
})
|
|
49306
|
+
]);
|
|
49307
|
+
object({
|
|
49308
|
+
targetId: string().min(1),
|
|
49309
|
+
configured: boolean(),
|
|
49310
|
+
source: _enum(["native", "assigned"]),
|
|
49311
|
+
state: _enum([
|
|
49312
|
+
"active",
|
|
49313
|
+
"stale",
|
|
49314
|
+
"released",
|
|
49315
|
+
"unavailable"
|
|
49316
|
+
]),
|
|
49317
|
+
reason: string().nullable()
|
|
49318
|
+
});
|
|
49319
|
+
/**
|
|
49320
|
+
* What a running-threshold proposal says (planning#405). The schema is the
|
|
49321
|
+
* type: the algorithm, the capability method and the UI all read `z.infer`.
|
|
49322
|
+
*
|
|
49323
|
+
* A proposal is read-only advice: it carries the numbers it was derived from
|
|
49324
|
+
* (`evidence`) so the operator can judge it, and an `insufficient` answer names
|
|
49325
|
+
* WHY with a typed reason and the catalog defaults, never an invented number.
|
|
49326
|
+
*/
|
|
49327
|
+
var RunningProposalTierSchema = _enum(["raw", "5m"]);
|
|
49328
|
+
var RunningProposalConfidenceSchema = _enum([
|
|
49329
|
+
"high",
|
|
49330
|
+
"medium",
|
|
49331
|
+
"low"
|
|
49332
|
+
]);
|
|
49333
|
+
var RunningProposalInsufficientReasonSchema = _enum([
|
|
49334
|
+
"no-history",
|
|
49335
|
+
"span-too-short",
|
|
49336
|
+
"truncated",
|
|
49337
|
+
"no-idle-floor",
|
|
49338
|
+
"no-runs",
|
|
49339
|
+
"no-separation"
|
|
49340
|
+
]);
|
|
49341
|
+
var RunningProposalReplaySchema = object({
|
|
49342
|
+
/** Intervals the real dwell expression held true over the window. */
|
|
49343
|
+
intervals: number().int().nonnegative(),
|
|
49344
|
+
/** Intervals outside every run, or a second interval inside one. */
|
|
49345
|
+
extra: number().int().nonnegative(),
|
|
49346
|
+
/** Runs without an interval of their own (none, or merged into a neighbour). */
|
|
49347
|
+
missed: number().int().nonnegative()
|
|
49348
|
+
});
|
|
49349
|
+
var RunningProposalEvidenceSchema = object({
|
|
49350
|
+
tier: RunningProposalTierSchema,
|
|
49351
|
+
spanMs: number().nonnegative(),
|
|
49352
|
+
from: number(),
|
|
49353
|
+
to: number(),
|
|
49354
|
+
points: number().int().nonnegative(),
|
|
49355
|
+
runsFound: number().int().nonnegative(),
|
|
49356
|
+
pausesFound: number().int().nonnegative(),
|
|
49357
|
+
longestPauseMs: number().nonnegative(),
|
|
49358
|
+
idleFloorW: number(),
|
|
49359
|
+
noiseCeilingW: number(),
|
|
49360
|
+
plateauW: number(),
|
|
49361
|
+
replay: RunningProposalReplaySchema
|
|
49362
|
+
});
|
|
49363
|
+
var RunningProposalInsufficientEvidenceSchema = object({
|
|
49364
|
+
spanMs: number().nonnegative(),
|
|
49365
|
+
points: number().int().nonnegative(),
|
|
49366
|
+
runsFound: number().int().nonnegative()
|
|
49367
|
+
});
|
|
49368
|
+
var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
|
|
49369
|
+
state: literal("proposal"),
|
|
49370
|
+
transform: ThresholdTransformDraftSchema,
|
|
49371
|
+
evidence: RunningProposalEvidenceSchema,
|
|
49372
|
+
confidence: RunningProposalConfidenceSchema,
|
|
49373
|
+
/** Why the confidence is not higher; empty when nothing held it back. */
|
|
49374
|
+
notes: array(string())
|
|
49375
|
+
}), object({
|
|
49376
|
+
state: literal("insufficient"),
|
|
49377
|
+
reason: RunningProposalInsufficientReasonSchema,
|
|
49378
|
+
evidence: RunningProposalInsufficientEvidenceSchema,
|
|
49379
|
+
/** The catalog defaults, offered as what stands when nothing better is known. */
|
|
49380
|
+
fallback: ThresholdTransformDraftSchema
|
|
49381
|
+
})]);
|
|
49382
|
+
/**
|
|
48192
49383
|
* The report of a one-shot "move this owner's plaintext secrets into the
|
|
48193
49384
|
* keychain" run (D878). COUNTS only - never a value, never a ref.
|
|
48194
49385
|
*
|
|
@@ -48478,6 +49669,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
|
|
|
48478
49669
|
kind: "mutation",
|
|
48479
49670
|
auth: "admin",
|
|
48480
49671
|
access: "view"
|
|
49672
|
+
}), method(object({
|
|
49673
|
+
deviceId: number().int(),
|
|
49674
|
+
cap: string().min(1),
|
|
49675
|
+
fieldPath: string().min(1),
|
|
49676
|
+
deviceType: _enum(DeviceType).optional()
|
|
49677
|
+
}), RunningThresholdProposalSchema, {
|
|
49678
|
+
auth: "admin",
|
|
49679
|
+
access: "view"
|
|
48481
49680
|
}), method(object({
|
|
48482
49681
|
blockId: string().min(1),
|
|
48483
49682
|
trace: AutomationRunTraceSchema.optional(),
|
|
@@ -49307,7 +50506,27 @@ method(object({
|
|
|
49307
50506
|
* exposes only the COMMON surface: link state, supported device
|
|
49308
50507
|
* kinds (so the UI can filter the device picker), and per-device
|
|
49309
50508
|
* expose/unexpose.
|
|
49310
|
-
|
|
50509
|
+
*
|
|
50510
|
+
* INSTANCES. One export addon can serve several independent targets (a
|
|
50511
|
+
* HomeKit bridge, a Home Assistant broker). `listInstances` names them.
|
|
50512
|
+
* `instanceId` on a READ is optional: absent = the whole addon (linked if any
|
|
50513
|
+
* enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
|
|
50514
|
+
* default instance's); present = that instance; an id the provider does not
|
|
50515
|
+
* serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
|
|
50516
|
+
* never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
|
|
50517
|
+
* change always names the target it changes. A single-instance provider
|
|
50518
|
+
* accepts an absent id on a read or its own id, and refuses any other.
|
|
50519
|
+
*/
|
|
50520
|
+
/** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
|
|
50521
|
+
var InstanceIdSchema$1 = string().min(1);
|
|
50522
|
+
var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
|
|
50523
|
+
/** One independent export target of an addon. */
|
|
50524
|
+
var ExportInstanceSchema = object({
|
|
50525
|
+
instanceId: InstanceIdSchema$1,
|
|
50526
|
+
name: string(),
|
|
50527
|
+
enabled: boolean(),
|
|
50528
|
+
kind: string().optional()
|
|
50529
|
+
});
|
|
49311
50530
|
var LinkStateSchema = _enum([
|
|
49312
50531
|
"unlinked",
|
|
49313
50532
|
"linked",
|
|
@@ -49358,10 +50577,14 @@ var ExposedDeviceSchema = object({
|
|
|
49358
50577
|
capabilities: array(string()).optional()
|
|
49359
50578
|
});
|
|
49360
50579
|
var ExposeInputSchema = object({
|
|
50580
|
+
instanceId: InstanceIdSchema$1,
|
|
49361
50581
|
deviceId: string(),
|
|
49362
50582
|
capabilities: array(string()).optional()
|
|
49363
50583
|
});
|
|
49364
|
-
var UnexposeInputSchema = object({
|
|
50584
|
+
var UnexposeInputSchema = object({
|
|
50585
|
+
instanceId: InstanceIdSchema$1,
|
|
50586
|
+
deviceId: string()
|
|
50587
|
+
});
|
|
49365
50588
|
/**
|
|
49366
50589
|
* What an exporter may expose besides devices (D968). An action and a
|
|
49367
50590
|
* notification target are LIST ENTRIES, never devices (D954): they enter an
|
|
@@ -49390,15 +50613,21 @@ var ExposedEntitySchema = object({
|
|
|
49390
50613
|
entity: ExportEntityRefSchema,
|
|
49391
50614
|
exposedAs: string().optional()
|
|
49392
50615
|
});
|
|
49393
|
-
var ExposeEntityInputSchema = object({
|
|
49394
|
-
|
|
49395
|
-
|
|
50616
|
+
var ExposeEntityInputSchema = object({
|
|
50617
|
+
instanceId: InstanceIdSchema$1,
|
|
50618
|
+
entity: ExportEntityRefSchema
|
|
50619
|
+
});
|
|
50620
|
+
var UnexposeEntityInputSchema = object({
|
|
50621
|
+
instanceId: InstanceIdSchema$1,
|
|
50622
|
+
entity: ExportEntityRefSchema
|
|
50623
|
+
});
|
|
50624
|
+
method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
|
|
49396
50625
|
kind: "mutation",
|
|
49397
50626
|
auth: "admin"
|
|
49398
50627
|
}), method(UnexposeInputSchema, _void(), {
|
|
49399
50628
|
kind: "mutation",
|
|
49400
50629
|
auth: "admin"
|
|
49401
|
-
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(
|
|
50630
|
+
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
|
|
49402
50631
|
kind: "mutation",
|
|
49403
50632
|
auth: "admin"
|
|
49404
50633
|
}), optionalMethod(UnexposeEntityInputSchema, _void(), {
|
|
@@ -52692,15 +53921,15 @@ var FanPresetCaptureResultSchema = object({
|
|
|
52692
53921
|
items: array(FanCaptureItemSchema),
|
|
52693
53922
|
omitted: array(FanCaptureOmissionSchema)
|
|
52694
53923
|
});
|
|
52695
|
-
var deviceInput$
|
|
53924
|
+
var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
|
|
52696
53925
|
DeviceType.Fan;
|
|
52697
|
-
method(deviceInput$
|
|
53926
|
+
method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
|
|
52698
53927
|
kind: "mutation",
|
|
52699
53928
|
auth: "admin"
|
|
52700
|
-
}), method(deviceInput$
|
|
53929
|
+
}), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
|
|
52701
53930
|
kind: "mutation",
|
|
52702
53931
|
auth: "protected"
|
|
52703
|
-
}), method(deviceInput$
|
|
53932
|
+
}), method(deviceInput$2.extend({
|
|
52704
53933
|
fields: array(FanCaptureFieldSchema).optional(),
|
|
52705
53934
|
steps: array(ActionStepSchema).max(20).default([])
|
|
52706
53935
|
}), FanPresetCaptureResultSchema, { auth: "protected" });
|
|
@@ -53196,6 +54425,12 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
53196
54425
|
}), object({ success: boolean() }), {
|
|
53197
54426
|
kind: "mutation",
|
|
53198
54427
|
auth: "admin"
|
|
54428
|
+
}), method(object({ id: string() }), object({
|
|
54429
|
+
rows: number().int().nonnegative(),
|
|
54430
|
+
secrets: number().int().nonnegative()
|
|
54431
|
+
}), {
|
|
54432
|
+
kind: "mutation",
|
|
54433
|
+
auth: "admin"
|
|
53199
54434
|
}), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
|
|
53200
54435
|
addonId: string(),
|
|
53201
54436
|
settings: record(string(), unknown())
|
|
@@ -53224,13 +54459,65 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
53224
54459
|
*/
|
|
53225
54460
|
var ConfigSchemaPassthrough$3 = custom();
|
|
53226
54461
|
var IntegrationInstanceIdSchema = string().min(1);
|
|
54462
|
+
var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
|
|
54463
|
+
/**
|
|
54464
|
+
* One kind of instance a provider can create (an adapter, an account type). The sign-in and
|
|
54465
|
+
* connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
|
|
54466
|
+
* hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
|
|
54467
|
+
* interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
|
|
54468
|
+
* instance id.
|
|
54469
|
+
*/
|
|
54470
|
+
var IntegrationInstanceKindSchema = object({
|
|
54471
|
+
/** Stable id of the kind, sent back as `createInstance.kind`. */
|
|
54472
|
+
id: string().min(1),
|
|
54473
|
+
label: string().min(1),
|
|
54474
|
+
/** The kind signs in through a server-held interactive session. */
|
|
54475
|
+
signIn: object({
|
|
54476
|
+
basePath: string().min(1),
|
|
54477
|
+
methods: array(_enum(["device-code", "proxy"])).optional()
|
|
54478
|
+
}).optional(),
|
|
54479
|
+
/** Relative hub path to start an interactive account link of an instance of this kind. */
|
|
54480
|
+
connectPath: string().min(1).optional(),
|
|
54481
|
+
/** Relative hub path to disconnect that account. */
|
|
54482
|
+
disconnectPath: string().min(1).optional()
|
|
54483
|
+
});
|
|
53227
54484
|
var IntegrationInstanceDescriptionSchema = object({
|
|
54485
|
+
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
54486
|
+
role: _enum([
|
|
54487
|
+
"devices",
|
|
54488
|
+
"services",
|
|
54489
|
+
"both"
|
|
54490
|
+
]),
|
|
54491
|
+
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
54492
|
+
discovery: object({
|
|
54493
|
+
mode: _enum([
|
|
54494
|
+
"none",
|
|
54495
|
+
"lan",
|
|
54496
|
+
"account"
|
|
54497
|
+
]),
|
|
54498
|
+
scopedToInstance: boolean()
|
|
54499
|
+
}).optional(),
|
|
53228
54500
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
53229
|
-
instanceMode:
|
|
54501
|
+
instanceMode: IntegrationInstanceModeSchema,
|
|
53230
54502
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
53231
54503
|
kind: string(),
|
|
53232
54504
|
/** True when `testInstance` really probes the remote. */
|
|
53233
|
-
canTest: boolean()
|
|
54505
|
+
canTest: boolean(),
|
|
54506
|
+
/**
|
|
54507
|
+
* The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
|
|
54508
|
+
* Add flow asks which first and sends it as `createInstance.kind`.
|
|
54509
|
+
*/
|
|
54510
|
+
kinds: array(IntegrationInstanceKindSchema).optional(),
|
|
54511
|
+
/**
|
|
54512
|
+
* The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
|
|
54513
|
+
* devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
|
|
54514
|
+
*/
|
|
54515
|
+
allowRestoreId: boolean().optional(),
|
|
54516
|
+
/**
|
|
54517
|
+
* `createInstance` without an `id` works: the provider mints it. Absent means the caller must
|
|
54518
|
+
* send an id (the UI then mints an opaque one).
|
|
54519
|
+
*/
|
|
54520
|
+
mintsId: boolean().optional()
|
|
53234
54521
|
});
|
|
53235
54522
|
var IntegrationInstanceStatusSchema = object({
|
|
53236
54523
|
state: _enum([
|
|
@@ -53239,7 +54526,9 @@ var IntegrationInstanceStatusSchema = object({
|
|
|
53239
54526
|
"error",
|
|
53240
54527
|
"unknown"
|
|
53241
54528
|
]),
|
|
53242
|
-
detail: string().optional()
|
|
54529
|
+
detail: string().optional(),
|
|
54530
|
+
/** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
|
|
54531
|
+
label: string().optional()
|
|
53243
54532
|
});
|
|
53244
54533
|
var IntegrationInstanceSchema = object({
|
|
53245
54534
|
id: IntegrationInstanceIdSchema,
|
|
@@ -53247,13 +54536,18 @@ var IntegrationInstanceSchema = object({
|
|
|
53247
54536
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
53248
54537
|
configured: boolean(),
|
|
53249
54538
|
/**
|
|
53250
|
-
* The instance's own on/off switch (D996)
|
|
53251
|
-
*
|
|
54539
|
+
* The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
|
|
54540
|
+
* so a row written before the field existed is on). Absent when the `integrations` registry
|
|
54541
|
+
* owns the switch (`describe().role` is `devices`): read it from the registry row.
|
|
53252
54542
|
*/
|
|
53253
|
-
enabled: boolean(),
|
|
54543
|
+
enabled: boolean().optional(),
|
|
54544
|
+
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
54545
|
+
kind: string().optional(),
|
|
53254
54546
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
53255
54547
|
formValues: record(string(), unknown()),
|
|
53256
|
-
status: IntegrationInstanceStatusSchema.optional()
|
|
54548
|
+
status: IntegrationInstanceStatusSchema.optional(),
|
|
54549
|
+
/** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
|
|
54550
|
+
lastCheckedAt: number().optional()
|
|
53257
54551
|
});
|
|
53258
54552
|
var IntegrationInstancePatchSchema = object({
|
|
53259
54553
|
id: IntegrationInstanceIdSchema,
|
|
@@ -53261,31 +54555,57 @@ var IntegrationInstancePatchSchema = object({
|
|
|
53261
54555
|
/** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
|
|
53262
54556
|
values: record(string(), unknown())
|
|
53263
54557
|
});
|
|
53264
|
-
|
|
53265
|
-
|
|
53266
|
-
|
|
53267
|
-
|
|
53268
|
-
|
|
53269
|
-
|
|
53270
|
-
|
|
53271
|
-
|
|
53272
|
-
|
|
53273
|
-
|
|
53274
|
-
|
|
53275
|
-
|
|
53276
|
-
|
|
53277
|
-
|
|
53278
|
-
|
|
53279
|
-
|
|
53280
|
-
|
|
53281
|
-
|
|
53282
|
-
}),
|
|
53283
|
-
|
|
53284
|
-
|
|
53285
|
-
}),
|
|
53286
|
-
|
|
53287
|
-
|
|
53288
|
-
|
|
54558
|
+
var integrationInstancesCapability = {
|
|
54559
|
+
name: "integration-instances",
|
|
54560
|
+
scope: "system",
|
|
54561
|
+
mode: "collection",
|
|
54562
|
+
methods: {
|
|
54563
|
+
describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
|
|
54564
|
+
/** The form; `id: null` is the create form. */
|
|
54565
|
+
instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
|
|
54566
|
+
listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
|
|
54567
|
+
getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
|
|
54568
|
+
/**
|
|
54569
|
+
* Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
|
|
54570
|
+
* without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
|
|
54571
|
+
*/
|
|
54572
|
+
createInstance: method(object({
|
|
54573
|
+
id: IntegrationInstanceIdSchema.optional(),
|
|
54574
|
+
name: string().min(1),
|
|
54575
|
+
kind: string().min(1).optional()
|
|
54576
|
+
}), object({ id: string() }), {
|
|
54577
|
+
kind: "mutation",
|
|
54578
|
+
auth: "admin"
|
|
54579
|
+
}),
|
|
54580
|
+
updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
|
|
54581
|
+
kind: "mutation",
|
|
54582
|
+
auth: "admin"
|
|
54583
|
+
}),
|
|
54584
|
+
/**
|
|
54585
|
+
* Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
|
|
54586
|
+
* form patch (D62, D996). An off instance keeps its record and secrets and does no work.
|
|
54587
|
+
*/
|
|
54588
|
+
setInstanceEnabled: method(object({
|
|
54589
|
+
id: IntegrationInstanceIdSchema,
|
|
54590
|
+
enabled: boolean()
|
|
54591
|
+
}), object({ ok: literal(true) }), {
|
|
54592
|
+
kind: "mutation",
|
|
54593
|
+
auth: "admin"
|
|
54594
|
+
}),
|
|
54595
|
+
/** Delete the record and its keychain refs together. */
|
|
54596
|
+
deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
|
|
54597
|
+
kind: "mutation",
|
|
54598
|
+
auth: "admin"
|
|
54599
|
+
}),
|
|
54600
|
+
testInstance: optionalMethod(object({
|
|
54601
|
+
id: IntegrationInstanceIdSchema.nullable(),
|
|
54602
|
+
values: record(string(), unknown())
|
|
54603
|
+
}), TestConnectionResultSchema$1, {
|
|
54604
|
+
kind: "mutation",
|
|
54605
|
+
auth: "admin"
|
|
54606
|
+
})
|
|
54607
|
+
}
|
|
54608
|
+
};
|
|
53289
54609
|
/**
|
|
53290
54610
|
* Two-way audio / talk-back cap. Clients open a WebRTC audio-only
|
|
53291
54611
|
* session (server-initiated offer pattern, identical to the main
|
|
@@ -55084,6 +56404,11 @@ var LogEntrySchema = object({
|
|
|
55084
56404
|
* flight, and not asking for the next slice ends the scan.
|
|
55085
56405
|
*/
|
|
55086
56406
|
var LogReadQuerySchema = object({
|
|
56407
|
+
/**
|
|
56408
|
+
* Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
|
|
56409
|
+
* the provider's first readable one, so a caller written before instances existed is unchanged.
|
|
56410
|
+
*/
|
|
56411
|
+
instanceId: string().min(1).max(200).optional(),
|
|
55087
56412
|
/** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
|
|
55088
56413
|
requestId: string().min(1).max(64).optional(),
|
|
55089
56414
|
/** The device the line is about (`tags.deviceId`, numeric). */
|
|
@@ -55146,6 +56471,8 @@ var LogReadResultSchema = object({
|
|
|
55146
56471
|
/** True when `cancelRead` ended this slice early. */
|
|
55147
56472
|
cancelled: boolean().optional()
|
|
55148
56473
|
});
|
|
56474
|
+
/** Names one destination instance; absent ⇒ the provider's first readable one. */
|
|
56475
|
+
var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
|
|
55149
56476
|
var LogReadCapabilitiesSchema = object({
|
|
55150
56477
|
/** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
|
|
55151
56478
|
readable: boolean(),
|
|
@@ -55164,7 +56491,7 @@ var LogReadCapabilitiesSchema = object({
|
|
|
55164
56491
|
/** How far back the store reaches, when the provider knows (days). */
|
|
55165
56492
|
retentionDays: number().nullable()
|
|
55166
56493
|
});
|
|
55167
|
-
method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(
|
|
56494
|
+
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() }), {
|
|
55168
56495
|
kind: "mutation",
|
|
55169
56496
|
auth: "admin"
|
|
55170
56497
|
});
|
|
@@ -66736,6 +68063,49 @@ object({
|
|
|
66736
68063
|
DeviceType.PetFeeder,
|
|
66737
68064
|
DeviceType.Heater
|
|
66738
68065
|
].filter((type) => type !== DeviceType.Sensor);
|
|
68066
|
+
var PowerSourceRefusalReasonSchema = _enum([
|
|
68067
|
+
"no-power-source",
|
|
68068
|
+
"switch-missing",
|
|
68069
|
+
"switch-offline",
|
|
68070
|
+
"switch-unknown",
|
|
68071
|
+
"restart-in-progress",
|
|
68072
|
+
"switch-call-failed"
|
|
68073
|
+
]);
|
|
68074
|
+
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
68075
|
+
accepted: literal(false),
|
|
68076
|
+
reason: PowerSourceRefusalReasonSchema,
|
|
68077
|
+
/** One line for the operator, naming the switch. */
|
|
68078
|
+
message: string()
|
|
68079
|
+
})]);
|
|
68080
|
+
var PowerSourceInfoSchema = object({
|
|
68081
|
+
/** False when the device has no power source linked; the rest is then empty. */
|
|
68082
|
+
linked: boolean(),
|
|
68083
|
+
switchDeviceId: number().nullable(),
|
|
68084
|
+
switchName: string().nullable(),
|
|
68085
|
+
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
68086
|
+
switchOn: boolean().nullable(),
|
|
68087
|
+
switchOnline: boolean().nullable(),
|
|
68088
|
+
restartInProgress: boolean(),
|
|
68089
|
+
offSeconds: number().int()
|
|
68090
|
+
});
|
|
68091
|
+
object({
|
|
68092
|
+
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
68093
|
+
switchOn: boolean().nullable(),
|
|
68094
|
+
offSeconds: number().int(),
|
|
68095
|
+
lastFetchedAt: number()
|
|
68096
|
+
});
|
|
68097
|
+
DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater;
|
|
68098
|
+
var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
|
|
68099
|
+
systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
68100
|
+
kind: "mutation",
|
|
68101
|
+
auth: "admin"
|
|
68102
|
+
}), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
68103
|
+
kind: "mutation",
|
|
68104
|
+
auth: "admin"
|
|
68105
|
+
}), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
68106
|
+
kind: "mutation",
|
|
68107
|
+
auth: "admin"
|
|
68108
|
+
});
|
|
66739
68109
|
/**
|
|
66740
68110
|
* Presence cap. Models HA `person.*` and `device_tracker.*` entities
|
|
66741
68111
|
* on `DeviceType.Presence`. Read-only — no setters: presence is
|
|
@@ -75077,6 +76447,25 @@ function buildDescriptor(capName, kind) {
|
|
|
75077
76447
|
* contributes one entry per bound cap present in this map.
|
|
75078
76448
|
*/
|
|
75079
76449
|
var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
|
|
76450
|
+
/**
|
|
76451
|
+
* What a remote key is CALLED and what it LOOKS like, for every role (D963),
|
|
76452
|
+
* shared by the admin UI and the viewer. A role is data on the wire; each
|
|
76453
|
+
* client resolves its name from `label` (English and Italian side by side) and
|
|
76454
|
+
* its glyph from `icon`, a lucide id from the shared icon catalogue — the
|
|
76455
|
+
* server sends an id, a client maps it to its own icon set.
|
|
76456
|
+
*
|
|
76457
|
+
* `legend` is text printed on the key INSTEAD of a glyph (a digit has no
|
|
76458
|
+
* pictogram): a client that can draw text shows it, one that cannot falls back
|
|
76459
|
+
* to `icon`.
|
|
76460
|
+
*/
|
|
76461
|
+
function digit(n) {
|
|
76462
|
+
return {
|
|
76463
|
+
label: t(n, n),
|
|
76464
|
+
icon: "hash",
|
|
76465
|
+
legend: n
|
|
76466
|
+
};
|
|
76467
|
+
}
|
|
76468
|
+
digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
|
|
75080
76469
|
DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
|
|
75081
76470
|
var btn = (idSuffix, row, col, init) => ({
|
|
75082
76471
|
idSuffix,
|
|
@@ -75204,6 +76593,34 @@ discriminatedUnion("kind", [object({
|
|
|
75204
76593
|
kind: literal("pronto"),
|
|
75205
76594
|
code: string()
|
|
75206
76595
|
})]);
|
|
76596
|
+
/**
|
|
76597
|
+
* Propose a running threshold from a device's own power history (planning#405).
|
|
76598
|
+
*
|
|
76599
|
+
* Pure and deterministic: the same series gives the same answer. The series is
|
|
76600
|
+
* a HELD-VALUE step series — a point's value stands until the next point — which
|
|
76601
|
+
* is how device-history stores a gauge, so "time at a level" is exact, not a
|
|
76602
|
+
* count of samples.
|
|
76603
|
+
*
|
|
76604
|
+
* 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
|
|
76605
|
+
* near F. A device that is not near one level most of the time has no floor.
|
|
76606
|
+
* 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
|
|
76607
|
+
* 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
|
|
76608
|
+
* the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
|
|
76609
|
+
* 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
|
|
76610
|
+
* grid a person would type; stop dwell covers the longest pause; start dwell
|
|
76611
|
+
* covers the longest standby blip above T.
|
|
76612
|
+
* 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
|
|
76613
|
+
* run must hold exactly one true interval and nothing may flap outside.
|
|
76614
|
+
*/
|
|
76615
|
+
var SECOND_MS = 1e3;
|
|
76616
|
+
var MINUTE_MS$1 = 60 * SECOND_MS;
|
|
76617
|
+
24 * (60 * MINUTE_MS$1);
|
|
76618
|
+
10 * MINUTE_MS$1;
|
|
76619
|
+
30 * SECOND_MS;
|
|
76620
|
+
60 * SECOND_MS;
|
|
76621
|
+
20 * MINUTE_MS$1;
|
|
76622
|
+
10 * SECOND_MS;
|
|
76623
|
+
10 * SECOND_MS;
|
|
75207
76624
|
DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
|
|
75208
76625
|
SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
|
|
75209
76626
|
String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
|
|
@@ -77581,6 +78998,13 @@ Object.freeze({
|
|
|
77581
78998
|
access: "create",
|
|
77582
78999
|
role: "admin"
|
|
77583
79000
|
},
|
|
79001
|
+
"coreBlocks.proposeRunningThreshold": {
|
|
79002
|
+
capName: "core-blocks",
|
|
79003
|
+
capScope: "system",
|
|
79004
|
+
addonId: null,
|
|
79005
|
+
access: "view",
|
|
79006
|
+
role: "admin"
|
|
79007
|
+
},
|
|
77584
79008
|
"coreBlocks.reportAutomationRun": {
|
|
77585
79009
|
capName: "core-blocks",
|
|
77586
79010
|
capScope: "system",
|
|
@@ -78092,6 +79516,13 @@ Object.freeze({
|
|
|
78092
79516
|
access: "view",
|
|
78093
79517
|
role: "admin"
|
|
78094
79518
|
},
|
|
79519
|
+
"deviceExport.listInstances": {
|
|
79520
|
+
capName: "device-export",
|
|
79521
|
+
capScope: "system",
|
|
79522
|
+
addonId: null,
|
|
79523
|
+
access: "view",
|
|
79524
|
+
role: "admin"
|
|
79525
|
+
},
|
|
78095
79526
|
"deviceExport.listSupportedDeviceKinds": {
|
|
78096
79527
|
capName: "device-export",
|
|
78097
79528
|
capScope: "system",
|
|
@@ -79744,6 +81175,13 @@ Object.freeze({
|
|
|
79744
81175
|
access: "create",
|
|
79745
81176
|
role: "admin"
|
|
79746
81177
|
},
|
|
81178
|
+
"integrations.clearSettings": {
|
|
81179
|
+
capName: "integrations",
|
|
81180
|
+
capScope: "system",
|
|
81181
|
+
addonId: null,
|
|
81182
|
+
access: "delete",
|
|
81183
|
+
role: "admin"
|
|
81184
|
+
},
|
|
79747
81185
|
"integrations.create": {
|
|
79748
81186
|
capName: "integrations",
|
|
79749
81187
|
capScope: "system",
|
|
@@ -87105,6 +88543,11 @@ Object.freeze({
|
|
|
87105
88543
|
form: "single",
|
|
87106
88544
|
optional: false
|
|
87107
88545
|
}],
|
|
88546
|
+
"coreBlocks.proposeRunningThreshold": [{
|
|
88547
|
+
name: "deviceId",
|
|
88548
|
+
form: "single",
|
|
88549
|
+
optional: false
|
|
88550
|
+
}],
|
|
87108
88551
|
"cover.close": [{
|
|
87109
88552
|
name: "deviceId",
|
|
87110
88553
|
form: "single",
|
|
@@ -89768,99 +91211,6 @@ Object.freeze({
|
|
|
89768
91211
|
"network-access": "ingress",
|
|
89769
91212
|
"smtp-provider": "email"
|
|
89770
91213
|
});
|
|
89771
|
-
/**
|
|
89772
|
-
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
89773
|
-
* `z.infer`, they do not restate the shape.
|
|
89774
|
-
*/
|
|
89775
|
-
var OverrideSourceSchema = object({
|
|
89776
|
-
addonId: string().min(1),
|
|
89777
|
-
stableId: string().min(1),
|
|
89778
|
-
cap: string().min(1),
|
|
89779
|
-
fieldPath: string().min(1)
|
|
89780
|
-
});
|
|
89781
|
-
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
89782
|
-
object({ kind: literal("identity") }),
|
|
89783
|
-
object({ kind: literal("invert") }),
|
|
89784
|
-
object({
|
|
89785
|
-
kind: literal("threshold"),
|
|
89786
|
-
comparison: _enum(["gte", "lte"]),
|
|
89787
|
-
threshold: number(),
|
|
89788
|
-
startDwellMs: number().nonnegative(),
|
|
89789
|
-
stopDwellMs: number().nonnegative()
|
|
89790
|
-
}),
|
|
89791
|
-
object({
|
|
89792
|
-
kind: literal("linear"),
|
|
89793
|
-
scale: number(),
|
|
89794
|
-
offset: number(),
|
|
89795
|
-
clampMin: number().optional(),
|
|
89796
|
-
clampMax: number().optional()
|
|
89797
|
-
}),
|
|
89798
|
-
object({
|
|
89799
|
-
kind: literal("enum-map"),
|
|
89800
|
-
mapping: record(string(), string().nullable())
|
|
89801
|
-
}),
|
|
89802
|
-
object({ kind: literal("number") })
|
|
89803
|
-
]);
|
|
89804
|
-
var ScalarOverrideDraftSchema = object({
|
|
89805
|
-
kind: literal("scalar"),
|
|
89806
|
-
cap: string().min(1),
|
|
89807
|
-
targetId: string().min(1),
|
|
89808
|
-
source: OverrideSourceSchema,
|
|
89809
|
-
transform: CustomizeTransformDraftSchema
|
|
89810
|
-
});
|
|
89811
|
-
var KeyedArrayOverrideDraftSchema = object({
|
|
89812
|
-
kind: literal("keyedArray"),
|
|
89813
|
-
cap: string().min(1),
|
|
89814
|
-
targetId: string().min(1),
|
|
89815
|
-
key: string().min(1),
|
|
89816
|
-
label: string().min(1),
|
|
89817
|
-
fieldPath: string().min(1),
|
|
89818
|
-
source: OverrideSourceSchema,
|
|
89819
|
-
transform: CustomizeTransformDraftSchema
|
|
89820
|
-
});
|
|
89821
|
-
var PulseOverrideDraftSchema = object({
|
|
89822
|
-
kind: literal("pulse"),
|
|
89823
|
-
cap: string().min(1),
|
|
89824
|
-
targetId: string().min(1),
|
|
89825
|
-
enabled: boolean(),
|
|
89826
|
-
debounceMs: number().nonnegative(),
|
|
89827
|
-
edges: array(_enum([
|
|
89828
|
-
"rising",
|
|
89829
|
-
"button",
|
|
89830
|
-
"native-event"
|
|
89831
|
-
])).min(1).optional(),
|
|
89832
|
-
sources: array(OverrideSourceSchema).min(1)
|
|
89833
|
-
});
|
|
89834
|
-
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
89835
|
-
"in-addition",
|
|
89836
|
-
"confirm",
|
|
89837
|
-
"replace"
|
|
89838
|
-
]).optional() });
|
|
89839
|
-
discriminatedUnion("kind", [
|
|
89840
|
-
ScalarOverrideDraftSchema,
|
|
89841
|
-
KeyedArrayOverrideDraftSchema,
|
|
89842
|
-
PulseOverrideDraftSchema,
|
|
89843
|
-
object({
|
|
89844
|
-
kind: literal("span"),
|
|
89845
|
-
cap: string().min(1),
|
|
89846
|
-
targetId: string().min(1),
|
|
89847
|
-
timeoutMs: number().nonnegative(),
|
|
89848
|
-
cooldownMs: number().nonnegative().optional(),
|
|
89849
|
-
sources: array(SpanSourceDraftSchema)
|
|
89850
|
-
})
|
|
89851
|
-
]);
|
|
89852
|
-
object({
|
|
89853
|
-
targetId: string().min(1),
|
|
89854
|
-
configured: boolean(),
|
|
89855
|
-
source: _enum(["native", "assigned"]),
|
|
89856
|
-
state: _enum([
|
|
89857
|
-
"active",
|
|
89858
|
-
"stale",
|
|
89859
|
-
"released",
|
|
89860
|
-
"unavailable"
|
|
89861
|
-
]),
|
|
89862
|
-
reason: string().nullable()
|
|
89863
|
-
});
|
|
89864
91214
|
var G711_SCALE_CORRECTION_DB = {
|
|
89865
91215
|
PCMU: 20 * Math.log10(4),
|
|
89866
91216
|
PCMA: 20 * Math.log10(8)
|
|
@@ -91282,6 +92632,39 @@ var ADDON_MIGRATIONS = [{
|
|
|
91282
92632
|
plan: (ports) => planDropKeys(PROVIDER_ONVIF_ID, DEFAULT_CREDENTIAL_KEYS, ports)
|
|
91283
92633
|
}];
|
|
91284
92634
|
//#endregion
|
|
92635
|
+
//#region src/onvif-instances.ts
|
|
92636
|
+
/**
|
|
92637
|
+
* ONVIF integration as an `integration-instances` provider (planning#416, D985, D1007): the
|
|
92638
|
+
* integration is one `connections` record under the registry id and its body holds only a name.
|
|
92639
|
+
* The connection of each camera (address, credentials) lives on the device, so the instance
|
|
92640
|
+
* form is empty and the integration is the owner the devices are stamped with.
|
|
92641
|
+
*/
|
|
92642
|
+
var onvifConnectionsEntryKind = defineEntryKind("connections", {
|
|
92643
|
+
title: "ONVIF integrations",
|
|
92644
|
+
schema: entryBody({}),
|
|
92645
|
+
writeTrigger: "an operator adds or removes the ONVIF integration"
|
|
92646
|
+
});
|
|
92647
|
+
/** Every kind this addon declares, keyed by its name. */
|
|
92648
|
+
var onvifRecordKinds = { connections: onvifConnectionsEntryKind };
|
|
92649
|
+
function buildOnvifInstances(records) {
|
|
92650
|
+
return new RecordIntegrationInstances(records, onvifConnectionsEntryKind, {
|
|
92651
|
+
description: {
|
|
92652
|
+
role: "devices",
|
|
92653
|
+
discovery: {
|
|
92654
|
+
mode: "lan",
|
|
92655
|
+
scopedToInstance: false
|
|
92656
|
+
},
|
|
92657
|
+
instanceMode: "single",
|
|
92658
|
+
kind: "connection",
|
|
92659
|
+
canTest: false
|
|
92660
|
+
},
|
|
92661
|
+
formSchema: () => ({ sections: [] }),
|
|
92662
|
+
initialBody: (name) => ({ name }),
|
|
92663
|
+
configured: () => true,
|
|
92664
|
+
onChanged: () => void 0
|
|
92665
|
+
});
|
|
92666
|
+
}
|
|
92667
|
+
//#endregion
|
|
91285
92668
|
//#region ../../node_modules/xml2js/lib/defaults.js
|
|
91286
92669
|
var require_defaults = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
91287
92670
|
(function() {
|
|
@@ -100635,6 +102018,8 @@ function onvifCamerasFromHits(hits) {
|
|
|
100635
102018
|
//#endregion
|
|
100636
102019
|
//#region src/addon.ts
|
|
100637
102020
|
var ADDON_ID = "provider-onvif";
|
|
102021
|
+
/** The date of the integration-rows and device-claim migrations (D1007). */
|
|
102022
|
+
var ONVIF_INTEGRATION_MIGRATIONS_DATE = "2026-11-05";
|
|
100638
102023
|
var OnvifProviderAddon = class extends BaseDeviceProvider {
|
|
100639
102024
|
addonId = ADDON_ID;
|
|
100640
102025
|
providerName = "ONVIF";
|
|
@@ -100647,15 +102032,32 @@ var OnvifProviderAddon = class extends BaseDeviceProvider {
|
|
|
100647
102032
|
* Retire the addon-wide default credentials (D986 Q8, D987): clear the password from the
|
|
100648
102033
|
* keychain, then delete both rows. Nothing reads them; each camera holds its own.
|
|
100649
102034
|
*/
|
|
100650
|
-
async migrateSettings() {
|
|
100651
|
-
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, {
|
|
102035
|
+
async migrateSettings(records) {
|
|
102036
|
+
await this.runMigrations([...defineAddonMigrations(ADDON_MIGRATIONS, {
|
|
100652
102037
|
readStored: (keys) => readRawKeys((key) => this.readStoredGlobalKey(key), PROVIDER_ONVIF_ID, keys),
|
|
100653
102038
|
clearDefaultPassword: () => this.clearSecrets(["defaultPassword"])
|
|
100654
|
-
})
|
|
102039
|
+
}), ...defineAddonMigrations(defaultIntegrationMigrations({
|
|
102040
|
+
addonId: ADDON_ID,
|
|
102041
|
+
date: ONVIF_INTEGRATION_MIGRATIONS_DATE,
|
|
102042
|
+
seq: ADDON_MIGRATIONS.length + 1
|
|
102043
|
+
}), defaultIntegrationPortsOver({
|
|
102044
|
+
api: defaultIntegrationApiOver(this.ctx.api),
|
|
102045
|
+
records,
|
|
102046
|
+
kind: onvifConnectionsEntryKind,
|
|
102047
|
+
info: (message) => this.ctx.logger.info(message),
|
|
102048
|
+
warn: (message) => this.ctx.logger.warn(message),
|
|
102049
|
+
warnDevice: (message, deviceId) => this.ctx.logger.warn(message, { tags: { deviceId } })
|
|
102050
|
+
}))]);
|
|
100655
102051
|
}
|
|
100656
102052
|
async onInitialize() {
|
|
100657
|
-
|
|
100658
|
-
|
|
102053
|
+
const records = this.ctx.records;
|
|
102054
|
+
if (records === void 0) throw new Error("provider-onvif: ctx.records is required (hub-only addon)");
|
|
102055
|
+
await records.declare(onvifRecordKinds);
|
|
102056
|
+
await this.migrateSettings(records);
|
|
102057
|
+
return [...this.providerRegistrationsFromSuper(await super.onInitialize()), {
|
|
102058
|
+
capability: integrationInstancesCapability,
|
|
102059
|
+
provider: buildOnvifInstances(records).toProvider()
|
|
102060
|
+
}];
|
|
100659
102061
|
}
|
|
100660
102062
|
async supportsDiscovery() {
|
|
100661
102063
|
return true;
|