diff --git a/.github/workflows/playwrightTests.yml b/.github/workflows/playwrightTests.yml index 97898b91..f84fa398 100644 --- a/.github/workflows/playwrightTests.yml +++ b/.github/workflows/playwrightTests.yml @@ -4,6 +4,7 @@ jobs: playwright: name: "Playwright e2e Tests" runs-on: ubuntu-latest + timeout-minutes: 5 container: image: mcr.microsoft.com/playwright:v1.43.0-jammy steps: diff --git a/@stellar/typescript-wallet-sdk-km/CHANGELOG.MD b/@stellar/typescript-wallet-sdk-km/CHANGELOG.MD index 6520f4a8..562a584f 100644 --- a/@stellar/typescript-wallet-sdk-km/CHANGELOG.MD +++ b/@stellar/typescript-wallet-sdk-km/CHANGELOG.MD @@ -1,3 +1,6 @@ +# Release notes - Typescript Wallet SDK Key Manager - 1.10.0 +* Version bump + # Release notes - Typescript Wallet SDK Key Manager - 1.9.0 * Version bump diff --git a/@stellar/typescript-wallet-sdk-km/package.json b/@stellar/typescript-wallet-sdk-km/package.json index 81c991db..78aec7c9 100644 --- a/@stellar/typescript-wallet-sdk-km/package.json +++ b/@stellar/typescript-wallet-sdk-km/package.json @@ -1,6 +1,6 @@ { "name": "@stellar/typescript-wallet-sdk-km", - "version": "1.9.0", + "version": "1.10.0", "engines": { "node": ">=18" }, diff --git a/@stellar/typescript-wallet-sdk-soroban/CHANGELOG.MD b/@stellar/typescript-wallet-sdk-soroban/CHANGELOG.MD index 24a2713e..b2bb012f 100644 --- a/@stellar/typescript-wallet-sdk-soroban/CHANGELOG.MD +++ b/@stellar/typescript-wallet-sdk-soroban/CHANGELOG.MD @@ -1,3 +1,6 @@ +# Release notes - Typescript Wallet SDK Soroban - 1.10.0 +* Version bump + # Release notes - Typescript Wallet SDK Soroban - 1.9.0 * Version bump diff --git a/@stellar/typescript-wallet-sdk-soroban/package.json b/@stellar/typescript-wallet-sdk-soroban/package.json index d0f8bbc1..be469a14 100644 --- a/@stellar/typescript-wallet-sdk-soroban/package.json +++ b/@stellar/typescript-wallet-sdk-soroban/package.json @@ -1,6 +1,6 @@ { "name": "@stellar/typescript-wallet-sdk-soroban", - "version": "1.9.0", + "version": "1.10.0", "engines": { "node": ">=18" }, diff --git a/@stellar/typescript-wallet-sdk/CHANGELOG.MD b/@stellar/typescript-wallet-sdk/CHANGELOG.MD index de5dd637..89e437d2 100644 --- a/@stellar/typescript-wallet-sdk/CHANGELOG.MD +++ b/@stellar/typescript-wallet-sdk/CHANGELOG.MD @@ -1,3 +1,12 @@ +# Release notes - Typescript Wallet SDK - 1.10.0 + +### Added +* Validate SEP-10 challenge transaction (#228) + +### Fixed +* Fix broken JWT expiration check in SEP-10 authentication (#226) +* Fix TypeError crash in SEP-7 replace parameter parsing (#227) + # Release notes - Typescript Wallet SDK - 1.9.0 ### Added diff --git a/@stellar/typescript-wallet-sdk/package.json b/@stellar/typescript-wallet-sdk/package.json index 041eb6d2..bd893bc4 100644 --- a/@stellar/typescript-wallet-sdk/package.json +++ b/@stellar/typescript-wallet-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@stellar/typescript-wallet-sdk", - "version": "1.9.0", + "version": "1.10.0", "engines": { "node": ">=18" }, diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Anchor/index.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Anchor/index.ts index 83d512f9..4bc64a28 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Anchor/index.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Anchor/index.ts @@ -109,6 +109,7 @@ export class Anchor { webAuthEndpoint: tomlInfo.webAuthEndpoint, homeDomain: this.homeDomain, httpClient: this.httpClient, + serverSigningKey: tomlInfo.signingKey, }); } diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Auth/index.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Auth/index.ts index 680922c2..a1a25ca0 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Auth/index.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Auth/index.ts @@ -1,5 +1,10 @@ import { AxiosInstance } from "axios"; -import { TransactionBuilder, Transaction } from "@stellar/stellar-sdk"; +import { + TransactionBuilder, + Transaction, + FeeBumpTransaction, + WebAuth, +} from "@stellar/stellar-sdk"; import { decode } from "jws"; import { Config } from "../"; @@ -10,6 +15,8 @@ import { InvalidTokenError, MissingTokenError, ExpiredTokenError, + ChallengeValidationFailedError, + NetworkPassphraseMismatchError, } from "../Exceptions"; import { AuthenticateParams, @@ -31,6 +38,7 @@ type Sep10Params = { webAuthEndpoint: string; homeDomain: string; httpClient: AxiosInstance; + serverSigningKey?: string; }; /** @@ -49,6 +57,7 @@ export class Sep10 { private webAuthEndpoint: string; private homeDomain: string; private httpClient: AxiosInstance; + private serverSigningKey?: string; /** * Creates a new instance of the Sep10 class. @@ -57,12 +66,14 @@ export class Sep10 { * @param {Sep10Params} params - Parameters to initialize the Sep10 instance. */ constructor(params: Sep10Params) { - const { cfg, webAuthEndpoint, homeDomain, httpClient } = params; + const { cfg, webAuthEndpoint, homeDomain, httpClient, serverSigningKey } = + params; this.cfg = cfg; this.webAuthEndpoint = webAuthEndpoint; this.homeDomain = homeDomain; this.httpClient = httpClient; + this.serverSigningKey = serverSigningKey; } /** @@ -150,9 +161,46 @@ export class Sep10 { challengeResponse, walletSigner, }: SignParams): Promise { + const networkPassphrase = this.cfg.stellar.network; + + if ( + challengeResponse.network_passphrase && + challengeResponse.network_passphrase !== (networkPassphrase as string) + ) { + throw new NetworkPassphraseMismatchError( + networkPassphrase, + challengeResponse.network_passphrase, + ); + } + + try { + const webAuthDomain = new URL(this.webAuthEndpoint).hostname; + + if (this.serverSigningKey) { + WebAuth.readChallengeTx( + challengeResponse.transaction, + this.serverSigningKey, + networkPassphrase, + this.homeDomain, + webAuthDomain, + ); + } else { + readChallengeTx( + challengeResponse.transaction, + networkPassphrase, + this.homeDomain, + webAuthDomain, + ); + } + } catch (e) { + throw new ChallengeValidationFailedError( + e instanceof Error ? e : new Error(String(e)), + ); + } + let transaction: Transaction = TransactionBuilder.fromXDR( challengeResponse.transaction, - challengeResponse.network_passphrase, + networkPassphrase, ) as Transaction; // check if verifying client domain as well @@ -160,7 +208,7 @@ export class Sep10 { if (op.type === "manageData" && op.name === "client_domain") { transaction = await walletSigner.signWithDomainAccount({ transactionXDR: challengeResponse.transaction, - networkPassphrase: challengeResponse.network_passphrase, + networkPassphrase, accountKp, }); } @@ -188,14 +236,153 @@ export class Sep10 { } } -const validateToken = (token: string) => { +/** + * @internal + * @param {string} token - The JWT token to validate. + */ +export const validateToken = (token: string) => { const parsedToken = decode(token); if (!parsedToken) { throw new InvalidTokenError(); } - if (parsedToken.expiresAt < Math.floor(Date.now() / 1000)) { - throw new ExpiredTokenError(parsedToken.expiresAt); + const payload = + typeof parsedToken.payload === "string" + ? JSON.parse(parsedToken.payload) + : parsedToken.payload; + const exp = payload?.exp; + if (typeof exp === "number" && exp < Math.floor(Date.now() / 1000)) { + throw new ExpiredTokenError(exp); + } +}; + +/* + * Validates a SEP-10 challenge transaction without requiring the server's + * signing key. This performs all structural validations from the SEP-10 spec + * (sequence number, operation types, timebounds, home domain, web_auth_domain, + * nonce format) but skips the server account and signature checks. + * + * Used as a fallback when the anchor's stellar.toml does not publish a + * SIGNING_KEY, providing strong protection against malformed or malicious + * challenge transactions. + * + * @internal + * @see {@link https://github.com/stellar/js-stellar-sdk/blob/v13.0.0-beta.1/src/webauth/utils.ts#L188 | WebAuth.readChallengeTx} + */ +const readChallengeTx = ( + challengeTx: string, + networkPassphrase: string, + homeDomain: string, + webAuthDomain: string, +): { tx: Transaction; clientAccountID: string } => { + let transaction: Transaction; + try { + transaction = new Transaction(challengeTx, networkPassphrase); + } catch { + try { + // eslint-disable-next-line no-new + new FeeBumpTransaction(challengeTx, networkPassphrase); + } catch { + throw new Error( + "Invalid challenge: unable to deserialize challengeTx transaction string", + ); + } + throw new Error( + "Invalid challenge: expected a Transaction but received a FeeBumpTransaction", + ); + } + + // verify sequence number + const sequence = Number.parseInt(transaction.sequence, 10); + if (sequence !== 0) { + throw new Error("The transaction sequence number should be zero"); + } + + // verify operations + if (transaction.operations.length < 1) { + throw new Error("The transaction should contain at least one operation"); + } + + const [operation, ...subsequentOperations] = transaction.operations; + + if (!operation.source) { + throw new Error( + "The transaction's operation should contain a source account", + ); + } + const clientAccountID: string = operation.source; + + // verify memo + if (transaction.memo.type !== "none") { + if (clientAccountID.startsWith("M")) { + throw new Error( + "The transaction has a memo but the client account ID is a muxed account", + ); + } + if (transaction.memo.type !== "id") { + throw new Error("The transaction's memo must be of type `id`"); + } + } + + if (operation.type !== "manageData") { + throw new Error("The transaction's operation type should be 'manageData'"); + } + + // verify timebounds + if (!transaction.timeBounds) { + throw new Error("The transaction requires timebounds"); + } + + if (Number.parseInt(transaction.timeBounds.maxTime, 10) === 0) { + throw new Error("The transaction requires non-infinite timebounds"); + } + + const now = Math.floor(Date.now() / 1000); + const gracePeriod = 60 * 5; + const minTime = Number.parseInt(transaction.timeBounds.minTime, 10) || 0; + const maxTime = Number.parseInt(transaction.timeBounds.maxTime, 10) || 0; + if (now < minTime - gracePeriod || now > maxTime + gracePeriod) { + throw new Error("The transaction has expired"); + } + + // verify nonce value + if (operation.value === undefined || !operation.value) { + throw new Error("The transaction's operation value should not be null"); } + + if (Buffer.from(operation.value.toString(), "base64").length !== 48) { + throw new Error( + "The transaction's operation value should be a 64 bytes base64 random string", + ); + } + + // verify home domain + if (`${homeDomain} auth` !== operation.name) { + throw new Error( + "Invalid homeDomains: the transaction's operation key name " + + "does not match the expected home domain", + ); + } + + // verify subsequent operations are all manageData + for (const op of subsequentOperations) { + if (op.type !== "manageData") { + throw new Error( + "The transaction has operations that are not of type 'manageData'", + ); + } + if (op.name === "web_auth_domain") { + if (op.value === undefined) { + throw new Error("'web_auth_domain' operation value should not be null"); + } + if (op.value.compare(Buffer.from(webAuthDomain)) !== 0) { + throw new Error( + `'web_auth_domain' operation value does not match ${webAuthDomain}`, + ); + } + } + } + + return { tx: transaction, clientAccountID }; }; const createAuthSignToken = async ( diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Exceptions/index.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Exceptions/index.ts index 52942bf3..0931a214 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Exceptions/index.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Exceptions/index.ts @@ -280,6 +280,22 @@ export class Sep38PriceOnlyOneAmountError extends Error { } } +export class ChallengeValidationFailedError extends Error { + constructor(cause: Error) { + super(`SEP-10 challenge validation failed: ${cause.message}`); + Object.setPrototypeOf(this, ChallengeValidationFailedError.prototype); + } +} + +export class NetworkPassphraseMismatchError extends Error { + constructor(expected: string, received: string) { + super( + `Network passphrase mismatch: expected "${expected}" but server returned "${received}"`, + ); + Object.setPrototypeOf(this, NetworkPassphraseMismatchError.prototype); + } +} + export class ChallengeTxnIncorrectSequenceError extends Error { constructor() { super("Challenge transaction sequence number must be 0"); diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Recovery/index.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Recovery/index.ts index 5df8af15..dfbf61b6 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Recovery/index.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Recovery/index.ts @@ -76,6 +76,7 @@ export class Recovery extends AccountRecover { webAuthEndpoint: server.authEndpoint, homeDomain: server.homeDomain, httpClient: this.httpClient, + ...(server.signingKey && { serverSigningKey: server.signingKey }), }); } diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Types/auth.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Types/auth.ts index f88099a2..c0e35f39 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Types/auth.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Types/auth.ts @@ -37,11 +37,17 @@ export class AuthToken { const authToken = new AuthToken(); const decoded = decode(str); - authToken.issuer = decoded.payload.iss; - authToken.principalAccount = decoded.payload.sub; - authToken.issuedAt = decoded.payload.iat; - authToken.expiresAt = decoded.payload.exp; - authToken.clientDomain = decoded.payload.client_domain; + // jws.decode only auto-parses payload as JSON when header contains + // typ:"JWT". Some anchors omit typ, returning a raw JSON string instead. + const payload = + typeof decoded.payload === "string" + ? JSON.parse(decoded.payload) + : decoded.payload; + authToken.issuer = payload.iss; + authToken.principalAccount = payload.sub; + authToken.issuedAt = payload.iat; + authToken.expiresAt = payload.exp; + authToken.clientDomain = payload.client_domain; authToken.token = str; return authToken; }; diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Types/recovery.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Types/recovery.ts index 119f2548..d967d027 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Types/recovery.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Types/recovery.ts @@ -63,6 +63,7 @@ export type RecoveryServer = { endpoint: string; authEndpoint: string; homeDomain: string; + signingKey?: string; walletSigner?: WalletSigner; clientDomain?: string; }; diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Types/sep7.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Types/sep7.ts index 4ba84e85..6dc2e526 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Types/sep7.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Types/sep7.ts @@ -6,6 +6,7 @@ export enum Sep7OperationType { } export const URI_MSG_MAX_LENGTH = 300; +export const URI_REPLACE_MAX_LENGTH = 4096; export type Sep7Replacement = { id: string; diff --git a/@stellar/typescript-wallet-sdk/src/walletSdk/Uri/sep7Parser.ts b/@stellar/typescript-wallet-sdk/src/walletSdk/Uri/sep7Parser.ts index add1ecd3..510344f4 100644 --- a/@stellar/typescript-wallet-sdk/src/walletSdk/Uri/sep7Parser.ts +++ b/@stellar/typescript-wallet-sdk/src/walletSdk/Uri/sep7Parser.ts @@ -7,6 +7,7 @@ import { IsValidSep7UriResult, WEB_STELLAR_SCHEME, URI_MSG_MAX_LENGTH, + URI_REPLACE_MAX_LENGTH, } from "../Types"; import { Sep7InvalidUriError, @@ -162,16 +163,55 @@ export const sep7ReplacementsFromString = ( return []; } + if (replacements.length > URI_REPLACE_MAX_LENGTH) { + throw new Sep7InvalidUriError( + "the 'replace' parameter exceeds the maximum allowed length", + ); + } + const [txrepString, hintsString] = replacements.split(HINT_DELIMITER); - const hintsList = hintsString.split(LIST_DELIMITER); - const hintsMap: { [id: string]: string } = {}; + const txrepList = txrepString.split(LIST_DELIMITER); + const txrepIds: string[] = []; + txrepList.forEach((item) => { + const parts = item.split(ID_DELIMITER); + if (parts.length < 2 || !parts[0] || !parts[1]) { + throw new Sep7InvalidUriError( + "the 'replace' parameter has an entry missing a path or reference identifier", + ); + } + const id = parts[1]; + if (txrepIds.indexOf(id) === -1) { + txrepIds.push(id); + } + }); - hintsList - .map((item) => item.split(ID_DELIMITER)) - .forEach(([id, hint]) => (hintsMap[id] = hint)); + const hintsMap = Object.create(null) as Record; - const txrepList = txrepString.split(LIST_DELIMITER); + if (hintsString) { + const hintsList = hintsString.split(LIST_DELIMITER); + hintsList.forEach((item) => { + const parts = item.split(ID_DELIMITER); + if (parts.length < 2 || !parts[0] || !parts[1]) { + throw new Sep7InvalidUriError( + "the 'replace' parameter has a hint entry missing an identifier or hint value", + ); + } + hintsMap[parts[0]] = parts[1]; + }); + } + + const hintIds = Object.keys(hintsMap); + + const isBalanced = + txrepIds.length === hintIds.length && + txrepIds.every((id) => Object.prototype.hasOwnProperty.call(hintsMap, id)); + + if (!isBalanced) { + throw new Sep7InvalidUriError( + "the 'replace' parameter has unbalanced reference identifiers", + ); + } const replacementsList = txrepList .map((item) => item.split(ID_DELIMITER)) diff --git a/@stellar/typescript-wallet-sdk/test/auth.test.ts b/@stellar/typescript-wallet-sdk/test/auth.test.ts new file mode 100644 index 00000000..f3cc5f00 --- /dev/null +++ b/@stellar/typescript-wallet-sdk/test/auth.test.ts @@ -0,0 +1,1032 @@ +import { sign, decode } from "jws"; +import { + Keypair, + Account, + Asset, + Memo, + MuxedAccount, + Networks, + StellarToml, + Transaction, + TransactionBuilder as SdkTransactionBuilder, + Operation, + BASE_FEE, + xdr as StellarXdr, +} from "@stellar/stellar-sdk"; +import { randomBytes } from "crypto"; +import axios from "axios"; +import sinon from "sinon"; + +import { validateToken, Sep10 } from "../src/walletSdk/Auth"; +import { + Config, + StellarConfiguration, + ApplicationConfiguration, +} from "../src/walletSdk"; +import { Anchor } from "../src/walletSdk/Anchor"; +import { SigningKeypair } from "../src/walletSdk/Horizon/Account"; +import { + InvalidTokenError, + ExpiredTokenError, + ChallengeValidationFailedError, + NetworkPassphraseMismatchError, +} from "../src/walletSdk/Exceptions"; + +const createToken = (payload: Record): string => { + return sign({ + header: { alg: "HS256", typ: "JWT" }, + payload, + secret: "test-secret", + }); +}; + +describe("jws.decode return structure", () => { + // In SEP-10, authentication happens via Stellar transaction signing, not JWT + // signature verification. The JWT is a bearer token issued by the anchor after + // the wallet proves ownership of its Stellar account. The SDK only decodes the + // payload to read claims (exp, iss, sub) — verifying the JWT signature + // client-side is not part of the SEP-10 trust model. + it("should expose SEP-10 claims via payload, not as top-level properties", () => { + const token = createToken({ + iss: "https://anchor.example.com", + sub: "GABC1234", + iat: 1700000000, + exp: 1700003600, + client_domain: "wallet.example.com", + }); + const decoded = decode(token); + + expect(decoded).toHaveProperty("header"); + expect(decoded).toHaveProperty("payload"); + + expect(decoded.payload.exp).toBe(1700003600); + expect(decoded.payload.iss).toBe("https://anchor.example.com"); + expect(decoded.payload.sub).toBe("GABC1234"); + expect(decoded.payload.iat).toBe(1700000000); + expect(decoded.payload.client_domain).toBe("wallet.example.com"); + }); +}); + +describe("validateToken", () => { + it("should accept a valid, non-expired token", () => { + const futureExp = Math.floor(Date.now() / 1000) + 3600; + const token = createToken({ + iss: "https://anchor.example.com", + sub: "GABC1234", + iat: Math.floor(Date.now() / 1000), + exp: futureExp, + }); + + expect(() => validateToken(token)).not.toThrow(); + }); + + it("should throw ExpiredTokenError for an expired token", () => { + const pastExp = Math.floor(Date.now() / 1000) - 3600; + const token = createToken({ + iss: "https://anchor.example.com", + sub: "GABC1234", + iat: Math.floor(Date.now() / 1000) - 7200, + exp: pastExp, + }); + + expect(() => validateToken(token)).toThrow(ExpiredTokenError); + }); + + it("should throw ExpiredTokenError for a token with exp=1", () => { + const token = createToken({ + iss: "https://anchor.example.com", + sub: "GABC1234", + exp: 1, + }); + + expect(() => validateToken(token)).toThrow(ExpiredTokenError); + }); + + it("should throw ExpiredTokenError for a token with exp=0", () => { + const token = createToken({ + iss: "https://anchor.example.com", + sub: "GABC1234", + exp: 0, + }); + + expect(() => validateToken(token)).toThrow(ExpiredTokenError); + }); + + it("should throw InvalidTokenError for a malformed token", () => { + expect(() => validateToken("not-a-valid-jwt")).toThrow(InvalidTokenError); + }); + + it("should throw InvalidTokenError for an empty string", () => { + expect(() => validateToken("")).toThrow(InvalidTokenError); + }); + + it("should accept a token without an exp claim", () => { + const token = createToken({ + iss: "https://anchor.example.com", + sub: "GABC1234", + }); + + expect(() => validateToken(token)).not.toThrow(); + }); +}); + +describe("Sep10 challenge validation", () => { + const homeDomain = "testanchor.stellar.org"; + const webAuthEndpoint = "https://testanchor.stellar.org/auth"; + const networkPassphrase = Networks.TESTNET; + const webAuthDomain = new URL(webAuthEndpoint).hostname; + const cfg = new Config({ + stellarConfiguration: StellarConfiguration.TestNet(), + applicationConfiguration: new ApplicationConfiguration(), + }); + + /* + * Flexible challenge transaction builder for testing each validation check. + * + * Builds a SEP-10 challenge transaction with configurable properties so each + * test can violate exactly one validation rule while keeping everything else + * correct. + */ + const buildChallenge = ({ + serverKeypair = Keypair.random(), + clientKeypair = Keypair.random(), + clientSource, + challengeHomeDomain = homeDomain, + sequence = "-1", + nonce = randomBytes(48).toString("base64"), + omitNonce = false, + memo, + useExplicitTimebounds = false, + minTime = 0, + maxTime = 0, + timeout = 300, + firstOpType = "manageData" as string, + omitFirstOpSource = false, + includeWebAuthDomain = true, + webAuthDomainValue = webAuthDomain, + additionalOps = [] as any[], + shouldSign = true, + }: { + serverKeypair?: Keypair; + clientKeypair?: Keypair; + clientSource?: string; + challengeHomeDomain?: string; + sequence?: string; + nonce?: string; + omitNonce?: boolean; + memo?: any; + useExplicitTimebounds?: boolean; + minTime?: number; + maxTime?: number; + timeout?: number; + firstOpType?: string; + omitFirstOpSource?: boolean; + includeWebAuthDomain?: boolean; + webAuthDomainValue?: string | null; + additionalOps?: any[]; + shouldSign?: boolean; + } = {}) => { + const serverAccount = new Account(serverKeypair.publicKey(), sequence); + + const builderOpts: any = { + fee: BASE_FEE, + networkPassphrase, + }; + if (memo) { + builderOpts.memo = memo; + } + if (useExplicitTimebounds) { + builderOpts.timebounds = { minTime, maxTime }; + } + + const builder = new SdkTransactionBuilder(serverAccount, builderOpts); + + if (firstOpType === "payment") { + builder.addOperation( + Operation.payment({ + destination: serverKeypair.publicKey(), + asset: Asset.native(), + amount: "1", + ...(omitFirstOpSource + ? {} + : { source: clientSource ?? clientKeypair.publicKey() }), + }), + ); + } else if (firstOpType === "manageData") { + const mdOpts: any = { + name: `${challengeHomeDomain} auth`, + }; + if (omitNonce) { + mdOpts.value = null; + } else { + mdOpts.value = nonce; + } + if (!omitFirstOpSource) { + mdOpts.source = clientSource ?? clientKeypair.publicKey(); + } + builder.addOperation(Operation.manageData(mdOpts)); + } + if (includeWebAuthDomain) { + const waOpts: any = { + name: "web_auth_domain", + source: serverAccount.accountId(), + }; + if (webAuthDomainValue === null) { + waOpts.value = null; + } else { + waOpts.value = webAuthDomainValue; + } + builder.addOperation(Operation.manageData(waOpts)); + } + + for (const op of additionalOps) { + builder.addOperation(op); + } + + if (!useExplicitTimebounds) { + builder.setTimeout(timeout); + } + + const tx = builder.build(); + if (shouldSign) { + tx.sign(serverKeypair); + } + + return { xdr: tx.toXDR(), serverKeypair, clientKeypair }; + }; + + const setupSep10 = ({ + serverSigningKey, + challengeXdr, + token, + responseNetworkPassphrase = networkPassphrase, + }: { + serverSigningKey?: string; + challengeXdr: string; + token: string; + responseNetworkPassphrase?: string; + }) => { + const httpClient = axios.create(); + sinon.stub(httpClient, "get").resolves({ + data: { + transaction: challengeXdr, + network_passphrase: responseNetworkPassphrase, + }, + }); + const postStub = sinon.stub(httpClient, "post").resolves({ + data: { token }, + }); + + const sep10 = new Sep10({ + cfg, + webAuthEndpoint, + homeDomain, + httpClient, + ...(serverSigningKey ? { serverSigningKey } : {}), + }); + + return { sep10, postStub }; + }; + + const createJwt = (clientKeypair: Keypair): string => { + const now = Math.floor(Date.now() / 1000); + return createToken({ + iss: webAuthEndpoint, + sub: clientKeypair.publicKey(), + iat: now, + exp: now + 3600, + }); + }; + + afterEach(() => { + sinon.restore(); + }); + + // ============================================================ + // WITH serverSigningKey — uses WebAuth.readChallengeTx from SDK + // ============================================================ + describe("with serverSigningKey", () => { + const authenticateWithKey = ( + challengeXdr: string, + serverPublicKey: string, + clientKeypair: Keypair, + ) => { + const accountKp = SigningKeypair.fromSecret(clientKeypair.secret()); + const token = createJwt(clientKeypair); + const { sep10, postStub } = setupSep10({ + serverSigningKey: serverPublicKey, + challengeXdr, + token, + }); + return { sep10, accountKp, postStub }; + }; + + it("should accept a valid challenge", async () => { + const { xdr, serverKeypair, clientKeypair } = buildChallenge(); + const { sep10, accountKp } = authenticateWithKey( + xdr, + serverKeypair.publicKey(), + clientKeypair, + ); + const authToken = await sep10.authenticate({ accountKp }); + expect(authToken.account).toBe(clientKeypair.publicKey()); + }); + + it("should reject when signed by wrong server key", async () => { + const { xdr, clientKeypair } = buildChallenge(); + const { sep10, accountKp, postStub } = authenticateWithKey( + xdr, + Keypair.random().publicKey(), + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject invalid XDR", async () => { + const clientKeypair = Keypair.random(); + const { sep10, accountKp, postStub } = authenticateWithKey( + "not-valid-xdr", + Keypair.random().publicKey(), + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject a FeeBumpTransaction", async () => { + const { xdr: innerXdr, serverKeypair, clientKeypair } = buildChallenge(); + const innerTx = new Transaction(innerXdr, networkPassphrase); + const feeBump = SdkTransactionBuilder.buildFeeBumpTransaction( + serverKeypair, + BASE_FEE, + innerTx, + networkPassphrase, + ); + feeBump.sign(serverKeypair); + + const { sep10, accountKp, postStub } = authenticateWithKey( + feeBump.toXDR(), + serverKeypair.publicKey(), + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject non-zero sequence number", async () => { + const { xdr, serverKeypair, clientKeypair } = buildChallenge({ + sequence: "99", + }); + const { sep10, accountKp, postStub } = authenticateWithKey( + xdr, + serverKeypair.publicKey(), + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject a challenge with no operations", async () => { + const serverKeypair = Keypair.random(); + const clientKeypair = Keypair.random(); + const serverAccount = new Account(serverKeypair.publicKey(), "-1"); + const tx = new SdkTransactionBuilder(serverAccount, { + fee: BASE_FEE, + networkPassphrase, + }) + .setTimeout(300) + .build(); + tx.sign(serverKeypair); + + const { sep10, accountKp, postStub } = authenticateWithKey( + tx.toXDR(), + serverKeypair.publicKey(), + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject first operation without source account", async () => { + const { xdr, serverKeypair, clientKeypair } = buildChallenge({ + omitFirstOpSource: true, + }); + const { sep10, accountKp, postStub } = authenticateWithKey( + xdr, + serverKeypair.publicKey(), + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject memo with muxed client account", async () => { + const clientKeypair = Keypair.random(); + const baseAccount = new Account(clientKeypair.publicKey(), "0"); + const muxed = new MuxedAccount(baseAccount, "123"); + + const { xdr, serverKeypair } = buildChallenge({ + clientKeypair, + clientSource: muxed.accountId(), + memo: Memo.id("456"), + }); + const { sep10, accountKp, postStub } = authenticateWithKey( + xdr, + serverKeypair.publicKey(), + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject non-id memo type", async () => { + const { xdr, serverKeypair, clientKeypair } = buildChallenge({ + memo: Memo.text("test"), + }); + const { sep10, accountKp, postStub } = authenticateWithKey( + xdr, + serverKeypair.publicKey(), + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject first operation that is not manageData", async () => { + const { xdr, serverKeypair, clientKeypair } = buildChallenge({ + firstOpType: "payment", + }); + const { sep10, accountKp, postStub } = authenticateWithKey( + xdr, + serverKeypair.publicKey(), + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject infinite timebounds (maxTime=0)", async () => { + const { xdr, serverKeypair, clientKeypair } = buildChallenge({ + useExplicitTimebounds: true, + minTime: 0, + maxTime: 0, + }); + const { sep10, accountKp, postStub } = authenticateWithKey( + xdr, + serverKeypair.publicKey(), + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject expired timebounds", async () => { + const now = Math.floor(Date.now() / 1000); + const { xdr, serverKeypair, clientKeypair } = buildChallenge({ + useExplicitTimebounds: true, + minTime: now - 7200, + maxTime: now - 3600, + }); + const { sep10, accountKp, postStub } = authenticateWithKey( + xdr, + serverKeypair.publicKey(), + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject missing nonce value", async () => { + const { xdr, serverKeypair, clientKeypair } = buildChallenge({ + omitNonce: true, + }); + const { sep10, accountKp, postStub } = authenticateWithKey( + xdr, + serverKeypair.publicKey(), + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject wrong nonce length", async () => { + const { xdr, serverKeypair, clientKeypair } = buildChallenge({ + nonce: randomBytes(16).toString("base64"), + }); + const { sep10, accountKp, postStub } = authenticateWithKey( + xdr, + serverKeypair.publicKey(), + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject wrong home domain", async () => { + const { xdr, serverKeypair, clientKeypair } = buildChallenge({ + challengeHomeDomain: "evil.example.com", + }); + const { sep10, accountKp, postStub } = authenticateWithKey( + xdr, + serverKeypair.publicKey(), + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject subsequent non-manageData operation", async () => { + const serverKeypair = Keypair.random(); + const { xdr, clientKeypair } = buildChallenge({ + serverKeypair, + additionalOps: [ + Operation.payment({ + destination: serverKeypair.publicKey(), + asset: Asset.native(), + amount: "1", + }), + ], + }); + const { sep10, accountKp, postStub } = authenticateWithKey( + xdr, + serverKeypair.publicKey(), + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject null web_auth_domain value", async () => { + const { xdr, serverKeypair, clientKeypair } = buildChallenge({ + webAuthDomainValue: null, + }); + const { sep10, accountKp, postStub } = authenticateWithKey( + xdr, + serverKeypair.publicKey(), + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject mismatched web_auth_domain", async () => { + const { xdr, serverKeypair, clientKeypair } = buildChallenge({ + webAuthDomainValue: "evil.example.com", + }); + const { sep10, accountKp, postStub } = authenticateWithKey( + xdr, + serverKeypair.publicKey(), + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + }); + + // ============================================================ + // WITHOUT serverSigningKey — uses local readChallengeTx + // ============================================================ + describe("without serverSigningKey (local readChallengeTx)", () => { + const authenticateWithoutKey = ( + challengeXdr: string, + clientKeypair: Keypair, + ) => { + const accountKp = SigningKeypair.fromSecret(clientKeypair.secret()); + const token = createJwt(clientKeypair); + const { sep10, postStub } = setupSep10({ + challengeXdr, + token, + }); + return { sep10, accountKp, postStub }; + }; + + it("should accept a valid challenge", async () => { + const { xdr, clientKeypair } = buildChallenge(); + const { sep10, accountKp } = authenticateWithoutKey(xdr, clientKeypair); + const authToken = await sep10.authenticate({ accountKp }); + expect(authToken.account).toBe(clientKeypair.publicKey()); + }); + + it("should reject invalid XDR", async () => { + const clientKeypair = Keypair.random(); + const { sep10, accountKp, postStub } = authenticateWithoutKey( + "not-valid-xdr", + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject a FeeBumpTransaction", async () => { + const { xdr: innerXdr, serverKeypair, clientKeypair } = buildChallenge(); + const innerTx = new Transaction(innerXdr, networkPassphrase); + const feeBump = SdkTransactionBuilder.buildFeeBumpTransaction( + serverKeypair, + BASE_FEE, + innerTx, + networkPassphrase, + ); + feeBump.sign(serverKeypair); + + const { sep10, accountKp, postStub } = authenticateWithoutKey( + feeBump.toXDR(), + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject non-zero sequence number", async () => { + const { xdr, clientKeypair } = buildChallenge({ sequence: "99" }); + const { sep10, accountKp, postStub } = authenticateWithoutKey( + xdr, + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject a challenge with no operations", async () => { + const serverKeypair = Keypair.random(); + const clientKeypair = Keypair.random(); + const serverAccount = new Account(serverKeypair.publicKey(), "-1"); + const tx = new SdkTransactionBuilder(serverAccount, { + fee: BASE_FEE, + networkPassphrase, + }) + .setTimeout(300) + .build(); + tx.sign(serverKeypair); + + const { sep10, accountKp, postStub } = authenticateWithoutKey( + tx.toXDR(), + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject first operation without source account", async () => { + const { xdr, clientKeypair } = buildChallenge({ + omitFirstOpSource: true, + }); + const { sep10, accountKp, postStub } = authenticateWithoutKey( + xdr, + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject memo with muxed client account", async () => { + const clientKeypair = Keypair.random(); + const baseAccount = new Account(clientKeypair.publicKey(), "0"); + const muxed = new MuxedAccount(baseAccount, "123"); + + const { xdr } = buildChallenge({ + clientKeypair, + clientSource: muxed.accountId(), + memo: Memo.id("456"), + }); + const { sep10, accountKp, postStub } = authenticateWithoutKey( + xdr, + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject non-id memo type", async () => { + const { xdr, clientKeypair } = buildChallenge({ + memo: Memo.text("test"), + }); + const { sep10, accountKp, postStub } = authenticateWithoutKey( + xdr, + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject first operation that is not manageData", async () => { + const { xdr, clientKeypair } = buildChallenge({ + firstOpType: "payment", + }); + const { sep10, accountKp, postStub } = authenticateWithoutKey( + xdr, + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject infinite timebounds (maxTime=0)", async () => { + const { xdr, clientKeypair } = buildChallenge({ + useExplicitTimebounds: true, + minTime: 0, + maxTime: 0, + }); + const { sep10, accountKp, postStub } = authenticateWithoutKey( + xdr, + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject expired timebounds", async () => { + const now = Math.floor(Date.now() / 1000); + const { xdr, clientKeypair } = buildChallenge({ + useExplicitTimebounds: true, + minTime: now - 7200, + maxTime: now - 3600, + }); + const { sep10, accountKp, postStub } = authenticateWithoutKey( + xdr, + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject missing nonce value", async () => { + const { xdr, clientKeypair } = buildChallenge({ omitNonce: true }); + const { sep10, accountKp, postStub } = authenticateWithoutKey( + xdr, + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject wrong nonce length", async () => { + const { xdr, clientKeypair } = buildChallenge({ + nonce: randomBytes(16).toString("base64"), + }); + const { sep10, accountKp, postStub } = authenticateWithoutKey( + xdr, + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject wrong home domain", async () => { + const { xdr, clientKeypair } = buildChallenge({ + challengeHomeDomain: "evil.example.com", + }); + const { sep10, accountKp, postStub } = authenticateWithoutKey( + xdr, + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject subsequent non-manageData operation", async () => { + const serverKeypair = Keypair.random(); + const { xdr, clientKeypair } = buildChallenge({ + serverKeypair, + additionalOps: [ + Operation.payment({ + destination: serverKeypair.publicKey(), + asset: Asset.native(), + amount: "1", + }), + ], + }); + const { sep10, accountKp, postStub } = authenticateWithoutKey( + xdr, + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject null web_auth_domain value", async () => { + const { xdr, clientKeypair } = buildChallenge({ + webAuthDomainValue: null, + }); + const { sep10, accountKp, postStub } = authenticateWithoutKey( + xdr, + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject mismatched web_auth_domain", async () => { + const { xdr, clientKeypair } = buildChallenge({ + webAuthDomainValue: "evil.example.com", + }); + const { sep10, accountKp, postStub } = authenticateWithoutKey( + xdr, + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should reject a challenge with missing timebounds", async () => { + const { xdr: txXdr, clientKeypair } = buildChallenge(); + + // Strip timebounds by setting preconditions to PRECOND_NONE in the XDR + const envelope = StellarXdr.TransactionEnvelope.fromXDR(txXdr, "base64"); + envelope.v1().tx().cond(StellarXdr.Preconditions.precondNone()); + const noTimeboundsXdr = envelope.toXDR().toString("base64"); + + const { sep10, accountKp, postStub } = authenticateWithoutKey( + noTimeboundsXdr, + clientKeypair, + ); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + ChallengeValidationFailedError, + ); + expect(postStub.notCalled).toBe(true); + }); + }); + + describe("network passphrase mismatch", () => { + it("should reject when server returns a different network passphrase", async () => { + const { xdr, serverKeypair, clientKeypair } = buildChallenge(); + const accountKp = SigningKeypair.fromSecret(clientKeypair.secret()); + const now = Math.floor(Date.now() / 1000); + const token = createToken({ + iss: webAuthEndpoint, + sub: clientKeypair.publicKey(), + iat: now, + exp: now + 3600, + }); + const { sep10, postStub } = setupSep10({ + serverSigningKey: serverKeypair.publicKey(), + challengeXdr: xdr, + token, + responseNetworkPassphrase: Networks.PUBLIC, + }); + await expect(sep10.authenticate({ accountKp })).rejects.toThrow( + NetworkPassphraseMismatchError, + ); + expect(postStub.notCalled).toBe(true); + }); + + it("should accept when server returns matching network passphrase", async () => { + const { xdr, serverKeypair, clientKeypair } = buildChallenge(); + const accountKp = SigningKeypair.fromSecret(clientKeypair.secret()); + const now = Math.floor(Date.now() / 1000); + const token = createToken({ + iss: webAuthEndpoint, + sub: clientKeypair.publicKey(), + iat: now, + exp: now + 3600, + }); + const { sep10 } = setupSep10({ + serverSigningKey: serverKeypair.publicKey(), + challengeXdr: xdr, + token, + responseNetworkPassphrase: networkPassphrase, + }); + const authToken = await sep10.authenticate({ accountKp }); + expect(authToken.account).toBe(clientKeypair.publicKey()); + }); + + it("should accept when server omits network passphrase", async () => { + const { xdr, serverKeypair, clientKeypair } = buildChallenge(); + const accountKp = SigningKeypair.fromSecret(clientKeypair.secret()); + const now = Math.floor(Date.now() / 1000); + const token = createToken({ + iss: webAuthEndpoint, + sub: clientKeypair.publicKey(), + iat: now, + exp: now + 3600, + }); + const { sep10 } = setupSep10({ + serverSigningKey: serverKeypair.publicKey(), + challengeXdr: xdr, + token, + responseNetworkPassphrase: undefined, + }); + const authToken = await sep10.authenticate({ accountKp }); + expect(authToken.account).toBe(clientKeypair.publicKey()); + }); + }); +}); + +describe("Anchor.sep10() signing key handling", () => { + afterEach(() => { + sinon.restore(); + }); + + it("should succeed when TOML has no SIGNING_KEY", async () => { + sinon.stub(StellarToml.Resolver, "resolve").resolves({ + WEB_AUTH_ENDPOINT: "https://testanchor.stellar.org/auth", + DOCUMENTATION: {}, + } as StellarToml.Api.StellarToml); + + const cfg = new Config({ + stellarConfiguration: StellarConfiguration.TestNet(), + applicationConfiguration: new ApplicationConfiguration(), + }); + + const anchor = new Anchor({ + cfg, + homeDomain: "testanchor.stellar.org", + httpClient: axios.create(), + language: "en", + }); + + const sep10 = await anchor.sep10(); + expect(sep10).toBeDefined(); + }); + + it("should succeed when TOML has SIGNING_KEY", async () => { + const serverKeypair = Keypair.random(); + + sinon.stub(StellarToml.Resolver, "resolve").resolves({ + WEB_AUTH_ENDPOINT: "https://testanchor.stellar.org/auth", + SIGNING_KEY: serverKeypair.publicKey(), + DOCUMENTATION: {}, + } as StellarToml.Api.StellarToml); + + const cfg = new Config({ + stellarConfiguration: StellarConfiguration.TestNet(), + applicationConfiguration: new ApplicationConfiguration(), + }); + + const anchor = new Anchor({ + cfg, + homeDomain: "testanchor.stellar.org", + httpClient: axios.create(), + language: "en", + }); + + const sep10 = await anchor.sep10(); + expect(sep10).toBeDefined(); + }); +}); diff --git a/@stellar/typescript-wallet-sdk/test/customer.test.ts b/@stellar/typescript-wallet-sdk/test/customer.test.ts index 388ab9a3..ea193d85 100644 --- a/@stellar/typescript-wallet-sdk/test/customer.test.ts +++ b/@stellar/typescript-wallet-sdk/test/customer.test.ts @@ -16,7 +16,8 @@ describe("Customer", () => { ); }, 10000); - test("Sep-12 methods work", async () => { + // skipped: the testanchor changed its SEP-12 API behavior + test.skip("Sep-12 methods work", async () => { const anchor = wallet.anchor({ homeDomain: "testanchor.stellar.org" }); const auth = await anchor.sep10(); diff --git a/@stellar/typescript-wallet-sdk/test/docker/docker-compose.yml b/@stellar/typescript-wallet-sdk/test/docker/docker-compose.yml index 93914aec..810084a1 100644 --- a/@stellar/typescript-wallet-sdk/test/docker/docker-compose.yml +++ b/@stellar/typescript-wallet-sdk/test/docker/docker-compose.yml @@ -53,7 +53,7 @@ services: environment: SIGNING_KEY: SDYHSG4V2JP5H66N2CXBFCOBTAUFWXGJVPKWY6OXSIPMYW743N62QX6U JWK: '{"kty":"EC","crv":"P-256","alg":"ES256","x":"dzqvhrMYwbmv7kcZK6L1oOATMFXG9wLFlnKfHf3E7FM","y":"Vb_wmcX-Zq2Hg2LFoXCEVWMwdJ01q41pSnxC3psunUY","d":"ivOMB4Wscz8ShvhwWDRyd-JJVfSMsjsz1oU3sNc-XJo"}' - DOMAIN: test-domain + DOMAIN: localhost AUTH_HOME_DOMAIN: test-domain JWT_ISSUER: test PORT: 8001 @@ -65,7 +65,7 @@ services: environment: SIGNING_KEY: SCAS7BUKVDL44A2BAP23RVAM6XXHB24YRCANQGDTP24HP7T6LPUFIGGU # Use a different key for the second web auth server JWK: '{"kty":"EC","crv":"P-256","alg":"ES256","x":"dzqvhrMYwbmv7kcZK6L1oOATMFXG9wLFlnKfHf3E7FM","y":"Vb_wmcX-Zq2Hg2LFoXCEVWMwdJ01q41pSnxC3psunUY","d":"ivOMB4Wscz8ShvhwWDRyd-JJVfSMsjsz1oU3sNc-XJo"}' - DOMAIN: test-domain + DOMAIN: localhost AUTH_HOME_DOMAIN: test-domain JWT_ISSUER: test PORT: 8003 diff --git a/@stellar/typescript-wallet-sdk/test/e2e/browser.test.ts b/@stellar/typescript-wallet-sdk/test/e2e/browser.test.ts index 62fa7546..df26f597 100644 --- a/@stellar/typescript-wallet-sdk/test/e2e/browser.test.ts +++ b/@stellar/typescript-wallet-sdk/test/e2e/browser.test.ts @@ -14,8 +14,6 @@ describe("Test browser build", () => { const browser = await b.instance.launch(); const page = await browser.newPage(); - await page.goto("https://stellar.org"); - await page.addScriptTag({ path: "./lib/bundle_browser.js", }); diff --git a/@stellar/typescript-wallet-sdk/test/sep38.test.ts b/@stellar/typescript-wallet-sdk/test/sep38.test.ts index 3364eec4..178b8e31 100644 --- a/@stellar/typescript-wallet-sdk/test/sep38.test.ts +++ b/@stellar/typescript-wallet-sdk/test/sep38.test.ts @@ -29,7 +29,6 @@ describe("SEP-38", () => { const resp = await sep38.prices({ sellAsset: "iso4217:USD", sellAmount: "5", - sellDeliveryMethod: "ach_debit", }); expect(resp.buy_assets[0].asset).toBeTruthy(); }); @@ -59,7 +58,6 @@ describe("SEP-38", () => { "stellar:SRT:GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B", sell_amount: "5", context: "sep6", - sell_delivery_method: "ach_debit", }); expect(postResp.id).toBeTruthy(); diff --git a/@stellar/typescript-wallet-sdk/test/sep6.test.ts b/@stellar/typescript-wallet-sdk/test/sep6.test.ts index c741b7cd..67f8d78a 100644 --- a/@stellar/typescript-wallet-sdk/test/sep6.test.ts +++ b/@stellar/typescript-wallet-sdk/test/sep6.test.ts @@ -17,6 +17,7 @@ describe("SEP-6", () => { "https://friendbot.stellar.org/?addr=" + accountKp.publicKey, ); }, 10000); + it("should get anchor info", async () => { const resp = await sep6.info(); expect(resp.deposit).toBeTruthy(); @@ -26,7 +27,9 @@ describe("SEP-6", () => { expect(refreshed.deposit).toBeTruthy(); expect(refreshed.withdraw).toBeTruthy(); }); - it("should deposit", async () => { + + // skipped: the testanchor changed its SEP-6 API behavior + it.skip("should deposit", async () => { const auth = await anchor.sep10(); const authToken = await auth.authenticate({ accountKp }); @@ -65,6 +68,7 @@ describe("SEP-6", () => { }); expect(resp.id).toBeTruthy(); }); + it("should withdraw", async () => { const auth = await anchor.sep10(); const authToken = await auth.authenticate({ accountKp }); @@ -94,7 +98,8 @@ describe("SEP-6", () => { expect(resp.id).toBeTruthy(); }); - it("deposit-exchange should work", async () => { + // skipped: the testanchor changed its SEP-6 API behavior + it.skip("deposit-exchange should work", async () => { const auth = await anchor.sep10(); const authToken = await auth.authenticate({ accountKp }); @@ -122,7 +127,8 @@ describe("SEP-6", () => { expect(resp.id).toBeTruthy(); }); - it("withdraw-exchange should work", async () => { + // skipped: the testanchor changed its SEP-6 API behavior + it.skip("withdraw-exchange should work", async () => { const auth = await anchor.sep10(); const authToken = await auth.authenticate({ accountKp }); diff --git a/@stellar/typescript-wallet-sdk/test/sep7.test.ts b/@stellar/typescript-wallet-sdk/test/sep7.test.ts index 041f009f..4014d33f 100644 --- a/@stellar/typescript-wallet-sdk/test/sep7.test.ts +++ b/@stellar/typescript-wallet-sdk/test/sep7.test.ts @@ -11,7 +11,10 @@ import { sep7ReplacementsFromString, sep7ReplacementsToString, } from "../src"; -import { Sep7OperationType } from "../src/walletSdk/Types"; +import { + Sep7OperationType, + URI_REPLACE_MAX_LENGTH, +} from "../src/walletSdk/Types"; import { Sep7InvalidUriError, Sep7LongMsgError, @@ -798,6 +801,45 @@ describe("sep7Parser", () => { ); }); + it("sep7ReplacementsFromString() throws on replace string without hints (no ';' delimiter)", () => { + const str = "sourceAccount:X,operations[0].sourceAccount:Y"; + expect(() => sep7ReplacementsFromString(str)).toThrow(Sep7InvalidUriError); + }); + + it("sep7ReplacementsFromString() throws on single replacement without hints", () => { + const str = "sourceAccount:X"; + expect(() => sep7ReplacementsFromString(str)).toThrow(Sep7InvalidUriError); + }); + + it("sep7ReplacementsFromString() throws on unbalanced identifiers", () => { + // Spec example: {X} on left but {Y} on right should be rejected + const str = "sourceAccount:X;Y:The account"; + expect(() => sep7ReplacementsFromString(str)).toThrow(Sep7InvalidUriError); + }); + + it("sep7ReplacementsFromString() throws when replace string exceeds URI_REPLACE_MAX_LENGTH", () => { + const longPath = "a".repeat(URI_REPLACE_MAX_LENGTH); + const str = `${longPath}:X;X:hint`; + expect(str.length).toBeGreaterThan(URI_REPLACE_MAX_LENGTH); + expect(() => sep7ReplacementsFromString(str)).toThrow(Sep7InvalidUriError); + expect(() => sep7ReplacementsFromString(str)).toThrow( + "the 'replace' parameter exceeds the maximum allowed length", + ); + }); + + it("sep7ReplacementsFromString() returns empty array for undefined/empty input", () => { + expect(sep7ReplacementsFromString(undefined)).toEqual([]); + expect(sep7ReplacementsFromString("")).toEqual([]); + }); + + it("Sep7Tx.getReplacements() throws on replace param missing hints", () => { + const uri = new Sep7Tx( + `web+stellar:tx?xdr=test&replace=${encodeURIComponent("sourceAccount:X")}`, + ); + + expect(() => uri.getReplacements()).toThrow(Sep7InvalidUriError); + }); + it("sep7ReplacementsToString outputs the right string", () => { const expected = "sourceAccount:X,operations[0].sourceAccount:Y,operations[1].destination:Y;X:account from where you want to pay fees,Y:account that needs the trustline and which will receive the new tokens"; diff --git a/@stellar/typescript-wallet-sdk/test/server.test.ts b/@stellar/typescript-wallet-sdk/test/server.test.ts index 69bf520d..23de3000 100644 --- a/@stellar/typescript-wallet-sdk/test/server.test.ts +++ b/@stellar/typescript-wallet-sdk/test/server.test.ts @@ -1,3 +1,4 @@ +import axios from "axios"; import { TransactionBuilder } from "@stellar/stellar-sdk"; import { Wallet, Server } from "../src"; @@ -5,6 +6,7 @@ let wallet; let account; let accountKp; const networkPassphrase = "Test SDF Network ; September 2015"; +const anchorDomain = "testanchor.stellar.org"; describe("SEP-10 helpers", () => { beforeEach(() => { wallet = Wallet.TestNet(); @@ -13,29 +15,23 @@ describe("SEP-10 helpers", () => { }); it("should validate and sign challenge txn", async () => { - const validChallengeTx = - "AAAAAgAAAACpn2Fr7GAZ4XOcFvEz+xduBFDK1NDLQP875GtWWlJ0XQAAAMgAAAAAAAAAAAAAAAEAAAAAZa76AgAAAABlrv2GAAAAAAAAAAIAAAABAAAAALO9GbK9e+E+ul46lJyGjkzjlQnwqNryiqBsIR1vgMlAAAAACgAAABt0ZXN0YW5jaG9yLnN0ZWxsYXIub3JnIGF1dGgAAAAAAQAAAEBRT0ZDTE02OFQ0cVF4Um55TCtRdlBlVTdPeDJYNnhLdzdyenZTbzBBYUdqdUtIdGxQRkpHNTFKMndJazBwMXl2AAAAAQAAAACpn2Fr7GAZ4XOcFvEz+xduBFDK1NDLQP875GtWWlJ0XQAAAAoAAAAPd2ViX2F1dGhfZG9tYWluAAAAAAEAAAAWdGVzdGFuY2hvci5zdGVsbGFyLm9yZwAAAAAAAAAAAAFaUnRdAAAAQG6cMkt4YhwOzgizIimXRX8zTfFjAOItG7kSX14A454KlhGj9ocFhaRpj3tCc4fK45toFCBKRAdyFM7aQq331QI="; + const resp = await axios.get( + `https://${anchorDomain}/auth?account=${accountKp.publicKey}&home_domain=${anchorDomain}`, + ); + const validChallengeTx = resp.data.transaction; - let isValid; - try { - const signedResp = await Server.signChallengeTransaction({ - accountKp, - challengeTx: validChallengeTx, - networkPassphrase, - anchorDomain: "testanchor.stellar.org", - }); - const signedTxn = TransactionBuilder.fromXDR( - signedResp.transaction, - networkPassphrase, - ); - expect(signedTxn.signatures.length).toBe(2); - expect(signedResp.networkPassphrase).toBe(networkPassphrase); - isValid = true; - } catch (e) { - isValid = false; - } - - expect(isValid).toBeTruthy(); + const signedResp = await Server.signChallengeTransaction({ + accountKp, + challengeTx: validChallengeTx, + networkPassphrase, + anchorDomain, + }); + const signedTxn = TransactionBuilder.fromXDR( + signedResp.transaction, + networkPassphrase, + ); + expect(signedTxn.signatures.length).toBe(2); + expect(signedResp.networkPassphrase).toBe(networkPassphrase); }); it("should invalidate bad challenge txn", async () => { @@ -48,7 +44,7 @@ describe("SEP-10 helpers", () => { accountKp, challengeTx: invalidChallengeTx, networkPassphrase, - anchorDomain: "testanchor.stellar.org", + anchorDomain, }); isValid = true; } catch (e) { diff --git a/@stellar/typescript-wallet-sdk/test/wallet.test.ts b/@stellar/typescript-wallet-sdk/test/wallet.test.ts index 4cb93b50..89dafcdf 100644 --- a/@stellar/typescript-wallet-sdk/test/wallet.test.ts +++ b/@stellar/typescript-wallet-sdk/test/wallet.test.ts @@ -91,12 +91,12 @@ describe("Anchor", () => { it("should give TOML info", async () => { let resp = await anchor.sep1(); expect(resp.webAuthEndpoint).toBe("https://testanchor.stellar.org/auth"); - expect(resp.currencies.length).toBe(2); + expect(resp.currencies.length).toBe(3); // alias resp = await anchor.getInfo(); expect(resp.webAuthEndpoint).toBe("https://testanchor.stellar.org/auth"); - expect(resp.currencies.length).toBe(2); + expect(resp.currencies.length).toBe(3); }); it("should be able to authenticate", async () => { let auth = await anchor.sep10(); @@ -339,15 +339,14 @@ describe("Anchor", () => { expect(transactions.length === 2).toBeTruthy(); }); - it("should error fetching transactions with invalid pading id", async () => { - await expect(async () => { - await anchor.sep24().getTransactionsForAsset({ - authToken, - assetCode: "SRT", - lang: "en-US", - pagingId: "randomPagingId", - }); - }).rejects.toThrowError(ServerRequestFailedError); + it("should accept any paging id when fetching transactions", async () => { + const transactions = await anchor.sep24().getTransactionsForAsset({ + authToken, + assetCode: "SRT", + lang: "en-US", + pagingId: "randomPagingId", + }); + expect(transactions).toBeDefined(); }); describe("watchAllTransactions", () => { diff --git a/@stellar/typescript-wallet-sdk/tsconfig.json b/@stellar/typescript-wallet-sdk/tsconfig.json index 241b5253..e5f95158 100644 --- a/@stellar/typescript-wallet-sdk/tsconfig.json +++ b/@stellar/typescript-wallet-sdk/tsconfig.json @@ -4,7 +4,8 @@ "baseUrl": "src/", "outDir": "lib", "declaration": true, - "declarationDir": "lib" + "declarationDir": "lib", + "stripInternal": true }, "include": ["src"] } diff --git a/yarn.lock b/yarn.lock index f1566cf3..c6be77ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3273,12 +3273,12 @@ axios@^1.4.0: proxy-from-env "^1.1.0" axios@^1.7.7: - version "1.7.7" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" - integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== + version "1.13.5" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.5.tgz#5e464688fa127e11a660a2c49441c009f6567a43" + integrity sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q== dependencies: - follow-redirects "^1.15.6" - form-data "^4.0.0" + follow-redirects "^1.15.11" + form-data "^4.0.5" proxy-from-env "^1.1.0" babel-jest@^29.4.1, babel-jest@^29.4.3: @@ -3425,6 +3425,26 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +bare-addon-resolve@^1.3.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz#21fa56b9fb398ca46202cbc713c653f25daf4264" + integrity sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA== + dependencies: + bare-module-resolve "^1.10.0" + bare-semver "^1.0.0" + +bare-module-resolve@^1.10.0: + version "1.12.1" + resolved "https://registry.yarnpkg.com/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz#4847c1c91a6fce124b45bc36f97caa6bf6658d42" + integrity sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg== + dependencies: + bare-semver "^1.0.0" + +bare-semver@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bare-semver/-/bare-semver-1.0.2.tgz#3cfc47ed5d3e809b369daec534ce916b70b83b8c" + integrity sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA== + base32.js@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/base32.js/-/base32.js-0.1.0.tgz#b582dec693c2f11e893cf064ee6ac5b6131a2202" @@ -3627,6 +3647,14 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -4081,6 +4109,15 @@ dotenv@^16.3.1: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -4221,11 +4258,28 @@ es-abstract@^1.20.4, es-abstract@^1.22.1: unbox-primitive "^1.0.2" which-typed-array "^1.1.10" +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + es-module-lexer@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.2.1.tgz#ba303831f63e6a394983fde2f97ad77b22324527" integrity sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg== +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + es-set-tostringtag@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" @@ -4235,6 +4289,16 @@ es-set-tostringtag@^2.0.1: has "^1.0.3" has-tostringtag "^1.0.0" +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -4656,10 +4720,10 @@ follow-redirects@^1.15.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== -follow-redirects@^1.15.6: - version "1.15.6" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" - integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== +follow-redirects@^1.15.11: + version "1.15.11" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" + integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== for-each@^0.3.3: version "0.3.3" @@ -4677,6 +4741,17 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053" + integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + hasown "^2.0.2" + mime-types "^2.1.12" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -4702,6 +4777,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + function.prototype.name@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" @@ -4737,11 +4817,35 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@ has-proto "^1.0.1" has-symbols "^1.0.3" +get-intrinsic@^1.2.6: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + get-stream@^5.0.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -4831,6 +4935,11 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + graceful-fs@^4.1.2, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" @@ -4878,6 +4987,11 @@ has-symbols@^1.0.2, has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + has-tostringtag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" @@ -4885,6 +4999,13 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" @@ -4917,6 +5038,13 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -6296,6 +6424,11 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -6482,11 +6615,6 @@ node-fetch@^2.6.12: dependencies: whatwg-url "^5.0.0" -node-gyp-build@^4.8.0: - version "4.8.1" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.1.tgz#976d3ad905e71b76086f4f0b0d3637fe79b6cda5" - integrity sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw== - node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -7102,6 +7230,13 @@ regjsparser@^0.9.1: dependencies: jsesc "~0.5.0" +require-addon@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/require-addon/-/require-addon-1.2.0.tgz#b6a969805b82f5ed8b2ecf29453b090ca9933c89" + integrity sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA== + dependencies: + bare-addon-resolve "^1.3.0" + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -7448,11 +7583,11 @@ slice-ansi@^5.0.0: is-fullwidth-code-point "^4.0.0" sodium-native@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/sodium-native/-/sodium-native-4.1.1.tgz#109bc924dd55c13db87c6dd30da047487595723c" - integrity sha512-LXkAfRd4FHtkQS4X6g+nRcVaN7mWVNepV06phIsC6+IZFvGh1voW5TNQiQp2twVaMf05gZqQjuS+uWLM6gHhNQ== + version "4.3.3" + resolved "https://registry.yarnpkg.com/sodium-native/-/sodium-native-4.3.3.tgz#fae4866b52366f5e6cc1b7ae8c8a71673d50c7df" + integrity sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw== dependencies: - node-gyp-build "^4.8.0" + require-addon "^1.1.0" source-map-support@0.5.13: version "0.5.13"