@7365admin1/core 3.32.2-staging.75 → 3.32.2-staging.77

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.
@@ -0,0 +1,19 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ Stop logging camera and mailer credentials
6
+
7
+ Six places wrote a secret or a resident's details to stdout on an ordinary code
8
+ path. `getTrafficJunction` dumped the whole `TSiteCamera` object — including the
9
+ camera's `username` and plaintext `password` — on every reconnect, and
10
+ `addPlateNumber` logged its argument, which carries the same password plus a
11
+ resident's plate number and owner name. `checkOutBySiteAndPlate` logged the
12
+ matched visitor transaction. `siteCameraRepo.updateById` logged the re-read
13
+ camera document, and `vehicleService.add` logged the whole array of a site's
14
+ cameras, both with credentials in them. `sendEmail` printed `MAILER_PASSWORD`
15
+ itself on every message sent.
16
+
17
+ All now log identifiers, counts and outcomes only, through the module's logger
18
+ rather than `console.log`; the mailer line is removed outright because it
19
+ carried nothing but the secret.
@@ -0,0 +1,20 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ Fix three Dahua HTTP API defects found by checking our integration against the
6
+ vendor specification (V3.37):
7
+
8
+ - The ANPR reconnect loop retried authentication ten times at one-second
9
+ intervals. The device locks the account for 1800 seconds after three failed
10
+ logins in thirty, so our own recovery could take ANPR down at a site for half
11
+ an hour. Authentication now backs off well past the device's counting window,
12
+ and a network fault is distinguished from an authentication failure.
13
+ - 401 and 403 were handled backwards. Per spec §3.4, 401 is the digest
14
+ challenge (retry) and 403 is a credentials rejection (stop). We treated 403 as
15
+ an account lock and killed the listener, and counted 401s toward a "wrong
16
+ password" warning.
17
+ - `updatePlateNumber`, `bulkInsertPlateNumber` and `removePlateNumber`
18
+ interpolated values into the CGI query string unencoded, against the spec's
19
+ stated RFC 3986 requirement. A plate or owner name containing a space or "&"
20
+ broke the request or appended extra parameters to it.
package/dist/index.js CHANGED
@@ -22700,7 +22700,6 @@ async function sendEmail({ to, subject, text, html }) {
22700
22700
  pass: MAILER_PASSWORD
22701
22701
  }
22702
22702
  });
