index.js 16 KB

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