@camstack/addon-provider-hikvision 1.2.124 → 1.2.125

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/dist/addon.js CHANGED
@@ -25023,6 +25023,39 @@ _enum([
25023
25023
  _enum(["deferred", "final"]);
25024
25024
  /** The camera's / provider's own words. Kept verbatim, never parsed. */
25025
25025
  var CLIP_STILL_REASON_HEADER = "x-camstack-reason";
25026
+ /**
25027
+ * Make a refusal sentence safe to put in an HTTP header.
25028
+ *
25029
+ * **An HTTP header value is Latin-1.** Node throws `Invalid character in
25030
+ * header content` on anything above U+00FF, the handler dies mid-response,
25031
+ * and the client gets a socket hang up — no status, no reason, nothing. The
25032
+ * mechanism that exists to EXPLAIN a refusal is then the thing that silences
25033
+ * it, and the surface shows "nothing happened".
25034
+ *
25035
+ * That is not hypothetical: measured 2026-09-23 on 1436, a Hikvision replay
25036
+ * refusal read `SETUP of the video track answered 500 — the camera refused
25037
+ * this replay`, and the EM DASH in it threw on every clip the operator
25038
+ * opened. This repository writes its refusals in prose, and its prose uses
25039
+ * `—`, `’` and `…` everywhere, so every reason string is a candidate.
25040
+ *
25041
+ * Substitutes rather than strips: an operator reading a log or a response
25042
+ * should get a readable sentence, not one with holes in it. The typographic
25043
+ * characters this repo actually writes have exact ASCII equivalents; anything
25044
+ * else non-Latin-1 becomes `?`, which is visible rather than silent. Control
25045
+ * characters go too — a newline in a header value is a response-splitting
25046
+ * bug, not a formatting one.
25047
+ */
25048
+ function clipReasonHeaderValue(reason) {
25049
+ const folded = reason.replace(/[\u2013\u2014\u2212]/g, "-").replace(/[\u2018\u2019\u201b]/g, "'").replace(/[\u201c\u201d]/g, "\"").replace(/\u2026/g, "...").replace(/[\u00a0\u2007\u202f]/g, " ");
25050
+ let out = "";
25051
+ for (const ch of folded) {
25052
+ const code = ch.codePointAt(0) ?? 0;
25053
+ if (code < 32 || code === 127) out += " ";
25054
+ else if (code > 255) out += "?";
25055
+ else out += ch;
25056
+ }
25057
+ return out.slice(0, 500).trim();
25058
+ }
25026
25059
  /** One {@link ClipStillRefusalCodeSchema} token, alone. */
25027
25060
  var CLIP_STILL_REASON_CODE_HEADER = "x-camstack-reason-code";
25028
25061
  /**
@@ -50289,7 +50322,7 @@ function createClipStreamHandler(deps) {
50289
50322
  status: resolved.status
50290
50323
  } });
50291
50324
  res.writeHead(resolved.status, {
50292
- [CLIP_STILL_REASON_HEADER]: resolved.detail.slice(0, 500),
50325
+ [CLIP_STILL_REASON_HEADER]: clipReasonHeaderValue(resolved.detail),
50293
50326
  [CLIP_STILL_REASON_CODE_HEADER]: resolved.code
50294
50327
  });
50295
50328
  res.end();
@@ -50362,7 +50395,7 @@ function createClipStreamHandler(deps) {
50362
50395
  }
50363
50396
  });
50364
50397
  res.writeHead(streamRefusalStatus(outcome.code), {
50365
- [CLIP_STILL_REASON_HEADER]: outcome.detail.slice(0, 500),
50398
+ [CLIP_STILL_REASON_HEADER]: clipReasonHeaderValue(outcome.detail),
50366
50399
  [CLIP_STILL_REASON_CODE_HEADER]: outcome.code
50367
50400
  });
50368
50401
  res.end();
@@ -53126,7 +53159,16 @@ function createClipService(deps) {
53126
53159
  logger,
53127
53160
  resolvePath: async (deviceId, clipKey) => {
53128
53161
  const file = await fileFor(deviceId, clipKey);
53129
- return file.kind === "ok" ? file.path : null;
53162
+ if (file.kind === "ok") return file.path;
53163
+ deps.logger.warn("videoclips: a clip file was refused", {
53164
+ tags: { deviceId },
53165
+ meta: {
53166
+ clipKey,
53167
+ branch: file.code,
53168
+ reason: file.detail
53169
+ }
53170
+ });
53171
+ return null;
53130
53172
  }
53131
53173
  }),
53132
53174
  streamHandler: createClipStreamHandler({
package/dist/addon.mjs CHANGED
@@ -25024,6 +25024,39 @@ _enum([
25024
25024
  _enum(["deferred", "final"]);
25025
25025
  /** The camera's / provider's own words. Kept verbatim, never parsed. */
25026
25026
  var CLIP_STILL_REASON_HEADER = "x-camstack-reason";
25027
+ /**
25028
+ * Make a refusal sentence safe to put in an HTTP header.
25029
+ *
25030
+ * **An HTTP header value is Latin-1.** Node throws `Invalid character in
25031
+ * header content` on anything above U+00FF, the handler dies mid-response,
25032
+ * and the client gets a socket hang up — no status, no reason, nothing. The
25033
+ * mechanism that exists to EXPLAIN a refusal is then the thing that silences
25034
+ * it, and the surface shows "nothing happened".
25035
+ *
25036
+ * That is not hypothetical: measured 2026-09-23 on 1436, a Hikvision replay
25037
+ * refusal read `SETUP of the video track answered 500 — the camera refused
25038
+ * this replay`, and the EM DASH in it threw on every clip the operator
25039
+ * opened. This repository writes its refusals in prose, and its prose uses
25040
+ * `—`, `’` and `…` everywhere, so every reason string is a candidate.
25041
+ *
25042
+ * Substitutes rather than strips: an operator reading a log or a response
25043
+ * should get a readable sentence, not one with holes in it. The typographic
25044
+ * characters this repo actually writes have exact ASCII equivalents; anything
25045
+ * else non-Latin-1 becomes `?`, which is visible rather than silent. Control
25046
+ * characters go too — a newline in a header value is a response-splitting
25047
+ * bug, not a formatting one.
25048
+ */
25049
+ function clipReasonHeaderValue(reason) {
25050
+ const folded = reason.replace(/[\u2013\u2014\u2212]/g, "-").replace(/[\u2018\u2019\u201b]/g, "'").replace(/[\u201c\u201d]/g, "\"").replace(/\u2026/g, "...").replace(/[\u00a0\u2007\u202f]/g, " ");
25051
+ let out = "";
25052
+ for (const ch of folded) {
25053
+ const code = ch.codePointAt(0) ?? 0;
25054
+ if (code < 32 || code === 127) out += " ";
25055
+ else if (code > 255) out += "?";
25056
+ else out += ch;
25057
+ }
25058
+ return out.slice(0, 500).trim();
25059
+ }
25027
25060
  /** One {@link ClipStillRefusalCodeSchema} token, alone. */
25028
25061
  var CLIP_STILL_REASON_CODE_HEADER = "x-camstack-reason-code";
25029
25062
  /**
@@ -50290,7 +50323,7 @@ function createClipStreamHandler(deps) {
50290
50323
  status: resolved.status
50291
50324
  } });
50292
50325
  res.writeHead(resolved.status, {
50293
- [CLIP_STILL_REASON_HEADER]: resolved.detail.slice(0, 500),
50326
+ [CLIP_STILL_REASON_HEADER]: clipReasonHeaderValue(resolved.detail),
50294
50327
  [CLIP_STILL_REASON_CODE_HEADER]: resolved.code
50295
50328
  });
50296
50329
  res.end();
@@ -50363,7 +50396,7 @@ function createClipStreamHandler(deps) {
50363
50396
  }
50364
50397
  });
50365
50398
  res.writeHead(streamRefusalStatus(outcome.code), {
50366
- [CLIP_STILL_REASON_HEADER]: outcome.detail.slice(0, 500),
50399
+ [CLIP_STILL_REASON_HEADER]: clipReasonHeaderValue(outcome.detail),
50367
50400
  [CLIP_STILL_REASON_CODE_HEADER]: outcome.code
50368
50401
  });
50369
50402
  res.end();
@@ -53127,7 +53160,16 @@ function createClipService(deps) {
53127
53160
  logger,
53128
53161
  resolvePath: async (deviceId, clipKey) => {
53129
53162
  const file = await fileFor(deviceId, clipKey);
53130
- return file.kind === "ok" ? file.path : null;
53163
+ if (file.kind === "ok") return file.path;
53164
+ deps.logger.warn("videoclips: a clip file was refused", {
53165
+ tags: { deviceId },
53166
+ meta: {
53167
+ clipKey,
53168
+ branch: file.code,
53169
+ reason: file.detail
53170
+ }
53171
+ });
53172
+ return null;
53131
53173
  }
53132
53174
  }),
53133
53175
  streamHandler: createClipStreamHandler({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-hikvision",
3
- "version": "1.2.124",
3
+ "version": "1.2.125",
4
4
  "description": "Hikvision camera device provider addon for CamStack — ISAPI over HTTP(S) with digest auth (snapshot, alarm stream, RTSP discovery)",
5
5
  "keywords": [
6
6
  "camstack",