@camstack/addon-smtp-nodemailer 1.2.50 → 1.2.52

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.
@@ -7739,7 +7739,8 @@ var OpsLogReasonSchema = _enum([
7739
7739
  "quota",
7740
7740
  "manual",
7741
7741
  "operator",
7742
- "maintenance"
7742
+ "maintenance",
7743
+ "orphaned-device"
7743
7744
  ]);
7744
7745
  /** One audit row, shared verbatim by both domains. */
7745
7746
  var OpsLogEntrySchema = object({
@@ -12770,10 +12771,39 @@ var DevicePersistConfigPayloadSchema = object({
12770
12771
  deviceId: number(),
12771
12772
  data: record(string(), unknown())
12772
12773
  });
12774
+ /** What a migration actually did, per switch. `unreachable` is a first-class
12775
+ * answer: a camera that could not be asked is not a camera that was silenced. */
12776
+ var MigrateSwitchOutcomeSchema = _enum([
12777
+ "off",
12778
+ "on",
12779
+ "not-offered",
12780
+ "unreachable"
12781
+ ]);
12782
+ var MigrateSwitchReportSchema = object({
12783
+ deviceId: number(),
12784
+ switchId: CameraSwitchIdSchema,
12785
+ outcome: MigrateSwitchOutcomeSchema,
12786
+ detail: string().optional()
12787
+ });
12788
+ var MigrateDeviceResultSchema = object({
12789
+ sourceId: number(),
12790
+ targetId: number(),
12791
+ switches: array(MigrateSwitchReportSchema).readonly(),
12792
+ /** Switches that could NOT be turned off on the source. Empty is the only
12793
+ * value meaning the replaced hardware is quiet. */
12794
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
12795
+ swapped: boolean()
12796
+ });
12773
12797
  method(object({
12774
12798
  addonId: string(),
12775
12799
  stableId: string()
12776
- }), 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({
12800
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
12801
+ sourceId: number(),
12802
+ targetId: number()
12803
+ }), MigrateDeviceResultSchema, {
12804
+ kind: "mutation",
12805
+ auth: "admin"
12806
+ }), 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({
12777
12807
  deviceId: number(),
12778
12808
  name: string()
12779
12809
  }), _void(), {
@@ -18870,6 +18900,34 @@ var KeyEventsForDeviceSchema = object({
18870
18900
  deviceId: number(),
18871
18901
  events: array(KeyEventSchema).readonly()
18872
18902
  });
18903
+ /** One non-empty bucket of the timeline histogram. */
18904
+ var EventDensityBucketSchema = object({
18905
+ bucketStart: number(),
18906
+ motion: number().int(),
18907
+ object: number().int(),
18908
+ audio: number().int()
18909
+ });
18910
+ /**
18911
+ * One camera's row in a `getEventDensityBatch` answer.
18912
+ *
18913
+ * TWO facts, and the timeline draws them differently:
18914
+ *
18915
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
18916
+ * claim: read, and nothing happened in the window.
18917
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
18918
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
18919
+ * not quiet.
18920
+ *
18921
+ * Fanned out per camera the difference was free — one query rejected while the
18922
+ * others resolved — and collapsing to a batch is the exact moment it is lost
18923
+ * silently. Every requested id gets a row: a caller that asked for twelve and
18924
+ * got eleven cannot tell which one is missing, or that any is.
18925
+ */
18926
+ var EventDensityForDeviceSchema = object({
18927
+ deviceId: number(),
18928
+ read: _enum(["read", "unreadable"]),
18929
+ buckets: array(EventDensityBucketSchema).readonly()
18930
+ });
18873
18931
  object({
18874
18932
  trackId: string(),
18875
18933
  className: string(),
@@ -19180,12 +19238,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19180
19238
  since: number(),
19181
19239
  until: number(),
19182
19240
  bucketMs: number().int().positive()
19183
- }), array(object({
19184
- bucketStart: number(),
19185
- motion: number().int(),
19186
- object: number().int(),
19187
- audio: number().int()
19188
- })).readonly()), method(object({
19241
+ }), array(EventDensityBucketSchema).readonly()), method(object({
19242
+ deviceIds: array(number()).min(1).max(200),
19243
+ since: number(),
19244
+ until: number(),
19245
+ bucketMs: number().int().positive()
19246
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
19189
19247
  deviceId: number(),
19190
19248
  cutoffMs: number()
19191
19249
  }), object({
@@ -26508,6 +26566,39 @@ var RecordingDaysSchema = object({
26508
26566
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
26509
26567
  days: array(number())
26510
26568
  });
26569
+ /**
26570
+ * One camera's row in a `getAvailabilityBatch` answer.
26571
+ *
26572
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
26573
+ * batch collapses the transport, not the work — plus the one thing the singular
26574
+ * method never had to say:
26575
+ *
26576
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
26577
+ * no footage in the window", which is a real claim.
26578
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
26579
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
26580
+ * that emptiness means NOTHING.
26581
+ *
26582
+ * A timeline that renders the second as the first tells an operator there is no
26583
+ * footage when the truth is that nobody looked.
26584
+ */
26585
+ var RecordingAvailabilityForDeviceSchema = object({
26586
+ deviceId: number(),
26587
+ read: _enum(["read", "unreadable"]),
26588
+ ranges: array(RecordingRangeSchema).readonly()
26589
+ });
26590
+ /**
26591
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
26592
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
26593
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
26594
+ * the date-picker's day dots must not spell them the same.
26595
+ */
26596
+ var RecordingDaysForDeviceSchema = object({
26597
+ deviceId: number(),
26598
+ read: _enum(["read", "unreadable"]),
26599
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
26600
+ days: array(number()).readonly()
26601
+ });
26511
26602
  var RecordingManifestSchema = object({
26512
26603
  deviceId: number(),
26513
26604
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -26742,6 +26833,13 @@ method(object({
26742
26833
  }), RecordingAvailabilitySchema, {
26743
26834
  kind: "query",
26744
26835
  auth: "protected"
26836
+ }), method(object({
26837
+ deviceIds: array(number()).min(1).max(200),
26838
+ fromMs: number(),
26839
+ toMs: number()
26840
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
26841
+ kind: "query",
26842
+ auth: "protected"
26745
26843
  }), method(object({
26746
26844
  deviceId: number(),
26747
26845
  fromMs: number(),
@@ -26750,6 +26848,14 @@ method(object({
26750
26848
  }), RecordingDaysSchema, {
26751
26849
  kind: "query",
26752
26850
  auth: "protected"
26851
+ }), method(object({
26852
+ deviceIds: array(number()).min(1).max(200),
26853
+ fromMs: number(),
26854
+ toMs: number(),
26855
+ tzOffsetMinutes: number()
26856
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
26857
+ kind: "query",
26858
+ auth: "protected"
26753
26859
  }), method(object({
26754
26860
  deviceId: number(),
26755
26861
  fromMs: number(),
@@ -30213,6 +30319,12 @@ Object.freeze({
30213
30319
  addonId: null,
30214
30320
  access: "view"
30215
30321
  },
30322
+ "deviceManager.migrateDevice": {
30323
+ capName: "device-manager",
30324
+ capScope: "system",
30325
+ addonId: null,
30326
+ access: "create"
30327
+ },
30216
30328
  "deviceManager.persistConfig": {
30217
30329
  capName: "device-manager",
30218
30330
  capScope: "system",
@@ -31947,6 +32059,12 @@ Object.freeze({
31947
32059
  addonId: null,
31948
32060
  access: "view"
31949
32061
  },
32062
+ "pipelineAnalytics.getEventDensityBatch": {
32063
+ capName: "pipeline-analytics",
32064
+ capScope: "device",
32065
+ addonId: null,
32066
+ access: "view"
32067
+ },
31950
32068
  "pipelineAnalytics.getEventMedia": {
31951
32069
  capName: "pipeline-analytics",
31952
32070
  capScope: "device",
@@ -33069,12 +33187,24 @@ Object.freeze({
33069
33187
  addonId: null,
33070
33188
  access: "view"
33071
33189
  },
33190
+ "recording.getAvailabilityBatch": {
33191
+ capName: "recording",
33192
+ capScope: "system",
33193
+ addonId: null,
33194
+ access: "view"
33195
+ },
33072
33196
  "recording.getDaysWithRecordings": {
33073
33197
  capName: "recording",
33074
33198
  capScope: "system",
33075
33199
  addonId: null,
33076
33200
  access: "view"
33077
33201
  },
33202
+ "recording.getDaysWithRecordingsBatch": {
33203
+ capName: "recording",
33204
+ capScope: "system",
33205
+ addonId: null,
33206
+ access: "view"
33207
+ },
33078
33208
  "recording.getDeviceConfig": {
33079
33209
  capName: "recording",
33080
33210
  capScope: "system",
@@ -35679,6 +35809,11 @@ Object.freeze({
35679
35809
  form: "single",
35680
35810
  optional: false
35681
35811
  }],
35812
+ "pipelineAnalytics.getEventDensityBatch": [{
35813
+ name: "deviceIds",
35814
+ form: "array",
35815
+ optional: false
35816
+ }],
35682
35817
  "pipelineAnalytics.getEventMedia": [{
35683
35818
  name: "deviceId",
35684
35819
  form: "single",
@@ -36094,11 +36229,21 @@ Object.freeze({
36094
36229
  form: "single",
36095
36230
  optional: false
36096
36231
  }],
36232
+ "recording.getAvailabilityBatch": [{
36233
+ name: "deviceIds",
36234
+ form: "array",
36235
+ optional: false
36236
+ }],
36097
36237
  "recording.getDaysWithRecordings": [{
36098
36238
  name: "deviceId",
36099
36239
  form: "single",
36100
36240
  optional: false
36101
36241
  }],
36242
+ "recording.getDaysWithRecordingsBatch": [{
36243
+ name: "deviceIds",
36244
+ form: "array",
36245
+ optional: false
36246
+ }],
36102
36247
  "recording.getDeviceConfig": [{
36103
36248
  name: "deviceId",
36104
36249
  form: "single",
@@ -7737,7 +7737,8 @@ var OpsLogReasonSchema = _enum([
7737
7737
  "quota",
7738
7738
  "manual",
7739
7739
  "operator",
7740
- "maintenance"
7740
+ "maintenance",
7741
+ "orphaned-device"
7741
7742
  ]);
7742
7743
  /** One audit row, shared verbatim by both domains. */
7743
7744
  var OpsLogEntrySchema = object({
@@ -12768,10 +12769,39 @@ var DevicePersistConfigPayloadSchema = object({
12768
12769
  deviceId: number(),
12769
12770
  data: record(string(), unknown())
12770
12771
  });
12772
+ /** What a migration actually did, per switch. `unreachable` is a first-class
12773
+ * answer: a camera that could not be asked is not a camera that was silenced. */
12774
+ var MigrateSwitchOutcomeSchema = _enum([
12775
+ "off",
12776
+ "on",
12777
+ "not-offered",
12778
+ "unreachable"
12779
+ ]);
12780
+ var MigrateSwitchReportSchema = object({
12781
+ deviceId: number(),
12782
+ switchId: CameraSwitchIdSchema,
12783
+ outcome: MigrateSwitchOutcomeSchema,
12784
+ detail: string().optional()
12785
+ });
12786
+ var MigrateDeviceResultSchema = object({
12787
+ sourceId: number(),
12788
+ targetId: number(),
12789
+ switches: array(MigrateSwitchReportSchema).readonly(),
12790
+ /** Switches that could NOT be turned off on the source. Empty is the only
12791
+ * value meaning the replaced hardware is quiet. */
12792
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
12793
+ swapped: boolean()
12794
+ });
12771
12795
  method(object({
12772
12796
  addonId: string(),
12773
12797
  stableId: string()
12774
- }), 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({
12798
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
12799
+ sourceId: number(),
12800
+ targetId: number()
12801
+ }), MigrateDeviceResultSchema, {
12802
+ kind: "mutation",
12803
+ auth: "admin"
12804
+ }), 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({
12775
12805
  deviceId: number(),
12776
12806
  name: string()
12777
12807
  }), _void(), {
@@ -18868,6 +18898,34 @@ var KeyEventsForDeviceSchema = object({
18868
18898
  deviceId: number(),
18869
18899
  events: array(KeyEventSchema).readonly()
18870
18900
  });
18901
+ /** One non-empty bucket of the timeline histogram. */
18902
+ var EventDensityBucketSchema = object({
18903
+ bucketStart: number(),
18904
+ motion: number().int(),
18905
+ object: number().int(),
18906
+ audio: number().int()
18907
+ });
18908
+ /**
18909
+ * One camera's row in a `getEventDensityBatch` answer.
18910
+ *
18911
+ * TWO facts, and the timeline draws them differently:
18912
+ *
18913
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
18914
+ * claim: read, and nothing happened in the window.
18915
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
18916
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
18917
+ * not quiet.
18918
+ *
18919
+ * Fanned out per camera the difference was free — one query rejected while the
18920
+ * others resolved — and collapsing to a batch is the exact moment it is lost
18921
+ * silently. Every requested id gets a row: a caller that asked for twelve and
18922
+ * got eleven cannot tell which one is missing, or that any is.
18923
+ */
18924
+ var EventDensityForDeviceSchema = object({
18925
+ deviceId: number(),
18926
+ read: _enum(["read", "unreadable"]),
18927
+ buckets: array(EventDensityBucketSchema).readonly()
18928
+ });
18871
18929
  object({
18872
18930
  trackId: string(),
18873
18931
  className: string(),
@@ -19178,12 +19236,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19178
19236
  since: number(),
19179
19237
  until: number(),
19180
19238
  bucketMs: number().int().positive()
19181
- }), array(object({
19182
- bucketStart: number(),
19183
- motion: number().int(),
19184
- object: number().int(),
19185
- audio: number().int()
19186
- })).readonly()), method(object({
19239
+ }), array(EventDensityBucketSchema).readonly()), method(object({
19240
+ deviceIds: array(number()).min(1).max(200),
19241
+ since: number(),
19242
+ until: number(),
19243
+ bucketMs: number().int().positive()
19244
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
19187
19245
  deviceId: number(),
19188
19246
  cutoffMs: number()
19189
19247
  }), object({
@@ -26506,6 +26564,39 @@ var RecordingDaysSchema = object({
26506
26564
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
26507
26565
  days: array(number())
26508
26566
  });
26567
+ /**
26568
+ * One camera's row in a `getAvailabilityBatch` answer.
26569
+ *
26570
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
26571
+ * batch collapses the transport, not the work — plus the one thing the singular
26572
+ * method never had to say:
26573
+ *
26574
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
26575
+ * no footage in the window", which is a real claim.
26576
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
26577
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
26578
+ * that emptiness means NOTHING.
26579
+ *
26580
+ * A timeline that renders the second as the first tells an operator there is no
26581
+ * footage when the truth is that nobody looked.
26582
+ */
26583
+ var RecordingAvailabilityForDeviceSchema = object({
26584
+ deviceId: number(),
26585
+ read: _enum(["read", "unreadable"]),
26586
+ ranges: array(RecordingRangeSchema).readonly()
26587
+ });
26588
+ /**
26589
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
26590
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
26591
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
26592
+ * the date-picker's day dots must not spell them the same.
26593
+ */
26594
+ var RecordingDaysForDeviceSchema = object({
26595
+ deviceId: number(),
26596
+ read: _enum(["read", "unreadable"]),
26597
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
26598
+ days: array(number()).readonly()
26599
+ });
26509
26600
  var RecordingManifestSchema = object({
26510
26601
  deviceId: number(),
26511
26602
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -26740,6 +26831,13 @@ method(object({
26740
26831
  }), RecordingAvailabilitySchema, {
26741
26832
  kind: "query",
26742
26833
  auth: "protected"
26834
+ }), method(object({
26835
+ deviceIds: array(number()).min(1).max(200),
26836
+ fromMs: number(),
26837
+ toMs: number()
26838
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
26839
+ kind: "query",
26840
+ auth: "protected"
26743
26841
  }), method(object({
26744
26842
  deviceId: number(),
26745
26843
  fromMs: number(),
@@ -26748,6 +26846,14 @@ method(object({
26748
26846
  }), RecordingDaysSchema, {
26749
26847
  kind: "query",
26750
26848
  auth: "protected"
26849
+ }), method(object({
26850
+ deviceIds: array(number()).min(1).max(200),
26851
+ fromMs: number(),
26852
+ toMs: number(),
26853
+ tzOffsetMinutes: number()
26854
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
26855
+ kind: "query",
26856
+ auth: "protected"
26751
26857
  }), method(object({
26752
26858
  deviceId: number(),
26753
26859
  fromMs: number(),
@@ -30211,6 +30317,12 @@ Object.freeze({
30211
30317
  addonId: null,
30212
30318
  access: "view"
30213
30319
  },
30320
+ "deviceManager.migrateDevice": {
30321
+ capName: "device-manager",
30322
+ capScope: "system",
30323
+ addonId: null,
30324
+ access: "create"
30325
+ },
30214
30326
  "deviceManager.persistConfig": {
30215
30327
  capName: "device-manager",
30216
30328
  capScope: "system",
@@ -31945,6 +32057,12 @@ Object.freeze({
31945
32057
  addonId: null,
31946
32058
  access: "view"
31947
32059
  },
32060
+ "pipelineAnalytics.getEventDensityBatch": {
32061
+ capName: "pipeline-analytics",
32062
+ capScope: "device",
32063
+ addonId: null,
32064
+ access: "view"
32065
+ },
31948
32066
  "pipelineAnalytics.getEventMedia": {
31949
32067
  capName: "pipeline-analytics",
31950
32068
  capScope: "device",
@@ -33067,12 +33185,24 @@ Object.freeze({
33067
33185
  addonId: null,
33068
33186
  access: "view"
33069
33187
  },
33188
+ "recording.getAvailabilityBatch": {
33189
+ capName: "recording",
33190
+ capScope: "system",
33191
+ addonId: null,
33192
+ access: "view"
33193
+ },
33070
33194
  "recording.getDaysWithRecordings": {
33071
33195
  capName: "recording",
33072
33196
  capScope: "system",
33073
33197
  addonId: null,
33074
33198
  access: "view"
33075
33199
  },
33200
+ "recording.getDaysWithRecordingsBatch": {
33201
+ capName: "recording",
33202
+ capScope: "system",
33203
+ addonId: null,
33204
+ access: "view"
33205
+ },
33076
33206
  "recording.getDeviceConfig": {
33077
33207
  capName: "recording",
33078
33208
  capScope: "system",
@@ -35677,6 +35807,11 @@ Object.freeze({
35677
35807
  form: "single",
35678
35808
  optional: false
35679
35809
  }],
35810
+ "pipelineAnalytics.getEventDensityBatch": [{
35811
+ name: "deviceIds",
35812
+ form: "array",
35813
+ optional: false
35814
+ }],
35680
35815
  "pipelineAnalytics.getEventMedia": [{
35681
35816
  name: "deviceId",
35682
35817
  form: "single",
@@ -36092,11 +36227,21 @@ Object.freeze({
36092
36227
  form: "single",
36093
36228
  optional: false
36094
36229
  }],
36230
+ "recording.getAvailabilityBatch": [{
36231
+ name: "deviceIds",
36232
+ form: "array",
36233
+ optional: false
36234
+ }],
36095
36235
  "recording.getDaysWithRecordings": [{
36096
36236
  name: "deviceId",
36097
36237
  form: "single",
36098
36238
  optional: false
36099
36239
  }],
36240
+ "recording.getDaysWithRecordingsBatch": [{
36241
+ name: "deviceIds",
36242
+ form: "array",
36243
+ optional: false
36244
+ }],
36100
36245
  "recording.getDeviceConfig": [{
36101
36246
  name: "deviceId",
36102
36247
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-smtp-nodemailer",
3
- "version": "1.2.50",
3
+ "version": "1.2.52",
4
4
  "description": "SMTP email provider addon for CamStack — wraps `nodemailer` and registers a `smtp-provider` cap collection entry. Used by magic-link login + notifier addons.",
5
5
  "keywords": [
6
6
  "camstack",