Skip to content

Commit 5d8bc23

Browse files
feat: new kms api example module branch
TICKET: WP-4379
1 parent 2637d83 commit 5d8bc23

File tree

12 files changed

+307
-0
lines changed

12 files changed

+307
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ modules/**/dist/
1717
modules/**/pack-scoped/
1818
coverage
1919
/.direnv/
20+
*.db
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# KMS API example (REST API)
2+
3+
Based on TDD specification [On-Prem Wallets(https://docs.google.com/document/d/1ku2agwirV3tHCJF350VF_uaVx73D6vu7yUBaDp-cxL0/edit?tab=t.0#heading=h.165ukudv7ejt)]
4+
5+
Made with ExpressJS, Typescript and sqlite3.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "examplekmsapi",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"scripts": {
6+
"test": "echo \"Error: no test specified\" && exit 1",
7+
"dev": "npx tsx --watch src/app.ts"
8+
},
9+
"keywords": [],
10+
"author": "",
11+
"license": "ISC",
12+
"description": "",
13+
"dependencies": {
14+
"body-parser": "^2.2.0",
15+
"express": "^5.1.0",
16+
"sqlite3": "^5.1.7"
17+
},
18+
"devDependencies": {
19+
"@tsconfig/node22": "^22.0.1",
20+
"@types/express": "^5.0.1",
21+
"@types/node": "^22.15.17",
22+
"tsx": "^4.19.4",
23+
"typescript": "^5.8.3"
24+
}
25+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- TODO: delete this comments! questions for Pranav/Mohammad
2+
-- Some fields with VARCHAR(X) not sure how many characters
3+
-- do we need for a coin.
4+
5+
-- I took the fields from the TDD doc on page 3
6+
-- Not sure also about the prv and pub key length, should we limit them?
7+
CREATE TABLE PRIVATE_KEYS(
8+
id TEXT PRIMARY KEY,
9+
coin VARCHAR(30) NOT NULL,
10+
source VARCHAR(15) CHECK(source IN ('user', 'backup')) NOT NULL,
11+
type VARCHAR(15) CHECK(type IN ('independent', 'tss')) NOT NULL,
12+
prv TEXT NOT NULL,
13+
pub TEXT NOT NULL);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import bodyParser from 'body-parser';
2+
import express from 'express';
3+
import { GET as keyGET } from './controllers/key/GET';
4+
import { POST as keyPOST } from './controllers/key/POST';
5+
import { checkApiKeyMiddleware } from './middlewares/authApiKeys';
6+
7+
// TODO: move to proper .env
8+
// Add note about the port to the README
9+
// Or hardcode it
10+
const PORT = '3000';
11+
12+
const app = express();
13+
app.use(bodyParser.json());
14+
app.use(checkApiKeyMiddleware);
15+
16+
// TODO: create router for keys controllers,
17+
// I added the controllers calls directly here.
18+
app.post('key', keyPOST);
19+
app.get('/key/:pub', keyGET);
20+
21+
app.listen(PORT, () => {
22+
console.log(`KMS API example listening on port ${PORT}`);
23+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Request, Response } from 'express';
2+
import db from '../../db';
3+
4+
type GetParamsType = {
5+
pub: string;
6+
};
7+
8+
export function GET(req: Request<GetParamsType>, res: Response) {
9+
const { pub } = req.params;
10+
//TODO: what happens if source comes empty? should we return an error? an empty result?
11+
const source = req.query.source;
12+
const data = db.query('SELECT (prv, type) FROM PRIVATE_KEY WHERE pub = ? AND source = ?', [pub, source]);
13+
14+
// TODO: not sure how to type this
15+
const { prv, type } = data;
16+
17+
// TODO: i know that we could chain res.status() with .json but what's the preferred way?
18+
res.status(200);
19+
return res.json({ prv, pub, source, type });
20+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { NextFunction, Request, Response } from 'express';
2+
import db from '../../db';
3+
import { ZodPostKeySchema } from './schemas';
4+
5+
export function POST(req: Request, res: Response, next: NextFunction) {
6+
try {
7+
ZodPostKeySchema.parse(req.body);
8+
} catch (e) {
9+
res.status(400);
10+
res.send({ message: 'Invalid data provided from client' });
11+
}
12+
13+
const { prv, pub, coin, source, type } = req.body;
14+
15+
// TODO:
16+
// check duplicated using the prv/pub key?
17+
// exploitable if we show an error?
18+
const keyObject = db.query('SELECT * from PRIVATE_KEYS WHERE prv = ? AND pub = ?', [prv, pub]);
19+
20+
// priv + pub should be unique so we raise an error
21+
if (keyObject) {
22+
res.status(409); // It could be also 403 but 409 is specific for dupplicates.
23+
// TODO: I could return the prv and pub in the error but seems exploitable as hell
24+
res.send({ message: `Error: Duplicated Key` });
25+
return;
26+
}
27+
28+
// From what i got on the TDD, as store you mean create a new entry right?
29+
// not sure about the note that says "for MPC pub would be the commonKeyChain
30+
// does "pub" comes empty at some point?
31+
try {
32+
// TODO: check how to type the queries???
33+
const data = db.query('INSERT INTO PRIVATE_KEYS(prv, pub, coin, source, type) values (?, ?, ?, ?, ?)', [
34+
prv,
35+
pub,
36+
coin,
37+
source,
38+
type,
39+
]);
40+
const { id: keyId } = data;
41+
res.status(200);
42+
return res.json({ keyId, coin, source, type, pub });
43+
} catch (e) {
44+
res.status(500);
45+
res.send({ message: 'Internal server error' }); // some unexpected error on DB, needs better login tho
46+
return;
47+
}
48+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { z } from 'zod';
2+
import { KeySource, KeyType, MultiSigCoins } from './types';
3+
4+
export const ZodPostKeySchema = z.object({
5+
prv: z.string(), // TODO: min/max length?
6+
pub: z.string(), // TODO: min/max length?
7+
coin: z.enum(MultiSigCoins),
8+
source: z.enum(KeySource),
9+
type: z.enum(KeyType),
10+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// TODO: add the full list of supported coins
2+
export const MultiSigCoins = ['btc', 'heth'] as const;
3+
export const KeySource = ['user', 'backup'] as const;
4+
export const KeyType = ['independent', 'tss'] as const;
5+
6+
export type MultiSigCoinsType = (typeof MultiSigCoins)[number];
7+
export type KeySourceType = (typeof KeySource)[number];
8+
export type KeyTypeType = (typeof KeyType)[number];
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import sqlite3 from 'sqlite3';
2+
3+
// TODO: better error handling
4+
const db = new sqlite3.Database('database.db', (err) => {
5+
if (err) console.error(err.message);
6+
});
7+
8+
// TODO: return type missing, params untyped
9+
function query(sql: string, params: any[]) {
10+
return db.prepare(sql).all(params);
11+
}
12+
13+
export default {
14+
query,
15+
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { NextFunction, Request, Response } from 'express';
2+
//TODO: move the list of API keys to a safer place like an env file
3+
const API_KEYS_EXTERNALS = ['abc', 'def'];
4+
5+
export function checkApiKeyMiddleware(req: Request, res: Response, next: NextFunction): void {
6+
const apiKey = req.headers['x-api-key'];
7+
let invalidKey = false;
8+
if (!apiKey) {
9+
invalidKey = true;
10+
} else if (typeof apiKey === 'string') {
11+
invalidKey = !API_KEYS_EXTERNALS.includes(apiKey);
12+
} else if (Array.isArray(apiKey)) {
13+
// Added the forced cast 'as' because for some reason typescript doesn't infers that
14+
// apiKey is an array at this point despite the check on L14
15+
invalidKey = !(apiKey as string[]).some((key) => API_KEYS_EXTERNALS.includes(key));
16+
}
17+
18+
if (invalidKey) {
19+
res.status(401).send({ message: 'Unauthorized' });
20+
return;
21+
}
22+
next();
23+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
{
2+
"compilerOptions": {
3+
/* Visit https://aka.ms/tsconfig to read more about this file */
4+
5+
/* Projects */
6+
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7+
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8+
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9+
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10+
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11+
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12+
13+
/* Language and Environment */
14+
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15+
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16+
// "jsx": "preserve", /* Specify what JSX code is generated. */
17+
// "libReplacement": true, /* Enable lib replacement. */
18+
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
19+
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
20+
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
21+
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
22+
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
23+
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
24+
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
25+
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
26+
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
27+
28+
/* Modules */
29+
"module": "commonjs", /* Specify what module code is generated. */
30+
// "rootDir": "./", /* Specify the root folder within your source files. */
31+
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
32+
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
33+
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
34+
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
35+
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
36+
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
37+
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
38+
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
39+
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
40+
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
41+
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
42+
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
43+
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
44+
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
45+
// "resolveJsonModule": true, /* Enable importing .json files. */
46+
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
47+
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
48+
49+
/* JavaScript Support */
50+
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
51+
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
52+
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
53+
54+
/* Emit */
55+
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
56+
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
57+
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
58+
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
59+
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
60+
// "noEmit": true, /* Disable emitting files from a compilation. */
61+
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
62+
"outDir": "./dist", /* Specify an output folder for all emitted files. */
63+
"rootDir": "./",
64+
// "removeComments": true, /* Disable emitting comments. */
65+
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
66+
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
67+
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
68+
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
69+
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
70+
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
71+
// "newLine": "crlf", /* Set the newline character for emitting files. */
72+
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
73+
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
74+
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
75+
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
76+
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
77+
78+
/* Interop Constraints */
79+
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
80+
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
81+
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
82+
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
83+
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
84+
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
85+
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
86+
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
87+
88+
/* Type Checking */
89+
"strict": true, /* Enable all strict type-checking options. */
90+
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
91+
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
92+
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
93+
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
94+
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
95+
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
96+
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
97+
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
98+
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
99+
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
100+
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
101+
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
102+
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
103+
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
104+
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
105+
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
106+
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
107+
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
108+
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
109+
110+
/* Completeness */
111+
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
112+
"skipLibCheck": true /* Skip type checking all .d.ts files. */
113+
},
114+
"include": ["**/*.ts"],
115+
"exclude": ["dist"]
116+
}

0 commit comments

Comments
 (0)