Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ dist
.DS_Store
tmp
.pnpm-store
session.json
.nx
24 changes: 24 additions & 0 deletions examples/auth-caching-relay-signer/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
require('dotenv').config();

const { Defender } = require('@openzeppelin/defender-sdk');
const fs = require('fs');

async function main() {
const creds = {
relayerApiKey: process.env.RELAYER_API_KEY,
relayerApiSecret: process.env.RELAYER_API_SECRET,
};
const client = new Defender(creds);

const relayerApiKey = client.relaySigner.getApiKey();
const accessToken = await client.relaySigner.getAccessToken();

// writes session.json with apiKey and accessToken
fs.writeFileSync('session.json', JSON.stringify({ relayerApiKey, accessToken }, null, 2));

console.log('Done! ✅');
}

if (require.main === module) {
main().catch(console.error);
}
29 changes: 29 additions & 0 deletions examples/auth-caching-relay-signer/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
require('dotenv').config();

const { Defender } = require('@openzeppelin/defender-sdk');
const fs = require('fs');

async function main() {
let cachedCredentials = {};

try {
cachedCredentials = JSON.parse(fs.readFileSync('session.json'));
} catch (e) {
console.error('❌ No credentials found. Please run "auth" command first.');
return;
}

try {
const client = new Defender(cachedCredentials);
const info = await client.relaySigner.getRelayerStatus();
console.log('relayerStatus', JSON.stringify(info, null, 2));
} catch (e) {
if (e.response?.status === 401) {
console.error('❌ Access token expired. Please run "auth" command again.');
}
}
}

if (require.main === module) {
main().catch(console.error);
}
16 changes: 16 additions & 0 deletions examples/auth-caching-relay-signer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@openzeppelin/defender-sdk-example-auth-caching",
"version": "1.13.1",
"private": true,
"main": "index.js",
"author": "Marcos Carlomagno <marcos.carlomagno@openzeppelin.com>",
"license": "MIT",
"scripts": {
"auth": "node auth.js",
"start": "node index.js"
},
"dependencies": {
"@openzeppelin/defender-sdk": "1.13.1",
"dotenv": "^16.3.1"
}
}
8 changes: 8 additions & 0 deletions packages/account/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ export class AccountClient extends BaseApiClient {
return process.env.DEFENDER_API_URL ?? 'https://defender-api.openzeppelin.com/';
}

public getApiKey(): string {
return this.getKey();
}

public getAccessToken(): Promise<string> {
return this.getToken();
}

