@solana/keychain-dfns 0.0.0 → 0.6.1

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.
Files changed (37) hide show
  1. package/README.md +136 -0
  2. package/dist/__tests__/dfns-signer.integration.test.d.ts +2 -0
  3. package/dist/__tests__/dfns-signer.integration.test.d.ts.map +1 -0
  4. package/dist/__tests__/dfns-signer.integration.test.js +17 -0
  5. package/dist/__tests__/dfns-signer.integration.test.js.map +1 -0
  6. package/dist/__tests__/dfns-signer.test.d.ts +2 -0
  7. package/dist/__tests__/dfns-signer.test.d.ts.map +1 -0
  8. package/dist/__tests__/dfns-signer.test.js +157 -0
  9. package/dist/__tests__/dfns-signer.test.js.map +1 -0
  10. package/dist/__tests__/setup.d.ts +45 -0
  11. package/dist/__tests__/setup.d.ts.map +1 -0
  12. package/dist/__tests__/setup.js +64 -0
  13. package/dist/__tests__/setup.js.map +1 -0
  14. package/dist/auth.d.ts +7 -0
  15. package/dist/auth.d.ts.map +1 -0
  16. package/dist/auth.js +117 -0
  17. package/dist/auth.js.map +1 -0
  18. package/dist/dfns-signer.d.ts +65 -0
  19. package/dist/dfns-signer.d.ts.map +1 -0
  20. package/dist/dfns-signer.js +331 -0
  21. package/dist/dfns-signer.js.map +1 -0
  22. package/dist/index.d.ts +3 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +2 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/types.d.ts +101 -0
  27. package/dist/types.d.ts.map +1 -0
  28. package/dist/types.js +2 -0
  29. package/dist/types.js.map +1 -0
  30. package/package.json +61 -8
  31. package/src/__tests__/dfns-signer.integration.test.ts +17 -0
  32. package/src/__tests__/dfns-signer.test.ts +217 -0
  33. package/src/__tests__/setup.ts +76 -0
  34. package/src/auth.ts +136 -0
  35. package/src/dfns-signer.ts +421 -0
  36. package/src/index.ts +2 -0
  37. package/src/types.ts +113 -0
