feat(inapp): add passkey authentication for native clients (#4022)

## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces support for `passkeyDomain` in in-app wallets, updates error handling in signup screens, and improves storage handling.

### Detailed summary
- Added `passkeyDomain` support in in-app wallets
- Updated error handling in signup screens
- Improved storage handling for in-app wallets

> The following files were skipped due to too many changes: `packages/thirdweb/src/wallets/in-app/web/utils/Storage/LocalStorage.ts`, `packages/thirdweb/src/utils/bytecode/cbor-decode.ts`, `packages/thirdweb/package.json`, `packages/thirdweb/src/wallets/wallet-connect/receiver/session-store.test.ts`, `pnpm-lock.yaml`, `packages/thirdweb/src/wallets/in-app/native/native-connector.ts`, `packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts`, `packages/thirdweb/src/wallets/in-app/core/authentication/passkeys.ts`, `packages/thirdweb/src/wallets/in-app/native/auth/passkeys.ts`, `packages/thirdweb/src/wallets/in-app/web/lib/auth/passkeys.ts`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
This commit is contained in:
joaquim-verges
2024-08-14 19:44:35 +00:00
parent 52678e02a8
commit a640a1e147
26 changed files with 885 additions and 275 deletions
+1
View File
@@ -18,6 +18,7 @@ export const WALLETS = [
"facebook",
],
mode: "redirect",
passkeyDomain: process.env.VERCEL_ENV ? "thirdweb.com" : undefined,
},
}),
createWallet("io.metamask"),
+20 -48
View File
@@ -117,54 +117,22 @@
},
"typesVersions": {
"*": {
"adapters/*": [
"./dist/types/exports/adapters/*.d.ts"
],
"auth": [
"./dist/types/exports/auth.d.ts"
],
"chains": [
"./dist/types/exports/chains.d.ts"
],
"contract": [
"./dist/types/exports/contract.d.ts"
],
"deploys": [
"./dist/types/exports/deploys.d.ts"
],
"event": [
"./dist/types/exports/event.d.ts"
],
"extensions/*": [
"./dist/types/exports/extensions/*.d.ts"
],
"pay": [
"./dist/types/exports/pay.d.ts"
],
"react": [
"./dist/types/exports/react.d.ts"
],
"react-native": [
"./dist/types/exports/react-native.d.ts"
],
"rpc": [
"./dist/types/exports/rpc.d.ts"
],
"storage": [
"./dist/types/exports/storage.d.ts"
],
"transaction": [
"./dist/types/exports/transaction.d.ts"
],
"utils": [
"./dist/types/exports/utils.d.ts"
],
"wallets": [
"./dist/types/exports/wallets.d.ts"
],
"wallets/*": [
"./dist/types/exports/wallets/*.d.ts"
]
"adapters/*": ["./dist/types/exports/adapters/*.d.ts"],
"auth": ["./dist/types/exports/auth.d.ts"],
"chains": ["./dist/types/exports/chains.d.ts"],
"contract": ["./dist/types/exports/contract.d.ts"],
"deploys": ["./dist/types/exports/deploys.d.ts"],
"event": ["./dist/types/exports/event.d.ts"],
"extensions/*": ["./dist/types/exports/extensions/*.d.ts"],
"pay": ["./dist/types/exports/pay.d.ts"],
"react": ["./dist/types/exports/react.d.ts"],
"react-native": ["./dist/types/exports/react-native.d.ts"],
"rpc": ["./dist/types/exports/rpc.d.ts"],
"storage": ["./dist/types/exports/storage.d.ts"],
"transaction": ["./dist/types/exports/transaction.d.ts"],
"utils": ["./dist/types/exports/utils.d.ts"],
"wallets": ["./dist/types/exports/wallets.d.ts"],
"wallets/*": ["./dist/types/exports/wallets/*.d.ts"]
}
},
"browser": {
@@ -249,6 +217,9 @@
"react-native-quick-crypto": {
"optional": true
},
"react-native-passkey": {
"optional": true
},
"react-native-svg": {
"optional": true
},
@@ -326,6 +297,7 @@
"msw": "^2.3.4",
"react-native": "0.74.5",
"react-native-aes-gcm-crypto": "0.2.2",
"react-native-passkey": "3.0.0-beta2",
"react-native-quick-crypto": "0.7.0-rc.6",
"react-native-svg": "15.3.0",
"typescript": "5.5.4",
@@ -10,9 +10,11 @@ export {
} from "../../wallets/in-app/native/auth/index.js";
export { type GetAuthenticatedUserParams } from "../../wallets/in-app/core/authentication/types.js";
export type {
InAppWalletCreationOptions,
InAppWalletAuth,
InAppWalletSocialAuth,
InAppWalletConnectionOptions,
} from "../../wallets/in-app/core/wallet/types.js";
// NOT SUPPORTED (yet)
export const hasStoredPasskey = () => {
throw new Error("Not supported in native");
};
export { hasStoredPasskey } from "../../wallets/in-app/native/auth/passkeys.js";
@@ -96,6 +96,9 @@ export function useAutoConnectCore(
setConnectionStatus("disconnected");
}
} catch (e) {
if (e instanceof Error) {
console.warn("Error auto connecting wallet:", e.message);
}
setConnectionStatus("disconnected");
}
} else {
@@ -2,6 +2,7 @@ import { useCallback, useState } from "react";
import type { Wallet } from "../../../../wallets/interfaces/wallet.js";
import type { ConnectManagerOptions } from "../../../../wallets/manager/index.js";
import { useConnectionManagerCtx } from "../../providers/connection-manager.js";
import { useSetActiveWalletConnectionStatus } from "./useSetActiveWalletConnectionStatus.js";
/**
* A hook to set a wallet as active wallet
@@ -36,6 +37,7 @@ import { useConnectionManagerCtx } from "../../providers/connection-manager.js";
export function useConnect(options?: ConnectManagerOptions) {
const manager = useConnectionManagerCtx("useConnect");
const { connect } = manager;
const setConnectionStatus = useSetActiveWalletConnectionStatus();
const [isConnecting, setIsConnecting] = useState(false);
const [error, setError] = useState<Error | null>(null);
@@ -43,23 +45,29 @@ export function useConnect(options?: ConnectManagerOptions) {
async (walletOrFn: Wallet | (() => Promise<Wallet>)) => {
// reset error state
setError(null);
setConnectionStatus("connecting");
if (typeof walletOrFn !== "function") {
return await connect(walletOrFn, options);
const account = await connect(walletOrFn, options);
setConnectionStatus("connected");
return account;
}
setIsConnecting(true);
try {
const w = await walletOrFn();
return await connect(w, options);
const account = await connect(w, options);
setConnectionStatus("connected");
return account;
} catch (e) {
console.error(e);
setError(e as Error);
setConnectionStatus("disconnected");
} finally {
setIsConnecting(false);
}
return null;
},
[connect, options],
[connect, options, setConnectionStatus],
);
return { connect: handleConnection, isConnecting, error } as const;
@@ -217,9 +217,6 @@ function SignupScreen(props: {
await linkProfile(wallet as Wallet<"inApp">, {
strategy: "passkey",
type: "sign-up",
}).catch((e) => {
setError(e.message);
throw e;
});
} else {
await wallet.connect({
@@ -231,7 +228,11 @@ function SignupScreen(props: {
await setLastAuthProvider("passkey", webLocalStorage);
}
done();
} catch {
} catch (e) {
console.error(e);
if (e instanceof Error) {
setError(`Error creating passkey: ${e.message}`);
}
setStatus("error");
}
}
@@ -3,7 +3,7 @@
// TODO: re-enable typescript and properly type this
// @ts-nocheck
// @ts-nocheck - TODO: re-enable typescript and properly type this
let src;
let srcEnd;
@@ -32,6 +32,60 @@ const defaultOptions = {
mapsAsObjects: true,
};
function readFixedString(length) {
let result
if (length < 16) {
if (result = shortStringInJS(length))
return result
}
if (length > 64 && decoder)
return decoder.decode(src.subarray(position, position += length))
const end = position + length
const units = []
result = ''
while (position < end) {
const byte1 = src[position++]
if ((byte1 & 0x80) === 0) {
// 1 byte
units.push(byte1)
} else if ((byte1 & 0xe0) === 0xc0) {
// 2 bytes
const byte2 = src[position++] & 0x3f
units.push(((byte1 & 0x1f) << 6) | byte2)
} else if ((byte1 & 0xf0) === 0xe0) {
// 3 bytes
const byte2 = src[position++] & 0x3f
const byte3 = src[position++] & 0x3f
units.push(((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3)
} else if ((byte1 & 0xf8) === 0xf0) {
// 4 bytes
const byte2 = src[position++] & 0x3f
const byte3 = src[position++] & 0x3f
const byte4 = src[position++] & 0x3f
let unit = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4
if (unit > 0xffff) {
unit -= 0x10000
units.push(((unit >>> 10) & 0x3ff) | 0xd800)
unit = 0xdc00 | (unit & 0x3ff)
}
units.push(unit)
} else {
units.push(byte1)
}
if (units.length >= 0x1000) {
result += fromCharCode.apply(String, units)
units.length = 0
}
}
if (units.length > 0) {
result += fromCharCode.apply(String, units)
}
return result
}
class Decoder {
constructor() {
Object.assign(this, defaultOptions);
@@ -451,4 +505,4 @@ for (let i = 0; i < 256; i++) {
Number(`1e${Math.floor(45.15 - i * 0.30103)}`))();
}
const defaultDecoder = new Decoder();
export const decode = defaultDecoder.decode;
export const decode = defaultDecoder.decode;
+11 -2
View File
@@ -116,8 +116,17 @@ function assertString(value: unknown): asserts value is string {
}
}
function base64UrlToBase64(base64url: string) {
return base64url.replaceAll("-", "+").replaceAll("_", "/");
export function base64UrlToBase64(base64url: string): string {
// Replace Base64URL characters with Base64 characters
let base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
// Add padding if necessary
const padding = base64.length % 4;
if (padding !== 0) {
base64 += "=".repeat(4 - padding);
}
return base64;
}
/**
@@ -1,52 +1,55 @@
import type { EcosystemWalletId } from "../../../../wallet-types.js";
import type { AsyncStorage } from "../../../../utils/storage/AsyncStorage.js";
import type { EcosystemWalletId } from "../../../wallet-types.js";
import {
AUTH_TOKEN_LOCAL_STORAGE_NAME,
DEVICE_SHARE_LOCAL_STORAGE_NAME,
PASSKEY_CREDENTIAL_ID_LOCAL_STORAGE_NAME,
WALLET_CONNECT_SESSIONS_LOCAL_STORAGE_NAME,
WALLET_USER_ID_LOCAL_STORAGE_NAME,
} from "../../../core/constants/settings.js";
} from "../constants/settings.js";
const data = new Map<string, string>();
/**
* @internal
*/
export class LocalStorage {
protected isSupported: boolean;
export class ClientScopedStorage {
protected key: string;
protected storage: AsyncStorage | null;
/**
* @internal
*/
constructor({
storage,
clientId,
ecosystemId,
}: {
storage: AsyncStorage | null;
clientId: string;
ecosystemId?: EcosystemWalletId;
}) {
this.isSupported = typeof window !== "undefined" && !!window.localStorage;
this.storage = storage;
this.key = getLocalStorageKey(clientId, ecosystemId);
}
protected async getItem(key: string): Promise<string | null> {
if (this.isSupported) {
return window.localStorage.getItem(key);
if (this.storage) {
return this.storage.getItem(key);
}
return data.get(key) ?? null;
}
protected async setItem(key: string, value: string): Promise<void> {
if (this.isSupported) {
return window.localStorage.setItem(key, value);
if (this.storage) {
return this.storage.setItem(key, value);
}
data.set(key, value);
}
protected async removeItem(key: string): Promise<boolean> {
const item = await this.getItem(key);
if (this.isSupported && item) {
window.localStorage.removeItem(key);
if (this.storage && item) {
this.storage.removeItem(key);
return true;
}
return false;
@@ -0,0 +1,209 @@
import type { ThirdwebClient } from "../../../../client/client.js";
import { getThirdwebBaseUrl } from "../../../../utils/domains.js";
import { getClientFetch } from "../../../../utils/fetch.js";
import type { AsyncStorage } from "../../../../utils/storage/AsyncStorage.js";
import type { Ecosystem } from "../../web/types.js";
import { ClientScopedStorage } from "./client-scoped-storage.js";
import type { AuthStoredTokenWithCookieReturnType } from "./types.js";
function getVerificationPath() {
return `${getThirdwebBaseUrl(
"inAppWallet",
)}/api/2024-05-05/login/passkey/callback`;
}
function getChallengePath(type: "sign-in" | "sign-up", username?: string) {
return `${getThirdwebBaseUrl(
"inAppWallet",
)}/api/2024-05-05/login/passkey?type=${type}${
username ? `&username=${username}` : ""
}`;
}
export type RegisterResult = {
authenticatorData: string;
credentialId: string;
clientData: string;
credential: {
publicKey: string;
algorithm: string;
};
origin: string;
};
export type AuthenticateResult = {
credentialId: string;
authenticatorData: string;
clientData: string;
signature: string;
origin: string;
};
export type RpInfo = { name: string; id: string };
export interface PasskeyClient {
register: (args: {
name: string;
challenge: string;
rp: RpInfo;
}) => Promise<RegisterResult>;
authenticate: (args: {
credentialId: string | undefined;
challenge: string;
rp: RpInfo;
}) => Promise<AuthenticateResult>;
isAvailable: () => boolean;
}
export async function registerPasskey(options: {
client: ThirdwebClient;
storage: AsyncStorage;
passkeyClient: PasskeyClient;
ecosystem?: Ecosystem;
username?: string;
rp: RpInfo;
}): Promise<AuthStoredTokenWithCookieReturnType> {
if (!options.passkeyClient.isAvailable()) {
throw new Error("Passkeys are not available on this device");
}
const storage = new ClientScopedStorage({
storage: options.storage,
clientId: options.client.clientId,
ecosystemId: options.ecosystem?.id,
});
const fetchWithId = getClientFetch(options.client, options.ecosystem);
const generatedName = options.username ?? generateUsername(options.ecosystem);
// 1. request challenge from server
const res = await fetchWithId(getChallengePath("sign-up", generatedName));
const challengeData = await res.json();
if (!challengeData.challenge) {
throw new Error("No challenge received");
}
const challenge = challengeData.challenge;
// 2. initiate registration
const registration = await options.passkeyClient.register({
name: generatedName,
challenge,
rp: options.rp,
});
const customHeaders: Record<string, string> = {};
if (options.ecosystem?.partnerId) {
customHeaders["x-ecosystem-partner-id"] = options.ecosystem.partnerId;
}
if (options.ecosystem?.id) {
customHeaders["x-ecosystem-id"] = options.ecosystem.id;
}
// 3. send the registration object to the server
const verifRes = await fetchWithId(getVerificationPath(), {
method: "POST",
headers: {
"Content-Type": "application/json",
...customHeaders,
},
body: JSON.stringify({
type: "sign-up",
authenticatorData: registration.authenticatorData,
credentialId: registration.credentialId,
serverVerificationId: challengeData.serverVerificationId,
clientData: registration.clientData,
username: generatedName,
credential: {
publicKey: registration.credential.publicKey,
algorithm: registration.credential.algorithm,
},
origin: registration.origin,
rpId: options.rp.id,
}),
});
const verifData = await verifRes.json();
if (!verifData || !verifData.storedToken) {
throw new Error(
`Error verifying passkey: ${verifData.message ?? "unknown error"}`,
);
}
// 4. store the credentialId in local storage
await storage.savePasskeyCredentialId(registration.credentialId);
// 5. returns back the IAW authentication token
return verifData;
}
export async function loginWithPasskey(options: {
client: ThirdwebClient;
storage: AsyncStorage;
passkeyClient: PasskeyClient;
rp: RpInfo;
ecosystem?: Ecosystem;
}): Promise<AuthStoredTokenWithCookieReturnType> {
if (!options.passkeyClient.isAvailable()) {
throw new Error("Passkeys are not available on this device");
}
const storage = new ClientScopedStorage({
storage: options.storage,
clientId: options.client.clientId,
ecosystemId: options.ecosystem?.id,
});
const fetchWithId = getClientFetch(options.client, options.ecosystem);
// 1. request challenge from server/iframe
const res = await fetchWithId(getChallengePath("sign-in"));
const challengeData = await res.json();
if (!challengeData.challenge) {
throw new Error("No challenge received");
}
const challenge = challengeData.challenge;
// 1.2. find the user's credentialId in local storage
const credentialId = (await storage.getPasskeyCredentialId()) ?? undefined;
// 2. initiate login
const authentication = await options.passkeyClient.authenticate({
credentialId,
challenge,
rp: options.rp,
});
const customHeaders: Record<string, string> = {};
if (options.ecosystem?.partnerId) {
customHeaders["x-ecosystem-partner-id"] = options.ecosystem.partnerId;
}
if (options.ecosystem?.id) {
customHeaders["x-ecosystem-id"] = options.ecosystem.id;
}
const verifRes = await fetchWithId(getVerificationPath(), {
method: "POST",
headers: {
"Content-Type": "application/json",
...customHeaders,
},
body: JSON.stringify({
type: "sign-in",
authenticatorData: authentication.authenticatorData,
credentialId: authentication.credentialId,
serverVerificationId: challengeData.serverVerificationId,
clientData: authentication.clientData,
signature: authentication.signature,
origin: authentication.origin,
rpId: options.rp.id,
}),
});
const verifData = await verifRes.json();
if (!verifData || !verifData.storedToken) {
throw new Error(
`Error verifying passkey: ${verifData.message ?? "unknown error"}`,
);
}
// 5. store the credentialId in local storage
await storage.savePasskeyCredentialId(authentication.credentialId);
// 6. return the auth'd user type
return verifData;
}
function generateUsername(ecosystem?: Ecosystem) {
return `${ecosystem?.id ?? "wallet"}-${new Date().toISOString()}`;
}
@@ -1,4 +1,3 @@
import type { AuthType } from "@passwordless-id/webauthn/dist/esm/types.js";
import type { Chain } from "../../../../chains/types.js";
import type { ThirdwebClient } from "../../../../client/client.js";
import type { Address } from "../../../../utils/address.js";
@@ -38,9 +37,23 @@ export type SingleStepAuthArgsType =
| { strategy: "iframe" }
| {
strategy: "passkey";
/**
* Whether to create a new passkey (sign up) or login to an existing one (sign in).
* You can use `hasStoredPasskey()` to check if a user has previously logged in with a passkey on this device.
*/
type: "sign-up" | "sign-in";
/**
* Optional name of the passkey to create, defaults to a generated name
*/
passkeyName?: string;
authenticatorType?: AuthType;
/**
* Optional domain, defaults to current window location.
* NOTE: this is required on native platforms.
*/
domain?: {
displayName: string;
hostname: string;
};
}
| {
strategy: "wallet";
@@ -29,9 +29,22 @@ export type InAppWalletAuth = AuthOption;
export type InAppWalletCreationOptions =
| {
auth?: {
/**
* List of authentication options to display in the Connect Modal
*/
options: InAppWalletAuth[];
/**
* Whether to display the social auth prompt in a popup or redirect
*/
mode?: "popup" | "redirect";
/**
* The domain of the passkey to use for authentication
*/
passkeyDomain?: string;
};
/**
* Metadata to display in the Connect Modal
*/
metadata?: {
image?: {
src: string;
@@ -40,8 +53,17 @@ export type InAppWalletCreationOptions =
alt?: string;
};
};
/**
* The partnerId of the ecosystem wallet to connect to
*/
partnerId?: string;
/**
* The smart account options to convert the wallet to a smart account
**/
smartAccount?: SmartWalletOptions;
/**
* Whether to hide the private key export button in the Connect Modal
*/
hidePrivateKeyExport?: boolean;
}
| undefined;
@@ -0,0 +1,269 @@
import { Passkey } from "react-native-passkey";
import { concat } from "viem";
import type { ThirdwebClient } from "../../../../client/client.js";
import { toBytes } from "../../../../utils/encoding/to-bytes.js";
import { keccak256 } from "../../../../utils/hashing/keccak256.js";
import { nativeLocalStorage } from "../../../../utils/storage/nativeStorage.js";
import {
base64ToString,
base64ToUint8Array,
base64UrlToBase64,
uint8ArrayToBase64,
} from "../../../../utils/uint8-array.js";
import type { EcosystemWalletId } from "../../../wallet-types.js";
import { ClientScopedStorage } from "../../core/authentication/client-scoped-storage.js";
import type {
AuthenticateResult,
PasskeyClient,
RegisterResult,
RpInfo,
} from "../../core/authentication/passkeys.js";
export class PasskeyNativeClient implements PasskeyClient {
isAvailable(): boolean {
return Passkey.isSupported();
}
async register(args: {
name: string;
challenge: string;
rp: RpInfo;
}): Promise<RegisterResult> {
const { name, challenge, rp } = args;
const result = await Passkey.create({
challenge,
user: {
displayName: name,
name: name,
id: keccak256(toBytes(name)),
},
authenticatorSelection: {
authenticatorAttachment: "platform",
residentKey: "required",
userVerification: "required",
requireResidentKey: true,
},
rp,
pubKeyCredParams: [
{
alg: -7,
type: "public-key",
},
],
});
const parsedResult =
typeof result === "string" ? JSON.parse(result) : result;
const { publicKey, authData } = await extractPublicKeyAndAuthData(
parsedResult.response,
);
const clientDataB64 = base64UrlToBase64(
parsedResult.response.clientDataJSON,
);
const clientDataParsed = JSON.parse(base64ToString(clientDataB64));
return {
authenticatorData: authData,
credentialId: parsedResult.id,
clientData: clientDataB64,
credential: {
publicKey,
algorithm: "ES256",
},
origin: clientDataParsed.origin,
};
}
async authenticate(args: {
credentialId: string | undefined;
challenge: string;
rp: RpInfo;
}): Promise<AuthenticateResult> {
const { credentialId, challenge, rp } = args;
const result = await Passkey.get({
challenge,
rpId: rp.id,
allowCredentials: credentialId
? [
{
id: credentialId,
type: "public-key",
// biome-ignore lint/suspicious/noExplicitAny: enum not exported
transports: ["hybrid" as any],
},
]
: [],
});
const parsedResult =
typeof result === "string" ? JSON.parse(result) : result;
const clientDataB64 = base64UrlToBase64(
parsedResult.response.clientDataJSON,
);
const clientDataParsed = JSON.parse(base64ToString(clientDataB64));
return {
authenticatorData: parsedResult.response.authenticatorData,
credentialId: parsedResult.id,
clientData: clientDataB64,
signature: parsedResult.response.signature,
origin: clientDataParsed.origin,
};
}
}
/**
* Returns whether this device has a stored passkey ready to be used for sign-in
* @param client - the thirdweb client
* @returns whether the device has a stored passkey
* @walletUtils
*/
export async function hasStoredPasskey(
client: ThirdwebClient,
ecosystemId?: EcosystemWalletId,
) {
const storage = new ClientScopedStorage({
storage: nativeLocalStorage,
clientId: client.clientId,
ecosystemId: ecosystemId,
});
const credId = await storage.getPasskeyCredentialId();
return !!credId;
}
type ExtractedData = {
publicKey: string;
authData: string;
};
async function extractPublicKeyAndAuthData(response: {
attestationObject: string;
}): Promise<ExtractedData> {
const { decode } = await import("../../../../utils/bytecode/cbor-decode.js");
const { attestationObject } = response;
const attestationBase64 = base64UrlToBase64(attestationObject);
// Decode the attestationObject from base64url
const attestationBytes = base64ToUint8Array(attestationBase64);
const decodedAttestationObject = decode(attestationBytes);
// Extract the authenticator data (authData) from the attestation object
const authData = Uint8Array.from(decodedAttestationObject.authData);
const decoded = decodeAuthData(authData);
if (!decoded.publicKey) {
throw new Error("No public key found");
}
const coseKey = decode(decoded.publicKey);
// convert to PEM format
const x = Uint8Array.from(coseKey[-2]); // x coordinate
const y = Uint8Array.from(coseKey[-3]); // y coordinate
// Concatenate x and y coordinates with the uncompressed point indicator (0x04)
const pubKey = concat([
Uint8Array.from([
// ASN.1 DER encoding, aka X.690 ... https://en.wikipedia.org/wiki/X.690
0x30, // DER Sequence
0x59, // Length (2+19 + 2+66) = 89
0x30, // DER Sequence
0x13, // Length (2+7 + 2+8) = 19
0x06, // DER OBJECT IDENTIFIER
0x07, // Length
0x2a,
0x86,
0x48,
0xce,
0x3d,
0x02,
0x01, // OID 1.2.840.10045.2.1 ecPublicKey
0x06, // DER OBJECT IDENTIFIER
0x08, // Length
0x2a,
0x86,
0x48,
0xce,
0x3d,
0x03,
0x01,
0x07, // OID 1.2.840.10045.3.1.7 prime256v1
0x03, // DER BIT STRING
0x42, // Length (32 + 32 + 1(null) + 1) = 66
0x00,
0x04, // ECC uncompressed... beginning of X9.62 Key
]), // Uncompressed point indicator
x,
y,
]);
return {
publicKey: uint8ArrayToBase64(pubKey),
authData: uint8ArrayToBase64(authData),
};
}
function decodeAuthData(authData: Uint8Array) {
let offset = 0;
// Step 1: Read the RP ID hash (32 bytes)
const rpIdHash = authData.slice(offset, offset + 32);
offset += 32;
// Step 2: Read the flags (1 byte)
const flags = authData[offset];
offset += 1;
if (!flags) {
throw new Error("No flags found");
}
// Step 3: Read the sign count (4 bytes)
const signCount = authData.slice(offset, offset + 4);
offset += 4;
// Step 4: Check if attested credential data is present
const attestedCredentialDataPresent = (flags & 0x40) !== 0;
let aaguid: Uint8Array | undefined;
let credentialIdLength: number | undefined;
let credentialId: Uint8Array | undefined;
let publicKey: Uint8Array | undefined;
if (attestedCredentialDataPresent) {
// Step 5: Read AAGUID (16 bytes)
aaguid = authData.slice(offset, offset + 16);
offset += 16;
const start = authData[offset];
const end = authData[offset + 1];
if (start === undefined || end === undefined) {
throw new Error("No credential ID found");
}
// Step 6: Read Credential ID Length (2 bytes)
credentialIdLength = (start << 8) + end;
offset += 2;
// Step 7: Read Credential ID (variable length)
credentialId = authData.slice(offset, offset + credentialIdLength);
offset += credentialIdLength;
// Step 8: Read Public Key (variable length, COSE-encoded)
publicKey = authData.slice(offset); // The rest is the public key
offset += publicKey.length;
}
return {
rpIdHash,
flags,
signCount,
aaguid,
credentialIdLength,
credentialId,
publicKey,
};
}
@@ -87,9 +87,8 @@ export async function fetchUserDetails(args: {
}): Promise<UserDetailsApiType> {
const url = new URL(ROUTE_EMBEDDED_WALLET_DETAILS);
if (args) {
if (args.email) {
url.searchParams.append("email", args.email);
}
// TODO (inapp) remove this, unused in the backend but still required
url.searchParams.append("email", args.email ?? "none");
url.searchParams.append("clientId", args.client.clientId);
}
const resp = await authFetchEmbeddedWalletUser(args.client, url.href, {
@@ -64,6 +64,7 @@ export function inAppWallet(
const { InAppNativeConnector } = await import("./native-connector.js");
return new InAppNativeConnector({
client,
passkeyDomain: createOptions?.auth?.passkeyDomain,
});
},
});
@@ -1,6 +1,12 @@
import type { Chain } from "../../../chains/types.js";
import type { ThirdwebClient } from "../../../client/client.js";
import { stringify } from "../../../utils/json.js";
import { nativeLocalStorage } from "../../../utils/storage/nativeStorage.js";
import type { Account, Wallet } from "../../interfaces/wallet.js";
import {
loginWithPasskey,
registerPasskey,
} from "../core/authentication/passkeys.js";
import { siweAuthenticate } from "../core/authentication/siwe.js";
import {
type AuthArgsType,
@@ -27,12 +33,14 @@ import {
} from "./auth/native-auth.js";
import { fetchUserDetails } from "./helpers/api/fetchers.js";
import { logoutUser } from "./helpers/auth/logout.js";
import { postAuth } from "./helpers/auth/middleware.js";
import { getWalletUserDetails } from "./helpers/storage/local.js";
import { getExistingUserAccount } from "./helpers/wallet/retrieval.js";
export type NativeConnectorOptions = {
client: ThirdwebClient;
partnerId?: string | undefined;
passkeyDomain?: string;
};
export class InAppNativeConnector implements InAppConnector {
@@ -107,6 +115,8 @@ export class InAppNativeConnector implements InAppConnector {
params.redirectUrl || (ExpoLinking.createURL("") as string);
return authenticate({ strategy, redirectUrl }, this.options.client);
}
case "passkey":
return this.passkeyAuth(params);
default:
throw new Error(`Unsupported authentication type: ${strategy}`);
}
@@ -164,7 +174,16 @@ export class InAppNativeConnector implements InAppConnector {
});
}
case "passkey": {
throw new Error("Passkey authentication is not implemented yet");
const authToken = await this.passkeyAuth(params);
const account = await this.getAccount();
return {
user: {
status: UserWalletStatus.LOGGED_IN_WALLET_INITIALIZED,
account,
authDetails: authToken.storedToken.authDetails,
walletAddress: account.address,
},
};
}
case "iframe": {
throw new Error("iframe_email_verification is not supported in native");
@@ -177,6 +196,75 @@ export class InAppNativeConnector implements InAppConnector {
}
}
private async passkeyAuth(args: {
type: "sign-up" | "sign-in";
passkeyName?: string;
client: ThirdwebClient;
ecosystem?: Ecosystem;
}): Promise<AuthStoredTokenWithCookieReturnType> {
const { type, passkeyName, client, ecosystem } = args;
const domain = this.options.passkeyDomain;
if (!domain) {
throw new Error(
"Passkey domain is required for native platforms. Please pass it in the 'auth' options when creating the inAppWallet().",
);
}
try {
const { PasskeyNativeClient } = await import("./auth/passkeys.js");
const passkeyClient = new PasskeyNativeClient();
const storage = nativeLocalStorage;
let authToken: AuthStoredTokenWithCookieReturnType;
if (type === "sign-up") {
authToken = await registerPasskey({
client,
ecosystem,
username: passkeyName,
passkeyClient,
storage,
rp: {
id: domain,
name: domain,
},
});
} else {
authToken = await loginWithPasskey({
client,
ecosystem,
passkeyClient,
storage,
rp: {
id: domain,
name: domain,
},
});
}
const toStoreToken: AuthStoredTokenWithCookieReturnType["storedToken"] = {
jwtToken: authToken.storedToken.jwtToken,
authDetails: authToken.storedToken.authDetails,
authProvider: authToken.storedToken.authProvider,
developerClientId: authToken.storedToken.developerClientId,
cookieString: authToken.storedToken.cookieString,
// we should always store the jwt cookie since there's no concept of cookie in react native
shouldStoreCookieString: true,
isNewUser: authToken.storedToken.isNewUser,
};
await postAuth({ storedToken: toStoreToken, client });
return authToken;
} catch (error) {
console.error(
`Error while signing in with passkey. ${(error as Error)?.message || typeof error === "object" ? stringify(error) : error}`,
);
if (error instanceof Error) {
throw new Error(`Error signing in with passkey: ${error.message}`);
}
throw new Error("An unknown error occurred signing in with passkey");
}
}
private async validateOtp(
options: MultiStepAuthArgsType & {
client: ThirdwebClient;
@@ -187,6 +187,7 @@ export function inAppWallet(
const { InAppWebConnector } = await import("./lib/web-connector.js");
return new InAppWebConnector({
client,
passkeyDomain: createOptions?.auth?.passkeyDomain,
});
},
});
@@ -1,4 +1,6 @@
import type { ThirdwebClient } from "../../../../../client/client.js";
import { webLocalStorage } from "../../../../../utils/storage/webStorage.js";
import { ClientScopedStorage } from "../../../core/authentication/client-scoped-storage.js";
import type {
AuthAndWalletRpcReturnType,
AuthLoginReturnType,
@@ -7,7 +9,6 @@ import type {
SendEmailOtpReturnType,
} from "../../../core/authentication/types.js";
import type { ClientIdWithQuerierType, Ecosystem } from "../../types.js";
import { LocalStorage } from "../../utils/Storage/LocalStorage.js";
import type { InAppWalletIframeCommunicator } from "../../utils/iFrameCommunication/InAppWalletIframeCommunicator.js";
import { BaseLogin } from "./base-login.js";
@@ -33,7 +34,7 @@ export type AuthQuerierTypes = {
export class Auth {
protected client: ThirdwebClient;
protected AuthQuerier: InAppWalletIframeCommunicator<AuthQuerierTypes>;
protected localStorage: LocalStorage;
protected localStorage: ClientScopedStorage;
protected onAuthSuccess: (
authResults: AuthAndWalletRpcReturnType,
) => Promise<AuthLoginReturnType>;
@@ -59,7 +60,8 @@ export class Auth {
this.client = client;
this.AuthQuerier = querier;
this.localStorage = new LocalStorage({
this.localStorage = new ClientScopedStorage({
storage: webLocalStorage,
clientId: client.clientId,
ecosystemId: ecosystem?.id,
});
@@ -1,160 +1,76 @@
import { client } from "@passwordless-id/webauthn";
import type { AuthType } from "@passwordless-id/webauthn/dist/esm/types.js";
import type { ThirdwebClient } from "../../../../../client/client.js";
import { getThirdwebBaseUrl } from "../../../../../utils/domains.js";
import { getClientFetch } from "../../../../../utils/fetch.js";
import { webLocalStorage } from "../../../../../utils/storage/webStorage.js";
import {
base64ToString,
base64UrlToBase64,
} from "../../../../../utils/uint8-array.js";
import type { EcosystemWalletId } from "../../../../wallet-types.js";
import type { AuthStoredTokenWithCookieReturnType } from "../../../core/authentication/types.js";
import type { Ecosystem } from "../../types.js";
import { LocalStorage } from "../../utils/Storage/LocalStorage.js";
import { ClientScopedStorage } from "../../../core/authentication/client-scoped-storage.js";
import type {
AuthenticateResult,
PasskeyClient,
RegisterResult,
RpInfo,
} from "../../../core/authentication/passkeys.js";
function getVerificationPath() {
return `${getThirdwebBaseUrl(
"inAppWallet",
)}/api/2024-05-05/login/passkey/callback`;
}
function getChallengePath(type: "sign-in" | "sign-up", username?: string) {
return `${getThirdwebBaseUrl(
"inAppWallet",
)}/api/2024-05-05/login/passkey?type=${type}${
username ? `&username=${username}` : ""
}`;
}
export async function registerPasskey(options: {
client: ThirdwebClient;
ecosystem?: Ecosystem;
authenticatorType?: AuthType;
username?: string;
}): Promise<AuthStoredTokenWithCookieReturnType> {
if (!client.isAvailable()) {
throw new Error("Passkeys are not available on this device");
}
// TODO inject this
const storage = new LocalStorage({
clientId: options.client.clientId,
ecosystemId: options.ecosystem?.id,
});
const fetchWithId = getClientFetch(options.client, options.ecosystem);
const generatedName = options.username ?? generateUsername(options.ecosystem);
// 1. request challenge from server
const res = await fetchWithId(getChallengePath("sign-up", generatedName));
const challengeData = await res.json();
if (!challengeData.challenge) {
throw new Error("No challenge received");
}
const challenge = challengeData.challenge;
// 2. initiate registration
const registration = await client.register(generatedName, challenge, {
authenticatorType: options.authenticatorType ?? "auto",
userVerification: "required",
attestation: true,
debug: false,
});
// 3. store the credentialId in local storage
await storage.savePasskeyCredentialId(registration.credential.id);
const customHeaders: Record<string, string> = {};
if (options.ecosystem?.partnerId) {
customHeaders["x-ecosystem-partner-id"] = options.ecosystem.partnerId;
}
if (options.ecosystem?.id) {
customHeaders["x-ecosystem-id"] = options.ecosystem.id;
export class PasskeyWebClient implements PasskeyClient {
isAvailable(): boolean {
return client.isAvailable();
}
// 4. send the registration object to the server
const verifRes = await fetchWithId(getVerificationPath(), {
method: "POST",
headers: {
"Content-Type": "application/json",
...customHeaders,
},
body: JSON.stringify({
type: "sign-up",
async register(args: {
name: string;
challenge: string;
rp: RpInfo;
}): Promise<RegisterResult> {
const { name, challenge, rp } = args;
const registration = await client.register(name, challenge, {
authenticatorType: "auto",
userVerification: "required",
domain: rp.id,
attestation: true,
debug: false,
});
const clientDataB64 = base64UrlToBase64(registration.clientData);
const clientDataParsed = JSON.parse(base64ToString(clientDataB64));
return {
authenticatorData: registration.authenticatorData,
credentialId: registration.credential.id,
serverVerificationId: challengeData.serverVerificationId,
clientData: registration.clientData,
username: generatedName,
credential: {
publicKey: registration.credential.publicKey,
algorithm: registration.credential.algorithm,
},
}),
});
const verifData = await verifRes.json();
if (!verifData) {
throw new Error("No token received");
}
// 5. returns back the IAW authentication token
return verifData;
}
export async function loginWithPasskey(options: {
client: ThirdwebClient;
ecosystem?: Ecosystem;
authenticatorType?: AuthType;
}): Promise<AuthStoredTokenWithCookieReturnType> {
if (!client.isAvailable()) {
throw new Error("Passkeys are not available on this device");
}
// TODO inject this
const storage = new LocalStorage({
clientId: options.client.clientId,
ecosystemId: options.ecosystem?.id,
});
const fetchWithId = getClientFetch(options.client, options.ecosystem);
// 1. request challenge from server/iframe
const res = await fetchWithId(getChallengePath("sign-in"));
const challengeData = await res.json();
if (!challengeData.challenge) {
throw new Error("No challenge received");
}
const challenge = challengeData.challenge;
// 1.2. find the user's credentialId in local storage
const credentialId = await storage.getPasskeyCredentialId();
const credentials = credentialId ? [credentialId] : [];
// 2. initiate login
const authentication = await client.authenticate(credentials, challenge, {
authenticatorType: options.authenticatorType ?? "auto",
userVerification: "required",
});
const customHeaders: Record<string, string> = {};
if (options.ecosystem?.partnerId) {
customHeaders["x-ecosystem-partner-id"] = options.ecosystem.partnerId;
}
if (options.ecosystem?.id) {
customHeaders["x-ecosystem-id"] = options.ecosystem.id;
origin: clientDataParsed.origin,
};
}
// 3. send the authentication object to the server/iframe
const verifRes = await fetchWithId(getVerificationPath(), {
method: "POST",
headers: {
"Content-Type": "application/json",
...customHeaders,
},
body: JSON.stringify({
type: "sign-in",
authenticatorData: authentication.authenticatorData,
credentialId: authentication.credentialId,
serverVerificationId: challengeData.serverVerificationId,
clientData: authentication.clientData,
signature: authentication.signature,
}),
});
// 5. store the credentialId in local storage
await storage.savePasskeyCredentialId(authentication.credentialId);
const verifData = await verifRes.json();
if (!verifData) {
throw new Error("No token received");
async authenticate(args: {
credentialId: string | undefined;
challenge: string;
rp: RpInfo;
}): Promise<AuthenticateResult> {
const { credentialId, challenge, rp } = args;
const result = await client.authenticate(
credentialId ? [credentialId] : [],
challenge,
{
authenticatorType: "auto",
userVerification: "required",
domain: rp.id,
},
);
const clientDataB64 = base64UrlToBase64(result.clientData);
const clientDataParsed = JSON.parse(base64ToString(clientDataB64));
return {
authenticatorData: result.authenticatorData,
credentialId: result.credentialId,
clientData: result.clientData,
signature: result.signature,
origin: clientDataParsed.origin,
};
}
// 6. return the auth'd user type
return verifData;
}
/**
@@ -167,14 +83,11 @@ export async function hasStoredPasskey(
client: ThirdwebClient,
ecosystemId?: EcosystemWalletId,
) {
const storage = new LocalStorage({
const storage = new ClientScopedStorage({
storage: webLocalStorage, // TODO (passkey) react native variant of this fn
clientId: client.clientId,
ecosystemId: ecosystemId,
});
const credId = await storage.getPasskeyCredentialId();
return !!credId;
}
function generateUsername(ecosystem?: Ecosystem) {
return `${ecosystem?.id ?? "wallet"}-${new Date().toISOString()}`;
}
@@ -8,12 +8,14 @@ import { getAddress } from "../../../../utils/address.js";
import { getThirdwebDomains } from "../../../../utils/domains.js";
import { type Hex, hexToString } from "../../../../utils/encoding/hex.js";
import { parseTypedData } from "../../../../utils/signatures/helpers/parseTypedData.js";
import { webLocalStorage } from "../../../../utils/storage/webStorage.js";
import type { Prettify } from "../../../../utils/type-utils.js";
import { getEcosystemPartnerPermissions } from "../../../ecosystem/get-ecosystem-partner-permissions.js";
import type {
Account,
SendTransactionOption,
} from "../../../interfaces/wallet.js";
import { ClientScopedStorage } from "../../core/authentication/client-scoped-storage.js";
import {
type GetUser,
type GetUserWalletStatusRpcReturnType,
@@ -29,7 +31,6 @@ import type {
SignTransactionReturnType,
SignedTypedDataReturnType,
} from "../types.js";
import { LocalStorage } from "../utils/Storage/LocalStorage.js";
import type { InAppWalletIframeCommunicator } from "../utils/iFrameCommunication/InAppWalletIframeCommunicator.js";
export type WalletManagementTypes = {
@@ -82,7 +83,7 @@ export class IFrameWallet {
protected walletManagerQuerier: InAppWalletIframeCommunicator<
WalletManagementTypes & WalletManagementUiTypes
>;
protected localStorage: LocalStorage;
protected localStorage: ClientScopedStorage;
/**
* Not meant to be initialized directly. Call {@link initializeUser} to get an instance
@@ -101,7 +102,8 @@ export class IFrameWallet {
this.ecosystem = ecosystem;
this.walletManagerQuerier = querier;
this.localStorage = new LocalStorage({
this.localStorage = new ClientScopedStorage({
storage: webLocalStorage,
clientId: client.clientId,
ecosystemId: ecosystem?.id,
});
@@ -1,7 +1,12 @@
import type { ThirdwebClient } from "../../../../client/client.js";
import { getThirdwebBaseUrl } from "../../../../utils/domains.js";
import { webLocalStorage } from "../../../../utils/storage/webStorage.js";
import type { SocialAuthOption } from "../../../../wallets/types.js";
import type { Account } from "../../../interfaces/wallet.js";
import {
loginWithPasskey,
registerPasskey,
} from "../../core/authentication/passkeys.js";
import { siweAuthenticate } from "../../core/authentication/siwe.js";
import {
type AuthLoginReturnType,
@@ -19,7 +24,6 @@ import { InAppWalletIframeCommunicator } from "../utils/iFrameCommunication/InAp
import { Auth, type AuthQuerierTypes } from "./auth/iframe-auth.js";
import { loginWithOauth, loginWithOauthRedirect } from "./auth/oauth.js";
import { sendOtp, verifyOtp } from "./auth/otp.js";
import { loginWithPasskey, registerPasskey } from "./auth/passkeys.js";
import { IFrameWallet } from "./in-app-account.js";
/**
@@ -34,6 +38,7 @@ export class InAppWebConnector implements InAppConnector {
* Used to manage the Auth state of the user.
*/
auth: Auth;
private passkeyDomain?: string;
private isClientIdLegacyPaper(clientId: string): boolean {
if (clientId.indexOf("-") > 0 && clientId.length === 36) {
@@ -51,6 +56,7 @@ export class InAppWebConnector implements InAppConnector {
client,
onAuthSuccess,
ecosystem,
passkeyDomain,
}: InAppWalletConstructorType) {
if (this.isClientIdLegacyPaper(client.clientId)) {
throw new Error(
@@ -59,6 +65,7 @@ export class InAppWebConnector implements InAppConnector {
}
const baseUrl = getThirdwebBaseUrl("inAppWallet");
this.client = client;
this.passkeyDomain = passkeyDomain;
this.querier = new InAppWalletIframeCommunicator({
clientId: client.clientId,
ecosystem,
@@ -180,20 +187,9 @@ export class InAppWebConnector implements InAppConnector {
jwt: args.jwt,
encryptionKey: args.encryptionKey,
});
case "passkey":
if (args.type === "sign-up") {
return registerPasskey({
client: this.wallet.client,
ecosystem: this.wallet.ecosystem,
authenticatorType: args.authenticatorType,
username: args.passkeyName,
});
}
return loginWithPasskey({
client: this.wallet.client,
ecosystem: this.wallet.ecosystem,
authenticatorType: args.authenticatorType,
});
case "passkey": {
return this.passkeyAuth(args);
}
case "auth_endpoint": {
return this.auth.authenticateWithCustomAuthEndpoint({
payload: args.payload,
@@ -262,20 +258,7 @@ export class InAppWebConnector implements InAppConnector {
return this.auth.loginWithModal();
}
case "passkey": {
if (args.type === "sign-up") {
const authToken = await registerPasskey({
client: this.wallet.client,
ecosystem: this.wallet.ecosystem,
authenticatorType: args.authenticatorType,
username: args.passkeyName,
});
return this.loginWithAuthToken(authToken);
}
const authToken = await loginWithPasskey({
client: this.wallet.client,
ecosystem: this.wallet.ecosystem,
authenticatorType: args.authenticatorType,
});
const authToken = await this.passkeyAuth(args);
return this.loginWithAuthToken(authToken);
}
case "phone":
@@ -299,6 +282,37 @@ export class InAppWebConnector implements InAppConnector {
async logout(): Promise<LogoutReturnType> {
return await this.auth.logout();
}
private async passkeyAuth(
args: Extract<SingleStepAuthArgsType, { strategy: "passkey" }>,
) {
const { PasskeyWebClient } = await import("./auth/passkeys.js");
const passkeyClient = new PasskeyWebClient();
const storage = webLocalStorage;
if (args.type === "sign-up") {
return registerPasskey({
client: this.wallet.client,
ecosystem: this.wallet.ecosystem,
username: args.passkeyName,
passkeyClient,
storage,
rp: {
id: this.passkeyDomain ?? window.location.hostname,
name: this.passkeyDomain ?? window.document.title,
},
});
}
return loginWithPasskey({
client: this.wallet.client,
ecosystem: this.wallet.ecosystem,
passkeyClient,
storage,
rp: {
id: args.domain?.hostname ?? window.location.hostname,
name: args.domain?.displayName ?? window.document.title,
},
});
}
}
function assertUnreachable(x: never, message?: string): never {
@@ -39,6 +39,11 @@ export type InAppWalletConstructorType = ClientIdConstructorType & {
* @param ecosystem - An optional set of options to connect to an ecosystem wallet.
*/
ecosystem?: Ecosystem;
/**
* The domain of the passkey to use for authentication
*/
passkeyDomain?: string;
};
export type ClientIdWithQuerierType = ClientIdConstructorType & {
@@ -1,6 +1,7 @@
import { webLocalStorage } from "../../../../../utils/storage/webStorage.js";
import { ClientScopedStorage } from "../../../core/authentication/client-scoped-storage.js";
import { IN_APP_WALLET_PATH } from "../../../core/constants/settings.js";
import type { Ecosystem } from "../../types.js";
import { LocalStorage } from "../Storage/LocalStorage.js";
import { IframeCommunicator } from "./IframeCommunicator.js";
/**
@@ -43,7 +44,8 @@ export class InAppWalletIframeCommunicator<
* @internal
*/
override async onIframeLoadedInitVariables() {
const localStorage = new LocalStorage({
const localStorage = new ClientScopedStorage({
storage: webLocalStorage,
clientId: this.clientId,
ecosystemId: this.ecosystem?.id,
});
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { LocalStorage } from "../../in-app/web/utils/Storage/LocalStorage.js";
import { ClientScopedStorage } from "../../in-app/core/authentication/client-scoped-storage.js";
import * as SessionStore from "./session-store.js";
vi.mock("../../in-app/web/utils/Storage/LocalStorage.js");
@@ -19,12 +19,14 @@ describe("SessionStore", () => {
it("initializes the session store with a new LocalStorage instance if not already initialized", () => {
expect(SessionStore.walletConnectSessions).toBeUndefined();
SessionStore.initializeSessionStore({ clientId: "test-client-id" });
expect(SessionStore.walletConnectSessions).toBeInstanceOf(LocalStorage);
expect(SessionStore.walletConnectSessions).toBeInstanceOf(
ClientScopedStorage,
);
});
it("does not reinitialize the session store if it is already initialized", () => {
SessionStore.setWalletConnectSessions(
mockLocalStorage as unknown as LocalStorage,
mockLocalStorage as unknown as ClientScopedStorage,
);
SessionStore.initializeSessionStore({ clientId: "test-client-id" });
expect(SessionStore.walletConnectSessions).toBe(mockLocalStorage);
@@ -39,7 +41,7 @@ describe("SessionStore", () => {
it("returns parsed sessions from storage", async () => {
SessionStore.setWalletConnectSessions(
mockLocalStorage as unknown as LocalStorage,
mockLocalStorage as unknown as ClientScopedStorage,
);
mockLocalStorage.getWalletConnectSessions.mockResolvedValue(
JSON.stringify([{ topic: "123" }]),
@@ -57,7 +59,7 @@ describe("SessionStore", () => {
it("saves a new session to the existing sessions", async () => {
SessionStore.setWalletConnectSessions(
mockLocalStorage as unknown as LocalStorage,
mockLocalStorage as unknown as ClientScopedStorage,
);
mockLocalStorage.getWalletConnectSessions.mockResolvedValue(
JSON.stringify([{ topic: "123" }]),
@@ -77,7 +79,7 @@ describe("SessionStore", () => {
it("removes a session from the existing sessions", async () => {
SessionStore.setWalletConnectSessions(
mockLocalStorage as unknown as LocalStorage,
mockLocalStorage as unknown as ClientScopedStorage,
);
mockLocalStorage.getWalletConnectSessions.mockResolvedValue(
JSON.stringify([{ topic: "123" }, { topic: "456" }]),
@@ -1,10 +1,10 @@
import { LocalStorage } from "../../in-app/web/utils/Storage/LocalStorage.js";
import { ClientScopedStorage } from "../../in-app/core/authentication/client-scoped-storage.js";
import type { WalletConnectSession } from "./types.js";
/**
* @internal
*/
export let walletConnectSessions: LocalStorage | undefined;
export let walletConnectSessions: ClientScopedStorage | undefined;
/**
* @internal
@@ -25,7 +25,8 @@ export function initializeSessionStore(options: {
clientId: string;
}) {
if (!walletConnectSessions) {
walletConnectSessions = new LocalStorage({
walletConnectSessions = new ClientScopedStorage({
storage: null, // TODO: inject storage
clientId: options.clientId,
});
}
@@ -73,7 +74,7 @@ export async function removeSession(
* @internal FOR TESTING ONLY
*/
export function setWalletConnectSessions(
storage: LocalStorage | undefined,
storage: ClientScopedStorage | undefined,
): void {
walletConnectSessions = storage;
}
+15 -1
View File
@@ -2351,6 +2351,9 @@ importers:
react-native-aes-gcm-crypto:
specifier: 0.2.2
version: 0.2.2([email protected](@babel/[email protected])(@babel/[email protected](@babel/[email protected]))(@types/[email protected])([email protected])([email protected])([email protected]))([email protected])
react-native-passkey:
specifier: 3.0.0-beta2
version: 3.0.0-beta2([email protected](@babel/[email protected])(@babel/[email protected](@babel/[email protected]))(@types/[email protected])([email protected])([email protected])([email protected]))([email protected])
react-native-quick-crypto:
specifier: 0.7.0-rc.6
version: 0.7.0-rc.6([email protected](@babel/[email protected])(@babel/[email protected](@babel/[email protected]))(@types/[email protected])([email protected])([email protected])([email protected]))([email protected])
@@ -16131,6 +16134,12 @@ packages:
react: '*'
react-native: '>=0.71.0'
[email protected]:
resolution: {integrity: sha512-6i6Jy/ZqSZHvxPDrLVzPsfDVFloREZWnZFdWYygh7x6BPLKLfaryrxQAQ+0eRY4EOFwGdgxIAZcy+zv0k0UXiw==}
peerDependencies:
react: '*'
react-native: '*'
[email protected]:
resolution: {integrity: sha512-xghaXpWdB0ji8OwYyo0fWezRroNxiNFCNFpGUIyE7+qc4gA/IGWnysIG5L0MbdoORv8FkTKUvfd6yCUN5R2VFA==}
peerDependencies:
@@ -30229,7 +30238,7 @@ snapshots:
pathe: 1.1.2
picocolors: 1.0.1
sirv: 2.0.4
vitest: 1.6.0(@types/node@22.0.2)(@vitest/[email protected])([email protected])([email protected])
vitest: 1.6.0(@types/node@20.14.9)(@vitest/[email protected])([email protected])([email protected])
'@vitest/[email protected]':
dependencies:
@@ -39827,6 +39836,11 @@ snapshots:
react: 18.3.1
react-native: 0.74.5(@babel/[email protected])(@babel/[email protected](@babel/[email protected]))(@types/[email protected])([email protected])([email protected])([email protected])
[email protected]([email protected](@babel/[email protected])(@babel/[email protected](@babel/[email protected]))(@types/[email protected])([email protected])([email protected])([email protected]))([email protected]):
dependencies:
react: 18.3.1
react-native: 0.74.5(@babel/[email protected])(@babel/[email protected](@babel/[email protected]))(@types/[email protected])([email protected])([email protected])([email protected])
[email protected]([email protected](@babel/[email protected])(@babel/[email protected](@babel/[email protected]))(@types/[email protected])([email protected])([email protected])([email protected]))([email protected]):
dependencies:
base64-js: 1.5.1