index.js 15 KB

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