Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
9 changes: 9 additions & 0 deletions packages/did-ssi-hub-store/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# @ew-did-registry/did-ssi-hub-store

This package provides SSI-HUB S3-based implementation of the IDidStore

## Structure

- dist - contains compiled javascript files
- src - contains source typescript files
- test - contains unit test files
4,736 changes: 4,736 additions & 0 deletions packages/did-ssi-hub-store/package-lock.json

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions packages/did-ssi-hub-store/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@ew-did-registry/did-ssi-hub-store",
"version": "0.9.0",
"publishConfig": {
"access": "public"
},
"description": "S3-based implementation of IDidStore",
"main": "./dist/index.js",
"browser": "./dist/index.esm.js",
"types": "./dist/index.d.ts",
"scripts": {
"compile": "webpack --config ../../webpack.config.js",
"lint": "../../node_modules/.bin/eslint src/**/*.ts",
"fix": "../../node_modules/.bin/eslint src/**/*.ts --fix"
},
"keywords": [
"EnergyWeb",
"claim",
"store",
"did"
],
"author": {
"name": "EnergyWeb",
"url": "https://www.energyweb.org/"
},
"license": "GPL-3.0-or-later",
"dependencies": {
"@ew-did-registry/did-store-interface": "0.9.0",
"axios": "^1.10.0",
"axios-retry": "^4.5.0"
},
"devDependencies": {
"@types/chance": "^1.1.3",
"@types/jest": "^29.5.14",
"@types/mocha": "^10.0.10",
"@types/node": "^15.12.3",
"chai-as-promised": "^7.1.1",
"chance": "^1.1.9",
"dotenv": "^16.0.3",
"ganache-cli": "^6.12.2",
"lodash": "^4.17.21",
"node-polyfill-webpack-plugin": "^1.1.4",
"ts-loader": "^9.2.6",
"ts-node": "^10.9.1",
"webpack": "^5.68.0",
"webpack-cli": "^4.9.2",
"webpack-merge": "^5.8.0"
}
}
142 changes: 142 additions & 0 deletions packages/did-ssi-hub-store/src/cacheCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import axios, {
AxiosError,
AxiosInstance,
AxiosRequestConfig,
InternalAxiosRequestConfig,
} from "axios";
import axiosRetry from "axios-retry";
import base64url from 'base64url';
import { utils, Wallet } from 'ethers';

export interface BaseCacheClientConfig {
baseURL: string;
privateKey: string;
}

export type CacheClientConfig =
| (BaseCacheClientConfig & { didPrefix: string; did?: undefined })
| (BaseCacheClientConfig & { didPrefix?: undefined; did: string });