22703
- console.log(MAILER_EMAIL, MAILER_PASSWORD, MAILER_TRANSPORT_HOST, MAILER_TRANSPORT_PORT);
22704
22703
  const mailOptions = {
22705
22704
  from: `Seven 365 <${MAILER_EMAIL}>`,
22706
22705
  to,
@@ -24778,6 +24777,45 @@ function shouldAlertOnFailure(consecutiveFailures) {
24778
24777
  return (consecutiveFailures - CAMERA_ALERT_EVERY_AFTER) % CAMERA_ALERT_EVERY_AFTER === 0;
24779
24778
  }
24780
24779
 
24780
+ // src/utils/dahua-protocol.util.ts
24781
+ var DAHUA_AUTH_ATTEMPTS_BEFORE_ALERT = 2;
24782
+ var AUTH_BACKOFF_MS = [6e4, 3e5, 9e5];
24783
+ function dahuaAuthBackoffMs(consecutiveAuthFailures) {
24784
+ const index = Math.min(
24785
+ Math.max(consecutiveAuthFailures, 1),
24786
+ AUTH_BACKOFF_MS.length
24787
+ );
24788
+ return AUTH_BACKOFF_MS[index - 1];
24789
+ }
24790
+ function classifyDahuaStatus(statusCode) {
24791
+ if (statusCode === 401)
24792
+ return "challenge";
24793
+ if (statusCode === 403)
24794
+ return "rejected";
24795
+ if (statusCode === 400 || statusCode === 500)
24796
+ return "fatal";
24797
+ return "proceed";
24798
+ }
24799
+ function classifyDahuaFailure(message) {
24800
+ const text = String(message ?? "");
24801
+ if (/\b403\b|forbidden/i.test(text))
24802
+ return "rejected";
24803
+ if (/\b401\b|unauthorized|invalid authority|digest/i.test(text)) {
24804
+ return "challenge";
24805
+ }
24806
+ return "transient";
24807
+ }
24808
+ var enc = (value) => encodeURIComponent(String(value));
24809
+ function buildPlateUpdateEndpoint(value) {
24810
+ return `/cgi-bin/recordUpdater.cgi?action=update&name=${enc(value.mode)}&recno=${enc(value.recno)}&PlateNumber=${enc(value.plateNumber)}&BeginTime=${enc(value.start)}&CancelTime=${enc(value.end)}&+OpenGate=${value.openGate}&MasterOfCar=${enc(value.owner)}`;
24811
+ }
24812
+ function buildPlateInsertEndpoint(value) {
24813
+ return `/cgi-bin/recordUpdater.cgi?action=insert&name=${enc(value.mode)}&PlateNumber=${enc(value.plateNumber)}&VehicleType=${enc(value.vehicleType)}&VehicleColor=${enc(value.vehicleColor)}&BeginTime=${enc(value.start)}&CancelTime=${enc(value.end)}&+OpenGate=${value.openGate}&MasterOfCar=${enc(value.owner)}`;
24814
+ }
24815
+ function buildPlateRemoveEndpoint(value) {
24816
+ return `/cgi-bin/recordUpdater.cgi?action=remove&recno=${enc(value.recno)}&name=${enc(value.mode)}`;
24817
+ }
24818
+
24781
24819
  // src/services/dahua.service.ts
24782
24820
  var cameraRegistry = /* @__PURE__ */ new Map();
24783
24821
  var _savedOnDetected;
@@ -24823,7 +24861,8 @@ async function useDahuaDigestWithRetry({
24823
24861
  retryDelayMs = 0
24824
24862
  }) {
24825
24863
  let lastError;
24826
- for (let attempt = 1; attempt <= retries; attempt++) {
24864
+ const effectiveRetries = Math.min(retries, DAHUA_AUTH_ATTEMPTS_BEFORE_ALERT);
24865
+ for (let attempt = 1; attempt <= effectiveRetries; attempt++) {
24827
24866
  try {
24828
24867
  const effectiveTimeout = streaming ? 0 : timeout;
24829
24868
  const response = await useDahuaDigest({
@@ -24850,9 +24889,9 @@ async function useDahuaDigestWithRetry({
24850
24889
  }
24851
24890
  const errorText = streaming ? `HTTP ${statusCode}` : getDahuaResponseText(response);
24852
24891
  const shouldRetry = isDahuaInvalidAuthority(errorText);
24853
- if (shouldRetry && attempt < retries) {
24892
+ if (shouldRetry && attempt < effectiveRetries) {
24854
24893
  loggerDahua.warn(
24855
- `[${host}] Dahua Invalid Authority. Retrying ${attempt}/${retries}`
24894
+ `[${host}] Dahua Invalid Authority. Retrying ${attempt}/${effectiveRetries}`
24856
24895
  );
24857
24896
  if (retryDelayMs > 0) {
24858
24897
  await sleep(retryDelayMs);
@@ -24864,9 +24903,9 @@ async function useDahuaDigestWithRetry({
24864
24903
  lastError = error;
24865
24904
  const message = error?.message || String(error);
24866
24905
  const shouldRetry = isDahuaInvalidAuthority(message);
24867
- if (shouldRetry && attempt < retries) {
24906
+ if (shouldRetry && attempt < effectiveRetries) {
24868
24907
  loggerDahua.warn(
24869
- `[${host}] Dahua Invalid Authority error. Retrying ${attempt}/${retries}`
24908
+ `[${host}] Dahua Invalid Authority error. Retrying ${attempt}/${effectiveRetries}`
24870
24909
  );
24871
24910
  if (retryDelayMs > 0) {
24872
24911
  await sleep(retryDelayMs);
@@ -24970,7 +25009,9 @@ function useDahuaService() {
24970
25009
  return;
24971
25010
  try {
24972
25011
  const result = await _checkOutBySiteAndPlate(site, plateNumber2);
24973
- console.log("checkOutBySiteAndPlate result", result);
25012
+ loggerDahua.info(
25013
+ `checkOutBySiteAndPlate matched=${Boolean(result)} type=${result?.type ?? "none"}`
25014
+ );
24974
25015
  if (onDetected2 && result?.type != "resident" && result?.site) {
24975
25016
  onDetected2({ reload: true, site: result?.site?.toString(), siteName: camera?.siteName, cameraDirection: camera?.direction, direction });
24976
25017
  }
@@ -25269,11 +25310,12 @@ function useDahuaService() {
25269
25310
  );
25270
25311
  }
25271
25312
  async function getTrafficJunction(camera, signal, onDetected) {
25272
- console.log(`getTrafficJunction camera object`, camera);
25313
+ loggerDahua.info(
25314
+ `getTrafficJunction starting for camera ${camera?._id} [${camera?.siteName}-${camera?.direction}] host ${camera?.host}`
25315
+ );
25273
25316
  let authFailureCount = 0;
25274
25317
  let connectionFailureCount = 0;
25275
25318
  let alertedFailureCount = 0;
25276
- const MAX_AUTH_RETRIES = 10;
25277
25319
  while (!signal.aborted) {
25278
25320
  let bufferQueue = null;
25279
25321
  let response = null;
@@ -25288,23 +25330,26 @@ function useDahuaService() {
25288
25330
  streaming: true
25289
25331
  });
25290
25332
  const statusCode = getDahuaStatusCode(response);
25291
- if (statusCode === 401) {
25292
- loggerDahua.error(`[${camera?.siteName}-${camera?.direction}] 401 Unauthorized - Handshake or Wrong Credentials`);
25293
- console.log(`[${camera?.siteName}-${camera?.direction}] 401 Unauthorized - Handshake or Wrong Credentials`);
25294
- throw new Error("401 Unauthorized - Handshake or Wrong Credentials");
25295
- } else if (statusCode === 403) {
25333
+ const verdict = classifyDahuaStatus(statusCode);
25334
+ if (verdict === "challenge") {
25335
+ loggerDahua.warn(`[${camera?.siteName}-${camera?.direction}] 401 - digest challenge not completed; will re-authenticate after backoff.`);
25336
+ throw new Error("401 Unauthorized - digest challenge not completed");
25337
+ } else if (verdict === "rejected") {
25296
25338
  loggerDahua.error(
25297
- `[${camera?.siteName}-${camera?.direction}] 403 Forbidden. Account locked or invalid permissions.`
25339
+ `[${camera?.siteName}-${camera?.direction}] 403 Forbidden - credentials rejected by the device. Not retrying.`
25298
25340
  );
25299
25341
  if (onDetected) {
25300
25342
  onDetected({
25301
25343
  site: camera?.site,
25302
25344
  direction: camera?.direction,
25345
+ // Superseded #1803's own wording: cameraCredentialsMessage already
25346
+ // says the login was refused and where to correct it, and no longer
25347
+ // claims a ten-minute lockout.
25303
25348
  messagePermanent: cameraCredentialsMessage(camera)
25304
25349
  });
25305
25350
  }
25306
25351
  return;
25307
- } else if ([400, 500].includes(statusCode)) {
25352
+ } else if (verdict === "fatal") {
25308
25353
  loggerDahua.error(`[${camera?.siteName}-${camera?.direction}] Connection error: ${statusCode}`);
25309
25354
  if (onDetected) {
25310
25355
  onDetected({ site: camera?.site, messagePermanent: cameraNotAuthorisedMessage(camera) });
@@ -25372,9 +25417,10 @@ function useDahuaService() {
25372
25417
  break;
25373
25418
  }
25374
25419
  const errMsg = String(error?.message || error);
25375
- if (errMsg.includes("403") || errMsg.includes("Forbidden")) {
25420
+ const failure = classifyDahuaFailure(errMsg);
25421
+ if (failure === "rejected") {
25376
25422
  loggerDahua.error(
25377
- `[${camera?.siteName}-${camera?.direction}] 403 Forbidden thrown by Dahua client. Account locked or invalid credentials.`
25423
+ `[${camera?.siteName}-${camera?.direction}] 403 Forbidden thrown by Dahua client - credentials rejected. Not retrying.`
25378
25424
  );
25379
25425
  if (onDetected) {
25380
25426
  onDetected({
@@ -25385,16 +25431,15 @@ function useDahuaService() {
25385
25431
  }
25386
25432
  return;
25387
25433
  }
25388
- const isAuthError = errMsg.includes("401") || errMsg.includes("Unauthorized") || errMsg.includes("Invalid Authority");
25389
- if (isAuthError) {
25434
+ if (failure === "challenge") {
25390
25435
  authFailureCount++;
25391
25436
  isAuthFailure = true;
25392
25437
  loggerDahua.warn(
25393
- `[${camera?.siteName}-${camera?.direction}] Auth attempt ${authFailureCount}/${MAX_AUTH_RETRIES}`
25438
+ `[${camera?.siteName}-${camera?.direction}] Authentication attempt ${authFailureCount} failed; next attempt in ${dahuaAuthBackoffMs(authFailureCount) / 1e3}s`
25394
25439
  );
25395
- if (authFailureCount >= MAX_AUTH_RETRIES) {
25440
+ if (authFailureCount === DAHUA_AUTH_ATTEMPTS_BEFORE_ALERT) {
25396
25441
  loggerDahua.error(
25397
- `[${camera?.siteName}-${camera?.direction}] Max auth failures reached (${MAX_AUTH_RETRIES}). Wrong credentials.`
25442
+ `[${camera?.siteName}-${camera?.direction}] ${authFailureCount} consecutive authentication failures. Slowing down and alerting the operator.`
25398
25443
  );
25399
25444
  if (onDetected) {
25400
25445
  onDetected({
@@ -25403,7 +25448,6 @@ function useDahuaService() {
25403
25448
  messagePermanent: cameraCredentialsMessage(camera)
25404
25449
  });
25405
25450
  }
25406
- return;
25407
25451
  }
25408
25452
  } else {
25409
25453
  authFailureCount = 0;
@@ -25443,7 +25487,7 @@ function useDahuaService() {
25443
25487
  }
25444
25488
  }
25445
25489
  if (!signal.aborted) {
25446
- const waitMs = isAuthFailure ? 1e3 : 1e4;
25490
+ const waitMs = isAuthFailure ? dahuaAuthBackoffMs(authFailureCount) : 1e4;
25447
25491
  await new Promise((res) => setTimeout(res, waitMs));
25448
25492
  }
25449
25493
  }
@@ -25456,7 +25500,9 @@ function useDahuaService() {
25456
25500
  return match ? match[1] : null;
25457
25501
  }
25458
25502
  async function addPlateNumber(value) {
25459
- console.log("addPlateNumber called with value:", value);
25503
+ loggerDahua.info(
25504
+ `addPlateNumber called for host ${value?.host} mode ${value?.mode}`
25505
+ );
25460
25506
  let recno = null;
25461
25507
  const validation = import_joi40.default.object({
25462
25508
  host: import_joi40.default.string().required(),
@@ -25601,7 +25647,15 @@ function useDahuaService() {
25601
25647
  const openGateString = String(value.isOpenGate);
25602
25648
  _isOpenGate = openGateString ? openGateString : "true";
25603
25649
  }
25604
- const endpoint = `/cgi-bin/recordUpdater.cgi?action=update&name=${value.mode}&recno=${value.recno}&PlateNumber=${value.plateNumber}&BeginTime=${value.start}&CancelTime=${value.end}&+OpenGate=${_isOpenGate}&MasterOfCar=${value.owner}`;
25650
+ const endpoint = buildPlateUpdateEndpoint({
25651
+ mode: value.mode,
25652
+ recno: value.recno,
25653
+ plateNumber: value.plateNumber,
25654
+ start: value.start,
25655
+ end: value.end,
25656
+ owner: value.owner,
25657
+ openGate: _isOpenGate
25658
+ });
25605
25659
  try {
25606
25660
  const response = await useDahuaDigest({
25607
25661
  host: value.host,
@@ -25634,7 +25688,10 @@ function useDahuaService() {
25634
25688
  host: value.host,
25635
25689
  username: value.username,
25636
25690
  password: value.password,
25637
- endpoint: `/cgi-bin/recordUpdater.cgi?action=remove&recno=${value.recno}&name=${value.mode}`
25691
+ endpoint: buildPlateRemoveEndpoint({
25692
+ recno: value.recno,
25693
+ mode: value.mode
25694
+ })
25638
25695
  });
25639
25696
  return response;
25640
25697
  } catch (error2) {
@@ -25690,7 +25747,16 @@ function useDahuaService() {
25690
25747
  value.vehicleColor = String(value.vehicleColor ?? "").substring(0, 31) || "unknown";
25691
25748
  const _openGate = String(value.isOpenGate);
25692
25749
  const isOpenGateString = _openGate && _openGate !== "undefined" ? _openGate : "true";
25693
- const endpoint = `/cgi-bin/recordUpdater.cgi?action=insert&name=${value.mode}&PlateNumber=${value.plateNumber}&VehicleType=${value.vehicleType}&VehicleColor=${value.vehicleColor}&BeginTime=${value.start}&CancelTime=${value.end}&+OpenGate=${isOpenGateString}&MasterOfCar=${value.owner}`;
25750
+ const endpoint = buildPlateInsertEndpoint({
25751
+ mode: value.mode,
25752
+ plateNumber: value.plateNumber,
25753
+ vehicleType: value.vehicleType,
25754
+ vehicleColor: value.vehicleColor,
25755
+ start: value.start,
25756
+ end: value.end,
25757
+ owner: value.owner,
25758
+ openGate: isOpenGateString
25759
+ });
25694
25760
  try {
25695
25761
  const response = await useDahuaDigest({
25696
25762
  host: value.host,
@@ -26116,7 +26182,7 @@ function useSiteCameraRepo() {
26116
26182
  }
26117
26183
  if (result?._id) {
26118
26184
  const result2 = await findOne({ _id: new import_mongodb46.ObjectId(result._id) }, void 0, { session });
26119
- console.log("updateById result2", result2);
26185
+ import_node_server_utils74.logger.info(`Site camera ${result2?._id} re-read after update; reconnecting listener`);
26120
26186
  if (result2?._id) {
26121
26187
  const { listenToCamera } = useDahuaService();
26122
26188
  await listenToCamera(result2);
@@ -27642,7 +27708,9 @@ function useVehicleService() {
27642
27708
  siteCameras.push(...siteCameraReq.items);
27643
27709
  page++;
27644
27710
  } while (page <= pages);
27645
- console.log("add vehicle service siteCameras", siteCameras);
27711
+ import_node_server_utils78.logger.info(
27712
+ `addVehicle resolved ${siteCameras.length} site camera(s) for site ${value?.site}`
27713
+ );
27646
27714
  }
27647
27715
  for (const plateNumber of plateNumbers) {
27648
27716
  const vehicleValue = {