@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.
Files changed (155) hide show
  1. package/dist/_cjs/actions/auth/getOAuthLoginUrl.js +18 -0
  2. package/dist/_cjs/actions/auth/getOAuthLoginUrl.js.map +1 -0
  3. package/dist/_cjs/actions/auth/getWhoami.js +2 -2
  4. package/dist/_cjs/actions/auth/getWhoami.js.map +1 -1
  5. package/dist/_cjs/actions/auth/index.js +3 -1
  6. package/dist/_cjs/actions/auth/index.js.map +1 -1
  7. package/dist/_cjs/actions/auth/loginWithStamp.js +5 -5
  8. package/dist/_cjs/actions/auth/loginWithStamp.js.map +1 -1
  9. package/dist/_cjs/actions/auth/registerWithOTP.js.map +1 -1
  10. package/dist/_cjs/actions/index.js +2 -1
  11. package/dist/_cjs/actions/index.js.map +1 -1
  12. package/dist/_cjs/actions/wallet/signingUtils.js +2 -2
  13. package/dist/_cjs/actions/wallet/signingUtils.js.map +1 -1
  14. package/dist/_cjs/client/authProxy.js +1 -1
  15. package/dist/_cjs/client/authProxy.js.map +1 -1
  16. package/dist/_cjs/client/createClient.js +5 -5
  17. package/dist/_cjs/client/createClient.js.map +1 -1
  18. package/dist/_cjs/client/decorators/client.js +1 -0
  19. package/dist/_cjs/client/decorators/client.js.map +1 -1
  20. package/dist/_cjs/client/transports/createTransport.js +5 -5
  21. package/dist/_cjs/client/transports/createTransport.js.map +1 -1
  22. package/dist/_cjs/client/transports/rest.js +5 -5
  23. package/dist/_cjs/client/transports/rest.js.map +1 -1
  24. package/dist/_cjs/constants.js +2 -1
  25. package/dist/_cjs/constants.js.map +1 -1
  26. package/dist/_cjs/core/createZeroDevWallet.js +38 -64
  27. package/dist/_cjs/core/createZeroDevWallet.js.map +1 -1
  28. package/dist/_cjs/index.js.map +1 -1
  29. package/dist/_cjs/stampers/indexedDbStamper.js +17 -2
  30. package/dist/_cjs/stampers/indexedDbStamper.js.map +1 -1
  31. package/dist/_cjs/stampers/webauthnStamper.js +23 -3
  32. package/dist/_cjs/stampers/webauthnStamper.js.map +1 -1
  33. package/dist/_cjs/utils/encryptOtpAttempt.js +57 -0
  34. package/dist/_cjs/utils/encryptOtpAttempt.js.map +1 -0
  35. package/dist/_cjs/utils/exportPrivateKey.js +1 -1
  36. package/dist/_cjs/utils/exportPrivateKey.js.map +1 -1
  37. package/dist/_cjs/utils/exportWallet.js +2 -6
  38. package/dist/_cjs/utils/exportWallet.js.map +1 -1
  39. package/dist/_cjs/utils/hpke.js +78 -0
  40. package/dist/_cjs/utils/hpke.js.map +1 -0
  41. package/dist/_cjs/utils/utils.js +5 -6
  42. package/dist/_cjs/utils/utils.js.map +1 -1
  43. package/dist/_esm/actions/auth/getOAuthLoginUrl.js +23 -0
  44. package/dist/_esm/actions/auth/getOAuthLoginUrl.js.map +1 -0
  45. package/dist/_esm/actions/auth/getWhoami.js +2 -2
  46. package/dist/_esm/actions/auth/getWhoami.js.map +1 -1
  47. package/dist/_esm/actions/auth/index.js +1 -0
  48. package/dist/_esm/actions/auth/index.js.map +1 -1
  49. package/dist/_esm/actions/auth/loginWithStamp.js +5 -5
  50. package/dist/_esm/actions/auth/loginWithStamp.js.map +1 -1
  51. package/dist/_esm/actions/auth/registerWithOTP.js.map +1 -1
  52. package/dist/_esm/actions/index.js +1 -1
  53. package/dist/_esm/actions/index.js.map +1 -1
  54. package/dist/_esm/actions/wallet/signingUtils.js +2 -2
  55. package/dist/_esm/actions/wallet/signingUtils.js.map +1 -1
  56. package/dist/_esm/client/authProxy.js +9 -4
  57. package/dist/_esm/client/authProxy.js.map +1 -1
  58. package/dist/_esm/client/createClient.js +5 -5
  59. package/dist/_esm/client/createClient.js.map +1 -1
  60. package/dist/_esm/client/decorators/client.js +2 -1
  61. package/dist/_esm/client/decorators/client.js.map +1 -1
  62. package/dist/_esm/client/transports/createTransport.js +5 -5
  63. package/dist/_esm/client/transports/createTransport.js.map +1 -1
  64. package/dist/_esm/client/transports/rest.js +5 -5
  65. package/dist/_esm/client/transports/rest.js.map +1 -1
  66. package/dist/_esm/constants.js +6 -0
  67. package/dist/_esm/constants.js.map +1 -1
  68. package/dist/_esm/core/createZeroDevWallet.js +42 -66
  69. package/dist/_esm/core/createZeroDevWallet.js.map +1 -1
  70. package/dist/_esm/index.js.map +1 -1
  71. package/dist/_esm/stampers/indexedDbStamper.js +17 -2
  72. package/dist/_esm/stampers/indexedDbStamper.js.map +1 -1
  73. package/dist/_esm/stampers/webauthnStamper.js +23 -4
  74. package/dist/_esm/stampers/webauthnStamper.js.map +1 -1
  75. package/dist/_esm/utils/encryptOtpAttempt.js +81 -0
  76. package/dist/_esm/utils/encryptOtpAttempt.js.map +1 -0
  77. package/dist/_esm/utils/exportPrivateKey.js +1 -1
  78. package/dist/_esm/utils/exportPrivateKey.js.map +1 -1
  79. package/dist/_esm/utils/exportWallet.js +2 -6
  80. package/dist/_esm/utils/exportWallet.js.map +1 -1
  81. package/dist/_esm/utils/hpke.js +119 -0
  82. package/dist/_esm/utils/hpke.js.map +1 -0
  83. package/dist/_esm/utils/utils.js +5 -6
  84. package/dist/_esm/utils/utils.js.map +1 -1
  85. package/dist/_types/actions/auth/getOAuthLoginUrl.d.ts +30 -0
  86. package/dist/_types/actions/auth/getOAuthLoginUrl.d.ts.map +1 -0
  87. package/dist/_types/actions/auth/index.d.ts +1 -0
  88. package/dist/_types/actions/auth/index.d.ts.map +1 -1
  89. package/dist/_types/actions/auth/loginWithStamp.d.ts +2 -1
  90. package/dist/_types/actions/auth/loginWithStamp.d.ts.map +1 -1
  91. package/dist/_types/actions/auth/registerWithOTP.d.ts +6 -0
  92. package/dist/_types/actions/auth/registerWithOTP.d.ts.map +1 -1
  93. package/dist/_types/actions/index.d.ts +1 -1
  94. package/dist/_types/actions/index.d.ts.map +1 -1
  95. package/dist/_types/client/authProxy.d.ts +13 -7
  96. package/dist/_types/client/authProxy.d.ts.map +1 -1
  97. package/dist/_types/client/decorators/client.d.ts +7 -1
  98. package/dist/_types/client/decorators/client.d.ts.map +1 -1
  99. package/dist/_types/client/transports/rest.d.ts +5 -4
  100. package/dist/_types/client/transports/rest.d.ts.map +1 -1
  101. package/dist/_types/client/types.d.ts +9 -9
  102. package/dist/_types/client/types.d.ts.map +1 -1
  103. package/dist/_types/constants.d.ts +1 -0
  104. package/dist/_types/constants.d.ts.map +1 -1
  105. package/dist/_types/core/createZeroDevWallet.d.ts +13 -0
  106. package/dist/_types/core/createZeroDevWallet.d.ts.map +1 -1
  107. package/dist/_types/index.d.ts +1 -1
  108. package/dist/_types/index.d.ts.map +1 -1
  109. package/dist/_types/stampers/index.d.ts +1 -1
  110. package/dist/_types/stampers/index.d.ts.map +1 -1
  111. package/dist/_types/stampers/indexedDbStamper.d.ts +2 -2
  112. package/dist/_types/stampers/indexedDbStamper.d.ts.map +1 -1
  113. package/dist/_types/stampers/types.d.ts +31 -5
  114. package/dist/_types/stampers/types.d.ts.map +1 -1
  115. package/dist/_types/stampers/webauthnStamper.d.ts +2 -2
  116. package/dist/_types/stampers/webauthnStamper.d.ts.map +1 -1
  117. package/dist/_types/types/session.d.ts +2 -3
  118. package/dist/_types/types/session.d.ts.map +1 -1
  119. package/dist/_types/utils/buildClientSignature.d.ts +3 -3
  120. package/dist/_types/utils/buildClientSignature.d.ts.map +1 -1
  121. package/dist/_types/utils/encryptOtpAttempt.d.ts +40 -0
  122. package/dist/_types/utils/encryptOtpAttempt.d.ts.map +1 -0
  123. package/dist/_types/utils/exportWallet.d.ts.map +1 -1
  124. package/dist/_types/utils/hpke.d.ts +38 -0
  125. package/dist/_types/utils/hpke.d.ts.map +1 -0
  126. package/dist/_types/utils/utils.d.ts.map +1 -1
  127. package/dist/tsconfig.build.tsbuildinfo +1 -1
  128. package/package.json +5 -1
  129. package/src/actions/auth/getOAuthLoginUrl.ts +48 -0
  130. package/src/actions/auth/getWhoami.ts +2 -2
  131. package/src/actions/auth/index.ts +5 -0
  132. package/src/actions/auth/loginWithStamp.ts +7 -6
  133. package/src/actions/auth/registerWithOTP.ts +6 -0
  134. package/src/actions/index.ts +3 -0
  135. package/src/actions/wallet/signingUtils.ts +2 -2
  136. package/src/client/authProxy.ts +14 -8
  137. package/src/client/createClient.ts +6 -6
  138. package/src/client/decorators/client.ts +13 -0
  139. package/src/client/transports/createTransport.ts +5 -5
  140. package/src/client/transports/rest.ts +11 -10
  141. package/src/client/types.ts +9 -9
  142. package/src/constants.ts +8 -0
  143. package/src/core/createZeroDevWallet.ts +58 -81
  144. package/src/index.ts +5 -2
  145. package/src/stampers/index.ts +2 -2
  146. package/src/stampers/indexedDbStamper.ts +24 -4
  147. package/src/stampers/types.ts +33 -5
  148. package/src/stampers/webauthnStamper.ts +27 -6
  149. package/src/types/session.ts +2 -3
  150. package/src/utils/buildClientSignature.ts +3 -3
  151. package/src/utils/encryptOtpAttempt.ts +142 -0
  152. package/src/utils/exportPrivateKey.ts +1 -1
  153. package/src/utils/exportWallet.ts +2 -6
  154. package/src/utils/hpke.ts +219 -0
  155. 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
