@camstack/addon-import-alexa 0.2.50 → 0.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.
package/dist/addon.js CHANGED
@@ -7876,7 +7876,8 @@ var OpsLogReasonSchema = _enum([
7876
7876
  "quota",
7877
7877
  "manual",
7878
7878
  "operator",
7879
- "maintenance"
7879
+ "maintenance",
7880
+ "orphaned-device"
7880
7881
  ]);
7881
7882
  /** One audit row, shared verbatim by both domains. */
7882
7883
  var OpsLogEntrySchema = object({
@@ -13256,10 +13257,39 @@ var DevicePersistConfigPayloadSchema = object({
13256
13257
  deviceId: number(),
13257
13258
  data: record(string(), unknown())
13258
13259
  });
13260
+ /** What a migration actually did, per switch. `unreachable` is a first-class
13261
+ * answer: a camera that could not be asked is not a camera that was silenced. */
13262
+ var MigrateSwitchOutcomeSchema = _enum([
13263
+ "off",
13264
+ "on",
13265
+ "not-offered",
13266
+ "unreachable"
13267
+ ]);
13268
+ var MigrateSwitchReportSchema = object({
13269
+ deviceId: number(),
13270
+ switchId: CameraSwitchIdSchema,
13271
+ outcome: MigrateSwitchOutcomeSchema,
13272
+ detail: string().optional()
13273
+ });
13274
+ var MigrateDeviceResultSchema = object({
13275
+ sourceId: number(),
13276
+ targetId: number(),
13277
+ switches: array(MigrateSwitchReportSchema).readonly(),
13278
+ /** Switches that could NOT be turned off on the source. Empty is the only
13279
+ * value meaning the replaced hardware is quiet. */
13280
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
13281
+ swapped: boolean()
13282
+ });
13259
13283
  method(object({
13260
13284
  addonId: string(),
13261
13285
  stableId: string()
13262
- }), 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({
13286
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
13287
+ sourceId: number(),
13288
+ targetId: number()
13289
+ }), MigrateDeviceResultSchema, {
13290
+ kind: "mutation",
13291
+ auth: "admin"
13292
+ }), 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({
13263
13293
  deviceId: number(),
13264
13294
  name: string()
13265
13295
  }), _void(), {
@@ -19434,6 +19464,34 @@ var KeyEventsForDeviceSchema = object({
19434
19464
  deviceId: number(),
19435
19465
  events: array(KeyEventSchema).readonly()
19436
19466
  });
19467
+ /** One non-empty bucket of the timeline histogram. */
19468
+ var EventDensityBucketSchema = object({
19469
+ bucketStart: number(),
19470
+ motion: number().int(),
19471
+ object: number().int(),
19472
+ audio: number().int()
19473
+ });
19474
+ /**
19475
+ * One camera's row in a `getEventDensityBatch` answer.
19476
+ *
19477
+ * TWO facts, and the timeline draws them differently:
19478
+ *
19479
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
19480
+ * claim: read, and nothing happened in the window.
19481
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
19482
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
19483
+ * not quiet.
19484
+ *
19485
+ * Fanned out per camera the difference was free — one query rejected while the
19486
+ * others resolved — and collapsing to a batch is the exact moment it is lost
19487
+ * silently. Every requested id gets a row: a caller that asked for twelve and
19488
+ * got eleven cannot tell which one is missing, or that any is.
19489
+ */
19490
+ var EventDensityForDeviceSchema = object({
19491
+ deviceId: number(),
19492
+ read: _enum(["read", "unreadable"]),
19493
+ buckets: array(EventDensityBucketSchema).readonly()
19494
+ });
19437
19495
  object({
19438
19496
  trackId: string(),
19439
19497
  className: string(),
@@ -19744,12 +19802,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19744
19802
  since: number(),
19745
19803
  until: number(),
19746
19804
  bucketMs: number().int().positive()
19747
- }), array(object({
19748
- bucketStart: number(),
19749
- motion: number().int(),
19750
- object: number().int(),
19751
- audio: number().int()
19752
- })).readonly()), method(object({
19805
+ }), array(EventDensityBucketSchema).readonly()), method(object({
19806
+ deviceIds: array(number()).min(1).max(200),
19807
+ since: number(),
19808
+ until: number(),
19809
+ bucketMs: number().int().positive()
19810
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
19753
19811
  deviceId: number(),
19754
19812
  cutoffMs: number()
19755
19813
  }), object({
@@ -28791,6 +28849,39 @@ var RecordingDaysSchema = object({
28791
28849
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
28792
28850
  days: array(number())
28793
28851
  });
28852
+ /**
28853
+ * One camera's row in a `getAvailabilityBatch` answer.
28854
+ *
28855
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
28856
+ * batch collapses the transport, not the work — plus the one thing the singular
28857
+ * method never had to say:
28858
+ *
28859
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
28860
+ * no footage in the window", which is a real claim.
28861
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
28862
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
28863
+ * that emptiness means NOTHING.
28864
+ *
28865
+ * A timeline that renders the second as the first tells an operator there is no
28866
+ * footage when the truth is that nobody looked.
28867
+ */
28868
+ var RecordingAvailabilityForDeviceSchema = object({
28869
+ deviceId: number(),
28870
+ read: _enum(["read", "unreadable"]),
28871
+ ranges: array(RecordingRangeSchema).readonly()
28872
+ });
28873
+ /**
28874
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
28875
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
28876
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
28877
+ * the date-picker's day dots must not spell them the same.
28878
+ */
28879
+ var RecordingDaysForDeviceSchema = object({
28880
+ deviceId: number(),
28881
+ read: _enum(["read", "unreadable"]),
28882
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
28883
+ days: array(number()).readonly()
28884
+ });
28794
28885
  var RecordingManifestSchema = object({
28795
28886
  deviceId: number(),
28796
28887
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -29025,6 +29116,13 @@ method(object({
29025
29116
  }), RecordingAvailabilitySchema, {
29026
29117
  kind: "query",
29027
29118
  auth: "protected"
29119
+ }), method(object({
29120
+ deviceIds: array(number()).min(1).max(200),
29121
+ fromMs: number(),
29122
+ toMs: number()
29123
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
29124
+ kind: "query",
29125
+ auth: "protected"
29028
29126
  }), method(object({
29029
29127
  deviceId: number(),
29030
29128
  fromMs: number(),
@@ -29033,6 +29131,14 @@ method(object({
29033
29131
  }), RecordingDaysSchema, {
29034
29132
  kind: "query",
29035
29133
  auth: "protected"
29134
+ }), method(object({
29135
+ deviceIds: array(number()).min(1).max(200),
29136
+ fromMs: number(),
29137
+ toMs: number(),
29138
+ tzOffsetMinutes: number()
29139
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
29140
+ kind: "query",
29141
+ auth: "protected"
29036
29142
  }), method(object({
29037
29143
  deviceId: number(),
29038
29144
  fromMs: number(),
@@ -34195,6 +34301,12 @@ Object.freeze({
34195
34301
  addonId: null,
34196
34302
  access: "view"
34197
34303
  },
34304
+ "deviceManager.migrateDevice": {
34305
+ capName: "device-manager",
34306
+ capScope: "system",
34307
+ addonId: null,
34308
+ access: "create"
34309
+ },
34198
34310
  "deviceManager.persistConfig": {
34199
34311
  capName: "device-manager",
34200
34312
  capScope: "system",
@@ -35929,6 +36041,12 @@ Object.freeze({
35929
36041
  addonId: null,
35930
36042
  access: "view"
35931
36043
  },
36044
+ "pipelineAnalytics.getEventDensityBatch": {
36045
+ capName: "pipeline-analytics",
36046
+ capScope: "device",
36047
+ addonId: null,
36048
+ access: "view"
36049
+ },
35932
36050
  "pipelineAnalytics.getEventMedia": {
35933
36051
  capName: "pipeline-analytics",
35934
36052
  capScope: "device",
@@ -37051,12 +37169,24 @@ Object.freeze({
37051
37169
  addonId: null,
37052
37170
  access: "view"
37053
37171
  },
37172
+ "recording.getAvailabilityBatch": {
37173
+ capName: "recording",
37174
+ capScope: "system",
37175
+ addonId: null,
37176
+ access: "view"
37177
+ },
37054
37178
  "recording.getDaysWithRecordings": {
37055
37179
  capName: "recording",
37056
37180
  capScope: "system",
37057
37181
  addonId: null,
37058
37182
  access: "view"
37059
37183
  },
37184
+ "recording.getDaysWithRecordingsBatch": {
37185
+ capName: "recording",
37186
+ capScope: "system",
37187
+ addonId: null,
37188
+ access: "view"
37189
+ },
37060
37190
  "recording.getDeviceConfig": {
37061
37191
  capName: "recording",
37062
37192
  capScope: "system",
@@ -39661,6 +39791,11 @@ Object.freeze({
39661
39791
  form: "single",
39662
39792
  optional: false
39663
39793
  }],
39794
+ "pipelineAnalytics.getEventDensityBatch": [{
39795
+ name: "deviceIds",
39796
+ form: "array",
39797
+ optional: false
39798
+ }],
39664
39799
  "pipelineAnalytics.getEventMedia": [{
39665
39800
  name: "deviceId",
39666
39801
  form: "single",
@@ -40076,11 +40211,21 @@ Object.freeze({
40076
40211
  form: "single",
40077
40212
  optional: false
40078
40213
  }],
40214
+ "recording.getAvailabilityBatch": [{
40215
+ name: "deviceIds",
40216
+ form: "array",
40217
+ optional: false
40218
+ }],
40079
40219
  "recording.getDaysWithRecordings": [{
40080
40220
  name: "deviceId",
40081
40221
  form: "single",
40082
40222
  optional: false
40083
40223
  }],
40224
+ "recording.getDaysWithRecordingsBatch": [{
40225
+ name: "deviceIds",
40226
+ form: "array",
40227
+ optional: false
40228
+ }],
40084
40229
  "recording.getDeviceConfig": [{
40085
40230
  name: "deviceId",
40086
40231
  form: "single",
package/dist/addon.mjs CHANGED
@@ -7876,7 +7876,8 @@ var OpsLogReasonSchema = _enum([
7876
7876
  "quota",
7877
7877
  "manual",
7878
7878
  "operator",
7879
- "maintenance"
7879
+ "maintenance",
7880
+ "orphaned-device"
7880
7881
  ]);
7881
7882
  /** One audit row, shared verbatim by both domains. */
7882
7883
  var OpsLogEntrySchema = object({
@@ -13256,10 +13257,39 @@ var DevicePersistConfigPayloadSchema = object({
13256
13257
  deviceId: number(),
13257
13258
  data: record(string(), unknown())
13258
13259
  });
13260
+ /** What a migration actually did, per switch. `unreachable` is a first-class
13261
+ * answer: a camera that could not be asked is not a camera that was silenced. */
13262
+ var MigrateSwitchOutcomeSchema = _enum([
13263
+ "off",
13264
+ "on",
13265
+ "not-offered",
13266
+ "unreachable"
13267
+ ]);
13268
+ var MigrateSwitchReportSchema = object({
13269
+ deviceId: number(),
13270
+ switchId: CameraSwitchIdSchema,
13271
+ outcome: MigrateSwitchOutcomeSchema,
13272
+ detail: string().optional()
13273
+ });
13274
+ var MigrateDeviceResultSchema = object({
13275
+ sourceId: number(),
13276
+ targetId: number(),
13277
+ switches: array(MigrateSwitchReportSchema).readonly(),
13278
+ /** Switches that could NOT be turned off on the source. Empty is the only
13279
+ * value meaning the replaced hardware is quiet. */
13280
+ sourceStillLive: array(CameraSwitchIdSchema).readonly(),
13281
+ swapped: boolean()
13282
+ });
13259
13283
  method(object({
13260
13284
  addonId: string(),
13261
13285
  stableId: string()
13262
- }), 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({
13286
+ }), object({ id: number() }), { kind: "mutation" }), method(object({
13287
+ sourceId: number(),
13288
+ targetId: number()
13289
+ }), MigrateDeviceResultSchema, {
13290
+ kind: "mutation",
13291
+ auth: "admin"
13292
+ }), 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({
13263
13293
  deviceId: number(),
13264
13294
  name: string()
13265
13295
  }), _void(), {
@@ -19434,6 +19464,34 @@ var KeyEventsForDeviceSchema = object({
19434
19464
  deviceId: number(),
19435
19465
  events: array(KeyEventSchema).readonly()
19436
19466
  });
19467
+ /** One non-empty bucket of the timeline histogram. */
19468
+ var EventDensityBucketSchema = object({
19469
+ bucketStart: number(),
19470
+ motion: number().int(),
19471
+ object: number().int(),
19472
+ audio: number().int()
19473
+ });
19474
+ /**
19475
+ * One camera's row in a `getEventDensityBatch` answer.
19476
+ *
19477
+ * TWO facts, and the timeline draws them differently:
19478
+ *
19479
+ * - `read: 'read'` — the histogram for this camera. `buckets: []` is a real
19480
+ * claim: read, and nothing happened in the window.
19481
+ * - `read: 'unreadable'` — nobody could answer for this camera. `buckets` is
19482
+ * empty and that emptiness means NOTHING; the timeline must render unknown,
19483
+ * not quiet.
19484
+ *
19485
+ * Fanned out per camera the difference was free — one query rejected while the
19486
+ * others resolved — and collapsing to a batch is the exact moment it is lost
19487
+ * silently. Every requested id gets a row: a caller that asked for twelve and
19488
+ * got eleven cannot tell which one is missing, or that any is.
19489
+ */
19490
+ var EventDensityForDeviceSchema = object({
19491
+ deviceId: number(),
19492
+ read: _enum(["read", "unreadable"]),
19493
+ buckets: array(EventDensityBucketSchema).readonly()
19494
+ });
19437
19495
  object({
19438
19496
  trackId: string(),
19439
19497
  className: string(),
@@ -19744,12 +19802,12 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19744
19802
  since: number(),
19745
19803
  until: number(),
19746
19804
  bucketMs: number().int().positive()
19747
- }), array(object({
19748
- bucketStart: number(),
19749
- motion: number().int(),
19750
- object: number().int(),
19751
- audio: number().int()
19752
- })).readonly()), method(object({
19805
+ }), array(EventDensityBucketSchema).readonly()), method(object({
19806
+ deviceIds: array(number()).min(1).max(200),
19807
+ since: number(),
19808
+ until: number(),
19809
+ bucketMs: number().int().positive()
19810
+ }), array(EventDensityForDeviceSchema).readonly()), method(object({
19753
19811
  deviceId: number(),
19754
19812
  cutoffMs: number()
19755
19813
  }), object({
@@ -28791,6 +28849,39 @@ var RecordingDaysSchema = object({
28791
28849
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
28792
28850
  days: array(number())
28793
28851
  });
28852
+ /**
28853
+ * One camera's row in a `getAvailabilityBatch` answer.
28854
+ *
28855
+ * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
28856
+ * batch collapses the transport, not the work — plus the one thing the singular
28857
+ * method never had to say:
28858
+ *
28859
+ * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
28860
+ * no footage in the window", which is a real claim.
28861
+ * - `read: 'unreadable'` — the recorder could not answer for this camera (its
28862
+ * calendar rung threw, its location is unmounted). `ranges` is empty and
28863
+ * that emptiness means NOTHING.
28864
+ *
28865
+ * A timeline that renders the second as the first tells an operator there is no
28866
+ * footage when the truth is that nobody looked.
28867
+ */
28868
+ var RecordingAvailabilityForDeviceSchema = object({
28869
+ deviceId: number(),
28870
+ read: _enum(["read", "unreadable"]),
28871
+ ranges: array(RecordingRangeSchema).readonly()
28872
+ });
28873
+ /**
28874
+ * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
28875
+ * {@link RecordingAvailabilityForDeviceSchema}: `days: []` on a `'read'` row is
28876
+ * "no footage in this month", `read: 'unreadable'` is "nobody could look", and
28877
+ * the date-picker's day dots must not spell them the same.
28878
+ */
28879
+ var RecordingDaysForDeviceSchema = object({
28880
+ deviceId: number(),
28881
+ read: _enum(["read", "unreadable"]),
28882
+ /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
28883
+ days: array(number()).readonly()
28884
+ });
28794
28885
  var RecordingManifestSchema = object({
28795
28886
  deviceId: number(),
28796
28887
  /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
@@ -29025,6 +29116,13 @@ method(object({
29025
29116
  }), RecordingAvailabilitySchema, {
29026
29117
  kind: "query",
29027
29118
  auth: "protected"
29119
+ }), method(object({
29120
+ deviceIds: array(number()).min(1).max(200),
29121
+ fromMs: number(),
29122
+ toMs: number()
29123
+ }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
29124
+ kind: "query",
29125
+ auth: "protected"
29028
29126
  }), method(object({
29029
29127
  deviceId: number(),
29030
29128
  fromMs: number(),
@@ -29033,6 +29131,14 @@ method(object({
29033
29131
  }), RecordingDaysSchema, {
29034
29132
  kind: "query",
29035
29133
  auth: "protected"
29134
+ }), method(object({
29135
+ deviceIds: array(number()).min(1).max(200),
29136
+ fromMs: number(),
29137
+ toMs: number(),
29138
+ tzOffsetMinutes: number()
29139
+ }), array(RecordingDaysForDeviceSchema).readonly(), {
29140
+ kind: "query",
29141
+ auth: "protected"
29036
29142
  }), method(object({
29037
29143
  deviceId: number(),
29038
29144
  fromMs: number(),
@@ -34195,6 +34301,12 @@ Object.freeze({
34195
34301
  addonId: null,
34196
34302
  access: "view"
34197
34303
  },
34304
+ "deviceManager.migrateDevice": {
34305
+ capName: "device-manager",
34306
+ capScope: "system",
34307
+ addonId: null,
34308
+ access: "create"
34309
+ },
34198
34310
  "deviceManager.persistConfig": {
34199
34311
  capName: "device-manager",
34200
34312
  capScope: "system",
@@ -35929,6 +36041,12 @@ Object.freeze({
35929
36041
  addonId: null,
35930
36042
  access: "view"
35931
36043
  },
36044
+ "pipelineAnalytics.getEventDensityBatch": {
36045
+ capName: "pipeline-analytics",
36046
+ capScope: "device",
36047
+ addonId: null,
36048
+ access: "view"
36049
+ },
35932
36050
  "pipelineAnalytics.getEventMedia": {
35933
36051
  capName: "pipeline-analytics",
35934
36052
  capScope: "device",
@@ -37051,12 +37169,24 @@ Object.freeze({
37051
37169
  addonId: null,
37052
37170
  access: "view"
37053
37171
  },
37172
+ "recording.getAvailabilityBatch": {
37173
+ capName: "recording",
37174
+ capScope: "system",
37175
+ addonId: null,
37176
+ access: "view"
37177
+ },
37054
37178
  "recording.getDaysWithRecordings": {
37055
37179
  capName: "recording",
37056
37180
  capScope: "system",
37057
37181
  addonId: null,
37058
37182
  access: "view"
37059
37183
  },
37184
+ "recording.getDaysWithRecordingsBatch": {
37185
+ capName: "recording",
37186
+ capScope: "system",
37187
+ addonId: null,
37188
+ access: "view"
37189
+ },
37060
37190
  "recording.getDeviceConfig": {
37061
37191
  capName: "recording",
37062
37192
  capScope: "system",
@@ -39661,6 +39791,11 @@ Object.freeze({
39661
39791
  form: "single",
39662
39792
  optional: false
39663
39793
  }],
39794
+ "pipelineAnalytics.getEventDensityBatch": [{
39795
+ name: "deviceIds",
39796
+ form: "array",
39797
+ optional: false
39798
+ }],
39664
39799
  "pipelineAnalytics.getEventMedia": [{
39665
39800
  name: "deviceId",
39666
39801
  form: "single",
@@ -40076,11 +40211,21 @@ Object.freeze({
40076
40211
  form: "single",
40077
40212
  optional: false
40078
40213
  }],
40214
+ "recording.getAvailabilityBatch": [{
40215
+ name: "deviceIds",
40216
+ form: "array",
40217
+ optional: false
40218
+ }],
40079
40219
  "recording.getDaysWithRecordings": [{
40080
40220
  name: "deviceId",
40081
40221
  form: "single",
40082
40222
  optional: false
40083
40223
  }],
40224
+ "recording.getDaysWithRecordingsBatch": [{
40225
+ name: "deviceIds",
40226
+ form: "array",
40227
+ optional: false
40228
+ }],
40084
40229
  "recording.getDeviceConfig": [{
40085
40230
  name: "deviceId",
40086
40231
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-import-alexa",
3
- "version": "0.2.50",
3
+ "version": "0.2.52",
4
4
  "description": "Alexa device-import provider for CamStack — imports the smart-home devices in a user's Alexa account via the unofficial alexa-remote2 cookie/token client (the inverse of the Alexa exporter)",
5
5
  "keywords": [
6
6
  "camstack",