index.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. "use strict";
  2. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  3. return new (P || (P = Promise))(function (resolve, reject) {
  4. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  5. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  6. function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
  7. step((generator = generator.apply(thisArg, _arguments || [])).next());
  8. });
  9. };
  10. var __importDefault = (this && this.__importDefault) || function (mod) {
  11. return (mod && mod.__esModule) ? mod : { "default": mod };
  12. };
  13. Object.defineProperty(exports, "__esModule", { value: true });
  14. const crypto = require("libp2p-crypto");
  15. const util = require("util");
  16. const storage_1 = require("./storage");
  17. const TextEncoder = util.TextEncoder;
  18. const ws_1 = __importDefault(require("ws"));
  19. const util_1 = require("./util");
  20. const webclient_node_1 = __importDefault(require("./webclient-node"));
  21. class BankClient {
  22. constructor(urlBase, ipfsUrlBase, storage = new storage_1.Storage('bankClient'), webClient = new webclient_node_1.default()) {
  23. this.urlBase = urlBase;
  24. this.ipfsUrlBase = ipfsUrlBase;
  25. this.storage = storage;
  26. this.webClient = webClient;
  27. this.wsUrlBase = urlBase.replace(/^http/i, 'ws');
  28. }
  29. static parseBankLink(bankLink) {
  30. if (!bankLink.startsWith('bank:')) {
  31. throw new Error('address must start with bank:');
  32. }
  33. const deprefixed = bankLink.substring(5);
  34. let host;
  35. let address;
  36. let topic;
  37. if (deprefixed[0] === '/' && deprefixed[1] === '/') {
  38. [host, address, topic] = deprefixed.substring(2).split('/');
  39. }
  40. else {
  41. [address, topic] = deprefixed.split('/');
  42. }
  43. if (!address || !topic) {
  44. throw new Error('cannot parse address and topic');
  45. }
  46. return { host, address, topic };
  47. }
  48. getPub() {
  49. return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
  50. yield this.bootstrap();
  51. this.getPriv().id((idErr, pubHash) => {
  52. if (idErr) {
  53. return reject(idErr);
  54. }
  55. resolve(pubHash);
  56. });
  57. }));
  58. }
  59. bootstrap() {
  60. if (this.bootstrapResult) {
  61. return Promise.resolve(this.bootstrapResult);
  62. }
  63. if (this.bootstrapPromise) {
  64. return this.bootstrapPromise;
  65. }
  66. return this.bootstrapPromise = new Promise((resolve, reject) => {
  67. this.storage.get('notaprivatekey').then(privateKeyFromStorage => {
  68. if (privateKeyFromStorage == null) {
  69. console.log('no private key in storage. generating new');
  70. crypto.keys.generateKeyPair('RSA', 2048, (generateErr, privateKey) => {
  71. if (generateErr) {
  72. return reject(generateErr);
  73. }
  74. privateKey.export('password', (exportErr, exportResult) => {
  75. if (exportErr) {
  76. return reject(exportErr);
  77. }
  78. this.storage.set('notaprivatekey', exportResult).then(err => {
  79. // whatever
  80. }).catch(reject);
  81. this.privateKey = privateKey;
  82. resolve(true);
  83. });
  84. });
  85. }
  86. else {
  87. // console.log('importing privatekey');
  88. crypto.keys.import(privateKeyFromStorage, 'password', (err, importedPrivateKey) => {
  89. if (err) {
  90. return reject(err);
  91. }
  92. this.privateKey = importedPrivateKey;
  93. // console.log(this.getPublicKeyString());
  94. // console.log(privateKeyFromStorage);
  95. resolve(true);
  96. });
  97. }
  98. }).catch(reject);
  99. });
  100. }
  101. getNonce() {
  102. return __awaiter(this, void 0, void 0, function* () {
  103. const nonce = yield this.webClient.request({
  104. method: 'GET',
  105. url: this.urlBase + '/bank/nonce'
  106. });
  107. return Number(nonce);
  108. });
  109. }
  110. getBalance() {
  111. return __awaiter(this, void 0, void 0, function* () {
  112. const nonce = yield this.getNonce();
  113. const retrieveRequest = yield this.makePlaintextPayload(JSON.stringify({
  114. _date: new Date().toISOString(),
  115. _nonce: nonce
  116. }));
  117. const topicURL = this.urlBase + '/bank/getbalance';
  118. const postResponse = yield this.webClient.requestJSON({
  119. body: retrieveRequest,
  120. method: 'POST',
  121. url: topicURL
  122. });
  123. return postResponse.balance;
  124. });
  125. }
  126. upload(params) {
  127. return __awaiter(this, void 0, void 0, function* () {
  128. const url = this.urlBase + '/bank/upload';
  129. const formData = {};
  130. if (params.fileData) {
  131. formData.file = {
  132. value: params.fileData,
  133. options: {
  134. filename: params.fileName
  135. }
  136. };
  137. }
  138. if (params.thumbFileData) {
  139. formData.thumb = {
  140. value: params.thumbFileData,
  141. options: {
  142. filename: params.thumbFileName
  143. }
  144. };
  145. }
  146. if (params.links) {
  147. formData.links = JSON.stringify(params.links);
  148. }
  149. for (const attr of ['title', 'text', 'type']) {
  150. if (params[attr] != null) {
  151. formData[attr] = params[attr];
  152. }
  153. }
  154. // console.log('formData', formData);
  155. const uploadResponse = yield this.webClient.requestJSON({
  156. formData,
  157. method: 'POST',
  158. url
  159. });
  160. // console.log('uploadResponse', uploadResponse);
  161. return uploadResponse.hash;
  162. });
  163. }
  164. uploadSlimJSON(item) {
  165. return __awaiter(this, void 0, void 0, function* () {
  166. const url = this.urlBase + '/bank/upload/slim';
  167. const uploadResponse = yield this.webClient.requestJSON({
  168. body: item,
  169. method: 'POST',
  170. url
  171. });
  172. // console.log('uploadResponse', uploadResponse);
  173. return uploadResponse.hash;
  174. });
  175. }
  176. uploadSlimText(item) {
  177. return __awaiter(this, void 0, void 0, function* () {
  178. const url = this.urlBase + '/bank/upload/slim';
  179. const uploadResponse = JSON.parse(yield this.webClient.request({
  180. body: item,
  181. headers: {
  182. 'content-type': 'text/plain'
  183. },
  184. method: 'POST',
  185. url
  186. }));
  187. // console.log('uploadResponse', uploadResponse);
  188. return uploadResponse.hash;
  189. });
  190. }
  191. appendBank(bankAddress, bankTopic, itemHash) {
  192. return __awaiter(this, void 0, void 0, function* () {
  193. const payload = yield this.makePlaintextPayload(itemHash);
  194. const topicURL = this.urlBase + '/bank/private/' + encodeURIComponent(bankAddress) + '/' + encodeURIComponent(bankTopic);
  195. yield this.webClient.requestJSON({
  196. body: payload,
  197. method: 'PUT',
  198. url: topicURL
  199. });
  200. });
  201. }
  202. retrievePrivate(peerAddr, topic) {
  203. return __awaiter(this, void 0, void 0, function* () {
  204. const nonce = yield this.getNonce();
  205. const retrieveRequest = yield this.makePlaintextPayload(JSON.stringify({
  206. _date: new Date().toISOString(),
  207. _nonce: nonce
  208. }));
  209. const topicURL = this.urlBase + '/bank/private/' + encodeURIComponent(peerAddr) + '/' + encodeURIComponent(topic);
  210. const result = yield this.webClient.request({
  211. body: JSON.stringify(retrieveRequest),
  212. headers: {
  213. 'content-type': 'application/json'
  214. },
  215. method: 'POST',
  216. url: topicURL
  217. });
  218. return result;
  219. });
  220. }
  221. subscribePrivate(peerAddr, topic, callback) {
  222. return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
  223. const nonce = yield this.getNonce();
  224. const retrieveRequest = yield this.makePlaintextPayload(JSON.stringify({
  225. _date: new Date().toISOString(),
  226. _nonce: nonce,
  227. addr: peerAddr,
  228. topic: topic
  229. }));
  230. const ws = new ws_1.default(this.wsUrlBase + '/bank/ws', {
  231. headers: {
  232. 'x-msg': retrieveRequest.msg,
  233. 'x-pub': retrieveRequest.pub,
  234. 'x-pubhash': retrieveRequest.pubHash,
  235. 'x-sig': retrieveRequest.sig
  236. }
  237. });
  238. ws.on('open', function open() {
  239. console.log('opened ws');
  240. resolve();
  241. });
  242. ws.on('message', data => {
  243. // console.log('ws message', data);
  244. callback();
  245. });
  246. ws.on('error', err => {
  247. reject(err);
  248. });
  249. }));
  250. }
  251. appendPrivate(peerAddr, topic, hash, replaceHash) {
  252. return __awaiter(this, void 0, void 0, function* () {
  253. const nonce = yield this.getNonce();
  254. const payload = yield this.makePlaintextPayload(JSON.stringify({
  255. _date: new Date().toISOString(),
  256. _nonce: nonce,
  257. hash,
  258. replaceHash
  259. }));
  260. const topicURL = this.urlBase + '/bank/private/' + encodeURIComponent(peerAddr) + '/' + encodeURIComponent(topic);
  261. const result = yield this.webClient.request({
  262. body: JSON.stringify(payload),
  263. headers: {
  264. 'content-type': 'application/json'
  265. },
  266. method: 'PUT',
  267. url: topicURL
  268. });
  269. });
  270. }
  271. getOrCreateContact(peerId, contactAddr) {
  272. return __awaiter(this, void 0, void 0, function* () {
  273. const contactList = yield this.retrievePrivate(peerId, '📇');
  274. const itemList = yield this.getItemsForCommaList(contactList);
  275. // console.log('contact hash for', contact, type, 'is', contactHash);
  276. const existing = itemList.filter(item => item.addrs && item.addrs.includes(contactAddr))[0];
  277. if (existing != null) {
  278. return existing;
  279. }
  280. const newItem = {
  281. addrs: [
  282. contactAddr
  283. ],
  284. id: util_1.uuid()
  285. };
  286. const newItemHash = yield this.uploadSlimJSON(newItem);
  287. yield this.appendPrivate(peerId, '📇', newItemHash);
  288. return newItem;
  289. });
  290. }
  291. getContactById(peerId, contactId) {
  292. return __awaiter(this, void 0, void 0, function* () {
  293. const contactList = yield this.retrievePrivate(peerId, '📇');
  294. const itemList = yield this.getItemsForCommaList(contactList);
  295. const existing = itemList.filter(item => item.id === contactId)[0];
  296. if (!existing) {
  297. throw new Error('Cannot find contact with id ' + contactId);
  298. }
  299. return existing;
  300. });
  301. }
  302. updateContact(peerId, contactId, newProperties) {
  303. return __awaiter(this, void 0, void 0, function* () {
  304. const existing = yield this.getContactById(peerId, contactId);
  305. const newProps = util_1.mergeDeep({}, newProperties);
  306. delete newProps.id;
  307. const newItem = util_1.mergeDeep(existing, newProps);
  308. delete newItem.hash;
  309. const newItemHash = yield this.uploadSlimJSON(newItem);
  310. yield this.appendPrivate(peerId, '📇', newItemHash, existing.hash);
  311. return yield this.getContactById(peerId, contactId);
  312. });
  313. }
  314. getItemsForCommaList(commaList) {
  315. return __awaiter(this, void 0, void 0, function* () {
  316. const itemHashes = commaList.split(',').filter(x => x.trim() !== '');
  317. const items = yield Promise.all(itemHashes.map(itemId => {
  318. return this.webClient.requestJSON({
  319. method: 'get',
  320. url: this.ipfsUrlBase + '/ipfs/' + itemId,
  321. });
  322. }));
  323. for (const item of items) {
  324. item.hash = itemHashes.shift();
  325. }
  326. return items;
  327. });
  328. }
  329. getPriv() {
  330. if (!this.privateKey) {
  331. throw new Error('missing private key');
  332. }
  333. return this.privateKey;
  334. }
  335. makePlaintextPayload(message) {
  336. const messageBytes = new TextEncoder().encode(message);
  337. return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
  338. yield this.bootstrap();
  339. this.privateKey.sign(messageBytes, (signErr, signatureBytes) => __awaiter(this, void 0, void 0, function* () {
  340. if (signErr) {
  341. reject(signErr);
  342. return;
  343. }
  344. const publicDERBytes = this.privateKey.public.bytes;
  345. this.privateKey.id((idErr, pubHash) => {
  346. if (idErr) {
  347. reject(idErr);
  348. return;
  349. }
  350. const result = {
  351. date: new Date().toISOString(),
  352. msg: util_1.encodeHex(messageBytes),
  353. pub: util_1.encodeHex(publicDERBytes),
  354. pubHash,
  355. sig: util_1.encodeHex(signatureBytes),
  356. };
  357. // console.log('result', result, signatureBytes);
  358. resolve(result);
  359. });
  360. }));
  361. }));
  362. }
  363. }
  364. exports.BankClient = BankClient;
  365. //# sourceMappingURL=index.js.map