@0xsequence/auth
Advanced tools
| 'use strict'; | ||
| Object.defineProperty(exports, '__esModule', { value: true }); | ||
| var ethers = require('ethers'); | ||
| var ethauth = require('@0xsequence/ethauth'); | ||
| var network = require('@0xsequence/network'); | ||
| var account = require('@0xsequence/account'); | ||
| var api = require('@0xsequence/api'); | ||
| var indexer = require('@0xsequence/indexer'); | ||
| var metadata = require('@0xsequence/metadata'); | ||
| var utils = require('@0xsequence/utils'); | ||
| var sessions = require('@0xsequence/sessions'); | ||
| var signhub = require('@0xsequence/signhub'); | ||
| var core = require('@0xsequence/core'); | ||
| // Default session expiration of ETHAuth token (1 week) | ||
| const DEFAULT_SESSION_EXPIRATION = 60 * 60 * 24 * 7; | ||
| const EXPIRATION_JWT_MARGIN = 60; // seconds | ||
| class Services { | ||
| constructor(account, settings, status = {}, projectAccessKey) { | ||
| this.account = account; | ||
| this.settings = settings; | ||
| this.status = status; | ||
| this._initialAuthRequest = void 0; | ||
| // proof strings are indexed by account address and app name, see getProofStringKey() | ||
| this.proofStrings = new Map(); | ||
| this.onAuthCallbacks = []; | ||
| this.apiClient = void 0; | ||
| this.metadataClient = void 0; | ||
| this.indexerClients = new Map(); | ||
| this.indexerGateway = void 0; | ||
| this.projectAccessKey = void 0; | ||
| this.projectAccessKey = projectAccessKey; | ||
| } | ||
| now() { | ||
| return Math.floor(Date.now() / 1000); | ||
| } | ||
| get expiration() { | ||
| var _this$settings$metada; | ||
| return Math.max((_this$settings$metada = this.settings.metadata.expiration) != null ? _this$settings$metada : DEFAULT_SESSION_EXPIRATION, 120); | ||
| } | ||
| onAuth(cb) { | ||
| this.onAuthCallbacks.push(cb); | ||
| return () => this.onAuthCallbacks = this.onAuthCallbacks.filter(c => c !== cb); | ||
| } | ||
| async dump() { | ||
| if (!this.status.jwt) return { | ||
| metadata: this.settings.metadata | ||
| }; | ||
| return { | ||
| jwt: { | ||
| token: await this.status.jwt.token, | ||
| expiration: this.status.jwt.expiration | ||
| }, | ||
| metadata: this.status.metadata | ||
| }; | ||
| } | ||
| auth(maxTries = 5) { | ||
| var _this = this; | ||
| if (this._initialAuthRequest) return this._initialAuthRequest; | ||
| this._initialAuthRequest = async function () { | ||
| const url = _this.settings.sequenceApiUrl; | ||
| if (!url) throw Error('No sequence api url'); | ||
| let jwtAuth; | ||
| for (let i = 1;; i++) { | ||
| try { | ||
| jwtAuth = (await _this.getJWT(true)).token; | ||
| break; | ||
| } catch (error) { | ||
| if (i === maxTries) { | ||
| console.error(`couldn't authenticate after ${maxTries} attempts`, error); | ||
| throw error; | ||
| } | ||
| } | ||
| } | ||
| return new api.SequenceAPIClient(url, undefined, jwtAuth); | ||
| }(); | ||
| return this._initialAuthRequest; | ||
| } | ||
| async getJWT(tryAuth) { | ||
| var _this2 = this; | ||
| const url = this.settings.sequenceApiUrl; | ||
| if (!url) throw Error('No sequence api url'); | ||
| // check if we already have or are waiting for a token | ||
| if (this.status.jwt) { | ||
| const _jwt = this.status.jwt; | ||
| const _token = await _jwt.token; | ||
| if (this.now() < _jwt.expiration) { | ||
| return { | ||
| token: _token, | ||
| expiration: _jwt.expiration | ||
| }; | ||
| } | ||
| // token expired, delete it and get a new one | ||
| this.status.jwt = undefined; | ||
| } | ||
| if (!tryAuth) { | ||
| throw new Error('no auth token in memory'); | ||
| } | ||
| const proofStringKey = this.getProofStringKey(); | ||
| const { | ||
| proofString, | ||
| expiration | ||
| } = this.getProofString(proofStringKey); | ||
| const jwt = { | ||
| token: proofString.then(async function (proofString) { | ||
| const api$1 = new api.SequenceAPIClient(url); | ||
| const authResp = await api$1.getAuthToken({ | ||
| ewtString: proofString | ||
| }); | ||
| if ((authResp == null ? void 0 : authResp.status) === true && authResp.jwtToken.length !== 0) { | ||
| return authResp.jwtToken; | ||
| } else { | ||
| if (!(await _this2.isProofStringValid(proofString))) { | ||
| _this2.proofStrings.delete(proofStringKey); | ||
| } | ||
| throw new Error('no auth token from server'); | ||
| } | ||
| }).catch(reason => { | ||
| this.status.jwt = undefined; | ||
| throw reason; | ||
| }), | ||
| expiration | ||
| }; | ||
| this.status.jwt = jwt; | ||
| jwt.token.then(token => { | ||
| this.onAuthCallbacks.forEach(cb => { | ||
| try { | ||
| cb({ | ||
| status: 'fulfilled', | ||
| value: token | ||
| }); | ||
| } catch (_unused) {} | ||
| }); | ||
| }).catch(reason => { | ||
| this.onAuthCallbacks.forEach(cb => { | ||
| try { | ||
| cb({ | ||
| status: 'rejected', | ||
| reason | ||
| }); | ||
| } catch (_unused2) {} | ||
| }); | ||
| }); | ||
| const token = await jwt.token; | ||
| return { | ||
| token, | ||
| expiration | ||
| }; | ||
| } | ||
| getProofStringKey() { | ||
| return `${this.account.address} - ${this.settings.metadata.name}`; | ||
| } | ||
| async isProofStringValid(proofString) { | ||
| try { | ||
| const ethAuth = new ethauth.ETHAuth(); | ||
| const chainId = BigInt(this.settings.sequenceApiChainId); | ||
| const found = network.findNetworkConfig(this.account.networks, chainId); | ||
| if (!found) { | ||
| throw Error('No network found'); | ||
| } | ||
| ethAuth.chainId = Number(chainId); | ||
| const network$1 = new ethers.ethers.Network(found.name, chainId); | ||
| // TODO: Modify ETHAuth so it can take a provider instead of a url | ||
| // ----- | ||
| // Can't pass jwt here since this is used for getting the jwt | ||
| ethAuth.provider = new ethers.ethers.JsonRpcProvider(utils.getFetchRequest(found.rpcUrl, this.projectAccessKey), network$1, { | ||
| staticNetwork: network$1 | ||
| }); | ||
| await ethAuth.decodeProof(proofString); | ||
| return true; | ||
| } catch (_unused3) { | ||
| return false; | ||
| } | ||
| } | ||
| async getAPIClient(tryAuth = true) { | ||
| if (!this.apiClient) { | ||
| const url = this.settings.sequenceApiUrl; | ||
| if (!url) throw Error('No sequence api url'); | ||
| const jwtAuth = (await this.getJWT(tryAuth)).token; | ||
| this.apiClient = new api.SequenceAPIClient(url, undefined, jwtAuth); | ||
| } | ||
| return this.apiClient; | ||
| } | ||
| async getMetadataClient(tryAuth = true) { | ||
| if (!this.metadataClient) { | ||
| const jwtAuth = (await this.getJWT(tryAuth)).token; | ||
| this.metadataClient = new metadata.SequenceMetadata(this.settings.sequenceMetadataUrl, undefined, jwtAuth); | ||
| } | ||
| return this.metadataClient; | ||
| } | ||
| async getIndexerClient(chainId, tryAuth = true) { | ||
| const network$1 = network.findNetworkConfig(this.account.networks, chainId); | ||
| if (!network$1) { | ||
| throw Error(`No network for chain ${chainId}`); | ||
| } | ||
| if (!this.indexerClients.has(network$1.chainId)) { | ||
| if (network$1.indexer) { | ||
| this.indexerClients.set(network$1.chainId, network$1.indexer); | ||
| } else if (network$1.indexerUrl) { | ||
| const jwtAuth = (await this.getJWT(tryAuth)).token; | ||
| this.indexerClients.set(network$1.chainId, new indexer.SequenceIndexer(network$1.indexerUrl, undefined, jwtAuth)); | ||
| } else { | ||
| throw Error(`No indexer url for chain ${chainId}`); | ||
| } | ||
| } | ||
| return this.indexerClients.get(network$1.chainId); | ||
| } | ||
| async getIndexerGateway(tryAuth = true) { | ||
| if (!this.indexerGateway) { | ||
| const jwtAuth = (await this.getJWT(tryAuth)).token; | ||
| this.indexerGateway = new indexer.SequenceIndexerGateway(this.settings.sequenceIndexerGatewayUrl, undefined, jwtAuth); | ||
| } | ||
| return this.indexerGateway; | ||
| } | ||
| getProofString(key) { | ||
| // check if we already have or are waiting for a proof string | ||
| if (this.proofStrings.has(key)) { | ||
| const _proofString = this.proofStrings.get(key); | ||
| if (this.now() < _proofString.expiration) { | ||
| return _proofString; | ||
| } | ||
| // proof string expired, delete it and make a new one | ||
| this.proofStrings.delete(key); | ||
| } | ||
| const proof = new ethauth.Proof({ | ||
| address: this.account.address | ||
| }); | ||
| proof.claims.app = this.settings.metadata.name; | ||
| if (typeof window === 'object') { | ||
| proof.claims.ogn = window.location.origin; | ||
| } | ||
| proof.setExpiryIn(this.expiration); | ||
| const ethAuth = new ethauth.ETHAuth(); | ||
| const chainId = BigInt(this.settings.sequenceApiChainId); | ||
| const found = network.findNetworkConfig(this.account.networks, chainId); | ||
| if (!found) { | ||
| throw Error('No network found'); | ||
| } | ||
| ethAuth.chainId = Number(chainId); | ||
| const network$1 = new ethers.ethers.Network(found.name, chainId); | ||
| // TODO: Modify ETHAuth so it can take a provider instead of a url | ||
| // ----- | ||
| // Can't pass jwt here since this is used for getting the jwt | ||
| ethAuth.provider = new ethers.ethers.JsonRpcProvider(utils.getFetchRequest(found.rpcUrl, this.projectAccessKey), network$1, { | ||
| staticNetwork: network$1 | ||
| }); | ||
| const expiration = this.now() + this.expiration - EXPIRATION_JWT_MARGIN; | ||
| const proofString = { | ||
| proofString: Promise.resolve( | ||
| // NOTICE: TODO: Here we ask the account to sign the message | ||
| // using whatever configuration we have ON-CHAIN, this means | ||
| // that the account will still use the v1 wallet, even if the migration | ||
| // was signed. | ||
| // | ||
| // This works for Sequence webapp v1 -> v2 because all v1 configurations share the same formula | ||
| // (torus + guard), but if we ever decide to allow cross-device login, then it will not work, because | ||
| // those other signers may not be part of the configuration. | ||
| // | ||
| this.account.signDigest(proof.messageDigest(), this.settings.sequenceApiChainId, true, 'eip6492')).then(s => { | ||
| proof.signature = s; | ||
| return ethAuth.encodeProof(proof, true); | ||
| }).catch(reason => { | ||
| this.proofStrings.delete(key); | ||
| throw reason; | ||
| }), | ||
| expiration | ||
| }; | ||
| this.proofStrings.set(key, proofString); | ||
| return proofString; | ||
| } | ||
| } | ||
| // signAuthorization will perform an EIP712 typed-data message signing of ETHAuth domain via the provided | ||
| // Signer and authorization options. | ||
| const signAuthorization = async (signer, chainId, options) => { | ||
| const address = ethers.ethers.getAddress(await signer.getAddress()); | ||
| if (!address || address === '' || address === '0x') { | ||
| throw ErrAccountIsRequired; | ||
| } | ||
| const proof = new ethauth.Proof(); | ||
| proof.address = address; | ||
| if (!options || !options.app || options.app === '') { | ||
| throw new AuthError('authorization options requires app to be set'); | ||
| } | ||
| proof.claims.app = options.app; | ||
| proof.claims.ogn = options.origin; | ||
| proof.claims.n = options.nonce; | ||
| proof.setExpiryIn(options.expiry ? Math.max(options.expiry, 200) : DEFAULT_SESSION_EXPIRATION); | ||
| const typedData = proof.messageTypedData(); | ||
| const chainIdNumber = network.toChainIdNumber(chainId); | ||
| proof.signature = await (signer instanceof account.Account ? | ||
| // Account can sign EIP-6492 signatures, so it doesn't require deploying the wallet | ||
| signer.signTypedData(typedData.domain, typedData.types, typedData.message, chainIdNumber, 'eip6492') : signer.signTypedData(typedData.domain, typedData.types, typedData.message, chainIdNumber)); | ||
| const ethAuth = new ethauth.ETHAuth(); | ||
| const proofString = await ethAuth.encodeProof(proof, true); | ||
| return { | ||
| typedData, | ||
| proofString | ||
| }; | ||
| }; | ||
| // TODO: review...... | ||
| class AuthError extends Error { | ||
| constructor(message) { | ||
| super(message); | ||
| this.name = 'AuthError'; | ||
| } | ||
| } | ||
| const ErrAccountIsRequired = new AuthError('auth error: account address is empty'); | ||
| function _extends() { | ||
| return _extends = Object.assign ? Object.assign.bind() : function (n) { | ||
| for (var e = 1; e < arguments.length; e++) { | ||
| var t = arguments[e]; | ||
| for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); | ||
| } | ||
| return n; | ||
| }, _extends.apply(null, arguments); | ||
| } | ||
| function isSessionDumpV1(obj) { | ||
| return obj.config && obj.metadata && obj.version === undefined; | ||
| } | ||
| function isSessionDumpV2(obj) { | ||
| return obj.version === 2 && obj.address; | ||
| } | ||
| // These chains are always validated for migrations | ||
| // if they are not available, the login will fail | ||
| const CRITICAL_CHAINS = [1, 137]; | ||
| const SessionSettingsDefault = { | ||
| contexts: core.commons.context.defaultContexts, | ||
| networks: network.allNetworks, | ||
| tracker: new sessions.trackers.remote.RemoteConfigTracker('https://sessions.sequence.app') | ||
| }; | ||
| class Session { | ||
| constructor(networks, contexts, account, services) { | ||
| this.networks = networks; | ||
| this.contexts = contexts; | ||
| this.account = account; | ||
| this.services = services; | ||
| } | ||
| async dump() { | ||
| const base = { | ||
| version: 2, | ||
| address: this.account.address | ||
| }; | ||
| if (this.services) { | ||
| return _extends({}, base, await this.services.dump()); | ||
| } | ||
| return base; | ||
| } | ||
| static async singleSigner(args) { | ||
| let { | ||
| signer | ||
| } = args; | ||
| if (typeof signer === 'string') { | ||
| signer = new ethers.ethers.Wallet(signer); | ||
| } | ||
| const orchestrator = new signhub.Orchestrator([signer]); | ||
| const referenceSigner = await signer.getAddress(); | ||
| const threshold = 1; | ||
| const addSigners = [{ | ||
| weight: 1, | ||
| address: referenceSigner | ||
| }]; | ||
| const selectWallet = args.selectWallet || async function (wallets) { | ||
| var _args$settings$tracke, _args$settings; | ||
| if (wallets.length === 0) return undefined; | ||
| // Find a wallet that was originally created | ||
| // as a 1/1 of the reference signer | ||
| const tracker = (_args$settings$tracke = (_args$settings = args.settings) == null ? void 0 : _args$settings.tracker) != null ? _args$settings$tracke : SessionSettingsDefault.tracker; | ||
| const configs = await Promise.all(wallets.map(async function (wallet) { | ||
| const imageHash = await tracker.imageHashOfCounterfactualWallet({ | ||
| wallet | ||
| }); | ||
| return { | ||
| wallet, | ||
| config: imageHash && (await tracker.configOfImageHash({ | ||
| imageHash: imageHash.imageHash | ||
| })) | ||
| }; | ||
| })); | ||
| for (const config of configs) { | ||
| if (!config.config) { | ||
| continue; | ||
| } | ||
| const coder = core.universal.genericCoderFor(config.config.version); | ||
| const signers = coder.config.signersOf(config.config); | ||
| if (signers.length === 1 && signers[0].address === referenceSigner) { | ||
| return config.wallet; | ||
| } | ||
| } | ||
| return undefined; | ||
| }; | ||
| return Session.open(_extends({}, args, { | ||
| orchestrator, | ||
| referenceSigner, | ||
| threshold, | ||
| addSigners, | ||
| selectWallet | ||
| })); | ||
| } | ||
| static async open(args) { | ||
| var _findNetworkConfig$ch, _findNetworkConfig, _settings$services$se, _settings$services, _networks$; | ||
| const { | ||
| referenceSigner, | ||
| threshold, | ||
| addSigners, | ||
| selectWallet, | ||
| onAccountAddress, | ||
| settings, | ||
| editConfigOnMigration, | ||
| onMigration, | ||
| orchestrator, | ||
| projectAccessKey | ||
| } = args; | ||
| const { | ||
| contexts, | ||
| networks, | ||
| tracker, | ||
| services | ||
| } = _extends({}, SessionSettingsDefault, settings); | ||
| // The reference network is mainnet, if mainnet is not available, we use the first network | ||
| const referenceChainId = (_findNetworkConfig$ch = (_findNetworkConfig = network.findNetworkConfig(networks, (_settings$services$se = settings == null || (_settings$services = settings.services) == null ? void 0 : _settings$services.sequenceApiChainId) != null ? _settings$services$se : network.ChainId.MAINNET)) == null ? void 0 : _findNetworkConfig.chainId) != null ? _findNetworkConfig$ch : (_networks$ = networks[0]) == null ? void 0 : _networks$.chainId; | ||
| if (!referenceChainId) throw Error('No reference chain found'); | ||
| const foundWallets = await tracker.walletsOfSigner({ | ||
| signer: referenceSigner | ||
| }); | ||
| const selectedWallet = await selectWallet(foundWallets.map(w => w.wallet)); | ||
| let account$1; | ||
| if (selectedWallet) { | ||
| onAccountAddress == null || onAccountAddress(selectedWallet); | ||
| // existing account, lets update it | ||
| account$1 = new account.Account({ | ||
| address: selectedWallet, | ||
| tracker, | ||
| networks, | ||
| contexts, | ||
| orchestrator, | ||
| projectAccessKey | ||
| }); | ||
| // Get the latest configuration of the wallet (on the reference chain) | ||
| // now this configuration should be of the latest version, so we can start | ||
| // manipulating it. | ||
| // NOTICE: We are performing the wallet update on a single chain, assuming that | ||
| // all other networks have the same configuration. This is not always true. | ||
| if (addSigners && addSigners.length > 0) { | ||
| // New wallets never need migrations | ||
| // (because we create them on the latest version) | ||
| let status = await account$1.status(referenceChainId); | ||
| // If the wallet was created originally on v2, then we can skip | ||
| // the migration checks all together. | ||
| if (status.original.version !== status.version || account$1.version !== status.version) { | ||
| // Account may not have been migrated yet, so we need to check | ||
| // if it has been migrated and if not, migrate it (in all chains) | ||
| const { | ||
| migratedAllChains: isFullyMigrated, | ||
| failedChains | ||
| } = await account$1.isMigratedAllChains(); | ||
| // Failed chains must not contain mainnet or polygon, otherwise we cannot proceed. | ||
| if (failedChains.some(c => CRITICAL_CHAINS.includes(c))) { | ||
| throw Error(`Failed to fetch account status on ${failedChains.join(', ')}`); | ||
| } | ||
| if (!isFullyMigrated) { | ||
| // This is an oportunity for whoever is opening the session to | ||
| // feed the orchestrator with more signers, so that the migration | ||
| // can be completed. | ||
| if (onMigration && !(await onMigration(account$1))) { | ||
| throw Error('Migration cancelled, cannot open session'); | ||
| } | ||
| const { | ||
| failedChains: _failedChains | ||
| } = await account$1.signAllMigrations(editConfigOnMigration || (c => c)); | ||
| if (_failedChains.some(c => CRITICAL_CHAINS.includes(c))) { | ||
| throw Error(`Failed to sign migrations on ${_failedChains.join(', ')}`); | ||
| } | ||
| // If we are using a dedupped tracker we need to invalidate the cache | ||
| // otherwise we run the risk of not seeing the signed migrations reflected. | ||
| if (sessions.trackers.isDedupedTracker(tracker)) { | ||
| tracker.invalidateCache(); | ||
| } | ||
| let isFullyMigrated2; | ||
| [isFullyMigrated2, status] = await Promise.all([account$1.isMigratedAllChains().then(r => r.migratedAllChains), account$1.status(referenceChainId)]); | ||
| if (!isFullyMigrated2) throw Error('Failed to migrate account'); | ||
| } | ||
| } | ||
| // NOTICE: We only need to do this because the API will not be able to | ||
| // validate the v2 signature (if the account has an onchain version of 1) | ||
| // we could speed this up by sending the migration alongside the jwt request | ||
| // and letting the API validate it offchain. | ||
| if (status.onChain.version !== status.version) { | ||
| await account$1.doBootstrap(referenceChainId, undefined, status); | ||
| } | ||
| const prevConfig = status.config; | ||
| const nextConfig = account$1.coders.config.editConfig(prevConfig, { | ||
| add: addSigners, | ||
| threshold | ||
| }); | ||
| // Only update the onchain config if the imageHash has changed | ||
| if (account$1.coders.config.imageHashOf(prevConfig) !== account$1.coders.config.imageHashOf(nextConfig)) { | ||
| const newConfig = account$1.coders.config.editConfig(nextConfig, { | ||
| checkpoint: account$1.coders.config.checkpointOf(prevConfig) + 1n | ||
| }); | ||
| await account$1.updateConfig(newConfig); | ||
| } | ||
| } | ||
| } else { | ||
| if (!addSigners || addSigners.length === 0) { | ||
| throw Error('Cannot create new account without signers'); | ||
| } | ||
| if (!threshold) { | ||
| throw Error('Cannot create new account without threshold'); | ||
| } | ||
| // fresh account | ||
| account$1 = await account.Account.new({ | ||
| config: { | ||
| threshold, | ||
| checkpoint: 0, | ||
| signers: addSigners | ||
| }, | ||
| tracker, | ||
| contexts, | ||
| orchestrator, | ||
| networks, | ||
| projectAccessKey | ||
| }); | ||
| onAccountAddress == null || onAccountAddress(account$1.address); | ||
| // sign a digest and send it to the tracker | ||
| // otherwise the tracker will not know about this account | ||
| await account$1.publishWitness(); | ||
| // safety check, the remove tracker should be able to find | ||
| // this account for the reference signer | ||
| const _foundWallets = await tracker.walletsOfSigner({ | ||
| signer: referenceSigner, | ||
| noCache: true | ||
| }); | ||
| if (!_foundWallets.some(w => w.wallet === account$1.address)) { | ||
| throw Error('Account not found on tracker'); | ||
| } | ||
| } | ||
| let servicesObj; | ||
| if (services) { | ||
| servicesObj = new Services(account$1, services); | ||
| servicesObj.auth(); // fire and forget | ||
| servicesObj.onAuth(result => { | ||
| if (result.status === 'fulfilled') { | ||
| account$1.setJwt(result.value); | ||
| } | ||
| }); | ||
| } | ||
| return new Session(networks, contexts, account$1, servicesObj); | ||
| } | ||
| static async load(args) { | ||
| const { | ||
| dump, | ||
| settings, | ||
| editConfigOnMigration, | ||
| onMigration, | ||
| orchestrator, | ||
| projectAccessKey | ||
| } = args; | ||
| const { | ||
| contexts, | ||
| networks, | ||
| tracker, | ||
| services | ||
| } = _extends({}, SessionSettingsDefault, settings); | ||
| let account$1; | ||
| if (isSessionDumpV1(dump)) { | ||
| var _dump$jwt$expiration, _dump$jwt, _dump$jwt2; | ||
| // Old configuration format used to also contain an "address" field | ||
| // but if it doesn't, it means that it was a "counterfactual" account | ||
| // not yet updated, so we need to compute the address | ||
| const oldAddress = dump.config.address || core.commons.context.addressOf(contexts[1], core.v1.config.ConfigCoder.imageHashOf(_extends({}, dump.config, { | ||
| version: 1 | ||
| }))); | ||
| const jwtExpired = ((_dump$jwt$expiration = (_dump$jwt = dump.jwt) == null ? void 0 : _dump$jwt.expiration) != null ? _dump$jwt$expiration : 0) < Math.floor(Date.now() / 1000); | ||
| account$1 = new account.Account({ | ||
| address: oldAddress, | ||
| tracker, | ||
| networks, | ||
| contexts, | ||
| orchestrator, | ||
| jwt: jwtExpired ? undefined : (_dump$jwt2 = dump.jwt) == null ? void 0 : _dump$jwt2.token, | ||
| projectAccessKey | ||
| }); | ||
| // TODO: This property may not hold if the user adds a new network | ||
| if (!(await account$1.isMigratedAllChains().then(r => r.migratedAllChains))) { | ||
| // This is an oportunity for whoever is opening the session to | ||
| // feed the orchestrator with more signers, so that the migration | ||
| // can be completed. | ||
| if (onMigration && !(await onMigration(account$1))) { | ||
| throw Error('Migration cancelled, cannot open session'); | ||
| } | ||
| console.log('Migrating account...'); | ||
| await account$1.signAllMigrations(editConfigOnMigration); | ||
| if (!(await account$1.isMigratedAllChains().then(r => r.migratedAllChains))) throw Error('Failed to migrate account'); | ||
| } | ||
| // We may need to update the JWT if the account has been migrated | ||
| } else if (isSessionDumpV2(dump)) { | ||
| var _dump$jwt$expiration2, _dump$jwt3, _dump$jwt4; | ||
| const jwtExpired = ((_dump$jwt$expiration2 = (_dump$jwt3 = dump.jwt) == null ? void 0 : _dump$jwt3.expiration) != null ? _dump$jwt$expiration2 : 0) < Math.floor(Date.now() / 1000); | ||
| account$1 = new account.Account({ | ||
| address: dump.address, | ||
| tracker, | ||
| networks, | ||
| contexts, | ||
| orchestrator, | ||
| jwt: jwtExpired ? undefined : (_dump$jwt4 = dump.jwt) == null ? void 0 : _dump$jwt4.token, | ||
| projectAccessKey | ||
| }); | ||
| } else { | ||
| throw Error('Invalid dump format'); | ||
| } | ||
| let servicesObj; | ||
| if (services) { | ||
| var _dump$jwt$expiration3; | ||
| servicesObj = new Services(account$1, services, dump.jwt && { | ||
| jwt: { | ||
| token: Promise.resolve(dump.jwt.token), | ||
| expiration: (_dump$jwt$expiration3 = dump.jwt.expiration) != null ? _dump$jwt$expiration3 : utils.jwtDecodeClaims(dump.jwt.token).exp | ||
| }, | ||
| metadata: dump.metadata | ||
| }); | ||
| } | ||
| return new Session(networks, contexts, account$1, servicesObj); | ||
| } | ||
| } | ||
| const ValidateSequenceWalletProof = (readerFor, tracker, context) => { | ||
| return async (_provider, chainId, proof) => { | ||
| const digest = proof.messageDigest(); | ||
| const isValid = await readerFor(chainId).isValidSignature(proof.address, digest, proof.signature); | ||
| return { | ||
| isValid | ||
| }; | ||
| }; | ||
| }; | ||
| exports.AuthError = AuthError; | ||
| exports.CRITICAL_CHAINS = CRITICAL_CHAINS; | ||
| exports.ErrAccountIsRequired = ErrAccountIsRequired; | ||
| exports.Session = Session; | ||
| exports.SessionSettingsDefault = SessionSettingsDefault; | ||
| exports.ValidateSequenceWalletProof = ValidateSequenceWalletProof; | ||
| exports.isSessionDumpV1 = isSessionDumpV1; | ||
| exports.isSessionDumpV2 = isSessionDumpV2; | ||
| exports.signAuthorization = signAuthorization; |
| 'use strict'; | ||
| Object.defineProperty(exports, '__esModule', { value: true }); | ||
| var ethers = require('ethers'); | ||
| var ethauth = require('@0xsequence/ethauth'); | ||
| var network = require('@0xsequence/network'); | ||
| var account = require('@0xsequence/account'); | ||
| var api = require('@0xsequence/api'); | ||
| var indexer = require('@0xsequence/indexer'); | ||
| var metadata = require('@0xsequence/metadata'); | ||
| var utils = require('@0xsequence/utils'); | ||
| var sessions = require('@0xsequence/sessions'); | ||
| var signhub = require('@0xsequence/signhub'); | ||
| var core = require('@0xsequence/core'); | ||
| // Default session expiration of ETHAuth token (1 week) | ||
| const DEFAULT_SESSION_EXPIRATION = 60 * 60 * 24 * 7; | ||
| const EXPIRATION_JWT_MARGIN = 60; // seconds | ||
| class Services { | ||
| constructor(account, settings, status = {}, projectAccessKey) { | ||
| this.account = account; | ||
| this.settings = settings; | ||
| this.status = status; | ||
| this._initialAuthRequest = void 0; | ||
| // proof strings are indexed by account address and app name, see getProofStringKey() | ||
| this.proofStrings = new Map(); | ||
| this.onAuthCallbacks = []; | ||
| this.apiClient = void 0; | ||
| this.metadataClient = void 0; | ||
| this.indexerClients = new Map(); | ||
| this.indexerGateway = void 0; | ||
| this.projectAccessKey = void 0; | ||
| this.projectAccessKey = projectAccessKey; | ||
| } | ||
| now() { | ||
| return Math.floor(Date.now() / 1000); | ||
| } | ||
| get expiration() { | ||
| var _this$settings$metada; | ||
| return Math.max((_this$settings$metada = this.settings.metadata.expiration) != null ? _this$settings$metada : DEFAULT_SESSION_EXPIRATION, 120); | ||
| } | ||
| onAuth(cb) { | ||
| this.onAuthCallbacks.push(cb); | ||
| return () => this.onAuthCallbacks = this.onAuthCallbacks.filter(c => c !== cb); | ||
| } | ||
| async dump() { | ||
| if (!this.status.jwt) return { | ||
| metadata: this.settings.metadata | ||
| }; | ||
| return { | ||
| jwt: { | ||
| token: await this.status.jwt.token, | ||
| expiration: this.status.jwt.expiration | ||
| }, | ||
| metadata: this.status.metadata | ||
| }; | ||
| } | ||
| auth(maxTries = 5) { | ||
| var _this = this; | ||
| if (this._initialAuthRequest) return this._initialAuthRequest; | ||
| this._initialAuthRequest = async function () { | ||
| const url = _this.settings.sequenceApiUrl; | ||
| if (!url) throw Error('No sequence api url'); | ||
| let jwtAuth; | ||
| for (let i = 1;; i++) { | ||
| try { | ||
| jwtAuth = (await _this.getJWT(true)).token; | ||
| break; | ||
| } catch (error) { | ||
| if (i === maxTries) { | ||
| console.error(`couldn't authenticate after ${maxTries} attempts`, error); | ||
| throw error; | ||
| } | ||
| } | ||
| } | ||
| return new api.SequenceAPIClient(url, undefined, jwtAuth); | ||
| }(); | ||
| return this._initialAuthRequest; | ||
| } | ||
| async getJWT(tryAuth) { | ||
| var _this2 = this; | ||
| const url = this.settings.sequenceApiUrl; | ||
| if (!url) throw Error('No sequence api url'); | ||
| // check if we already have or are waiting for a token | ||
| if (this.status.jwt) { | ||
| const _jwt = this.status.jwt; | ||
| const _token = await _jwt.token; | ||
| if (this.now() < _jwt.expiration) { | ||
| return { | ||
| token: _token, | ||
| expiration: _jwt.expiration | ||
| }; | ||
| } | ||
| // token expired, delete it and get a new one | ||
| this.status.jwt = undefined; | ||
| } | ||
| if (!tryAuth) { | ||
| throw new Error('no auth token in memory'); | ||
| } | ||
| const proofStringKey = this.getProofStringKey(); | ||
| const { | ||
| proofString, | ||
| expiration | ||
| } = this.getProofString(proofStringKey); | ||
| const jwt = { | ||
| token: proofString.then(async function (proofString) { | ||
| const api$1 = new api.SequenceAPIClient(url); | ||
| const authResp = await api$1.getAuthToken({ | ||
| ewtString: proofString | ||
| }); | ||
| if ((authResp == null ? void 0 : authResp.status) === true && authResp.jwtToken.length !== 0) { | ||
| return authResp.jwtToken; | ||
| } else { | ||
| if (!(await _this2.isProofStringValid(proofString))) { | ||
| _this2.proofStrings.delete(proofStringKey); | ||
| } | ||
| throw new Error('no auth token from server'); | ||
| } | ||
| }).catch(reason => { | ||
| this.status.jwt = undefined; | ||
| throw reason; | ||
| }), | ||
| expiration | ||
| }; | ||
| this.status.jwt = jwt; | ||
| jwt.token.then(token => { | ||
| this.onAuthCallbacks.forEach(cb => { | ||
| try { | ||
| cb({ | ||
| status: 'fulfilled', | ||
| value: token | ||
| }); | ||
| } catch (_unused) {} | ||
| }); | ||
| }).catch(reason => { | ||
| this.onAuthCallbacks.forEach(cb => { | ||
| try { | ||
| cb({ | ||
| status: 'rejected', | ||
| reason | ||
| }); | ||
| } catch (_unused2) {} | ||
| }); | ||
| }); | ||
| const token = await jwt.token; | ||
| return { | ||
| token, | ||
| expiration | ||
| }; | ||
| } | ||
| getProofStringKey() { | ||
| return `${this.account.address} - ${this.settings.metadata.name}`; | ||
| } | ||
| async isProofStringValid(proofString) { | ||
| try { | ||
| const ethAuth = new ethauth.ETHAuth(); | ||
| const chainId = BigInt(this.settings.sequenceApiChainId); | ||
| const found = network.findNetworkConfig(this.account.networks, chainId); | ||
| if (!found) { | ||
| throw Error('No network found'); | ||
| } | ||
| ethAuth.chainId = Number(chainId); | ||
| const network$1 = new ethers.ethers.Network(found.name, chainId); | ||
| // TODO: Modify ETHAuth so it can take a provider instead of a url | ||
| // ----- | ||
| // Can't pass jwt here since this is used for getting the jwt | ||
| ethAuth.provider = new ethers.ethers.JsonRpcProvider(utils.getFetchRequest(found.rpcUrl, this.projectAccessKey), network$1, { | ||
| staticNetwork: network$1 | ||
| }); | ||
| await ethAuth.decodeProof(proofString); | ||
| return true; | ||
| } catch (_unused3) { | ||
| return false; | ||
| } | ||
| } | ||
| async getAPIClient(tryAuth = true) { | ||
| if (!this.apiClient) { | ||
| const url = this.settings.sequenceApiUrl; | ||
| if (!url) throw Error('No sequence api url'); | ||
| const jwtAuth = (await this.getJWT(tryAuth)).token; | ||
| this.apiClient = new api.SequenceAPIClient(url, undefined, jwtAuth); | ||
| } | ||
| return this.apiClient; | ||
| } | ||
| async getMetadataClient(tryAuth = true) { | ||
| if (!this.metadataClient) { | ||
| const jwtAuth = (await this.getJWT(tryAuth)).token; | ||
| this.metadataClient = new metadata.SequenceMetadata(this.settings.sequenceMetadataUrl, undefined, jwtAuth); | ||
| } | ||
| return this.metadataClient; | ||
| } | ||
| async getIndexerClient(chainId, tryAuth = true) { | ||
| const network$1 = network.findNetworkConfig(this.account.networks, chainId); | ||
| if (!network$1) { | ||
| throw Error(`No network for chain ${chainId}`); | ||
| } | ||
| if (!this.indexerClients.has(network$1.chainId)) { | ||
| if (network$1.indexer) { | ||
| this.indexerClients.set(network$1.chainId, network$1.indexer); | ||
| } else if (network$1.indexerUrl) { | ||
| const jwtAuth = (await this.getJWT(tryAuth)).token; | ||
| this.indexerClients.set(network$1.chainId, new indexer.SequenceIndexer(network$1.indexerUrl, undefined, jwtAuth)); | ||
| } else { | ||
| throw Error(`No indexer url for chain ${chainId}`); | ||
| } | ||
| } | ||
| return this.indexerClients.get(network$1.chainId); | ||
| } | ||
| async getIndexerGateway(tryAuth = true) { | ||
| if (!this.indexerGateway) { | ||
| const jwtAuth = (await this.getJWT(tryAuth)).token; | ||
| this.indexerGateway = new indexer.SequenceIndexerGateway(this.settings.sequenceIndexerGatewayUrl, undefined, jwtAuth); | ||
| } | ||
| return this.indexerGateway; | ||
| } | ||
| getProofString(key) { | ||
| // check if we already have or are waiting for a proof string | ||
| if (this.proofStrings.has(key)) { | ||
| const _proofString = this.proofStrings.get(key); | ||
| if (this.now() < _proofString.expiration) { | ||
| return _proofString; | ||
| } | ||
| // proof string expired, delete it and make a new one | ||
| this.proofStrings.delete(key); | ||
| } | ||
| const proof = new ethauth.Proof({ | ||
| address: this.account.address | ||
| }); | ||
| proof.claims.app = this.settings.metadata.name; | ||
| if (typeof window === 'object') { | ||
| proof.claims.ogn = window.location.origin; | ||
| } | ||
| proof.setExpiryIn(this.expiration); | ||
| const ethAuth = new ethauth.ETHAuth(); | ||
| const chainId = BigInt(this.settings.sequenceApiChainId); | ||
| const found = network.findNetworkConfig(this.account.networks, chainId); | ||
| if (!found) { | ||
| throw Error('No network found'); | ||
| } | ||
| ethAuth.chainId = Number(chainId); | ||
| const network$1 = new ethers.ethers.Network(found.name, chainId); | ||
| // TODO: Modify ETHAuth so it can take a provider instead of a url | ||
| // ----- | ||
| // Can't pass jwt here since this is used for getting the jwt | ||
| ethAuth.provider = new ethers.ethers.JsonRpcProvider(utils.getFetchRequest(found.rpcUrl, this.projectAccessKey), network$1, { | ||
| staticNetwork: network$1 | ||
| }); | ||
| const expiration = this.now() + this.expiration - EXPIRATION_JWT_MARGIN; | ||
| const proofString = { | ||
| proofString: Promise.resolve( | ||
| // NOTICE: TODO: Here we ask the account to sign the message | ||
| // using whatever configuration we have ON-CHAIN, this means | ||
| // that the account will still use the v1 wallet, even if the migration | ||
| // was signed. | ||
| // | ||
| // This works for Sequence webapp v1 -> v2 because all v1 configurations share the same formula | ||
| // (torus + guard), but if we ever decide to allow cross-device login, then it will not work, because | ||
| // those other signers may not be part of the configuration. | ||
| // | ||
| this.account.signDigest(proof.messageDigest(), this.settings.sequenceApiChainId, true, 'eip6492')).then(s => { | ||
| proof.signature = s; | ||
| return ethAuth.encodeProof(proof, true); | ||
| }).catch(reason => { | ||
| this.proofStrings.delete(key); | ||
| throw reason; | ||
| }), | ||
| expiration | ||
| }; | ||
| this.proofStrings.set(key, proofString); | ||
| return proofString; | ||
| } | ||
| } | ||
| // signAuthorization will perform an EIP712 typed-data message signing of ETHAuth domain via the provided | ||
| // Signer and authorization options. | ||
| const signAuthorization = async (signer, chainId, options) => { | ||
| const address = ethers.ethers.getAddress(await signer.getAddress()); | ||
| if (!address || address === '' || address === '0x') { | ||
| throw ErrAccountIsRequired; | ||
| } | ||
| const proof = new ethauth.Proof(); | ||
| proof.address = address; | ||
| if (!options || !options.app || options.app === '') { | ||
| throw new AuthError('authorization options requires app to be set'); | ||
| } | ||
| proof.claims.app = options.app; | ||
| proof.claims.ogn = options.origin; | ||
| proof.claims.n = options.nonce; | ||
| proof.setExpiryIn(options.expiry ? Math.max(options.expiry, 200) : DEFAULT_SESSION_EXPIRATION); | ||
| const typedData = proof.messageTypedData(); | ||
| const chainIdNumber = network.toChainIdNumber(chainId); | ||
| proof.signature = await (signer instanceof account.Account ? | ||
| // Account can sign EIP-6492 signatures, so it doesn't require deploying the wallet | ||
| signer.signTypedData(typedData.domain, typedData.types, typedData.message, chainIdNumber, 'eip6492') : signer.signTypedData(typedData.domain, typedData.types, typedData.message, chainIdNumber)); | ||
| const ethAuth = new ethauth.ETHAuth(); | ||
| const proofString = await ethAuth.encodeProof(proof, true); | ||
| return { | ||
| typedData, | ||
| proofString | ||
| }; | ||
| }; | ||
| // TODO: review...... | ||
| class AuthError extends Error { | ||
| constructor(message) { | ||
| super(message); | ||
| this.name = 'AuthError'; | ||
| } | ||
| } | ||
| const ErrAccountIsRequired = new AuthError('auth error: account address is empty'); | ||
| function _extends() { | ||
| return _extends = Object.assign ? Object.assign.bind() : function (n) { | ||
| for (var e = 1; e < arguments.length; e++) { | ||
| var t = arguments[e]; | ||
| for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); | ||
| } | ||
| return n; | ||
| }, _extends.apply(null, arguments); | ||
| } | ||
| function isSessionDumpV1(obj) { | ||
| return obj.config && obj.metadata && obj.version === undefined; | ||
| } | ||
| function isSessionDumpV2(obj) { | ||
| return obj.version === 2 && obj.address; | ||
| } | ||
| // These chains are always validated for migrations | ||
| // if they are not available, the login will fail | ||
| const CRITICAL_CHAINS = [1, 137]; | ||
| const SessionSettingsDefault = { | ||
| contexts: core.commons.context.defaultContexts, | ||
| networks: network.allNetworks, | ||
| tracker: new sessions.trackers.remote.RemoteConfigTracker('https://sessions.sequence.app') | ||
| }; | ||
| class Session { | ||
| constructor(networks, contexts, account, services) { | ||
| this.networks = networks; | ||
| this.contexts = contexts; | ||
| this.account = account; | ||
| this.services = services; | ||
| } | ||
| async dump() { | ||
| const base = { | ||
| version: 2, | ||
| address: this.account.address | ||
| }; | ||
| if (this.services) { | ||
| return _extends({}, base, await this.services.dump()); | ||
| } | ||
| return base; | ||
| } | ||
| static async singleSigner(args) { | ||
| let { | ||
| signer | ||
| } = args; | ||
| if (typeof signer === 'string') { | ||
| signer = new ethers.ethers.Wallet(signer); | ||
| } | ||
| const orchestrator = new signhub.Orchestrator([signer]); | ||
| const referenceSigner = await signer.getAddress(); | ||
| const threshold = 1; | ||
| const addSigners = [{ | ||
| weight: 1, | ||
| address: referenceSigner | ||
| }]; | ||
| const selectWallet = args.selectWallet || async function (wallets) { | ||
| var _args$settings$tracke, _args$settings; | ||
| if (wallets.length === 0) return undefined; | ||
| // Find a wallet that was originally created | ||
| // as a 1/1 of the reference signer | ||
| const tracker = (_args$settings$tracke = (_args$settings = args.settings) == null ? void 0 : _args$settings.tracker) != null ? _args$settings$tracke : SessionSettingsDefault.tracker; | ||
| const configs = await Promise.all(wallets.map(async function (wallet) { | ||
| const imageHash = await tracker.imageHashOfCounterfactualWallet({ | ||
| wallet | ||
| }); | ||
| return { | ||
| wallet, | ||
| config: imageHash && (await tracker.configOfImageHash({ | ||
| imageHash: imageHash.imageHash | ||
| })) | ||
| }; | ||
| })); | ||
| for (const config of configs) { | ||
| if (!config.config) { | ||
| continue; | ||
| } | ||
| const coder = core.universal.genericCoderFor(config.config.version); | ||
| const signers = coder.config.signersOf(config.config); | ||
| if (signers.length === 1 && signers[0].address === referenceSigner) { | ||
| return config.wallet; | ||
| } | ||
| } | ||
| return undefined; | ||
| }; | ||
| return Session.open(_extends({}, args, { | ||
| orchestrator, | ||
| referenceSigner, | ||
| threshold, | ||
| addSigners, | ||
| selectWallet | ||
| })); | ||
| } | ||
| static async open(args) { | ||
| var _findNetworkConfig$ch, _findNetworkConfig, _settings$services$se, _settings$services, _networks$; | ||
| const { | ||
| referenceSigner, | ||
| threshold, | ||
| addSigners, | ||
| selectWallet, | ||
| onAccountAddress, | ||
| settings, | ||
| editConfigOnMigration, | ||
| onMigration, | ||
| orchestrator, | ||
| projectAccessKey | ||
| } = args; | ||
| const { | ||
| contexts, | ||
| networks, | ||
| tracker, | ||
| services | ||
| } = _extends({}, SessionSettingsDefault, settings); | ||
| // The reference network is mainnet, if mainnet is not available, we use the first network | ||
| const referenceChainId = (_findNetworkConfig$ch = (_findNetworkConfig = network.findNetworkConfig(networks, (_settings$services$se = settings == null || (_settings$services = settings.services) == null ? void 0 : _settings$services.sequenceApiChainId) != null ? _settings$services$se : network.ChainId.MAINNET)) == null ? void 0 : _findNetworkConfig.chainId) != null ? _findNetworkConfig$ch : (_networks$ = networks[0]) == null ? void 0 : _networks$.chainId; | ||
| if (!referenceChainId) throw Error('No reference chain found'); | ||
| const foundWallets = await tracker.walletsOfSigner({ | ||
| signer: referenceSigner | ||
| }); | ||
| const selectedWallet = await selectWallet(foundWallets.map(w => w.wallet)); | ||
| let account$1; | ||
| if (selectedWallet) { | ||
| onAccountAddress == null || onAccountAddress(selectedWallet); | ||
| // existing account, lets update it | ||
| account$1 = new account.Account({ | ||
| address: selectedWallet, | ||
| tracker, | ||
| networks, | ||
| contexts, | ||
| orchestrator, | ||
| projectAccessKey | ||
| }); | ||
| // Get the latest configuration of the wallet (on the reference chain) | ||
| // now this configuration should be of the latest version, so we can start | ||
| // manipulating it. | ||
| // NOTICE: We are performing the wallet update on a single chain, assuming that | ||
| // all other networks have the same configuration. This is not always true. | ||
| if (addSigners && addSigners.length > 0) { | ||
| // New wallets never need migrations | ||
| // (because we create them on the latest version) | ||
| let status = await account$1.status(referenceChainId); | ||
| // If the wallet was created originally on v2, then we can skip | ||
| // the migration checks all together. | ||
| if (status.original.version !== status.version || account$1.version !== status.version) { | ||
| // Account may not have been migrated yet, so we need to check | ||
| // if it has been migrated and if not, migrate it (in all chains) | ||
| const { | ||
| migratedAllChains: isFullyMigrated, | ||
| failedChains | ||
| } = await account$1.isMigratedAllChains(); | ||
| // Failed chains must not contain mainnet or polygon, otherwise we cannot proceed. | ||
| if (failedChains.some(c => CRITICAL_CHAINS.includes(c))) { | ||
| throw Error(`Failed to fetch account status on ${failedChains.join(', ')}`); | ||
| } | ||
| if (!isFullyMigrated) { | ||
| // This is an oportunity for whoever is opening the session to | ||
| // feed the orchestrator with more signers, so that the migration | ||
| // can be completed. | ||
| if (onMigration && !(await onMigration(account$1))) { | ||
| throw Error('Migration cancelled, cannot open session'); | ||
| } | ||
| const { | ||
| failedChains: _failedChains | ||
| } = await account$1.signAllMigrations(editConfigOnMigration || (c => c)); | ||
| if (_failedChains.some(c => CRITICAL_CHAINS.includes(c))) { | ||
| throw Error(`Failed to sign migrations on ${_failedChains.join(', ')}`); | ||
| } | ||
| // If we are using a dedupped tracker we need to invalidate the cache | ||
| // otherwise we run the risk of not seeing the signed migrations reflected. | ||
| if (sessions.trackers.isDedupedTracker(tracker)) { | ||
| tracker.invalidateCache(); | ||
| } | ||
| let isFullyMigrated2; | ||
| [isFullyMigrated2, status] = await Promise.all([account$1.isMigratedAllChains().then(r => r.migratedAllChains), account$1.status(referenceChainId)]); | ||
| if (!isFullyMigrated2) throw Error('Failed to migrate account'); | ||
| } | ||
| } | ||
| // NOTICE: We only need to do this because the API will not be able to | ||
| // validate the v2 signature (if the account has an onchain version of 1) | ||
| // we could speed this up by sending the migration alongside the jwt request | ||
| // and letting the API validate it offchain. | ||
| if (status.onChain.version !== status.version) { | ||
| await account$1.doBootstrap(referenceChainId, undefined, status); | ||
| } | ||
| const prevConfig = status.config; | ||
| const nextConfig = account$1.coders.config.editConfig(prevConfig, { | ||
| add: addSigners, | ||
| threshold | ||
| }); | ||
| // Only update the onchain config if the imageHash has changed | ||
| if (account$1.coders.config.imageHashOf(prevConfig) !== account$1.coders.config.imageHashOf(nextConfig)) { | ||
| const newConfig = account$1.coders.config.editConfig(nextConfig, { | ||
| checkpoint: account$1.coders.config.checkpointOf(prevConfig) + 1n | ||
| }); | ||
| await account$1.updateConfig(newConfig); | ||
| } | ||
| } | ||
| } else { | ||
| if (!addSigners || addSigners.length === 0) { | ||
| throw Error('Cannot create new account without signers'); | ||
| } | ||
| if (!threshold) { | ||
| throw Error('Cannot create new account without threshold'); | ||
| } | ||
| // fresh account | ||
| account$1 = await account.Account.new({ | ||
| config: { | ||
| threshold, | ||
| checkpoint: 0, | ||
| signers: addSigners | ||
| }, | ||
| tracker, | ||
| contexts, | ||
| orchestrator, | ||
| networks, | ||
| projectAccessKey | ||
| }); | ||
| onAccountAddress == null || onAccountAddress(account$1.address); | ||
| // sign a digest and send it to the tracker | ||
| // otherwise the tracker will not know about this account | ||
| await account$1.publishWitness(); | ||
| // safety check, the remove tracker should be able to find | ||
| // this account for the reference signer | ||
| const _foundWallets = await tracker.walletsOfSigner({ | ||
| signer: referenceSigner, | ||
| noCache: true | ||
| }); | ||
| if (!_foundWallets.some(w => w.wallet === account$1.address)) { | ||
| throw Error('Account not found on tracker'); | ||
| } | ||
| } | ||
| let servicesObj; | ||
| if (services) { | ||
| servicesObj = new Services(account$1, services); | ||
| servicesObj.auth(); // fire and forget | ||
| servicesObj.onAuth(result => { | ||
| if (result.status === 'fulfilled') { | ||
| account$1.setJwt(result.value); | ||
| } | ||
| }); | ||
| } | ||
| return new Session(networks, contexts, account$1, servicesObj); | ||
| } | ||
| static async load(args) { | ||
| const { | ||
| dump, | ||
| settings, | ||
| editConfigOnMigration, | ||
| onMigration, | ||
| orchestrator, | ||
| projectAccessKey | ||
| } = args; | ||
| const { | ||
| contexts, | ||
| networks, | ||
| tracker, | ||
| services | ||
| } = _extends({}, SessionSettingsDefault, settings); | ||
| let account$1; | ||
| if (isSessionDumpV1(dump)) { | ||
| var _dump$jwt$expiration, _dump$jwt, _dump$jwt2; | ||
| // Old configuration format used to also contain an "address" field | ||
| // but if it doesn't, it means that it was a "counterfactual" account | ||
| // not yet updated, so we need to compute the address | ||
| const oldAddress = dump.config.address || core.commons.context.addressOf(contexts[1], core.v1.config.ConfigCoder.imageHashOf(_extends({}, dump.config, { | ||
| version: 1 | ||
| }))); | ||
| const jwtExpired = ((_dump$jwt$expiration = (_dump$jwt = dump.jwt) == null ? void 0 : _dump$jwt.expiration) != null ? _dump$jwt$expiration : 0) < Math.floor(Date.now() / 1000); | ||
| account$1 = new account.Account({ | ||
| address: oldAddress, | ||
| tracker, | ||
| networks, | ||
| contexts, | ||
| orchestrator, | ||
| jwt: jwtExpired ? undefined : (_dump$jwt2 = dump.jwt) == null ? void 0 : _dump$jwt2.token, | ||
| projectAccessKey | ||
| }); | ||
| // TODO: This property may not hold if the user adds a new network | ||
| if (!(await account$1.isMigratedAllChains().then(r => r.migratedAllChains))) { | ||
| // This is an oportunity for whoever is opening the session to | ||
| // feed the orchestrator with more signers, so that the migration | ||
| // can be completed. | ||
| if (onMigration && !(await onMigration(account$1))) { | ||
| throw Error('Migration cancelled, cannot open session'); | ||
| } | ||
| console.log('Migrating account...'); | ||
| await account$1.signAllMigrations(editConfigOnMigration); | ||
| if (!(await account$1.isMigratedAllChains().then(r => r.migratedAllChains))) throw Error('Failed to migrate account'); | ||
| } | ||
| // We may need to update the JWT if the account has been migrated | ||
| } else if (isSessionDumpV2(dump)) { | ||
| var _dump$jwt$expiration2, _dump$jwt3, _dump$jwt4; | ||
| const jwtExpired = ((_dump$jwt$expiration2 = (_dump$jwt3 = dump.jwt) == null ? void 0 : _dump$jwt3.expiration) != null ? _dump$jwt$expiration2 : 0) < Math.floor(Date.now() / 1000); | ||
| account$1 = new account.Account({ | ||
| address: dump.address, | ||
| tracker, | ||
| networks, | ||
| contexts, | ||
| orchestrator, | ||
| jwt: jwtExpired ? undefined : (_dump$jwt4 = dump.jwt) == null ? void 0 : _dump$jwt4.token, | ||
| projectAccessKey | ||
| }); | ||
| } else { | ||
| throw Error('Invalid dump format'); | ||
| } | ||
| let servicesObj; | ||
| if (services) { | ||
| var _dump$jwt$expiration3; | ||
| servicesObj = new Services(account$1, services, dump.jwt && { | ||
| jwt: { | ||
| token: Promise.resolve(dump.jwt.token), | ||
| expiration: (_dump$jwt$expiration3 = dump.jwt.expiration) != null ? _dump$jwt$expiration3 : utils.jwtDecodeClaims(dump.jwt.token).exp | ||
| }, | ||
| metadata: dump.metadata | ||
| }); | ||
| } | ||
| return new Session(networks, contexts, account$1, servicesObj); | ||
| } | ||
| } | ||
| const ValidateSequenceWalletProof = (readerFor, tracker, context) => { | ||
| return async (_provider, chainId, proof) => { | ||
| const digest = proof.messageDigest(); | ||
| const isValid = await readerFor(chainId).isValidSignature(proof.address, digest, proof.signature); | ||
| return { | ||
| isValid | ||
| }; | ||
| }; | ||
| }; | ||
| exports.AuthError = AuthError; | ||
| exports.CRITICAL_CHAINS = CRITICAL_CHAINS; | ||
| exports.ErrAccountIsRequired = ErrAccountIsRequired; | ||
| exports.Session = Session; | ||
| exports.SessionSettingsDefault = SessionSettingsDefault; | ||
| exports.ValidateSequenceWalletProof = ValidateSequenceWalletProof; | ||
| exports.isSessionDumpV1 = isSessionDumpV1; | ||
| exports.isSessionDumpV2 = isSessionDumpV2; | ||
| exports.signAuthorization = signAuthorization; |
| import { ethers } from 'ethers'; | ||
| import { ETHAuth, Proof } from '@0xsequence/ethauth'; | ||
| import { findNetworkConfig, toChainIdNumber, allNetworks, ChainId } from '@0xsequence/network'; | ||
| import { Account } from '@0xsequence/account'; | ||
| import { SequenceAPIClient } from '@0xsequence/api'; | ||
| import { SequenceIndexer, SequenceIndexerGateway } from '@0xsequence/indexer'; | ||
| import { SequenceMetadata } from '@0xsequence/metadata'; | ||
| import { getFetchRequest, jwtDecodeClaims } from '@0xsequence/utils'; | ||
| import { trackers } from '@0xsequence/sessions'; | ||
| import { Orchestrator } from '@0xsequence/signhub'; | ||
| import { commons, universal, v1 } from '@0xsequence/core'; | ||
| // Default session expiration of ETHAuth token (1 week) | ||
| const DEFAULT_SESSION_EXPIRATION = 60 * 60 * 24 * 7; | ||
| const EXPIRATION_JWT_MARGIN = 60; // seconds | ||
| class Services { | ||
| constructor(account, settings, status = {}, projectAccessKey) { | ||
| this.account = account; | ||
| this.settings = settings; | ||
| this.status = status; | ||
| this._initialAuthRequest = void 0; | ||
| // proof strings are indexed by account address and app name, see getProofStringKey() | ||
| this.proofStrings = new Map(); | ||
| this.onAuthCallbacks = []; | ||
| this.apiClient = void 0; | ||
| this.metadataClient = void 0; | ||
| this.indexerClients = new Map(); | ||
| this.indexerGateway = void 0; | ||
| this.projectAccessKey = void 0; | ||
| this.projectAccessKey = projectAccessKey; | ||
| } | ||
| now() { | ||
| return Math.floor(Date.now() / 1000); | ||
| } | ||
| get expiration() { | ||
| var _this$settings$metada; | ||
| return Math.max((_this$settings$metada = this.settings.metadata.expiration) != null ? _this$settings$metada : DEFAULT_SESSION_EXPIRATION, 120); | ||
| } | ||
| onAuth(cb) { | ||
| this.onAuthCallbacks.push(cb); | ||
| return () => this.onAuthCallbacks = this.onAuthCallbacks.filter(c => c !== cb); | ||
| } | ||
| async dump() { | ||
| if (!this.status.jwt) return { | ||
| metadata: this.settings.metadata | ||
| }; | ||
| return { | ||
| jwt: { | ||
| token: await this.status.jwt.token, | ||
| expiration: this.status.jwt.expiration | ||
| }, | ||
| metadata: this.status.metadata | ||
| }; | ||
| } | ||
| auth(maxTries = 5) { | ||
| var _this = this; | ||
| if (this._initialAuthRequest) return this._initialAuthRequest; | ||
| this._initialAuthRequest = async function () { | ||
| const url = _this.settings.sequenceApiUrl; | ||
| if (!url) throw Error('No sequence api url'); | ||
| let jwtAuth; | ||
| for (let i = 1;; i++) { | ||
| try { | ||
| jwtAuth = (await _this.getJWT(true)).token; | ||
| break; | ||
| } catch (error) { | ||
| if (i === maxTries) { | ||
| console.error(`couldn't authenticate after ${maxTries} attempts`, error); | ||
| throw error; | ||
| } | ||
| } | ||
| } | ||
| return new SequenceAPIClient(url, undefined, jwtAuth); | ||
| }(); | ||
| return this._initialAuthRequest; | ||
| } | ||
| async getJWT(tryAuth) { | ||
| var _this2 = this; | ||
| const url = this.settings.sequenceApiUrl; | ||
| if (!url) throw Error('No sequence api url'); | ||
| // check if we already have or are waiting for a token | ||
| if (this.status.jwt) { | ||
| const _jwt = this.status.jwt; | ||
| const _token = await _jwt.token; | ||
| if (this.now() < _jwt.expiration) { | ||
| return { | ||
| token: _token, | ||
| expiration: _jwt.expiration | ||
| }; | ||
| } | ||
| // token expired, delete it and get a new one | ||
| this.status.jwt = undefined; | ||
| } | ||
| if (!tryAuth) { | ||
| throw new Error('no auth token in memory'); | ||
| } | ||
| const proofStringKey = this.getProofStringKey(); | ||
| const { | ||
| proofString, | ||
| expiration | ||
| } = this.getProofString(proofStringKey); | ||
| const jwt = { | ||
| token: proofString.then(async function (proofString) { | ||
| const api = new SequenceAPIClient(url); | ||
| const authResp = await api.getAuthToken({ | ||
| ewtString: proofString | ||
| }); | ||
| if ((authResp == null ? void 0 : authResp.status) === true && authResp.jwtToken.length !== 0) { | ||
| return authResp.jwtToken; | ||
| } else { | ||
| if (!(await _this2.isProofStringValid(proofString))) { | ||
| _this2.proofStrings.delete(proofStringKey); | ||
| } | ||
| throw new Error('no auth token from server'); | ||
| } | ||
| }).catch(reason => { | ||
| this.status.jwt = undefined; | ||
| throw reason; | ||
| }), | ||
| expiration | ||
| }; | ||
| this.status.jwt = jwt; | ||
| jwt.token.then(token => { | ||
| this.onAuthCallbacks.forEach(cb => { | ||
| try { | ||
| cb({ | ||
| status: 'fulfilled', | ||
| value: token | ||
| }); | ||
| } catch (_unused) {} | ||
| }); | ||
| }).catch(reason => { | ||
| this.onAuthCallbacks.forEach(cb => { | ||
| try { | ||
| cb({ | ||
| status: 'rejected', | ||
| reason | ||
| }); | ||
| } catch (_unused2) {} | ||
| }); | ||
| }); | ||
| const token = await jwt.token; | ||
| return { | ||
| token, | ||
| expiration | ||
| }; | ||
| } | ||
| getProofStringKey() { | ||
| return `${this.account.address} - ${this.settings.metadata.name}`; | ||
| } | ||
| async isProofStringValid(proofString) { | ||
| try { | ||
| const ethAuth = new ETHAuth(); | ||
| const chainId = BigInt(this.settings.sequenceApiChainId); | ||
| const found = findNetworkConfig(this.account.networks, chainId); | ||
| if (!found) { | ||
| throw Error('No network found'); | ||
| } | ||
| ethAuth.chainId = Number(chainId); | ||
| const network = new ethers.Network(found.name, chainId); | ||
| // TODO: Modify ETHAuth so it can take a provider instead of a url | ||
| // ----- | ||
| // Can't pass jwt here since this is used for getting the jwt | ||
| ethAuth.provider = new ethers.JsonRpcProvider(getFetchRequest(found.rpcUrl, this.projectAccessKey), network, { | ||
| staticNetwork: network | ||
| }); | ||
| await ethAuth.decodeProof(proofString); | ||
| return true; | ||
| } catch (_unused3) { | ||
| return false; | ||
| } | ||
| } | ||
| async getAPIClient(tryAuth = true) { | ||
| if (!this.apiClient) { | ||
| const url = this.settings.sequenceApiUrl; | ||
| if (!url) throw Error('No sequence api url'); | ||
| const jwtAuth = (await this.getJWT(tryAuth)).token; | ||
| this.apiClient = new SequenceAPIClient(url, undefined, jwtAuth); | ||
| } | ||
| return this.apiClient; | ||
| } | ||
| async getMetadataClient(tryAuth = true) { | ||
| if (!this.metadataClient) { | ||
| const jwtAuth = (await this.getJWT(tryAuth)).token; | ||
| this.metadataClient = new SequenceMetadata(this.settings.sequenceMetadataUrl, undefined, jwtAuth); | ||
| } | ||
| return this.metadataClient; | ||
| } | ||
| async getIndexerClient(chainId, tryAuth = true) { | ||
| const network = findNetworkConfig(this.account.networks, chainId); | ||
| if (!network) { | ||
| throw Error(`No network for chain ${chainId}`); | ||
| } | ||
| if (!this.indexerClients.has(network.chainId)) { | ||
| if (network.indexer) { | ||
| this.indexerClients.set(network.chainId, network.indexer); | ||
| } else if (network.indexerUrl) { | ||
| const jwtAuth = (await this.getJWT(tryAuth)).token; | ||
| this.indexerClients.set(network.chainId, new SequenceIndexer(network.indexerUrl, undefined, jwtAuth)); | ||
| } else { | ||
| throw Error(`No indexer url for chain ${chainId}`); | ||
| } | ||
| } | ||
| return this.indexerClients.get(network.chainId); | ||
| } | ||
| async getIndexerGateway(tryAuth = true) { | ||
| if (!this.indexerGateway) { | ||
| const jwtAuth = (await this.getJWT(tryAuth)).token; | ||
| this.indexerGateway = new SequenceIndexerGateway(this.settings.sequenceIndexerGatewayUrl, undefined, jwtAuth); | ||
| } | ||
| return this.indexerGateway; | ||
| } | ||
| getProofString(key) { | ||
| // check if we already have or are waiting for a proof string | ||
| if (this.proofStrings.has(key)) { | ||
| const _proofString = this.proofStrings.get(key); | ||
| if (this.now() < _proofString.expiration) { | ||
| return _proofString; | ||
| } | ||
| // proof string expired, delete it and make a new one | ||
| this.proofStrings.delete(key); | ||
| } | ||
| const proof = new Proof({ | ||
| address: this.account.address | ||
| }); | ||
| proof.claims.app = this.settings.metadata.name; | ||
| if (typeof window === 'object') { | ||
| proof.claims.ogn = window.location.origin; | ||
| } | ||
| proof.setExpiryIn(this.expiration); | ||
| const ethAuth = new ETHAuth(); | ||
| const chainId = BigInt(this.settings.sequenceApiChainId); | ||
| const found = findNetworkConfig(this.account.networks, chainId); | ||
| if (!found) { | ||
| throw Error('No network found'); | ||
| } | ||
| ethAuth.chainId = Number(chainId); | ||
| const network = new ethers.Network(found.name, chainId); | ||
| // TODO: Modify ETHAuth so it can take a provider instead of a url | ||
| // ----- | ||
| // Can't pass jwt here since this is used for getting the jwt | ||
| ethAuth.provider = new ethers.JsonRpcProvider(getFetchRequest(found.rpcUrl, this.projectAccessKey), network, { | ||
| staticNetwork: network | ||
| }); | ||
| const expiration = this.now() + this.expiration - EXPIRATION_JWT_MARGIN; | ||
| const proofString = { | ||
| proofString: Promise.resolve( | ||
| // NOTICE: TODO: Here we ask the account to sign the message | ||
| // using whatever configuration we have ON-CHAIN, this means | ||
| // that the account will still use the v1 wallet, even if the migration | ||
| // was signed. | ||
| // | ||
| // This works for Sequence webapp v1 -> v2 because all v1 configurations share the same formula | ||
| // (torus + guard), but if we ever decide to allow cross-device login, then it will not work, because | ||
| // those other signers may not be part of the configuration. | ||
| // | ||
| this.account.signDigest(proof.messageDigest(), this.settings.sequenceApiChainId, true, 'eip6492')).then(s => { | ||
| proof.signature = s; | ||
| return ethAuth.encodeProof(proof, true); | ||
| }).catch(reason => { | ||
| this.proofStrings.delete(key); | ||
| throw reason; | ||
| }), | ||
| expiration | ||
| }; | ||
| this.proofStrings.set(key, proofString); | ||
| return proofString; | ||
| } | ||
| } | ||
| // signAuthorization will perform an EIP712 typed-data message signing of ETHAuth domain via the provided | ||
| // Signer and authorization options. | ||
| const signAuthorization = async (signer, chainId, options) => { | ||
| const address = ethers.getAddress(await signer.getAddress()); | ||
| if (!address || address === '' || address === '0x') { | ||
| throw ErrAccountIsRequired; | ||
| } | ||
| const proof = new Proof(); | ||
| proof.address = address; | ||
| if (!options || !options.app || options.app === '') { | ||
| throw new AuthError('authorization options requires app to be set'); | ||
| } | ||
| proof.claims.app = options.app; | ||
| proof.claims.ogn = options.origin; | ||
| proof.claims.n = options.nonce; | ||
| proof.setExpiryIn(options.expiry ? Math.max(options.expiry, 200) : DEFAULT_SESSION_EXPIRATION); | ||
| const typedData = proof.messageTypedData(); | ||
| const chainIdNumber = toChainIdNumber(chainId); | ||
| proof.signature = await (signer instanceof Account ? | ||
| // Account can sign EIP-6492 signatures, so it doesn't require deploying the wallet | ||
| signer.signTypedData(typedData.domain, typedData.types, typedData.message, chainIdNumber, 'eip6492') : signer.signTypedData(typedData.domain, typedData.types, typedData.message, chainIdNumber)); | ||
| const ethAuth = new ETHAuth(); | ||
| const proofString = await ethAuth.encodeProof(proof, true); | ||
| return { | ||
| typedData, | ||
| proofString | ||
| }; | ||
| }; | ||
| // TODO: review...... | ||
| class AuthError extends Error { | ||
| constructor(message) { | ||
| super(message); | ||
| this.name = 'AuthError'; | ||
| } | ||
| } | ||
| const ErrAccountIsRequired = new AuthError('auth error: account address is empty'); | ||
| function _extends() { | ||
| return _extends = Object.assign ? Object.assign.bind() : function (n) { | ||
| for (var e = 1; e < arguments.length; e++) { | ||
| var t = arguments[e]; | ||
| for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); | ||
| } | ||
| return n; | ||
| }, _extends.apply(null, arguments); | ||
| } | ||
| function isSessionDumpV1(obj) { | ||
| return obj.config && obj.metadata && obj.version === undefined; | ||
| } | ||
| function isSessionDumpV2(obj) { | ||
| return obj.version === 2 && obj.address; | ||
| } | ||
| // These chains are always validated for migrations | ||
| // if they are not available, the login will fail | ||
| const CRITICAL_CHAINS = [1, 137]; | ||
| const SessionSettingsDefault = { | ||
| contexts: commons.context.defaultContexts, | ||
| networks: allNetworks, | ||
| tracker: new trackers.remote.RemoteConfigTracker('https://sessions.sequence.app') | ||
| }; | ||
| class Session { | ||
| constructor(networks, contexts, account, services) { | ||
| this.networks = networks; | ||
| this.contexts = contexts; | ||
| this.account = account; | ||
| this.services = services; | ||
| } | ||
| async dump() { | ||
| const base = { | ||
| version: 2, | ||
| address: this.account.address | ||
| }; | ||
| if (this.services) { | ||
| return _extends({}, base, await this.services.dump()); | ||
| } | ||
| return base; | ||
| } | ||
| static async singleSigner(args) { | ||
| let { | ||
| signer | ||
| } = args; | ||
| if (typeof signer === 'string') { | ||
| signer = new ethers.Wallet(signer); | ||
| } | ||
| const orchestrator = new Orchestrator([signer]); | ||
| const referenceSigner = await signer.getAddress(); | ||
| const threshold = 1; | ||
| const addSigners = [{ | ||
| weight: 1, | ||
| address: referenceSigner | ||
| }]; | ||
| const selectWallet = args.selectWallet || async function (wallets) { | ||
| var _args$settings$tracke, _args$settings; | ||
| if (wallets.length === 0) return undefined; | ||
| // Find a wallet that was originally created | ||
| // as a 1/1 of the reference signer | ||
| const tracker = (_args$settings$tracke = (_args$settings = args.settings) == null ? void 0 : _args$settings.tracker) != null ? _args$settings$tracke : SessionSettingsDefault.tracker; | ||
| const configs = await Promise.all(wallets.map(async function (wallet) { | ||
| const imageHash = await tracker.imageHashOfCounterfactualWallet({ | ||
| wallet | ||
| }); | ||
| return { | ||
| wallet, | ||
| config: imageHash && (await tracker.configOfImageHash({ | ||
| imageHash: imageHash.imageHash | ||
| })) | ||
| }; | ||
| })); | ||
| for (const config of configs) { | ||
| if (!config.config) { | ||
| continue; | ||
| } | ||
| const coder = universal.genericCoderFor(config.config.version); | ||
| const signers = coder.config.signersOf(config.config); | ||
| if (signers.length === 1 && signers[0].address === referenceSigner) { | ||
| return config.wallet; | ||
| } | ||
| } | ||
| return undefined; | ||
| }; | ||
| return Session.open(_extends({}, args, { | ||
| orchestrator, | ||
| referenceSigner, | ||
| threshold, | ||
| addSigners, | ||
| selectWallet | ||
| })); | ||
| } | ||
| static async open(args) { | ||
| var _findNetworkConfig$ch, _findNetworkConfig, _settings$services$se, _settings$services, _networks$; | ||
| const { | ||
| referenceSigner, | ||
| threshold, | ||
| addSigners, | ||
| selectWallet, | ||
| onAccountAddress, | ||
| settings, | ||
| editConfigOnMigration, | ||
| onMigration, | ||
| orchestrator, | ||
| projectAccessKey | ||
| } = args; | ||
| const { | ||
| contexts, | ||
| networks, | ||
| tracker, | ||
| services | ||
| } = _extends({}, SessionSettingsDefault, settings); | ||
| // The reference network is mainnet, if mainnet is not available, we use the first network | ||
| const referenceChainId = (_findNetworkConfig$ch = (_findNetworkConfig = findNetworkConfig(networks, (_settings$services$se = settings == null || (_settings$services = settings.services) == null ? void 0 : _settings$services.sequenceApiChainId) != null ? _settings$services$se : ChainId.MAINNET)) == null ? void 0 : _findNetworkConfig.chainId) != null ? _findNetworkConfig$ch : (_networks$ = networks[0]) == null ? void 0 : _networks$.chainId; | ||
| if (!referenceChainId) throw Error('No reference chain found'); | ||
| const foundWallets = await tracker.walletsOfSigner({ | ||
| signer: referenceSigner | ||
| }); | ||
| const selectedWallet = await selectWallet(foundWallets.map(w => w.wallet)); | ||
| let account; | ||
| if (selectedWallet) { | ||
| onAccountAddress == null || onAccountAddress(selectedWallet); | ||
| // existing account, lets update it | ||
| account = new Account({ | ||
| address: selectedWallet, | ||
| tracker, | ||
| networks, | ||
| contexts, | ||
| orchestrator, | ||
| projectAccessKey | ||
| }); | ||
| // Get the latest configuration of the wallet (on the reference chain) | ||
| // now this configuration should be of the latest version, so we can start | ||
| // manipulating it. | ||
| // NOTICE: We are performing the wallet update on a single chain, assuming that | ||
| // all other networks have the same configuration. This is not always true. | ||
| if (addSigners && addSigners.length > 0) { | ||
| // New wallets never need migrations | ||
| // (because we create them on the latest version) | ||
| let status = await account.status(referenceChainId); | ||
| // If the wallet was created originally on v2, then we can skip | ||
| // the migration checks all together. | ||
| if (status.original.version !== status.version || account.version !== status.version) { | ||
| // Account may not have been migrated yet, so we need to check | ||
| // if it has been migrated and if not, migrate it (in all chains) | ||
| const { | ||
| migratedAllChains: isFullyMigrated, | ||
| failedChains | ||
| } = await account.isMigratedAllChains(); | ||
| // Failed chains must not contain mainnet or polygon, otherwise we cannot proceed. | ||
| if (failedChains.some(c => CRITICAL_CHAINS.includes(c))) { | ||
| throw Error(`Failed to fetch account status on ${failedChains.join(', ')}`); | ||
| } | ||
| if (!isFullyMigrated) { | ||
| // This is an oportunity for whoever is opening the session to | ||
| // feed the orchestrator with more signers, so that the migration | ||
| // can be completed. | ||
| if (onMigration && !(await onMigration(account))) { | ||
| throw Error('Migration cancelled, cannot open session'); | ||
| } | ||
| const { | ||
| failedChains: _failedChains | ||
| } = await account.signAllMigrations(editConfigOnMigration || (c => c)); | ||
| if (_failedChains.some(c => CRITICAL_CHAINS.includes(c))) { | ||
| throw Error(`Failed to sign migrations on ${_failedChains.join(', ')}`); | ||
| } | ||
| // If we are using a dedupped tracker we need to invalidate the cache | ||
| // otherwise we run the risk of not seeing the signed migrations reflected. | ||
| if (trackers.isDedupedTracker(tracker)) { | ||
| tracker.invalidateCache(); | ||
| } | ||
| let isFullyMigrated2; | ||
| [isFullyMigrated2, status] = await Promise.all([account.isMigratedAllChains().then(r => r.migratedAllChains), account.status(referenceChainId)]); | ||
| if (!isFullyMigrated2) throw Error('Failed to migrate account'); | ||
| } | ||
| } | ||
| // NOTICE: We only need to do this because the API will not be able to | ||
| // validate the v2 signature (if the account has an onchain version of 1) | ||
| // we could speed this up by sending the migration alongside the jwt request | ||
| // and letting the API validate it offchain. | ||
| if (status.onChain.version !== status.version) { | ||
| await account.doBootstrap(referenceChainId, undefined, status); | ||
| } | ||
| const prevConfig = status.config; | ||
| const nextConfig = account.coders.config.editConfig(prevConfig, { | ||
| add: addSigners, | ||
| threshold | ||
| }); | ||
| // Only update the onchain config if the imageHash has changed | ||
| if (account.coders.config.imageHashOf(prevConfig) !== account.coders.config.imageHashOf(nextConfig)) { | ||
| const newConfig = account.coders.config.editConfig(nextConfig, { | ||
| checkpoint: account.coders.config.checkpointOf(prevConfig) + 1n | ||
| }); | ||
| await account.updateConfig(newConfig); | ||
| } | ||
| } | ||
| } else { | ||
| if (!addSigners || addSigners.length === 0) { | ||
| throw Error('Cannot create new account without signers'); | ||
| } | ||
| if (!threshold) { | ||
| throw Error('Cannot create new account without threshold'); | ||
| } | ||
| // fresh account | ||
| account = await Account.new({ | ||
| config: { | ||
| threshold, | ||
| checkpoint: 0, | ||
| signers: addSigners | ||
| }, | ||
| tracker, | ||
| contexts, | ||
| orchestrator, | ||
| networks, | ||
| projectAccessKey | ||
| }); | ||
| onAccountAddress == null || onAccountAddress(account.address); | ||
| // sign a digest and send it to the tracker | ||
| // otherwise the tracker will not know about this account | ||
| await account.publishWitness(); | ||
| // safety check, the remove tracker should be able to find | ||
| // this account for the reference signer | ||
| const _foundWallets = await tracker.walletsOfSigner({ | ||
| signer: referenceSigner, | ||
| noCache: true | ||
| }); | ||
| if (!_foundWallets.some(w => w.wallet === account.address)) { | ||
| throw Error('Account not found on tracker'); | ||
| } | ||
| } | ||
| let servicesObj; | ||
| if (services) { | ||
| servicesObj = new Services(account, services); | ||
| servicesObj.auth(); // fire and forget | ||
| servicesObj.onAuth(result => { | ||
| if (result.status === 'fulfilled') { | ||
| account.setJwt(result.value); | ||
| } | ||
| }); | ||
| } | ||
| return new Session(networks, contexts, account, servicesObj); | ||
| } | ||
| static async load(args) { | ||
| const { | ||
| dump, | ||
| settings, | ||
| editConfigOnMigration, | ||
| onMigration, | ||
| orchestrator, | ||
| projectAccessKey | ||
| } = args; | ||
| const { | ||
| contexts, | ||
| networks, | ||
| tracker, | ||
| services | ||
| } = _extends({}, SessionSettingsDefault, settings); | ||
| let account; | ||
| if (isSessionDumpV1(dump)) { | ||
| var _dump$jwt$expiration, _dump$jwt, _dump$jwt2; | ||
| // Old configuration format used to also contain an "address" field | ||
| // but if it doesn't, it means that it was a "counterfactual" account | ||
| // not yet updated, so we need to compute the address | ||
| const oldAddress = dump.config.address || commons.context.addressOf(contexts[1], v1.config.ConfigCoder.imageHashOf(_extends({}, dump.config, { | ||
| version: 1 | ||
| }))); | ||
| const jwtExpired = ((_dump$jwt$expiration = (_dump$jwt = dump.jwt) == null ? void 0 : _dump$jwt.expiration) != null ? _dump$jwt$expiration : 0) < Math.floor(Date.now() / 1000); | ||
| account = new Account({ | ||
| address: oldAddress, | ||
| tracker, | ||
| networks, | ||
| contexts, | ||
| orchestrator, | ||
| jwt: jwtExpired ? undefined : (_dump$jwt2 = dump.jwt) == null ? void 0 : _dump$jwt2.token, | ||
| projectAccessKey | ||
| }); | ||
| // TODO: This property may not hold if the user adds a new network | ||
| if (!(await account.isMigratedAllChains().then(r => r.migratedAllChains))) { | ||
| // This is an oportunity for whoever is opening the session to | ||
| // feed the orchestrator with more signers, so that the migration | ||
| // can be completed. | ||
| if (onMigration && !(await onMigration(account))) { | ||
| throw Error('Migration cancelled, cannot open session'); | ||
| } | ||
| console.log('Migrating account...'); | ||
| await account.signAllMigrations(editConfigOnMigration); | ||
| if (!(await account.isMigratedAllChains().then(r => r.migratedAllChains))) throw Error('Failed to migrate account'); | ||
| } | ||
| // We may need to update the JWT if the account has been migrated | ||
| } else if (isSessionDumpV2(dump)) { | ||
| var _dump$jwt$expiration2, _dump$jwt3, _dump$jwt4; | ||
| const jwtExpired = ((_dump$jwt$expiration2 = (_dump$jwt3 = dump.jwt) == null ? void 0 : _dump$jwt3.expiration) != null ? _dump$jwt$expiration2 : 0) < Math.floor(Date.now() / 1000); | ||
| account = new Account({ | ||
| address: dump.address, | ||
| tracker, | ||
| networks, | ||
| contexts, | ||
| orchestrator, | ||
| jwt: jwtExpired ? undefined : (_dump$jwt4 = dump.jwt) == null ? void 0 : _dump$jwt4.token, | ||
| projectAccessKey | ||
| }); | ||
| } else { | ||
| throw Error('Invalid dump format'); | ||
| } | ||
| let servicesObj; | ||
| if (services) { | ||
| var _dump$jwt$expiration3; | ||
| servicesObj = new Services(account, services, dump.jwt && { | ||
| jwt: { | ||
| token: Promise.resolve(dump.jwt.token), | ||
| expiration: (_dump$jwt$expiration3 = dump.jwt.expiration) != null ? _dump$jwt$expiration3 : jwtDecodeClaims(dump.jwt.token).exp | ||
| }, | ||
| metadata: dump.metadata | ||
| }); | ||
| } | ||
| return new Session(networks, contexts, account, servicesObj); | ||
| } | ||
| } | ||
| const ValidateSequenceWalletProof = (readerFor, tracker, context) => { | ||
| return async (_provider, chainId, proof) => { | ||
| const digest = proof.messageDigest(); | ||
| const isValid = await readerFor(chainId).isValidSignature(proof.address, digest, proof.signature); | ||
| return { | ||
| isValid | ||
| }; | ||
| }; | ||
| }; | ||
| export { AuthError, CRITICAL_CHAINS, ErrAccountIsRequired, Session, SessionSettingsDefault, ValidateSequenceWalletProof, isSessionDumpV1, isSessionDumpV2, signAuthorization }; |
| import { ChainIdLike } from '@0xsequence/network'; | ||
| import { TypedData } from '@0xsequence/utils'; | ||
| import { Signer } from '@0xsequence/wallet'; | ||
| import { Account } from '@0xsequence/account'; | ||
| export interface AuthorizationOptions { | ||
| app?: string; | ||
| origin?: string; | ||
| expiry?: number; | ||
| nonce?: number; | ||
| } | ||
| export interface ETHAuthProof { | ||
| typedData: TypedData; | ||
| proofString: string; | ||
| } | ||
| export declare const signAuthorization: (signer: Signer | Account, chainId: ChainIdLike, options: AuthorizationOptions) => Promise<ETHAuthProof>; | ||
| export declare class AuthError extends Error { | ||
| constructor(message?: string); | ||
| } | ||
| export declare const ErrAccountIsRequired: AuthError; |
| export * from "./authorization.js"; | ||
| export * from "./session.js"; | ||
| export * from "./proof.js"; |
| import { commons } from '@0xsequence/core'; | ||
| import { ValidatorFunc } from '@0xsequence/ethauth'; | ||
| import { tracker } from '@0xsequence/sessions'; | ||
| export declare const ValidateSequenceWalletProof: (readerFor: (chainId: number) => commons.reader.Reader, tracker: tracker.ConfigTracker, context: commons.context.WalletContext) => ValidatorFunc; |
| import { Account } from '@0xsequence/account'; | ||
| import { SequenceAPIClient } from '@0xsequence/api'; | ||
| import { Indexer, SequenceIndexerGateway } from '@0xsequence/indexer'; | ||
| import { SequenceMetadata } from '@0xsequence/metadata'; | ||
| import { ChainIdLike } from '@0xsequence/network'; | ||
| import { ethers } from 'ethers'; | ||
| export type SessionMeta = { | ||
| name: string; | ||
| expiration?: number; | ||
| }; | ||
| export type ServicesSettings = { | ||
| metadata: SessionMeta; | ||
| sequenceApiUrl: string; | ||
| sequenceApiChainId: ethers.BigNumberish; | ||
| sequenceMetadataUrl: string; | ||
| sequenceIndexerGatewayUrl: string; | ||
| }; | ||
| export type SessionJWT = { | ||
| token: string; | ||
| expiration: number; | ||
| }; | ||
| export type SessionJWTPromise = { | ||
| token: Promise<string>; | ||
| expiration: number; | ||
| }; | ||
| export type ProofStringPromise = { | ||
| proofString: Promise<string>; | ||
| expiration: number; | ||
| }; | ||
| export declare const DEFAULT_SESSION_EXPIRATION: number; | ||
| export declare const LONG_SESSION_EXPIRATION = 30000000; | ||
| export declare class Services { | ||
| readonly account: Account; | ||
| readonly settings: ServicesSettings; | ||
| readonly status: { | ||
| jwt?: SessionJWTPromise; | ||
| metadata?: SessionMeta; | ||
| }; | ||
| _initialAuthRequest: Promise<SequenceAPIClient>; | ||
| private readonly proofStrings; | ||
| private onAuthCallbacks; | ||
| private apiClient; | ||
| private metadataClient; | ||
| private indexerClients; | ||
| private indexerGateway; | ||
| private projectAccessKey?; | ||
| constructor(account: Account, settings: ServicesSettings, status?: { | ||
| jwt?: SessionJWTPromise; | ||
| metadata?: SessionMeta; | ||
| }, projectAccessKey?: string); | ||
| private now; | ||
| get expiration(): number; | ||
| onAuth(cb: (result: PromiseSettledResult<string>) => void): () => ((result: PromiseSettledResult<string>) => void)[]; | ||
| dump(): Promise<{ | ||
| jwt?: SessionJWT; | ||
| metadata?: SessionMeta; | ||
| }>; | ||
| auth(maxTries?: number): Promise<SequenceAPIClient>; | ||
| private getJWT; | ||
| private getProofStringKey; | ||
| private isProofStringValid; | ||
| getAPIClient(tryAuth?: boolean): Promise<SequenceAPIClient>; | ||
| getMetadataClient(tryAuth?: boolean): Promise<SequenceMetadata>; | ||
| getIndexerClient(chainId: ChainIdLike, tryAuth?: boolean): Promise<Indexer>; | ||
| getIndexerGateway(tryAuth?: boolean): Promise<SequenceIndexerGateway>; | ||
| private getProofString; | ||
| } |
| import { NetworkConfig } from '@0xsequence/network'; | ||
| import { Account } from '@0xsequence/account'; | ||
| import { ethers } from 'ethers'; | ||
| import { tracker } from '@0xsequence/sessions'; | ||
| import { SignatureOrchestrator, signers } from '@0xsequence/signhub'; | ||
| import { migrator } from '@0xsequence/migration'; | ||
| import { commons, v1 } from '@0xsequence/core'; | ||
| import { Services, ServicesSettings, SessionJWT, SessionMeta } from "./services.js"; | ||
| export interface SessionDumpV1 { | ||
| config: Omit<v1.config.WalletConfig, 'version'> & { | ||
| address?: string; | ||
| }; | ||
| jwt?: SessionJWT; | ||
| metadata: SessionMeta; | ||
| } | ||
| export interface SessionDumpV2 { | ||
| version: 2; | ||
| address: string; | ||
| jwt?: SessionJWT; | ||
| metadata?: SessionMeta; | ||
| } | ||
| export declare function isSessionDumpV1(obj: any): obj is SessionDumpV1; | ||
| export declare function isSessionDumpV2(obj: any): obj is SessionDumpV2; | ||
| export declare const CRITICAL_CHAINS: number[]; | ||
| export type SessionSettings = { | ||
| services?: ServicesSettings; | ||
| contexts: commons.context.VersionedContext; | ||
| networks: NetworkConfig[]; | ||
| tracker: tracker.ConfigTracker & migrator.PresignedMigrationTracker; | ||
| }; | ||
| export declare const SessionSettingsDefault: SessionSettings; | ||
| export declare class Session { | ||
| networks: NetworkConfig[]; | ||
| contexts: commons.context.VersionedContext; | ||
| account: Account; | ||
| services?: Services | undefined; | ||
| constructor(networks: NetworkConfig[], contexts: commons.context.VersionedContext, account: Account, services?: Services | undefined); | ||
| dump(): Promise<SessionDumpV2>; | ||
| static singleSigner(args: { | ||
| settings?: Partial<SessionSettings>; | ||
| signer: ethers.Signer | signers.SapientSigner | string; | ||
| selectWallet?: (wallets: string[]) => Promise<string | undefined>; | ||
| onAccountAddress?: (address: string) => void; | ||
| onMigration?: (account: Account) => Promise<boolean>; | ||
| editConfigOnMigration?: (config: commons.config.Config) => commons.config.Config; | ||
| projectAccessKey: string; | ||
| }): Promise<Session>; | ||
| static open(args: { | ||
| settings?: Partial<SessionSettings>; | ||
| orchestrator: SignatureOrchestrator; | ||
| addSigners?: commons.config.SimpleSigner[]; | ||
| referenceSigner: string; | ||
| threshold?: ethers.BigNumberish; | ||
| selectWallet: (wallets: string[]) => Promise<string | undefined>; | ||
| onAccountAddress?: (address: string) => void; | ||
| editConfigOnMigration?: (config: commons.config.Config) => commons.config.Config; | ||
| onMigration?: (account: Account) => Promise<boolean>; | ||
| projectAccessKey?: string; | ||
| }): Promise<Session>; | ||
| static load(args: { | ||
| settings?: Partial<SessionSettings>; | ||
| orchestrator: SignatureOrchestrator; | ||
| dump: SessionDumpV1 | SessionDumpV2; | ||
| editConfigOnMigration: (config: commons.config.Config) => commons.config.Config; | ||
| onMigration?: (account: Account) => Promise<boolean>; | ||
| projectAccessKey?: string; | ||
| }): Promise<Session>; | ||
| } |
@@ -1,2 +0,2 @@ | ||
| export * from "../src/index.js"; | ||
| //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMHhzZXF1ZW5jZS1hdXRoLmNqcy5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBIn0= | ||
| export * from "./declarations/src/index.js"; | ||
| //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMHhzZXF1ZW5jZS1hdXRoLmNqcy5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi9kZWNsYXJhdGlvbnMvc3JjL2luZGV4LmQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEifQ== |
@@ -1,16 +0,7 @@ | ||
| "use strict"; | ||
| // this file might look strange and you might be wondering what it's for | ||
| // it's lets you import your source files by importing this entrypoint | ||
| // as you would import it if it was built with preconstruct build | ||
| // this file is slightly different to some others though | ||
| // it has a require hook which compiles your code with Babel | ||
| // this means that you don't have to set up @babel/register or anything like that | ||
| // but you can still require this module and it'll be compiled | ||
| 'use strict'; | ||
| // this bit of code imports the require hook and registers it | ||
| let unregister = require("../../../node_modules/.pnpm/@preconstruct+hook@0.4.0/node_modules/@preconstruct/hook").___internalHook(typeof __dirname === 'undefined' ? undefined : __dirname, "../../..", ".."); | ||
| // this re-exports the source file | ||
| module.exports = require("../src/index.ts"); | ||
| unregister(); | ||
| if (process.env.NODE_ENV === "production") { | ||
| module.exports = require("./0xsequence-auth.cjs.prod.js"); | ||
| } else { | ||
| module.exports = require("./0xsequence-auth.cjs.dev.js"); | ||
| } |
+14
-14
| { | ||
| "name": "@0xsequence/auth", | ||
| "version": "2.3.19", | ||
| "version": "2.3.20", | ||
| "description": "auth sub-package for Sequence", | ||
@@ -13,14 +13,14 @@ "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/auth", | ||
| "@0xsequence/ethauth": "^1.0.0", | ||
| "@0xsequence/abi": "2.3.19", | ||
| "@0xsequence/account": "2.3.19", | ||
| "@0xsequence/indexer": "2.3.19", | ||
| "@0xsequence/metadata": "2.3.19", | ||
| "@0xsequence/migration": "2.3.19", | ||
| "@0xsequence/sessions": "2.3.19", | ||
| "@0xsequence/core": "2.3.19", | ||
| "@0xsequence/api": "2.3.19", | ||
| "@0xsequence/signhub": "2.3.19", | ||
| "@0xsequence/wallet": "2.3.19", | ||
| "@0xsequence/utils": "2.3.19", | ||
| "@0xsequence/network": "2.3.19" | ||
| "@0xsequence/account": "2.3.20", | ||
| "@0xsequence/api": "2.3.20", | ||
| "@0xsequence/indexer": "2.3.20", | ||
| "@0xsequence/metadata": "2.3.20", | ||
| "@0xsequence/migration": "2.3.20", | ||
| "@0xsequence/network": "2.3.20", | ||
| "@0xsequence/sessions": "2.3.20", | ||
| "@0xsequence/signhub": "2.3.20", | ||
| "@0xsequence/utils": "2.3.20", | ||
| "@0xsequence/core": "2.3.20", | ||
| "@0xsequence/wallet": "2.3.20", | ||
| "@0xsequence/abi": "2.3.20" | ||
| }, | ||
@@ -36,3 +36,3 @@ "peerDependencies": { | ||
| "mockttp": "^3.6.0", | ||
| "@0xsequence/tests": "2.3.19" | ||
| "@0xsequence/tests": "2.3.20" | ||
| }, | ||
@@ -39,0 +39,0 @@ "files": [ |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
122628
188.89%18
80%2788
288.3%6
200%5
400%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated
Updated
Updated
Updated
Updated
Updated
Updated
Updated
Updated
Updated
Updated