@camstack/addon-decoder-nodeav 1.2.51 → 1.2.53

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/index.js CHANGED
@@ -7718,7 +7718,8 @@ var OpsLogReasonSchema = _enum([
7718
7718
  "quota",
7719
7719
  "manual",
7720
7720
  "operator",
7721
- "maintenance"
7721
+ "maintenance",
7722
+ "orphaned-device"
7722
7723
  ]);
7723
7724
  /** One audit row, shared verbatim by both domains. */
7724
7725
  var OpsLogEntrySchema = object({
@@ -12879,10 +12880,39 @@ var DevicePersistConfigPayloadSchema = object({
12879
12880
  deviceId: number(),
12880
12881
  data: record(string(), unknown())
12881
12882
  });
12883
+ /** What a migration actually did, per switch. `unreachable` is a first-class
12884
+ * answer: a camera that could not be asked is not a camera that was silenced. */
12885
+ var MigrateSwitchOutcomeSchema = _enum([
12886
+ "off",
12887
+ "on",
12888
+ "not-offered",
12889
+ "unreachable"
12890
+ ]);
12891
+ var MigrateSwitchReportSchema = object({
12892
+ deviceId: number(),
12893
+ switchId: CameraSwitchIdSchema,
12894
+ outcome: MigrateSwitchOutcomeSchema,
12895
+ detail: string().optional()
12896
+ });
12897
+ var MigrateDeviceResultSchema = object({
12898
+ sourceId: number(),
12899
+ targetId: number(),
12900
+ switches: array(MigrateSwitchReportSchema).readonly(),
12901
+ /** Switches that could NOT be turned off on the source. Empty is the only
12902
+ * value meaning the replaced hardware is quiet. */
12903
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
12904
+ swapped: boolean()
12905
+ });
12882
12906
  method(object({
12883
12907
  addonId: string(),
12884
12908
  stableId: string()
12885
- }), object({ id: number() }), { kind: "mutation" }), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), { kind: "mutation" }), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), DeviceMetaSchema.nullable()), method(object({
12909
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
12910
+ sourceId: number(),
12911
+ targetId: number()
12912
+ }), MigrateDeviceResultSchema, {
12913
+ kind: "mutation",
12914
+ auth: "admin"
12915
+ }), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), { kind: "mutation" }), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), DeviceMetaSchema.nullable()), method(object({
12886
12916
  deviceId: number(),
12887
12917
  name: string()
12888
12918
  }), _void(), {
@@ -18979,6 +19009,34 @@ var KeyEventsForDeviceSchema = object({
18979
19009
  deviceId: number(),
18980
19010
  events: array(KeyEventSchema).readonly()
18981
19011
  });
19012
+ /** One non-empty bucket of the timeline histogram. */
19013
+ var EventDensityBucketSchema = object({
19014
+ bucketStart: number(),
19015
+ motion: number().int(),
19016
+ object: number().int(),
19017
+ audio: number().int()
19018
+ });
19019
+ /**
19020
+ * One camera's row in a `getEventDensityBatch` answer.
19021
+ *
19022
+ * TWO facts, and the timeline draws them differently:
19023
+ *
19024
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
19025
+ * claim: read, and nothing happened in the window.
19026
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
19027
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
19028
+ * not quiet.
19029
+ *
19030
+ * Fanned out per camera the difference was free — one query rejected while the
19031
+ * others resolved — and collapsing to a batch is the exact moment it is lost
19032
+ * silently. Every requested id gets a row: a caller that asked for twelve and
19033
+ * got eleven cannot tell which one is missing, or that any is.
19034
+ */
19035
+ var EventDensityForDeviceSchema = object({
19036
+ deviceId: number(),
19037
+ read: _enum(["read", "unreadable"]),
19038
+ buckets: array(EventDensityBucketSchema).readonly()
19039
+ });
18982
19040
  object({
18983
19041
  trackId: string(),
18984
19042
  className: string(),
@@ -19289,12 +19347,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19289
19347
  since: number(),
19290
19348
  until: number(),
19291
19349
  bucketMs: number().int().positive()
19292
- }), array(object({
19293
- bucketStart: number(),
19294
- motion: number().int(),
19295
- object: number().int(),
19296
- audio: number().int()
19297
- })).readonly()), method(object({
19350
+ }), array(EventDensityBucketSchema).readonly()), method(object({
19351
+ deviceIds: array(number()).min(1).max(200),
19352
+ since: number(),
19353
+ until: number(),
19354
+ bucketMs: number().int().positive()
19355
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
19298
19356
  deviceId: number(),
19299
19357
  cutoffMs: number()
19300
19358
  }), object({
@@ -26604,6 +26662,39 @@ var RecordingDaysSchema = object({
26604
26662
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
26605
26663
  days: array(number())
26606
26664
  });
26665
+ /**
26666
+ * One camera's row in a `getAvailabilityBatch` answer.
26667
+ *
26668
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
26669
+ * batch collapses the transport, not the work — plus the one thing the singular
26670
+ * method never had to say:
26671
+ *
26672
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
26673
+ * no footage in the window", which is a real claim.
26674
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
26675
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
26676
+ * that emptiness means NOTHING.
26677
+ *
26678
+ * A timeline that renders the second as the first tells an operator there is no
26679
+ * footage when the truth is that nobody looked.
26680
+ */
26681
+ var RecordingAvailabilityForDeviceSchema = object({
26682
+ deviceId: number(),
26683
+ read: _enum(["read", "unreadable"]),
26684
+ ranges: array(RecordingRangeSchema).readonly()
26685
+ });
26686
+ /**
26687
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
26688
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
26689
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
26690
+ * the date-picker's day dots must not spell them the same.
26691
+ */
26692
+ var RecordingDaysForDeviceSchema = object({
26693
+ deviceId: number(),
26694
+ read: _enum(["read", "unreadable"]),
26695
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
26696
+ days: array(number()).readonly()
26697
+ });
26607
26698
  var RecordingManifestSchema = object({
26608
26699
  deviceId: number(),
26609
26700
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -26838,6 +26929,13 @@ method(object({
26838
26929
  }), RecordingAvailabilitySchema, {
26839
26930
  kind: "query",
26840
26931
  auth: "protected"
26932
+ }), method(object({
26933
+ deviceIds: array(number()).min(1).max(200),
26934
+ fromMs: number(),
26935
+ toMs: number()
26936
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
26937
+ kind: "query",
26938
+ auth: "protected"
26841
26939
  }), method(object({
26842
26940
  deviceId: number(),
26843
26941
  fromMs: number(),
@@ -26846,6 +26944,14 @@ method(object({
26846
26944
  }), RecordingDaysSchema, {
26847
26945
  kind: "query",
26848
26946
  auth: "protected"
26947
+ }), method(object({
26948
+ deviceIds: array(number()).min(1).max(200),
26949
+ fromMs: number(),
26950
+ toMs: number(),
26951
+ tzOffsetMinutes: number()
26952
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
26953
+ kind: "query",
26954
+ auth: "protected"
26849
26955
  }), method(object({
26850
26956
  deviceId: number(),
26851
26957
  fromMs: number(),
@@ -30309,6 +30415,12 @@ Object.freeze({
30309
30415
  addonId: null,
30310
30416
  access: "view"
30311
30417
  },
30418
+ "deviceManager.migrateDevice": {
30419
+ capName: "device-manager",
30420
+ capScope: "system",
30421
+ addonId: null,
30422
+ access: "create"
30423
+ },
30312
30424
  "deviceManager.persistConfig": {
30313
30425
  capName: "device-manager",
30314
30426
  capScope: "system",
@@ -32043,6 +32155,12 @@ Object.freeze({
32043
32155
  addonId: null,
32044
32156
  access: "view"
32045
32157
  },
32158
+ "pipelineAnalytics.getEventDensityBatch": {
32159
+ capName: "pipeline-analytics",
32160
+ capScope: "device",
32161
+ addonId: null,
32162
+ access: "view"
32163
+ },
32046
32164
  "pipelineAnalytics.getEventMedia": {
32047
32165
  capName: "pipeline-analytics",
32048
32166
  capScope: "device",
@@ -33165,12 +33283,24 @@ Object.freeze({
33165
33283
  addonId: null,
33166
33284
  access: "view"
33167
33285
  },
33286
+ "recording.getAvailabilityBatch": {
33287
+ capName: "recording",
33288
+ capScope: "system",
33289
+ addonId: null,
33290
+ access: "view"
33291
+ },
33168
33292
  "recording.getDaysWithRecordings": {
33169
33293
  capName: "recording",
33170
33294
  capScope: "system",
33171
33295
  addonId: null,
33172
33296
  access: "view"
33173
33297
  },
33298
+ "recording.getDaysWithRecordingsBatch": {
33299
+ capName: "recording",
33300
+ capScope: "system",
33301
+ addonId: null,
33302
+ access: "view"
33303
+ },
33174
33304
  "recording.getDeviceConfig": {
33175
33305
  capName: "recording",
33176
33306
  capScope: "system",
@@ -35775,6 +35905,11 @@ Object.freeze({
35775
35905
  form: "single",
35776
35906
  optional: false
35777
35907
  }],
35908
+ "pipelineAnalytics.getEventDensityBatch": [{
35909
+ name: "deviceIds",
35910
+ form: "array",
35911
+ optional: false
35912
+ }],
35778
35913
  "pipelineAnalytics.getEventMedia": [{
35779
35914
  name: "deviceId",
35780
35915
  form: "single",
@@ -36190,11 +36325,21 @@ Object.freeze({
36190
36325
  form: "single",
36191
36326
  optional: false
36192
36327
  }],
36328
+ "recording.getAvailabilityBatch": [{
36329
+ name: "deviceIds",
36330
+ form: "array",
36331
+ optional: false
36332
+ }],
36193
36333
  "recording.getDaysWithRecordings": [{
36194
36334
  name: "deviceId",
36195
36335
  form: "single",
36196
36336
  optional: false
36197
36337
  }],
36338
+ "recording.getDaysWithRecordingsBatch": [{
36339
+ name: "deviceIds",
36340
+ form: "array",
36341
+ optional: false
36342
+ }],
36198
36343
  "recording.getDeviceConfig": [{
36199
36344
  name: "deviceId",
36200
36345
  form: "single",
package/dist/index.mjs CHANGED
@@ -7714,7 +7714,8 @@ var OpsLogReasonSchema = _enum([
7714
7714
  "quota",
7715
7715
  "manual",
7716
7716
  "operator",
7717
- "maintenance"
7717
+ "maintenance",
7718
+ "orphaned-device"
7718
7719
  ]);
7719
7720
  /** One audit row, shared verbatim by both domains. */
7720
7721
  var OpsLogEntrySchema = object({
@@ -12875,10 +12876,39 @@ var DevicePersistConfigPayloadSchema = object({
12875
12876
  deviceId: number(),
12876
12877
  data: record(string(), unknown())
12877
12878
  });
12879
+ /** What a migration actually did, per switch. `unreachable` is a first-class
12880
+ * answer: a camera that could not be asked is not a camera that was silenced. */
12881
+ var MigrateSwitchOutcomeSchema = _enum([
12882
+ "off",
12883
+ "on",
12884
+ "not-offered",
12885
+ "unreachable"
12886
+ ]);
12887
+ var MigrateSwitchReportSchema = object({
12888
+ deviceId: number(),
12889
+ switchId: CameraSwitchIdSchema,
12890
+ outcome: MigrateSwitchOutcomeSchema,
12891
+ detail: string().optional()
12892
+ });
12893
+ var MigrateDeviceResultSchema = object({
12894
+ sourceId: number(),
12895
+ targetId: number(),
12896
+ switches: array(MigrateSwitchReportSchema).readonly(),
12897
+ /** Switches that could NOT be turned off on the source. Empty is the only
12898
+ * value meaning the replaced hardware is quiet. */
12899
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
12900
+ swapped: boolean()
12901
+ });
12878
12902
  method(object({
12879
12903
  addonId: string(),
12880
12904
  stableId: string()
12881
- }), object({ id: number() }), { kind: "mutation" }), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), { kind: "mutation" }), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), DeviceMetaSchema.nullable()), method(object({
12905
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
12906
+ sourceId: number(),
12907
+ targetId: number()
12908
+ }), MigrateDeviceResultSchema, {
12909
+ kind: "mutation",
12910
+ auth: "admin"
12911
+ }), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), { kind: "mutation" }), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), DeviceMetaSchema.nullable()), method(object({
12882
12912
  deviceId: number(),
12883
12913
  name: string()
12884
12914
  }), _void(), {
@@ -18975,6 +19005,34 @@ var KeyEventsForDeviceSchema = object({
18975
19005
  deviceId: number(),
18976
19006
  events: array(KeyEventSchema).readonly()
18977
19007
  });
19008
+ /** One non-empty bucket of the timeline histogram. */
19009
+ var EventDensityBucketSchema = object({
19010
+ bucketStart: number(),
19011
+ motion: number().int(),
19012
+ object: number().int(),
19013
+ audio: number().int()
19014
+ });
19015
+ /**
19016
+ * One camera's row in a `getEventDensityBatch` answer.
19017
+ *
19018
+ * TWO facts, and the timeline draws them differently:
19019
+ *
19020
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
19021
+ * claim: read, and nothing happened in the window.
19022
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
19023
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
19024
+ * not quiet.
19025
+ *
19026
+ * Fanned out per camera the difference was free — one query rejected while the
19027
+ * others resolved — and collapsing to a batch is the exact moment it is lost
19028
+ * silently. Every requested id gets a row: a caller that asked for twelve and
19029
+ * got eleven cannot tell which one is missing, or that any is.
19030
+ */
19031
+ var EventDensityForDeviceSchema = object({
19032
+ deviceId: number(),
19033
+ read: _enum(["read", "unreadable"]),
19034
+ buckets: array(EventDensityBucketSchema).readonly()
19035
+ });
18978
19036
  object({
18979
19037
  trackId: string(),
18980
19038
  className: string(),
@@ -19285,12 +19343,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19285
19343
  since: number(),
19286
19344
  until: number(),
19287
19345
  bucketMs: number().int().positive()
19288
- }), array(object({
19289
- bucketStart: number(),
19290
- motion: number().int(),
19291
- object: number().int(),
19292
- audio: number().int()
19293
- })).readonly()), method(object({
19346
+ }), array(EventDensityBucketSchema).readonly()), method(object({
19347
+ deviceIds: array(number()).min(1).max(200),
19348
+ since: number(),
19349
+ until: number(),
19350
+ bucketMs: number().int().positive()
19351
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
19294
19352
  deviceId: number(),
19295
19353
  cutoffMs: number()
19296
19354
  }), object({
@@ -26600,6 +26658,39 @@ var RecordingDaysSchema = object({
26600
26658
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
26601
26659
  days: array(number())
26602
26660
  });
26661
+ /**
26662
+ * One camera's row in a `getAvailabilityBatch` answer.
26663
+ *
26664
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
26665
+ * batch collapses the transport, not the work — plus the one thing the singular
26666
+ * method never had to say:
26667
+ *
26668
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
26669
+ * no footage in the window", which is a real claim.
26670
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
26671
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
26672
+ * that emptiness means NOTHING.
26673
+ *
26674
+ * A timeline that renders the second as the first tells an operator there is no
26675
+ * footage when the truth is that nobody looked.
26676
+ */
26677
+ var RecordingAvailabilityForDeviceSchema = object({
26678
+ deviceId: number(),
26679
+ read: _enum(["read", "unreadable"]),
26680
+ ranges: array(RecordingRangeSchema).readonly()
26681
+ });
26682
+ /**
26683
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
26684
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
26685
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
26686
+ * the date-picker's day dots must not spell them the same.
26687
+ */
26688
+ var RecordingDaysForDeviceSchema = object({
26689
+ deviceId: number(),
26690
+ read: _enum(["read", "unreadable"]),
26691
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
26692
+ days: array(number()).readonly()
26693
+ });
26603
26694
  var RecordingManifestSchema = object({
26604
26695
  deviceId: number(),
26605
26696
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -26834,6 +26925,13 @@ method(object({
26834
26925
  }), RecordingAvailabilitySchema, {
26835
26926
  kind: "query",
26836
26927
  auth: "protected"
26928
+ }), method(object({
26929
+ deviceIds: array(number()).min(1).max(200),
26930
+ fromMs: number(),
26931
+ toMs: number()
26932
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
26933
+ kind: "query",
26934
+ auth: "protected"
26837
26935
  }), method(object({
26838
26936
  deviceId: number(),
26839
26937
  fromMs: number(),
@@ -26842,6 +26940,14 @@ method(object({
26842
26940
  }), RecordingDaysSchema, {
26843
26941
  kind: "query",
26844
26942
  auth: "protected"
26943
+ }), method(object({
26944
+ deviceIds: array(number()).min(1).max(200),
26945
+ fromMs: number(),
26946
+ toMs: number(),
26947
+ tzOffsetMinutes: number()
26948
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
26949
+ kind: "query",
26950
+ auth: "protected"
26845
26951
  }), method(object({
26846
26952
  deviceId: number(),
26847
26953
  fromMs: number(),
@@ -30305,6 +30411,12 @@ Object.freeze({
30305
30411
  addonId: null,
30306
30412
  access: "view"
30307
30413
  },
30414
+ "deviceManager.migrateDevice": {
30415
+ capName: "device-manager",
30416
+ capScope: "system",
30417
+ addonId: null,
30418
+ access: "create"
30419
+ },
30308
30420
  "deviceManager.persistConfig": {
30309
30421
  capName: "device-manager",
30310
30422
  capScope: "system",
@@ -32039,6 +32151,12 @@ Object.freeze({
32039
32151
  addonId: null,
32040
32152
  access: "view"
32041
32153
  },
32154
+ "pipelineAnalytics.getEventDensityBatch": {
32155
+ capName: "pipeline-analytics",
32156
+ capScope: "device",
32157
+ addonId: null,
32158
+ access: "view"
32159
+ },
32042
32160
  "pipelineAnalytics.getEventMedia": {
32043
32161
  capName: "pipeline-analytics",
32044
32162
  capScope: "device",
@@ -33161,12 +33279,24 @@ Object.freeze({
33161
33279
  addonId: null,
33162
33280
  access: "view"
33163
33281
  },
33282
+ "recording.getAvailabilityBatch": {
33283
+ capName: "recording",
33284
+ capScope: "system",
33285
+ addonId: null,
33286
+ access: "view"
33287
+ },
33164
33288
  "recording.getDaysWithRecordings": {
33165
33289
  capName: "recording",
33166
33290
  capScope: "system",
33167
33291
  addonId: null,
33168
33292
  access: "view"
33169
33293
  },
33294
+ "recording.getDaysWithRecordingsBatch": {
33295
+ capName: "recording",
33296
+ capScope: "system",
33297
+ addonId: null,
33298
+ access: "view"
33299
+ },
33170
33300
  "recording.getDeviceConfig": {
33171
33301
  capName: "recording",
33172
33302
  capScope: "system",
@@ -35771,6 +35901,11 @@ Object.freeze({
35771
35901
  form: "single",
35772
35902
  optional: false
35773
35903
  }],
35904
+ "pipelineAnalytics.getEventDensityBatch": [{
35905
+ name: "deviceIds",
35906
+ form: "array",
35907
+ optional: false
35908
+ }],
35774
35909
  "pipelineAnalytics.getEventMedia": [{
35775
35910
  name: "deviceId",
35776
35911
  form: "single",
@@ -36186,11 +36321,21 @@ Object.freeze({
36186
36321
  form: "single",
36187
36322
  optional: false
36188
36323
  }],
36324
+ "recording.getAvailabilityBatch": [{
36325
+ name: "deviceIds",
36326
+ form: "array",
36327
+ optional: false
36328
+ }],
36189
36329
  "recording.getDaysWithRecordings": [{
36190
36330
  name: "deviceId",
36191
36331
  form: "single",
36192
36332
  optional: false
36193
36333
  }],
36334
+ "recording.getDaysWithRecordingsBatch": [{
36335
+ name: "deviceIds",
36336
+ form: "array",
36337
+ optional: false
36338
+ }],
36194
36339
  "recording.getDeviceConfig": [{
36195
36340
  name: "deviceId",
36196
36341
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-decoder-nodeav",
3
- "version": "1.2.51",
3
+ "version": "1.2.53",
4
4
  "description": "Standalone in-process node-av decoder addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",