@eluvio/elv-client-js 4.0.86 → 4.0.87

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eluvio/elv-client-js",
3
- "version": "4.0.86",
3
+ "version": "4.0.87",
4
4
  "description": "Javascript client for the Eluvio Content Fabric",
5
5
  "main": "src/index.js",
6
6
  "author": "Kevin Talmadge",
@@ -0,0 +1,98 @@
1
+ const UrlJoin = require("url-join");
2
+ const HttpClient = require("./HttpClient");
3
+ const UUID = require("uuid").v4;
4
+
5
+ const {ValidateParameters} = require("./Validation");
6
+
7
+ const ContentObjectAudit = {
8
+ async AuditContentObject({client, libraryId, objectId, versionHash, salt, samples, live=false}) {
9
+ if(!salt){
10
+ salt = client.utils.B64(UUID());
11
+ }
12
+
13
+ if(!samples) {
14
+ samples = [
15
+ Math.random() * 0.33,
16
+ Math.random() * 0.33 + 0.33,
17
+ Math.random() * 0.33 + 0.66
18
+ ];
19
+ }
20
+
21
+ // Ensure only a max of 3 samples
22
+ samples = samples.slice(0, 3);
23
+
24
+ if(versionHash) {
25
+ objectId = client.utils.DecodeVersionHash(versionHash).objectId;
26
+ }
27
+
28
+ if(!libraryId) {
29
+ libraryId = await client.ContentObjectLibraryId({objectId});
30
+ }
31
+
32
+ ValidateParameters({libraryId, objectId, versionHash});
33
+
34
+ let queryParams = {salt, samples};
35
+
36
+
37
+ if(live) {
38
+ queryParams.now = Date.now();
39
+ }
40
+
41
+ // Test against the node the client is currently using, plus a batch of fresh nodes
42
+ const uris = [
43
+ client.HttpClient.uris[client.HttpClient.uriIndex],
44
+ ...(await client.Configuration({
45
+ configUrl: client.configUrl,
46
+ clientIP: client.clientIP,
47
+ region: client.region
48
+ })).fabricURIs
49
+ ]
50
+ .filter((v, i, s) => s.indexOf(v) === i);
51
+
52
+ const httpClient = new HttpClient({uris});
53
+
54
+ let path = UrlJoin("qlibs", libraryId, "q", versionHash || objectId, live ? "call/live/audit" : "audit");
55
+ let responses = await httpClient.RequestAll({
56
+ headers: await client.authClient.AuthorizationHeader({libraryId, objectId, versionHash}),
57
+ queryParams: queryParams,
58
+ method: "GET",
59
+ path
60
+ });
61
+
62
+ let auditHash, verified;
63
+ let audits = [];
64
+ for(const response of responses) {
65
+ let url = new URL(response.url);
66
+ let audit = { host: url.hostname };
67
+
68
+ if(!response.ok) {
69
+ audit.error = response;
70
+ audit.errorMessage = response.message || JSON.stringify(response);
71
+ } else {
72
+ let res = await client.utils.ResponseToJson(response);
73
+ if(auditHash === undefined) {
74
+ auditHash = res.audit_hash;
75
+ } else if(res.audit_hash !== auditHash) {
76
+ verified = false;
77
+ } else if(verified === undefined) {
78
+ verified = true;
79
+ }
80
+
81
+ audit.audit_hash = res.audit_hash;
82
+ }
83
+
84
+ audits.push(audit);
85
+ }
86
+
87
+ verified = verified || false;
88
+
89
+ return {
90
+ verified,
91
+ salt,
92
+ samples,
93
+ audits,
94
+ };
95
+ },
96
+ };
97
+
98
+ module.exports = ContentObjectAudit;
package/src/ElvClient.js CHANGED
@@ -9,8 +9,6 @@ const EthClient = require("./EthClient");
9
9
  const UserProfileClient = require("./UserProfileClient");
10
10
  const HttpClient = require("./HttpClient");
11
11
  const RemoteSigner = require("./RemoteSigner");
12
-
13
- // const ContentObjectVerification = require("./ContentObjectVerification");
14
12
  const Utils = require("./Utils");
15
13
  const Crypto = require("./Crypto");