export class CacheClient {
private loginEndpoint: string;
private refreshEndpoint: string;
private accessToken: string | null = null;
private refreshToken: string | null = null;
private signer: Wallet;
private did: string;
private identityToken: string | null = null;
public api: AxiosInstance;

constructor(config: CacheClientConfig) {
this.signer = new Wallet(config.privateKey);
this.did = config.did ? config.did : this.ensureDidPrefixSuffix(config.didPrefix!) + this.signer.address;

Check warning on line 32 in packages/did-ssi-hub-store/src/cacheCache.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/did-ssi-hub-store/src/cacheCache.ts#L32

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
this.loginEndpoint = config.baseURL + '/login';
this.refreshEndpoint = config.baseURL + '/refresh_token';

this.api = axios.create({ baseURL: config.baseURL });

axiosRetry(this.api as any, {

Check warning on line 38 in packages/did-ssi-hub-store/src/cacheCache.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/did-ssi-hub-store/src/cacheCache.ts#L38

[@typescript-eslint/no-explicit-any] Unexpected any. Specify a different type.
retries: 3,
retryDelay: axiosRetry.exponentialDelay,
retryCondition: err =>
axiosRetry.isNetworkOrIdempotentRequestError(err) ||
Boolean(err.response && err.response.status >= 500 && err.response.status < 600),
});

this.api.interceptors.request.use(
async (config: InternalAxiosRequestConfig) => {
if (!this.accessToken) {
await this._login();
}
config.headers = config.headers ?? {};
(config.headers as any).Authorization = `Bearer ${this.accessToken}`;

Check warning on line 52 in packages/did-ssi-hub-store/src/cacheCache.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/did-ssi-hub-store/src/cacheCache.ts#L52

[@typescript-eslint/no-explicit-any] Unexpected any. Specify a different type.
return config;
}
);

this.api.interceptors.response.use(
res => res,
err => this._handle401(err)
);
}

private ensureDidPrefixSuffix(didPrefix: string): string {
return didPrefix.endsWith(':') ? didPrefix : didPrefix + ':';
}


private async _login() {
await this.createIdentityToken();
const res = await axios.post(
this.loginEndpoint,
{ identityToken: this.identityToken },
{
headers: {
"accept": "*/*",
"Content-Type": "application/json",
},
}
);
this.accessToken = res.data.token;
this.refreshToken = res.data.refreshToken;
}

private async _refresh() {
const res = await axios.post(this.refreshEndpoint, {
refreshToken: this.refreshToken,
});
// Try both field names, prefer token if present
this.accessToken = res.data.token || res.data.accessToken;
this.refreshToken = res.data.refreshToken || this.refreshToken;
}

private async _handle401(err: AxiosError) {
const original = err.config as AxiosRequestConfig & { _retry?: boolean };
if (err.response && err.response.status === 401 && !original._retry) {
original._retry = true;

try {
await this._refresh();
} catch {
await this._login();
}
if (original.headers) {
(original.headers as any).Authorization = "Bearer " + this.accessToken;

Check warning on line 104 in packages/did-ssi-hub-store/src/cacheCache.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/did-ssi-hub-store/src/cacheCache.ts#L104

[@typescript-eslint/no-explicit-any] Unexpected any. Specify a different type.
}
return this.api(original);
}
return Promise.reject(err);
}

private async createIdentityToken(): Promise<void> {
const header = {
alg: 'ES256',
typ: 'JWT',
};
const encodedHeader = base64url(JSON.stringify(header));
const ttl: number = 1000 * 5;
const payload = {
iss: this.did,
claimData: {
blockNumber: 999999999999,
},
iat: Math.floor(Date.now() / 1000), // Current
exp: Math.floor(Date.now() / 1000) + ttl // Expires in 5 minutes
};

const encodedPayload = base64url(JSON.stringify(payload));
const message = utils.arrayify(
utils.keccak256(Buffer.from(`${encodedHeader}.${encodedPayload}`))
);
const sig = await this.signer.signMessage(message);
const encodedSig = base64url(sig);

this.identityToken = `${encodedHeader}.${encodedPayload}.${encodedSig}`;
}

// Helper HTTP methods
get<T = any>(...a: Parameters<AxiosInstance["get"]>) { return this.api.get<T>(...a); }

Check warning on line 138 in packages/did-ssi-hub-store/src/cacheCache.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/did-ssi-hub-store/src/cacheCache.ts#L138

[@typescript-eslint/no-explicit-any] Unexpected any. Specify a different type.
post<T = any>(...a: Parameters<AxiosInstance["post"]>) { return this.api.post<T>(...a); }

Check warning on line 139 in packages/did-ssi-hub-store/src/cacheCache.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/did-ssi-hub-store/src/cacheCache.ts#L139

[@typescript-eslint/no-explicit-any] Unexpected any. Specify a different type.
put<T = any>(...a: Parameters<AxiosInstance["put"]>) { return this.api.put<T>(...a); }

Check warning on line 140 in packages/did-ssi-hub-store/src/cacheCache.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/did-ssi-hub-store/src/cacheCache.ts#L140

[@typescript-eslint/no-explicit-any] Unexpected any. Specify a different type.
delete<T = any>(...a: Parameters<AxiosInstance["delete"]>) { return this.api.delete<T>(...a); }

Check warning on line 141 in packages/did-ssi-hub-store/src/cacheCache.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/did-ssi-hub-store/src/cacheCache.ts#L141

[@typescript-eslint/no-explicit-any] Unexpected any. Specify a different type.
}
42 changes: 42 additions & 0 deletions packages/did-ssi-hub-store/src/didStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { IDidStore } from "@ew-did-registry/did-store-interface";
import { CacheClient, CacheClientConfig } from "./cacheCache";

