index.js 16 KB

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