16
14
  const {LogMessage} = require("./LogMessage");
@@ -161,6 +159,8 @@ class ElvClient {
161
159
  assumeV3=false,
162
160
  service="default"
163
161
  }) {
162
+ this.Configuration = ElvClient.Configuration;
163
+
164
164
  this.utils = Utils;
165
165
 
166
166
  this.contentSpaceId = contentSpaceId;
@@ -390,6 +390,8 @@ class ElvClient {
390
390
  });
391
391
 
392
392
  client.configUrl = configUrl;
393
+ client.region = region;
394
+ client.clientIP = clientIP;
393
395
 
394
396
  return client;
395
397
  }
@@ -469,6 +471,8 @@ class ElvClient {
469
471
  region
470
472
  });
471
473
 
474
+ this.region = region;
475
+
472
476
  this.authServiceURIs = authServiceURIs;
473
477
  this.fabricURIs = fabricURIs;
474
478
  this.ethereumURIs = ethereumURIs;
@@ -318,6 +318,8 @@ class FrameClient {
318
318
  "AddContentObjectGroupPermission",
319
319
  "AddLibraryContentType",
320
320
  "AssetMetadata",
321
+ "AuditContentObject",
322
+ "AuditStream",
321
323
  "AvailableDRMs",
322
324
  "AvailableOfferings",
323
325
  "AwaitPending",
@@ -497,7 +499,6 @@ class FrameClient {
497
499
  "UploadPartChunk",
498
500
  "UploadStatus",
499
501
  "UseRegion",
500
- "VerifyContentObject",
501
502
  "Visibility",
502
503
  "WriteTokenNodeUrl"
503
504
  ];
package/src/HttpClient.js CHANGED
@@ -15,8 +15,9 @@ class HttpClient {
15
15
  this.retries = Math.max(3, uris.length);
16
16
  }
17
17
 
18
- BaseURI() {
19
- return new URI(this.uris[this.uriIndex]);
18
+ BaseURI(uriIndex) {
19
+ if(uriIndex === undefined) { uriIndex = this.uriIndex; }
20
+ return new URI(this.uris[uriIndex]);
20
21
  }
21
22
 
22
23
  static Fetch(url, params={}) {
@@ -59,9 +60,10 @@ class HttpClient {
59
60
  attempts=0,
60
61
  allowFailover=true,
61
62
  forceFailover=false,
62
- allowRetry=true
63
+ allowRetry=true,
64
+ uriIndex
63
65
  }) {
64
- let baseURI = this.BaseURI();
66
+ let baseURI = this.BaseURI(uriIndex);
65
67
 
66
68
  // If URL contains a write token, it must go to the correct server and can not fail over
67
69
  const writeTokenMatch = path.replace(/^\//, "").match(/(qlibs\/ilib[a-zA-Z0-9]+|q|qid)\/(tqw__[a-zA-Z0-9]+)/);
@@ -141,6 +143,7 @@ class HttpClient {
141
143
  bodyType,
142
144
  headers,
143
145
  attempts: attempts + 1,
146
+ uriIndex,
144
147
  forceFailover
145
148
  });
146
149
  }
@@ -176,6 +179,35 @@ class HttpClient {
176
179
  return response;
177
180
  }
178
181
 
182
+ async RequestAll({
183
+ method,
184
+ path,
185
+ queryParams={},
186
+ body,
187
+ bodyType="JSON",
188
+ headers={},
189
+ }) {
190
+ return await Promise.all(
191
+ Array.from(new Array(this.uris.length).keys())
192
+ .map(async uriIndex => {
193
+ try {
194
+ return await this.Request({
195
+ method,
196
+ path,
197
+ queryParams,
198
+ body,
199
+ bodyType,
200
+ headers,
201
+ allowFailover: false,
202
+ uriIndex
203
+ });
204
+ } catch(error) {
205
+ return error;
206
+ }
207
+ })
208
+ );
209
+ }
210
+
179
211
  URL({path, queryParams={}}) {
180
212
  let baseURI = this.BaseURI();
181
213
 
@@ -8,6 +8,7 @@ const UrlJoin = require("url-join");
8
8
  const objectPath = require("object-path");
9
9
 
10
10
  const HttpClient = require("../HttpClient");
11
+ const ContentObjectAudit = require("../ContentObjectAudit");
11
12
 
12
13
  const {
13
14
  ValidateLibrary,
@@ -3054,24 +3055,26 @@ exports.Collection = async function({collectionType}) {
3054
3055
  /* Verification */
3055
3056
 
3056
3057
  /**
3057
- * Verify the specified content object
3058
+ * Audit the specified content object against several content fabric nodes
3058
3059
  *
3059
3060
  * @methodGroup Content Objects
3060
3061
  * @namedParams
3061
- * @param {string} libraryId - ID of the library
3062
- * @param {string} objectId - ID of the object
3063
- * @param {string} versionHash - Hash of the content object version
3062
+ * @param {string=} libraryId - ID of the library
3063
+ * @param {string=} objectId - ID of the object
3064
+ * @param {string=} versionHash - Version hash of the object -- if not specified, latest version is returned
3065
+ * @param {string=} salt - base64-encoded byte sequence for salting the audit hash
3066
+ * @param {Array<number>=} samples - list of percentages (0.0 - <1.0) used for sampling the content part list, up to 3
3064
3067
  *
3065
- * @returns {Promise<Object>} - Response describing verification results
3068
+ * @returns {Promise<Object>} - Response describing audit results
3066
3069
  */
3067
- exports.VerifyContentObject = async function({libraryId, objectId, versionHash}) {
3068
- ValidateParameters({libraryId, objectId, versionHash});
3069
-
3070
- return await ContentObjectVerification.VerifyContentObject({
3070
+ exports.AuditContentObject = async function({libraryId, objectId, versionHash, salt, samples}) {
3071
+ return await ContentObjectAudit.AuditContentObject({
3071
3072
  client: this,
3072
3073
  libraryId,
3073
3074
  objectId,
3074
- versionHash
3075
+ versionHash,
3076
+ salt,
3077
+ samples
3075
3078
  });
3076
3079
  };
3077
3080
 
@@ -157,7 +157,7 @@ class LiveConf {
157
157
  recordingBitrate: Math.max(this.probeData.streams[index].bit_rate, 128000),
158
158
  recordingChannels: this.probeData.streams[index].channels,
159
159
  playoutLabel: `Audio ${index}`
160
- }
160
+ };
161
161
  }
162
162
  }
163
163
  return audioStreams;
@@ -382,36 +382,36 @@ class LiveConf {
382
382
  return sync_id;
383
383
  }
384
384
 
385
- /*
385
+ /*
386
386
  * Generate audio streams recording configuration based on the optional custom settings.
387
387
  * If no custom "audio" section is present, record all the acceptable audio streams found in the probe
388
388
  */
389
389
  generateAudioStreamsConfig({customSettings}) {
390
390
 
391
391
  let audioStreams = {};
392
- if (customSettings && customSettings.audio) {
393
- for (let i = 0; i < Object.keys(customSettings.audio).length; i ++) {
392
+ if(customSettings && customSettings.audio) {
393
+ for(let i = 0; i < Object.keys(customSettings.audio).length; i ++) {
394
394
  let audioIdx = Object.keys(customSettings.audio)[i];
395
395
  let audio = customSettings.audio[audioIdx];
396
396
  audioStreams[audioIdx] = {
397
397
  recordingBitrate: audio.recording_bitrate || 192000,
398
398
  recordingChannels: audio.recording_channels || 2,
399
399
  };
400
- if (audio.playout) {
401
- audioStreams[audioIdx].playoutLabel = audio.playout_label || `Audio ${audioIdx}`
400
+ if(audio.playout) {
401
+ audioStreams[audioIdx].playoutLabel = audio.playout_label || `Audio ${audioIdx}`;
402
402
  }
403
403
  }
404
404
  }
405
405
 
406
406
  // If no audio streams specified in custom config, set up all the suitable audio streams in the probe
407
- if (!customSettings.audio) {
407
+ if(!customSettings.audio) {
408
408
  audioStreams = this.getAudioStreamsFromProbe();
409
409
  }
410
410
 
411
411
  return audioStreams;
412
412
  }
413
413
 
414
- /*
414
+ /*
415
415
  * Generate the live recording config as required by QFAB, based on defaults and optional custom settings.
416
416
  */
417
417
  generateLiveConf({customSettings}) {
@@ -439,7 +439,7 @@ class LiveConf {
439
439
  conf.live_recording.recording_config.recording_params.xc_params.enc_height = videoStream.height;
440
440
  conf.live_recording.recording_config.recording_params.xc_params.enc_width = videoStream.width;
441
441
 
442
- for (let i =0; i < Object.keys(audioStreams).length; i ++) {
442
+ for(let i =0; i < Object.keys(audioStreams).length; i ++) {
443
443
  conf.live_recording.recording_config.recording_params.xc_params.audio_index[i] = parseInt(Object.keys(audioStreams)[i]);
444
444
  }
445
445
 
@@ -549,7 +549,7 @@ class LiveConf {
549
549
  let globalAudioBitrate = 0;
550
550
  let nAudio = 0;
551
551
 
552
- for (let i = 0; i < Object.keys(audioStreams).length; i ++ ) {
552
+ for(let i = 0; i < Object.keys(audioStreams).length; i ++ ) {
553
553
  let audioLadderSpec = {...LadderSpecAudio};
554
554
  const audioIndex = Object.keys(audioStreams)[i];
555
555
  const audio = audioStreams[audioIndex];
@@ -561,7 +561,7 @@ class LiveConf {
561
561
  audioLadderSpec.stream_label = audio.playoutLabel ? audio.playoutLabel : null;
562
562
 
563
563
  conf.live_recording.recording_config.recording_params.ladder_specs.push(audioLadderSpec);
564
- if (audio.recordingBitrate > globalAudioBitrate) {
564
+ if(audio.recordingBitrate > globalAudioBitrate) {
565
565
  globalAudioBitrate = audio.recordingBitrate;
566
566
  }
567
567
  nAudio ++;
@@ -10,6 +10,7 @@ const fs = require("fs");
10
10
  const HttpClient = require("../HttpClient");
11
11
  const Fraction = require("fraction.js");
12
12
  const {ValidateObject, ValidatePresence} = require("../Validation");
13
+ const ContentObjectAudit = require("../ContentObjectAudit");
13
14
 
14
15
  const MakeTxLessToken = async({client, libraryId, objectId, versionHash}) => {
15
16
  const tok = await client.authClient.AuthorizationToken({libraryId, objectId,
@@ -445,7 +446,7 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
445
446
  let videoLastFinalizationTimeEpochSec = -1;
446
447
  let videoFinalizedParts = 0;
447
448
  let sinceLastFinalize = -1;
448
- if (period.finalized_parts_info && period.finalized_parts_info.video && period.finalized_parts_info.video.last_finalization_time) {
449
+ if(period.finalized_parts_info && period.finalized_parts_info.video && period.finalized_parts_info.video.last_finalization_time) {
449
450
  videoLastFinalizationTimeEpochSec = period.finalized_parts_info.video.last_finalization_time / 1000000;
450
451
  videoFinalizedParts = period.finalized_parts_info.video.n_parts;
451
452
  sinceLastFinalize = Math.floor(new Date().getTime() / 1000) - videoLastFinalizationTimeEpochSec;
@@ -733,7 +734,7 @@ exports.StreamCreate = async function({name, start=false}) {
733
734
  */
734
735
  exports.StreamStartOrStopOrReset = async function({name, op}) {
735
736
  try {
736
- let status = await this.StreamStatus({name})
737
+ let status = await this.StreamStatus({name});
737
738
  if(status.state != "stopped") {
738
739
  if(op === "start") {
739
740
  status.error = "Unable to start stream - state: " + status.state;
@@ -1937,3 +1938,27 @@ exports.StreamAddWatermark = async function({
1937
1938
 
1938
1939
  return response;
1939
1940
  };
1941
+
1942
+ /**
1943
+ * Audit the specified live stream against several content fabric nodes
1944
+ *
1945
+ * @methodGroup Live Stream
1946
+ * @namedParams
1947
+ * @param {string=} objectId - Object ID of the live stream
1948
+ * @param {string=} versionHash - Version hash of the live stream -- if not specified, latest version is returned
1949
+ * @param {string=} salt - base64-encoded byte sequence for salting the audit hash
1950
+ * @param {Array<number>=} samples - list of percentages (0.0 - <1.0) used for sampling the content part list, up to 3
1951
+ *
1952
+ * @returns {Promise<Object>} - Response describing audit results
1953
+ */
1954
+ exports.AuditStream = async function({objectId, versionHash, salt, samples}) {
1955
+ return await ContentObjectAudit.AuditContentObject({
1956
+ client: this,
1957
+ libraryId,
1958
+ objectId,
1959
+ versionHash,
1960
+ salt,
1961
+ samples,
1962
+ live: true
1963
+ });
1964
+ };
@@ -1,210 +0,0 @@
1
- const CBOR = require("cbor");
2
- const SJCL = require("sjcl");
3
- const MultiHash = require("multihashes");
4
- const DeepEqual = require("deep-equal");
5
- const Utils = require("./Utils");
6
-
7
- const ContentObjectVerification = {
8
- async VerifyContentObject({client, libraryId, objectId, versionHash}) {
9
- let response = {
10
- hash: versionHash
11
- };
12
-
13
- const partHash = Utils.DecodeVersionHash(versionHash).partHash;
14
-
15
- const qpartsResponse = await client.QParts({libraryId, objectId, partHash, format: "arrayBuffer"})
16
- .then(response => Buffer.from(response));
17
- const partVerification = ContentObjectVerification._VerifyPart({partHash: partHash, qpartsResponse: qpartsResponse});
18
-
19
- if(partVerification.valid) {
20
- response.qref = { valid: true };
21
- } else {
22
- response.qref = { valid: false, error: partVerification.error.message };
23
- }
24
-
25
- response.qref.hash = partHash;
26
-
27
- if(response.qref.valid) {
28
- // Validate Metadata
29
- const qmdHash = partVerification.cbor.QmdHash.value;
30
- const metadataPartHash = "hqp_" + MultiHash.toB58String(qmdHash.slice(1, qmdHash.length));
31
- const metadataPartResponse = await client.QParts({libraryId, objectId, partHash: metadataPartHash, format: "arrayBuffer"})
32
- .then(response => Buffer.from(response));
33
-
34
- const metadataVerification = ContentObjectVerification._VerifyPart({partHash: metadataPartHash, qpartsResponse: metadataPartResponse});
35
-
36
- if(metadataVerification.valid) {
37
- response.qmd = { valid: true };
38
- } else {
39
- response.qmd = { valid: false, error: metadataVerification.error.message };
40
- }
41
-
42
- response.qmd.hash = metadataPartHash;
43
-
44
- if(response.qmd.valid && libraryId) {
45
- // If the library ID is provided, compare some metadata in the CBOR response
46
- // to the metadata from the /meta endpoint
47
- const metadata = await client.ContentObjectMetadata({
48
- libraryId: libraryId,
49
- objectId,
50
- versionHash: partHash.replace("hqp_", "hq__")
51
- });
52
-
53
- response.qmd.check = ContentObjectVerification._VerifyMetadata({metadataCbor: metadataVerification.cbor, metadata: metadata});
54
- }
55
-
56
- // Validate Qstruct
57
-
58
- const qstructHash = partVerification.cbor.QstructHash.value;
59
- const structPartHash = "hqp_" + MultiHash.toB58String(qstructHash.slice(1, qstructHash.length));
60
- const structPartResponse = await client.QParts({libraryId, objectId, partHash: structPartHash, format: "arrayBuffer"})
61
- .then(response => Buffer.from(response));
62
- const structVerification = ContentObjectVerification._VerifyPart({partHash: structPartHash, qpartsResponse: structPartResponse});
63
-
64
- if(structVerification.valid) {
65
- response.qstruct = { valid: true };
66
- } else {
67
- response.qstruct = { valid: false, error: structVerification.error.message };
68
- }
69
-
70
- response.qstruct.hash = structPartHash;
71
-
72
- if(response.qstruct.valid) {
73
- response.qstruct.parts = ContentObjectVerification._FormatQStruct(structVerification.cbor.Parts);
74
- }
75
- }
76
-
77
- response.valid =
78
- response.qref.valid &&
79
- response.qmd.valid &&
80
- response.qstruct.valid &&
81
- (!response.qmd.check || response.qmd.check.valid);
82
-
83
- return response;
84
- },
85
-
86
- // Content verification methods //
87
-
88
- _FormatQStruct(structParts) {
89
- if(!structParts) { return []; }
90
-
91
- return structParts.map(structPart => {
92
- return {
93
- hash: "hqp_" + MultiHash.toB58String(structPart.Hash.value.slice(1, structPart.Hash.length)),
94
- size: structPart.Size
95
- };
96
- });
97
- },
98
-
99
- _Hash(thing) {
100
- function fromBits(arr) {
101
- var out = [], bl = SJCL.bitArray.bitLength(arr), i, tmp;
102
- for(i=0; i<bl/8; i++) {
103
- if((i&3) === 0) {
104
- tmp = arr[i/4];
105
- }
106
- out.push(tmp >>> 24);
107
- tmp <<= 8;
108
- }
109
- return out;
110
- }
111
-
112
- function toBits(bytes) {
113
- var out = [], i, tmp=0;
114
- for(i=0; i<bytes.length; i++) {
115
- tmp = tmp << 8 | bytes[i];
116
- if((i&3) === 3) {
117
- out.push(tmp);
118
- tmp = 0;
119
- }
120
- }
121
- if(i&3) {
122
- out.push(SJCL.bitArray.partial(8*(i&3), tmp));
123
- }
124
- return out;
125
- }
126
-
127
- let digest = SJCL.hash.sha256.hash(toBits(thing));
128
- let bytes = fromBits(digest);
129
- let out = Buffer.from(bytes, "binary");
130
-
131
- return MultiHash.toB58String(MultiHash.encode(out, "sha2-256"));
132
- },
133
-
134
- _ParseCBOR(cborResponse) {
135
- let buffer = cborResponse.slice(7, cborResponse.length);
136
- let hex = buffer.toString("hex");
137
- return CBOR.decodeFirstSync(hex);
138
- },
139
-
140
- _VerifyPart({partHash, qpartsResponse}) {
141
- try {
142
- if(ContentObjectVerification._Hash(qpartsResponse) !== partHash.replace("hqp_", "")) {
143
- throw Error("Hashes do not match");
144
- }
145
-
146
- let cbor = ContentObjectVerification._ParseCBOR(qpartsResponse);
147
-
148
- return {
149
- valid: true,
150
- cbor: cbor
151
- };
152
- } catch(error) {
153
- return {
154
- valid: false,
155
- error: error
156
- };
157
- }
158
- },
159
-
160
- _VerifyMetadata({metadataCbor, metadata}) {
161
- if(!metadataCbor) { metadataCbor = {}; }
162
- if(!metadata) { metadata = {}; }
163
-
164
- let response = {
165
- valid: true,
166
- invalidValues: []
167
- };
168
-
169
- const cborKeys = Object.keys(metadataCbor);
170
- const metadataKeys = Object.keys(metadata);
171
-
172
- // Find any difference between top level keys
173
- const differentKeys = cborKeys
174
- .filter(x => !metadataKeys.includes(x))
175
- .concat(metadataKeys.filter(x => !cborKeys.includes(x)));
176
-
177
- for(const key of differentKeys) {
178
- const cborValue = metadataCbor[key];
179
- const metadataValue = metadata[key];
180
-
181
- response.invalidValues.push({
182
- key: key,
183
- cborValue: cborValue,
184
- metadataValue: metadataValue
185
- });
186
- }
187
-
188
- // Deep comparison of up to 5 keys
189
- for(const fieldToValidate of Object.keys(metadataCbor).slice(0, 5)) {
190
- const cborValue = metadataCbor[fieldToValidate];
191
- const metadataValue = metadata[fieldToValidate];
192
-
193
- if(!DeepEqual(cborValue, metadataValue)) {
194
- response.invalidValues.push({
195
- key: fieldToValidate,
196
- cborValue: cborValue,
197
- metadataValue: metadataValue
198
- });
199
- }
200
- }
201
-
202
- if(response.invalidValues.length !== 0) {
203
- response.valid = false;
204
- }
205
-
206
- return response;
207
- },
208
- };
209
-
210
- module.exports = ContentObjectVerification;