From 84a879b331c98d0fd56bb4e1e53a75944ac6f17c Mon Sep 17 00:00:00 2001 From: Cesare Naldi Date: Thu, 19 Dec 2024 14:26:03 +0100 Subject: [PATCH] feat: seamless rollover token refresh --- packages/client/package.json | 1 + packages/client/src/clients.test.ts | 110 +++++++++- packages/client/src/clients.ts | 106 +++++----- packages/client/testing-utils.ts | 35 +++- pnpm-lock.yaml | 300 +++++++++++++++++++++++++++- 5 files changed, 488 insertions(+), 64 deletions(-) diff --git a/packages/client/package.json b/packages/client/package.json index 1aabb459a..f2a5eb7bc 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -79,6 +79,7 @@ "@lens-network/sdk": "canary", "@lens-protocol/metadata": "next", "ethers": "^6.13.4", + "msw": "^2.7.0", "tsup": "^8.3.5", "typescript": "^5.6.3", "viem": "^2.21.53", diff --git a/packages/client/src/clients.test.ts b/packages/client/src/clients.test.ts index ac15012e3..dd8041d72 100644 --- a/packages/client/src/clients.test.ts +++ b/packages/client/src/clients.test.ts @@ -1,13 +1,16 @@ -import { testnet } from '@lens-protocol/env'; import { url, assertErr, assertOk, evmAddress, signatureFrom } from '@lens-protocol/types'; +import { HttpResponse, graphql, passthrough } from 'msw'; +import { setupServer } from 'msw/node'; import { privateKeyToAccount } from 'viem/accounts'; -import { describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -import { HealthQuery, Role } from '@lens-protocol/graphql'; +import { CurrentSessionQuery, HealthQuery, RefreshMutation, Role } from '@lens-protocol/graphql'; +import { createGraphQLErrorObject, createPublicClient } from '../testing-utils'; import { currentSession } from './actions'; import { PublicClient } from './clients'; -import { UnexpectedError } from './errors'; +import { GraphQLErrorCode, UnauthenticatedError, UnexpectedError } from './errors'; +import { delay } from './utils'; const signer = privateKeyToAccount(import.meta.env.PRIVATE_KEY); const owner = evmAddress(signer.address); @@ -15,10 +18,7 @@ const account = evmAddress(import.meta.env.TEST_ACCOUNT); const app = evmAddress(import.meta.env.TEST_APP); describe(`Given an instance of the ${PublicClient.name}`, () => { - const client = PublicClient.create({ - environment: testnet, - origin: 'http://example.com', - }); + const client = createPublicClient(); describe('When authenticating via the low-level methods', () => { it('Then it should authenticate and stay authenticated', async () => { @@ -104,4 +104,98 @@ describe(`Given an instance of the ${PublicClient.name}`, () => { expect(result.error).toBeInstanceOf(UnexpectedError); }); }); + + describe('And a SessionClient created from it', () => { + describe('When a request fails with UNAUTHENTICATED extension code', () => { + const server = setupServer( + graphql.query( + CurrentSessionQuery, + (_) => + HttpResponse.json({ + errors: [createGraphQLErrorObject(GraphQLErrorCode.UNAUTHENTICATED)], + }), + { + once: true, + }, + ), + // Pass through all other operations + graphql.operation(() => passthrough()), + ); + + beforeAll(() => { + server.listen(); + }); + + afterAll(() => { + server.close(); + }); + + it( + 'Then it should silently refresh credentials and retry the request', + { timeout: 5000 }, + async () => { + const authenticated = await client.login({ + accountOwner: { + account, + owner, + app, + }, + signMessage: (message) => signer.signMessage({ message }), + }); + assertOk(authenticated); + + // wait 1 second to make sure the new tokens have 'expiry at' different from the previous ones + await delay(1000); + + const result = await currentSession(authenticated.value); + + assertOk(result); + }, + ); + }); + + describe('When a token refresh fails', () => { + const server = setupServer( + graphql.query(CurrentSessionQuery, (_) => + HttpResponse.json({ + errors: [createGraphQLErrorObject(GraphQLErrorCode.UNAUTHENTICATED)], + }), + ), + graphql.mutation(RefreshMutation, (_) => + HttpResponse.json({ + errors: [createGraphQLErrorObject(GraphQLErrorCode.BAD_USER_INPUT)], + }), + ), + // Pass through all other operations + graphql.operation(() => passthrough()), + ); + + beforeAll(() => { + server.listen(); + }); + + afterAll(() => { + server.close(); + }); + it( + `Then it should return a '${UnauthenticatedError.name}' to the original request caller`, + { timeout: 5000 }, + async () => { + const authenticated = await client.login({ + accountOwner: { + account, + owner, + app, + }, + signMessage: (message) => signer.signMessage({ message }), + }); + assertOk(authenticated); + + const result = await currentSession(authenticated.value); + assertErr(result); + expect(result.error).toBeInstanceOf(UnauthenticatedError); + }, + ); + }); + }); }); diff --git a/packages/client/src/clients.ts b/packages/client/src/clients.ts index 394949d40..35f592578 100644 --- a/packages/client/src/clients.ts +++ b/packages/client/src/clients.ts @@ -1,4 +1,4 @@ -import { AuthenticateMutation, ChallengeMutation } from '@lens-protocol/graphql'; +import { AuthenticateMutation, ChallengeMutation, RefreshMutation } from '@lens-protocol/graphql'; import type { AuthenticationChallenge, ChallengeRequest, @@ -12,24 +12,23 @@ import { type TxHash, errAsync, invariant, - never, okAsync, signatureFrom, } from '@lens-protocol/types'; import { type AnyVariables, - type Operation, + type Exchange, type OperationResult, type OperationResultSource, type TypedDocumentNode, type Client as UrqlClient, createClient, fetchExchange, - mapExchange, } from '@urql/core'; import { type Logger, getLogger } from 'loglevel'; import type { SwitchAccountRequest } from '@lens-protocol/graphql'; +import { type AuthConfig, authExchange } from '@urql/exchange-auth'; import { type AuthenticatedUser, authenticatedUser } from './AuthenticatedUser'; import { switchAccount, transactionStatus } from './actions'; import type { ClientConfig } from './config'; @@ -75,25 +74,12 @@ abstract class AbstractClient { this.urql = createClient({ url: context.environment.backend, - exchanges: [ - mapExchange({ - onOperation: async (operation: Operation) => { - this.logger.debug( - 'Operation:', - // biome-ignore lint/suspicious/noExplicitAny: This is a debug log - (operation.query.definitions[0] as any)?.name?.value ?? 'Unknown', - ); - return { - ...operation, - context: { - ...operation.context, - fetchOptions: await this.fetchOptions(), - }, - }; - }, - }), - fetchExchange, - ], + fetchOptions: { + headers: { + ...(this.context.origin ? { Origin: this.context.origin } : {}), + }, + }, + exchanges: this.exchanges(), }); } @@ -119,12 +105,8 @@ abstract class AbstractClient { return this.resultFrom(this.urql.mutation(document, variables)).map(takeValue); } - protected fetchOptions(): RequestInit | Promise { - return { - headers: { - ...(this.context.origin ? { Origin: this.context.origin } : {}), - }, - }; + protected exchanges(): Exchange[] { + return [fetchExchange]; } protected resultFrom( @@ -308,13 +290,15 @@ class SessionClient extends AbstractClient< /** * The current authentication tokens if available. + * + * @internal */ getCredentials(): ResultAsync { return ResultAsync.fromPromise(this.credentials.get(), (err) => UnexpectedError.from(err)); } /** - * @internal + * The AuthenticatedUser associated with the current session. */ getAuthenticatedUser(): ResultAsync { return this.getCredentials().andThen((credentials) => { @@ -362,14 +346,14 @@ class SessionClient extends AbstractClient< > { return switchAccount(this, request) .andThen((result) => { - if (result.__typename === 'ForbiddenError') { - return AuthenticationError.from(result.reason).asResultAsync(); + if (result.__typename === 'AuthenticationTokens') { + return okAsync(result); } - return okAsync(result); + return AuthenticationError.from(result.reason).asResultAsync(); }) .map(async (tokens) => { await this.credentials.set(tokens); - return this; + return new SessionClient(this.parent); }); } @@ -457,18 +441,50 @@ class SessionClient extends AbstractClient< throw TransactionIndexingError.from(`Timeout waiting for transaction ${txHash}`); } - protected override async fetchOptions(): Promise { - const base = await super.fetchOptions(); - const credentials = (await this.credentials.get()) ?? never('No credentials found'); + protected override exchanges(): Exchange[] { + return [ + authExchange(async (utils): Promise => { + let credentials = await this.getCredentials().unwrapOr(null); - return { - ...base, - headers: { - ...base.headers, - 'x-access-token': credentials.accessToken, - // Authorization: `Bearer ${this.tokens.accessToken}`, - }, - }; + return { + addAuthToOperation: (operation) => { + if (!credentials) return operation; + + return utils.appendHeaders(operation, { + Authorization: `Bearer ${credentials.accessToken}`, + }); + }, + + didAuthError: (error) => hasExtensionCode(error, GraphQLErrorCode.UNAUTHENTICATED), + + refreshAuth: async () => { + const result = await utils.mutate(RefreshMutation, { + request: { + refreshToken: credentials?.refreshToken, + }, + }); + + if (result.data) { + switch (result.data.value.__typename) { + case 'AuthenticationTokens': + credentials = result.data?.value; + await this.credentials.set(result.data?.value); + break; + + case 'ForbiddenError': + throw AuthenticationError.from(result.data.value.reason); + + default: + throw AuthenticationError.from( + `Unsupported refresh token response ${result.data.value}`, + ); + } + } + }, + }; + }), + fetchExchange, + ]; } private handleAuthentication< diff --git a/packages/client/testing-utils.ts b/packages/client/testing-utils.ts index c65d2901b..4ef04a9d0 100644 --- a/packages/client/testing-utils.ts +++ b/packages/client/testing-utils.ts @@ -2,7 +2,7 @@ import { chains } from '@lens-network/sdk/viem'; import { evmAddress } from '@lens-protocol/types'; import { http, type Account, type Transport, type WalletClient, createWalletClient } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; -import { PublicClient, testnet } from './src'; +import { GraphQLErrorCode, PublicClient, testnet } from './src'; const pk = privateKeyToAccount(import.meta.env.PRIVATE_KEY); const account = evmAddress(import.meta.env.TEST_ACCOUNT); @@ -10,11 +10,15 @@ const app = evmAddress(import.meta.env.TEST_APP); export const signer = evmAddress(pk.address); -export function loginAsAccountOwner() { - const client = PublicClient.create({ +export function createPublicClient() { + return PublicClient.create({ environment: testnet, origin: 'http://example.com', }); +} + +export function loginAsAccountOwner() { + const client = createPublicClient(); return client.login({ accountOwner: { @@ -27,10 +31,7 @@ export function loginAsAccountOwner() { } export function loginAsOnboardingUser() { - const client = PublicClient.create({ - environment: testnet, - origin: 'http://example.com', - }); + const client = createPublicClient(); return client.login({ onboardingUser: { @@ -48,3 +49,23 @@ export function signerWallet(): WalletClient = { + [GraphQLErrorCode.UNAUTHENTICATED]: + "Unauthenticated - Authentication is required to access ''", + [GraphQLErrorCode.FORBIDDEN]: "Forbidden - You are not authorized to access ''", + [GraphQLErrorCode.INTERNAL_SERVER_ERROR]: 'Internal server error - Please try again later', + [GraphQLErrorCode.BAD_USER_INPUT]: 'Bad user input - Please check the input and try again', + [GraphQLErrorCode.BAD_REQUEST]: 'Bad request - Please check the request and try again', +}; + +export function createGraphQLErrorObject(code: GraphQLErrorCode) { + return { + message: messages[code], + locations: [], + path: [], + extensions: { + code: code, + }, + }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cdab556f..7dc26b605 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,7 +43,7 @@ importers: version: 5.0.1(typescript@5.7.2)(vite@5.4.11(@types/node@22.10.1)) vitest: specifier: ^2.1.3 - version: 2.1.8(@types/node@22.10.1)(happy-dom@15.11.7) + version: 2.1.8(@types/node@22.10.1)(happy-dom@15.11.7)(msw@2.7.0(@types/node@22.10.1)(typescript@5.7.2)) packages/client: dependencies: @@ -81,6 +81,9 @@ importers: ethers: specifier: ^6.13.4 version: 6.13.4 + msw: + specifier: ^2.7.0 + version: 2.7.0(@types/node@22.10.1)(typescript@5.7.2) tsup: specifier: ^8.3.5 version: 8.3.5(postcss@8.4.49)(tsx@4.19.2)(typescript@5.7.2) @@ -301,6 +304,15 @@ packages: cpu: [x64] os: [win32] + '@bundled-es-modules/cookie@2.0.1': + resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} + + '@bundled-es-modules/statuses@1.0.1': + resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} + + '@bundled-es-modules/tough-cookie@0.1.6': + resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} + '@changesets/apply-release-plan@7.0.6': resolution: {integrity: sha512-TKhVLtiwtQOgMAC0fCJfmv93faiViKSDqr8oMEqrnNs99gtSC1sZh/aEMS9a+dseU1ESZRCK+ofLgGY7o0fw/Q==} @@ -801,10 +813,30 @@ packages: graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 typescript: ^5.0.0 + '@inquirer/confirm@5.1.0': + resolution: {integrity: sha512-osaBbIMEqVFjTX5exoqPXs6PilWQdjaLhGtMDXMXg/yxkHXNq43GlxGyTA35lK2HpzUgDN+Cjh/2AmqCN0QJpw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + + '@inquirer/core@10.1.1': + resolution: {integrity: sha512-rmZVXy9iZvO3ZStEe/ayuuwIJ23LSF13aPMlLMTQARX6lGUBDHGV8UB5i9MRrfy0+mZwt5/9bdy8llszSD3NQA==} + engines: {node: '>=18'} + '@inquirer/figures@1.0.7': resolution: {integrity: sha512-m+Trk77mp54Zma6xLkLuY+mvanPxlE4A7yNKs2HBiyZ4UkVs28Mv5c/pgWrHeInx+USHeX/WEPzjrWrcJiQgjw==} engines: {node: '>=18'} + '@inquirer/figures@1.0.8': + resolution: {integrity: sha512-tKd+jsmhq21AP1LhexC0pPwsCxEhGgAkg28byjJAd+xhmIs8LUX8JbUc3vBf3PhLxWiB5EvyBE5X7JSPAqMAqg==} + engines: {node: '>=18'} + + '@inquirer/type@3.0.1': + resolution: {integrity: sha512-+ksJMIy92sOAiAccGpcKZUc3bYO07cADnscIxHBknEm3uNts3movSmBofc1908BNy5edKscxYeAdaX1NXkHS6A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -861,6 +893,10 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@mswjs/interceptors@0.37.3': + resolution: {integrity: sha512-USvgCL/uOGFtVa6SVyRrC8kIAedzRohxIXN5LISlg5C5vLZCn7dgMFVSNhSF9cuBEFrm/O2spDWEZeMnw4ZXYg==} + engines: {node: '>=18'} + '@noble/curves@1.2.0': resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} @@ -900,6 +936,15 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1127,6 +1172,9 @@ packages: '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} @@ -1154,9 +1202,15 @@ packages: '@types/react@18.3.12': resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==} + '@types/statuses@2.0.5': + resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} + '@types/through@0.0.33': resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@urql/core@5.0.8': resolution: {integrity: sha512-1GOnUw7/a9bzkcM0+U8U5MmxW2A7FE5YquuEmcJzTtW5tIs2EoS4F2ITpuKBjRBbyRjZgO860nWFPo1m4JImGA==} @@ -1384,6 +1438,10 @@ packages: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -1415,6 +1473,10 @@ packages: constant-case@3.0.4: resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -1530,6 +1592,10 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -1637,6 +1703,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.3.0: resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} engines: {node: '>=18'} @@ -1732,6 +1802,9 @@ packages: header-case@2.0.4: resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + homedir-polyfill@1.0.3: resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} engines: {node: '>=0.10.0'} @@ -1800,6 +1873,9 @@ packages: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} engines: {node: '>=12'} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -2038,10 +2114,24 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msw@2.7.0: + resolution: {integrity: sha512-BIodwZ19RWfCbYTxWTUfTXc+sg4OwjCAgxU1ZsgmggX/7S3LdUifsbUPJs61j0rWb19CZRGY5if77duhc0uXzw==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + mute-stream@1.0.0: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -2110,6 +2200,9 @@ packages: outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + ox@0.1.2: resolution: {integrity: sha512-ak/8K0Rtphg9vnRJlbOdaX9R7cmxD2MiSthjWGaQdMk3D7hrAlDoM+6Lxn7hN52Za3vrXfZ7enfke/5WjolDww==} peerDependencies: @@ -2196,6 +2289,9 @@ packages: resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} engines: {node: 20 || >=22} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -2266,10 +2362,16 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -2307,6 +2409,13 @@ packages: regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-dir@1.0.1: resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} engines: {node: '>=0.10.0'} @@ -2435,6 +2544,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + std-env@3.8.0: resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} @@ -2442,6 +2555,9 @@ packages: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2530,6 +2646,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} @@ -2646,12 +2766,19 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + upper-case-first@2.0.2: resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} upper-case@2.0.2: resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==} + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + urql@4.2.1: resolution: {integrity: sha512-Y/cVi1rsR96kbF3VX6YgfHw1V3kJ287nR1TqpjGzVPyAmXuwRvBvABI+sHY2WOquWCNh+kvFapM/LLYeiC3wig==} peerDependencies: @@ -2824,6 +2951,18 @@ packages: utf-8-validate: optional: true + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yoctocolors-cjs@2.1.2: resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} engines: {node: '>=18'} @@ -2906,6 +3045,19 @@ snapshots: '@biomejs/cli-win32-x64@1.9.4': optional: true + '@bundled-es-modules/cookie@2.0.1': + dependencies: + cookie: 0.7.2 + + '@bundled-es-modules/statuses@1.0.1': + dependencies: + statuses: 2.0.1 + + '@bundled-es-modules/tough-cookie@0.1.6': + dependencies: + '@types/tough-cookie': 4.0.5 + tough-cookie: 4.1.4 + '@changesets/apply-release-plan@7.0.6': dependencies: '@changesets/config': 3.0.4 @@ -3274,8 +3426,34 @@ snapshots: graphql: 16.9.0 typescript: 5.7.2 + '@inquirer/confirm@5.1.0(@types/node@22.10.1)': + dependencies: + '@inquirer/core': 10.1.1(@types/node@22.10.1) + '@inquirer/type': 3.0.1(@types/node@22.10.1) + '@types/node': 22.10.1 + + '@inquirer/core@10.1.1(@types/node@22.10.1)': + dependencies: + '@inquirer/figures': 1.0.8 + '@inquirer/type': 3.0.1(@types/node@22.10.1) + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.2 + transitivePeerDependencies: + - '@types/node' + '@inquirer/figures@1.0.7': {} + '@inquirer/figures@1.0.8': {} + + '@inquirer/type@3.0.1(@types/node@22.10.1)': + dependencies: + '@types/node': 22.10.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -3335,6 +3513,15 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 + '@mswjs/interceptors@0.37.3': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + '@noble/curves@1.2.0': dependencies: '@noble/hashes': 1.3.2 @@ -3367,6 +3554,15 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -3528,6 +3724,8 @@ snapshots: '@types/aria-query@5.0.4': {} + '@types/cookie@0.6.0': {} + '@types/estree@1.0.6': {} '@types/fined@1.1.5': {} @@ -3559,10 +3757,14 @@ snapshots: '@types/prop-types': 15.7.13 csstype: 3.1.3 + '@types/statuses@2.0.5': {} + '@types/through@0.0.33': dependencies: '@types/node': 22.10.1 + '@types/tough-cookie@4.0.5': {} + '@urql/core@5.0.8(graphql@16.9.0)': dependencies: '@0no-co/graphql.web': 1.0.9(graphql@16.9.0) @@ -3582,12 +3784,13 @@ snapshots: chai: 5.1.2 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.8(vite@5.4.11(@types/node@22.10.1))': + '@vitest/mocker@2.1.8(msw@2.7.0(@types/node@22.10.1)(typescript@5.7.2))(vite@5.4.11(@types/node@22.10.1))': dependencies: '@vitest/spy': 2.1.8 estree-walker: 3.0.3 magic-string: 0.30.14 optionalDependencies: + msw: 2.7.0(@types/node@22.10.1)(typescript@5.7.2) vite: 5.4.11(@types/node@22.10.1) '@vitest/pretty-format@2.1.8': @@ -3796,6 +3999,12 @@ snapshots: cli-width@4.1.0: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clone@1.0.4: {} color-convert@1.9.3: @@ -3822,6 +4031,8 @@ snapshots: tslib: 2.8.0 upper-case: 2.0.2 + cookie@0.7.2: {} + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -3993,6 +4204,8 @@ snapshots: '@esbuild/win32-ia32': 0.24.0 '@esbuild/win32-x64': 0.24.0 + escalade@3.2.0: {} + escape-string-regexp@1.0.5: {} escape-string-regexp@5.0.0: {} @@ -4106,6 +4319,8 @@ snapshots: function-bind@1.1.2: {} + get-caller-file@2.0.5: {} + get-east-asian-width@1.3.0: {} get-intrinsic@1.2.6: @@ -4241,6 +4456,8 @@ snapshots: capital-case: 1.0.4 tslib: 2.8.0 + headers-polyfill@4.0.3: {} + homedir-polyfill@1.0.3: dependencies: parse-passwd: 1.0.0 @@ -4304,6 +4521,8 @@ snapshots: is-interactive@2.0.0: {} + is-node-process@1.2.0: {} + is-number@7.0.0: {} is-path-cwd@3.0.0: {} @@ -4494,8 +4713,35 @@ snapshots: ms@2.1.3: {} + msw@2.7.0(@types/node@22.10.1)(typescript@5.7.2): + dependencies: + '@bundled-es-modules/cookie': 2.0.1 + '@bundled-es-modules/statuses': 1.0.1 + '@bundled-es-modules/tough-cookie': 0.1.6 + '@inquirer/confirm': 5.1.0(@types/node@22.10.1) + '@mswjs/interceptors': 0.37.3 + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/until': 2.1.0 + '@types/cookie': 0.6.0 + '@types/statuses': 2.0.5 + graphql: 16.9.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + strict-event-emitter: 0.5.1 + type-fest: 4.30.0 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - '@types/node' + mute-stream@1.0.0: {} + mute-stream@2.0.0: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -4589,6 +4835,8 @@ snapshots: outdent@0.5.0: {} + outvariant@1.4.3: {} + ox@0.1.2(typescript@5.7.2)(zod@3.23.8): dependencies: '@adraffy/ens-normalize': 1.11.0 @@ -4674,6 +4922,8 @@ snapshots: lru-cache: 11.0.1 minipass: 7.1.2 + path-to-regexp@6.3.0: {} + path-type@4.0.0: {} pathe@1.1.2: {} @@ -4728,8 +4978,14 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + psl@1.15.0: + dependencies: + punycode: 2.3.1 + punycode@2.3.1: {} + querystringify@2.2.0: {} + queue-microtask@1.2.3: {} react-dom@18.3.1(react@18.3.1): @@ -4767,6 +5023,10 @@ snapshots: regenerator-runtime@0.14.1: {} + require-directory@2.1.1: {} + + requires-port@1.0.0: {} + resolve-dir@1.0.1: dependencies: expand-tilde: 2.0.2 @@ -4924,10 +5184,14 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.1: {} + std-env@3.8.0: {} stdin-discarder@0.2.2: {} + strict-event-emitter@0.5.1: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -5017,6 +5281,13 @@ snapshots: dependencies: is-number: 7.0.0 + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + tr46@1.0.1: dependencies: punycode: 2.3.1 @@ -5111,6 +5382,8 @@ snapshots: universalify@0.1.2: {} + universalify@0.2.0: {} + upper-case-first@2.0.2: dependencies: tslib: 2.8.0 @@ -5119,6 +5392,11 @@ snapshots: dependencies: tslib: 2.8.0 + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + urql@4.2.1(graphql@16.9.0)(react@18.3.1): dependencies: '@urql/core': 5.0.8(graphql@16.9.0) @@ -5189,10 +5467,10 @@ snapshots: '@types/node': 22.10.1 fsevents: 2.3.3 - vitest@2.1.8(@types/node@22.10.1)(happy-dom@15.11.7): + vitest@2.1.8(@types/node@22.10.1)(happy-dom@15.11.7)(msw@2.7.0(@types/node@22.10.1)(typescript@5.7.2)): dependencies: '@vitest/expect': 2.1.8 - '@vitest/mocker': 2.1.8(vite@5.4.11(@types/node@22.10.1)) + '@vitest/mocker': 2.1.8(msw@2.7.0(@types/node@22.10.1)(typescript@5.7.2))(vite@5.4.11(@types/node@22.10.1)) '@vitest/pretty-format': 2.1.8 '@vitest/runner': 2.1.8 '@vitest/snapshot': 2.1.8 @@ -5287,6 +5565,20 @@ snapshots: ws@8.18.0: {} + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yoctocolors-cjs@2.1.2: {} zksync-ethers@6.15.3(ethers@6.13.4):