From 95b91ad6edcd53dd70db328097c25990f61acb88 Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Mon, 25 Sep 2023 14:02:41 +0400 Subject: [PATCH 01/16] [backend] save file hashes --- backend/main/main-canister.mo | 53 +++++++++++++++++++++++++++++++---- mops.toml | 1 + 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/backend/main/main-canister.mo b/backend/main/main-canister.mo index 2a66b7e7..494d7d91 100644 --- a/backend/main/main-canister.mo +++ b/backend/main/main-canister.mo @@ -21,8 +21,8 @@ import Prim "mo:prim"; import {DAY} "mo:time-consts"; import {ic} "mo:ic"; import Map "mo:map/Map"; - import Backup "mo:backup"; +import Sha256 "mo:sha2/Sha256"; import Utils "../utils"; import Semver "./semver"; @@ -69,6 +69,7 @@ actor { var packageConfigs = TrieMap.TrieMap(Text.equal, Text.hash); var packagePublications = TrieMap.TrieMap(Text.equal, Text.hash); var fileIdsByPackage = TrieMap.TrieMap(Text.equal, Text.hash); + var hashByFileId = TrieMap.TrieMap(Text.equal, Text.hash); var packageFileStats = TrieMap.TrieMap(Text.equal, Text.hash); var packageTestStats = TrieMap.TrieMap(Text.equal, Text.hash); var packageNotes = TrieMap.TrieMap(Text.equal, Text.hash); @@ -98,6 +99,7 @@ actor { let publishingPackageFileStats = TrieMap.TrieMap(Text.equal, Text.hash); let publishingTestStats = TrieMap.TrieMap(Text.equal, Text.hash); let publishingNotes = TrieMap.TrieMap(Text.equal, Text.hash); + let publishingFileHashers = TrieMap.TrieMap(Text.equal, Text.hash); // PRIVATE func _getHighestVersion(name : PackageName) : ?PackageVersion { @@ -419,6 +421,10 @@ actor { case (_) {}; }; + // add temp hasher + let hasher = Sha256.Digest(#sha256); + publishingFileHashers.put(fileId, hasher); + // upload first chunk if (chunkCount != 0) { let uploadRes = await storageManager.uploadChunk(publishing.storage, fileId, 0, firstChunk); @@ -428,6 +434,9 @@ actor { }; case (_) {}; }; + + // compute hash of the first chunk + hasher.writeBlob(firstChunk); }; let pubFile : PublishingFile = { @@ -497,6 +506,9 @@ actor { return pkgSizeRes; }; + let ?hasher = publishingFileHashers.get(fileId) else return #err("Hasher not found"); + hasher.writeBlob(chunk); + #ok; }; @@ -565,15 +577,24 @@ actor { file.id; }); + let publicFileIds = Array.filter(fileIds, func(fileId : Text.Text) : Bool { + not Text.endsWith(fileId, #text("docs.tgz")); + }); + + fileIdsByPackage.put(packageId, publicFileIds); + + // store file hashes + for (fileId in publicFileIds.vals()) { + let ?hasher = publishingFileHashers.get(fileId) else return #err("Hasher not found"); + hashByFileId.put(fileId, hasher.sum()); + }; + + // finish uploads let res = await storageManager.finishUploads(publishing.storage, fileIds); if (Result.isErr(res)) { return res; }; - fileIdsByPackage.put(packageId, Array.filter(fileIds, func(fileId : Text.Text) : Bool { - not Text.endsWith(fileId, #text("docs.tgz")); - })); - _updateHighestConfig(publishing.config); let versions = Option.get(packageVersions.get(publishing.config.name), []); @@ -883,6 +904,18 @@ actor { Result.fromOption(fileIdsByPackage.get(packageId), "Package '" # packageId # "' not found"); }; + public shared ({caller}) func getFileHashes(name : PackageName, version : PackageVersion) : async Result.Result<[(FileId, Blob)], Err> { + let packageId = name # "@" # version; + let ?fileIds = fileIdsByPackage.get(packageId) else return #err("Package '" # packageId # "' not found"); + + let buf = Buffer.Buffer<(FileId, Blob)>(fileIds.size()); + for (fileId in fileIds.vals()) { + let ?hash = hashByFileId.get(fileId) else return #err("File hash not found for " # fileId); + buf.add((fileId, hash)); + }; + #ok(Buffer.toArray(buf)); + }; + public shared ({caller}) func notifyInstall(name : PackageName, version : PackageVersion) { let packageId = name # "@" # version; @@ -1225,6 +1258,7 @@ actor { #packageConfigs : [(PackageId, PackageConfigV2)]; #highestConfigs : [(PackageName, PackageConfigV2)]; #fileIdsByPackage : [(PackageId, [FileId])]; + #hashByFileId : [(FileId, Blob)]; #packageTestStats : [(PackageId, TestStats)]; #packageNotes : [(PackageId, Text)]; #downloadLog : DownloadLog.Stable; @@ -1250,6 +1284,7 @@ actor { await backup.uploadChunk(to_candid(#v3(#packageVersions(Iter.toArray(packageVersions.entries()))) : BackupChunk)); await backup.uploadChunk(to_candid(#v3(#packageOwners(Iter.toArray(packageOwners.entries()))) : BackupChunk)); await backup.uploadChunk(to_candid(#v3(#fileIdsByPackage(Iter.toArray(fileIdsByPackage.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v3(#hashByFileId(Iter.toArray(hashByFileId.entries()))) : BackupChunk)); await backup.uploadChunk(to_candid(#v3(#packageTestStats(Iter.toArray(packageTestStats.entries()))) : BackupChunk)); await backup.uploadChunk(to_candid(#v3(#packageNotes(Iter.toArray(packageNotes.entries()))) : BackupChunk)); await backup.uploadChunk(to_candid(#v3(#downloadLog(downloadLog.toStable())) : BackupChunk)); @@ -1281,6 +1316,9 @@ actor { case (#fileIdsByPackage(fileIdsByPackageStable)) { fileIdsByPackage := TrieMap.fromEntries(fileIdsByPackageStable.vals(), Text.equal, Text.hash); }; + case (#hashByFileId(hashByFileIdStable)) { + hashByFileId := TrieMap.fromEntries(hashByFileIdStable.vals(), Text.equal, Text.hash); + }; case (#packageTestStats(packageTestStatsStable)) { packageTestStats := TrieMap.fromEntries(packageTestStatsStable.vals(), Text.equal, Text.hash); }; @@ -1316,6 +1354,7 @@ actor { stable var highestConfigsStableV2 : [(PackageName, PackageConfigV2)] = []; stable var fileIdsByPackageStable : [(PackageId, [FileId])] = []; + stable var hashByFileIdStable : [(FileId, Blob)] = []; stable var packageFileStatsStable : [(PackageId, PackageFileStats)] = []; stable var packageTestStatsStable : [(PackageId, TestStats)] = []; stable var packageNotesStable : [(PackageId, Text)] = []; @@ -1329,6 +1368,7 @@ actor { packageVersionsStable := Iter.toArray(packageVersions.entries()); packageOwnersStable := Iter.toArray(packageOwners.entries()); fileIdsByPackageStable := Iter.toArray(fileIdsByPackage.entries()); + hashByFileIdStable := Iter.toArray(hashByFileId.entries()); packageFileStatsStable := Iter.toArray(packageFileStats.entries()); packageTestStatsStable := Iter.toArray(packageTestStats.entries()); packageNotesStable := Iter.toArray(packageNotes.entries()); @@ -1353,6 +1393,9 @@ actor { fileIdsByPackage := TrieMap.fromEntries(fileIdsByPackageStable.vals(), Text.equal, Text.hash); fileIdsByPackageStable := []; + hashByFileId := TrieMap.fromEntries(hashByFileIdStable.vals(), Text.equal, Text.hash); + hashByFileIdStable := []; + packageFileStats := TrieMap.fromEntries(packageFileStatsStable.vals(), Text.equal, Text.hash); packageFileStatsStable := []; diff --git a/mops.toml b/mops.toml index 0a57fbdf..1a101238 100644 --- a/mops.toml +++ b/mops.toml @@ -5,6 +5,7 @@ chronosphere = "https://github.com/enzoh/chronosphere#master" map = "8.1.0" ic = "0.2.0" backup = "1.1.0" +sha2 = "0.0.4" [dev-dependencies] test = "1.0.0" From 28279ca6c60951c6fde0ffd8f287195a6ec365ee Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Wed, 18 Oct 2023 11:57:40 +0400 Subject: [PATCH 02/16] use moc 0.10.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cfa1ac3b..95327cf5 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "lint:frontend": "eslint frontend --ext .svelte,.ts", "decl:cli": "dfx generate main", "decl:frontend": "cp -r cli/declarations frontend", - "deploy": "DFX_MOC_PATH=$(mocv bin 0.10.0)/moc dfx deploy --no-wallet --identity ${IDENTITY} --network ${NETWORK}", + "deploy": "DFX_MOC_PATH=$(mocv bin 0.10.1)/moc dfx deploy --no-wallet --identity ${IDENTITY} --network ${NETWORK}", "deploy-local": "NODE_ENV=development IDENTITY=default NETWORK=local npm run deploy && npm run decl", "deploy-staging": "NODE_ENV=production IDENTITY=mops NETWORK=staging npm run deploy", "deploy-ic": "NODE_ENV=production IDENTITY=mops NETWORK=ic npm run deploy", From 66874b4de5e2bcbc037ff5edb15692079fc3c0b6 Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Wed, 18 Oct 2023 11:57:54 +0400 Subject: [PATCH 03/16] use `base`0.10.0` --- mops.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mops.toml b/mops.toml index dd5bbd3b..0458ec9c 100644 --- a/mops.toml +++ b/mops.toml @@ -1,5 +1,5 @@ [dependencies] -base = "0.9.4" +base = "0.10.0" time-consts = "1.0.0" map = "8.1.0" ic = "0.2.0" From 642b7ef3b174856557e38935c33f766cff43a721 Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Wed, 18 Oct 2023 12:11:33 +0400 Subject: [PATCH 04/16] fix ci --- .github/workflows/mops-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mops-test.yml b/.github/workflows/mops-test.yml index b6774767..07f2a015 100644 --- a/.github/workflows/mops-test.yml +++ b/.github/workflows/mops-test.yml @@ -10,9 +10,9 @@ on: jobs: test: strategy: - max-parallel: 2 + max-parallel: 3 matrix: - moc-version: [0.9.3, latest] + moc-version: [0.10.0] mops-version: [./cli, ic-mops@latest, ic-mops@0.27.1] node-version: [16, 20] From 7f0e94a1d1a53e0ab90dcd9ddd876bc2032c1e31 Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Wed, 18 Oct 2023 12:12:20 +0400 Subject: [PATCH 05/16] backup v4 --- backend/main/main-canister.mo | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/backend/main/main-canister.mo b/backend/main/main-canister.mo index fd6eef63..666992c6 100644 --- a/backend/main/main-canister.mo +++ b/backend/main/main-canister.mo @@ -1348,7 +1348,7 @@ actor { let backupManager = Backup.BackupManager(backupState); type BackupChunk = { - #v3 : { + #v4 : { #packagePublications : [(PackageId, PackagePublication)]; #packageVersions : [(PackageName, [PackageVersion])]; #packageOwners : [(PackageName, Principal)]; @@ -1375,20 +1375,20 @@ actor { }; func _backup() : async () { - let backup = backupManager.NewBackup("v3"); + let backup = backupManager.NewBackup("v4"); await backup.startBackup(); - await backup.uploadChunk(to_candid(#v3(#packagePublications(Iter.toArray(packagePublications.entries()))) : BackupChunk)); - await backup.uploadChunk(to_candid(#v3(#packageVersions(Iter.toArray(packageVersions.entries()))) : BackupChunk)); - await backup.uploadChunk(to_candid(#v3(#packageOwners(Iter.toArray(packageOwners.entries()))) : BackupChunk)); - await backup.uploadChunk(to_candid(#v3(#fileIdsByPackage(Iter.toArray(fileIdsByPackage.entries()))) : BackupChunk)); - await backup.uploadChunk(to_candid(#v3(#hashByFileId(Iter.toArray(hashByFileId.entries()))) : BackupChunk)); - await backup.uploadChunk(to_candid(#v3(#packageTestStats(Iter.toArray(packageTestStats.entries()))) : BackupChunk)); - await backup.uploadChunk(to_candid(#v3(#packageNotes(Iter.toArray(packageNotes.entries()))) : BackupChunk)); - await backup.uploadChunk(to_candid(#v3(#downloadLog(downloadLog.toStable())) : BackupChunk)); - await backup.uploadChunk(to_candid(#v3(#storageManager(storageManager.toStable())) : BackupChunk)); - await backup.uploadChunk(to_candid(#v3(#users(users.toStable())) : BackupChunk)); - await backup.uploadChunk(to_candid(#v3(#highestConfigs(Iter.toArray(highestConfigs.entries()))) : BackupChunk)); - await backup.uploadChunk(to_candid(#v3(#packageConfigs(Iter.toArray(packageConfigs.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v4(#packagePublications(Iter.toArray(packagePublications.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v4(#packageVersions(Iter.toArray(packageVersions.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v4(#packageOwners(Iter.toArray(packageOwners.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v4(#fileIdsByPackage(Iter.toArray(fileIdsByPackage.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v4(#hashByFileId(Iter.toArray(hashByFileId.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v4(#packageTestStats(Iter.toArray(packageTestStats.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v4(#packageNotes(Iter.toArray(packageNotes.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v4(#downloadLog(downloadLog.toStable())) : BackupChunk)); + await backup.uploadChunk(to_candid(#v4(#storageManager(storageManager.toStable())) : BackupChunk)); + await backup.uploadChunk(to_candid(#v4(#users(users.toStable())) : BackupChunk)); + await backup.uploadChunk(to_candid(#v4(#highestConfigs(Iter.toArray(highestConfigs.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v4(#packageConfigs(Iter.toArray(packageConfigs.entries()))) : BackupChunk)); await backup.finishBackup(); }; @@ -1398,7 +1398,7 @@ actor { assert(Utils.isAdmin(caller)); await backupManager.restore(backupId, func(blob : Blob) { - let ?#v3(chunk) : ?BackupChunk = from_candid(blob) else Debug.trap("Failed to restore chunk"); + let ?#v4(chunk) : ?BackupChunk = from_candid(blob) else Debug.trap("Failed to restore chunk"); switch (chunk) { case (#packagePublications(packagePublicationsStable)) { From acf67a36c3888244052b6d7374465cadcfc31c3f Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Wed, 18 Oct 2023 13:07:25 +0400 Subject: [PATCH 06/16] integrity check WIP --- backend/main/main-canister.mo | 23 ++++++++++++++++++++--- cli/.npmrc | 1 + cli/declarations/main/main.did | 17 +++++++++++++++++ cli/declarations/main/main.did.d.ts | 7 +++++++ cli/declarations/main/main.did.js | 14 ++++++++++++++ cli/integrity.ts | 10 ++++++++++ cli/package-lock.json | 7 ++++--- cli/package.json | 1 + frontend/.npmrc | 1 + frontend/declarations/main/main.did | 17 +++++++++++++++++ frontend/declarations/main/main.did.d.ts | 7 +++++++ frontend/declarations/main/main.did.js | 14 ++++++++++++++ package.json | 2 +- 13 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 cli/.npmrc create mode 100644 cli/integrity.ts create mode 100644 frontend/.npmrc diff --git a/backend/main/main-canister.mo b/backend/main/main-canister.mo index 666992c6..02349169 100644 --- a/backend/main/main-canister.mo +++ b/backend/main/main-canister.mo @@ -908,10 +908,8 @@ actor { Result.fromOption(fileIdsByPackage.get(packageId), "Package '" # packageId # "' not found"); }; - public shared ({caller}) func getFileHashes(name : PackageName, version : PackageVersion) : async Result.Result<[(FileId, Blob)], Err> { - let packageId = name # "@" # version; + func _getFileHashes(packageId : PackageId) : Result.Result<[(FileId, Blob)], Err> { let ?fileIds = fileIdsByPackage.get(packageId) else return #err("Package '" # packageId # "' not found"); - let buf = Buffer.Buffer<(FileId, Blob)>(fileIds.size()); for (fileId in fileIds.vals()) { let ?hash = hashByFileId.get(fileId) else return #err("File hash not found for " # fileId); @@ -920,6 +918,25 @@ actor { #ok(Buffer.toArray(buf)); }; + public shared ({caller}) func getFileHashes(name : PackageName, version : PackageVersion) : async Result.Result<[(FileId, Blob)], Err> { + let packageId = name # "@" # version; + _getFileHashes(packageId); + }; + + public shared ({caller}) func getFileHashesByPackageIds(packageIds : [PackageId]) : async [(PackageId, [(FileId, Blob)])] { + let buf = Buffer.Buffer<(PackageId, [(FileId, Blob)])>(packageIds.size()); + + for (packageId in packageIds.vals()) { + let hashes = switch (_getFileHashes(packageId)) { + case (#ok(hashes)) hashes; + case (#err(_)) []; + }; + buf.add((packageId, hashes)); + }; + + Buffer.toArray(buf); + }; + func _notifyInstall(name : PackageName, version : PackageVersion, downloader : Principal) { let packageId = name # "@" # version; diff --git a/cli/.npmrc b/cli/.npmrc new file mode 100644 index 00000000..cad57977 --- /dev/null +++ b/cli/.npmrc @@ -0,0 +1 @@ +save-exact = true \ No newline at end of file diff --git a/cli/declarations/main/main.did b/cli/declarations/main/main.did index 7a4e8cda..a7e23097 100644 --- a/cli/declarations/main/main.did +++ b/cli/declarations/main/main.did @@ -73,6 +73,14 @@ type Script = name: text; value: text; }; +type Result_8 = + variant { + err: Err; + ok: vec record { + FileId; + blob; + }; + }; type Result_7 = variant { err: Err; @@ -311,6 +319,15 @@ service : { (vec DownloadsSnapshot__1) query; getDownloadTrendByPackageName: (PackageName) -> (vec DownloadsSnapshot__1) query; + getFileHashes: (PackageName, PackageVersion) -> (Result_8); + getFileHashesByPackageIds: (vec PackageId) -> + (vec record { + PackageId; + vec record { + FileId; + blob; + }; + }); getFileIds: (PackageName, PackageVersion) -> (Result_7) query; getHighestSemverBatch: (vec record { diff --git a/cli/declarations/main/main.did.d.ts b/cli/declarations/main/main.did.d.ts index f8ed5d82..6d11e94f 100644 --- a/cli/declarations/main/main.did.d.ts +++ b/cli/declarations/main/main.did.d.ts @@ -173,6 +173,8 @@ export type Result_6 = { 'ok' : Array<[PackageName, PackageVersion]> } | { 'err' : Err }; export type Result_7 = { 'ok' : Array } | { 'err' : Err }; +export type Result_8 = { 'ok' : Array<[FileId, Uint8Array | number[]]> } | + { 'err' : Err }; export interface Script { 'value' : string, 'name' : string } export type SemverPart = { 'major' : null } | { 'minor' : null } | @@ -251,6 +253,11 @@ export interface _SERVICE { [PackageName], Array >, + 'getFileHashes' : ActorMethod<[PackageName, PackageVersion], Result_8>, + 'getFileHashesByPackageIds' : ActorMethod< + [Array], + Array<[PackageId, Array<[FileId, Uint8Array | number[]]>]> + >, 'getFileIds' : ActorMethod<[PackageName, PackageVersion], Result_7>, 'getHighestSemverBatch' : ActorMethod< [Array<[PackageName, PackageVersion, SemverPart]>], diff --git a/cli/declarations/main/main.did.js b/cli/declarations/main/main.did.js index 3941594e..2a13e62c 100644 --- a/cli/declarations/main/main.did.js +++ b/cli/declarations/main/main.did.js @@ -28,6 +28,10 @@ export const idlFactory = ({ IDL }) => { 'downloads' : IDL.Nat, }); const FileId = IDL.Text; + const Result_8 = IDL.Variant({ + 'ok' : IDL.Vec(IDL.Tuple(FileId, IDL.Vec(IDL.Nat8))), + 'err' : Err, + }); const Result_7 = IDL.Variant({ 'ok' : IDL.Vec(FileId), 'err' : Err }); const SemverPart = IDL.Variant({ 'major' : IDL.Null, @@ -256,6 +260,16 @@ export const idlFactory = ({ IDL }) => { [IDL.Vec(DownloadsSnapshot__1)], ['query'], ), + 'getFileHashes' : IDL.Func([PackageName, PackageVersion], [Result_8], []), + 'getFileHashesByPackageIds' : IDL.Func( + [IDL.Vec(PackageId)], + [ + IDL.Vec( + IDL.Tuple(PackageId, IDL.Vec(IDL.Tuple(FileId, IDL.Vec(IDL.Nat8)))) + ), + ], + [], + ), 'getFileIds' : IDL.Func( [PackageName, PackageVersion], [Result_7], diff --git a/cli/integrity.ts b/cli/integrity.ts new file mode 100644 index 00000000..efb9e4e8 --- /dev/null +++ b/cli/integrity.ts @@ -0,0 +1,10 @@ +import fs from 'node:fs'; +import {sha256} from '@noble/hashes/sha256'; +import {bytesToHex} from '@noble/hashes/utils'; +import {mainActor} from './mops.js'; + +export async function getFileHashesFromRegistry(packageIds: string[]): Promise<{[packageId: string]: {[fileId: string]: string;};}> { + let actor = await mainActor(); + let fileHashesByPackageIds = await actor.getFileHashesByPackageIds(packageIds); + return fileHashesByPackageIds; +} \ No newline at end of file diff --git a/cli/package-lock.json b/cli/package-lock.json index f6a57fc5..1045a4f1 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -15,6 +15,7 @@ "@dfinity/identity-secp256k1": "^0.18.1", "@dfinity/principal": "^0.18.1", "@iarna/toml": "^2.2.5", + "@noble/hashes": "1.3.2", "as-table": "^1.0.55", "cacheable-request": "10.2.12", "camelcase": "^7.0.1", @@ -1047,9 +1048,9 @@ } }, "node_modules/@noble/hashes": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", - "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", "engines": { "node": ">= 16" }, diff --git a/cli/package.json b/cli/package.json index d8d3a44a..9473b8c5 100644 --- a/cli/package.json +++ b/cli/package.json @@ -38,6 +38,7 @@ "@dfinity/identity-secp256k1": "^0.18.1", "@dfinity/principal": "^0.18.1", "@iarna/toml": "^2.2.5", + "@noble/hashes": "1.3.2", "as-table": "^1.0.55", "cacheable-request": "10.2.12", "camelcase": "^7.0.1", diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 00000000..cad57977 --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1 @@ +save-exact = true \ No newline at end of file diff --git a/frontend/declarations/main/main.did b/frontend/declarations/main/main.did index 7a4e8cda..a7e23097 100644 --- a/frontend/declarations/main/main.did +++ b/frontend/declarations/main/main.did @@ -73,6 +73,14 @@ type Script = name: text; value: text; }; +type Result_8 = + variant { + err: Err; + ok: vec record { + FileId; + blob; + }; + }; type Result_7 = variant { err: Err; @@ -311,6 +319,15 @@ service : { (vec DownloadsSnapshot__1) query; getDownloadTrendByPackageName: (PackageName) -> (vec DownloadsSnapshot__1) query; + getFileHashes: (PackageName, PackageVersion) -> (Result_8); + getFileHashesByPackageIds: (vec PackageId) -> + (vec record { + PackageId; + vec record { + FileId; + blob; + }; + }); getFileIds: (PackageName, PackageVersion) -> (Result_7) query; getHighestSemverBatch: (vec record { diff --git a/frontend/declarations/main/main.did.d.ts b/frontend/declarations/main/main.did.d.ts index f8ed5d82..6d11e94f 100644 --- a/frontend/declarations/main/main.did.d.ts +++ b/frontend/declarations/main/main.did.d.ts @@ -173,6 +173,8 @@ export type Result_6 = { 'ok' : Array<[PackageName, PackageVersion]> } | { 'err' : Err }; export type Result_7 = { 'ok' : Array } | { 'err' : Err }; +export type Result_8 = { 'ok' : Array<[FileId, Uint8Array | number[]]> } | + { 'err' : Err }; export interface Script { 'value' : string, 'name' : string } export type SemverPart = { 'major' : null } | { 'minor' : null } | @@ -251,6 +253,11 @@ export interface _SERVICE { [PackageName], Array >, + 'getFileHashes' : ActorMethod<[PackageName, PackageVersion], Result_8>, + 'getFileHashesByPackageIds' : ActorMethod< + [Array], + Array<[PackageId, Array<[FileId, Uint8Array | number[]]>]> + >, 'getFileIds' : ActorMethod<[PackageName, PackageVersion], Result_7>, 'getHighestSemverBatch' : ActorMethod< [Array<[PackageName, PackageVersion, SemverPart]>], diff --git a/frontend/declarations/main/main.did.js b/frontend/declarations/main/main.did.js index 3941594e..2a13e62c 100644 --- a/frontend/declarations/main/main.did.js +++ b/frontend/declarations/main/main.did.js @@ -28,6 +28,10 @@ export const idlFactory = ({ IDL }) => { 'downloads' : IDL.Nat, }); const FileId = IDL.Text; + const Result_8 = IDL.Variant({ + 'ok' : IDL.Vec(IDL.Tuple(FileId, IDL.Vec(IDL.Nat8))), + 'err' : Err, + }); const Result_7 = IDL.Variant({ 'ok' : IDL.Vec(FileId), 'err' : Err }); const SemverPart = IDL.Variant({ 'major' : IDL.Null, @@ -256,6 +260,16 @@ export const idlFactory = ({ IDL }) => { [IDL.Vec(DownloadsSnapshot__1)], ['query'], ), + 'getFileHashes' : IDL.Func([PackageName, PackageVersion], [Result_8], []), + 'getFileHashesByPackageIds' : IDL.Func( + [IDL.Vec(PackageId)], + [ + IDL.Vec( + IDL.Tuple(PackageId, IDL.Vec(IDL.Tuple(FileId, IDL.Vec(IDL.Nat8)))) + ), + ], + [], + ), 'getFileIds' : IDL.Func( [PackageName, PackageVersion], [Result_7], diff --git a/package.json b/package.json index 95327cf5..bce1c453 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "decl:cli": "dfx generate main", "decl:frontend": "cp -r cli/declarations frontend", "deploy": "DFX_MOC_PATH=$(mocv bin 0.10.1)/moc dfx deploy --no-wallet --identity ${IDENTITY} --network ${NETWORK}", - "deploy-local": "NODE_ENV=development IDENTITY=default NETWORK=local npm run deploy && npm run decl", + "deploy-local": "NODE_ENV=development dfx deploy main --no-wallet --identity default && npm run decl", "deploy-staging": "NODE_ENV=production IDENTITY=mops NETWORK=staging npm run deploy", "deploy-ic": "NODE_ENV=production IDENTITY=mops NETWORK=ic npm run deploy", "deploy:main": "NODE_ENV=development IDENTITY=default NETWORK=local npm run deploy main", From 857cbd1eda036a2270d52778e1e8446f6ee092fe Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Thu, 19 Oct 2023 09:49:37 +0400 Subject: [PATCH 07/16] [backend] add `computeHashesForExistingFiles` --- backend/main/main-canister.mo | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/backend/main/main-canister.mo b/backend/main/main-canister.mo index 02349169..eb316f7c 100644 --- a/backend/main/main-canister.mo +++ b/backend/main/main-canister.mo @@ -746,6 +746,26 @@ actor { Buffer.toArray(buf); }; + public shared ({caller}) func computeHashesForExistingFiles() : async () { + assert(Utils.isAdmin(caller)); + + for ((packageId, fileIds) in fileIdsByPackage.entries()) { + let ?publication = packagePublications.get(packageId) else Debug.trap("Package publication '" # packageId # "' not found"); + let storage = actor(Principal.toText(publication.storage)) : Storage.Storage; + + for (fileId in fileIds.vals()) { + let #ok(fileMeta) = await storage.getFileMeta(fileId) else Debug.trap("File meta '" # fileId # "' not found"); + + let hasher = Sha256.Digest(#sha256); + for (i in Iter.range(1, fileMeta.chunkCount)) { + let #ok(chunk) = await storage.downloadChunk(fileId, i) else Debug.trap("File chunk '" # fileId # "' not found"); + hasher.writeBlob(chunk); + }; + hashByFileId.put(fileId, hasher.sum()); + }; + }; + }; + // AIRDROP let cyclesPerOwner = 15_000_000_000_000; // 15 TC let cyclesPerPackage = 1_000_000_000_000; // 1 TC From 15cc57c0cfacbf68ac4876a2e215badc39167d4c Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Thu, 19 Oct 2023 11:34:16 +0400 Subject: [PATCH 08/16] integrity check WIP --- cli/cli.ts | 1 + cli/declarations/main/main.did | 1 + cli/declarations/main/main.did.d.ts | 1 + cli/declarations/main/main.did.js | 1 + cli/integrity.ts | 96 +++++++++++++++++++++++- frontend/declarations/main/main.did | 1 + frontend/declarations/main/main.did.d.ts | 1 + frontend/declarations/main/main.did.js | 1 + 8 files changed, 101 insertions(+), 2 deletions(-) diff --git a/cli/cli.ts b/cli/cli.ts index 6ceb4aa6..7b28ea7e 100644 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -25,6 +25,7 @@ import {sync} from './commands/sync.js'; import {outdated} from './commands/outdated.js'; import {update} from './commands/update.js'; import {transferOwnership} from './commands/transfer-ownership.js'; +import {checkIntegrity, checkLockFile, saveLockFile} from './integrity.js'; // import {docs} from './commands/docs.js'; program.name('mops'); diff --git a/cli/declarations/main/main.did b/cli/declarations/main/main.did index a7e23097..40488979 100644 --- a/cli/declarations/main/main.did +++ b/cli/declarations/main/main.did @@ -304,6 +304,7 @@ type DepChange = service : { backup: () -> (); claimAirdrop: (principal) -> (text); + computeHashesForExistingFiles: () -> (); diff: (text, text) -> (PackageChanges__1) query; finishPublish: (PublishingId) -> (Result); getAirdropAmount: () -> (nat) query; diff --git a/cli/declarations/main/main.did.d.ts b/cli/declarations/main/main.did.d.ts index 6d11e94f..ad33270c 100644 --- a/cli/declarations/main/main.did.d.ts +++ b/cli/declarations/main/main.did.d.ts @@ -235,6 +235,7 @@ export interface User__1 { export interface _SERVICE { 'backup' : ActorMethod<[], undefined>, 'claimAirdrop' : ActorMethod<[Principal], string>, + 'computeHashesForExistingFiles' : ActorMethod<[], undefined>, 'diff' : ActorMethod<[string, string], PackageChanges__1>, 'finishPublish' : ActorMethod<[PublishingId], Result>, 'getAirdropAmount' : ActorMethod<[], bigint>, diff --git a/cli/declarations/main/main.did.js b/cli/declarations/main/main.did.js index 2a13e62c..d79fb01f 100644 --- a/cli/declarations/main/main.did.js +++ b/cli/declarations/main/main.did.js @@ -239,6 +239,7 @@ export const idlFactory = ({ IDL }) => { return IDL.Service({ 'backup' : IDL.Func([], [], []), 'claimAirdrop' : IDL.Func([IDL.Principal], [IDL.Text], []), + 'computeHashesForExistingFiles' : IDL.Func([], [], []), 'diff' : IDL.Func([IDL.Text, IDL.Text], [PackageChanges__1], ['query']), 'finishPublish' : IDL.Func([PublishingId], [Result], []), 'getAirdropAmount' : IDL.Func([], [IDL.Nat], ['query']), diff --git a/cli/integrity.ts b/cli/integrity.ts index efb9e4e8..8d28d736 100644 --- a/cli/integrity.ts +++ b/cli/integrity.ts @@ -1,10 +1,102 @@ import fs from 'node:fs'; +import path from 'node:path'; import {sha256} from '@noble/hashes/sha256'; import {bytesToHex} from '@noble/hashes/utils'; -import {mainActor} from './mops.js'; +import {getDependencyType, getRootDir, mainActor} from './mops.js'; +import {resolvePackages} from './resolve-packages.js'; -export async function getFileHashesFromRegistry(packageIds: string[]): Promise<{[packageId: string]: {[fileId: string]: string;};}> { +export async function getFileHashesFromRegistry(packageIds: string[]): Promise<[string, [string, Uint8Array | number[]][]][]> { let actor = await mainActor(); let fileHashesByPackageIds = await actor.getFileHashesByPackageIds(packageIds); return fileHashesByPackageIds; +} + +export async function checkIntegrity() { + let rootDir = getRootDir(); + let resolvedPackages = await resolvePackages(); + let packageIds = Object.entries(resolvedPackages) + .filter(([_, version]) => getDependencyType(version) === 'mops') + .map(([name, version]) => `${name}@${version}`); + let fileHashesFromRegistry = await getFileHashesFromRegistry(packageIds); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (let [_packageId, fileHashes] of fileHashesFromRegistry) { + for (let [fileId, hash] of fileHashes) { + let remoteHash = new Uint8Array(hash); + let localData = fs.readFileSync(path.join(rootDir, '.mops', fileId)); + let localHash = await sha256(localData); + + if (bytesToHex(localHash) !== bytesToHex(remoteHash)) { + console.error('Integrity check failed.'); + console.error(`Mismatched hash for ${fileId}: ${bytesToHex(localHash)} vs ${bytesToHex(remoteHash)}`); + process.exit(1); + } + } + } +} + +export async function saveLockFile() { + let rootDir = getRootDir(); + let resolvedPackages = await resolvePackages(); + let packageIds = Object.entries(resolvedPackages) + .filter(([_, version]) => getDependencyType(version) === 'mops') + .map(([name, version]) => `${name}@${version}`); + let fileHashes = await getFileHashesFromRegistry(packageIds); + + let lockFileJson = { + version: 1, + hashes: fileHashes.reduce((acc, [packageId, fileHashes]) => { + acc[packageId] = fileHashes.reduce((acc, [fileId, hash]) => { + acc[fileId] = bytesToHex(new Uint8Array(hash)); + return acc; + }, {} as Record); + return acc; + }, {} as Record>), + }; + + fs.writeFileSync(rootDir + '/mops-lock.json', JSON.stringify(lockFileJson, null, 2)); +} + +export async function checkLockFile() { + let rootDir = getRootDir(); + let resolvedPackages = await resolvePackages(); + let packageIds = Object.entries(resolvedPackages) + .filter(([_name, version]) => getDependencyType(version) === 'mops') + .map(([name, version]) => `${name}@${version}`); + + let fileHashesFromRegistry = await getFileHashesFromRegistry(packageIds); + + let lockFileJson = JSON.parse(fs.readFileSync(rootDir + '/mops-lock.json').toString()); + if (lockFileJson.version !== 1) { + console.error(`Invalid lock file version: ${lockFileJson.version}`); + process.exit(1); + } + // if (lockFileJson.packageIds.length !== packageIds.length) { + // console.error(`Mismatched packageIds: ${JSON.stringify(lockFileJson.packageIds)} vs ${JSON.stringify(packageIds)}`); + // process.exit(1); + // } + // for (let i = 0; i < packageIds.length; i++) { + // if (lockFileJson.packageIds[i] !== packageIds[i]) { + // console.error(`Mismatched packageIds: ${JSON.stringify(lockFileJson.packageIds)} vs ${JSON.stringify(packageIds)}`); + // process.exit(1); + // } + // } + for (let [packageId, fileHashes] of fileHashesFromRegistry) { + let hashes = lockFileJson.hashes[packageId]; + if (!hashes) { + console.error(`Missing packageId ${packageId} in lock file`); + process.exit(1); + } + for (let [fileId, hash] of fileHashes) { + let lockFileHash = hashes[fileId]; + if (!lockFileHash) { + console.error(`Missing fileId ${fileId} in lock file`); + process.exit(1); + } + if (lockFileHash !== bytesToHex(new Uint8Array(hash))) { + console.error(`Mismatched hash for ${fileId}: ${lockFileHash} vs ${bytesToHex(new Uint8Array(hash))}`); + process.exit(1); + } + } + } } \ No newline at end of file diff --git a/frontend/declarations/main/main.did b/frontend/declarations/main/main.did index a7e23097..40488979 100644 --- a/frontend/declarations/main/main.did +++ b/frontend/declarations/main/main.did @@ -304,6 +304,7 @@ type DepChange = service : { backup: () -> (); claimAirdrop: (principal) -> (text); + computeHashesForExistingFiles: () -> (); diff: (text, text) -> (PackageChanges__1) query; finishPublish: (PublishingId) -> (Result); getAirdropAmount: () -> (nat) query; diff --git a/frontend/declarations/main/main.did.d.ts b/frontend/declarations/main/main.did.d.ts index 6d11e94f..ad33270c 100644 --- a/frontend/declarations/main/main.did.d.ts +++ b/frontend/declarations/main/main.did.d.ts @@ -235,6 +235,7 @@ export interface User__1 { export interface _SERVICE { 'backup' : ActorMethod<[], undefined>, 'claimAirdrop' : ActorMethod<[Principal], string>, + 'computeHashesForExistingFiles' : ActorMethod<[], undefined>, 'diff' : ActorMethod<[string, string], PackageChanges__1>, 'finishPublish' : ActorMethod<[PublishingId], Result>, 'getAirdropAmount' : ActorMethod<[], bigint>, diff --git a/frontend/declarations/main/main.did.js b/frontend/declarations/main/main.did.js index 2a13e62c..d79fb01f 100644 --- a/frontend/declarations/main/main.did.js +++ b/frontend/declarations/main/main.did.js @@ -239,6 +239,7 @@ export const idlFactory = ({ IDL }) => { return IDL.Service({ 'backup' : IDL.Func([], [], []), 'claimAirdrop' : IDL.Func([IDL.Principal], [IDL.Text], []), + 'computeHashesForExistingFiles' : IDL.Func([], [], []), 'diff' : IDL.Func([IDL.Text, IDL.Text], [PackageChanges__1], ['query']), 'finishPublish' : IDL.Func([PublishingId], [Result], []), 'getAirdropAmount' : IDL.Func([], [IDL.Nat], ['query']), From fdf9db247a9c14bb5523ad19d37b55ad8c03c885 Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Thu, 19 Oct 2023 12:59:12 +0400 Subject: [PATCH 09/16] [backend] fix default package for `dfx 0.15.1` --- backend/main/main-canister.mo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/main/main-canister.mo b/backend/main/main-canister.mo index eb316f7c..20749c4c 100644 --- a/backend/main/main-canister.mo +++ b/backend/main/main-canister.mo @@ -850,7 +850,7 @@ actor { case ("0.14.3") [("base", "0.9.3")]; case ("0.14.4") [("base", "0.9.3")]; case ("0.15.0") [("base", "0.9.7")]; - case ("0.15.1") [("base", "0.9.7")]; + case ("0.15.1") [("base", "0.9.8")]; case (_) { switch (_getHighestVersion("base")) { case (?ver) [("base", ver)]; From 3febad83f8e3d9e8312462009b58d46bdab79d51 Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Fri, 27 Oct 2023 10:47:43 +0400 Subject: [PATCH 10/16] [docs] fix "edit this page" link --- docs/docusaurus.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index cabc33a2..896c9732 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -42,7 +42,7 @@ const config = { sidebarPath: require.resolve('./sidebars.js'), // Please change this to your repo. // Remove this to remove the "edit this page" links. - editUrl: 'https://github.com/ZenVoich/mops/docs/', + editUrl: 'https://github.com/ZenVoich/mops/edit/main/docs/', }, blog: false, // blog: { From f558f7b5f3e6ddece4685eb8bcc3ceedd14394b3 Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Fri, 27 Oct 2023 10:59:14 +0400 Subject: [PATCH 11/16] [backend] add default `base` for dfx 0.15.2 --- backend/main/main-canister.mo | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/main/main-canister.mo b/backend/main/main-canister.mo index 20749c4c..a4ad930e 100644 --- a/backend/main/main-canister.mo +++ b/backend/main/main-canister.mo @@ -851,6 +851,7 @@ actor { case ("0.14.4") [("base", "0.9.3")]; case ("0.15.0") [("base", "0.9.7")]; case ("0.15.1") [("base", "0.9.8")]; + case ("0.15.2") [("base", "0.10.1")]; case (_) { switch (_getHighestVersion("base")) { case (?ver) [("base", ver)]; From b9302b2e34d0d3467f2344c636699e6656db2a3a Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Fri, 27 Oct 2023 12:20:46 +0400 Subject: [PATCH 12/16] [cli] fix integrity check --- cli/cli.ts | 1 - cli/integrity.ts | 152 ++++++++++++++++++++++++++++++++++------------- 2 files changed, 110 insertions(+), 43 deletions(-) diff --git a/cli/cli.ts b/cli/cli.ts index 9bfe7282..b2cb225b 100644 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -26,7 +26,6 @@ import {outdated} from './commands/outdated.js'; import {update} from './commands/update.js'; import {bench} from './commands/bench.js'; import {transferOwnership} from './commands/transfer-ownership.js'; -import {checkIntegrity, checkLockFile, saveLockFile} from './integrity.js'; // import {docs} from './commands/docs.js'; program.name('mops'); diff --git a/cli/integrity.ts b/cli/integrity.ts index 8d28d736..819b8971 100644 --- a/cli/integrity.ts +++ b/cli/integrity.ts @@ -5,30 +5,56 @@ import {bytesToHex} from '@noble/hashes/utils'; import {getDependencyType, getRootDir, mainActor} from './mops.js'; import {resolvePackages} from './resolve-packages.js'; -export async function getFileHashesFromRegistry(packageIds: string[]): Promise<[string, [string, Uint8Array | number[]][]][]> { +type LockFileV1 = { + version: 1; + mopsTomlHash: string; + hashes: Record>; +}; + +async function getFileHashesFromRegistry(): Promise<[string, [string, Uint8Array | number[]][]][]> { + let packageIds = await getResolvedMopsPackageIds(); let actor = await mainActor(); let fileHashesByPackageIds = await actor.getFileHashesByPackageIds(packageIds); return fileHashesByPackageIds; } -export async function checkIntegrity() { - let rootDir = getRootDir(); +async function getResolvedMopsPackageIds(): Promise { let resolvedPackages = await resolvePackages(); let packageIds = Object.entries(resolvedPackages) .filter(([_, version]) => getDependencyType(version) === 'mops') .map(([name, version]) => `${name}@${version}`); - let fileHashesFromRegistry = await getFileHashesFromRegistry(packageIds); + return packageIds; +} + +// get hash of local file from '.mops' dir by fileId +export function getLocalFileHash(fileId: string): string { + let rootDir = getRootDir(); + let file = path.join(rootDir, '.mops', fileId); + if (!fs.existsSync(file)) { + console.error(`Missing file ${fileId} in .mops dir`); + process.exit(1); + } + let fileData = fs.readFileSync(file); + return bytesToHex(sha256(fileData)); +} + +function getMopsTomlHash(): string { + return bytesToHex(sha256(fs.readFileSync(getRootDir() + '/mops.toml'))); +} + +// compare hashes of local files with hashes from the registry +export async function checkIntegrity() { + let fileHashesFromRegistry = await getFileHashesFromRegistry(); // eslint-disable-next-line @typescript-eslint/no-unused-vars for (let [_packageId, fileHashes] of fileHashesFromRegistry) { for (let [fileId, hash] of fileHashes) { let remoteHash = new Uint8Array(hash); - let localData = fs.readFileSync(path.join(rootDir, '.mops', fileId)); - let localHash = await sha256(localData); + let localHash = getLocalFileHash(fileId); - if (bytesToHex(localHash) !== bytesToHex(remoteHash)) { + if (localHash !== bytesToHex(remoteHash)) { console.error('Integrity check failed.'); - console.error(`Mismatched hash for ${fileId}: ${bytesToHex(localHash)} vs ${bytesToHex(remoteHash)}`); + console.error(`Mismatched hash for ${fileId}: ${localHash} vs ${bytesToHex(remoteHash)}`); process.exit(1); } } @@ -37,14 +63,21 @@ export async function checkIntegrity() { export async function saveLockFile() { let rootDir = getRootDir(); - let resolvedPackages = await resolvePackages(); - let packageIds = Object.entries(resolvedPackages) - .filter(([_, version]) => getDependencyType(version) === 'mops') - .map(([name, version]) => `${name}@${version}`); - let fileHashes = await getFileHashesFromRegistry(packageIds); + let fileHashes = await getFileHashesFromRegistry(); + let lockFile = path.join(rootDir, 'mops-lock.json'); - let lockFileJson = { + // if lock file exists and mops.toml hasn't changed, don't update it + if (fs.existsSync(lockFile)) { + let lockFileJson: LockFileV1 = JSON.parse(fs.readFileSync(lockFile).toString()); + let mopsTomlHash = getMopsTomlHash(); + if (mopsTomlHash === lockFileJson.mopsTomlHash) { + return; + } + } + + let lockFileJson: LockFileV1 = { version: 1, + mopsTomlHash: getMopsTomlHash(), hashes: fileHashes.reduce((acc, [packageId, fileHashes]) => { acc[packageId] = fileHashes.reduce((acc, [fileId, hash]) => { acc[fileId] = bytesToHex(new Uint8Array(hash)); @@ -54,47 +87,82 @@ export async function saveLockFile() { }, {} as Record>), }; - fs.writeFileSync(rootDir + '/mops-lock.json', JSON.stringify(lockFileJson, null, 2)); + fs.writeFileSync(lockFile, JSON.stringify(lockFileJson, null, 2)); } +// compare hashes of local files with hashes from the lock file export async function checkLockFile() { let rootDir = getRootDir(); - let resolvedPackages = await resolvePackages(); - let packageIds = Object.entries(resolvedPackages) - .filter(([_name, version]) => getDependencyType(version) === 'mops') - .map(([name, version]) => `${name}@${version}`); + let lockFile = path.join(rootDir, 'mops-lock.json'); + let packageIds = await getResolvedMopsPackageIds(); - let fileHashesFromRegistry = await getFileHashesFromRegistry(packageIds); + // check if lock file exists + if (!fs.existsSync(lockFile)) { + // BREAKING CHANGE + // console.error('Missing lock file. Run `mops install` to generate it.'); + // process.exit(1); + return; + } - let lockFileJson = JSON.parse(fs.readFileSync(rootDir + '/mops-lock.json').toString()); + let lockFileJson: LockFileV1 = JSON.parse(fs.readFileSync(lockFile).toString()); + + // check lock file version if (lockFileJson.version !== 1) { - console.error(`Invalid lock file version: ${lockFileJson.version}`); + console.error('Integrity check failed'); + console.error(`Invalid lock file version: ${lockFileJson.version}. Supported versions: 1`); + process.exit(1); + } + + // check mops.toml hash + if (lockFileJson.mopsTomlHash !== getMopsTomlHash()) { + console.error('Integrity check failed'); + console.error('Mismatched mops.toml hash'); + console.error(`Locked hash: ${lockFileJson.mopsTomlHash}`); + console.error(`Actual hash: ${getMopsTomlHash()}`); + process.exit(1); + } + + // check number of packages + if (Object.keys(lockFileJson.hashes).length !== packageIds.length) { + console.error('Integrity check failed'); + console.error(`Mismatched number of resolved packages: ${JSON.stringify(Object.keys(lockFileJson.hashes).length)} vs ${JSON.stringify(packageIds.length)}`); process.exit(1); } - // if (lockFileJson.packageIds.length !== packageIds.length) { - // console.error(`Mismatched packageIds: ${JSON.stringify(lockFileJson.packageIds)} vs ${JSON.stringify(packageIds)}`); - // process.exit(1); - // } - // for (let i = 0; i < packageIds.length; i++) { - // if (lockFileJson.packageIds[i] !== packageIds[i]) { - // console.error(`Mismatched packageIds: ${JSON.stringify(lockFileJson.packageIds)} vs ${JSON.stringify(packageIds)}`); - // process.exit(1); - // } - // } - for (let [packageId, fileHashes] of fileHashesFromRegistry) { - let hashes = lockFileJson.hashes[packageId]; - if (!hashes) { - console.error(`Missing packageId ${packageId} in lock file`); + + // check if resolved packages are in the lock file + for (let packageId of packageIds) { + if (!(packageId in lockFileJson.hashes)) { + console.error('Integrity check failed'); + console.error(`Missing package ${packageId} in lock file`); process.exit(1); } - for (let [fileId, hash] of fileHashes) { - let lockFileHash = hashes[fileId]; - if (!lockFileHash) { - console.error(`Missing fileId ${fileId} in lock file`); + } + + for (let [packageId, hashes] of Object.entries(lockFileJson.hashes)) { + + // check if package is in resolved packages + if (!packageIds.includes(packageId)) { + console.error('Integrity check failed'); + console.error(`Package ${packageId} in lock file but not in resolved packages`); + process.exit(1); + } + + for (let [fileId, lockedHash] of Object.entries(hashes)) { + + // check if file belongs to package + if (!fileId.startsWith(packageId)) { + console.error('Integrity check failed'); + console.error(`File ${fileId} in lock file does not belong to package ${packageId}`); process.exit(1); } - if (lockFileHash !== bytesToHex(new Uint8Array(hash))) { - console.error(`Mismatched hash for ${fileId}: ${lockFileHash} vs ${bytesToHex(new Uint8Array(hash))}`); + + // local file hash vs hash from lock file + let localHash = getLocalFileHash(fileId); + if (lockedHash !== localHash) { + console.error('Integrity check failed'); + console.error(`Mismatched hash for ${fileId}`); + console.error(`Locked hash: ${lockedHash}`); + console.error(`Actual hash: ${localHash}`); process.exit(1); } } From a5bbd6675393595b2d450799faed6eece82eb69e Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Fri, 27 Oct 2023 12:25:53 +0400 Subject: [PATCH 13/16] update declarations --- frontend/declarations/bench/bench.did | 22 +++++++++++++--------- frontend/declarations/bench/bench.did.d.ts | 10 +++++++--- frontend/declarations/bench/bench.did.js | 22 +++++++++++++--------- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/frontend/declarations/bench/bench.did b/frontend/declarations/bench/bench.did index 06020217..3a98cd6e 100644 --- a/frontend/declarations/bench/bench.did +++ b/frontend/declarations/bench/bench.did @@ -1,7 +1,11 @@ -type anon_class_9_1 = +type anon_class_10_1 = service { + getSchema: () -> (BenchSchema) query; + getStats: () -> (BenchResult) query; init: () -> (BenchSchema); - runCell: (nat, nat) -> (BenchResult); + runCellQuery: (nat, nat) -> (BenchResult) query; + runCellUpdate: (nat, nat) -> (BenchResult); + runCellUpdateAwait: (nat, nat) -> (BenchResult); }; type BenchSchema = record { @@ -12,11 +16,11 @@ type BenchSchema = }; type BenchResult = record { - instructions: nat; - rts_collector_instructions: nat; - rts_heap_size: nat; - rts_memory_size: nat; - rts_mutator_instructions: nat; - rts_total_allocation: nat; + instructions: int; + rts_collector_instructions: int; + rts_heap_size: int; + rts_memory_size: int; + rts_mutator_instructions: int; + rts_total_allocation: int; }; -service : () -> anon_class_9_1 +service : () -> anon_class_10_1 diff --git a/frontend/declarations/bench/bench.did.d.ts b/frontend/declarations/bench/bench.did.d.ts index 6791730e..7e43e999 100644 --- a/frontend/declarations/bench/bench.did.d.ts +++ b/frontend/declarations/bench/bench.did.d.ts @@ -15,8 +15,12 @@ export interface BenchSchema { 'rows' : Array, 'description' : string, } -export interface anon_class_9_1 { +export interface anon_class_10_1 { + 'getSchema' : ActorMethod<[], BenchSchema>, + 'getStats' : ActorMethod<[], BenchResult>, 'init' : ActorMethod<[], BenchSchema>, - 'runCell' : ActorMethod<[bigint, bigint], BenchResult>, + 'runCellQuery' : ActorMethod<[bigint, bigint], BenchResult>, + 'runCellUpdate' : ActorMethod<[bigint, bigint], BenchResult>, + 'runCellUpdateAwait' : ActorMethod<[bigint, bigint], BenchResult>, } -export interface _SERVICE extends anon_class_9_1 {} +export interface _SERVICE extends anon_class_10_1 {} diff --git a/frontend/declarations/bench/bench.did.js b/frontend/declarations/bench/bench.did.js index c3de6954..9c61cf35 100644 --- a/frontend/declarations/bench/bench.did.js +++ b/frontend/declarations/bench/bench.did.js @@ -6,17 +6,21 @@ export const idlFactory = ({ IDL }) => { 'description' : IDL.Text, }); const BenchResult = IDL.Record({ - 'instructions' : IDL.Nat, - 'rts_memory_size' : IDL.Nat, - 'rts_total_allocation' : IDL.Nat, - 'rts_collector_instructions' : IDL.Nat, - 'rts_mutator_instructions' : IDL.Nat, - 'rts_heap_size' : IDL.Nat, + 'instructions' : IDL.Int, + 'rts_memory_size' : IDL.Int, + 'rts_total_allocation' : IDL.Int, + 'rts_collector_instructions' : IDL.Int, + 'rts_mutator_instructions' : IDL.Int, + 'rts_heap_size' : IDL.Int, }); - const anon_class_9_1 = IDL.Service({ + const anon_class_10_1 = IDL.Service({ + 'getSchema' : IDL.Func([], [BenchSchema], ['query']), + 'getStats' : IDL.Func([], [BenchResult], ['query']), 'init' : IDL.Func([], [BenchSchema], []), - 'runCell' : IDL.Func([IDL.Nat, IDL.Nat], [BenchResult], []), + 'runCellQuery' : IDL.Func([IDL.Nat, IDL.Nat], [BenchResult], ['query']), + 'runCellUpdate' : IDL.Func([IDL.Nat, IDL.Nat], [BenchResult], []), + 'runCellUpdateAwait' : IDL.Func([IDL.Nat, IDL.Nat], [BenchResult], []), }); - return anon_class_9_1; + return anon_class_10_1; }; export const init = ({ IDL }) => { return []; }; From 15282fdadc00357a0bbba28176215830a1385ea0 Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Fri, 27 Oct 2023 12:26:06 +0400 Subject: [PATCH 14/16] mops-lock.json -> mops.lock --- cli/integrity.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/integrity.ts b/cli/integrity.ts index 819b8971..a5459f76 100644 --- a/cli/integrity.ts +++ b/cli/integrity.ts @@ -64,7 +64,7 @@ export async function checkIntegrity() { export async function saveLockFile() { let rootDir = getRootDir(); let fileHashes = await getFileHashesFromRegistry(); - let lockFile = path.join(rootDir, 'mops-lock.json'); + let lockFile = path.join(rootDir, 'mops.lock'); // if lock file exists and mops.toml hasn't changed, don't update it if (fs.existsSync(lockFile)) { @@ -93,7 +93,7 @@ export async function saveLockFile() { // compare hashes of local files with hashes from the lock file export async function checkLockFile() { let rootDir = getRootDir(); - let lockFile = path.join(rootDir, 'mops-lock.json'); + let lockFile = path.join(rootDir, 'mops.lock'); let packageIds = await getResolvedMopsPackageIds(); // check if lock file exists From 9982d3de05eb384f26e2a3a1d2d7174ced1ce800 Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Fri, 27 Oct 2023 15:45:01 +0400 Subject: [PATCH 15/16] [cli] add `--lockfile` arg --- cli/cli.ts | 13 +++++--- cli/commands/add.ts | 11 ++++++- cli/commands/install-all.ts | 11 ++++++- cli/commands/install.ts | 6 ++-- cli/commands/remove.ts | 12 +++++++- cli/commands/sync.ts | 61 +++++++++++++++++++++---------------- cli/commands/update.ts | 11 ++++++- cli/integrity.ts | 31 ++++++++++++++----- 8 files changed, 111 insertions(+), 45 deletions(-) diff --git a/cli/cli.ts b/cli/cli.ts index b2cb225b..27eb2f61 100644 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -49,6 +49,7 @@ program .description('Install the package and save it to mops.toml') .option('--dev') .option('--verbose') + .addOption(new Option('--lockfile ', 'Lockfile action').choices(['save', 'ignore'])) .action(async (pkg, options) => { if (!checkConfigFile()) { process.exit(1); @@ -64,6 +65,7 @@ program .option('--dev', 'Remove from dev-dependencies instead of dependencies') .option('--verbose', 'Show more information') .option('--dry-run', 'Do not actually remove anything') + .addOption(new Option('--lockfile ', 'Lockfile action').choices(['save', 'check', 'ignore'])) .action(async (pkg, options) => { if (!checkConfigFile()) { process.exit(1); @@ -77,6 +79,7 @@ program .alias('i') .description('Install all dependencies specified in mops.toml') .option('--verbose') + .addOption(new Option('--lockfile ', 'Lockfile action').choices(['save', 'check', 'ignore'])) .action(async (pkg, options) => { if (!checkConfigFile()) { process.exit(1); @@ -313,8 +316,9 @@ program program .command('sync') .description('Add missing packages and remove unused packages') - .action(async () => { - await sync(); + .addOption(new Option('--lockfile ', 'Lockfile action').choices(['save', 'ignore'])) + .action(async (options) => { + await sync(options); }); // outdated @@ -329,8 +333,9 @@ program program .command('update [pkg]') .description('Update dependencies specified in mops.toml') - .action(async (pkg) => { - await update(pkg); + .addOption(new Option('--lockfile ', 'Lockfile action').choices(['save', 'ignore'])) + .action(async (pkg, options) => { + await update(pkg, options); }); // transfer-ownership diff --git a/cli/commands/add.ts b/cli/commands/add.ts index f15cc7fb..8be932cd 100644 --- a/cli/commands/add.ts +++ b/cli/commands/add.ts @@ -5,8 +5,15 @@ import {checkConfigFile, getGithubCommit, getHighestVersion, parseGithubURL, rea import {installFromGithub} from '../vessel.js'; import {install} from './install.js'; import {notifyInstalls} from '../notify-installs.js'; +import {checkIntegrity} from '../integrity.js'; -export async function add(name: string, {verbose = false, dev = false} = {}) { +type AddOptions = { + verbose?: boolean; + dev?: boolean; + lockfile?: 'save' | 'ignore'; +}; + +export async function add(name: string, {verbose = false, dev = false, lockfile}: AddOptions = {}) { if (!checkConfigFile()) { return; } @@ -103,4 +110,6 @@ export async function add(name: string, {verbose = false, dev = false} = {}) { logUpdate.clear(); console.log(chalk.green('Package installed ') + `${pkgDetails.name} = "${pkgDetails.repo || pkgDetails.path || pkgDetails.version}"`); + + await checkIntegrity(lockfile); } \ No newline at end of file diff --git a/cli/commands/install-all.ts b/cli/commands/install-all.ts index d550b641..33482a15 100644 --- a/cli/commands/install-all.ts +++ b/cli/commands/install-all.ts @@ -4,8 +4,15 @@ import {checkConfigFile, readConfig} from '../mops.js'; import {install} from './install.js'; import {installFromGithub} from '../vessel.js'; import {notifyInstalls} from '../notify-installs.js'; +import {checkIntegrity} from '../integrity.js'; -export async function installAll({verbose = false, silent = false} = {}) { +type InstallAllOptions = { + verbose?: boolean; + silent?: boolean; + lockfile?: 'save' | 'check' | 'ignore'; +} + +export async function installAll({verbose = false, silent = false, lockfile}: InstallAllOptions = {}) { if (!checkConfigFile()) { return; } @@ -35,4 +42,6 @@ export async function installAll({verbose = false, silent = false} = {}) { logUpdate.clear(); console.log(chalk.green('All packages installed')); } + + await checkIntegrity(lockfile); } \ No newline at end of file diff --git a/cli/commands/install.ts b/cli/commands/install.ts index ff5926f5..391a70a1 100644 --- a/cli/commands/install.ts +++ b/cli/commands/install.ts @@ -137,8 +137,8 @@ export async function install(pkg: string, version = '', {verbose = false, silen installedDeps = {...installedDeps, [pkg]: version}; } - if (ok) { - return installedDeps; + if (!ok) { + return false; } - return false; + return installedDeps; } \ No newline at end of file diff --git a/cli/commands/remove.ts b/cli/commands/remove.ts index 923b6412..9e7572ec 100644 --- a/cli/commands/remove.ts +++ b/cli/commands/remove.ts @@ -3,8 +3,16 @@ import {deleteSync} from 'del'; import chalk from 'chalk'; import {formatDir, formatGithubDir, checkConfigFile, readConfig, writeConfig} from '../mops.js'; import {Config, Dependency} from '../types.js'; +import {checkIntegrity} from '../integrity.js'; -export async function remove(name: string, {dev = false, verbose = false, dryRun = false} = {}) { +type RemoveOptions = { + verbose?: boolean; + dev?: boolean; + dryRun?: boolean; + lockfile?: 'save' | 'ignore'; +}; + +export async function remove(name: string, {dev = false, verbose = false, dryRun = false, lockfile}: RemoveOptions = {}) { if (!checkConfigFile()) { return; } @@ -92,4 +100,6 @@ export async function remove(name: string, {dev = false, verbose = false, dryRun dryRun || writeConfig(config); console.log(chalk.green('Package removed ') + `${name} = "${version}"`); + + await checkIntegrity(lockfile); } \ No newline at end of file diff --git a/cli/commands/sync.ts b/cli/commands/sync.ts index 70f4b57e..49518caa 100644 --- a/cli/commands/sync.ts +++ b/cli/commands/sync.ts @@ -5,6 +5,40 @@ import chalk from 'chalk'; import {checkConfigFile, getRootDir, readConfig} from '../mops.js'; import {add} from './add.js'; import {remove} from './remove.js'; +import {checkIntegrity} from '../integrity.js'; + +type SyncOptions = { + lockfile?: 'save' | 'ignore'; +}; + +export async function sync({lockfile}: SyncOptions = {}) { + if (!checkConfigFile()) { + return; + } + + let missing = await getMissingPackages(); + let unused = await getUnusedPackages(); + + missing.length && console.log(`${chalk.yellow('Missing packages:')} ${missing.join(', ')}`); + unused.length && console.log(`${chalk.yellow('Unused packages:')} ${unused.join(', ')}`); + + let config = readConfig(); + let deps = new Set(Object.keys(config.dependencies || {})); + let devDeps = new Set(Object.keys(config['dev-dependencies'] || {})); + + // add missing packages + for (let pkg of missing) { + await add(pkg); + } + + // remove unused packages + for (let pkg of unused) { + let dev = devDeps.has(pkg) && !deps.has(pkg); + await remove(pkg, {dev}); + } + + await checkIntegrity(lockfile); +} let ignore = [ '**/node_modules/**', @@ -71,31 +105,4 @@ async function getUnusedPackages(): Promise { allDeps.delete(pkg); } return [...allDeps]; -} - -export async function sync() { - if (!checkConfigFile()) { - return; - } - - let missing = await getMissingPackages(); - let unused = await getUnusedPackages(); - - missing.length && console.log(`${chalk.yellow('Missing packages:')} ${missing.join(', ')}`); - unused.length && console.log(`${chalk.yellow('Unused packages:')} ${unused.join(', ')}`); - - let config = readConfig(); - let deps = new Set(Object.keys(config.dependencies || {})); - let devDeps = new Set(Object.keys(config['dev-dependencies'] || {})); - - // add missing packages - for (let pkg of missing) { - await add(pkg); - } - - // remove unused packages - for (let pkg of unused) { - let dev = devDeps.has(pkg) && !deps.has(pkg); - await remove(pkg, {dev}); - } } \ No newline at end of file diff --git a/cli/commands/update.ts b/cli/commands/update.ts index 7cead457..b56afbdf 100644 --- a/cli/commands/update.ts +++ b/cli/commands/update.ts @@ -2,8 +2,15 @@ import chalk from 'chalk'; import {checkConfigFile, getGithubCommit, parseGithubURL, readConfig} from '../mops.js'; import {add} from './add.js'; import {getAvailableUpdates} from './available-updates.js'; +import {checkIntegrity} from '../integrity.js'; -export async function update(pkg?: string) { +type UpdateOptions = { + verbose?: boolean; + dev?: boolean; + lockfile?: 'save' | 'check' | 'ignore'; +}; + +export async function update(pkg?: string, {lockfile}: UpdateOptions = {}) { if (!checkConfigFile()) { return; } @@ -48,4 +55,6 @@ export async function update(pkg?: string) { await add(`${dep[0]}@${dep[2]}`, {dev}); } } + + await checkIntegrity(lockfile); } \ No newline at end of file diff --git a/cli/integrity.ts b/cli/integrity.ts index a5459f76..7d352dea 100644 --- a/cli/integrity.ts +++ b/cli/integrity.ts @@ -11,6 +11,21 @@ type LockFileV1 = { hashes: Record>; }; +export async function checkIntegrity(lock?: 'save' | 'check' | 'ignore') { + let force = !!lock; + + if (!lock) { + lock = process.env['CI'] ? 'check' : 'save'; + } + + if (lock === 'save') { + await saveLockFile(); + } + else if (lock === 'check') { + await checkLockFile(force); + } +} + async function getFileHashesFromRegistry(): Promise<[string, [string, Uint8Array | number[]][]][]> { let packageIds = await getResolvedMopsPackageIds(); let actor = await mainActor(); @@ -43,7 +58,7 @@ function getMopsTomlHash(): string { } // compare hashes of local files with hashes from the registry -export async function checkIntegrity() { +export async function checkRemote() { let fileHashesFromRegistry = await getFileHashesFromRegistry(); // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -63,7 +78,6 @@ export async function checkIntegrity() { export async function saveLockFile() { let rootDir = getRootDir(); - let fileHashes = await getFileHashesFromRegistry(); let lockFile = path.join(rootDir, 'mops.lock'); // if lock file exists and mops.toml hasn't changed, don't update it @@ -75,6 +89,8 @@ export async function saveLockFile() { } } + let fileHashes = await getFileHashesFromRegistry(); + let lockFileJson: LockFileV1 = { version: 1, mopsTomlHash: getMopsTomlHash(), @@ -91,20 +107,21 @@ export async function saveLockFile() { } // compare hashes of local files with hashes from the lock file -export async function checkLockFile() { +export async function checkLockFile(force = false) { let rootDir = getRootDir(); let lockFile = path.join(rootDir, 'mops.lock'); - let packageIds = await getResolvedMopsPackageIds(); // check if lock file exists if (!fs.existsSync(lockFile)) { - // BREAKING CHANGE - // console.error('Missing lock file. Run `mops install` to generate it.'); - // process.exit(1); + if (force) { + console.error('Missing lock file. Run `mops install` to generate it.'); + process.exit(1); + } return; } let lockFileJson: LockFileV1 = JSON.parse(fs.readFileSync(lockFile).toString()); + let packageIds = await getResolvedMopsPackageIds(); // check lock file version if (lockFileJson.version !== 1) { From 3c0393135e51881b4812fbcf96ebc53d5e8e2528 Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Tue, 31 Oct 2023 12:27:54 +0400 Subject: [PATCH 16/16] fix integrity check --- backend/main/main-canister.mo | 3 ++- cli/cli.ts | 2 +- cli/commands/update.ts | 2 +- cli/integrity.ts | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/main/main-canister.mo b/backend/main/main-canister.mo index 8f0cbf26..b99aa365 100644 --- a/backend/main/main-canister.mo +++ b/backend/main/main-canister.mo @@ -590,6 +590,7 @@ actor { for (fileId in publicFileIds.vals()) { let ?hasher = publishingFileHashers.get(fileId) else return #err("Hasher not found"); hashByFileId.put(fileId, hasher.sum()); + publishingFileHashers.delete(fileId); }; // finish uploads @@ -757,7 +758,7 @@ actor { let #ok(fileMeta) = await storage.getFileMeta(fileId) else Debug.trap("File meta '" # fileId # "' not found"); let hasher = Sha256.Digest(#sha256); - for (i in Iter.range(1, fileMeta.chunkCount)) { + for (i in Iter.range(0, fileMeta.chunkCount - 1)) { let #ok(chunk) = await storage.downloadChunk(fileId, i) else Debug.trap("File chunk '" # fileId # "' not found"); hasher.writeBlob(chunk); }; diff --git a/cli/cli.ts b/cli/cli.ts index 27eb2f61..92cdef3f 100644 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -65,7 +65,7 @@ program .option('--dev', 'Remove from dev-dependencies instead of dependencies') .option('--verbose', 'Show more information') .option('--dry-run', 'Do not actually remove anything') - .addOption(new Option('--lockfile ', 'Lockfile action').choices(['save', 'check', 'ignore'])) + .addOption(new Option('--lockfile ', 'Lockfile action').choices(['save', 'ignore'])) .action(async (pkg, options) => { if (!checkConfigFile()) { process.exit(1); diff --git a/cli/commands/update.ts b/cli/commands/update.ts index b56afbdf..e2e00e20 100644 --- a/cli/commands/update.ts +++ b/cli/commands/update.ts @@ -7,7 +7,7 @@ import {checkIntegrity} from '../integrity.js'; type UpdateOptions = { verbose?: boolean; dev?: boolean; - lockfile?: 'save' | 'check' | 'ignore'; + lockfile?: 'save' | 'ignore'; }; export async function update(pkg?: string, {lockfile}: UpdateOptions = {}) { diff --git a/cli/integrity.ts b/cli/integrity.ts index 7d352dea..d45ea67d 100644 --- a/cli/integrity.ts +++ b/cli/integrity.ts @@ -20,6 +20,7 @@ export async function checkIntegrity(lock?: 'save' | 'check' | 'ignore') { if (lock === 'save') { await saveLockFile(); + await checkLockFile(force); } else if (lock === 'check') { await checkLockFile(force);