@camstack/addon-export-hap 1.2.36 → 1.2.38

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.
@@ -84,7 +84,7 @@ function carryForward(base, existing, keys) {
84
84
  return out;
85
85
  }
86
86
  //#endregion
87
- //#region ../types/dist/event-category-XfKNtfCc.mjs
87
+ //#region ../types/dist/event-category-CIa_iT6b.mjs
88
88
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
89
89
  EventCategory["SystemBoot"] = "system.boot";
90
90
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -100,6 +100,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
100
100
  */
101
101
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
102
102
  /**
103
+ * The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
104
+ * Emitted only when the material on disk actually changed, so an
105
+ * operator who trusted the old certificate by hand is told rather than
106
+ * discovering it as a browser error. Payload `TlsCertChangedPayload`.
107
+ *
108
+ * Rule: docs/decisions/adr-0227-*.md
109
+ */
110
+ EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
111
+ /**
103
112
  * A newer addon or server-root package version was found by the
104
113
  * authoritative registry check. Emitted once when any observed
105
114
  * `latestVersion` changes (or a package/node first appears behind);
@@ -8731,6 +8740,15 @@ var LabelDefinitionSchema = object({
8731
8740
  description: string().optional(),
8732
8741
  icon: string().optional()
8733
8742
  });
8743
+ var ClassMapDefinitionSchema = object({
8744
+ mapping: record(string(), _enum([
8745
+ "person",
8746
+ "vehicle",
8747
+ "animal",
8748
+ "package"
8749
+ ])),
8750
+ preserveOriginal: boolean()
8751
+ });
8734
8752
  var MODEL_FORMATS = [
8735
8753
  "onnx",
8736
8754
  "coreml",
@@ -8909,7 +8927,13 @@ var ModelCatalogEntrySchema = object({
8909
8927
  * `id` stays the source of truth for resolution/download/persistence; grouping
8910
8928
  * is a presentation overlay resolved back to an `id`.
8911
8929
  */
8912
- group: ModelVariantGroupSchema.optional()
8930
+ group: ModelVariantGroupSchema.optional(),
8931
+ /**
8932
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8933
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8934
+ * labels already ARE the CamStack macros (Scrypted identity map).
8935
+ */
8936
+ classMap: ClassMapDefinitionSchema.optional()
8913
8937
  });
