@stellar/typescript-wallet-sdk 1.9.0 → 1.10.0
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/CHANGELOG.MD +14 -0
- package/lib/bundle.js +5807 -1314
- package/lib/bundle.js.map +1 -1
- package/lib/bundle_browser.js +428 -7717
- package/lib/bundle_browser.js.map +1 -1
- package/lib/walletSdk/Auth/index.d.ts +2 -0
- package/lib/walletSdk/Exceptions/index.d.ts +6 -0
- package/lib/walletSdk/Types/recovery.d.ts +1 -0
- package/lib/walletSdk/Types/sep7.d.ts +1 -0
- package/package.json +1 -1
- package/src/walletSdk/Anchor/index.ts +1 -0
- package/src/walletSdk/Auth/index.ts +194 -7
- package/src/walletSdk/Exceptions/index.ts +16 -0
- package/src/walletSdk/Recovery/index.ts +1 -0
- package/src/walletSdk/Types/auth.ts +11 -5
- package/src/walletSdk/Types/recovery.ts +1 -0
- package/src/walletSdk/Types/sep7.ts +1 -0
- package/src/walletSdk/Uri/sep7Parser.ts +46 -6
- package/test/auth.test.ts +1032 -0
- package/test/customer.test.ts +2 -1
- package/test/docker/docker-compose.yml +2 -2
- package/test/e2e/browser.test.ts +0 -2
- package/test/integration/README.md +3 -8
- package/test/integration/anchorplatform.test.ts +2 -1
- package/test/sep38.test.ts +0 -2
- package/test/sep6.test.ts +9 -3
- package/test/sep7.test.ts +43 -1
- package/test/server.test.ts +19 -23
- package/test/wallet.test.ts +10 -11
- package/tsconfig.json +2 -1
|
@@ -7,6 +7,7 @@ type Sep10Params = {
|
|
|
7
7
|
webAuthEndpoint: string;
|
|
8
8
|
homeDomain: string;
|
|
9
9
|
httpClient: AxiosInstance;
|
|
10
|
+
serverSigningKey?: string;
|
|
10
11
|
};
|
|
11
12
|
/**
|
|
12
13
|
* @alias Auth alias for Sep10 class.
|
|
@@ -23,6 +24,7 @@ export declare class Sep10 {
|
|
|
23
24
|
private webAuthEndpoint;
|
|
24
25
|
private homeDomain;
|
|
25
26
|
private httpClient;
|
|
27
|
+
private serverSigningKey?;
|
|
26
28
|
/**
|
|
27
29
|
* Creates a new instance of the Sep10 class.
|
|
28
30
|
*
|
|
@@ -103,6 +103,12 @@ export declare class NoAccountAndNoSponsorError extends Error {
|
|
|
103
103
|
export declare class Sep38PriceOnlyOneAmountError extends Error {
|
|
104
104
|
constructor();
|
|
105
105
|
}
|
|
106
|
+
export declare class ChallengeValidationFailedError extends Error {
|
|
107
|
+
constructor(cause: Error);
|
|
108
|
+
}
|
|
109
|
+
export declare class NetworkPassphraseMismatchError extends Error {
|
|
110
|
+
constructor(expected: string, received: string);
|
|
111
|
+
}
|
|
106
112
|
export declare class ChallengeTxnIncorrectSequenceError extends Error {
|
|
107
113
|
constructor();
|
|
108
114
|
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { AxiosInstance } from "axios";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
TransactionBuilder,
|
|
4
|
+
Transaction,
|
|
5
|
+
FeeBumpTransaction,
|
|
6
|
+
WebAuth,
|
|
7
|
+
} from "@stellar/stellar-sdk";
|
|
3
8
|
import { decode } from "jws";
|
|
4
9
|
|
|
5
10
|
import { Config } from "../";
|
|
@@ -10,6 +15,8 @@ import {
|
|
|
10
15
|
InvalidTokenError,
|
|
11
16
|
MissingTokenError,
|
|
12
17
|
ExpiredTokenError,
|
|
18
|
+
ChallengeValidationFailedError,
|
|
19
|
+
NetworkPassphraseMismatchError,
|
|
13
20
|
} from "../Exceptions";
|
|
14
21
|
import {
|
|
15
22
|
AuthenticateParams,
|
|
@@ -31,6 +38,7 @@ type Sep10Params = {
|
|
|
31
38
|
webAuthEndpoint: string;
|
|
32
39
|
homeDomain: string;
|
|
33
40
|
httpClient: AxiosInstance;
|
|
41
|
+
serverSigningKey?: string;
|
|
34
42
|
};
|
|
35
43
|
|
|
36
44
|
/**
|
|
@@ -49,6 +57,7 @@ export class Sep10 {
|
|
|
49
57
|
private webAuthEndpoint: string;
|
|
50
58
|
private homeDomain: string;
|
|
51
59
|
private httpClient: AxiosInstance;
|
|
60
|
+
private serverSigningKey?: string;
|
|
52
61
|
|
|
53
62
|
/**
|
|
54
63
|
* Creates a new instance of the Sep10 class.
|
|
@@ -57,12 +66,14 @@ export class Sep10 {
|
|
|
57
66
|
* @param {Sep10Params} params - Parameters to initialize the Sep10 instance.
|
|
58
67
|
*/
|
|
59
68
|
constructor(params: Sep10Params) {
|
|
60
|
-
const { cfg, webAuthEndpoint, homeDomain, httpClient } =
|
|
69
|
+
const { cfg, webAuthEndpoint, homeDomain, httpClient, serverSigningKey } =
|
|
70
|
+
params;
|
|
61
71
|
|
|
62
72
|
this.cfg = cfg;
|
|
63
73
|
this.webAuthEndpoint = webAuthEndpoint;
|
|
64
74
|
this.homeDomain = homeDomain;
|
|
65
75
|
this.httpClient = httpClient;
|
|
76
|
+
this.serverSigningKey = serverSigningKey;
|
|
66
77
|
}
|
|
67
78
|
|
|
68
79
|
/**
|
|
@@ -150,9 +161,46 @@ export class Sep10 {
|
|
|
150
161
|
challengeResponse,
|
|
151
162
|
walletSigner,
|
|
152
163
|
}: SignParams): Promise<Transaction> {
|
|
164
|
+
const networkPassphrase = this.cfg.stellar.network;
|
|
165
|
+
|
|
166
|
+
if (
|
|
167
|
+
challengeResponse.network_passphrase &&
|
|
168
|
+
challengeResponse.network_passphrase !== (networkPassphrase as string)
|
|
169
|
+
) {
|
|
170
|
+
throw new NetworkPassphraseMismatchError(
|
|
171
|
+
networkPassphrase,
|
|
172
|
+
challengeResponse.network_passphrase,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const webAuthDomain = new URL(this.webAuthEndpoint).hostname;
|
|
178
|
+
|
|
179
|
+
if (this.serverSigningKey) {
|
|
180
|
+
WebAuth.readChallengeTx(
|
|
181
|
+
challengeResponse.transaction,
|
|
182
|
+
this.serverSigningKey,
|
|
183
|
+
networkPassphrase,
|
|
184
|
+
this.homeDomain,
|
|
185
|
+
webAuthDomain,
|
|
186
|
+
);
|
|
187
|
+
} else {
|
|
188
|
+
readChallengeTx(
|
|
189
|
+
challengeResponse.transaction,
|
|
190
|
+
networkPassphrase,
|
|
191
|
+
this.homeDomain,
|
|
192
|
+
webAuthDomain,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
} catch (e) {
|
|
196
|
+
throw new ChallengeValidationFailedError(
|
|
197
|
+
e instanceof Error ? e : new Error(String(e)),
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
153
201
|
let transaction: Transaction = TransactionBuilder.fromXDR(
|
|
154
202
|
challengeResponse.transaction,
|
|
155
|
-
|
|
203
|
+
networkPassphrase,
|
|
156
204
|
) as Transaction;
|
|
157
205
|
|
|
158
206
|
// check if verifying client domain as well
|
|
@@ -160,7 +208,7 @@ export class Sep10 {
|
|
|
160
208
|
if (op.type === "manageData" && op.name === "client_domain") {
|
|
161
209
|
transaction = await walletSigner.signWithDomainAccount({
|
|
162
210
|
transactionXDR: challengeResponse.transaction,
|
|
163
|
-
networkPassphrase
|
|
211
|
+
networkPassphrase,
|
|
164
212
|
accountKp,
|
|
165
213
|
});
|
|
166
214
|
}
|
|
@@ -188,14 +236,153 @@ export class Sep10 {
|
|
|
188
236
|
}
|
|
189
237
|
}
|
|
190
238
|
|
|
191
|
-
|
|
239
|
+
/**
|
|
240
|
+
* @internal
|
|
241
|
+
* @param {string} token - The JWT token to validate.
|
|
242
|
+
*/
|
|
243
|
+
export const validateToken = (token: string) => {
|
|
192
244
|
const parsedToken = decode(token);
|
|
193
245
|
if (!parsedToken) {
|
|
194
246
|
throw new InvalidTokenError();
|
|
195
247
|
}
|
|
196
|
-
|
|
197
|
-
|
|
248
|
+
const payload =
|
|
249
|
+
typeof parsedToken.payload === "string"
|
|
250
|
+
? JSON.parse(parsedToken.payload)
|
|
251
|
+
: parsedToken.payload;
|
|
252
|
+
const exp = payload?.exp;
|
|
253
|
+
if (typeof exp === "number" && exp < Math.floor(Date.now() / 1000)) {
|
|
254
|
+
throw new ExpiredTokenError(exp);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
/*
|
|
259
|
+
* Validates a SEP-10 challenge transaction without requiring the server's
|
|
260
|
+
* signing key. This performs all structural validations from the SEP-10 spec
|
|
261
|
+
* (sequence number, operation types, timebounds, home domain, web_auth_domain,
|
|
262
|
+
* nonce format) but skips the server account and signature checks.
|
|
263
|
+
*
|
|
264
|
+
* Used as a fallback when the anchor's stellar.toml does not publish a
|
|
265
|
+
* SIGNING_KEY, providing strong protection against malformed or malicious
|
|
266
|
+
* challenge transactions.
|
|
267
|
+
*
|
|
268
|
+
* @internal
|
|
269
|
+
* @see {@link https://github.com/stellar/js-stellar-sdk/blob/v13.0.0-beta.1/src/webauth/utils.ts#L188 | WebAuth.readChallengeTx}
|
|
270
|
+
*/
|
|
271
|
+
const readChallengeTx = (
|
|
272
|
+
challengeTx: string,
|
|
273
|
+
networkPassphrase: string,
|
|
274
|
+
homeDomain: string,
|
|
275
|
+
webAuthDomain: string,
|
|
276
|
+
): { tx: Transaction; clientAccountID: string } => {
|
|
277
|
+
let transaction: Transaction;
|
|
278
|
+
try {
|
|
279
|
+
transaction = new Transaction(challengeTx, networkPassphrase);
|
|
280
|
+
} catch {
|
|
281
|
+
try {
|
|
282
|
+
// eslint-disable-next-line no-new
|
|
283
|
+
new FeeBumpTransaction(challengeTx, networkPassphrase);
|
|
284
|
+
} catch {
|
|
285
|
+
throw new Error(
|
|
286
|
+
"Invalid challenge: unable to deserialize challengeTx transaction string",
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
throw new Error(
|
|
290
|
+
"Invalid challenge: expected a Transaction but received a FeeBumpTransaction",
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// verify sequence number
|
|
295
|
+
const sequence = Number.parseInt(transaction.sequence, 10);
|
|
296
|
+
if (sequence !== 0) {
|
|
297
|
+
throw new Error("The transaction sequence number should be zero");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// verify operations
|
|
301
|
+
if (transaction.operations.length < 1) {
|
|
302
|
+
throw new Error("The transaction should contain at least one operation");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const [operation, ...subsequentOperations] = transaction.operations;
|
|
306
|
+
|
|
307
|
+
if (!operation.source) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
"The transaction's operation should contain a source account",
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
const clientAccountID: string = operation.source;
|
|
313
|
+
|
|
314
|
+
// verify memo
|
|
315
|
+
if (transaction.memo.type !== "none") {
|
|
316
|
+
if (clientAccountID.startsWith("M")) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
"The transaction has a memo but the client account ID is a muxed account",
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
if (transaction.memo.type !== "id") {
|
|
322
|
+
throw new Error("The transaction's memo must be of type `id`");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (operation.type !== "manageData") {
|
|
327
|
+
throw new Error("The transaction's operation type should be 'manageData'");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// verify timebounds
|
|
331
|
+
if (!transaction.timeBounds) {
|
|
332
|
+
throw new Error("The transaction requires timebounds");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (Number.parseInt(transaction.timeBounds.maxTime, 10) === 0) {
|
|
336
|
+
throw new Error("The transaction requires non-infinite timebounds");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const now = Math.floor(Date.now() / 1000);
|
|
340
|
+
const gracePeriod = 60 * 5;
|
|
341
|
+
const minTime = Number.parseInt(transaction.timeBounds.minTime, 10) || 0;
|
|
342
|
+
const maxTime = Number.parseInt(transaction.timeBounds.maxTime, 10) || 0;
|
|
343
|
+
if (now < minTime - gracePeriod || now > maxTime + gracePeriod) {
|
|
344
|
+
throw new Error("The transaction has expired");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// verify nonce value
|
|
348
|
+
if (operation.value === undefined || !operation.value) {
|
|
349
|
+
throw new Error("The transaction's operation value should not be null");
|
|
198
350
|
}
|
|
351
|
+
|
|
352
|
+
if (Buffer.from(operation.value.toString(), "base64").length !== 48) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
"The transaction's operation value should be a 64 bytes base64 random string",
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// verify home domain
|
|
359
|
+
if (`${homeDomain} auth` !== operation.name) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
"Invalid homeDomains: the transaction's operation key name " +
|
|
362
|
+
"does not match the expected home domain",
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// verify subsequent operations are all manageData
|
|
367
|
+
for (const op of subsequentOperations) {
|
|
368
|
+
if (op.type !== "manageData") {
|
|
369
|
+
throw new Error(
|
|
370
|
+
"The transaction has operations that are not of type 'manageData'",
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
if (op.name === "web_auth_domain") {
|
|
374
|
+
if (op.value === undefined) {
|
|
375
|
+
throw new Error("'web_auth_domain' operation value should not be null");
|
|
376
|
+
}
|
|
377
|
+
if (op.value.compare(Buffer.from(webAuthDomain)) !== 0) {
|
|
378
|
+
throw new Error(
|
|
379
|
+
`'web_auth_domain' operation value does not match ${webAuthDomain}`,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return { tx: transaction, clientAccountID };
|
|
199
386
|
};
|
|
200
387
|
|
|
201
388
|
const createAuthSignToken = async (
|
|
@@ -280,6 +280,22 @@ export class Sep38PriceOnlyOneAmountError extends Error {
|
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
+
export class ChallengeValidationFailedError extends Error {
|
|
284
|
+
constructor(cause: Error) {
|
|
285
|
+
super(`SEP-10 challenge validation failed: ${cause.message}`);
|
|
286
|
+
Object.setPrototypeOf(this, ChallengeValidationFailedError.prototype);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export class NetworkPassphraseMismatchError extends Error {
|
|
291
|
+
constructor(expected: string, received: string) {
|
|
292
|
+
super(
|
|
293
|
+
`Network passphrase mismatch: expected "${expected}" but server returned "${received}"`,
|
|
294
|
+
);
|
|
295
|
+
Object.setPrototypeOf(this, NetworkPassphraseMismatchError.prototype);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
283
299
|
export class ChallengeTxnIncorrectSequenceError extends Error {
|
|
284
300
|
constructor() {
|
|
285
301
|
super("Challenge transaction sequence number must be 0");
|
|
@@ -37,11 +37,17 @@ export class AuthToken {
|
|
|
37
37
|
const authToken = new AuthToken();
|
|
38
38
|
|
|
39
39
|
const decoded = decode(str);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
// jws.decode only auto-parses payload as JSON when header contains
|
|
41
|
+
// typ:"JWT". Some anchors omit typ, returning a raw JSON string instead.
|
|
42
|
+
const payload =
|
|
43
|
+
typeof decoded.payload === "string"
|
|
44
|
+
? JSON.parse(decoded.payload)
|
|
45
|
+
: decoded.payload;
|
|
46
|
+
authToken.issuer = payload.iss;
|
|
47
|
+
authToken.principalAccount = payload.sub;
|
|
48
|
+
authToken.issuedAt = payload.iat;
|
|
49
|
+
authToken.expiresAt = payload.exp;
|
|
50
|
+
authToken.clientDomain = payload.client_domain;
|
|
45
51
|
authToken.token = str;
|
|
46
52
|
return authToken;
|
|
47
53
|
};
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
IsValidSep7UriResult,
|
|
8
8
|
WEB_STELLAR_SCHEME,
|
|
9
9
|
URI_MSG_MAX_LENGTH,
|
|
10
|
+
URI_REPLACE_MAX_LENGTH,
|
|
10
11
|
} from "../Types";
|
|
11
12
|
import {
|
|
12
13
|
Sep7InvalidUriError,
|
|
@@ -162,16 +163,55 @@ export const sep7ReplacementsFromString = (
|
|
|
162
163
|
return [];
|
|
163
164
|
}
|
|
164
165
|
|
|
166
|
+
if (replacements.length > URI_REPLACE_MAX_LENGTH) {
|
|
167
|
+
throw new Sep7InvalidUriError(
|
|
168
|
+
"the 'replace' parameter exceeds the maximum allowed length",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
165
172
|
const [txrepString, hintsString] = replacements.split(HINT_DELIMITER);
|
|
166
|
-
const hintsList = hintsString.split(LIST_DELIMITER);
|
|
167
173
|
|
|
168
|
-
const
|
|
174
|
+
const txrepList = txrepString.split(LIST_DELIMITER);
|
|
175
|
+
const txrepIds: string[] = [];
|
|
176
|
+
txrepList.forEach((item) => {
|
|
177
|
+
const parts = item.split(ID_DELIMITER);
|
|
178
|
+
if (parts.length < 2 || !parts[0] || !parts[1]) {
|
|
179
|
+
throw new Sep7InvalidUriError(
|
|
180
|
+
"the 'replace' parameter has an entry missing a path or reference identifier",
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
const id = parts[1];
|
|
184
|
+
if (txrepIds.indexOf(id) === -1) {
|
|
185
|
+
txrepIds.push(id);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
169
188
|
|
|
170
|
-
|
|
171
|
-
.map((item) => item.split(ID_DELIMITER))
|
|
172
|
-
.forEach(([id, hint]) => (hintsMap[id] = hint));
|
|
189
|
+
const hintsMap = Object.create(null) as Record<string, string>;
|
|
173
190
|
|
|
174
|
-
|
|
191
|
+
if (hintsString) {
|
|
192
|
+
const hintsList = hintsString.split(LIST_DELIMITER);
|
|
193
|
+
hintsList.forEach((item) => {
|
|
194
|
+
const parts = item.split(ID_DELIMITER);
|
|
195
|
+
if (parts.length < 2 || !parts[0] || !parts[1]) {
|
|
196
|
+
throw new Sep7InvalidUriError(
|
|
197
|
+
"the 'replace' parameter has a hint entry missing an identifier or hint value",
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
hintsMap[parts[0]] = parts[1];
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const hintIds = Object.keys(hintsMap);
|
|
205
|
+
|
|
206
|
+
const isBalanced =
|
|
207
|
+
txrepIds.length === hintIds.length &&
|
|
208
|
+
txrepIds.every((id) => Object.prototype.hasOwnProperty.call(hintsMap, id));
|
|
209
|
+
|
|
210
|
+
if (!isBalanced) {
|
|
211
|
+
throw new Sep7InvalidUriError(
|
|
212
|
+
"the 'replace' parameter has unbalanced reference identifiers",
|
|
213
|
+
);
|
|
214
|
+
}
|
|
175
215
|
|
|
176
216
|
const replacementsList = txrepList
|
|
177
217
|
.map((item) => item.split(ID_DELIMITER))
|