@camstack/addon-notifiers 1.2.29 → 1.2.31

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.
Files changed (3) hide show
  1. package/dist/addon.js +258 -17
  2. package/dist/addon.mjs +258 -17
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -29,7 +29,7 @@ let node_fs = require("node:fs");
29
29
  let node_path = require("node:path");
30
30
  node_path = __toESM(node_path);
31
31
  let node_url = require("node:url");
32
- //#region ../types/dist/event-category-XfKNtfCc.mjs
32
+ //#region ../types/dist/event-category-CIa_iT6b.mjs
33
33
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
34
34
  EventCategory["SystemBoot"] = "system.boot";
35
35
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -45,6 +45,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
45
45
  */
46
46
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
47
47
  /**
48
+ * The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
49
+ * Emitted only when the material on disk actually changed, so an
50
+ * operator who trusted the old certificate by hand is told rather than
51
+ * discovering it as a browser error. Payload `TlsCertChangedPayload`.
52
+ *
53
+ * Rule: docs/decisions/adr-0227-*.md
54
+ */
55
+ EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
56
+ /**
48
57
  * A newer addon or server-root package version was found by the
49
58
  * authoritative registry check. Emitted once when any observed
50
59
  * `latestVersion` changes (or a package/node first appears behind);
@@ -8144,6 +8153,15 @@ var LabelDefinitionSchema = object({
8144
8153
  description: string().optional(),
8145
8154
  icon: string().optional()
8146
8155
  });
8156
+ var ClassMapDefinitionSchema = object({
8157
+ mapping: record(string(), _enum([
8158
+ "person",
8159
+ "vehicle",
8160
+ "animal",
8161
+ "package"
8162
+ ])),
8163
+ preserveOriginal: boolean()
8164
+ });
8147
8165
  var MODEL_FORMATS = [
8148
8166
  "onnx",
8149
8167
  "coreml",
@@ -8322,7 +8340,13 @@ var ModelCatalogEntrySchema = object({
8322
8340
  * `id` stays the source of truth for resolution/download/persistence; grouping
8323
8341
  * is a presentation overlay resolved back to an `id`.
8324
8342
  */
8325
- group: ModelVariantGroupSchema.optional()
8343
+ group: ModelVariantGroupSchema.optional(),
8344
+ /**
8345
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8346
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8347
+ * labels already ARE the CamStack macros (Scrypted identity map).
8348
+ */
8349
+ classMap: ClassMapDefinitionSchema.optional()
8326
8350
  });
