@camstack/types 1.2.50 → 1.2.52

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/node.d.ts CHANGED
@@ -4,6 +4,8 @@ export { ensureFfmpeg, getFfmpegDownloadUrl } from './deps/ffmpeg-downloader.js'
4
4
  export { ensurePython, installPythonPackages, installPythonRequirements, getPythonDownloadUrl, PYTHON_VERSION, } from './deps/python-downloader.js';
5
5
  export { FilesystemStorageProvider } from './storage/filesystem-storage-provider.js';
6
6
  export { canonicalHash } from './utils/canonical-hash.js';
7
+ export { signExpiringUrl, verifyExpiringUrl } from './utils/expiring-url-signature.js';
8
+ export type { VerifyExpiringUrlInput } from './utils/expiring-url-signature.js';
7
9
  export { canonicalDeviceFingerprint, diffExportTargets, resolveExportFingerprint, } from './utils/export-reconciler.js';
8
10
  export type { DeviceExportShape, ExportDelta, ExportTargetEntry, } from './utils/export-reconciler.js';
9
11
  export { FfmpegProcess } from './ffmpeg/process.js';
package/dist/node.js CHANGED
@@ -601,6 +601,61 @@ var FilesystemStorageProvider = class {
601
601
  }
602
602
  };
603
603
  //#endregion
604
+ //#region src/utils/expiring-url-signature.ts
605
+ /**
606
+ * The ONE derivation of "a signed, expiring URL".
607
+ *
608
+ * This repo mints unguessable, self-expiring links in three places now — the
609
+ * notification artifact plane, the Home Assistant media plane, and the snapshot
610
+ * link plane. The first two grew independently and are byte-identical logic
611
+ * (`hmac(secret, "<id>:<exp>")`, expiry checked before a constant-time compare),
612
+ * each restating the crypto locally because **addons never import each other**.
613
+ *
614
+ * That reason is real, and the conclusion drawn from it was wrong. Two copies of
615
+ * a signing scheme is how one of them quietly ends up with a different TTL, a
616
+ * different compare, or a missing expiry check, and nothing fails until a link
617
+ * that should have died keeps working. The fix is the same one D52 applies to
618
+ * crop geometry: one derivation, in a place every addon may depend on. A
619
+ * framework package is exactly that place — `@camstack/types/node`, off the root
620
+ * entry because `node:crypto` must never be traversed by a browser bundler.
621
+ *
622
+ * What this module deliberately does NOT decide: the TTL, the base URL, the
623
+ * shape of `id`, and the access level of the route. Those are per-plane policy
624
+ * and each caller states them where a reader can see them.
625
+ */
626
+ /**
627
+ * The signature over `(id, expMs)`.
628
+ *
629
+ * `id` is whatever the plane uses to name the thing being served — an artifact
630
+ * id, a track id, a `<deviceId>:<width>` pair. It is joined with `:` so a caller
631
+ * must not put a `:` inside a field whose boundary matters; where a plane has
632
+ * more than one field, it composes them itself and owns that ambiguity.
633
+ */
634
+ function signExpiringUrl(secret, id, expMs) {
635
+ return (0, node_crypto.createHmac)("sha256", secret).update(`${id}:${String(expMs)}`).digest("hex");
636
+ }
637
+ /**
638
+ * Verify a request's `(id, exp, sig)`.
639
+ *
640
+ * **Expiry is checked BEFORE the compare**, so an expired link is refused
641
+ * whether or not its signature is valid — a leaked URL stops working on its own
642
+ * and cannot be kept alive by holding a correct signature. The compare itself is
643
+ * constant-time so a public route cannot be probed for the signature byte by
644
+ * byte.
645
+ */
646
+ function verifyExpiringUrl(input) {
647
+ const { secret, id, exp, sig, nowMs } = input;
648
+ if (exp === void 0 || sig === void 0) return false;
649
+ const expMs = typeof exp === "number" ? exp : Number(exp);
650
+ if (!Number.isFinite(expMs)) return false;
651
+ if (expMs <= nowMs) return false;
652
+ const expected = signExpiringUrl(secret, id, expMs);
653
+ const a = Buffer.from(expected, "utf8");
654
+ const b = Buffer.from(sig, "utf8");
655
+ if (a.length !== b.length) return false;
656
+ return (0, node_crypto.timingSafeEqual)(a, b);
657
+ }
658
+ //#endregion
604
659
  //#region src/utils/export-reconciler.ts
