@camstack/addon-provider-reolink 1.2.222 → 1.2.224
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 +12 -0
- package/dist/addon.js +1154 -74
- package/dist/addon.mjs +1154 -74
- package/package.json +6 -2
package/dist/addon.js
CHANGED
|
@@ -105254,6 +105254,12 @@ var numberProcessor = (schema, ctx, _json, _params) => {
|
|
|
105254
105254
|
var booleanProcessor = (_schema, _ctx, json, _params) => {
|
|
105255
105255
|
json.type = "boolean";
|
|
105256
105256
|
};
|
|
105257
|
+
var bigintProcessor = (_schema, ctx, _json, _params) => {
|
|
105258
|
+
if (ctx.unrepresentable === "throw") throw new Error("BigInt cannot be represented in JSON Schema");
|
|
105259
|
+
};
|
|
105260
|
+
var symbolProcessor = (_schema, ctx, _json, _params) => {
|
|
105261
|
+
if (ctx.unrepresentable === "throw") throw new Error("Symbols cannot be represented in JSON Schema");
|
|
105262
|
+
};
|
|
105257
105263
|
var nullProcessor = (_schema, ctx, json, _params) => {
|
|
105258
105264
|
if (ctx.target === "openapi-3.0") {
|
|
105259
105265
|
json.type = "string";
|
|
@@ -105261,12 +105267,17 @@ var nullProcessor = (_schema, ctx, json, _params) => {
|
|
|
105261
105267
|
json.enum = [null];
|
|
105262
105268
|
} else json.type = "null";
|
|
105263
105269
|
};
|
|
105270
|
+
var undefinedProcessor = (_schema, ctx, _json, _params) => {
|
|
105271
|
+
if (ctx.unrepresentable === "throw") throw new Error("Undefined cannot be represented in JSON Schema");
|
|
105272
|
+
};
|
|
105264
105273
|
var voidProcessor = (_schema, ctx, _json, _params) => {
|
|
105265
105274
|
if (ctx.unrepresentable === "throw") throw new Error("Void cannot be represented in JSON Schema");
|
|
105266
105275
|
};
|
|
105267
105276
|
var neverProcessor = (_schema, _ctx, json, _params) => {
|
|
105268
105277
|
json.not = {};
|
|
105269
105278
|
};
|
|
105279
|
+
var anyProcessor = (_schema, _ctx, _json, _params) => {};
|
|
105280
|
+
var unknownProcessor = (_schema, _ctx, _json, _params) => {};
|
|
105270
105281
|
var dateProcessor = (_schema, ctx, _json, _params) => {
|
|
105271
105282
|
if (ctx.unrepresentable === "throw") throw new Error("Date cannot be represented in JSON Schema");
|
|
105272
105283
|
};
|
|
@@ -105298,12 +105309,53 @@ var literalProcessor = (schema, ctx, json, _params) => {
|
|
|
105298
105309
|
json.enum = vals;
|
|
105299
105310
|
}
|
|
105300
105311
|
};
|
|
105312
|
+
var nanProcessor = (_schema, ctx, _json, _params) => {
|
|
105313
|
+
if (ctx.unrepresentable === "throw") throw new Error("NaN cannot be represented in JSON Schema");
|
|
105314
|
+
};
|
|
105315
|
+
var templateLiteralProcessor = (schema, _ctx, json, _params) => {
|
|
105316
|
+
const _json = json;
|
|
105317
|
+
const pattern = schema._zod.pattern;
|
|
105318
|
+
if (!pattern) throw new Error("Pattern not found in template literal");
|
|
105319
|
+
_json.type = "string";
|
|
105320
|
+
_json.pattern = pattern.source;
|
|
105321
|
+
};
|
|
105322
|
+
var fileProcessor = (schema, _ctx, json, _params) => {
|
|
105323
|
+
const _json = json;
|
|
105324
|
+
const file = {
|
|
105325
|
+
type: "string",
|
|
105326
|
+
format: "binary",
|
|
105327
|
+
contentEncoding: "binary"
|
|
105328
|
+
};
|
|
105329
|
+
const { minimum, maximum, mime } = schema._zod.bag;
|
|
105330
|
+
if (minimum !== void 0) file.minLength = minimum;
|
|
105331
|
+
if (maximum !== void 0) file.maxLength = maximum;
|
|
105332
|
+
if (mime) if (mime.length === 1) {
|
|
105333
|
+
file.contentMediaType = mime[0];
|
|
105334
|
+
Object.assign(_json, file);
|
|
105335
|
+
} else {
|
|
105336
|
+
Object.assign(_json, file);
|
|
105337
|
+
_json.anyOf = mime.map((m) => ({ contentMediaType: m }));
|
|
105338
|
+
}
|
|
105339
|
+
else Object.assign(_json, file);
|
|
105340
|
+
};
|
|
105341
|
+
var successProcessor = (_schema, _ctx, json, _params) => {
|
|
105342
|
+
json.type = "boolean";
|
|
105343
|
+
};
|
|
105301
105344
|
var customProcessor = (_schema, ctx, _json, _params) => {
|
|
105302
105345
|
if (ctx.unrepresentable === "throw") throw new Error("Custom types cannot be represented in JSON Schema");
|
|
105303
105346
|
};
|
|
105347
|
+
var functionProcessor = (_schema, ctx, _json, _params) => {
|
|
105348
|
+
if (ctx.unrepresentable === "throw") throw new Error("Function types cannot be represented in JSON Schema");
|
|
105349
|
+
};
|
|
105304
105350
|
var transformProcessor = (_schema, ctx, _json, _params) => {
|
|
105305
105351
|
if (ctx.unrepresentable === "throw") throw new Error("Transforms cannot be represented in JSON Schema");
|
|
105306
105352
|
};
|
|
105353
|
+
var mapProcessor = (_schema, ctx, _json, _params) => {
|
|
105354
|
+
if (ctx.unrepresentable === "throw") throw new Error("Map cannot be represented in JSON Schema");
|
|
105355
|
+
};
|
|
105356
|
+
var setProcessor = (_schema, ctx, _json, _params) => {
|
|
105357
|
+
if (ctx.unrepresentable === "throw") throw new Error("Set cannot be represented in JSON Schema");
|
|
105358
|
+
};
|
|
105307
105359
|
var arrayProcessor = (schema, ctx, _json, params) => {
|
|
105308
105360
|
const json = _json;
|
|
105309
105361
|
const def = schema._zod.def;
|
|
@@ -105508,6 +105560,12 @@ var readonlyProcessor = (schema, ctx, json, params) => {
|
|
|
105508
105560
|
seen.ref = def.innerType;
|
|
105509
105561
|
json.readOnly = true;
|
|
105510
105562
|
};
|
|
105563
|
+
var promiseProcessor = (schema, ctx, _json, params) => {
|
|
105564
|
+
const def = schema._zod.def;
|
|
105565
|
+
process$1(def.innerType, ctx, params);
|
|
105566
|
+
const seen = ctx.seen.get(schema);
|
|
105567
|
+
seen.ref = def.innerType;
|
|
105568
|
+
};
|
|
105511
105569
|
var optionalProcessor = (schema, ctx, _json, params) => {
|
|
105512
105570
|
const def = schema._zod.def;
|
|
105513
105571
|
process$1(def.innerType, ctx, params);
|
|
@@ -105520,6 +105578,81 @@ var lazyProcessor = (schema, ctx, _json, params) => {
|
|
|
105520
105578
|
const seen = ctx.seen.get(schema);
|
|
105521
105579
|
seen.ref = innerType;
|
|
105522
105580
|
};
|
|
105581
|
+
var allProcessors = {
|
|
105582
|
+
string: stringProcessor,
|
|
105583
|
+
number: numberProcessor,
|
|
105584
|
+
boolean: booleanProcessor,
|
|
105585
|
+
bigint: bigintProcessor,
|
|
105586
|
+
symbol: symbolProcessor,
|
|
105587
|
+
null: nullProcessor,
|
|
105588
|
+
undefined: undefinedProcessor,
|
|
105589
|
+
void: voidProcessor,
|
|
105590
|
+
never: neverProcessor,
|
|
105591
|
+
any: anyProcessor,
|
|
105592
|
+
unknown: unknownProcessor,
|
|
105593
|
+
date: dateProcessor,
|
|
105594
|
+
enum: enumProcessor,
|
|
105595
|
+
literal: literalProcessor,
|
|
105596
|
+
nan: nanProcessor,
|
|
105597
|
+
template_literal: templateLiteralProcessor,
|
|
105598
|
+
file: fileProcessor,
|
|
105599
|
+
success: successProcessor,
|
|
105600
|
+
custom: customProcessor,
|
|
105601
|
+
function: functionProcessor,
|
|
105602
|
+
transform: transformProcessor,
|
|
105603
|
+
map: mapProcessor,
|
|
105604
|
+
set: setProcessor,
|
|
105605
|
+
array: arrayProcessor,
|
|
105606
|
+
object: objectProcessor,
|
|
105607
|
+
union: unionProcessor,
|
|
105608
|
+
intersection: intersectionProcessor,
|
|
105609
|
+
tuple: tupleProcessor,
|
|
105610
|
+
record: recordProcessor,
|
|
105611
|
+
nullable: nullableProcessor,
|
|
105612
|
+
nonoptional: nonoptionalProcessor,
|
|
105613
|
+
default: defaultProcessor,
|
|
105614
|
+
prefault: prefaultProcessor,
|
|
105615
|
+
catch: catchProcessor,
|
|
105616
|
+
pipe: pipeProcessor,
|
|
105617
|
+
readonly: readonlyProcessor,
|
|
105618
|
+
promise: promiseProcessor,
|
|
105619
|
+
optional: optionalProcessor,
|
|
105620
|
+
lazy: lazyProcessor
|
|
105621
|
+
};
|
|
105622
|
+
function toJSONSchema(input, params) {
|
|
105623
|
+
if ("_idmap" in input) {
|
|
105624
|
+
const registry = input;
|
|
105625
|
+
const ctx = initializeContext({
|
|
105626
|
+
...params,
|
|
105627
|
+
processors: allProcessors
|
|
105628
|
+
});
|
|
105629
|
+
const defs = {};
|
|
105630
|
+
for (const entry of registry._idmap.entries()) {
|
|
105631
|
+
const [_, schema] = entry;
|
|
105632
|
+
process$1(schema, ctx);
|
|
105633
|
+
}
|
|
105634
|
+
const schemas = {};
|
|
105635
|
+
ctx.external = {
|
|
105636
|
+
registry,
|
|
105637
|
+
uri: params?.uri,
|
|
105638
|
+
defs
|
|
105639
|
+
};
|
|
105640
|
+
for (const entry of registry._idmap.entries()) {
|
|
105641
|
+
const [key, schema] = entry;
|
|
105642
|
+
extractDefs(ctx, schema);
|
|
105643
|
+
schemas[key] = finalize(ctx, schema);
|
|
105644
|
+
}
|
|
105645
|
+
if (Object.keys(defs).length > 0) schemas.__shared = { [ctx.target === "draft-2020-12" ? "$defs" : "definitions"]: defs };
|
|
105646
|
+
return { schemas };
|
|
105647
|
+
}
|
|
105648
|
+
const ctx = initializeContext({
|
|
105649
|
+
...params,
|
|
105650
|
+
processors: allProcessors
|
|
105651
|
+
});
|
|
105652
|
+
process$1(input, ctx);
|
|
105653
|
+
extractDefs(ctx, input);
|
|
105654
|
+
return finalize(ctx, input);
|
|
105655
|
+
}
|
|
105523
105656
|
//#endregion
|
|
105524
105657
|
//#region ../../node_modules/zod/v4/classic/iso.js
|
|
105525
105658
|
var ZodISODateTime = /*@__PURE__*/ $constructor("ZodISODateTime", (inst, def) => {
|
|
@@ -106544,7 +106677,7 @@ var ZodIssueCode = {
|
|
|
106544
106677
|
var ZodFirstPartyTypeKind;
|
|
106545
106678
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
106546
106679
|
//#endregion
|
|
106547
|
-
//#region ../types/dist/sleep-
|
|
106680
|
+
//#region ../types/dist/sleep-D1jjWAr8.mjs
|
|
106548
106681
|
/**
|
|
106549
106682
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
106550
106683
|
* window to float samples (D455).
|
|
@@ -106699,6 +106832,9 @@ function isSecretMarkerLike(value) {
|
|
|
106699
106832
|
function isSecretClear(value) {
|
|
106700
106833
|
return isRecord$7(value) && value["$secretClear"] === true;
|
|
106701
106834
|
}
|
|
106835
|
+
function isSecretFieldView(value) {
|
|
106836
|
+
return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
|
|
106837
|
+
}
|
|
106702
106838
|
/**
|
|
106703
106839
|
* The UI view of a stored secret field value: a marker becomes "set", inline
|
|
106704
106840
|
* plaintext becomes "set (inline)", nothing becomes `null`. A value never
|
|
@@ -107350,11 +107486,36 @@ function projectStore(store, perNodeKeys, nodeId) {
|
|
|
107350
107486
|
}
|
|
107351
107487
|
return out;
|
|
107352
107488
|
}
|
|
107489
|
+
/**
|
|
107490
|
+
* The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
|
|
107491
|
+
*
|
|
107492
|
+
* A ref is DETERMINISTIC from where the value lives — the owner addon, the
|
|
107493
|
+
* scope (global, one node, one device, one integration, one row of a business
|
|
107494
|
+
* collection) and the field path inside that scope — so saving the same field
|
|
107495
|
+
* twice updates the same ref, and the ref itself says which settings blob
|
|
107496
|
+
* points at it. It is also the AAD of the value's ciphertext: a ciphertext
|
|
107497
|
+
* copied onto another ref does not decrypt.
|
|
107498
|
+
*
|
|
107499
|
+
* Array items are addressed by ITEM ID, never by index
|
|
107500
|
+
* (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
|
|
107501
|
+
*
|
|
107502
|
+
* Pure and browser-safe: no node builtins, no I/O.
|
|
107503
|
+
*/
|
|
107504
|
+
/** The URI scheme of every keychain ref. */
|
|
107505
|
+
var SECRET_REF_SCHEME = "secret://";
|
|
107353
107506
|
var GLOBAL_SCOPE = { kind: "global" };
|
|
107354
107507
|
/** The scope every addon-wide (not per-node, not per-device) setting lives in. */
|
|
107355
107508
|
function globalSecretScope() {
|
|
107356
107509
|
return GLOBAL_SCOPE;
|
|
107357
107510
|
}
|
|
107511
|
+
/**
|
|
107512
|
+
* An addon id as a ref owner. A hub in-process builtin's context id is
|
|
107513
|
+
* `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
|
|
107514
|
+
* addon running forked.
|
|
107515
|
+
*/
|
|
107516
|
+
function normalizeSecretOwner(addonId) {
|
|
107517
|
+
return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
|
|
107518
|
+
}
|
|
107358
107519
|
function encodeSegment(value) {
|
|
107359
107520
|
return encodeURIComponent(value);
|
|
107360
107521
|
}
|
|
@@ -107365,6 +107526,16 @@ function decodeSegment(value) {
|
|
|
107365
107526
|
return null;
|
|
107366
107527
|
}
|
|
107367
107528
|
}
|
|
107529
|
+
/** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
|
|
107530
|
+
function formatSecretScope(scope) {
|
|
107531
|
+
switch (scope.kind) {
|
|
107532
|
+
case "global": return "global";
|
|
107533
|
+
case "node": return `node/${encodeSegment(scope.nodeId)}`;
|
|
107534
|
+
case "device": return `device/${scope.deviceId}`;
|
|
107535
|
+
case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
|
|
107536
|
+
case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
|
|
107537
|
+
}
|
|
107538
|
+
}
|
|
107368
107539
|
/**
|
|
107369
107540
|
* The field path of a value inside an array item, addressed by the item's id:
|
|
107370
107541
|
* `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
|
|
@@ -107373,6 +107544,13 @@ function decodeSegment(value) {
|
|
|
107373
107544
|
function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
|
|
107374
107545
|
return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
|
|
107375
107546
|
}
|
|
107547
|
+
/** Build a ref. Throws on an empty owner or field path — both are bugs. */
|
|
107548
|
+
function buildSecretRef(parts) {
|
|
107549
|
+
const owner = normalizeSecretOwner(parts.owner);
|
|
107550
|
+
if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
|
|
107551
|
+
if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
|
|
107552
|
+
return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
|
|
107553
|
+
}
|
|
107376
107554
|
function parseScope(segments) {
|
|
107377
107555
|
switch (segments[0]) {
|
|
107378
107556
|
case "global": return {
|
|
@@ -109285,6 +109463,45 @@ function normalizeAddonInitResult(result) {
|
|
|
109285
109463
|
if (Array.isArray(result)) return { providers: result };
|
|
109286
109464
|
return result;
|
|
109287
109465
|
}
|
|
109466
|
+
var PREFIX = "records/";
|
|
109467
|
+
/** The base of every records failure. `detail` is the human part of the message. */
|
|
109468
|
+
var RecordsError = class extends Error {
|
|
109469
|
+
code;
|
|
109470
|
+
detail;
|
|
109471
|
+
constructor(code, detail) {
|
|
109472
|
+
super(`${PREFIX}${code}: ${detail}`);
|
|
109473
|
+
this.name = "RecordsError";
|
|
109474
|
+
this.code = code;
|
|
109475
|
+
this.detail = detail;
|
|
109476
|
+
}
|
|
109477
|
+
};
|
|
109478
|
+
/** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
|
|
109479
|
+
var RevConflictError = class extends RecordsError {
|
|
109480
|
+
constructor(detail) {
|
|
109481
|
+
super("rev-conflict", detail);
|
|
109482
|
+
this.name = "RevConflictError";
|
|
109483
|
+
}
|
|
109484
|
+
};
|
|
109485
|
+
/** The keychain could not store or serve a value: explicit, never an empty string. */
|
|
109486
|
+
var RecordSecretUnavailableError = class extends RecordsError {
|
|
109487
|
+
constructor(detail) {
|
|
109488
|
+
super("secret-unavailable", detail);
|
|
109489
|
+
this.name = "RecordSecretUnavailableError";
|
|
109490
|
+
}
|
|
109491
|
+
};
|
|
109492
|
+
var InvalidRecordError = class extends RecordsError {
|
|
109493
|
+
constructor(detail) {
|
|
109494
|
+
super("invalid-record", detail);
|
|
109495
|
+
this.name = "InvalidRecordError";
|
|
109496
|
+
}
|
|
109497
|
+
};
|
|
109498
|
+
/** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
|
|
109499
|
+
var DeclarationRefusedError = class extends RecordsError {
|
|
109500
|
+
constructor(detail) {
|
|
109501
|
+
super("declaration-refused", detail);
|
|
109502
|
+
this.name = "DeclarationRefusedError";
|
|
109503
|
+
}
|
|
109504
|
+
};
|
|
109288
109505
|
/** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
|
|
109289
109506
|
/** The files a kind may live in. `history` is device-history's (D850), never records. */
|
|
109290
109507
|
var RECORD_DATABASES = ["settings", "events"];
|
|
@@ -109300,6 +109517,183 @@ var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
|
|
|
109300
109517
|
var RECORD_MAX_DATA_BYTES = 1024 * 1024;
|
|
109301
109518
|
var RECORD_BATCH_MAX = 1e3;
|
|
109302
109519
|
var RECORD_QUERY_MAX = 1e3;
|
|
109520
|
+
/** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
|
|
109521
|
+
var RECORD_SETTINGS_MAX_ROWS = 5e4;
|
|
109522
|
+
/** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
|
|
109523
|
+
var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
109524
|
+
/**
|
|
109525
|
+
* Secret fields of a record, addressed the way a Zod schema declares them
|
|
109526
|
+
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
109527
|
+
* (`auth.accessToken`, `headers[].value`).
|
|
109528
|
+
*
|
|
109529
|
+
* `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
|
|
109530
|
+
* names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
|
|
109531
|
+
* key releases exactly that key's secret.
|
|
109532
|
+
*
|
|
109533
|
+
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
109534
|
+
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
109535
|
+
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
109536
|
+
* leaf names its element: `headers[X-Api].value`.
|
|
109537
|
+
*
|
|
109538
|
+
* Pure and browser-safe. Nothing here mutates: every transform returns a new value.
|
|
109539
|
+
*/
|
|
109540
|
+
/** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
|
|
109541
|
+
function parseSecretPath(path) {
|
|
109542
|
+
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
109543
|
+
return path.split(".").map((raw) => {
|
|
109544
|
+
const each = raw.endsWith("[]");
|
|
109545
|
+
const map = raw.endsWith("{}");
|
|
109546
|
+
const key = each || map ? raw.slice(0, -2) : raw;
|
|
109547
|
+
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
109548
|
+
return {
|
|
109549
|
+
key,
|
|
109550
|
+
each,
|
|
109551
|
+
map
|
|
109552
|
+
};
|
|
109553
|
+
});
|
|
109554
|
+
}
|
|
109555
|
+
/** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
|
|
109556
|
+
function secretArrayPrefixes(path) {
|
|
109557
|
+
const segments = parseSecretPath(path);
|
|
109558
|
+
const prefixes = [];
|
|
109559
|
+
const walked = [];
|
|
109560
|
+
for (const segment of segments) {
|
|
109561
|
+
walked.push(segment.key);
|
|
109562
|
+
if (segment.each) prefixes.push(walked.join("."));
|
|
109563
|
+
}
|
|
109564
|
+
return prefixes;
|
|
109565
|
+
}
|
|
109566
|
+
function isRecord$4(value) {
|
|
109567
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
109568
|
+
}
|
|
109569
|
+
function elementId(element, idKey, declared, prefix) {
|
|
109570
|
+
const id = isRecord$4(element) ? element[idKey] : void 0;
|
|
109571
|
+
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`);
|
|
109572
|
+
return id;
|
|
109573
|
+
}
|
|
109574
|
+
/**
|
|
109575
|
+
* Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
|
|
109576
|
+
* visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
|
|
109577
|
+
* result is a new body, `data` is untouched.
|
|
109578
|
+
*/
|
|
109579
|
+
var REMOVE_LEAF = Symbol("remove-leaf");
|
|
109580
|
+
function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
|
|
109581
|
+
if (!isRecord$4(node)) return node;
|
|
109582
|
+
const segment = segments[index];
|
|
109583
|
+
if (segment === void 0) return node;
|
|
109584
|
+
if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
|
|
109585
|
+
const child = node[segment.key];
|
|
109586
|
+
const keys = [...walkedKeys, segment.key];
|
|
109587
|
+
const last = index === segments.length - 1;
|
|
109588
|
+
if (segment.each) {
|
|
109589
|
+
if (!Array.isArray(child)) return node;
|
|
109590
|
+
const prefix = keys.join(".");
|
|
109591
|
+
const idKey = arrayKeys[prefix];
|
|
109592
|
+
if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
|
|
109593
|
+
const mapped = child.map((element) => {
|
|
109594
|
+
const id = elementId(element, idKey, declared, prefix);
|
|
109595
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
|
|
109596
|
+
return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
|
|
109597
|
+
});
|
|
109598
|
+
return {
|
|
109599
|
+
...node,
|
|
109600
|
+
[segment.key]: mapped
|
|
109601
|
+
};
|
|
109602
|
+
}
|
|
109603
|
+
if (segment.map) {
|
|
109604
|
+
if (!isRecord$4(child)) return node;
|
|
109605
|
+
const kept = Object.entries(child).map(([mapKey, value]) => {
|
|
109606
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
|
|
109607
|
+
if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
|
|
109608
|
+
return [mapKey, visit({
|
|
109609
|
+
declared,
|
|
109610
|
+
concrete,
|
|
109611
|
+
value
|
|
109612
|
+
})];
|
|
109613
|
+
}).filter(([, value]) => value !== REMOVE_LEAF);
|
|
109614
|
+
return {
|
|
109615
|
+
...node,
|
|
109616
|
+
[segment.key]: Object.fromEntries(kept)
|
|
109617
|
+
};
|
|
109618
|
+
}
|
|
109619
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
109620
|
+
if (last) {
|
|
109621
|
+
const replacement = visit({
|
|
109622
|
+
declared,
|
|
109623
|
+
concrete,
|
|
109624
|
+
value: child
|
|
109625
|
+
});
|
|
109626
|
+
if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
|
|
109627
|
+
return {
|
|
109628
|
+
...node,
|
|
109629
|
+
[segment.key]: replacement
|
|
109630
|
+
};
|
|
109631
|
+
}
|
|
109632
|
+
return {
|
|
109633
|
+
...node,
|
|
109634
|
+
[segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
|
|
109635
|
+
};
|
|
109636
|
+
}
|
|
109637
|
+
/** Replace/remove every present secret leaf; returns a NEW body. */
|
|
109638
|
+
function mapSecretLeaves(data, paths, arrayKeys, visit) {
|
|
109639
|
+
let current = data;
|
|
109640
|
+
for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
|
|
109641
|
+
return current;
|
|
109642
|
+
}
|
|
109643
|
+
/** Every present secret leaf, read-only. */
|
|
109644
|
+
function collectSecretLeaves(data, paths, arrayKeys) {
|
|
109645
|
+
const found = [];
|
|
109646
|
+
mapSecretLeaves(data, paths, arrayKeys, (leaf) => {
|
|
109647
|
+
found.push(leaf);
|
|
109648
|
+
return leaf.value;
|
|
109649
|
+
});
|
|
109650
|
+
return found;
|
|
109651
|
+
}
|
|
109652
|
+
/**
|
|
109653
|
+
* The keychain collection name a kind's secrets live under. Prefixed so a
|
|
109654
|
+
* record's refs never collide with the refs a legacy collection of the same name
|
|
109655
|
+
* already holds during an adoption.
|
|
109656
|
+
*/
|
|
109657
|
+
function recordSecretCollection(kind) {
|
|
109658
|
+
return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
|
|
109659
|
+
}
|
|
109660
|
+
/** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
|
|
109661
|
+
function recordSecretRef(owner, kind, rowId, concretePath) {
|
|
109662
|
+
return buildSecretRef({
|
|
109663
|
+
owner,
|
|
109664
|
+
scope: {
|
|
109665
|
+
kind: "row",
|
|
109666
|
+
collection: recordSecretCollection(kind),
|
|
109667
|
+
rowId
|
|
109668
|
+
},
|
|
109669
|
+
fieldPath: concretePath
|
|
109670
|
+
});
|
|
109671
|
+
}
|
|
109672
|
+
/**
|
|
109673
|
+
* A stored secret, as an admin read shows it: a VIEW naming the row's own
|
|
109674
|
+
* keychain ref (`set: true`), never the value and no longer the bare redaction
|
|
109675
|
+
* token. The form hands it to the secret field, which reveals the value by ref
|
|
109676
|
+
* through the audited `keychain-admin.reveal`; sent back untouched it keeps the
|
|
109677
|
+
* stored secret (the records door reads a view as KEEP).
|
|
109678
|
+
*
|
|
109679
|
+
* ONE implementation for the entries service and for every addon that serves
|
|
109680
|
+
* its own instances (`RecordIntegrationInstances`).
|
|
109681
|
+
*/
|
|
109682
|
+
function isRecord$3(value) {
|
|
109683
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
109684
|
+
}
|
|
109685
|
+
function secretViewsOf(owner, kind, id, decl, data) {
|
|
109686
|
+
if (decl.secretPaths.length === 0) return data;
|
|
109687
|
+
const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
|
|
109688
|
+
if (leaf.value !== "__camstack_redacted__") return leaf.value;
|
|
109689
|
+
return {
|
|
109690
|
+
$secret: recordSecretRef(owner, kind, id, leaf.concrete),
|
|
109691
|
+
v: 1,
|
|
109692
|
+
set: true
|
|
109693
|
+
};
|
|
109694
|
+
});
|
|
109695
|
+
return isRecord$3(shown) ? shown : data;
|
|
109696
|
+
}
|
|
109303
109697
|
/**
|
|
109304
109698
|
* The share-view scope kinds a share-link token can carry. The hub mints
|
|
109305
109699
|
* and validates them (`ShareViewGrantSchema` builds its
|
|
@@ -109459,6 +109853,126 @@ var RecordKindDeclarationSchema = object({
|
|
|
109459
109853
|
}).strict();
|
|
109460
109854
|
/** The kind names: lower-case, dash separated, short enough for an index name. */
|
|
109461
109855
|
var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
|
|
109856
|
+
/** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
|
|
109857
|
+
function assertValidDeclaration(name, decl) {
|
|
109858
|
+
const refuse = (why) => {
|
|
109859
|
+
throw new DeclarationRefusedError(`kind "${name}": ${why}`);
|
|
109860
|
+
};
|
|
109861
|
+
if (!RecordKindNameSchema.safeParse(name).success) refuse("the name must be lower-case-dashed");
|
|
109862
|
+
const seen = /* @__PURE__ */ new Set();
|
|
109863
|
+
for (const index of decl.indexes) {
|
|
109864
|
+
if (seen.has(index.name)) refuse(`index name "${index.name}" is declared twice`);
|
|
109865
|
+
seen.add(index.name);
|
|
109866
|
+
}
|
|
109867
|
+
const { retention } = decl;
|
|
109868
|
+
const hasSecrets = decl.secretPaths.length > 0;
|
|
109869
|
+
if ("ttlMs" in retention) {
|
|
109870
|
+
if (retention.from === "at" && decl.at !== "required") refuse("a ttl from \"at\" needs at: \"required\"");
|
|
109871
|
+
if (hasSecrets) refuse("a kind with secret fields cannot use engine TTL (its keychain values would be orphaned)");
|
|
109872
|
+
}
|
|
109873
|
+
if ("maxRows" in retention || "maxRowsPerDevice" in retention) {
|
|
109874
|
+
if (retention.maxRowsPerDevice !== void 0 && decl.device !== "required") refuse("maxRowsPerDevice needs device: \"required\"");
|
|
109875
|
+
if (decl.at !== "required") refuse("a row bound trims the oldest \"at\": it needs at: \"required\"");
|
|
109876
|
+
if (hasSecrets) refuse("a kind with secret fields cannot use an engine row bound");
|
|
109877
|
+
const bound = Math.max(retention.maxRows ?? 0, retention.maxRowsPerDevice ?? 0);
|
|
109878
|
+
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)`);
|
|
109879
|
+
}
|
|
109880
|
+
if (decl.deviceRemoval === "delete" && decl.device === "none") refuse("deviceRemoval: \"delete\" needs a device: \"required\" or \"optional\"");
|
|
109881
|
+
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`);
|
|
109882
|
+
}
|
|
109883
|
+
function bodyFieldsOf(schema) {
|
|
109884
|
+
return { ...toJSONSchema(schema, {
|
|
109885
|
+
unrepresentable: "any",
|
|
109886
|
+
io: "input"
|
|
109887
|
+
}) };
|
|
109888
|
+
}
|
|
109889
|
+
function isRecordLike(value) {
|
|
109890
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
109891
|
+
}
|
|
109892
|
+
/** Does every branch of the body schema carry `field`? (A discriminated union has several.) */
|
|
109893
|
+
function hasNameField(json, field) {
|
|
109894
|
+
const branches = json["oneOf"] ?? json["anyOf"];
|
|
109895
|
+
if (Array.isArray(branches)) return branches.length > 0 && branches.every((b) => isRecordLike(b) && hasNameField(b, field));
|
|
109896
|
+
const properties = json["properties"];
|
|
109897
|
+
return isRecordLike(properties) && Object.prototype.hasOwnProperty.call(properties, field);
|
|
109898
|
+
}
|
|
109899
|
+
/**
|
|
109900
|
+
* Declare the kinds of a package. The keys are the kind names. The result is
|
|
109901
|
+
* what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
|
|
109902
|
+
* types its handle from.
|
|
109903
|
+
*/
|
|
109904
|
+
function defineRecordKinds(specs) {
|
|
109905
|
+
const out = {};
|
|
109906
|
+
for (const [name, spec] of Object.entries(specs)) {
|
|
109907
|
+
const secretPaths = collectZodSecretPaths(spec.schema);
|
|
109908
|
+
const nameField = spec.entry?.nameField ?? "name";
|
|
109909
|
+
const fields = bodyFieldsOf(spec.schema);
|
|
109910
|
+
if (spec.entry !== void 0 && !hasNameField(fields, nameField)) throw new DeclarationRefusedError(`kind "${name}": an entry kind's schema needs a "${nameField}" field`);
|
|
109911
|
+
const parsed = RecordKindDeclarationSchema.parse({
|
|
109912
|
+
database: spec.database,
|
|
109913
|
+
class: spec.class,
|
|
109914
|
+
writeTrigger: spec.writeTrigger,
|
|
109915
|
+
device: spec.device,
|
|
109916
|
+
at: spec.at,
|
|
109917
|
+
indexes: spec.indexes === void 0 ? void 0 : [...spec.indexes],
|
|
109918
|
+
retention: spec.retention,
|
|
109919
|
+
deviceRemoval: spec.deviceRemoval,
|
|
109920
|
+
maxDataBytes: spec.maxDataBytes,
|
|
109921
|
+
secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
|
|
109922
|
+
secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
|
|
109923
|
+
transactFamily: spec.transactFamily,
|
|
109924
|
+
entry: spec.entry === void 0 ? void 0 : {
|
|
109925
|
+
title: spec.entry.title,
|
|
109926
|
+
nameField,
|
|
109927
|
+
fields
|
|
109928
|
+
}
|
|
109929
|
+
});
|
|
109930
|
+
assertValidDeclaration(name, parsed);
|
|
109931
|
+
out[name] = {
|
|
109932
|
+
name,
|
|
109933
|
+
schema: spec.schema,
|
|
109934
|
+
declaration: parsed
|
|
109935
|
+
};
|
|
109936
|
+
}
|
|
109937
|
+
return out;
|
|
109938
|
+
}
|
|
109939
|
+
/**
|
|
109940
|
+
* Entry kinds (D881): every list of user-created entries (notification
|
|
109941
|
+
* targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
|
|
109942
|
+
* kind-specific body, and the admin UI lists/adds/edits/deletes all of them
|
|
109943
|
+
* through the `record-entries` capability with password fields derived from
|
|
109944
|
+
* the schema (`.meta({ secret: true })`).
|
|
109945
|
+
*/
|
|
109946
|
+
/** The body of a plain entry: a name and the fields the kind adds. */
|
|
109947
|
+
function entryBody(shape) {
|
|
109948
|
+
return object({
|
|
109949
|
+
name: string().min(1),
|
|
109950
|
+
...shape
|
|
109951
|
+
});
|
|
109952
|
+
}
|
|
109953
|
+
/**
|
|
109954
|
+
* Declare one entry kind. An entry is configuration: never trimmed by the
|
|
109955
|
+
* engine, never aged, belonging to no device.
|
|
109956
|
+
*/
|
|
109957
|
+
function defineEntryKind(name, spec) {
|
|
109958
|
+
const defined = defineRecordKinds({ [name]: {
|
|
109959
|
+
schema: spec.schema,
|
|
109960
|
+
class: "config",
|
|
109961
|
+
writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
|
|
109962
|
+
retention: { none: "an entry goes only when the operator deletes it" },
|
|
109963
|
+
...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
|
|
109964
|
+
...spec.database === void 0 ? {} : { database: spec.database },
|
|
109965
|
+
...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
|
|
109966
|
+
...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
|
|
109967
|
+
...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
|
|
109968
|
+
entry: {
|
|
109969
|
+
title: spec.title,
|
|
109970
|
+
...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
|
|
109971
|
+
}
|
|
109972
|
+
} })[name];
|
|
109973
|
+
if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
|
|
109974
|
+
return defined;
|
|
109975
|
+
}
|
|
109462
109976
|
object({
|
|
109463
109977
|
field: string().min(1),
|
|
109464
109978
|
on: literal("advance")
|
|
@@ -113631,6 +114145,205 @@ var IntegrationInstanceRefSchema = object({
|
|
|
113631
114145
|
]),
|
|
113632
114146
|
candidateStableId: string().optional()
|
|
113633
114147
|
});
|
|
114148
|
+
/**
|
|
114149
|
+
* `RecordIntegrationInstances` - the `integration-instances` provider over ONE
|
|
114150
|
+
* entry kind, so an addon that holds a connection serves it in ~30 lines.
|
|
114151
|
+
*
|
|
114152
|
+
* Every instance is one record of the addon's own kind, keyed by the
|
|
114153
|
+
* `integrations` registry id. The records door does the secret work: a field the
|
|
114154
|
+
* kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
|
|
114155
|
+
* deleting the row deletes the secret, and one row's value is never another's.
|
|
114156
|
+
*
|
|
114157
|
+
* Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
|
|
114158
|
+
* token or the view the form was given KEEP the stored secret;
|
|
114159
|
+
* `{ $secretClear: true }` clears it; any other string replaces it.
|
|
114160
|
+
*/
|
|
114161
|
+
/** The most instances one addon lists (an integrations page, not a table). */
|
|
114162
|
+
var INSTANCES_LIST_MAX = 1e3;
|
|
114163
|
+
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
114164
|
+
function topLevelSecretKeys(paths) {
|
|
114165
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
|
|
114166
|
+
}
|
|
114167
|
+
/** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
|
|
114168
|
+
function secretMapKeys(paths) {
|
|
114169
|
+
return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
|
|
114170
|
+
}
|
|
114171
|
+
/**
|
|
114172
|
+
* A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
|
|
114173
|
+
* view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
|
|
114174
|
+
* or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
|
|
114175
|
+
*/
|
|
114176
|
+
function secretMapPatch(value) {
|
|
114177
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
114178
|
+
const out = {};
|
|
114179
|
+
for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
|
|
114180
|
+
else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
|
|
114181
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
114182
|
+
}
|
|
114183
|
+
/** Does the kind's body schema carry an `enabled` field? */
|
|
114184
|
+
function declaresEnabled(kind) {
|
|
114185
|
+
const json = toJSONSchema(kind.schema, {
|
|
114186
|
+
unrepresentable: "any",
|
|
114187
|
+
io: "input"
|
|
114188
|
+
});
|
|
114189
|
+
const properties = Reflect.get(json, "properties");
|
|
114190
|
+
return typeof properties === "object" && properties !== null && "enabled" in properties;
|
|
114191
|
+
}
|
|
114192
|
+
var RecordIntegrationInstances = class {
|
|
114193
|
+
records;
|
|
114194
|
+
kind;
|
|
114195
|
+
hooks;
|
|
114196
|
+
handle;
|
|
114197
|
+
secretKeys;
|
|
114198
|
+
secretMaps;
|
|
114199
|
+
nameField;
|
|
114200
|
+
/** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
|
|
114201
|
+
registryOwnsEnabled;
|
|
114202
|
+
constructor(records, kind, hooks) {
|
|
114203
|
+
this.records = records;
|
|
114204
|
+
this.kind = kind;
|
|
114205
|
+
this.hooks = hooks;
|
|
114206
|
+
this.handle = records.kind(kind);
|
|
114207
|
+
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
114208
|
+
this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
|
|
114209
|
+
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
114210
|
+
this.registryOwnsEnabled = hooks.description.role === "devices";
|
|
114211
|
+
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"`);
|
|
114212
|
+
}
|
|
114213
|
+
/**
|
|
114214
|
+
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
114215
|
+
* locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
|
|
114216
|
+
* caller can keep what it had instead of reconfiguring with an empty secret (D49).
|
|
114217
|
+
*/
|
|
114218
|
+
async readResolved(id) {
|
|
114219
|
+
try {
|
|
114220
|
+
const record = await this.handle.getResolved(id);
|
|
114221
|
+
return record === null ? null : {
|
|
114222
|
+
id,
|
|
114223
|
+
body: record.data,
|
|
114224
|
+
enabled: record.data.enabled !== false,
|
|
114225
|
+
secrets: "resolved"
|
|
114226
|
+
};
|
|
114227
|
+
} catch (err) {
|
|
114228
|
+
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
114229
|
+
const stored = await this.handle.get(id);
|
|
114230
|
+
if (stored === null) return null;
|
|
114231
|
+
return {
|
|
114232
|
+
id,
|
|
114233
|
+
body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
|
|
114234
|
+
enabled: stored.data.enabled !== false,
|
|
114235
|
+
secrets: "unavailable"
|
|
114236
|
+
};
|
|
114237
|
+
}
|
|
114238
|
+
}
|
|
114239
|
+
/** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
|
|
114240
|
+
async getResolved(id) {
|
|
114241
|
+
return (await this.readResolved(id))?.body ?? null;
|
|
114242
|
+
}
|
|
114243
|
+
/** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
|
|
114244
|
+
async listResolved() {
|
|
114245
|
+
const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
|
|
114246
|
+
const out = [];
|
|
114247
|
+
for (const record of page.records) {
|
|
114248
|
+
const read = await this.readResolved(record.id);
|
|
114249
|
+
if (read !== null) out.push(read);
|
|
114250
|
+
}
|
|
114251
|
+
return out;
|
|
114252
|
+
}
|
|
114253
|
+
viewOf(record) {
|
|
114254
|
+
const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
|
|
114255
|
+
const rawName = record.data[this.nameField];
|
|
114256
|
+
const kind = this.hooks.kindOf?.(record.data);
|
|
114257
|
+
const health = this.hooks.health?.(record.data, record.id);
|
|
114258
|
+
return {
|
|
114259
|
+
id: record.id,
|
|
114260
|
+
name: typeof rawName === "string" ? rawName : record.id,
|
|
114261
|
+
configured: this.hooks.configured(record.data),
|
|
114262
|
+
...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
|
|
114263
|
+
formValues: shown,
|
|
114264
|
+
...kind !== void 0 ? { kind } : {},
|
|
114265
|
+
...health?.status !== void 0 ? { status: health.status } : {},
|
|
114266
|
+
...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
|
|
114267
|
+
};
|
|
114268
|
+
}
|
|
114269
|
+
describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
|
|
114270
|
+
...this.hooks.description,
|
|
114271
|
+
mintsId: true
|
|
114272
|
+
};
|
|
114273
|
+
instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
|
|
114274
|
+
listInstances = async () => {
|
|
114275
|
+
return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
|
|
114276
|
+
};
|
|
114277
|
+
getInstance = async ({ id }) => {
|
|
114278
|
+
const record = await this.handle.get(id);
|
|
114279
|
+
return record === null ? null : this.viewOf(record);
|
|
114280
|
+
};
|
|
114281
|
+
/** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
|
|
114282
|
+
createInstance = async ({ id, name, kind }) => {
|
|
114283
|
+
const minted = id ?? this.hooks.mintId?.({
|
|
114284
|
+
name,
|
|
114285
|
+
...kind !== void 0 ? { kind } : {}
|
|
114286
|
+
});
|
|
114287
|
+
if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
|
|
114288
|
+
await this.handle.put({
|
|
114289
|
+
id: minted,
|
|
114290
|
+
data: this.hooks.initialBody(name, kind)
|
|
114291
|
+
}, { ifRev: 0 });
|
|
114292
|
+
await this.hooks.onChanged?.(minted, "created");
|
|
114293
|
+
return { id: minted };
|
|
114294
|
+
};
|
|
114295
|
+
updateInstance = async ({ id, name, values }) => {
|
|
114296
|
+
const patch = {};
|
|
114297
|
+
for (const [key, value] of Object.entries(values)) {
|
|
114298
|
+
if (value === void 0) continue;
|
|
114299
|
+
if (this.secretMaps.has(key)) {
|
|
114300
|
+
const entries = secretMapPatch(value);
|
|
114301
|
+
if (entries !== null) patch[key] = entries;
|
|
114302
|
+
} else if (!this.secretKeys.has(key)) patch[key] = value;
|
|
114303
|
+
else if (isSecretClear(value)) patch[key] = null;
|
|
114304
|
+
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
114305
|
+
}
|
|
114306
|
+
if (name !== void 0) patch[this.nameField] = name;
|
|
114307
|
+
await this.handle.patch(id, patch);
|
|
114308
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
114309
|
+
return { ok: true };
|
|
114310
|
+
};
|
|
114311
|
+
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
114312
|
+
setInstanceEnabled = async ({ id, enabled }) => {
|
|
114313
|
+
if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
|
|
114314
|
+
await this.handle.patch(id, { enabled });
|
|
114315
|
+
await this.hooks.onChanged?.(id, "enabled");
|
|
114316
|
+
return { ok: true };
|
|
114317
|
+
};
|
|
114318
|
+
/** The row and its keychain refs go together (the records door). */
|
|
114319
|
+
deleteInstance = async ({ id }) => {
|
|
114320
|
+
await this.handle.delete(id);
|
|
114321
|
+
await this.hooks.onChanged?.(id, "deleted");
|
|
114322
|
+
return { ok: true };
|
|
114323
|
+
};
|
|
114324
|
+
testInstance = async (input) => {
|
|
114325
|
+
if (this.hooks.test === void 0) return {
|
|
114326
|
+
success: false,
|
|
114327
|
+
status: "unsupported",
|
|
114328
|
+
error: "This integration declares no connection test"
|
|
114329
|
+
};
|
|
114330
|
+
return this.hooks.test(input);
|
|
114331
|
+
};
|
|
114332
|
+
/** The flat provider object to hand to `registerProvider`. */
|
|
114333
|
+
toProvider() {
|
|
114334
|
+
return {
|
|
114335
|
+
describe: this.describe,
|
|
114336
|
+
instanceFormSchema: this.instanceFormSchema,
|
|
114337
|
+
listInstances: this.listInstances,
|
|
114338
|
+
getInstance: this.getInstance,
|
|
114339
|
+
createInstance: this.createInstance,
|
|
114340
|
+
updateInstance: this.updateInstance,
|
|
114341
|
+
setInstanceEnabled: this.setInstanceEnabled,
|
|
114342
|
+
deleteInstance: this.deleteInstance,
|
|
114343
|
+
testInstance: this.testInstance
|
|
114344
|
+
};
|
|
114345
|
+
}
|
|
114346
|
+
};
|
|
113634
114347
|
var EncodeProfileSchema = object({
|
|
113635
114348
|
video: object({
|
|
113636
114349
|
codec: _enum([
|
|
@@ -114735,6 +115448,130 @@ var MigrationOwnerStepSchema = object({
|
|
|
114735
115448
|
/** The ledger state the engine found: `pending` has no row yet. */
|
|
114736
115449
|
state: union([literal("pending"), MigrationStatusSchema])
|
|
114737
115450
|
});
|
|
115451
|
+
/**
|
|
115452
|
+
* Move an integration's legacy `integration_settings` rows into a typed record of the addon's own
|
|
115453
|
+
* kind (planning#416, the one store). The record is keyed by the `integrations` registry id, so
|
|
115454
|
+
* the integration id, the devices stamped with it and the automations that name it do not change.
|
|
115455
|
+
*
|
|
115456
|
+
* Order, so a crash or a locked keychain is safe at every point:
|
|
115457
|
+
*
|
|
115458
|
+
* 1. Phase A, read only: every row of every integration of the addon is read with its secrets
|
|
115459
|
+
* resolved. One that cannot be read (a locked keychain) throws, naming the integration, before
|
|
115460
|
+
* anything is written; `block-owner` then holds the addon.
|
|
115461
|
+
* 2. Phase B, per integration: the addon's `bodyFrom` builds the body; the record is put with
|
|
115462
|
+
* `ifRev: 0` (an existing record is never overwritten). A record this pass CREATED is read back
|
|
115463
|
+
* and every secret leaf must equal what the rows held, else the record is deleted and the pass
|
|
115464
|
+
* throws; an EXISTING record must hold every secret the rows carry, else the rows are kept and
|
|
115465
|
+
* a warning names the path. Only then are the rows (and their keychain values) cleared.
|
|
115466
|
+
*
|
|
115467
|
+
* Idempotent: an integration whose rows are gone and whose record exists is skipped, so a rerun is
|
|
115468
|
+
* a no-op. A default integration with no rows still gets its record (an empty body, D985).
|
|
115469
|
+
* Never logs or throws a secret value; messages name integration ids and secret PATHS only.
|
|
115470
|
+
*/
|
|
115471
|
+
function secretLeavesOf(body, spec) {
|
|
115472
|
+
const out = /* @__PURE__ */ new Map();
|
|
115473
|
+
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);
|
|
115474
|
+
return out;
|
|
115475
|
+
}
|
|
115476
|
+
function why(err) {
|
|
115477
|
+
return err instanceof Error ? err.message : String(err);
|
|
115478
|
+
}
|
|
115479
|
+
async function moveIntegrationRowsToRecords(ports, spec) {
|
|
115480
|
+
const integrations = await ports.listIntegrations(spec.addonId);
|
|
115481
|
+
if (integrations.length === 0) return {
|
|
115482
|
+
outcome: "already-done",
|
|
115483
|
+
summary: { integrations: 0 }
|
|
115484
|
+
};
|
|
115485
|
+
const rowsOf = /* @__PURE__ */ new Map();
|
|
115486
|
+
for (const integration of integrations) try {
|
|
115487
|
+
rowsOf.set(integration.id, await ports.readRows(integration.id));
|
|
115488
|
+
} catch (err) {
|
|
115489
|
+
throw new Error(`integration '${integration.id}': the legacy settings cannot be read with their secrets (${why(err)}) - nothing moved`, { cause: err });
|
|
115490
|
+
}
|
|
115491
|
+
let created = 0;
|
|
115492
|
+
let cleared = 0;
|
|
115493
|
+
let kept = 0;
|
|
115494
|
+
for (const integration of integrations) {
|
|
115495
|
+
const values = rowsOf.get(integration.id) ?? {};
|
|
115496
|
+
const hasRows = Object.keys(values).length > 0;
|
|
115497
|
+
const body = spec.bodyFrom(values, ports, integration);
|
|
115498
|
+
const put = await ports.putRecord(integration.id, body);
|
|
115499
|
+
if (put === "created") created += 1;
|
|
115500
|
+
if (!hasRows) continue;
|
|
115501
|
+
const wanted = secretLeavesOf(body, spec);
|
|
115502
|
+
const back = await ports.readRecord(integration.id);
|
|
115503
|
+
if (back === null) throw new Error(`integration '${integration.id}': the record is gone after it was put`);
|
|
115504
|
+
const held = secretLeavesOf(back, spec);
|
|
115505
|
+
if (put === "created") {
|
|
115506
|
+
const wrong = [...wanted].filter(([path, value]) => held.get(path) !== value).map(([p]) => p);
|
|
115507
|
+
if (wrong.length > 0) {
|
|
115508
|
+
await ports.deleteRecord(integration.id);
|
|
115509
|
+
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`);
|
|
115510
|
+
}
|
|
115511
|
+
} else {
|
|
115512
|
+
const missing = [...wanted].filter(([path]) => !held.has(path)).map(([p]) => p);
|
|
115513
|
+
if (missing.length > 0) {
|
|
115514
|
+
ports.warn(`integration '${integration.id}': the existing record holds no ${missing.join(", ")}; the legacy settings are kept, not cleared`);
|
|
115515
|
+
kept += 1;
|
|
115516
|
+
continue;
|
|
115517
|
+
}
|
|
115518
|
+
}
|
|
115519
|
+
await ports.clearRows(integration.id);
|
|
115520
|
+
cleared += 1;
|
|
115521
|
+
}
|
|
115522
|
+
return created === 0 && cleared === 0 ? {
|
|
115523
|
+
outcome: "already-done",
|
|
115524
|
+
summary: {
|
|
115525
|
+
integrations: integrations.length,
|
|
115526
|
+
kept
|
|
115527
|
+
}
|
|
115528
|
+
} : {
|
|
115529
|
+
outcome: "done",
|
|
115530
|
+
summary: {
|
|
115531
|
+
integrations: integrations.length,
|
|
115532
|
+
created,
|
|
115533
|
+
cleared,
|
|
115534
|
+
kept
|
|
115535
|
+
}
|
|
115536
|
+
};
|
|
115537
|
+
}
|
|
115538
|
+
/**
|
|
115539
|
+
* The addon-side ports of `moveIntegrationRowsToRecords` (planning#416): an addon runs the move
|
|
115540
|
+
* from its own process, so the legacy rows are reached through the hub (`ctx.api.integrations`:
|
|
115541
|
+
* `list`, `getSettings` - which answers the OWNER its secrets resolved - and `clearSettings`) and
|
|
115542
|
+
* the new record through the addon's own records handle. The hub deletes the rows and their
|
|
115543
|
+
* keychain values; the addon never touches the registry.
|
|
115544
|
+
*/
|
|
115545
|
+
function integrationRowsPortsOverApi(deps) {
|
|
115546
|
+
return {
|
|
115547
|
+
warn: (message) => deps.log.warn(message),
|
|
115548
|
+
listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
|
|
115549
|
+
id: i.id,
|
|
115550
|
+
name: i.name
|
|
115551
|
+
})),
|
|
115552
|
+
readRows: (integrationId) => deps.api.getSettings({ id: integrationId }),
|
|
115553
|
+
putRecord: async (id, body) => {
|
|
115554
|
+
if (await deps.records.get(id) !== null) return "exists";
|
|
115555
|
+
try {
|
|
115556
|
+
await deps.records.put({
|
|
115557
|
+
id,
|
|
115558
|
+
data: body
|
|
115559
|
+
}, { ifRev: 0 });
|
|
115560
|
+
} catch (err) {
|
|
115561
|
+
if (err instanceof RevConflictError) return "exists";
|
|
115562
|
+
throw err;
|
|
115563
|
+
}
|
|
115564
|
+
return "created";
|
|
115565
|
+
},
|
|
115566
|
+
readRecord: async (id) => (await deps.records.getResolved(id))?.data ?? null,
|
|
115567
|
+
deleteRecord: async (id) => {
|
|
115568
|
+
await deps.records.delete(id);
|
|
115569
|
+
},
|
|
115570
|
+
clearRows: async (integrationId) => {
|
|
115571
|
+
await deps.api.clearSettings({ id: integrationId });
|
|
115572
|
+
}
|
|
115573
|
+
};
|
|
115574
|
+
}
|
|
114738
115575
|
function ordinalOf(id) {
|
|
114739
115576
|
const digits = id.replace(/\D+/g, "");
|
|
114740
115577
|
return digits.length > 0 ? Number(digits) : Number.POSITIVE_INFINITY;
|
|
@@ -114809,6 +115646,40 @@ async function claimDevicesToDefaultIntegration(ports, spec) {
|
|
|
114809
115646
|
}
|
|
114810
115647
|
};
|
|
114811
115648
|
}
|
|
115649
|
+
/**
|
|
115650
|
+
* The two migrations of a device addon whose integrations hold NO settings of their own
|
|
115651
|
+
* (planning#416, S-A): every integration becomes a record of the addon's own kind with a name and
|
|
115652
|
+
* nothing else (D985, the default integration is an instance with an empty body), then the devices
|
|
115653
|
+
* that carry no `integrationId` are claimed to the addon's default integration.
|
|
115654
|
+
*
|
|
115655
|
+
* 1. `rows-to-record` (block-owner): `moveIntegrationRowsToRecords` over the hub API. Rows an
|
|
115656
|
+
* addon of this shape does not own (a key nothing reads) are named in a warning, by key, and
|
|
115657
|
+
* dropped with the rows once the record is read back.
|
|
115658
|
+
* 2. `claim-devices` (tolerate): `claimDevicesToDefaultIntegration`; with no integration to claim
|
|
115659
|
+
* to it fails and is retried on the next boot.
|
|
115660
|
+
*
|
|
115661
|
+
* Both run in the addon process through the hub API (`ctx.api`), BEFORE the addon registers its
|
|
115662
|
+
* `integration-instances` provider: while a provider is registered the hub answers `getSettings`
|
|
115663
|
+
* from the record, not from the rows.
|
|
115664
|
+
*/
|
|
115665
|
+
function defaultIntegrationPortsOver(deps) {
|
|
115666
|
+
return {
|
|
115667
|
+
...integrationRowsPortsOverApi({
|
|
115668
|
+
api: deps.api,
|
|
115669
|
+
records: deps.records.kind({
|
|
115670
|
+
name: deps.kind.name,
|
|
115671
|
+
declaration: deps.kind.declaration,
|
|
115672
|
+
schema: record(string(), unknown())
|
|
115673
|
+
}),
|
|
115674
|
+
log: { warn: deps.warn }
|
|
115675
|
+
}),
|
|
115676
|
+
...claimDevicesPortsOver({
|
|
115677
|
+
api: deps.api,
|
|
115678
|
+
info: deps.info,
|
|
115679
|
+
warnDevice: deps.warnDevice
|
|
115680
|
+
})
|
|
115681
|
+
};
|
|
115682
|
+
}
|
|
114812
115683
|
/** The hub side of the device claim, over `ctx.api`. */
|
|
114813
115684
|
function claimDevicesPortsOver(deps) {
|
|
114814
115685
|
return {
|
|
@@ -157074,32 +157945,57 @@ var IntegrationInstancePatchSchema = object({
|
|
|
157074
157945
|
/** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
|
|
157075
157946
|
values: record(string(), unknown())
|
|
157076
157947
|
});
|
|
157077
|
-
|
|
157078
|
-
|
|
157079
|
-
|
|
157080
|
-
|
|
157081
|
-
|
|
157082
|
-
|
|
157083
|
-
|
|
157084
|
-
|
|
157085
|
-
|
|
157086
|
-
|
|
157087
|
-
|
|
157088
|
-
|
|
157089
|
-
|
|
157090
|
-
|
|
157091
|
-
|
|
157092
|
-
|
|
157093
|
-
|
|
157094
|
-
|
|
157095
|
-
|
|
157096
|
-
|
|
157097
|
-
|
|
157098
|
-
|
|
157099
|
-
}),
|
|
157100
|
-
|
|
157101
|
-
|
|
157102
|
-
})
|
|
157948
|
+
var integrationInstancesCapability = {
|
|
157949
|
+
name: "integration-instances",
|
|
157950
|
+
scope: "system",
|
|
157951
|
+
mode: "collection",
|
|
157952
|
+
methods: {
|
|
157953
|
+
describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
|
|
157954
|
+
/** The form; `id: null` is the create form. */
|
|
157955
|
+
instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
|
|
157956
|
+
listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
|
|
157957
|
+
getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
|
|
157958
|
+
/**
|
|
157959
|
+
* Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
|
|
157960
|
+
* without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
|
|
157961
|
+
*/
|
|
157962
|
+
createInstance: method(object({
|
|
157963
|
+
id: IntegrationInstanceIdSchema.optional(),
|
|
157964
|
+
name: string().min(1),
|
|
157965
|
+
kind: string().min(1).optional()
|
|
157966
|
+
}), object({ id: string() }), {
|
|
157967
|
+
kind: "mutation",
|
|
157968
|
+
auth: "admin"
|
|
157969
|
+
}),
|
|
157970
|
+
updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
|
|
157971
|
+
kind: "mutation",
|
|
157972
|
+
auth: "admin"
|
|
157973
|
+
}),
|
|
157974
|
+
/**
|
|
157975
|
+
* Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
|
|
157976
|
+
* form patch (D62, D996). An off instance keeps its record and secrets and does no work.
|
|
157977
|
+
*/
|
|
157978
|
+
setInstanceEnabled: method(object({
|
|
157979
|
+
id: IntegrationInstanceIdSchema,
|
|
157980
|
+
enabled: boolean()
|
|
157981
|
+
}), object({ ok: literal(true) }), {
|
|
157982
|
+
kind: "mutation",
|
|
157983
|
+
auth: "admin"
|
|
157984
|
+
}),
|
|
157985
|
+
/** Delete the record and its keychain refs together. */
|
|
157986
|
+
deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
|
|
157987
|
+
kind: "mutation",
|
|
157988
|
+
auth: "admin"
|
|
157989
|
+
}),
|
|
157990
|
+
testInstance: optionalMethod(object({
|
|
157991
|
+
id: IntegrationInstanceIdSchema.nullable(),
|
|
157992
|
+
values: record(string(), unknown())
|
|
157993
|
+
}), TestConnectionResultSchema$1, {
|
|
157994
|
+
kind: "mutation",
|
|
157995
|
+
auth: "admin"
|
|
157996
|
+
})
|
|
157997
|
+
}
|
|
157998
|
+
};
|
|
157103
157999
|
/**
|
|
157104
158000
|
* Two-way audio / talk-back cap. Clients open a WebRTC audio-only
|
|
157105
158001
|
* session (server-initiated offer pattern, identical to the main
|
|
@@ -220350,7 +221246,7 @@ function text(stored, key) {
|
|
|
220350
221246
|
const value = stored[key];
|
|
220351
221247
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
220352
221248
|
}
|
|
220353
|
-
function portOf(stored, ports) {
|
|
221249
|
+
function portOf$1(stored, ports) {
|
|
220354
221250
|
const value = stored["emailPushPort"];
|
|
220355
221251
|
const numeric = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
220356
221252
|
if (Number.isInteger(numeric) && numeric >= 1 && numeric <= 65535) return numeric;
|
|
@@ -220372,7 +221268,7 @@ async function moveEmailPushToMailbox(ports) {
|
|
|
220372
221268
|
};
|
|
220373
221269
|
const bindHost = text(stored, "emailPushBindHost");
|
|
220374
221270
|
await ports.stashPending({
|
|
220375
|
-
port: portOf(stored, ports),
|
|
221271
|
+
port: portOf$1(stored, ports),
|
|
220376
221272
|
...bindHost === null ? {} : { bindHost },
|
|
220377
221273
|
domain: text(stored, "emailPushDomain") ?? DEFAULT_DOMAIN,
|
|
220378
221274
|
requireAuth: stored["emailPushRequireAuth"] !== false,
|
|
@@ -220384,6 +221280,188 @@ async function moveEmailPushToMailbox(ports) {
|
|
|
220384
221280
|
summary: { moved: true }
|
|
220385
221281
|
};
|
|
220386
221282
|
}
|
|
221283
|
+
var reolinkConnectionsEntryKind = defineEntryKind("connections", {
|
|
221284
|
+
title: "Reolink integrations",
|
|
221285
|
+
schema: entryBody({
|
|
221286
|
+
host: string().nullish(),
|
|
221287
|
+
port: number().int().min(1).max(65535).nullish(),
|
|
221288
|
+
rtspPort: number().int().min(1).max(65535).nullish(),
|
|
221289
|
+
username: string().nullish(),
|
|
221290
|
+
password: string().optional().meta({ secret: true }),
|
|
221291
|
+
transport: _enum(["tcp", "udp"]).nullish(),
|
|
221292
|
+
uid: string().nullish(),
|
|
221293
|
+
debugGeneral: boolean().nullish(),
|
|
221294
|
+
debugSocketLogs: array(ReolinkSocketDebugFlagSchema).nullish()
|
|
221295
|
+
}),
|
|
221296
|
+
writeTrigger: "an operator adds, edits or removes a Reolink integration, or a Hub device is migrated into one"
|
|
221297
|
+
});
|
|
221298
|
+
/** Every kind this addon declares, keyed by its name. */
|
|
221299
|
+
var reolinkRecordKinds = { connections: reolinkConnectionsEntryKind };
|
|
221300
|
+
/** The record body as the connection form's flat values: no name, no unset keys. */
|
|
221301
|
+
function hubSettingsOf(body) {
|
|
221302
|
+
const out = {};
|
|
221303
|
+
for (const [key, value] of Object.entries(body)) {
|
|
221304
|
+
if (key === "name" || value === null || value === void 0) continue;
|
|
221305
|
+
out[key] = value;
|
|
221306
|
+
}
|
|
221307
|
+
return out;
|
|
221308
|
+
}
|
|
221309
|
+
/**
|
|
221310
|
+
* What a resolved record says about its hub. A keychain that is locked yields a record WITHOUT
|
|
221311
|
+
* its password: that is `password-locked`, never an empty password (D49).
|
|
221312
|
+
*/
|
|
221313
|
+
function hubConnectionOf(resolved) {
|
|
221314
|
+
const read = readHubConnection(hubSettingsOf(resolved.body));
|
|
221315
|
+
return read.kind === "no-password" && resolved.secrets === "unavailable" ? { kind: "password-locked" } : read;
|
|
221316
|
+
}
|
|
221317
|
+
/**
|
|
221318
|
+
* What each enabled integration's RECORD says about its hub: the one reader of the connection.
|
|
221319
|
+
* A record that is absent reads as `no-host` (no session), exactly as an empty settings blob did.
|
|
221320
|
+
*/
|
|
221321
|
+
async function readHubConnections(instances, integrations) {
|
|
221322
|
+
const out = [];
|
|
221323
|
+
for (const integration of integrations) {
|
|
221324
|
+
const resolved = await instances.readResolved(integration.id);
|
|
221325
|
+
out.push({
|
|
221326
|
+
id: integration.id,
|
|
221327
|
+
name: integration.name,
|
|
221328
|
+
read: resolved === null ? { kind: "no-host" } : hubConnectionOf(resolved)
|
|
221329
|
+
});
|
|
221330
|
+
}
|
|
221331
|
+
return out;
|
|
221332
|
+
}
|
|
221333
|
+
function buildReolinkInstances(records, deps) {
|
|
221334
|
+
return new RecordIntegrationInstances(records, reolinkConnectionsEntryKind, {
|
|
221335
|
+
description: {
|
|
221336
|
+
role: "devices",
|
|
221337
|
+
discovery: {
|
|
221338
|
+
mode: "lan",
|
|
221339
|
+
scopedToInstance: false
|
|
221340
|
+
},
|
|
221341
|
+
instanceMode: "multiple",
|
|
221342
|
+
kind: "connection",
|
|
221343
|
+
canTest: false
|
|
221344
|
+
},
|
|
221345
|
+
formSchema: () => buildHubInstanceSchema(),
|
|
221346
|
+
initialBody: (name) => ({ name }),
|
|
221347
|
+
configured: (body) => (body.host ?? "").length === 0 || (body.password ?? "").length > 0,
|
|
221348
|
+
onChanged: () => deps.onChanged()
|
|
221349
|
+
});
|
|
221350
|
+
}
|
|
221351
|
+
/**
|
|
221352
|
+
* The provider handed to the hub: the default integration (no connection) shows no hub form, an
|
|
221353
|
+
* NVR / Home Hub one does, and a create wizard (no id yet) sets up a hub.
|
|
221354
|
+
*/
|
|
221355
|
+
function reolinkInstancesProvider(instances, deps) {
|
|
221356
|
+
return {
|
|
221357
|
+
...instances.toProvider(),
|
|
221358
|
+
instanceFormSchema: async ({ id }) => {
|
|
221359
|
+
if (id === null) return buildHubInstanceSchema();
|
|
221360
|
+
const [evidence, body] = await Promise.all([deps.registryEvidenceOf(id), instances.getResolved(id)]);
|
|
221361
|
+
return evidence !== null && isHubIntegration({
|
|
221362
|
+
originStableId: evidence.originStableId,
|
|
221363
|
+
info: evidence.info,
|
|
221364
|
+
settings: body === null ? {} : hubSettingsOf(body)
|
|
221365
|
+
}) || body === null ? buildHubInstanceSchema() : { sections: [] };
|
|
221366
|
+
}
|
|
221367
|
+
};
|
|
221368
|
+
}
|
|
221369
|
+
//#endregion
|
|
221370
|
+
//#region src/migrations/integration-rows-to-record.ts
|
|
221371
|
+
/**
|
|
221372
|
+
* Every Reolink integration moves from legacy `integration_settings` rows into its own
|
|
221373
|
+
* `connections` record under the registry id (planning#416, D1007). An NVR / Home Hub integration
|
|
221374
|
+
* carries its connection (host, ports, username, transport, uid, the two debug fields) and its
|
|
221375
|
+
* password, a keychain secret declared by the kind; the default integration carries a name only.
|
|
221376
|
+
* Each integration's record is built from ITS OWN rows, never from another's.
|
|
221377
|
+
*
|
|
221378
|
+
* Keys nothing reads any more are named in a warning, by key, and dropped with the rows once the
|
|
221379
|
+
* record has been read back. A value of the wrong type is left out and named by key as well.
|
|
221380
|
+
*/
|
|
221381
|
+
var REOLINK_ROWS_TO_RECORD_ID = "provider-reolink/2026-11-05-integration-rows-to-record";
|
|
221382
|
+
var TEXT_KEYS = [
|
|
221383
|
+
"host",
|
|
221384
|
+
"username",
|
|
221385
|
+
"uid"
|
|
221386
|
+
];
|
|
221387
|
+
var PORT_KEYS = ["port", "rtspPort"];
|
|
221388
|
+
function portOf(value) {
|
|
221389
|
+
const n = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
|
|
221390
|
+
return typeof n === "number" && Number.isInteger(n) && n >= 1 && n <= 65535 ? n : null;
|
|
221391
|
+
}
|
|
221392
|
+
function textOf(value) {
|
|
221393
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
|
|
221394
|
+
}
|
|
221395
|
+
/** The record body of one integration, from its resolved legacy rows. Never logs a value. */
|
|
221396
|
+
function reolinkBodyFromRows(values, log, integration) {
|
|
221397
|
+
const body = { name: integration.name };
|
|
221398
|
+
const unusable = [];
|
|
221399
|
+
const known = new Set([
|
|
221400
|
+
...TEXT_KEYS,
|
|
221401
|
+
...PORT_KEYS,
|
|
221402
|
+
"password",
|
|
221403
|
+
"transport",
|
|
221404
|
+
"debugGeneral",
|
|
221405
|
+
"debugSocketLogs"
|
|
221406
|
+
]);
|
|
221407
|
+
for (const key of TEXT_KEYS) {
|
|
221408
|
+
if (!(key in values)) continue;
|
|
221409
|
+
const text = textOf(values[key]);
|
|
221410
|
+
if (text === null) unusable.push(key);
|
|
221411
|
+
else body[key] = text;
|
|
221412
|
+
}
|
|
221413
|
+
for (const key of PORT_KEYS) {
|
|
221414
|
+
if (!(key in values)) continue;
|
|
221415
|
+
const port = portOf(values[key]);
|
|
221416
|
+
if (port === null) unusable.push(key);
|
|
221417
|
+
else body[key] = port;
|
|
221418
|
+
}
|
|
221419
|
+
if ("password" in values) {
|
|
221420
|
+
const password = values["password"];
|
|
221421
|
+
if (typeof password === "string" && password.length > 0) body["password"] = password;
|
|
221422
|
+
else unusable.push("password");
|
|
221423
|
+
}
|
|
221424
|
+
if ("transport" in values) {
|
|
221425
|
+
const transport = values["transport"];
|
|
221426
|
+
if (transport === "tcp" || transport === "udp") body["transport"] = transport;
|
|
221427
|
+
else unusable.push("transport");
|
|
221428
|
+
}
|
|
221429
|
+
if ("debugGeneral" in values) {
|
|
221430
|
+
const flag = values["debugGeneral"];
|
|
221431
|
+
if (typeof flag === "boolean") body["debugGeneral"] = flag;
|
|
221432
|
+
else unusable.push("debugGeneral");
|
|
221433
|
+
}
|
|
221434
|
+
if ("debugSocketLogs" in values) {
|
|
221435
|
+
const flags = values["debugSocketLogs"];
|
|
221436
|
+
const parsed = Array.isArray(flags) ? flags.map((f) => ReolinkSocketDebugFlagSchema.safeParse(f)) : null;
|
|
221437
|
+
if (parsed !== null && parsed.every((p) => p.success)) body["debugSocketLogs"] = parsed.flatMap((p) => p.success ? [p.data] : []);
|
|
221438
|
+
else unusable.push("debugSocketLogs");
|
|
221439
|
+
}
|
|
221440
|
+
const unknown = Object.keys(values).filter((key) => !known.has(key));
|
|
221441
|
+
if (unknown.length > 0) log.warn(`integration '${integration.id}': legacy settings nothing reads any more are dropped: ${unknown.join(", ")}`);
|
|
221442
|
+
if (unusable.length > 0) log.warn(`integration '${integration.id}': legacy settings with an unusable value are left out: ${unusable.join(", ")}`);
|
|
221443
|
+
return reolinkConnectionsEntryKind.schema.parse(body);
|
|
221444
|
+
}
|
|
221445
|
+
var REOLINK_ROWS_TO_RECORD_MIGRATION = {
|
|
221446
|
+
meta: {
|
|
221447
|
+
id: REOLINK_ROWS_TO_RECORD_ID,
|
|
221448
|
+
owner: "provider-reolink",
|
|
221449
|
+
level: "addon-state",
|
|
221450
|
+
seq: 4,
|
|
221451
|
+
dependsOn: [],
|
|
221452
|
+
store: "settings",
|
|
221453
|
+
scope: "cluster",
|
|
221454
|
+
onFailure: "block-owner",
|
|
221455
|
+
bodyKind: "procedure",
|
|
221456
|
+
description: "Move every Reolink integration (the default one and each NVR / Home Hub) into its own connection record keyed by its registry id, the password as a keychain secret, then clear its legacy integration settings rows",
|
|
221457
|
+
fixtureMark: "a legacy setting key the migration does not know"
|
|
221458
|
+
},
|
|
221459
|
+
procedure: (ports) => moveIntegrationRowsToRecords(ports, {
|
|
221460
|
+
addonId: "provider-reolink",
|
|
221461
|
+
secretPaths: reolinkConnectionsEntryKind.declaration.secretPaths,
|
|
221462
|
+
bodyFrom: reolinkBodyFromRows
|
|
221463
|
+
})
|
|
221464
|
+
};
|
|
220387
221465
|
/** The `provider-reolink` catalogue (D987): offered to the engine when the addon initialises. */
|
|
220388
221466
|
var REOLINK_MIGRATIONS = [{
|
|
220389
221467
|
meta: {
|
|
@@ -220417,17 +221495,17 @@ var REOLINK_MIGRATIONS = [{
|
|
|
220417
221495
|
plan: planDropWebhookIntakeSettings
|
|
220418
221496
|
}];
|
|
220419
221497
|
/**
|
|
220420
|
-
*
|
|
220421
|
-
*
|
|
220422
|
-
*
|
|
221498
|
+
* The integration wave (D1007) in declaration order, which must follow `seq`: the device claim
|
|
221499
|
+
* shipped first (seq 3) and the rows move follows it (seq 4). The engine refuses the whole
|
|
221500
|
+
* catalogue when a seq does not increase, and the addon then does not start.
|
|
220423
221501
|
*/
|
|
220424
|
-
var
|
|
221502
|
+
var REOLINK_INTEGRATION_MIGRATIONS = [claimDevicesMigration({
|
|
220425
221503
|
addonId: "provider-reolink",
|
|
220426
221504
|
date: "2026-11-04",
|
|
220427
221505
|
seq: 3,
|
|
220428
221506
|
dependsOn: [],
|
|
220429
221507
|
eligible: (integration) => integration.info[MIGRATED_FROM_HUB_KEY] === void 0
|
|
220430
|
-
});
|
|
221508
|
+
}), REOLINK_ROWS_TO_RECORD_MIGRATION];
|
|
220431
221509
|
//#endregion
|
|
220432
221510
|
//#region src/reolink-adoption-provider.ts
|
|
220433
221511
|
var DEVICES_FILTER = {
|
|
@@ -220697,7 +221775,7 @@ var ReolinkHubManager = class {
|
|
|
220697
221775
|
const { logger } = this.#deps;
|
|
220698
221776
|
const wanted = /* @__PURE__ */ new Set();
|
|
220699
221777
|
for (const integration of integrations) {
|
|
220700
|
-
const read =
|
|
221778
|
+
const { read } = integration;
|
|
220701
221779
|
if (read.kind === "no-host") continue;
|
|
220702
221780
|
wanted.add(integration.id);
|
|
220703
221781
|
if (read.kind !== "ok") {
|
|
@@ -221065,6 +222143,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
221065
222143
|
*/
|
|
221066
222144
|
autodetectCache = new AutodetectCache();
|
|
221067
222145
|
hubManager = null;
|
|
222146
|
+
instances = null;
|
|
221068
222147
|
/**
|
|
221069
222148
|
* The camera mailbox bridge (`email-push-bridge.ts`, D999): battery cameras mail motion to a
|
|
221070
222149
|
* mailbox of the `smtp-inbound` broker, the bridge turns each message into the camera's
|
|
@@ -221310,25 +222389,17 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
221310
222389
|
}
|
|
221311
222390
|
/**
|
|
221312
222391
|
* Make the hub sessions match the live integration list: one per enabled integration with a
|
|
221313
|
-
* host. Idempotent; runs at boot, after a migration
|
|
221314
|
-
* integration's
|
|
222392
|
+
* host. Idempotent; runs at boot, after a migration, after an instance change and on every
|
|
222393
|
+
* lifecycle event. When any integration's record cannot be read the whole pass is skipped — an unreadable list must
|
|
221315
222394
|
* never close a healthy session.
|
|
221316
222395
|
*/
|
|
221317
222396
|
async reconcileIntegrations() {
|
|
221318
222397
|
const manager = this.hubManager;
|
|
221319
|
-
|
|
222398
|
+
const instances = this.instances;
|
|
222399
|
+
if (manager === null || instances === null) return;
|
|
221320
222400
|
try {
|
|
221321
|
-
const
|
|
221322
|
-
|
|
221323
|
-
for (const integration of mine) {
|
|
221324
|
-
const settings = await this.ctx.api.integrations.getSettings.query({ id: integration.id });
|
|
221325
|
-
withSettings.push({
|
|
221326
|
-
id: integration.id,
|
|
221327
|
-
name: integration.name,
|
|
221328
|
-
settings
|
|
221329
|
-
});
|
|
221330
|
-
}
|
|
221331
|
-
await manager.reconcile(withSettings);
|
|
222401
|
+
const withConnection = await readHubConnections(instances, (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id && i.enabled));
|
|
222402
|
+
await manager.reconcile(withConnection);
|
|
221332
222403
|
} catch (err) {
|
|
221333
222404
|
this.ctx.logger.warn("Reolink integration reconcile failed — sessions left as they were", { meta: { error: errMsg$3(err) } });
|
|
221334
222405
|
}
|
|
@@ -221501,28 +222572,6 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
221501
222572
|
globalSettingsSchema() {
|
|
221502
222573
|
return { sections: [...buildClipRefreshSettingsSchema().sections] };
|
|
221503
222574
|
}
|
|
221504
|
-
/**
|
|
221505
|
-
* Per-instance form (D985): an NVR / Home Hub integration shows its connection only; the
|
|
221506
|
-
* default integration keeps the full schema (`null`). A create wizard has no id yet and is
|
|
221507
|
-
* always setting up a NEW instance — the default one already exists — so it gets the hub form.
|
|
221508
|
-
*/
|
|
221509
|
-
async integrationSettingsSchema(instance) {
|
|
221510
|
-
if (instance.integrationId === null) return buildHubInstanceSchema();
|
|
221511
|
-
try {
|
|
221512
|
-
const [integration, settings] = await Promise.all([this.ctx.api.integrations.get.query({ id: instance.integrationId }), this.ctx.api.integrations.getSettings.query({ id: instance.integrationId })]);
|
|
221513
|
-
return isHubIntegration({
|
|
221514
|
-
originStableId: integration.originStableId,
|
|
221515
|
-
info: integration.info,
|
|
221516
|
-
settings
|
|
221517
|
-
}) ? buildHubInstanceSchema() : null;
|
|
221518
|
-
} catch (err) {
|
|
221519
|
-
this.ctx.logger.warn("Reolink integration form: instance lookup failed — full schema shown", { meta: {
|
|
221520
|
-
integrationId: instance.integrationId,
|
|
221521
|
-
error: errMsg$3(err)
|
|
221522
|
-
} });
|
|
221523
|
-
return null;
|
|
221524
|
-
}
|
|
221525
|
-
}
|
|
221526
222575
|
async onInitialize() {
|
|
221527
222576
|
const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
|
|
221528
222577
|
this.logChannels = createLogChannelsProvider(this.ctx.logger.child("log-channels"));
|
|
@@ -221567,11 +222616,38 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
221567
222616
|
this.wireClipCatalogRefreshers();
|
|
221568
222617
|
await this.serveClipPlanes(this.clipService);
|
|
221569
222618
|
await this.serveWebhookPlane();
|
|
221570
|
-
|
|
222619
|
+
const records = this.ctx.records;
|
|
222620
|
+
if (records === void 0) throw new Error("provider-reolink: ctx.records is required (hub-only addon)");
|
|
222621
|
+
await records.declare(reolinkRecordKinds);
|
|
222622
|
+
const integrationPorts = defaultIntegrationPortsOver({
|
|
221571
222623
|
api: defaultIntegrationApiOver(this.ctx.api),
|
|
222624
|
+
records,
|
|
222625
|
+
kind: reolinkConnectionsEntryKind,
|
|
221572
222626
|
info: (message) => this.ctx.logger.info(message),
|
|
222627
|
+
warn: (message) => this.ctx.logger.warn(message),
|
|
221573
222628
|
warnDevice: (message, deviceId) => this.ctx.logger.warn(message, { tags: { deviceId } })
|
|
221574
|
-
})
|
|
222629
|
+
});
|
|
222630
|
+
const registryEvidenceOf = async (id) => {
|
|
222631
|
+
try {
|
|
222632
|
+
const integration = await this.ctx.api.integrations.get.query({ id });
|
|
222633
|
+
return {
|
|
222634
|
+
originStableId: integration.originStableId,
|
|
222635
|
+
info: { ...integration.info }
|
|
222636
|
+
};
|
|
222637
|
+
} catch (err) {
|
|
222638
|
+
this.ctx.logger.warn("Reolink integration form: instance lookup failed — hub form shown", { meta: {
|
|
222639
|
+
integrationId: id,
|
|
222640
|
+
error: errMsg$3(err)
|
|
222641
|
+
} });
|
|
222642
|
+
return null;
|
|
222643
|
+
}
|
|
222644
|
+
};
|
|
222645
|
+
const instances = buildReolinkInstances(records, {
|
|
222646
|
+
onChanged: () => this.reconcileIntegrations(),
|
|
222647
|
+
registryEvidenceOf
|
|
222648
|
+
});
|
|
222649
|
+
this.instances = instances;
|
|
222650
|
+
await this.runMigrations([...defineAddonMigrations(REOLINK_MIGRATIONS, this.migrationPorts()), ...defineAddonMigrations(REOLINK_INTEGRATION_MIGRATIONS, integrationPorts)]);
|
|
221575
222651
|
this.wireEmailBridge();
|
|
221576
222652
|
const wireSleepPushResolvers = (event, via) => {
|
|
221577
222653
|
const data = event.data;
|
|
@@ -221610,7 +222686,11 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
221610
222686
|
capability: deviceRemovalHookCapability,
|
|
221611
222687
|
provider: createEmailPushRemovalHook({ resync: () => this.ensureEmailBridge().sync() })
|
|
221612
222688
|
});
|
|
221613
|
-
|
|
222689
|
+
regs.push({
|
|
222690
|
+
capability: integrationInstancesCapability,
|
|
222691
|
+
provider: reolinkInstancesProvider(instances, { registryEvidenceOf })
|
|
222692
|
+
});
|
|
222693
|
+
return { providers: regs };
|
|
221614
222694
|
}
|
|
221615
222695
|
/** What the catalogue's migration reads and writes (D987). */
|
|
221616
222696
|
migrationPorts() {
|