emblem-vault-ai-signers 0.1.3 → 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 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
- const info = await fetchVaultInfo(this._config);
21
- this._address = info.evmAddress;
22
- this._vaultId = info.vaultId;
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",
@@ -5,11 +26,11 @@ export async function emblemPost(path, body, { apiKey, baseUrl = "https://api.em
5
26
  "content-type": "application/json",
6
27
  "x-api-key": apiKey,
7
28
  },
8
- body: JSON.stringify(body),
29
+ body: JSON.stringify(body, (key, value) => typeof value === "bigint" ? value.toString() : value),
9
30
  });
10
31
  if (!res.ok) {
11
32
  const text = await res.text().catch(() => "");
12
- throw new Error(`Emblem signer error ${res.status}: ${text || res.statusText}`);
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(`Emblem signer error ${res.status}: ${text || res.statusText}`);
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 = Number(out.nonce);
26
+ out.nonce = toSafeNumber(out.nonce, 'nonce');
26
27
  if (out.chainId !== undefined)
27
- out.chainId = Number(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 { emblemGet, emblemPost } from "./http.js";
1
+ import { emblemPost } from "./http.js";
2
2
  export async function fetchVaultInfo(config) {
3
- let data;
4
- try {
5
- data = await emblemGet("/vault/info", config);
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
- catch (err) {
8
- // Some environments may require POST for this endpoint; try POST fallback
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
- payload = String(message);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emblem-vault-ai-signers",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Emblem Vault remote signer adapters for viem and ethers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,7 +23,9 @@
23
23
  "test": "vitest run --reporter=verbose",
24
24
  "test:watch": "vitest --reporter=verbose",
25
25
  "test:ci": "vitest run --reporter=default",
26
- "test:integration": "vitest -c vitest.int.config.ts run --reporter=verbose"
26
+ "test:integration": "vitest -c vitest.int.config.ts run --reporter=verbose",
27
+ "test:all": "npm test && npm run test:integration",
28
+ "release:patch": "npm version patch && npm run build && npm run test:all && npm publish"
27
29
  },
28
30
  "keywords": [
29
31
  "emblem",