@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.mjs
CHANGED
|
@@ -105246,6 +105246,12 @@ var numberProcessor = (schema, ctx, _json, _params) => {
|
|
|
105246
105246
|
var booleanProcessor = (_schema, _ctx, json, _params) => {
|
|
105247
105247
|
json.type = "boolean";
|
|
105248
105248
|
};
|
|
105249
|
+
var bigintProcessor = (_schema, ctx, _json, _params) => {
|
|
105250
|
+
if (ctx.unrepresentable === "throw") throw new Error("BigInt cannot be represented in JSON Schema");
|
|
105251
|
+
};
|
|
105252
|
+
var symbolProcessor = (_schema, ctx, _json, _params) => {
|
|
105253
|
+
if (ctx.unrepresentable === "throw") throw new Error("Symbols cannot be represented in JSON Schema");
|
|
105254
|
+
};
|
|
105249
105255
|
var nullProcessor = (_schema, ctx, json, _params) => {
|
|
105250
105256
|
if (ctx.target === "openapi-3.0") {
|
|
105251
105257
|
json.type = "string";
|
|
@@ -105253,12 +105259,17 @@ var nullProcessor = (_schema, ctx, json, _params) => {
|
|
|
105253
105259
|
json.enum = [null];
|
|
105254
105260
|
} else json.type = "null";
|
|
105255
105261
|
};
|
|
105262
|
+
var undefinedProcessor = (_schema, ctx, _json, _params) => {
|
|
105263
|
+
if (ctx.unrepresentable === "throw") throw new Error("Undefined cannot be represented in JSON Schema");
|
|
105264
|
+
};
|
|
105256
105265
|
var voidProcessor = (_schema, ctx, _json, _params) => {
|
|
105257
105266
|
if (ctx.unrepresentable === "throw") throw new Error("Void cannot be represented in JSON Schema");
|
|
105258
105267
|
};
|
|
105259
105268
|
var neverProcessor = (_schema, _ctx, json, _params) => {
|
|
105260
105269
|
json.not = {};
|
|
105261
105270
|
};
|
|
105271
|
+
var anyProcessor = (_schema, _ctx, _json, _params) => {};
|
|
105272
|
+
var unknownProcessor = (_schema, _ctx, _json, _params) => {};
|
|
105262
105273
|
var dateProcessor = (_schema, ctx, _json, _params) => {
|
|
105263
105274
|
if (ctx.unrepresentable === "throw") throw new Error("Date cannot be represented in JSON Schema");
|
|
105264
105275
|
};
|
|
@@ -105290,12 +105301,53 @@ var literalProcessor = (schema, ctx, json, _params) => {
|
|
|
105290
105301
|
json.enum = vals;
|
|
105291
105302
|
}
|
|
105292
105303
|
};
|
|
105304
|
+
var nanProcessor = (_schema, ctx, _json, _params) => {
|
|
105305
|
+
if (ctx.unrepresentable === "throw") throw new Error("NaN cannot be represented in JSON Schema");
|
|
105306
|
+
};
|
|
105307
|
+
var templateLiteralProcessor = (schema, _ctx, json, _params) => {
|
|
105308
|
+
const _json = json;
|
|
105309
|
+
const pattern = schema._zod.pattern;
|
|
105310
|
+
if (!pattern) throw new Error("Pattern not found in template literal");
|
|
105311
|
+
_json.type = "string";
|
|
105312
|
+
_json.pattern = pattern.source;
|
|
105313
|
+
};
|
|
105314
|
+
var fileProcessor = (schema, _ctx, json, _params) => {
|
|
105315
|
+
const _json = json;
|
|
105316
|
+
const file = {
|
|
105317
|
+
type: "string",
|
|
105318
|
+
format: "binary",
|
|
105319
|
+
contentEncoding: "binary"
|
|
105320
|
+
};
|
|
105321
|
+
const { minimum, maximum, mime } = schema._zod.bag;
|
|
105322
|
+
if (minimum !== void 0) file.minLength = minimum;
|
|
105323
|
+
if (maximum !== void 0) file.maxLength = maximum;
|
|
105324
|
+
if (mime) if (mime.length === 1) {
|
|
105325
|
+
file.contentMediaType = mime[0];
|
|
105326
|
+
Object.assign(_json, file);
|
|
105327
|
+
} else {
|
|
105328
|
+
Object.assign(_json, file);
|
|
105329
|
+
_json.anyOf = mime.map((m) => ({ contentMediaType: m }));
|
|
105330
|
+
}
|
|
105331
|
+
else Object.assign(_json, file);
|
|
105332
|
+
};
|
|
105333
|
+
var successProcessor = (_schema, _ctx, json, _params) => {
|
|
105334
|
+
json.type = "boolean";
|
|
105335
|
+
};
|
|
105293
105336
|
var customProcessor = (_schema, ctx, _json, _params) => {
|
|
105294
105337
|
if (ctx.unrepresentable === "throw") throw new Error("Custom types cannot be represented in JSON Schema");
|
|
105295
105338
|
};
|
|
105339
|
+
var functionProcessor = (_schema, ctx, _json, _params) => {
|
|
105340
|
+
if (ctx.unrepresentable === "throw") throw new Error("Function types cannot be represented in JSON Schema");
|
|
105341
|
+
};
|
|
105296
105342
|
var transformProcessor = (_schema, ctx, _json, _params) => {
|
|
105297
105343
|
if (ctx.unrepresentable === "throw") throw new Error("Transforms cannot be represented in JSON Schema");
|
|
105298
105344
|
};
|
|
105345
|
+
var mapProcessor = (_schema, ctx, _json, _params) => {
|
|
105346
|
+
if (ctx.unrepresentable === "throw") throw new Error("Map cannot be represented in JSON Schema");
|
|
105347
|
+
};
|
|
105348
|
+
var setProcessor = (_schema, ctx, _json, _params) => {
|
|
105349
|
+
if (ctx.unrepresentable === "throw") throw new Error("Set cannot be represented in JSON Schema");
|
|
105350
|
+
};
|
|
105299
105351
|
var arrayProcessor = (schema, ctx, _json, params) => {
|
|
105300
105352
|
const json = _json;
|
|
105301
105353
|
const def = schema._zod.def;
|
|
@@ -105500,6 +105552,12 @@ var readonlyProcessor = (schema, ctx, json, params) => {
|
|
|
105500
105552
|
seen.ref = def.innerType;
|
|
105501
105553
|
json.readOnly = true;
|
|
105502
105554
|
};
|
|
105555
|
+
var promiseProcessor = (schema, ctx, _json, params) => {
|
|
105556
|
+
const def = schema._zod.def;
|
|
105557
|
+
process$1(def.innerType, ctx, params);
|
|
105558
|
+
const seen = ctx.seen.get(schema);
|
|
105559
|
+
seen.ref = def.innerType;
|
|
105560
|
+
};
|
|
105503
105561
|
var optionalProcessor = (schema, ctx, _json, params) => {
|
|
105504
105562
|
const def = schema._zod.def;
|
|
105505
105563
|
process$1(def.innerType, ctx, params);
|
|
@@ -105512,6 +105570,81 @@ var lazyProcessor = (schema, ctx, _json, params) => {
|
|
|
105512
105570
|
const seen = ctx.seen.get(schema);
|
|
105513
105571
|
seen.ref = innerType;
|
|
105514
105572
|
};
|
|
105573
|
+
var allProcessors = {
|
|
105574
|
+
string: stringProcessor,
|
|
105575
|
+
number: numberProcessor,
|
|
105576
|
+
boolean: booleanProcessor,
|
|
105577
|
+
bigint: bigintProcessor,
|
|
105578
|
+
symbol: symbolProcessor,
|
|
105579
|
+
null: nullProcessor,
|
|
105580
|
+
undefined: undefinedProcessor,
|
|
105581
|
+
void: voidProcessor,
|
|
105582
|
+
never: neverProcessor,
|
|
105583
|
+
any: anyProcessor,
|
|
105584
|
+
unknown: unknownProcessor,
|
|
105585
|
+
date: dateProcessor,
|
|
105586
|
+
enum: enumProcessor,
|
|
105587
|
+
literal: literalProcessor,
|
|
105588
|
+
nan: nanProcessor,
|
|
105589
|
+
template_literal: templateLiteralProcessor,
|
|
105590
|
+
file: fileProcessor,
|
|
105591
|
+
success: successProcessor,
|
|
105592
|
+
custom: customProcessor,
|
|
105593
|
+
function: functionProcessor,
|
|
105594
|
+
transform: transformProcessor,
|
|
105595
|
+
map: mapProcessor,
|
|
105596
|
+
set: setProcessor,
|
|
105597
|
+
array: arrayProcessor,
|
|
105598
|
+
object: objectProcessor,
|
|
105599
|
+
union: unionProcessor,
|
|
105600
|
+
intersection: intersectionProcessor,
|
|
105601
|
+
tuple: tupleProcessor,
|
|
105602
|
+
record: recordProcessor,
|
|
105603
|
+
nullable: nullableProcessor,
|
|
105604
|
+
nonoptional: nonoptionalProcessor,
|
|
105605
|
+
default: defaultProcessor,
|
|
105606
|
+
prefault: prefaultProcessor,
|
|
105607
|
+
catch: catchProcessor,
|
|
105608
|
+
pipe: pipeProcessor,
|
|
105609
|
+
readonly: readonlyProcessor,
|
|
105610
|
+
promise: promiseProcessor,
|
|
105611
|
+
optional: optionalProcessor,
|
|
105612
|
+
lazy: lazyProcessor
|
|
105613
|
+
};
|
|
105614
|
+
function toJSONSchema(input, params) {
|
|
105615
|
+
if ("_idmap" in input) {
|
|
105616
|
+
const registry = input;
|
|
105617
|
+
const ctx = initializeContext({
|
|
105618
|
+
...params,
|
|
105619
|
+
processors: allProcessors
|
|
105620
|
+
});
|
|
105621
|
+
const defs = {};
|
|
105622
|
+
for (const entry of registry._idmap.entries()) {
|
|
105623
|
+
const [_, schema] = entry;
|
|
105624
|
+
process$1(schema, ctx);
|
|
105625
|
+
}
|
|
105626
|
+
const schemas = {};
|
|
105627
|
+
ctx.external = {
|
|
105628
|
+
registry,
|
|
105629
|
+
uri: params?.uri,
|
|
105630
|
+
defs
|
|
105631
|
+
};
|
|
105632
|
+
for (const entry of registry._idmap.entries()) {
|
|
105633
|
+
const [key, schema] = entry;
|
|
105634
|
+
extractDefs(ctx, schema);
|
|
105635
|
+
schemas[key] = finalize(ctx, schema);
|
|
105636
|
+
}
|
|
105637
|
+
if (Object.keys(defs).length > 0) schemas.__shared = { [ctx.target === "draft-2020-12" ? "$defs" : "definitions"]: defs };
|
|
105638
|
+
return { schemas };
|
|
105639
|
+
}
|
|
105640
|
+
const ctx = initializeContext({
|
|
105641
|
+
...params,
|
|
105642
|
+
processors: allProcessors
|
|
105643
|
+
});
|
|
105644
|
+
process$1(input, ctx);
|
|
105645
|
+
extractDefs(ctx, input);
|
|
105646
|
+
return finalize(ctx, input);
|
|
105647
|
+
}
|
|
105515
105648
|
//#endregion
|
|
105516
105649
|
//#region ../../node_modules/zod/v4/classic/iso.js
|
|
105517
105650
|
var ZodISODateTime = /*@__PURE__*/ $constructor("ZodISODateTime", (inst, def) => {
|
|
@@ -106536,7 +106669,7 @@ var ZodIssueCode = {
|
|
|
106536
106669
|
var ZodFirstPartyTypeKind;
|
|
106537
106670
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
106538
106671
|
//#endregion
|
|
106539
|
-
//#region ../types/dist/sleep-
|
|
106672
|
+
//#region ../types/dist/sleep-D1jjWAr8.mjs
|
|
106540
106673
|
/**
|
|
106541
106674
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
106542
106675
|
* window to float samples (D455).
|
|
@@ -106691,6 +106824,9 @@ function isSecretMarkerLike(value) {
|
|
|
106691
106824
|
function isSecretClear(value) {
|
|
106692
106825
|
return isRecord$7(value) && value["$secretClear"] === true;
|
|
106693
106826
|
}
|
|
106827
|
+
function isSecretFieldView(value) {
|
|
106828
|
+
return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
|
|
106829
|
+
}
|
|
106694
106830
|
/**
|
|
106695
106831
|
* The UI view of a stored secret field value: a marker becomes "set", inline
|
|
106696
106832
|
* plaintext becomes "set (inline)", nothing becomes `null`. A value never
|
|
@@ -107342,11 +107478,36 @@ function projectStore(store, perNodeKeys, nodeId) {
|
|
|
107342
107478
|
}
|
|
107343
107479
|
return out;
|
|
107344
107480
|
}
|
|
107481
|
+
/**
|
|
107482
|
+
* The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
|
|
107483
|
+
*
|
|
107484
|
+
* A ref is DETERMINISTIC from where the value lives — the owner addon, the
|
|
107485
|
+
* scope (global, one node, one device, one integration, one row of a business
|
|
107486
|
+
* collection) and the field path inside that scope — so saving the same field
|
|
107487
|
+
* twice updates the same ref, and the ref itself says which settings blob
|
|
107488
|
+
* points at it. It is also the AAD of the value's ciphertext: a ciphertext
|
|
107489
|
+
* copied onto another ref does not decrypt.
|
|
107490
|
+
*
|
|
107491
|
+
* Array items are addressed by ITEM ID, never by index
|
|
107492
|
+
* (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
|
|
107493
|
+
*
|
|
107494
|
+
* Pure and browser-safe: no node builtins, no I/O.
|
|
107495
|
+
*/
|
|
107496
|
+
/** The URI scheme of every keychain ref. */
|
|
107497
|
+
var SECRET_REF_SCHEME = "secret://";
|
|
107345
107498
|
var GLOBAL_SCOPE = { kind: "global" };
|
|
107346
107499
|
/** The scope every addon-wide (not per-node, not per-device) setting lives in. */
|
|
107347
107500
|
function globalSecretScope() {
|
|
107348
107501
|
return GLOBAL_SCOPE;
|
|
107349
107502
|
}
|
|
107503
|
+
/**
|
|
107504
|
+
* An addon id as a ref owner. A hub in-process builtin's context id is
|
|
107505
|
+
* `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
|
|
107506
|
+
* addon running forked.
|
|
107507
|
+
*/
|
|
107508
|
+
function normalizeSecretOwner(addonId) {
|
|
107509
|
+
return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
|
|
107510
|
+
}
|
|
107350
107511
|
function encodeSegment(value) {
|
|
107351
107512
|
return encodeURIComponent(value);
|
|
107352
107513
|
}
|
|
@@ -107357,6 +107518,16 @@ function decodeSegment(value) {
|
|
|
107357
107518
|
return null;
|
|
107358
107519
|
}
|
|
107359
107520
|
}
|
|
107521
|
+
/** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
|
|
107522
|
+
function formatSecretScope(scope) {
|
|
107523
|
+
switch (scope.kind) {
|
|
107524
|
+
case "global": return "global";
|
|
107525
|
+
case "node": return `node/${encodeSegment(scope.nodeId)}`;
|
|
107526
|
+
case "device": return `device/${scope.deviceId}`;
|
|
107527
|
+
case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
|
|
107528
|
+
case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
|
|
107529
|
+
}
|
|
107530
|
+
}
|
|
107360
107531
|
/**
|
|
107361
107532
|
* The field path of a value inside an array item, addressed by the item's id:
|
|
107362
107533
|
* `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
|
|
@@ -107365,6 +107536,13 @@ function decodeSegment(value) {
|
|
|
107365
107536
|
function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
|
|
107366
107537
|
return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
|
|
107367
107538
|
}
|
|
107539
|
+
/** Build a ref. Throws on an empty owner or field path — both are bugs. */
|
|
107540
|
+
function buildSecretRef(parts) {
|
|
107541
|
+
const owner = normalizeSecretOwner(parts.owner);
|
|
107542
|
+
if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
|
|
107543
|
+
if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
|
|
107544
|
+
return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
|
|
107545
|
+
}
|
|
107368
107546
|
function parseScope(segments) {
|
|
107369
107547
|
switch (segments[0]) {
|
|
107370
107548
|
case "global": return {
|
|
@@ -109277,6 +109455,45 @@ function normalizeAddonInitResult(result) {
|
|
|
109277
109455
|
if (Array.isArray(result)) return { providers: result };
|
|
109278
109456
|
return result;
|
|
109279
109457
|
}
|
|
109458
|
+
var PREFIX = "records/";
|
|
109459
|
+
/** The base of every records failure. `detail` is the human part of the message. */
|
|
109460
|
+
var RecordsError = class extends Error {
|
|
109461
|
+
code;
|
|
109462
|
+
detail;
|
|
109463
|
+
constructor(code, detail) {
|
|
109464
|
+
super(`${PREFIX}${code}: ${detail}`);
|
|
109465
|
+
this.name = "RecordsError";
|
|
109466
|
+
this.code = code;
|
|
109467
|
+
this.detail = detail;
|
|
109468
|
+
}
|
|
109469
|
+
};
|
|
109470
|
+
/** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
|
|
109471
|
+
var RevConflictError = class extends RecordsError {
|
|
109472
|
+
constructor(detail) {
|
|
109473
|
+
super("rev-conflict", detail);
|
|
109474
|
+
this.name = "RevConflictError";
|
|
109475
|
+
}
|
|
109476
|
+
};
|
|
109477
|
+
/** The keychain could not store or serve a value: explicit, never an empty string. */
|
|
109478
|
+
var RecordSecretUnavailableError = class extends RecordsError {
|
|
109479
|
+
constructor(detail) {
|
|
109480
|
+
super("secret-unavailable", detail);
|
|
109481
|
+
this.name = "RecordSecretUnavailableError";
|
|
109482
|
+
}
|
|
109483
|
+
};
|
|
109484
|
+
var InvalidRecordError = class extends RecordsError {
|
|
109485
|
+
constructor(detail) {
|
|
109486
|
+
super("invalid-record", detail);
|
|
109487
|
+
this.name = "InvalidRecordError";
|
|
109488
|
+
}
|
|
109489
|
+
};
|
|
109490
|
+
/** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
|
|
109491
|
+
var DeclarationRefusedError = class extends RecordsError {
|
|
109492
|
+
constructor(detail) {
|
|
109493
|
+
super("declaration-refused", detail);
|
|
109494
|
+
this.name = "DeclarationRefusedError";
|
|
109495
|
+
}
|
|
109496
|
+
};
|
|
109280
109497
|
/** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
|
|
109281
109498
|
/** The files a kind may live in. `history` is device-history's (D850), never records. */
|
|
109282
109499
|
var RECORD_DATABASES = ["settings", "events"];
|
|
@@ -109292,6 +109509,183 @@ var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
|
|
|
109292
109509
|
var RECORD_MAX_DATA_BYTES = 1024 * 1024;
|
|
109293
109510
|
var RECORD_BATCH_MAX = 1e3;
|
|
109294
109511
|
var RECORD_QUERY_MAX = 1e3;
|
|
109512
|
+
/** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
|
|
109513
|
+
var RECORD_SETTINGS_MAX_ROWS = 5e4;
|
|
109514
|
+
/** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
|
|
109515
|
+
var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
109516
|
+
/**
|
|
109517
|
+
* Secret fields of a record, addressed the way a Zod schema declares them
|
|
109518
|
+
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
109519
|
+
* (`auth.accessToken`, `headers[].value`).
|
|
109520
|
+
*
|
|
109521
|
+
* `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
|
|
109522
|
+
* names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
|
|
109523
|
+
* key releases exactly that key's secret.
|
|
109524
|
+
*
|
|
109525
|
+
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
109526
|
+
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
109527
|
+
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
109528
|
+
* leaf names its element: `headers[X-Api].value`.
|
|
109529
|
+
*
|
|
109530
|
+
* Pure and browser-safe. Nothing here mutates: every transform returns a new value.
|
|
109531
|
+
*/
|
|
109532
|
+
/** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
|
|
109533
|
+
function parseSecretPath(path) {
|
|
109534
|
+
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
109535
|
+
return path.split(".").map((raw) => {
|
|
109536
|
+
const each = raw.endsWith("[]");
|
|
109537
|
+
const map = raw.endsWith("{}");
|
|
109538
|
+
const key = each || map ? raw.slice(0, -2) : raw;
|
|
109539
|
+
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
109540
|
+
return {
|
|
109541
|
+
key,
|
|
109542
|
+
each,
|
|
109543
|
+
map
|
|
109544
|
+
};
|
|
109545
|
+
});
|
|
109546
|
+
}
|
|
109547
|
+
/** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
|
|
109548
|
+
function secretArrayPrefixes(path) {
|
|
109549
|
+
const segments = parseSecretPath(path);
|
|
109550
|
+
const prefixes = [];
|
|
109551
|
+
const walked = [];
|
|
109552
|
+
for (const segment of segments) {
|
|
109553
|
+
walked.push(segment.key);
|
|
109554
|
+
if (segment.each) prefixes.push(walked.join("."));
|
|
109555
|
+
}
|
|
109556
|
+
return prefixes;
|
|
109557
|
+
}
|
|
109558
|
+
function isRecord$4(value) {
|
|
109559
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
109560
|
+
}
|
|
109561
|
+
function elementId(element, idKey, declared, prefix) {
|
|
109562
|
+
const id = isRecord$4(element) ? element[idKey] : void 0;
|
|
109563
|
+
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`);
|
|
109564
|
+
return id;
|
|
109565
|
+
}
|
|
109566
|
+
/**
|
|
109567
|
+
* Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
|
|
109568
|
+
* visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
|
|
109569
|
+
* result is a new body, `data` is untouched.
|
|
109570
|
+
*/
|
|
109571
|
+
var REMOVE_LEAF = Symbol("remove-leaf");
|
|
109572
|
+
function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
|
|
109573
|
+
if (!isRecord$4(node)) return node;
|
|
109574
|
+
const segment = segments[index];
|
|
109575
|
+
if (segment === void 0) return node;
|
|
109576
|
+
if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
|
|
109577
|
+
const child = node[segment.key];
|
|
109578
|
+
const keys = [...walkedKeys, segment.key];
|
|
109579
|
+
const last = index === segments.length - 1;
|
|
109580
|
+
if (segment.each) {
|
|
109581
|
+
if (!Array.isArray(child)) return node;
|
|
109582
|
+
const prefix = keys.join(".");
|
|
109583
|
+
const idKey = arrayKeys[prefix];
|
|
109584
|
+
if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
|
|
109585
|
+
const mapped = child.map((element) => {
|
|
109586
|
+
const id = elementId(element, idKey, declared, prefix);
|
|
109587
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
|
|
109588
|
+
return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
|
|
109589
|
+
});
|
|
109590
|
+
return {
|
|
109591
|
+
...node,
|
|
109592
|
+
[segment.key]: mapped
|
|
109593
|
+
};
|
|
109594
|
+
}
|
|
109595
|
+
if (segment.map) {
|
|
109596
|
+
if (!isRecord$4(child)) return node;
|
|
109597
|
+
const kept = Object.entries(child).map(([mapKey, value]) => {
|
|
109598
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
|
|
109599
|
+
if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
|
|
109600
|
+
return [mapKey, visit({
|
|
109601
|
+
declared,
|
|
109602
|
+
concrete,
|
|
109603
|
+
value
|
|
109604
|
+
})];
|
|
109605
|
+
}).filter(([, value]) => value !== REMOVE_LEAF);
|
|
109606
|
+
return {
|
|
109607
|
+
...node,
|
|
109608
|
+
[segment.key]: Object.fromEntries(kept)
|
|
109609
|
+
};
|
|
109610
|
+
}
|
|
109611
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
109612
|
+
if (last) {
|
|
109613
|
+
const replacement = visit({
|
|
109614
|
+
declared,
|
|
109615
|
+
concrete,
|
|
109616
|
+
value: child
|
|
109617
|
+
});
|
|
109618
|
+
if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
|
|
109619
|
+
return {
|
|
109620
|
+
...node,
|
|
109621
|
+
[segment.key]: replacement
|
|
109622
|
+
};
|
|
109623
|
+
}
|
|
109624
|
+
return {
|
|
109625
|
+
...node,
|
|
109626
|
+
[segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
|
|
109627
|
+
};
|
|
109628
|
+
}
|
|
109629
|
+
/** Replace/remove every present secret leaf; returns a NEW body. */
|
|
109630
|
+
function mapSecretLeaves(data, paths, arrayKeys, visit) {
|
|
109631
|
+
let current = data;
|
|
109632
|
+
for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
|
|
109633
|
+
return current;
|
|
109634
|
+
}
|
|
109635
|
+
/** Every present secret leaf, read-only. */
|
|
109636
|
+
function collectSecretLeaves(data, paths, arrayKeys) {
|
|
109637
|
+
const found = [];
|
|
109638
|
+
mapSecretLeaves(data, paths, arrayKeys, (leaf) => {
|
|
109639
|
+
found.push(leaf);
|
|
109640
|
+
return leaf.value;
|
|
109641
|
+
});
|
|
109642
|
+
return found;
|
|
109643
|
+
}
|
|
109644
|
+
/**
|
|
109645
|
+
* The keychain collection name a kind's secrets live under. Prefixed so a
|
|
109646
|
+
* record's refs never collide with the refs a legacy collection of the same name
|
|
109647
|
+
* already holds during an adoption.
|
|
109648
|
+
*/
|
|
109649
|
+
function recordSecretCollection(kind) {
|
|
109650
|
+
return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
|
|
109651
|
+
}
|
|
109652
|
+
/** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
|
|
109653
|
+
function recordSecretRef(owner, kind, rowId, concretePath) {
|
|
109654
|
+
return buildSecretRef({
|
|
109655
|
+
owner,
|
|
109656
|
+
scope: {
|
|
109657
|
+
kind: "row",
|
|
109658
|
+
collection: recordSecretCollection(kind),
|
|
109659
|
+
rowId
|
|
109660
|
+
},
|
|
109661
|
+
fieldPath: concretePath
|
|
109662
|
+
});
|
|
109663
|
+
}
|
|
109664
|
+
/**
|
|
109665
|
+
* A stored secret, as an admin read shows it: a VIEW naming the row's own
|
|
109666
|
+
* keychain ref (`set: true`), never the value and no longer the bare redaction
|
|
109667
|
+
* token. The form hands it to the secret field, which reveals the value by ref
|
|
109668
|
+
* through the audited `keychain-admin.reveal`; sent back untouched it keeps the
|
|
109669
|
+
* stored secret (the records door reads a view as KEEP).
|
|
109670
|
+
*
|
|
109671
|
+
* ONE implementation for the entries service and for every addon that serves
|
|
109672
|
+
* its own instances (`RecordIntegrationInstances`).
|
|
109673
|
+
*/
|
|
109674
|
+
function isRecord$3(value) {
|
|
109675
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
109676
|
+
}
|
|
109677
|
+
function secretViewsOf(owner, kind, id, decl, data) {
|
|
109678
|
+
if (decl.secretPaths.length === 0) return data;
|
|
109679
|
+
const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
|
|
109680
|
+
if (leaf.value !== "__camstack_redacted__") return leaf.value;
|
|
109681
|
+
return {
|
|
109682
|
+
$secret: recordSecretRef(owner, kind, id, leaf.concrete),
|
|
109683
|
+
v: 1,
|
|
109684
|
+
set: true
|
|
109685
|
+
};
|
|
109686
|
+
});
|
|
109687
|
+
return isRecord$3(shown) ? shown : data;
|
|
109688
|
+
}
|
|
109295
109689
|
/**
|
|
109296
109690
|
* The share-view scope kinds a share-link token can carry. The hub mints
|
|
109297
109691
|
* and validates them (`ShareViewGrantSchema` builds its
|
|
@@ -109451,6 +109845,126 @@ var RecordKindDeclarationSchema = object({
|
|
|
109451
109845
|
}).strict();
|
|
109452
109846
|
/** The kind names: lower-case, dash separated, short enough for an index name. */
|
|
109453
109847
|
var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
|
|
109848
|
+
/** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
|
|
109849
|
+
function assertValidDeclaration(name, decl) {
|
|
109850
|
+
const refuse = (why) => {
|
|
109851
|
+
throw new DeclarationRefusedError(`kind "${name}": ${why}`);
|
|
109852
|
+
};
|
|
109853
|
+
if (!RecordKindNameSchema.safeParse(name).success) refuse("the name must be lower-case-dashed");
|
|
109854
|
+
const seen = /* @__PURE__ */ new Set();
|
|
109855
|
+
for (const index of decl.indexes) {
|
|
109856
|
+
if (seen.has(index.name)) refuse(`index name "${index.name}" is declared twice`);
|
|
109857
|
+
seen.add(index.name);
|
|
109858
|
+
}
|
|
109859
|
+
const { retention } = decl;
|
|
109860
|
+
const hasSecrets = decl.secretPaths.length > 0;
|
|
109861
|
+
if ("ttlMs" in retention) {
|
|
109862
|
+
if (retention.from === "at" && decl.at !== "required") refuse("a ttl from \"at\" needs at: \"required\"");
|
|
109863
|
+
if (hasSecrets) refuse("a kind with secret fields cannot use engine TTL (its keychain values would be orphaned)");
|
|
109864
|
+
}
|
|
109865
|
+
if ("maxRows" in retention || "maxRowsPerDevice" in retention) {
|
|
109866
|
+
if (retention.maxRowsPerDevice !== void 0 && decl.device !== "required") refuse("maxRowsPerDevice needs device: \"required\"");
|
|
109867
|
+
if (decl.at !== "required") refuse("a row bound trims the oldest \"at\": it needs at: \"required\"");
|
|
109868
|
+
if (hasSecrets) refuse("a kind with secret fields cannot use an engine row bound");
|
|
109869
|
+
const bound = Math.max(retention.maxRows ?? 0, retention.maxRowsPerDevice ?? 0);
|
|
109870
|
+
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)`);
|
|
109871
|
+
}
|
|
109872
|
+
if (decl.deviceRemoval === "delete" && decl.device === "none") refuse("deviceRemoval: \"delete\" needs a device: \"required\" or \"optional\"");
|
|
109873
|
+
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`);
|
|
109874
|
+
}
|
|
109875
|
+
function bodyFieldsOf(schema) {
|
|
109876
|
+
return { ...toJSONSchema(schema, {
|
|
109877
|
+
unrepresentable: "any",
|
|
109878
|
+
io: "input"
|
|
109879
|
+
}) };
|
|
109880
|
+
}
|
|
109881
|
+
function isRecordLike(value) {
|
|
109882
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
109883
|
+
}
|
|
109884
|
+
/** Does every branch of the body schema carry `field`? (A discriminated union has several.) */
|
|
109885
|
+
function hasNameField(json, field) {
|
|
109886
|
+
const branches = json["oneOf"] ?? json["anyOf"];
|
|
109887
|
+
if (Array.isArray(branches)) return branches.length > 0 && branches.every((b) => isRecordLike(b) && hasNameField(b, field));
|
|
109888
|
+
const properties = json["properties"];
|
|
109889
|
+
return isRecordLike(properties) && Object.prototype.hasOwnProperty.call(properties, field);
|
|
109890
|
+
}
|
|
109891
|
+
/**
|
|
109892
|
+
* Declare the kinds of a package. The keys are the kind names. The result is
|
|
109893
|
+
* what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
|
|
109894
|
+
* types its handle from.
|
|
109895
|
+
*/
|
|
109896
|
+
function defineRecordKinds(specs) {
|
|
109897
|
+
const out = {};
|
|
109898
|
+
for (const [name, spec] of Object.entries(specs)) {
|
|
109899
|
+
const secretPaths = collectZodSecretPaths(spec.schema);
|
|
109900
|
+
const nameField = spec.entry?.nameField ?? "name";
|
|
109901
|
+
const fields = bodyFieldsOf(spec.schema);
|
|
109902
|
+
if (spec.entry !== void 0 && !hasNameField(fields, nameField)) throw new DeclarationRefusedError(`kind "${name}": an entry kind's schema needs a "${nameField}" field`);
|
|
109903
|
+
const parsed = RecordKindDeclarationSchema.parse({
|
|
109904
|
+
database: spec.database,
|
|
109905
|
+
class: spec.class,
|
|
109906
|
+
writeTrigger: spec.writeTrigger,
|
|
109907
|
+
device: spec.device,
|
|
109908
|
+
at: spec.at,
|
|
109909
|
+
indexes: spec.indexes === void 0 ? void 0 : [...spec.indexes],
|
|
109910
|
+
retention: spec.retention,
|
|
109911
|
+
deviceRemoval: spec.deviceRemoval,
|
|
109912
|
+
maxDataBytes: spec.maxDataBytes,
|
|
109913
|
+
secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
|
|
109914
|
+
secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
|
|
109915
|
+
transactFamily: spec.transactFamily,
|
|
109916
|
+
entry: spec.entry === void 0 ? void 0 : {
|
|
109917
|
+
title: spec.entry.title,
|
|
109918
|
+
nameField,
|
|
109919
|
+
fields
|
|
109920
|
+
}
|
|
109921
|
+
});
|
|
109922
|
+
assertValidDeclaration(name, parsed);
|
|
109923
|
+
out[name] = {
|
|
109924
|
+
name,
|
|
109925
|
+
schema: spec.schema,
|
|
109926
|
+
declaration: parsed
|
|
109927
|
+
};
|
|
109928
|
+
}
|
|
109929
|
+
return out;
|
|
109930
|
+
}
|
|
109931
|
+
/**
|
|
109932
|
+
* Entry kinds (D881): every list of user-created entries (notification
|
|
109933
|
+
* targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
|
|
109934
|
+
* kind-specific body, and the admin UI lists/adds/edits/deletes all of them
|
|
109935
|
+
* through the `record-entries` capability with password fields derived from
|
|
109936
|
+
* the schema (`.meta({ secret: true })`).
|
|
109937
|
+
*/
|
|
109938
|
+
/** The body of a plain entry: a name and the fields the kind adds. */
|
|
109939
|
+
function entryBody(shape) {
|
|
109940
|
+
return object({
|
|
109941
|
+
name: string().min(1),
|
|
109942
|
+
...shape
|
|
109943
|
+
});
|
|
109944
|
+
}
|
|
109945
|
+
/**
|
|
109946
|
+
* Declare one entry kind. An entry is configuration: never trimmed by the
|
|
109947
|
+
* engine, never aged, belonging to no device.
|
|
109948
|
+
*/
|
|
109949
|
+
function defineEntryKind(name, spec) {
|
|
109950
|
+
const defined = defineRecordKinds({ [name]: {
|
|
109951
|
+
schema: spec.schema,
|
|
109952
|
+
class: "config",
|
|
109953
|
+
writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
|
|
109954
|
+
retention: { none: "an entry goes only when the operator deletes it" },
|
|
109955
|
+
...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
|
|
109956
|
+
...spec.database === void 0 ? {} : { database: spec.database },
|
|
109957
|
+
...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
|
|
109958
|
+
...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
|
|
109959
|
+
...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
|
|
109960
|
+
entry: {
|
|
109961
|
+
title: spec.title,
|
|
109962
|
+
...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
|
|
109963
|
+
}
|
|
109964
|
+
} })[name];
|
|
109965
|
+
if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
|
|
109966
|
+
return defined;
|
|
109967
|
+
}
|
|
109454
109968
|
object({
|
|
109455
109969
|
field: string().min(1),
|
|
109456
109970
|
on: literal("advance")
|
|
@@ -113623,6 +114137,205 @@ var IntegrationInstanceRefSchema = object({
|
|
|
113623
114137
|
]),
|
|
113624
114138
|
candidateStableId: string().optional()
|
|
113625
114139
|
});
|
|
114140
|
+
/**
|
|
114141
|
+
* `RecordIntegrationInstances` - the `integration-instances` provider over ONE
|
|
114142
|
+
* entry kind, so an addon that holds a connection serves it in ~30 lines.
|
|
114143
|
+
*
|
|
114144
|
+
* Every instance is one record of the addon's own kind, keyed by the
|
|
114145
|
+
* `integrations` registry id. The records door does the secret work: a field the
|
|
114146
|
+
* kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
|
|
114147
|
+
* deleting the row deletes the secret, and one row's value is never another's.
|
|
114148
|
+
*
|
|
114149
|
+
* Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
|
|
114150
|
+
* token or the view the form was given KEEP the stored secret;
|
|
114151
|
+
* `{ $secretClear: true }` clears it; any other string replaces it.
|
|
114152
|
+
*/
|
|
114153
|
+
/** The most instances one addon lists (an integrations page, not a table). */
|
|
114154
|
+
var INSTANCES_LIST_MAX = 1e3;
|
|
114155
|
+
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
114156
|
+
function topLevelSecretKeys(paths) {
|
|
114157
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
|
|
114158
|
+
}
|
|
114159
|
+
/** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
|
|
114160
|
+
function secretMapKeys(paths) {
|
|
114161
|
+
return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
|
|
114162
|
+
}
|
|
114163
|
+
/**
|
|
114164
|
+
* A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
|
|
114165
|
+
* view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
|
|
114166
|
+
* or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
|
|
114167
|
+
*/
|
|
114168
|
+
function secretMapPatch(value) {
|
|
114169
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
114170
|
+
const out = {};
|
|
114171
|
+
for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
|
|
114172
|
+
else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
|
|
114173
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
114174
|
+
}
|
|
114175
|
+
/** Does the kind's body schema carry an `enabled` field? */
|
|
114176
|
+
function declaresEnabled(kind) {
|
|
114177
|
+
const json = toJSONSchema(kind.schema, {
|
|
114178
|
+
unrepresentable: "any",
|
|
114179
|
+
io: "input"
|
|
114180
|
+
});
|
|
114181
|
+
const properties = Reflect.get(json, "properties");
|
|
114182
|
+
return typeof properties === "object" && properties !== null && "enabled" in properties;
|
|
114183
|
+
}
|
|
114184
|
+
var RecordIntegrationInstances = class {
|
|
114185
|
+
records;
|
|
114186
|
+
kind;
|
|
114187
|
+
hooks;
|
|
114188
|
+
handle;
|
|
114189
|
+
secretKeys;
|
|
114190
|
+
secretMaps;
|
|
114191
|
+
nameField;
|
|
114192
|
+
/** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
|
|
114193
|
+
registryOwnsEnabled;
|
|
114194
|
+
constructor(records, kind, hooks) {
|
|
114195
|
+
this.records = records;
|
|
114196
|
+
this.kind = kind;
|
|
114197
|
+
this.hooks = hooks;
|
|
114198
|
+
this.handle = records.kind(kind);
|
|
114199
|
+
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
114200
|
+
this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
|
|
114201
|
+
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
114202
|
+
this.registryOwnsEnabled = hooks.description.role === "devices";
|
|
114203
|
+
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"`);
|
|
114204
|
+
}
|
|
114205
|
+
/**
|
|
114206
|
+
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
114207
|
+
* locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
|
|
114208
|
+
* caller can keep what it had instead of reconfiguring with an empty secret (D49).
|
|
114209
|
+
*/
|
|
114210
|
+
async readResolved(id) {
|
|
114211
|
+
try {
|
|
114212
|
+
const record = await this.handle.getResolved(id);
|
|
114213
|
+
return record === null ? null : {
|
|
114214
|
+
id,
|
|
114215
|
+
body: record.data,
|
|
114216
|
+
enabled: record.data.enabled !== false,
|
|
114217
|
+
secrets: "resolved"
|
|
114218
|
+
};
|
|
114219
|
+
} catch (err) {
|
|
114220
|
+
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
114221
|
+
const stored = await this.handle.get(id);
|
|
114222
|
+
if (stored === null) return null;
|
|
114223
|
+
return {
|
|
114224
|
+
id,
|
|
114225
|
+
body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
|
|
114226
|
+
enabled: stored.data.enabled !== false,
|
|
114227
|
+
secrets: "unavailable"
|
|
114228
|
+
};
|
|
114229
|
+
}
|
|
114230
|
+
}
|
|
114231
|
+
/** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
|
|
114232
|
+
async getResolved(id) {
|
|
114233
|
+
return (await this.readResolved(id))?.body ?? null;
|
|
114234
|
+
}
|
|
114235
|
+
/** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
|
|
114236
|
+
async listResolved() {
|
|
114237
|
+
const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
|
|
114238
|
+
const out = [];
|
|
114239
|
+
for (const record of page.records) {
|
|
114240
|
+
const read = await this.readResolved(record.id);
|
|
114241
|
+
if (read !== null) out.push(read);
|
|
114242
|
+
}
|
|
114243
|
+
return out;
|
|
114244
|
+
}
|
|
114245
|
+
viewOf(record) {
|
|
114246
|
+
const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
|
|
114247
|
+
const rawName = record.data[this.nameField];
|
|
114248
|
+
const kind = this.hooks.kindOf?.(record.data);
|
|
114249
|
+
const health = this.hooks.health?.(record.data, record.id);
|
|
114250
|
+
return {
|
|
114251
|
+
id: record.id,
|
|
114252
|
+
name: typeof rawName === "string" ? rawName : record.id,
|
|
114253
|
+
configured: this.hooks.configured(record.data),
|
|
114254
|
+
...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
|
|
114255
|
+
formValues: shown,
|
|
114256
|
+
...kind !== void 0 ? { kind } : {},
|
|
114257
|
+
...health?.status !== void 0 ? { status: health.status } : {},
|
|
114258
|
+
...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
|
|
114259
|
+
};
|
|
114260
|
+
}
|
|
114261
|
+
describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
|
|
114262
|
+
...this.hooks.description,
|
|
114263
|
+
mintsId: true
|
|
114264
|
+
};
|
|
114265
|
+
instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
|
|
114266
|
+
listInstances = async () => {
|
|
114267
|
+
return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
|
|
114268
|
+
};
|
|
114269
|
+
getInstance = async ({ id }) => {
|
|
114270
|
+
const record = await this.handle.get(id);
|
|
114271
|
+
return record === null ? null : this.viewOf(record);
|
|
114272
|
+
};
|
|
114273
|
+
/** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
|
|
114274
|
+
createInstance = async ({ id, name, kind }) => {
|
|
114275
|
+
const minted = id ?? this.hooks.mintId?.({
|
|
114276
|
+
name,
|
|
114277
|
+
...kind !== void 0 ? { kind } : {}
|
|
114278
|
+
});
|
|
114279
|
+
if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
|
|
114280
|
+
await this.handle.put({
|
|
114281
|
+
id: minted,
|
|
114282
|
+
data: this.hooks.initialBody(name, kind)
|
|
114283
|
+
}, { ifRev: 0 });
|
|
114284
|
+
await this.hooks.onChanged?.(minted, "created");
|
|
114285
|
+
return { id: minted };
|
|
114286
|
+
};
|
|
114287
|
+
updateInstance = async ({ id, name, values }) => {
|
|
114288
|
+
const patch = {};
|
|
114289
|
+
for (const [key, value] of Object.entries(values)) {
|
|
114290
|
+
if (value === void 0) continue;
|
|
114291
|
+
if (this.secretMaps.has(key)) {
|
|
114292
|
+
const entries = secretMapPatch(value);
|
|
114293
|
+
if (entries !== null) patch[key] = entries;
|
|
114294
|
+
} else if (!this.secretKeys.has(key)) patch[key] = value;
|
|
114295
|
+
else if (isSecretClear(value)) patch[key] = null;
|
|
114296
|
+
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
114297
|
+
}
|
|
114298
|
+
if (name !== void 0) patch[this.nameField] = name;
|
|
114299
|
+
await this.handle.patch(id, patch);
|
|
114300
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
114301
|
+
return { ok: true };
|
|
114302
|
+
};
|
|
114303
|
+
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
114304
|
+
setInstanceEnabled = async ({ id, enabled }) => {
|
|
114305
|
+
if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
|
|
114306
|
+
await this.handle.patch(id, { enabled });
|
|
114307
|
+
await this.hooks.onChanged?.(id, "enabled");
|
|
114308
|
+
return { ok: true };
|
|
114309
|
+
};
|
|
114310
|
+
/** The row and its keychain refs go together (the records door). */
|
|
114311
|
+
deleteInstance = async ({ id }) => {
|
|
114312
|
+
await this.handle.delete(id);
|
|
114313
|
+
await this.hooks.onChanged?.(id, "deleted");
|
|
114314
|
+
return { ok: true };
|
|
114315
|
+
};
|
|
114316
|
+
testInstance = async (input) => {
|
|
114317
|
+
if (this.hooks.test === void 0) return {
|
|
114318
|
+
success: false,
|
|
114319
|
+
status: "unsupported",
|
|
114320
|
+
error: "This integration declares no connection test"
|
|
114321
|
+
};
|
|
114322
|
+
return this.hooks.test(input);
|
|
114323
|
+
};
|
|
114324
|
+
/** The flat provider object to hand to `registerProvider`. */
|
|
114325
|
+
toProvider() {
|
|
114326
|
+
return {
|
|
114327
|
+
describe: this.describe,
|
|
114328
|
+
instanceFormSchema: this.instanceFormSchema,
|
|
114329
|
+
listInstances: this.listInstances,
|
|
114330
|
+
getInstance: this.getInstance,
|
|
114331
|
+
createInstance: this.createInstance,
|
|
114332
|
+
updateInstance: this.updateInstance,
|
|
114333
|
+
setInstanceEnabled: this.setInstanceEnabled,
|
|
114334
|
+
deleteInstance: this.deleteInstance,
|
|
114335
|
+
testInstance: this.testInstance
|
|
114336
|
+
};
|
|
114337
|
+
}
|
|
114338
|
+
};
|
|
113626
114339
|
var EncodeProfileSchema = object({
|
|
113627
114340
|
video: object({
|
|
113628
114341
|
codec: _enum([
|
|
@@ -114727,6 +115440,130 @@ var MigrationOwnerStepSchema = object({
|
|
|
114727
115440
|
/** The ledger state the engine found: `pending` has no row yet. */
|
|
114728
115441
|
state: union([literal("pending"), MigrationStatusSchema])
|
|
114729
115442
|
});
|
|
115443
|
+
/**
|
|
115444
|
+
* Move an integration's legacy `integration_settings` rows into a typed record of the addon's own
|
|
115445
|
+
* kind (planning#416, the one store). The record is keyed by the `integrations` registry id, so
|
|
115446
|
+
* the integration id, the devices stamped with it and the automations that name it do not change.
|
|
115447
|
+
*
|
|
115448
|
+
* Order, so a crash or a locked keychain is safe at every point:
|
|
115449
|
+
*
|
|
115450
|
+
* 1. Phase A, read only: every row of every integration of the addon is read with its secrets
|
|
115451
|
+
* resolved. One that cannot be read (a locked keychain) throws, naming the integration, before
|
|
115452
|
+
* anything is written; `block-owner` then holds the addon.
|
|
115453
|
+
* 2. Phase B, per integration: the addon's `bodyFrom` builds the body; the record is put with
|
|
115454
|
+
* `ifRev: 0` (an existing record is never overwritten). A record this pass CREATED is read back
|
|
115455
|
+
* and every secret leaf must equal what the rows held, else the record is deleted and the pass
|
|
115456
|
+
* throws; an EXISTING record must hold every secret the rows carry, else the rows are kept and
|
|
115457
|
+
* a warning names the path. Only then are the rows (and their keychain values) cleared.
|
|
115458
|
+
*
|
|
115459
|
+
* Idempotent: an integration whose rows are gone and whose record exists is skipped, so a rerun is
|
|
115460
|
+
* a no-op. A default integration with no rows still gets its record (an empty body, D985).
|
|
115461
|
+
* Never logs or throws a secret value; messages name integration ids and secret PATHS only.
|
|
115462
|
+
*/
|
|
115463
|
+
function secretLeavesOf(body, spec) {
|
|
115464
|
+
const out = /* @__PURE__ */ new Map();
|
|
115465
|
+
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);
|
|
115466
|
+
return out;
|
|
115467
|
+
}
|
|
115468
|
+
function why(err) {
|
|
115469
|
+
return err instanceof Error ? err.message : String(err);
|
|
115470
|
+
}
|
|
115471
|
+
async function moveIntegrationRowsToRecords(ports, spec) {
|
|
115472
|
+
const integrations = await ports.listIntegrations(spec.addonId);
|
|
115473
|
+
if (integrations.length === 0) return {
|
|
115474
|
+
outcome: "already-done",
|
|
115475
|
+
summary: { integrations: 0 }
|
|
115476
|
+
};
|
|
115477
|
+
const rowsOf = /* @__PURE__ */ new Map();
|
|
115478
|
+
for (const integration of integrations) try {
|
|
115479
|
+
rowsOf.set(integration.id, await ports.readRows(integration.id));
|
|
115480
|
+
} catch (err) {
|
|
115481
|
+
throw new Error(`integration '${integration.id}': the legacy settings cannot be read with their secrets (${why(err)}) - nothing moved`, { cause: err });
|
|
115482
|
+
}
|
|
115483
|
+
let created = 0;
|
|
115484
|
+
let cleared = 0;
|
|
115485
|
+
let kept = 0;
|
|
115486
|
+
for (const integration of integrations) {
|
|
115487
|
+
const values = rowsOf.get(integration.id) ?? {};
|
|
115488
|
+
const hasRows = Object.keys(values).length > 0;
|
|
115489
|
+
const body = spec.bodyFrom(values, ports, integration);
|
|
115490
|
+
const put = await ports.putRecord(integration.id, body);
|
|
115491
|
+
if (put === "created") created += 1;
|
|
115492
|
+
if (!hasRows) continue;
|
|
115493
|
+
const wanted = secretLeavesOf(body, spec);
|
|
115494
|
+
const back = await ports.readRecord(integration.id);
|
|
115495
|
+
if (back === null) throw new Error(`integration '${integration.id}': the record is gone after it was put`);
|
|
115496
|
+
const held = secretLeavesOf(back, spec);
|
|
115497
|
+
if (put === "created") {
|
|
115498
|
+
const wrong = [...wanted].filter(([path, value]) => held.get(path) !== value).map(([p]) => p);
|
|
115499
|
+
if (wrong.length > 0) {
|
|
115500
|
+
await ports.deleteRecord(integration.id);
|
|
115501
|
+
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`);
|
|
115502
|
+
}
|
|
115503
|
+
} else {
|
|
115504
|
+
const missing = [...wanted].filter(([path]) => !held.has(path)).map(([p]) => p);
|
|
115505
|
+
if (missing.length > 0) {
|
|
115506
|
+
ports.warn(`integration '${integration.id}': the existing record holds no ${missing.join(", ")}; the legacy settings are kept, not cleared`);
|
|
115507
|
+
kept += 1;
|
|
115508
|
+
continue;
|
|
115509
|
+
}
|
|
115510
|
+
}
|
|
115511
|
+
await ports.clearRows(integration.id);
|
|
115512
|
+
cleared += 1;
|
|
115513
|
+
}
|
|
115514
|
+
return created === 0 && cleared === 0 ? {
|
|
115515
|
+
outcome: "already-done",
|
|
115516
|
+
summary: {
|
|
115517
|
+
integrations: integrations.length,
|
|
115518
|
+
kept
|
|
115519
|
+
}
|
|
115520
|
+
} : {
|
|
115521
|
+
outcome: "done",
|
|
115522
|
+
summary: {
|
|
115523
|
+
integrations: integrations.length,
|
|
115524
|
+
created,
|
|
115525
|
+
cleared,
|
|
115526
|
+
kept
|
|
115527
|
+
}
|
|
115528
|
+
};
|
|
115529
|
+
}
|
|
115530
|
+
/**
|
|
115531
|
+
* The addon-side ports of `moveIntegrationRowsToRecords` (planning#416): an addon runs the move
|
|
115532
|
+
* from its own process, so the legacy rows are reached through the hub (`ctx.api.integrations`:
|
|
115533
|
+
* `list`, `getSettings` - which answers the OWNER its secrets resolved - and `clearSettings`) and
|
|
115534
|
+
* the new record through the addon's own records handle. The hub deletes the rows and their
|
|
115535
|
+
* keychain values; the addon never touches the registry.
|
|
115536
|
+
*/
|
|
115537
|
+
function integrationRowsPortsOverApi(deps) {
|
|
115538
|
+
return {
|
|
115539
|
+
warn: (message) => deps.log.warn(message),
|
|
115540
|
+
listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
|
|
115541
|
+
id: i.id,
|
|
115542
|
+
name: i.name
|
|
115543
|
+
})),
|
|
115544
|
+
readRows: (integrationId) => deps.api.getSettings({ id: integrationId }),
|
|
115545
|
+
putRecord: async (id, body) => {
|
|
115546
|
+
if (await deps.records.get(id) !== null) return "exists";
|
|
115547
|
+
try {
|
|
115548
|
+
await deps.records.put({
|
|
115549
|
+
id,
|
|
115550
|
+
data: body
|
|
115551
|
+
}, { ifRev: 0 });
|
|
115552
|
+
} catch (err) {
|
|
115553
|
+
if (err instanceof RevConflictError) return "exists";
|
|
115554
|
+
throw err;
|
|
115555
|
+
}
|
|
115556
|
+
return "created";
|
|
115557
|
+
},
|
|
115558
|
+
readRecord: async (id) => (await deps.records.getResolved(id))?.data ?? null,
|
|
115559
|
+
deleteRecord: async (id) => {
|
|
115560
|
+
await deps.records.delete(id);
|
|
115561
|
+
},
|
|
115562
|
+
clearRows: async (integrationId) => {
|
|
115563
|
+
await deps.api.clearSettings({ id: integrationId });
|
|
115564
|
+
}
|
|
115565
|
+
};
|
|
115566
|
+
}
|
|
114730
115567
|
function ordinalOf(id) {
|
|
114731
115568
|
const digits = id.replace(/\D+/g, "");
|
|
114732
115569
|
return digits.length > 0 ? Number(digits) : Number.POSITIVE_INFINITY;
|
|
@@ -114801,6 +115638,40 @@ async function claimDevicesToDefaultIntegration(ports, spec) {
|
|
|
114801
115638
|
}
|
|
114802
115639
|
};
|
|
114803
115640
|
}
|
|
115641
|
+
/**
|
|
115642
|
+
* The two migrations of a device addon whose integrations hold NO settings of their own
|
|
115643
|
+
* (planning#416, S-A): every integration becomes a record of the addon's own kind with a name and
|
|
115644
|
+
* nothing else (D985, the default integration is an instance with an empty body), then the devices
|
|
115645
|
+
* that carry no `integrationId` are claimed to the addon's default integration.
|
|
115646
|
+
*
|
|
115647
|
+
* 1. `rows-to-record` (block-owner): `moveIntegrationRowsToRecords` over the hub API. Rows an
|
|
115648
|
+
* addon of this shape does not own (a key nothing reads) are named in a warning, by key, and
|
|
115649
|
+
* dropped with the rows once the record is read back.
|
|
115650
|
+
* 2. `claim-devices` (tolerate): `claimDevicesToDefaultIntegration`; with no integration to claim
|
|
115651
|
+
* to it fails and is retried on the next boot.
|
|
115652
|
+
*
|
|
115653
|
+
* Both run in the addon process through the hub API (`ctx.api`), BEFORE the addon registers its
|
|
115654
|
+
* `integration-instances` provider: while a provider is registered the hub answers `getSettings`
|
|
115655
|
+
* from the record, not from the rows.
|
|
115656
|
+
*/
|
|
115657
|
+
function defaultIntegrationPortsOver(deps) {
|
|
115658
|
+
return {
|
|
115659
|
+
...integrationRowsPortsOverApi({
|
|
115660
|
+
api: deps.api,
|
|
115661
|
+
records: deps.records.kind({
|
|
115662
|
+
name: deps.kind.name,
|
|
115663
|
+
declaration: deps.kind.declaration,
|
|
115664
|
+
schema: record(string(), unknown())
|
|
115665
|
+
}),
|
|
115666
|
+
log: { warn: deps.warn }
|
|
115667
|
+
}),
|
|
115668
|
+
...claimDevicesPortsOver({
|
|
115669
|
+
api: deps.api,
|
|
115670
|
+
info: deps.info,
|
|
115671
|
+
warnDevice: deps.warnDevice
|
|
115672
|
+
})
|
|
115673
|
+
};
|
|
115674
|
+
}
|
|
114804
115675
|
/** The hub side of the device claim, over `ctx.api`. */
|
|
114805
115676
|
function claimDevicesPortsOver(deps) {
|
|
114806
115677
|
return {
|
|
@@ -157066,32 +157937,57 @@ var IntegrationInstancePatchSchema = object({
|
|
|
157066
157937
|
/** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
|
|
157067
157938
|
values: record(string(), unknown())
|
|
157068
157939
|
});
|
|
157069
|
-
|
|
157070
|
-
|
|
157071
|
-
|
|
157072
|
-
|
|
157073
|
-
|
|
157074
|
-
|
|
157075
|
-
|
|
157076
|
-
|
|
157077
|
-
|
|
157078
|
-
|
|
157079
|
-
|
|
157080
|
-
|
|
157081
|
-
|
|
157082
|
-
|
|
157083
|
-
|
|
157084
|
-
|
|
157085
|
-
|
|
157086
|
-
|
|
157087
|
-
|
|
157088
|
-
|
|
157089
|
-
|
|
157090
|
-
|
|
157091
|
-
}),
|
|
157092
|
-
|
|
157093
|
-
|
|
157094
|
-
})
|
|
157940
|
+
var integrationInstancesCapability = {
|
|
157941
|
+
name: "integration-instances",
|
|
157942
|
+
scope: "system",
|
|
157943
|
+
mode: "collection",
|
|
157944
|
+
methods: {
|
|
157945
|
+
describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
|
|
157946
|
+
/** The form; `id: null` is the create form. */
|
|
157947
|
+
instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
|
|
157948
|
+
listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
|
|
157949
|
+
getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
|
|
157950
|
+
/**
|
|
157951
|
+
* Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
|
|
157952
|
+
* without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
|
|
157953
|
+
*/
|
|
157954
|
+
createInstance: method(object({
|
|
157955
|
+
id: IntegrationInstanceIdSchema.optional(),
|
|
157956
|
+
name: string().min(1),
|
|
157957
|
+
kind: string().min(1).optional()
|
|
157958
|
+
}), object({ id: string() }), {
|
|
157959
|
+
kind: "mutation",
|
|
157960
|
+
auth: "admin"
|
|
157961
|
+
}),
|
|
157962
|
+
updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
|
|
157963
|
+
kind: "mutation",
|
|
157964
|
+
auth: "admin"
|
|
157965
|
+
}),
|
|
157966
|
+
/**
|
|
157967
|
+
* Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
|
|
157968
|
+
* form patch (D62, D996). An off instance keeps its record and secrets and does no work.
|
|
157969
|
+
*/
|
|
157970
|
+
setInstanceEnabled: method(object({
|
|
157971
|
+
id: IntegrationInstanceIdSchema,
|
|
157972
|
+
enabled: boolean()
|
|
157973
|
+
}), object({ ok: literal(true) }), {
|
|
157974
|
+
kind: "mutation",
|
|
157975
|
+
auth: "admin"
|
|
157976
|
+
}),
|
|
157977
|
+
/** Delete the record and its keychain refs together. */
|
|
157978
|
+
deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
|
|
157979
|
+
kind: "mutation",
|
|
157980
|
+
auth: "admin"
|
|
157981
|
+
}),
|
|
157982
|
+
testInstance: optionalMethod(object({
|
|
157983
|
+
id: IntegrationInstanceIdSchema.nullable(),
|
|
157984
|
+
values: record(string(), unknown())
|
|
157985
|
+
}), TestConnectionResultSchema$1, {
|
|
157986
|
+
kind: "mutation",
|
|
157987
|
+
auth: "admin"
|
|
157988
|
+
})
|
|
157989
|
+
}
|
|
157990
|
+
};
|
|
157095
157991
|
/**
|
|
157096
157992
|
* Two-way audio / talk-back cap. Clients open a WebRTC audio-only
|
|
157097
157993
|
* session (server-initiated offer pattern, identical to the main
|
|
@@ -220342,7 +221238,7 @@ function text(stored, key) {
|
|
|
220342
221238
|
const value = stored[key];
|
|
220343
221239
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
220344
221240
|
}
|
|
220345
|
-
function portOf(stored, ports) {
|
|
221241
|
+
function portOf$1(stored, ports) {
|
|
220346
221242
|
const value = stored["emailPushPort"];
|
|
220347
221243
|
const numeric = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
220348
221244
|
if (Number.isInteger(numeric) && numeric >= 1 && numeric <= 65535) return numeric;
|
|
@@ -220364,7 +221260,7 @@ async function moveEmailPushToMailbox(ports) {
|
|
|
220364
221260
|
};
|
|
220365
221261
|
const bindHost = text(stored, "emailPushBindHost");
|
|
220366
221262
|
await ports.stashPending({
|
|
220367
|
-
port: portOf(stored, ports),
|
|
221263
|
+
port: portOf$1(stored, ports),
|
|
220368
221264
|
...bindHost === null ? {} : { bindHost },
|
|
220369
221265
|
domain: text(stored, "emailPushDomain") ?? DEFAULT_DOMAIN,
|
|
220370
221266
|
requireAuth: stored["emailPushRequireAuth"] !== false,
|
|
@@ -220376,6 +221272,188 @@ async function moveEmailPushToMailbox(ports) {
|
|
|
220376
221272
|
summary: { moved: true }
|
|
220377
221273
|
};
|
|
220378
221274
|
}
|
|
221275
|
+
var reolinkConnectionsEntryKind = defineEntryKind("connections", {
|
|
221276
|
+
title: "Reolink integrations",
|
|
221277
|
+
schema: entryBody({
|
|
221278
|
+
host: string().nullish(),
|
|
221279
|
+
port: number().int().min(1).max(65535).nullish(),
|
|
221280
|
+
rtspPort: number().int().min(1).max(65535).nullish(),
|
|
221281
|
+
username: string().nullish(),
|
|
221282
|
+
password: string().optional().meta({ secret: true }),
|
|
221283
|
+
transport: _enum(["tcp", "udp"]).nullish(),
|
|
221284
|
+
uid: string().nullish(),
|
|
221285
|
+
debugGeneral: boolean().nullish(),
|
|
221286
|
+
debugSocketLogs: array(ReolinkSocketDebugFlagSchema).nullish()
|
|
221287
|
+
}),
|
|
221288
|
+
writeTrigger: "an operator adds, edits or removes a Reolink integration, or a Hub device is migrated into one"
|
|
221289
|
+
});
|
|
221290
|
+
/** Every kind this addon declares, keyed by its name. */
|
|
221291
|
+
var reolinkRecordKinds = { connections: reolinkConnectionsEntryKind };
|
|
221292
|
+
/** The record body as the connection form's flat values: no name, no unset keys. */
|
|
221293
|
+
function hubSettingsOf(body) {
|
|
221294
|
+
const out = {};
|
|
221295
|
+
for (const [key, value] of Object.entries(body)) {
|
|
221296
|
+
if (key === "name" || value === null || value === void 0) continue;
|
|
221297
|
+
out[key] = value;
|
|
221298
|
+
}
|
|
221299
|
+
return out;
|
|
221300
|
+
}
|
|
221301
|
+
/**
|
|
221302
|
+
* What a resolved record says about its hub. A keychain that is locked yields a record WITHOUT
|
|
221303
|
+
* its password: that is `password-locked`, never an empty password (D49).
|
|
221304
|
+
*/
|
|
221305
|
+
function hubConnectionOf(resolved) {
|
|
221306
|
+
const read = readHubConnection(hubSettingsOf(resolved.body));
|
|
221307
|
+
return read.kind === "no-password" && resolved.secrets === "unavailable" ? { kind: "password-locked" } : read;
|
|
221308
|
+
}
|
|
221309
|
+
/**
|
|
221310
|
+
* What each enabled integration's RECORD says about its hub: the one reader of the connection.
|
|
221311
|
+
* A record that is absent reads as `no-host` (no session), exactly as an empty settings blob did.
|
|
221312
|
+
*/
|
|
221313
|
+
async function readHubConnections(instances, integrations) {
|
|
221314
|
+
const out = [];
|
|
221315
|
+
for (const integration of integrations) {
|
|
221316
|
+
const resolved = await instances.readResolved(integration.id);
|
|
221317
|
+
out.push({
|
|
221318
|
+
id: integration.id,
|
|
221319
|
+
name: integration.name,
|
|
221320
|
+
read: resolved === null ? { kind: "no-host" } : hubConnectionOf(resolved)
|
|
221321
|
+
});
|
|
221322
|
+
}
|
|
221323
|
+
return out;
|
|
221324
|
+
}
|
|
221325
|
+
function buildReolinkInstances(records, deps) {
|
|
221326
|
+
return new RecordIntegrationInstances(records, reolinkConnectionsEntryKind, {
|
|
221327
|
+
description: {
|
|
221328
|
+
role: "devices",
|
|
221329
|
+
discovery: {
|
|
221330
|
+
mode: "lan",
|
|
221331
|
+
scopedToInstance: false
|
|
221332
|
+
},
|
|
221333
|
+
instanceMode: "multiple",
|
|
221334
|
+
kind: "connection",
|
|
221335
|
+
canTest: false
|
|
221336
|
+
},
|
|
221337
|
+
formSchema: () => buildHubInstanceSchema(),
|
|
221338
|
+
initialBody: (name) => ({ name }),
|
|
221339
|
+
configured: (body) => (body.host ?? "").length === 0 || (body.password ?? "").length > 0,
|
|
221340
|
+
onChanged: () => deps.onChanged()
|
|
221341
|
+
});
|
|
221342
|
+
}
|
|
221343
|
+
/**
|
|
221344
|
+
* The provider handed to the hub: the default integration (no connection) shows no hub form, an
|
|
221345
|
+
* NVR / Home Hub one does, and a create wizard (no id yet) sets up a hub.
|
|
221346
|
+
*/
|
|
221347
|
+
function reolinkInstancesProvider(instances, deps) {
|
|
221348
|
+
return {
|
|
221349
|
+
...instances.toProvider(),
|
|
221350
|
+
instanceFormSchema: async ({ id }) => {
|
|
221351
|
+
if (id === null) return buildHubInstanceSchema();
|
|
221352
|
+
const [evidence, body] = await Promise.all([deps.registryEvidenceOf(id), instances.getResolved(id)]);
|
|
221353
|
+
return evidence !== null && isHubIntegration({
|
|
221354
|
+
originStableId: evidence.originStableId,
|
|
221355
|
+
info: evidence.info,
|
|
221356
|
+
settings: body === null ? {} : hubSettingsOf(body)
|
|
221357
|
+
}) || body === null ? buildHubInstanceSchema() : { sections: [] };
|
|
221358
|
+
}
|
|
221359
|
+
};
|
|
221360
|
+
}
|
|
221361
|
+
//#endregion
|
|
221362
|
+
//#region src/migrations/integration-rows-to-record.ts
|
|
221363
|
+
/**
|
|
221364
|
+
* Every Reolink integration moves from legacy `integration_settings` rows into its own
|
|
221365
|
+
* `connections` record under the registry id (planning#416, D1007). An NVR / Home Hub integration
|
|
221366
|
+
* carries its connection (host, ports, username, transport, uid, the two debug fields) and its
|
|
221367
|
+
* password, a keychain secret declared by the kind; the default integration carries a name only.
|
|
221368
|
+
* Each integration's record is built from ITS OWN rows, never from another's.
|
|
221369
|
+
*
|
|
221370
|
+
* Keys nothing reads any more are named in a warning, by key, and dropped with the rows once the
|
|
221371
|
+
* record has been read back. A value of the wrong type is left out and named by key as well.
|
|
221372
|
+
*/
|
|
221373
|
+
var REOLINK_ROWS_TO_RECORD_ID = "provider-reolink/2026-11-05-integration-rows-to-record";
|
|
221374
|
+
var TEXT_KEYS = [
|
|
221375
|
+
"host",
|
|
221376
|
+
"username",
|
|
221377
|
+
"uid"
|
|
221378
|
+
];
|
|
221379
|
+
var PORT_KEYS = ["port", "rtspPort"];
|
|
221380
|
+
function portOf(value) {
|
|
221381
|
+
const n = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
|
|
221382
|
+
return typeof n === "number" && Number.isInteger(n) && n >= 1 && n <= 65535 ? n : null;
|
|
221383
|
+
}
|
|
221384
|
+
function textOf(value) {
|
|
221385
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
|
|
221386
|
+
}
|
|
221387
|
+
/** The record body of one integration, from its resolved legacy rows. Never logs a value. */
|
|
221388
|
+
function reolinkBodyFromRows(values, log, integration) {
|
|
221389
|
+
const body = { name: integration.name };
|
|
221390
|
+
const unusable = [];
|
|
221391
|
+
const known = new Set([
|
|
221392
|
+
...TEXT_KEYS,
|
|
221393
|
+
...PORT_KEYS,
|
|
221394
|
+
"password",
|
|
221395
|
+
"transport",
|
|
221396
|
+
"debugGeneral",
|
|
221397
|
+
"debugSocketLogs"
|
|
221398
|
+
]);
|
|
221399
|
+
for (const key of TEXT_KEYS) {
|
|
221400
|
+
if (!(key in values)) continue;
|
|
221401
|
+
const text = textOf(values[key]);
|
|
221402
|
+
if (text === null) unusable.push(key);
|
|
221403
|
+
else body[key] = text;
|
|
221404
|
+
}
|
|
221405
|
+
for (const key of PORT_KEYS) {
|
|
221406
|
+
if (!(key in values)) continue;
|
|
221407
|
+
const port = portOf(values[key]);
|
|
221408
|
+
if (port === null) unusable.push(key);
|
|
221409
|
+
else body[key] = port;
|
|
221410
|
+
}
|
|
221411
|
+
if ("password" in values) {
|
|
221412
|
+
const password = values["password"];
|
|
221413
|
+
if (typeof password === "string" && password.length > 0) body["password"] = password;
|
|
221414
|
+
else unusable.push("password");
|
|
221415
|
+
}
|
|
221416
|
+
if ("transport" in values) {
|
|
221417
|
+
const transport = values["transport"];
|
|
221418
|
+
if (transport === "tcp" || transport === "udp") body["transport"] = transport;
|
|
221419
|
+
else unusable.push("transport");
|
|
221420
|
+
}
|
|
221421
|
+
if ("debugGeneral" in values) {
|
|
221422
|
+
const flag = values["debugGeneral"];
|
|
221423
|
+
if (typeof flag === "boolean") body["debugGeneral"] = flag;
|
|
221424
|
+
else unusable.push("debugGeneral");
|
|
221425
|
+
}
|
|
221426
|
+
if ("debugSocketLogs" in values) {
|
|
221427
|
+
const flags = values["debugSocketLogs"];
|
|
221428
|
+
const parsed = Array.isArray(flags) ? flags.map((f) => ReolinkSocketDebugFlagSchema.safeParse(f)) : null;
|
|
221429
|
+
if (parsed !== null && parsed.every((p) => p.success)) body["debugSocketLogs"] = parsed.flatMap((p) => p.success ? [p.data] : []);
|
|
221430
|
+
else unusable.push("debugSocketLogs");
|
|
221431
|
+
}
|
|
221432
|
+
const unknown = Object.keys(values).filter((key) => !known.has(key));
|
|
221433
|
+
if (unknown.length > 0) log.warn(`integration '${integration.id}': legacy settings nothing reads any more are dropped: ${unknown.join(", ")}`);
|
|
221434
|
+
if (unusable.length > 0) log.warn(`integration '${integration.id}': legacy settings with an unusable value are left out: ${unusable.join(", ")}`);
|
|
221435
|
+
return reolinkConnectionsEntryKind.schema.parse(body);
|
|
221436
|
+
}
|
|
221437
|
+
var REOLINK_ROWS_TO_RECORD_MIGRATION = {
|
|
221438
|
+
meta: {
|
|
221439
|
+
id: REOLINK_ROWS_TO_RECORD_ID,
|
|
221440
|
+
owner: "provider-reolink",
|
|
221441
|
+
level: "addon-state",
|
|
221442
|
+
seq: 4,
|
|
221443
|
+
dependsOn: [],
|
|
221444
|
+
store: "settings",
|
|
221445
|
+
scope: "cluster",
|
|
221446
|
+
onFailure: "block-owner",
|
|
221447
|
+
bodyKind: "procedure",
|
|
221448
|
+
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",
|
|
221449
|
+
fixtureMark: "a legacy setting key the migration does not know"
|
|
221450
|
+
},
|
|
221451
|
+
procedure: (ports) => moveIntegrationRowsToRecords(ports, {
|
|
221452
|
+
addonId: "provider-reolink",
|
|
221453
|
+
secretPaths: reolinkConnectionsEntryKind.declaration.secretPaths,
|
|
221454
|
+
bodyFrom: reolinkBodyFromRows
|
|
221455
|
+
})
|
|
221456
|
+
};
|
|
220379
221457
|
/** The `provider-reolink` catalogue (D987): offered to the engine when the addon initialises. */
|
|
220380
221458
|
var REOLINK_MIGRATIONS = [{
|
|
220381
221459
|
meta: {
|
|
@@ -220409,17 +221487,17 @@ var REOLINK_MIGRATIONS = [{
|
|
|
220409
221487
|
plan: planDropWebhookIntakeSettings
|
|
220410
221488
|
}];
|
|
220411
221489
|
/**
|
|
220412
|
-
*
|
|
220413
|
-
*
|
|
220414
|
-
*
|
|
221490
|
+
* The integration wave (D1007) in declaration order, which must follow `seq`: the device claim
|
|
221491
|
+
* shipped first (seq 3) and the rows move follows it (seq 4). The engine refuses the whole
|
|
221492
|
+
* catalogue when a seq does not increase, and the addon then does not start.
|
|
220415
221493
|
*/
|
|
220416
|
-
var
|
|
221494
|
+
var REOLINK_INTEGRATION_MIGRATIONS = [claimDevicesMigration({
|
|
220417
221495
|
addonId: "provider-reolink",
|
|
220418
221496
|
date: "2026-11-04",
|
|
220419
221497
|
seq: 3,
|
|
220420
221498
|
dependsOn: [],
|
|
220421
221499
|
eligible: (integration) => integration.info[MIGRATED_FROM_HUB_KEY] === void 0
|
|
220422
|
-
});
|
|
221500
|
+
}), REOLINK_ROWS_TO_RECORD_MIGRATION];
|
|
220423
221501
|
//#endregion
|
|
220424
221502
|
//#region src/reolink-adoption-provider.ts
|
|
220425
221503
|
var DEVICES_FILTER = {
|
|
@@ -220689,7 +221767,7 @@ var ReolinkHubManager = class {
|
|
|
220689
221767
|
const { logger } = this.#deps;
|
|
220690
221768
|
const wanted = /* @__PURE__ */ new Set();
|
|
220691
221769
|
for (const integration of integrations) {
|
|
220692
|
-
const read =
|
|
221770
|
+
const { read } = integration;
|
|
220693
221771
|
if (read.kind === "no-host") continue;
|
|
220694
221772
|
wanted.add(integration.id);
|
|
220695
221773
|
if (read.kind !== "ok") {
|
|
@@ -221057,6 +222135,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
221057
222135
|
*/
|
|
221058
222136
|
autodetectCache = new AutodetectCache();
|
|
221059
222137
|
hubManager = null;
|
|
222138
|
+
instances = null;
|
|
221060
222139
|
/**
|
|
221061
222140
|
* The camera mailbox bridge (`email-push-bridge.ts`, D999): battery cameras mail motion to a
|
|
221062
222141
|
* mailbox of the `smtp-inbound` broker, the bridge turns each message into the camera's
|
|
@@ -221302,25 +222381,17 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
221302
222381
|
}
|
|
221303
222382
|
/**
|
|
221304
222383
|
* Make the hub sessions match the live integration list: one per enabled integration with a
|
|
221305
|
-
* host. Idempotent; runs at boot, after a migration
|
|
221306
|
-
* integration's
|
|
222384
|
+
* host. Idempotent; runs at boot, after a migration, after an instance change and on every
|
|
222385
|
+
* lifecycle event. When any integration's record cannot be read the whole pass is skipped — an unreadable list must
|
|
221307
222386
|
* never close a healthy session.
|
|
221308
222387
|
*/
|
|
221309
222388
|
async reconcileIntegrations() {
|
|
221310
222389
|
const manager = this.hubManager;
|
|
221311
|
-
|
|
222390
|
+
const instances = this.instances;
|
|
222391
|
+
if (manager === null || instances === null) return;
|
|
221312
222392
|
try {
|
|
221313
|
-
const
|
|
221314
|
-
|
|
221315
|
-
for (const integration of mine) {
|
|
221316
|
-
const settings = await this.ctx.api.integrations.getSettings.query({ id: integration.id });
|
|
221317
|
-
withSettings.push({
|
|
221318
|
-
id: integration.id,
|
|
221319
|
-
name: integration.name,
|
|
221320
|
-
settings
|
|
221321
|
-
});
|
|
221322
|
-
}
|
|
221323
|
-
await manager.reconcile(withSettings);
|
|
222393
|
+
const withConnection = await readHubConnections(instances, (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id && i.enabled));
|
|
222394
|
+
await manager.reconcile(withConnection);
|
|
221324
222395
|
} catch (err) {
|
|
221325
222396
|
this.ctx.logger.warn("Reolink integration reconcile failed — sessions left as they were", { meta: { error: errMsg$3(err) } });
|
|
221326
222397
|
}
|
|
@@ -221493,28 +222564,6 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
221493
222564
|
globalSettingsSchema() {
|
|
221494
222565
|
return { sections: [...buildClipRefreshSettingsSchema().sections] };
|
|
221495
222566
|
}
|
|
221496
|
-
/**
|
|
221497
|
-
* Per-instance form (D985): an NVR / Home Hub integration shows its connection only; the
|
|
221498
|
-
* default integration keeps the full schema (`null`). A create wizard has no id yet and is
|
|
221499
|
-
* always setting up a NEW instance — the default one already exists — so it gets the hub form.
|
|
221500
|
-
*/
|
|
221501
|
-
async integrationSettingsSchema(instance) {
|
|
221502
|
-
if (instance.integrationId === null) return buildHubInstanceSchema();
|
|
221503
|
-
try {
|
|
221504
|
-
const [integration, settings] = await Promise.all([this.ctx.api.integrations.get.query({ id: instance.integrationId }), this.ctx.api.integrations.getSettings.query({ id: instance.integrationId })]);
|
|
221505
|
-
return isHubIntegration({
|
|
221506
|
-
originStableId: integration.originStableId,
|
|
221507
|
-
info: integration.info,
|
|
221508
|
-
settings
|
|
221509
|
-
}) ? buildHubInstanceSchema() : null;
|
|
221510
|
-
} catch (err) {
|
|
221511
|
-
this.ctx.logger.warn("Reolink integration form: instance lookup failed — full schema shown", { meta: {
|
|
221512
|
-
integrationId: instance.integrationId,
|
|
221513
|
-
error: errMsg$3(err)
|
|
221514
|
-
} });
|
|
221515
|
-
return null;
|
|
221516
|
-
}
|
|
221517
|
-
}
|
|
221518
222567
|
async onInitialize() {
|
|
221519
222568
|
const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
|
|
221520
222569
|
this.logChannels = createLogChannelsProvider(this.ctx.logger.child("log-channels"));
|
|
@@ -221559,11 +222608,38 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
221559
222608
|
this.wireClipCatalogRefreshers();
|
|
221560
222609
|
await this.serveClipPlanes(this.clipService);
|
|
221561
222610
|
await this.serveWebhookPlane();
|
|
221562
|
-
|
|
222611
|
+
const records = this.ctx.records;
|
|
222612
|
+
if (records === void 0) throw new Error("provider-reolink: ctx.records is required (hub-only addon)");
|
|
222613
|
+
await records.declare(reolinkRecordKinds);
|
|
222614
|
+
const integrationPorts = defaultIntegrationPortsOver({
|
|
221563
222615
|
api: defaultIntegrationApiOver(this.ctx.api),
|
|
222616
|
+
records,
|
|
222617
|
+
kind: reolinkConnectionsEntryKind,
|
|
221564
222618
|
info: (message) => this.ctx.logger.info(message),
|
|
222619
|
+
warn: (message) => this.ctx.logger.warn(message),
|
|
221565
222620
|
warnDevice: (message, deviceId) => this.ctx.logger.warn(message, { tags: { deviceId } })
|
|
221566
|
-
})
|
|
222621
|
+
});
|
|
222622
|
+
const registryEvidenceOf = async (id) => {
|
|
222623
|
+
try {
|
|
222624
|
+
const integration = await this.ctx.api.integrations.get.query({ id });
|
|
222625
|
+
return {
|
|
222626
|
+
originStableId: integration.originStableId,
|
|
222627
|
+
info: { ...integration.info }
|
|
222628
|
+
};
|
|
222629
|
+
} catch (err) {
|
|
222630
|
+
this.ctx.logger.warn("Reolink integration form: instance lookup failed — hub form shown", { meta: {
|
|
222631
|
+
integrationId: id,
|
|
222632
|
+
error: errMsg$3(err)
|
|
222633
|
+
} });
|
|
222634
|
+
return null;
|
|
222635
|
+
}
|
|
222636
|
+
};
|
|
222637
|
+
const instances = buildReolinkInstances(records, {
|
|
222638
|
+
onChanged: () => this.reconcileIntegrations(),
|
|
222639
|
+
registryEvidenceOf
|
|
222640
|
+
});
|
|
222641
|
+
this.instances = instances;
|
|
222642
|
+
await this.runMigrations([...defineAddonMigrations(REOLINK_MIGRATIONS, this.migrationPorts()), ...defineAddonMigrations(REOLINK_INTEGRATION_MIGRATIONS, integrationPorts)]);
|
|
221567
222643
|
this.wireEmailBridge();
|
|
221568
222644
|
const wireSleepPushResolvers = (event, via) => {
|
|
221569
222645
|
const data = event.data;
|
|
@@ -221602,7 +222678,11 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
221602
222678
|
capability: deviceRemovalHookCapability,
|
|
221603
222679
|
provider: createEmailPushRemovalHook({ resync: () => this.ensureEmailBridge().sync() })
|
|
221604
222680
|
});
|
|
221605
|
-
|
|
222681
|
+
regs.push({
|
|
222682
|
+
capability: integrationInstancesCapability,
|
|
222683
|
+
provider: reolinkInstancesProvider(instances, { registryEvidenceOf })
|
|
222684
|
+
});
|
|
222685
|
+
return { providers: regs };
|
|
221606
222686
|
}
|
|
221607
222687
|
/** What the catalogue's migration reads and writes (D987). */
|
|
221608
222688
|
migrationPorts() {
|