/**
* Implements decentralized storage in S3 Bucket
*/
export class DidStore implements IDidStore {
private client: CacheClient;

constructor(cacheConfig: CacheClientConfig) {
this.client = new CacheClient(cacheConfig);
}

/**
* @param claim stringified content
*/
async save(claim: string): Promise<string> {
const res = await this.client.post("/s3", { data: claim });
return res.data;
}

/**
* Looks up content identified by `cid`. If no content found during `timeout`, then `ContentNotFound` error is thrown.
* @param cid CID of the content
* @param timeout time limit for getting response, milliseconds
* @returns stringified content
*/
async get(uri: string): Promise<string> {
const res = await this.client.get(`/s3/${uri}`);
return res.data;
}


async delete(uri: string): Promise<boolean> {

Check warning on line 34 in packages/did-ssi-hub-store/src/didStore.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/did-ssi-hub-store/src/didStore.ts#L34

[@typescript-eslint/no-unused-vars] 'uri' is defined but never used.
try {
return false;
} catch (err: any) {

Check warning on line 37 in packages/did-ssi-hub-store/src/didStore.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/did-ssi-hub-store/src/didStore.ts#L37

[@typescript-eslint/no-explicit-any] Unexpected any. Specify a different type.
if (err.name === "NoSuchKey") return false;
throw err;
}
}
}
2 changes: 2 additions & 0 deletions packages/did-ssi-hub-store/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './cacheCache';
export * from './didStore';
1 change: 1 addition & 0 deletions packages/did-ssi-hub-store/test/big-claim.txt

Large diffs are not rendered by default.

69 changes: 69 additions & 0 deletions packages/did-ssi-hub-store/test/did-store.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import chai, { expect } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { Chance } from 'chance';
import * as fs from 'fs';
import path from 'path';
import { DidStore } from '../src';
import { credential } from './verifiable-credential';
import { Context } from 'mocha';

Check warning on line 8 in packages/did-ssi-hub-store/test/did-store.test.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/did-ssi-hub-store/test/did-store.test.ts#L8

[@typescript-eslint/no-unused-vars] 'Context' is defined but never used.

chai.use(chaiAsPromised);

const chance = new Chance();

Check warning on line 12 in packages/did-ssi-hub-store/test/did-store.test.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/did-ssi-hub-store/test/did-store.test.ts#L12

[@typescript-eslint/no-unused-vars] 'chance' is assigned a value but never used.

const testSuite = function () {

it('should persist multiple claims sequentially', async function () {
for (const i of '0123456789') {
const claim = `TEST CLAIM ${i}`;
const cid = await this.s3Store.save(claim);
const stored = await this.s3Store.get(cid);
expect(stored).equal(claim);
}
});

it('should persist big claim', async function () {
const claim = fs.readFileSync('./test/big-claim.txt').toString('utf8');
const cid = await this.s3Store.save(claim);
const stored = await this.s3Store.get(cid);
expect(stored.length).equal(claim.length);
expect(stored).equal(claim);
});

it('should persist object', async function () {
const content = JSON.stringify(credential);
const cid = await this.s3Store.save(content);
const stored = await this.s3Store.get(cid);
expect(stored).equal(content);
});

it('should persist array', async function () {
const content = JSON.stringify([1, 2, 3]);
const cid = await this.s3Store.save(content);
const stored = await this.s3Store.get(cid);
expect(stored).equal(content);
});
};

