UNPKG

blockstack

Version:

The Blockstack Javascript library for authentication, identity, and storage.

851 lines 37.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const hub_1 = require("./hub"); exports.connectToGaiaHub = hub_1.connectToGaiaHub; exports.uploadToGaiaHub = hub_1.uploadToGaiaHub; exports.BLOCKSTACK_GAIA_HUB_LABEL = hub_1.BLOCKSTACK_GAIA_HUB_LABEL; const ec_1 = require("../encryption/ec"); const keys_1 = require("../keys"); const profileLookup_1 = require("../profiles/profileLookup"); const errors_1 = require("../errors"); const authConstants_1 = require("../auth/authConstants"); const utils_1 = require("../utils"); const fetchUtil_1 = require("../fetchUtil"); const SIGNATURE_FILE_SUFFIX = '.sig'; /** * Fetch the public read URL of a user file for the specified app. * @param {String} path - the path to the file to read * @param {String} username - The Blockstack ID of the user to look up * @param {String} appOrigin - The app origin * @param {String} [zoneFileLookupURL=null] - The URL * to use for zonefile lookup. If falsey, this will use the * blockstack.js's [[getNameInfo]] function instead. * @return {Promise<string>} that resolves to the public read URL of the file * or rejects with an error */ function getUserAppFileUrl(path, username, appOrigin, zoneFileLookupURL) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const profile = yield profileLookup_1.lookupProfile(username, zoneFileLookupURL); let bucketUrl = null; if (profile.hasOwnProperty('apps')) { if (profile.apps.hasOwnProperty(appOrigin)) { const url = profile.apps[appOrigin]; const bucket = url.replace(/\/?(\?|#|$)/, '/$1'); bucketUrl = `${bucket}${path}`; } } return bucketUrl; }); } exports.getUserAppFileUrl = getUserAppFileUrl; /** * Encrypts the data provided with the app public key. * @param {String|Buffer} content - data to encrypt * @param {Object} [options=null] - options object * @param {String} options.publicKey - the hex string of the ECDSA public * key to use for encryption. If not provided, will use user's appPublicKey. * @return {String} Stringified ciphertext object */ function encryptContent(caller, content, options) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const opts = Object.assign({}, options); let privateKey; if (!opts.publicKey) { privateKey = caller.loadUserData().appPrivateKey; opts.publicKey = keys_1.getPublicKeyFromPrivate(privateKey); } let wasString; if (typeof opts.wasString === 'boolean') { wasString = opts.wasString; } else { wasString = typeof content === 'string'; } const contentBuffer = typeof content === 'string' ? Buffer.from(content) : content; const cipherObject = yield ec_1.encryptECIES(opts.publicKey, contentBuffer, wasString, opts.cipherTextEncoding); let cipherPayload = JSON.stringify(cipherObject); if (opts.sign) { if (typeof opts.sign === 'string') { privateKey = opts.sign; } else if (!privateKey) { privateKey = caller.loadUserData().appPrivateKey; } const signatureObject = ec_1.signECDSA(privateKey, cipherPayload); const signedCipherObject = { signature: signatureObject.signature, publicKey: signatureObject.publicKey, cipherText: cipherPayload }; cipherPayload = JSON.stringify(signedCipherObject); } return cipherPayload; }); } exports.encryptContent = encryptContent; /** * Decrypts data encrypted with `encryptContent` with the * transit private key. * @param {String|Buffer} content - encrypted content. * @param {Object} [options=null] - options object * @param {String} options.privateKey - the hex string of the ECDSA private * key to use for decryption. If not provided, will use user's appPrivateKey. * @return {String|Buffer} decrypted content. */ function decryptContent(caller, content, options) { const opts = Object.assign({}, options); if (!opts.privateKey) { opts.privateKey = caller.loadUserData().appPrivateKey; } try { const cipherObject = JSON.parse(content); return ec_1.decryptECIES(opts.privateKey, cipherObject); } catch (err) { if (err instanceof SyntaxError) { throw new Error('Failed to parse encrypted content JSON. The content may not ' + 'be encrypted. If using getFile, try passing { decrypt: false }.'); } else { throw err; } } } exports.decryptContent = decryptContent; /* Get the gaia address used for servicing multiplayer reads for the given * (username, app) pair. * @private * @ignore */ function getGaiaAddress(caller, app, username, zoneFileLookupURL) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const opts = normalizeOptions(caller, { app, username, zoneFileLookupURL }); let fileUrl; if (username) { fileUrl = yield getUserAppFileUrl('/', opts.username, opts.app, opts.zoneFileLookupURL); } else { const gaiaHubConfig = yield caller.getOrSetLocalGaiaHubConnection(); fileUrl = yield hub_1.getFullReadUrl('/', gaiaHubConfig); } const matches = fileUrl.match(/([13][a-km-zA-HJ-NP-Z0-9]{26,35})/); if (!matches) { throw new Error('Failed to parse gaia address'); } return matches[matches.length - 1]; }); } /** * @param {Object} [options=null] - options object * @param {String} options.username - the Blockstack ID to lookup for multi-player storage * @param {String} options.app - the app to lookup for multi-player storage - * defaults to current origin * * @ignore */ function normalizeOptions(caller, options) { const opts = Object.assign({}, options); if (opts.username) { if (!opts.app) { if (!caller.appConfig) { throw new errors_1.InvalidStateError('Missing AppConfig'); } opts.app = caller.appConfig.appDomain; } if (!opts.zoneFileLookupURL) { if (!caller.appConfig) { throw new errors_1.InvalidStateError('Missing AppConfig'); } if (!caller.store) { throw new errors_1.InvalidStateError('Missing store UserSession'); } const sessionData = caller.store.getSessionData(); // Use the user specified coreNode if available, otherwise use the app specified coreNode. const configuredCoreNode = sessionData.userData.coreNode || caller.appConfig.coreNode; if (configuredCoreNode) { opts.zoneFileLookupURL = `${configuredCoreNode}${authConstants_1.NAME_LOOKUP_PATH}`; } } } return opts; } /** * * @param {String} path - the path to the file to read * @returns {Promise<string>} that resolves to the URL or rejects with an error */ function getFileUrl(caller, path, options) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const opts = normalizeOptions(caller, options); let readUrl; if (opts.username) { readUrl = yield getUserAppFileUrl(path, opts.username, opts.app, opts.zoneFileLookupURL); } else { const gaiaHubConfig = yield caller.getOrSetLocalGaiaHubConnection(); readUrl = yield hub_1.getFullReadUrl(path, gaiaHubConfig); } if (!readUrl) { throw new Error('Missing readURL'); } else { return readUrl; } }); } exports.getFileUrl = getFileUrl; /* Handle fetching the contents from a given path. Handles both * multi-player reads and reads from own storage. * @private * @ignore */ function getFileContents(caller, path, app, username, zoneFileLookupURL, forceText) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const opts = { app, username, zoneFileLookupURL }; const readUrl = yield getFileUrl(caller, path, opts); const response = yield fetchUtil_1.fetchPrivate(readUrl); if (!response.ok) { throw yield utils_1.getBlockstackErrorFromResponse(response, `getFile ${path} failed.`, null); } let contentType = response.headers.get('Content-Type'); if (typeof contentType === 'string') { contentType = contentType.toLowerCase(); } const etag = response.headers.get('ETag'); if (etag) { const sessionData = caller.store.getSessionData(); sessionData.etags[path] = etag; caller.store.setSessionData(sessionData); } if (forceText || contentType === null || contentType.startsWith('text') || contentType.startsWith('application/json')) { return response.text(); } else { return response.arrayBuffer(); } }); } /* Handle fetching an unencrypted file, its associated signature * and then validate it. Handles both multi-player reads and reads * from own storage. * @private * @ignore */ function getFileSignedUnencrypted(caller, path, opt) { return tslib_1.__awaiter(this, void 0, void 0, function* () { // future optimization note: // in the case of _multi-player_ reads, this does a lot of excess // profile lookups to figure out where to read files // do browsers cache all these requests if Content-Cache is set? const sigPath = `${path}${SIGNATURE_FILE_SUFFIX}`; try { const [fileContents, signatureContents, gaiaAddress] = yield Promise.all([ getFileContents(caller, path, opt.app, opt.username, opt.zoneFileLookupURL, false), getFileContents(caller, sigPath, opt.app, opt.username, opt.zoneFileLookupURL, true), getGaiaAddress(caller, opt.app, opt.username, opt.zoneFileLookupURL) ]); if (!fileContents) { return fileContents; } if (!gaiaAddress) { throw new errors_1.SignatureVerificationError('Failed to get gaia address for verification of: ' + `${path}`); } if (!signatureContents || typeof signatureContents !== 'string') { throw new errors_1.SignatureVerificationError('Failed to obtain signature for file: ' + `${path} -- looked in ${path}${SIGNATURE_FILE_SUFFIX}`); } let signature; let publicKey; try { const sigObject = JSON.parse(signatureContents); signature = sigObject.signature; publicKey = sigObject.publicKey; } catch (err) { if (err instanceof SyntaxError) { throw new Error('Failed to parse signature content JSON ' + `(path: ${path}${SIGNATURE_FILE_SUFFIX})` + ' The content may be corrupted.'); } else { throw err; } } const signerAddress = keys_1.publicKeyToAddress(publicKey); if (gaiaAddress !== signerAddress) { throw new errors_1.SignatureVerificationError(`Signer pubkey address (${signerAddress}) doesn't` + ` match gaia address (${gaiaAddress})`); } else if (!ec_1.verifyECDSA(fileContents, publicKey, signature)) { throw new errors_1.SignatureVerificationError('Contents do not match ECDSA signature: ' + `path: ${path}, signature: ${path}${SIGNATURE_FILE_SUFFIX}`); } else { return fileContents; } } catch (err) { // For missing .sig files, throw `SignatureVerificationError` instead of `DoesNotExist` error. if (err instanceof errors_1.DoesNotExist && err.message.indexOf(sigPath) >= 0) { throw new errors_1.SignatureVerificationError('Failed to obtain signature for file: ' + `${path} -- looked in ${path}${SIGNATURE_FILE_SUFFIX}`); } else { throw err; } } }); } /* Handle signature verification and decryption for contents which are * expected to be signed and encrypted. This works for single and * multiplayer reads. In the case of multiplayer reads, it uses the * gaia address for verification of the claimed public key. * @private * @ignore */ function handleSignedEncryptedContents(caller, path, storedContents, app, privateKey, username, zoneFileLookupURL) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const appPrivateKey = privateKey || caller.loadUserData().appPrivateKey; const appPublicKey = keys_1.getPublicKeyFromPrivate(appPrivateKey); let address; if (username) { address = yield getGaiaAddress(caller, app, username, zoneFileLookupURL); } else { address = keys_1.publicKeyToAddress(appPublicKey); } if (!address) { throw new errors_1.SignatureVerificationError('Failed to get gaia address for verification of: ' + `${path}`); } let sigObject; try { sigObject = JSON.parse(storedContents); } catch (err) { if (err instanceof SyntaxError) { throw new Error('Failed to parse encrypted, signed content JSON. The content may not ' + 'be encrypted. If using getFile, try passing' + ' { verify: false, decrypt: false }.'); } else { throw err; } } const signature = sigObject.signature; const signerPublicKey = sigObject.publicKey; const cipherText = sigObject.cipherText; const signerAddress = keys_1.publicKeyToAddress(signerPublicKey); if (!signerPublicKey || !cipherText || !signature) { throw new errors_1.SignatureVerificationError('Failed to get signature verification data from file:' + ` ${path}`); } else if (signerAddress !== address) { throw new errors_1.SignatureVerificationError(`Signer pubkey address (${signerAddress}) doesn't` + ` match gaia address (${address})`); } else if (!ec_1.verifyECDSA(cipherText, signerPublicKey, signature)) { throw new errors_1.SignatureVerificationError('Contents do not match ECDSA signature in file:' + ` ${path}`); } else if (typeof (privateKey) === 'string') { const decryptOpt = { privateKey }; return caller.decryptContent(cipherText, decryptOpt); } else { return caller.decryptContent(cipherText); } }); } /** * Retrieves the specified file from the app's data store. * @param {String} path - the path to the file to read * @returns {Promise} that resolves to the raw data in the file * or rejects with an error */ function getFile(caller, path, options) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const defaults = { decrypt: true, verify: false, username: null, app: utils_1.getGlobalObject('location', { returnEmptyObject: true }).origin, zoneFileLookupURL: null }; const opt = Object.assign({}, defaults, options); // in the case of signature verification, but no // encryption expected, need to fetch _two_ files. if (opt.verify && !opt.decrypt) { return getFileSignedUnencrypted(caller, path, opt); } const storedContents = yield getFileContents(caller, path, opt.app, opt.username, opt.zoneFileLookupURL, !!opt.decrypt); if (storedContents === null) { return storedContents; } else if (opt.decrypt && !opt.verify) { if (typeof storedContents !== 'string') { throw new Error('Expected to get back a string for the cipherText'); } if (typeof (opt.decrypt) === 'string') { const decryptOpt = { privateKey: opt.decrypt }; return caller.decryptContent(storedContents, decryptOpt); } else { return caller.decryptContent(storedContents); } } else if (opt.decrypt && opt.verify) { if (typeof storedContents !== 'string') { throw new Error('Expected to get back a string for the cipherText'); } let decryptionKey; if (typeof (opt.decrypt) === 'string') { decryptionKey = opt.decrypt; } return handleSignedEncryptedContents(caller, path, storedContents, opt.app, decryptionKey, opt.username, opt.zoneFileLookupURL); } else if (!opt.verify && !opt.decrypt) { return storedContents; } else { throw new Error('Should be unreachable.'); } }); } exports.getFile = getFile; /** @ignore */ class FileContentLoader { constructor(content, contentType) { this.wasString = typeof content === 'string'; this.content = FileContentLoader.normalizeContentDataType(content, contentType); this.contentType = contentType || this.detectContentType(); this.contentByteLength = this.detectContentLength(); } static normalizeContentDataType(content, contentType) { try { if (typeof content === 'string') { // If a charset is specified it must be either utf8 or ascii, otherwise the encoded content // length cannot be reliably detected. If no charset specified it will be treated as utf8. const charset = (contentType || '').toLowerCase().replace('-', ''); if (charset.includes('charset') && !charset.includes('charset=utf8') && !charset.includes('charset=ascii')) { throw new Error(`Unable to determine byte length with charset: ${contentType}`); } if (typeof TextEncoder !== 'undefined') { const encodedString = new TextEncoder().encode(content); return Buffer.from(encodedString.buffer); } return Buffer.from(content); } else if (Buffer.isBuffer(content)) { return content; } else if (ArrayBuffer.isView(content)) { return Buffer.from(content.buffer, content.byteOffset, content.byteLength); } else if (typeof Blob !== 'undefined' && content instanceof Blob) { return content; } else if (typeof ArrayBuffer !== 'undefined' && content instanceof ArrayBuffer) { return Buffer.from(content); } else if (Array.isArray(content)) { // Provided with a regular number `Array` -- this is either an (old) method // of representing an octet array, or a dev error. Perform basic check for octet array. if (content.length > 0 && (!Number.isInteger(content[0]) || content[0] < 0 || content[0] > 255)) { throw new Error(`Unexpected array values provided as file data: value "${content[0]}" at index 0 is not an octet number. ${this.supportedTypesMsg}`); } return Buffer.from(content); } else { const typeName = Object.prototype.toString.call(content); throw new Error(`Unexpected type provided as file data: ${typeName}. ${this.supportedTypesMsg}`); } } catch (error) { console.error(error); throw new Error(`Error processing data: ${error}`); } } detectContentType() { if (this.wasString) { return 'text/plain; charset=utf-8'; } else if (typeof Blob !== 'undefined' && this.content instanceof Blob && this.content.type) { return this.content.type; } else { return 'application/octet-stream'; } } detectContentLength() { if (ArrayBuffer.isView(this.content) || Buffer.isBuffer(this.content)) { return this.content.byteLength; } else if (typeof Blob !== 'undefined' && this.content instanceof Blob) { return this.content.size; } const typeName = Object.prototype.toString.call(this.content); const error = new Error(`Unexpected type "${typeName}" while detecting content length`); console.error(error); throw error; } loadContent() { return tslib_1.__awaiter(this, void 0, void 0, function* () { try { if (Buffer.isBuffer(this.content)) { return this.content; } else if (ArrayBuffer.isView(this.content)) { return Buffer.from(this.content.buffer, this.content.byteOffset, this.content.byteLength); } else if (typeof Blob !== 'undefined' && this.content instanceof Blob) { const reader = new FileReader(); const readPromise = new Promise((resolve, reject) => { reader.onerror = (err) => { reject(err); }; reader.onload = () => { const arrayBuffer = reader.result; resolve(Buffer.from(arrayBuffer)); }; reader.readAsArrayBuffer(this.content); }); const result = yield readPromise; return result; } else { const typeName = Object.prototype.toString.call(this.content); throw new Error(`Unexpected type ${typeName}`); } } catch (error) { console.error(error); const loadContentError = new Error(`Error loading content: ${error}`); console.error(loadContentError); throw loadContentError; } }); } load() { if (this.loadedData === undefined) { this.loadedData = this.loadContent(); } return this.loadedData; } } FileContentLoader.supportedTypesMsg = 'Supported types are: `string` (to be UTF8 encoded), ' + '`Buffer`, `Blob`, `File`, `ArrayBuffer`, `UInt8Array` or any other typed array buffer. '; /** * Determines if a gaia error response is possible to recover from * by refreshing the gaiaHubConfig, and retrying the request. */ function isRecoverableGaiaError(error) { if (!error || !error.hubError || !error.hubError.statusCode) { return false; } const statusCode = error.hubError.statusCode; // 401 Unauthorized: possible expired, but renewable auth token. if (statusCode === 401) { return true; } // 409 Conflict: possible concurrent writes to a file. if (statusCode === 409) { return true; } // 500s: possible server-side transient error if (statusCode >= 500 && statusCode <= 599) { return true; } return false; } /** * Stores the data provided in the app's data store to to the file specified. * @param {UserSession} caller - internal use only: the usersession * @param {String} path - the path to store the data in * @param {String|Buffer|ArrayBufferView|Blob} content - the data to store in the file * @param {PutFileOptions} options - the putfile options * @return {Promise} that resolves if the operation succeed and rejects * if it failed */ function putFile(caller, path, content, options) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const defaults = { encrypt: true, sign: false, cipherTextEncoding: 'hex', dangerouslyIgnoreEtag: false }; const opt = Object.assign({}, defaults, options); const gaiaHubConfig = yield caller.getOrSetLocalGaiaHubConnection(); const maxUploadBytes = utils_1.megabytesToBytes(gaiaHubConfig.max_file_upload_size_megabytes); const hasMaxUpload = maxUploadBytes > 0; const contentLoader = new FileContentLoader(content, opt.contentType); let contentType = contentLoader.contentType; // When not encrypting the content length can be checked immediately. if (!opt.encrypt && hasMaxUpload && contentLoader.contentByteLength > maxUploadBytes) { const sizeErrMsg = `The max file upload size for this hub is ${maxUploadBytes} bytes, the given content is ${contentLoader.contentByteLength} bytes`; const sizeErr = new errors_1.PayloadTooLargeError(sizeErrMsg, null, maxUploadBytes); console.error(sizeErr); throw sizeErr; } // When encrypting, the content length must be calculated. Certain types like `Blob`s must // be loaded into memory. if (opt.encrypt && hasMaxUpload) { const encryptedSize = ec_1.eciesGetJsonStringLength({ contentLength: contentLoader.contentByteLength, wasString: contentLoader.wasString, sign: !!opt.sign, cipherTextEncoding: opt.cipherTextEncoding }); if (encryptedSize > maxUploadBytes) { const sizeErrMsg = `The max file upload size for this hub is ${maxUploadBytes} bytes, the given content is ${encryptedSize} bytes after encryption`; const sizeErr = new errors_1.PayloadTooLargeError(sizeErrMsg, null, maxUploadBytes); console.error(sizeErr); throw sizeErr; } } let etag; let newFile = true; const sessionData = caller.store.getSessionData(); if (!opt.dangerouslyIgnoreEtag) { if (sessionData.etags[path]) { newFile = false; etag = sessionData.etags[path]; } } let uploadFn; // In the case of signing, but *not* encrypting, we perform two uploads. if (!opt.encrypt && opt.sign) { const contentData = yield contentLoader.load(); let privateKey; if (typeof opt.sign === 'string') { privateKey = opt.sign; } else { privateKey = caller.loadUserData().appPrivateKey; } const signatureObject = ec_1.signECDSA(privateKey, contentData); const signatureContent = JSON.stringify(signatureObject); uploadFn = (hubConfig) => tslib_1.__awaiter(this, void 0, void 0, function* () { const writeResponse = (yield Promise.all([ hub_1.uploadToGaiaHub(path, contentData, hubConfig, contentType, newFile, etag, opt.dangerouslyIgnoreEtag), hub_1.uploadToGaiaHub(`${path}${SIGNATURE_FILE_SUFFIX}`, signatureContent, hubConfig, 'application/json') ]))[0]; if (writeResponse.etag) { sessionData.etags[path] = writeResponse.etag; caller.store.setSessionData(sessionData); } return writeResponse.publicURL; }); } else { // In all other cases, we only need one upload. let contentForUpload; if (!opt.encrypt && !opt.sign) { // If content does not need encrypted or signed, it can be passed directly // to the fetch request without loading into memory. contentForUpload = contentLoader.content; } else { // Use the `encrypt` key, otherwise the `sign` key, if neither are specified // then use the current user's app public key. let publicKey; if (typeof opt.encrypt === 'string') { publicKey = opt.encrypt; } else if (typeof opt.sign === 'string') { publicKey = keys_1.getPublicKeyFromPrivate(opt.sign); } else { publicKey = keys_1.getPublicKeyFromPrivate(caller.loadUserData().appPrivateKey); } const contentData = yield contentLoader.load(); contentForUpload = yield encryptContent(caller, contentData, { publicKey, wasString: contentLoader.wasString, cipherTextEncoding: opt.cipherTextEncoding, sign: opt.sign }); contentType = 'application/json'; } uploadFn = (hubConfig) => tslib_1.__awaiter(this, void 0, void 0, function* () { const writeResponse = yield hub_1.uploadToGaiaHub(path, contentForUpload, hubConfig, contentType, newFile, etag, opt.dangerouslyIgnoreEtag); if (writeResponse.etag) { sessionData.etags[path] = writeResponse.etag; caller.store.setSessionData(sessionData); } return writeResponse.publicURL; }); } try { return yield uploadFn(gaiaHubConfig); } catch (error) { // If the upload fails on first attempt, it could be due to a recoverable // error which may succeed by refreshing the config and retrying. if (isRecoverableGaiaError(error)) { console.error(error); console.error('Possible recoverable error during Gaia upload, retrying...'); const freshHubConfig = yield caller.setLocalGaiaHubConnection(); return yield uploadFn(freshHubConfig); } else { throw error; } } }); } exports.putFile = putFile; /** * Deletes the specified file from the app's data store. * @param path - The path to the file to delete. * @param options - Optional options object. * @param options.wasSigned - Set to true if the file was originally signed * in order for the corresponding signature file to also be deleted. * @returns Resolves when the file has been removed or rejects with an error. */ function deleteFile(caller, path, options) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const gaiaHubConfig = yield caller.getOrSetLocalGaiaHubConnection(); const opts = Object.assign({}, options); const sessionData = caller.store.getSessionData(); if (opts.wasSigned) { // If signed, delete both the content file and the .sig file try { yield hub_1.deleteFromGaiaHub(path, gaiaHubConfig); yield hub_1.deleteFromGaiaHub(`${path}${SIGNATURE_FILE_SUFFIX}`, gaiaHubConfig); delete sessionData.etags[path]; caller.store.setSessionData(sessionData); } catch (error) { const freshHubConfig = yield caller.setLocalGaiaHubConnection(); yield hub_1.deleteFromGaiaHub(path, freshHubConfig); yield hub_1.deleteFromGaiaHub(`${path}${SIGNATURE_FILE_SUFFIX}`, gaiaHubConfig); delete sessionData.etags[path]; caller.store.setSessionData(sessionData); } } else { try { yield hub_1.deleteFromGaiaHub(path, gaiaHubConfig); delete sessionData.etags[path]; caller.store.setSessionData(sessionData); } catch (error) { const freshHubConfig = yield caller.setLocalGaiaHubConnection(); yield hub_1.deleteFromGaiaHub(path, freshHubConfig); delete sessionData.etags[path]; caller.store.setSessionData(sessionData); } } }); } exports.deleteFile = deleteFile; /** * Get the app storage bucket URL * @param {String} gaiaHubUrl - the gaia hub URL * @param {String} appPrivateKey - the app private key used to generate the app address * @returns {Promise} That resolves to the URL of the app index file * or rejects if it fails */ function getAppBucketUrl(gaiaHubUrl, appPrivateKey) { return hub_1.getBucketUrl(gaiaHubUrl, appPrivateKey); } exports.getAppBucketUrl = getAppBucketUrl; /** * Loop over the list of files in a Gaia hub, and run a callback on each entry. * Not meant to be called by external clients. * @param {GaiaHubConfig} hubConfig - the Gaia hub config * @param {String | null} page - the page ID * @param {number} callCount - the loop count * @param {number} fileCount - the number of files listed so far * @param {function} callback - the callback to invoke on each file. If it returns a falsey * value, then the loop stops. If it returns a truthy value, the loop continues. * @returns {Promise} that resolves to the number of files listed. * @private * @ignore */ function listFilesLoop(caller, hubConfig, page, callCount, fileCount, callback) { return tslib_1.__awaiter(this, void 0, void 0, function* () { if (callCount > 65536) { // this is ridiculously huge, and probably indicates // a faulty Gaia hub anyway (e.g. on that serves endless data) throw new Error('Too many entries to list'); } hubConfig = hubConfig || (yield caller.getOrSetLocalGaiaHubConnection()); let response; try { const pageRequest = JSON.stringify({ page }); const fetchOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': `${pageRequest.length}`, Authorization: `bearer ${hubConfig.token}` }, body: pageRequest }; response = yield fetchUtil_1.fetchPrivate(`${hubConfig.server}/list-files/${hubConfig.address}`, fetchOptions); if (!response.ok) { throw yield utils_1.getBlockstackErrorFromResponse(response, 'ListFiles failed.', hubConfig); } } catch (error) { // If error occurs on the first call, perform a gaia re-connection and retry. // Same logic as other gaia requests (putFile, getFile, etc). if (callCount === 0) { const freshHubConfig = yield caller.setLocalGaiaHubConnection(); return listFilesLoop(caller, freshHubConfig, page, callCount + 1, 0, callback); } throw error; } const responseText = yield response.text(); const responseJSON = JSON.parse(responseText); const entries = responseJSON.entries; const nextPage = responseJSON.page; if (entries === null || entries === undefined) { // indicates a misbehaving Gaia hub or a misbehaving driver // (i.e. the data is malformed) throw new Error('Bad listFiles response: no entries'); } let entriesLength = 0; for (let i = 0; i < entries.length; i++) { // An entry array can have null entries, signifying a filtered entry and that there may be // additional pages if (entries[i] !== null) { entriesLength++; const rc = callback(entries[i]); if (!rc) { // callback indicates that we're done return fileCount + i; } } } if (nextPage && entries.length > 0) { // keep going -- have more entries return listFilesLoop(caller, hubConfig, nextPage, callCount + 1, fileCount + entriesLength, callback); } else { // no more entries -- end of data return fileCount + entriesLength; } }); } /** * List the set of files in this application's Gaia storage bucket. * @param {function} callback - a callback to invoke on each named file that * returns `true` to continue the listing operation or `false` to end it * @return {Promise} that resolves to the total number of listed files. * If the call is ended early by the callback, the last file is excluded. * If an error occurs the entire call is rejected. */ function listFiles(caller, callback) { return listFilesLoop(caller, null, null, 0, 0, callback); } exports.listFiles = listFiles; //# sourceMappingURL=index.js.map