Skip to content

Commit

Permalink
feat: add unwrapping feature to OA CLI (#188)
Browse files Browse the repository at this point in the history
* feat: add codes for unwrap cli

* chore: removed unneccesary variables

* chore: add test file for unwrap

* chore: update readme

* chore: update unwrap success message

* chore: fix eslint complaints

* chore: refactor and changing return types

* chore: removed validation errors for unwrap handler and fix eslint complaints

* chore: update test file

* chore: fix eslint complaints

* chore: refactor writeOutput function to disk and update readme

* chore: export enum Output to be used by wrap and unwrap

* chore: update imports
  • Loading branch information
zixiang2018 authored Feb 9, 2022
1 parent c62cf7a commit 7e9998b
Show file tree
Hide file tree
Showing 14 changed files with 419 additions and 34 deletions.
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ npx -p @govtechsg/open-attestation-cli open-attestation <arguments>
| Encrypt document ||||
| Decrypt document ||||
| Wrap document ||||
| Unwrap document ||||
| Verify document ||||
| Change holder (Title Escrow) ||||
| Nominate change of owner (Title Escrow) ||||
Expand Down Expand Up @@ -151,6 +152,38 @@ open-attestation wrap ./examples/raw-documents/ ./examples/wrapped-documents/ --

> **_NOTE:_** For transferable records, you should wrap them individually as each of them would be minted to a unique title escrow that represents the beneficiary and holder entities of the document. For more information about title escrow, refer [here](https://www.openattestation.com/docs/integrator-section/transferable-record/title-escrow).
### Unwrapping documents

This command processes a document in the input directory. It will unwrap the wrapped document to its raw document form to be displayed on the console.

Example:

```bash
open-attestation unwrap ./examples/v2/wrapped-documents/example.0.json

✔ success The document has been unwrapped
```

The command will display the result in the console. If you need to save the file you can use the `--output-file` option.

Example:

```bash
open-attestation unwrap ./examples/v2/wrapped-documents/example.0.json --output-file ./examples/v2/raw-documents/example.0.json

✔ success The document has been unwrapped
```

If you need to unwrap a folder you will need to provide the `--output-dir` option to specify which folder the documents must be unwrapped in.

Example:

```bash
open-attestation unwrap ./examples/v2/wrapped-documents --output-dir ./examples/v2/raw-documents

✔ success The documents have been individually unwrapped into folder ./examples/v2/raw-documents
```

### Document privacy filter

This allows document holders to generate valid documents which obfuscates certain fields. For example, sensitive information that you wish not to disclose.
Expand Down
3 changes: 2 additions & 1 deletion performance-tests/wrap-performance-test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Output, wrap } from "../src/implementations/wrap";
import { wrap } from "../src/implementations/wrap";
import { Output } from "../src/implementations/utils/disk";
import { performance } from "perf_hooks";
import { existsSync, mkdirSync, rmdirSync, promises } from "fs";
import { SchemaId } from "@govtechsg/open-attestation";
Expand Down
22 changes: 22 additions & 0 deletions src/__tests__/fixture/2.0/unwrapped-example.1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"id": "53b75bbe",
"name": "Govtech Demo Certificate",
"$template": {
"name": "GOVTECH_DEMO",
"type": "EMBEDDED_RENDERER",
"url": "https://demo-renderer.opencerts.io"
},
"issuers": [
{
"name": "Govtech",
"documentStore": "0x718B518565B81097b185661EBba3966Ff32A0039",
"identityProof": {
"type": "DNS-TXT",
"location": "example.openattestation.com"
}
}
],
"recipient": {
"name": "Your Name"
}
}
33 changes: 33 additions & 0 deletions src/__tests__/fixture/2.0/unwrapped-example.2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"$template": {
"name": "main",
"type": "EMBEDDED_RENDERER",
"url": "https://tutorial-renderer.openattestation.com"
},
"recipient": {
"name": "John Doe"
},
"issuers": [
{
"name": "Demo Issuer",
"documentStore": "0xBBb55Bd1D709955241CAaCb327A765e2b6D69c8b",
"identityProof": {
"type": "DNS-TXT",
"location": "few-green-cat.sandbox.openattestation.com"
}
}
],
"signatoryAuthentication": {
"signature": "-",
"actualDateTime": "2020-05-29T09:46:34Z",
"statement": "The undersigned hereby declares that the above-stated information is correct and that the goods exported to [importer] comply with the origin requirements",
"description": "The undersigned hereby declares that the above-stated information"
},
"name": "Test Certificate",
"issueLocation": { "iD": "None", "name": "Adelaide" },
"status": "issued",
"isPreferential": true,
"freeTradeAgreement": "Test",
"iD": "Test",
"issueDateTime": "IssueDate"
}
31 changes: 31 additions & 0 deletions src/__tests__/fixture/2.0/wrapped-example.1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"version": "https://schema.openattestation.com/2.0/schema.json",
"data": {
"id": "189b1a8c-37a5-41d3-81be-a8937a80ed87:string:53b75bbe",
"name": "ff654adf-a018-49e3-bb63-87ffd070e901:string:Govtech Demo Certificate",
"$template": {
"name": "3400b6f8-2c7f-4f41-9c00-764e34fa3408:string:GOVTECH_DEMO",
"type": "4e4a1393-7051-451d-a83a-f71731b2a344:string:EMBEDDED_RENDERER",
"url": "d267a44b-66c6-4b7c-a249-a2910c6f35a8:string:https://demo-renderer.opencerts.io"
},
"issuers": [
{
"name": "797a7a59-9e89-47c1-b5db-e7f2134d76bf:string:Govtech",
"documentStore": "0276bfc6-01e8-4933-abbb-525698cc15a1:string:0x718B518565B81097b185661EBba3966Ff32A0039",
"identityProof": {
"type": "16f50562-241e-4200-b118-7af692d0d04d:string:DNS-TXT",
"location": "cacf4c12-a038-4419-8083-5b886cf5a410:string:example.openattestation.com"
}
}
],
"recipient": {
"name": "38587afb-16c0-48b4-8bfb-9d87ad430641:string:Your Name"
}
},
"signature": {
"type": "SHA3MerkleProof",
"targetHash": "cc7362fb3dc939987cae9e86900ade8c1d687b0c3419de776cd559c3eb211614",
"proof": [],
"merkleRoot": "cc7362fb3dc939987cae9e86900ade8c1d687b0c3419de776cd559c3eb211614"
}
}
45 changes: 45 additions & 0 deletions src/__tests__/fixture/2.0/wrapped-example.2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"version": "https://schema.openattestation.com/2.0/schema.json",
"data": {
"$template": {
"name": "f25e1526-2b29-4c86-a037-5432aad11160:string:main",
"type": "b6776f4f-eb98-4bb0-a4d3-53cb028e57df:string:EMBEDDED_RENDERER",
"url": "72db66a4-d9c4-4914-8082-8989e35c5575:string:https://tutorial-renderer.openattestation.com"
},
"recipient": {
"name": "d8c26d1e-8b90-49c1-a181-e8526a9912da:string:John Doe"
},
"issuers": [
{
"name": "ee379bb3-e5a6-466b-acec-4bc84c1cf128:string:Demo Issuer",
"documentStore": "fe02631a-88af-404c-a35c-b4eb7c983b39:string:0xBBb55Bd1D709955241CAaCb327A765e2b6D69c8b",
"identityProof": {
"type": "cfa8ec95-714c-4a61-9ab3-6dfa4b7f408f:string:DNS-TXT",
"location": "33448be7-0746-4cd2-a741-659774799393:string:few-green-cat.sandbox.openattestation.com"
}
}
],
"signatoryAuthentication": {
"signature": "7eb0a2bd-5250-4ec0-a660-02d7d0ec7ce2:string:-",
"actualDateTime": "3d7bc93b-1e68-404a-81c4-24f063c1c35c:string:2020-05-29T09:46:34Z",
"statement": "fd72e086-343f-45b9-95bd-e471717777ed:string:The undersigned hereby declares that the above-stated information is correct and that the goods exported to [importer] comply with the origin requirements",
"description": "b24f5a7f-80f3-4a55-9a1b-5304af4b16fb:string:The undersigned hereby declares that the above-stated information"
},
"name": "a460e8b2-7269-4ead-aec0-66be57f75d5f:string:Test Certificate",
"issueLocation": {
"iD": "5d7fb90a-b579-475e-b899-a035be95022d:string:None",
"name": "e4b9083b-4c02-4855-9dd2-cb0670080ec9:string:Adelaide"
},
"status": "a03a3377-a34c-42b9-9a70-f9f74a50e7d4:string:issued",
"isPreferential": "e3e18fc5-c100-4f3b-8c13-7632fcb388ab:boolean:true",
"freeTradeAgreement": "ee7b9c4d-217e-4dd0-b7fa-dd47053c2f5f:string:Test",
"iD": "e855cb01-6688-46d8-b7e0-b9a89293262a:string:Test",
"issueDateTime": "8281cecd-4433-4202-953e-eac3ac887e1c:string:IssueDate"
},
"signature": {
"type": "SHA3MerkleProof",
"targetHash": "cea06d5bcb7650527308f74a4bffe13272989a2c33c8a7513f38b0c0c9527a52",
"proof": [],
"merkleRoot": "cea06d5bcb7650527308f74a4bffe13272989a2c33c8a7513f38b0c0c9527a52"
}
}
93 changes: 93 additions & 0 deletions src/__tests__/unwrap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { unwrapIndividualDocuments } from "../implementations/unwrap";
import { Output } from "../implementations/utils/disk";
import fs from "fs";
import wrappedFileFixture1 from "./fixture/2.0/wrapped-example.1.json";
import unwrappedFileFixture1 from "./fixture/2.0/unwrapped-example.1.json";
import wrappedFileFixture2 from "./fixture/2.0/wrapped-example.2.json";
import unwrappedFileFixture2 from "./fixture/2.0/unwrapped-example.2.json";