describe('[DID-STORE-PACKAGE]', function () {
this.timeout(0);

before(async function () {
(await import('dotenv')).config({ path: path.resolve(__dirname, '.env') });
});

describe('[DEVELOP S3]', function () {
before(async function () {
this.s3Store = new DidStore(process.env.S3_BUCKET!, {

Check warning on line 57 in packages/did-ssi-hub-store/test/did-store.test.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/did-ssi-hub-store/test/did-store.test.ts#L57

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
region: process.env.AWS_REGION!,

Check warning on line 58 in packages/did-ssi-hub-store/test/did-store.test.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/did-ssi-hub-store/test/did-store.test.ts#L58

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,

Check warning on line 60 in packages/did-ssi-hub-store/test/did-store.test.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/did-ssi-hub-store/test/did-store.test.ts#L60

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,

Check warning on line 61 in packages/did-ssi-hub-store/test/did-store.test.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/did-ssi-hub-store/test/did-store.test.ts#L61

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
},
});
});

testSuite();
});

});
77 changes: 77 additions & 0 deletions packages/did-ssi-hub-store/test/verifiable-credential.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
export const credential = {
'@context': [
'https://www.w3.org/2018/credentials/v1',
'https://w3id.org/vc/status-list/2021/v1',
],
id: 'urn:uuid:e463c294-17bd-42d1-817a-0248bfa149f3',
type: ['VerifiableCredential', 'EWFRole'],
credentialSubject: {
id: 'did:ethr:0x0539:0x0d1d4e623D10F9FBA5Db95830F7d3839406C6AF2',
issuerFields: [],
role: { namespace: 'admin', version: '1' },
},
issuer: 'did:ethr:0x539:0x0d1d4e623d10f9fba5db95830f7d3839406c6af2',
issuanceDate: '2022-06-24T11:28:28.103Z',
proof: {
'@context': 'https://w3id.org/security/suites/eip712sig-2021/v1',
type: 'EthereumEip712Signature2021',
proofPurpose: 'assertionMethod',
proofValue:
'0xd4274533512a8715247fcfd854458bf427bcfb285672383730225b811c9428db015b8e98f46eb40c53e798c0914333ae1aa3b947ae60e6e60cc09bcb469f22e31c',
verificationMethod:
'did:ethr:0x539:0x0d1d4e623d10f9fba5db95830f7d3839406c6af2#controller',
created: '2022-06-24T11:28:28.105Z',
eip712Domain: {
domain: {},
messageSchema: {
CredentialSubject: [
{ name: 'id', type: 'string' },
{ name: 'role', type: 'EWFRole' },
{ name: 'issuerFields', type: 'IssuerFields[]' },
],
EIP712Domain: [],
EWFRole: [
{ name: 'namespace', type: 'string' },
{ name: 'version', type: 'string' },
],
IssuerFields: [
{ name: 'key', type: 'string' },
{ name: 'value', type: 'string' },
],
Proof: [
{ name: '@context', type: 'string' },
{ name: 'verificationMethod', type: 'string' },
{ name: 'created', type: 'string' },
{ name: 'proofPurpose', type: 'string' },
{ name: 'type', type: 'string' },
],
StatusList2021Entry: [
{ name: 'id', type: 'string' },
{ name: 'type', type: 'string' },
{ name: 'statusPurpose', type: 'string' },
{ name: 'statusListIndex', type: 'string' },
{ name: 'statusListCredential', type: 'string' },
],
VerifiableCredential: [
{ name: '@context', type: 'string[]' },
{ name: 'id', type: 'string' },
{ name: 'type', type: 'string[]' },
{ name: 'issuer', type: 'string' },
{ name: 'issuanceDate', type: 'string' },
{ name: 'credentialSubject', type: 'CredentialSubject' },
{ name: 'proof', type: 'Proof' },
{ name: 'credentialStatus', type: 'StatusList2021Entry' },
],
},
primaryType: 'VerifiableCredential',
},
},
credentialStatus: {
id: 'https://credential-status/admin',
type: 'Entry2021',
statusPurpose: 'REVOCATION',
statusListCredential:
'https://isc.energyweb.org/api/v1/status-list/700e7ad4-5309-421c-bcf9-43acfa89c0e4',
statusListIndex: '0',
},
};
Loading
Loading