index.js 16 KB

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