@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 +1 -1
- package/src/ContentObjectAudit.js +98 -0
- package/src/ElvClient.js +6 -2
- package/src/FrameClient.js +2 -1
- package/src/HttpClient.js +36 -4
- package/src/client/ContentAccess.js +13 -10
- package/src/client/LiveConf.js +11 -11
- package/src/client/LiveStream.js +27 -2
- package/src/ContentObjectVerification.js +0 -210
package/package.json
CHANGED
|
@@ -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;
|
package/src/FrameClient.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
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 -
|
|
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
|
|
3068
|
+
* @returns {Promise<Object>} - Response describing audit results
|
|
3066
3069
|
*/
|
|
3067
|
-
exports.
|
|
3068
|
-
|
|
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
|
|
package/src/client/LiveConf.js
CHANGED
|
@@ -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
|
|
393
|
-
for
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
564
|
+
if(audio.recordingBitrate > globalAudioBitrate) {
|
|
565
565
|
globalAudioBitrate = audio.recordingBitrate;
|
|
566
566
|
}
|
|
567
567
|
nAudio ++;
|
package/src/client/LiveStream.js
CHANGED
|
@@ -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
|
|
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;
|