@camstack/addon-provider-reolink 1.2.218 → 1.2.219
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/addon.js +1140 -802
- package/dist/addon.mjs +1149 -812
- package/dist/{esm-DsdWoSZo.mjs → esm-CO4o0BWI.mjs} +3 -3
- package/i18n/en.json +0 -11
- package/i18n/it.json +0 -11
- package/package.json +6 -3
package/dist/addon.js
CHANGED
|
@@ -29171,7 +29171,7 @@ var require_lz4 = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
|
|
|
29171
29171
|
};
|
|
29172
29172
|
}));
|
|
29173
29173
|
//#endregion
|
|
29174
|
-
//#region node_modules/@apocaliss92/nodelink-js/dist/chunk-
|
|
29174
|
+
//#region node_modules/@apocaliss92/nodelink-js/dist/chunk-DBCXE3EJ.js
|
|
29175
29175
|
var import_fxp = require_fxp();
|
|
29176
29176
|
var import_lz4 = /* @__PURE__ */ require_chunk.__toESM(require_lz4(), 1);
|
|
29177
29177
|
function bcHeaderLen(messageClass, buf, bodyLen) {
|
|
@@ -31248,6 +31248,13 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
31248
31248
|
* others wait on the same promise.
|
|
31249
31249
|
*/
|
|
31250
31250
|
tcpConnectPromise;
|
|
31251
|
+
/**
|
|
31252
|
+
* Bound for a single TCP handshake. Without this (and without rejecting on
|
|
31253
|
+
* `close`), a socket destroyed mid-connect left `tcpConnectPromise` pending
|
|
31254
|
+
* forever — every later `sendXml`/`connect` that joined the same promise
|
|
31255
|
+
* hung past its request timeout (which only started AFTER connect resolved).
|
|
31256
|
+
*/
|
|
31257
|
+
static TCP_CONNECT_TIMEOUT_MS = 1e4;
|
|
31251
31258
|
msgNum = 0;
|
|
31252
31259
|
loggedIn = false;
|
|
31253
31260
|
subscribed = false;
|
|
@@ -31843,6 +31850,26 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
31843
31850
|
}
|
|
31844
31851
|
}
|
|
31845
31852
|
}
|
|
31853
|
+
/**
|
|
31854
|
+
* Connect, but fail if the handshake does not settle within `timeoutMs`.
|
|
31855
|
+
* Returns remaining ms so callers can apply the rest of their budget to the
|
|
31856
|
+
* request itself (request timers used to start only AFTER connect resolved,
|
|
31857
|
+
* so a wedged `tcpConnectPromise` made every send hang forever).
|
|
31858
|
+
*/
|
|
31859
|
+
async connectWithDeadline(timeoutMs, label) {
|
|
31860
|
+
const started = Date.now();
|
|
31861
|
+
let timer;
|
|
31862
|
+
try {
|
|
31863
|
+
await Promise.race([this.connect(), new Promise((_, reject) => {
|
|
31864
|
+
timer = setTimeout(() => {
|
|
31865
|
+
reject(/* @__PURE__ */ new Error(`Baichuan connect timeout after ${timeoutMs}ms (${label})`));
|
|
31866
|
+
}, Math.max(1, timeoutMs));
|
|
31867
|
+
})]);
|
|
31868
|
+
} finally {
|
|
31869
|
+
if (timer) clearTimeout(timer);
|
|
31870
|
+
}
|
|
31871
|
+
return Math.max(1, timeoutMs - (Date.now() - started));
|
|
31872
|
+
}
|
|
31846
31873
|
async connect() {
|
|
31847
31874
|
const desired = this.opts.transport ?? "tcp";
|
|
31848
31875
|
if (desired === "tcp") {
|
|
@@ -31976,8 +32003,30 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
31976
32003
|
this.emit("error", err);
|
|
31977
32004
|
});
|
|
31978
32005
|
await new Promise((resolve, reject) => {
|
|
31979
|
-
|
|
31980
|
-
|
|
32006
|
+
let settled = false;
|
|
32007
|
+
const settle = (fn) => {
|
|
32008
|
+
if (settled) return;
|
|
32009
|
+
settled = true;
|
|
32010
|
+
clearTimeout(timer);
|
|
32011
|
+
sock.off("connect", onConnect);
|
|
32012
|
+
sock.off("error", onError);
|
|
32013
|
+
sock.off("close", onClose);
|
|
32014
|
+
fn();
|
|
32015
|
+
};
|
|
32016
|
+
const onConnect = () => settle(() => resolve());
|
|
32017
|
+
const onError = (e) => settle(() => reject(e));
|
|
32018
|
+
const onClose = () => settle(() => reject(/* @__PURE__ */ new Error("Baichuan TCP connect aborted: socket closed")));
|
|
32019
|
+
const timer = setTimeout(() => {
|
|
32020
|
+
settle(() => {
|
|
32021
|
+
try {
|
|
32022
|
+
if (!sock.destroyed) sock.destroy();
|
|
32023
|
+
} catch {}
|
|
32024
|
+
reject(/* @__PURE__ */ new Error(`TCP connection timeout after ${_BaichuanClient.TCP_CONNECT_TIMEOUT_MS}ms`));
|
|
32025
|
+
});
|
|
32026
|
+
}, _BaichuanClient.TCP_CONNECT_TIMEOUT_MS);
|
|
32027
|
+
sock.once("connect", onConnect);
|
|
32028
|
+
sock.once("error", onError);
|
|
32029
|
+
sock.once("close", onClose);
|
|
31981
32030
|
});
|
|
31982
32031
|
const sid = this.socketSessionId;
|
|
31983
32032
|
const localAddress = sock.localAddress;
|
|
@@ -32888,7 +32937,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
32888
32937
|
async sendXml(params) {
|
|
32889
32938
|
const internal = params.internal === true;
|
|
32890
32939
|
if (!internal) this.touchUserActivity(`sendXml cmdId=${params.cmdId}`);
|
|
32891
|
-
|
|
32940
|
+
const timeoutMs = params.timeoutMs ?? 1e4;
|
|
32941
|
+
const remainingMs = await this.connectWithDeadline(timeoutMs, `sendXml cmdId=${params.cmdId}`);
|
|
32892
32942
|
if (!internal) this.kickIdleDisconnectTimer();
|
|
32893
32943
|
const channel = params.channel ?? this.opts.channel ?? 0;
|
|
32894
32944
|
const channelId = params.channelIdOverride ?? (params.channel == null ? this.hostChannelId : channel + 1);
|
|
@@ -32913,12 +32963,11 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
32913
32963
|
const enc = params.encryption ?? this.enc;
|
|
32914
32964
|
const bodyBytes = this.encodeBodyXml(extXml, payloadXml, channelId, enc);
|
|
32915
32965
|
const wire = Buffer.concat([header, bodyBytes]);
|
|
32916
|
-
const timeoutMs = params.timeoutMs ?? 1e4;
|
|
32917
32966
|
const framePromise = new Promise((resolve, reject) => {
|
|
32918
32967
|
const t = setTimeout(() => {
|
|
32919
32968
|
this.pending.delete(pendingKey);
|
|
32920
32969
|
reject(/* @__PURE__ */ new Error(`Baichuan timeout cmdId=${cmdId} msgNum=${msgNum}`));
|
|
32921
|
-
},
|
|
32970
|
+
}, remainingMs);
|
|
32922
32971
|
this.pending.set(pendingKey, {
|
|
32923
32972
|
resolve: (f) => {
|
|
32924
32973
|
clearTimeout(t);
|
|
@@ -32975,7 +33024,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
32975
33024
|
async sendFrame(params) {
|
|
32976
33025
|
const internal = params.internal === true;
|
|
32977
33026
|
if (!internal) this.touchUserActivity(`sendFrame cmdId=${params.cmdId}`);
|
|
32978
|
-
|
|
33027
|
+
const timeoutMs = params.timeoutMs ?? 1e4;
|
|
33028
|
+
const remainingMs = await this.connectWithDeadline(timeoutMs, `sendFrame cmdId=${params.cmdId}`);
|
|
32979
33029
|
if (!internal) this.kickIdleDisconnectTimer();
|
|
32980
33030
|
const channel = params.channel ?? this.opts.channel ?? 0;
|
|
32981
33031
|
const channelId = params.channelIdOverride ?? (params.channel == null ? this.hostChannelId : channel + 1);
|
|
@@ -33000,12 +33050,11 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
33000
33050
|
const enc = params.encryption ?? this.enc;
|
|
33001
33051
|
const bodyBytes = this.encodeBodyXml(extXml, payloadXml, channelId, enc);
|
|
33002
33052
|
const wire = Buffer.concat([header, bodyBytes]);
|
|
33003
|
-
const timeoutMs = params.timeoutMs ?? 1e4;
|
|
33004
33053
|
const framePromise = new Promise((resolve, reject) => {
|
|
33005
33054
|
const t = setTimeout(() => {
|
|
33006
33055
|
this.pending.delete(pendingKey);
|
|
33007
33056
|
reject(/* @__PURE__ */ new Error(`Baichuan timeout cmdId=${cmdId} msgNum=${msgNum}`));
|
|
33008
|
-
},
|
|
33057
|
+
}, remainingMs);
|
|
33009
33058
|
this.pending.set(pendingKey, {
|
|
33010
33059
|
resolve: (f) => {
|
|
33011
33060
|
clearTimeout(t);
|
|
@@ -37615,10 +37664,18 @@ var mapToSimpleEvent = (event) => {
|
|
|
37615
37664
|
}
|
|
37616
37665
|
return null;
|
|
37617
37666
|
};
|
|
37618
|
-
var shouldAcceptSleepPushSource = (source, preference,
|
|
37667
|
+
var shouldAcceptSleepPushSource = (source, preference, options) => {
|
|
37619
37668
|
if (source !== "email" && source !== "baichuanWebhook") return true;
|
|
37669
|
+
const opts = typeof options === "boolean" ? {
|
|
37670
|
+
webhookSupported: options,
|
|
37671
|
+
webhookDeliverySeen: options
|
|
37672
|
+
} : options;
|
|
37620
37673
|
const pref = preference === "webhook" || preference === "email" ? preference : "auto";
|
|
37621
|
-
|
|
37674
|
+
if (pref === "email") return source === "email";
|
|
37675
|
+
if (pref === "webhook") return source === "baichuanWebhook";
|
|
37676
|
+
if (!opts.webhookSupported) return source === "email";
|
|
37677
|
+
if (opts.webhookDeliverySeen) return source === "baichuanWebhook";
|
|
37678
|
+
return true;
|
|
37622
37679
|
};
|
|
37623
37680
|
var DEFAULT_SHELTER_CANVAS = {
|
|
37624
37681
|
width: 540,
|
|
@@ -38966,19 +39023,10 @@ function parsePtzGuardXml(xml) {
|
|
|
38966
39023
|
};
|
|
38967
39024
|
}
|
|
38968
39025
|
var emitter = new events.EventEmitter();
|
|
38969
|
-
var lastEventByCamera = /* @__PURE__ */ new Map();
|
|
38970
|
-
var MAX_GLOBAL_EVENTS = 300;
|
|
38971
|
-
var globalRecentEvents = [];
|
|
38972
39026
|
function onEmailPushEvent(handler) {
|
|
38973
39027
|
emitter.on("event", handler);
|
|
38974
39028
|
return () => emitter.off("event", handler);
|
|
38975
39029
|
}
|
|
38976
|
-
function emitEmailPushEvent(event) {
|
|
38977
|
-
lastEventByCamera.set(event.cameraId, event);
|
|
38978
|
-
globalRecentEvents.unshift(event);
|
|
38979
|
-
if (globalRecentEvents.length > MAX_GLOBAL_EVENTS) globalRecentEvents.length = MAX_GLOBAL_EVENTS;
|
|
38980
|
-
emitter.emit("event", event);
|
|
38981
|
-
}
|
|
38982
39030
|
function mapEmailPushInferredType(inferred) {
|
|
38983
39031
|
switch (inferred) {
|
|
38984
39032
|
case "motion":
|
|
@@ -38991,6 +39039,52 @@ function mapEmailPushInferredType(inferred) {
|
|
|
38991
39039
|
default: return "motion";
|
|
38992
39040
|
}
|
|
38993
39041
|
}
|
|
39042
|
+
var AI_OBJECT_MAP = {
|
|
39043
|
+
people: "people",
|
|
39044
|
+
person: "people",
|
|
39045
|
+
human: "people",
|
|
39046
|
+
vehicle: "vehicle",
|
|
39047
|
+
car: "vehicle",
|
|
39048
|
+
dog_cat: "animal",
|
|
39049
|
+
dog: "animal",
|
|
39050
|
+
cat: "animal",
|
|
39051
|
+
pet: "animal",
|
|
39052
|
+
face: "face",
|
|
39053
|
+
package: "package"
|
|
39054
|
+
};
|
|
39055
|
+
function xmlTag(xml, tag2) {
|
|
39056
|
+
return (new RegExp(`<${tag2}>([^<]*)</${tag2}>`, "i").exec(xml)?.[1] ?? "").trim().toLowerCase();
|
|
39057
|
+
}
|
|
39058
|
+
function splitCsv(value) {
|
|
39059
|
+
return value.split(",").map((s) => s.trim().toLowerCase()).filter((s) => s.length > 0);
|
|
39060
|
+
}
|
|
39061
|
+
function mapBaichuanWebhookAlarmXmlToSimpleEvents(xml) {
|
|
39062
|
+
if (!xml || !/<AlarmEvent\b/i.test(xml)) return [];
|
|
39063
|
+
const out = /* @__PURE__ */ new Set();
|
|
39064
|
+
for (const match of xml.matchAll(/<AlarmEvent\b[^>]*>([\s\S]*?)<\/AlarmEvent>/gi)) {
|
|
39065
|
+
const block = match[1] ?? "";
|
|
39066
|
+
const status = splitCsv(xmlTag(block, "status"));
|
|
39067
|
+
const ai = splitCsv(xmlTag(block, "AItype") || xmlTag(block, "aiType") || xmlTag(block, "aitype"));
|
|
39068
|
+
if (status.includes("visitor")) {
|
|
39069
|
+
out.add("doorbell");
|
|
39070
|
+
continue;
|
|
39071
|
+
}
|
|
39072
|
+
const objects = [];
|
|
39073
|
+
for (const token of ai) {
|
|
39074
|
+
if (token === "none" || token === "other") continue;
|
|
39075
|
+
const mapped = AI_OBJECT_MAP[token];
|
|
39076
|
+
if (mapped) objects.push(mapped);
|
|
39077
|
+
}
|
|
39078
|
+
if (objects.length > 0) {
|
|
39079
|
+
for (const t of objects) out.add(t);
|
|
39080
|
+
continue;
|
|
39081
|
+
}
|
|
39082
|
+
const statusIndicatesMotion = status.some((s) => s !== "none" && s !== "visitor" && s !== "dn" && s !== "daynight");
|
|
39083
|
+
const aiIndicatesMotion = ai.some((a) => a !== "none");
|
|
39084
|
+
if (statusIndicatesMotion || aiIndicatesMotion) out.add("motion");
|
|
39085
|
+
}
|
|
39086
|
+
return [...out];
|
|
39087
|
+
}
|
|
38994
39088
|
var emitter2 = new events.EventEmitter();
|
|
38995
39089
|
var lastEventByCamera2 = /* @__PURE__ */ new Map();
|
|
38996
39090
|
var MAX_GLOBAL_EVENTS2 = 300;
|
|
@@ -39015,6 +39109,11 @@ function mapBaichuanWebhookToSimpleEvents(event) {
|
|
|
39015
39109
|
if (reason === "pir") return ["motion", "awake"];
|
|
39016
39110
|
return ["awake"];
|
|
39017
39111
|
}
|
|
39112
|
+
function mapBaichuanWebhookParsedToSimpleEvents(parsed) {
|
|
39113
|
+
if (parsed.kind === "event") return mapBaichuanWebhookToSimpleEvents(parsed);
|
|
39114
|
+
if (parsed.cmdId === 33) return mapBaichuanWebhookAlarmXmlToSimpleEvents(parsed.xml);
|
|
39115
|
+
return [];
|
|
39116
|
+
}
|
|
39018
39117
|
var parser = new import_fxp.XMLParser({
|
|
39019
39118
|
ignoreAttributes: false,
|
|
39020
39119
|
attributeNamePrefix: "@_",
|
|
@@ -42592,13 +42691,19 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
42592
42691
|
const matches = "match" in params ? params.match : (event) => event.cameraId === params.cameraId;
|
|
42593
42692
|
return onBaichuanWebhookEvent((event) => {
|
|
42594
42693
|
if (!matches(event)) return;
|
|
42595
|
-
const types =
|
|
42694
|
+
const types = mapBaichuanWebhookParsedToSimpleEvents(event.raw);
|
|
42596
42695
|
for (const type of types) this.dispatchSimpleEvent({
|
|
42597
42696
|
type,
|
|
42598
42697
|
channel,
|
|
42599
42698
|
timestamp: event.receivedAtMs,
|
|
42600
42699
|
source: "baichuanWebhook"
|
|
42601
42700
|
});
|
|
42701
|
+
if (types.some((t) => t !== "motion" && t !== "doorbell" && t !== "awake" && t !== "sleeping" && t !== "other") && !types.includes("motion")) this.dispatchSimpleEvent({
|
|
42702
|
+
type: "motion",
|
|
42703
|
+
channel,
|
|
42704
|
+
timestamp: event.receivedAtMs,
|
|
42705
|
+
source: "baichuanWebhook"
|
|
42706
|
+
});
|
|
42602
42707
|
if (onEvent) try {
|
|
42603
42708
|
onEvent(event);
|
|
42604
42709
|
} catch (err) {
|
|
@@ -44677,7 +44782,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
44677
44782
|
const uid = await this.ensureUidForRecordings(channel, void 0);
|
|
44678
44783
|
const payloadXml = buildFileInfoListReplayByNameXml({
|
|
44679
44784
|
channel,
|
|
44680
|
-
xmlChannelId:
|
|
44785
|
+
xmlChannelId: channel,
|
|
44681
44786
|
name: params.fileName,
|
|
44682
44787
|
uid,
|
|
44683
44788
|
streamType
|
|
@@ -44733,6 +44838,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
44733
44838
|
});
|
|
44734
44839
|
await dedicatedClient.sendXml({
|
|
44735
44840
|
cmdId: 7,
|
|
44841
|
+
extensionXml: "",
|
|
44736
44842
|
channel,
|
|
44737
44843
|
payloadXml: stopXml,
|
|
44738
44844
|
messageClass: require_chunk_LXENEW4N.BC_CLASS_MODERN_24,
|
|
@@ -44785,7 +44891,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
44785
44891
|
const headerChannelIdOverride = this.resolveHeaderChannelIdForLogicalChannel(channel);
|
|
44786
44892
|
const payloadXml = buildFileInfoListReplayByIdXml({
|
|
44787
44893
|
channel,
|
|
44788
|
-
xmlChannelId:
|
|
44894
|
+
xmlChannelId: channel,
|
|
44789
44895
|
id: params.fileName,
|
|
44790
44896
|
...uid ? { uid } : {},
|
|
44791
44897
|
streamType
|
|
@@ -44842,6 +44948,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
44842
44948
|
});
|
|
44843
44949
|
await dedicatedClient.sendXml({
|
|
44844
44950
|
cmdId: 7,
|
|
44951
|
+
extensionXml: "",
|
|
44845
44952
|
channel,
|
|
44846
44953
|
payloadXml: stopXml,
|
|
44847
44954
|
messageClass: require_chunk_LXENEW4N.BC_CLASS_MODERN_24,
|
|
@@ -45772,13 +45879,13 @@ ${stderr}`));
|
|
|
45772
45879
|
if (isNvr) uid = await this.ensureUidForRecordings(channel, params.uid);
|
|
45773
45880
|
const payloadXml = ident.includes("/") ? buildFileInfoListReplayByIdXml({
|
|
45774
45881
|
channel,
|
|
45775
|
-
xmlChannelId:
|
|
45882
|
+
xmlChannelId: channel,
|
|
45776
45883
|
id: ident,
|
|
45777
45884
|
...uid ? { uid } : {},
|
|
45778
45885
|
streamType
|
|
45779
45886
|
}) : buildFileInfoListReplayByNameXml({
|
|
45780
45887
|
channel,
|
|
45781
|
-
xmlChannelId:
|
|
45888
|
+
xmlChannelId: channel,
|
|
45782
45889
|
name: ident,
|
|
45783
45890
|
...uid ? { uid } : {},
|
|
45784
45891
|
streamType
|
|
@@ -45945,6 +46052,7 @@ ${stderr}`));
|
|
|
45945
46052
|
try {
|
|
45946
46053
|
await this.client.sendXml({
|
|
45947
46054
|
cmdId: 7,
|
|
46055
|
+
extensionXml: "",
|
|
45948
46056
|
channel: params.channel,
|
|
45949
46057
|
payloadXml: buildFileInfoListStopXml({
|
|
45950
46058
|
channel: params.channel,
|
|
@@ -98122,10 +98230,8 @@ var require_mailparser = /* @__PURE__ */ require_chunk.__commonJSMin(((exports,
|
|
|
98122
98230
|
simpleParser: require_simple_parser()
|
|
98123
98231
|
};
|
|
98124
98232
|
}));
|
|
98125
|
-
|
|
98126
|
-
|
|
98127
|
-
var import_smtp_server = require_smtp_server();
|
|
98128
|
-
var import_mailparser = require_mailparser();
|
|
98233
|
+
require_smtp_server();
|
|
98234
|
+
require_mailparser();
|
|
98129
98235
|
function buildRfc4571Sdp(video, audio) {
|
|
98130
98236
|
let out = "v=0\r\n";
|
|
98131
98237
|
out += "o=- 0 0 IN IP4 0.0.0.0\r\n";
|
|
@@ -100533,271 +100639,18 @@ function resolveAlawmulaw() {
|
|
|
100533
100639
|
throw new Error("alawmulaw: unable to resolve mulaw/alaw codecs from module exports");
|
|
100534
100640
|
}
|
|
100535
100641
|
var { mulaw, alaw } = resolveAlawmulaw();
|
|
100536
|
-
|
|
100537
|
-
const
|
|
100538
|
-
const keyPath = path.default.join(params.dir, "key.pem");
|
|
100539
|
-
const warn = params.warn ?? ((m) => console.warn(`[email-push-tls] ${m}`));
|
|
100540
|
-
if (!fs.default.existsSync(certPath) || !fs.default.existsSync(keyPath)) {
|
|
100541
|
-
warn(`Email-push TLS requested but ${certPath} or ${keyPath} is missing. Place a PEM cert+key under that directory to enable STARTTLS, or disable TLS in the consumer config to silence this warning.`);
|
|
100542
|
-
return;
|
|
100543
|
-
}
|
|
100544
|
-
return {
|
|
100545
|
-
cert: fs.default.readFileSync(certPath),
|
|
100546
|
-
key: fs.default.readFileSync(keyPath)
|
|
100547
|
-
};
|
|
100548
|
-
}
|
|
100549
|
-
async function defaultLoadTls(dir, warn) {
|
|
100550
|
-
return loadEmailPushTls({
|
|
100551
|
-
dir,
|
|
100552
|
-
warn
|
|
100553
|
-
});
|
|
100554
|
-
}
|
|
100555
|
-
function classifyMessage(parsed) {
|
|
100556
|
-
const haystack = `${parsed.subject ?? ""} ${parsed.text ?? ""}`.toLowerCase();
|
|
100642
|
+
function classifyEmailPushMessage(input) {
|
|
100643
|
+
const haystack = `${(input.subject ?? "").toLowerCase()} ${(input.text ?? "").toLowerCase()}`;
|
|
100557
100644
|
if (/person|people|human/.test(haystack)) return "people";
|
|
100558
100645
|
if (/vehicle|car|truck/.test(haystack)) return "vehicle";
|
|
100559
100646
|
if (/dog[_\s-]?cat|pet|animal/.test(haystack)) return "animal";
|
|
100560
100647
|
if (/face/.test(haystack)) return "face";
|
|
100561
100648
|
if (/package|parcel/.test(haystack)) return "package";
|
|
100562
|
-
if (/
|
|
100649
|
+
if (/ring(?:ing)?\s+button|doorbell\s+(?:press|ring|button)|visitor\s+at|someone\s+rang/.test(haystack)) return "doorbell";
|
|
100563
100650
|
if (/motion|alarm|alert|detect/.test(haystack)) return "motion";
|
|
100651
|
+
if (/\bdoorbell\b/.test(haystack) && !/\bfrom\s+[^\n]*doorbell/.test(haystack)) return "doorbell";
|
|
100564
100652
|
return "other";
|
|
100565
100653
|
}
|
|
100566
|
-
function createEmailPushServer(params) {
|
|
100567
|
-
const log = {
|
|
100568
|
-
debug: params.logger?.debug ?? (() => {}),
|
|
100569
|
-
info: params.logger?.info ?? (() => {}),
|
|
100570
|
-
warn: params.logger?.warn ?? (() => {}),
|
|
100571
|
-
error: params.logger?.error ?? (() => {})
|
|
100572
|
-
};
|
|
100573
|
-
let config = params.config;
|
|
100574
|
-
let server;
|
|
100575
|
-
let status = buildInitialStatus(config);
|
|
100576
|
-
function parseRecipient(rcpt) {
|
|
100577
|
-
const at = rcpt.lastIndexOf("@");
|
|
100578
|
-
if (at <= 0 || at === rcpt.length - 1) return void 0;
|
|
100579
|
-
return {
|
|
100580
|
-
local: rcpt.slice(0, at),
|
|
100581
|
-
domain: rcpt.slice(at + 1).toLowerCase()
|
|
100582
|
-
};
|
|
100583
|
-
}
|
|
100584
|
-
function resolveCameraIdFromRecipient(rcpt) {
|
|
100585
|
-
const parsed = parseRecipient(rcpt);
|
|
100586
|
-
if (!parsed) return void 0;
|
|
100587
|
-
if (parsed.domain !== config.domain.toLowerCase()) return void 0;
|
|
100588
|
-
const match = parsed.local.match(/^cam-(.+)$/);
|
|
100589
|
-
if (!match || !match[1]) return void 0;
|
|
100590
|
-
return params.cameraResolver(match[1]);
|
|
100591
|
-
}
|
|
100592
|
-
async function handleIncomingMessage(cameraId, recipient, raw) {
|
|
100593
|
-
let parsed;
|
|
100594
|
-
try {
|
|
100595
|
-
parsed = await (0, import_mailparser.simpleParser)(raw);
|
|
100596
|
-
} catch (err) {
|
|
100597
|
-
log.warn(`Failed to parse mail for ${cameraId}: ${err instanceof Error ? err.message : err}`);
|
|
100598
|
-
status.messagesRejected++;
|
|
100599
|
-
return;
|
|
100600
|
-
}
|
|
100601
|
-
const receivedAtMs = Date.now();
|
|
100602
|
-
const event = {
|
|
100603
|
-
cameraId,
|
|
100604
|
-
recipient,
|
|
100605
|
-
inferredType: classifyMessage(parsed),
|
|
100606
|
-
receivedAtMs,
|
|
100607
|
-
subject: parsed.subject ?? "",
|
|
100608
|
-
from: typeof parsed.from === "object" && parsed.from !== null && "text" in parsed.from ? String(parsed.from.text) : "",
|
|
100609
|
-
bodyExcerpt: (parsed.text ?? "").slice(0, 500)
|
|
100610
|
-
};
|
|
100611
|
-
status.messagesAccepted++;
|
|
100612
|
-
log.info(`Email push for camera=${cameraId} type=${event.inferredType} subject="${event.subject.slice(0, 80)}"`);
|
|
100613
|
-
emitEmailPushEvent(event);
|
|
100614
|
-
}
|
|
100615
|
-
async function start() {
|
|
100616
|
-
if (server) {
|
|
100617
|
-
log.debug("startEmailPushServer called but server already running");
|
|
100618
|
-
return;
|
|
100619
|
-
}
|
|
100620
|
-
const tlsOptions = config.tls ? await (params.loadTls ?? defaultLoadTls)(config.tlsDir ?? "./email-push-tls", (m) => log.warn(m)) : void 0;
|
|
100621
|
-
status = buildInitialStatus(config);
|
|
100622
|
-
const next = new import_smtp_server.SMTPServer({
|
|
100623
|
-
authOptional: !config.requireAuth,
|
|
100624
|
-
disabledCommands: config.requireAuth ? [] : ["AUTH"],
|
|
100625
|
-
allowInsecureAuth: !config.tls,
|
|
100626
|
-
size: config.maxMessageBytes,
|
|
100627
|
-
maxClients: 50,
|
|
100628
|
-
socketTimeout: 6e4,
|
|
100629
|
-
closeTimeout: 3e4,
|
|
100630
|
-
logger: {
|
|
100631
|
-
info: (...args) => log.info(formatSmtpLogArgs(args)),
|
|
100632
|
-
debug: (...args) => log.debug(formatSmtpLogArgs(args)),
|
|
100633
|
-
error: (...args) => log.warn(formatSmtpLogArgs(args))
|
|
100634
|
-
},
|
|
100635
|
-
...tlsOptions ? {
|
|
100636
|
-
secure: false,
|
|
100637
|
-
...tlsOptions
|
|
100638
|
-
} : {},
|
|
100639
|
-
onConnect(session, callback) {
|
|
100640
|
-
log.info(`SMTP connect from ${session.remoteAddress} (sessionId=${session.id})`);
|
|
100641
|
-
callback();
|
|
100642
|
-
},
|
|
100643
|
-
onClose(session) {
|
|
100644
|
-
log.debug(`SMTP close ${session.remoteAddress} (sessionId=${session.id})`);
|
|
100645
|
-
},
|
|
100646
|
-
onMailFrom(address, session, callback) {
|
|
100647
|
-
log.info(`SMTP MAIL FROM ${address.address} (sessionId=${session.id})`);
|
|
100648
|
-
callback();
|
|
100649
|
-
},
|
|
100650
|
-
onAuth(auth, session, callback) {
|
|
100651
|
-
const expectedUser = config.authUsername;
|
|
100652
|
-
const expectedPass = config.authPassword;
|
|
100653
|
-
if (!expectedUser || !expectedPass) {
|
|
100654
|
-
log.warn(`SMTP AUTH rejected from ${session.remoteAddress} (sessionId=${session.id}): server has no credentials configured`);
|
|
100655
|
-
return callback(/* @__PURE__ */ new Error("Email push auth not configured"));
|
|
100656
|
-
}
|
|
100657
|
-
const stripDomain = (u) => {
|
|
100658
|
-
const at = u.lastIndexOf("@");
|
|
100659
|
-
if (at < 0) return u;
|
|
100660
|
-
const local = u.slice(0, at);
|
|
100661
|
-
return u.slice(at + 1).toLowerCase() === config.domain.toLowerCase() ? local : u;
|
|
100662
|
-
};
|
|
100663
|
-
if (stripDomain(auth.username ?? "") === stripDomain(expectedUser) && auth.password === expectedPass) {
|
|
100664
|
-
log.info(`SMTP AUTH ok method=${auth.method} user=${auth.username} (sessionId=${session.id})`);
|
|
100665
|
-
return callback(null, { user: auth.username });
|
|
100666
|
-
}
|
|
100667
|
-
log.warn(`SMTP AUTH FAIL method=${auth.method} from=${session.remoteAddress} triedUser="${auth.username}" expectedUser="${expectedUser}" triedPasswordLen=${auth.password?.length ?? 0} (sessionId=${session.id})`);
|
|
100668
|
-
return callback(/* @__PURE__ */ new Error("Invalid username or password"));
|
|
100669
|
-
},
|
|
100670
|
-
onRcptTo(address, session, callback) {
|
|
100671
|
-
const cameraId = resolveCameraIdFromRecipient(address.address);
|
|
100672
|
-
if (!cameraId) {
|
|
100673
|
-
status.messagesRejected++;
|
|
100674
|
-
log.warn(`SMTP RCPT TO rejected ${address.address} \u2014 unknown recipient (sessionId=${session.id})`);
|
|
100675
|
-
return callback(/* @__PURE__ */ new Error(`Unknown recipient ${address.address} (not registered)`));
|
|
100676
|
-
}
|
|
100677
|
-
log.info(`SMTP RCPT TO ${address.address} \u2192 camera=${cameraId} (sessionId=${session.id})`);
|
|
100678
|
-
callback();
|
|
100679
|
-
},
|
|
100680
|
-
onData(stream$5, session, callback) {
|
|
100681
|
-
const startedAt = Date.now();
|
|
100682
|
-
log.debug(`SMTP DATA start (sessionId=${session.id} from=${session.envelope.mailFrom ? session.envelope.mailFrom.address : "?"})`);
|
|
100683
|
-
const chunks = [];
|
|
100684
|
-
stream$5.on("data", (chunk) => chunks.push(chunk));
|
|
100685
|
-
stream$5.on("end", () => {
|
|
100686
|
-
const recipients = session.envelope.rcptTo?.map((r) => r.address) ?? [];
|
|
100687
|
-
const buffer = Buffer.concat(chunks);
|
|
100688
|
-
log.info(`SMTP DATA received ${buffer.length}B in ${Date.now() - startedAt}ms for ${recipients.length} recipient(s) (sessionId=${session.id})`);
|
|
100689
|
-
const matched = recipients.map((r) => ({
|
|
100690
|
-
recipient: r,
|
|
100691
|
-
cameraId: resolveCameraIdFromRecipient(r)
|
|
100692
|
-
})).filter((m) => Boolean(m.cameraId));
|
|
100693
|
-
if (matched.length === 0) {
|
|
100694
|
-
status.messagesRejected++;
|
|
100695
|
-
log.warn(`SMTP DATA dropped \u2014 no recognised recipients in [${recipients.join(", ")}] (sessionId=${session.id})`);
|
|
100696
|
-
return callback(/* @__PURE__ */ new Error("No recognised recipients"));
|
|
100697
|
-
}
|
|
100698
|
-
Promise.all(matched.map((m) => handleIncomingMessage(m.cameraId, m.recipient, buffer))).then(() => callback()).catch((err) => {
|
|
100699
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
100700
|
-
log.error(`Email push pipeline error (sessionId=${session.id}): ${msg}`);
|
|
100701
|
-
status.lastErrorMessage = msg;
|
|
100702
|
-
callback(new Error(msg));
|
|
100703
|
-
});
|
|
100704
|
-
});
|
|
100705
|
-
stream$5.on("error", (err) => {
|
|
100706
|
-
log.warn(`SMTP stream error (sessionId=${session.id}): ${err.message}`);
|
|
100707
|
-
callback(err);
|
|
100708
|
-
});
|
|
100709
|
-
}
|
|
100710
|
-
});
|
|
100711
|
-
next.on("error", (err) => {
|
|
100712
|
-
status.lastErrorMessage = err.message;
|
|
100713
|
-
log.error(`Email push server error: ${err.code ?? ""} ${err.message}`);
|
|
100714
|
-
const netServer = next.server;
|
|
100715
|
-
if (server === next && netServer && netServer.listening === false) {
|
|
100716
|
-
status.running = false;
|
|
100717
|
-
setTimeout(() => {
|
|
100718
|
-
if (server !== next) return;
|
|
100719
|
-
try {
|
|
100720
|
-
next.listen(config.port, config.bindHost, () => {
|
|
100721
|
-
status.running = true;
|
|
100722
|
-
log.warn(`Email push SMTP re-listened on ${config.bindHost}:${config.port} after error`);
|
|
100723
|
-
});
|
|
100724
|
-
} catch (e) {
|
|
100725
|
-
log.error(`Email push re-listen failed: ${e.message}`);
|
|
100726
|
-
}
|
|
100727
|
-
}, 5e3).unref?.();
|
|
100728
|
-
}
|
|
100729
|
-
});
|
|
100730
|
-
await new Promise((resolve, reject) => {
|
|
100731
|
-
next.listen(config.port, config.bindHost, () => {
|
|
100732
|
-
status.running = true;
|
|
100733
|
-
status.startedAtMs = Date.now();
|
|
100734
|
-
log.info(`Email push SMTP listening on ${config.bindHost}:${config.port} (domain=${config.domain}, auth=${config.requireAuth}, tls=${config.tls})`);
|
|
100735
|
-
resolve();
|
|
100736
|
-
});
|
|
100737
|
-
next.once("error", reject);
|
|
100738
|
-
});
|
|
100739
|
-
server = next;
|
|
100740
|
-
}
|
|
100741
|
-
async function stop() {
|
|
100742
|
-
if (!server) return;
|
|
100743
|
-
const active = server;
|
|
100744
|
-
server = void 0;
|
|
100745
|
-
await new Promise((resolve) => {
|
|
100746
|
-
active.close(() => resolve());
|
|
100747
|
-
});
|
|
100748
|
-
status.running = false;
|
|
100749
|
-
log.info("Email push SMTP server stopped");
|
|
100750
|
-
}
|
|
100751
|
-
async function restart() {
|
|
100752
|
-
await stop();
|
|
100753
|
-
await start();
|
|
100754
|
-
}
|
|
100755
|
-
return {
|
|
100756
|
-
start,
|
|
100757
|
-
stop,
|
|
100758
|
-
restart,
|
|
100759
|
-
getStatus() {
|
|
100760
|
-
return { ...status };
|
|
100761
|
-
},
|
|
100762
|
-
updateConfig(next) {
|
|
100763
|
-
config = next;
|
|
100764
|
-
}
|
|
100765
|
-
};
|
|
100766
|
-
}
|
|
100767
|
-
function formatSmtpLogArgs(args) {
|
|
100768
|
-
if (args.length === 0) return "[smtp]";
|
|
100769
|
-
const [first, ...rest] = args;
|
|
100770
|
-
let tag = "";
|
|
100771
|
-
let formatArgs = args;
|
|
100772
|
-
if (first && typeof first === "object" && !Array.isArray(first)) {
|
|
100773
|
-
const meta = first;
|
|
100774
|
-
const tnx = typeof meta.tnx === "string" ? meta.tnx : void 0;
|
|
100775
|
-
const cid = typeof meta.cid === "string" ? meta.cid : void 0;
|
|
100776
|
-
const sid = typeof meta.sid === "string" ? meta.sid : void 0;
|
|
100777
|
-
const bits = [];
|
|
100778
|
-
if (tnx) bits.push(tnx);
|
|
100779
|
-
if (cid) bits.push(`cid=${cid}`);
|
|
100780
|
-
if (sid) bits.push(`sid=${sid}`);
|
|
100781
|
-
if (bits.length > 0) tag = ` [${bits.join(" ")}]`;
|
|
100782
|
-
formatArgs = rest;
|
|
100783
|
-
}
|
|
100784
|
-
return `[smtp]${tag} ${(0, util.format)(...formatArgs)}`;
|
|
100785
|
-
}
|
|
100786
|
-
function buildInitialStatus(config) {
|
|
100787
|
-
return {
|
|
100788
|
-
enabled: true,
|
|
100789
|
-
running: false,
|
|
100790
|
-
port: config.port,
|
|
100791
|
-
bindHost: config.bindHost,
|
|
100792
|
-
domain: config.domain,
|
|
100793
|
-
requireAuth: config.requireAuth,
|
|
100794
|
-
tls: config.tls,
|
|
100795
|
-
messagesAccepted: 0,
|
|
100796
|
-
messagesRejected: 0,
|
|
100797
|
-
startedAtMs: void 0,
|
|
100798
|
-
lastErrorMessage: void 0
|
|
100799
|
-
};
|
|
100800
|
-
}
|
|
100801
100654
|
function asRecord(value) {
|
|
100802
100655
|
if (value && typeof value === "object" && !Array.isArray(value)) return value;
|
|
100803
100656
|
}
|
|
@@ -101814,13 +101667,13 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
101814
101667
|
return EventCategory;
|
|
101815
101668
|
}({});
|
|
101816
101669
|
//#endregion
|
|
101817
|
-
//#region ../types/dist/device-type-
|
|
101670
|
+
//#region ../types/dist/device-type-CfHw_quR.mjs
|
|
101818
101671
|
/**
|
|
101819
101672
|
import { errMsg } from '@camstack/types'
|
|
101820
101673
|
* Extract a human-readable message from an unknown error value.
|
|
101821
101674
|
* Replaces the ubiquitous `errMsg(err)` pattern.
|
|
101822
101675
|
*/
|
|
101823
|
-
function errMsg$
|
|
101676
|
+
function errMsg$3(err) {
|
|
101824
101677
|
if (err instanceof Error) return err.message;
|
|
101825
101678
|
if (typeof err === "string") return err;
|
|
101826
101679
|
return String(err);
|
|
@@ -102003,6 +101856,9 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
|
|
|
102003
101856
|
DeviceFeature["CoverPositionable"] = "cover-positionable";
|
|
102004
101857
|
/** Cover exposes slat-tilt control. Gates the tilt slider UI. */
|
|
102005
101858
|
DeviceFeature["CoverTilt"] = "cover-tilt";
|
|
101859
|
+
/** Cover has a real stop: `stop` halts the travel in place. Gates the stop
|
|
101860
|
+
* control in exporters; a provider whose `stop` is a no-op never declares it. */
|
|
101861
|
+
DeviceFeature["CoverStoppable"] = "cover-stoppable";
|
|
102006
101862
|
/** Valve exposes intermediate position control (0..100). Gates the
|
|
102007
101863
|
* position slider / drag surface UI. */
|
|
102008
101864
|
DeviceFeature["ValvePositionable"] = "valve-positionable";
|
|
@@ -106868,7 +106724,7 @@ var ZodIssueCode = {
|
|
|
106868
106724
|
var ZodFirstPartyTypeKind;
|
|
106869
106725
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
106870
106726
|
//#endregion
|
|
106871
|
-
//#region ../types/dist/sleep-
|
|
106727
|
+
//#region ../types/dist/sleep-QsoHs66J.mjs
|
|
106872
106728
|
/**
|
|
106873
106729
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
106874
106730
|
* window to float samples (D455).
|
|
@@ -106985,7 +106841,7 @@ function isKeychainStoredField(field) {
|
|
|
106985
106841
|
var SECRET_CLEAR = { $secretClear: true };
|
|
106986
106842
|
/** The `'***'` token D634 redaction writes for non-admins. */
|
|
106987
106843
|
var LEGACY_DEVICE_REDACTION = "***";
|
|
106988
|
-
function isRecord$
|
|
106844
|
+
function isRecord$7(value) {
|
|
106989
106845
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
106990
106846
|
}
|
|
106991
106847
|
function makeSecretMarker(ref) {
|
|
@@ -106996,16 +106852,16 @@ function makeSecretMarker(ref) {
|
|
|
106996
106852
|
}
|
|
106997
106853
|
/** A stored marker: an object whose `$secret` is a non-empty string. */
|
|
106998
106854
|
function isSecretMarker(value) {
|
|
106999
|
-
if (!isRecord$
|
|
106855
|
+
if (!isRecord$7(value)) return false;
|
|
107000
106856
|
const ref = value["$secret"];
|
|
107001
106857
|
return typeof ref === "string" && ref.length > 0;
|
|
107002
106858
|
}
|
|
107003
106859
|
/** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
|
|
107004
106860
|
function isSecretMarkerLike(value) {
|
|
107005
|
-
return isRecord$
|
|
106861
|
+
return isRecord$7(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
|
|
107006
106862
|
}
|
|
107007
106863
|
function isSecretClear(value) {
|
|
107008
|
-
return isRecord$
|
|
106864
|
+
return isRecord$7(value) && value["$secretClear"] === true;
|
|
107009
106865
|
}
|
|
107010
106866
|
/**
|
|
107011
106867
|
* The UI view of a stored secret field value: a marker becomes "set", inline
|
|
@@ -107388,6 +107244,53 @@ function emitReadiness(bus, params) {
|
|
|
107388
107244
|
ts
|
|
107389
107245
|
}));
|
|
107390
107246
|
}
|
|
107247
|
+
/** Why a catalogue entry is not a runnable body of its declared kind. */
|
|
107248
|
+
var AddonMigrationDefinitionError = class extends Error {
|
|
107249
|
+
id;
|
|
107250
|
+
constructor(id, message) {
|
|
107251
|
+
super(`${id}: ${message}`);
|
|
107252
|
+
this.id = id;
|
|
107253
|
+
this.name = "AddonMigrationDefinitionError";
|
|
107254
|
+
}
|
|
107255
|
+
};
|
|
107256
|
+
/**
|
|
107257
|
+
* Bind a catalogue to the addon's ports. Throws {@link AddonMigrationDefinitionError} when
|
|
107258
|
+
* an entry's `bodyKind` does not match the body it carries, or is `sql` (a hub-only
|
|
107259
|
+
* body). A throwing body never escapes `execute`: it is reported as a failed result.
|
|
107260
|
+
*/
|
|
107261
|
+
function defineAddonMigrations(catalogue, ports) {
|
|
107262
|
+
return catalogue.map((entry) => {
|
|
107263
|
+
const { meta } = entry;
|
|
107264
|
+
if ("plan" in entry) {
|
|
107265
|
+
if (meta.bodyKind !== "plan") throw new AddonMigrationDefinitionError(meta.id, `carries a plan body but declares "${meta.bodyKind}"`);
|
|
107266
|
+
return {
|
|
107267
|
+
meta,
|
|
107268
|
+
execute: () => guarded(async () => ({
|
|
107269
|
+
kind: "plan",
|
|
107270
|
+
plan: await entry.plan(ports)
|
|
107271
|
+
}))
|
|
107272
|
+
};
|
|
107273
|
+
}
|
|
107274
|
+
if (meta.bodyKind !== "procedure") throw new AddonMigrationDefinitionError(meta.id, `carries a procedure body but declares "${meta.bodyKind}"`);
|
|
107275
|
+
return {
|
|
107276
|
+
meta,
|
|
107277
|
+
execute: () => guarded(async () => ({
|
|
107278
|
+
kind: "procedure",
|
|
107279
|
+
result: await entry.procedure(ports)
|
|
107280
|
+
}))
|
|
107281
|
+
};
|
|
107282
|
+
});
|
|
107283
|
+
}
|
|
107284
|
+
async function guarded(work) {
|
|
107285
|
+
try {
|
|
107286
|
+
return await work();
|
|
107287
|
+
} catch (err) {
|
|
107288
|
+
return {
|
|
107289
|
+
kind: "failed",
|
|
107290
|
+
error: errMsg$3(err)
|
|
107291
|
+
};
|
|
107292
|
+
}
|
|
107293
|
+
}
|
|
107391
107294
|
var NOT_RUN = {
|
|
107392
107295
|
declared: false,
|
|
107393
107296
|
snapshot: null,
|
|
@@ -107415,7 +107318,7 @@ async function runAddonMigrations(host, migrations, logger) {
|
|
|
107415
107318
|
metas: migrations.map((m) => m.meta)
|
|
107416
107319
|
});
|
|
107417
107320
|
} catch (err) {
|
|
107418
|
-
log.error("the migration engine refused or could not take this addon catalogue — none ran", { meta: { error: errMsg$
|
|
107321
|
+
log.error("the migration engine refused or could not take this addon catalogue — none ran", { meta: { error: errMsg$3(err) } });
|
|
107419
107322
|
return {
|
|
107420
107323
|
...NOT_RUN,
|
|
107421
107324
|
blockOwner: migrations.filter((m) => m.meta.onFailure === "block-owner").map((m) => m.meta.id)
|
|
@@ -107468,7 +107371,7 @@ async function runAddonMigrations(host, migrations, logger) {
|
|
|
107468
107371
|
} catch (err) {
|
|
107469
107372
|
log.error("migration could not be completed with the engine", {
|
|
107470
107373
|
tags,
|
|
107471
|
-
meta: { error: errMsg$
|
|
107374
|
+
meta: { error: errMsg$3(err) }
|
|
107472
107375
|
});
|
|
107473
107376
|
failed.push(step.id);
|
|
107474
107377
|
}
|
|
@@ -107711,7 +107614,7 @@ var KeychainWriteRefusedError = class extends Error {
|
|
|
107711
107614
|
}
|
|
107712
107615
|
};
|
|
107713
107616
|
var KEEP = { kind: "keep" };
|
|
107714
|
-
function isRecord$
|
|
107617
|
+
function isRecord$6(value) {
|
|
107715
107618
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
107716
107619
|
}
|
|
107717
107620
|
function defaultMintId() {
|
|
@@ -107724,7 +107627,7 @@ function collectSecretMarkerRefs(value, out = []) {
|
|
|
107724
107627
|
return out;
|
|
107725
107628
|
}
|
|
107726
107629
|
if (Array.isArray(value)) for (const item of value) collectSecretMarkerRefs(item, out);
|
|
107727
|
-
else if (isRecord$
|
|
107630
|
+
else if (isRecord$6(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
|
|
107728
107631
|
return out;
|
|
107729
107632
|
}
|
|
107730
107633
|
async function storedValueEquals(secrets, marker, candidate) {
|
|
@@ -107779,7 +107682,7 @@ function rowId(row, idKey) {
|
|
|
107779
107682
|
return null;
|
|
107780
107683
|
}
|
|
107781
107684
|
async function writeArray(ctx, node, incoming, stored, fieldPath) {
|
|
107782
|
-
const storedRows = Array.isArray(stored) ? stored.filter(isRecord$
|
|
107685
|
+
const storedRows = Array.isArray(stored) ? stored.filter(isRecord$6) : [];
|
|
107783
107686
|
if (!Array.isArray(incoming)) {
|
|
107784
107687
|
ctx.deletes.push(...collectSecretMarkerRefs(storedRows));
|
|
107785
107688
|
return {
|
|
@@ -107795,7 +107698,7 @@ async function writeArray(ctx, node, incoming, stored, fieldPath) {
|
|
|
107795
107698
|
const kept = /* @__PURE__ */ new Set();
|
|
107796
107699
|
const rows = [];
|
|
107797
107700
|
for (const row of incoming) {
|
|
107798
|
-
if (!isRecord$
|
|
107701
|
+
if (!isRecord$6(row)) {
|
|
107799
107702
|
rows.push(row);
|
|
107800
107703
|
continue;
|
|
107801
107704
|
}
|
|
@@ -107820,7 +107723,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
|
|
|
107820
107723
|
case "leaf": return writeLeaf(ctx, incoming, stored, fieldPath);
|
|
107821
107724
|
case "array": return writeArray(ctx, node, incoming, stored, fieldPath);
|
|
107822
107725
|
case "object": {
|
|
107823
|
-
if (!isRecord$
|
|
107726
|
+
if (!isRecord$6(incoming)) {
|
|
107824
107727
|
if (incoming === void 0) return KEEP;
|
|
107825
107728
|
ctx.deletes.push(...collectSecretMarkerRefs(stored));
|
|
107826
107729
|
return {
|
|
@@ -107828,7 +107731,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
|
|
|
107828
107731
|
value: incoming
|
|
107829
107732
|
};
|
|
107830
107733
|
}
|
|
107831
|
-
const storedObj = isRecord$
|
|
107734
|
+
const storedObj = isRecord$6(stored) ? stored : {};
|
|
107832
107735
|
return {
|
|
107833
107736
|
kind: "value",
|
|
107834
107737
|
value: await writeMembers(ctx, node.children, incoming, storedObj, (m) => `${fieldPath}.${m}`)
|
|
@@ -107951,7 +107854,7 @@ async function resolveSecretMarkers(value, secrets, options = {}) {
|
|
|
107951
107854
|
value: items
|
|
107952
107855
|
};
|
|
107953
107856
|
}
|
|
107954
|
-
if (isRecord$
|
|
107857
|
+
if (isRecord$6(v)) {
|
|
107955
107858
|
const out = {};
|
|
107956
107859
|
for (const [k, child] of Object.entries(v)) {
|
|
107957
107860
|
const s = substitute(child);
|
|
@@ -107983,7 +107886,7 @@ var SecretOverwriteRefusedError = class extends Error {
|
|
|
107983
107886
|
this.name = "SecretOverwriteRefusedError";
|
|
107984
107887
|
}
|
|
107985
107888
|
};
|
|
107986
|
-
function isRecord$
|
|
107889
|
+
function isRecord$5(value) {
|
|
107987
107890
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
107988
107891
|
}
|
|
107989
107892
|
function resolvedValue(pass, marker) {
|
|
@@ -108012,7 +107915,7 @@ function inArray(pass, incoming, stored) {
|
|
|
108012
107915
|
return incoming.map((item, index) => {
|
|
108013
107916
|
const twin = stored[index];
|
|
108014
107917
|
if (isSecretMarker(twin)) return typeof item === "string" && item === resolvedValue(pass, twin) ? twin : item;
|
|
108015
|
-
if (isRecord$
|
|
107918
|
+
if (isRecord$5(item) && isRecord$5(twin)) return inArrayRow(pass, item, twin);
|
|
108016
107919
|
if (Array.isArray(item) && Array.isArray(twin)) return inArray(pass, item, twin);
|
|
108017
107920
|
return item;
|
|
108018
107921
|
});
|
|
@@ -108023,7 +107926,7 @@ function inArrayRow(pass, incoming, stored) {
|
|
|
108023
107926
|
const twin = stored[key];
|
|
108024
107927
|
if (isSecretMarker(twin)) {
|
|
108025
107928
|
if (typeof value === "string" && value === resolvedValue(pass, twin)) out[key] = twin;
|
|
108026
|
-
} else if (isRecord$
|
|
107929
|
+
} else if (isRecord$5(value) && isRecord$5(twin)) out[key] = inArrayRow(pass, value, twin);
|
|
108027
107930
|
else if (Array.isArray(value) && Array.isArray(twin)) out[key] = inArray(pass, value, twin);
|
|
108028
107931
|
}
|
|
108029
107932
|
return out;
|
|
@@ -108031,7 +107934,7 @@ function inArrayRow(pass, incoming, stored) {
|
|
|
108031
107934
|
async function walk(pass, incoming, stored) {
|
|
108032
107935
|
if (isSecretMarker(stored)) return atMarker(pass, incoming, stored);
|
|
108033
107936
|
if (Array.isArray(incoming) && Array.isArray(stored)) return inArray(pass, incoming, stored);
|
|
108034
|
-
if (!isRecord$
|
|
107937
|
+
if (!isRecord$5(incoming) || !isRecord$5(stored)) return incoming;
|
|
108035
107938
|
const out = { ...incoming };
|
|
108036
107939
|
for (const [key, value] of Object.entries(incoming)) out[key] = await walk(pass, value, stored[key]);
|
|
108037
107940
|
return out;
|
|
@@ -108062,7 +107965,7 @@ async function restoreUnchangedSecrets(input) {
|
|
|
108062
107965
|
function isJsonEqual(a, b) {
|
|
108063
107966
|
if (Object.is(a, b)) return true;
|
|
108064
107967
|
if (Array.isArray(a)) return Array.isArray(b) && a.length === b.length && a.every((v, i) => isJsonEqual(v, b[i]));
|
|
108065
|
-
if (isRecord$
|
|
107968
|
+
if (isRecord$5(a) && isRecord$5(b)) {
|
|
108066
107969
|
const keys = Object.keys(a);
|
|
108067
107970
|
if (keys.length !== Object.keys(b).length) return false;
|
|
108068
107971
|
return keys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && isJsonEqual(a[k], b[k]));
|
|
@@ -108086,7 +107989,7 @@ function markerKeysOf(blob, owner) {
|
|
|
108086
107989
|
}
|
|
108087
107990
|
function nodeFromStored(value, owner) {
|
|
108088
107991
|
if (isSecretMarker(value)) return isSecretRefOwnedBy(value.$secret, owner) ? { kind: "leaf" } : null;
|
|
108089
|
-
if (!isRecord$
|
|
107992
|
+
if (!isRecord$5(value)) return null;
|
|
108090
107993
|
const children = /* @__PURE__ */ new Map();
|
|
108091
107994
|
for (const [key, child] of Object.entries(value)) {
|
|
108092
107995
|
const node = nodeFromStored(child, owner);
|
|
@@ -108705,6 +108608,44 @@ var BaseAddon = class {
|
|
|
108705
108608
|
return null;
|
|
108706
108609
|
}
|
|
108707
108610
|
/**
|
|
108611
|
+
* D986 scoped settings: the form shape of the CLUSTER-wide settings one capability this addon
|
|
108612
|
+
* provides owns, or `null` when it has none. `cap` is a capability name the addon's manifest
|
|
108613
|
+
* declares. Stored as `addon-settings` rows `<addonId>.<field>`. The keys of this schema, of
|
|
108614
|
+
* {@link nodeSettingsSchema} and of {@link deviceDefaultsSchema} are disjoint, and a key never
|
|
108615
|
+
* contains `@` (guard `check-scoped-settings-declared`). SHAPE only: values are read by scope.
|
|
108616
|
+
*/
|
|
108617
|
+
async clusterSettingsSchema(_cap) {
|
|
108618
|
+
return null;
|
|
108619
|
+
}
|
|
108620
|
+
/**
|
|
108621
|
+
* D986: the form shape of the settings one capability owns PER NODE, or `null`. Stored as
|
|
108622
|
+
* `addon-settings` rows `<addonId>.<field>@<nodeId>` carrying `nodeId` in the row data, so a
|
|
108623
|
+
* forgotten node's rows go in one central delete.
|
|
108624
|
+
*/
|
|
108625
|
+
async nodeSettingsSchema(_cap) {
|
|
108626
|
+
return null;
|
|
108627
|
+
}
|
|
108628
|
+
/**
|
|
108629
|
+
* D986: the form shape of the FLEET DEFAULTS of the per-device settings one capability owns,
|
|
108630
|
+
* or `null`. Stored as `addon-settings` rows `<addonId>.<field>`; a device resolves its value
|
|
108631
|
+
* as device row, then this default, then the schema default (`resolveDeviceSettings`).
|
|
108632
|
+
*/
|
|
108633
|
+
async deviceDefaultsSchema(_cap) {
|
|
108634
|
+
return null;
|
|
108635
|
+
}
|
|
108636
|
+
/**
|
|
108637
|
+
* The host's read of the scoped declarations: the schema one capability declares for `scope`,
|
|
108638
|
+
* or `null`. Beside {@link getIntegrationFormSchema}, the same shape: the hook is protected, the
|
|
108639
|
+
* host asks this method.
|
|
108640
|
+
*/
|
|
108641
|
+
async getScopedSettingsSchema(scope, cap) {
|
|
108642
|
+
switch (scope) {
|
|
108643
|
+
case "cluster": return this.clusterSettingsSchema(cap);
|
|
108644
|
+
case "node": return this.nodeSettingsSchema(cap);
|
|
108645
|
+
case "device-defaults": return this.deviceDefaultsSchema(cap);
|
|
108646
|
+
}
|
|
108647
|
+
}
|
|
108648
|
+
/**
|
|
108708
108649
|
* What the create wizard and the integration edit page render for one instance: the addon's
|
|
108709
108650
|
* per-instance schema when it declares one, else its global schema (what they always showed).
|
|
108710
108651
|
*/
|
|
@@ -109498,6 +109439,21 @@ function normalizeAddonInitResult(result) {
|
|
|
109498
109439
|
if (Array.isArray(result)) return { providers: result };
|
|
109499
109440
|
return result;
|
|
109500
109441
|
}
|
|
109442
|
+
/** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
|
|
109443
|
+
/** The files a kind may live in. `history` is device-history's (D850), never records. */
|
|
109444
|
+
var RECORD_DATABASES = ["settings", "events"];
|
|
109445
|
+
/** The classes of a kind: the `@durable` vocabulary, one authority (replaces the annotation). */
|
|
109446
|
+
var RECORD_CLASSES = [
|
|
109447
|
+
"config",
|
|
109448
|
+
"ledger",
|
|
109449
|
+
"audit",
|
|
109450
|
+
"mirror",
|
|
109451
|
+
"ephemeral"
|
|
109452
|
+
];
|
|
109453
|
+
var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
|
|
109454
|
+
var RECORD_MAX_DATA_BYTES = 1024 * 1024;
|
|
109455
|
+
var RECORD_BATCH_MAX = 1e3;
|
|
109456
|
+
var RECORD_QUERY_MAX = 1e3;
|
|
109501
109457
|
/**
|
|
109502
109458
|
* The share-view scope kinds a share-link token can carry. The hub mints
|
|
109503
109459
|
* and validates them (`ShareViewGrantSchema` builds its
|
|
@@ -109522,21 +109478,6 @@ var PeerBytesTicketSchema = object({
|
|
|
109522
109478
|
*/
|
|
109523
109479
|
declaredBytes: number().int().nonnegative().nullable()
|
|
109524
109480
|
});
|
|
109525
|
-
/** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
|
|
109526
|
-
/** The files a kind may live in. `history` is device-history's (D850), never records. */
|
|
109527
|
-
var RECORD_DATABASES = ["settings", "events"];
|
|
109528
|
-
/** The classes of a kind: the `@durable` vocabulary, one authority (replaces the annotation). */
|
|
109529
|
-
var RECORD_CLASSES = [
|
|
109530
|
-
"config",
|
|
109531
|
-
"ledger",
|
|
109532
|
-
"audit",
|
|
109533
|
-
"mirror",
|
|
109534
|
-
"ephemeral"
|
|
109535
|
-
];
|
|
109536
|
-
var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
|
|
109537
|
-
var RECORD_MAX_DATA_BYTES = 1024 * 1024;
|
|
109538
|
-
var RECORD_BATCH_MAX = 1e3;
|
|
109539
|
-
var RECORD_QUERY_MAX = 1e3;
|
|
109540
109481
|
function defOf(schema) {
|
|
109541
109482
|
const zodBag = Reflect.get(schema, "_zod");
|
|
109542
109483
|
if (typeof zodBag !== "object" || zodBag === null) return {};
|
|
@@ -110148,6 +110089,81 @@ var ProfileRtspEntrySchema = object({
|
|
|
110148
110089
|
codec: string().optional(),
|
|
110149
110090
|
resolution: CamStreamResolutionSchema.optional()
|
|
110150
110091
|
});
|
|
110092
|
+
/** Every debug channel's name starts with this; the ring denies it by default. */
|
|
110093
|
+
var DEBUG_CHANNEL_PREFIX = "debug.";
|
|
110094
|
+
function isDebugChannelName(name) {
|
|
110095
|
+
return name.startsWith(DEBUG_CHANNEL_PREFIX);
|
|
110096
|
+
}
|
|
110097
|
+
/**
|
|
110098
|
+
* Who is watching what, in THIS process. In-memory, moved off the log path.
|
|
110099
|
+
*
|
|
110100
|
+
* `active` is a plain boolean FIELD, read first: with no watch anywhere, a
|
|
110101
|
+
* `ctx.debug` call is one load and one branch.
|
|
110102
|
+
*/
|
|
110103
|
+
var DebugInterest = class {
|
|
110104
|
+
active = false;
|
|
110105
|
+
windows = /* @__PURE__ */ new Map();
|
|
110106
|
+
/**
|
|
110107
|
+
* Replace the interest with the FULL set of windows. Non-debug windows are
|
|
110108
|
+
* ignored and an absent one is dropped, so a lost disarm cannot leave a
|
|
110109
|
+
* channel running.
|
|
110110
|
+
*/
|
|
110111
|
+
apply(windows, nowMs) {
|
|
110112
|
+
this.windows.clear();
|
|
110113
|
+
for (const window of windows) {
|
|
110114
|
+
if (!isDebugChannelName(window.channel) || window.armedUntilMs <= nowMs) continue;
|
|
110115
|
+
const ids = window.deviceIds;
|
|
110116
|
+
this.windows.set(window.channel, {
|
|
110117
|
+
untilMs: window.armedUntilMs,
|
|
110118
|
+
devices: ids === null || ids.length === 0 ? null : new Set(ids)
|
|
110119
|
+
});
|
|
110120
|
+
}
|
|
110121
|
+
this.active = this.windows.size > 0;
|
|
110122
|
+
}
|
|
110123
|
+
/** Drop what has lapsed. Called on a timer, never from a log path. */
|
|
110124
|
+
tick(nowMs) {
|
|
110125
|
+
const closed = [];
|
|
110126
|
+
for (const [name, window] of this.windows) if (window.untilMs <= nowMs) {
|
|
110127
|
+
this.windows.delete(name);
|
|
110128
|
+
closed.push(name);
|
|
110129
|
+
}
|
|
110130
|
+
this.active = this.windows.size > 0;
|
|
110131
|
+
return closed;
|
|
110132
|
+
}
|
|
110133
|
+
/** `channel` is the full name, `debug.<channel>`. */
|
|
110134
|
+
wants(channel, deviceId) {
|
|
110135
|
+
if (!this.active) return false;
|
|
110136
|
+
const window = this.windows.get(channel);
|
|
110137
|
+
if (window === void 0) return false;
|
|
110138
|
+
return window.devices === null || window.devices.has(deviceId);
|
|
110139
|
+
}
|
|
110140
|
+
};
|
|
110141
|
+
var interestInstance = null;
|
|
110142
|
+
function getDebugInterest() {
|
|
110143
|
+
interestInstance ??= new DebugInterest();
|
|
110144
|
+
return interestInstance;
|
|
110145
|
+
}
|
|
110146
|
+
/**
|
|
110147
|
+
* Build `ctx.debug`. The thunk runs ONLY when somebody watches this
|
|
110148
|
+
* (channel, device) pair, and the line carries `tags: { deviceId }` plus the
|
|
110149
|
+
* `debug.<channel>` name as `logChannel`.
|
|
110150
|
+
*/
|
|
110151
|
+
function createDebugEmitter(logger, interest = getDebugInterest()) {
|
|
110152
|
+
return (channel, deviceId, thunk) => {
|
|
110153
|
+
if (!interest.active) return;
|
|
110154
|
+
const name = `${DEBUG_CHANNEL_PREFIX}${channel}`;
|
|
110155
|
+
if (!interest.wants(name, deviceId)) return;
|
|
110156
|
+
const line = thunk();
|
|
110157
|
+
const tags = {
|
|
110158
|
+
deviceId,
|
|
110159
|
+
logChannel: name
|
|
110160
|
+
};
|
|
110161
|
+
logger.info(line.message, line.meta === void 0 ? { tags } : {
|
|
110162
|
+
tags,
|
|
110163
|
+
meta: line.meta
|
|
110164
|
+
});
|
|
110165
|
+
};
|
|
110166
|
+
}
|
|
110151
110167
|
/**
|
|
110152
110168
|
* The model weight formats a catalog entry can ship. Schema-free on purpose:
|
|
110153
110169
|
* `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
|
|
@@ -110423,6 +110439,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
110423
110439
|
"humidity-sensor",
|
|
110424
110440
|
"image",
|
|
110425
110441
|
"image-settings",
|
|
110442
|
+
"integration-instances",
|
|
110426
110443
|
"integrations",
|
|
110427
110444
|
"intercom",
|
|
110428
110445
|
"ir-transmitter",
|
|
@@ -112394,7 +112411,7 @@ function sleep(ms) {
|
|
|
112394
112411
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
112395
112412
|
}
|
|
112396
112413
|
//#endregion
|
|
112397
|
-
//#region ../types/dist/composition-
|
|
112414
|
+
//#region ../types/dist/composition-Dh3RfMx5.mjs
|
|
112398
112415
|
/**
|
|
112399
112416
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
112400
112417
|
*
|
|
@@ -113608,6 +113625,94 @@ var NodeColorIdSchema = _enum([
|
|
|
113608
113625
|
"node-g",
|
|
113609
113626
|
"node-h"
|
|
113610
113627
|
]);
|
|
113628
|
+
//#endregion
|
|
113629
|
+
//#region ../types/dist/index.mjs
|
|
113630
|
+
/**
|
|
113631
|
+
* The `smtp-inbound` broker kind (D999): a mailbox a device can send mail to.
|
|
113632
|
+
*
|
|
113633
|
+
* The `broker` cap carries these as `Record<string, unknown>` on the wire; the
|
|
113634
|
+
* provider and its consumers parse them with the schemas below at BOTH ends, so
|
|
113635
|
+
* the shape is declared once.
|
|
113636
|
+
*/
|
|
113637
|
+
/** The broker kind tag. */
|
|
113638
|
+
var SMTP_INBOUND_KIND = "smtp-inbound";
|
|
113639
|
+
/** The id the first mailbox is minted under (adopt-id), so a consumer can find it again. */
|
|
113640
|
+
var SMTP_INBOUND_DEFAULT_ID = "smtp-inbound-default";
|
|
113641
|
+
var SMTP_INBOUND_DEFAULT_PORT = 2525;
|
|
113642
|
+
var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
|
|
113643
|
+
var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
|
|
113644
|
+
/** The text excerpt a message carries on the bus. */
|
|
113645
|
+
var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
|
|
113646
|
+
var PortSchema = number().int().min(1).max(65535);
|
|
113647
|
+
/** A recipient local-part: what stands before the `@`. */
|
|
113648
|
+
var SmtpRecipientSchema = string().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
|
|
113649
|
+
object({
|
|
113650
|
+
port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
|
|
113651
|
+
bindHost: string().min(1).default("0.0.0.0"),
|
|
113652
|
+
domain: string().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
|
|
113653
|
+
requireAuth: boolean().default(true),
|
|
113654
|
+
username: string().default(""),
|
|
113655
|
+
password: string().default("").meta({ secret: true }),
|
|
113656
|
+
starttls: boolean().default(false),
|
|
113657
|
+
maxMessageBytes: number().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
|
|
113658
|
+
});
|
|
113659
|
+
/**
|
|
113660
|
+
* What `getBrokerConfig` answers: what a device needs to send mail here.
|
|
113661
|
+
* `host` is null when the `internal` role of the network palette is not set;
|
|
113662
|
+
* `hostReason` then says where to set it.
|
|
113663
|
+
*/
|
|
113664
|
+
var SmtpInboundConfigSchema = object({
|
|
113665
|
+
host: string().min(1).nullable(),
|
|
113666
|
+
hostReason: string().nullable(),
|
|
113667
|
+
port: PortSchema,
|
|
113668
|
+
domain: string().min(1),
|
|
113669
|
+
requireAuth: boolean(),
|
|
113670
|
+
username: string(),
|
|
113671
|
+
password: string(),
|
|
113672
|
+
starttls: boolean()
|
|
113673
|
+
});
|
|
113674
|
+
object({
|
|
113675
|
+
recipients: array(SmtpRecipientSchema).min(1),
|
|
113676
|
+
/**
|
|
113677
|
+
* Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
|
|
113678
|
+
* recipients (the way a consumer that restarted, or lost a device, declares its set again);
|
|
113679
|
+
* a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
|
|
113680
|
+
* Absent, the subscriber is anonymous and unique.
|
|
113681
|
+
*/
|
|
113682
|
+
subscriberId: string().min(1).max(64).optional()
|
|
113683
|
+
});
|
|
113684
|
+
var SmtpInboundAttachmentSchema = object({
|
|
113685
|
+
filename: string(),
|
|
113686
|
+
contentType: string(),
|
|
113687
|
+
bytes: number().int().nonnegative()
|
|
113688
|
+
});
|
|
113689
|
+
/** The payload of one `broker.message` (the key is the recipient local-part). */
|
|
113690
|
+
var SmtpInboundMessageSchema = object({
|
|
113691
|
+
/** Monotonic per mailbox; a gap tells a consumer it missed an event. */
|
|
113692
|
+
seq: number().int().positive(),
|
|
113693
|
+
messageId: string(),
|
|
113694
|
+
receivedAt: number().int().nonnegative(),
|
|
113695
|
+
from: string(),
|
|
113696
|
+
to: string(),
|
|
113697
|
+
subject: string(),
|
|
113698
|
+
text: string().max(SMTP_INBOUND_TEXT_MAX_CHARS),
|
|
113699
|
+
attachments: array(SmtpInboundAttachmentSchema)
|
|
113700
|
+
});
|
|
113701
|
+
/** What `getState('since:<seq>')` answers: every retained message after `seq`. */
|
|
113702
|
+
var SmtpInboundSinceSchema = object({
|
|
113703
|
+
/** The newest seq issued so far (0 before any mail). */
|
|
113704
|
+
lastSeq: number().int().nonnegative(),
|
|
113705
|
+
/** The oldest seq still retained; messages below it are gone. */
|
|
113706
|
+
oldestSeq: number().int().nonnegative(),
|
|
113707
|
+
messages: array(object({
|
|
113708
|
+
key: string(),
|
|
113709
|
+
message: SmtpInboundMessageSchema
|
|
113710
|
+
}))
|
|
113711
|
+
});
|
|
113712
|
+
/** The key a consumer passes to `getState` to replay the ring, and its parser. */
|
|
113713
|
+
function smtpSinceKey(seq) {
|
|
113714
|
+
return `since:${seq}`;
|
|
113715
|
+
}
|
|
113611
113716
|
new Set([
|
|
113612
113717
|
"track",
|
|
113613
113718
|
"face",
|
|
@@ -116773,81 +116878,6 @@ var TIMEZONES = [
|
|
|
116773
116878
|
function findTimezone(id) {
|
|
116774
116879
|
return TIMEZONES.find((tz) => tz.id === id);
|
|
116775
116880
|
}
|
|
116776
|
-
/** Every debug channel's name starts with this; the ring denies it by default. */
|
|
116777
|
-
var DEBUG_CHANNEL_PREFIX = "debug.";
|
|
116778
|
-
function isDebugChannelName(name) {
|
|
116779
|
-
return name.startsWith(DEBUG_CHANNEL_PREFIX);
|
|
116780
|
-
}
|
|
116781
|
-
/**
|
|
116782
|
-
* Who is watching what, in THIS process. In-memory, moved off the log path.
|
|
116783
|
-
*
|
|
116784
|
-
* `active` is a plain boolean FIELD, read first: with no watch anywhere, a
|
|
116785
|
-
* `ctx.debug` call is one load and one branch.
|
|
116786
|
-
*/
|
|
116787
|
-
var DebugInterest = class {
|
|
116788
|
-
active = false;
|
|
116789
|
-
windows = /* @__PURE__ */ new Map();
|
|
116790
|
-
/**
|
|
116791
|
-
* Replace the interest with the FULL set of windows. Non-debug windows are
|
|
116792
|
-
* ignored and an absent one is dropped, so a lost disarm cannot leave a
|
|
116793
|
-
* channel running.
|
|
116794
|
-
*/
|
|
116795
|
-
apply(windows, nowMs) {
|
|
116796
|
-
this.windows.clear();
|
|
116797
|
-
for (const window of windows) {
|
|
116798
|
-
if (!isDebugChannelName(window.channel) || window.armedUntilMs <= nowMs) continue;
|
|
116799
|
-
const ids = window.deviceIds;
|
|
116800
|
-
this.windows.set(window.channel, {
|
|
116801
|
-
untilMs: window.armedUntilMs,
|
|
116802
|
-
devices: ids === null || ids.length === 0 ? null : new Set(ids)
|
|
116803
|
-
});
|
|
116804
|
-
}
|
|
116805
|
-
this.active = this.windows.size > 0;
|
|
116806
|
-
}
|
|
116807
|
-
/** Drop what has lapsed. Called on a timer, never from a log path. */
|
|
116808
|
-
tick(nowMs) {
|
|
116809
|
-
const closed = [];
|
|
116810
|
-
for (const [name, window] of this.windows) if (window.untilMs <= nowMs) {
|
|
116811
|
-
this.windows.delete(name);
|
|
116812
|
-
closed.push(name);
|
|
116813
|
-
}
|
|
116814
|
-
this.active = this.windows.size > 0;
|
|
116815
|
-
return closed;
|
|
116816
|
-
}
|
|
116817
|
-
/** `channel` is the full name, `debug.<channel>`. */
|
|
116818
|
-
wants(channel, deviceId) {
|
|
116819
|
-
if (!this.active) return false;
|
|
116820
|
-
const window = this.windows.get(channel);
|
|
116821
|
-
if (window === void 0) return false;
|
|
116822
|
-
return window.devices === null || window.devices.has(deviceId);
|
|
116823
|
-
}
|
|
116824
|
-
};
|
|
116825
|
-
var interestInstance = null;
|
|
116826
|
-
function getDebugInterest() {
|
|
116827
|
-
interestInstance ??= new DebugInterest();
|
|
116828
|
-
return interestInstance;
|
|
116829
|
-
}
|
|
116830
|
-
/**
|
|
116831
|
-
* Build `ctx.debug`. The thunk runs ONLY when somebody watches this
|
|
116832
|
-
* (channel, device) pair, and the line carries `tags: { deviceId }` plus the
|
|
116833
|
-
* `debug.<channel>` name as `logChannel`.
|
|
116834
|
-
*/
|
|
116835
|
-
function createDebugEmitter(logger, interest = getDebugInterest()) {
|
|
116836
|
-
return (channel, deviceId, thunk) => {
|
|
116837
|
-
if (!interest.active) return;
|
|
116838
|
-
const name = `${DEBUG_CHANNEL_PREFIX}${channel}`;
|
|
116839
|
-
if (!interest.wants(name, deviceId)) return;
|
|
116840
|
-
const line = thunk();
|
|
116841
|
-
const tags = {
|
|
116842
|
-
deviceId,
|
|
116843
|
-
logChannel: name
|
|
116844
|
-
};
|
|
116845
|
-
logger.info(line.message, line.meta === void 0 ? { tags } : {
|
|
116846
|
-
tags,
|
|
116847
|
-
meta: line.meta
|
|
116848
|
-
});
|
|
116849
|
-
};
|
|
116850
|
-
}
|
|
116851
116881
|
/**
|
|
116852
116882
|
* Per-component log CHANNELS — the gate a hot path consults, and the registry
|
|
116853
116883
|
* an addon declares its channels in.
|
|
@@ -122524,6 +122554,7 @@ var SYSTEM_CAP_VOCABULARY = {
|
|
|
122524
122554
|
"load-contribution": system(INTERNAL),
|
|
122525
122555
|
"local-network": system(NETWORK),
|
|
122526
122556
|
"log-channels": system(INTERNAL),
|
|
122557
|
+
"integration-instances": system(INTERNAL),
|
|
122527
122558
|
"log-destination": system(INTERNAL),
|
|
122528
122559
|
"login-method": system(IDENTITY),
|
|
122529
122560
|
"mesh-network": system(NETWORK),
|
|
@@ -144730,7 +144761,7 @@ var TargetKindCapsSchema = object({
|
|
|
144730
144761
|
* the union is large and not meant for runtime validation here; the exported
|
|
144731
144762
|
* `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
|
|
144732
144763
|
*/
|
|
144733
|
-
var ConfigSchemaPassthrough$
|
|
144764
|
+
var ConfigSchemaPassthrough$4 = unknown();
|
|
144734
144765
|
var TargetKindSchema = object({
|
|
144735
144766
|
kind: string(),
|
|
144736
144767
|
label: string(),
|
|
@@ -144767,7 +144798,7 @@ var TargetKindSchema = object({
|
|
|
144767
144798
|
* its raster path, which is the safe default for an unknown image.
|
|
144768
144799
|
*/
|
|
144769
144800
|
iconMediaType: string().optional(),
|
|
144770
|
-
configSchema: ConfigSchemaPassthrough$
|
|
144801
|
+
configSchema: ConfigSchemaPassthrough$4,
|
|
144771
144802
|
/**
|
|
144772
144803
|
* The config keys that decide WHERE this kind's secrets are sent (server,
|
|
144773
144804
|
* base URL, instance, chat, recipient). A save that changes any of them
|
|
@@ -153663,10 +153694,23 @@ var DeviceDataEntrySchema = object({
|
|
|
153663
153694
|
/** Rows (or files) held for the device in the store. */
|
|
153664
153695
|
count: number().int().nonnegative()
|
|
153665
153696
|
});
|
|
153666
|
-
|
|
153667
|
-
|
|
153668
|
-
|
|
153669
|
-
|
|
153697
|
+
var deviceRemovalHookCapability = {
|
|
153698
|
+
name: "device-removal-hook",
|
|
153699
|
+
scope: "system",
|
|
153700
|
+
mode: "collection",
|
|
153701
|
+
internal: true,
|
|
153702
|
+
methods: {
|
|
153703
|
+
/** Delete everything this addon keeps for the deleted device. Idempotent. */
|
|
153704
|
+
onDeviceRemoved: method(DeviceRemovedInputSchema, DeviceRemovedResultSchema, {
|
|
153705
|
+
kind: "mutation",
|
|
153706
|
+
auth: "admin"
|
|
153707
|
+
}),
|
|
153708
|
+
/** Which devices this addon holds data for, per store. Reads only. */
|
|
153709
|
+
listDeviceData: method(_void(), array(DeviceDataEntrySchema).readonly(), { auth: "admin" })
|
|
153710
|
+
},
|
|
153711
|
+
/** In-process only — enumerated through `addons.listCapabilityProviders`. */
|
|
153712
|
+
mount: { kind: "skip" }
|
|
153713
|
+
};
|
|
153670
153714
|
/**
|
|
153671
153715
|
* device-runs - the durable record of ONE run of a device: a washing cycle, a
|
|
153672
153716
|
* dishwasher cycle, later a mow or a clean (D992).
|
|
@@ -156529,6 +156573,89 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
156529
156573
|
auth: "admin"
|
|
156530
156574
|
});
|
|
156531
156575
|
/**
|
|
156576
|
+
* `integration-instances` - the connection of an addon that holds ONE or SEVERAL
|
|
156577
|
+
* (an account, a relay, a destination) as typed records (D985, D986). The addon's
|
|
156578
|
+
* own function page reads and writes them (Logging for Loki); the Integrations
|
|
156579
|
+
* page lists only the integrations that bring devices (D996).
|
|
156580
|
+
*
|
|
156581
|
+
* A collection of providers: each addon that holds connections registers one and a
|
|
156582
|
+
* caller reaches a provider with the pinned `addonId`.
|
|
156583
|
+
*
|
|
156584
|
+
* Identity: the instance id is immutable and opaque to the caller. A record the
|
|
156585
|
+
* Loki migration made keeps the `integrations` registry id it was created under
|
|
156586
|
+
* (`int_NNNN`); a single instance made by its function page takes the addon id.
|
|
156587
|
+
* `name` is the editable label.
|
|
156588
|
+
*
|
|
156589
|
+
* Secrets: every field the instance kind marks `.meta({ secret: true })` reaches
|
|
156590
|
+
* the form as a `SecretFieldView`, never text (D789). A patch value omitted, `''`
|
|
156591
|
+
* or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
|
|
156592
|
+
* clears it.
|
|
156593
|
+
*/
|
|
156594
|
+
var ConfigSchemaPassthrough$3 = custom();
|
|
156595
|
+
var IntegrationInstanceIdSchema = string().min(1);
|
|
156596
|
+
var IntegrationInstanceDescriptionSchema = object({
|
|
156597
|
+
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
156598
|
+
instanceMode: _enum(["single", "multiple"]),
|
|
156599
|
+
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
156600
|
+
kind: string(),
|
|
156601
|
+
/** True when `testInstance` really probes the remote. */
|
|
156602
|
+
canTest: boolean()
|
|
156603
|
+
});
|
|
156604
|
+
var IntegrationInstanceStatusSchema = object({
|
|
156605
|
+
state: _enum([
|
|
156606
|
+
"ok",
|
|
156607
|
+
"off",
|
|
156608
|
+
"error",
|
|
156609
|
+
"unknown"
|
|
156610
|
+
]),
|
|
156611
|
+
detail: string().optional()
|
|
156612
|
+
});
|
|
156613
|
+
var IntegrationInstanceSchema = object({
|
|
156614
|
+
id: IntegrationInstanceIdSchema,
|
|
156615
|
+
name: string(),
|
|
156616
|
+
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
156617
|
+
configured: boolean(),
|
|
156618
|
+
/**
|
|
156619
|
+
* The instance's own on/off switch (D996): always present, derived from the record body
|
|
156620
|
+
* (`enabled !== false`, so a row written before the field existed is on).
|
|
156621
|
+
*/
|
|
156622
|
+
enabled: boolean(),
|
|
156623
|
+
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
156624
|
+
formValues: record(string(), unknown()),
|
|
156625
|
+
status: IntegrationInstanceStatusSchema.optional()
|
|
156626
|
+
});
|
|
156627
|
+
var IntegrationInstancePatchSchema = object({
|
|
156628
|
+
id: IntegrationInstanceIdSchema,
|
|
156629
|
+
name: string().min(1).optional(),
|
|
156630
|
+
/** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
|
|
156631
|
+
values: record(string(), unknown())
|
|
156632
|
+
});
|
|
156633
|
+
method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }), method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }), method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }), method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }), method(object({
|
|
156634
|
+
id: IntegrationInstanceIdSchema,
|
|
156635
|
+
name: string().min(1)
|
|
156636
|
+
}), object({ id: string() }), {
|
|
156637
|
+
kind: "mutation",
|
|
156638
|
+
auth: "admin"
|
|
156639
|
+
}), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
|
|
156640
|
+
kind: "mutation",
|
|
156641
|
+
auth: "admin"
|
|
156642
|
+
}), method(object({
|
|
156643
|
+
id: IntegrationInstanceIdSchema,
|
|
156644
|
+
enabled: boolean()
|
|
156645
|
+
}), object({ ok: literal(true) }), {
|
|
156646
|
+
kind: "mutation",
|
|
156647
|
+
auth: "admin"
|
|
156648
|
+
}), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
|
|
156649
|
+
kind: "mutation",
|
|
156650
|
+
auth: "admin"
|
|
156651
|
+
}), optionalMethod(object({
|
|
156652
|
+
id: IntegrationInstanceIdSchema.nullable(),
|
|
156653
|
+
values: record(string(), unknown())
|
|
156654
|
+
}), TestConnectionResultSchema$1, {
|
|
156655
|
+
kind: "mutation",
|
|
156656
|
+
auth: "admin"
|
|
156657
|
+
});
|
|
156658
|
+
/**
|
|
156532
156659
|
* Two-way audio / talk-back cap. Clients open a WebRTC audio-only
|
|
156533
156660
|
* session (server-initiated offer pattern, identical to the main
|
|
156534
156661
|
* video WebRTC flow) and the browser captures the mic; the server
|
|
@@ -167337,7 +167464,14 @@ var NcMediaPolicySchema = object({
|
|
|
167337
167464
|
*/
|
|
167338
167465
|
profile: CamProfileSchema.optional(),
|
|
167339
167466
|
/** See {@link NcLinkedCameraModeSchema}. */
|
|
167340
|
-
linkedCameras: NcLinkedCameraModeSchema.optional()
|
|
167467
|
+
linkedCameras: NcLinkedCameraModeSchema.optional(),
|
|
167468
|
+
/**
|
|
167469
|
+
* Sensor rules only: may this rule wake a SLEEPING battery camera to take
|
|
167470
|
+
* the linked still it attaches (D998)? Absent means yes, an explicit `false`
|
|
167471
|
+
* opts out. Deliberately not a Zod default: a rule that never chose keeps
|
|
167472
|
+
* following the hub's. One bounded wake per event; nothing is attached later.
|
|
167473
|
+
*/
|
|
167474
|
+
wakeSleepingCamera: boolean().optional()
|
|
167341
167475
|
});
|
|
167342
167476
|
/**
|
|
167343
167477
|
* Cooldown GRANULARITY over the subject's class — how much a fired
|
|
@@ -176500,7 +176634,16 @@ var smokeCapability = {
|
|
|
176500
176634
|
* future fields without breaking the existing call sites.
|
|
176501
176635
|
*/
|
|
176502
176636
|
var EmailAddressSchema = email();
|
|
176637
|
+
/**
|
|
176638
|
+
* Which relay of a multi-relay provider answers (D996). Absent = the provider's first / default
|
|
176639
|
+
* relay, so a caller written before relays existed keeps working unchanged. A provider with one
|
|
176640
|
+
* fixed relay ignores it. This is the integration instance id (`int_NNNN`), NOT the addon id: the
|
|
176641
|
+
* collection pin `addonId` still picks the provider, `instanceId` picks the relay inside it.
|
|
176642
|
+
*/
|
|
176643
|
+
var InstanceIdSchema = string().min(1).optional();
|
|
176644
|
+
var RelayPinSchema = object({ instanceId: InstanceIdSchema });
|
|
176503
176645
|
var SendEmailInputSchema = object({
|
|
176646
|
+
instanceId: InstanceIdSchema,
|
|
176504
176647
|
to: union([EmailAddressSchema, array(EmailAddressSchema).min(1)]),
|
|
176505
176648
|
cc: array(EmailAddressSchema).optional(),
|
|
176506
176649
|
bcc: array(EmailAddressSchema).optional(),
|
|
@@ -176536,11 +176679,11 @@ method(SendEmailInputSchema, SendEmailResultSchema, {
|
|
|
176536
176679
|
kind: "mutation",
|
|
176537
176680
|
auth: "admin",
|
|
176538
176681
|
access: "create"
|
|
176539
|
-
}), method(
|
|
176682
|
+
}), method(RelayPinSchema, SmtpStatusSchema, {
|
|
176540
176683
|
kind: "mutation",
|
|
176541
176684
|
auth: "admin",
|
|
176542
176685
|
access: "view"
|
|
176543
|
-
}), method(
|
|
176686
|
+
}), method(RelayPinSchema, SmtpStatusSchema, { auth: "admin" });
|
|
176544
176687
|
/**
|
|
176545
176688
|
* A single device snapshot returned as base64 JPEG/PNG.
|
|
176546
176689
|
*
|
|
@@ -176608,10 +176751,12 @@ var snapshotCapability = {
|
|
|
176608
176751
|
* even when the cache is well within the device's
|
|
176609
176752
|
* `snapshotMaxAgeS` window.
|
|
176610
176753
|
*
|
|
176611
|
-
* **`force` is
|
|
176754
|
+
* **`force` is a demand with a named principal, not a freshness
|
|
176755
|
+
* preference** — an operator button, the scheduled refresh (D847), or a
|
|
176756
|
+
* notification rule that left the camera's wake allowed (D998). On a
|
|
176612
176757
|
* battery camera it is the one thing that walks past the wrapper's
|
|
176613
|
-
* sleep gate and wakes the camera, so a
|
|
176614
|
-
*
|
|
176758
|
+
* sleep gate and wakes the camera, so a poller, a thumbnail or a
|
|
176759
|
+
* prestream must NEVER set it. Every such caller
|
|
176615
176760
|
* gets the cached frame, which on a sleeping battery camera is the
|
|
176616
176761
|
* correct answer: stale but honest beats woken.
|
|
176617
176762
|
*/
|
|
@@ -181954,7 +182099,7 @@ function withClipEventLabels(provider, deps) {
|
|
|
181954
182099
|
listClips
|
|
181955
182100
|
};
|
|
181956
182101
|
}
|
|
181957
|
-
DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
|
|
182102
|
+
DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
|
|
181958
182103
|
SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
|
|
181959
182104
|
String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
|
|
181960
182105
|
DeviceRole.BinarySensor, DeviceRole.MotionSensor;
|
|
@@ -187152,6 +187297,69 @@ Object.freeze({
|
|
|
187152
187297
|
access: "create",
|
|
187153
187298
|
role: "admin"
|
|
187154
187299
|
},
|
|
187300
|
+
"integrationInstances.createInstance": {
|
|
187301
|
+
capName: "integration-instances",
|
|
187302
|
+
capScope: "system",
|
|
187303
|
+
addonId: null,
|
|
187304
|
+
access: "create",
|
|
187305
|
+
role: "admin"
|
|
187306
|
+
},
|
|
187307
|
+
"integrationInstances.deleteInstance": {
|
|
187308
|
+
capName: "integration-instances",
|
|
187309
|
+
capScope: "system",
|
|
187310
|
+
addonId: null,
|
|
187311
|
+
access: "delete",
|
|
187312
|
+
role: "admin"
|
|
187313
|
+
},
|
|
187314
|
+
"integrationInstances.describe": {
|
|
187315
|
+
capName: "integration-instances",
|
|
187316
|
+
capScope: "system",
|
|
187317
|
+
addonId: null,
|
|
187318
|
+
access: "view",
|
|
187319
|
+
role: "admin"
|
|
187320
|
+
},
|
|
187321
|
+
"integrationInstances.getInstance": {
|
|
187322
|
+
capName: "integration-instances",
|
|
187323
|
+
capScope: "system",
|
|
187324
|
+
addonId: null,
|
|
187325
|
+
access: "view",
|
|
187326
|
+
role: "admin"
|
|
187327
|
+
},
|
|
187328
|
+
"integrationInstances.instanceFormSchema": {
|
|
187329
|
+
capName: "integration-instances",
|
|
187330
|
+
capScope: "system",
|
|
187331
|
+
addonId: null,
|
|
187332
|
+
access: "view",
|
|
187333
|
+
role: "admin"
|
|
187334
|
+
},
|
|
187335
|
+
"integrationInstances.listInstances": {
|
|
187336
|
+
capName: "integration-instances",
|
|
187337
|
+
capScope: "system",
|
|
187338
|
+
addonId: null,
|
|
187339
|
+
access: "view",
|
|
187340
|
+
role: "admin"
|
|
187341
|
+
},
|
|
187342
|
+
"integrationInstances.setInstanceEnabled": {
|
|
187343
|
+
capName: "integration-instances",
|
|
187344
|
+
capScope: "system",
|
|
187345
|
+
addonId: null,
|
|
187346
|
+
access: "create",
|
|
187347
|
+
role: "admin"
|
|
187348
|
+
},
|
|
187349
|
+
"integrationInstances.testInstance": {
|
|
187350
|
+
capName: "integration-instances",
|
|
187351
|
+
capScope: "system",
|
|
187352
|
+
addonId: null,
|
|
187353
|
+
access: "create",
|
|
187354
|
+
role: "admin"
|
|
187355
|
+
},
|
|
187356
|
+
"integrationInstances.updateInstance": {
|
|
187357
|
+
capName: "integration-instances",
|
|
187358
|
+
capScope: "system",
|
|
187359
|
+
addonId: null,
|
|
187360
|
+
access: "create",
|
|
187361
|
+
role: "admin"
|
|
187362
|
+
},
|
|
187155
187363
|
"integrations.create": {
|
|
187156
187364
|
capName: "integrations",
|
|
187157
187365
|
capScope: "system",
|
|
@@ -205787,7 +205995,7 @@ var ReolinkHubSession = class {
|
|
|
205787
205995
|
this.getApi().catch((err) => {
|
|
205788
205996
|
this.#logger.warn("Reolink hub initial connect failed — will retry", {
|
|
205789
205997
|
tags: { integrationId: this.integrationId },
|
|
205790
|
-
meta: { error: errMsg$
|
|
205998
|
+
meta: { error: errMsg$3(err) }
|
|
205791
205999
|
});
|
|
205792
206000
|
});
|
|
205793
206001
|
}
|
|
@@ -205806,7 +206014,7 @@ var ReolinkHubSession = class {
|
|
|
205806
206014
|
this.getApi().catch((err) => {
|
|
205807
206015
|
this.#logger.warn("Reolink hub re-dial after settings change failed", {
|
|
205808
206016
|
tags: { integrationId: this.integrationId },
|
|
205809
|
-
meta: { error: errMsg$
|
|
206017
|
+
meta: { error: errMsg$3(err) }
|
|
205810
206018
|
});
|
|
205811
206019
|
});
|
|
205812
206020
|
}
|
|
@@ -205867,7 +206075,7 @@ var ReolinkHubSession = class {
|
|
|
205867
206075
|
api.client.on("error", (err) => {
|
|
205868
206076
|
this.#logger.warn("Reolink hub socket error — scheduling reconnect", {
|
|
205869
206077
|
tags: { integrationId: this.integrationId },
|
|
205870
|
-
meta: { error: errMsg$
|
|
206078
|
+
meta: { error: errMsg$3(err) }
|
|
205871
206079
|
});
|
|
205872
206080
|
this.#scheduleReconnect("socket-error");
|
|
205873
206081
|
});
|
|
@@ -205936,7 +206144,7 @@ var ReolinkHubSession = class {
|
|
|
205936
206144
|
isMultifocal: d.isMultifocal === true
|
|
205937
206145
|
}));
|
|
205938
206146
|
} catch (err) {
|
|
205939
|
-
lastError = errMsg$
|
|
206147
|
+
lastError = errMsg$3(err);
|
|
205940
206148
|
this.#logger.warn("Reolink hub channel refresh failed", {
|
|
205941
206149
|
tags: { integrationId: this.integrationId },
|
|
205942
206150
|
meta: { error: lastError }
|
|
@@ -206010,7 +206218,7 @@ var ReolinkHubSession = class {
|
|
|
206010
206218
|
meta: {
|
|
206011
206219
|
from: repair.from,
|
|
206012
206220
|
to: repair.to,
|
|
206013
|
-
error: errMsg$
|
|
206221
|
+
error: errMsg$3(err)
|
|
206014
206222
|
}
|
|
206015
206223
|
});
|
|
206016
206224
|
}
|
|
@@ -206123,7 +206331,7 @@ var ReolinkHubSession = class {
|
|
|
206123
206331
|
} catch (err) {
|
|
206124
206332
|
this.#logger.debug("Reolink hub: per-channel battery refresh failed", {
|
|
206125
206333
|
tags: { integrationId: this.integrationId },
|
|
206126
|
-
meta: { error: errMsg$
|
|
206334
|
+
meta: { error: errMsg$3(err) }
|
|
206127
206335
|
});
|
|
206128
206336
|
}
|
|
206129
206337
|
}
|
|
@@ -206171,7 +206379,7 @@ var ReolinkHubSession = class {
|
|
|
206171
206379
|
tags: { deviceId: child.id },
|
|
206172
206380
|
meta: {
|
|
206173
206381
|
channel,
|
|
206174
|
-
error: errMsg$
|
|
206382
|
+
error: errMsg$3(err)
|
|
206175
206383
|
}
|
|
206176
206384
|
});
|
|
206177
206385
|
}
|
|
@@ -209197,7 +209405,7 @@ var IntercomOrchestrator = class {
|
|
|
209197
209405
|
if (this.opts.wakeBeforeStart) try {
|
|
209198
209406
|
await this.opts.wakeBeforeStart();
|
|
209199
209407
|
} catch (err) {
|
|
209200
|
-
this.opts.logger.warn("intercom: pre-wake failed", { meta: { error: errMsg$
|
|
209408
|
+
this.opts.logger.warn("intercom: pre-wake failed", { meta: { error: errMsg$2(err) } });
|
|
209201
209409
|
throw err;
|
|
209202
209410
|
}
|
|
209203
209411
|
const talkSession = new ReolinkIntercomSession({
|
|
@@ -209212,7 +209420,7 @@ var IntercomOrchestrator = class {
|
|
|
209212
209420
|
try {
|
|
209213
209421
|
await talkSession.start();
|
|
209214
209422
|
} catch (err) {
|
|
209215
|
-
this.opts.logger.warn("intercom: talk session open failed", { meta: { error: errMsg$
|
|
209423
|
+
this.opts.logger.warn("intercom: talk session open failed", { meta: { error: errMsg$2(err) } });
|
|
209216
209424
|
throw err;
|
|
209217
209425
|
}
|
|
209218
209426
|
const realCameraRate = talkSession.sampleRate;
|
|
@@ -209231,7 +209439,7 @@ var IntercomOrchestrator = class {
|
|
|
209231
209439
|
} catch (err) {
|
|
209232
209440
|
await talkSession.stop().catch(() => {});
|
|
209233
209441
|
this.opts.logger.warn("intercom: audio-codec createDecodeSession failed", { meta: {
|
|
209234
|
-
error: errMsg$
|
|
209442
|
+
error: errMsg$2(err),
|
|
209235
209443
|
opusRate,
|
|
209236
209444
|
opusChannels,
|
|
209237
209445
|
targetRate
|
|
@@ -209253,7 +209461,7 @@ var IntercomOrchestrator = class {
|
|
|
209253
209461
|
this.session = active;
|
|
209254
209462
|
peer.onOpusFrame((frame, pts) => {
|
|
209255
209463
|
this.handleOpusFrame(active, frame, pts).catch((err) => {
|
|
209256
|
-
this.opts.logger.debug("intercom: opus frame pump error (dropped)", { meta: { error: errMsg$
|
|
209464
|
+
this.opts.logger.debug("intercom: opus frame pump error (dropped)", { meta: { error: errMsg$2(err) } });
|
|
209257
209465
|
});
|
|
209258
209466
|
});
|
|
209259
209467
|
let offer;
|
|
@@ -209261,7 +209469,7 @@ var IntercomOrchestrator = class {
|
|
|
209261
209469
|
offer = await peer.createOffer();
|
|
209262
209470
|
} catch (err) {
|
|
209263
209471
|
await this.stop(sessionId, "start-failed-cleanup").catch(() => {});
|
|
209264
|
-
this.opts.logger.warn("intercom: webrtc createOffer failed", { meta: { error: errMsg$
|
|
209472
|
+
this.opts.logger.warn("intercom: webrtc createOffer failed", { meta: { error: errMsg$2(err) } });
|
|
209265
209473
|
throw err;
|
|
209266
209474
|
}
|
|
209267
209475
|
this.opts.logger.info("intercom session opened", { meta: {
|
|
@@ -209310,7 +209518,7 @@ var IntercomOrchestrator = class {
|
|
|
209310
209518
|
} catch (err) {
|
|
209311
209519
|
this.opts.logger.debug("intercom: peer.close error (continuing)", { meta: {
|
|
209312
209520
|
sessionId,
|
|
209313
|
-
error: errMsg$
|
|
209521
|
+
error: errMsg$2(err)
|
|
209314
209522
|
} });
|
|
209315
209523
|
}
|
|
209316
209524
|
try {
|
|
@@ -209321,7 +209529,7 @@ var IntercomOrchestrator = class {
|
|
|
209321
209529
|
} catch (err) {
|
|
209322
209530
|
this.opts.logger.debug("intercom: audio-codec.closeSession error (continuing)", { meta: {
|
|
209323
209531
|
sessionId,
|
|
209324
|
-
error: errMsg$
|
|
209532
|
+
error: errMsg$2(err)
|
|
209325
209533
|
} });
|
|
209326
209534
|
}
|
|
209327
209535
|
try {
|
|
@@ -209329,7 +209537,7 @@ var IntercomOrchestrator = class {
|
|
|
209329
209537
|
} catch (err) {
|
|
209330
209538
|
this.opts.logger.debug("intercom: talk-session.stop error (continuing)", { meta: {
|
|
209331
209539
|
sessionId,
|
|
209332
|
-
error: errMsg$
|
|
209540
|
+
error: errMsg$2(err)
|
|
209333
209541
|
} });
|
|
209334
209542
|
}
|
|
209335
209543
|
this.opts.logger.info("intercom session closed", { meta: {
|
|
@@ -209385,7 +209593,7 @@ var IntercomOrchestrator = class {
|
|
|
209385
209593
|
function generateSessionId() {
|
|
209386
209594
|
return `intercom-${Date.now().toString(36)}-${Math.floor(Math.random() * 16777215).toString(36)}`;
|
|
209387
209595
|
}
|
|
209388
|
-
function errMsg$
|
|
209596
|
+
function errMsg$2(err) {
|
|
209389
209597
|
return err instanceof Error ? err.message : String(err);
|
|
209390
209598
|
}
|
|
209391
209599
|
//#endregion
|
|
@@ -209444,7 +209652,7 @@ var WeriftIntercomPeer = class {
|
|
|
209444
209652
|
for (const cb of this.opusCallbacks) try {
|
|
209445
209653
|
cb(payload, ptsMs);
|
|
209446
209654
|
} catch (err) {
|
|
209447
|
-
this.opts.logger.debug("intercom-peer: opus callback threw", { meta: { error: errMsg(err) } });
|
|
209655
|
+
this.opts.logger.debug("intercom-peer: opus callback threw", { meta: { error: errMsg$1(err) } });
|
|
209448
209656
|
}
|
|
209449
209657
|
});
|
|
209450
209658
|
if (rtpSub && typeof rtpSub.unsubscribe === "function") this.rtpUnsubscribe = () => rtpSub.unsubscribe?.();
|
|
@@ -209492,7 +209700,7 @@ var WeriftIntercomPeer = class {
|
|
|
209492
209700
|
}
|
|
209493
209701
|
}
|
|
209494
209702
|
};
|
|
209495
|
-
function errMsg(err) {
|
|
209703
|
+
function errMsg$1(err) {
|
|
209496
209704
|
return err instanceof Error ? err.message : String(err);
|
|
209497
209705
|
}
|
|
209498
209706
|
//#endregion
|
|
@@ -210968,7 +211176,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
210968
211176
|
* `true` iff this camera can run its own email/SMTP push path. Hub
|
|
210969
211177
|
* children are excluded: they share the parent's TCP socket and have
|
|
210970
211178
|
* no independent email config, so the NVR/Hub handles email centrally.
|
|
210971
|
-
* Consumed by the
|
|
211179
|
+
* Consumed by the camera mailbox bridge (`email-push-bridge.ts`)
|
|
210972
211180
|
* to decide which cameras accept inbound mail + can be auto-configured.
|
|
210973
211181
|
*/
|
|
210974
211182
|
get isEmailPushEligible() {
|
|
@@ -211674,12 +211882,12 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
211674
211882
|
}
|
|
211675
211883
|
if (!this.emailPushParamsResolver) {
|
|
211676
211884
|
this.ctx.logger.warn("email-push: resolver not wired — restart the Reolink addon", { tags: { deviceId: this.id } });
|
|
211677
|
-
throw userError("reolink.email-push-not-wired", void 0, "
|
|
211885
|
+
throw userError("reolink.email-push-not-wired", void 0, "The camera mailbox is not wired — restart the Reolink addon");
|
|
211678
211886
|
}
|
|
211679
211887
|
const params = await this.emailPushParamsResolver();
|
|
211680
211888
|
if (!params) {
|
|
211681
|
-
this.ctx.logger.warn("email-push: no manager params —
|
|
211682
|
-
throw new Error("
|
|
211889
|
+
this.ctx.logger.warn("email-push: no manager params — this camera cannot use the camera mailbox", { tags: { deviceId: this.id } });
|
|
211890
|
+
throw new Error("This camera cannot use the camera mailbox.");
|
|
211683
211891
|
}
|
|
211684
211892
|
const triggers = this.config.get("emailPushTriggers") ?? [
|
|
211685
211893
|
"MD",
|
|
@@ -218663,15 +218871,6 @@ function mapInferredTypeToSimpleEvents(inferred) {
|
|
|
218663
218871
|
default: return ["motion"];
|
|
218664
218872
|
}
|
|
218665
218873
|
}
|
|
218666
|
-
/** Default SMTP listen port. Avoid privileged 25; Reolink firmwares are
|
|
218667
|
-
* happy on any port the email config points at. */
|
|
218668
|
-
var DEFAULT_EMAIL_PUSH_PORT = 2525;
|
|
218669
|
-
/** Virtual mail domain — only used to extract the camera id from the
|
|
218670
|
-
* recipient (`cam-<id>@<domain>`). Never resolved via DNS. */
|
|
218671
|
-
var DEFAULT_EMAIL_PUSH_DOMAIN = "camstack.local";
|
|
218672
|
-
/** Reolink snapshots fit comfortably; 25 MB covers video attachments
|
|
218673
|
-
* too without letting a runaway upload exhaust memory. */
|
|
218674
|
-
var DEFAULT_EMAIL_PUSH_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
|
|
218675
218874
|
/**
|
|
218676
218875
|
* Enumerate candidate camera-facing LAN addresses, best first (private
|
|
218677
218876
|
* RFC-1918 ranges ranked ahead of public). Link-local and internal
|
|
@@ -218698,15 +218897,15 @@ function listLanHosts() {
|
|
|
218698
218897
|
}
|
|
218699
218898
|
//#endregion
|
|
218700
218899
|
//#region src/baichuan-webhook-server.ts
|
|
218701
|
-
function readString
|
|
218900
|
+
function readString(raw, key, fallback) {
|
|
218702
218901
|
const v = raw[key];
|
|
218703
218902
|
return typeof v === "string" && v.length > 0 ? v : fallback;
|
|
218704
218903
|
}
|
|
218705
|
-
function readNumber
|
|
218904
|
+
function readNumber(raw, key, fallback) {
|
|
218706
218905
|
const v = raw[key];
|
|
218707
218906
|
return typeof v === "number" && Number.isFinite(v) ? v : fallback;
|
|
218708
218907
|
}
|
|
218709
|
-
function readBoolean
|
|
218908
|
+
function readBoolean(raw, key, fallback) {
|
|
218710
218909
|
const v = raw[key];
|
|
218711
218910
|
return typeof v === "boolean" ? v : fallback;
|
|
218712
218911
|
}
|
|
@@ -218722,9 +218921,9 @@ var ReolinkBaichuanWebhookServer = class {
|
|
|
218722
218921
|
if (this.settings) return this.settings;
|
|
218723
218922
|
const raw = await this.deps.readStore();
|
|
218724
218923
|
this.settings = {
|
|
218725
|
-
enabled: readBoolean
|
|
218726
|
-
port: readNumber
|
|
218727
|
-
bindHost: readString
|
|
218924
|
+
enabled: readBoolean(raw, "baichuanWebhookEnabled", true),
|
|
218925
|
+
port: readNumber(raw, "baichuanWebhookPort", DEFAULT_BAICHUAN_WEBHOOK_PORT),
|
|
218926
|
+
bindHost: readString(raw, "baichuanWebhookBindHost", "0.0.0.0")
|
|
218728
218927
|
};
|
|
218729
218928
|
return this.settings;
|
|
218730
218929
|
}
|
|
@@ -219031,338 +219230,420 @@ function buildCreationFormSchema() {
|
|
|
219031
219230
|
] };
|
|
219032
219231
|
}
|
|
219033
219232
|
//#endregion
|
|
219034
|
-
//#region src/email-push-
|
|
219035
|
-
/**
|
|
219036
|
-
* The
|
|
219037
|
-
*
|
|
219038
|
-
*
|
|
219039
|
-
|
|
219040
|
-
|
|
219041
|
-
|
|
219042
|
-
*
|
|
219043
|
-
*
|
|
219044
|
-
*
|
|
219045
|
-
*
|
|
219046
|
-
|
|
219047
|
-
|
|
219048
|
-
|
|
219049
|
-
|
|
219050
|
-
|
|
219051
|
-
|
|
219052
|
-
|
|
219053
|
-
|
|
219054
|
-
|
|
219055
|
-
|
|
219056
|
-
|
|
219057
|
-
|
|
219058
|
-
|
|
219059
|
-
|
|
219060
|
-
|
|
219061
|
-
|
|
219062
|
-
|
|
219063
|
-
|
|
219064
|
-
|
|
219065
|
-
|
|
219066
|
-
|
|
219067
|
-
|
|
219068
|
-
|
|
219069
|
-
|
|
219070
|
-
|
|
219071
|
-
|
|
219072
|
-
|
|
219073
|
-
|
|
219074
|
-
* "integration" into an operator-facing tab bar (D268 → D269).
|
|
219075
|
-
*/
|
|
219076
|
-
function buildEmailPushSettingsSchema(recommendedHost) {
|
|
219077
|
-
return { sections: [{
|
|
219078
|
-
id: "email-push",
|
|
219079
|
-
title: "Battery-cam Motion (Email/SMTP Push)",
|
|
219080
|
-
titleKey: "provider-reolink:emailPushSettings.s_email-push.title",
|
|
219081
|
-
description: "Lets battery cameras report motion by email without being woken.",
|
|
219082
|
-
descriptionKey: "provider-reolink:emailPushSettings.s_email-push.description",
|
|
219083
|
-
columns: 2,
|
|
219084
|
-
fields: [
|
|
219085
|
-
{
|
|
219086
|
-
type: "boolean",
|
|
219087
|
-
key: "emailPushEnabled",
|
|
219088
|
-
label: "Enable Email Push Server",
|
|
219089
|
-
labelKey: "provider-reolink:emailPushSettings.s_email-push.emailPushEnabled.label",
|
|
219090
|
-
default: true
|
|
219091
|
-
},
|
|
219092
|
-
{
|
|
219093
|
-
type: "info",
|
|
219094
|
-
key: "emailPushRecommendedHost",
|
|
219095
|
-
label: "Camera-facing host",
|
|
219096
|
-
labelKey: "provider-reolink:emailPushSettings.s_email-push.emailPushRecommendedHost.label",
|
|
219097
|
-
content: `Point cameras at this host: ${recommendedHost}`
|
|
219098
|
-
},
|
|
219099
|
-
{
|
|
219100
|
-
type: "number",
|
|
219101
|
-
key: "emailPushPort",
|
|
219102
|
-
label: "SMTP Port",
|
|
219103
|
-
labelKey: "provider-reolink:emailPushSettings.s_email-push.emailPushPort.label",
|
|
219104
|
-
default: DEFAULT_EMAIL_PUSH_PORT,
|
|
219105
|
-
min: 1,
|
|
219106
|
-
max: 65535
|
|
219107
|
-
},
|
|
219108
|
-
{
|
|
219109
|
-
type: "text",
|
|
219110
|
-
key: "emailPushDomain",
|
|
219111
|
-
label: "Virtual Domain",
|
|
219112
|
-
labelKey: "provider-reolink:emailPushSettings.s_email-push.emailPushDomain.label",
|
|
219113
|
-
default: DEFAULT_EMAIL_PUSH_DOMAIN,
|
|
219114
|
-
placeholder: DEFAULT_EMAIL_PUSH_DOMAIN
|
|
219115
|
-
}
|
|
219116
|
-
]
|
|
219117
|
-
}, {
|
|
219118
|
-
id: "email-push-auth",
|
|
219119
|
-
title: "Email Push Authentication",
|
|
219120
|
-
titleKey: "provider-reolink:emailPushSettings.s_email-push-auth.title",
|
|
219121
|
-
description: "Random values are generated on first boot; re-run Auto-configure on cameras after changing them.",
|
|
219122
|
-
descriptionKey: "provider-reolink:emailPushSettings.s_email-push-auth.description",
|
|
219123
|
-
columns: 2,
|
|
219124
|
-
fields: [
|
|
219125
|
-
{
|
|
219126
|
-
type: "boolean",
|
|
219127
|
-
key: "emailPushRequireAuth",
|
|
219128
|
-
label: "Require AUTH",
|
|
219129
|
-
labelKey: "provider-reolink:emailPushSettings.s_email-push-auth.emailPushRequireAuth.label",
|
|
219130
|
-
default: true
|
|
219131
|
-
},
|
|
219132
|
-
{
|
|
219133
|
-
type: "text",
|
|
219134
|
-
key: "emailPushAuthUsername",
|
|
219135
|
-
label: "Auth Username",
|
|
219136
|
-
labelKey: "provider-reolink:emailPushSettings.s_email-push-auth.emailPushAuthUsername.label"
|
|
219233
|
+
//#region src/email-push-bridge.ts
|
|
219234
|
+
/**
|
|
219235
|
+
* The Reolink side of the `smtp-inbound` broker kind (D999).
|
|
219236
|
+
*
|
|
219237
|
+
* Battery cameras report motion by mail. The mailbox they mail is a `broker` of kind
|
|
219238
|
+
* `smtp-inbound` owned by `smtp-nodemailer`; this bridge
|
|
219239
|
+
* - creates it on first need (`ensureMailbox`: one in-process promise, adopt-id, never at a bare boot),
|
|
219240
|
+
* - declares the `cam-<stableId>` recipients of the standalone cameras (`sync`),
|
|
219241
|
+
* - turns each `broker.message` into the camera's `handleSimpleEvent({ source: 'email' })`,
|
|
219242
|
+
* - replays what the bus lost from the broker's ring (seq gap, a broker that came back, subscribe).
|
|
219243
|
+
* It never deletes the mailbox: the operator owns it, and the bridge recreates it at the next need.
|
|
219244
|
+
* Events are telemetry (D8); the ring is the RPC reconcile.
|
|
219245
|
+
*/
|
|
219246
|
+
/** The addon that owns `smtp-inbound` mailboxes: routes the id-less calls (`add`). */
|
|
219247
|
+
var SMTP_BROKER_ADDON_ID = "smtp-nodemailer";
|
|
219248
|
+
/** Who this addon is, among the subscribers of a mailbox. */
|
|
219249
|
+
var REOLINK_SUBSCRIBER_ID = "provider-reolink";
|
|
219250
|
+
var MAILBOX_NAME = "Camera mailbox";
|
|
219251
|
+
var RECIPIENT_PREFIX = "cam-";
|
|
219252
|
+
/** The real port: `ctx.api.broker`, every call routed to the addon that owns the kind. */
|
|
219253
|
+
function createBrokerPort(api) {
|
|
219254
|
+
const route = { addonId: SMTP_BROKER_ADDON_ID };
|
|
219255
|
+
return {
|
|
219256
|
+
list: async () => api.broker.list.query({ kind: SMTP_INBOUND_KIND }),
|
|
219257
|
+
add: async ({ id, name, settings }) => api.broker.add.mutate({
|
|
219258
|
+
kind: SMTP_INBOUND_KIND,
|
|
219259
|
+
name,
|
|
219260
|
+
id,
|
|
219261
|
+
settings: { ...settings },
|
|
219262
|
+
...route
|
|
219263
|
+
}),
|
|
219264
|
+
getBrokerConfig: async (id) => api.broker.getBrokerConfig.query({
|
|
219265
|
+
id,
|
|
219266
|
+
...route
|
|
219267
|
+
}),
|
|
219268
|
+
subscribe: async (brokerId, recipients) => api.broker.subscribe.mutate({
|
|
219269
|
+
brokerId,
|
|
219270
|
+
filter: {
|
|
219271
|
+
recipients: [...recipients],
|
|
219272
|
+
subscriberId: REOLINK_SUBSCRIBER_ID
|
|
219137
219273
|
},
|
|
219138
|
-
|
|
219139
|
-
|
|
219140
|
-
|
|
219141
|
-
|
|
219142
|
-
|
|
219143
|
-
|
|
219144
|
-
|
|
219145
|
-
|
|
219146
|
-
|
|
219147
|
-
|
|
219148
|
-
|
|
219149
|
-
|
|
219150
|
-
|
|
219151
|
-
function readString(raw, key, fallback) {
|
|
219152
|
-
const v = raw[key];
|
|
219153
|
-
return typeof v === "string" && v.length > 0 ? v : fallback;
|
|
219274
|
+
...route
|
|
219275
|
+
}),
|
|
219276
|
+
unsubscribe: async (brokerId, subscriptionId) => api.broker.unsubscribe.mutate({
|
|
219277
|
+
brokerId,
|
|
219278
|
+
subscriptionId,
|
|
219279
|
+
...route
|
|
219280
|
+
}),
|
|
219281
|
+
getState: async (brokerId, key) => api.broker.getState.query({
|
|
219282
|
+
brokerId,
|
|
219283
|
+
key,
|
|
219284
|
+
...route
|
|
219285
|
+
})
|
|
219286
|
+
};
|
|
219154
219287
|
}
|
|
219155
|
-
|
|
219156
|
-
|
|
219157
|
-
|
|
219288
|
+
var BrokerMessageEventSchema = object({
|
|
219289
|
+
brokerId: string(),
|
|
219290
|
+
subscriptionId: string(),
|
|
219291
|
+
key: string(),
|
|
219292
|
+
payload: unknown()
|
|
219293
|
+
});
|
|
219294
|
+
var ReadyStateEventSchema = object({
|
|
219295
|
+
capName: string(),
|
|
219296
|
+
state: string()
|
|
219297
|
+
});
|
|
219298
|
+
var BrokerStatusEventSchema = object({
|
|
219299
|
+
brokerId: string(),
|
|
219300
|
+
status: string(),
|
|
219301
|
+
error: string().optional()
|
|
219302
|
+
});
|
|
219303
|
+
function errMsg(err) {
|
|
219304
|
+
return err instanceof Error ? err.message : String(err);
|
|
219158
219305
|
}
|
|
219159
|
-
function
|
|
219160
|
-
|
|
219161
|
-
return typeof v === "boolean" ? v : fallback;
|
|
219306
|
+
function isInUse(err) {
|
|
219307
|
+
return /already in use/i.test(errMsg(err));
|
|
219162
219308
|
}
|
|
219163
|
-
|
|
219164
|
-
* Owns the lib's SMTP intake for the Reolink addon. Lifecycle:
|
|
219165
|
-
* - `start()` from the provider's `onInitialize` (no-op when disabled).
|
|
219166
|
-
* - `restart()` from `onConfigChanged` so port/domain/auth edits apply.
|
|
219167
|
-
* - `stop()` from the provider's `onShutdown`.
|
|
219168
|
-
*/
|
|
219169
|
-
var ReolinkEmailPushServer = class {
|
|
219309
|
+
var EmailPushBridge = class {
|
|
219170
219310
|
deps;
|
|
219171
|
-
|
|
219172
|
-
|
|
219173
|
-
|
|
219311
|
+
mailbox = null;
|
|
219312
|
+
brokerId = null;
|
|
219313
|
+
subscriptionId = null;
|
|
219314
|
+
/** The recipient set the broker holds for this addon, as a key. */
|
|
219315
|
+
declared = null;
|
|
219316
|
+
lastSeq = 0;
|
|
219317
|
+
/** The seq baseline of the current mailbox was taken (before the first subscribe). */
|
|
219318
|
+
seeded = false;
|
|
219319
|
+
/** The last failure that was reported, so a mailbox provider that stays away is said once. */
|
|
219320
|
+
lastFailure = null;
|
|
219321
|
+
chain = Promise.resolve();
|
|
219174
219322
|
constructor(deps) {
|
|
219175
219323
|
this.deps = deps;
|
|
219176
219324
|
}
|
|
219177
|
-
/**
|
|
219178
|
-
|
|
219179
|
-
|
|
219180
|
-
|
|
219181
|
-
|
|
219182
|
-
|
|
219183
|
-
|
|
219184
|
-
|
|
219185
|
-
|
|
219186
|
-
|
|
219187
|
-
|
|
219188
|
-
|
|
219189
|
-
|
|
219190
|
-
|
|
219191
|
-
|
|
219192
|
-
|
|
219193
|
-
|
|
219194
|
-
|
|
219195
|
-
|
|
219196
|
-
|
|
219197
|
-
|
|
219198
|
-
|
|
219199
|
-
|
|
219325
|
+
/** The id of the mailbox, creating it when none exists. One in-flight promise. */
|
|
219326
|
+
ensureMailbox() {
|
|
219327
|
+
this.mailbox ??= this.findOrCreate().catch((err) => {
|
|
219328
|
+
this.mailbox = null;
|
|
219329
|
+
throw err;
|
|
219330
|
+
});
|
|
219331
|
+
return this.mailbox;
|
|
219332
|
+
}
|
|
219333
|
+
async findOrCreate() {
|
|
219334
|
+
const found = pickMailbox(await this.deps.port.list());
|
|
219335
|
+
if (found !== null) {
|
|
219336
|
+
await this.deps.pending?.clear();
|
|
219337
|
+
return found;
|
|
219338
|
+
}
|
|
219339
|
+
const pending = await this.deps.pending?.read() ?? null;
|
|
219340
|
+
try {
|
|
219341
|
+
const created = await this.deps.port.add({
|
|
219342
|
+
id: SMTP_INBOUND_DEFAULT_ID,
|
|
219343
|
+
name: MAILBOX_NAME,
|
|
219344
|
+
...pending === null ? {} : { settings: pending }
|
|
219345
|
+
});
|
|
219346
|
+
await this.deps.pending?.clear();
|
|
219347
|
+
this.deps.logger.info("email-push: created the camera mailbox", { meta: {
|
|
219348
|
+
brokerId: created.id,
|
|
219349
|
+
carriedLegacySettings: pending !== null
|
|
219350
|
+
} });
|
|
219351
|
+
return created.id;
|
|
219200
219352
|
} catch (err) {
|
|
219201
|
-
|
|
219353
|
+
if (!isInUse(err)) throw err;
|
|
219354
|
+
const again = pickMailbox(await this.deps.port.list());
|
|
219355
|
+
if (again === null) throw err;
|
|
219356
|
+
await this.deps.pending?.clear();
|
|
219357
|
+
return again;
|
|
219202
219358
|
}
|
|
219203
|
-
this.settings = {
|
|
219204
|
-
enabled: readBoolean(raw, "emailPushEnabled", true),
|
|
219205
|
-
port: readNumber(raw, "emailPushPort", DEFAULT_EMAIL_PUSH_PORT),
|
|
219206
|
-
bindHost: readString(raw, "emailPushBindHost", "0.0.0.0"),
|
|
219207
|
-
domain: readString(raw, "emailPushDomain", DEFAULT_EMAIL_PUSH_DOMAIN),
|
|
219208
|
-
requireAuth: readBoolean(raw, "emailPushRequireAuth", true),
|
|
219209
|
-
authUsername,
|
|
219210
|
-
authPassword,
|
|
219211
|
-
maxMessageBytes: readNumber(raw, "emailPushMaxMessageBytes", DEFAULT_EMAIL_PUSH_MAX_MESSAGE_BYTES)
|
|
219212
|
-
};
|
|
219213
|
-
return this.settings;
|
|
219214
219359
|
}
|
|
219215
|
-
|
|
219216
|
-
|
|
219217
|
-
reloadSettings() {
|
|
219218
|
-
this.settings = void 0;
|
|
219219
|
-
}
|
|
219220
|
-
buildConfig(settings) {
|
|
219221
|
-
return {
|
|
219222
|
-
port: settings.port,
|
|
219223
|
-
bindHost: settings.bindHost,
|
|
219224
|
-
domain: settings.domain,
|
|
219225
|
-
requireAuth: settings.requireAuth,
|
|
219226
|
-
authUsername: settings.authUsername,
|
|
219227
|
-
authPassword: settings.authPassword,
|
|
219228
|
-
tls: false,
|
|
219229
|
-
maxMessageBytes: settings.maxMessageBytes
|
|
219230
|
-
};
|
|
219231
|
-
}
|
|
219232
|
-
/**
|
|
219233
|
-
* Resolve a recipient local-part (`<id>`, already stripped of the
|
|
219234
|
-
* `cam-` prefix by the lib) to a camera id. Returns the matching
|
|
219235
|
-
* camera's `stableId` (which the lib echoes back as `event.cameraId`)
|
|
219236
|
-
* or `undefined` to reject the recipient at RCPT TO.
|
|
219237
|
-
*
|
|
219238
|
-
* Hub-attached children are skipped: they share the parent's TCP
|
|
219239
|
-
* socket and have no independent email path, so accepting one would
|
|
219240
|
-
* silently mis-route. Match is on `stableId` (durable across
|
|
219241
|
-
* re-adoption) — the same value `buildCameraSetupParams` writes into
|
|
219242
|
-
* the camera's email config.
|
|
219243
|
-
*/
|
|
219244
|
-
resolveCameraId(localPart) {
|
|
219245
|
-
const cam = this.findCameraByStableId(localPart);
|
|
219246
|
-
if (!cam || !cam.isEmailPushEligible) return void 0;
|
|
219247
|
-
return localPart;
|
|
219360
|
+
recipients() {
|
|
219361
|
+
return this.deps.listCameras().filter((cam) => cam.isEmailPushEligible).map((cam) => `${RECIPIENT_PREFIX}${cam.stableId}`).sort();
|
|
219248
219362
|
}
|
|
219249
|
-
|
|
219250
|
-
|
|
219363
|
+
enqueue(job) {
|
|
219364
|
+
const run = this.chain.then(job);
|
|
219365
|
+
this.chain = run.then(() => void 0, () => void 0);
|
|
219366
|
+
return run;
|
|
219251
219367
|
}
|
|
219252
|
-
/**
|
|
219253
|
-
*
|
|
219254
|
-
|
|
219255
|
-
|
|
219256
|
-
|
|
219257
|
-
|
|
219258
|
-
|
|
219259
|
-
|
|
219260
|
-
|
|
219261
|
-
|
|
219262
|
-
|
|
219263
|
-
|
|
219264
|
-
|
|
219265
|
-
|
|
219266
|
-
|
|
219267
|
-
|
|
219368
|
+
/**
|
|
219369
|
+
* Declare the recipients of the eligible cameras (again). Cheap when nothing changed.
|
|
219370
|
+
* A failure is logged, never thrown: a camera that cannot be declared does not stop the addon.
|
|
219371
|
+
*/
|
|
219372
|
+
sync(options = {}) {
|
|
219373
|
+
return this.enqueue(async () => {
|
|
219374
|
+
try {
|
|
219375
|
+
await this.doSync(options.force === true);
|
|
219376
|
+
this.lastFailure = null;
|
|
219377
|
+
} catch (err) {
|
|
219378
|
+
this.declared = null;
|
|
219379
|
+
this.subscriptionId = null;
|
|
219380
|
+
const message = errMsg(err);
|
|
219381
|
+
if (message !== this.lastFailure) {
|
|
219382
|
+
this.lastFailure = message;
|
|
219383
|
+
this.deps.logger.warn("email-push: the camera mailbox is not available; battery cameras cannot mail motion until it is", { meta: { error: message } });
|
|
219384
|
+
}
|
|
219268
219385
|
}
|
|
219269
219386
|
});
|
|
219270
|
-
for (const type of mapInferredTypeToSimpleEvents(event.inferredType)) cam.handleSimpleEvent({
|
|
219271
|
-
type,
|
|
219272
|
-
channel: cam.emailPushChannel,
|
|
219273
|
-
timestamp: event.receivedAtMs,
|
|
219274
|
-
source: "email"
|
|
219275
|
-
});
|
|
219276
219387
|
}
|
|
219277
|
-
|
|
219278
|
-
|
|
219279
|
-
|
|
219280
|
-
|
|
219281
|
-
|
|
219282
|
-
this.
|
|
219388
|
+
async doSync(force) {
|
|
219389
|
+
const recipients = this.recipients();
|
|
219390
|
+
if (recipients.length === 0) {
|
|
219391
|
+
if (this.brokerId !== null && this.subscriptionId !== null) await this.deps.port.unsubscribe(this.brokerId, this.subscriptionId);
|
|
219392
|
+
this.subscriptionId = null;
|
|
219393
|
+
this.declared = null;
|
|
219283
219394
|
return;
|
|
219284
219395
|
}
|
|
219285
|
-
|
|
219286
|
-
|
|
219396
|
+
const key = recipients.join(",");
|
|
219397
|
+
if (!force && this.declared === key && this.subscriptionId !== null) return;
|
|
219398
|
+
const brokerId = await this.ensureMailbox();
|
|
219399
|
+
await this.seedBaseline(brokerId);
|
|
219400
|
+
const { subscriptionId } = await this.deps.port.subscribe(brokerId, recipients);
|
|
219401
|
+
this.brokerId = brokerId;
|
|
219402
|
+
this.subscriptionId = subscriptionId;
|
|
219403
|
+
this.declared = key;
|
|
219404
|
+
await this.reconcile();
|
|
219405
|
+
}
|
|
219406
|
+
/**
|
|
219407
|
+
* What the mailbox retains from BEFORE this subscription is old news (a restart of this addon
|
|
219408
|
+
* must not re-fire a quarter hour of alarms). Nothing for our recipients is accepted before the
|
|
219409
|
+
* subscribe, so the mailbox's seq read now is an exact baseline: only what arrives after it is
|
|
219410
|
+
* processed, and a seq that went back means the mailbox restarted.
|
|
219411
|
+
*/
|
|
219412
|
+
async seedBaseline(brokerId) {
|
|
219413
|
+
const since = await this.readSince(brokerId, 0);
|
|
219414
|
+
if (since === null) {
|
|
219415
|
+
if (!this.seeded || this.brokerId !== brokerId) throw new Error("the camera mailbox state could not be read");
|
|
219287
219416
|
return;
|
|
219288
219417
|
}
|
|
219289
|
-
this.
|
|
219290
|
-
|
|
219291
|
-
|
|
219292
|
-
|
|
219293
|
-
|
|
219294
|
-
|
|
219295
|
-
|
|
219296
|
-
|
|
219418
|
+
if (!this.seeded || this.brokerId !== brokerId || since.lastSeq < this.lastSeq) {
|
|
219419
|
+
this.lastSeq = since.lastSeq;
|
|
219420
|
+
this.seeded = true;
|
|
219421
|
+
}
|
|
219422
|
+
}
|
|
219423
|
+
/** A `broker.message` event off the bus. */
|
|
219424
|
+
onMessage(raw) {
|
|
219425
|
+
return this.enqueue(async () => {
|
|
219426
|
+
const event = BrokerMessageEventSchema.safeParse(raw);
|
|
219427
|
+
if (!event.success) return;
|
|
219428
|
+
if (this.brokerId === null) {
|
|
219429
|
+
this.deps.logger.warn("email-push: a mailbox message arrived before the mailbox was known — dropped", { meta: {
|
|
219430
|
+
brokerId: event.data.brokerId,
|
|
219431
|
+
key: event.data.key
|
|
219432
|
+
} });
|
|
219433
|
+
return;
|
|
219434
|
+
}
|
|
219435
|
+
if (event.data.brokerId !== this.brokerId) return;
|
|
219436
|
+
const message = SmtpInboundMessageSchema.safeParse(event.data.payload);
|
|
219437
|
+
if (!message.success) {
|
|
219438
|
+
this.deps.logger.warn("email-push: a mailbox message did not parse — dropped", { meta: { key: event.data.key } });
|
|
219439
|
+
return;
|
|
219440
|
+
}
|
|
219441
|
+
if (message.data.seq <= this.lastSeq) return;
|
|
219442
|
+
if (message.data.seq > this.lastSeq + 1) {
|
|
219443
|
+
await this.reconcile();
|
|
219444
|
+
return;
|
|
219297
219445
|
}
|
|
219446
|
+
this.process(event.data.key, message.data);
|
|
219298
219447
|
});
|
|
219299
|
-
|
|
219300
|
-
|
|
219301
|
-
|
|
219302
|
-
|
|
219303
|
-
|
|
219304
|
-
|
|
219305
|
-
|
|
219306
|
-
|
|
219448
|
+
}
|
|
219449
|
+
/** A `broker.status-changed` event: a mailbox that came back has forgotten its subscriptions. */
|
|
219450
|
+
onStatusChanged(raw) {
|
|
219451
|
+
const event = BrokerStatusEventSchema.safeParse(raw);
|
|
219452
|
+
if (!event.success || event.data.brokerId !== this.brokerId) return Promise.resolve();
|
|
219453
|
+
if (event.data.status !== "connected") {
|
|
219454
|
+
this.deps.logger.warn("email-push: the camera mailbox is not connected", { meta: {
|
|
219455
|
+
status: event.data.status,
|
|
219456
|
+
error: event.data.error
|
|
219307
219457
|
} });
|
|
219308
|
-
|
|
219309
|
-
this.deps.logger.error("email-push: SMTP intake failed to start", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
219310
|
-
this.busOff?.();
|
|
219311
|
-
this.busOff = void 0;
|
|
219312
|
-
this.instance = void 0;
|
|
219458
|
+
return Promise.resolve();
|
|
219313
219459
|
}
|
|
219460
|
+
return this.sync({ force: true });
|
|
219314
219461
|
}
|
|
219315
|
-
/**
|
|
219316
|
-
|
|
219317
|
-
|
|
219318
|
-
|
|
219319
|
-
|
|
219462
|
+
/**
|
|
219463
|
+
* The mailbox provider (any `broker` provider) became ready: the subscription it may have lost
|
|
219464
|
+
* is declared again, and a mailbox that never existed is created if a camera needs one.
|
|
219465
|
+
*/
|
|
219466
|
+
onProviderReady(raw) {
|
|
219467
|
+
const event = ReadyStateEventSchema.safeParse(raw);
|
|
219468
|
+
if (!event.success || event.data.capName !== "broker" || event.data.state !== "ready") return Promise.resolve();
|
|
219469
|
+
return this.sync({ force: true });
|
|
219320
219470
|
}
|
|
219321
|
-
/**
|
|
219322
|
-
|
|
219323
|
-
|
|
219324
|
-
|
|
219325
|
-
|
|
219326
|
-
if (this.
|
|
219327
|
-
|
|
219328
|
-
|
|
219471
|
+
/** Replay what the ring holds after the last seq we processed. */
|
|
219472
|
+
async reconcile() {
|
|
219473
|
+
const id = this.brokerId;
|
|
219474
|
+
if (id === null) return;
|
|
219475
|
+
let since = await this.readSince(id, this.lastSeq);
|
|
219476
|
+
if (since !== null && since.lastSeq < this.lastSeq) {
|
|
219477
|
+
this.lastSeq = 0;
|
|
219478
|
+
since = await this.readSince(id, 0);
|
|
219479
|
+
}
|
|
219480
|
+
if (since === null) return;
|
|
219481
|
+
for (const entry of since.messages) this.process(entry.key, entry.message);
|
|
219482
|
+
}
|
|
219483
|
+
async readSince(id, seq) {
|
|
219484
|
+
try {
|
|
219485
|
+
const raw = await this.deps.port.getState(id, smtpSinceKey(seq));
|
|
219486
|
+
const parsed = SmtpInboundSinceSchema.safeParse(raw);
|
|
219487
|
+
if (parsed.success) return parsed.data;
|
|
219488
|
+
this.deps.logger.warn("email-push: the mailbox replay did not parse", { meta: { brokerId: id } });
|
|
219489
|
+
} catch (err) {
|
|
219490
|
+
this.deps.logger.warn("email-push: the mailbox replay could not be read", { meta: {
|
|
219491
|
+
brokerId: id,
|
|
219492
|
+
error: errMsg(err)
|
|
219493
|
+
} });
|
|
219329
219494
|
}
|
|
219495
|
+
return null;
|
|
219496
|
+
}
|
|
219497
|
+
process(key, message) {
|
|
219498
|
+
if (message.seq <= this.lastSeq) return;
|
|
219499
|
+
this.lastSeq = message.seq;
|
|
219500
|
+
const cam = this.deps.listCameras().find((c) => `${RECIPIENT_PREFIX}${c.stableId}` === key);
|
|
219501
|
+
if (cam === void 0) {
|
|
219502
|
+
this.deps.logger.warn("email-push: mail for an unknown camera — dropping", { meta: {
|
|
219503
|
+
key,
|
|
219504
|
+
seq: message.seq
|
|
219505
|
+
} });
|
|
219506
|
+
return;
|
|
219507
|
+
}
|
|
219508
|
+
const inferred = classifyEmailPushMessage({
|
|
219509
|
+
subject: message.subject,
|
|
219510
|
+
text: message.text
|
|
219511
|
+
});
|
|
219512
|
+
this.deps.logger.info("email-push: motion received from battery camera", {
|
|
219513
|
+
tags: { deviceId: cam.id },
|
|
219514
|
+
meta: {
|
|
219515
|
+
inferredType: inferred,
|
|
219516
|
+
subject: message.subject.slice(0, 80),
|
|
219517
|
+
seq: message.seq
|
|
219518
|
+
}
|
|
219519
|
+
});
|
|
219520
|
+
for (const type of mapInferredTypeToSimpleEvents(inferred)) cam.handleSimpleEvent({
|
|
219521
|
+
type,
|
|
219522
|
+
channel: cam.emailPushChannel,
|
|
219523
|
+
timestamp: message.receivedAt,
|
|
219524
|
+
source: "email"
|
|
219525
|
+
});
|
|
219330
219526
|
}
|
|
219331
219527
|
/**
|
|
219332
|
-
*
|
|
219333
|
-
*
|
|
219334
|
-
* resolvable LAN address (no camera-reachable host), or the camera is
|
|
219335
|
-
* hub-attached / not eligible. The caller passes these straight into
|
|
219336
|
-
* `api.setupEmailPushToManager`.
|
|
219528
|
+
* What a camera needs to mail the mailbox. `null` for a camera that cannot do email push.
|
|
219529
|
+
* Throws the reason when the hub has no internal address: there is no fallback host.
|
|
219337
219530
|
*/
|
|
219338
219531
|
async buildCameraSetupParams(cam) {
|
|
219339
219532
|
if (!cam.isEmailPushEligible) return null;
|
|
219340
|
-
const
|
|
219341
|
-
const
|
|
219342
|
-
if (!
|
|
219533
|
+
const brokerId = await this.ensureMailbox();
|
|
219534
|
+
const parsed = SmtpInboundConfigSchema.safeParse(await this.deps.port.getBrokerConfig(brokerId));
|
|
219535
|
+
if (!parsed.success) throw new Error("The camera mailbox answered a configuration this build cannot read");
|
|
219536
|
+
const config = parsed.data;
|
|
219537
|
+
if (config.host === null) throw new Error(config.hostReason ?? "The hub has no internal address (System → Network)");
|
|
219538
|
+
await this.sync();
|
|
219343
219539
|
return {
|
|
219344
|
-
managerHost,
|
|
219345
|
-
managerPort:
|
|
219346
|
-
domain:
|
|
219347
|
-
recipientLocalPart:
|
|
219348
|
-
...
|
|
219349
|
-
authUsername:
|
|
219350
|
-
authPassword:
|
|
219540
|
+
managerHost: config.host,
|
|
219541
|
+
managerPort: config.port,
|
|
219542
|
+
domain: config.domain,
|
|
219543
|
+
recipientLocalPart: `${RECIPIENT_PREFIX}${cam.stableId}`,
|
|
219544
|
+
...config.requireAuth ? {
|
|
219545
|
+
authUsername: config.username,
|
|
219546
|
+
authPassword: config.password
|
|
219351
219547
|
} : {}
|
|
219352
219548
|
};
|
|
219353
219549
|
}
|
|
219354
219550
|
};
|
|
219551
|
+
function pickMailbox(list) {
|
|
219552
|
+
return list.find((b) => b.id === "smtp-inbound-default")?.id ?? list[0]?.id ?? null;
|
|
219553
|
+
}
|
|
219355
219554
|
//#endregion
|
|
219356
|
-
//#region src/email-push-
|
|
219357
|
-
|
|
219358
|
-
|
|
219359
|
-
|
|
219360
|
-
|
|
219361
|
-
|
|
219362
|
-
|
|
219363
|
-
|
|
219555
|
+
//#region src/email-push-removal-hook.ts
|
|
219556
|
+
/**
|
|
219557
|
+
* A removed camera leaves its `cam-<stableId>` recipient in the mailbox (D865). The addon holds
|
|
219558
|
+
* no per-device DATA for the intake (the mailbox is owned by the mail addon), so removal only
|
|
219559
|
+
* redeclares the recipient set without the camera. Driven by the tombstone, never by absence.
|
|
219560
|
+
*/
|
|
219561
|
+
function createEmailPushRemovalHook(deps) {
|
|
219562
|
+
return {
|
|
219563
|
+
onDeviceRemoved: async () => {
|
|
219564
|
+
await deps.resync();
|
|
219565
|
+
return { counts: { "mailbox-recipients": 0 } };
|
|
219566
|
+
},
|
|
219567
|
+
listDeviceData: async () => []
|
|
219568
|
+
};
|
|
219364
219569
|
}
|
|
219365
219570
|
//#endregion
|
|
219571
|
+
//#region src/migrations/email-push-to-mailbox.ts
|
|
219572
|
+
/** The pending mailbox as kept in the addon store: everything but the password (a keychain secret). */
|
|
219573
|
+
var PendingMailboxSchema = object({
|
|
219574
|
+
port: number().int(),
|
|
219575
|
+
bindHost: string().optional(),
|
|
219576
|
+
domain: string(),
|
|
219577
|
+
requireAuth: boolean(),
|
|
219578
|
+
username: string()
|
|
219579
|
+
});
|
|
219580
|
+
/** The keys an install that ever used the intake has; any one of them means "carry it over". */
|
|
219581
|
+
var LEGACY_KEYS = [
|
|
219582
|
+
"emailPushPort",
|
|
219583
|
+
"emailPushDomain",
|
|
219584
|
+
"emailPushRequireAuth",
|
|
219585
|
+
"emailPushAuthUsername",
|
|
219586
|
+
"emailPushAuthPassword",
|
|
219587
|
+
"emailPushBindHost"
|
|
219588
|
+
];
|
|
219589
|
+
var DEFAULT_PORT = 2525;
|
|
219590
|
+
var DEFAULT_DOMAIN = "camstack.local";
|
|
219591
|
+
function text(stored, key) {
|
|
219592
|
+
const value = stored[key];
|
|
219593
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
219594
|
+
}
|
|
219595
|
+
function portOf(stored, ports) {
|
|
219596
|
+
const value = stored["emailPushPort"];
|
|
219597
|
+
const numeric = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
219598
|
+
if (Number.isInteger(numeric) && numeric >= 1 && numeric <= 65535) return numeric;
|
|
219599
|
+
if (value !== void 0) ports.warn(`email-push migration: the legacy port is not usable; using ${DEFAULT_PORT}`);
|
|
219600
|
+
return DEFAULT_PORT;
|
|
219601
|
+
}
|
|
219602
|
+
async function moveEmailPushToMailbox(ports) {
|
|
219603
|
+
const stored = await ports.readStored();
|
|
219604
|
+
if (!LEGACY_KEYS.some((key) => stored[key] !== void 0)) return {
|
|
219605
|
+
outcome: "already-done",
|
|
219606
|
+
summary: { moved: false }
|
|
219607
|
+
};
|
|
219608
|
+
if (stored["emailPushEnabled"] === false) return {
|
|
219609
|
+
outcome: "already-done",
|
|
219610
|
+
summary: {
|
|
219611
|
+
moved: false,
|
|
219612
|
+
disabled: true
|
|
219613
|
+
}
|
|
219614
|
+
};
|
|
219615
|
+
const bindHost = text(stored, "emailPushBindHost");
|
|
219616
|
+
await ports.stashPending({
|
|
219617
|
+
port: portOf(stored, ports),
|
|
219618
|
+
...bindHost === null ? {} : { bindHost },
|
|
219619
|
+
domain: text(stored, "emailPushDomain") ?? DEFAULT_DOMAIN,
|
|
219620
|
+
requireAuth: stored["emailPushRequireAuth"] !== false,
|
|
219621
|
+
username: text(stored, "emailPushAuthUsername") ?? "",
|
|
219622
|
+
password: text(stored, "emailPushAuthPassword") ?? ""
|
|
219623
|
+
});
|
|
219624
|
+
return {
|
|
219625
|
+
outcome: "done",
|
|
219626
|
+
summary: { moved: true }
|
|
219627
|
+
};
|
|
219628
|
+
}
|
|
219629
|
+
/** The `provider-reolink` catalogue (D987): offered to the engine when the addon initialises. */
|
|
219630
|
+
var REOLINK_MIGRATIONS = [{
|
|
219631
|
+
meta: {
|
|
219632
|
+
id: "provider-reolink/2026-11-02-email-push-to-mailbox",
|
|
219633
|
+
owner: "provider-reolink",
|
|
219634
|
+
level: "addon-state",
|
|
219635
|
+
seq: 1,
|
|
219636
|
+
dependsOn: [],
|
|
219637
|
+
store: "settings",
|
|
219638
|
+
scope: "cluster",
|
|
219639
|
+
onFailure: "tolerate",
|
|
219640
|
+
bodyKind: "procedure",
|
|
219641
|
+
description: "Keep the SMTP intake settings (port, domain, credentials) as the pending camera mailbox, created by the bridge when the smtp-inbound broker is reachable, so no camera is reconfigured",
|
|
219642
|
+
fixtureMark: "a setting key the migration does not know"
|
|
219643
|
+
},
|
|
219644
|
+
procedure: moveEmailPushToMailbox
|
|
219645
|
+
}];
|
|
219646
|
+
//#endregion
|
|
219366
219647
|
//#region src/reolink-discovery-map.ts
|
|
219367
219648
|
/**
|
|
219368
219649
|
* Flatten a host (IP / hostname) into a flat row-key slug — shared by `generateStableId` and the
|
|
@@ -219582,7 +219863,7 @@ function buildReolinkAdoptionProvider(deps) {
|
|
|
219582
219863
|
lastDiscoveryAt: null,
|
|
219583
219864
|
candidateCount: 0,
|
|
219584
219865
|
adoptedCount: 0,
|
|
219585
|
-
lastError: errMsg$
|
|
219866
|
+
lastError: errMsg$3(err)
|
|
219586
219867
|
};
|
|
219587
219868
|
}
|
|
219588
219869
|
},
|
|
@@ -219643,7 +219924,7 @@ function buildReolinkAdoptionProvider(deps) {
|
|
|
219643
219924
|
logger.warn("reolink adopt: failed to adopt channel", { meta: {
|
|
219644
219925
|
childNativeId,
|
|
219645
219926
|
integrationId,
|
|
219646
|
-
error: errMsg$
|
|
219927
|
+
error: errMsg$3(err)
|
|
219647
219928
|
} });
|
|
219648
219929
|
failures++;
|
|
219649
219930
|
}
|
|
@@ -219823,7 +220104,7 @@ async function planHubMigration(saved, ports) {
|
|
|
219823
220104
|
try {
|
|
219824
220105
|
integrations = await ports.listIntegrations();
|
|
219825
220106
|
} catch (err) {
|
|
219826
|
-
ports.logger.warn("Reolink hub migration: integrations unreadable — hubs left in place", { meta: { error: errMsg$
|
|
220107
|
+
ports.logger.warn("Reolink hub migration: integrations unreadable — hubs left in place", { meta: { error: errMsg$3(err) } });
|
|
219827
220108
|
return {
|
|
219828
220109
|
rows: saved.filter((row) => row.type !== DeviceType.Hub),
|
|
219829
220110
|
retirements: []
|
|
@@ -219850,7 +220131,7 @@ async function planHubMigration(saved, ports) {
|
|
|
219850
220131
|
ports.logger.warn("Reolink hub migration failed — the hub device is left in place", {
|
|
219851
220132
|
tags: { deviceId: hub.id },
|
|
219852
220133
|
meta: {
|
|
219853
|
-
error: errMsg$
|
|
220134
|
+
error: errMsg$3(err),
|
|
219854
220135
|
name: hub.name
|
|
219855
220136
|
}
|
|
219856
220137
|
});
|
|
@@ -219888,7 +220169,7 @@ async function retireMigratedHubs(retirements, ports) {
|
|
|
219888
220169
|
} catch (err) {
|
|
219889
220170
|
ports.logger.warn("Reolink hub retirement failed — retried next boot", {
|
|
219890
220171
|
tags: { deviceId: r.hubDeviceId },
|
|
219891
|
-
meta: { error: errMsg$
|
|
220172
|
+
meta: { error: errMsg$3(err) }
|
|
219892
220173
|
});
|
|
219893
220174
|
}
|
|
219894
220175
|
return retired;
|
|
@@ -220134,7 +220415,7 @@ async function enrichHostIdentifiersFromLiveApi(detection, logger) {
|
|
|
220134
220415
|
}
|
|
220135
220416
|
var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
220136
220417
|
/** The email-push SMTP password is hidden state — kept in the keychain (D787). */
|
|
220137
|
-
secretStatePaths = ["emailPushAuthPassword"];
|
|
220418
|
+
secretStatePaths = ["emailPushAuthPassword", "mailboxPendingPassword"];
|
|
220138
220419
|
addonId = REOLINK_ADDON_ID;
|
|
220139
220420
|
providerName = "Reolink";
|
|
220140
220421
|
deviceClasses = {
|
|
@@ -220148,14 +220429,13 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220148
220429
|
* so a Test click and the subsequent Save share one Baichuan login.
|
|
220149
220430
|
*/
|
|
220150
220431
|
autodetectCache = new AutodetectCache();
|
|
220432
|
+
hubManager = null;
|
|
220151
220433
|
/**
|
|
220152
|
-
*
|
|
220153
|
-
*
|
|
220154
|
-
*
|
|
220155
|
-
* lifecycle and restarted on settings change.
|
|
220434
|
+
* The camera mailbox bridge (`email-push-bridge.ts`, D999): battery cameras mail motion to a
|
|
220435
|
+
* mailbox of the `smtp-inbound` broker, the bridge turns each message into the camera's
|
|
220436
|
+
* simple event. Built in `onInitialize`, once the addon context exists.
|
|
220156
220437
|
*/
|
|
220157
|
-
|
|
220158
|
-
emailPushServer;
|
|
220438
|
+
emailBridge;
|
|
220159
220439
|
baichuanWebhookServer;
|
|
220160
220440
|
/** The `log-channels` provider for this process. Stopped on shutdown so a
|
|
220161
220441
|
* diagnostic timer is never the reason a runner refuses to exit. */
|
|
@@ -220187,54 +220467,94 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220187
220467
|
}
|
|
220188
220468
|
/**
|
|
220189
220469
|
* Enumerate this addon's live `ReolinkCamera` instances from the
|
|
220190
|
-
* global device registry. Used by the
|
|
220470
|
+
* global device registry. Used by the camera mailbox bridge to resolve
|
|
220191
220471
|
* recipients + route inbound motion. Returns `[]` when the registry
|
|
220192
220472
|
* isn't available (worker context — providers are hub-only here).
|
|
220193
220473
|
*/
|
|
220194
220474
|
listReolinkCameras() {
|
|
220195
220475
|
const all = this.ctx.kernel.deviceRegistry?.getAll() ?? [];
|
|
220196
|
-
const
|
|
220476
|
+
const emailBridge = this.emailBridge;
|
|
220197
220477
|
const webhookServer = this.baichuanWebhookServer;
|
|
220198
220478
|
const cameras = [];
|
|
220199
220479
|
for (const d of all) if (d instanceof ReolinkCamera) {
|
|
220200
|
-
if (
|
|
220480
|
+
if (emailBridge) d.setEmailPushParamsResolver(() => emailBridge.buildCameraSetupParams(d));
|
|
220201
220481
|
if (webhookServer) d.setBaichuanWebhookParamsResolver(() => webhookServer.buildCameraSetupParams(d));
|
|
220202
220482
|
cameras.push(d);
|
|
220203
220483
|
}
|
|
220204
220484
|
return cameras;
|
|
220205
220485
|
}
|
|
220206
220486
|
/**
|
|
220207
|
-
*
|
|
220208
|
-
*
|
|
220209
|
-
* (needs `this.ctx`) and memoised.
|
|
220487
|
+
* The legacy SMTP AUTH password: hidden state, kept in the keychain (D787). Only the
|
|
220488
|
+
* `email-push-to-mailbox` migration reads it, to carry it into the mailbox.
|
|
220210
220489
|
*/
|
|
220211
|
-
_emailPushAuthUsernameState = null;
|
|
220212
220490
|
_emailPushAuthPasswordState = null;
|
|
220213
|
-
get emailPushAuthUsernameState() {
|
|
220214
|
-
if (!this._emailPushAuthUsernameState) this._emailPushAuthUsernameState = this.state("emailPushAuthUsername", string(), "");
|
|
220215
|
-
return this._emailPushAuthUsernameState;
|
|
220216
|
-
}
|
|
220217
220491
|
get emailPushAuthPasswordState() {
|
|
220218
220492
|
if (!this._emailPushAuthPasswordState) this._emailPushAuthPasswordState = this.state("emailPushAuthPassword", string(), "");
|
|
220219
220493
|
return this._emailPushAuthPasswordState;
|
|
220220
220494
|
}
|
|
220221
|
-
/**
|
|
220222
|
-
|
|
220223
|
-
|
|
220224
|
-
|
|
220495
|
+
/**
|
|
220496
|
+
* The mailbox the old intake described, waiting for a provider to create it: the migration
|
|
220497
|
+
* writes it (no network), the bridge reads it when it creates the mailbox and clears it after.
|
|
220498
|
+
* The password is a keychain secret of its own (`mailboxPendingPassword`).
|
|
220499
|
+
*/
|
|
220500
|
+
_mailboxPendingState = null;
|
|
220501
|
+
_mailboxPendingPasswordState = null;
|
|
220502
|
+
get mailboxPendingState() {
|
|
220503
|
+
this._mailboxPendingState ??= this.state("mailboxPending", PendingMailboxSchema.nullable(), null);
|
|
220504
|
+
return this._mailboxPendingState;
|
|
220505
|
+
}
|
|
220506
|
+
get mailboxPendingPasswordState() {
|
|
220507
|
+
this._mailboxPendingPasswordState ??= this.state("mailboxPendingPassword", string(), "");
|
|
220508
|
+
return this._mailboxPendingPasswordState;
|
|
220509
|
+
}
|
|
220510
|
+
async stashPendingMailbox(settings) {
|
|
220511
|
+
const { password, ...rest } = settings;
|
|
220512
|
+
await this.mailboxPendingPasswordState.set(password);
|
|
220513
|
+
await this.mailboxPendingState.set(rest);
|
|
220514
|
+
}
|
|
220515
|
+
/** The pending settings, or null; throws when the stored password cannot be resolved. */
|
|
220516
|
+
async readPendingMailbox() {
|
|
220517
|
+
const pending = await this.mailboxPendingState.get();
|
|
220518
|
+
if (pending === null) return null;
|
|
220519
|
+
const password = await this.mailboxPendingPasswordState.get();
|
|
220520
|
+
const raw = (await this.resolveGlobalStore())["mailboxPendingPassword"];
|
|
220521
|
+
if (password.length === 0 && raw !== void 0 && raw !== "" && isSecretMarker(raw)) throw new Error("the pending camera mailbox password could not be resolved");
|
|
220522
|
+
return {
|
|
220523
|
+
...pending,
|
|
220524
|
+
password
|
|
220525
|
+
};
|
|
220526
|
+
}
|
|
220527
|
+
async clearPendingMailbox() {
|
|
220528
|
+
if (await this.mailboxPendingState.get() === null) return;
|
|
220529
|
+
await this.mailboxPendingState.set(null);
|
|
220530
|
+
await this.mailboxPendingPasswordState.set("");
|
|
220531
|
+
}
|
|
220532
|
+
/** The camera mailbox bridge, built once. */
|
|
220533
|
+
ensureEmailBridge() {
|
|
220534
|
+
this.emailBridge ??= new EmailPushBridge({
|
|
220535
|
+
port: createBrokerPort(this.ctx.api),
|
|
220225
220536
|
listCameras: () => this.listReolinkCameras(),
|
|
220226
|
-
|
|
220227
|
-
|
|
220228
|
-
|
|
220229
|
-
|
|
220230
|
-
writeStore: async (patch) => {
|
|
220231
|
-
const username = patch["emailPushAuthUsername"];
|
|
220232
|
-
if (typeof username === "string") await this.emailPushAuthUsernameState.set(username);
|
|
220233
|
-
const password = patch["emailPushAuthPassword"];
|
|
220234
|
-
if (typeof password === "string") await this.emailPushAuthPasswordState.set(password);
|
|
220537
|
+
logger: this.ctx.logger.child("email-push"),
|
|
220538
|
+
pending: {
|
|
220539
|
+
read: () => this.readPendingMailbox(),
|
|
220540
|
+
clear: () => this.clearPendingMailbox()
|
|
220235
220541
|
}
|
|
220236
220542
|
});
|
|
220237
|
-
return this.
|
|
220543
|
+
return this.emailBridge;
|
|
220544
|
+
}
|
|
220545
|
+
/**
|
|
220546
|
+
* The stored intake settings the migration carries into the mailbox. A password that is
|
|
220547
|
+
* stored but cannot be resolved (a locked keychain) aborts: it is never read as empty.
|
|
220548
|
+
*/
|
|
220549
|
+
async readLegacyEmailPush() {
|
|
220550
|
+
const raw = await this.resolveGlobalStore();
|
|
220551
|
+
const password = await this.emailPushAuthPasswordState.get();
|
|
220552
|
+
const stored = raw["emailPushAuthPassword"];
|
|
220553
|
+
if (password.length === 0 && stored !== void 0 && stored !== "" && isSecretMarker(stored)) throw new Error("the legacy email-push password could not be resolved");
|
|
220554
|
+
return password.length > 0 ? {
|
|
220555
|
+
...raw,
|
|
220556
|
+
emailPushAuthPassword: password
|
|
220557
|
+
} : raw;
|
|
220238
220558
|
}
|
|
220239
220559
|
/** Lazily build the HaCfg webhook intake, wiring its provider deps. */
|
|
220240
220560
|
ensureBaichuanWebhookServer() {
|
|
@@ -220311,10 +220631,9 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220311
220631
|
}
|
|
220312
220632
|
this.logChannels?.stop();
|
|
220313
220633
|
await this.clipService?.disposeClipStreamDial().catch(() => {});
|
|
220314
|
-
await this.emailPushServer?.stop().catch(() => {});
|
|
220315
220634
|
await this.baichuanWebhookServer?.stop().catch(() => {});
|
|
220316
220635
|
await this.hubManager?.shutdown().catch((err) => {
|
|
220317
|
-
this.ctx.logger.warn("Reolink NVR sessions did not close cleanly", { meta: { error: errMsg$
|
|
220636
|
+
this.ctx.logger.warn("Reolink NVR sessions did not close cleanly", { meta: { error: errMsg$3(err) } });
|
|
220318
220637
|
});
|
|
220319
220638
|
this.hubManager = null;
|
|
220320
220639
|
reolinkHubSessions.clear();
|
|
@@ -220342,7 +220661,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220342
220661
|
}
|
|
220343
220662
|
await manager.reconcile(withSettings);
|
|
220344
220663
|
} catch (err) {
|
|
220345
|
-
this.ctx.logger.warn("Reolink integration reconcile failed — sessions left as they were", { meta: { error: errMsg$
|
|
220664
|
+
this.ctx.logger.warn("Reolink integration reconcile failed — sessions left as they were", { meta: { error: errMsg$3(err) } });
|
|
220346
220665
|
}
|
|
220347
220666
|
}
|
|
220348
220667
|
subscribeIntegrationLifecycle() {
|
|
@@ -220507,9 +220826,6 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220507
220826
|
*/
|
|
220508
220827
|
async onConfigChanged() {
|
|
220509
220828
|
await this.refreshClipRefreshGlobalStore();
|
|
220510
|
-
await this.emailPushServer?.restart().catch((err) => {
|
|
220511
|
-
this.ctx.logger.warn("email-push: restart after settings change failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
220512
|
-
});
|
|
220513
220829
|
await this.baichuanWebhookServer?.restart().catch((err) => {
|
|
220514
220830
|
this.ctx.logger.warn("baichuan-webhook: restart after settings change failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
220515
220831
|
});
|
|
@@ -220529,12 +220845,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220529
220845
|
* for core to select from.
|
|
220530
220846
|
*/
|
|
220531
220847
|
globalSettingsSchema() {
|
|
220532
|
-
|
|
220533
|
-
return { sections: [
|
|
220534
|
-
...buildEmailPushSettingsSchema(recommendedHost).sections,
|
|
220535
|
-
...buildBaichuanWebhookSettingsSchema(recommendedHost).sections,
|
|
220536
|
-
...buildClipRefreshSettingsSchema().sections
|
|
220537
|
-
] };
|
|
220848
|
+
return { sections: [...buildBaichuanWebhookSettingsSchema(listLanHosts()[0] ?? "127.0.0.1").sections, ...buildClipRefreshSettingsSchema().sections] };
|
|
220538
220849
|
}
|
|
220539
220850
|
/**
|
|
220540
220851
|
* Per-instance form (D985): an NVR / Home Hub integration shows its connection only; the
|
|
@@ -220553,18 +220864,17 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220553
220864
|
} catch (err) {
|
|
220554
220865
|
this.ctx.logger.warn("Reolink integration form: instance lookup failed — full schema shown", { meta: {
|
|
220555
220866
|
integrationId: instance.integrationId,
|
|
220556
|
-
error: errMsg$
|
|
220867
|
+
error: errMsg$3(err)
|
|
220557
220868
|
} });
|
|
220558
220869
|
return null;
|
|
220559
220870
|
}
|
|
220560
220871
|
}
|
|
220561
220872
|
/**
|
|
220562
|
-
*
|
|
220563
|
-
*
|
|
220564
|
-
* `email-push-schema.ts` / `baichuan-webhook-schema.ts`.
|
|
220873
|
+
* The HaCfg webhook section IS this integration's configuration — one intake serving
|
|
220874
|
+
* every Reolink camera. See `baichuan-webhook-schema.ts`. (The camera mailbox is a broker.)
|
|
220565
220875
|
*/
|
|
220566
220876
|
integrationSettingSections() {
|
|
220567
|
-
return [...
|
|
220877
|
+
return [...BAICHUAN_WEBHOOK_SECTION_IDS];
|
|
220568
220878
|
}
|
|
220569
220879
|
async onInitialize() {
|
|
220570
220880
|
const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
|
|
@@ -220609,9 +220919,8 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220609
220919
|
this.startClipCatalogRefresh(this.clipService);
|
|
220610
220920
|
this.wireClipCatalogRefreshers();
|
|
220611
220921
|
await this.serveClipPlanes(this.clipService);
|
|
220612
|
-
this.
|
|
220613
|
-
|
|
220614
|
-
});
|
|
220922
|
+
await this.runMigrations(defineAddonMigrations(REOLINK_MIGRATIONS, this.migrationPorts()));
|
|
220923
|
+
this.wireEmailBridge();
|
|
220615
220924
|
this.ensureBaichuanWebhookServer().start().catch((err) => {
|
|
220616
220925
|
this.ctx.logger.warn("baichuan-webhook: HTTP intake failed to start at init", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
220617
220926
|
});
|
|
@@ -220621,8 +220930,9 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220621
220930
|
if (typeof data.deviceId !== "number") return;
|
|
220622
220931
|
const dev = this.ctx.kernel.deviceRegistry?.getById(data.deviceId);
|
|
220623
220932
|
if (dev instanceof ReolinkCamera) {
|
|
220624
|
-
const
|
|
220625
|
-
dev.setEmailPushParamsResolver(() =>
|
|
220933
|
+
const emailBridge = this.ensureEmailBridge();
|
|
220934
|
+
dev.setEmailPushParamsResolver(() => emailBridge.buildCameraSetupParams(dev));
|
|
220935
|
+
emailBridge.sync();
|
|
220626
220936
|
const webhookServer = this.ensureBaichuanWebhookServer();
|
|
220627
220937
|
dev.setBaichuanWebhookParamsResolver(() => webhookServer.buildCameraSetupParams(dev));
|
|
220628
220938
|
this.wireClipCatalogRefreshers();
|
|
@@ -220648,8 +220958,36 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220648
220958
|
capability: deviceAdoptionCapability,
|
|
220649
220959
|
provider: this.buildAdoptionProvider()
|
|
220650
220960
|
});
|
|
220961
|
+
regs.push({
|
|
220962
|
+
capability: deviceRemovalHookCapability,
|
|
220963
|
+
provider: createEmailPushRemovalHook({ resync: () => this.ensureEmailBridge().sync() })
|
|
220964
|
+
});
|
|
220651
220965
|
return regs;
|
|
220652
220966
|
}
|
|
220967
|
+
/** What the catalogue's migration reads and writes (D987). */
|
|
220968
|
+
migrationPorts() {
|
|
220969
|
+
return {
|
|
220970
|
+
readStored: () => this.readLegacyEmailPush(),
|
|
220971
|
+
stashPending: (settings) => this.stashPendingMailbox(settings),
|
|
220972
|
+
warn: (message) => {
|
|
220973
|
+
this.ctx.logger.warn(message);
|
|
220974
|
+
}
|
|
220975
|
+
};
|
|
220976
|
+
}
|
|
220977
|
+
/** Listen to the mailbox's messages and keep the declared recipients current. */
|
|
220978
|
+
wireEmailBridge() {
|
|
220979
|
+
const bridge = this.ensureEmailBridge();
|
|
220980
|
+
this.subscribe({ category: EventCategory.BrokerMessage }, (e) => {
|
|
220981
|
+
bridge.onMessage(e.data);
|
|
220982
|
+
});
|
|
220983
|
+
this.subscribe({ category: EventCategory.BrokerStatusChanged }, (e) => {
|
|
220984
|
+
bridge.onStatusChanged(e.data);
|
|
220985
|
+
});
|
|
220986
|
+
this.subscribe({ category: EventCategory.SystemReadyState }, (e) => {
|
|
220987
|
+
bridge.onProviderReady(e.data);
|
|
220988
|
+
});
|
|
220989
|
+
bridge.sync();
|
|
220990
|
+
}
|
|
220653
220991
|
/** The clip catalog, its thumbnail cache and its two routes (D549). */
|
|
220654
220992
|
clipService = null;
|
|
220655
220993
|
clipRefreshTimer = null;
|
|
@@ -221013,8 +221351,8 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
221013
221351
|
onAfterCreate: async (device) => {
|
|
221014
221352
|
if (device instanceof ReolinkCamera) {
|
|
221015
221353
|
device.adoptApi(detectedApi);
|
|
221016
|
-
const
|
|
221017
|
-
device.setEmailPushParamsResolver(() =>
|
|
221354
|
+
const emailBridge = this.ensureEmailBridge();
|
|
221355
|
+
device.setEmailPushParamsResolver(() => emailBridge.buildCameraSetupParams(device));
|
|
221018
221356
|
const webhookServer = this.ensureBaichuanWebhookServer();
|
|
221019
221357
|
device.setBaichuanWebhookParamsResolver(() => webhookServer.buildCameraSetupParams(device));
|
|
221020
221358
|
}
|