605
660
  /**
606
661
  * Compute the stable 64-char lowercase-hex fingerprint of a device's
@@ -1446,3 +1501,5 @@ exports.getPythonDownloadUrl = getPythonDownloadUrl;
1446
1501
  exports.installPythonPackages = installPythonPackages;
1447
1502
  exports.installPythonRequirements = installPythonRequirements;
1448
1503
  exports.resolveExportFingerprint = resolveExportFingerprint;
1504
+ exports.signExpiringUrl = signExpiringUrl;
1505
+ exports.verifyExpiringUrl = verifyExpiringUrl;
package/dist/node.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { n as Fmp4BoxSplitter, o as buildFfmpegArgs, t as canonicalHash, u as isSoftwareDecode } from "./canonical-hash-rO1sRmEK.mjs";
2
2
  import { t as errMsg } from "./err-msg-IQTHeDzc.mjs";
3
- import { createHash, randomUUID } from "node:crypto";
3
+ import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
4
4
  import * as fs from "node:fs";
5
5
  import { chmodSync, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
6
6
  import * as path from "node:path";
@@ -578,6 +578,61 @@ var FilesystemStorageProvider = class {
578
578
  }
579
579
  };
580
580
  //#endregion
581
+ //#region src/utils/expiring-url-signature.ts
582
+ /**
583
+ * The ONE derivation of "a signed, expiring URL".
584
+ *
585
+ * This repo mints unguessable, self-expiring links in three places now — the
586
+ * notification artifact plane, the Home Assistant media plane, and the snapshot
587
+ * link plane. The first two grew independently and are byte-identical logic
588
+ * (`hmac(secret, "<id>:<exp>")`, expiry checked before a constant-time compare),
589
+ * each restating the crypto locally because **addons never import each other**.
590
+ *
591
+ * That reason is real, and the conclusion drawn from it was wrong. Two copies of
592
+ * a signing scheme is how one of them quietly ends up with a different TTL, a
593
+ * different compare, or a missing expiry check, and nothing fails until a link
594
+ * that should have died keeps working. The fix is the same one D52 applies to
595
+ * crop geometry: one derivation, in a place every addon may depend on. A
596
+ * framework package is exactly that place — `@camstack/types/node`, off the root
597
+ * entry because `node:crypto` must never be traversed by a browser bundler.
598
+ *
599
+ * What this module deliberately does NOT decide: the TTL, the base URL, the
600
+ * shape of `id`, and the access level of the route. Those are per-plane policy
601
+ * and each caller states them where a reader can see them.
602
+ */
603
+ /**
604
+ * The signature over `(id, expMs)`.
605
+ *
606
+ * `id` is whatever the plane uses to name the thing being served — an artifact
607
+ * id, a track id, a `<deviceId>:<width>` pair. It is joined with `:` so a caller
608
+ * must not put a `:` inside a field whose boundary matters; where a plane has
609
+ * more than one field, it composes them itself and owns that ambiguity.
610
+ */
611
+ function signExpiringUrl(secret, id, expMs) {
612
+ return createHmac("sha256", secret).update(`${id}:${String(expMs)}`).digest("hex");
613
+ }
614
+ /**
615
+ * Verify a request's `(id, exp, sig)`.
616
+ *
617
+ * **Expiry is checked BEFORE the compare**, so an expired link is refused
618
+ * whether or not its signature is valid — a leaked URL stops working on its own
619
+ * and cannot be kept alive by holding a correct signature. The compare itself is
620
+ * constant-time so a public route cannot be probed for the signature byte by
621
+ * byte.
622
+ */
623
+ function verifyExpiringUrl(input) {
624
+ const { secret, id, exp, sig, nowMs } = input;
625
+ if (exp === void 0 || sig === void 0) return false;
626
+ const expMs = typeof exp === "number" ? exp : Number(exp);
627
+ if (!Number.isFinite(expMs)) return false;
628
+ if (expMs <= nowMs) return false;
629
+ const expected = signExpiringUrl(secret, id, expMs);
630
+ const a = Buffer.from(expected, "utf8");
631
+ const b = Buffer.from(sig, "utf8");
632
+ if (a.length !== b.length) return false;
633
+ return timingSafeEqual(a, b);
634
+ }
635
+ //#endregion
581
636
  //#region src/utils/export-reconciler.ts
