@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 +45 -3
- package/dist/addon.mjs +45 -3
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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",
|