@camstack/addon-mqtt-broker 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.
@@ -7747,7 +7747,8 @@ var OpsLogReasonSchema = _enum([
7747
7747
  "quota",
7748
7748
  "manual",
7749
7749
  "operator",
7750
- "maintenance"
7750
+ "maintenance",
7751
+ "orphaned-device"
7751
7752
  ]);
7752
7753
  /** One audit row, shared verbatim by both domains. */
7753
7754
  var OpsLogEntrySchema = object({
@@ -12827,10 +12828,39 @@ var DevicePersistConfigPayloadSchema = object({
12827
12828
  deviceId: number(),
12828
12829
  data: record(string(), unknown())
12829
12830
  });
12831
+ /** What a migration actually did, per switch. `unreachable` is a first-class
12832
+ * answer: a camera that could not be asked is not a camera that was silenced. */
12833
+ var MigrateSwitchOutcomeSchema = _enum([
12834
+ "off",
12835
+ "on",
12836
+ "not-offered",
12837
+ "unreachable"
12838
+ ]);
12839
+ var MigrateSwitchReportSchema = object({
12840
+ deviceId: number(),
12841
+ switchId: CameraSwitchIdSchema,
12842
+ outcome: MigrateSwitchOutcomeSchema,
12843
+ detail: string().optional()
12844
+ });
12845
+ var MigrateDeviceResultSchema = object({
12846
+ sourceId: number(),
12847
+ targetId: number(),
12848
+ switches: array(MigrateSwitchReportSchema).readonly(),
12849
+ /** Switches that could NOT be turned off on the source. Empty is the only
12850
+ * value meaning the replaced hardware is quiet. */
12851
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
12852
+ swapped: boolean()
12853
+ });
12830
12854
  method(object({
12831
12855
  addonId: string(),
12832
12856
  stableId: string()
12833
- }), 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({
12857
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
12858
+ sourceId: number(),
12859
+ targetId: number()
12860
+ }), MigrateDeviceResultSchema, {
12861
+ kind: "mutation",
12862
+ auth: "admin"
12863
+ }), 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({
12834
12864
  deviceId: number(),
12835
12865
  name: string()
12836
12866
  }), _void(), {
@@ -18946,6 +18976,34 @@ var KeyEventsForDeviceSchema = object({
18946
18976
  deviceId: number(),
18947
18977
  events: array(KeyEventSchema).readonly()
18948
18978
  });
18979
+ /** One non-empty bucket of the timeline histogram. */
18980
+ var EventDensityBucketSchema = object({
18981
+ bucketStart: number(),
18982
+ motion: number().int(),
18983
+ object: number().int(),
18984
+ audio: number().int()
18985
+ });
18986
+ /**
18987
+ * One camera's row in a `getEventDensityBatch` answer.
18988
+ *
18989
+ * TWO facts, and the timeline draws them differently:
18990
+ *
18991
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
18992
+ * claim: read, and nothing happened in the window.
18993
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
18994
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
18995
+ * not quiet.
18996
+ *
18997
+ * Fanned out per camera the difference was free — one query rejected while the
18998
+ * others resolved — and collapsing to a batch is the exact moment it is lost
18999
+ * silently. Every requested id gets a row: a caller that asked for twelve and
19000
+ * got eleven cannot tell which one is missing, or that any is.
19001
+ */
19002
+ var EventDensityForDeviceSchema = object({
19003
+ deviceId: number(),
19004
+ read: _enum(["read", "unreadable"]),
19005
+ buckets: array(EventDensityBucketSchema).readonly()
19006
+ });
18949
19007
  object({
18950
19008
  trackId: string(),
18951
19009
  className: string(),
@@ -19256,12 +19314,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19256
19314
  since: number(),
19257
19315
  until: number(),
19258
19316
  bucketMs: number().int().positive()
19259
- }), array(object({
19260
- bucketStart: number(),
19261
- motion: number().int(),
19262
- object: number().int(),
19263
- audio: number().int()
19264
- })).readonly()), method(object({
19317
+ }), array(EventDensityBucketSchema).readonly()), method(object({
19318
+ deviceIds: array(number()).min(1).max(200),
19319
+ since: number(),
19320
+ until: number(),
19321
+ bucketMs: number().int().positive()
19322
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
19265
19323
  deviceId: number(),
19266
19324
  cutoffMs: number()
19267
19325
  }), object({
@@ -26571,6 +26629,39 @@ var RecordingDaysSchema = object({
26571
26629
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
26572
26630
  days: array(number())
26573
26631
  });
26632
+ /**
26633
+ * One camera's row in a `getAvailabilityBatch` answer.
26634
+ *
26635
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
26636
+ * batch collapses the transport, not the work — plus the one thing the singular
26637
+ * method never had to say:
26638
+ *
26639
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
26640
+ * no footage in the window", which is a real claim.
26641
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
26642
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
26643
+ * that emptiness means NOTHING.
26644
+ *
26645
+ * A timeline that renders the second as the first tells an operator there is no
26646
+ * footage when the truth is that nobody looked.
26647
+ */
26648
+ var RecordingAvailabilityForDeviceSchema = object({
26649
+ deviceId: number(),
26650
+ read: _enum(["read", "unreadable"]),
26651
+ ranges: array(RecordingRangeSchema).readonly()
26652
+ });
26653
+ /**
26654
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
26655
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
26656
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
26657
+ * the date-picker's day dots must not spell them the same.
26658
+ */
26659
+ var RecordingDaysForDeviceSchema = object({
26660
+ deviceId: number(),
26661
+ read: _enum(["read", "unreadable"]),
26662
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
26663
+ days: array(number()).readonly()
26664
+ });
26574
26665
  var RecordingManifestSchema = object({
26575
26666
  deviceId: number(),
26576
26667
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -26805,6 +26896,13 @@ method(object({
26805
26896
  }), RecordingAvailabilitySchema, {
26806
26897
  kind: "query",
26807
26898
  auth: "protected"
26899
+ }), method(object({
26900
+ deviceIds: array(number()).min(1).max(200),
26901
+ fromMs: number(),
26902
+ toMs: number()
26903
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
26904
+ kind: "query",
26905
+ auth: "protected"
26808
26906
  }), method(object({
26809
26907
  deviceId: number(),
26810
26908
  fromMs: number(),
@@ -26813,6 +26911,14 @@ method(object({
26813
26911
  }), RecordingDaysSchema, {
26814
26912
  kind: "query",
26815
26913
  auth: "protected"
26914
+ }), method(object({
26915
+ deviceIds: array(number()).min(1).max(200),
26916
+ fromMs: number(),
26917
+ toMs: number(),
26918
+ tzOffsetMinutes: number()
26919
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
26920
+ kind: "query",
26921
+ auth: "protected"
26816
26922
  }), method(object({
26817
26923
  deviceId: number(),
26818
26924
  fromMs: number(),
@@ -30276,6 +30382,12 @@ Object.freeze({
30276
30382
  addonId: null,
30277
30383
  access: "view"
30278
30384
  },
30385
+ "deviceManager.migrateDevice": {
30386
+ capName: "device-manager",
30387
+ capScope: "system",
30388
+ addonId: null,
30389
+ access: "create"
30390
+ },
30279
30391
  "deviceManager.persistConfig": {
30280
30392
  capName: "device-manager",
30281
30393
  capScope: "system",
@@ -32010,6 +32122,12 @@ Object.freeze({
32010
32122
  addonId: null,
32011
32123
  access: "view"
32012
32124
  },
32125
+ "pipelineAnalytics.getEventDensityBatch": {
32126
+ capName: "pipeline-analytics",
32127
+ capScope: "device",
32128
+ addonId: null,
32129
+ access: "view"
32130
+ },
32013
32131
  "pipelineAnalytics.getEventMedia": {
32014
32132
  capName: "pipeline-analytics",
32015
32133
  capScope: "device",
@@ -33132,12 +33250,24 @@ Object.freeze({
33132
33250
  addonId: null,
33133
33251
  access: "view"
33134
33252
  },
33253
+ "recording.getAvailabilityBatch": {
33254
+ capName: "recording",
33255
+ capScope: "system",
33256
+ addonId: null,
33257
+ access: "view"
33258
+ },
33135
33259
  "recording.getDaysWithRecordings": {
33136
33260
  capName: "recording",
33137
33261
  capScope: "system",
33138
33262
  addonId: null,
33139
33263
  access: "view"
33140
33264
  },
33265
+ "recording.getDaysWithRecordingsBatch": {
33266
+ capName: "recording",
33267
+ capScope: "system",
33268
+ addonId: null,
33269
+ access: "view"
33270
+ },
33141
33271
  "recording.getDeviceConfig": {
33142
33272
  capName: "recording",
33143
33273
  capScope: "system",
@@ -35742,6 +35872,11 @@ Object.freeze({
35742
35872
  form: "single",
35743
35873
  optional: false
35744
35874
  }],
35875
+ "pipelineAnalytics.getEventDensityBatch": [{
35876
+ name: "deviceIds",
35877
+ form: "array",
35878
+ optional: false
35879
+ }],
35745
35880
  "pipelineAnalytics.getEventMedia": [{
35746
35881
  name: "deviceId",
35747
35882
  form: "single",
@@ -36157,11 +36292,21 @@ Object.freeze({
36157
36292
  form: "single",
36158
36293
  optional: false
36159
36294
  }],
36295
+ "recording.getAvailabilityBatch": [{
36296
+ name: "deviceIds",
36297
+ form: "array",
36298
+ optional: false
36299
+ }],
36160
36300
  "recording.getDaysWithRecordings": [{
36161
36301
  name: "deviceId",
36162
36302
  form: "single",
36163
36303
  optional: false
36164
36304
  }],
36305
+ "recording.getDaysWithRecordingsBatch": [{
36306
+ name: "deviceIds",
36307
+ form: "array",
36308
+ optional: false
36309
+ }],
36165
36310
  "recording.getDeviceConfig": [{
36166
36311
  name: "deviceId",
36167
36312
  form: "single",
@@ -7742,7 +7742,8 @@ var OpsLogReasonSchema = _enum([
7742
7742
  "quota",
7743
7743
  "manual",
7744
7744
  "operator",
7745
- "maintenance"
7745
+ "maintenance",
7746
+ "orphaned-device"
7746
7747
  ]);
7747
7748
  /** One audit row, shared verbatim by both domains. */
7748
7749
  var OpsLogEntrySchema = object({
@@ -12822,10 +12823,39 @@ var DevicePersistConfigPayloadSchema = object({
12822
12823
  deviceId: number(),
12823
12824
  data: record(string(), unknown())
12824
12825
  });
12826
+ /** What a migration actually did, per switch. `unreachable` is a first-class
12827
+ * answer: a camera that could not be asked is not a camera that was silenced. */
12828
+ var MigrateSwitchOutcomeSchema = _enum([
12829
+ "off",
12830
+ "on",
12831
+ "not-offered",
12832
+ "unreachable"
12833
+ ]);
12834
+ var MigrateSwitchReportSchema = object({
12835
+ deviceId: number(),
12836
+ switchId: CameraSwitchIdSchema,
12837
+ outcome: MigrateSwitchOutcomeSchema,
12838
+ detail: string().optional()
12839
+ });
12840
+ var MigrateDeviceResultSchema = object({
12841
+ sourceId: number(),
12842
+ targetId: number(),
12843
+ switches: array(MigrateSwitchReportSchema).readonly(),
12844
+ /** Switches that could NOT be turned off on the source. Empty is the only
12845
+ * value meaning the replaced hardware is quiet. */
12846
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
12847
+ swapped: boolean()
12848
+ });
12825
12849
  method(object({
12826
12850
  addonId: string(),
12827
12851
  stableId: string()
12828
- }), 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({
12852
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
12853
+ sourceId: number(),
12854
+ targetId: number()
12855
+ }), MigrateDeviceResultSchema, {
12856
+ kind: "mutation",
12857
+ auth: "admin"
12858
+ }), 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({
12829
12859
  deviceId: number(),
12830
12860
  name: string()
12831
12861
  }), _void(), {
@@ -18941,6 +18971,34 @@ var KeyEventsForDeviceSchema = object({
18941
18971
  deviceId: number(),
18942
18972
  events: array(KeyEventSchema).readonly()
18943
18973
  });
18974
+ /** One non-empty bucket of the timeline histogram. */
18975
+ var EventDensityBucketSchema = object({
18976
+ bucketStart: number(),
18977
+ motion: number().int(),
18978
+ object: number().int(),
18979
+ audio: number().int()
18980
+ });
18981
+ /**
18982
+ * One camera's row in a `getEventDensityBatch` answer.
18983
+ *
18984
+ * TWO facts, and the timeline draws them differently:
18985
+ *
18986
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
18987
+ * claim: read, and nothing happened in the window.
18988
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
18989
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
18990
+ * not quiet.
18991
+ *
18992
+ * Fanned out per camera the difference was free — one query rejected while the
18993
+ * others resolved — and collapsing to a batch is the exact moment it is lost
18994
+ * silently. Every requested id gets a row: a caller that asked for twelve and
18995
+ * got eleven cannot tell which one is missing, or that any is.
18996
+ */
18997
+ var EventDensityForDeviceSchema = object({
18998
+ deviceId: number(),
18999
+ read: _enum(["read", "unreadable"]),
19000
+ buckets: array(EventDensityBucketSchema).readonly()
19001
+ });
18944
19002
  object({
18945
19003
  trackId: string(),
18946
19004
  className: string(),
@@ -19251,12 +19309,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19251
19309
  since: number(),
19252
19310
  until: number(),
19253
19311
  bucketMs: number().int().positive()
19254
- }), array(object({
19255
- bucketStart: number(),
19256
- motion: number().int(),
19257
- object: number().int(),
19258
- audio: number().int()
19259
- })).readonly()), method(object({
19312
+ }), array(EventDensityBucketSchema).readonly()), method(object({
19313
+ deviceIds: array(number()).min(1).max(200),
19314
+ since: number(),
19315
+ until: number(),
19316
+ bucketMs: number().int().positive()
19317
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
19260
19318
  deviceId: number(),
19261
19319
  cutoffMs: number()
19262
19320
  }), object({
@@ -26566,6 +26624,39 @@ var RecordingDaysSchema = object({
26566
26624
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
26567
26625
  days: array(number())
26568
26626
  });
26627
+ /**
26628
+ * One camera's row in a `getAvailabilityBatch` answer.
26629
+ *
26630
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
26631
+ * batch collapses the transport, not the work — plus the one thing the singular
26632
+ * method never had to say:
26633
+ *
26634
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
26635
+ * no footage in the window", which is a real claim.
26636
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
26637
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
26638
+ * that emptiness means NOTHING.
26639
+ *
26640
+ * A timeline that renders the second as the first tells an operator there is no
26641
+ * footage when the truth is that nobody looked.
26642
+ */
26643
+ var RecordingAvailabilityForDeviceSchema = object({
26644
+ deviceId: number(),
26645
+ read: _enum(["read", "unreadable"]),
26646
+ ranges: array(RecordingRangeSchema).readonly()
26647
+ });
26648
+ /**
26649
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
26650
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
26651
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
26652
+ * the date-picker's day dots must not spell them the same.
26653
+ */
26654
+ var RecordingDaysForDeviceSchema = object({
26655
+ deviceId: number(),
26656
+ read: _enum(["read", "unreadable"]),
26657
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
26658
+ days: array(number()).readonly()
26659
+ });
26569
26660
  var RecordingManifestSchema = object({
26570
26661
  deviceId: number(),
26571
26662
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -26800,6 +26891,13 @@ method(object({
26800
26891
  }), RecordingAvailabilitySchema, {
26801
26892
  kind: "query",
26802
26893
  auth: "protected"
26894
+ }), method(object({
26895
+ deviceIds: array(number()).min(1).max(200),
26896
+ fromMs: number(),
26897
+ toMs: number()
26898
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
26899
+ kind: "query",
26900
+ auth: "protected"
26803
26901
  }), method(object({
26804
26902
  deviceId: number(),
26805
26903
  fromMs: number(),
@@ -26808,6 +26906,14 @@ method(object({
26808
26906
  }), RecordingDaysSchema, {
26809
26907
  kind: "query",
26810
26908
  auth: "protected"
26909
+ }), method(object({
26910
+ deviceIds: array(number()).min(1).max(200),
26911
+ fromMs: number(),
26912
+ toMs: number(),
26913
+ tzOffsetMinutes: number()
26914
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
26915
+ kind: "query",
26916
+ auth: "protected"
26811
26917
  }), method(object({
26812
26918
  deviceId: number(),
26813
26919
  fromMs: number(),
@@ -30271,6 +30377,12 @@ Object.freeze({
30271
30377
  addonId: null,
30272
30378
  access: "view"
30273
30379
  },
30380
+ "deviceManager.migrateDevice": {
30381
+ capName: "device-manager",
30382
+ capScope: "system",
30383
+ addonId: null,
30384
+ access: "create"
30385
+ },
30274
30386
  "deviceManager.persistConfig": {
30275
30387
  capName: "device-manager",
30276
30388
  capScope: "system",
@@ -32005,6 +32117,12 @@ Object.freeze({
32005
32117
  addonId: null,
32006
32118
  access: "view"
32007
32119
  },
32120
+ "pipelineAnalytics.getEventDensityBatch": {
32121
+ capName: "pipeline-analytics",
32122
+ capScope: "device",
32123
+ addonId: null,
32124
+ access: "view"
32125
+ },
32008
32126
  "pipelineAnalytics.getEventMedia": {
32009
32127
  capName: "pipeline-analytics",
32010
32128
  capScope: "device",
@@ -33127,12 +33245,24 @@ Object.freeze({
33127
33245
  addonId: null,
33128
33246
  access: "view"
33129
33247
  },
33248
+ "recording.getAvailabilityBatch": {
33249
+ capName: "recording",
33250
+ capScope: "system",
33251
+ addonId: null,
33252
+ access: "view"
33253
+ },
33130
33254
  "recording.getDaysWithRecordings": {
33131
33255
  capName: "recording",
33132
33256
  capScope: "system",
33133
33257
  addonId: null,
33134
33258
  access: "view"
33135
33259
  },
33260
+ "recording.getDaysWithRecordingsBatch": {
33261
+ capName: "recording",
33262
+ capScope: "system",
33263
+ addonId: null,
33264
+ access: "view"
33265
+ },
33136
33266
  "recording.getDeviceConfig": {
33137
33267
  capName: "recording",
33138
33268
  capScope: "system",
@@ -35737,6 +35867,11 @@ Object.freeze({
35737
35867
  form: "single",
35738
35868
  optional: false
35739
35869
  }],
35870
+ "pipelineAnalytics.getEventDensityBatch": [{
35871
+ name: "deviceIds",
35872
+ form: "array",
35873
+ optional: false
35874
+ }],
35740
35875
  "pipelineAnalytics.getEventMedia": [{
35741
35876
  name: "deviceId",
35742
35877
  form: "single",
@@ -36152,11 +36287,21 @@ Object.freeze({
36152
36287
  form: "single",
36153
36288
  optional: false
36154
36289
  }],
36290
+ "recording.getAvailabilityBatch": [{
36291
+ name: "deviceIds",
36292
+ form: "array",
36293
+ optional: false
36294
+ }],
36155
36295
  "recording.getDaysWithRecordings": [{
36156
36296
  name: "deviceId",
36157
36297
  form: "single",
36158
36298
  optional: false
36159
36299
  }],
36300
+ "recording.getDaysWithRecordingsBatch": [{
36301
+ name: "deviceIds",
36302
+ form: "array",
36303
+ optional: false
36304
+ }],
36160
36305
  "recording.getDeviceConfig": [{
36161
36306
  name: "deviceId",
36162
36307
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-mqtt-broker",
3
- "version": "1.2.51",
3
+ "version": "1.2.53",
4
4
  "description": "MQTT broker registry addon for CamStack — manages external broker entries + an optional embedded aedes broker. Consumers spin up their own `mqtt.js` clients via the `mqtt-broker` cap.",
5
5
  "keywords": [
6
6
  "camstack",