public async getUsage(params?: { date?: string | Date; quotas: string[] }): Promise<AccountUsageResponse> {
const searchParams = new URLSearchParams({
...(params?.quotas && { quotas: params.quotas.join(',') }),
Expand Down
8 changes: 8 additions & 0 deletions packages/action/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ export class ActionClient extends BaseApiClient {
return process.env.DEFENDER_API_URL || 'https://defender-api.openzeppelin.com/';
}

public getApiKey(): string {
return this.getKey();
}

public getAccessToken(): Promise<string> {
return this.getToken();
}

public async list(): Promise<ActionListResponse> {
return this.apiCall(async (api) => {
return await api.get(`/actions`);
Expand Down
44 changes: 38 additions & 6 deletions packages/base/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ type ApiFunction<TResponse> = (api: AxiosInstance) => Promise<TResponse>;
export abstract class BaseApiClient {
private api: AxiosInstance | undefined;
private session: CognitoUserSession | undefined;

private sessionV2: { accessToken: string; refreshToken: string } | undefined;
private apiSecret: string;
private apiSecret: string | undefined;
private httpsAgent?: https.Agent;
private retryConfig: RetryConfig;
private accessToken: string | undefined;
private authConfig: AuthConfig;

protected apiKey: string;
Expand All @@ -35,30 +37,38 @@ export abstract class BaseApiClient {
protected abstract getApiUrl(type?: AuthType): string;

public constructor(params: {
apiKey: string;
apiSecret: string;
apiKey?: string;
apiSecret?: string;
httpsAgent?: https.Agent;
retryConfig?: Partial<RetryConfig>;
accessToken?: string;
authConfig?: AuthConfig;
}) {
if (!params.apiKey) throw new Error(`API key is required`);
if (!params.apiSecret) throw new Error(`API secret is required`);
if (!params.apiSecret && !params.accessToken) throw new Error(`API secret or access token is required`);

this.apiKey = params.apiKey;
this.apiSecret = params.apiSecret;
this.accessToken = params.accessToken;
this.httpsAgent = params.httpsAgent;
this.retryConfig = { retries: 3, retryDelay: exponentialDelay, ...params.retryConfig };
this.authConfig = params.authConfig ?? { useCredentialsCaching: true, type: 'admin' };
}

private async getAccessToken(): Promise<string> {
public async getAccessToken(): Promise<string> {
if (this.accessToken) return this.accessToken;
if (!this.apiSecret) throw new Error('Cannot authenticate without API secret.');

const userPass = { Username: this.apiKey, Password: this.apiSecret };
const poolData = { UserPoolId: this.getPoolId(), ClientId: this.getPoolClientId() };
const auth = await authenticate(userPass, poolData);
return auth.getAccessToken().getJwtToken();
}

private async getAccessTokenV2(): Promise<string> {
public async getAccessTokenV2(): Promise<string> {
if (this.accessToken) return this.accessToken;
if (!this.apiSecret) throw new Error('Cannot authenticate without API secret.');

if (!this.authConfig.type) throw new Error('Auth type is required to authenticate in auth v2');
const credentials = {
apiKey: this.apiKey,
Expand All @@ -71,6 +81,7 @@ export abstract class BaseApiClient {

private async refreshSession(): Promise<string> {
if (!this.session) return this.getAccessToken();
if (!this.apiSecret) throw new Error('Cannot authenticate without API secret.');
const userPass = { Username: this.apiKey, Password: this.apiSecret };
const poolData = { UserPoolId: this.getPoolId(), ClientId: this.getPoolClientId() };
this.session = await refreshSession(userPass, poolData, this.session);
Expand All @@ -80,6 +91,7 @@ export abstract class BaseApiClient {
private async refreshSessionV2(): Promise<string> {
if (!this.authConfig.type) throw new Error('Auth type is required to refresh session in auth v2');
if (!this.sessionV2) return this.getAccessTokenV2();
if (!this.apiSecret) throw new Error('Cannot authenticate without API secret.');
const credentials = {
apiKey: this.apiKey,
secretKey: this.apiSecret,
Expand All @@ -90,6 +102,20 @@ export abstract class BaseApiClient {
return auth.accessToken;
}

protected getKey(): string {
return this.apiKey;
}

protected async getToken(): Promise<string> {
if (this.accessToken) return this.accessToken;
if (!this.apiSecret) throw new Error('Cannot authenticate without API secret.');

const userPass = { Username: this.apiKey, Password: this.apiSecret };
const poolData = { UserPoolId: this.getPoolId(), ClientId: this.getPoolClientId() };
this.session = await authenticate(userPass, poolData);
return this.session.getAccessToken().getJwtToken();
}

protected async init(): Promise<AxiosInstance> {
if (!this.api) {
const accessToken = this.authConfig.useCredentialsCaching
Expand All @@ -105,6 +131,9 @@ export abstract class BaseApiClient {
if (!this.session && !this.sessionV2) {
return this.init();
}
if (!this.apiSecret) {
throw new Error('Cannot refresh session without API secret.');
}
try {
const accessToken = this.authConfig.useCredentialsCaching
? await this.refreshSessionV2()
Expand Down Expand Up @@ -139,6 +168,9 @@ export abstract class BaseApiClient {

// this means ID token has expired so we'll recreate session and try again
if (isAuthenticationError(error)) {
// if using custom access token, throw error.
if (this.accessToken) throw error;

this.api = undefined;

const api = await this.refresh();
Expand Down
6 changes: 3 additions & 3 deletions packages/base/src/utils/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export type PublicNetwork =
| 'fantomtest'
| 'fuji'
| 'fuse'
| 'geist-polter'
| 'geist-mainnet'
| 'geist-polter'
| 'hedera'
| 'hederatest'
| 'holesky'
Expand Down Expand Up @@ -73,8 +73,8 @@ export const Networks: Network[] = [
'fantomtest',
'fuji',
'fuse',
'geist-polter',
'geist-mainnet',
'geist-polter',
'hedera',
'hederatest',
'holesky',
Expand Down Expand Up @@ -140,8 +140,8 @@ export const chainIds: { [key in Network]: number } = {
'fantomtest': 4002,
'fuji': 43113,
'fuse': 122,
'geist-polter': 631571,
'geist-mainnet': 63157,
'geist-polter': 631571,
'hedera': 295,
'hederatest': 296,
'holesky': 17000,
Expand Down
6 changes: 5 additions & 1 deletion packages/defender-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface DefenderOptions {
apiSecret?: string;
relayerApiKey?: string;
relayerApiSecret?: string;
accessToken?: string;
credentials?: ActionRelayerParams;
relayerARN?: string;
httpsAgent?: https.Agent;
Expand All @@ -41,7 +42,7 @@ function getClient<T>(Client: Newable<T>, credentials: Partial<ClientParams> | A
!isApiCredentials(credentials) &&
!isActionKVStoreCredentials(credentials)
) {
throw new Error(`API key and secret are required`);
throw new Error(`API authentication credentials required`);
}

return new Client(credentials);
Expand All @@ -52,6 +53,7 @@ export class Defender {
private apiSecret: string | undefined;
private relayerApiKey: string | undefined;
private relayerApiSecret: string | undefined;
private accessToken: string | undefined;
private actionCredentials: ActionRelayerParams | undefined;
private actionRelayerArn: string | undefined;
private actionKVStoreArn: string | undefined;
Expand All @@ -64,6 +66,7 @@ export class Defender {
this.apiSecret = options.apiSecret;
this.relayerApiKey = options.relayerApiKey;
this.relayerApiSecret = options.relayerApiSecret;
this.accessToken = options.accessToken;
// support for using relaySigner from Defender Actions
this.actionCredentials = options.credentials;
this.actionRelayerArn = options.relayerARN;
Expand Down Expand Up @@ -189,6 +192,7 @@ export class Defender {
...(this.actionRelayerArn ? { relayerARN: this.actionRelayerArn } : undefined),
...(this.relayerApiKey ? { apiKey: this.relayerApiKey } : undefined),
...(this.relayerApiSecret ? { apiSecret: this.relayerApiSecret } : undefined),
...(this.accessToken ? { accessToken: this.accessToken } : undefined),
});
}

Expand Down
2 changes: 1 addition & 1 deletion packages/defender-sdk/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function isActionRelayerCredentials(credentials: Partial<ClientParams> |
}

export function isApiCredentials(credentials: Partial<ClientParams> | ActionRelayerParams): boolean {
return 'apiKey' in credentials && 'apiSecret' in credentials;
return 'apiKey' in credentials && ('accessToken' in credentials || 'apiSecret' in credentials);
}

export function isActionKVStoreCredentials(credentials: Partial<ClientParams> | ActionRelayerParams): boolean {
Expand Down
8 changes: 8 additions & 0 deletions packages/deploy/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ export class DeployClient extends BaseApiClient {
return process.env.DEFENDER_API_URL || 'https://defender-api.openzeppelin.com/';
}

public getApiKey(): string {
return this.getKey();
}

public getAccessToken(): Promise<string> {
return this.getToken();
}

public async deployContract(params: DeployContractRequest): Promise<DeploymentResponse> {
if (isEmpty(params.artifactUri) && isEmpty(params.artifactPayload))
throw new Error(
Expand Down
8 changes: 8 additions & 0 deletions packages/monitor/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ export class MonitorClient extends BaseApiClient {
return process.env.DEFENDER_API_URL || 'https://defender-api.openzeppelin.com/v2/';
}

public getApiKey(): string {
return this.getKey();
}

public getAccessToken(): Promise<string> {
return this.getToken();
}

public async list(): Promise<ListMonitorResponse> {
return this.apiCall(async (api) => {
return await api.get(`/monitors`);
Expand Down
8 changes: 8 additions & 0 deletions packages/network/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ export class NetworkClient extends BaseApiClient {
return process.env.DEFENDER_API_URL || 'https://defender-api.openzeppelin.com/';
}

public getApiKey(): string {
return this.getKey();
}

public getAccessToken(): Promise<string> {
return this.getToken();
}

public async listSupportedNetworks(params?: ListNetworkRequestOptions): Promise<Network[]> {
return this.apiCall(async (api) => {
return await api.get(params && params.networkType ? `${PATH}?type=${params.networkType}` : `${PATH}`);
Expand Down
8 changes: 8 additions & 0 deletions packages/notification-channel/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ export class NotificationChannelClient extends BaseApiClient {
return process.env.DEFENDER_API_URL || 'https://defender-api.openzeppelin.com/v2/';
}

public getApiKey(): string {
return this.getKey();
}

public getAccessToken(): Promise<string> {
return this.getToken();
}

public async create(notification: CreateNotificationRequest): Promise<NotificationResponse> {
return this.apiCall(async (api) => {
return await api.post(`${PATH}/${notification.type}`, notification);
Expand Down
8 changes: 8 additions & 0 deletions packages/proposal/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ export class ProposalClient extends BaseApiClient {
return process.env.DEFENDER_API_URL || 'https://defender-api.openzeppelin.com/';
}

public getApiKey(): string {
return this.getKey();
}

public getAccessToken(): Promise<string> {
return this.getToken();
}

public async addContract(contract: Contract): Promise<Contract> {
return this.apiCall(async (api) => {
return (await api.put('/contracts', contract)) as Contract;
Expand Down
8 changes: 8 additions & 0 deletions packages/relay-signer/src/action/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ export class ActionRelayer extends BaseActionClient implements IRelayer {
this.jsonRpcRequestNextId = 0;
}

public getApiKey(): string {
throw new Error('Method not available for action relayers.');
}

public getAccessToken(): Promise<string> {
throw new Error('Method not available for action relayers.');
}

public async sendTransaction(payload: RelayerTransactionPayload): Promise<RelayerTransaction> {
return this.execute({ action: 'send-tx', payload });
}
Expand Down
Loading