@privy-io/node 0.1.0-alpha.2 → 0.1.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 +40 -0
- package/client.d.mts +3 -3
- package/client.d.mts.map +1 -1
- package/client.d.ts +3 -3
- package/client.d.ts.map +1 -1
- package/client.js +1 -1
- package/client.js.map +1 -1
- package/client.mjs +1 -1
- package/client.mjs.map +1 -1
- package/index.d.mts +6 -0
- package/index.d.mts.map +1 -1
- package/index.d.ts +6 -0
- package/index.d.ts.map +1 -1
- package/index.js +7 -1
- package/index.js.map +1 -1
- package/index.mjs +2 -0
- package/index.mjs.map +1 -1
- package/internal/utils/values.js +3 -3
- package/internal/utils/values.js.map +1 -1
- package/internal/utils/values.mjs +3 -3
- package/internal/utils/values.mjs.map +1 -1
- package/lib/auth.d.mts +68 -0
- package/lib/auth.d.mts.map +1 -0
- package/lib/auth.d.ts +68 -0
- package/lib/auth.d.ts.map +1 -0
- package/lib/auth.js +130 -0
- package/lib/auth.js.map +1 -0
- package/lib/auth.mjs +123 -0
- package/lib/auth.mjs.map +1 -0
- package/lib/identity-token.d.mts +15 -0
- package/lib/identity-token.d.mts.map +1 -0
- package/lib/identity-token.d.ts +15 -0
- package/lib/identity-token.d.ts.map +1 -0
- package/lib/identity-token.js +273 -0
- package/lib/identity-token.js.map +1 -0
- package/lib/identity-token.mjs +268 -0
- package/lib/identity-token.mjs.map +1 -0
- package/lib/user-utils.d.mts +22 -0
- package/lib/user-utils.d.mts.map +1 -0
- package/lib/user-utils.d.ts +22 -0
- package/lib/user-utils.d.ts.map +1 -0
- package/lib/user-utils.js +15 -0
- package/lib/user-utils.js.map +1 -0
- package/lib/user-utils.mjs +11 -0
- package/lib/user-utils.mjs.map +1 -0
- package/package.json +19 -1
- package/public-api/PrivyClient.d.mts +2 -1
- package/public-api/PrivyClient.d.mts.map +1 -1
- package/public-api/PrivyClient.d.ts +2 -1
- package/public-api/PrivyClient.d.ts.map +1 -1
- package/public-api/PrivyClient.js +10 -3
- package/public-api/PrivyClient.js.map +1 -1
- package/public-api/PrivyClient.mjs +10 -3
- package/public-api/PrivyClient.mjs.map +1 -1
- package/public-api/services/ethereum.d.mts.map +1 -1
- package/public-api/services/ethereum.d.ts.map +1 -1
- package/public-api/services/ethereum.js +0 -6
- package/public-api/services/ethereum.js.map +1 -1
- package/public-api/services/ethereum.mjs +0 -6
- package/public-api/services/ethereum.mjs.map +1 -1
- package/public-api/services/solana.d.mts.map +1 -1
- package/public-api/services/solana.d.ts.map +1 -1
- package/public-api/services/solana.js +0 -3
- package/public-api/services/solana.js.map +1 -1
- package/public-api/services/solana.mjs +0 -3
- package/public-api/services/solana.mjs.map +1 -1
- package/public-api/services/users.d.mts +23 -1
- package/public-api/services/users.d.mts.map +1 -1
- package/public-api/services/users.d.ts +23 -1
- package/public-api/services/users.d.ts.map +1 -1
- package/public-api/services/users.js +24 -0
- package/public-api/services/users.js.map +1 -1
- package/public-api/services/users.mjs +24 -0
- package/public-api/services/users.mjs.map +1 -1
- package/public-api/services/utils/auth.d.mts +18 -0
- package/public-api/services/utils/auth.d.mts.map +1 -0
- package/public-api/services/utils/auth.d.ts +18 -0
- package/public-api/services/utils/auth.d.ts.map +1 -0
- package/public-api/services/utils/auth.js +33 -0
- package/public-api/services/utils/auth.js.map +1 -0
- package/public-api/services/utils/auth.mjs +29 -0
- package/public-api/services/utils/auth.mjs.map +1 -0
- package/public-api/services/utils/request-formatter.d.mts +5 -0
- package/public-api/services/utils/request-formatter.d.mts.map +1 -0
- package/public-api/services/utils/request-formatter.d.ts +5 -0
- package/public-api/services/utils/request-formatter.d.ts.map +1 -0
- package/public-api/services/utils/request-formatter.js +12 -0
- package/public-api/services/utils/request-formatter.js.map +1 -0
- package/public-api/services/utils/request-formatter.mjs +8 -0
- package/public-api/services/utils/request-formatter.mjs.map +1 -0
- package/public-api/services/utils/request-signer.d.mts +25 -0
- package/public-api/services/utils/request-signer.d.mts.map +1 -0
- package/public-api/services/utils/request-signer.d.ts +25 -0
- package/public-api/services/utils/request-signer.d.ts.map +1 -0
- package/public-api/services/utils/request-signer.js +17 -0
- package/public-api/services/utils/request-signer.js.map +1 -0
- package/public-api/services/utils/request-signer.mjs +13 -0
- package/public-api/services/utils/request-signer.mjs.map +1 -0
- package/public-api/services/utils.d.mts +12 -22
- package/public-api/services/utils.d.mts.map +1 -1
- package/public-api/services/utils.d.ts +12 -22
- package/public-api/services/utils.d.ts.map +1 -1
- package/public-api/services/utils.js +15 -9
- package/public-api/services/utils.js.map +1 -1
- package/public-api/services/utils.mjs +16 -10
- package/public-api/services/utils.mjs.map +1 -1
- package/resources/index.d.mts +1 -1
- package/resources/index.d.mts.map +1 -1
- package/resources/index.d.ts +1 -1
- package/resources/index.d.ts.map +1 -1
- package/resources/policies.d.mts +2 -1
- package/resources/policies.d.mts.map +1 -1
- package/resources/policies.d.ts +2 -1
- package/resources/policies.d.ts.map +1 -1
- package/resources/users.d.mts +35 -24
- package/resources/users.d.mts.map +1 -1
- package/resources/users.d.ts +35 -24
- package/resources/users.d.ts.map +1 -1
- package/resources/users.js +16 -16
- package/resources/users.js.map +1 -1
- package/resources/users.mjs +16 -16
- package/resources/users.mjs.map +1 -1
- package/resources/wallets/wallets.d.mts +3 -18
- package/resources/wallets/wallets.d.mts.map +1 -1
- package/resources/wallets/wallets.d.ts +3 -18
- package/resources/wallets/wallets.d.ts.map +1 -1
- package/resources/wallets/wallets.js.map +1 -1
- package/resources/wallets/wallets.mjs.map +1 -1
- package/src/client.ts +4 -4
- package/src/index.ts +15 -0
- package/src/internal/utils/values.ts +3 -3
- package/src/lib/auth.ts +210 -0
- package/src/lib/identity-token.ts +280 -0
- package/src/lib/user-utils.ts +31 -0
- package/src/public-api/PrivyClient.ts +12 -2
- package/src/public-api/services/ethereum.ts +0 -8
- package/src/public-api/services/solana.ts +0 -4
- package/src/public-api/services/users.ts +36 -2
- package/src/public-api/services/utils/auth.ts +41 -0
- package/src/public-api/services/utils/request-formatter.ts +6 -0
- package/src/public-api/services/utils/request-signer.ts +35 -0
- package/src/public-api/services/utils.ts +21 -31
- package/src/resources/index.ts +1 -1
- package/src/resources/policies.ts +2 -1
- package/src/resources/users.ts +50 -29
- package/src/resources/wallets/wallets.ts +6 -30
- package/src/version.ts +1 -1
- package/src/viem.ts +223 -0
- package/version.d.mts +1 -1
- package/version.d.mts.map +1 -1
- package/version.d.ts +1 -1
- package/version.d.ts.map +1 -1
- package/version.js +1 -1
- package/version.js.map +1 -1
- package/version.mjs +1 -1
- package/version.mjs.map +1 -1
- package/viem.d.mts +22 -0
- package/viem.d.mts.map +1 -0
- package/viem.d.ts +22 -0
- package/viem.d.ts.map +1 -0
- package/viem.js +183 -0
- package/viem.js.map +1 -0
- package/viem.mjs +180 -0
- package/viem.mjs.map +1 -0
package/src/lib/auth.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRemoteJWKSet,
|
|
3
|
+
CryptoKey,
|
|
4
|
+
importSPKI,
|
|
5
|
+
errors as joseErrors,
|
|
6
|
+
jwtVerify,
|
|
7
|
+
JWTVerifyGetKey,
|
|
8
|
+
JWTVerifyResult,
|
|
9
|
+
} from 'jose';
|
|
10
|
+
import { PrivyAPIError } from '../core/error';
|
|
11
|
+
import { parseUserFromIdentityTokenPayload } from './identity-token';
|
|
12
|
+
import { User } from '../resources';
|
|
13
|
+
|
|
14
|
+
const JWT_ALGORITHM = 'ES256';
|
|
15
|
+
const JWT_ISSUER = 'privy.io';
|
|
16
|
+
|
|
17
|
+
export type VerifyAuthTokenInput = {
|
|
18
|
+
/** The authentication token to verify. */
|
|
19
|
+
auth_token: string;
|
|
20
|
+
/** The Privy app ID to verify the token against. */
|
|
21
|
+
app_id: string;
|
|
22
|
+
/**
|
|
23
|
+
* The verification key to use to verify the token, or a mechanism to get the it such as via JWKS.
|
|
24
|
+
* You can find this verification key (or a JWKS endpoint) in the Privy dashboard.
|
|
25
|
+
* @see {@link createRemoteJWKSet}
|
|
26
|
+
* @see {@link importSPKI}
|
|
27
|
+
*/
|
|
28
|
+
verification_key: CryptoKey | JWTVerifyGetKey | string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type VerifyAuthTokenResponse = {
|
|
32
|
+
/** The Privy app ID for which the token was issued. */
|
|
33
|
+
app_id: string;
|
|
34
|
+
/** The issuer of the token. */
|
|
35
|
+
issuer: string;
|
|
36
|
+
/** The issued at unix timestamp of the token. */
|
|
37
|
+
issued_at: number;
|
|
38
|
+
/** The expiration unix timestamp of the token. */
|
|
39
|
+
expiration: number;
|
|
40
|
+
/** The ID of the session for which the token was issued. */
|
|
41
|
+
session_id: string;
|
|
42
|
+
/** The ID of the user for which the token was issued. */
|
|
43
|
+
user_id: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Verifies a JWT issued by privy.io for the given app ID.
|
|
48
|
+
* This serves both auth tokens and identity tokens.
|
|
49
|
+
* @returns The verify result along with the token's payload.
|
|
50
|
+
* @throws If the token is invalid.
|
|
51
|
+
*/
|
|
52
|
+
async function verifyPrivyIssuedJwt(
|
|
53
|
+
jwt: string,
|
|
54
|
+
appId: string,
|
|
55
|
+
verificationKey: CryptoKey | JWTVerifyGetKey,
|
|
56
|
+
): Promise<JWTVerifyResult> {
|
|
57
|
+
// Because of a type difference, the calls cannot be merged into one.
|
|
58
|
+
let verifiedToken: JWTVerifyResult;
|
|
59
|
+
if (typeof verificationKey !== 'function') {
|
|
60
|
+
verifiedToken = await jwtVerify(jwt, verificationKey, {
|
|
61
|
+
typ: 'JWT',
|
|
62
|
+
algorithms: [JWT_ALGORITHM],
|
|
63
|
+
issuer: JWT_ISSUER,
|
|
64
|
+
audience: appId,
|
|
65
|
+
}).catch(mapAndThrowJoseErrors);
|
|
66
|
+
} else {
|
|
67
|
+
verifiedToken = await jwtVerify(jwt, verificationKey, {
|
|
68
|
+
typ: 'JWT',
|
|
69
|
+
algorithms: [JWT_ALGORITHM],
|
|
70
|
+
issuer: JWT_ISSUER,
|
|
71
|
+
audience: appId,
|
|
72
|
+
}).catch(mapAndThrowJoseErrors);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return verifiedToken;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Verifies a Privy-issued authentication token.
|
|
80
|
+
*
|
|
81
|
+
* @returns The payload of the token if it is valid.
|
|
82
|
+
* @throws If the token is invalid.
|
|
83
|
+
*/
|
|
84
|
+
export async function verifyAuthToken({
|
|
85
|
+
auth_token: authToken,
|
|
86
|
+
app_id: appId,
|
|
87
|
+
verification_key: verificationKeyOrString,
|
|
88
|
+
}: VerifyAuthTokenInput): Promise<VerifyAuthTokenResponse> {
|
|
89
|
+
const verificationKey =
|
|
90
|
+
typeof verificationKeyOrString === 'string' ?
|
|
91
|
+
await importSPKI(verificationKeyOrString, JWT_ALGORITHM)
|
|
92
|
+
: verificationKeyOrString;
|
|
93
|
+
const verifiedToken = await verifyPrivyIssuedJwt(authToken, appId, verificationKey);
|
|
94
|
+
return {
|
|
95
|
+
app_id: throwIfNotString(verifiedToken.payload.aud),
|
|
96
|
+
issuer: throwIfNotString(verifiedToken.payload.iss),
|
|
97
|
+
issued_at: throwIfNotNumber(verifiedToken.payload.iat),
|
|
98
|
+
expiration: throwIfNotNumber(verifiedToken.payload.exp),
|
|
99
|
+
session_id: throwIfNotString(verifiedToken.payload['sid']),
|
|
100
|
+
user_id: throwIfNotString(verifiedToken.payload.sub),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type VerifyIdentityTokenInput = {
|
|
105
|
+
/** The identity token to verify. */
|
|
106
|
+
identity_token: string;
|
|
107
|
+
/** The Privy app ID to verify the token against. */
|
|
108
|
+
app_id: string;
|
|
109
|
+
/**
|
|
110
|
+
* The verification key to use to verify the token, or a mechanism to get the it such as via JWKS.
|
|
111
|
+
* You can find this verification key (or a JWKS endpoint) in the Privy dashboard.
|
|
112
|
+
* @see {@link createRemoteJWKSet}
|
|
113
|
+
* @see {@link importSPKI}
|
|
114
|
+
*/
|
|
115
|
+
verification_key: CryptoKey | JWTVerifyGetKey | string;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Verifies an identity token, parsing it into a `User` object if it is valid.
|
|
120
|
+
*
|
|
121
|
+
* @returns The user object parsed from the identity token.
|
|
122
|
+
* @throws If the token or its payload is invalid.
|
|
123
|
+
*/
|
|
124
|
+
export async function verifyIdentityToken({
|
|
125
|
+
identity_token: identityToken,
|
|
126
|
+
app_id: appId,
|
|
127
|
+
verification_key: verificationKeyOrString,
|
|
128
|
+
}: VerifyIdentityTokenInput): Promise<User> {
|
|
129
|
+
const verificationKey =
|
|
130
|
+
typeof verificationKeyOrString === 'string' ?
|
|
131
|
+
await importSPKI(verificationKeyOrString, JWT_ALGORITHM)
|
|
132
|
+
: verificationKeyOrString;
|
|
133
|
+
const verifiedToken = await verifyPrivyIssuedJwt(identityToken, appId, verificationKey);
|
|
134
|
+
|
|
135
|
+
if (!verifiedToken.payload) {
|
|
136
|
+
throw new InvalidAuthTokenError('Unable to parse identity token');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return parseUserFromIdentityTokenPayload(verifiedToken.payload);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export class InvalidAuthTokenError extends PrivyAPIError {}
|
|
143
|
+
|
|
144
|
+
/** Used for asserting the values in the token payload are strings. */
|
|
145
|
+
function throwIfNotString(value: unknown): string {
|
|
146
|
+
if (!value || typeof value !== 'string') {
|
|
147
|
+
throw new InvalidAuthTokenError("Token's payload is invalid");
|
|
148
|
+
}
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Used for asserting the values in the token payload are numbers. */
|
|
153
|
+
function throwIfNotNumber(value: unknown): number {
|
|
154
|
+
if (!value || typeof value !== 'number') {
|
|
155
|
+
throw new InvalidAuthTokenError("Token's payload is invalid");
|
|
156
|
+
}
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Used to catch errors thrown by async `jose` functions and map to our own error types.
|
|
162
|
+
* This method will **always** throw an error, so it's return type is `never`.
|
|
163
|
+
*/
|
|
164
|
+
function mapAndThrowJoseErrors(error: unknown): never {
|
|
165
|
+
if (error instanceof joseErrors.JWTExpired) {
|
|
166
|
+
throw new InvalidAuthTokenError('Authentication token expired');
|
|
167
|
+
} else if (error instanceof joseErrors.JWTClaimValidationFailed || error instanceof joseErrors.JWTInvalid) {
|
|
168
|
+
throw new InvalidAuthTokenError('Authentication token is invalid');
|
|
169
|
+
} else {
|
|
170
|
+
throw new InvalidAuthTokenError('Failed to verify authentication token');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface CreatePrivyAppJWKSInput {
|
|
175
|
+
appId: string;
|
|
176
|
+
apiUrl: string;
|
|
177
|
+
headers: Record<string, string>;
|
|
178
|
+
verificationKeyOverride?: string | undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export type PrivyAppJWKS = JWTVerifyGetKey;
|
|
182
|
+
|
|
183
|
+
export function createPrivyAppJWKS({
|
|
184
|
+
appId,
|
|
185
|
+
apiUrl,
|
|
186
|
+
headers,
|
|
187
|
+
verificationKeyOverride,
|
|
188
|
+
}: CreatePrivyAppJWKSInput): PrivyAppJWKS {
|
|
189
|
+
if (verificationKeyOverride !== undefined) {
|
|
190
|
+
// Use a closure to cache the verification key once imported
|
|
191
|
+
let verificationKey: CryptoKey;
|
|
192
|
+
return async () => {
|
|
193
|
+
if (verificationKey === undefined) {
|
|
194
|
+
try {
|
|
195
|
+
verificationKey = await importSPKI(verificationKeyOverride, JWT_ALGORITHM);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
throw new InvalidAuthTokenError('Failed to import the provided verification key override');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return verificationKey;
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const url = new URL(`${apiUrl}/v1/apps/${appId}/jwks.json`);
|
|
205
|
+
return createRemoteJWKSet(url, {
|
|
206
|
+
cacheMaxAge: 60 * 60 * 1000, // 60 minutes
|
|
207
|
+
cooldownDuration: 10 * 60 * 1000, // 10 minutes
|
|
208
|
+
headers,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { JWTPayload } from 'jose';
|
|
2
|
+
import { User } from '../resources';
|
|
3
|
+
import { PrivyAPIError } from '../error';
|
|
4
|
+
import { EmbeddedWalletLinkedAccount, ExternalWalletLinkedAccount } from './user-utils';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parses the payload of an identity token (JWT) into a `User` object.
|
|
8
|
+
* Note that the user object may be incomplete due to the size constraints of the identity token.
|
|
9
|
+
*
|
|
10
|
+
* @param payload The payload of the identity token.
|
|
11
|
+
* @returns The user object parsed from the identity token.
|
|
12
|
+
* @throws If the payload is invalid.
|
|
13
|
+
*/
|
|
14
|
+
export function parseUserFromIdentityTokenPayload(payload: JWTPayload): User {
|
|
15
|
+
const customMetadata = parseCustomMetadataClaim(payload);
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
id: payload.sub as string,
|
|
19
|
+
created_at: parseInt(payload['cr'] as string),
|
|
20
|
+
is_guest: payload['guest'] === 't',
|
|
21
|
+
linked_accounts: parseLinkedAccountsClaim(payload),
|
|
22
|
+
...(customMetadata ? { custom_metadata: customMetadata } : {}),
|
|
23
|
+
has_accepted_terms: false,
|
|
24
|
+
mfa_methods: [],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseLinkedAccountsClaim(payload: JWTPayload): User['linked_accounts'] {
|
|
29
|
+
const linkedAccountsClaim = payload['linked_accounts'];
|
|
30
|
+
if (typeof linkedAccountsClaim !== 'string') {
|
|
31
|
+
throw new InvalidIdentityTokenError('Unable to parse identity token');
|
|
32
|
+
}
|
|
33
|
+
const parsedLinkedAccounts = JSON.parse(linkedAccountsClaim) as unknown;
|
|
34
|
+
if (!Array.isArray(parsedLinkedAccounts)) {
|
|
35
|
+
throw new InvalidIdentityTokenError('Unable to parse identity token');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return parsedLinkedAccounts
|
|
39
|
+
.map(mapIdLinkedAccountToUserLinkedAccount)
|
|
40
|
+
.filter((account) => account !== null);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function mapIdLinkedAccountToUserLinkedAccount(account: any): User['linked_accounts'][number] | null {
|
|
44
|
+
if (account.type === 'email') {
|
|
45
|
+
return {
|
|
46
|
+
type: 'email',
|
|
47
|
+
address: account.address,
|
|
48
|
+
first_verified_at: null,
|
|
49
|
+
verified_at: account.lv,
|
|
50
|
+
latest_verified_at: account.lv,
|
|
51
|
+
} satisfies User.LinkedAccountEmail;
|
|
52
|
+
}
|
|
53
|
+
if (account.type === 'phone') {
|
|
54
|
+
return {
|
|
55
|
+
type: 'phone',
|
|
56
|
+
phoneNumber: account.phone_number,
|
|
57
|
+
first_verified_at: null,
|
|
58
|
+
verified_at: account.lv,
|
|
59
|
+
latest_verified_at: account.lv,
|
|
60
|
+
} satisfies User.LinkedAccountPhone;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Parses all wallet types
|
|
64
|
+
if (account.type === 'wallet') {
|
|
65
|
+
if (account.wallet_client_type === 'privy') {
|
|
66
|
+
return {
|
|
67
|
+
type: 'wallet',
|
|
68
|
+
wallet_client_type: 'privy',
|
|
69
|
+
wallet_client: 'privy',
|
|
70
|
+
connector_type: 'embedded',
|
|
71
|
+
id: account.id,
|
|
72
|
+
address: account.address,
|
|
73
|
+
chain_type: account.chain_type,
|
|
74
|
+
first_verified_at: null,
|
|
75
|
+
verified_at: account.lv,
|
|
76
|
+
latest_verified_at: account.lv,
|
|
77
|
+
// The following fields are not present in the identity token,
|
|
78
|
+
// but are required. We set them to some safe defaults.
|
|
79
|
+
chain_id: '',
|
|
80
|
+
delegated: false,
|
|
81
|
+
imported: false,
|
|
82
|
+
public_key: '',
|
|
83
|
+
wallet_index: 0,
|
|
84
|
+
recovery_method: account.id ? 'privy-v2' : 'privy',
|
|
85
|
+
} satisfies EmbeddedWalletLinkedAccount;
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
type: 'wallet',
|
|
89
|
+
wallet_client: 'unknown',
|
|
90
|
+
address: account.address,
|
|
91
|
+
chain_type: account.chain_type,
|
|
92
|
+
first_verified_at: null,
|
|
93
|
+
verified_at: account.lv,
|
|
94
|
+
latest_verified_at: account.lv,
|
|
95
|
+
} satisfies ExternalWalletLinkedAccount;
|
|
96
|
+
}
|
|
97
|
+
if (account.type === 'smart_wallet') {
|
|
98
|
+
return {
|
|
99
|
+
type: 'smart_wallet',
|
|
100
|
+
address: account.address,
|
|
101
|
+
smart_wallet_type: account.smart_wallet_type,
|
|
102
|
+
first_verified_at: null,
|
|
103
|
+
verified_at: account.lv,
|
|
104
|
+
latest_verified_at: account.lv,
|
|
105
|
+
} satisfies User.LinkedAccountSmartWallet;
|
|
106
|
+
}
|
|
107
|
+
if (account.type === 'farcaster') {
|
|
108
|
+
return {
|
|
109
|
+
type: 'farcaster',
|
|
110
|
+
fid: account.fid,
|
|
111
|
+
username: account.username,
|
|
112
|
+
first_verified_at: null,
|
|
113
|
+
verified_at: account.lv,
|
|
114
|
+
latest_verified_at: account.lv,
|
|
115
|
+
owner_address: account.oa,
|
|
116
|
+
} satisfies User.LinkedAccountFarcaster;
|
|
117
|
+
}
|
|
118
|
+
if (account.type === 'google_oauth') {
|
|
119
|
+
return {
|
|
120
|
+
type: 'google_oauth',
|
|
121
|
+
subject: account.subject,
|
|
122
|
+
email: account.email,
|
|
123
|
+
name: account.name,
|
|
124
|
+
first_verified_at: null,
|
|
125
|
+
verified_at: account.lv,
|
|
126
|
+
latest_verified_at: account.lv,
|
|
127
|
+
} satisfies User.LinkedAccountGoogleOAuth;
|
|
128
|
+
}
|
|
129
|
+
if (account.type === 'twitter_oauth') {
|
|
130
|
+
// We send along three potential URL shapes here based on possible profile picture URLs, all
|
|
131
|
+
// done to preserve size.
|
|
132
|
+
// 1. pfp begins with `default`, in which case we use the default profile picture URL structure
|
|
133
|
+
// 2. pfp does not start with https, in which case we assume the profile picture URL structure
|
|
134
|
+
// 3. Otherwise, we use the pfp URL as-is
|
|
135
|
+
let pfp = account.pfp ? account.pfp : null;
|
|
136
|
+
if (pfp?.startsWith('default')) {
|
|
137
|
+
pfp = `https://abs.twimg.com/sticky/default_profile_images/${pfp}`;
|
|
138
|
+
} else if (!pfp?.startsWith('https://')) {
|
|
139
|
+
pfp = `https://pbs.twimg.com/profile_images/${pfp}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
type: 'twitter_oauth',
|
|
144
|
+
subject: account.subject,
|
|
145
|
+
username: account.username,
|
|
146
|
+
name: account.name ? account.name : null,
|
|
147
|
+
profile_picture_url: pfp,
|
|
148
|
+
first_verified_at: null,
|
|
149
|
+
verified_at: account.lv,
|
|
150
|
+
latest_verified_at: account.lv,
|
|
151
|
+
} satisfies User.LinkedAccountTwitterOAuth;
|
|
152
|
+
}
|
|
153
|
+
if (account.type === 'discord_oauth') {
|
|
154
|
+
return {
|
|
155
|
+
type: 'discord_oauth',
|
|
156
|
+
subject: account.subject,
|
|
157
|
+
username: account.username,
|
|
158
|
+
email: null, // not a part of the identity token
|
|
159
|
+
first_verified_at: null,
|
|
160
|
+
verified_at: account.lv,
|
|
161
|
+
latest_verified_at: account.lv,
|
|
162
|
+
} satisfies User.LinkedAccountDiscordOAuth;
|
|
163
|
+
}
|
|
164
|
+
if (account.type === 'github_oauth') {
|
|
165
|
+
return {
|
|
166
|
+
type: 'github_oauth',
|
|
167
|
+
subject: account.subject,
|
|
168
|
+
username: account.username,
|
|
169
|
+
email: null, // not a part of the identity token
|
|
170
|
+
name: null, // not a part of the identity token
|
|
171
|
+
first_verified_at: null,
|
|
172
|
+
verified_at: account.lv,
|
|
173
|
+
latest_verified_at: account.lv,
|
|
174
|
+
} satisfies User.LinkedAccountGitHubOAuth;
|
|
175
|
+
}
|
|
176
|
+
if (account.type === 'spotify_oauth') {
|
|
177
|
+
return {
|
|
178
|
+
type: 'spotify_oauth',
|
|
179
|
+
subject: account.subject,
|
|
180
|
+
email: null, // not a part of the identity token
|
|
181
|
+
name: null, // not a part of the identity token
|
|
182
|
+
first_verified_at: null,
|
|
183
|
+
verified_at: account.lv,
|
|
184
|
+
latest_verified_at: account.lv,
|
|
185
|
+
} satisfies User.LinkedAccountSpotifyOAuth;
|
|
186
|
+
}
|
|
187
|
+
if (account.type === 'instagram_oauth') {
|
|
188
|
+
return {
|
|
189
|
+
type: 'instagram_oauth',
|
|
190
|
+
subject: account.subject,
|
|
191
|
+
username: account.username,
|
|
192
|
+
first_verified_at: null,
|
|
193
|
+
verified_at: account.lv,
|
|
194
|
+
latest_verified_at: account.lv,
|
|
195
|
+
} satisfies User.LinkedAccountInstagramOAuth;
|
|
196
|
+
}
|
|
197
|
+
if (account.type === 'tiktok_oauth') {
|
|
198
|
+
return {
|
|
199
|
+
type: 'tiktok_oauth',
|
|
200
|
+
subject: account.subject,
|
|
201
|
+
username: account.username,
|
|
202
|
+
name: null, // not a part of the identity token
|
|
203
|
+
first_verified_at: null,
|
|
204
|
+
verified_at: account.lv,
|
|
205
|
+
latest_verified_at: account.lv,
|
|
206
|
+
} satisfies User.LinkedAccountTiktokOAuth;
|
|
207
|
+
}
|
|
208
|
+
if (account.type === 'linkedin_oauth') {
|
|
209
|
+
return {
|
|
210
|
+
type: 'linkedin_oauth',
|
|
211
|
+
subject: account.subject,
|
|
212
|
+
email: account.email,
|
|
213
|
+
first_verified_at: null,
|
|
214
|
+
verified_at: account.lv,
|
|
215
|
+
latest_verified_at: account.lv,
|
|
216
|
+
} satisfies User.LinkedAccountLinkedInOAuth;
|
|
217
|
+
}
|
|
218
|
+
if (account.type === 'apple_oauth') {
|
|
219
|
+
return {
|
|
220
|
+
type: 'apple_oauth',
|
|
221
|
+
subject: account.subject,
|
|
222
|
+
email: account.email,
|
|
223
|
+
first_verified_at: null,
|
|
224
|
+
verified_at: account.lv,
|
|
225
|
+
latest_verified_at: account.lv,
|
|
226
|
+
} satisfies User.LinkedAccountAppleOAuth;
|
|
227
|
+
}
|
|
228
|
+
if (account.type === 'cross_app') {
|
|
229
|
+
return {
|
|
230
|
+
type: 'cross_app',
|
|
231
|
+
subject: account.subject,
|
|
232
|
+
provider_app_id: account.provider_app_id,
|
|
233
|
+
embedded_wallets: account.embedded_wallets,
|
|
234
|
+
smart_wallets: account.smart_wallets,
|
|
235
|
+
first_verified_at: null,
|
|
236
|
+
verified_at: account.lv,
|
|
237
|
+
latest_verified_at: account.lv,
|
|
238
|
+
} satisfies User.LinkedAccountCrossApp;
|
|
239
|
+
}
|
|
240
|
+
if (account.type === 'custom_auth') {
|
|
241
|
+
return {
|
|
242
|
+
type: 'custom_auth',
|
|
243
|
+
custom_user_id: account.custom_user_id,
|
|
244
|
+
first_verified_at: null,
|
|
245
|
+
verified_at: account.lv,
|
|
246
|
+
latest_verified_at: account.lv,
|
|
247
|
+
} satisfies User.LinkedAccountCustomJwt;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (account.type === 'telegram') {
|
|
251
|
+
return {
|
|
252
|
+
type: 'telegram',
|
|
253
|
+
telegram_user_id: account.telegram_user_id,
|
|
254
|
+
username: account.username,
|
|
255
|
+
first_verified_at: null,
|
|
256
|
+
verified_at: account.lv,
|
|
257
|
+
latest_verified_at: account.lv,
|
|
258
|
+
} satisfies User.LinkedAccountTelegram;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function parseCustomMetadataClaim(payload: JWTPayload): User['custom_metadata'] {
|
|
265
|
+
const customMetadataClaim = payload['custom_metadata'];
|
|
266
|
+
if (customMetadataClaim === undefined) {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
if (typeof customMetadataClaim !== 'string') {
|
|
270
|
+
throw new InvalidIdentityTokenError('Unable to parse identity token');
|
|
271
|
+
}
|
|
272
|
+
const parsedCustomMetadata = JSON.parse(customMetadataClaim) as unknown;
|
|
273
|
+
if (!parsedCustomMetadata || typeof parsedCustomMetadata !== 'object') {
|
|
274
|
+
throw new InvalidIdentityTokenError('Unable to parse identity token');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return parsedCustomMetadata as User['custom_metadata'];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export class InvalidIdentityTokenError extends PrivyAPIError {}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { User } from '../resources/users';
|
|
2
|
+
|
|
3
|
+
export type LinkedAccount = User['linked_accounts'][number];
|
|
4
|
+
|
|
5
|
+
// prettier-ignore
|
|
6
|
+
export type ExternalWalletLinkedAccount = Exclude<
|
|
7
|
+
Extract<LinkedAccount, { type: 'wallet' }>,
|
|
8
|
+
EmbeddedWalletLinkedAccount
|
|
9
|
+
>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* An embedded wallet linked account is a wallet owned by the user that can be used with the Privy API.
|
|
13
|
+
*/
|
|
14
|
+
export type EmbeddedWalletLinkedAccount = Extract<
|
|
15
|
+
LinkedAccount,
|
|
16
|
+
{ type: 'wallet'; wallet_client_type: 'privy'; connector_type: 'embedded' }
|
|
17
|
+
>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Determines if a given linked account is an embedded wallet account.
|
|
21
|
+
* Embedded wallet accounts are wallets that can be used with the Privy API.
|
|
22
|
+
*
|
|
23
|
+
* @param account a linked account
|
|
24
|
+
* @returns whether the account is an embedded wallet account
|
|
25
|
+
*/
|
|
26
|
+
export const isEmbeddedWalletLinkedAccount = (
|
|
27
|
+
account: User['linked_accounts'][number],
|
|
28
|
+
): account is EmbeddedWalletLinkedAccount =>
|
|
29
|
+
account.type === 'wallet' &&
|
|
30
|
+
account.wallet_client_type === 'privy' &&
|
|
31
|
+
account.connector_type === 'embedded';
|
|
@@ -7,6 +7,7 @@ import { PrivyUsersService } from './services/users';
|
|
|
7
7
|
import { PrivyUtils } from './services/utils';
|
|
8
8
|
import { JwtExchangeService } from '../lib/jwt-exchange';
|
|
9
9
|
import { VERSION } from '../version';
|
|
10
|
+
import { createPrivyAppJWKS } from '../lib/auth';
|
|
10
11
|
|
|
11
12
|
type InternalClientOptions = Omit<ClientOptions, 'appID' | 'appSecret' | 'baseUrl'>;
|
|
12
13
|
|
|
@@ -15,6 +16,7 @@ export interface PrivyClientOptions extends InternalClientOptions {
|
|
|
15
16
|
appSecret: string;
|
|
16
17
|
apiUrl?: string;
|
|
17
18
|
authorizationKeyCacheMaxCapacity?: number;
|
|
19
|
+
jwtVerificationKey?: string;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
const DEFAULT_AUTHORIZATION_KEY_CACHE_MAX_CAPACITY = 1000;
|
|
@@ -40,6 +42,7 @@ export class PrivyClient {
|
|
|
40
42
|
apiUrl,
|
|
41
43
|
authorizationKeyCacheMaxCapacity = DEFAULT_AUTHORIZATION_KEY_CACHE_MAX_CAPACITY,
|
|
42
44
|
defaultHeaders,
|
|
45
|
+
jwtVerificationKey,
|
|
43
46
|
...clientOptions
|
|
44
47
|
}: PrivyClientOptions) {
|
|
45
48
|
this.privyApiClient = new PrivyAPI({
|
|
@@ -53,6 +56,13 @@ export class PrivyClient {
|
|
|
53
56
|
'privy-client': `node:${VERSION}`,
|
|
54
57
|
},
|
|
55
58
|
});
|
|
59
|
+
const appJwks = createPrivyAppJWKS({
|
|
60
|
+
appId: this.privyApiClient.appID,
|
|
61
|
+
apiUrl: this.privyApiClient.baseURL,
|
|
62
|
+
headers: { 'privy-client': `node:${VERSION}` },
|
|
63
|
+
verificationKeyOverride: jwtVerificationKey,
|
|
64
|
+
});
|
|
65
|
+
|
|
56
66
|
this.jwtExchangeService = new JwtExchangeService(
|
|
57
67
|
this.privyApiClient.wallets,
|
|
58
68
|
authorizationKeyCacheMaxCapacity,
|
|
@@ -61,8 +71,8 @@ export class PrivyClient {
|
|
|
61
71
|
this.policiesService = new PrivyPoliciesService(this.privyApiClient, this);
|
|
62
72
|
this.transactionsService = new PrivyTransactionsService(this.privyApiClient);
|
|
63
73
|
this.keyQuorumsService = new PrivyKeyQuorumsService(this.privyApiClient, this);
|
|
64
|
-
this.usersService = new PrivyUsersService(this.privyApiClient);
|
|
65
|
-
this.utilsService = new PrivyUtils(this);
|
|
74
|
+
this.usersService = new PrivyUsersService(this.privyApiClient, appJwks);
|
|
75
|
+
this.utilsService = new PrivyUtils(this.privyApiClient, this, appJwks);
|
|
66
76
|
}
|
|
67
77
|
|
|
68
78
|
public wallets(): PrivyWalletsService {
|
|
@@ -57,10 +57,6 @@ export class PrivyEthereumService {
|
|
|
57
57
|
chain_type: 'ethereum',
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
if (!response.data) {
|
|
61
|
-
throw new Error(response.error?.message ?? 'Unexpected response from Privy API');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
60
|
return response.data;
|
|
65
61
|
}
|
|
66
62
|
|
|
@@ -100,10 +96,6 @@ export class PrivyEthereumService {
|
|
|
100
96
|
chain_type: 'ethereum',
|
|
101
97
|
});
|
|
102
98
|
|
|
103
|
-
if (!response.data) {
|
|
104
|
-
throw new Error(response.error?.message ?? 'Unexpected response from Privy API');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
99
|
return response.data;
|
|
108
100
|
}
|
|
109
101
|
}
|
|
@@ -1,3 +1,37 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PrivyAPI } from '../../client';
|
|
2
|
+
import { PrivyAppJWKS, verifyIdentityToken } from '../../lib/auth';
|
|
3
|
+
import { User, Users } from '../../resources';
|
|
2
4
|
|
|
3
|
-
export class PrivyUsersService extends Users {
|
|
5
|
+
export class PrivyUsersService extends Users {
|
|
6
|
+
private appJwks: PrivyAppJWKS;
|
|
7
|
+
constructor(privyApiClient: PrivyAPI, appJwks: PrivyAppJWKS) {
|
|
8
|
+
super(privyApiClient);
|
|
9
|
+
this.appJwks = appJwks;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Gets a user object from the identity token.
|
|
14
|
+
* This verifies the token and parses the payload into a `User` object.
|
|
15
|
+
* Note the user object may be incomplete due to the size constraints of the identity token.
|
|
16
|
+
*
|
|
17
|
+
* @param input.id_token the identity token to parse.
|
|
18
|
+
* @returns the user object parsed from the identity token.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const idToken = req.cookies.get('privy-id-token'); // or however your framework surfaces cookies
|
|
22
|
+
* const user = await client.users().get({idToken});
|
|
23
|
+
*/
|
|
24
|
+
public async get({ id_token }: PrivyUsersService.GetInput): Promise<User> {
|
|
25
|
+
return verifyIdentityToken({
|
|
26
|
+
identity_token: id_token,
|
|
27
|
+
app_id: this._client.appID,
|
|
28
|
+
verification_key: this.appJwks,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export namespace PrivyUsersService {
|
|
34
|
+
export type GetInput = {
|
|
35
|
+
id_token: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { PrivyAPI } from '../../../client';
|
|
2
|
+
import {
|
|
3
|
+
PrivyAppJWKS,
|
|
4
|
+
verifyAuthToken,
|
|
5
|
+
VerifyAuthTokenResponse,
|
|
6
|
+
verifyIdentityToken,
|
|
7
|
+
} from '../../../lib/auth';
|
|
8
|
+
import { User } from '../../../resources';
|
|
9
|
+
|
|
10
|
+
export class PrivyAuthUtils {
|
|
11
|
+
private appJwks: PrivyAppJWKS;
|
|
12
|
+
private privyAppID: string;
|
|
13
|
+
|
|
14
|
+
constructor(privyApiClient: PrivyAPI, appJwks: PrivyAppJWKS) {
|
|
15
|
+
this.appJwks = appJwks;
|
|
16
|
+
this.privyAppID = privyApiClient.appID;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Verifies the authentication token, and returns the payload if it is valid.
|
|
21
|
+
*
|
|
22
|
+
* @param authToken - The authentication token to verify.
|
|
23
|
+
* @returns The payload of the token if it is valid.
|
|
24
|
+
* @throws If the token is invalid.
|
|
25
|
+
*/
|
|
26
|
+
public async verifyAuthToken(authToken: string): Promise<VerifyAuthTokenResponse> {
|
|
27
|
+
return verifyAuthToken({
|
|
28
|
+
auth_token: authToken,
|
|
29
|
+
app_id: this.privyAppID,
|
|
30
|
+
verification_key: this.appJwks,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public async verifyIdentityToken(identityToken: string): Promise<User> {
|
|
35
|
+
return verifyIdentityToken({
|
|
36
|
+
identity_token: identityToken,
|
|
37
|
+
app_id: this.privyAppID,
|
|
38
|
+
verification_key: this.appJwks,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|