jest.mock("fs");

describe("unwrap", () => {
describe("unwrapIndividualDocuments", () => {
it("unwrap a single wrapped document", async () => {
// @ts-ignore
jest.spyOn(fs, "lstatSync").mockReturnValue({ isDirectory: () => true });
jest.spyOn(fs, "readdir").mockImplementation((options, callback) => {
// @ts-ignore
return callback(null, ["wrapped-example.1.json"]);
});
jest.spyOn(fs, "readFileSync").mockImplementation((path) => {
// @ts-ignore
if (path.includes("wrapped-example.1.json")) {
return JSON.stringify(wrappedFileFixture1);
}
return "";
});
jest.spyOn(fs, "writeFileSync").mockImplementation((path, document) => {
if (typeof path !== "string") throw new Error("path is not string");
if (path.includes("wrapped-example.1.json")) {
const documentForCompare = JSON.parse(JSON.parse(JSON.stringify(document)));
// eslint-disable-next-line jest/no-conditional-expect
expect(documentForCompare).toMatchObject(unwrappedFileFixture1);
return;
}
throw new Error(`unhandled ${path} in spy`);
});

const unwrappedDocumentCount = await unwrapIndividualDocuments(
"./fixture/2.0",
"./fixture/2.0",
Output.Directory
);

expect(unwrappedDocumentCount).toEqual(1);
});
});

describe("unwrapMultipleDocuments", () => {
it("unwrap multiple wrapped documents", async () => {
// @ts-ignore
jest.spyOn(fs, "lstatSync").mockReturnValue({ isDirectory: () => true });
jest.spyOn(fs, "readdir").mockImplementation((options, callback) => {
// @ts-ignore
return callback(null, ["wrapped-example.1.json", "wrapped-example.2.json"]);
});
jest.spyOn(fs, "readFileSync").mockImplementation((path) => {
// @ts-ignore
if (path.includes("wrapped-example.1.json")) {
return JSON.stringify(wrappedFileFixture1);
}
// @ts-ignore
else if (path.includes("wrapped-example.2.json")) {
return JSON.stringify(wrappedFileFixture2);
}
return "";
});
jest.spyOn(fs, "writeFileSync").mockImplementation((path, document) => {
if (typeof path !== "string") throw new Error("path is not string");
if (path.includes("wrapped-example.1.json")) {
const documentForCompare = JSON.parse(JSON.parse(JSON.stringify(document)));
// eslint-disable-next-line jest/no-conditional-expect
expect(documentForCompare).toMatchObject(unwrappedFileFixture1);
return;
} else if (path.includes("wrapped-example.2.json")) {
const documentForCompare = JSON.parse(JSON.parse(JSON.stringify(document)));
// eslint-disable-next-line jest/no-conditional-expect
expect(documentForCompare).toMatchObject(unwrappedFileFixture2);
return;
}
throw new Error(`unhandled ${path} in spy`);
});

const unwrappedDocumentCount = await unwrapIndividualDocuments(
"./fixture/2.0",
"./fixture/2.0",
Output.Directory
);

expect(unwrappedDocumentCount).toEqual(2);
});
});
});
3 changes: 2 additions & 1 deletion src/__tests__/wrap.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { appendProofToDocuments, merkleHashmap, Output } from "../implementations/wrap";
import { appendProofToDocuments, merkleHashmap } from "../implementations/wrap";
import { Output } from "../implementations/utils/disk";
import fs from "fs";
import { utils } from "@govtechsg/open-attestation";

