@camstack/addon-provider-reolink 1.2.74 → 1.2.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -8008,7 +8008,8 @@ var OpsLogReasonSchema = _enum([
8008
8008
  "quota",
8009
8009
  "manual",
8010
8010
  "operator",
8011
- "maintenance"
8011
+ "maintenance",
8012
+ "orphaned-device"
8012
8013
  ]);
8013
8014
  /** One audit row, shared verbatim by both domains. */
8014
8015
  var OpsLogEntrySchema = object({
@@ -13596,10 +13597,39 @@ var DevicePersistConfigPayloadSchema = object({
13596
13597
  deviceId: number(),
13597
13598
  data: record(string(), unknown())
13598
13599
  });
13600
+ /** What a migration actually did, per switch. `unreachable` is a first-class
13601
+ * answer: a camera that could not be asked is not a camera that was silenced. */
13602
+ var MigrateSwitchOutcomeSchema = _enum([
13603
+ "off",
13604
+ "on",
13605
+ "not-offered",
13606
+ "unreachable"
13607
+ ]);
13608
+ var MigrateSwitchReportSchema = object({
13609
+ deviceId: number(),
13610
+ switchId: CameraSwitchIdSchema,
13611
+ outcome: MigrateSwitchOutcomeSchema,
13612
+ detail: string().optional()
13613
+ });
13614
+ var MigrateDeviceResultSchema = object({
13615
+ sourceId: number(),
13616
+ targetId: number(),
13617
+ switches: array(MigrateSwitchReportSchema).readonly(),
13618
+ /** Switches that could NOT be turned off on the source. Empty is the only
13619
+ * value meaning the replaced hardware is quiet. */
13620
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
13621
+ swapped: boolean()
13622
+ });
13599
13623
  method(object({
13600
13624
  addonId: string(),
13601
13625
  stableId: string()
13602
- }), 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({
13626
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
13627
+ sourceId: number(),
13628
+ targetId: number()
13629
+ }), MigrateDeviceResultSchema, {
13630
+ kind: "mutation",
13631
+ auth: "admin"
13632
+ }), 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({
13603
13633
  deviceId: number(),
13604
13634
  name: string()
13605
13635
  }), _void(), {
@@ -19814,6 +19844,34 @@ var KeyEventsForDeviceSchema = object({
19814
19844
  deviceId: number(),
19815
19845
  events: array(KeyEventSchema).readonly()
19816
19846
  });
19847
+ /** One non-empty bucket of the timeline histogram. */
19848
+ var EventDensityBucketSchema = object({
19849
+ bucketStart: number(),
19850
+ motion: number().int(),
19851
+ object: number().int(),
19852
+ audio: number().int()
19853
+ });
19854
+ /**
19855
+ * One camera's row in a `getEventDensityBatch` answer.
19856
+ *
19857
+ * TWO facts, and the timeline draws them differently:
19858
+ *
19859
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
19860
+ * claim: read, and nothing happened in the window.
19861
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
19862
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
19863
+ * not quiet.
19864
+ *
19865
+ * Fanned out per camera the difference was free — one query rejected while the
19866
+ * others resolved — and collapsing to a batch is the exact moment it is lost
19867
+ * silently. Every requested id gets a row: a caller that asked for twelve and
19868
+ * got eleven cannot tell which one is missing, or that any is.
19869
+ */
19870
+ var EventDensityForDeviceSchema = object({
19871
+ deviceId: number(),
19872
+ read: _enum(["read", "unreadable"]),
19873
+ buckets: array(EventDensityBucketSchema).readonly()
19874
+ });
19817
19875
  object({
19818
19876
  trackId: string(),
19819
19877
  className: string(),
@@ -20124,12 +20182,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20124
20182
  since: number(),
20125
20183
  until: number(),
20126
20184
  bucketMs: number().int().positive()
20127
- }), array(object({
20128
- bucketStart: number(),
20129
- motion: number().int(),
20130
- object: number().int(),
20131
- audio: number().int()
20132
- })).readonly()), method(object({
20185
+ }), array(EventDensityBucketSchema).readonly()), method(object({
20186
+ deviceIds: array(number()).min(1).max(200),
20187
+ since: number(),
20188
+ until: number(),
20189
+ bucketMs: number().int().positive()
20190
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
20133
20191
  deviceId: number(),
20134
20192
  cutoffMs: number()
20135
20193
  }), object({
@@ -29345,6 +29403,39 @@ var RecordingDaysSchema = object({
29345
29403
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
29346
29404
  days: array(number())
29347
29405
  });
29406
+ /**
29407
+ * One camera's row in a `getAvailabilityBatch` answer.
29408
+ *
29409
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
29410
+ * batch collapses the transport, not the work — plus the one thing the singular
29411
+ * method never had to say:
29412
+ *
29413
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
29414
+ * no footage in the window", which is a real claim.
29415
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
29416
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
29417
+ * that emptiness means NOTHING.
29418
+ *
29419
+ * A timeline that renders the second as the first tells an operator there is no
29420
+ * footage when the truth is that nobody looked.
29421
+ */
29422
+ var RecordingAvailabilityForDeviceSchema = object({
29423
+ deviceId: number(),
29424
+ read: _enum(["read", "unreadable"]),
29425
+ ranges: array(RecordingRangeSchema).readonly()
29426
+ });
29427
+ /**
29428
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
29429
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
29430
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
29431
+ * the date-picker's day dots must not spell them the same.
29432
+ */
29433
+ var RecordingDaysForDeviceSchema = object({
29434
+ deviceId: number(),
29435
+ read: _enum(["read", "unreadable"]),
29436
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
29437
+ days: array(number()).readonly()
29438
+ });
29348
29439
  var RecordingManifestSchema = object({
29349
29440
  deviceId: number(),
29350
29441
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -29579,6 +29670,13 @@ method(object({
29579
29670
  }), RecordingAvailabilitySchema, {
29580
29671
  kind: "query",
29581
29672
  auth: "protected"
29673
+ }), method(object({
29674
+ deviceIds: array(number()).min(1).max(200),
29675
+ fromMs: number(),
29676
+ toMs: number()
29677
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
29678
+ kind: "query",
29679
+ auth: "protected"
29582
29680
  }), method(object({
29583
29681
  deviceId: number(),
29584
29682
  fromMs: number(),
@@ -29587,6 +29685,14 @@ method(object({
29587
29685
  }), RecordingDaysSchema, {
29588
29686
  kind: "query",
29589
29687
  auth: "protected"
29688
+ }), method(object({
29689
+ deviceIds: array(number()).min(1).max(200),
29690
+ fromMs: number(),
29691
+ toMs: number(),
29692
+ tzOffsetMinutes: number()
29693
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
29694
+ kind: "query",
29695
+ auth: "protected"
29590
29696
  }), method(object({
29591
29697
  deviceId: number(),
29592
29698
  fromMs: number(),
@@ -35114,6 +35220,12 @@ Object.freeze({
35114
35220
  addonId: null,
35115
35221
  access: "view"
35116
35222
  },
35223
+ "deviceManager.migrateDevice": {
35224
+ capName: "device-manager",
35225
+ capScope: "system",
35226
+ addonId: null,
35227
+ access: "create"
35228
+ },
35117
35229
  "deviceManager.persistConfig": {
35118
35230
  capName: "device-manager",
35119
35231
  capScope: "system",
@@ -36848,6 +36960,12 @@ Object.freeze({
36848
36960
  addonId: null,
36849
36961
  access: "view"
36850
36962
  },
36963
+ "pipelineAnalytics.getEventDensityBatch": {
36964
+ capName: "pipeline-analytics",
36965
+ capScope: "device",
36966
+ addonId: null,
36967
+ access: "view"
36968
+ },
36851
36969
  "pipelineAnalytics.getEventMedia": {
36852
36970
  capName: "pipeline-analytics",
36853
36971
  capScope: "device",
@@ -37970,12 +38088,24 @@ Object.freeze({
37970
38088
  addonId: null,
37971
38089
  access: "view"
37972
38090
  },
38091
+ "recording.getAvailabilityBatch": {
38092
+ capName: "recording",
38093
+ capScope: "system",
38094
+ addonId: null,
38095
+ access: "view"
38096
+ },
37973
38097
  "recording.getDaysWithRecordings": {
37974
38098
  capName: "recording",
37975
38099
  capScope: "system",
37976
38100
  addonId: null,
37977
38101
  access: "view"
37978
38102
  },
38103
+ "recording.getDaysWithRecordingsBatch": {
38104
+ capName: "recording",
38105
+ capScope: "system",
38106
+ addonId: null,
38107
+ access: "view"
38108
+ },
37979
38109
  "recording.getDeviceConfig": {
37980
38110
  capName: "recording",
37981
38111
  capScope: "system",
@@ -40580,6 +40710,11 @@ Object.freeze({
40580
40710
  form: "single",
40581
40711
  optional: false
40582
40712
  }],
40713
+ "pipelineAnalytics.getEventDensityBatch": [{
40714
+ name: "deviceIds",
40715
+ form: "array",
40716
+ optional: false
40717
+ }],
40583
40718
  "pipelineAnalytics.getEventMedia": [{
40584
40719
  name: "deviceId",
40585
40720
  form: "single",
@@ -40995,11 +41130,21 @@ Object.freeze({
40995
41130
  form: "single",
40996
41131
  optional: false
40997
41132
  }],
41133
+ "recording.getAvailabilityBatch": [{
41134
+ name: "deviceIds",
41135
+ form: "array",
41136
+ optional: false
41137
+ }],
40998
41138
  "recording.getDaysWithRecordings": [{
40999
41139
  name: "deviceId",
41000
41140
  form: "single",
41001
41141
  optional: false
41002
41142
  }],
41143
+ "recording.getDaysWithRecordingsBatch": [{
41144
+ name: "deviceIds",
41145
+ form: "array",
41146
+ optional: false
41147
+ }],
41003
41148
  "recording.getDeviceConfig": [{
41004
41149
  name: "deviceId",
41005
41150
  form: "single",
package/dist/addon.mjs CHANGED
@@ -8003,7 +8003,8 @@ var OpsLogReasonSchema = _enum([
8003
8003
  "quota",
8004
8004
  "manual",
8005
8005
  "operator",
8006
- "maintenance"
8006
+ "maintenance",
8007
+ "orphaned-device"
8007
8008
  ]);
8008
8009
  /** One audit row, shared verbatim by both domains. */
8009
8010
  var OpsLogEntrySchema = object({
@@ -13591,10 +13592,39 @@ var DevicePersistConfigPayloadSchema = object({
13591
13592
  deviceId: number(),
13592
13593
  data: record(string(), unknown())
13593
13594
  });
13595
+ /** What a migration actually did, per switch. `unreachable` is a first-class
13596
+ * answer: a camera that could not be asked is not a camera that was silenced. */
13597
+ var MigrateSwitchOutcomeSchema = _enum([
13598
+ "off",
13599
+ "on",
13600
+ "not-offered",
13601
+ "unreachable"
13602
+ ]);
13603
+ var MigrateSwitchReportSchema = object({
13604
+ deviceId: number(),
13605
+ switchId: CameraSwitchIdSchema,
13606
+ outcome: MigrateSwitchOutcomeSchema,
13607
+ detail: string().optional()
13608
+ });
13609
+ var MigrateDeviceResultSchema = object({
13610
+ sourceId: number(),
13611
+ targetId: number(),
13612
+ switches: array(MigrateSwitchReportSchema).readonly(),
13613
+ /** Switches that could NOT be turned off on the source. Empty is the only
13614
+ * value meaning the replaced hardware is quiet. */
13615
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
13616
+ swapped: boolean()
13617
+ });
13594
13618
  method(object({
13595
13619
  addonId: string(),
13596
13620
  stableId: string()
13597
- }), 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({
13621
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
13622
+ sourceId: number(),
13623
+ targetId: number()
13624
+ }), MigrateDeviceResultSchema, {
13625
+ kind: "mutation",
13626
+ auth: "admin"
13627
+ }), 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({
13598
13628
  deviceId: number(),
13599
13629
  name: string()
13600
13630
  }), _void(), {
@@ -19809,6 +19839,34 @@ var KeyEventsForDeviceSchema = object({
19809
19839
  deviceId: number(),
19810
19840
  events: array(KeyEventSchema).readonly()
19811
19841
  });
19842
+ /** One non-empty bucket of the timeline histogram. */
19843
+ var EventDensityBucketSchema = object({
19844
+ bucketStart: number(),
19845
+ motion: number().int(),
19846
+ object: number().int(),
19847
+ audio: number().int()
19848
+ });
19849
+ /**
19850
+ * One camera's row in a `getEventDensityBatch` answer.
19851
+ *
19852
+ * TWO facts, and the timeline draws them differently:
19853
+ *
19854
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
19855
+ * claim: read, and nothing happened in the window.
19856
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
19857
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
19858
+ * not quiet.
19859
+ *
19860
+ * Fanned out per camera the difference was free — one query rejected while the
19861
+ * others resolved — and collapsing to a batch is the exact moment it is lost
19862
+ * silently. Every requested id gets a row: a caller that asked for twelve and
19863
+ * got eleven cannot tell which one is missing, or that any is.
19864
+ */
19865
+ var EventDensityForDeviceSchema = object({
19866
+ deviceId: number(),
19867
+ read: _enum(["read", "unreadable"]),
19868
+ buckets: array(EventDensityBucketSchema).readonly()
19869
+ });
19812
19870
  object({
19813
19871
  trackId: string(),
19814
19872
  className: string(),
@@ -20119,12 +20177,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20119
20177
  since: number(),
20120
20178
  until: number(),
20121
20179
  bucketMs: number().int().positive()
20122
- }), array(object({
20123
- bucketStart: number(),
20124
- motion: number().int(),
20125
- object: number().int(),
20126
- audio: number().int()
20127
- })).readonly()), method(object({
20180
+ }), array(EventDensityBucketSchema).readonly()), method(object({
20181
+ deviceIds: array(number()).min(1).max(200),
20182
+ since: number(),
20183
+ until: number(),
20184
+ bucketMs: number().int().positive()
20185
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
20128
20186
  deviceId: number(),
20129
20187
  cutoffMs: number()
20130
20188
  }), object({
@@ -29340,6 +29398,39 @@ var RecordingDaysSchema = object({
29340
29398
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
29341
29399
  days: array(number())
29342
29400
  });
29401
+ /**
29402
+ * One camera's row in a `getAvailabilityBatch` answer.
29403
+ *
29404
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
29405
+ * batch collapses the transport, not the work — plus the one thing the singular
29406
+ * method never had to say:
29407
+ *
29408
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
29409
+ * no footage in the window", which is a real claim.
29410
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
29411
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
29412
+ * that emptiness means NOTHING.
29413
+ *
29414
+ * A timeline that renders the second as the first tells an operator there is no
29415
+ * footage when the truth is that nobody looked.
29416
+ */
29417
+ var RecordingAvailabilityForDeviceSchema = object({
29418
+ deviceId: number(),
29419
+ read: _enum(["read", "unreadable"]),
29420
+ ranges: array(RecordingRangeSchema).readonly()
29421
+ });
29422
+ /**
29423
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
29424
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
29425
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
29426
+ * the date-picker's day dots must not spell them the same.
29427
+ */
29428
+ var RecordingDaysForDeviceSchema = object({
29429
+ deviceId: number(),
29430
+ read: _enum(["read", "unreadable"]),
29431
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
29432
+ days: array(number()).readonly()
29433
+ });
29343
29434
  var RecordingManifestSchema = object({
29344
29435
  deviceId: number(),
29345
29436
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -29574,6 +29665,13 @@ method(object({
29574
29665
  }), RecordingAvailabilitySchema, {
29575
29666
  kind: "query",
29576
29667
  auth: "protected"
29668
+ }), method(object({
29669
+ deviceIds: array(number()).min(1).max(200),
29670
+ fromMs: number(),
29671
+ toMs: number()
29672
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
29673
+ kind: "query",
29674
+ auth: "protected"
29577
29675
  }), method(object({
29578
29676
  deviceId: number(),
29579
29677
  fromMs: number(),
@@ -29582,6 +29680,14 @@ method(object({
29582
29680
  }), RecordingDaysSchema, {
29583
29681
  kind: "query",
29584
29682
  auth: "protected"
29683
+ }), method(object({
29684
+ deviceIds: array(number()).min(1).max(200),
29685
+ fromMs: number(),
29686
+ toMs: number(),
29687
+ tzOffsetMinutes: number()
29688
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
29689
+ kind: "query",
29690
+ auth: "protected"
29585
29691
  }), method(object({
29586
29692
  deviceId: number(),
29587
29693
  fromMs: number(),
@@ -35109,6 +35215,12 @@ Object.freeze({
35109
35215
  addonId: null,
35110
35216
  access: "view"
35111
35217
  },
35218
+ "deviceManager.migrateDevice": {
35219
+ capName: "device-manager",
35220
+ capScope: "system",
35221
+ addonId: null,
35222
+ access: "create"
35223
+ },
35112
35224
  "deviceManager.persistConfig": {
35113
35225
  capName: "device-manager",
35114
35226
  capScope: "system",
@@ -36843,6 +36955,12 @@ Object.freeze({
36843
36955
  addonId: null,
36844
36956
  access: "view"
36845
36957
  },
36958
+ "pipelineAnalytics.getEventDensityBatch": {
36959
+ capName: "pipeline-analytics",
36960
+ capScope: "device",
36961
+ addonId: null,
36962
+ access: "view"
36963
+ },
36846
36964
  "pipelineAnalytics.getEventMedia": {
36847
36965
  capName: "pipeline-analytics",
36848
36966
  capScope: "device",
@@ -37965,12 +38083,24 @@ Object.freeze({
37965
38083
  addonId: null,
37966
38084
  access: "view"
37967
38085
  },
38086
+ "recording.getAvailabilityBatch": {
38087
+ capName: "recording",
38088
+ capScope: "system",
38089
+ addonId: null,
38090
+ access: "view"
38091
+ },
37968
38092
  "recording.getDaysWithRecordings": {
37969
38093
  capName: "recording",
37970
38094
  capScope: "system",
37971
38095
  addonId: null,
37972
38096
  access: "view"
37973
38097
  },
38098
+ "recording.getDaysWithRecordingsBatch": {
38099
+ capName: "recording",
38100
+ capScope: "system",
38101
+ addonId: null,
38102
+ access: "view"
38103
+ },
37974
38104
  "recording.getDeviceConfig": {
37975
38105
  capName: "recording",
37976
38106
  capScope: "system",
@@ -40575,6 +40705,11 @@ Object.freeze({
40575
40705
  form: "single",
40576
40706
  optional: false
40577
40707
  }],
40708
+ "pipelineAnalytics.getEventDensityBatch": [{
40709
+ name: "deviceIds",
40710
+ form: "array",
40711
+ optional: false
40712
+ }],
40578
40713
  "pipelineAnalytics.getEventMedia": [{
40579
40714
  name: "deviceId",
40580
40715
  form: "single",
@@ -40990,11 +41125,21 @@ Object.freeze({
40990
41125
  form: "single",
40991
41126
  optional: false
40992
41127
  }],
41128
+ "recording.getAvailabilityBatch": [{
41129
+ name: "deviceIds",
41130
+ form: "array",
41131
+ optional: false
41132
+ }],
40993
41133
  "recording.getDaysWithRecordings": [{
40994
41134
  name: "deviceId",
40995
41135
  form: "single",
40996
41136
  optional: false
40997
41137
  }],
41138
+ "recording.getDaysWithRecordingsBatch": [{
41139
+ name: "deviceIds",
41140
+ form: "array",
41141
+ optional: false
41142
+ }],
40998
41143
  "recording.getDeviceConfig": [{
40999
41144
  name: "deviceId",
41000
41145
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-reolink",
3
- "version": "1.2.74",
3
+ "version": "1.2.76",
4
4
  "description": "Reolink camera device provider addon for CamStack — native Baichuan protocol",
5
5
  "keywords": [
6
6
  "camstack",