├── CODEOWNERS ├── src ├── index.ts ├── commands │ ├── clear.ts │ ├── paystring-show.ts │ ├── keys-clear.ts │ ├── paystring-from-url.ts │ ├── paystring-to-url.ts │ ├── paystring-save.ts │ ├── paystring-init.ts │ ├── keys-print.ts │ ├── paystring-load.ts │ ├── key-load.ts │ ├── key-generate.ts │ ├── keys-list.ts │ ├── index.ts │ ├── crypto-address-remove.ts │ ├── paystring-verify.ts │ ├── pem-utils.ts │ ├── files.ts │ ├── crypto-address-add.ts │ ├── paystring-sign.ts │ ├── paystring-inspect.ts │ ├── localstorage.ts │ └── Command.ts └── cli.ts ├── tsconfig.eslint.json ├── .eslintignore ├── .prettierignore ├── .prettierrc.js ├── test └── unit │ ├── identity-key.pem │ ├── getSigningKeyFromFile.test.ts │ ├── loadPayString.test.ts │ ├── jwkToPem.test.ts │ └── signPayString.test.ts ├── .mocharc.js ├── Dockerfile ├── lefthook.yml ├── nyc.config.js ├── tsconfig.json ├── .gitlab-ci.yml ├── .vscode └── launch.json ├── .eslintrc.js ├── package.json ├── .gitignore ├── README.md └── LICENSE /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hbergren 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './commands' 2 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*", "typings/**/*", "test/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Do not lint generated files. 2 | src/generated 3 | build 4 | dist 5 | 6 | # Don't ever lint node_modules 7 | node_modules 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Do not lint generated files. 2 | src/generated 3 | build 4 | dist 5 | .nyc_output 6 | 7 | # Don't ever lint node_modules and NPM stuff 8 | .npm 9 | node_modules 10 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 2, 3 | printWidth: 80, 4 | singleQuote: true, 5 | semi: false, 6 | trailingComma: 'all', 7 | arrowParens: 'always', 8 | } 9 | -------------------------------------------------------------------------------- /test/unit/identity-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgywzMHwumNjJl44zd 3 | Mr9/J8hOvaLzS3maty54GQBmWoyhRANCAASrJWapK6/OMmWc1FE3KnOBDAWWtS4b 4 | YMXBh7Cj1aFlhPp6hXMZROZ1+S/EfLguSvanESlYj49SSbqTS88v5lkg 5 | -----END PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | require: ['ts-node/register', 'source-map-support/register'], 5 | extension: ['ts'], 6 | 7 | // Do not look for mocha opts file 8 | opts: false, 9 | 10 | // Warn if test exceed 75ms duration 11 | slow: 75, 12 | 13 | // Fail if tests exceed 10000ms 14 | timeout: 10000, 15 | 16 | // Check for global variable leaks 17 | 'check-leaks': true, 18 | } 19 | -------------------------------------------------------------------------------- /test/unit/getSigningKeyFromFile.test.ts: -------------------------------------------------------------------------------- 1 | import 'mocha' 2 | import { assert } from 'chai' 3 | 4 | import { getSigningKeyFromFile } from '../../src/commands/pem-utils' 5 | 6 | import TEST_JWK from './jwkToPem.test' 7 | 8 | describe('when getSigningKeyFromFile()', function (): void { 9 | it('given pem file containing EC private returns ES256 jwk', async function () { 10 | const jwk = await getSigningKeyFromFile('test/unit/identity-key.pem') 11 | assert.deepEqual(jwk, TEST_JWK) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine 2 | 3 | ADD . / paystring-cli/ 4 | 5 | RUN cd paystring-cli/ &&\ 6 | npm set unsafe-perm true &&\ 7 | npm cache clean --force &&\ 8 | npm install &&\ 9 | npm run build && \ 10 | npm link 11 | 12 | FROM node:12-alpine 13 | 14 | RUN mkdir /opt/paystring-cli 15 | 16 | WORKDIR /opt/paystring-cli 17 | 18 | COPY --from=0 /paystring-cli/dist /opt/paystring-cli/dist 19 | COPY --from=0 /paystring-cli/node_modules /opt/paystring-cli/node_modules 20 | 21 | ENTRYPOINT ["node", "/opt/paystring-cli/dist/cli.js"] 22 | -------------------------------------------------------------------------------- /src/commands/clear.ts: -------------------------------------------------------------------------------- 1 | import Command from './Command' 2 | 3 | /** 4 | * Clears the terminal console. 5 | */ 6 | export default class ClearCommand extends Command { 7 | /** 8 | * @override 9 | */ 10 | protected async action(): Promise { 11 | // eslint-disable-next-line no-console -- needed to clear the cli console 12 | console.clear() 13 | } 14 | 15 | /** 16 | * @override 17 | */ 18 | protected command(): string { 19 | return 'clear' 20 | } 21 | 22 | /** 23 | * @override 24 | */ 25 | protected description(): string { 26 | return 'clear the terminal' 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/commands/paystring-show.ts: -------------------------------------------------------------------------------- 1 | import Command from './Command' 2 | 3 | /** 4 | * Prints the currently loaded PayString PaymentInformation to the console. 5 | */ 6 | export default class ShowPayStringCommand extends Command { 7 | protected async action(): Promise { 8 | const info = this.getPaymentInfo() 9 | this.logPaymentInfo(info) 10 | } 11 | 12 | /** 13 | * @override 14 | */ 15 | protected command(): string { 16 | return 'show' 17 | } 18 | 19 | /** 20 | * @override 21 | */ 22 | protected description(): string { 23 | return 'Shows the currently loaded PayString' 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | # Refer to following link for examples and full documentation: 2 | # https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md 3 | # 4 | pre-push: 5 | parallel: true 6 | commands: 7 | packages-audit: 8 | tags: security 9 | run: npm audit 10 | eslint: 11 | tags: style 12 | run: npm run lintNoFix 13 | test: 14 | tags: test 15 | run: npm run test 16 | tsc: 17 | tags: typescript 18 | run: tsc --noEmit --incremental false 19 | 20 | pre-commit: 21 | parallel: true 22 | commands: 23 | eslint: 24 | glob: '*.ts' 25 | run: npx eslint {staged_files} 26 | -------------------------------------------------------------------------------- /src/commands/keys-clear.ts: -------------------------------------------------------------------------------- 1 | import Command from './Command' 2 | 3 | /** 4 | * Clears currently loaded server and/or identity keys from local strorage. 5 | */ 6 | export default class ClearKeysCommand extends Command { 7 | /** 8 | * @override 9 | */ 10 | protected async action(): Promise { 11 | this.localStorage.removeItem('identity-keys') 12 | this.vorpal.log('cleared') 13 | } 14 | 15 | /** 16 | * @override 17 | */ 18 | protected command(): string { 19 | return 'keys clear' 20 | } 21 | 22 | /** 23 | * @override 24 | */ 25 | protected description(): string { 26 | return 'clears all loaded keys' 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /nyc.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | extension: ['.ts', '.tsx'], 5 | include: ['src/**/*.ts'], 6 | 7 | // Instrument all files, not just ones touched by test suite 8 | all: true, 9 | 10 | // Check if coverage is within thresholds. 11 | // TODO: Enable coverage requirements? 12 | // 'check-coverage': true, 13 | // branches: 80, 14 | // lines: 80, 15 | // functions: 80, 16 | // statements: 80, 17 | 18 | // Controls color highlighting for tests. 19 | // >= 95% is green, 80-95 is yellow, and < 80 is red. 20 | watermarks: { 21 | lines: [80, 95], 22 | functions: [80, 95], 23 | branches: [80, 95], 24 | statements: [80, 95], 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/paystring-from-url.ts: -------------------------------------------------------------------------------- 1 | import { convertUrlToPayString } from '@paystring/utils' 2 | import * as Vorpal from 'vorpal' 3 | 4 | import Command from './Command' 5 | 6 | /** 7 | * Converts a url (eg https://xpring.money/test) to a PayString (eg test$xpring.money). 8 | */ 9 | export default class UrlToPayStringCommand extends Command { 10 | /** 11 | * @override 12 | */ 13 | protected command(): string { 14 | return 'from-url ' 15 | } 16 | 17 | /** 18 | * @override 19 | */ 20 | protected description(): string { 21 | return 'convert a URL to a PayString' 22 | } 23 | 24 | /** 25 | * @override 26 | */ 27 | protected async action(args: Vorpal.Args): Promise { 28 | const payString = convertUrlToPayString(args.url) 29 | this.vorpal.log(payString) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/commands/paystring-to-url.ts: -------------------------------------------------------------------------------- 1 | import { convertPayStringToUrl } from '@paystring/utils' 2 | import * as Vorpal from 'vorpal' 3 | 4 | import Command from './Command' 5 | 6 | /** 7 | * Converts a PayString (eg test$xpring.money) to it's URL (eg https://xpring.money/test). 8 | */ 9 | export default class PayStringToUrlCommand extends Command { 10 | /** 11 | * @override 12 | */ 13 | protected async action(args: Vorpal.Args): Promise { 14 | const url = convertPayStringToUrl(args.payString).href 15 | this.vorpal.log(url) 16 | } 17 | 18 | /** 19 | * @override 20 | */ 21 | protected command(): string { 22 | return 'to-url ' 23 | } 24 | 25 | /** 26 | * @override 27 | */ 28 | protected description(): string { 29 | return 'converts PayString to url' 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*.ts"], 3 | "compileOnSave": true, 4 | "compilerOptions": { 5 | // Basic Options 6 | "project": ".", 7 | "outDir": "./dist", 8 | "target": "es2019", 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "incremental": true, 12 | // Add support for custom typings 13 | "baseUrl": "./", 14 | "paths": { 15 | "*": ["@types/*"] 16 | }, 17 | // Source Maps & Declaration Files 18 | "declaration": true, 19 | "declarationMap": true, 20 | "sourceMap": true, 21 | // Strict Mode 22 | "strict": true, 23 | // Checks not in "strict" mode 24 | "noUnusedLocals": true, 25 | "noUnusedParameters": true, 26 | "noImplicitReturns": true, 27 | "noFallthroughCasesInSwitch": true, 28 | "forceConsistentCasingInFileNames": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | ### 2 | 3 | stages: 4 | - test 5 | 6 | lint: 7 | stage: test 8 | image: 9 | name: node:12 10 | before_script: 11 | - npm i --cache .npm --prefer-offline --no-audit --progress=false 12 | script: 13 | - npm run lintNoFix 14 | 15 | code coverage: 16 | stage: test 17 | image: 18 | name: node:12 19 | before_script: 20 | - npm i --cache .npm --no-audit --progress=false --prefer-offline -g nyc codecov 21 | - npm i --cache .npm --no-audit --progress=false --prefer-offline 22 | script: 23 | - npm run build 24 | - nyc npm test 25 | - mkdir coverage 26 | - nyc report --reporter=text-lcov > coverage/coverage.json 27 | - codecov 28 | 29 | link checker: 30 | stage: test 31 | image: 32 | name: golang:1.14-alpine 33 | before_script: 34 | - apk add git 35 | - export GO111MODULE=on 36 | - go get -u github.com/raviqqe/liche 37 | script: 38 | - liche -r ${CI_PROJECT_DIR} 39 | -------------------------------------------------------------------------------- /test/unit/loadPayString.test.ts: -------------------------------------------------------------------------------- 1 | import 'mocha' 2 | import { assert } from 'chai' 3 | 4 | import { loadPayString } from '../../src/commands/Command' 5 | 6 | describe('loadPayString()', function (): void { 7 | it('Returns a PaymentInformation given a valid PayString', async function (): Promise< 8 | void 9 | > { 10 | const info = await loadPayString('nhartner$xpring.money') 11 | assert.equal(info.payId, 'nhartner$xpring.money') 12 | assert.isTrue(info.addresses.length > 0) 13 | }) 14 | 15 | it('Gives error when PayString returns 404', async function (): Promise< 16 | void 17 | > { 18 | try { 19 | await loadPayString('bogusaccount$xpring.money') 20 | assert.fail('expected error') 21 | } catch (error) { 22 | assert.deepEqual( 23 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- Error is untyped 24 | error.message, 25 | 'Received HTTP status 404 on https://xpring.money/bogusaccount', 26 | ) 27 | } 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /src/commands/paystring-save.ts: -------------------------------------------------------------------------------- 1 | import { splitPayString } from '@paystring/utils' 2 | 3 | import Command from './Command' 4 | import { overwriteFile } from './files' 5 | 6 | /** 7 | * Saves the PaymentInformation for the currently loaded payString to a json file. 8 | */ 9 | export default class SavePayStringCommand extends Command { 10 | protected async action(): Promise { 11 | const info = this.getPaymentInfo() 12 | if (info.payId) { 13 | const userHost = splitPayString(info.payId) 14 | const filename = `${userHost[0]}.json` 15 | await overwriteFile(filename, JSON.stringify(info, null, 2)) 16 | this.vorpal.log(`Saved to ${filename}`) 17 | } else { 18 | this.vorpal.log(`missing payString`) 19 | } 20 | } 21 | 22 | /** 23 | * @override 24 | */ 25 | protected command(): string { 26 | return 'save' 27 | } 28 | 29 | /** 30 | * @override 31 | */ 32 | protected description(): string { 33 | return 'Save the currently loaded PayString' 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/unit/jwkToPem.test.ts: -------------------------------------------------------------------------------- 1 | import 'mocha' 2 | import { promises } from 'fs' 3 | 4 | import { assert } from 'chai' 5 | 6 | import { jwkToPem } from '../../src/commands/pem-utils' 7 | 8 | const TEST_JWK = { 9 | kid: '4QDE254qxnrSYgCm2uEUdXmIzLiQaAX2i_eq2fDF4I4', 10 | kty: 'EC', 11 | alg: 'ES256', 12 | crv: 'P-256', 13 | // eslint-disable-next-line id-length -- jwk property 14 | d: 'ywzMHwumNjJl44zdMr9/J8hOvaLzS3maty54GQBmWow=', 15 | // eslint-disable-next-line id-length -- jwk property 16 | x: 'qyVmqSuvzjJlnNRRNypzgQwFlrUuG2DFwYewo9WhZYQ=', 17 | // eslint-disable-next-line id-length -- jwk property 18 | y: '+nqFcxlE5nX5L8R8uC5K9qcRKViPj1JJupNLzy/mWSA=', 19 | } 20 | 21 | describe('when jwkToPem()', function (): void { 22 | it('given jwk containing EC private returns expected pem', async function () { 23 | const pem = jwkToPem(TEST_JWK) 24 | const expectedPem = await promises.readFile( 25 | 'test/unit/identity-key.pem', 26 | 'ascii', 27 | ) 28 | assert.equal(pem, expectedPem) 29 | }) 30 | }) 31 | 32 | export default TEST_JWK 33 | -------------------------------------------------------------------------------- /src/commands/paystring-init.ts: -------------------------------------------------------------------------------- 1 | import { PaymentInformation } from '@paystring/utils' 2 | import * as Vorpal from 'vorpal' 3 | 4 | import Command from './Command' 5 | 6 | /** 7 | * Initializes a new PayString PaymentInformation object that can be decorated with addresses and signed using 8 | * signing keys. 9 | */ 10 | export default class InitPayStringCommand extends Command { 11 | /** 12 | * @override 13 | */ 14 | protected async action(args: Vorpal.Args): Promise { 15 | const info: PaymentInformation = { 16 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Vorpal.Args isn't typed 17 | payId: args.payString, 18 | addresses: [], 19 | verifiedAddresses: [], 20 | } 21 | this.localStorage.setPaymentInfo(info) 22 | this.logPaymentInfo(info) 23 | } 24 | 25 | /** 26 | * @override 27 | */ 28 | protected command(): string { 29 | return 'init ' 30 | } 31 | 32 | /** 33 | * @override 34 | */ 35 | protected description(): string { 36 | return 'initializes a new PayString' 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/keys-print.ts: -------------------------------------------------------------------------------- 1 | import Command from './Command' 2 | import { jwkToPem } from './pem-utils' 3 | 4 | /** 5 | * Prints, to console, a summary of the identity and server keys that are currently loaded in 6 | * local storage and available to use for signing. 7 | */ 8 | export default class PrintKeysCommand extends Command { 9 | /** 10 | * @override 11 | */ 12 | protected async action(): Promise { 13 | this.printKeys('identity-keys') 14 | } 15 | 16 | /** 17 | * @override 18 | */ 19 | protected command(): string { 20 | return 'keys print' 21 | } 22 | 23 | /** 24 | * @override 25 | */ 26 | protected description(): string { 27 | return 'print keys that have been loaded in pem format' 28 | } 29 | 30 | /** 31 | * Prints the key as pem to the console. 32 | * 33 | * @param name - The name of the key to print. 34 | */ 35 | private printKeys(name: string): void { 36 | const keys = this.localStorage.getSigningKeys(name) 37 | keys.forEach((key) => { 38 | const pem = jwkToPem(key) 39 | this.vorpal.log(pem) 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/commands/paystring-load.ts: -------------------------------------------------------------------------------- 1 | import * as Vorpal from 'vorpal' 2 | 3 | import Command, { loadPayString } from './Command' 4 | 5 | /** 6 | * Loads a PayString from the remote server. For example, "load test$xpring.money" will 7 | * make an HTTP call the https://xpring.money/test with Accept: application/payid+json 8 | * header so that all the addresses are returned. If successful, the PaymentInformation 9 | * is saved to localstorage as the current payString in context. 10 | */ 11 | export default class LoadPayStringCommand extends Command { 12 | /** 13 | * @override 14 | */ 15 | protected async action(args: Vorpal.Args): Promise { 16 | const { payString } = args 17 | const info = await loadPayString(payString) 18 | this.localStorage.setPaymentInfo(info) 19 | this.logPaymentInfo(info) 20 | } 21 | 22 | /** 23 | * @override 24 | */ 25 | protected command(): string { 26 | return 'load ' 27 | } 28 | 29 | /** 30 | * @override 31 | */ 32 | protected description(): string { 33 | return 'loads a PayString from PayString server' 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/commands/key-load.ts: -------------------------------------------------------------------------------- 1 | import * as Vorpal from 'vorpal' 2 | 3 | import Command from './Command' 4 | import { getSigningKeyFromFile } from './pem-utils' 5 | 6 | /** 7 | * Loads an identity key from a PEM file. 8 | * The loaded key is placed in local storage for use by the sign command. 9 | */ 10 | export default class LoadIdentityKeyCommand extends Command { 11 | /** 12 | * @override 13 | */ 14 | protected async action(args: Vorpal.Args): Promise { 15 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Vorpal.Args isn't typed 16 | const filePath: string = args.filePath 17 | this.vorpal.log(`loading identity-key from ${filePath}`) 18 | const key = await getSigningKeyFromFile(filePath) 19 | this.vorpal.log(`loaded identity-key from ${filePath}. Sign away.`) 20 | this.localStorage.addSigningKey('identity-keys', key) 21 | } 22 | 23 | /** 24 | * @override 25 | */ 26 | protected command(): string { 27 | return 'keys load ' 28 | } 29 | 30 | /** 31 | * @override 32 | */ 33 | protected description(): string { 34 | return 'load identity-key from file' 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/commands/key-generate.ts: -------------------------------------------------------------------------------- 1 | import { generateNewKey } from '@paystring/utils' 2 | 3 | import Command from './Command' 4 | import { writeFile } from './files' 5 | import { jwkToPem } from './pem-utils' 6 | 7 | /** 8 | * Generates an identity key, loads the key into local storage and saves the key 9 | * to file in pem format. 10 | */ 11 | export default class GenerateIdentityKeyCommand extends Command { 12 | /** 13 | * @override 14 | */ 15 | protected async action(): Promise { 16 | const key = await generateNewKey() 17 | const pem = jwkToPem(key) 18 | try { 19 | const filename = await writeFile('./identity-key.pem', pem) 20 | this.vorpal.log(`wrote key to ${filename}`) 21 | } catch { 22 | this.vorpal.log('failed to write key, outputting instead') 23 | this.vorpal.log(pem) 24 | } 25 | this.localStorage.addSigningKey('identity-keys', key) 26 | } 27 | 28 | /** 29 | * @override 30 | */ 31 | protected command(): string { 32 | return 'keys generate' 33 | } 34 | 35 | /** 36 | * @override 37 | */ 38 | protected description(): string { 39 | return 'generates and saves a new identity key' 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/commands/keys-list.ts: -------------------------------------------------------------------------------- 1 | import Command from './Command' 2 | 3 | /** 4 | * Prints, to console, a summary of the identity and server keys that are currently loaded in 5 | * local storage and available to use for signing. 6 | */ 7 | export default class ListKeysCommand extends Command { 8 | /** 9 | * @override 10 | */ 11 | protected async action(): Promise { 12 | this.printKeys('identity-keys') 13 | } 14 | 15 | /** 16 | * @override 17 | */ 18 | protected command(): string { 19 | return 'keys list' 20 | } 21 | 22 | /** 23 | * @override 24 | */ 25 | protected description(): string { 26 | return 'lists keys that have been loaded' 27 | } 28 | 29 | /** 30 | * Prints a key summary to the console. 31 | * 32 | * @param name - The name of the key to print. 33 | */ 34 | private printKeys(name: string): void { 35 | const keys = this.localStorage.getSigningKeys(name) 36 | keys.forEach((key) => { 37 | const kid = key.kid ?? 'not set' 38 | const kty = key.kty ?? 'not set' 39 | if (typeof key.crv === 'string') { 40 | this.vorpal.log(`${name}: type=${kty}, type=${key.crv}, id=${kid}`) 41 | } else { 42 | this.vorpal.log(`${name}: type=${kty}, id=${kid}`) 43 | } 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Command } from './Command' 2 | export { default as PayStringToUrlCommand } from './paystring-to-url' 3 | export { default as UrlToPayStringCommand } from './paystring-from-url' 4 | export { default as ClearCommand } from './clear' 5 | export { default as AddCryptoAddressCommand } from './crypto-address-add' 6 | export { default as RemoveCryptoAddressCommand } from './crypto-address-remove' 7 | export { default as GenerateIdentityKeyCommand } from './key-generate' 8 | export { default as LoadIdentityKeyCommand } from './key-load' 9 | export { default as ClearKeysCommand } from './keys-clear' 10 | export { default as ListKeysCommand } from './keys-list' 11 | export { default as PrintKeysCommand } from './keys-print' 12 | export { default as LocalStorage } from './localstorage' 13 | export { default as InitPayStringCommand } from './paystring-init' 14 | export { default as InspectPayStringCommand } from './paystring-inspect' 15 | export { default as LoadPayStringCommand } from './paystring-load' 16 | export { default as SavePayStringCommand } from './paystring-save' 17 | export { default as ShowPayStringCommand } from './paystring-show' 18 | export { default as SignPayStringCommand } from './paystring-sign' 19 | export { default as VerifyPayStringCommand } from './paystring-verify' 20 | -------------------------------------------------------------------------------- /src/commands/crypto-address-remove.ts: -------------------------------------------------------------------------------- 1 | import * as Vorpal from 'vorpal' 2 | 3 | import Command from './Command' 4 | 5 | /** 6 | * Removes a crypto address from the current PayID in scope. 7 | */ 8 | export default class RemoveCryptoAddressCommand extends Command { 9 | /** 10 | * @override 11 | */ 12 | protected async action(args: Vorpal.Args): Promise { 13 | const info = this.getPaymentInfo() 14 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Vorpal.Args isn't typed 15 | const inputAddress: string = args.address 16 | const toRemove = info.addresses.find((address) => { 17 | if ('address' in address.addressDetails) { 18 | return address.addressDetails.address === args.address 19 | } 20 | return false 21 | }) 22 | if (!toRemove) { 23 | this.vorpal.log(`address ${inputAddress} not found`) 24 | return 25 | } 26 | info.addresses.splice(info.addresses.indexOf(toRemove), 1) 27 | this.localStorage.setPaymentInfo(info) 28 | this.logPaymentInfo(info) 29 | } 30 | 31 | /** 32 | * @override 33 | */ 34 | protected command(): string { 35 | return 'crypto-address remove
' 36 | } 37 | 38 | /** 39 | * @override 40 | */ 41 | protected description(): string { 42 | return 'remove an address from the current PayID' 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Mocha All", 11 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 12 | "skipFiles": ["/**", "node_modules/**/*.js"], 13 | "args": [ 14 | "--timeout", 15 | "999999", 16 | "--colors", 17 | "--config", 18 | "${workspaceFolder}/.mocharc.js", 19 | "${workspaceFolder}/test/**/*.test.ts" 20 | ], 21 | "console": "integratedTerminal", 22 | "internalConsoleOptions": "neverOpen" 23 | }, 24 | { 25 | "type": "node", 26 | "request": "launch", 27 | "name": "Mocha Current File", 28 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 29 | "skipFiles": ["/**", "node_modules/**/*.js"], 30 | "args": [ 31 | "--timeout", 32 | "999999", 33 | "--colors", 34 | "--config", 35 | "${workspaceFolder}/.mocharc.js", 36 | "${file}" 37 | ], 38 | "console": "integratedTerminal", 39 | "internalConsoleOptions": "neverOpen" 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/commands/paystring-verify.ts: -------------------------------------------------------------------------------- 1 | import { 2 | convertJsonToAddress, 3 | PaymentInformation, 4 | verifyPayString, 5 | } from '@paystring/utils' 6 | import * as Vorpal from 'vorpal' 7 | 8 | import Command from './Command' 9 | 10 | /** 11 | * Verifies the signatures and certs for verified addresses of the currently loaded PayString. 12 | */ 13 | export default class VerifyPayStringCommand extends Command { 14 | /** 15 | * @override 16 | */ 17 | protected async action(args: Vorpal.Args): Promise { 18 | const info = await this.payStringFromArgsOrLocalStorage(args) 19 | if (await verifyPayString(info)) { 20 | const addresses = info.verifiedAddresses.map((address) => { 21 | return convertJsonToAddress(address.payload) 22 | }) 23 | const copy: PaymentInformation = { 24 | payId: info.payId, 25 | addresses, 26 | verifiedAddresses: info.verifiedAddresses, 27 | } 28 | this.logPaymentInfo(copy) 29 | this.vorpal.log(`Successfully verified ${copy.payId}`) 30 | } else { 31 | this.vorpal.log(`Failed to verify ${info.payId}`) 32 | } 33 | } 34 | 35 | /** 36 | * @override 37 | */ 38 | protected command(): string { 39 | return 'verify [payString]' 40 | } 41 | 42 | /** 43 | * @override 44 | */ 45 | protected description(): string { 46 | return 'Verify the loaded PayString or an optionally specified PayString' 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | 4 | parser: '@typescript-eslint/parser', // Make ESLint compatible with TypeScript 5 | parserOptions: { 6 | // Enable linting rules with type information from our tsconfig 7 | tsconfigRootDir: __dirname, 8 | project: ['./tsconfig.eslint.json'], 9 | 10 | sourceType: 'module', // Allow the use of imports / ES modules 11 | 12 | ecmaFeatures: { 13 | impliedStrict: true, // Enable global strict mode 14 | }, 15 | }, 16 | 17 | // Specify global variables that are predefined 18 | env: { 19 | node: true, // Enable node global variables & Node.js scoping 20 | es2020: true, // Add all ECMAScript 2020 globals and automatically set the ecmaVersion parser option to ES2020 21 | }, 22 | 23 | plugins: [], 24 | extends: [ 25 | '@xpring-eng/eslint-config-mocha', 26 | '@xpring-eng/eslint-config-base', 27 | ], 28 | 29 | rules: { 30 | // linter doesn't seem to understand ESM imports used by jose, even though typescript handles them just fine. 31 | "node/no-missing-import": ["error", { 32 | allowModules: ["jose"], 33 | }], 34 | "import/no-unresolved": [ 35 | 2, 36 | { 37 | ignore: [ 38 | 'jose' 39 | ] 40 | }], 41 | }, 42 | overrides: [ 43 | { 44 | "files": ["*cli.ts"], 45 | "rules": { 46 | "node/shebang": "off" 47 | }, 48 | }, 49 | { 50 | "files": ["src/commands/*.ts"], 51 | "rules": { 52 | "class-methods-use-this": "off" 53 | }, 54 | }], 55 | } 56 | -------------------------------------------------------------------------------- /test/unit/signPayString.test.ts: -------------------------------------------------------------------------------- 1 | import 'mocha' 2 | import { 3 | AddressDetailsType, 4 | generateNewKey, 5 | IdentityKeySigningParams, 6 | PaymentInformation, 7 | } from '@paystring/utils' 8 | import { assert } from 'chai' 9 | 10 | import { signPayString } from '../../src/commands/paystring-sign' 11 | 12 | const info: PaymentInformation = { 13 | payId: 'boaty$mcboatface.com', 14 | addresses: [ 15 | { 16 | paymentNetwork: 'boatcoin', 17 | environment: 'seanet', 18 | addressDetailsType: AddressDetailsType.CryptoAddress, 19 | addressDetails: { 20 | address: 'xyz12345', 21 | }, 22 | }, 23 | ], 24 | verifiedAddresses: [], 25 | } 26 | 27 | describe('when signPayString()', function (): void { 28 | let signingKey: IdentityKeySigningParams 29 | 30 | beforeEach('create key', async function (): Promise { 31 | const key = await generateNewKey() 32 | signingKey = new IdentityKeySigningParams(key, 'ES256') 33 | }) 34 | 35 | it('called with keepAddresses=true, then addresses property is retained', async function (): Promise< 36 | void 37 | > { 38 | const result = await signPayString(info, [signingKey], true) 39 | assert.equal(result.addresses, info.addresses) 40 | assert.lengthOf(result.verifiedAddresses, 1) 41 | }) 42 | 43 | it('called with keepAddresses=false, then addresses property is cleared', async function (): Promise< 44 | void 45 | > { 46 | const result = await signPayString(info, [signingKey], false) 47 | assert.isEmpty(result.addresses) 48 | assert.lengthOf(result.verifiedAddresses, 1) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /src/commands/pem-utils.ts: -------------------------------------------------------------------------------- 1 | import { promises } from 'fs' 2 | 3 | /* eslint-disable eslint-comments/no-unlimited-disable -- too many rules to disable */ 4 | /* eslint-disable -- the linter hates ec-key import because it has no typedefs */ 5 | const ECKey = require('ec-key') 6 | import { JWK } from 'jose/webcrypto/types' 7 | import calculateThumbprint from 'jose/jwk/thumbprint' 8 | import { getDefaultAlgorithm } from '@paystring/utils' 9 | 10 | /** 11 | * Reads JWK key from a file. 12 | * 13 | * @param path - The full file path of the key file. 14 | * @returns A JWK key. 15 | */ 16 | export async function getSigningKeyFromFile(path: string): Promise { 17 | const pem = await promises.readFile(path, 'ascii') 18 | return pemToJwk(pem) 19 | } 20 | 21 | async function pemToJwk(pem: string): Promise { 22 | try { 23 | const privateKey = new ECKey(pem, 'pem') 24 | const jwk = { 25 | kty: 'EC', 26 | d: privateKey.d.toString('base64'), 27 | x: privateKey.x.toString('base64'), 28 | y: privateKey.y.toString('base64'), 29 | crv: privateKey.jsonCurve, 30 | } 31 | const thumbprint = await calculateThumbprint(jwk) 32 | return { ...jwk, kid: thumbprint, alg: getDefaultAlgorithm(jwk) } 33 | } catch (e) { 34 | throw new Error("could not read pem: " + e.message) 35 | } 36 | } 37 | 38 | /** 39 | * Converts a JWK to a PEM file string (including the header and footer sections). 40 | * 41 | * @param jwk - The JWK to convert. 42 | * @return A PEM string. 43 | */ 44 | export function jwkToPem(jwk: JWK): string { 45 | return new ECKey(jwk).toString('pem') 46 | } 47 | 48 | /* eslint-enable */ 49 | -------------------------------------------------------------------------------- /src/commands/files.ts: -------------------------------------------------------------------------------- 1 | import { promises } from 'fs' 2 | 3 | // the maximum number of attempts at finding a unique filename (by appending a suffix). 4 | const MAX_ATTEMPTS = 100 5 | 6 | /** 7 | * Writes content to file but does not overwrite any existing file with the same name. 8 | * If a file with the given name already exists, attempts to write to the file by 9 | * appending a suffix. For example, if filename "foo.txt" already exists, it will attempt to 10 | * write the file as "foo.txt.1" then "foo.txt.2" until it finds an unused filename. 11 | * 12 | * @param filename - The filename to write to. 13 | * @param data - The content to write to the file. 14 | * @param index - The suffix to append to the filename. 15 | * @returns The actual filename that was written to. 16 | * @throws Error if no unique filename could be found. 17 | */ 18 | export async function writeFile( 19 | filename: string, 20 | data: string, 21 | index = 0, 22 | ): Promise { 23 | const suffixedFilename = index === 0 ? filename : `${filename}.${index}` 24 | return promises 25 | .writeFile(suffixedFilename, data, { flag: 'wx' }) 26 | .then(() => suffixedFilename) 27 | .catch(async (err) => { 28 | if (index <= MAX_ATTEMPTS) { 29 | return writeFile(filename, data, index + 1) 30 | } 31 | throw err 32 | }) 33 | } 34 | 35 | /** 36 | * Writes content to file and overwrites any existing file with the same name. 37 | * 38 | * @param filename - The filename to write to. 39 | * @param data - The content to write to the file. 40 | * @returns Promise because the file I/O is done async. 41 | */ 42 | export async function overwriteFile( 43 | filename: string, 44 | data: string, 45 | ): Promise { 46 | return promises.writeFile(filename, data) 47 | } 48 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as Vorpal from 'vorpal' 4 | 5 | import * as cmd from './commands' 6 | 7 | const vorpal = new Vorpal() 8 | 9 | // This enables command history within the CLI similar to bash. 10 | vorpal.history('payString') 11 | 12 | const localStorage = new cmd.LocalStorage('payString', vorpal) 13 | new cmd.ClearCommand(vorpal, localStorage).setup() 14 | new cmd.AddCryptoAddressCommand(vorpal, localStorage).setup() 15 | new cmd.RemoveCryptoAddressCommand(vorpal, localStorage).setup() 16 | new cmd.ClearKeysCommand(vorpal, localStorage).setup() 17 | new cmd.GenerateIdentityKeyCommand(vorpal, localStorage).setup() 18 | new cmd.ListKeysCommand(vorpal, localStorage).setup() 19 | new cmd.LoadIdentityKeyCommand(vorpal, localStorage).setup() 20 | new cmd.PrintKeysCommand(vorpal, localStorage).setup() 21 | new cmd.InitPayStringCommand(vorpal, localStorage).setup() 22 | new cmd.InspectPayStringCommand(vorpal, localStorage).setup() 23 | new cmd.LoadPayStringCommand(vorpal, localStorage).setup() 24 | new cmd.ShowPayStringCommand(vorpal, localStorage).setup() 25 | new cmd.SignPayStringCommand(vorpal, localStorage).setup() 26 | new cmd.VerifyPayStringCommand(vorpal, localStorage).setup() 27 | new cmd.SavePayStringCommand(vorpal, localStorage).setup() 28 | new cmd.UrlToPayStringCommand(vorpal, localStorage).setup() 29 | new cmd.PayStringToUrlCommand(vorpal, localStorage).setup() 30 | 31 | // The CLI can be run in interactive mode or to run a single command and terminate. 32 | // For CLI mode, process.argv will have 2 values (e.g. node dist/cli.js) even if using the alias 'paystring-utils'. 33 | // For single command mode there will be additional arguments for the single command. 34 | if (process.argv.length > 2) { 35 | vorpal.parse(process.argv) 36 | } else { 37 | vorpal.delimiter('$').show() 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/crypto-address-add.ts: -------------------------------------------------------------------------------- 1 | import { Address, AddressDetailsType } from '@paystring/utils' 2 | import * as Vorpal from 'vorpal' 3 | 4 | import Command from './Command' 5 | 6 | /** 7 | * Command to add a crypto address to the current PayID in scope. 8 | */ 9 | export default class AddCryptoAddressCommand extends Command { 10 | /** 11 | * @override 12 | */ 13 | protected async action(args: Vorpal.Args): Promise { 14 | const info = this.getPaymentInfo() 15 | /* eslint-disable @typescript-eslint/no-unsafe-assignment -- Vorpal.Args isn't typed */ 16 | const paymentNetwork: string = args.paymentNetwork 17 | const environment: string = args.environment 18 | const cryptoAddress: string = args.address 19 | const tag: string | undefined = args.tag 20 | /* eslint-enable @typescript-eslint/no-unsafe-assignment */ 21 | 22 | const address: Address = { 23 | paymentNetwork: paymentNetwork.toUpperCase(), 24 | environment: environment.toUpperCase(), 25 | addressDetailsType: AddressDetailsType.CryptoAddress, 26 | addressDetails: { 27 | address: cryptoAddress, 28 | tag, 29 | }, 30 | } 31 | const updatedAddresses = info.addresses.concat(address) 32 | 33 | const updated = { 34 | payId: info.payId, 35 | addresses: updatedAddresses, 36 | verifiedAddresses: info.verifiedAddresses, 37 | } 38 | 39 | this.localStorage.setPaymentInfo(updated) 40 | this.logPaymentInfo(updated) 41 | } 42 | 43 | /** 44 | * @override 45 | */ 46 | protected command(): string { 47 | return 'crypto-address add
[tag]' 48 | } 49 | 50 | /** 51 | * @override 52 | */ 53 | protected description(): string { 54 | return 'start building a new PayID' 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@paystring/paystring-cli", 3 | "version": "2.0.0", 4 | "description": "CLI for PayString", 5 | "homepage": "https://github.com/paystring/paystring-cli#readme", 6 | "bugs": { 7 | "url": "https://github.com/paystring/paystring-cli/issues" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/paystring/paystring-cli.git" 12 | }, 13 | "license": "Apache-2.0", 14 | "author": "", 15 | "main": "dist/index.js", 16 | "types": "dist/index.d.ts", 17 | "bin": { 18 | "paystring": "./dist/cli.js" 19 | }, 20 | "files": [ 21 | "dist/*" 22 | ], 23 | "scripts": { 24 | "build": "rm -rf dist && tsc --project .", 25 | "lint": "eslint . --ext .ts --fix --max-warnings 0 && prettier --write '**/*.{md,json}'", 26 | "lintNoFix": "eslint . --ext .ts --max-warnings 0 && prettier --check '**/*.{md,json}'", 27 | "preinstall": "test -e ./package-lock.json && npx npm-force-resolutions || true", 28 | "test": "nyc mocha 'test/**/*.test.ts'" 29 | }, 30 | "dependencies": { 31 | "@paystring/utils": "^2.0.0", 32 | "axios": "^0.19.2", 33 | "beautify-json": "^1.0.1", 34 | "ec-key": "0.0.4", 35 | "jose": "^3.5.0", 36 | "vorpal": "^1.12.0" 37 | }, 38 | "devDependencies": { 39 | "@arkweid/lefthook": "^0.7.2", 40 | "@fintechstudios/eslint-plugin-chai-as-promised": "^3.0.2", 41 | "@types/chai": "^4.2.11", 42 | "@types/mocha": "^7.0.2", 43 | "@types/node": "^14.0.14", 44 | "@types/node-forge": "^0.9.4", 45 | "@types/pem-jwk": "^1.5.0", 46 | "@types/vorpal": "^1.12.0", 47 | "@typescript-eslint/eslint-plugin": "^3.7.1", 48 | "@typescript-eslint/parser": "^3.7.1", 49 | "@xpring-eng/eslint-config-base": "^0.11.0", 50 | "@xpring-eng/eslint-config-mocha": "^1.0.0", 51 | "chai": "^4.2.0", 52 | "eslint": "^7.7.0", 53 | "eslint-plugin-array-func": "^3.1.6", 54 | "eslint-plugin-eslint-comments": "^3.2.0", 55 | "eslint-plugin-import": "^2.22.0", 56 | "eslint-plugin-jsdoc": "^29.0.0", 57 | "eslint-plugin-mocha": "^7.0.1", 58 | "eslint-plugin-node": "^11.1.0", 59 | "eslint-plugin-prettier": "^3.1.4", 60 | "eslint-plugin-tsdoc": "^0.2.5", 61 | "mocha": "^8.1.1", 62 | "nyc": "^15.1.0", 63 | "prettier": "^2.0.5", 64 | "ts-node": "^8.10.2", 65 | "tsc-watch": "^4.2.9", 66 | "typescript": "^3.9.7" 67 | }, 68 | "engines": { 69 | "node": ">=12.0.0", 70 | "npm": ">=6", 71 | "yarn": "please use npm" 72 | }, 73 | "resolutions": { 74 | "lodash": "^4.17.20" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,node 3 | # Edit at https://www.gitignore.io/?templates=osx,node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Anything in the build directory 43 | build/ 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional REPL history 62 | .node_repl_history 63 | 64 | # Output of 'npm pack' 65 | *.tgz 66 | 67 | # Yarn Integrity file 68 | .yarn-integrity 69 | 70 | # dotenv environment variables file 71 | .env 72 | .env.test 73 | 74 | # parcel-bundler cache (https://parceljs.org/) 75 | .cache 76 | 77 | # next.js build output 78 | .next 79 | 80 | # nuxt.js build output 81 | .nuxt 82 | 83 | # rollup.js default build output 84 | dist/ 85 | 86 | # Uncomment the public line if your project uses Gatsby 87 | # https://nextjs.org/blog/next-9-1#public-directory-support 88 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 89 | # public 90 | 91 | # Storybook build outputs 92 | .out 93 | .storybook-out 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # Temporary folders 108 | tmp/ 109 | temp/ 110 | 111 | ### OSX ### 112 | # General 113 | .DS_Store 114 | .AppleDouble 115 | .LSOverride 116 | 117 | # Icon must end with two \r 118 | Icon 119 | 120 | # Thumbnails 121 | ._* 122 | 123 | # Files that might appear in the root of a volume 124 | .DocumentRevisions-V100 125 | .fseventsd 126 | .Spotlight-V100 127 | .TemporaryItems 128 | .Trashes 129 | .VolumeIcon.icns 130 | .com.apple.timemachine.donotpresent 131 | 132 | # Directories potentially created on remote AFP share 133 | .AppleDB 134 | .AppleDesktop 135 | Network Trash Folder 136 | Temporary Items 137 | .apdisk 138 | 139 | # IntelliJ nonsense 140 | /.idea 141 | 142 | # Lefthook 143 | /.lefthook-local 144 | lefthook-local.yml 145 | -------------------------------------------------------------------------------- /src/commands/paystring-sign.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getDefaultAlgorithm, 3 | IdentityKeySigningParams, 4 | PaymentInformation, 5 | signWithKeys, 6 | } from '@paystring/utils' 7 | import * as Vorpal from 'vorpal' 8 | 9 | import Command from './Command' 10 | 11 | /** 12 | * Signs the currently loaded PayString PaymentInformation using the loaded signings keys. 13 | */ 14 | export default class SignPayStringCommand extends Command { 15 | /** 16 | * @override 17 | */ 18 | public setup(): Vorpal.Command { 19 | return super 20 | .setup() 21 | .option( 22 | '-k, --keep-addresses', 23 | 'Keep the unverified addresses section after signing.', 24 | ) 25 | } 26 | 27 | /** 28 | * @override 29 | */ 30 | protected async action(args: Vorpal.Args): Promise { 31 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Vorpal.options isn't typed 32 | const isKeepAddresses: boolean = args.options['keep-addresses'] ?? false 33 | const info = this.getPaymentInfo() 34 | const payId = info.payId 35 | if (!payId) { 36 | this.vorpal.log('missing payid') 37 | return 38 | } 39 | const signingKeys = this.getSigningKey() 40 | if (signingKeys.length === 0) { 41 | this.vorpal.log( 42 | 'you must generate or load a key before signing using ' + 43 | `'keys generate' or 'keys load'`, 44 | ) 45 | return 46 | } 47 | 48 | const updated = await signPayString(info, signingKeys, isKeepAddresses) 49 | 50 | this.localStorage.setPaymentInfo(updated) 51 | this.logPaymentInfo(updated) 52 | } 53 | 54 | /** 55 | * @override 56 | */ 57 | protected command(): string { 58 | return 'sign' 59 | } 60 | 61 | /** 62 | * @override 63 | */ 64 | protected description(): string { 65 | return 'sign the loaded PayString with the loaded signing keys' 66 | } 67 | 68 | /** 69 | * Gets the signing key and converts it to SigningKeyParams. 70 | * 71 | * @returns Keys. 72 | */ 73 | private getSigningKey(): IdentityKeySigningParams[] { 74 | const identityKeys = this.localStorage.getSigningKeys('identity-keys') 75 | return identityKeys.map( 76 | (key) => new IdentityKeySigningParams(key, getDefaultAlgorithm(key)), 77 | ) 78 | } 79 | } 80 | 81 | /** 82 | * Signs all the addresses for the given payment information and returns 83 | * with verified address. 84 | * 85 | * @param info - The payment information to sign. 86 | * @param signingKeys - The keys to sign with. 87 | * @param isKeepAddresses - If true, the unverified addresses property will be retained instead of cleared. 88 | * @returns A copy of the PaymentInformation but with verified addresses. 89 | */ 90 | export async function signPayString( 91 | info: PaymentInformation, 92 | signingKeys: IdentityKeySigningParams[], 93 | isKeepAddresses: boolean, 94 | ): Promise { 95 | const payId = info.payId 96 | const updatedAddresses = await Promise.all( 97 | info.addresses.map(async (address) => 98 | signWithKeys(payId, address, signingKeys), 99 | ), 100 | ) 101 | const updated = { 102 | payId: info.payId, 103 | addresses: isKeepAddresses ? info.addresses : [], 104 | verifiedAddresses: updatedAddresses, 105 | } 106 | return updated 107 | } 108 | -------------------------------------------------------------------------------- /src/commands/paystring-inspect.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PaymentInformationInspector, 3 | SignatureInspectionResult, 4 | } from '@paystring/utils' 5 | import * as Vorpal from 'vorpal' 6 | 7 | import Command from './Command' 8 | import LocalStorage from './localstorage' 9 | 10 | /** 11 | * Inspects the currently loaded PayString and prints inspection details to the console. 12 | * Inspection looks at verified addresses and verifies the signatures and certificate chain (if present). 13 | */ 14 | export default class InspectPayStringCommand extends Command { 15 | private readonly paymentInformationInspector: PaymentInformationInspector 16 | 17 | /** 18 | * Creates new command. 19 | * 20 | * @param vorpal - The vorpal instance to use. 21 | * @param localStorage - The localstorage to use. 22 | */ 23 | public constructor(vorpal: Vorpal, localStorage: LocalStorage) { 24 | super(vorpal, localStorage) 25 | this.paymentInformationInspector = new PaymentInformationInspector() 26 | } 27 | 28 | /** 29 | * @override 30 | */ 31 | protected command(): string { 32 | return 'inspect [payString]' 33 | } 34 | 35 | /** 36 | * @override 37 | */ 38 | protected description(): string { 39 | return 'Inspect signatures on the loaded PayString or from an optionally specified PayString' 40 | } 41 | 42 | /** 43 | * @override 44 | */ 45 | protected async action(args: Vorpal.Args): Promise { 46 | const info = await this.payStringFromArgsOrLocalStorage(args) 47 | const result = await this.paymentInformationInspector.inspect(info) 48 | this.vorpal.log(`${info.payId} ${validString(result.isVerified)}`) 49 | result.verifiedAddressesResults.forEach((addressResult) => { 50 | const address = addressResult.address 51 | const cryptoAddress = 52 | 'address' in address.addressDetails 53 | ? address.addressDetails.address 54 | : '' 55 | 56 | const environment: string = 57 | typeof address.environment === 'string' ? address.environment : '' 58 | this.vorpal.log( 59 | `Found verified ${address.paymentNetwork} ${environment} address ${cryptoAddress}`, 60 | ) 61 | this.vorpal.log( 62 | `- Signed with ${addressResult.signaturesResults.length} signature(s)`, 63 | ) 64 | addressResult.signaturesResults.forEach((signatureResult, sigIndex) => { 65 | this.inspectSignatureResult(sigIndex, signatureResult) 66 | }) 67 | }) 68 | } 69 | 70 | /** 71 | * Inspects and logs information about the inspection result. 72 | * 73 | * @param sigIndex - The index of the signature. 74 | * @param signatureResult -The signature result to inspect. 75 | */ 76 | private inspectSignatureResult( 77 | sigIndex: number, 78 | signatureResult: SignatureInspectionResult, 79 | ): void { 80 | this.vorpal.log( 81 | `- Signature ${sigIndex + 1} ${validString( 82 | signatureResult.isSignatureValid, 83 | )}`, 84 | ) 85 | if (signatureResult.jwk && signatureResult.keyType) { 86 | const thumbprint = signatureResult.jwk.kid ?? 'not set' 87 | const kty = signatureResult.jwk.kty ?? 'no key type' 88 | this.vorpal.log( 89 | ` - Signed with ${kty} ${signatureResult.keyType} with thumbprint ${thumbprint}`, 90 | ) 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * Prints either 'is verfied' or 'is NOT verfied' based on the valid flag. 97 | * 98 | * @param valid - Flag for is / is NOT. 99 | * @returns The is/is NOT string. 100 | */ 101 | function validString(valid: boolean): string { 102 | return `${valid ? 'is' : 'is NOT'} verified` 103 | } 104 | -------------------------------------------------------------------------------- /src/commands/localstorage.ts: -------------------------------------------------------------------------------- 1 | import { PaymentInformation } from '@paystring/utils' 2 | import { JWK } from 'jose/webcrypto/types' 3 | import * as Vorpal from 'vorpal' 4 | 5 | /** 6 | * Facade layer for the Vorpal localstorage object. Provides typed methods for accessing things in 7 | * local storage like signing keys and PayString PaymentInformation. Objects in local storage are 8 | * stored as JSON strings. 9 | */ 10 | export default class LocalStorage { 11 | private readonly localStorage: VorpalLocalStorage 12 | 13 | /** 14 | * Constructs the facade and initializes the localstorage instance. 15 | * 16 | * @param payString - The payString for this local storage. 17 | * @param vorpal - The vorpal instance to use. 18 | */ 19 | public constructor(payString: string, vorpal: Vorpal) { 20 | // initializes the local storage instance 21 | vorpal.localStorage(payString) 22 | // The Vorpal API for local storage is really poorly defined and the type defs do not match the actual API. 23 | // see https://github.com/dthree/vorpal/wiki/API-%7C-vorpal#vorpallocalstorageid 24 | // So many things that make typescript linter upset that it's easiest to just disable all rules for this one line. 25 | /* eslint-disable eslint-comments/no-unlimited-disable -- to many rules to disable */ 26 | /* eslint-disable -- the linter hates this import */ 27 | this.localStorage = (vorpal.localStorage as unknown) as VorpalLocalStorage 28 | /* eslint-enable */ 29 | } 30 | 31 | /* eslint-disable @typescript-eslint/consistent-type-assertions -- getters/setters enforce consistent types */ 32 | /** 33 | * Gets the PaymentInformation instance from local storage. 34 | * 35 | * @returns The instance or undefined if none exists. 36 | */ 37 | public getPaymentInfo(): PaymentInformation | undefined { 38 | return this.getItem('paystring') as PaymentInformation 39 | } 40 | 41 | /** 42 | * Updates the PaymentInformation in local storage. 43 | * 44 | * @param info - The value to store. 45 | */ 46 | public setPaymentInfo(info: PaymentInformation): void { 47 | this.setItem('paystring', JSON.stringify(info)) 48 | } 49 | 50 | /** 51 | * Gets a named signing key from local storage. 52 | * 53 | * @param name - The name of the key. 54 | * @returns The key or null. 55 | */ 56 | public getSigningKeys(name: string): JWK[] { 57 | const existing = this.getItem(name) 58 | if (existing) { 59 | return existing as JWK[] 60 | } 61 | return [] 62 | } 63 | /* eslint-enable @typescript-eslint/consistent-type-assertions */ 64 | 65 | /** 66 | * Sets value for a named signing key from local storage. 67 | * 68 | * @param name - The name of the key. 69 | * @param key - The key to store. 70 | */ 71 | public addSigningKey(name: string, key: JWK): void { 72 | const keys = this.getSigningKeys(name) 73 | const updated = keys.concat(key) 74 | this.setItem(name, JSON.stringify(updated)) 75 | } 76 | 77 | /** 78 | * Removes an item from localstorage. 79 | * 80 | * @param name - The name of the item to remove. 81 | */ 82 | public removeItem(name: string): void { 83 | this.localStorage.removeItem(name) 84 | } 85 | 86 | /** 87 | * Gets item from localstorage. If value exists, also parses JSON value. 88 | * 89 | * @param name - The name of the item to get. 90 | * @returns The object or undefined if not in localstore. 91 | */ 92 | private getItem(name: string): JWK[] | PaymentInformation | undefined { 93 | const rawValue = this.localStorage.getItem(name) 94 | if (rawValue && typeof rawValue === 'string') { 95 | try { 96 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- because JSON 97 | return JSON.parse(rawValue) 98 | } catch { 99 | return undefined 100 | } 101 | } 102 | return undefined 103 | } 104 | 105 | /** 106 | * Sets the value of a item in localstorage. 107 | * 108 | * @param name - The name of the item. 109 | * @param value - The value to store. 110 | */ 111 | private setItem(name: string, value: string): void { 112 | this.localStorage.setItem(name, value) 113 | } 114 | } 115 | 116 | /** 117 | * VorpalLocalStorage almost but does not quite implement the LocalStorage API. This interface 118 | * reflects the methods that are actually implemented. 119 | */ 120 | interface VorpalLocalStorage { 121 | getItem: (key: string) => string | unknown 122 | setItem: (key: string, value: string) => void 123 | removeItem: (key: string) => void 124 | } 125 | -------------------------------------------------------------------------------- /src/commands/Command.ts: -------------------------------------------------------------------------------- 1 | import { convertPayStringToUrl, PaymentInformation } from '@paystring/utils' 2 | import axios, { AxiosResponse } from 'axios' 3 | import * as Vorpal from 'vorpal' 4 | import { Args } from 'vorpal' 5 | 6 | import LocalStorage from './localstorage' 7 | 8 | /* eslint-disable eslint-comments/no-unlimited-disable -- to many rules to disable */ 9 | /* eslint-disable -- the linter hates this import */ 10 | const { jsonBeautify } = require('beautify-json') 11 | /* eslint-enable */ 12 | 13 | /** 14 | * Base class that other commands extend. The base class does command registration to Vorpal and 15 | * also manages the local storage access. 16 | */ 17 | abstract class Command { 18 | protected readonly vorpal: Vorpal 19 | 20 | protected readonly localStorage: LocalStorage 21 | 22 | /** 23 | * Base constructor for a Vorpal command that uses local storage. 24 | * 25 | * @param vorpal - The vorpal instance to use. 26 | * @param localStorage - The shared local storage instance to use. 27 | */ 28 | public constructor(vorpal: Vorpal, localStorage: LocalStorage) { 29 | this.localStorage = localStorage 30 | this.vorpal = vorpal 31 | } 32 | 33 | /** 34 | * Sets up and registers the vorpal command. 35 | * 36 | * @returns The registered command. 37 | */ 38 | public setup(): Vorpal.Command { 39 | // Register the concrete command to Vorpal. 40 | // Execute the concrete action inside a try/catch wrapper 41 | return this.vorpal.command(this.command(), this.description()).action( 42 | async (args: Args): Promise => { 43 | await this.action(args).catch((error) => { 44 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- error has any type 45 | const { message } = error 46 | this.vorpal.log(message) 47 | }) 48 | }, 49 | ) 50 | } 51 | 52 | /** 53 | * Returns the payment information from local storage. 54 | * 55 | * @returns PaymentInfo. 56 | * @throws Error if no info found. 57 | */ 58 | protected getPaymentInfo(): PaymentInformation { 59 | const info = this.localStorage.getPaymentInfo() 60 | if (info === undefined) { 61 | throw new Error( 62 | `error: no PayID loaded. Run 'payid init' or 'payid load' first.`, 63 | ) 64 | } 65 | return info 66 | } 67 | 68 | /** 69 | * Pretty prints JSON to the console. 70 | * 71 | * @param info - Payment info to log. 72 | */ 73 | protected logPaymentInfo(info: PaymentInformation): void { 74 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call -- no type def for this library 75 | jsonBeautify(JSON.stringify(info, null, 2)) 76 | } 77 | 78 | /** 79 | * Retrieves a PayString using the optional payString argument or else returns the PaymentInformation 80 | * currently loaded in local storage. 81 | * 82 | * @param args - The vorpal args object. 83 | * @returns PaymentInformation from args or local storage. Or error if can't be loaded. 84 | */ 85 | protected async payStringFromArgsOrLocalStorage( 86 | args: Args, 87 | ): Promise { 88 | const { payId: payString } = args 89 | if (payString) { 90 | return loadPayString(payString) 91 | } 92 | return this.getPaymentInfo() 93 | } 94 | 95 | /** 96 | * The vorpal command. 97 | * 98 | * @returns The vorpal command. 99 | */ 100 | protected abstract command(): string 101 | 102 | /** 103 | * The vorpal description. 104 | * 105 | * @returns The vorpal description. 106 | */ 107 | protected abstract description(): string 108 | 109 | /** 110 | * Executes the action for the command. 111 | * 112 | * @param args - Arguments provided by user from command line. 113 | */ 114 | protected abstract async action(args: Args): Promise 115 | } 116 | 117 | /** 118 | * Loads a PayID from a remote server. 119 | * 120 | * @param payString - The PayID to lookup. 121 | * @returns A promise that resolves to the PaymentInformation for the PayID. 122 | */ 123 | export async function loadPayString( 124 | payString: string, 125 | ): Promise { 126 | const url = convertPayStringToUrl(payString).href 127 | return axios 128 | .get(url, { 129 | headers: { 130 | 'payid-version': '1.0', 131 | accept: 'application/payid+json', 132 | }, 133 | }) 134 | .then((response) => { 135 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- axios response.data has an any type 136 | const info: PaymentInformation = response.data 137 | return info 138 | }) 139 | .catch((error) => { 140 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- axios error has an any type 141 | const { 142 | response, 143 | message, 144 | }: { response?: AxiosResponse; message: string } = error 145 | if (response) { 146 | throw new Error(`Received HTTP status ${response.status} on ${url}`) 147 | } 148 | throw new Error(`Bad request ${url}. Error: ${message}.`) 149 | }) 150 | } 151 | 152 | export default Command 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `@paystring/paystring-cli` 2 | 3 | ![NPM version badge](https://img.shields.io/npm/v/@paystring/paystring-cli) 4 | 5 | Command-line interface to create, fetch, sign, and verify PayStrings. 6 | Based on the Typescript PayString [Utils](https://github.com/paystring/utils) library. 7 | 8 | ## Prerequisites 9 | 10 | Before you install PayString CLI locally, ensure that both [node](https://nodejs.org/en/download/) and 11 | [npm](https://docs.npmjs.com/downloading-and-installing-packages-locally) are installed locally. 12 | 13 | You can also run PayString CLI as a Docker container. If you run commands that cause information to be stored locally, that information only persists for the duration of the container. 14 | 15 | ## Installation 16 | 17 | To install PayString CLI, run the command: 18 | 19 | ``` 20 | npm install -g @paystring/paystring-cli 21 | ``` 22 | 23 | This command installs PayString CLI as a global npm module and links it as a `paystring` executable 24 | (typically under /usr/local/bin/paystring). 25 | 26 | Alternatively, install and run via Docker: 27 | 28 | ``` 29 | docker run xpring/paystring-cli 30 | ``` 31 | 32 | ## Interactive vs. single command mode 33 | 34 | You can run PayString CLI in either interactive mode or non-interactive (single command) mode. 35 | In interactive mode, a prompt is displayed, and you can run multiple commands from this prompt. Run the `exit` command to leave interactive mode. 36 | 37 | Interactive mode retains a history of executed commands that you can access by with the up arrow key. Use the key for command completion. 38 | 39 | In non-interactive mode, you run a single command, based on supplied command line arguments, and then the CLI exits. 40 | No prompt is displayed in this mode. Non-interactive mode is useful for running commands from a script, or to chain the results 41 | of multiple commands together. 42 | 43 | To run the CLI in interactive mode, run `paystring`. You can now enter ` arguments` for each command you want to run. 44 | 45 | To run the CLI in non-interactive mode, run `paystring `. 46 | 47 | Examples of non-interactive mode: 48 | 49 | The following command lists information about the specified PayString. 50 | 51 | ``` 52 | paystring load 'nhartner$xpring.money' 53 | ``` 54 | 55 | You can run multiple commands chained together. This set of commands initializes a new or existing PayString, associates a specified crypto-address for the specified currency and network, and then saves the PayString with this information. 56 | 57 | ``` 58 | paystring init 'my$paystring.com' && paystring crypto-address add btc mainnet notARealAddress && paystring save 59 | ``` 60 | 61 | _Note_: when passing a PayString as an argument in non-interactive mode, the PayString must be escaped or quoted 62 | to avoid the '\$' being interpolated as a variable by the shell. 63 | 64 | ## Commands 65 | 66 | The following commands are available: 67 | 68 | ``` 69 | help [command...] Provides help for a given command. 70 | exit Exits application. 71 | clear Clears the terminal. 72 | crypto-address add
[tag] Starts building a new PayString. 73 | crypto-address remove
Removes an address from the current PayString. 74 | keys clear Clears all loaded keys. 75 | keys generate Generates and saves a new identity key. 76 | keys list Lists keys that have been loaded. 77 | keys load Loads identity-key from file. 78 | keys print Prints keys that have been loaded in pem format. 79 | init Initializes a new PayString. 80 | inspect [paystring] Inspects signatures on the loaded PayString or from an optionally specified PayString. 81 | load Loads a PayString from PayString server. 82 | show Shows the currently loaded PayString. 83 | sign Signs the loaded PayString with the loaded signing keys. 84 | verify [paystring] Verifies the loaded PayString or an optionally specified PayString. 85 | save Saves the currently loaded PayString. 86 | from-url Converts a URL to a PayString. 87 | to-url Converts a PayString to a URL. 88 | 89 | ``` 90 | 91 | ## Use Cases 92 | 93 | ### Load a PayString 94 | 95 | Load an existing PayString from a remote server: 96 | 97 | ``` 98 | load nhartner$xpring.money 99 | ``` 100 | 101 | This command fetches all the PayString address mappings for the given PayString from the remote 102 | server and displays the resulting JSON. 103 | 104 | ### Create a new PayString 105 | 106 | The following set of commands demonstrates how to create a new PayString, attach multiple 107 | address mappings, and save the result to a JSON file. 108 | 109 | ``` 110 | init example$mypaystring.com 111 | crypto-address add xrpl mainnet rP3t3JStqWPYd8H88WfBYh3v84qqYzbHQ6 12345 112 | crypto-address add btc mainnet 3M2CH71P6uZTra1PsjiEhNFB7kCENShCgt 113 | save 114 | ``` 115 | 116 | The PayString JSON representation specified here is saved to the local filesystem as example.json. 117 | 118 | ### Identity Keys 119 | 120 | The PayString protocol supports signing address mappings using one or more cryptographic keys. 121 | PayString CLI provides several commands to generate and load keys. Once a key is generated 122 | or loaded by PayString CLI, it is retained in PayString CLI's local storage for use when you sign your PayString. 123 | 124 | You can generate multiple identity keys by using the `keys generate` and `keys load` commands. 125 | 126 | To remove all loaded keys from the CLI's local storage, use the `keys clear` command. 127 | To see all keys currently loaded into PayString CLI, use the `keys list` command. 128 | 129 | To generate a new key run: 130 | 131 | ``` 132 | keys generate 133 | ``` 134 | 135 | This generates a new key and saves it to a file named `identity-key.pem`. To load a previously 136 | created identity key, run `keys load `. 137 | 138 | ### Sign a PayString 139 | 140 | Before you sign an PayString, you must either load the PayString using the `load` command, or create a PayString using the 141 | `init` command, and you must execute commands so that the PayString one or more crypto-addresses. 142 | 143 | Once a PayString has been initialized or loaded, you can sign it using an [identity key](#identity-keys). You must either generate a new key, or load an existing one. Once your PayString has been loaded or initialized, and your identity key has been generated or loaded, 144 | you can sign the PayString using `sign` command. The `sign` command signs each of your PayString address 145 | mappings using the loaded identity keys, and outputs the resulting PayString with a `verifiedAddress` field. 146 | 147 | By default, the sign command clears the unsigned `addresses` from the results. If you wish to 148 | retain unsigned addresses after signing, use `sign --keep-addresses` or `sign -k` instead. 149 | 150 | Finally, run the `save` command to save your PayString, with signed addresses, to file. 151 | 152 | ### Inspect a Verified PayString 153 | 154 | Two commands are available to verify a PayString's verified addresses. 155 | 156 | - `verify` - checks if all the verified addresses have valid signatures. 157 | - `inspect` - displays details information about each verified address and signatures. 158 | 159 | ## Create, sign, and inspect a PayString 160 | 161 | With a combination of commands, you can create a PayString, add an address mapping, generate an identity key, 162 | sign your PayString address mapping, and then inspect the final result. 163 | 164 | ``` 165 | init example$mypaystring.com 166 | crypto-address add xrpl mainnet rP3t3JStqWPYd8H88WfBYh3v84qqYzbHQ6 167 | keys generate 168 | sign 169 | inspect 170 | ``` 171 | 172 | ## Legal 173 | 174 | By using, reproducing, or distributing this code, you agree to the terms and conditions for use (including the Limitation of Liability) in the [Apache License 2.0](https://github.com/paystring/payid-cli/blob/master/LICENSE). If you do not agree, you may not use, reproduce, or distribute the code. **This code is not authorised for download in Australia. Any persons located in Australia are expressly prohibited from downloading, using, reproducing or distributing the code.** 175 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Ripple Labs Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------