Expand Down
76 changes: 76 additions & 0 deletions src/commands/unwrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Argv } from "yargs";
import signale from "signale";
import { unwrap } from "../implementations/unwrap";
import { isDir, Output } from "../implementations/utils/disk";
interface UnwrapCommand {
wrappedDocumentsPath: string;
outputDir?: string;
outputFile?: string;
silent?: boolean;
}

export const command = "unwrap <wrapped-documents-path> [options]";

export const describe = "Unwrap a document";

export const builder = (yargs: Argv): Argv =>
yargs
.positional("wrapped-documents-path", {
description: "Directory containing the issued wrapped document or a single wrapped document file",
normalize: true,
type: "string",
})
.option("output-file", {
alias: "of",
description: "Write output to a file. Only use when <unwrapped-documents-dir> is a document",
type: "string",
conflicts: "output-dir",
})
.option("output-dir", {
alias: "od",
description: "Write output to a directory",
type: "string",
conflicts: "output-file",
})
.option("silent", {
alias: "silent",
description: "Disable console outputs when outputting to stdout",
type: "boolean",
});

export const handler = async (args: UnwrapCommand): Promise<void | undefined> => {
try {
const outputPathType = args.outputDir ? Output.Directory : args.outputFile ? Output.File : Output.StdOut;
const outputPath = args.outputDir || args.outputFile; // undefined when we use std out

// when input type is directory, output type must only be directory
if (isDir(args.wrappedDocumentsPath) && outputPathType !== Output.Directory) {
signale.error(
"Output path type can only be directory when using directory as raw documents path, use --output-dir"
);
process.exit(1);
}

// when outputting to stdout, disable signale so that the logs do not interfere
if (args.silent) {
signale.disable();
}

const rawDocsCount = await unwrap({
inputPath: args.wrappedDocumentsPath,
outputPath,
outputPathType,
});

if (rawDocsCount) {
if (rawDocsCount > 1) {
signale.success(`The documents have been individually unwrapped into folder ${outputPath}`);
} else {
signale.success(`The document has been unwrapped`);
}
}
} catch (err) {
signale.error(err.message);
process.exit(1);
}
};
4 changes: 2 additions & 2 deletions src/commands/wrap.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Argv } from "yargs";
import signale from "signale";
import { Output, wrap } from "../implementations/wrap";
import { wrap } from "../implementations/wrap";
import { transformValidationErrors } from "../implementations/wrap/ajvErrorTransformer";
import { isDir } from "../implementations/utils/disk";
import { isDir, Output } from "../implementations/utils/disk";
import { SchemaId } from "@govtechsg/open-attestation";

interface WrapCommand {
Expand Down
1 change: 1 addition & 0 deletions src/implementations/unwrap/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./unwrap";
Loading

0 comments on commit 7e9998b

Please sign in to comment.