@zerodev/wallet-core 0.0.1-alpha.17 → 0.0.1-alpha.19
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/dist/_cjs/actions/auth/getOAuthLoginUrl.js +18 -0
- package/dist/_cjs/actions/auth/getOAuthLoginUrl.js.map +1 -0
- package/dist/_cjs/actions/auth/getWhoami.js +2 -2
- package/dist/_cjs/actions/auth/getWhoami.js.map +1 -1
- package/dist/_cjs/actions/auth/index.js +3 -1
- package/dist/_cjs/actions/auth/index.js.map +1 -1
- package/dist/_cjs/actions/auth/loginWithStamp.js +5 -5
- package/dist/_cjs/actions/auth/loginWithStamp.js.map +1 -1
- package/dist/_cjs/actions/auth/registerWithOTP.js.map +1 -1
- package/dist/_cjs/actions/index.js +2 -1
- package/dist/_cjs/actions/index.js.map +1 -1
- package/dist/_cjs/actions/wallet/signingUtils.js +2 -2
- package/dist/_cjs/actions/wallet/signingUtils.js.map +1 -1
- package/dist/_cjs/client/authProxy.js +1 -1
- package/dist/_cjs/client/authProxy.js.map +1 -1
- package/dist/_cjs/client/createClient.js +5 -5
- package/dist/_cjs/client/createClient.js.map +1 -1
- package/dist/_cjs/client/decorators/client.js +1 -0
- package/dist/_cjs/client/decorators/client.js.map +1 -1
- package/dist/_cjs/client/transports/createTransport.js +5 -5
- package/dist/_cjs/client/transports/createTransport.js.map +1 -1
- package/dist/_cjs/client/transports/rest.js +5 -5
- package/dist/_cjs/client/transports/rest.js.map +1 -1
- package/dist/_cjs/constants.js +2 -1
- package/dist/_cjs/constants.js.map +1 -1
- package/dist/_cjs/core/createZeroDevWallet.js +38 -64
- package/dist/_cjs/core/createZeroDevWallet.js.map +1 -1
- package/dist/_cjs/index.js.map +1 -1
- package/dist/_cjs/stampers/indexedDbStamper.js +17 -2
- package/dist/_cjs/stampers/indexedDbStamper.js.map +1 -1
- package/dist/_cjs/stampers/webauthnStamper.js +23 -3
- package/dist/_cjs/stampers/webauthnStamper.js.map +1 -1
- package/dist/_cjs/utils/encryptOtpAttempt.js +57 -0
- package/dist/_cjs/utils/encryptOtpAttempt.js.map +1 -0
- package/dist/_cjs/utils/exportPrivateKey.js +1 -1
- package/dist/_cjs/utils/exportPrivateKey.js.map +1 -1
- package/dist/_cjs/utils/exportWallet.js +2 -6
- package/dist/_cjs/utils/exportWallet.js.map +1 -1
- package/dist/_cjs/utils/hpke.js +78 -0
- package/dist/_cjs/utils/hpke.js.map +1 -0
- package/dist/_cjs/utils/utils.js +5 -6
- package/dist/_cjs/utils/utils.js.map +1 -1
- package/dist/_esm/actions/auth/getOAuthLoginUrl.js +23 -0
- package/dist/_esm/actions/auth/getOAuthLoginUrl.js.map +1 -0
- package/dist/_esm/actions/auth/getWhoami.js +2 -2
- package/dist/_esm/actions/auth/getWhoami.js.map +1 -1
- package/dist/_esm/actions/auth/index.js +1 -0
- package/dist/_esm/actions/auth/index.js.map +1 -1
- package/dist/_esm/actions/auth/loginWithStamp.js +5 -5
- package/dist/_esm/actions/auth/loginWithStamp.js.map +1 -1
- package/dist/_esm/actions/auth/registerWithOTP.js.map +1 -1
- package/dist/_esm/actions/index.js +1 -1
- package/dist/_esm/actions/index.js.map +1 -1
- package/dist/_esm/actions/wallet/signingUtils.js +2 -2
- package/dist/_esm/actions/wallet/signingUtils.js.map +1 -1
- package/dist/_esm/client/authProxy.js +9 -4
- package/dist/_esm/client/authProxy.js.map +1 -1
- package/dist/_esm/client/createClient.js +5 -5
- package/dist/_esm/client/createClient.js.map +1 -1
- package/dist/_esm/client/decorators/client.js +2 -1
- package/dist/_esm/client/decorators/client.js.map +1 -1
- package/dist/_esm/client/transports/createTransport.js +5 -5
- package/dist/_esm/client/transports/createTransport.js.map +1 -1
- package/dist/_esm/client/transports/rest.js +5 -5
- package/dist/_esm/client/transports/rest.js.map +1 -1
- package/dist/_esm/constants.js +6 -0
- package/dist/_esm/constants.js.map +1 -1
- package/dist/_esm/core/createZeroDevWallet.js +42 -66
- package/dist/_esm/core/createZeroDevWallet.js.map +1 -1
- package/dist/_esm/index.js.map +1 -1
- package/dist/_esm/stampers/indexedDbStamper.js +17 -2
- package/dist/_esm/stampers/indexedDbStamper.js.map +1 -1
- package/dist/_esm/stampers/webauthnStamper.js +23 -4
- package/dist/_esm/stampers/webauthnStamper.js.map +1 -1
- package/dist/_esm/utils/encryptOtpAttempt.js +81 -0
- package/dist/_esm/utils/encryptOtpAttempt.js.map +1 -0
- package/dist/_esm/utils/exportPrivateKey.js +1 -1
- package/dist/_esm/utils/exportPrivateKey.js.map +1 -1
- package/dist/_esm/utils/exportWallet.js +2 -6
- package/dist/_esm/utils/exportWallet.js.map +1 -1
- package/dist/_esm/utils/hpke.js +119 -0
- package/dist/_esm/utils/hpke.js.map +1 -0
- package/dist/_esm/utils/utils.js +5 -6
- package/dist/_esm/utils/utils.js.map +1 -1
- package/dist/_types/actions/auth/getOAuthLoginUrl.d.ts +30 -0
- package/dist/_types/actions/auth/getOAuthLoginUrl.d.ts.map +1 -0
- package/dist/_types/actions/auth/index.d.ts +1 -0
- package/dist/_types/actions/auth/index.d.ts.map +1 -1
- package/dist/_types/actions/auth/loginWithStamp.d.ts +2 -1
- package/dist/_types/actions/auth/loginWithStamp.d.ts.map +1 -1
- package/dist/_types/actions/auth/registerWithOTP.d.ts +6 -0
- package/dist/_types/actions/auth/registerWithOTP.d.ts.map +1 -1
- package/dist/_types/actions/index.d.ts +1 -1
- package/dist/_types/actions/index.d.ts.map +1 -1
- package/dist/_types/client/authProxy.d.ts +13 -7
- package/dist/_types/client/authProxy.d.ts.map +1 -1
- package/dist/_types/client/decorators/client.d.ts +7 -1
- package/dist/_types/client/decorators/client.d.ts.map +1 -1
- package/dist/_types/client/transports/rest.d.ts +5 -4
- package/dist/_types/client/transports/rest.d.ts.map +1 -1
- package/dist/_types/client/types.d.ts +9 -9
- package/dist/_types/client/types.d.ts.map +1 -1
- package/dist/_types/constants.d.ts +1 -0
- package/dist/_types/constants.d.ts.map +1 -1
- package/dist/_types/core/createZeroDevWallet.d.ts +13 -0
- package/dist/_types/core/createZeroDevWallet.d.ts.map +1 -1
- package/dist/_types/index.d.ts +1 -1
- package/dist/_types/index.d.ts.map +1 -1
- package/dist/_types/stampers/index.d.ts +1 -1
- package/dist/_types/stampers/index.d.ts.map +1 -1
- package/dist/_types/stampers/indexedDbStamper.d.ts +2 -2
- package/dist/_types/stampers/indexedDbStamper.d.ts.map +1 -1
- package/dist/_types/stampers/types.d.ts +31 -5
- package/dist/_types/stampers/types.d.ts.map +1 -1
- package/dist/_types/stampers/webauthnStamper.d.ts +2 -2
- package/dist/_types/stampers/webauthnStamper.d.ts.map +1 -1
- package/dist/_types/types/session.d.ts +2 -3
- package/dist/_types/types/session.d.ts.map +1 -1
- package/dist/_types/utils/buildClientSignature.d.ts +3 -3
- package/dist/_types/utils/buildClientSignature.d.ts.map +1 -1
- package/dist/_types/utils/encryptOtpAttempt.d.ts +40 -0
- package/dist/_types/utils/encryptOtpAttempt.d.ts.map +1 -0
- package/dist/_types/utils/exportWallet.d.ts.map +1 -1
- package/dist/_types/utils/hpke.d.ts +38 -0
- package/dist/_types/utils/hpke.d.ts.map +1 -0
- package/dist/_types/utils/utils.d.ts.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +5 -1
- package/src/actions/auth/getOAuthLoginUrl.ts +48 -0
- package/src/actions/auth/getWhoami.ts +2 -2
- package/src/actions/auth/index.ts +5 -0
- package/src/actions/auth/loginWithStamp.ts +7 -6
- package/src/actions/auth/registerWithOTP.ts +6 -0
- package/src/actions/index.ts +3 -0
- package/src/actions/wallet/signingUtils.ts +2 -2
- package/src/client/authProxy.ts +14 -8
- package/src/client/createClient.ts +6 -6
- package/src/client/decorators/client.ts +13 -0
- package/src/client/transports/createTransport.ts +5 -5
- package/src/client/transports/rest.ts +11 -10
- package/src/client/types.ts +9 -9
- package/src/constants.ts +8 -0
- package/src/core/createZeroDevWallet.ts +58 -81
- package/src/index.ts +5 -2
- package/src/stampers/index.ts +2 -2
- package/src/stampers/indexedDbStamper.ts +24 -4
- package/src/stampers/types.ts +33 -5
- package/src/stampers/webauthnStamper.ts +27 -6
- package/src/types/session.ts +2 -3
- package/src/utils/buildClientSignature.ts +3 -3
- package/src/utils/encryptOtpAttempt.ts +142 -0
- package/src/utils/exportPrivateKey.ts +1 -1
- package/src/utils/exportWallet.ts +2 -6
- package/src/utils/hpke.ts +219 -0
- package/src/utils/utils.ts +5 -6
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getWebAuthnAttestation } from '@turnkey/http'
|
|
2
1
|
import type { LocalAccount } from 'viem/accounts'
|
|
3
2
|
import type {
|
|
4
3
|
EmailCustomization,
|
|
@@ -17,6 +16,7 @@ import {
|
|
|
17
16
|
KMS_SERVER_URL,
|
|
18
17
|
} from '../constants.js'
|
|
19
18
|
import { createIndexedDbStamper } from '../stampers/indexedDbStamper.js'
|
|
19
|
+
import type { ApiKeyStamper, PasskeyStamper } from '../stampers/types.js'
|
|
20
20
|
import { createWebauthnStamper } from '../stampers/webauthnStamper.js'
|
|
21
21
|
import { createWebStorageAdapter } from '../storage/adapters.js'
|
|
22
22
|
import {
|
|
@@ -25,19 +25,16 @@ import {
|
|
|
25
25
|
} from '../storage/manager.js'
|
|
26
26
|
import { SessionType, type ZeroDevWalletSession } from '../types/session.js'
|
|
27
27
|
import { buildClientSignature } from '../utils/buildClientSignature.js'
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
generateCompressedPublicKeyFromKeyPair,
|
|
31
|
-
generateRandomBuffer,
|
|
32
|
-
humanReadableDateTime,
|
|
33
|
-
parseSession,
|
|
34
|
-
} from '../utils/utils.js'
|
|
28
|
+
import { encryptOtpAttempt } from '../utils/encryptOtpAttempt.js'
|
|
29
|
+
import { humanReadableDateTime, parseSession } from '../utils/utils.js'
|
|
35
30
|
export interface ZeroDevWalletConfig {
|
|
36
31
|
organizationId?: string
|
|
37
32
|
proxyBaseUrl?: string
|
|
38
33
|
projectId: string
|
|
39
34
|
sessionStorage?: StorageAdapter
|
|
40
35
|
rpId?: string
|
|
36
|
+
apiKeyStamper?: ApiKeyStamper
|
|
37
|
+
passkeyStamper?: PasskeyStamper
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
// Re-export EmailCustomization for convenience
|
|
@@ -72,6 +69,11 @@ export type AuthParams =
|
|
|
72
69
|
mode: 'verifyOtp'
|
|
73
70
|
otpId: string
|
|
74
71
|
otpCode: string
|
|
72
|
+
/**
|
|
73
|
+
* The encryption target bundle returned by the matching `sendOtp` call.
|
|
74
|
+
* Required — used to HPKE-encrypt the OTP attempt to the enclave.
|
|
75
|
+
*/
|
|
76
|
+
otpEncryptionTargetBundle: string
|
|
75
77
|
}
|
|
76
78
|
| {
|
|
77
79
|
type: 'magicLink'
|
|
@@ -85,6 +87,11 @@ export type AuthParams =
|
|
|
85
87
|
mode: 'verify'
|
|
86
88
|
otpId: string
|
|
87
89
|
code: string
|
|
90
|
+
/**
|
|
91
|
+
* The encryption target bundle returned by the matching `sendMagicLink`
|
|
92
|
+
* (a.k.a. magicLink `send`) call. Required for the encrypted-OTP flow.
|
|
93
|
+
*/
|
|
94
|
+
otpEncryptionTargetBundle: string
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
export interface ZeroDevWalletSDK {
|
|
@@ -123,13 +130,14 @@ export async function createZeroDevWallet(
|
|
|
123
130
|
sessionStorage || createWebStorageAdapter(),
|
|
124
131
|
)
|
|
125
132
|
|
|
126
|
-
const
|
|
133
|
+
const apiKeyStamper = config.apiKeyStamper ?? (await createIndexedDbStamper())
|
|
127
134
|
|
|
128
|
-
const
|
|
135
|
+
const passkeyStamper =
|
|
136
|
+
config.passkeyStamper ?? (await createWebauthnStamper({ rpId }))
|
|
129
137
|
|
|
130
138
|
const client = createClient({
|
|
131
|
-
|
|
132
|
-
|
|
139
|
+
apiKeyStamper,
|
|
140
|
+
passkeyStamper,
|
|
133
141
|
transport: zeroDevWalletTransport({
|
|
134
142
|
baseUrl: config.proxyBaseUrl || `${KMS_SERVER_URL}/api/v1`,
|
|
135
143
|
}),
|
|
@@ -140,8 +148,8 @@ export async function createZeroDevWallet(
|
|
|
140
148
|
return {
|
|
141
149
|
client,
|
|
142
150
|
async getPublicKey() {
|
|
143
|
-
await client.
|
|
144
|
-
const compressedPublicKey = await client.
|
|
151
|
+
await client.apiKeyStamper.resetKeyPair()
|
|
152
|
+
const compressedPublicKey = await client.apiKeyStamper.getPublicKey()
|
|
145
153
|
return compressedPublicKey
|
|
146
154
|
},
|
|
147
155
|
|
|
@@ -180,30 +188,22 @@ export async function createZeroDevWallet(
|
|
|
180
188
|
if (!activeSession) {
|
|
181
189
|
throw new Error('No active session')
|
|
182
190
|
}
|
|
183
|
-
if (activeSession.stamperType === '
|
|
184
|
-
const newKeyPair = await crypto.subtle.generateKey(
|
|
185
|
-
{
|
|
186
|
-
name: 'ECDSA',
|
|
187
|
-
namedCurve: 'P-256',
|
|
188
|
-
},
|
|
189
|
-
false,
|
|
190
|
-
['sign', 'verify'],
|
|
191
|
-
)
|
|
191
|
+
if (activeSession.stamperType === 'apiKey') {
|
|
192
192
|
const compressedPublicKeyHex =
|
|
193
|
-
await
|
|
193
|
+
await client.apiKeyStamper.prepareKeyRotation()
|
|
194
194
|
const data = await client.loginWithStamp({
|
|
195
195
|
targetPublicKey: compressedPublicKeyHex,
|
|
196
196
|
projectId,
|
|
197
197
|
organizationId: activeSession.organizationId,
|
|
198
|
-
stampWith: '
|
|
198
|
+
stampWith: 'apiKey',
|
|
199
199
|
})
|
|
200
|
-
await client.
|
|
200
|
+
await client.apiKeyStamper.commitKeyRotation()
|
|
201
201
|
const parsedSession = parseSession(data.session)
|
|
202
202
|
const session: ZeroDevWalletSession = {
|
|
203
203
|
id: `session_indexedDb_${Date.now()}`,
|
|
204
204
|
userId: parsedSession.userId,
|
|
205
205
|
organizationId: parsedSession.organizationId,
|
|
206
|
-
stamperType: '
|
|
206
|
+
stamperType: 'apiKey',
|
|
207
207
|
sessionType: SessionType.READ_WRITE,
|
|
208
208
|
token: data.session,
|
|
209
209
|
expiry: parsedSession.expiry,
|
|
@@ -229,17 +229,15 @@ export async function createZeroDevWallet(
|
|
|
229
229
|
if (data.session) {
|
|
230
230
|
// Parse the JWT to get session data
|
|
231
231
|
const parsedSession = parseSession(data.session)
|
|
232
|
-
const publicKey = await client.indexedDbStamper.getPublicKey()
|
|
233
232
|
const session: ZeroDevWalletSession = {
|
|
234
233
|
id: `session_oauth_${Date.now()}`,
|
|
235
234
|
userId: parsedSession.userId,
|
|
236
235
|
organizationId: parsedSession.organizationId,
|
|
237
|
-
stamperType: '
|
|
236
|
+
stamperType: 'apiKey',
|
|
238
237
|
sessionType: parsedSession.sessionType || SessionType.READ_WRITE,
|
|
239
238
|
token: data.session,
|
|
240
239
|
expiry: parsedSession.expiry,
|
|
241
240
|
createdAt: Date.now(),
|
|
242
|
-
publicKey: publicKey || '',
|
|
243
241
|
}
|
|
244
242
|
await sessionStorageManager.storeSession(session, session.id)
|
|
245
243
|
}
|
|
@@ -252,62 +250,35 @@ export async function createZeroDevWallet(
|
|
|
252
250
|
'mode' in params &&
|
|
253
251
|
params.mode === 'register'
|
|
254
252
|
) {
|
|
255
|
-
await client.
|
|
256
|
-
const tempPublicKey = await client.
|
|
253
|
+
await client.apiKeyStamper.resetKeyPair()
|
|
254
|
+
const tempPublicKey = await client.apiKeyStamper.getPublicKey()
|
|
257
255
|
if (!tempPublicKey) {
|
|
258
256
|
throw new Error('Failed to get public key')
|
|
259
257
|
}
|
|
260
|
-
const challenge = generateRandomBuffer()
|
|
261
|
-
const encodedChallenge = base64UrlEncode(challenge)
|
|
262
|
-
const authenticatorUserId = generateRandomBuffer()
|
|
263
258
|
const name = `ZeroDevWallet-${humanReadableDateTime()}`
|
|
264
|
-
const attestation
|
|
265
|
-
|
|
259
|
+
const { attestation, encodedChallenge } =
|
|
260
|
+
await passkeyStamper.register({
|
|
266
261
|
rp: { id: rpId, name: '' },
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
{
|
|
270
|
-
type: 'public-key',
|
|
271
|
-
alg: -7,
|
|
272
|
-
},
|
|
273
|
-
{
|
|
274
|
-
type: 'public-key',
|
|
275
|
-
alg: -257,
|
|
276
|
-
},
|
|
277
|
-
],
|
|
278
|
-
user: {
|
|
279
|
-
id: authenticatorUserId,
|
|
280
|
-
name,
|
|
281
|
-
displayName: name,
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
|
-
})
|
|
262
|
+
userName: name,
|
|
263
|
+
})
|
|
285
264
|
const data = await client.registerWithPasskey({
|
|
286
265
|
attestation,
|
|
287
266
|
challenge: encodedChallenge,
|
|
288
267
|
projectId,
|
|
289
268
|
encodedPublicKey: tempPublicKey,
|
|
290
269
|
})
|
|
291
|
-
const newKeyPair = await crypto.subtle.generateKey(
|
|
292
|
-
{
|
|
293
|
-
name: 'ECDSA',
|
|
294
|
-
namedCurve: 'P-256',
|
|
295
|
-
},
|
|
296
|
-
false,
|
|
297
|
-
['sign', 'verify'],
|
|
298
|
-
)
|
|
299
270
|
const compressedPublicKeyHex =
|
|
300
|
-
await
|
|
271
|
+
await client.apiKeyStamper.prepareKeyRotation()
|
|
301
272
|
const loginData = await client.loginWithStamp({
|
|
302
273
|
projectId,
|
|
303
274
|
targetPublicKey: compressedPublicKeyHex,
|
|
304
275
|
organizationId: data.subOrganizationId,
|
|
305
276
|
})
|
|
306
|
-
await client.
|
|
277
|
+
await client.apiKeyStamper.commitKeyRotation()
|
|
307
278
|
const parsedSession = parseSession(loginData.session)
|
|
308
279
|
const session: ZeroDevWalletSession = {
|
|
309
280
|
id: `session_indexedDb_${Date.now()}`,
|
|
310
|
-
stamperType: '
|
|
281
|
+
stamperType: 'apiKey',
|
|
311
282
|
createdAt: Date.now(),
|
|
312
283
|
sessionType: SessionType.READ_WRITE,
|
|
313
284
|
userId: parsedSession.userId,
|
|
@@ -325,9 +296,8 @@ export async function createZeroDevWallet(
|
|
|
325
296
|
'mode' in params &&
|
|
326
297
|
params.mode === 'login'
|
|
327
298
|
) {
|
|
328
|
-
await client.
|
|
329
|
-
const generatedPublicKey =
|
|
330
|
-
await client.indexedDbStamper.getPublicKey()
|
|
299
|
+
await client.apiKeyStamper.resetKeyPair()
|
|
300
|
+
const generatedPublicKey = await client.apiKeyStamper.getPublicKey()
|
|
331
301
|
if (!generatedPublicKey) {
|
|
332
302
|
throw new Error('Failed to get public key')
|
|
333
303
|
}
|
|
@@ -335,12 +305,12 @@ export async function createZeroDevWallet(
|
|
|
335
305
|
targetPublicKey: generatedPublicKey,
|
|
336
306
|
projectId,
|
|
337
307
|
organizationId,
|
|
338
|
-
stampWith: '
|
|
308
|
+
stampWith: 'passkey',
|
|
339
309
|
})
|
|
340
310
|
const parsedSession = parseSession(loginData.session)
|
|
341
311
|
const session: ZeroDevWalletSession = {
|
|
342
312
|
id: `session_indexedDb_${Date.now()}`,
|
|
343
|
-
stamperType: '
|
|
313
|
+
stamperType: 'apiKey',
|
|
344
314
|
createdAt: Date.now(),
|
|
345
315
|
sessionType: SessionType.READ_WRITE,
|
|
346
316
|
userId: parsedSession.userId,
|
|
@@ -379,6 +349,7 @@ export async function createZeroDevWallet(
|
|
|
379
349
|
mode: 'verifyOtp',
|
|
380
350
|
otpId: params.otpId,
|
|
381
351
|
otpCode: params.code,
|
|
352
|
+
otpEncryptionTargetBundle: params.otpEncryptionTargetBundle,
|
|
382
353
|
}
|
|
383
354
|
}
|
|
384
355
|
} else {
|
|
@@ -401,17 +372,25 @@ export async function createZeroDevWallet(
|
|
|
401
372
|
}
|
|
402
373
|
|
|
403
374
|
if (otpParams.mode === 'verifyOtp') {
|
|
404
|
-
const { otpId, otpCode } = otpParams
|
|
375
|
+
const { otpId, otpCode, otpEncryptionTargetBundle } = otpParams
|
|
405
376
|
|
|
406
377
|
// Step 1: Generate new key pair
|
|
407
|
-
await client.
|
|
408
|
-
const targetPublicKey = await client.
|
|
378
|
+
await client.apiKeyStamper.resetKeyPair()
|
|
379
|
+
const targetPublicKey = await client.apiKeyStamper.getPublicKey()
|
|
409
380
|
|
|
410
381
|
if (!targetPublicKey) {
|
|
411
382
|
throw new Error('Failed to get public key')
|
|
412
383
|
}
|
|
413
384
|
|
|
414
|
-
// Step
|
|
385
|
+
// Step 2a: HPKE-seal the OTP attempt to the enclave's per-session
|
|
386
|
+
// target key. The auth proxy never sees the plaintext OTP code.
|
|
387
|
+
const encryptedOtpBundle = await encryptOtpAttempt({
|
|
388
|
+
otpCode,
|
|
389
|
+
publicKey: targetPublicKey,
|
|
390
|
+
encryptionTargetBundle: otpEncryptionTargetBundle,
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
// Step 2b: Verify OTP via Auth Proxy
|
|
415
394
|
if (!cachedAuthProxyConfigId) {
|
|
416
395
|
const { authProxyConfigId } = await client.getAuthProxyConfigId()
|
|
417
396
|
cachedAuthProxyConfigId = authProxyConfigId
|
|
@@ -422,15 +401,14 @@ export async function createZeroDevWallet(
|
|
|
422
401
|
|
|
423
402
|
const { verificationToken } = await authProxyClient.verifyOtp({
|
|
424
403
|
otpId,
|
|
425
|
-
|
|
426
|
-
public_key: targetPublicKey,
|
|
404
|
+
encryptedOtpBundle,
|
|
427
405
|
})
|
|
428
406
|
|
|
429
407
|
// Step 3: Build client signature
|
|
430
408
|
const clientSignature = await buildClientSignature({
|
|
431
409
|
verificationToken,
|
|
432
410
|
publicKey: targetPublicKey,
|
|
433
|
-
stamper: client.
|
|
411
|
+
stamper: client.apiKeyStamper,
|
|
434
412
|
})
|
|
435
413
|
|
|
436
414
|
// Step 4: Login via backend (not Auth Proxy!)
|
|
@@ -447,13 +425,12 @@ export async function createZeroDevWallet(
|
|
|
447
425
|
id: `session_otp_${Date.now()}`,
|
|
448
426
|
userId: parsedSession.userId,
|
|
449
427
|
organizationId: parsedSession.organizationId,
|
|
450
|
-
stamperType: '
|
|
428
|
+
stamperType: 'apiKey',
|
|
451
429
|
sessionType:
|
|
452
430
|
parsedSession.sessionType || SessionType.READ_WRITE,
|
|
453
431
|
token: data.session,
|
|
454
432
|
expiry: parsedSession.expiry,
|
|
455
433
|
createdAt: Date.now(),
|
|
456
|
-
publicKey: targetPublicKey,
|
|
457
434
|
}
|
|
458
435
|
await sessionStorageManager.storeSession(session, session.id)
|
|
459
436
|
}
|
|
@@ -469,7 +446,7 @@ export async function createZeroDevWallet(
|
|
|
469
446
|
|
|
470
447
|
async logout() {
|
|
471
448
|
await sessionStorageManager.clearAllSessions()
|
|
472
|
-
await client.
|
|
449
|
+
await client.apiKeyStamper.resetKeyPair()
|
|
473
450
|
return true
|
|
474
451
|
},
|
|
475
452
|
|
package/src/index.ts
CHANGED
|
@@ -80,9 +80,12 @@ export {
|
|
|
80
80
|
createWebauthnStamper,
|
|
81
81
|
} from './stampers/index.js'
|
|
82
82
|
export type {
|
|
83
|
+
ApiKeyStamper,
|
|
84
|
+
Attestation,
|
|
83
85
|
IframeStamper,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
PasskeyRegistrationOptions,
|
|
87
|
+
PasskeyRegistrationResult,
|
|
88
|
+
PasskeyStamper,
|
|
86
89
|
} from './stampers/types.js'
|
|
87
90
|
// Storage
|
|
88
91
|
export type { StorageAdapter, StorageManager } from './storage/manager.js'
|
package/src/stampers/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { createIframeStamper } from './iframeStamper.js'
|
|
2
2
|
export { createIndexedDbStamper } from './indexedDbStamper.js'
|
|
3
3
|
export type {
|
|
4
|
+
ApiKeyStamper,
|
|
4
5
|
IframeStamper,
|
|
5
|
-
|
|
6
|
-
WebauthnStamper,
|
|
6
|
+
PasskeyStamper,
|
|
7
7
|
} from './types.js'
|
|
8
8
|
export { createWebauthnStamper } from './webauthnStamper.js'
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { IndexedDbStamper as TurnkeyIndexedDbStamper } from '@turnkey/indexed-db-stamper'
|
|
2
|
-
import
|
|
2
|
+
import { generateCompressedPublicKeyFromKeyPair } from '../utils/utils.js'
|
|
3
|
+
import type { ApiKeyStamper } from './types.js'
|
|
3
4
|
|
|
4
|
-
export async function createIndexedDbStamper(): Promise<
|
|
5
|
+
export async function createIndexedDbStamper(): Promise<ApiKeyStamper> {
|
|
5
6
|
const inner = new TurnkeyIndexedDbStamper()
|
|
6
7
|
await inner.init()
|
|
7
8
|
|
|
9
|
+
let pendingKeyPair: CryptoKeyPair | null = null
|
|
10
|
+
|
|
8
11
|
return {
|
|
9
12
|
async getPublicKey() {
|
|
10
13
|
return await inner.getPublicKey()
|
|
@@ -15,8 +18,25 @@ export async function createIndexedDbStamper(): Promise<IndexedDbStamper> {
|
|
|
15
18
|
async clear() {
|
|
16
19
|
await inner.clear()
|
|
17
20
|
},
|
|
18
|
-
async resetKeyPair(
|
|
19
|
-
|
|
21
|
+
async resetKeyPair() {
|
|
22
|
+
pendingKeyPair = null
|
|
23
|
+
await inner.resetKeyPair()
|
|
24
|
+
},
|
|
25
|
+
async prepareKeyRotation() {
|
|
26
|
+
const keyPair = await crypto.subtle.generateKey(
|
|
27
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
28
|
+
false,
|
|
29
|
+
['sign', 'verify'],
|
|
30
|
+
)
|
|
31
|
+
pendingKeyPair = keyPair
|
|
32
|
+
return await generateCompressedPublicKeyFromKeyPair(keyPair)
|
|
33
|
+
},
|
|
34
|
+
async commitKeyRotation() {
|
|
35
|
+
if (!pendingKeyPair) {
|
|
36
|
+
throw new Error('No pending key rotation to commit')
|
|
37
|
+
}
|
|
38
|
+
await inner.resetKeyPair(pendingKeyPair)
|
|
39
|
+
pendingKeyPair = null
|
|
20
40
|
},
|
|
21
41
|
}
|
|
22
42
|
}
|
package/src/stampers/types.ts
CHANGED
|
@@ -5,8 +5,6 @@ export type Stamp = {
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export type Stamper = {
|
|
8
|
-
/** retrieve public key compressed or otherwise as per the stamper */
|
|
9
|
-
getPublicKey: () => Promise<string | null>
|
|
10
8
|
/** produce Turnkey header value for a given request body */
|
|
11
9
|
stamp: (payload: string) => Promise<Stamp>
|
|
12
10
|
/** clear local state (embedded key, IDB keypair, etc.) */
|
|
@@ -16,6 +14,8 @@ export type Stamper = {
|
|
|
16
14
|
export type KeyFormat = 'Hexadecimal' | 'Solana'
|
|
17
15
|
|
|
18
16
|
export type IframeStamper = Stamper & {
|
|
17
|
+
/** retrieve public key compressed or otherwise as per the stamper */
|
|
18
|
+
getPublicKey: () => Promise<string | null>
|
|
19
19
|
init(): Promise<string>
|
|
20
20
|
injectCredentialBundle(bundle: string): Promise<boolean>
|
|
21
21
|
injectWalletExportBundle(
|
|
@@ -30,7 +30,35 @@ export type IframeStamper = Stamper & {
|
|
|
30
30
|
applySettings(settings: { styles?: Record<string, string> }): Promise<boolean>
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export type
|
|
34
|
-
|
|
33
|
+
export type ApiKeyStamper = Stamper & {
|
|
34
|
+
/** retrieve public key compressed or otherwise as per the stamper */
|
|
35
|
+
getPublicKey: () => Promise<string | null>
|
|
36
|
+
/** Generate + activate a new key pair immediately (simple cases: login init, logout). */
|
|
37
|
+
resetKeyPair: () => Promise<void>
|
|
38
|
+
/** Generate a new key pair internally, return its compressed public key, but keep the OLD key active for stamp(). */
|
|
39
|
+
prepareKeyRotation: () => Promise<string>
|
|
40
|
+
/** Promote the pending key to active. Call after the server accepts the new key. */
|
|
41
|
+
commitKeyRotation: () => Promise<void>
|
|
42
|
+
}
|
|
43
|
+
export type Attestation = {
|
|
44
|
+
attestationObject: string
|
|
45
|
+
clientDataJson: string
|
|
46
|
+
credentialId: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type PasskeyRegistrationOptions = {
|
|
50
|
+
rp: { id: string; name: string }
|
|
51
|
+
userName: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type PasskeyRegistrationResult = {
|
|
55
|
+
attestation: Attestation
|
|
56
|
+
encodedChallenge: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type PasskeyStamper = Stamper & {
|
|
60
|
+
/** Create a new passkey credential. Owns challenge and user ID generation internally. */
|
|
61
|
+
register: (
|
|
62
|
+
options: PasskeyRegistrationOptions,
|
|
63
|
+
) => Promise<PasskeyRegistrationResult>
|
|
35
64
|
}
|
|
36
|
-
export type WebauthnStamper = Stamper
|
|
@@ -1,21 +1,42 @@
|
|
|
1
|
+
import { getWebAuthnAttestation } from '@turnkey/http'
|
|
1
2
|
import { WebauthnStamper as TurnkeyWebauthnStamper } from '@turnkey/webauthn-stamper'
|
|
2
|
-
import
|
|
3
|
+
import { base64UrlEncode, generateRandomBuffer } from '../utils/utils.js'
|
|
4
|
+
import type { PasskeyRegistrationOptions, PasskeyStamper } from './types.js'
|
|
3
5
|
|
|
4
6
|
export async function createWebauthnStamper({
|
|
5
7
|
rpId,
|
|
6
8
|
}: {
|
|
7
9
|
rpId: string
|
|
8
|
-
}): Promise<
|
|
10
|
+
}): Promise<PasskeyStamper> {
|
|
9
11
|
const inner = new TurnkeyWebauthnStamper({ rpId })
|
|
10
12
|
|
|
11
13
|
return {
|
|
12
|
-
async getPublicKey() {
|
|
13
|
-
// return await inner.();
|
|
14
|
-
return null
|
|
15
|
-
},
|
|
16
14
|
async stamp(payload: string) {
|
|
17
15
|
return await inner.stamp(payload)
|
|
18
16
|
},
|
|
19
17
|
async clear() {},
|
|
18
|
+
async register(options: PasskeyRegistrationOptions) {
|
|
19
|
+
const challenge = generateRandomBuffer()
|
|
20
|
+
const encodedChallenge = base64UrlEncode(challenge)
|
|
21
|
+
const authenticatorUserId = generateRandomBuffer()
|
|
22
|
+
|
|
23
|
+
const attestation = await getWebAuthnAttestation({
|
|
24
|
+
publicKey: {
|
|
25
|
+
rp: options.rp,
|
|
26
|
+
challenge,
|
|
27
|
+
pubKeyCredParams: [
|
|
28
|
+
{ type: 'public-key', alg: -7 },
|
|
29
|
+
{ type: 'public-key', alg: -257 },
|
|
30
|
+
],
|
|
31
|
+
user: {
|
|
32
|
+
id: authenticatorUserId,
|
|
33
|
+
name: options.userName,
|
|
34
|
+
displayName: options.userName,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
return { attestation, encodedChallenge }
|
|
40
|
+
},
|
|
20
41
|
}
|
|
21
42
|
}
|
package/src/types/session.ts
CHANGED
|
@@ -3,7 +3,7 @@ export enum SessionType {
|
|
|
3
3
|
READ_WRITE = 'SESSION_TYPE_READ_WRITE',
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
export type StamperType = '
|
|
6
|
+
export type StamperType = 'apiKey' | 'passkey'
|
|
7
7
|
|
|
8
8
|
export type ZeroDevWalletSession = {
|
|
9
9
|
id: string
|
|
@@ -11,8 +11,7 @@ export type ZeroDevWalletSession = {
|
|
|
11
11
|
organizationId: string
|
|
12
12
|
stamperType: StamperType
|
|
13
13
|
sessionType?: SessionType
|
|
14
|
-
token
|
|
15
|
-
publicKey?: string
|
|
14
|
+
token: string
|
|
16
15
|
expiry: number
|
|
17
16
|
createdAt: number
|
|
18
17
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ApiKeyStamper } from '../stampers/types.js'
|
|
2
2
|
import { derToRawSignature } from './derToRawSignature.js'
|
|
3
3
|
|
|
4
4
|
export type BuildClientSignatureParams = {
|
|
@@ -6,8 +6,8 @@ export type BuildClientSignatureParams = {
|
|
|
6
6
|
verificationToken: string
|
|
7
7
|
/** The compressed public key hex */
|
|
8
8
|
publicKey: string
|
|
9
|
-
/** The
|
|
10
|
-
stamper:
|
|
9
|
+
/** The API key stamper for signing */
|
|
10
|
+
stamper: ApiKeyStamper
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps the OTP code + client public key in a Turnkey-compatible HPKE bundle
|
|
3
|
+
* for the `/v1/otp_verify_v2` auth-proxy endpoint.
|
|
4
|
+
*
|
|
5
|
+
* Bundle flow (RFC 9180 mode_base over Turnkey's TLS Fetcher enclave):
|
|
6
|
+
* 1. The backend's /init/otp returns a signed envelope that contains an
|
|
7
|
+
* ephemeral HPKE public key (`targetPublic`) generated fresh by the
|
|
8
|
+
* enclave for this OTP attempt.
|
|
9
|
+
* 2. We verify the envelope's ECDSA signature against a pinned production
|
|
10
|
+
* key (`TURNKEY_TLS_FETCHER_SIGN_PUBLIC_KEY`) so a compromised proxy
|
|
11
|
+
* cannot substitute its own ephemeral key.
|
|
12
|
+
* 3. We HPKE-seal `{otp_code, public_key}` to `targetPublic`. The auth proxy
|
|
13
|
+
* forwards the ciphertext to the enclave; only the enclave can decrypt
|
|
14
|
+
* it. The enclave then issues a `verificationToken` bound to the public
|
|
15
|
+
* key embedded in the plaintext.
|
|
16
|
+
*
|
|
17
|
+
* See: tkhq/go-sdk `examples/email_otp` and `pkg/enclave_encrypt`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { p256 } from '@noble/curves/nist.js'
|
|
21
|
+
import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js'
|
|
22
|
+
import { TURNKEY_TLS_FETCHER_SIGN_PUBLIC_KEY } from '../constants.js'
|
|
23
|
+
import { hpkeSealP256 } from './hpke.js'
|
|
24
|
+
|
|
25
|
+
const BUNDLE_DATA_VERSION = 'v1.0.0'
|
|
26
|
+
|
|
27
|
+
type EncryptionTargetEnvelope = {
|
|
28
|
+
version: string
|
|
29
|
+
/** Hex-encoded JSON: `{ targetPublic, organizationId, userId? }` */
|
|
30
|
+
data: string
|
|
31
|
+
/** Hex-encoded ECDSA-P256 signature over the raw `data` bytes (DER). */
|
|
32
|
+
dataSignature: string
|
|
33
|
+
/** Hex-encoded uncompressed P-256 pubkey of the signing enclave. */
|
|
34
|
+
enclaveQuorumPublic: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type SignedTargetData = {
|
|
38
|
+
targetPublic: string
|
|
39
|
+
organizationId?: string
|
|
40
|
+
userId?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type EncryptOtpAttemptParams = {
|
|
44
|
+
/** The OTP code the user entered. */
|
|
45
|
+
otpCode: string
|
|
46
|
+
/**
|
|
47
|
+
* The client's session public key (compressed P-256 hex). The enclave binds
|
|
48
|
+
* this key into the `verificationToken` it issues.
|
|
49
|
+
*/
|
|
50
|
+
publicKey: string
|
|
51
|
+
/** The signed envelope returned by `/auth/init/otp`. */
|
|
52
|
+
encryptionTargetBundle: string
|
|
53
|
+
/**
|
|
54
|
+
* Test-only override for the pinned signing key. Production callers should
|
|
55
|
+
* leave this undefined; it exists so tests don't have to use the real key.
|
|
56
|
+
*/
|
|
57
|
+
dangerouslyOverrideSignerPublicKey?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns a JSON string ready to be sent as `encryptedOtpBundle` on
|
|
62
|
+
* `POST /v1/otp_verify_v2`.
|
|
63
|
+
*/
|
|
64
|
+
export async function encryptOtpAttempt({
|
|
65
|
+
otpCode,
|
|
66
|
+
publicKey,
|
|
67
|
+
encryptionTargetBundle,
|
|
68
|
+
dangerouslyOverrideSignerPublicKey,
|
|
69
|
+
}: EncryptOtpAttemptParams): Promise<string> {
|
|
70
|
+
const expectedSignerHex =
|
|
71
|
+
dangerouslyOverrideSignerPublicKey ?? TURNKEY_TLS_FETCHER_SIGN_PUBLIC_KEY
|
|
72
|
+
|
|
73
|
+
let envelope: EncryptionTargetEnvelope
|
|
74
|
+
try {
|
|
75
|
+
envelope = JSON.parse(encryptionTargetBundle)
|
|
76
|
+
} catch (err) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`encryptOtpAttempt: failed to parse encryption target bundle: ${(err as Error).message}`,
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (envelope.version !== BUNDLE_DATA_VERSION) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`encryptOtpAttempt: unsupported bundle version ${envelope.version}`,
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
envelope.enclaveQuorumPublic.toLowerCase() !==
|
|
90
|
+
expectedSignerHex.toLowerCase()
|
|
91
|
+
) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
'encryptOtpAttempt: enclave quorum public key does not match pinned signing key',
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const dataBytes = hexToBytes(envelope.data)
|
|
98
|
+
const signatureBytes = hexToBytes(envelope.dataSignature)
|
|
99
|
+
const signerPublicKeyBytes = hexToBytes(envelope.enclaveQuorumPublic)
|
|
100
|
+
|
|
101
|
+
// The Go side does sha256(data) then ASN.1 DER ECDSA verify, without
|
|
102
|
+
// enforcing low-S. Match that here.
|
|
103
|
+
const valid = p256.verify(signatureBytes, dataBytes, signerPublicKeyBytes, {
|
|
104
|
+
prehash: true,
|
|
105
|
+
format: 'der',
|
|
106
|
+
lowS: false,
|
|
107
|
+
})
|
|
108
|
+
if (!valid) {
|
|
109
|
+
throw new Error('encryptOtpAttempt: invalid enclave signature on bundle')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let signedData: SignedTargetData
|
|
113
|
+
try {
|
|
114
|
+
signedData = JSON.parse(new TextDecoder().decode(dataBytes))
|
|
115
|
+
} catch (err) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`encryptOtpAttempt: failed to parse signed bundle data: ${(err as Error).message}`,
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
if (!signedData.targetPublic) {
|
|
121
|
+
throw new Error('encryptOtpAttempt: missing targetPublic in signed data')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const targetPublicKey = hexToBytes(signedData.targetPublic)
|
|
125
|
+
|
|
126
|
+
// Plaintext shape matches what the Go example marshals:
|
|
127
|
+
// { otp_code: string, public_key: string }
|
|
128
|
+
const plaintext = new TextEncoder().encode(
|
|
129
|
+
JSON.stringify({ otp_code: otpCode, public_key: publicKey }),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const { encappedPublic, ciphertext } = await hpkeSealP256({
|
|
133
|
+
receiverPublicKey: targetPublicKey,
|
|
134
|
+
plaintext,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Wire format = the Go SDK's `ClientSendMsg`: Bytes fields hex-encoded.
|
|
138
|
+
return JSON.stringify({
|
|
139
|
+
encappedPublic: bytesToHex(encappedPublic),
|
|
140
|
+
ciphertext: bytesToHex(ciphertext),
|
|
141
|
+
})
|
|
142
|
+
}
|