- base64UrlEncode,
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 indexedDbStamper = await createIndexedDbStamper()
133
+ const apiKeyStamper = config.apiKeyStamper ?? (await createIndexedDbStamper())
127
134
 
128
- const webauthnStamper = await createWebauthnStamper({ rpId })
135
+ const passkeyStamper =
136
+ config.passkeyStamper ?? (await createWebauthnStamper({ rpId }))
129
137
 
130
138
  const client = createClient({
131
- indexedDbStamper,
132
- webauthnStamper,
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.indexedDbStamper.resetKeyPair()
144
- const compressedPublicKey = await client.indexedDbStamper.getPublicKey()
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 === 'indexedDb') {
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 generateCompressedPublicKeyFromKeyPair(newKeyPair)
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: 'indexedDb',
198
+ stampWith: 'apiKey',
199
199
  })
200
- await client.indexedDbStamper.resetKeyPair(newKeyPair)
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: 'indexedDb',
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: 'indexedDb',
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.indexedDbStamper.resetKeyPair()
256
- const tempPublicKey = await client.indexedDbStamper.getPublicKey()
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 = await getWebAuthnAttestation({
265
- publicKey: {
259
+ const { attestation, encodedChallenge } =
260
+ await passkeyStamper.register({
266
261
  rp: { id: rpId, name: '' },
267
- challenge,
268
- pubKeyCredParams: [
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 generateCompressedPublicKeyFromKeyPair(newKeyPair)
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.indexedDbStamper.resetKeyPair(newKeyPair)
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: 'indexedDb',
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.indexedDbStamper.resetKeyPair()
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: 'webauthn',
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: 'indexedDb',
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.indexedDbStamper.resetKeyPair()
408
- const targetPublicKey = await client.indexedDbStamper.getPublicKey()
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 2: Verify OTP via Auth Proxy
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
- otpCode,
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.indexedDbStamper,
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: 'indexedDb',
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.indexedDbStamper.resetKeyPair()
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
- IndexedDbStamper,
85
- WebauthnStamper,
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'
@@ -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
- IndexedDbStamper,
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 type { IndexedDbStamper } from './types.js'
2
+ import { generateCompressedPublicKeyFromKeyPair } from '../utils/utils.js'
3
+ import type { ApiKeyStamper } from './types.js'
3
4
 
4
- export async function createIndexedDbStamper(): Promise<IndexedDbStamper> {
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(externalKeyPair?: CryptoKeyPair) {
19
- await inner.resetKeyPair(externalKeyPair)
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
  }
@@ -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 IndexedDbStamper = Stamper & {
34
- resetKeyPair: (externalKeyPair?: CryptoKeyPair) => Promise<void>
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 type { WebauthnStamper } from './types.js'
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<WebauthnStamper> {
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
  }
@@ -3,7 +3,7 @@ export enum SessionType {
3
3
  READ_WRITE = 'SESSION_TYPE_READ_WRITE',
4
4
  }
5
5
 
6
- export type StamperType = 'iframe' | 'indexedDb' | 'passkey'
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?: string
15
- publicKey?: string
14
+ token: string
16
15
  expiry: number
17
16
  createdAt: number
18
17
  }
@@ -1,4 +1,4 @@
1
- import type { IndexedDbStamper } from '../stampers/types.js'
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 IndexedDB stamper for signing */
10
- stamper: IndexedDbStamper
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
+ }