@@ -0,0 +1,421 @@
1
+ import { Address, assertIsAddress } from '@solana/addresses';
2
+ import { getBase16Decoder, getBase16Encoder, getBase58Decoder } from '@solana/codecs-strings';
3
+ import {
4
+ assertSignatureValid,
5
+ createSignatureDictionary,
6
+ SignerErrorCode,
7
+ SolanaSigner,
8
+ throwSignerError,
9
+ } from '@solana/keychain-core';
10
+ import { SignatureBytes } from '@solana/keys';
11
+ import { SignableMessage, SignatureDictionary } from '@solana/signers';
12
+ import {
13
+ getTransactionEncoder,
14
+ Transaction,
15
+ TransactionWithinSizeLimit,
16
+ TransactionWithLifetime,
17
+ } from '@solana/transactions';
18
+
19
+ import { signUserAction } from './auth.js';
20
+ import type {
21
+ DfnsSignerConfig,
22
+ GenerateSignatureRequest,
23
+ GenerateSignatureResponse,
24
+ GetWalletResponse,
25
+ } from './types.js';
26
+
27
+ export async function createDfnsSigner<TAddress extends string = string>(
28
+ config: DfnsSignerConfig,
29
+ ): Promise<SolanaSigner<TAddress>> {
30
+ return await DfnsSigner.create(config);
31
+ }
32
+
33
+ const DEFAULT_API_BASE_URL = 'https://api.dfns.io';
34
+
35
+ let base16Encoder: ReturnType<typeof getBase16Encoder> | undefined;
36
+ let base16Decoder: ReturnType<typeof getBase16Decoder> | undefined;
37
+ let base58Decoder: ReturnType<typeof getBase58Decoder> | undefined;
38
+
39
+ function hexToBytes(hex: string): Uint8Array {
40
+ base16Encoder ||= getBase16Encoder();
41
+ const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
42
+ return new Uint8Array(base16Encoder.encode(clean));
43
+ }
44
+
45
+ function bytesToHex(bytes: Uint8Array): string {
46
+ base16Decoder ||= getBase16Decoder();
47
+ return base16Decoder.decode(bytes);
48
+ }
49
+ function bytesToBase58(bytes: Uint8Array): string {
50
+ base58Decoder ||= getBase58Decoder();
51
+ return base58Decoder.decode(bytes);
52
+ }
53
+
54
+ /**
55
+ * Dfns-based signer for Solana transactions.
56
+ *
57
+ * Uses Dfns Keys API to sign Solana transactions and messages.
58
+ * Requires a Dfns account with an active Ed25519 Solana wallet.
59
+ *
60
+ * Use the static `create()` factory to construct an instance — it validates
61
+ * the wallet status, key scheme, and curve via the Dfns API.
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * const signer = await DfnsSigner.create({
66
+ * authToken: 'your-service-account-token',
67
+ * credId: 'your-credential-id',
68
+ * privateKeyPem: '-----BEGIN PRIVATE KEY-----\n...',
69
+ * walletId: 'your-wallet-id',
70
+ * });
71
+ * const signed = await signTransactionMessageWithSigners(transactionMessage, [signer]);
72
+ * ```
73
+ *
74
+ * @deprecated Prefer `createDfnsSigner()`. Class export will be removed in a future version.
75
+ */
76
+ export class DfnsSigner<TAddress extends string = string> implements SolanaSigner<TAddress> {
77
+ readonly address: Address<TAddress>;
78
+ private readonly authToken: string;
79
+ private readonly credId: string;
80
+ private readonly privateKeyPem: string;
81
+ private readonly walletId: string;
82
+ private readonly apiBaseUrl: string;
83
+ private readonly keyId: string;
84
+
85
+ private readonly requestDelayMs: number;
86
+
87
+ private constructor(config: {
88
+ address: Address<TAddress>;
89
+ apiBaseUrl: string;
90
+ authToken: string;
91
+ credId: string;
92
+ keyId: string;
93
+ privateKeyPem: string;
94
+ requestDelayMs: number;
95
+ walletId: string;
96
+ }) {
97
+ this.address = config.address;
98
+ this.authToken = config.authToken;
99
+ this.credId = config.credId;
100
+ this.privateKeyPem = config.privateKeyPem;
101
+ this.walletId = config.walletId;
102
+ this.apiBaseUrl = config.apiBaseUrl;
103
+ this.keyId = config.keyId;
104
+ this.requestDelayMs = config.requestDelayMs;
105
+ }
106
+
107
+ /**
108
+ * Create and initialize a DfnsSigner.
109
+ *
110
+ * Validates config fields, fetches the wallet from Dfns, and checks that
111
+ * the wallet is active with an EdDSA/ed25519 signing key.
112
+ * @deprecated Use `createDfnsSigner()` instead.
113
+ */
114
+ static async create<TAddress extends string = string>(config: DfnsSignerConfig): Promise<DfnsSigner<TAddress>> {
115
+ if (!config.authToken) {
116
+ throwSignerError(SignerErrorCode.CONFIG_ERROR, {
117
+ message: 'Missing required authToken field',
118
+ });
119
+ }
120
+
121
+ if (!config.credId) {
122
+ throwSignerError(SignerErrorCode.CONFIG_ERROR, {
123
+ message: 'Missing required credId field',
124
+ });
125
+ }
126
+
127
+ if (!config.privateKeyPem) {
128
+ throwSignerError(SignerErrorCode.CONFIG_ERROR, {
129
+ message: 'Missing required privateKeyPem field',
130
+ });
131
+ }
132
+
133
+ if (!config.walletId) {
134
+ throwSignerError(SignerErrorCode.CONFIG_ERROR, {
135
+ message: 'Missing required walletId field',
136
+ });
137
+ }
138
+
139
+ const apiBaseUrl = config.apiBaseUrl ?? DEFAULT_API_BASE_URL;
140
+ const requestDelayMs = config.requestDelayMs ?? 0;
141
+
142
+ if (requestDelayMs < 0) {
143
+ throwSignerError(SignerErrorCode.CONFIG_ERROR, {
144
+ message: 'requestDelayMs must not be negative',
145
+ });
146
+ }
147
+ if (requestDelayMs > 3000) {
148
+ console.warn(
149
+ 'requestDelayMs is greater than 3000ms, this may result in blockhash expiration errors for signing messages/transactions',
150
+ );
151
+ }
152
+
153
+ const wallet = await fetchWallet(apiBaseUrl, config.authToken, config.walletId);
154
+
155
+ if (wallet.status !== 'Active') {
156
+ throwSignerError(SignerErrorCode.CONFIG_ERROR, {
157
+ message: `Wallet is not active: ${wallet.status}`,
158
+ });
159
+ }
160
+
161
+ if (wallet.signingKey.scheme !== 'EdDSA') {
162
+ throwSignerError(SignerErrorCode.CONFIG_ERROR, {
163
+ message: `Unsupported key scheme: ${wallet.signingKey.scheme} (expected EdDSA)`,
164
+ });
165
+ }
166
+
167
+ if (wallet.signingKey.curve !== 'ed25519') {
168
+ throwSignerError(SignerErrorCode.CONFIG_ERROR, {
169
+ message: `Unsupported key curve: ${wallet.signingKey.curve} (expected ed25519)`,
170
+ });
171
+ }
172
+
173
+ const pubkeyBytes = hexToBytes(wallet.signingKey.publicKey);
174
+ const bs58Address = bytesToBase58(pubkeyBytes);
175
+
176
+ let address: Address<TAddress>;
177
+ try {
178
+ assertIsAddress(bs58Address);
179
+ address = bs58Address as Address<TAddress>;
180
+ } catch (error) {
181
+ throwSignerError(SignerErrorCode.INVALID_PUBLIC_KEY, {
182
+ cause: error,
183
+ message: 'Invalid public key from Dfns wallet',
184
+ });
185
+ }
186
+
187
+ return new DfnsSigner<TAddress>({
188
+ address,
189
+ apiBaseUrl,
190
+ authToken: config.authToken,
191
+ credId: config.credId,
192
+ keyId: wallet.signingKey.id,
193
+ privateKeyPem: config.privateKeyPem,
194
+ requestDelayMs,
195
+ walletId: config.walletId,
196
+ });
197
+ }
198
+
199
+ private async delay(index: number): Promise<void> {
200
+ if (this.requestDelayMs > 0 && index > 0) {
201
+ await new Promise(resolve => setTimeout(resolve, index * this.requestDelayMs));
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Sign multiple messages using Dfns
207
+ */
208
+ async signMessages(messages: readonly SignableMessage[]): Promise<readonly SignatureDictionary[]> {
209
+ return await Promise.all(
210
+ messages.map(async (message, index) => {
211
+ await this.delay(index);
212
+ const messageBytes =
213
+ message.content instanceof Uint8Array
214
+ ? message.content
215
+ : new Uint8Array(Array.from(message.content));
216
+ const signatureBytes = await this.sendSignatureRequest({
217
+ kind: 'Message',
218
+ message: `0x${bytesToHex(messageBytes)}`,
219
+ });
220
+ await assertSignatureValid({
221
+ data: messageBytes,
222
+ signature: signatureBytes,
223
+ signerAddress: this.address,
224
+ });
225
+ return createSignatureDictionary({
226
+ signature: signatureBytes,
227
+ signerAddress: this.address,
228
+ });
229
+ }),
230
+ );
231
+ }
232
+
233
+ /**
234
+ * Sign multiple transactions using Dfns
235
+ */
236
+ async signTransactions(
237
+ transactions: readonly (Transaction & TransactionWithinSizeLimit & TransactionWithLifetime)[],
238
+ ): Promise<readonly SignatureDictionary[]> {
239
+ const txEncoder = getTransactionEncoder();
240
+ return await Promise.all(
241
+ transactions.map(async (transaction, index) => {
242
+ await this.delay(index);
243
+ const txBytes = txEncoder.encode(transaction);
244
+ const signatureBytes = await this.sendSignatureRequest({
245
+ blockchainKind: 'Solana',
246
+ kind: 'Transaction',
247
+ transaction: `0x${bytesToHex(new Uint8Array(txBytes))}`,
248
+ });
249
+ await assertSignatureValid({
250
+ data: transaction.messageBytes,
251
+ signature: signatureBytes,
252
+ signerAddress: this.address,
253
+ });
254
+ return createSignatureDictionary({
255
+ signature: signatureBytes,
256
+ signerAddress: this.address,
257
+ });
258
+ }),
259
+ );
260
+ }
261
+
262
+ /**
263
+ * Check if Dfns API is available
264
+ */
265
+ async isAvailable(): Promise<boolean> {
266
+ try {
267
+ await fetchWallet(this.apiBaseUrl, this.authToken, this.walletId);
268
+ return true;
269
+ } catch {
270
+ return false;
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Send a signature request to the Dfns Keys API
276
+ */
277
+ private async sendSignatureRequest(request: GenerateSignatureRequest): Promise<SignatureBytes> {
278
+ const httpPath = `/keys/${this.keyId}/signatures`;
279
+ const requestBody = JSON.stringify(request);
280
+
281
+ const userAction = await signUserAction(
282
+ this.apiBaseUrl,
283
+ this.authToken,
284
+ this.credId,
285
+ this.privateKeyPem,
286
+ 'POST',
287
+ httpPath,
288
+ requestBody,
289
+ );
290
+
291
+ const url = `${this.apiBaseUrl}${httpPath}`;
292
+ let response: Response;
293
+ try {
294
+ response = await fetch(url, {
295
+ body: requestBody,
296
+ headers: {
297
+ Authorization: `Bearer ${this.authToken}`,
298
+ 'Content-Type': 'application/json',
299
+ 'x-dfns-useraction': userAction,
300
+ },
301
+ method: 'POST',
302
+ });
303
+ } catch (error) {
304
+ throwSignerError(SignerErrorCode.HTTP_ERROR, {
305
+ cause: error,
306
+ message: 'Dfns network request failed',
307
+ url,
308
+ });
309
+ }
310
+
311
+ if (!response.ok) {
312
+ const errorText = await response.text().catch(() => 'Failed to read error response');
313
+ throwSignerError(SignerErrorCode.REMOTE_API_ERROR, {
314
+ message: `Dfns signing API error: ${response.status}`,
315
+ response: errorText,
316
+ status: response.status,
317
+ });
318
+ }
319
+
320
+ let sigResponse: GenerateSignatureResponse;
321
+ try {
322
+ sigResponse = (await response.json()) as GenerateSignatureResponse;
323
+ } catch (error) {
324
+ throwSignerError(SignerErrorCode.PARSING_ERROR, {
325
+ cause: error,
326
+ message: 'Failed to parse Dfns signature response',
327
+ });
328
+ }
329
+
330
+ if (sigResponse.status === 'Failed') {
331
+ throwSignerError(SignerErrorCode.SIGNING_FAILED, {
332
+ message: 'Dfns signing failed',
333
+ });
334
+ }
335
+
336
+ if (sigResponse.status !== 'Signed') {
337
+ throwSignerError(SignerErrorCode.SIGNING_FAILED, {
338
+ message: `Unexpected signature status: ${sigResponse.status} (may require policy approval)`,
339
+ });
340
+ }
341
+
342
+ if (!sigResponse.signature) {
343
+ throwSignerError(SignerErrorCode.SIGNING_FAILED, {
344
+ message: 'Signature components missing from response',
345
+ });
346
+ }
347
+
348
+ return combineSignature(sigResponse.signature.r, sigResponse.signature.s);
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Fetch wallet details from Dfns
354
+ */
355
+ async function fetchWallet(apiBaseUrl: string, authToken: string, walletId: string): Promise<GetWalletResponse> {
356
+ const url = `${apiBaseUrl}/wallets/${walletId}`;
357
+ let response: Response;
358
+ try {
359
+ response = await fetch(url, {
360
+ headers: {
361
+ Authorization: `Bearer ${authToken}`,
362
+ },
363
+ method: 'GET',
364
+ });
365
+ } catch (error) {
366
+ throwSignerError(SignerErrorCode.HTTP_ERROR, {
367
+ cause: error,
368
+ message: 'Dfns network request failed',
369
+ url,
370
+ });
371
+ }
372
+
373
+ if (!response.ok) {
374
+ const errorText = await response.text().catch(() => 'Failed to read error response');
375
+ throwSignerError(SignerErrorCode.REMOTE_API_ERROR, {
376
+ message: `Dfns API error: ${response.status}`,
377
+ response: errorText,
378
+ status: response.status,
379
+ });
380
+ }
381
+
382
+ try {
383
+ return (await response.json()) as GetWalletResponse;
384
+ } catch (error) {
385
+ throwSignerError(SignerErrorCode.PARSING_ERROR, {
386
+ cause: error,
387
+ message: 'Failed to parse Dfns wallet response',
388
+ });
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Pad signature component to exactly 32 bytes.
394
+ * Components from Dfns may be shorter than 32 bytes and need left-padding with zeros.
395
+ */
396
+ function padSignatureComponent(hex: string): Uint8Array {
397
+ const bytes = hexToBytes(hex);
398
+
399
+ if (bytes.length > 32) {
400
+ throwSignerError(SignerErrorCode.SIGNING_FAILED, {
401
+ message: `Invalid signature component length: ${bytes.length} (max 32)`,
402
+ });
403
+ }
404
+
405
+ const padded = new Uint8Array(32);
406
+ padded.set(bytes, 32 - bytes.length);
407
+ return padded;
408
+ }
409
+
410
+ /**
411
+ * Combine r and s hex-encoded components into a 64-byte Ed25519 signature.
412
+ * Each component is individually validated and left-padded to 32 bytes.
413
+ */
414
+ function combineSignature(r: string, s: string): SignatureBytes {
415
+ const rBytes = padSignatureComponent(r);
416
+ const sBytes = padSignatureComponent(s);
417
+ const combined = new Uint8Array(64);
418
+ combined.set(rBytes, 0);
419
+ combined.set(sBytes, 32);
420
+ return combined as SignatureBytes;
421
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { DfnsSigner, createDfnsSigner } from './dfns-signer.js';
2
+ export type { DfnsSignerConfig } from './types.js';
package/src/types.ts ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Configuration for creating a DfnsSigner
3
+ */
4
+ export interface DfnsSignerConfig {
5
+ /** API base URL (default: "https://api.dfns.io") */
6
+ apiBaseUrl?: string;
7
+
8
+ /** Service account token or personal access token */
9
+ authToken: string;
10
+
11
+ /** Credential ID for user action signing */
12
+ credId: string;
13
+
14
+ /** Private key in PEM format for signing user action challenges (Ed25519, P256, or RSA) */
15
+ privateKeyPem: string;
16
+
17
+ /** Optional delay in ms between concurrent signing requests to avoid rate limits (default: 0) */
18
+ requestDelayMs?: number;
19
+
20
+ /** Dfns wallet ID */
21
+ walletId: string;
22
+ }
23
+
24
+ /**
25
+ * Dfns wallet response
26
+ */
27
+ export interface GetWalletResponse {
28
+ id: string;
29
+ signingKey: {
30
+ curve: string;
31
+ id: string;
32
+ publicKey: string;
33
+ scheme: string;
34
+ };
35
+ status: string;
36
+ }
37
+
38
+ /**
39
+ * Message signature request body for Dfns Keys API
40
+ */
41
+ export interface GenerateMessageSignatureRequest {
42
+ kind: 'Message';
43
+ message: string;
44
+ }
45
+
46
+ /**
47
+ * Transaction signature request body for Dfns Keys API
48
+ */
49
+ export interface GenerateTransactionSignatureRequest {
50
+ blockchainKind: string;
51
+ kind: 'Transaction';
52
+ transaction: string;
53
+ }
54
+
55
+ export type GenerateSignatureRequest = GenerateMessageSignatureRequest | GenerateTransactionSignatureRequest;
56
+
57
+ /**
58
+ * Signature response from Dfns Keys API
59
+ */
60
+ export interface GenerateSignatureResponse {
61
+ id: string;
62
+ signature?: SignatureComponents;
63
+ signedData?: string;
64
+ status: string;
65
+ }
66
+
67
+ export interface SignatureComponents {
68
+ r: string;
69
+ s: string;
70
+ }
71
+
72
+ /**
73
+ * User action challenge init request
74
+ */
75
+ export interface UserActionInitRequest {
76
+ userActionHttpMethod: string;
77
+ userActionHttpPath: string;
78
+ userActionPayload: string;
79
+ userActionServerKind: string;
80
+ }
81
+
82
+ /**
83
+ * User action challenge init response
84
+ */
85
+ export interface UserActionInitResponse {
86
+ allowCredentials: {
87
+ key: Array<{ id: string }>;
88
+ };
89
+ challenge: string;
90
+ challengeIdentifier: string;
91
+ }
92
+
93
+ /**
94
+ * User action sign request
95
+ */
96
+ export interface UserActionSignRequest {
97
+ challengeIdentifier: string;
98
+ firstFactor: {
99
+ credentialAssertion: {
100
+ clientData: string;
101
+ credId: string;
102
+ signature: string;
103
+ };
104
+ kind: string;
105
+ };
106
+ }
107
+
108
+ /**
109
+ * User action sign response
110
+ */
111
+ export interface UserActionResponse {
112
+ userAction: string;
113
+ }