582
637
  /**
583
638
  * Compute the stable 64-char lowercase-hex fingerprint of a device's
@@ -1403,4 +1458,4 @@ var Fmp4FragmentChild = class {
1403
1458
  }
1404
1459
  };
1405
1460
  //#endregion
1406
- export { FfmpegProcess, FilesystemStorageProvider, Fmp4FragmentChild, Fmp4FragmentPlane, PYTHON_VERSION, buildBinaryPath, canonicalDeviceFingerprint, canonicalHash, diffExportTargets, downloadBinary, ensureBinary, ensureFfmpeg, ensurePython, findInPath, getFfmpegDownloadUrl, getPlatformInfo, getPythonDownloadUrl, installPythonPackages, installPythonRequirements, resolveExportFingerprint };
1461
+ export { FfmpegProcess, FilesystemStorageProvider, Fmp4FragmentChild, Fmp4FragmentPlane, PYTHON_VERSION, buildBinaryPath, canonicalDeviceFingerprint, canonicalHash, diffExportTargets, downloadBinary, ensureBinary, ensureFfmpeg, ensurePython, findInPath, getFfmpegDownloadUrl, getPlatformInfo, getPythonDownloadUrl, installPythonPackages, installPythonRequirements, resolveExportFingerprint, signExpiringUrl, verifyExpiringUrl };
@@ -3279,6 +3279,7 @@ function createDeviceProxy(api, binding, opts) {
3279
3279
  getSnapshot: (input) => dispatch("snapshot", "snapshot", "getSnapshot", "query", input),
3280
3280
  invalidateCache: (input) => dispatch("snapshot", "snapshot", "invalidateCache", "mutation", input),
3281
3281
  getSnapshotOverview: (input) => dispatch("snapshot", "snapshot", "getSnapshotOverview", "query", input),
3282
+ getSnapshotLinks: (input) => dispatch("snapshot", "snapshot", "getSnapshotLinks", "query", input),
3282
3283
  getStatus: (input) => dispatch("snapshot", "snapshot", "getStatus", "query", input),
3283
3284
  getDeviceSettingsContribution: (input) => dispatch("snapshot", "snapshot", "getDeviceSettingsContribution", "query", input),
3284
3285
  getDeviceLiveContribution: (input) => dispatch("snapshot", "snapshot", "getDeviceLiveContribution", "query", input),
@@ -3279,6 +3279,7 @@ function createDeviceProxy(api, binding, opts) {
3279
3279
  getSnapshot: (input) => dispatch("snapshot", "snapshot", "getSnapshot", "query", input),
3280
3280
  invalidateCache: (input) => dispatch("snapshot", "snapshot", "invalidateCache", "mutation", input),
3281
3281
  getSnapshotOverview: (input) => dispatch("snapshot", "snapshot", "getSnapshotOverview", "query", input),
3282
+ getSnapshotLinks: (input) => dispatch("snapshot", "snapshot", "getSnapshotLinks", "query", input),
3282
3283
  getStatus: (input) => dispatch("snapshot", "snapshot", "getStatus", "query", input),
3283
3284
  getDeviceSettingsContribution: (input) => dispatch("snapshot", "snapshot", "getDeviceSettingsContribution", "query", input),
3284
3285
  getDeviceLiveContribution: (input) => dispatch("snapshot", "snapshot", "getDeviceLiveContribution", "query", input),
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The signature over `(id, expMs)`.
3
+ *
4
+ * `id` is whatever the plane uses to name the thing being served — an artifact
5
+ * id, a track id, a `<deviceId>:<width>` pair. It is joined with `:` so a caller
6
+ * must not put a `:` inside a field whose boundary matters; where a plane has
7
+ * more than one field, it composes them itself and owns that ambiguity.
8
+ */
9
+ export declare function signExpiringUrl(secret: string, id: string, expMs: number): string;
10
+ export interface VerifyExpiringUrlInput {
11
+ readonly secret: string;
12
+ readonly id: string;
13
+ /** Raw query value — a string off the wire, or already a number. */
14
+ readonly exp: string | number | undefined;
15
+ readonly sig: string | undefined;
16
+ readonly nowMs: number;
17
+ }
18
+ /**
19
+ * Verify a request's `(id, exp, sig)`.
20
+ *
21
+ * **Expiry is checked BEFORE the compare**, so an expired link is refused
22
+ * whether or not its signature is valid — a leaked URL stops working on its own
23
+ * and cannot be kept alive by holding a correct signature. The compare itself is
24
+ * constant-time so a public route cannot be probed for the signature byte by
25
+ * byte.
26
+ */
27
+ export declare function verifyExpiringUrl(input: VerifyExpiringUrlInput): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/types",
3
- "version": "1.2.50",
3
+ "version": "1.2.52",
4
4
  "description": "Shared types, interfaces, and model catalogs for the CamStack detection ecosystem",
5
5
  "keywords": [
6
6
  "camstack",