emblem-vault-ai-signers 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/dist/ethers.d.ts +1 -0
- package/dist/ethers.js +13 -3
- package/dist/http.js +23 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +11 -2
- package/dist/utils.js +3 -2
- package/dist/validation.d.ts +53 -0
- package/dist/validation.js +111 -0
- package/dist/vault.js +9 -7
- package/dist/viem.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -208,6 +208,82 @@ On first use, both adapters query `GET /vault/info` with header `x-api-key` to o
|
|
|
208
208
|
|
|
209
209
|
Transactions are normalized to hex/number-like fields before submission.
|
|
210
210
|
|
|
211
|
+
## Security Considerations
|
|
212
|
+
|
|
213
|
+
### Client-Side Usage
|
|
214
|
+
|
|
215
|
+
This library is designed for environments where **users provide their own API keys**. When used client-side (browser/dApp):
|
|
216
|
+
|
|
217
|
+
#### Trust Model
|
|
218
|
+
- **Users must trust the dApp code** - Any JavaScript running in the browser has full access to API keys
|
|
219
|
+
- **No technical enforcement possible** - Browser dev tools, breakpoints, and code injection can modify any behavior
|
|
220
|
+
- **baseUrl is configurable** - Required for development/staging/production environments
|
|
221
|
+
- **Transparent by design** - This is a feature, not a bug
|
|
222
|
+
|
|
223
|
+
#### What This Means
|
|
224
|
+
```javascript
|
|
225
|
+
// Users provide their OWN API keys to YOUR dApp
|
|
226
|
+
const client = createEmblemClient({
|
|
227
|
+
apiKey: userApiKey, // User's key, not yours
|
|
228
|
+
baseUrl: "https://api.emblemvault.ai"
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
If a user runs your dApp code, they are trusting it with their API key and signing authority. There is no way to prevent malicious dApp code from:
|
|
233
|
+
- Logging API keys
|
|
234
|
+
- Intercepting `fetch()` calls
|
|
235
|
+
- Changing the `baseUrl`
|
|
236
|
+
- Making unauthorized signing requests
|
|
237
|
+
|
|
238
|
+
**This is the same trust model as MetaMask and other browser wallets.**
|
|
239
|
+
|
|
240
|
+
### Best Practices for Users
|
|
241
|
+
|
|
242
|
+
1. **Only use trusted dApps** - Verify the source and reputation
|
|
243
|
+
2. **Review open source code** when possible
|
|
244
|
+
3. **Use separate API keys** for different dApps
|
|
245
|
+
4. **Monitor signing activity** in your Emblem dashboard
|
|
246
|
+
5. **Test with staging keys first** before using production
|
|
247
|
+
|
|
248
|
+
### Best Practices for Implementers
|
|
249
|
+
|
|
250
|
+
1. **Open source your dApp** - Allow security audits
|
|
251
|
+
2. **Document your security model** - Be transparent about API key handling
|
|
252
|
+
3. **Minimize dependencies** - Reduce supply chain attack surface
|
|
253
|
+
4. **Use Content Security Policy** - Add CSP headers to protect against XSS
|
|
254
|
+
5. **Never log or store user API keys** - Only use them in-memory for signing
|
|
255
|
+
6. **Implement proper error handling** - Don't expose API keys in error messages
|
|
256
|
+
|
|
257
|
+
### Server-Side Usage
|
|
258
|
+
|
|
259
|
+
When used server-side (Node.js):
|
|
260
|
+
- Store API keys in environment variables
|
|
261
|
+
- Never expose keys to client-side code
|
|
262
|
+
- Use proper access controls and authentication
|
|
263
|
+
- Implement rate limiting if exposing signing endpoints
|
|
264
|
+
|
|
265
|
+
### Development vs Production
|
|
266
|
+
|
|
267
|
+
This library supports multiple environments via `baseUrl`:
|
|
268
|
+
|
|
269
|
+
```javascript
|
|
270
|
+
// Development
|
|
271
|
+
const devClient = createEmblemClient({
|
|
272
|
+
apiKey: process.env.DEV_API_KEY,
|
|
273
|
+
baseUrl: "https://dev-api.emblemvault.ai"
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Production
|
|
277
|
+
const prodClient = createEmblemClient({
|
|
278
|
+
apiKey: process.env.PROD_API_KEY,
|
|
279
|
+
baseUrl: "https://api.emblemvault.ai"
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
API keys from one environment do not work in another, providing natural isolation.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
211
287
|
## Testing
|
|
212
288
|
|
|
213
289
|
- Copy `.env.example` to `.env` and set:
|
package/dist/ethers.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export declare class EmblemEthersWallet extends AbstractSigner {
|
|
|
6
6
|
private _address;
|
|
7
7
|
private _vaultId;
|
|
8
8
|
private _chainId;
|
|
9
|
+
private _initPromise?;
|
|
9
10
|
constructor(config: EmblemRemoteConfig, provider?: Provider | null, seed?: {
|
|
10
11
|
address?: `0x${string}`;
|
|
11
12
|
vaultId?: string;
|
package/dist/ethers.js
CHANGED
|
@@ -17,9 +17,18 @@ export class EmblemEthersWallet extends AbstractSigner {
|
|
|
17
17
|
this._chainId = seed.chainId;
|
|
18
18
|
}
|
|
19
19
|
async initialize() {
|
|
20
|
-
|
|
21
|
-
this.
|
|
22
|
-
|
|
20
|
+
// Prevent race condition: cache the promise
|
|
21
|
+
if (this._initPromise)
|
|
22
|
+
return this._initPromise;
|
|
23
|
+
this._initPromise = fetchVaultInfo(this._config).then(info => {
|
|
24
|
+
this._address = info.evmAddress;
|
|
25
|
+
this._vaultId = info.vaultId;
|
|
26
|
+
}).catch(err => {
|
|
27
|
+
// Clear promise on error to allow retry
|
|
28
|
+
this._initPromise = undefined;
|
|
29
|
+
throw err;
|
|
30
|
+
});
|
|
31
|
+
return this._initPromise;
|
|
23
32
|
}
|
|
24
33
|
async getAddress() {
|
|
25
34
|
if (!this._address)
|
|
@@ -67,6 +76,7 @@ export class EmblemEthersWallet extends AbstractSigner {
|
|
|
67
76
|
await this.initialize();
|
|
68
77
|
const from = tx.from;
|
|
69
78
|
const addr = await this.getAddress();
|
|
79
|
+
// Validate from address if present, ensure it matches signer
|
|
70
80
|
if (from && from.toLowerCase() !== addr.toLowerCase()) {
|
|
71
81
|
throw new Error("transaction from does not match signer address");
|
|
72
82
|
}
|
package/dist/http.js
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
function sanitizeErrorMessage(status, text) {
|
|
2
|
+
// Sanitize error messages to avoid leaking sensitive server information
|
|
3
|
+
let errorMessage = `Emblem signer error ${status}`;
|
|
4
|
+
if (status >= 500) {
|
|
5
|
+
errorMessage += ": Internal server error";
|
|
6
|
+
}
|
|
7
|
+
else if (status === 401 || status === 403) {
|
|
8
|
+
errorMessage += ": Authentication failed";
|
|
9
|
+
}
|
|
10
|
+
else if (status === 404) {
|
|
11
|
+
errorMessage += ": Resource not found";
|
|
12
|
+
}
|
|
13
|
+
else if (status === 405) {
|
|
14
|
+
errorMessage += ": Method not allowed";
|
|
15
|
+
}
|
|
16
|
+
else if (text) {
|
|
17
|
+
// For 4xx client errors, include limited error details
|
|
18
|
+
errorMessage += `: ${text.substring(0, 200)}`; // Limit to 200 chars
|
|
19
|
+
}
|
|
20
|
+
return errorMessage;
|
|
21
|
+
}
|
|
1
22
|
export async function emblemPost(path, body, { apiKey, baseUrl = "https://api.emblemvault.ai" }) {
|
|
2
23
|
const res = await fetch(`${baseUrl}${path}`, {
|
|
3
24
|
method: "POST",
|
|
@@ -9,7 +30,7 @@ export async function emblemPost(path, body, { apiKey, baseUrl = "https://api.em
|
|
|
9
30
|
});
|
|
10
31
|
if (!res.ok) {
|
|
11
32
|
const text = await res.text().catch(() => "");
|
|
12
|
-
throw new Error(
|
|
33
|
+
throw new Error(sanitizeErrorMessage(res.status, text));
|
|
13
34
|
}
|
|
14
35
|
return res.json();
|
|
15
36
|
}
|
|
@@ -22,7 +43,7 @@ export async function emblemGet(path, { apiKey, baseUrl = "https://api.emblemvau
|
|
|
22
43
|
});
|
|
23
44
|
if (!res.ok) {
|
|
24
45
|
const text = await res.text().catch(() => "");
|
|
25
|
-
throw new Error(
|
|
46
|
+
throw new Error(sanitizeErrorMessage(res.status, text));
|
|
26
47
|
}
|
|
27
48
|
return res.json();
|
|
28
49
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { Provider } from "ethers";
|
|
2
2
|
import type { EmblemRemoteConfig } from "./types.js";
|
|
3
3
|
export type { EmblemRemoteConfig, Hex, VaultInfo } from "./types.js";
|
|
4
|
+
export type { EmblemSecurityConfig } from "./validation.js";
|
|
5
|
+
export { isBrowserEnvironment, isNodeEnvironment } from "./validation.js";
|
|
4
6
|
import { EmblemEthersWallet } from "./ethers.js";
|
|
5
7
|
import { EmblemSolanaSigner } from "./solana.js";
|
|
6
8
|
import { EmblemWeb3Adapter } from "./web3.js";
|
|
9
|
+
import { type EmblemSecurityConfig } from "./validation.js";
|
|
7
10
|
export declare class EmblemVaultClient {
|
|
8
11
|
private readonly config;
|
|
9
12
|
private _infoPromise?;
|
|
10
|
-
constructor(config: EmblemRemoteConfig);
|
|
13
|
+
constructor(config: EmblemRemoteConfig | EmblemSecurityConfig);
|
|
11
14
|
/** Lazily fetch and cache vault info */
|
|
12
15
|
private getInfo;
|
|
13
16
|
toViemAccount(): Promise<{
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
|
+
export { isBrowserEnvironment, isNodeEnvironment } from "./validation.js";
|
|
1
2
|
import { toViemAccount } from "./viem.js";
|
|
2
3
|
import { toEthersWallet } from "./ethers.js";
|
|
3
4
|
import { toSolanaKitSigner, toSolanaWeb3Signer } from "./solana.js";
|
|
4
5
|
import { toWeb3Adapter } from "./web3.js";
|
|
5
6
|
import { fetchVaultInfo } from "./vault.js";
|
|
7
|
+
import { validateConfig } from "./validation.js";
|
|
6
8
|
export class EmblemVaultClient {
|
|
7
9
|
constructor(config) {
|
|
10
|
+
// Comprehensive security validation
|
|
11
|
+
validateConfig(config);
|
|
8
12
|
this.config = config;
|
|
9
13
|
}
|
|
10
14
|
/** Lazily fetch and cache vault info */
|
|
11
15
|
getInfo() {
|
|
12
|
-
if (!this._infoPromise)
|
|
13
|
-
this._infoPromise = fetchVaultInfo(this.config)
|
|
16
|
+
if (!this._infoPromise) {
|
|
17
|
+
this._infoPromise = fetchVaultInfo(this.config).catch(err => {
|
|
18
|
+
// Clear cache on error to allow retry
|
|
19
|
+
this._infoPromise = undefined;
|
|
20
|
+
throw err;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
14
23
|
return this._infoPromise;
|
|
15
24
|
}
|
|
16
25
|
async toViemAccount() {
|
package/dist/utils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toSafeNumber } from "./validation.js";
|
|
1
2
|
export function toHexIfBigInt(v) {
|
|
2
3
|
return typeof v === "bigint" ? ("0x" + v.toString(16)) : v;
|
|
3
4
|
}
|
|
@@ -22,9 +23,9 @@ export function normalizeTxForEmblem(tx) {
|
|
|
22
23
|
if (out.maxPriorityFeePerGas !== undefined)
|
|
23
24
|
out.maxPriorityFeePerGas = toHexIfBigInt(out.maxPriorityFeePerGas);
|
|
24
25
|
if (out.nonce !== undefined)
|
|
25
|
-
out.nonce =
|
|
26
|
+
out.nonce = toSafeNumber(out.nonce, 'nonce');
|
|
26
27
|
if (out.chainId !== undefined)
|
|
27
|
-
out.chainId =
|
|
28
|
+
out.chainId = toSafeNumber(out.chainId, 'chainId');
|
|
28
29
|
// Some backends only accept legacy fields; fold EIP-1559 into gasPrice and drop unsupported keys
|
|
29
30
|
if (out.maxFeePerGas !== undefined || out.maxPriorityFeePerGas !== undefined) {
|
|
30
31
|
if (out.gasPrice === undefined && out.maxFeePerGas !== undefined) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { EmblemRemoteConfig } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Environment detection utilities for warning about unsafe usage patterns
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Detect if code is running in a browser environment
|
|
7
|
+
*/
|
|
8
|
+
export declare function isBrowserEnvironment(): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Check if we're in a Node.js server environment
|
|
11
|
+
*/
|
|
12
|
+
export declare function isNodeEnvironment(): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Validate API key format and warn about potential security issues
|
|
15
|
+
*/
|
|
16
|
+
export declare function validateApiKey(apiKey: string, options?: {
|
|
17
|
+
warnOnBrowser?: boolean;
|
|
18
|
+
}): void;
|
|
19
|
+
/**
|
|
20
|
+
* Validate baseUrl format
|
|
21
|
+
*/
|
|
22
|
+
export declare function validateBaseUrl(baseUrl?: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Validate Ethereum address format
|
|
25
|
+
*/
|
|
26
|
+
export declare function validateEthereumAddress(address: string): void;
|
|
27
|
+
/**
|
|
28
|
+
* Validate vault ID
|
|
29
|
+
*/
|
|
30
|
+
export declare function validateVaultId(vaultId: string): void;
|
|
31
|
+
/**
|
|
32
|
+
* Safe number conversion with bounds checking
|
|
33
|
+
*/
|
|
34
|
+
export declare function toSafeNumber(value: any, fieldName: string): number;
|
|
35
|
+
/**
|
|
36
|
+
* Extended config with security options
|
|
37
|
+
*/
|
|
38
|
+
export interface EmblemSecurityConfig extends EmblemRemoteConfig {
|
|
39
|
+
/**
|
|
40
|
+
* Suppress browser environment warning
|
|
41
|
+
* @default false
|
|
42
|
+
*/
|
|
43
|
+
warnOnBrowser?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Enable debug logging for security-related checks
|
|
46
|
+
* @default false
|
|
47
|
+
*/
|
|
48
|
+
debugSecurity?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Comprehensive configuration validation
|
|
52
|
+
*/
|
|
53
|
+
export declare function validateConfig(config: EmblemSecurityConfig): void;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment detection utilities for warning about unsafe usage patterns
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Detect if code is running in a browser environment
|
|
6
|
+
*/
|
|
7
|
+
export function isBrowserEnvironment() {
|
|
8
|
+
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Check if we're in a Node.js server environment
|
|
12
|
+
*/
|
|
13
|
+
export function isNodeEnvironment() {
|
|
14
|
+
return typeof process !== 'undefined' &&
|
|
15
|
+
process.versions != null &&
|
|
16
|
+
process.versions.node != null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Validate API key format and warn about potential security issues
|
|
20
|
+
*/
|
|
21
|
+
export function validateApiKey(apiKey, options = {}) {
|
|
22
|
+
if (!apiKey || typeof apiKey !== 'string') {
|
|
23
|
+
throw new Error('apiKey is required and must be a string');
|
|
24
|
+
}
|
|
25
|
+
if (apiKey.trim() === '') {
|
|
26
|
+
throw new Error('apiKey cannot be empty or whitespace');
|
|
27
|
+
}
|
|
28
|
+
// Warn if API key looks like it might be exposed in client-side code
|
|
29
|
+
if (options.warnOnBrowser !== false && isBrowserEnvironment()) {
|
|
30
|
+
console.warn('[Emblem Security Warning] API key is being used in a browser environment. ' +
|
|
31
|
+
'API keys should only be used server-side (Node.js). ' +
|
|
32
|
+
'Client-side usage exposes your API key to anyone who can view the page source. ' +
|
|
33
|
+
'To suppress this warning, pass { warnOnBrowser: false } to createEmblemClient().');
|
|
34
|
+
}
|
|
35
|
+
// Check for common mistakes
|
|
36
|
+
if (apiKey.startsWith('pk_') || apiKey.startsWith('sk_')) {
|
|
37
|
+
// Looks like a typical API key format - good
|
|
38
|
+
}
|
|
39
|
+
else if (apiKey.length < 16) {
|
|
40
|
+
console.warn('[Emblem Security Warning] API key seems unusually short. Is this correct?');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Validate baseUrl format
|
|
45
|
+
*/
|
|
46
|
+
export function validateBaseUrl(baseUrl) {
|
|
47
|
+
if (!baseUrl)
|
|
48
|
+
return; // undefined is ok, will use default
|
|
49
|
+
if (!baseUrl.startsWith('http://') && !baseUrl.startsWith('https://')) {
|
|
50
|
+
throw new Error('baseUrl must be a valid HTTP(S) URL');
|
|
51
|
+
}
|
|
52
|
+
// Warn about http (not https)
|
|
53
|
+
if (baseUrl.startsWith('http://') && !baseUrl.includes('localhost') && !baseUrl.includes('127.0.0.1')) {
|
|
54
|
+
console.warn('[Emblem Security Warning] baseUrl uses HTTP instead of HTTPS. This is insecure for production use.');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Validate Ethereum address format
|
|
59
|
+
*/
|
|
60
|
+
export function validateEthereumAddress(address) {
|
|
61
|
+
if (!address || typeof address !== 'string') {
|
|
62
|
+
throw new Error('Address is required');
|
|
63
|
+
}
|
|
64
|
+
if (!address.startsWith('0x')) {
|
|
65
|
+
throw new Error('Address must start with 0x');
|
|
66
|
+
}
|
|
67
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
|
|
68
|
+
throw new Error('Invalid Ethereum address format');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Validate vault ID
|
|
73
|
+
*/
|
|
74
|
+
export function validateVaultId(vaultId) {
|
|
75
|
+
if (!vaultId || typeof vaultId !== 'string') {
|
|
76
|
+
throw new Error('vaultId is required');
|
|
77
|
+
}
|
|
78
|
+
if (vaultId.trim() === '') {
|
|
79
|
+
throw new Error('vaultId cannot be empty');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Safe number conversion with bounds checking
|
|
84
|
+
*/
|
|
85
|
+
export function toSafeNumber(value, fieldName) {
|
|
86
|
+
const num = Number(value);
|
|
87
|
+
if (!Number.isSafeInteger(num)) {
|
|
88
|
+
throw new Error(`${fieldName} value ${value} exceeds safe integer range (max: ${Number.MAX_SAFE_INTEGER})`);
|
|
89
|
+
}
|
|
90
|
+
return num;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Comprehensive configuration validation
|
|
94
|
+
*/
|
|
95
|
+
export function validateConfig(config) {
|
|
96
|
+
// Validate API key
|
|
97
|
+
validateApiKey(config.apiKey, { warnOnBrowser: config.warnOnBrowser });
|
|
98
|
+
// Validate baseUrl if provided
|
|
99
|
+
if (config.baseUrl) {
|
|
100
|
+
validateBaseUrl(config.baseUrl);
|
|
101
|
+
}
|
|
102
|
+
// Security audit logging
|
|
103
|
+
if (config.debugSecurity) {
|
|
104
|
+
console.log('[Emblem Security Debug]', {
|
|
105
|
+
environment: isBrowserEnvironment() ? 'browser' : 'node',
|
|
106
|
+
hasBaseUrl: !!config.baseUrl,
|
|
107
|
+
apiKeyLength: config.apiKey.length,
|
|
108
|
+
timestamp: new Date().toISOString(),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
package/dist/vault.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { emblemPost } from "./http.js";
|
|
2
2
|
export async function fetchVaultInfo(config) {
|
|
3
|
-
|
|
4
|
-
try
|
|
5
|
-
|
|
3
|
+
// Note: The server only supports POST for /vault/info
|
|
4
|
+
// No need to try GET first, just use POST directly
|
|
5
|
+
const data = await emblemPost("/vault/info", {}, config);
|
|
6
|
+
// Validate response data
|
|
7
|
+
if (!data.vaultId || !data.address || !data.evmAddress) {
|
|
8
|
+
throw new Error('Invalid vault info response: missing required fields');
|
|
6
9
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
data = await emblemPost("/vault/info", {}, config);
|
|
10
|
+
if (!data.evmAddress.startsWith('0x')) {
|
|
11
|
+
throw new Error('Invalid evmAddress format in response');
|
|
10
12
|
}
|
|
11
13
|
return {
|
|
12
14
|
vaultId: data.vaultId,
|
package/dist/viem.js
CHANGED
|
@@ -23,7 +23,8 @@ export async function toViemAccount(config, infoOverride) {
|
|
|
23
23
|
payload = message;
|
|
24
24
|
}
|
|
25
25
|
else {
|
|
26
|
-
|
|
26
|
+
// Don't silently convert objects to "[object Object]"
|
|
27
|
+
throw new Error(`Unsupported message type: ${typeof message}. Expected string, Uint8Array, or hex string.`);
|
|
27
28
|
}
|
|
28
29
|
const data = await emblemPost("/sign-eth-message", { vaultId, message: payload }, config);
|
|
29
30
|
return data.signature;
|