@eluvio/elv-client-js 4.0.85 → 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/dist/ElvClient-min.js +12 -13
- package/dist/ElvClient-node-min.js +13 -14
- package/dist/ElvFrameClient-min.js +9 -9
- package/dist/ElvPermissionsClient-min.js +10 -10
- package/dist/ElvWalletClient-min.js +13 -14
- package/dist/ElvWalletClient-node-min.js +13 -14
- package/dist/src/AuthorizationClient.js +5 -4
- package/dist/src/Crypto.js +2 -2
- package/dist/src/ElvClient.js +2 -2
- package/dist/src/EthClient.js +2 -2
- package/dist/src/FrameClient.js +3 -3
- package/dist/src/PermissionsClient.js +2 -2
- package/dist/src/Utils.js +2 -2
- package/dist/src/abr_profiles/abr_profile_live_to_vod.js +0 -7
- package/dist/src/client/ABRPublishing.js +2 -2
- package/dist/src/client/AccessGroups.js +2 -2
- package/dist/src/client/ContentAccess.js +757 -821
- package/dist/src/client/ContentManagement.js +6 -59
- package/dist/src/client/Contracts.js +2 -2
- package/dist/src/client/Files.js +2 -2
- package/dist/src/client/LiveConf.js +35 -144
- package/dist/src/client/LiveStream.js +529 -1054
- package/dist/src/client/NFT.js +2 -2
- package/dist/src/walletClient/ClientMethods.js +2 -2
- package/dist/src/walletClient/Profile.js +2 -2
- package/dist/src/walletClient/Utils.js +2 -2
- package/dist/src/walletClient/index.js +2 -2
- package/package.json +1 -1
- package/src/ContentObjectAudit.js +98 -0
- package/src/ElvClient.js +86 -83
- package/src/FrameClient.js +2 -1
- package/src/HttpClient.js +36 -4
- package/src/RemoteSigner.js +54 -0
- package/src/client/ContentAccess.js +17 -14
- package/src/client/ContentManagement.js +0 -1
- package/src/client/LiveConf.js +13 -13
- package/src/client/LiveStream.js +40 -12
- package/src/walletClient/index.js +40 -13
- package/src/ContentObjectVerification.js +0 -210
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;
|
|
@@ -232,13 +232,13 @@ class LiveConf {
|
|
|
232
232
|
return seg;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
|
|
235
|
+
/*
|
|
236
236
|
* Calculate output timebase from the encoder (codec) timebase. The videoTimeBase parameter
|
|
237
237
|
* represents the encoder timebase. The format muxer will change it so it is greater than 10000.
|
|
238
238
|
*/
|
|
239
239
|
calcOutputTimebase(codecTimebase) {
|
|
240
240
|
let outputTimebase = codecTimebase;
|
|
241
|
-
while
|
|
241
|
+
while(outputTimebase < 10000)
|
|
242
242
|
outputTimebase = outputTimebase * 2;
|
|
243
243
|
return outputTimebase;
|
|
244
244
|
}
|
|
@@ -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,
|
|
@@ -29,18 +30,18 @@ const CueInfo = async ({eventId, status}) => {
|
|
|
29
30
|
const lroStatusResponse = await this.utils.ResponseToJson(
|
|
30
31
|
await HttpClient.Fetch(status.lro_status_url)
|
|
31
32
|
);
|
|
32
|
-
console.log("lroStatusResponse", lroStatusResponse)
|
|
33
|
+
console.log("lroStatusResponse", lroStatusResponse);
|
|
33
34
|
cues = lroStatusResponse.custom.cues;
|
|
34
|
-
} catch
|
|
35
|
+
} catch(error) {
|
|
35
36
|
console.log("LRO status failed", error);
|
|
36
37
|
return {error: "failed to retrieve status", eventId};
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
let eventStart, eventEnd;
|
|
40
|
-
for
|
|
41
|
-
for
|
|
42
|
-
if
|
|
43
|
-
switch
|
|
41
|
+
for(const value of Object.values(cues)) {
|
|
42
|
+
for(const event of Object.values(value.descriptors)) {
|
|
43
|
+
if(event.id == eventId) {
|
|
44
|
+
switch(event.type_id) {
|
|
44
45
|
case 32:
|
|
45
46
|
case 16:
|
|
46
47
|
eventStart = value.insertion_time;
|
|
@@ -56,7 +57,7 @@ const CueInfo = async ({eventId, status}) => {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
return {eventStart, eventEnd, eventId};
|
|
59
|
-
}
|
|
60
|
+
};
|
|
60
61
|
|
|
61
62
|
/**
|
|
62
63
|
* Set the offering for the live stream
|
|
@@ -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;
|
|
@@ -860,7 +861,7 @@ exports.StreamStopSession = async function({name}) {
|
|
|
860
861
|
return {
|
|
861
862
|
state: status.state,
|
|
862
863
|
error: "The stream must be stopped before terminating"
|
|
863
|
-
}
|
|
864
|
+
};
|
|
864
865
|
}
|
|
865
866
|
|
|
866
867
|
await this.DeleteWriteToken({
|
|
@@ -1355,7 +1356,10 @@ exports.StreamConfig = async function({name, customSettings={}, probeMetadata})
|
|
|
1355
1356
|
status.error = "No node matching stream URL " + streamUrl.href;
|
|
1356
1357
|
return status;
|
|
1357
1358
|
}
|
|
1358
|
-
const node =
|
|
1359
|
+
const node = {
|
|
1360
|
+
endpoints: nodes[0].services.fabric_api.urls,
|
|
1361
|
+
id: nodes[0].id
|
|
1362
|
+
};
|
|
1359
1363
|
status.node = node;
|
|
1360
1364
|
let endpoint = node.endpoints[0];
|
|
1361
1365
|
|
|
@@ -1547,7 +1551,7 @@ exports.StreamListUrls = async function({siteId}={}) {
|
|
|
1547
1551
|
url,
|
|
1548
1552
|
active: activeUrlMap[url] || false
|
|
1549
1553
|
};
|
|
1550
|
-
})
|
|
1554
|
+
});
|
|
1551
1555
|
});
|
|
1552
1556
|
|
|
1553
1557
|
return streamUrlStatus;
|
|
@@ -1934,3 +1938,27 @@ exports.StreamAddWatermark = async function({
|
|
|
1934
1938
|
|
|
1935
1939
|
return response;
|
|
1936
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
|
+
};
|
|
@@ -395,7 +395,15 @@ class ElvWalletClient {
|
|
|
395
395
|
*
|
|
396
396
|
* @methodGroup Login
|
|
397
397
|
*/
|
|
398
|
-
LogOut() {
|
|
398
|
+
async LogOut() {
|
|
399
|
+
if(this.__authorization.nonce) {
|
|
400
|
+
try {
|
|
401
|
+
await this.client.signer.ReleaseCSAT({accessToken: this.AuthToken()});
|
|
402
|
+
} catch(error) {
|
|
403
|
+
this.Log("Failed to release token", true, error);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
399
407
|
this.__authorization = {};
|
|
400
408
|
this.loggedIn = false;
|
|
401
409
|
|
|
@@ -410,6 +418,14 @@ class ElvWalletClient {
|
|
|
410
418
|
}
|
|
411
419
|
}
|
|
412
420
|
|
|
421
|
+
async TokenStatus() {
|
|
422
|
+
if(!this.__authorization || !this.__authorization.nonce) {
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return await this.client.signer.CSATStatus({accessToken: this.AuthToken()});
|
|
427
|
+
}
|
|
428
|
+
|
|
413
429
|
/**
|
|
414
430
|
* Authenticate with an ElvWalletClient authorization token
|
|
415
431
|
*
|
|
@@ -457,7 +473,7 @@ class ElvWalletClient {
|
|
|
457
473
|
* - signingToken - Identical to `authToken`, but also includes the ability to perform arbitrary signatures with the custodial wallet. This token should be protected and should not be
|
|
458
474
|
* shared with third parties.
|
|
459
475
|
*/
|
|
460
|
-
async AuthenticateOAuth({idToken, tenantId, email, signerURIs, shareEmail=false}) {
|
|
476
|
+
async AuthenticateOAuth({idToken, tenantId, email, signerURIs, shareEmail=false, extraData={}, nonce, createRemoteToken=true, force=false}) {
|
|
461
477
|
let tokenDuration = 24;
|
|
462
478
|
|
|
463
479
|
if(!tenantId && this.selectedMarketplaceInfo) {
|
|
@@ -466,13 +482,21 @@ class ElvWalletClient {
|
|
|
466
482
|
tenantId = this.selectedMarketplaceInfo.tenantId;
|
|
467
483
|
}
|
|
468
484
|
|
|
469
|
-
await this.client.SetRemoteSigner({idToken, tenantId, signerURIs, extraData: { share_email: shareEmail }, unsignedPublicAuth: true});
|
|
485
|
+
await this.client.SetRemoteSigner({idToken, tenantId, signerURIs, extraData: { ...extraData, share_email: shareEmail }, unsignedPublicAuth: true});
|
|
470
486
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
487
|
+
let fabricToken, expiresAt;
|
|
488
|
+
if(createRemoteToken && this.client.signer.remoteSigner) {
|
|
489
|
+
expiresAt = Date.now() + 24 * 60 * 60 * 1000;
|
|
490
|
+
const tokenResponse = await this.client.signer.RetrieveCSAT({email, nonce, force});
|
|
491
|
+
fabricToken = tokenResponse.token;
|
|
492
|
+
nonce = tokenResponse.nonce;
|
|
493
|
+
} else {
|
|
494
|
+
expiresAt = Date.now() + tokenDuration * 60 * 60 * 1000;
|
|
495
|
+
fabricToken = await this.client.CreateFabricToken({
|
|
496
|
+
duration: tokenDuration * 60 * 60 * 1000,
|
|
497
|
+
context: email ? {usr: {email}} : {}
|
|
498
|
+
});
|
|
499
|
+
}
|
|
476
500
|
const address = this.client.utils.FormatAddress(this.client.CurrentAccountAddress());
|
|
477
501
|
|
|
478
502
|
if(!email) {
|
|
@@ -496,7 +520,8 @@ class ElvWalletClient {
|
|
|
496
520
|
signerURIs,
|
|
497
521
|
walletType: "Custodial",
|
|
498
522
|
walletName: "Eluvio",
|
|
499
|
-
register: true
|
|
523
|
+
register: true,
|
|
524
|
+
nonce
|
|
500
525
|
}),
|
|
501
526
|
signingToken: this.SetAuthorization({
|
|
502
527
|
clusterToken: this.client.signer.authToken,
|
|
@@ -507,7 +532,8 @@ class ElvWalletClient {
|
|
|
507
532
|
expiresAt,
|
|
508
533
|
signerURIs,
|
|
509
534
|
walletType: "Custodial",
|
|
510
|
-
walletName: "Eluvio"
|
|
535
|
+
walletName: "Eluvio",
|
|
536
|
+
nonce
|
|
511
537
|
})
|
|
512
538
|
};
|
|
513
539
|
}
|
|
@@ -568,7 +594,7 @@ class ElvWalletClient {
|
|
|
568
594
|
return this.__authorization.fabricToken;
|
|
569
595
|
}
|
|
570
596
|
|
|
571
|
-
SetAuthorization({clusterToken, fabricToken, tenantId, address, email, expiresAt, signerURIs, walletType, walletName, register=false}) {
|
|
597
|
+
SetAuthorization({clusterToken, fabricToken, tenantId, address, email, expiresAt, signerURIs, walletType, walletName, nonce, register=false}) {
|
|
572
598
|
address = this.client.utils.FormatAddress(address);
|
|
573
599
|
|
|
574
600
|
this.__authorization = {
|
|
@@ -578,7 +604,8 @@ class ElvWalletClient {
|
|
|
578
604
|
email,
|
|
579
605
|
expiresAt,
|
|
580
606
|
walletType,
|
|
581
|
-
walletName
|
|
607
|
+
walletName,
|
|
608
|
+
nonce
|
|
582
609
|
};
|
|
583
610
|
|
|
584
611
|
if(clusterToken) {
|
|
@@ -1328,7 +1355,7 @@ class ElvWalletClient {
|
|
|
1328
1355
|
|
|
1329
1356
|
if(op === "nft-claim-entitlement") {
|
|
1330
1357
|
let [op, marketplace, sku, purchaseId ] = status.op.split(":");
|
|
1331
|
-
confirmationId = purchaseId
|
|
1358
|
+
confirmationId = purchaseId;
|
|
1332
1359
|
if(status.extra && status.extra["0"]) {
|
|
1333
1360
|
address = status.extra["0"].token_addr;
|
|
1334
1361
|
tokenId = status.extra["0"].token_id;
|
|
@@ -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;
|