rxome-generator 1.0.2 → 1.0.4-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.html +834 -0
- package/README.md +551 -231
- package/demos/demo_data_full.json +1 -1
- package/demos/demo_data_full_compressed.json +1 -1
- package/demos/demo_data_without_key.json +1 -2
- package/demos/qrcode.json +1 -0
- package/index.js +3 -3
- package/lib/package-info.js +4 -0
- package/lib/phenopackets.js +1950 -0
- package/lib/rxome-api-demo.js +26 -0
- package/lib/rxome-api.js +247 -0
- package/lib/rxome-api.test.js +9 -9
- package/lib/{rxome-generator.cjs → rxome-generator.js} +90 -80
- package/lib/rxome-generator.test.js +24 -17
- package/package.json +20 -11
- package/rxcode.js +61 -7
- package/rxcode.test.js +38 -39
- package/README.TODO +0 -4
- package/lib/rxome-api-demo.cjs +0 -58
- package/lib/rxome-api.cjs +0 -135
- package/qrcode.png +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const CRYPT_PRIVATE_KEY = '-----BEGIN PGP PRIVATE KEY BLOCK-----\n\nxYYEY0gdfxYJKwYBBAHaRw8BAQdAYqNejQxT4gE+w2nwBvP+pe19P152F6LV\n8sqM/Qhut2D+CQMIlk6wdgn1LOXgt14o00FVGL49l+pQB8umf27PPnWrJ9IS\nIElQjaCsRYA3ZD/rnDUZiBGVS9++PaegYL339QT2bDp8l6VVtvcxG77svZ2n\na80Xcnh0ZXN0IDxpbmZvQHJ4b21lLm5ldD7CjAQQFgoAHQUCY0gdfwQLCQcI\nAxUICgQWAAIBAhkBAhsDAh4BACEJEGvtGF4xeK/wFiEEP1IE7WS9w5iQJRD8\na+0YXjF4r/CW7AD7BlIn1BHx8kOdyrt6E0L1EKIUi88Q3jQghmvlQomsIzIB\nAKW3e7gYkQJufFTlTWmD5dYmP4v3DfAGvkmFljOvHfwGx4sEY0gdfxIKKwYB\nBAGXVQEFAQEHQNBoBiWwb9t6WCMulp6/opgVJ88iKOY9MpAoZ5dyEbJwAwEI\nB/4JAwhZhPonkWKqteBKH35kf07JpJVMX8LWmZCqdFqXw8tmsU81LtCxVRl8\nexJ0vJor/6LmBUnzMrVSG3S0PCVLw0hAfCH4nN9HxT5gEc1mFFfSwngEGBYI\nAAkFAmNIHX8CGwwAIQkQa+0YXjF4r/AWIQQ/UgTtZL3DmJAlEPxr7RheMXiv\n8FlgAP9S+Oc82N6iSA4gj9hOt6dz7E4YE/3XGf+7uVQb3xVYSQD6A4X+cisS\nfqCVd5bPCMqAQQHjHgGBQawPK/PXyk9JxQQ=\n=idJc\n-----END PGP PRIVATE KEY BLOCK-----\n';
|
|
2
|
+
|
|
3
|
+
const CRYPT_PUBLIC_KEY = '-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxjMEY0gdfxYJKwYBBAHaRw8BAQdAYqNejQxT4gE+w2nwBvP+pe19P152F6LV\n8sqM/Qhut2DNF3J4dGVzdCA8aW5mb0ByeG9tZS5uZXQ+wowEEBYKAB0FAmNI\nHX8ECwkHCAMVCAoEFgACAQIZAQIbAwIeAQAhCRBr7RheMXiv8BYhBD9SBO1k\nvcOYkCUQ/GvtGF4xeK/wluwA+wZSJ9QR8fJDncq7ehNC9RCiFIvPEN40IIZr\n5UKJrCMyAQClt3u4GJECbnxU5U1pg+XWJj+L9w3wBr5JhZYzrx38Bs44BGNI\nHX8SCisGAQQBl1UBBQEBB0DQaAYlsG/belgjLpaev6KYFSfPIijmPTKQKGeX\nchGycAMBCAfCeAQYFggACQUCY0gdfwIbDAAhCRBr7RheMXiv8BYhBD9SBO1k\nvcOYkCUQ/GvtGF4xeK/wWWAA/1L45zzY3qJIDiCP2E63p3PsThgT/dcZ/7u5\nVBvfFVhJAPoDhf5yKxJ+oJV3ls8IyoBBAeMeAYFBrA8r89fKT0nFBA==\n=odkw\n-----END PGP PUBLIC KEY BLOCK-----\n';
|
|
4
|
+
|
|
5
|
+
const R_ID = 'rxome';
|
|
6
|
+
const R_PUBLIC_KEY = '4OShAt7RQ/RJA0qAvoaOQ2jx7SFBYef70XPIz7r9924=';
|
|
7
|
+
const R_PRIVATE_KEY = 'Rf7VbeUBQmjvAagwsWx6riaZYc7h4OBD4CuxYyZ5bgA=';
|
|
8
|
+
// const R_PUBLIC_KEY = '60uReCXTn7KTEIExM4KveKstBGI3TaSrQss4biaesNs=';
|
|
9
|
+
// const R_PRIVATE_KEY = 'lBSkSxe/+UBWOeF5OJdQgf9qZhiI85hYE6yJCuWjCNk=';
|
|
10
|
+
// const R_PRIVATE_KEY = 'NamaTB+xwDFxtkQyBBkjRr5GEaXNtCw/G4qydnhQk5Y=';
|
|
11
|
+
// const R_PUBLIC_KEY = 'XvbhLWKbA1wfKsx3B7FKQuDQsZTZ/dMXWiD1MehBxZg=';
|
|
12
|
+
|
|
13
|
+
const J_ID = 'rxomej';
|
|
14
|
+
const J_PRIVATE_KEY = 'QhcoRruGBVP39XCh8BujCE+q42qCRy/tu2CQ4YmRBgg=';
|
|
15
|
+
const J_PUBLIC_KEY = 'XL/i8jrJC55AdOV3zYHIIa095De5eYbDqWDPDW2r8tk=';
|
|
16
|
+
|
|
17
|
+
const DEMO_CREDENTIALS = {
|
|
18
|
+
id: R_ID,
|
|
19
|
+
user: 'test@rxome.net',
|
|
20
|
+
key: R_PRIVATE_KEY
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export { CRYPT_PRIVATE_KEY, CRYPT_PUBLIC_KEY, DEMO_CREDENTIALS };
|
|
24
|
+
export const DEMO_API_PRIVATE_KEY = R_PRIVATE_KEY;
|
|
25
|
+
export const DEMO_API_PUBLIC_KEY = R_PUBLIC_KEY;
|
|
26
|
+
export const DEMO_API_ID = R_ID;
|
package/lib/rxome-api.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// Using native fetch API instead of axios for better webpack compatibility
|
|
2
|
+
import * as ED from 'noble-ed25519';
|
|
3
|
+
import Protobuf from 'protobufjs';
|
|
4
|
+
import { stringify } from 'querystring';
|
|
5
|
+
|
|
6
|
+
const API = 'https://app.findme2care.de';
|
|
7
|
+
const TESTAPI = 'https://stage.findme2care.de';
|
|
8
|
+
const APIENTRY = 'api/v1';
|
|
9
|
+
const VSTR = 'API1.0'
|
|
10
|
+
const IS_DEMO = '_DEMO_PSEUDONYM_';
|
|
11
|
+
|
|
12
|
+
export { API, TESTAPI, APIENTRY, VSTR, IS_DEMO };
|
|
13
|
+
|
|
14
|
+
/************************************************************************************
|
|
15
|
+
* Helper functions
|
|
16
|
+
************************************************************************************/
|
|
17
|
+
|
|
18
|
+
// have issues:
|
|
19
|
+
const base64ToBufferVintage = data => atob(data.toString());
|
|
20
|
+
//const bufferToBase64 = data => Buffer.from(data).toString('base64');
|
|
21
|
+
|
|
22
|
+
const unpack = (arr) => Uint8Array.from( arr.map( c => c.charCodeAt(0) ));
|
|
23
|
+
|
|
24
|
+
let readSigKey = name => { console.log("Error: not supported") }
|
|
25
|
+
|
|
26
|
+
const base64ToBuffer = enc => {
|
|
27
|
+
const len = Protobuf.util.base64.length(enc);
|
|
28
|
+
let buf = new Protobuf.util.Array( len );
|
|
29
|
+
Protobuf.util.base64.decode(enc, buf, 0);
|
|
30
|
+
return buf;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const bufferToBase64 = buf => {
|
|
34
|
+
return Protobuf.util.base64.encode(buf, 0, buf.length);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { base64ToBuffer, bufferToBase64, unpack };
|
|
38
|
+
|
|
39
|
+
/************************************************************************************
|
|
40
|
+
* Functions for node.js only
|
|
41
|
+
************************************************************************************/
|
|
42
|
+
|
|
43
|
+
// Node.js specific functions that require fs module
|
|
44
|
+
readSigKey = async name => {
|
|
45
|
+
try {
|
|
46
|
+
const FS = await import('fs');
|
|
47
|
+
if (!FS.existsSync( name )) {
|
|
48
|
+
throw 'Key file not found!';
|
|
49
|
+
}
|
|
50
|
+
return FS.readFileSync( name ).slice(0,44);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
if (e instanceof Error && e.code !== "MODULE_NOT_FOUND") {
|
|
53
|
+
throw e;
|
|
54
|
+
}
|
|
55
|
+
throw new Error('fs module not available - cannot read key file');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const generateApiKeys = async () => {
|
|
60
|
+
const privateKey = ED.utils.randomPrivateKey();
|
|
61
|
+
const publicKey = await ED.getPublicKey(privateKey);
|
|
62
|
+
return {
|
|
63
|
+
privateKey: privateKey,
|
|
64
|
+
publicKey: publicKey
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const writeApiKeys = async (name = 'rxome', dir = '.') => {
|
|
69
|
+
try {
|
|
70
|
+
const FS = await import('fs');
|
|
71
|
+
const key = await generateApiKeys();
|
|
72
|
+
await Promise.all([
|
|
73
|
+
FS.writeFile( `${dir}/${name}.private.apikey`, bufferToBase64( key.privateKey ), { mode: 0o600 }, err => { if (err) throw err; } ),
|
|
74
|
+
FS.writeFile( `${dir}/${name}.public.apikey`, bufferToBase64( key.publicKey ), { mode: 0o600 }, err => { if (err) throw err; } )
|
|
75
|
+
]);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
if (e instanceof Error && e.code !== "MODULE_NOT_FOUND") {
|
|
78
|
+
throw e;
|
|
79
|
+
}
|
|
80
|
+
throw new Error('fs module not available - cannot write API keys');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { readSigKey };
|
|
85
|
+
|
|
86
|
+
/************************************************************************************/
|
|
87
|
+
|
|
88
|
+
export const signData = async( keyId, user, keyB64, created, debug = false ) => {
|
|
89
|
+
const message = `x-date: ${created}\nx-rxome-user: ${user}`
|
|
90
|
+
const messageUi8 = unpack( Array.from(message) );
|
|
91
|
+
|
|
92
|
+
const key = unpack( [...base64ToBufferVintage(keyB64)] );
|
|
93
|
+
debug && console.log('Base 64 key: ', keyB64);
|
|
94
|
+
debug && console.log('Binary key: ', key, " Key length: ", key.length);
|
|
95
|
+
|
|
96
|
+
const signature = await ED.sign( messageUi8, key);
|
|
97
|
+
const sigB64 = bufferToBase64( signature );
|
|
98
|
+
const auth=`Signature keyId="${keyId}",algorithm="ed25519",headers="x-date x-rxome-user",signature="${sigB64}",created="${created}"`
|
|
99
|
+
debug && console.log('Auth string: ', auth);
|
|
100
|
+
|
|
101
|
+
return auth;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const fetchData = async ( url, credentials, pseudonym = '', debug = false ) => {
|
|
105
|
+
debug && console.log( 'Fetching from', url )
|
|
106
|
+
|
|
107
|
+
const created = Date.now();
|
|
108
|
+
const keyId = credentials.keyId || "rxome";
|
|
109
|
+
const user = credentials.user || `${keyId}@rxome.net`;
|
|
110
|
+
const keyB64 = credentials.key || await readSigKey( process.cwd()+'/'+credentials.keyFile );
|
|
111
|
+
|
|
112
|
+
const auth = await signData( keyId, user, keyB64, created, debug );
|
|
113
|
+
|
|
114
|
+
// Build URL with query parameters
|
|
115
|
+
const urlParams = new URLSearchParams({
|
|
116
|
+
pslab: !!pseudonym.trim(),
|
|
117
|
+
demo: pseudonym === IS_DEMO
|
|
118
|
+
});
|
|
119
|
+
const fullUrl = `${url}?${urlParams}`;
|
|
120
|
+
|
|
121
|
+
// Create AbortController for timeout
|
|
122
|
+
const controller = new AbortController();
|
|
123
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
124
|
+
|
|
125
|
+
return fetch(fullUrl, {
|
|
126
|
+
method: 'GET',
|
|
127
|
+
headers: {
|
|
128
|
+
Authorization: auth,
|
|
129
|
+
'x-date': created,
|
|
130
|
+
'x-rxome-user': user
|
|
131
|
+
},
|
|
132
|
+
signal: controller.signal
|
|
133
|
+
})
|
|
134
|
+
.then(async response => {
|
|
135
|
+
clearTimeout(timeoutId);
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
let errorData = null;
|
|
138
|
+
try {
|
|
139
|
+
errorData = await response.json();
|
|
140
|
+
} catch (e) {
|
|
141
|
+
// If response can't be parsed as JSON, use default error
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const error = new Error(`HTTP error! status: ${response.status}`);
|
|
145
|
+
error.response = {
|
|
146
|
+
status: response.status,
|
|
147
|
+
statusText: response.statusText,
|
|
148
|
+
headers: response.headers,
|
|
149
|
+
data: errorData
|
|
150
|
+
};
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
return response.json();
|
|
154
|
+
})
|
|
155
|
+
.then(data => {
|
|
156
|
+
debug && console.log( "Result Data= ", data );
|
|
157
|
+
return data;
|
|
158
|
+
})
|
|
159
|
+
.catch(error => {
|
|
160
|
+
clearTimeout(timeoutId);
|
|
161
|
+
if (error.name === 'AbortError') {
|
|
162
|
+
const timeoutError = new Error('Request timeout');
|
|
163
|
+
timeoutError.response = {
|
|
164
|
+
status: 408,
|
|
165
|
+
statusText: 'Request Timeout',
|
|
166
|
+
data: { message: 'Request timeout' }
|
|
167
|
+
};
|
|
168
|
+
throw timeoutError;
|
|
169
|
+
}
|
|
170
|
+
throw error;
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const pushData = async ( url, credentials, msg, debug = false ) => {
|
|
175
|
+
debug && console.log( 'Pushing to ', url )
|
|
176
|
+
console.log( 'Pushing to ', url )
|
|
177
|
+
|
|
178
|
+
const created = Date.now();
|
|
179
|
+
const keyId = credentials.keyId || "rxome";
|
|
180
|
+
const user = credentials.user || `${keyId}@rxome.net`;
|
|
181
|
+
const keyB64 = credentials.key || await readSigKey( process.cwd()+'/'+credentials.keyFile );
|
|
182
|
+
|
|
183
|
+
const auth = await signData( keyId, user, keyB64, created, debug );
|
|
184
|
+
|
|
185
|
+
// Create AbortController for timeout
|
|
186
|
+
const controller = new AbortController();
|
|
187
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
188
|
+
|
|
189
|
+
return fetch(url, {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers: {
|
|
192
|
+
'Content-Type': 'application/json',
|
|
193
|
+
Authorization: auth,
|
|
194
|
+
'x-date': created,
|
|
195
|
+
'x-rxome-user': user
|
|
196
|
+
},
|
|
197
|
+
body: JSON.stringify(msg),
|
|
198
|
+
signal: controller.signal
|
|
199
|
+
})
|
|
200
|
+
.then(response => {
|
|
201
|
+
clearTimeout(timeoutId);
|
|
202
|
+
if (!response.ok) {
|
|
203
|
+
// Create error object similar to axios
|
|
204
|
+
const error = new Error(`HTTP error! status: ${response.status}`);
|
|
205
|
+
error.response = {
|
|
206
|
+
status: response.status,
|
|
207
|
+
statusText: response.statusText,
|
|
208
|
+
headers: response.headers,
|
|
209
|
+
data: null
|
|
210
|
+
};
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
return response.json();
|
|
214
|
+
})
|
|
215
|
+
.then(data => {
|
|
216
|
+
debug && console.log( "Result Data= ", data );
|
|
217
|
+
return data;
|
|
218
|
+
})
|
|
219
|
+
.catch( (error) => {
|
|
220
|
+
clearTimeout(timeoutId);
|
|
221
|
+
let msg = null;
|
|
222
|
+
if (error.response) {
|
|
223
|
+
// Request made and server responded
|
|
224
|
+
if (error.response && error.response.data && error.response.data.message){
|
|
225
|
+
console.log( '[SendData Response Error] ', error.response.data.message );
|
|
226
|
+
msg = `Response Error: ${error.response.data.message}`;
|
|
227
|
+
console.log(error.response.data);
|
|
228
|
+
} else {
|
|
229
|
+
console.log( '[SendData Response Error] no message' );
|
|
230
|
+
msg = 'Unknown response error';
|
|
231
|
+
}
|
|
232
|
+
console.log(error.response.status);
|
|
233
|
+
console.log(error.response.headers);
|
|
234
|
+
} else if (error.name === 'AbortError') {
|
|
235
|
+
// Request was aborted (timeout)
|
|
236
|
+
console.log('[SendData Timeout Error]');
|
|
237
|
+
msg = 'Request timeout';
|
|
238
|
+
} else {
|
|
239
|
+
// Something happened in setting up the request that triggered an Error
|
|
240
|
+
console.log('[SendData Request Error] ', error.message);
|
|
241
|
+
msg = `Request Error: ${error.message}`;
|
|
242
|
+
}
|
|
243
|
+
const err = new Error( `Error sending data: ${msg}` );
|
|
244
|
+
console.log(err.name + ': ' + err.message);
|
|
245
|
+
throw err;
|
|
246
|
+
})
|
|
247
|
+
}
|
package/lib/rxome-api.test.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import * as RxAPI from './rxome-api.js';
|
|
2
|
+
import { readFileSync, existsSync, statSync } from 'fs';
|
|
3
|
+
import * as ED from 'noble-ed25519';
|
|
4
4
|
|
|
5
5
|
describe('API access', () => {
|
|
6
6
|
test.skip('generates valid API access keys', async () => {
|
|
7
7
|
await RxAPI.writeApiKeys( '__TESTSUITE_jesttest' );
|
|
8
|
-
expect(
|
|
9
|
-
expect(
|
|
10
|
-
expect(
|
|
11
|
-
expect(
|
|
8
|
+
expect( existsSync('__TESTSUITE_jesttest.private.apikey') ).toBe( true );
|
|
9
|
+
expect( existsSync('__TESTSUITE_jesttest.public.apikey') ).toBe( true );
|
|
10
|
+
expect( statSync('__TESTSUITE_jesttest.private.apikey').size - 44 ).toBeLessThan( 2 );
|
|
11
|
+
expect( statSync('__TESTSUITE_jesttest.public.apikey').size - 44 ).toBeLessThan( 2 );
|
|
12
12
|
|
|
13
13
|
const message='Answer to life the universe and everything';
|
|
14
14
|
const messageUi8 = RxAPI.unpack(Array.from(message));
|
|
15
|
-
const privKey = RxAPI.unpack([...RxAPI.base64ToBuffer(
|
|
16
|
-
const pubKey = RxAPI.unpack([...RxAPI.base64ToBuffer(
|
|
15
|
+
const privKey = RxAPI.unpack([...RxAPI.base64ToBuffer( readFileSync('__TESTSUITE_jesttest.private.apikey'))])
|
|
16
|
+
const pubKey = RxAPI.unpack([...RxAPI.base64ToBuffer( readFileSync('__TESTSUITE_jesttest.public.apikey'))])
|
|
17
17
|
const signature = await ED.sign(messageUi8, privKey);
|
|
18
18
|
const isValid = await ED.verify(signature, messageUi8, pubKey);
|
|
19
19
|
expect( isValid ).toBeTruthy;
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
// https://openpgpjs.org/
|
|
2
|
-
|
|
2
|
+
import * as PGP from 'openpgp';
|
|
3
3
|
|
|
4
4
|
// https://github.com/soldair/node-qrcode
|
|
5
|
-
|
|
5
|
+
import QRCode from 'qrcode';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import jsonKeyConverter from 'json-key-converter';
|
|
8
|
+
import Protobuf from 'protobufjs';
|
|
9
|
+
import PhenoPacketDescriptor from "./phenopackets.js";
|
|
10
|
+
import { version } from './package-info.js';
|
|
10
11
|
|
|
11
12
|
//const { constants } = require('buffer');
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
import * as RxAPI from './rxome-api.js';
|
|
14
15
|
|
|
15
16
|
const apiVer = '1.0';
|
|
16
17
|
const RXAPI = RxAPI.API;
|
|
@@ -24,35 +25,42 @@ const ProtoBufRoot = Protobuf.Root.fromJSON( PhenoPacketDescriptor );
|
|
|
24
25
|
const PhenoPacket = ProtoBufRoot.lookupType('org.phenopackets.schema.v2.Phenopacket')
|
|
25
26
|
|
|
26
27
|
const ENUM_SEX = ['UNKNOWN', 'FEMALE', 'MALE', 'OTHER']
|
|
27
|
-
|
|
28
|
+
export { ENUM_SEX };
|
|
28
29
|
|
|
29
30
|
const ENUM_PROG_STATE = ['UNKNOWN_PROGRESS', 'IN_PROGRESS', 'COMPLETED', 'SOLVED', 'UNSOLVED']
|
|
30
|
-
|
|
31
|
+
export { ENUM_PROG_STATE };
|
|
31
32
|
|
|
32
|
-
const ENUM_ACMG =
|
|
33
|
+
const ENUM_ACMG =
|
|
33
34
|
['NOT_PROVIDED', 'BENIGN', 'LIKELY_BENIGN', 'UNCERTAIN_SIGNIFICANCE', 'LIKELY_PATHOGENIC', 'PATHOGENIC'];
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
export { ENUM_ACMG };
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
/************************************************************************************
|
|
39
40
|
* Functions for node.js only
|
|
40
41
|
************************************************************************************/
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
exports.generateRxomeKeyFiles = async (name = 'rxome', dir = '.') => {
|
|
47
|
-
const { privateKey, publicKey } = await exports.generateRxomeKeys( name );
|
|
43
|
+
export const generateRxomeKeyFiles = async (name = 'rxome', dir = '.') => {
|
|
44
|
+
try {
|
|
45
|
+
const FS = await import('fs/promises');
|
|
46
|
+
const { privateKey, publicKey } = await generateRxomeKeys( name );
|
|
48
47
|
await Promise.all([
|
|
49
48
|
FS.writeFile(`${dir}/${name}.private.key`, privateKey, { mode: 0o600 }, err => { console.log(err); }),
|
|
50
49
|
FS.writeFile(`${dir}/${name}.public.key`, publicKey, { mode: 0o600 }, function (err) { console.log(err); })
|
|
51
50
|
]);
|
|
52
51
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
catch (e) {
|
|
53
|
+
if (e instanceof Error && e.code !== "MODULE_NOT_FOUND") {
|
|
54
|
+
throw e;
|
|
55
|
+
} else {
|
|
56
|
+
throw new Error('fs module not available - cannot write key files');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const writeQR = async ( filename, data, api = RXAPI, apiEntry = APIENTRY ) => {
|
|
62
|
+
try {
|
|
63
|
+
const {qrData, pseudonym, content} = await prepareQR( data, api, apiEntry );
|
|
56
64
|
//const base64Data = qr_code.replace(/^data:image\/png;base64,/, "");
|
|
57
65
|
//FS.writeFile(filename, base64Data, 'base64', (err) => {console.log(err)} );
|
|
58
66
|
QRCode.toFile( filename, JSON.stringify( qrData ), { type: 'png'} )
|
|
@@ -61,19 +69,20 @@ try {
|
|
|
61
69
|
qr_content: content
|
|
62
70
|
}
|
|
63
71
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
catch (e) {
|
|
73
|
+
if (e instanceof Error && e.code !== "MODULE_NOT_FOUND") {
|
|
74
|
+
throw e;
|
|
75
|
+
} else {
|
|
76
|
+
throw new Error('fs module not available - cannot write QR file');
|
|
77
|
+
}
|
|
69
78
|
}
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
/************************************************************************************/
|
|
73
82
|
|
|
74
83
|
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
export const generateRxomeKeys = async (name = 'rxome') => {
|
|
85
|
+
const { privateKey, publicKey, revocationCertificate } = await PGP.generateKey({
|
|
77
86
|
type: 'ecc',
|
|
78
87
|
curve: 'curve25519',
|
|
79
88
|
//type: 'rsa',
|
|
@@ -82,41 +91,42 @@ exports.generateRxomeKeys = async (name = 'rxome') => {
|
|
|
82
91
|
passphrase: PASSPHRASE,
|
|
83
92
|
format: 'armored'
|
|
84
93
|
});
|
|
94
|
+
return { privateKey, publicKey, revocationCertificate };
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
|
|
88
|
-
|
|
98
|
+
export const fetchKey = async ( credentials, pseudonym = '', api = RXAPI, debug = false, apiEntry = APIENTRY ) => {
|
|
89
99
|
const result = await RxAPI.fetchData( `${api}/${apiEntry}/getpseudonym`, credentials, pseudonym, debug )
|
|
90
|
-
if (
|
|
100
|
+
if ( result.pseudonym === '' )
|
|
91
101
|
result.pseudonym = pseudonym;
|
|
92
102
|
return result;
|
|
93
103
|
}
|
|
94
|
-
/*
|
|
104
|
+
/*
|
|
95
105
|
yields: { key: PGP_key, version: ..., pseudonym: ... }
|
|
96
106
|
*/
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
export const fetchDemoPrivateKey = async ( credentials, api=RXTESTAPI, debug = false, apiEntry = APIENTRY ) =>
|
|
100
110
|
await RxAPI.fetchData( `${api}/${apiEntry}/getprivatedemokey`, credentials, '', debug )
|
|
101
111
|
|
|
102
|
-
/*
|
|
103
|
-
yields: { private_key: PGP_key }
|
|
112
|
+
/*
|
|
113
|
+
yields: { private_key: PGP_key }
|
|
104
114
|
*/
|
|
105
115
|
|
|
106
|
-
|
|
116
|
+
export const fetchRxomeKey = async ( credentials, api = RXAPI, debug = false, apiEntry = APIENTRY ) =>
|
|
107
117
|
await RxAPI.fetchData( `${api}/${apiEntry}/getrxomekey`, credentials, '', debug )
|
|
108
118
|
|
|
109
|
-
/*
|
|
110
|
-
yields: { key: PGP_key, version: ... }
|
|
119
|
+
/*
|
|
120
|
+
yields: { key: PGP_key, version: ... }
|
|
111
121
|
*/
|
|
112
|
-
|
|
122
|
+
export const convert_to_snake_case = data => jsonKeyConverter.convert( data, { camel: false } );
|
|
113
123
|
|
|
114
|
-
|
|
124
|
+
export const convertToCamelCase = data => jsonKeyConverter.convert( data, { camel: true } );
|
|
115
125
|
|
|
116
|
-
|
|
126
|
+
export const compressPhenoPacket = ( data ) => {
|
|
117
127
|
const compressedData = {
|
|
118
128
|
...data,
|
|
119
|
-
};
|
|
129
|
+
};
|
|
120
130
|
if ( data.phenotypicFeatures ) {
|
|
121
131
|
compressedData.compressedFeatures = {
|
|
122
132
|
includes: data.phenotypicFeatures.filter( feat => feat.excluded === undefined || feat.excluded === false || feat.excluded === 'false' ).map( feat => feat.type.id ),
|
|
@@ -128,26 +138,26 @@ exports.compressPhenoPacket = ( data ) => {
|
|
|
128
138
|
}
|
|
129
139
|
|
|
130
140
|
|
|
131
|
-
|
|
141
|
+
export const sanitizeSex = ( str ) =>
|
|
132
142
|
isNaN(+(str.toString()))
|
|
133
143
|
? Math.max(0, ENUM_SEX.indexOf(str.toUpperCase().replace('_SEX', '')))
|
|
134
144
|
: +str;
|
|
135
145
|
|
|
136
146
|
|
|
137
|
-
|
|
147
|
+
export const sanitizeProgState = ( str ) =>
|
|
138
148
|
isNaN(+(str.toString()))
|
|
139
149
|
? Math.max(0, ENUM_PROG_STATE.indexOf(str.toUpperCase()))
|
|
140
150
|
: +str;
|
|
141
151
|
|
|
142
152
|
|
|
143
|
-
|
|
153
|
+
export const sanitizeACMG = ( str ) =>
|
|
144
154
|
isNaN(+(str.toString()))
|
|
145
155
|
? Math.max(0, ENUM_ACMG.indexOf(str.toUpperCase()))
|
|
146
156
|
: +str
|
|
147
157
|
|
|
148
158
|
|
|
149
|
-
|
|
150
|
-
res = {};
|
|
159
|
+
export const whiteListPhenoPacket = ( data ) => {
|
|
160
|
+
let res = {};
|
|
151
161
|
data.id && (res.id = data.id);
|
|
152
162
|
data.comment && (res.comment = data.comment);
|
|
153
163
|
data.subject && (res.subject = data.subject);
|
|
@@ -161,48 +171,48 @@ exports.whiteListPhenoPacket = ( data ) => {
|
|
|
161
171
|
}
|
|
162
172
|
|
|
163
173
|
|
|
164
|
-
|
|
174
|
+
export const sanitizePhenoPacket = ( data, key = '' ) => {
|
|
165
175
|
if ( data instanceof Object ) {
|
|
166
176
|
if (Array.isArray( data )) {
|
|
167
|
-
return data.map( x =>
|
|
177
|
+
return data.map( x => sanitizePhenoPacket(x));
|
|
168
178
|
} else {
|
|
169
179
|
let obj={};
|
|
170
|
-
for ( let k in data ) {
|
|
171
|
-
obj[k] =
|
|
180
|
+
for ( let k in data ) {
|
|
181
|
+
obj[k] = sanitizePhenoPacket( data[k], k );
|
|
172
182
|
}
|
|
173
183
|
return obj;
|
|
174
184
|
}
|
|
175
185
|
} else {
|
|
176
186
|
switch( key ){
|
|
177
187
|
case 'sex':
|
|
178
|
-
return
|
|
188
|
+
return sanitizeSex( data );
|
|
179
189
|
case 'progressStatus':
|
|
180
|
-
return
|
|
190
|
+
return sanitizeProgState( data );
|
|
181
191
|
case 'acmgPathogenicityClassification':
|
|
182
|
-
return
|
|
183
|
-
default:
|
|
192
|
+
return sanitizeACMG( data );
|
|
193
|
+
default:
|
|
184
194
|
return data;
|
|
185
195
|
}
|
|
186
196
|
}
|
|
187
197
|
}
|
|
188
198
|
|
|
189
199
|
|
|
190
|
-
|
|
200
|
+
export const verifyPhenoPacket = (data) => (
|
|
191
201
|
PhenoPacket.verify( data )
|
|
192
202
|
)
|
|
193
203
|
|
|
194
204
|
|
|
195
|
-
|
|
205
|
+
export const encodePhenoPacket = (data) => (
|
|
196
206
|
PhenoPacket.encode( data ).finish()
|
|
197
207
|
)
|
|
198
208
|
|
|
199
209
|
|
|
200
|
-
|
|
210
|
+
export const decodePhenoPacket = (data) => (
|
|
201
211
|
PhenoPacket.decode( data )
|
|
202
212
|
)
|
|
203
213
|
|
|
204
214
|
|
|
205
|
-
|
|
215
|
+
export const encode_serial = async (publicKeyStr, message) => {
|
|
206
216
|
const publicKey = await PGP.readKey({ armoredKey: publicKeyStr });
|
|
207
217
|
const encrypted = await PGP.encrypt({
|
|
208
218
|
message: await PGP.createMessage({ text: message }),
|
|
@@ -214,10 +224,10 @@ exports.encode_serial = async (publicKeyStr, message) => {
|
|
|
214
224
|
}
|
|
215
225
|
|
|
216
226
|
|
|
217
|
-
|
|
227
|
+
export const encode = async (publicKeyStr, message, binary = false) => {
|
|
218
228
|
return await Promise.all([
|
|
219
229
|
PGP.readKey({ armoredKey: publicKeyStr }),
|
|
220
|
-
PGP.createMessage( (binary
|
|
230
|
+
PGP.createMessage( (binary
|
|
221
231
|
? { binary: message, format: 'binary' }
|
|
222
232
|
: { text: message }) )
|
|
223
233
|
]).then( async ([publicKey, message]) => {
|
|
@@ -233,7 +243,7 @@ exports.encode = async (publicKeyStr, message, binary = false) => {
|
|
|
233
243
|
}
|
|
234
244
|
|
|
235
245
|
|
|
236
|
-
|
|
246
|
+
export const decode_serial = async (privateKeyStr, encrypted) => {
|
|
237
247
|
const privateKey = await PGP.readPrivateKey( { armoredKey: privateKeyStr } );
|
|
238
248
|
const privateKeyDecr = await PGP.decryptKey({
|
|
239
249
|
privateKey: privateKey,
|
|
@@ -250,16 +260,16 @@ exports.decode_serial = async (privateKeyStr, encrypted) => {
|
|
|
250
260
|
}
|
|
251
261
|
|
|
252
262
|
|
|
253
|
-
|
|
263
|
+
export const decode = async (privateKeyStr, encrypted, binary = false) => {
|
|
254
264
|
return await Promise.all([
|
|
255
265
|
PGP.readPrivateKey({ armoredKey: privateKeyStr })
|
|
256
266
|
.then( (privateKey) => {
|
|
257
267
|
return PGP.decryptKey({
|
|
258
268
|
privateKey: privateKey,
|
|
259
|
-
passphrase: PASSPHRASE
|
|
269
|
+
passphrase: PASSPHRASE
|
|
260
270
|
})
|
|
261
271
|
}),
|
|
262
|
-
PGP.readMessage( (binary
|
|
272
|
+
PGP.readMessage( (binary
|
|
263
273
|
? {binaryMessage: encrypted}
|
|
264
274
|
: {armoredMessage: encrypted} )
|
|
265
275
|
)
|
|
@@ -282,27 +292,28 @@ exports.decode = async (privateKeyStr, encrypted, binary = false) => {
|
|
|
282
292
|
}
|
|
283
293
|
|
|
284
294
|
|
|
285
|
-
|
|
295
|
+
export const prepareQR = async ( data, api = RXAPI, apiEntry = APIENTRY ) => {
|
|
296
|
+
console.log(`RxOME QR Generator version: ${version}`);
|
|
286
297
|
const { metaData, credentials, ...medical } = data;
|
|
287
298
|
|
|
288
|
-
const whiteListMedical =
|
|
289
|
-
const sanitizedMedical =
|
|
290
|
-
const compressedMedical =
|
|
291
|
-
const protobufMedical =
|
|
299
|
+
const whiteListMedical = whiteListPhenoPacket( medical );
|
|
300
|
+
const sanitizedMedical = sanitizePhenoPacket( whiteListMedical );
|
|
301
|
+
const compressedMedical = compressPhenoPacket( sanitizedMedical );
|
|
302
|
+
const protobufMedical = encodePhenoPacket( compressedMedical );
|
|
292
303
|
const base64Medical = RxAPI.bufferToBase64(protobufMedical);
|
|
293
304
|
|
|
294
|
-
const key = await
|
|
305
|
+
const key = await fetchKey( credentials, (metaData && metaData.pseudonym) || '', api, false, apiEntry );
|
|
295
306
|
|
|
296
307
|
// check:
|
|
297
308
|
const buff = RxAPI.base64ToBuffer( base64Medical );
|
|
298
|
-
const pheno =
|
|
309
|
+
const pheno = decodePhenoPacket( buff );
|
|
299
310
|
const medicalDeciphered = JSON.parse( JSON.stringify( pheno ));
|
|
300
311
|
// console.log( JSON.stringify(medicalDeciphered,' ', 2 ))
|
|
301
312
|
|
|
302
|
-
const cipher = await
|
|
313
|
+
const cipher = await encode( key.key, base64Medical );
|
|
303
314
|
|
|
304
315
|
//delete metaData.pseudonym;
|
|
305
|
-
const newMetaData = Object.fromEntries(
|
|
316
|
+
const newMetaData = Object.fromEntries(
|
|
306
317
|
Object.entries( metaData ).filter( ([key, val]) => key !== 'pseudonym')
|
|
307
318
|
)
|
|
308
319
|
|
|
@@ -312,7 +323,7 @@ exports.prepareQR = async ( data, api = RXAPI, apiEntry = APIENTRY ) => {
|
|
|
312
323
|
labid: key.lab_id,
|
|
313
324
|
keyver: key.version,
|
|
314
325
|
apiver: apiVer,
|
|
315
|
-
pseudonym: key.pseudonym,
|
|
326
|
+
pseudonym: key.pseudonym,
|
|
316
327
|
payload: cipher.toString()
|
|
317
328
|
}
|
|
318
329
|
|
|
@@ -331,8 +342,8 @@ exports.prepareQR = async ( data, api = RXAPI, apiEntry = APIENTRY ) => {
|
|
|
331
342
|
}
|
|
332
343
|
|
|
333
344
|
|
|
334
|
-
|
|
335
|
-
const {qrData, pseudonym, content} = await
|
|
345
|
+
export const makeQR = async ( data, api = RXAPI, apiEntry = APIENTRY ) => {
|
|
346
|
+
const {qrData, pseudonym, content} = await prepareQR( data, api, apiEntry );
|
|
336
347
|
return {
|
|
337
348
|
qr_code: await QRCode.toDataURL( JSON.stringify( qrData )),
|
|
338
349
|
pseudonym: pseudonym,
|
|
@@ -341,8 +352,8 @@ exports.makeQR = async ( data, api = RXAPI, apiEntry = APIENTRY ) => {
|
|
|
341
352
|
}
|
|
342
353
|
}
|
|
343
354
|
|
|
344
|
-
|
|
345
|
-
return
|
|
355
|
+
export const versionStr = () => {
|
|
356
|
+
return `RxOME QR Generator ${version}, 2025 Tom Kamphans, GeneTalk GmbH/RxOme GmbH`
|
|
346
357
|
}
|
|
347
358
|
|
|
348
359
|
|
|
@@ -356,5 +367,4 @@ exports.versionStr = () => {
|
|
|
356
367
|
// const clearBin = Buffer.from (clearBufferBin, 'Binary');
|
|
357
368
|
// const phenoPrimeBin = Coder.decodePhenoPacket(clearBin);
|
|
358
369
|
// const phenoBin = JSON.parse(JSON.stringify(phenoPrimeBin));
|
|
359
|
-
// ////
|
|
360
|
-
|
|
370
|
+
// ////
|