index.js 18 KB

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