Compare commits

..
Author SHA1 Message Date
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
7858632429 Version Packages (#1451)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-04 18:15:57 -07:00
Phillip HoandGitHub 0647f12498 [service-utils] Remove optional fields in logging (#1453) 2023-08-05 00:24:56 +00:00
3e1c4045e7 chore: Add logHttpRequest helper func (#1448)
Co-authored-by: Danny Friday <[email protected]>
2023-08-04 23:18:54 +00:00
iketwandGitHub 3ba8ba4288 [RN] Upgrade coinbase wallet sdk (#1452) 2023-08-04 22:28:37 +00:00
Adam MajmudarandGitHub dfd120a3a9 [Auth] Split into functions & expose all auth functions on client (#1392) 2023-08-04 22:09:11 +00:00
Joaquim VergesandGitHub 262edc6a46 [SDK] Sanitize shared metadata descriptions strings (#1450) 2023-08-04 19:35:03 +00:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
4f79053ec3 Version Packages (#1446)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-04 10:41:06 -07:00
Joaquim VergesandGitHub 2a91113a76 [SDK] Revert sanitizing inputs (#1449) 2023-08-04 10:31:04 -07:00
Danny FridayandGitHub b103872daf Export extractAuthorizationData for CF Workers (#1447) 2023-08-03 23:54:08 +00:00
Mariano FuentesandGitHub 7f54012ec6 CLI warnings (#1442) 2023-08-03 22:48:25 +00:00
51 changed files with 3690 additions and 1075 deletions
+16
View File
@@ -1,5 +1,21 @@
# @thirdweb-dev/auth
## 3.2.27
### Patch Changes
- [#1392](https://github.com/thirdweb-dev/js/pull/1392) [`dfd120a3`](https://github.com/thirdweb-dev/js/commit/dfd120a3a9d1582c8b174265c92bf43dbbaf5c86) Thanks [@adam-maj](https://github.com/adam-maj)! - Expose functions from auth and update useAuth
- Updated dependencies []:
- @thirdweb-dev/wallets@1.1.10
## 3.2.26
### Patch Changes
- Updated dependencies []:
- @thirdweb-dev/wallets@1.1.9
## 3.2.25
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/auth",
"version": "3.2.25",
"version": "3.2.27",
"main": "dist/thirdweb-dev-auth.cjs.js",
"module": "dist/thirdweb-dev-auth.esm.js",
"browser": {
+73 -423
View File
@@ -1,28 +1,28 @@
import {
THIRDWEB_AUTH_DEFAULT_LOGIN_PAYLOAD_DURATION_IN_SECONDS,
THIRDWEB_AUTH_DEFAULT_TOKEN_DURATION_IN_SECONDS,
} from "../constants";
authenticateJWT,
generateJWT,
parseJWT,
refreshJWT,
} from "./functions/jwt";
import {
LoginOptions,
LoginPayload,
GenerateOptions,
LoginPayloadData,
LoginPayloadDataSchema,
AuthenticationPayloadDataSchema,
AuthenticationPayloadData,
VerifyOptionsSchema,
VerifyOptions,
GenerateOptionsSchema,
AuthenticateOptionsSchema,
AuthenticateOptions,
User,
Json,
LoginOptionsSchema,
AuthenticationPayload,
AuthenticationPayloadDataInput,
} from "./schema";
import { isBrowser } from "./utils";
buildAndSignLoginPayload,
buildLoginPayload,
signLoginPayload,
verifyLoginPayload,
} from "./functions/login";
import type { GenericAuthWallet } from "@thirdweb-dev/wallets";
import { Json, User } from "./schema/common";
import {
LoginOptionsWithOptionalDomain,
LoginPayload,
LoginPayloadData,
} from "./schema/login";
import { VerifyOptionsWithOptionalDomain } from "./schema/verify";
import { GenerateOptionsWithOptionalDomain } from "./schema/generate";
import {
AuthenticateOptionsWithOptionalDomain,
AuthenticationPayload,
} from "./schema/authenticate";
export class ThirdwebAuth {
private domain: string;
@@ -37,432 +37,82 @@ export class ThirdwebAuth {
this.wallet = wallet;
}
public async payload(options?: LoginOptions): Promise<LoginPayloadData> {
const parsedOptions = LoginOptionsSchema.parse(options);
let chainId: string | undefined = parsedOptions?.chainId;
if (!chainId && this.wallet.getChainId) {
try {
chainId = (await this.wallet.getChainId()).toString();
} catch {
// ignore error
}
}
return LoginPayloadDataSchema.parse({
type: this.wallet.type,
domain: parsedOptions?.domain || this.domain,
address: parsedOptions?.address || (await this.wallet.getAddress()),
statement: parsedOptions?.statement,
version: parsedOptions?.version,
uri: parsedOptions?.uri,
chain_id: chainId,
nonce: parsedOptions?.nonce,
expiration_time:
parsedOptions?.expirationTime ||
new Date(
Date.now() +
1000 * THIRDWEB_AUTH_DEFAULT_LOGIN_PAYLOAD_DURATION_IN_SECONDS,
),
invalid_before:
parsedOptions?.invalidBefore ||
new Date(
Date.now() -
1000 * THIRDWEB_AUTH_DEFAULT_LOGIN_PAYLOAD_DURATION_IN_SECONDS,
),
resources: parsedOptions?.resources,
public async payload(
options?: LoginOptionsWithOptionalDomain,
): Promise<LoginPayloadData> {
return buildLoginPayload({
wallet: this.wallet,
options: this.formatOptions(options),
});
}
public async loginWithPayload(
payload: LoginPayloadData,
): Promise<LoginPayload> {
const message = this.generateMessage(payload);
const signature = await this.wallet.signMessage(message);
return {
payload,
signature,
};
return signLoginPayload({ wallet: this.wallet, payload });
}
public async login(options?: LoginOptions): Promise<LoginPayload> {
const payloadData = await this.payload(options);
return await this.loginWithPayload(payloadData);
public async login(
options?: LoginOptionsWithOptionalDomain,
): Promise<LoginPayload> {
return buildAndSignLoginPayload({
wallet: this.wallet,
options: this.formatOptions(options),
});
}
public async verify(
payload: LoginPayload,
options?: VerifyOptions,
options?: VerifyOptionsWithOptionalDomain,
): Promise<string> {
const parsedOptions = VerifyOptionsSchema.parse(options);
if (payload.payload.type !== this.wallet.type) {
throw new Error(
`Expected chain type '${this.wallet.type}' does not match chain type on payload '${payload.payload.type}'`,
);
}
// Check that the intended domain matches the domain of the payload
const domain = parsedOptions?.domain || this.domain;
if (payload.payload.domain !== domain) {
throw new Error(
`Expected domain '${domain}' does not match domain on payload '${payload.payload.domain}'`,
);
}
// Check that the payload statement matches the expected statement
if (parsedOptions?.statement) {
if (payload.payload.statement !== parsedOptions.statement) {
throw new Error(
`Expected statement '${parsedOptions.statement}' does not match statement on payload '${payload.payload.statement}'`,
);
}
}
// Check that the intended URI matches the URI of the payload
if (parsedOptions?.uri) {
if (payload.payload.uri !== parsedOptions.uri) {
throw new Error(
`Expected URI '${parsedOptions.uri}' does not match URI on payload '${payload.payload.uri}'`,
);
}
}
// Check that the intended version matches the version of the payload
if (parsedOptions?.version) {
if (payload.payload.version !== parsedOptions.version) {
throw new Error(
`Expected version '${parsedOptions.version}' does not match version on payload '${payload.payload.version}'`,
);
}
}
// Check that the intended chain ID matches the chain ID of the payload
if (parsedOptions?.chainId) {
if (payload.payload.chain_id !== parsedOptions.chainId) {
throw new Error(
`Expected chain ID '${parsedOptions.chainId}' does not match chain ID on payload '${payload.payload.chain_id}'`,
);
}
}
// Check that the payload nonce is valid
if (parsedOptions?.validateNonce !== undefined) {
try {
await parsedOptions.validateNonce(payload.payload.nonce);
} catch (err) {
throw new Error(`Login request nonce is invalid`);
}
}
// Check that it isn't before the invalid before time
const currentTime = new Date();
if (currentTime < new Date(payload.payload.invalid_before)) {
throw new Error(`Login request is not yet valid`);
}
// Check that the payload hasn't expired
if (currentTime > new Date(payload.payload.expiration_time)) {
throw new Error(`Login request has expired`);
}
// Check that the specified resources are present on the payload
if (parsedOptions?.resources) {
const missingResources = parsedOptions.resources.filter(
(resource) => !payload.payload.resources?.includes(resource),
);
if (missingResources.length > 0) {
throw new Error(
`Login request is missing required resources: ${missingResources.join(
", ",
)}`,
);
}
}
// Check that the signing address is the claimed wallet address
const message = this.generateMessage(payload.payload);
const chainId =
this.wallet.type === "evm" && payload.payload.chain_id
? parseInt(payload.payload.chain_id)
: undefined;
const verified = await this.verifySignature(
message,
payload.signature,
payload.payload.address,
chainId,
);
if (!verified) {
throw new Error(
`Signer address does not match payload address '${payload.payload.address.toLowerCase()}'`,
);
}
return payload.payload.address;
return verifyLoginPayload({
wallet: this.wallet,
payload,
options: this.formatOptions(options),
});
}
public async generate(
payload: LoginPayload,
options?: GenerateOptions,
options?: GenerateOptionsWithOptionalDomain,
): Promise<string> {
if (isBrowser()) {
throw new Error(
"Authentication tokens should not be generated in the browser, as they must be signed by a server-side admin wallet.",
);
}
const parsedOptions = GenerateOptionsSchema.parse(options);
const domain = parsedOptions?.domain || this.domain;
const userAddress = await this.verify(payload, {
domain,
...parsedOptions?.verifyOptions,
});
let session: Json | undefined = undefined;
if (typeof parsedOptions?.session === "function") {
const sessionTrigger = (await parsedOptions.session(userAddress)) as Json;
if (sessionTrigger) {
session = sessionTrigger;
}
} else {
session = parsedOptions?.session;
}
const adminAddress = await this.wallet.getAddress();
return this.createToken({
iss: adminAddress,
sub: userAddress,
aud: domain,
nbf: parsedOptions?.invalidBefore || new Date(),
exp:
parsedOptions?.expirationTime ||
new Date(
Date.now() + 1000 * THIRDWEB_AUTH_DEFAULT_TOKEN_DURATION_IN_SECONDS,
),
iat: new Date(),
jti: parsedOptions?.tokenId,
ctx: session,
});
}
public async refresh(token: string, expirationTime?: Date): Promise<string> {
const { payload } = this.parseToken(token);
return this.createToken({
iss: payload.iss,
sub: payload.sub,
aud: payload.aud,
nbf: new Date(),
exp:
expirationTime ||
new Date(
Date.now() + 1000 * THIRDWEB_AUTH_DEFAULT_TOKEN_DURATION_IN_SECONDS,
),
iat: new Date(),
ctx: payload.ctx,
});
}
/**
* Authenticate With Token
* @remarks Server-side function that authenticates the provided JWT token. This function verifies that
* the provided authentication token is valid and returns the address of the authenticated wallet.
*
* @param domain - The domain of the server-side application doing authentication
* @param token - The authentication token being used
* @returns The address of the authenticated wallet
*
* @example
* ```javascript
* const domain = "example.com";
* const loginPayload = await sdk.auth.login(domain);
* const token = await sdk.auth.generateAuthToken(domain, loginPayload);
*
* // Authenticate the token and get the address of authenticating users wallet
* const address = sdk.auth.authenticate(domain, token);
* ```
*/
public async authenticate<TSession extends Json = Json>(
token: string,
options?: AuthenticateOptions,
): Promise<User<TSession>> {
if (isBrowser()) {
throw new Error(
"Should not authenticate tokens in the browser, as they must be verified by the server-side admin wallet.",
);
}
const parsedOptions = AuthenticateOptionsSchema.parse(options);
const domain = parsedOptions?.domain || this.domain;
const { payload, signature } = this.parseToken(token);
// Check that the payload unique ID is valid
if (parsedOptions?.validateTokenId !== undefined) {
try {
await parsedOptions.validateTokenId(payload.jti);
} catch (err) {
throw new Error(`Token ID is invalid`);
}
}
// Check that the token audience matches the domain
if (payload.aud !== domain) {
throw new Error(
`Expected token to be for the domain '${domain}', but found token with domain '${payload.aud}'`,
);
}
// Check that the token is past the invalid before time
const currentTime = Math.floor(new Date().getTime() / 1000);
if (currentTime < payload.nbf) {
throw new Error(
`This token is invalid before epoch time '${payload.nbf}', current epoch time is '${currentTime}'`,
);
}
// Check that the token hasn't expired
if (currentTime > payload.exp) {
throw new Error(
`This token expired at epoch time '${payload.exp}', current epoch time is '${currentTime}'`,
);
}
// Check that the connected wallet matches the token issuer
const connectedAddress = await this.wallet.getAddress();
if (connectedAddress.toLowerCase() !== payload.iss.toLowerCase()) {
throw new Error(
`Expected the connected wallet address '${connectedAddress}' to match the token issuer address '${payload.iss}'`,
);
}
let chainId: number | undefined = undefined;
if (this.wallet.getChainId) {
try {
chainId = await this.wallet.getChainId();
} catch {
// ignore error
}
}
const verified = await this.verifySignature(
JSON.stringify(payload),
signature,
connectedAddress,
chainId,
);
if (!verified) {
throw new Error(
`The connected wallet address '${connectedAddress}' did not sign the token`,
);
}
return {
address: payload.sub,
session: payload.ctx as TSession | undefined,
};
}
public parseToken(token: string): AuthenticationPayload {
const encodedPayload = token.split(".")[1];
const encodedSignature = token.split(".")[2];
const payload: AuthenticationPayloadData = JSON.parse(
Buffer.from(encodedPayload, "base64").toString(),
);
const signature = Buffer.from(encodedSignature, "base64").toString();
return {
return generateJWT({
wallet: this.wallet,
payload,
signature,
};
options: this.formatOptions(options),
});
}
private async createToken(
payload: AuthenticationPayloadDataInput,
): Promise<string> {
const payloadData = AuthenticationPayloadDataSchema.parse(payload);
const message = JSON.stringify(payloadData);
const signature = await this.wallet.signMessage(message);
// Header used for JWT token specifying hash algorithm
const header = {
// Specify ECDSA with SHA-256 for hashing algorithm
alg: "ES256",
typ: "JWT",
};
const encodedHeader = Buffer.from(JSON.stringify(header)).toString(
"base64",
);
const encodedData = Buffer.from(JSON.stringify(payloadData))
.toString("base64")
.replace(/=/g, "");
const encodedSignature = Buffer.from(signature).toString("base64");
// Generate a JWT token with base64 encoded header, payload, and signature
const token = `${encodedHeader}.${encodedData}.${encodedSignature}`;
return token;
public async refresh(jwt: string, expirationTime?: Date): Promise<string> {
return refreshJWT({
wallet: this.wallet,
jwt,
options: { expirationTime },
});
}
private async verifySignature(
message: string,
signature: string,
address: string,
chainId?: number,
) {
return this.wallet.verifySignature(message, signature, address, chainId);
public async authenticate<TSession extends Json = Json>(
jwt: string,
options?: AuthenticateOptionsWithOptionalDomain,
): Promise<User<TSession>> {
return authenticateJWT({
wallet: this.wallet,
jwt,
options: this.formatOptions(options),
});
}
/**
* Generates a EIP-4361 & CAIP-122 compliant message to sign based on the login payload
*/
private generateMessage(payload: LoginPayloadData): string {
const typeField = payload.type === "evm" ? "Ethereum" : "Solana";
const header = `${payload.domain} wants you to sign in with your ${typeField} account:`;
let prefix = [header, payload.address].join("\n");
prefix = [prefix, payload.statement].join("\n\n");
if (payload.statement) {
prefix += "\n";
}
public parseToken(jwt: string): AuthenticationPayload {
return parseJWT(jwt);
}
const suffixArray = [];
if (payload.uri) {
const uriField = `URI: ${payload.uri}`;
suffixArray.push(uriField);
}
const versionField = `Version: ${payload.version}`;
suffixArray.push(versionField);
if (payload.chain_id) {
const chainField = `Chain ID: ` + payload.chain_id || "1";
suffixArray.push(chainField);
}
const nonceField = `Nonce: ${payload.nonce}`;
suffixArray.push(nonceField);
const issuedAtField = `Issued At: ${payload.issued_at}`;
suffixArray.push(issuedAtField);
const expiryField = `Expiration Time: ${payload.expiration_time}`;
suffixArray.push(expiryField);
if (payload.invalid_before) {
const invalidBeforeField = `Not Before: ${payload.invalid_before}`;
suffixArray.push(invalidBeforeField);
}
if (payload.resources) {
suffixArray.push(
[`Resources:`, ...payload.resources.map((x) => `- ${x}`)].join("\n"),
);
}
const suffix = suffixArray.join("\n");
return [prefix, suffix].join("\n");
private formatOptions<TOptions extends { domain?: string }>(
options?: TOptions,
): Omit<TOptions, "domain"> & { domain: string } {
return options
? { ...options, domain: options?.domain || this.domain }
: ({ domain: this.domain } as Omit<TOptions, "domain"> & {
domain: string;
});
}
}
+226
View File
@@ -0,0 +1,226 @@
import {
AuthenticateOptionsSchema,
AuthenticationPayload,
AuthenticationPayloadData,
AuthenticationPayloadDataSchema,
} from "../schema/authenticate";
import { Json, User } from "../schema/common";
import {
BuildJwtParams,
RefreshJwtParams,
GenerateJwtParams,
AuthenticateJwtParams,
} from "../schema/functions";
import { GenerateOptionsSchema } from "../schema/generate";
import { RefreshOptionsSchema } from "../schema/refresh";
import { verifyLoginPayload } from "./login";
function isBrowser() {
return typeof window !== "undefined";
}
function base64encode(data: string): string {
if (isBrowser()) {
return window.btoa(data);
}
return Buffer.from(data).toString("base64").replace(/=/g, "");
}
function base64decode(data: string): string {
if (isBrowser()) {
return window.atob(data);
}
return Buffer.from(data, "base64").toString();
}
/**
* Build JWT token based on the authentication payload
*/
async function buildJWT({ wallet, payload }: BuildJwtParams): Promise<string> {
const payloadData = AuthenticationPayloadDataSchema.parse(payload);
const message = JSON.stringify(payloadData);
const signature = await wallet.signMessage(message);
// Header used for JWT token specifying hash algorithm
const header = {
// Specify ECDSA with SHA-256 for hashing algorithm
alg: "ES256",
typ: "JWT",
};
const encodedHeader = base64encode(JSON.stringify(header));
const encodedData = base64encode(JSON.stringify(payloadData));
const encodedSignature = base64encode(signature);
// Generate a JWT with base64 encoded header, payload, and signature
const jwt = `${encodedHeader}.${encodedData}.${encodedSignature}`;
return jwt;
}
/**
* Generate a new JWT using a login payload
*/
export async function generateJWT({
wallet,
payload,
options,
}: GenerateJwtParams): Promise<string> {
const parsedOptions = GenerateOptionsSchema.parse(options);
const userAddress = await verifyLoginPayload({
wallet,
payload,
options: {
domain: parsedOptions.domain,
...parsedOptions.verifyOptions,
},
});
let session: Json | undefined = undefined;
if (typeof parsedOptions?.session === "function") {
const sessionTrigger = (await parsedOptions.session(userAddress)) as Json;
if (sessionTrigger) {
session = sessionTrigger;
}
} else {
session = parsedOptions?.session;
}
const adminAddress = await wallet.getAddress();
return buildJWT({
wallet,
payload: {
iss: adminAddress,
sub: userAddress,
aud: parsedOptions.domain,
nbf: parsedOptions?.invalidBefore || new Date(),
exp: parsedOptions.expirationTime,
iat: new Date(),
jti: parsedOptions?.tokenId,
ctx: session,
},
});
}
/**
* Parse data from an encoded auth JWT
*/
export function parseJWT(jwt: string): AuthenticationPayload {
const encodedPayload = jwt.split(".")[1];
const encodedSignature = jwt.split(".")[2];
const payload: AuthenticationPayloadData = JSON.parse(
base64decode(encodedPayload),
);
const signature = base64decode(encodedSignature);
return {
payload,
signature,
};
}
/**
* Refresh an existing JWT
*/
export async function refreshJWT({
wallet,
jwt,
options,
}: RefreshJwtParams): Promise<string> {
const { payload } = parseJWT(jwt);
const parsedOptions = RefreshOptionsSchema.parse(options);
return buildJWT({
wallet,
payload: {
iss: payload.iss,
sub: payload.sub,
aud: payload.aud,
nbf: new Date(),
exp: parsedOptions.expirationTime,
iat: new Date(),
ctx: payload.ctx,
},
});
}
/**
* Validate a JWT and extract the user's info
*/
export async function authenticateJWT<TSession extends Json = Json>({
wallet,
jwt,
options,
}: AuthenticateJwtParams): Promise<User<TSession>> {
const parsedOptions = AuthenticateOptionsSchema.parse(options);
const { payload, signature } = parseJWT(jwt);
// Check that the payload unique ID is valid
if (parsedOptions?.validateTokenId !== undefined) {
try {
await parsedOptions.validateTokenId(payload.jti);
} catch (err) {
throw new Error(`Token ID is invalid`);
}
}
// Check that the token audience matches the domain
if (payload.aud !== parsedOptions.domain) {
throw new Error(
`Expected token to be for the domain '${parsedOptions.domain}', but found token with domain '${payload.aud}'`,
);
}
// Check that the token is past the invalid before time
const currentTime = Math.floor(new Date().getTime() / 1000);
if (currentTime < payload.nbf) {
throw new Error(
`This token is invalid before epoch time '${payload.nbf}', current epoch time is '${currentTime}'`,
);
}
// Check that the token hasn't expired
if (currentTime > payload.exp) {
throw new Error(
`This token expired at epoch time '${payload.exp}', current epoch time is '${currentTime}'`,
);
}
// Check that the connected wallet matches the token issuer
const issuerAddress = parsedOptions.issuerAddress
? parsedOptions.issuerAddress
: await wallet.getAddress();
if (issuerAddress.toLowerCase() !== payload.iss.toLowerCase()) {
throw new Error(
`The expected issuer address '${issuerAddress}' did not match the token issuer address '${payload.iss}'`,
);
}
let chainId: number | undefined = undefined;
if (wallet.getChainId) {
try {
chainId = await wallet.getChainId();
} catch {
// ignore error
}
}
const verified = await wallet.verifySignature(
JSON.stringify(payload),
signature,
issuerAddress,
chainId,
);
if (!verified) {
throw new Error(
`The expected signer address '${issuerAddress}' did not sign the token`,
);
}
return {
address: payload.sub,
session: payload.ctx as TSession | undefined,
};
}
+224
View File
@@ -0,0 +1,224 @@
import {
BuildLoginPayloadParams,
SignLoginPayloadParams,
VerifyLoginPayloadParams,
} from "../schema/functions";
import {
LoginOptionsSchema,
LoginPayload,
LoginPayloadData,
LoginPayloadDataSchema,
} from "../schema/login";
import { VerifyOptionsSchema } from "../schema/verify";
/**
* Create an EIP-4361 & CAIP-122 compliant message to sign based on the login payload
*/
function createLoginMessage(payload: LoginPayloadData): string {
const typeField = payload.type === "evm" ? "Ethereum" : "Solana";
const header = `${payload.domain} wants you to sign in with your ${typeField} account:`;
let prefix = [header, payload.address].join("\n");
prefix = [prefix, payload.statement].join("\n\n");
if (payload.statement) {
prefix += "\n";
}
const suffixArray = [];
if (payload.uri) {
const uriField = `URI: ${payload.uri}`;
suffixArray.push(uriField);
}
const versionField = `Version: ${payload.version}`;
suffixArray.push(versionField);
if (payload.chain_id) {
const chainField = `Chain ID: ` + payload.chain_id || "1";
suffixArray.push(chainField);
}
const nonceField = `Nonce: ${payload.nonce}`;
suffixArray.push(nonceField);
const issuedAtField = `Issued At: ${payload.issued_at}`;
suffixArray.push(issuedAtField);
const expiryField = `Expiration Time: ${payload.expiration_time}`;
suffixArray.push(expiryField);
if (payload.invalid_before) {
const invalidBeforeField = `Not Before: ${payload.invalid_before}`;
suffixArray.push(invalidBeforeField);
}
if (payload.resources) {
suffixArray.push(
[`Resources:`, ...payload.resources.map((x) => `- ${x}`)].join("\n"),
);
}
const suffix = suffixArray.join("\n");
return [prefix, suffix].join("\n");
}
export async function buildLoginPayload({
wallet,
options,
}: BuildLoginPayloadParams): Promise<LoginPayloadData> {
const parsedOptions = LoginOptionsSchema.parse(options);
let chainId: string | undefined = parsedOptions?.chainId;
if (!chainId && wallet.getChainId) {
try {
chainId = (await wallet.getChainId()).toString();
} catch {
// ignore error
}
}
return LoginPayloadDataSchema.parse({
type: wallet.type,
domain: parsedOptions.domain,
address: parsedOptions?.address || (await wallet.getAddress()),
statement: parsedOptions?.statement,
version: parsedOptions?.version,
uri: parsedOptions?.uri,
chain_id: chainId,
nonce: parsedOptions?.nonce,
expiration_time: parsedOptions.expirationTime,
invalid_before: parsedOptions.invalidBefore,
resources: parsedOptions?.resources,
});
}
export async function signLoginPayload({
wallet,
payload,
}: SignLoginPayloadParams): Promise<LoginPayload> {
const message = createLoginMessage(payload);
const signature = await wallet.signMessage(message);
return {
payload,
signature,
};
}
export async function buildAndSignLoginPayload({
wallet,
options,
}: BuildLoginPayloadParams): Promise<LoginPayload> {
const payload = await buildLoginPayload({ wallet, options });
return signLoginPayload({ wallet, payload });
}
export async function verifyLoginPayload({
wallet,
payload,
options,
}: VerifyLoginPayloadParams): Promise<string> {
const parsedOptions = VerifyOptionsSchema.parse(options);
if (payload.payload.type !== wallet.type) {
throw new Error(
`Expected chain type '${wallet.type}' does not match chain type on payload '${payload.payload.type}'`,
);
}
// Check that the intended domain matches the domain of the payload
if (payload.payload.domain !== parsedOptions.domain) {
throw new Error(
`Expected domain '${parsedOptions.domain}' does not match domain on payload '${payload.payload.domain}'`,
);
}
// Check that the payload statement matches the expected statement
if (parsedOptions?.statement) {
if (payload.payload.statement !== parsedOptions.statement) {
throw new Error(
`Expected statement '${parsedOptions.statement}' does not match statement on payload '${payload.payload.statement}'`,
);
}
}
// Check that the intended URI matches the URI of the payload
if (parsedOptions?.uri) {
if (payload.payload.uri !== parsedOptions.uri) {
throw new Error(
`Expected URI '${parsedOptions.uri}' does not match URI on payload '${payload.payload.uri}'`,
);
}
}
// Check that the intended version matches the version of the payload
if (parsedOptions?.version) {
if (payload.payload.version !== parsedOptions.version) {
throw new Error(
`Expected version '${parsedOptions.version}' does not match version on payload '${payload.payload.version}'`,
);
}
}
// Check that the intended chain ID matches the chain ID of the payload
if (parsedOptions?.chainId) {
if (payload.payload.chain_id !== parsedOptions.chainId) {
throw new Error(
`Expected chain ID '${parsedOptions.chainId}' does not match chain ID on payload '${payload.payload.chain_id}'`,
);
}
}
// Check that the payload nonce is valid
if (parsedOptions?.validateNonce !== undefined) {
try {
await parsedOptions.validateNonce(payload.payload.nonce);
} catch (err) {
throw new Error(`Login request nonce is invalid`);
}
}
// Check that it isn't before the invalid before time
const currentTime = new Date();
if (currentTime < new Date(payload.payload.invalid_before)) {
throw new Error(`Login request is not yet valid`);
}
// Check that the payload hasn't expired
if (currentTime > new Date(payload.payload.expiration_time)) {
throw new Error(`Login request has expired`);
}
// Check that the specified resources are present on the payload
if (parsedOptions?.resources) {
const missingResources = parsedOptions.resources.filter(
(resource) => !payload.payload.resources?.includes(resource),
);
if (missingResources.length > 0) {
throw new Error(
`Login request is missing required resources: ${missingResources.join(
", ",
)}`,
);
}
}
// Check that the signing address is the claimed wallet address
const message = createLoginMessage(payload.payload);
const chainId =
wallet.type === "evm" && payload.payload.chain_id
? parseInt(payload.payload.chain_id)
: undefined;
const verified = await wallet.verifySignature(
message,
payload.signature,
payload.payload.address,
chainId,
);
if (!verified) {
throw new Error(
`Signer address does not match payload address '${payload.payload.address.toLowerCase()}'`,
);
}
return payload.payload.address;
}
+24 -1
View File
@@ -1,2 +1,25 @@
// Export thirdweb auth class
export { ThirdwebAuth } from "./auth";
export * from "./schema";
// Export individual auth functions
export {
generateJWT,
parseJWT,
refreshJWT,
authenticateJWT,
} from "./functions/jwt";
export {
buildLoginPayload,
signLoginPayload,
buildAndSignLoginPayload,
verifyLoginPayload,
} from "./functions/login";
// Export schema files individually
export * from "./schema/authenticate";
export * from "./schema/common";
export * from "./schema/functions";
export * from "./schema/generate";
export * from "./schema/login";
export * from "./schema/refresh";
export * from "./schema/verify";
-216
View File
@@ -1,216 +0,0 @@
import { utils, BigNumber } from "ethers";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
export const AddressSchema = z.string().refine(
(arg) => utils.isAddress(arg),
(out) => {
return {
message: `${out} is not a valid address`,
};
},
);
export const RawDateSchema = z.date().transform((i) => {
return BigNumber.from(Math.floor(i.getTime() / 1000));
});
export const AccountTypeSchema = z.union([
z.literal("evm"),
z.literal("solana"),
]);
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
type Literal = z.infer<typeof literalSchema>;
export type Json = Literal | { [key: string]: Json } | Json[];
const JsonSchema: z.ZodType<Json> = z.lazy(
() => z.union([literalSchema, z.array(JsonSchema), z.record(JsonSchema)]),
{ invalid_type_error: "Provided value was not valid JSON" },
);
/**
* @internal
*/
export const LoginOptionsSchema = z
.object({
domain: z.string().optional(),
address: z.string().optional(),
statement: z.string().optional(),
uri: z.string().optional(),
version: z.string().optional(),
chainId: z.string().optional(),
nonce: z.string().optional(),
expirationTime: z.date().optional(),
invalidBefore: z.date().optional(),
resources: z.array(z.string()).optional(),
})
.optional();
/**
* @internal
*/
export const LoginPayloadDataSchema = z.object({
type: AccountTypeSchema,
domain: z.string(),
address: z.string(),
statement: z
.string()
.default(
"Please ensure that the domain above matches the URL of the current website.",
),
uri: z.string().optional(),
version: z.string().default("1"),
chain_id: z.string().optional(),
nonce: z.string().default(uuidv4()),
issued_at: z
.date()
.default(new Date())
.transform((d) => d.toISOString()),
expiration_time: z.date().transform((d) => d.toISOString()),
invalid_before: z
.date()
.default(new Date())
.transform((d) => d.toISOString()),
resources: z.array(z.string()).optional(),
});
/**
* @internal
*/
export const LoginPayloadSchema = z.object({
payload: LoginPayloadDataSchema,
signature: z.string(),
});
/**
* @internal
*/
const VerifyOptionsSchemaRequired = z.object({
domain: z.string().optional(),
statement: z.string().optional(),
uri: z.string().optional(),
version: z.string().optional(),
chainId: z.string().optional(),
validateNonce: z.function().args(z.string()).optional(),
resources: z.array(z.string()).optional(),
});
/**
* @internal
*/
export const VerifyOptionsSchema = VerifyOptionsSchemaRequired.optional();
/**
* @internal
*/
export const GenerateOptionsSchema = z
.object({
domain: z.string().optional(),
tokenId: z.string().optional(),
expirationTime: z.date().optional(),
invalidBefore: z.date().optional(),
session: z.union([JsonSchema, z.function().args(z.string())]).optional(),
verifyOptions: VerifyOptionsSchemaRequired.omit({
domain: true,
}).optional(),
})
.optional();
/**
* @internal
*/
export const AuthenticationPayloadDataSchema = z.object({
iss: z.string(),
sub: z.string(),
aud: z.string(),
exp: RawDateSchema.transform((b) => b.toNumber()),
nbf: RawDateSchema.transform((b) => b.toNumber()),
iat: RawDateSchema.transform((b) => b.toNumber()),
jti: z.string().default(uuidv4()),
ctx: JsonSchema.optional(),
});
/**
* @internal
*/
export const AuthenticationPayloadSchema = z.object({
payload: AuthenticationPayloadDataSchema,
signature: z.string(),
});
/**
* @internal
*/
export const AuthenticateOptionsSchema = z
.object({
domain: z.string().optional(),
validateTokenId: z.function().args(z.string()).optional(),
})
.optional();
/**
* @public
*/
export type LoginOptions = z.input<typeof LoginOptionsSchema>;
/**
* @public
*/
export type LoginPayloadData = z.output<typeof LoginPayloadDataSchema>;
/**
* @public
*/
export type LoginPayload = z.output<typeof LoginPayloadSchema>;
/**
* @public
*/
export type VerifyOptions = z.input<typeof VerifyOptionsSchema>;
/**
* @public
*/
export type GenerateOptions = z.input<typeof GenerateOptionsSchema>;
/**
* @public
*/
export type AuthenticationPayloadData = z.output<
typeof AuthenticationPayloadDataSchema
>;
/**
* @internal
*/
export type AuthenticationPayloadDataInput = z.input<
typeof AuthenticationPayloadDataSchema
>;
/**
* @public
*/
export type AuthenticationPayload = z.output<
typeof AuthenticationPayloadSchema
>;
/**
* @public
*/
export type AuthenticateOptions = z.output<typeof AuthenticateOptionsSchema>;
/**
* @public
*/
export type User<TContext extends Json = Json> = {
address: string;
session?: TContext;
};
export const LoginPayloadOutputSchema = LoginPayloadSchema.extend({
payload: LoginPayloadDataSchema.extend({
issued_at: z.string(),
expiration_time: z.string(),
invalid_before: z.string(),
}),
});
@@ -0,0 +1,46 @@
import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import { AddressSchema, JsonSchema, RawDateSchema } from "./common";
export const AuthenticationPayloadDataSchema = z.object({
iss: z.string(),
sub: z.string(),
aud: z.string(),
exp: RawDateSchema,
nbf: RawDateSchema,
iat: RawDateSchema,
jti: z.string().default(uuidv4()),
ctx: JsonSchema.optional(),
});
export const AuthenticationPayloadSchema = z.object({
payload: AuthenticationPayloadDataSchema,
signature: z.string(),
});
export const AuthenticateOptionsSchema = z.object({
domain: z.string(),
issuerAddress: AddressSchema.optional(),
validateTokenId: z.function().args(z.string()).optional(),
});
export type AuthenticationPayloadDataInput = z.input<
typeof AuthenticationPayloadDataSchema
>;
export type AuthenticationPayloadData = z.output<
typeof AuthenticationPayloadDataSchema
>;
export type AuthenticationPayload = z.output<
typeof AuthenticationPayloadSchema
>;
export type AuthenticateOptions = z.output<typeof AuthenticateOptionsSchema>;
export type AuthenticateOptionsWithOptionalDomain = Omit<
AuthenticateOptions,
"domain"
> & {
domain?: string;
};
+34
View File
@@ -0,0 +1,34 @@
import { utils } from "ethers";
import { z } from "zod";
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
type Literal = z.infer<typeof literalSchema>;
export type Json = Literal | { [key: string]: Json } | Json[];
export const JsonSchema: z.ZodType<Json> = z.lazy(
() => z.union([literalSchema, z.array(JsonSchema), z.record(JsonSchema)]),
{ invalid_type_error: "Provided value was not valid JSON" },
);
export const AddressSchema = z.string().refine(
(arg) => utils.isAddress(arg),
(out) => {
return {
message: `${out} is not a valid address`,
};
},
);
export const RawDateSchema = z.date().transform((i) => {
return Math.floor(i.getTime() / 1000);
});
export const AccountTypeSchema = z.union([
z.literal("evm"),
z.literal("solana"),
]);
export type User<TContext extends Json = Json> = {
address: string;
session?: TContext;
};
@@ -0,0 +1,48 @@
import { GenericAuthWallet } from "@thirdweb-dev/wallets";
import { LoginOptions, LoginPayload, LoginPayloadData } from "./login";
import { VerifyOptions } from "./verify";
import {
AuthenticateOptions,
AuthenticationPayloadDataInput,
} from "./authenticate";
import { RefreshOptions } from "./refresh";
import { GenerateOptions } from "./generate";
export type BuildLoginPayloadParams = {
wallet: GenericAuthWallet;
options: LoginOptions;
};
export type SignLoginPayloadParams = {
wallet: GenericAuthWallet;
payload: LoginPayloadData;
};
export type VerifyLoginPayloadParams = {
wallet: GenericAuthWallet;
payload: LoginPayload;
options: VerifyOptions;
};
export type BuildJwtParams = {
wallet: GenericAuthWallet;
payload: AuthenticationPayloadDataInput;
};
export type GenerateJwtParams = {
wallet: GenericAuthWallet;
payload: LoginPayload;
options: GenerateOptions;
};
export type RefreshJwtParams = {
wallet: GenericAuthWallet;
jwt: string;
options?: RefreshOptions;
};
export type AuthenticateJwtParams = {
wallet: GenericAuthWallet;
jwt: string;
options: AuthenticateOptions;
};
+31
View File
@@ -0,0 +1,31 @@
import { z } from "zod";
import { VerifyOptionsSchema } from "./verify";
import { JsonSchema } from "./common";
import { THIRDWEB_AUTH_DEFAULT_TOKEN_DURATION_IN_SECONDS } from "../../constants";
export const GenerateOptionsSchema = z.object({
domain: z.string(),
tokenId: z.string().optional(),
expirationTime: z
.date()
.default(
() =>
new Date(
Date.now() + 1000 * THIRDWEB_AUTH_DEFAULT_TOKEN_DURATION_IN_SECONDS,
),
),
invalidBefore: z.date().optional(),
session: z.union([JsonSchema, z.function().args(z.string())]).optional(),
verifyOptions: VerifyOptionsSchema.omit({
domain: true,
}).optional(),
});
export type GenerateOptions = z.input<typeof GenerateOptionsSchema>;
export type GenerateOptionsWithOptionalDomain = Omit<
GenerateOptions,
"domain"
> & {
domain?: string;
};
+81
View File
@@ -0,0 +1,81 @@
import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import { AccountTypeSchema } from "./common";
import { THIRDWEB_AUTH_DEFAULT_LOGIN_PAYLOAD_DURATION_IN_SECONDS } from "../../constants";
export const LoginOptionsSchema = z.object({
domain: z.string(),
address: z.string().optional(),
statement: z.string().optional(),
uri: z.string().optional(),
version: z.string().optional(),
chainId: z.string().optional(),
nonce: z.string().optional(),
expirationTime: z
.date()
.default(
() =>
new Date(
Date.now() +
1000 * THIRDWEB_AUTH_DEFAULT_LOGIN_PAYLOAD_DURATION_IN_SECONDS,
),
),
invalidBefore: z
.date()
.default(
() =>
new Date(
Date.now() -
1000 * THIRDWEB_AUTH_DEFAULT_LOGIN_PAYLOAD_DURATION_IN_SECONDS,
),
),
resources: z.array(z.string()).optional(),
});
export const LoginPayloadDataSchema = z.object({
type: AccountTypeSchema,
domain: z.string(),
address: z.string(),
statement: z
.string()
.default(
"Please ensure that the domain above matches the URL of the current website.",
),
uri: z.string().optional(),
version: z.string().default("1"),
chain_id: z.string().optional(),
nonce: z.string().default(uuidv4()),
issued_at: z
.date()
.default(new Date())
.transform((d) => d.toISOString()),
expiration_time: z.date().transform((d) => d.toISOString()),
invalid_before: z
.date()
.default(new Date())
.transform((d) => d.toISOString()),
resources: z.array(z.string()).optional(),
});
export const LoginPayloadSchema = z.object({
payload: LoginPayloadDataSchema,
signature: z.string(),
});
export const LoginPayloadOutputSchema = LoginPayloadSchema.extend({
payload: LoginPayloadDataSchema.extend({
issued_at: z.string(),
expiration_time: z.string(),
invalid_before: z.string(),
}),
});
export type LoginOptions = z.input<typeof LoginOptionsSchema>;
export type LoginPayloadData = z.output<typeof LoginPayloadDataSchema>;
export type LoginPayload = z.output<typeof LoginPayloadSchema>;
export type LoginOptionsWithOptionalDomain = Omit<LoginOptions, "domain"> & {
domain?: string;
};
+15
View File
@@ -0,0 +1,15 @@
import { z } from "zod";
import { THIRDWEB_AUTH_DEFAULT_TOKEN_DURATION_IN_SECONDS } from "../../constants";
export const RefreshOptionsSchema = z.object({
expirationTime: z
.date()
.default(
() =>
new Date(
Date.now() + 1000 * THIRDWEB_AUTH_DEFAULT_TOKEN_DURATION_IN_SECONDS,
),
),
});
export type RefreshOptions = z.input<typeof RefreshOptionsSchema>;
+17
View File
@@ -0,0 +1,17 @@
import { z } from "zod";
export const VerifyOptionsSchema = z.object({
domain: z.string(),
statement: z.string().optional(),
uri: z.string().optional(),
version: z.string().optional(),
chainId: z.string().optional(),
validateNonce: z.function().args(z.string()).optional(),
resources: z.array(z.string()).optional(),
});
export type VerifyOptions = z.input<typeof VerifyOptionsSchema>;
export type VerifyOptionsWithOptionalDomain = Omit<VerifyOptions, "domain"> & {
domain?: string;
};
-9
View File
@@ -1,9 +0,0 @@
/**
* @internal
*/
export const isBrowser = () => typeof window !== "undefined";
/**
* @internal
*/
export const isNode = () => !isBrowser();
+1 -1
View File
@@ -1,5 +1,5 @@
import { ThirdwebAuth } from "../../core";
import { Json, LoginPayloadOutputSchema, User } from "../../core/schema";
import { Json, LoginPayloadOutputSchema, User } from "../../core";
import type { GenericAuthWallet } from "@thirdweb-dev/wallets";
import { Request } from "express";
import { z } from "zod";
+1 -2
View File
@@ -1,5 +1,4 @@
import { ThirdwebAuth } from "../../core";
import { Json, LoginPayloadOutputSchema, User } from "../../core/schema";
import { Json, LoginPayloadOutputSchema, ThirdwebAuth, User } from "../../core";
import type { GenericAuthWallet } from "@thirdweb-dev/wallets";
import { GetServerSidePropsContext, NextApiRequest } from "next";
import { NextRequest } from "next/server";
+22 -2
View File
@@ -3,7 +3,7 @@ import { EthersWallet } from "@thirdweb-dev/wallets/evm/wallets/ethers";
import { expect } from "chai";
import { Wallet } from "ethers";
describe("Wallet Authentication", async () => {
describe("Wallet Authentication - EVM", async () => {
let adminWallet: any, signerWallet: any, attackerWallet: any;
let auth: ThirdwebAuth;
@@ -272,7 +272,7 @@ describe("Wallet Authentication", async () => {
expect.fail();
} catch (err: any) {
expect(err.message).to.contain(
`Expected the connected wallet address '${await signerWallet.getAddress()}' to match the token issuer address '${await adminWallet.getAddress()}'`,
`The expected issuer address '${await signerWallet.getAddress()}' did not match the token issuer address '${await adminWallet.getAddress()}'`,
);
}
});
@@ -350,4 +350,24 @@ describe("Wallet Authentication", async () => {
role: "admin",
});
});
it("Should authenticate with issuer address", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const token = await auth.generate(payload);
auth.updateWallet(attackerWallet);
try {
await auth.authenticate(token);
expect.fail();
} catch (err: any) {
expect(err.message).to.contain("The expected issuer address");
}
const user = await auth.authenticate(token, {
issuerAddress: await adminWallet.getAddress(),
});
expect(user.address).to.equal(await signerWallet.getAddress());
});
});
+2 -2
View File
@@ -3,7 +3,7 @@ import { Keypair } from "@solana/web3.js";
import { KeypairWallet } from "@thirdweb-dev/wallets/solana/wallets/keypair";
import { expect } from "chai";
describe("Wallet Authentication", async () => {
describe("Wallet Authentication - Solana", async () => {
let adminWallet: any, signerWallet: any, attackerWallet: any;
let auth: ThirdwebAuth;
@@ -272,7 +272,7 @@ describe("Wallet Authentication", async () => {
expect.fail();
} catch (err: any) {
expect(err.message).to.contain(
`Expected the connected wallet address '${await signerWallet.getAddress()}' to match the token issuer address '${await adminWallet.getAddress()}'`,
`The expected issuer address '${await signerWallet.getAddress()}' did not match the token issuer address '${await adminWallet.getAddress()}'`,
);
}
});
+6
View File
@@ -1,5 +1,11 @@
# @thirdweb-dev/chains
## 0.1.42
### Patch Changes
- [#1450](https://github.com/thirdweb-dev/js/pull/1450) [`262edc6a`](https://github.com/thirdweb-dev/js/commit/262edc6a46792da88f49ff6ef0a756a932a6a0cf) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - update chains
## 0.1.41
### Patch Changes
+2 -1
View File
@@ -7,7 +7,8 @@ export default {
"https://rpc.syscoin.org",
"https://rpc.ankr.com/syscoin/${ANKR_API_KEY}",
"https://syscoin.public-rpc.com",
"wss://rpc.syscoin.org/wss"
"wss://rpc.syscoin.org/wss",
"https://syscoin-evm.publicnode.com"
],
"faucets": [
"https://faucet.syscoin.org"
+2 -1
View File
@@ -5,7 +5,8 @@ export default {
"rpc": [
"https://syscoin-tanenbaum-testnet.rpc.thirdweb.com/${THIRDWEB_API_KEY}",
"https://rpc.tanenbaum.io",
"wss://rpc.tanenbaum.io/wss"
"wss://rpc.tanenbaum.io/wss",
"https://syscoin-tanenbaum-evm.publicnode.com"
],
"faucets": [
"https://faucet.tanenbaum.io"
+2 -1
View File
@@ -11,7 +11,8 @@ export default {
"rpc": [
"https://mind-smart-chain.rpc.thirdweb.com/${THIRDWEB_API_KEY}",
"https://rpc-msc.mindchain.info/",
"https://seednode.mindchain.info"
"https://seednode.mindchain.info",
"wss://seednode.mindchain.info/ws"
],
"faucets": [],
"nativeCurrency": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/chains",
"version": "0.1.41",
"version": "0.1.42",
"main": "dist/thirdweb-dev-chains.cjs.js",
"module": "dist/thirdweb-dev-chains.esm.js",
"browser": {
File diff suppressed because one or more lines are too long
+17
View File
@@ -1,5 +1,22 @@
# thirdweb
## 0.11.10
### Patch Changes
- Updated dependencies [[`262edc6a`](https://github.com/thirdweb-dev/js/commit/262edc6a46792da88f49ff6ef0a756a932a6a0cf), [`262edc6a`](https://github.com/thirdweb-dev/js/commit/262edc6a46792da88f49ff6ef0a756a932a6a0cf)]:
- @thirdweb-dev/chains@0.1.42
- @thirdweb-dev/sdk@3.10.46
## 0.11.9
### Patch Changes
- [#1442](https://github.com/thirdweb-dev/js/pull/1442) [`7f54012e`](https://github.com/thirdweb-dev/js/commit/7f54012ec648b727b60784990e71b0efb4690934) Thanks [@Marfuen](https://github.com/Marfuen)! - - Warn users on windows to not use the powershell as there are some compatibility issues with it at the moment.
- Warn users if they are pasting a client id instead of a secret key.
- Updated dependencies [[`2a91113a`](https://github.com/thirdweb-dev/js/commit/2a91113a760733fcff2aec90041f69e15de33905)]:
- @thirdweb-dev/sdk@3.10.45
## 0.11.8
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "thirdweb",
"main": "dist/cli/index.js",
"version": "0.11.8",
"version": "0.11.10",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/cli",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
+19
View File
@@ -2,6 +2,7 @@ import chalk from "chalk";
import prompts from "prompts";
import Cache, { CacheEntry } from "sync-disk-cache";
import { ApiResponse } from "../lib/types";
import os from "os";
export async function loginUser(
cache: Cache,
@@ -40,6 +41,10 @@ export function getSession(cache: Cache) {
export async function createSession(cache: Cache) {
try {
const isWindows = os.type() === "Windows_NT";
if (isWindows) {
console.log(chalk.yellow("Windows detected: if you are using powershell, there are some known issues with it that we are actively working on, please use git bash or the command prompt. Thank you for your understanding."));
}
const response = await prompts({
type: "invisible",
name: "apiSecretKey",
@@ -48,6 +53,20 @@ export async function createSession(cache: Cache) {
)}`,
});
const keyPassed = response.apiSecretKey;
if (!keyPassed) {
console.log(chalk.red("You need to pass an API secret key"));
process.exit(1);
}
if (keyPassed.length === 32) {
console.log(chalk.red(`This is not a valid secret key. To get your secret key
1. Create an API key at https://thirdweb.com/create-api-key
2. Store and copy your Secret Key. This will be shown only once.
3. Paste it in the CLI when prompted`));
process.exit(1);
}
try {
await validateKey(response.apiSecretKey);
} catch (error) {
+20
View File
@@ -1,5 +1,25 @@
# @thirdweb-dev/react-core
## 3.14.27
### Patch Changes
- [#1392](https://github.com/thirdweb-dev/js/pull/1392) [`dfd120a3`](https://github.com/thirdweb-dev/js/commit/dfd120a3a9d1582c8b174265c92bf43dbbaf5c86) Thanks [@adam-maj](https://github.com/adam-maj)! - Expose functions from auth and update useAuth
- Updated dependencies [[`262edc6a`](https://github.com/thirdweb-dev/js/commit/262edc6a46792da88f49ff6ef0a756a932a6a0cf), [`262edc6a`](https://github.com/thirdweb-dev/js/commit/262edc6a46792da88f49ff6ef0a756a932a6a0cf), [`dfd120a3`](https://github.com/thirdweb-dev/js/commit/dfd120a3a9d1582c8b174265c92bf43dbbaf5c86)]:
- @thirdweb-dev/chains@0.1.42
- @thirdweb-dev/sdk@3.10.46
- @thirdweb-dev/auth@3.2.27
- @thirdweb-dev/wallets@1.1.10
## 3.14.26
### Patch Changes
- Updated dependencies [[`2a91113a`](https://github.com/thirdweb-dev/js/commit/2a91113a760733fcff2aec90041f69e15de33905)]:
- @thirdweb-dev/sdk@3.10.45
- @thirdweb-dev/wallets@1.1.9
## 3.14.25
### Patch Changes
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react-core",
"version": "3.14.25",
"version": "3.14.27",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react-core",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
@@ -104,6 +104,7 @@
"@thirdweb-dev/sdk": "workspace:*",
"@thirdweb-dev/storage": "workspace:*",
"@thirdweb-dev/wallets": "workspace:*",
"@thirdweb-dev/auth": "workspace:*",
"mime": "3.0.0",
"tiny-invariant": "^1.2.0"
},
+14 -17
View File
@@ -1,10 +1,9 @@
import { LoginOptions } from "@thirdweb-dev/auth";
import { ThirdwebAuth } from "@thirdweb-dev/auth";
import { useWallet } from "../../../core/hooks/wallet-hooks";
import { useThirdwebAuthContext } from "../../contexts/thirdweb-auth";
import { doLogin } from "./useLogin";
import invariant from "tiny-invariant";
import { useMemo } from "react";
export { useLogin, doLogin } from "./useLogin";
export { useLogin } from "./useLogin";
export { useLogout } from "./useLogout";
export type { UserWithData } from "./useUser";
export { useUser } from "./useUser";
@@ -14,17 +13,15 @@ export function useAuth() {
const wallet = useWallet();
const authConfig = useThirdwebAuthContext();
return {
login: async (options?: LoginOptions) => {
invariant(
authConfig,
"Please specify an authConfig in the ThirdwebProvider",
);
invariant(wallet, "Need a connected a wallet!");
return doLogin(wallet, {
domain: authConfig.domain,
...options,
});
},
};
return useMemo(() => {
if (!authConfig?.domain) {
return undefined;
}
if (!wallet) {
return undefined;
}
return new ThirdwebAuth(wallet, authConfig.domain)
}, [wallet, authConfig, authConfig?.domain])
}
@@ -2,12 +2,7 @@ import { useWallet } from "../../../core/hooks/wallet-hooks";
import { useThirdwebAuthContext } from "../../contexts/thirdweb-auth";
import { cacheKeys } from "../../utils/cache-keys";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type {
LoginOptions,
LoginPayload,
LoginPayloadData,
} from "@thirdweb-dev/auth";
import { GenericAuthWallet } from "@thirdweb-dev/wallets";
import { signLoginPayload, type LoginPayloadData } from "@thirdweb-dev/auth";
import invariant from "tiny-invariant";
import { AUTH_TOKEN_STORAGE_KEY } from "../../../core/constants/auth";
@@ -59,7 +54,7 @@ export function useLogin() {
throw new Error(`Failed to get payload`);
}
const payload = await doLoginWithPayload(wallet, payloadData);
const payload = await signLoginPayload({ wallet, payload: payloadData });
res = await fetch(`${authConfig.authUrl}/login`, {
method: "POST",
@@ -93,106 +88,3 @@ export function useLogin() {
isLoading: login.isLoading,
};
}
export async function doLoginWithPayload(
wallet: GenericAuthWallet,
payload: LoginPayloadData,
): Promise<LoginPayload> {
const message = generateMessage(payload);
const signature = await wallet.signMessage(message);
return {
payload,
signature,
};
}
export async function doLogin(
wallet: GenericAuthWallet,
options?: LoginOptions,
): Promise<LoginPayload> {
let chainId: string | undefined = options?.chainId;
if (!chainId && wallet.getChainId) {
try {
chainId = (await wallet.getChainId()).toString();
} catch {
// ignore error
}
}
invariant(options?.domain, "Please specify a domain to login with.");
// generate a pseudo-random nonce if none is provided
const nonce =
options?.nonce || Math.floor(Math.random() * 1000000000).toString();
const payloadData: LoginPayloadData = {
type: wallet.type,
domain: options.domain,
address: await wallet.getAddress(),
statement:
options?.statement ||
"Please ensure that the domain above matches the URL of the current website.",
version: options?.version || "1",
uri: options?.uri,
chain_id: chainId,
nonce: options?.nonce || nonce,
issued_at: new Date().toISOString(),
expiration_time: new Date(
options?.expirationTime || Date.now() + 1000 * 60 * 10,
).toISOString(),
invalid_before: new Date(
options?.invalidBefore || Date.now() - 1000 * 60 * 10,
).toISOString(),
resources: options?.resources,
};
return doLoginWithPayload(wallet, payloadData);
}
// generate straight from auth
function generateMessage(payload: LoginPayloadData): string {
const typeField = payload.type === "evm" ? "Ethereum" : "Solana";
const header = `${payload.domain} wants you to sign in with your ${typeField} account:`;
let prefix = [header, payload.address].join("\n");
prefix = [prefix, payload.statement].join("\n\n");
if (payload.statement) {
prefix += "\n";
}
const suffixArray = [];
if (payload.uri) {
const uriField = `URI: ${payload.uri}`;
suffixArray.push(uriField);
}
const versionField = `Version: ${payload.version}`;
suffixArray.push(versionField);
if (payload.chain_id) {
const chainField = `Chain ID: ` + payload.chain_id || "1";
suffixArray.push(chainField);
}
const nonceField = `Nonce: ${payload.nonce}`;
suffixArray.push(nonceField);
const issuedAtField = `Issued At: ${payload.issued_at}`;
suffixArray.push(issuedAtField);
const expiryField = `Expiration Time: ${payload.expiration_time}`;
suffixArray.push(expiryField);
if (payload.invalid_before) {
const invalidBeforeField = `Not Before: ${payload.invalid_before}`;
suffixArray.push(invalidBeforeField);
}
if (payload.resources) {
suffixArray.push(
[`Resources:`, ...payload.resources.map((x) => `- ${x}`)].join("\n"),
);
}
const suffix = suffixArray.join("\n");
return [prefix, suffix].join("\n");
}
@@ -1,5 +1,9 @@
# @thirdweb-dev/react-native-compat
## 0.2.46
## 0.2.45
## 0.2.44
## 0.2.43
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@thirdweb-dev/react-native-compat",
"description": "Shims for Thirdweb in React Native Projects",
"version": "0.2.44",
"version": "0.2.46",
"author": "thirdweb eng <[email protected]>",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react-native-compat",
"license": "Apache-2.0",
+21
View File
@@ -1,5 +1,26 @@
# @thirdweb-dev/react-native
## 0.2.46
### Patch Changes
- [#1452](https://github.com/thirdweb-dev/js/pull/1452) [`3ba8ba42`](https://github.com/thirdweb-dev/js/commit/3ba8ba428845f5b2ae90ca73855be275fc78373c) Thanks [@iketw](https://github.com/iketw)! - Upgrade Coinbase wallet SDK; it adds support for React Native 0.72.3
- Updated dependencies [[`262edc6a`](https://github.com/thirdweb-dev/js/commit/262edc6a46792da88f49ff6ef0a756a932a6a0cf), [`262edc6a`](https://github.com/thirdweb-dev/js/commit/262edc6a46792da88f49ff6ef0a756a932a6a0cf), [`dfd120a3`](https://github.com/thirdweb-dev/js/commit/dfd120a3a9d1582c8b174265c92bf43dbbaf5c86)]:
- @thirdweb-dev/chains@0.1.42
- @thirdweb-dev/sdk@3.10.46
- @thirdweb-dev/react-core@3.14.27
- @thirdweb-dev/wallets@1.1.10
## 0.2.45
### Patch Changes
- Updated dependencies [[`2a91113a`](https://github.com/thirdweb-dev/js/commit/2a91113a760733fcff2aec90041f69e15de33905)]:
- @thirdweb-dev/sdk@3.10.45
- @thirdweb-dev/react-core@3.14.26
- @thirdweb-dev/wallets@1.1.9
## 0.2.44
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react-native",
"version": "0.2.44",
"version": "0.2.46",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react-native",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
@@ -24,7 +24,7 @@
"android"
],
"dependencies": {
"@coinbase/wallet-mobile-sdk": "1.0.6",
"@coinbase/wallet-mobile-sdk": "1.0.7",
"@magic-sdk/provider": "17.2.0",
"@magic-sdk/react-native-bare": "^18.5.0",
"@shopify/restyle": "^2.4.2",
+19
View File
@@ -1,5 +1,24 @@
# @thirdweb-dev/react
## 3.14.27
### Patch Changes
- Updated dependencies [[`262edc6a`](https://github.com/thirdweb-dev/js/commit/262edc6a46792da88f49ff6ef0a756a932a6a0cf), [`262edc6a`](https://github.com/thirdweb-dev/js/commit/262edc6a46792da88f49ff6ef0a756a932a6a0cf), [`dfd120a3`](https://github.com/thirdweb-dev/js/commit/dfd120a3a9d1582c8b174265c92bf43dbbaf5c86)]:
- @thirdweb-dev/chains@0.1.42
- @thirdweb-dev/sdk@3.10.46
- @thirdweb-dev/react-core@3.14.27
- @thirdweb-dev/wallets@1.1.10
## 3.14.26
### Patch Changes
- Updated dependencies [[`2a91113a`](https://github.com/thirdweb-dev/js/commit/2a91113a760733fcff2aec90041f69e15de33905)]:
- @thirdweb-dev/sdk@3.10.45
- @thirdweb-dev/react-core@3.14.26
- @thirdweb-dev/wallets@1.1.9
## 3.14.25
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react",
"version": "3.14.25",
"version": "3.14.27",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
+15
View File
@@ -1,5 +1,20 @@
# @thirdweb-dev/sdk
## 3.10.46
### Patch Changes
- [#1450](https://github.com/thirdweb-dev/js/pull/1450) [`262edc6a`](https://github.com/thirdweb-dev/js/commit/262edc6a46792da88f49ff6ef0a756a932a6a0cf) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Sanitize description strings for OpenEdition sharedMetadata
- Updated dependencies [[`262edc6a`](https://github.com/thirdweb-dev/js/commit/262edc6a46792da88f49ff6ef0a756a932a6a0cf)]:
- @thirdweb-dev/chains@0.1.42
## 3.10.45
### Patch Changes
- [#1449](https://github.com/thirdweb-dev/js/pull/1449) [`2a91113a`](https://github.com/thirdweb-dev/js/commit/2a91113a760733fcff2aec90041f69e15de33905) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Revert sanitizing inputs
## 3.10.44
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/sdk",
"version": "3.10.44",
"version": "3.10.46",
"description": "The main thirdweb SDK.",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/sdk",
"license": "Apache-2.0",
+1 -30
View File
@@ -12,41 +12,12 @@ import { z } from "zod";
export const BasicNFTInput = /* @__PURE__ */ (() =>
z.object({
name: z.union([z.string(), z.number()]).optional().nullable(),
description: z
.string()
.nullable()
.optional()
.nullable()
.transform((i) => {
return sanitizeJSONString(i);
}),
description: z.string().nullable().optional().nullable(),
image: FileOrBufferOrStringSchema.nullable().optional(),
animation_url: FileOrBufferOrStringSchema.optional().nullable(),
}))();
function sanitizeJSONString(val: string | undefined | null) {
if (!val) {
return val;
}
return JSON.parse(JSON.stringify(val, escape));
}
function escape(key: string, val: any) {
if (typeof val !== "string") {
return val;
}
return val
.replace(/[\"]/g, '\\"')
.replace(/[\\]/g, "\\\\")
.replace(/[\/]/g, "\\/")
.replace(/[\b]/g, "\\b")
.replace(/[\f]/g, "\\f")
.replace(/[\n]/g, "\\n")
.replace(/[\r]/g, "\\r")
.replace(/[\t]/g, "\\t");
}
/**
* @internal
*/
@@ -87,6 +87,10 @@ export class Erc721SharedMetadata implements DetectableFeature {
metadata: BasicNFTInput,
): Promise<Transaction<TransactionResult>> => {
const parsedMetadata = BasicNFTInput.parse(metadata);
// cleanup description
parsedMetadata.description = this.sanitizeJSONString(
parsedMetadata.description,
);
// take the input and upload image and animation if it is not a URI already
const batch = [];
@@ -122,4 +126,12 @@ export class Erc721SharedMetadata implements DetectableFeature {
});
},
);
private sanitizeJSONString(val: string | undefined | null) {
if (!val) {
return val;
}
const sanitized = JSON.stringify(val);
return sanitized.slice(1, sanitized.length - 1);
}
}
+16
View File
@@ -1,5 +1,21 @@
# @thirdweb-dev/service-utils
## 0.4.0
### Minor Changes
- [#1448](https://github.com/thirdweb-dev/js/pull/1448) [`3e1c4045`](https://github.com/thirdweb-dev/js/commit/3e1c4045e7c58e2fe58e2ab6a7f767c8f5e206e9) Thanks [@arcoraven](https://github.com/arcoraven)! - Add logHttpRequest helper func
### Patch Changes
- [#1453](https://github.com/thirdweb-dev/js/pull/1453) [`0647f124`](https://github.com/thirdweb-dev/js/commit/0647f12498ed1cdd5aca4dcea5bd3cf0d5d3a23b) Thanks [@arcoraven](https://github.com/arcoraven)! - Remove clientId from logRequest
## 0.3.1
### Patch Changes
- [#1447](https://github.com/thirdweb-dev/js/pull/1447) [`b103872d`](https://github.com/thirdweb-dev/js/commit/b103872daff87b032082a433713d16b9dee13082) Thanks [@nessup](https://github.com/nessup)! - Export extractAuthorizationData for CF Workers
## 0.3.0
### Minor Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/service-utils",
"version": "0.3.0",
"version": "0.4.0",
"main": "dist/thirdweb-dev-service-utils.cjs.js",
"module": "dist/thirdweb-dev-service-utils.esm.js",
"exports": {
+36 -2
View File
@@ -1,4 +1,8 @@
import type { ExecutionContext, KVNamespace } from "@cloudflare/workers-types";
import type {
ExecutionContext,
KVNamespace,
Response,
} from "@cloudflare/workers-types";
import type {
ApiKeyMetadata,
AccountMetadata,
@@ -73,7 +77,7 @@ export async function authorizeWorker(
});
}
async function extractAuthorizationData(
export async function extractAuthorizationData(
authInput: AuthInput,
): Promise<AuthorizationInput> {
const requestUrl = new URL(authInput.req.url);
@@ -167,3 +171,33 @@ function bufferToHex(buffer: ArrayBuffer) {
.map((x) => x.toString(16).padStart(2, "0"))
.join("");
}
export async function logHttpRequest({
source,
clientId,
req,
res,
isAuthed,
statusMessage,
}: AuthInput & {
source: string;
res: Response;
isAuthed?: boolean;
statusMessage?: Error | string;
}) {
const authorizationData = await extractAuthorizationData({ req, clientId });
console.log(
JSON.stringify({
source,
pathname: req.url,
hasSecretKey: !!authorizationData.secretKey,
hasClientId: !!authorizationData.clientId,
hasJwt: !!authorizationData.jwt,
clientId: authorizationData.clientId,
isAuthed: !!isAuthed ?? null,
status: res.status,
}),
);
console.log(`statusMessage=${statusMessage ?? res.statusText}`);
}
+36 -2
View File
@@ -1,10 +1,12 @@
import type { IncomingHttpHeaders, IncomingMessage } from "node:http";
import { createHash } from "node:crypto";
import { authorize } from "../core/authorize";
import type { IncomingHttpHeaders, IncomingMessage } from "node:http";
import type { AuthorizationInput } from "../core/authorize";
import type { CoreServiceConfig } from "../core/api";
import { authorize } from "../core/authorize";
import type { AuthorizationResult } from "../core/authorize/types";
import type { CoreAuthInput } from "../core/types";
import type { ServerResponse } from "http";
export * from "../core/services";
@@ -147,3 +149,35 @@ export function hashSecretKey(secretKey: string) {
export function deriveClientIdFromSecretKeyHash(secretKeyHash: string) {
return secretKeyHash.slice(0, 32);
}
export function logHttpRequest({
source,
clientId,
req,
res,
isAuthed,
statusMessage,
}: AuthInput & {
source: string;
res: ServerResponse;
isAuthed?: boolean;
statusMessage?: Error | string;
}) {
const authorizationData = extractAuthorizationData({ req, clientId });
const _statusMessage = statusMessage ?? res.statusMessage;
console.log(
JSON.stringify({
source,
pathname: req.url,
hasSecretKey: !!authorizationData.secretKey,
hasClientId: !!authorizationData.clientId,
hasJwt: !!authorizationData.jwt,
clientId: authorizationData.clientId,
isAuthed: !!isAuthed ?? null,
status: res.statusCode,
statusMessage: _statusMessage,
}),
);
console.log(`statusMessage=${_statusMessage}`);
}
+19
View File
@@ -1,5 +1,24 @@
# @thirdweb-dev/unity-js-bridge
## 0.2.64
### Patch Changes
- Updated dependencies [[`262edc6a`](https://github.com/thirdweb-dev/js/commit/262edc6a46792da88f49ff6ef0a756a932a6a0cf), [`262edc6a`](https://github.com/thirdweb-dev/js/commit/262edc6a46792da88f49ff6ef0a756a932a6a0cf), [`dfd120a3`](https://github.com/thirdweb-dev/js/commit/dfd120a3a9d1582c8b174265c92bf43dbbaf5c86)]:
- @thirdweb-dev/chains@0.1.42
- @thirdweb-dev/sdk@3.10.46
- @thirdweb-dev/auth@3.2.27
- @thirdweb-dev/wallets@1.1.10
## 0.2.63
### Patch Changes
- Updated dependencies [[`2a91113a`](https://github.com/thirdweb-dev/js/commit/2a91113a760733fcff2aec90041f69e15de33905)]:
- @thirdweb-dev/sdk@3.10.45
- @thirdweb-dev/wallets@1.1.9
- @thirdweb-dev/auth@3.2.26
## 0.2.62
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/unity-js-bridge",
"version": "0.2.62",
"version": "0.2.64",
"main": "dist/thirdweb-unity-bridge.js",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/unity-js-bridge",
"license": "Apache-2.0",
+15
View File
@@ -1,5 +1,20 @@
# @thirdweb-dev/wallets
## 1.1.10
### Patch Changes
- Updated dependencies [[`262edc6a`](https://github.com/thirdweb-dev/js/commit/262edc6a46792da88f49ff6ef0a756a932a6a0cf), [`262edc6a`](https://github.com/thirdweb-dev/js/commit/262edc6a46792da88f49ff6ef0a756a932a6a0cf)]:
- @thirdweb-dev/chains@0.1.42
- @thirdweb-dev/sdk@3.10.46
## 1.1.9
### Patch Changes
- Updated dependencies [[`2a91113a`](https://github.com/thirdweb-dev/js/commit/2a91113a760733fcff2aec90041f69e15de33905)]:
- @thirdweb-dev/sdk@3.10.45
## 1.1.8
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/wallets",
"version": "1.1.8",
"version": "1.1.10",
"main": "dist/thirdweb-dev-wallets.cjs.js",
"module": "dist/thirdweb-dev-wallets.esm.js",
"types": "dist/thirdweb-dev-wallets.cjs.d.ts",
+2533 -242
View File
File diff suppressed because it is too large Load Diff