index.js 16 KB

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