diff --git a/packages/auth/src/auth.ts b/packages/auth/src/auth.ts index 1552e8017..a18f6e5ea 100644 --- a/packages/auth/src/auth.ts +++ b/packages/auth/src/auth.ts @@ -6,18 +6,18 @@ import { isLaterVersion, getGlobalObject, LoginFailedError, - Logger + Logger, + BLOCKSTACK_DEFAULT_GAIA_HUB_URL } from '@stacks/common' import { getAddressFromDID } from './dids' import { decryptPrivateKey } from './messages' import { - BLOCKSTACK_DEFAULT_GAIA_HUB_URL, NAME_LOOKUP_PATH } from './constants' import { extractProfile } from './legacy/profiles/profileTokens' import { UserSession } from './userSession' import { config } from './legacy/config' -import { GaiaHubConfig } from './legacy/storage/hub' +// import { GaiaHubConfig } from './legacy/storage/hub' import { hexStringToECPair } from '@stacks/encryption' @@ -63,7 +63,7 @@ export interface UserData { // This is the data that gets used when the `new blockstack.Person(profile)` class is used. profile: any; // private: does not get sent to webapp at all. - gaiaHubConfig?: GaiaHubConfig; + gaiaHubConfig?: any; } /** diff --git a/packages/auth/src/legacy/encryption/ec.ts b/packages/auth/src/legacy/encryption/ec.ts index 3e00300e1..f0bc3a587 100644 --- a/packages/auth/src/legacy/encryption/ec.ts +++ b/packages/auth/src/legacy/encryption/ec.ts @@ -6,7 +6,7 @@ import { getPublicKeyFromPrivate } from '../keys' import { hashSha256Sync, hashSha512Sync } from './sha2Hash' import { createHmacSha256 } from './hmacSha256' import { createCipher } from './aesCipher' -import { getAesCbcOutputLength, getBase64OutputLength } from '../utils' +import { getAesCbcOutputLength, getBase64OutputLength } from '@stacks/common' const ecurve = new EllipticCurve('secp256k1') diff --git a/packages/auth/src/legacy/profiles/profileTokens.ts b/packages/auth/src/legacy/profiles/profileTokens.ts index 12fde46fc..e0fdc9fc8 100644 --- a/packages/auth/src/legacy/profiles/profileTokens.ts +++ b/packages/auth/src/legacy/profiles/profileTokens.ts @@ -1,7 +1,7 @@ import { ECPair } from 'bitcoinjs-lib' import { decodeToken, SECP256K1Client, TokenSigner, TokenVerifier } from 'jsontokens' import { TokenInterface } from 'jsontokens/lib/decode' -import { nextYear, makeUUID4 } from '../utils' +import { nextYear, makeUUID4 } from '@stacks/common' import { ecPairToAddress } from '../keys' /** diff --git a/packages/auth/src/legacy/public.ts b/packages/auth/src/legacy/public.ts index 50e5c4df8..a4342d8ee 100644 --- a/packages/auth/src/legacy/public.ts +++ b/packages/auth/src/legacy/public.ts @@ -1,10 +1,5 @@ export * from './profiles' -export { - EncryptionOptions, EncryptContentOptions, - PutFileOptions, getUserAppFileUrl, GetFileUrlOptions, GetFileOptions, getAppBucketUrl -} from './storage' - export { makeDIDFromAddress, makeDIDFromPublicKey, getDIDType, getAddressFromDID } from '../dids' @@ -14,11 +9,6 @@ export { hexStringToECPair, ecPairToHexString, ecPairToAddress } from './keys' -export { - nextYear, nextMonth, nextHour, makeUUID4, updateQueryStringParameter, - isLaterVersion, isSameOriginAbsoluteUrl -} from './utils' - export { transactions, safety, TransactionSigner, PubkeyHashSigner, addUTXOsToFund, estimateTXBytes diff --git a/packages/auth/src/legacy/storage/hub.ts b/packages/auth/src/legacy/storage/hub.ts deleted file mode 100644 index 39eea8516..000000000 --- a/packages/auth/src/legacy/storage/hub.ts +++ /dev/null @@ -1,223 +0,0 @@ - -import { Transaction, script, ECPair } from 'bitcoinjs-lib' -import { TokenSigner } from 'jsontokens' -import { getBlockstackErrorFromResponse } from '../utils' -import { fetchPrivate } from '../fetchUtil' -import { getPublicKeyFromPrivate, ecPairToAddress, hexStringToECPair, } from '../keys' -import { Logger } from '../logger' -import { randomBytes } from '../encryption/cryptoRandom' -import { hashSha256Sync } from '../encryption/sha2Hash' - -/** - * @ignore - */ -export const BLOCKSTACK_GAIA_HUB_LABEL = 'blockstack-gaia-hub-config' - -/** - * The configuration for the user's Gaia storage provider. - */ -export interface GaiaHubConfig { - address: string, - url_prefix: string, - token: string, - max_file_upload_size_megabytes: number | undefined, - server: string -} - -interface UploadResponse { - publicURL: string, - etag?: string -} - -/** - * - * @param filename - * @param contents - * @param hubConfig - * @param contentType - * - * @ignore - */ -export async function uploadToGaiaHub( - filename: string, - contents: Blob | Buffer | ArrayBufferView | string, - hubConfig: GaiaHubConfig, - contentType: string = 'application/octet-stream', - newFile: boolean = true, - etag?: string -): Promise { - Logger.debug(`uploadToGaiaHub: uploading ${filename} to ${hubConfig.server}`) - - const headers: { [key: string]: string; } = { - 'Content-Type': contentType, - Authorization: `bearer ${hubConfig.token}` - } - - if (newFile) { - headers['If-None-Match'] = '*' - } else if (etag) { - headers['If-Match'] = etag - } - - const response = await fetchPrivate( - `${hubConfig.server}/store/${hubConfig.address}/${filename}`, { - method: 'POST', - headers, - body: contents - } - ) - if (!response.ok) { - throw await getBlockstackErrorFromResponse(response, 'Error when uploading to Gaia hub.', hubConfig) - } - const responseText = await response.text() - const responseJSON = JSON.parse(responseText) - - return responseJSON -} - -export async function deleteFromGaiaHub( - filename: string, - hubConfig: GaiaHubConfig -): Promise { - Logger.debug(`deleteFromGaiaHub: deleting ${filename} from ${hubConfig.server}`) - const response = await fetchPrivate( - `${hubConfig.server}/delete/${hubConfig.address}/${filename}`, { - method: 'DELETE', - headers: { - Authorization: `bearer ${hubConfig.token}` - } - } - ) - if (!response.ok) { - throw await getBlockstackErrorFromResponse(response, 'Error deleting file from Gaia hub.', hubConfig) - } -} - -/** - * - * @param filename - * @param hubConfig - * - * @ignore - */ -export function getFullReadUrl(filename: string, - hubConfig: GaiaHubConfig): Promise { - return Promise.resolve(`${hubConfig.url_prefix}${hubConfig.address}/${filename}`) -} - -/** - * - * @param challengeText - * @param signerKeyHex - * - * @ignore - */ -function makeLegacyAuthToken(challengeText: string, signerKeyHex: string): string { - // only sign specific legacy auth challenges. - let parsedChallenge - try { - parsedChallenge = JSON.parse(challengeText) - } catch (err) { - throw new Error('Failed in parsing legacy challenge text from the gaia hub.') - } - if (parsedChallenge[0] === 'gaiahub' - && parsedChallenge[3] === 'blockstack_storage_please_sign') { - const signer = hexStringToECPair(signerKeyHex - + (signerKeyHex.length === 64 ? '01' : '')) - const digest = hashSha256Sync(Buffer.from(challengeText)) - - const signatureBuffer = signer.sign(digest) - const signatureWithHash = script.signature.encode( - signatureBuffer, Transaction.SIGHASH_NONE) - - // We only want the DER encoding so remove the sighash version byte at the end. - // See: https://github.com/bitcoinjs/bitcoinjs-lib/issues/1241#issuecomment-428062912 - const signature = signatureWithHash.toString('hex').slice(0, -2) - - const publickey = getPublicKeyFromPrivate(signerKeyHex) - const token = Buffer.from(JSON.stringify( - { publickey, signature } - )).toString('base64') - return token - } else { - throw new Error('Failed to connect to legacy gaia hub. If you operate this hub, please update.') - } -} - -/** - * - * @param hubInfo - * @param signerKeyHex - * @param hubUrl - * @param associationToken - * - * @ignore - */ -function makeV1GaiaAuthToken(hubInfo: any, - signerKeyHex: string, - hubUrl: string, - associationToken?: string): string { - const challengeText = hubInfo.challenge_text - const handlesV1Auth = (hubInfo.latest_auth_version - && parseInt(hubInfo.latest_auth_version.slice(1), 10) >= 1) - const iss = getPublicKeyFromPrivate(signerKeyHex) - - if (!handlesV1Auth) { - return makeLegacyAuthToken(challengeText, signerKeyHex) - } - - const salt = randomBytes(16).toString('hex') - const payload = { - gaiaChallenge: challengeText, - hubUrl, - iss, - salt, - associationToken - } - const token = new TokenSigner('ES256K', signerKeyHex).sign(payload) - return `v1:${token}` -} - -/** - * - * @ignore - */ -export async function connectToGaiaHub( - gaiaHubUrl: string, - challengeSignerHex: string, - associationToken?: string -): Promise { - Logger.debug(`connectToGaiaHub: ${gaiaHubUrl}/hub_info`) - - const response = await fetchPrivate(`${gaiaHubUrl}/hub_info`) - const hubInfo = await response.json() - const readURL = hubInfo.read_url_prefix - const token = makeV1GaiaAuthToken(hubInfo, challengeSignerHex, gaiaHubUrl, associationToken) - const address = ecPairToAddress(hexStringToECPair(challengeSignerHex - + (challengeSignerHex.length === 64 ? '01' : ''))) - return { - url_prefix: readURL, - max_file_upload_size_megabytes: hubInfo.max_file_upload_size_megabytes, - address, - token, - server: gaiaHubUrl - } -} - -/** - * - * @param gaiaHubUrl - * @param appPrivateKey - * - * @ignore - */ -export async function getBucketUrl(gaiaHubUrl: string, appPrivateKey: string): Promise { - const challengeSigner = ECPair.fromPrivateKey(Buffer.from(appPrivateKey, 'hex')) - const response = await fetchPrivate(`${gaiaHubUrl}/hub_info`) - const responseText = await response.text() - const responseJSON = JSON.parse(responseText) - const readURL = responseJSON.read_url_prefix - const address = ecPairToAddress(challengeSigner) - const bucketUrl = `${readURL}${address}/` - return bucketUrl -} diff --git a/packages/auth/src/legacy/storage/index.ts b/packages/auth/src/legacy/storage/index.ts deleted file mode 100644 index f2e3b66fb..000000000 --- a/packages/auth/src/legacy/storage/index.ts +++ /dev/null @@ -1,1004 +0,0 @@ - - -import { - getFullReadUrl, - connectToGaiaHub, uploadToGaiaHub, getBucketUrl, BLOCKSTACK_GAIA_HUB_LABEL, - GaiaHubConfig, - deleteFromGaiaHub -} from './hub' -// export { type GaiaHubConfig } from './hub' - -import { - encryptECIES, decryptECIES, signECDSA, verifyECDSA, eciesGetJsonStringLength, - SignedCipherObject, CipherTextEncoding -} from '../encryption/ec' -import { getPublicKeyFromPrivate, publicKeyToAddress } from '../keys' -import { lookupProfile } from '../profiles/profileLookup' -import { - InvalidStateError, - SignatureVerificationError, - DoesNotExist, - PayloadTooLargeError, - GaiaHubError -} from '../errors' - -import { UserSession } from '../../userSession' -import { NAME_LOOKUP_PATH } from '../../constants' -import { getGlobalObject, getBlockstackErrorFromResponse, megabytesToBytes } from '../utils' -import { fetchPrivate } from '../fetchUtil' - -export interface EncryptionOptions { - /** - * If set to `true` the data is signed using ECDSA on SHA256 hashes with the user's - * app private key. If a string is specified, it is used as the private key instead - * of the user's app private key. - * @default false - */ - sign?: boolean | string; - /** - * String encoding format for the cipherText buffer. - * Currently defaults to 'hex' for legacy backwards-compatibility. - * Only used if the `encrypt` option is also used. - * Note: in the future this should default to 'base64' for the significant - * file size reduction. - */ - cipherTextEncoding?: CipherTextEncoding; - /** - * Specifies if the original unencrypted content is a ASCII or UTF-8 string. - * For example stringified JSON. - * If true, then when the ciphertext is decrypted, it will be returned as - * a `string` type variable, otherwise will be returned as a Buffer. - */ - wasString?: boolean; -} - -/** - * Specify encryption options, and whether to sign the ciphertext. - */ -export interface EncryptContentOptions extends EncryptionOptions { - /** - * Encrypt the data with this key. - * If not provided then the current user's app public key is used. - */ - publicKey?: string; -} - -/** - * Specify a valid MIME type, encryption options, and whether to sign the [[UserSession.putFile]]. - */ -export interface PutFileOptions extends EncryptionOptions { - /** - * Specifies the Content-Type header for unencrypted data. - * If the `encrypt` is enabled, this option is ignored, and the - * Content-Type header is set to `application/json` for the ciphertext - * JSON envelope. - */ - contentType?: string; - /** - * Encrypt the data with the app public key. - * If a string is specified, it is used as the public key. - * If the boolean `true` is specified then the current user's app public key is used. - * @default true - */ - encrypt?: boolean | string; -} - -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} that resolves to the public read URL of the file - * or rejects with an error - */ -export async function getUserAppFileUrl( - path: string, username: string, appOrigin: string, - zoneFileLookupURL?: string -): Promise { - const profile = await lookupProfile(username, zoneFileLookupURL) - let bucketUrl: string = 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 -} - -/** - * 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 - */ -export async function encryptContent( - caller: UserSession, - content: string | Buffer, - options?: EncryptContentOptions -): Promise { - const opts = Object.assign({}, options) - let privateKey: string - if (!opts.publicKey) { - privateKey = caller.loadUserData().appPrivateKey - opts.publicKey = getPublicKeyFromPrivate(privateKey) - } - let wasString: boolean - if (typeof opts.wasString === 'boolean') { - wasString = opts.wasString - } else { - wasString = typeof content === 'string' - } - const contentBuffer = typeof content === 'string' ? Buffer.from(content) : content - const cipherObject = await 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 = signECDSA(privateKey, cipherPayload) - const signedCipherObject: SignedCipherObject = { - signature: signatureObject.signature, - publicKey: signatureObject.publicKey, - cipherText: cipherPayload - } - cipherPayload = JSON.stringify(signedCipherObject) - } - return cipherPayload -} - -/** - * 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. - */ -export function decryptContent( - caller: UserSession, - content: string, - options?: { - privateKey?: string - }, -): Promise { - const opts = Object.assign({}, options) - if (!opts.privateKey) { - opts.privateKey = caller.loadUserData().appPrivateKey - } - - try { - const cipherObject = JSON.parse(content) - return 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 - } - } -} - -/* Get the gaia address used for servicing multiplayer reads for the given - * (username, app) pair. - * @private - * @ignore - */ -async function getGaiaAddress( - caller: UserSession, app: string, - username?: string, zoneFileLookupURL?: string, -): Promise { - const opts = normalizeOptions(caller, { app, username, zoneFileLookupURL }) - let fileUrl: string - if (username) { - fileUrl = await getUserAppFileUrl('/', opts.username, opts.app, opts.zoneFileLookupURL) - } else { - const gaiaHubConfig = await caller.getOrSetLocalGaiaHubConnection() - fileUrl = await 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: UserSession, - options?: { - app?: string, - username?: string, - zoneFileLookupURL?: string - } & T, -) { - const opts = Object.assign({}, options) - if (opts.username) { - if (!opts.app) { - if (!caller.appConfig) { - throw new InvalidStateError('Missing AppConfig') - } - opts.app = caller.appConfig.appDomain - } - if (!opts.zoneFileLookupURL) { - if (!caller.appConfig) { - throw new InvalidStateError('Missing AppConfig') - } - if (!caller.store) { - throw new 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}${NAME_LOOKUP_PATH}` - } - } - } - return opts -} - -/** - * - * @param {String} path - the path to the file to read - * @returns {Promise} that resolves to the URL or rejects with an error - */ -export async function getFileUrl( - caller: UserSession, - path: string, - options?: GetFileUrlOptions -): Promise { - const opts = normalizeOptions(caller, options) - - let readUrl: string - if (opts.username) { - readUrl = await getUserAppFileUrl(path, opts.username, opts.app, opts.zoneFileLookupURL) - } else { - const gaiaHubConfig = await caller.getOrSetLocalGaiaHubConnection() - readUrl = await getFullReadUrl(path, gaiaHubConfig) - } - - if (!readUrl) { - throw new Error('Missing readURL') - } else { - return readUrl - } -} - -/* Handle fetching the contents from a given path. Handles both - * multi-player reads and reads from own storage. - * @private - * @ignore - */ -async function getFileContents(caller: UserSession, path: string, app: string, - username: string | undefined, - zoneFileLookupURL: string | undefined, - forceText: boolean): Promise { - const opts = { app, username, zoneFileLookupURL } - const readUrl = await getFileUrl(caller, path, opts) - const response = await fetchPrivate(readUrl) - if (!response.ok) { - throw await 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 - */ -async function getFileSignedUnencrypted(caller: UserSession, path: string, opt: GetFileOptions) { - // 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] = await 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 SignatureVerificationError('Failed to get gaia address for verification of: ' - + `${path}`) - } - if (!signatureContents || typeof signatureContents !== 'string') { - throw new 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 = publicKeyToAddress(publicKey) - if (gaiaAddress !== signerAddress) { - throw new SignatureVerificationError(`Signer pubkey address (${signerAddress}) doesn't` - + ` match gaia address (${gaiaAddress})`) - } else if (!verifyECDSA(fileContents, publicKey, signature)) { - throw new 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 DoesNotExist && err.message.indexOf(sigPath) >= 0) { - throw new 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 - */ -async function handleSignedEncryptedContents(caller: UserSession, path: string, - storedContents: string, app: string, - privateKey?: string, username?: string, - zoneFileLookupURL?: string -): Promise { - const appPrivateKey = privateKey || caller.loadUserData().appPrivateKey - - const appPublicKey = getPublicKeyFromPrivate(appPrivateKey) - - let address: string - if (username) { - address = await getGaiaAddress(caller, app, username, zoneFileLookupURL) - } else { - address = publicKeyToAddress(appPublicKey) - } - if (!address) { - throw new 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 = publicKeyToAddress(signerPublicKey) - - if (!signerPublicKey || !cipherText || !signature) { - throw new SignatureVerificationError( - 'Failed to get signature verification data from file:' - + ` ${path}` - ) - } else if (signerAddress !== address) { - throw new SignatureVerificationError(`Signer pubkey address (${signerAddress}) doesn't` - + ` match gaia address (${address})`) - } else if (!verifyECDSA(cipherText, signerPublicKey, signature)) { - throw new 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) - } -} - -export interface GetFileUrlOptions { - /** - * The Blockstack ID to lookup for multi-player storage. - * If not specified, the currently signed in username is used. - */ - username?: string; - /** - * The app to lookup for multi-player storage - defaults to current origin. - * @default `window.location.origin` - * Only if available in the executing environment, otherwise `undefined`. - */ - app?: string; - /** - * The URL to use for zonefile lookup. If falsey, this will use - * the blockstack.js's [[getNameInfo]] function instead. - */ - zoneFileLookupURL?: string; -} - -/** - * Used to pass options to [[UserSession.getFile]] - */ -export interface GetFileOptions extends GetFileUrlOptions { - /** - * Try to decrypt the data with the app private key. - * If a string is specified, it is used as the private key. - * @default true - */ - decrypt?: boolean | string; - /** - * Whether the content should be verified, only to be used - * when [[UserSession.putFile]] was set to `sign = true`. - * @default false - */ - verify?: boolean; -} - -/** - * 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 - */ -export async function getFile( - caller: UserSession, - path: string, - options?: GetFileOptions, -) { - const defaults: GetFileOptions = { - decrypt: true, - verify: false, - username: null, - app: 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 = await 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.') - } -} - -/** @ignore */ -type PutFileContent = string | Buffer | ArrayBufferView | ArrayBufferLike | Blob - -/** @ignore */ -class FileContentLoader { - readonly content: Buffer | Blob - - readonly wasString: boolean - - readonly contentType: string - - readonly contentByteLength: number - - private loadedData?: Promise - - static readonly supportedTypesMsg = 'Supported types are: `string` (to be UTF8 encoded), ' - + '`Buffer`, `Blob`, `File`, `ArrayBuffer`, `UInt8Array` or any other typed array buffer. ' - - constructor(content: PutFileContent, contentType: string) { - this.wasString = typeof content === 'string' - this.content = FileContentLoader.normalizeContentDataType(content, contentType) - this.contentType = contentType || this.detectContentType() - this.contentByteLength = this.detectContentLength() - } - - private static normalizeContentDataType(content: PutFileContent, - contentType: string): Buffer | Blob { - 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}`) - } - } - - private detectContentType(): string { - 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' - } - } - - private detectContentLength(): number { - 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 - } - - private async loadContent(): Promise { - 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 as ArrayBuffer - resolve(Buffer.from(arrayBuffer)) - } - reader.readAsArrayBuffer(this.content as Blob) - }) - const result = await 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(): Promise { - if (this.loadedData === undefined) { - this.loadedData = this.loadContent() - } - return this.loadedData - } -} - -/** - * Determines if a gaia error response is possible to recover from - * by refreshing the gaiaHubConfig, and retrying the request. - */ -function isRecoverableGaiaError(error: GaiaHubError): boolean { - 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 {String} path - the path to store the data in - * @param {String|Buffer} content - the data to store in the file - * @return {Promise} that resolves if the operation succeed and rejects - * if it failed - */ -export async function putFile( - caller: UserSession, - path: string, - content: string | Buffer | ArrayBufferView | Blob, - options?: PutFileOptions -): Promise { - const defaults: PutFileOptions = { - encrypt: true, - sign: false, - cipherTextEncoding: 'hex' - } - const opt = Object.assign({}, defaults, options) - - const gaiaHubConfig = await caller.getOrSetLocalGaiaHubConnection() - const maxUploadBytes = 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 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 = 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 PayloadTooLargeError(sizeErrMsg, null, maxUploadBytes) - console.error(sizeErr) - throw sizeErr - } - } - - let etag: string - let newFile = true - - const sessionData = caller.store.getSessionData(); - if (sessionData.etags[path]) { - newFile = false - etag = sessionData.etags[path] - } - - let uploadFn: (hubConfig: GaiaHubConfig) => Promise - - // In the case of signing, but *not* encrypting, we perform two uploads. - if (!opt.encrypt && opt.sign) { - const contentData = await contentLoader.load() - let privateKey: string - if (typeof opt.sign === 'string') { - privateKey = opt.sign - } else { - privateKey = caller.loadUserData().appPrivateKey - } - const signatureObject = signECDSA(privateKey, contentData) - const signatureContent = JSON.stringify(signatureObject) - - uploadFn = async (hubConfig: GaiaHubConfig) => { - const writeResponse = (await Promise.all([ - uploadToGaiaHub(path, contentData, hubConfig, contentType, newFile, etag), - 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: string | Buffer | Blob - 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: string - if (typeof opt.encrypt === 'string') { - publicKey = opt.encrypt - } else if (typeof opt.sign === 'string') { - publicKey = getPublicKeyFromPrivate(opt.sign) - } else { - publicKey = getPublicKeyFromPrivate(caller.loadUserData().appPrivateKey) - } - const contentData = await contentLoader.load() - contentForUpload = await encryptContent(caller, contentData, { - publicKey, - wasString: contentLoader.wasString, - cipherTextEncoding: opt.cipherTextEncoding, - sign: opt.sign - }) - contentType = 'application/json' - } - - uploadFn = async (hubConfig: GaiaHubConfig) => { - const writeResponse = await uploadToGaiaHub( - path, contentForUpload, hubConfig, contentType, newFile, etag - ) - if (writeResponse.etag) { - sessionData.etags[path] = writeResponse.etag; - caller.store.setSessionData(sessionData); - } - return writeResponse.publicURL - } - } - - try { - return await 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 = await caller.setLocalGaiaHubConnection() - return await uploadFn(freshHubConfig) - } else { - throw error - } - } -} - -/** - * 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. - */ -export async function deleteFile( - caller: UserSession, - path: string, - options?: { - wasSigned?: boolean; - } -) { - const gaiaHubConfig = await 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 { - await deleteFromGaiaHub(path, gaiaHubConfig) - await deleteFromGaiaHub(`${path}${SIGNATURE_FILE_SUFFIX}`, gaiaHubConfig) - delete sessionData.etags[path]; - caller.store.setSessionData(sessionData); - } catch (error) { - const freshHubConfig = await caller.setLocalGaiaHubConnection() - await deleteFromGaiaHub(path, freshHubConfig) - await deleteFromGaiaHub(`${path}${SIGNATURE_FILE_SUFFIX}`, gaiaHubConfig) - delete sessionData.etags[path]; - caller.store.setSessionData(sessionData); - } - } else { - try { - await deleteFromGaiaHub(path, gaiaHubConfig) - delete sessionData.etags[path]; - caller.store.setSessionData(sessionData); - } catch (error) { - const freshHubConfig = await caller.setLocalGaiaHubConnection() - await deleteFromGaiaHub(path, freshHubConfig) - delete sessionData.etags[path]; - caller.store.setSessionData(sessionData); - } - } -} - -/** - * 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 - */ -export function getAppBucketUrl(gaiaHubUrl: string, appPrivateKey: string) { - return getBucketUrl(gaiaHubUrl, appPrivateKey) -} - -/** - * 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 - */ -async function listFilesLoop( - caller: UserSession, - hubConfig: GaiaHubConfig | null, - page: string | null, - callCount: number, - fileCount: number, - callback: (name: string) => boolean -): Promise { - 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 || await caller.getOrSetLocalGaiaHubConnection() - let response: 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 = await fetchPrivate(`${hubConfig.server}/list-files/${hubConfig.address}`, fetchOptions) - if (!response.ok) { - throw await 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 = await caller.setLocalGaiaHubConnection() - return listFilesLoop(caller, freshHubConfig, page, callCount + 1, 0, callback) - } - throw error - } - - const responseText = await 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. - */ -export function listFiles( - caller: UserSession, - callback: (name: string) => boolean -): Promise { - return listFilesLoop(caller, null, null, 0, 0, callback) -} - -export { connectToGaiaHub, uploadToGaiaHub, BLOCKSTACK_GAIA_HUB_LABEL } diff --git a/packages/auth/src/legacy/utils.ts b/packages/auth/src/legacy/utils.ts deleted file mode 100644 index 573b441d8..000000000 --- a/packages/auth/src/legacy/utils.ts +++ /dev/null @@ -1,403 +0,0 @@ -import { Logger } from './logger' -import { - BadPathError, - ConflictError, - DoesNotExist, - GaiaHubErrorResponse, - NotEnoughProofError, - PayloadTooLargeError, - ValidationError, - PreconditionFailedError -} from './errors' - - -/** - * @ignore - */ -export const BLOCKSTACK_HANDLER = 'blockstack' - -/** - * Time - * @private - * @ignore - */ -export function nextYear() { - return new Date( - new Date().setFullYear( - new Date().getFullYear() + 1 - ) - ) -} - -/** - * Time - * @private - * @ignore - */ -export function nextMonth() { - return new Date( - new Date().setMonth( - new Date().getMonth() + 1 - ) - ) -} - -/** - * Time - * @private - * @ignore - */ -export function nextHour() { - return new Date( - new Date().setHours( - new Date().getHours() + 1 - ) - ) -} - -/** - * Converts megabytes to bytes. Returns 0 if the input is not a finite number. - * @ignore - */ -export function megabytesToBytes(megabytes: number): number { - if (!Number.isFinite(megabytes)) { - return 0 - } - return Math.floor(megabytes * 1024 * 1024) -} - -/** - * Calculate the AES-CBC ciphertext output byte length a given input length. - * AES has a fixed block size of 16-bytes regardless key size. - * @ignore - */ -export function getAesCbcOutputLength(inputByteLength: number) { - // AES-CBC block mode rounds up to the next block size. - const cipherTextLength = (Math.floor(inputByteLength / 16) + 1) * 16 - return cipherTextLength -} - -/** - * Calculate the base64 encoded string length for a given input length. - * This is equivalent to the byte length when the string is ASCII or UTF8-8 - * encoded. - * @param number - */ -export function getBase64OutputLength(inputByteLength: number) { - const encodedLength = (Math.ceil(inputByteLength / 3) * 4) - return encodedLength -} - -/** - * Query Strings - * @private - * @ignore - */ - -export function updateQueryStringParameter(uri: string, key: string, value: string) { - const re = new RegExp(`([?&])${key}=.*?(&|$)`, 'i') - const separator = uri.indexOf('?') !== -1 ? '&' : '?' - if (uri.match(re)) { - return uri.replace(re, `$1${key}=${value}$2`) - } else { - return `${uri}${separator}${key}=${value}` - } -} - -/** - * Versioning - * @param {string} v1 - the left half of the version inequality - * @param {string} v2 - right half of the version inequality - * @returns {bool} iff v1 >= v2 - * @private - * @ignore - */ - -export function isLaterVersion(v1: string, v2: string) { - if (v1 === undefined) { - v1 = '0.0.0' - } - - if (v2 === undefined) { - v2 = '0.0.0' - } - - const v1tuple = v1.split('.').map(x => parseInt(x, 10)) - const v2tuple = v2.split('.').map(x => parseInt(x, 10)) - - for (let index = 0; index < v2.length; index++) { - if (index >= v1.length) { - v2tuple.push(0) - } - if (v1tuple[index] < v2tuple[index]) { - return false - } - } - return true -} - -/** - * UUIDs - * @private - * @ignore - */ -export function makeUUID4() { - let d = new Date().getTime() - if (typeof performance !== 'undefined' && typeof performance.now === 'function') { - d += performance.now() // use high-precision timer if available - } - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { - const r = (d + Math.random() * 16) % 16 | 0 - d = Math.floor(d / 16) - return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16) - }) -} - -/** - * Checks if both urls pass the same origin check & are absolute - * @param {[type]} uri1 first uri to check - * @param {[type]} uri2 second uri to check - * @return {Boolean} true if they pass the same origin check - * @private - * @ignore - */ -export function isSameOriginAbsoluteUrl(uri1: string, uri2: string) { - try { - // The globally scoped WHATWG `URL` class is available in modern browsers and - // NodeJS v10 or higher. In older NodeJS versions it must be required from the - // `url` module. - let parseUrl: (url: string) => URL - if (typeof URL !== 'undefined') { - parseUrl = url => new URL(url) - } else { - try { - // eslint-disable-next-line import/no-nodejs-modules, global-require - const nodeUrl = (require('url') as typeof import('url')).URL - parseUrl = url => new nodeUrl(url) - } catch (error) { - console.log(error) - console.error('Global URL class is not available') - } - } - - const parsedUri1 = parseUrl(uri1) - const parsedUri2 = parseUrl(uri2) - - const port1 = parseInt(parsedUri1.port || '0', 10) | 0 || (parsedUri1.protocol === 'https:' ? 443 : 80) - const port2 = parseInt(parsedUri2.port || '0', 10) | 0 || (parsedUri2.protocol === 'https:' ? 443 : 80) - - const match = { - scheme: parsedUri1.protocol === parsedUri2.protocol, - hostname: parsedUri1.hostname === parsedUri2.hostname, - port: port1 === port2, - absolute: (uri1.includes('http://') || uri1.includes('https://')) - && (uri2.includes('http://') || uri2.includes('https://')) - } - - return match.scheme && match.hostname && match.port && match.absolute - } catch (error) { - console.log(error) - console.log('Parsing error in same URL origin check') - // Parse error - return false - } -} - -/** - * Returns the global scope `Window`, `WorkerGlobalScope`, or `NodeJS.Global` if available in the - * currently executing environment. - * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/self - * @see https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/self - * @see https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope - * - * This could be switched to `globalThis` once it is standardized and widely available. - * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis - * @ignore - */ -export function getGlobalScope(): Window { - if (typeof self !== 'undefined') { - return self - } - if (typeof window !== 'undefined') { - return window - } - // This function is meant to be called when accessing APIs that are typically only available in - // web-browser/DOM environments, but we also want to support situations where running in Node.js - // environment, and a polyfill was added to the Node.js `global` object scope without adding the - // `window` global object as well. - if (typeof global !== 'undefined') { - return (global as unknown) as Window - } - throw new Error('Unexpected runtime environment - no supported global scope (`window`, `self`, `global`) available') -} - - -function getAPIUsageErrorMessage( - scopeObject: unknown, - apiName: string, - usageDesc?: string -): string { - if (usageDesc) { - return `Use of '${usageDesc}' requires \`${apiName}\` which is unavailable on the '${scopeObject}' object within the currently executing environment.` - } else { - return `\`${apiName}\` is unavailable on the '${scopeObject}' object within the currently executing environment.` - } -} - -interface GetGlobalObjectOptions { - /** - * Throw an error if the object is not found. - * @default false - */ - throwIfUnavailable?: boolean; - /** - * Additional information to include in an error if thrown. - */ - usageDesc?: string; - /** - * If the object is not found, return an new empty object instead of undefined. - * Requires [[throwIfUnavailable]] to be falsey. - * @default false - */ - returnEmptyObject?: boolean; -} - -/** - * Returns an object from the global scope (`Window` or `WorkerGlobalScope`) if it - * is available within the currently executing environment. - * When executing within the Node.js runtime these APIs are unavailable and will be - * `undefined` unless the API is provided via polyfill. - * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/self - * @ignore - */ -export function getGlobalObject>( - name: K, - { throwIfUnavailable, usageDesc, returnEmptyObject }: GetGlobalObjectOptions = { } -): Window[K] { - let globalScope: Window - try { - globalScope = getGlobalScope() - if (globalScope) { - const obj = globalScope[name] - if (obj) { - return obj - } - } - } catch (error) { - Logger.error(`Error getting object '${name}' from global scope '${globalScope}': ${error}`) - } - if (throwIfUnavailable) { - const errMsg = getAPIUsageErrorMessage(globalScope, name.toString(), usageDesc) - Logger.error(errMsg) - throw new Error(errMsg) - } - if (returnEmptyObject) { - return {} as any - } - return undefined -} - -/** - * Returns a specified subset of objects from the global scope (`Window` or `WorkerGlobalScope`) - * if they are available within the currently executing environment. - * When executing within the Node.js runtime these APIs are unavailable will be `undefined` - * unless the API is provided via polyfill. - * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/self - * @ignore - */ -export function getGlobalObjects>( - names: K[], - { throwIfUnavailable, usageDesc, returnEmptyObject }: GetGlobalObjectOptions = {} -): Pick { - let globalScope: Window - try { - globalScope = getGlobalScope() - } catch (error) { - Logger.error(`Error getting global scope: ${error}`) - if (throwIfUnavailable) { - const errMsg = getAPIUsageErrorMessage(globalScope, names[0].toString(), usageDesc) - Logger.error(errMsg) - throw errMsg - } else if (returnEmptyObject) { - globalScope = {} as any - } - } - - const result: Pick = {} as any - for (let i = 0; i < names.length; i++) { - const name = names[i] - try { - if (globalScope) { - const obj = globalScope[name] - if (obj) { - result[name] = obj - } else if (throwIfUnavailable) { - const errMsg = getAPIUsageErrorMessage(globalScope, name.toString(), usageDesc) - Logger.error(errMsg) - throw new Error(errMsg) - } else if (returnEmptyObject) { - result[name] = {} as any - } - } - } catch (error) { - if (throwIfUnavailable) { - const errMsg = getAPIUsageErrorMessage(globalScope, name.toString(), usageDesc) - Logger.error(errMsg) - throw new Error(errMsg) - } - } - } - return result -} - -async function getGaiaErrorResponse(response: Response): Promise { - let responseMsg = '' - let responseJson: any | undefined - try { - responseMsg = await response.text() - try { - responseJson = JSON.parse(responseMsg) - } catch (error) { - // Use text instead - } - } catch (error) { - Logger.debug(`Error getting bad http response text: ${error}`) - } - const status = response.status - const statusText = response.statusText - const body = responseJson || responseMsg - return { status, statusText, body } -} - -/** - * Returns a BlockstackError correlating to the given HTTP response, - * with the provided errorMsg. Throws if the HTTP response is 'ok'. - */ -export async function getBlockstackErrorFromResponse( - response: Response, - errorMsg: string, - hubConfig: import('./storage/hub').GaiaHubConfig | null -): Promise { - if (response.ok) { - throw new Error('Cannot get a BlockstackError from a valid response.') - } - const gaiaResponse = await getGaiaErrorResponse(response) - if (gaiaResponse.status === 401) { - return new ValidationError(errorMsg, gaiaResponse) - } else if (gaiaResponse.status === 402) { - return new NotEnoughProofError(errorMsg, gaiaResponse) - } else if (gaiaResponse.status === 403) { - return new BadPathError(errorMsg, gaiaResponse) - } else if (gaiaResponse.status === 404) { - throw new DoesNotExist(errorMsg, gaiaResponse) - } else if (gaiaResponse.status === 409) { - return new ConflictError(errorMsg, gaiaResponse) - } else if (gaiaResponse.status === 412) { - return new PreconditionFailedError(errorMsg, gaiaResponse) - } else if (gaiaResponse.status === 413) { - const maxBytes = megabytesToBytes(hubConfig?.max_file_upload_size_megabytes) - return new PayloadTooLargeError(errorMsg, gaiaResponse, maxBytes) - } else { - return new Error(errorMsg) - } -} diff --git a/packages/auth/src/sessionData.ts b/packages/auth/src/sessionData.ts index d01b2771e..9a091d87f 100644 --- a/packages/auth/src/sessionData.ts +++ b/packages/auth/src/sessionData.ts @@ -1,5 +1,5 @@ -import { GaiaHubConfig } from './legacy/storage/hub' +// import { GaiaHubConfig } from './legacy/storage/hub' import { InvalidStateError } from '@stacks/common' import { UserData } from './auth' @@ -39,13 +39,13 @@ export class SessionData { this.etags = options.etags ? options.etags : {} } - getGaiaHubConfig(): GaiaHubConfig { - return this.userData && this.userData.gaiaHubConfig - } + // getGaiaHubConfig(): GaiaHubConfig { + // return this.userData && this.userData.gaiaHubConfig + // } - setGaiaHubConfig(config: GaiaHubConfig): void { - this.userData.gaiaHubConfig = config - } + // setGaiaHubConfig(config: GaiaHubConfig): void { + // this.userData.gaiaHubConfig = config + // } static fromJSON(json: any): SessionData { if (json.version !== SESSION_VERSION) {