8327
8351
  var ConvertTargetSchema = discriminatedUnion("format", [object({
8328
8352
  format: literal("openvino"),
@@ -8351,7 +8375,8 @@ var ModelConvertMetadataSchema = object({
8351
8375
  "ocr",
8352
8376
  "segmentation"
8353
8377
  ]),
8354
- faceAlignment: boolean().optional()
8378
+ faceAlignment: boolean().optional(),
8379
+ classMap: ClassMapDefinitionSchema.optional()
8355
8380
  });
8356
8381
  var ConvertResultSchema = object({
8357
8382
  entry: ModelCatalogEntrySchema,
@@ -17397,6 +17422,33 @@ var NativeCropRefSchema = object({
17397
17422
  h: number()
17398
17423
  })
17399
17424
  });
17425
+ object({
17426
+ crop: object({
17427
+ left: number(),
17428
+ top: number(),
17429
+ width: number().positive(),
17430
+ height: number().positive()
17431
+ }).optional(),
17432
+ content: object({
17433
+ width: number().int().positive(),
17434
+ height: number().int().positive()
17435
+ }),
17436
+ fit: _enum(["stretch", "contain"]),
17437
+ format: _enum([
17438
+ "rgb",
17439
+ "gray",
17440
+ "jpeg"
17441
+ ])
17442
+ });
17443
+ var FrameRefSchema = object({
17444
+ registryId: string().min(1),
17445
+ id: string().min(1),
17446
+ width: number().int().positive(),
17447
+ height: number().int().positive(),
17448
+ format: _enum(["rgb", "gray"]),
17449
+ timestamp: number(),
17450
+ capturedAt: number().optional()
17451
+ });
17400
17452
  var ModelFormatSchema$1 = _enum([
17401
17453
  "onnx",
17402
17454
  "coreml",
@@ -17641,6 +17693,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
17641
17693
  steps: array(PipelineStepInputSchema).min(1),
17642
17694
  frame: FrameInputSchema.optional(),
17643
17695
  /**
17696
+ * Process-local lazy frame. Valid only when caller and provider resolve
17697
+ * in the same execution-group process; split/cross-node callers use
17698
+ * `frame`/`image` inline compatibility instead.
17699
+ */
17700
+ frameRef: FrameRefSchema.optional(),
17701
+ /**
17644
17702
  * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
17645
17703
  * the decoded pixels live in. One more member of the one-of
17646
17704
  * frame/frameHandle/image/imageBase64/referenceImage group.
@@ -17896,7 +17954,10 @@ var NativeCropResultSchema = object({
17896
17954
  * Which source served this crop, so a quality-sensitive consumer (the native
17897
17955
  * `keyFrame`) can reject a degraded fallback:
17898
17956
  * - `native` — cut from the decode worker's retained NATIVE surface (the
17899
- * quality path).
17957
+ * quality path). A subject-tile serve is also native-resolution and stays
17958
+ * `native` here: the public enum cannot name `tile` without a breaking cap
17959
+ * change. Runner telemetry distinguishes lease vs tile via `source` on the
17960
+ * internal crop result (`nativeHits` vs `tileHits`).
17900
17961
  * - `ram-fullframe` — the native surface MISSED but the request was full-frame,
17901
17962
  * so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
17902
17963
  * the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
@@ -18387,12 +18448,41 @@ var RunnerLocalLoadSchema = object({
18387
18448
  * legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
18388
18449
  * working unchanged when they switch to reading from the runner cap.
18389
18450
  */
18451
+ var FrameLazyCountersSchema = object({
18452
+ framesDecoded: number(),
18453
+ framesAdmitted: number(),
18454
+ framesDroppedPixelFree: number(),
18455
+ viewsMaterialized: number(),
18456
+ viewsSkipped: number(),
18457
+ workerToRunnerBytes: number(),
18458
+ runnerToPoolRawBytes: number(),
18459
+ runnerToPoolJpegBytes: number(),
18460
+ onDemandFullFrameRequests: number(),
18461
+ onDemandCropRequests: number(),
18462
+ nativeHits: number(),
18463
+ nativeMisses: number(),
18464
+ tileHits: number(),
18465
+ tileMisses: number(),
18466
+ fallbackHits: number(),
18467
+ fallbackMisses: number(),
18468
+ retainedWritesAvoided: number(),
18469
+ residentRefs: number(),
18470
+ residentBytes: number(),
18471
+ releases: number(),
18472
+ evictions: number(),
18473
+ staleMisses: number()
18474
+ });
18475
+ var FrameLazyMetricsSchema = object({
18476
+ node: FrameLazyCountersSchema,
18477
+ cameras: array(FrameLazyCountersSchema.extend({ deviceId: number() }))
18478
+ });
18390
18479
  var RunnerLocalMetricsSchema = object({
18391
18480
  nodeId: string(),
18392
18481
  activeCameras: number(),
18393
18482
  throttledCameras: number(),
18394
18483
  avgInferenceTimeMs: number(),
18395
- queueDepth: number()
18484
+ queueDepth: number(),
18485
+ frameLazy: FrameLazyMetricsSchema.optional()
18396
18486
  });
18397
18487
  method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mutation" }), method(object({ deviceId: number() }), object({ success: literal(true) }), { kind: "mutation" }), method(ReportMotionInputSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), RunnerLocalLoadSchema), method(_void(), RunnerLocalMetricsSchema), method(object({ deviceId: number() }), CameraMetricsSchema.nullable()), method(_void(), array(CameraMetricsWithDeviceIdSchema).readonly()), method(_void(), array(number()).readonly()), method(object({
18398
18488
  handle: FrameHandleSchema,
@@ -19692,6 +19782,9 @@ method(_void(), ProviderInfoSchema), method(object({ config: record(string(), un
19692
19782
  location: StorageLocationSchema,
19693
19783
  relativePath: string()
19694
19784
  }), _void(), { kind: "mutation" }), method(object({ location: StorageLocationSchema }), number().nullable()), method(BeginUploadInputSchema, BeginUploadResultSchema, { kind: "mutation" }), method(WriteChunkInputSchema, _void(), { kind: "mutation" }), method(FinalizeUploadInputSchema, _void(), { kind: "mutation" }), method(AbortUploadInputSchema, _void(), { kind: "mutation" }), method(BeginDownloadInputSchema, BeginDownloadResultSchema, { kind: "mutation" }), method(ReadChunkInputSchema, _instanceof(Uint8Array)), method(EndDownloadInputSchema, _void(), { kind: "mutation" });
19785
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
19786
+ var ProfileSettingsSchemaBridge = unknown().nullable();
19787
+ var ProfileSettingsBagSchema = record(string(), unknown());
19695
19788
  /**
19696
19789
  * A live terminal session hosted by the provider addon. Output and input do
19697
19790
  * NOT flow through the capability — they use the addon data plane
@@ -19721,7 +19814,14 @@ var TerminalSessionInfoSchema = object({
19721
19814
  var TerminalProfileInfoSchema = object({
19722
19815
  profileId: string(),
19723
19816
  label: string(),
19724
- description: string().optional()
19817
+ description: string().optional(),
19818
+ /** Spawn defaults the instance form copies on create. */
19819
+ executable: string().optional(),
19820
+ args: array(string()).readonly().optional(),
19821
+ cwd: string().optional(),
19822
+ environment: array(string()).readonly().optional(),
19823
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
19824
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
19725
19825
  });
19726
19826
  /**
19727
19827
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -19734,7 +19834,12 @@ var TerminalInstanceInfoSchema = object({
19734
19834
  profileId: string(),
19735
19835
  profileLabel: string(),
19736
19836
  name: string(),
19737
- enabled: boolean()
19837
+ enabled: boolean(),
19838
+ executable: string(),
19839
+ args: array(string()).readonly(),
19840
+ cwd: string(),
19841
+ environment: array(string()).readonly(),
19842
+ profileSettings: ProfileSettingsBagSchema
19738
19843
  });
19739
19844
  var TerminalLegacyCameraSchema = object({
19740
19845
  stableId: string(),
@@ -19764,7 +19869,23 @@ var TerminalOutputBatchSchema = object({
19764
19869
  method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
19765
19870
  targetNodeId: string().min(1),
19766
19871
  profileId: string().min(1),
19767
- name: string().trim().min(1).max(160).optional()
19872
+ name: string().trim().min(1).max(160).optional(),
19873
+ executable: string().max(1024).optional(),
19874
+ args: array(string().max(2048)).max(64).optional(),
19875
+ cwd: string().max(1024).optional(),
19876
+ environment: array(string().max(4096)).max(64).optional(),
19877
+ profileSettings: ProfileSettingsBagSchema.optional()
19878
+ }), TerminalInstanceInfoSchema, {
19879
+ kind: "mutation",
19880
+ auth: "admin"
19881
+ }), method(object({
19882
+ instanceId: string().min(1),
19883
+ name: string().trim().min(1).max(160).optional(),
19884
+ executable: string().max(1024).optional(),
19885
+ args: array(string().max(2048)).max(64).optional(),
19886
+ cwd: string().max(1024).optional(),
19887
+ environment: array(string().max(4096)).max(64).optional(),
19888
+ profileSettings: ProfileSettingsBagSchema.optional()
19768
19889
  }), TerminalInstanceInfoSchema, {
19769
19890
  kind: "mutation",
19770
19891
  auth: "admin"
@@ -19786,7 +19907,11 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
19786
19907
  }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
19787
19908
  profileId: string(),
19788
19909
  cols: number().int().positive(),
19789
- rows: number().int().positive()
19910
+ rows: number().int().positive(),
19911
+ executable: string().max(1024).optional(),
19912
+ args: array(string().max(2048)).max(64).optional(),
19913
+ cwd: string().max(1024).optional(),
19914
+ environment: array(string().max(4096)).max(64).optional()
19790
19915
  }), TerminalSessionInfoSchema, {
19791
19916
  kind: "mutation",
19792
19917
  auth: "admin"
@@ -22568,10 +22693,10 @@ DeviceType.LawnMower, method(object({ deviceId: number().int().nonnegative() }),
22568
22693
  *
22569
22694
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
22570
22695
  * to receive an ordered list of candidate base URLs it should race
22571
- * on connect — LAN IPv4 first (lowest latency when on same network),
22572
- * then public hostname (if a tunnel is up), then IPv6. The SDK can
22573
- * race them with short timeouts and stick with the winner for the
22574
- * session.
22696
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
22697
+ * when on the same network), then public hostname (if a tunnel is
22698
+ * up). The SDK can race them with short timeouts and stick with the
22699
+ * winner for the session.
22575
22700
  *
22576
22701
  * Why hub-only: agents are not directly addressable by the operator's
22577
22702
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -22726,6 +22851,17 @@ var NotificationEndpointSchema = object({
22726
22851
  /** What the ranking currently resolves to (null when nothing is reachable). */
22727
22852
  resolved: string().nullable()
22728
22853
  });
22854
+ /**
22855
+ * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
22856
+ * AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
22857
+ * currently expands to, so the UI can show the effective set either way.
22858
+ */
22859
+ var ViewerEndpointsSchema = object({
22860
+ /** The operator's explicit race set, or empty for AUTO. */
22861
+ baseUrls: array(string()).readonly(),
22862
+ /** What the ranking currently resolves to (may be empty if nothing is up). */
22863
+ resolved: array(string()).readonly()
22864
+ });
22729
22865
  var AllowedAddressesSchema = object({
22730
22866
  /**
22731
22867
  * Allowlist of interface addresses operators have explicitly opted
@@ -22734,6 +22870,20 @@ var AllowedAddressesSchema = object({
22734
22870
  * Network Addresses admin page and persisted by the addon.
22735
22871
  */
22736
22872
  addresses: array(string()).readonly() });
22873
+ var TlsStatusSchema = object({
22874
+ mode: _enum([
22875
+ "generated",
22876
+ "uploaded",
22877
+ "disabled"
22878
+ ]),
22879
+ leafFingerprintSha256: string().nullable(),
22880
+ caFingerprintSha256: string().nullable(),
22881
+ validTo: string().nullable(),
22882
+ sans: array(string()),
22883
+ caCertPem: string().nullable(),
22884
+ reissueError: string().nullable(),
22885
+ restartRequired: boolean()
22886
+ });
22737
22887
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
22738
22888
  /**
22739
22889
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -22743,17 +22893,31 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
22743
22893
  */
22744
22894
  port: number().int().min(1).max(65535).optional(),
22745
22895
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
22746
- * candidate. Default `true`. */
22896
+ * candidate. Default `false` — loopback is not a client route. */
22747
22897
  includeLoopback: boolean().optional(),
22748
- /** Skip IPv6 entries. Some legacy clients can't parse them.
22749
- * Default `false`. */
22898
+ /** Skip IPv6 entries. Default `false` the palette includes stable
22899
+ * LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
22900
+ * hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
22750
22901
  ipv4Only: boolean().optional(),
22751
22902
  /** Scheme to emit for LAN/loopback URLs. Default `'http'`.
22752
22903
  * Pass `'https'` when the caller is itself loaded over HTTPS
22753
22904
  * to avoid mixed-content blocks in the browser. The public
22754
22905
  * tunnel always emits `https://` regardless. */
22755
22906
  scheme: _enum(["http", "https"]).optional()
22756
- }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
22907
+ }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), ViewerEndpointsSchema), method(object({ baseUrls: array(string()).readonly() }), ViewerEndpointsSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" }), method(_void(), TlsStatusSchema), method(object({ reason: string().optional() }), TlsStatusSchema, {
22908
+ kind: "mutation",
22909
+ auth: "admin"
22910
+ }), method(object({
22911
+ certPem: string().min(1),
22912
+ keyPem: string().min(1),
22913
+ caPem: string().optional()
22914
+ }), TlsStatusSchema, {
22915
+ kind: "mutation",
22916
+ auth: "admin"
22917
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
22918
+ kind: "mutation",
22919
+ auth: "admin"
22920
+ });
22757
22921
  object({
22758
22922
  /** Lifecycle state of the lock. `jammed` means the motor reported
22759
22923
  * failure to reach the target — operator intervention required. */
@@ -28516,6 +28680,12 @@ Object.freeze({
28516
28680
  addonId: null,
28517
28681
  access: "create"
28518
28682
  },
28683
+ "localNetwork.downloadCa": {
28684
+ capName: "local-network",
28685
+ capScope: "system",
28686
+ addonId: null,
28687
+ access: "view"
28688
+ },
28519
28689
  "localNetwork.getAllowedAddresses": {
28520
28690
  capName: "local-network",
28521
28691
  capScope: "system",
@@ -28540,18 +28710,42 @@ Object.freeze({
28540
28710
  addonId: null,
28541
28711
  access: "view"
28542
28712
  },
28713
+ "localNetwork.getTlsStatus": {
28714
+ capName: "local-network",
28715
+ capScope: "system",
28716
+ addonId: null,
28717
+ access: "view"
28718
+ },
28719
+ "localNetwork.getViewerEndpoints": {
28720
+ capName: "local-network",
28721
+ capScope: "system",
28722
+ addonId: null,
28723
+ access: "view"
28724
+ },
28543
28725
  "localNetwork.list": {
28544
28726
  capName: "local-network",
28545
28727
  capScope: "system",
28546
28728
  addonId: null,
28547
28729
  access: "view"
28548
28730
  },
28731
+ "localNetwork.regenerateCertificate": {
28732
+ capName: "local-network",
28733
+ capScope: "system",
28734
+ addonId: null,
28735
+ access: "create"
28736
+ },
28549
28737
  "localNetwork.resetAllowlistToBestMatch": {
28550
28738
  capName: "local-network",
28551
28739
  capScope: "system",
28552
28740
  addonId: null,
28553
28741
  access: "delete"
28554
28742
  },
28743
+ "localNetwork.revertToGeneratedCertificate": {
28744
+ capName: "local-network",
28745
+ capScope: "system",
28746
+ addonId: null,
28747
+ access: "create"
28748
+ },
28555
28749
  "localNetwork.setAllowedAddresses": {
28556
28750
  capName: "local-network",
28557
28751
  capScope: "system",
@@ -28564,6 +28758,18 @@ Object.freeze({
28564
28758
  addonId: null,
28565
28759
  access: "create"
28566
28760
  },
28761
+ "localNetwork.setViewerEndpoints": {
28762
+ capName: "local-network",
28763
+ capScope: "system",
28764
+ addonId: null,
28765
+ access: "create"
28766
+ },
28767
+ "localNetwork.uploadCertificate": {
28768
+ capName: "local-network",
28769
+ capScope: "system",
28770
+ addonId: null,
28771
+ access: "create"
28772
+ },
28567
28773
  "lockControl.lock": {
28568
28774
  capName: "lock-control",
28569
28775
  capScope: "device",
@@ -31444,6 +31650,12 @@ Object.freeze({
31444
31650
  addonId: null,
31445
31651
  access: "create"
31446
31652
  },
31653
+ "terminalSession.updateInstance": {
31654
+ capName: "terminal-session",
31655
+ capScope: "system",
31656
+ addonId: null,
31657
+ access: "create"
31658
+ },
31447
31659
  "terminalSession.writeInput": {
31448
31660
  capName: "terminal-session",
31449
31661
  capScope: "system",
@@ -34138,6 +34350,35 @@ Object.freeze(Object.fromEntries([{
34138
34350
  }]
34139
34351
  }].map((s) => [s.stepId, s.defaultModelId])));
34140
34352
  string().min(1);
34353
+ var CLUSTER_STEP_SETTING_FIELDS = [{
34354
+ stepId: "face-embedding",
34355
+ key: "minLandmarkFaceSize",
34356
+ label: "Min face size for recognition (detection px)",
34357
+ description: "Refuse to embed a face smaller than this IN THE DETECTION FRAME. Applies to every node — the enrolled gallery is one index, and two admission floors would fill it from two policies. 0 disables the gate.",
34358
+ type: "slider",
34359
+ min: 0,
34360
+ max: 64,
34361
+ step: 2,
34362
+ default: 24
34363
+ }];
34364
+ function clusterStepSettingKey(stepId, fieldKey) {
34365
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
34366
+ }
34367
+ var ClusterSettingNumberSchema = number().finite();
34368
+ function readClusterStepSettings(config) {
34369
+ const out = {};
34370
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
34371
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
34372
+ const value = parsed.success ? parsed.data : field.default;
34373
+ const existing = out[field.stepId] ?? {};
34374
+ out[field.stepId] = {
34375
+ ...existing,
34376
+ [field.key]: value
34377
+ };
34378
+ }
34379
+ return out;
34380
+ }
34381
+ readClusterStepSettings({});
34141
34382
  object({
34142
34383
  /**
34143
34384
  * Fraction of the box's own size added on EACH side before cutting.
package/dist/addon.mjs CHANGED
@@ -2,7 +2,7 @@ import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
2
2
  import { readFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- //#region ../types/dist/event-category-XfKNtfCc.mjs
5
+ //#region ../types/dist/event-category-CIa_iT6b.mjs
6
6
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
7
7
  EventCategory["SystemBoot"] = "system.boot";
8
8
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -18,6 +18,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
18
18
  */
19
19
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
20
20
  /**
21
+ * The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
22
+ * Emitted only when the material on disk actually changed, so an
23
+ * operator who trusted the old certificate by hand is told rather than
24
+ * discovering it as a browser error. Payload `TlsCertChangedPayload`.
25
+ *
26
+ * Rule: docs/decisions/adr-0227-*.md
27
+ */
28
+ EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
29
+ /**
21
30
  * A newer addon or server-root package version was found by the
22
31
  * authoritative registry check. Emitted once when any observed
23
32
  * `latestVersion` changes (or a package/node first appears behind);
@@ -8117,6 +8126,15 @@ var LabelDefinitionSchema = object({
8117
8126
  description: string().optional(),
8118
8127
  icon: string().optional()
8119
8128
  });
8129
+ var ClassMapDefinitionSchema = object({
8130
+ mapping: record(string(), _enum([
8131
+ "person",
8132
+ "vehicle",
8133
+ "animal",
8134
+ "package"
8135
+ ])),
8136
+ preserveOriginal: boolean()
8137
+ });
8120
8138
  var MODEL_FORMATS = [
8121
8139
  "onnx",
8122
8140
  "coreml",
@@ -8295,7 +8313,13 @@ var ModelCatalogEntrySchema = object({
8295
8313
  * `id` stays the source of truth for resolution/download/persistence; grouping
8296
8314
  * is a presentation overlay resolved back to an `id`.
8297
8315
  */
8298
- group: ModelVariantGroupSchema.optional()
8316
+ group: ModelVariantGroupSchema.optional(),
8317
+ /**
8318
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8319
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8320
+ * labels already ARE the CamStack macros (Scrypted identity map).
8321
+ */
8322
+ classMap: ClassMapDefinitionSchema.optional()
8299
8323
  });
8300
8324
  var ConvertTargetSchema = discriminatedUnion("format", [object({
8301
8325
  format: literal("openvino"),
@@ -8324,7 +8348,8 @@ var ModelConvertMetadataSchema = object({
8324
8348
  "ocr",
8325
8349
  "segmentation"
8326
8350
  ]),
8327
- faceAlignment: boolean().optional()
8351
+ faceAlignment: boolean().optional(),
8352
+ classMap: ClassMapDefinitionSchema.optional()
8328
8353
  });
8329
8354
  var ConvertResultSchema = object({
8330
8355
  entry: ModelCatalogEntrySchema,
@@ -17370,6 +17395,33 @@ var NativeCropRefSchema = object({
17370
17395
  h: number()
17371
17396
  })
17372
17397
  });
17398
+ object({
17399
+ crop: object({
17400
+ left: number(),
17401
+ top: number(),
17402
+ width: number().positive(),
17403
+ height: number().positive()
17404
+ }).optional(),
17405
+ content: object({
17406
+ width: number().int().positive(),
17407
+ height: number().int().positive()
17408
+ }),
17409
+ fit: _enum(["stretch", "contain"]),
17410
+ format: _enum([
17411
+ "rgb",
17412
+ "gray",
17413
+ "jpeg"
17414
+ ])
17415
+ });
17416
+ var FrameRefSchema = object({
17417
+ registryId: string().min(1),
17418
+ id: string().min(1),
17419
+ width: number().int().positive(),
17420
+ height: number().int().positive(),
17421
+ format: _enum(["rgb", "gray"]),
17422
+ timestamp: number(),
17423
+ capturedAt: number().optional()
17424
+ });
17373
17425
  var ModelFormatSchema$1 = _enum([
17374
17426
  "onnx",
17375
17427
  "coreml",
@@ -17614,6 +17666,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
17614
17666
  steps: array(PipelineStepInputSchema).min(1),
17615
17667
  frame: FrameInputSchema.optional(),
17616
17668
  /**
17669
+ * Process-local lazy frame. Valid only when caller and provider resolve
17670
+ * in the same execution-group process; split/cross-node callers use
17671
+ * `frame`/`image` inline compatibility instead.
17672
+ */
17673
+ frameRef: FrameRefSchema.optional(),
17674
+ /**
17617
17675
  * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
17618
17676
  * the decoded pixels live in. One more member of the one-of
17619
17677
  * frame/frameHandle/image/imageBase64/referenceImage group.
@@ -17869,7 +17927,10 @@ var NativeCropResultSchema = object({
17869
17927
  * Which source served this crop, so a quality-sensitive consumer (the native
17870
17928
  * `keyFrame`) can reject a degraded fallback:
17871
17929
  * - `native` — cut from the decode worker's retained NATIVE surface (the
17872
- * quality path).
17930
+ * quality path). A subject-tile serve is also native-resolution and stays
17931
+ * `native` here: the public enum cannot name `tile` without a breaking cap
17932
+ * change. Runner telemetry distinguishes lease vs tile via `source` on the
17933
+ * internal crop result (`nativeHits` vs `tileHits`).
17873
17934
  * - `ram-fullframe` — the native surface MISSED but the request was full-frame,
17874
17935
  * so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
17875
17936
  * the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
@@ -18360,12 +18421,41 @@ var RunnerLocalLoadSchema = object({
18360
18421
  * legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
18361
18422
  * working unchanged when they switch to reading from the runner cap.
18362
18423
  */
18424
+ var FrameLazyCountersSchema = object({
18425
+ framesDecoded: number(),
18426
+ framesAdmitted: number(),
18427
+ framesDroppedPixelFree: number(),
18428
+ viewsMaterialized: number(),
18429
+ viewsSkipped: number(),
18430
+ workerToRunnerBytes: number(),
18431
+ runnerToPoolRawBytes: number(),
18432
+ runnerToPoolJpegBytes: number(),
18433
+ onDemandFullFrameRequests: number(),
18434
+ onDemandCropRequests: number(),
18435
+ nativeHits: number(),
18436
+ nativeMisses: number(),
18437
+ tileHits: number(),
18438
+ tileMisses: number(),
18439
+ fallbackHits: number(),
18440
+ fallbackMisses: number(),
18441
+ retainedWritesAvoided: number(),
18442
+ residentRefs: number(),
18443
+ residentBytes: number(),
18444
+ releases: number(),
18445
+ evictions: number(),
18446
+ staleMisses: number()
18447
+ });
18448
+ var FrameLazyMetricsSchema = object({
18449
+ node: FrameLazyCountersSchema,
18450
+ cameras: array(FrameLazyCountersSchema.extend({ deviceId: number() }))
18451
+ });
18363
18452
  var RunnerLocalMetricsSchema = object({
18364
18453
  nodeId: string(),
18365
18454
  activeCameras: number(),
18366
18455
  throttledCameras: number(),
18367
18456
  avgInferenceTimeMs: number(),
18368
- queueDepth: number()
18457
+ queueDepth: number(),
18458
+ frameLazy: FrameLazyMetricsSchema.optional()
18369
18459
  });
18370
18460
  method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mutation" }), method(object({ deviceId: number() }), object({ success: literal(true) }), { kind: "mutation" }), method(ReportMotionInputSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), RunnerLocalLoadSchema), method(_void(), RunnerLocalMetricsSchema), method(object({ deviceId: number() }), CameraMetricsSchema.nullable()), method(_void(), array(CameraMetricsWithDeviceIdSchema).readonly()), method(_void(), array(number()).readonly()), method(object({
18371
18461
  handle: FrameHandleSchema,
@@ -19665,6 +19755,9 @@ method(_void(), ProviderInfoSchema), method(object({ config: record(string(), un
19665
19755
  location: StorageLocationSchema,
19666
19756
  relativePath: string()
19667
19757
  }), _void(), { kind: "mutation" }), method(object({ location: StorageLocationSchema }), number().nullable()), method(BeginUploadInputSchema, BeginUploadResultSchema, { kind: "mutation" }), method(WriteChunkInputSchema, _void(), { kind: "mutation" }), method(FinalizeUploadInputSchema, _void(), { kind: "mutation" }), method(AbortUploadInputSchema, _void(), { kind: "mutation" }), method(BeginDownloadInputSchema, BeginDownloadResultSchema, { kind: "mutation" }), method(ReadChunkInputSchema, _instanceof(Uint8Array)), method(EndDownloadInputSchema, _void(), { kind: "mutation" });
19758
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
19759
+ var ProfileSettingsSchemaBridge = unknown().nullable();
19760
+ var ProfileSettingsBagSchema = record(string(), unknown());
19668
19761
  /**
19669
19762
  * A live terminal session hosted by the provider addon. Output and input do
19670
19763
  * NOT flow through the capability — they use the addon data plane
@@ -19694,7 +19787,14 @@ var TerminalSessionInfoSchema = object({
19694
19787
  var TerminalProfileInfoSchema = object({
19695
19788
  profileId: string(),
19696
19789
  label: string(),
19697
- description: string().optional()
19790
+ description: string().optional(),
19791
+ /** Spawn defaults the instance form copies on create. */
19792
+ executable: string().optional(),
19793
+ args: array(string()).readonly().optional(),
19794
+ cwd: string().optional(),
19795
+ environment: array(string()).readonly().optional(),
19796
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
19797
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
19698
19798
  });
19699
19799
  /**
19700
19800
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -19707,7 +19807,12 @@ var TerminalInstanceInfoSchema = object({
19707
19807
  profileId: string(),
19708
19808
  profileLabel: string(),
19709
19809
  name: string(),
19710
- enabled: boolean()
19810
+ enabled: boolean(),
19811
+ executable: string(),
19812
+ args: array(string()).readonly(),
19813
+ cwd: string(),
19814
+ environment: array(string()).readonly(),
19815
+ profileSettings: ProfileSettingsBagSchema
19711
19816
  });
19712
19817
  var TerminalLegacyCameraSchema = object({
19713
19818
  stableId: string(),
@@ -19737,7 +19842,23 @@ var TerminalOutputBatchSchema = object({
19737
19842
  method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
19738
19843
  targetNodeId: string().min(1),
19739
19844
  profileId: string().min(1),
19740
- name: string().trim().min(1).max(160).optional()
19845
+ name: string().trim().min(1).max(160).optional(),
19846
+ executable: string().max(1024).optional(),
19847
+ args: array(string().max(2048)).max(64).optional(),
19848
+ cwd: string().max(1024).optional(),
19849
+ environment: array(string().max(4096)).max(64).optional(),
19850
+ profileSettings: ProfileSettingsBagSchema.optional()
19851
+ }), TerminalInstanceInfoSchema, {
19852
+ kind: "mutation",
19853
+ auth: "admin"
19854
+ }), method(object({
19855
+ instanceId: string().min(1),
19856
+ name: string().trim().min(1).max(160).optional(),
19857
+ executable: string().max(1024).optional(),
19858
+ args: array(string().max(2048)).max(64).optional(),
19859
+ cwd: string().max(1024).optional(),
19860
+ environment: array(string().max(4096)).max(64).optional(),
19861
+ profileSettings: ProfileSettingsBagSchema.optional()
19741
19862
  }), TerminalInstanceInfoSchema, {
19742
19863
  kind: "mutation",
19743
19864
  auth: "admin"
@@ -19759,7 +19880,11 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
19759
19880
  }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
19760
19881
  profileId: string(),
19761
19882
  cols: number().int().positive(),
19762
- rows: number().int().positive()
19883
+ rows: number().int().positive(),
19884
+ executable: string().max(1024).optional(),
19885
+ args: array(string().max(2048)).max(64).optional(),
19886
+ cwd: string().max(1024).optional(),
19887
+ environment: array(string().max(4096)).max(64).optional()
19763
19888
  }), TerminalSessionInfoSchema, {
19764
19889
  kind: "mutation",
19765
19890
  auth: "admin"
@@ -22541,10 +22666,10 @@ DeviceType.LawnMower, method(object({ deviceId: number().int().nonnegative() }),
22541
22666
  *
22542
22667
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
22543
22668
  * to receive an ordered list of candidate base URLs it should race
22544
- * on connect — LAN IPv4 first (lowest latency when on same network),
22545
- * then public hostname (if a tunnel is up), then IPv6. The SDK can
22546
- * race them with short timeouts and stick with the winner for the
22547
- * session.
22669
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
22670
+ * when on the same network), then public hostname (if a tunnel is
22671
+ * up). The SDK can race them with short timeouts and stick with the
22672
+ * winner for the session.
22548
22673
  *
22549
22674
  * Why hub-only: agents are not directly addressable by the operator's
22550
22675
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -22699,6 +22824,17 @@ var NotificationEndpointSchema = object({
22699
22824
  /** What the ranking currently resolves to (null when nothing is reachable). */
22700
22825
  resolved: string().nullable()
22701
22826
  });
22827
+ /**
22828
+ * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
22829
+ * AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
22830
+ * currently expands to, so the UI can show the effective set either way.
22831
+ */
22832
+ var ViewerEndpointsSchema = object({
22833
+ /** The operator's explicit race set, or empty for AUTO. */
22834
+ baseUrls: array(string()).readonly(),
22835
+ /** What the ranking currently resolves to (may be empty if nothing is up). */
22836
+ resolved: array(string()).readonly()
22837
+ });
22702
22838
  var AllowedAddressesSchema = object({
22703
22839
  /**
22704
22840
  * Allowlist of interface addresses operators have explicitly opted
@@ -22707,6 +22843,20 @@ var AllowedAddressesSchema = object({
22707
22843
  * Network Addresses admin page and persisted by the addon.
22708
22844
  */
22709
22845
  addresses: array(string()).readonly() });
22846
+ var TlsStatusSchema = object({
22847
+ mode: _enum([
22848
+ "generated",
22849
+ "uploaded",
22850
+ "disabled"
22851
+ ]),
22852
+ leafFingerprintSha256: string().nullable(),
22853
+ caFingerprintSha256: string().nullable(),
22854
+ validTo: string().nullable(),
22855
+ sans: array(string()),
22856
+ caCertPem: string().nullable(),
22857
+ reissueError: string().nullable(),
22858
+ restartRequired: boolean()
22859
+ });
22710
22860
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
22711
22861
  /**
22712
22862
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -22716,17 +22866,31 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
22716
22866
  */
22717
22867
  port: number().int().min(1).max(65535).optional(),
22718
22868
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
22719
- * candidate. Default `true`. */
22869
+ * candidate. Default `false` — loopback is not a client route. */
22720
22870
  includeLoopback: boolean().optional(),
22721
- /** Skip IPv6 entries. Some legacy clients can't parse them.
22722
- * Default `false`. */
22871
+ /** Skip IPv6 entries. Default `false` the palette includes stable
22872
+ * LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
22873
+ * hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
22723
22874
  ipv4Only: boolean().optional(),
22724
22875
  /** Scheme to emit for LAN/loopback URLs. Default `'http'`.
22725
22876
  * Pass `'https'` when the caller is itself loaded over HTTPS
22726
22877
  * to avoid mixed-content blocks in the browser. The public
22727
22878
  * tunnel always emits `https://` regardless. */
22728
22879
  scheme: _enum(["http", "https"]).optional()
22729
- }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
22880
+ }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), ViewerEndpointsSchema), method(object({ baseUrls: array(string()).readonly() }), ViewerEndpointsSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" }), method(_void(), TlsStatusSchema), method(object({ reason: string().optional() }), TlsStatusSchema, {
22881
+ kind: "mutation",
22882
+ auth: "admin"
22883
+ }), method(object({
22884
+ certPem: string().min(1),
22885
+ keyPem: string().min(1),
22886
+ caPem: string().optional()
22887
+ }), TlsStatusSchema, {
22888
+ kind: "mutation",
22889
+ auth: "admin"
22890
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
22891
+ kind: "mutation",
22892
+ auth: "admin"
22893
+ });
22730
22894
  object({
22731
22895
  /** Lifecycle state of the lock. `jammed` means the motor reported
22732
22896
  * failure to reach the target — operator intervention required. */
@@ -28489,6 +28653,12 @@ Object.freeze({
28489
28653
  addonId: null,
28490
28654
  access: "create"
28491
28655
  },
28656
+ "localNetwork.downloadCa": {
28657
+ capName: "local-network",
28658
+ capScope: "system",
28659
+ addonId: null,
28660
+ access: "view"
28661
+ },
28492
28662
  "localNetwork.getAllowedAddresses": {
28493
28663
  capName: "local-network",
28494
28664
  capScope: "system",
@@ -28513,18 +28683,42 @@ Object.freeze({
28513
28683
  addonId: null,
28514
28684
  access: "view"
28515
28685
  },
28686
+ "localNetwork.getTlsStatus": {
28687
+ capName: "local-network",
28688
+ capScope: "system",
28689
+ addonId: null,
28690
+ access: "view"
28691
+ },
28692
+ "localNetwork.getViewerEndpoints": {
28693
+ capName: "local-network",
28694
+ capScope: "system",
28695
+ addonId: null,
28696
+ access: "view"
28697
+ },
28516
28698
  "localNetwork.list": {
28517
28699
  capName: "local-network",
28518
28700
  capScope: "system",
28519
28701
  addonId: null,
28520
28702
  access: "view"
28521
28703
  },
28704
+ "localNetwork.regenerateCertificate": {
28705
+ capName: "local-network",
28706
+ capScope: "system",
28707
+ addonId: null,
28708
+ access: "create"
28709
+ },
28522
28710
  "localNetwork.resetAllowlistToBestMatch": {
28523
28711
  capName: "local-network",
28524
28712
  capScope: "system",
28525
28713
  addonId: null,
28526
28714
  access: "delete"
28527
28715
  },
28716
+ "localNetwork.revertToGeneratedCertificate": {
28717
+ capName: "local-network",
28718
+ capScope: "system",
28719
+ addonId: null,
28720
+ access: "create"
28721
+ },
28528
28722
  "localNetwork.setAllowedAddresses": {
28529
28723
  capName: "local-network",
28530
28724
  capScope: "system",
@@ -28537,6 +28731,18 @@ Object.freeze({
28537
28731
  addonId: null,
28538
28732
  access: "create"
28539
28733
  },
28734
+ "localNetwork.setViewerEndpoints": {
28735
+ capName: "local-network",
28736
+ capScope: "system",
28737
+ addonId: null,
28738
+ access: "create"
28739
+ },
28740
+ "localNetwork.uploadCertificate": {
28741
+ capName: "local-network",
28742
+ capScope: "system",
28743
+ addonId: null,
28744
+ access: "create"
28745
+ },
28540
28746
  "lockControl.lock": {
28541
28747
  capName: "lock-control",
28542
28748
  capScope: "device",
@@ -31417,6 +31623,12 @@ Object.freeze({
31417
31623
  addonId: null,
31418
31624
  access: "create"
31419
31625
  },
31626
+ "terminalSession.updateInstance": {
31627
+ capName: "terminal-session",
31628
+ capScope: "system",
31629
+ addonId: null,
31630
+ access: "create"
31631
+ },
31420
31632
  "terminalSession.writeInput": {
31421
31633
  capName: "terminal-session",
31422
31634
  capScope: "system",
@@ -34111,6 +34323,35 @@ Object.freeze(Object.fromEntries([{
34111
34323
  }]
34112
34324
  }].map((s) => [s.stepId, s.defaultModelId])));
34113
34325
  string().min(1);
34326
+ var CLUSTER_STEP_SETTING_FIELDS = [{
34327
+ stepId: "face-embedding",
34328
+ key: "minLandmarkFaceSize",
34329
+ label: "Min face size for recognition (detection px)",
34330
+ description: "Refuse to embed a face smaller than this IN THE DETECTION FRAME. Applies to every node — the enrolled gallery is one index, and two admission floors would fill it from two policies. 0 disables the gate.",
34331
+ type: "slider",
34332
+ min: 0,
34333
+ max: 64,
34334
+ step: 2,
34335
+ default: 24
34336
+ }];
34337
+ function clusterStepSettingKey(stepId, fieldKey) {
34338
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
34339
+ }
34340
+ var ClusterSettingNumberSchema = number().finite();
34341
+ function readClusterStepSettings(config) {
34342
+ const out = {};
34343
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
34344
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
34345
+ const value = parsed.success ? parsed.data : field.default;
34346
+ const existing = out[field.stepId] ?? {};
34347
+ out[field.stepId] = {
34348
+ ...existing,
34349
+ [field.key]: value
34350
+ };
34351
+ }
34352
+ return out;
34353
+ }
34354
+ readClusterStepSettings({});
34114
34355
  object({
34115
34356
  /**
34116
34357
  * Fraction of the box's own size added on EACH side before cutting.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-notifiers",
3
- "version": "1.2.29",
3
+ "version": "1.2.31",
4
4
  "description": "System notifiers addon for CamStack — a `notification-output` collection provider hosting per-kind notifier adapters (ntfy, pushover, gotify, telegram, discord, webhook, zentik).",
5
5
  "keywords": [
6
6
  "camstack",