blockstack
Version:
The Blockstack Javascript library for authentication, identity, and storage.
851 lines • 37.2 kB
JavaScript
;
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