8914
8938
  var ConvertTargetSchema = discriminatedUnion("format", [object({
8915
8939
  format: literal("openvino"),
@@ -8938,7 +8962,8 @@ var ModelConvertMetadataSchema = object({
8938
8962
  "ocr",
8939
8963
  "segmentation"
8940
8964
  ]),
8941
- faceAlignment: boolean().optional()
8965
+ faceAlignment: boolean().optional(),
8966
+ classMap: ClassMapDefinitionSchema.optional()
8942
8967
  });
8943
8968
  var ConvertResultSchema = object({
8944
8969
  entry: ModelCatalogEntrySchema,
@@ -18010,6 +18035,33 @@ var NativeCropRefSchema = object({
18010
18035
  h: number()
18011
18036
  })
18012
18037
  });
18038
+ object({
18039
+ crop: object({
18040
+ left: number(),
18041
+ top: number(),
18042
+ width: number().positive(),
18043
+ height: number().positive()
18044
+ }).optional(),
18045
+ content: object({
18046
+ width: number().int().positive(),
18047
+ height: number().int().positive()
18048
+ }),
18049
+ fit: _enum(["stretch", "contain"]),
18050
+ format: _enum([
18051
+ "rgb",
18052
+ "gray",
18053
+ "jpeg"
18054
+ ])
18055
+ });
18056
+ var FrameRefSchema = object({
18057
+ registryId: string().min(1),
18058
+ id: string().min(1),
18059
+ width: number().int().positive(),
18060
+ height: number().int().positive(),
18061
+ format: _enum(["rgb", "gray"]),
18062
+ timestamp: number(),
18063
+ capturedAt: number().optional()
18064
+ });
18013
18065
  var ModelFormatSchema$1 = _enum([
18014
18066
  "onnx",
18015
18067
  "coreml",
@@ -18254,6 +18306,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
18254
18306
  steps: array(PipelineStepInputSchema).min(1),
18255
18307
  frame: FrameInputSchema.optional(),
18256
18308
  /**
18309
+ * Process-local lazy frame. Valid only when caller and provider resolve
18310
+ * in the same execution-group process; split/cross-node callers use
18311
+ * `frame`/`image` inline compatibility instead.
18312
+ */
18313
+ frameRef: FrameRefSchema.optional(),
18314
+ /**
18257
18315
  * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
18258
18316
  * the decoded pixels live in. One more member of the one-of
18259
18317
  * frame/frameHandle/image/imageBase64/referenceImage group.
@@ -18509,7 +18567,10 @@ var NativeCropResultSchema = object({
18509
18567
  * Which source served this crop, so a quality-sensitive consumer (the native
18510
18568
  * `keyFrame`) can reject a degraded fallback:
18511
18569
  * - `native` — cut from the decode worker's retained NATIVE surface (the
18512
- * quality path).
18570
+ * quality path). A subject-tile serve is also native-resolution and stays
18571
+ * `native` here: the public enum cannot name `tile` without a breaking cap
18572
+ * change. Runner telemetry distinguishes lease vs tile via `source` on the
18573
+ * internal crop result (`nativeHits` vs `tileHits`).
18513
18574
  * - `ram-fullframe` — the native surface MISSED but the request was full-frame,
18514
18575
  * so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
18515
18576
  * the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
@@ -19000,12 +19061,41 @@ var RunnerLocalLoadSchema = object({
19000
19061
  * legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
19001
19062
  * working unchanged when they switch to reading from the runner cap.
19002
19063
  */
19064
+ var FrameLazyCountersSchema = object({
19065
+ framesDecoded: number(),
19066
+ framesAdmitted: number(),
19067
+ framesDroppedPixelFree: number(),
19068
+ viewsMaterialized: number(),
19069
+ viewsSkipped: number(),
19070
+ workerToRunnerBytes: number(),
19071
+ runnerToPoolRawBytes: number(),
19072
+ runnerToPoolJpegBytes: number(),
19073
+ onDemandFullFrameRequests: number(),
19074
+ onDemandCropRequests: number(),
19075
+ nativeHits: number(),
19076
+ nativeMisses: number(),
19077
+ tileHits: number(),
19078
+ tileMisses: number(),
19079
+ fallbackHits: number(),
19080
+ fallbackMisses: number(),
19081
+ retainedWritesAvoided: number(),
19082
+ residentRefs: number(),
19083
+ residentBytes: number(),
19084
+ releases: number(),
19085
+ evictions: number(),
19086
+ staleMisses: number()
19087
+ });
19088
+ var FrameLazyMetricsSchema = object({
19089
+ node: FrameLazyCountersSchema,
19090
+ cameras: array(FrameLazyCountersSchema.extend({ deviceId: number() }))
19091
+ });
19003
19092
  var RunnerLocalMetricsSchema = object({
19004
19093
  nodeId: string(),
19005
19094
  activeCameras: number(),
19006
19095
  throttledCameras: number(),
19007
19096
  avgInferenceTimeMs: number(),
19008
- queueDepth: number()
19097
+ queueDepth: number(),
19098
+ frameLazy: FrameLazyMetricsSchema.optional()
19009
19099
  });
19010
19100
  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({
19011
19101
  handle: FrameHandleSchema,
@@ -20305,6 +20395,9 @@ method(_void(), ProviderInfoSchema), method(object({ config: record(string(), un
20305
20395
  location: StorageLocationSchema,
20306
20396
  relativePath: string()
20307
20397
  }), _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" });
20398
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
20399
+ var ProfileSettingsSchemaBridge = unknown().nullable();
20400
+ var ProfileSettingsBagSchema = record(string(), unknown());
20308
20401
  /**
20309
20402
  * A live terminal session hosted by the provider addon. Output and input do
20310
20403
  * NOT flow through the capability — they use the addon data plane
@@ -20334,7 +20427,14 @@ var TerminalSessionInfoSchema = object({
20334
20427
  var TerminalProfileInfoSchema = object({
20335
20428
  profileId: string(),
20336
20429
  label: string(),
20337
- description: string().optional()
20430
+ description: string().optional(),
20431
+ /** Spawn defaults the instance form copies on create. */
20432
+ executable: string().optional(),
20433
+ args: array(string()).readonly().optional(),
20434
+ cwd: string().optional(),
20435
+ environment: array(string()).readonly().optional(),
20436
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
20437
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
20338
20438
  });
20339
20439
  /**
20340
20440
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -20347,7 +20447,12 @@ var TerminalInstanceInfoSchema = object({
20347
20447
  profileId: string(),
20348
20448
  profileLabel: string(),
20349
20449
  name: string(),
20350
- enabled: boolean()
20450
+ enabled: boolean(),
20451
+ executable: string(),
20452
+ args: array(string()).readonly(),
20453
+ cwd: string(),
20454
+ environment: array(string()).readonly(),
20455
+ profileSettings: ProfileSettingsBagSchema
20351
20456
  });
20352
20457
  var TerminalLegacyCameraSchema = object({
20353
20458
  stableId: string(),
@@ -20377,7 +20482,23 @@ var TerminalOutputBatchSchema = object({
20377
20482
  method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
20378
20483
  targetNodeId: string().min(1),
20379
20484
  profileId: string().min(1),
20380
- name: string().trim().min(1).max(160).optional()
20485
+ name: string().trim().min(1).max(160).optional(),
20486
+ executable: string().max(1024).optional(),
20487
+ args: array(string().max(2048)).max(64).optional(),
20488
+ cwd: string().max(1024).optional(),
20489
+ environment: array(string().max(4096)).max(64).optional(),
20490
+ profileSettings: ProfileSettingsBagSchema.optional()
20491
+ }), TerminalInstanceInfoSchema, {
20492
+ kind: "mutation",
20493
+ auth: "admin"
20494
+ }), method(object({
20495
+ instanceId: string().min(1),
20496
+ name: string().trim().min(1).max(160).optional(),
20497
+ executable: string().max(1024).optional(),
20498
+ args: array(string().max(2048)).max(64).optional(),
20499
+ cwd: string().max(1024).optional(),
20500
+ environment: array(string().max(4096)).max(64).optional(),
20501
+ profileSettings: ProfileSettingsBagSchema.optional()
20381
20502
  }), TerminalInstanceInfoSchema, {
20382
20503
  kind: "mutation",
20383
20504
  auth: "admin"
@@ -20399,7 +20520,11 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
20399
20520
  }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
20400
20521
  profileId: string(),
20401
20522
  cols: number().int().positive(),
20402
- rows: number().int().positive()
20523
+ rows: number().int().positive(),
20524
+ executable: string().max(1024).optional(),
20525
+ args: array(string().max(2048)).max(64).optional(),
20526
+ cwd: string().max(1024).optional(),
20527
+ environment: array(string().max(4096)).max(64).optional()
20403
20528
  }), TerminalSessionInfoSchema, {
20404
20529
  kind: "mutation",
20405
20530
  auth: "admin"
@@ -23217,10 +23342,10 @@ DeviceType.LawnMower, method(object({ deviceId: number().int().nonnegative() }),
23217
23342
  *
23218
23343
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
23219
23344
  * to receive an ordered list of candidate base URLs it should race
23220
- * on connect — LAN IPv4 first (lowest latency when on same network),
23221
- * then public hostname (if a tunnel is up), then IPv6. The SDK can
23222
- * race them with short timeouts and stick with the winner for the
23223
- * session.
23345
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
23346
+ * when on the same network), then public hostname (if a tunnel is
23347
+ * up). The SDK can race them with short timeouts and stick with the
23348
+ * winner for the session.
23224
23349
  *
23225
23350
  * Why hub-only: agents are not directly addressable by the operator's
23226
23351
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -23375,6 +23500,17 @@ var NotificationEndpointSchema = object({
23375
23500
  /** What the ranking currently resolves to (null when nothing is reachable). */
23376
23501
  resolved: string().nullable()
23377
23502
  });
23503
+ /**
23504
+ * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
23505
+ * AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
23506
+ * currently expands to, so the UI can show the effective set either way.
23507
+ */
23508
+ var ViewerEndpointsSchema = object({
23509
+ /** The operator's explicit race set, or empty for AUTO. */
23510
+ baseUrls: array(string()).readonly(),
23511
+ /** What the ranking currently resolves to (may be empty if nothing is up). */
23512
+ resolved: array(string()).readonly()
23513
+ });
23378
23514
  var AllowedAddressesSchema = object({
23379
23515
  /**
23380
23516
  * Allowlist of interface addresses operators have explicitly opted
@@ -23383,6 +23519,20 @@ var AllowedAddressesSchema = object({
23383
23519
  * Network Addresses admin page and persisted by the addon.
23384
23520
  */
23385
23521
  addresses: array(string()).readonly() });
23522
+ var TlsStatusSchema = object({
23523
+ mode: _enum([
23524
+ "generated",
23525
+ "uploaded",
23526
+ "disabled"
23527
+ ]),
23528
+ leafFingerprintSha256: string().nullable(),
23529
+ caFingerprintSha256: string().nullable(),
23530
+ validTo: string().nullable(),
23531
+ sans: array(string()),
23532
+ caCertPem: string().nullable(),
23533
+ reissueError: string().nullable(),
23534
+ restartRequired: boolean()
23535
+ });
23386
23536
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
23387
23537
  /**
23388
23538
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -23392,17 +23542,31 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
23392
23542
  */
23393
23543
  port: number().int().min(1).max(65535).optional(),
23394
23544
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
23395
- * candidate. Default `true`. */
23545
+ * candidate. Default `false` — loopback is not a client route. */
23396
23546
  includeLoopback: boolean().optional(),
23397
- /** Skip IPv6 entries. Some legacy clients can't parse them.
23398
- * Default `false`. */
23547
+ /** Skip IPv6 entries. Default `false` the palette includes stable
23548
+ * LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
23549
+ * hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
23399
23550
  ipv4Only: boolean().optional(),
23400
23551
  /** Scheme to emit for LAN/loopback URLs. Default `'http'`.
23401
23552
  * Pass `'https'` when the caller is itself loaded over HTTPS
23402
23553
  * to avoid mixed-content blocks in the browser. The public
23403
23554
  * tunnel always emits `https://` regardless. */
23404
23555
  scheme: _enum(["http", "https"]).optional()
23405
- }), 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" });
23556
+ }), 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, {
23557
+ kind: "mutation",
23558
+ auth: "admin"
23559
+ }), method(object({
23560
+ certPem: string().min(1),
23561
+ keyPem: string().min(1),
23562
+ caPem: string().optional()
23563
+ }), TlsStatusSchema, {
23564
+ kind: "mutation",
23565
+ auth: "admin"
23566
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
23567
+ kind: "mutation",
23568
+ auth: "admin"
23569
+ });
23406
23570
  var LockControlStatusSchema = object({
23407
23571
  /** Lifecycle state of the lock. `jammed` means the motor reported
23408
23572
  * failure to reach the target — operator intervention required. */
@@ -29202,6 +29366,12 @@ Object.freeze({
29202
29366
  addonId: null,
29203
29367
  access: "create"
29204
29368
  },
29369
+ "localNetwork.downloadCa": {
29370
+ capName: "local-network",
29371
+ capScope: "system",
29372
+ addonId: null,
29373
+ access: "view"
29374
+ },
29205
29375
  "localNetwork.getAllowedAddresses": {
29206
29376
  capName: "local-network",
29207
29377
  capScope: "system",
@@ -29226,18 +29396,42 @@ Object.freeze({
29226
29396
  addonId: null,
29227
29397
  access: "view"
29228
29398
  },
29399
+ "localNetwork.getTlsStatus": {
29400
+ capName: "local-network",
29401
+ capScope: "system",
29402
+ addonId: null,
29403
+ access: "view"
29404
+ },
29405
+ "localNetwork.getViewerEndpoints": {
29406
+ capName: "local-network",
29407
+ capScope: "system",
29408
+ addonId: null,
29409
+ access: "view"
29410
+ },
29229
29411
  "localNetwork.list": {
29230
29412
  capName: "local-network",
29231
29413
  capScope: "system",
29232
29414
  addonId: null,
29233
29415
  access: "view"
29234
29416
  },
29417
+ "localNetwork.regenerateCertificate": {
29418
+ capName: "local-network",
29419
+ capScope: "system",
29420
+ addonId: null,
29421
+ access: "create"
29422
+ },
29235
29423
  "localNetwork.resetAllowlistToBestMatch": {
29236
29424
  capName: "local-network",
29237
29425
  capScope: "system",
29238
29426
  addonId: null,
29239
29427
  access: "delete"
29240
29428
  },
29429
+ "localNetwork.revertToGeneratedCertificate": {
29430
+ capName: "local-network",
29431
+ capScope: "system",
29432
+ addonId: null,
29433
+ access: "create"
29434
+ },
29241
29435
  "localNetwork.setAllowedAddresses": {
29242
29436
  capName: "local-network",
29243
29437
  capScope: "system",
@@ -29250,6 +29444,18 @@ Object.freeze({
29250
29444
  addonId: null,
29251
29445
  access: "create"
29252
29446
  },
29447
+ "localNetwork.setViewerEndpoints": {
29448
+ capName: "local-network",
29449
+ capScope: "system",
29450
+ addonId: null,
29451
+ access: "create"
29452
+ },
29453
+ "localNetwork.uploadCertificate": {
29454
+ capName: "local-network",
29455
+ capScope: "system",
29456
+ addonId: null,
29457
+ access: "create"
29458
+ },
29253
29459
  "lockControl.lock": {
29254
29460
  capName: "lock-control",
29255
29461
  capScope: "device",
@@ -32130,6 +32336,12 @@ Object.freeze({
32130
32336
  addonId: null,
32131
32337
  access: "create"
32132
32338
  },
32339
+ "terminalSession.updateInstance": {
32340
+ capName: "terminal-session",
32341
+ capScope: "system",
32342
+ addonId: null,
32343
+ access: "create"
32344
+ },
32133
32345
  "terminalSession.writeInput": {
32134
32346
  capName: "terminal-session",
32135
32347
  capScope: "system",
@@ -34617,6 +34829,35 @@ Object.freeze(Object.fromEntries([{
34617
34829
  }]
34618
34830
  }].map((s) => [s.stepId, s.defaultModelId])));
34619
34831
  string().min(1);
34832
+ var CLUSTER_STEP_SETTING_FIELDS = [{
34833
+ stepId: "face-embedding",
34834
+ key: "minLandmarkFaceSize",
34835
+ label: "Min face size for recognition (detection px)",
34836
+ 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.",
34837
+ type: "slider",
34838
+ min: 0,
34839
+ max: 64,
34840
+ step: 2,
34841
+ default: 24
34842
+ }];
34843
+ function clusterStepSettingKey(stepId, fieldKey) {
34844
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
34845
+ }
34846
+ var ClusterSettingNumberSchema = number().finite();
34847
+ function readClusterStepSettings(config) {
34848
+ const out = {};
34849
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
34850
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
34851
+ const value = parsed.success ? parsed.data : field.default;
34852
+ const existing = out[field.stepId] ?? {};
34853
+ out[field.stepId] = {
34854
+ ...existing,
34855
+ [field.key]: value
34856
+ };
34857
+ }
34858
+ return out;
34859
+ }
34860
+ readClusterStepSettings({});
34620
34861
  object({
34621
34862
  /**
34622
34863
  * Fraction of the box's own size added on EACH side before cutting.
@@ -72,7 +72,7 @@ function carryForward(base, existing, keys) {
72
72
  return out;
73
73
  }
74
74
  //#endregion
75
- //#region ../types/dist/event-category-XfKNtfCc.mjs
75
+ //#region ../types/dist/event-category-CIa_iT6b.mjs
76
76
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
77
77
  EventCategory["SystemBoot"] = "system.boot";
78
78
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -88,6 +88,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
88
88
  */
89
89
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
90
90
  /**
91
+ * The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
92
+ * Emitted only when the material on disk actually changed, so an
93
+ * operator who trusted the old certificate by hand is told rather than
94
+ * discovering it as a browser error. Payload `TlsCertChangedPayload`.
95
+ *
96
+ * Rule: docs/decisions/adr-0227-*.md
97
+ */
98
+ EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
99
+ /**
91
100
  * A newer addon or server-root package version was found by the
92
101
  * authoritative registry check. Emitted once when any observed
93
102
  * `latestVersion` changes (or a package/node first appears behind);
@@ -8719,6 +8728,15 @@ var LabelDefinitionSchema = object({
8719
8728
  description: string().optional(),
8720
8729
  icon: string().optional()
8721
8730
  });
8731
+ var ClassMapDefinitionSchema = object({
8732
+ mapping: record(string(), _enum([
8733
+ "person",
8734
+ "vehicle",
8735
+ "animal",
8736
+ "package"
8737
+ ])),
8738
+ preserveOriginal: boolean()
8739
+ });
8722
8740
  var MODEL_FORMATS = [
8723
8741
  "onnx",
8724
8742
  "coreml",
@@ -8897,7 +8915,13 @@ var ModelCatalogEntrySchema = object({
8897
8915
  * `id` stays the source of truth for resolution/download/persistence; grouping
8898
8916
  * is a presentation overlay resolved back to an `id`.
8899
8917
  */
8900
- group: ModelVariantGroupSchema.optional()
8918
+ group: ModelVariantGroupSchema.optional(),
8919
+ /**
8920
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8921
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8922
+ * labels already ARE the CamStack macros (Scrypted identity map).
8923
+ */
8924
+ classMap: ClassMapDefinitionSchema.optional()
8901
8925
  });
8902
8926
  var ConvertTargetSchema = discriminatedUnion("format", [object({
8903
8927
  format: literal("openvino"),
@@ -8926,7 +8950,8 @@ var ModelConvertMetadataSchema = object({
8926
8950
  "ocr",
8927
8951
  "segmentation"
8928
8952
  ]),
8929
- faceAlignment: boolean().optional()
8953
+ faceAlignment: boolean().optional(),
8954
+ classMap: ClassMapDefinitionSchema.optional()
8930
8955
  });
8931
8956
  var ConvertResultSchema = object({
8932
8957
  entry: ModelCatalogEntrySchema,
@@ -17998,6 +18023,33 @@ var NativeCropRefSchema = object({
17998
18023
  h: number()
17999
18024
  })
18000
18025
  });
18026
+ object({
18027
+ crop: object({
18028
+ left: number(),
18029
+ top: number(),
18030
+ width: number().positive(),
18031
+ height: number().positive()
18032
+ }).optional(),
18033
+ content: object({
18034
+ width: number().int().positive(),
18035
+ height: number().int().positive()
18036
+ }),
18037
+ fit: _enum(["stretch", "contain"]),
18038
+ format: _enum([
18039
+ "rgb",
18040
+ "gray",
18041
+ "jpeg"
18042
+ ])
18043
+ });
18044
+ var FrameRefSchema = object({
18045
+ registryId: string().min(1),
18046
+ id: string().min(1),
18047
+ width: number().int().positive(),
18048
+ height: number().int().positive(),
18049
+ format: _enum(["rgb", "gray"]),
18050
+ timestamp: number(),
18051
+ capturedAt: number().optional()
18052
+ });
18001
18053
  var ModelFormatSchema$1 = _enum([
18002
18054
  "onnx",
18003
18055
  "coreml",
@@ -18242,6 +18294,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
18242
18294
  steps: array(PipelineStepInputSchema).min(1),
18243
18295
  frame: FrameInputSchema.optional(),
18244
18296
  /**
18297
+ * Process-local lazy frame. Valid only when caller and provider resolve
18298
+ * in the same execution-group process; split/cross-node callers use
18299
+ * `frame`/`image` inline compatibility instead.
18300
+ */
18301
+ frameRef: FrameRefSchema.optional(),
18302
+ /**
18245
18303
  * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
18246
18304
  * the decoded pixels live in. One more member of the one-of
18247
18305
  * frame/frameHandle/image/imageBase64/referenceImage group.
@@ -18497,7 +18555,10 @@ var NativeCropResultSchema = object({
18497
18555
  * Which source served this crop, so a quality-sensitive consumer (the native
18498
18556
  * `keyFrame`) can reject a degraded fallback:
18499
18557
  * - `native` — cut from the decode worker's retained NATIVE surface (the
18500
- * quality path).
18558
+ * quality path). A subject-tile serve is also native-resolution and stays
18559
+ * `native` here: the public enum cannot name `tile` without a breaking cap
18560
+ * change. Runner telemetry distinguishes lease vs tile via `source` on the
18561
+ * internal crop result (`nativeHits` vs `tileHits`).
18501
18562
  * - `ram-fullframe` — the native surface MISSED but the request was full-frame,
18502
18563
  * so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
18503
18564
  * the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
@@ -18988,12 +19049,41 @@ var RunnerLocalLoadSchema = object({
18988
19049
  * legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
18989
19050
  * working unchanged when they switch to reading from the runner cap.
18990
19051
  */
19052
+ var FrameLazyCountersSchema = object({
19053
+ framesDecoded: number(),
19054
+ framesAdmitted: number(),
19055
+ framesDroppedPixelFree: number(),
19056
+ viewsMaterialized: number(),
19057
+ viewsSkipped: number(),
19058
+ workerToRunnerBytes: number(),
19059
+ runnerToPoolRawBytes: number(),
19060
+ runnerToPoolJpegBytes: number(),
19061
+ onDemandFullFrameRequests: number(),
19062
+ onDemandCropRequests: number(),
19063
+ nativeHits: number(),
19064
+ nativeMisses: number(),
19065
+ tileHits: number(),
19066
+ tileMisses: number(),
19067
+ fallbackHits: number(),
19068
+ fallbackMisses: number(),
19069
+ retainedWritesAvoided: number(),
19070
+ residentRefs: number(),
19071
+ residentBytes: number(),
19072
+ releases: number(),
19073
+ evictions: number(),
19074
+ staleMisses: number()
19075
+ });
19076
+ var FrameLazyMetricsSchema = object({
19077
+ node: FrameLazyCountersSchema,
19078
+ cameras: array(FrameLazyCountersSchema.extend({ deviceId: number() }))
19079
+ });
18991
19080
  var RunnerLocalMetricsSchema = object({
18992
19081
  nodeId: string(),
18993
19082
  activeCameras: number(),
18994
19083
  throttledCameras: number(),
18995
19084
  avgInferenceTimeMs: number(),
18996
- queueDepth: number()
19085
+ queueDepth: number(),
19086
+ frameLazy: FrameLazyMetricsSchema.optional()
18997
19087
  });
18998
19088
  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({
18999
19089
  handle: FrameHandleSchema,
@@ -20293,6 +20383,9 @@ method(_void(), ProviderInfoSchema), method(object({ config: record(string(), un
20293
20383
  location: StorageLocationSchema,
20294
20384
  relativePath: string()
20295
20385
  }), _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" });
20386
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
20387
+ var ProfileSettingsSchemaBridge = unknown().nullable();
20388
+ var ProfileSettingsBagSchema = record(string(), unknown());
20296
20389
  /**
20297
20390
  * A live terminal session hosted by the provider addon. Output and input do
20298
20391
  * NOT flow through the capability — they use the addon data plane
@@ -20322,7 +20415,14 @@ var TerminalSessionInfoSchema = object({
20322
20415
  var TerminalProfileInfoSchema = object({
20323
20416
  profileId: string(),
20324
20417
  label: string(),
20325
- description: string().optional()
20418
+ description: string().optional(),
20419
+ /** Spawn defaults the instance form copies on create. */
20420
+ executable: string().optional(),
20421
+ args: array(string()).readonly().optional(),
20422
+ cwd: string().optional(),
20423
+ environment: array(string()).readonly().optional(),
20424
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
20425
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
20326
20426
  });
20327
20427
  /**
20328
20428
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -20335,7 +20435,12 @@ var TerminalInstanceInfoSchema = object({
20335
20435
  profileId: string(),
20336
20436
  profileLabel: string(),
20337
20437
  name: string(),
20338
- enabled: boolean()
20438
+ enabled: boolean(),
20439
+ executable: string(),
20440
+ args: array(string()).readonly(),
20441
+ cwd: string(),
20442
+ environment: array(string()).readonly(),
20443
+ profileSettings: ProfileSettingsBagSchema
20339
20444
  });
20340
20445
  var TerminalLegacyCameraSchema = object({
20341
20446
  stableId: string(),
@@ -20365,7 +20470,23 @@ var TerminalOutputBatchSchema = object({
20365
20470
  method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
20366
20471
  targetNodeId: string().min(1),
20367
20472
  profileId: string().min(1),
20368
- name: string().trim().min(1).max(160).optional()
20473
+ name: string().trim().min(1).max(160).optional(),
20474
+ executable: string().max(1024).optional(),
20475
+ args: array(string().max(2048)).max(64).optional(),
20476
+ cwd: string().max(1024).optional(),
20477
+ environment: array(string().max(4096)).max(64).optional(),
20478
+ profileSettings: ProfileSettingsBagSchema.optional()
20479
+ }), TerminalInstanceInfoSchema, {
20480
+ kind: "mutation",
20481
+ auth: "admin"
20482
+ }), method(object({
20483
+ instanceId: string().min(1),
20484
+ name: string().trim().min(1).max(160).optional(),
20485
+ executable: string().max(1024).optional(),
20486
+ args: array(string().max(2048)).max(64).optional(),
20487
+ cwd: string().max(1024).optional(),
20488
+ environment: array(string().max(4096)).max(64).optional(),
20489
+ profileSettings: ProfileSettingsBagSchema.optional()
20369
20490
  }), TerminalInstanceInfoSchema, {
20370
20491
  kind: "mutation",
20371
20492
  auth: "admin"
@@ -20387,7 +20508,11 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
20387
20508
  }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
20388
20509
  profileId: string(),
20389
20510
  cols: number().int().positive(),
20390
- rows: number().int().positive()
20511
+ rows: number().int().positive(),
20512
+ executable: string().max(1024).optional(),
20513
+ args: array(string().max(2048)).max(64).optional(),
20514
+ cwd: string().max(1024).optional(),
20515
+ environment: array(string().max(4096)).max(64).optional()
20391
20516
  }), TerminalSessionInfoSchema, {
20392
20517
  kind: "mutation",
20393
20518
  auth: "admin"
@@ -23205,10 +23330,10 @@ DeviceType.LawnMower, method(object({ deviceId: number().int().nonnegative() }),
23205
23330
  *
23206
23331
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
23207
23332
  * to receive an ordered list of candidate base URLs it should race
23208
- * on connect — LAN IPv4 first (lowest latency when on same network),
23209
- * then public hostname (if a tunnel is up), then IPv6. The SDK can
23210
- * race them with short timeouts and stick with the winner for the
23211
- * session.
23333
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
23334
+ * when on the same network), then public hostname (if a tunnel is
23335
+ * up). The SDK can race them with short timeouts and stick with the
23336
+ * winner for the session.
23212
23337
  *
23213
23338
  * Why hub-only: agents are not directly addressable by the operator's
23214
23339
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -23363,6 +23488,17 @@ var NotificationEndpointSchema = object({
23363
23488
  /** What the ranking currently resolves to (null when nothing is reachable). */
23364
23489
  resolved: string().nullable()
23365
23490
  });
23491
+ /**
23492
+ * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
23493
+ * AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
23494
+ * currently expands to, so the UI can show the effective set either way.
23495
+ */
23496
+ var ViewerEndpointsSchema = object({
23497
+ /** The operator's explicit race set, or empty for AUTO. */
23498
+ baseUrls: array(string()).readonly(),
23499
+ /** What the ranking currently resolves to (may be empty if nothing is up). */
23500
+ resolved: array(string()).readonly()
23501
+ });
23366
23502
  var AllowedAddressesSchema = object({
23367
23503
  /**
23368
23504
  * Allowlist of interface addresses operators have explicitly opted
@@ -23371,6 +23507,20 @@ var AllowedAddressesSchema = object({
23371
23507
  * Network Addresses admin page and persisted by the addon.
23372
23508
  */
23373
23509
  addresses: array(string()).readonly() });
23510
+ var TlsStatusSchema = object({
23511
+ mode: _enum([
23512
+ "generated",
23513
+ "uploaded",
23514
+ "disabled"
23515
+ ]),
23516
+ leafFingerprintSha256: string().nullable(),
23517
+ caFingerprintSha256: string().nullable(),
23518
+ validTo: string().nullable(),
23519
+ sans: array(string()),
23520
+ caCertPem: string().nullable(),
23521
+ reissueError: string().nullable(),
23522
+ restartRequired: boolean()
23523
+ });
23374
23524
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
23375
23525
  /**
23376
23526
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -23380,17 +23530,31 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
23380
23530
  */
23381
23531
  port: number().int().min(1).max(65535).optional(),
23382
23532
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
23383
- * candidate. Default `true`. */
23533
+ * candidate. Default `false` — loopback is not a client route. */
23384
23534
  includeLoopback: boolean().optional(),
23385
- /** Skip IPv6 entries. Some legacy clients can't parse them.
23386
- * Default `false`. */
23535
+ /** Skip IPv6 entries. Default `false` the palette includes stable
23536
+ * LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
23537
+ * hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
23387
23538
  ipv4Only: boolean().optional(),
23388
23539
  /** Scheme to emit for LAN/loopback URLs. Default `'http'`.
23389
23540
  * Pass `'https'` when the caller is itself loaded over HTTPS
23390
23541
  * to avoid mixed-content blocks in the browser. The public
23391
23542
  * tunnel always emits `https://` regardless. */
23392
23543
  scheme: _enum(["http", "https"]).optional()
23393
- }), 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" });
23544
+ }), 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, {
23545
+ kind: "mutation",
23546
+ auth: "admin"
23547
+ }), method(object({
23548
+ certPem: string().min(1),
23549
+ keyPem: string().min(1),
23550
+ caPem: string().optional()
23551
+ }), TlsStatusSchema, {
23552
+ kind: "mutation",
23553
+ auth: "admin"
23554
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
23555
+ kind: "mutation",
23556
+ auth: "admin"
23557
+ });
23394
23558
  var LockControlStatusSchema = object({
23395
23559
  /** Lifecycle state of the lock. `jammed` means the motor reported
23396
23560
  * failure to reach the target — operator intervention required. */
@@ -29190,6 +29354,12 @@ Object.freeze({
29190
29354
  addonId: null,
29191
29355
  access: "create"
29192
29356
  },
29357
+ "localNetwork.downloadCa": {
29358
+ capName: "local-network",
29359
+ capScope: "system",
29360
+ addonId: null,
29361
+ access: "view"
29362
+ },
29193
29363
  "localNetwork.getAllowedAddresses": {
29194
29364
  capName: "local-network",
29195
29365
  capScope: "system",
@@ -29214,18 +29384,42 @@ Object.freeze({
29214
29384
  addonId: null,
29215
29385
  access: "view"
29216
29386
  },
29387
+ "localNetwork.getTlsStatus": {
29388
+ capName: "local-network",
29389
+ capScope: "system",
29390
+ addonId: null,
29391
+ access: "view"
29392
+ },
29393
+ "localNetwork.getViewerEndpoints": {
29394
+ capName: "local-network",
29395
+ capScope: "system",
29396
+ addonId: null,
29397
+ access: "view"
29398
+ },
29217
29399
  "localNetwork.list": {
29218
29400
  capName: "local-network",
29219
29401
  capScope: "system",
29220
29402
  addonId: null,
29221
29403
  access: "view"
29222
29404
  },
29405
+ "localNetwork.regenerateCertificate": {
29406
+ capName: "local-network",
29407
+ capScope: "system",
29408
+ addonId: null,
29409
+ access: "create"
29410
+ },
29223
29411
  "localNetwork.resetAllowlistToBestMatch": {
29224
29412
  capName: "local-network",
29225
29413
  capScope: "system",
29226
29414
  addonId: null,
29227
29415
  access: "delete"
29228
29416
  },
29417
+ "localNetwork.revertToGeneratedCertificate": {
29418
+ capName: "local-network",
29419
+ capScope: "system",
29420
+ addonId: null,
29421
+ access: "create"
29422
+ },
29229
29423
  "localNetwork.setAllowedAddresses": {
29230
29424
  capName: "local-network",
29231
29425
  capScope: "system",
@@ -29238,6 +29432,18 @@ Object.freeze({
29238
29432
  addonId: null,
29239
29433
  access: "create"
29240
29434
  },
29435
+ "localNetwork.setViewerEndpoints": {
29436
+ capName: "local-network",
29437
+ capScope: "system",
29438
+ addonId: null,
29439
+ access: "create"
29440
+ },
29441
+ "localNetwork.uploadCertificate": {
29442
+ capName: "local-network",
29443
+ capScope: "system",
29444
+ addonId: null,
29445
+ access: "create"
29446
+ },
29241
29447
  "lockControl.lock": {
29242
29448
  capName: "lock-control",
29243
29449
  capScope: "device",
@@ -32118,6 +32324,12 @@ Object.freeze({
32118
32324
  addonId: null,
32119
32325
  access: "create"
32120
32326
  },
32327
+ "terminalSession.updateInstance": {
32328
+ capName: "terminal-session",
32329
+ capScope: "system",
32330
+ addonId: null,
32331
+ access: "create"
32332
+ },
32121
32333
  "terminalSession.writeInput": {
32122
32334
  capName: "terminal-session",
32123
32335
  capScope: "system",
@@ -34605,6 +34817,35 @@ Object.freeze(Object.fromEntries([{
34605
34817
  }]
34606
34818
  }].map((s) => [s.stepId, s.defaultModelId])));
34607
34819
  string().min(1);
34820
+ var CLUSTER_STEP_SETTING_FIELDS = [{
34821
+ stepId: "face-embedding",
34822
+ key: "minLandmarkFaceSize",
34823
+ label: "Min face size for recognition (detection px)",
34824
+ 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.",
34825
+ type: "slider",
34826
+ min: 0,
34827
+ max: 64,
34828
+ step: 2,
34829
+ default: 24
34830
+ }];
34831
+ function clusterStepSettingKey(stepId, fieldKey) {
34832
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
34833
+ }
34834
+ var ClusterSettingNumberSchema = number().finite();
34835
+ function readClusterStepSettings(config) {
34836
+ const out = {};
34837
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
34838
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
34839
+ const value = parsed.success ? parsed.data : field.default;
34840
+ const existing = out[field.stepId] ?? {};
34841
+ out[field.stepId] = {
34842
+ ...existing,
34843
+ [field.key]: value
34844
+ };
34845
+ }
34846
+ return out;
34847
+ }
34848
+ readClusterStepSettings({});
34608
34849
  object({
34609
34850
  /**
34610
34851
  * 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-export-hap",
3
- "version": "1.2.36",
3
+ "version": "1.2.38",
4
4
  "description": "HomeKit (HAP) exporter for CamStack devices. Publishes each exposed device as its own HomeKit accessory: cameras and doorbells with SRTP streaming, HomeKit Secure Video, motion, two-way audio, PTZ and battery; switches, lights, locks and sensors through a capability→service table.",
5
5
  "keywords": [
6
6
  "camstack",