@zerodev/wallet-core 0.0.3 → 0.0.5
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/getAuthenticators.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/logout.js +23 -0
- package/dist/_cjs/actions/auth/logout.js.map +1 -0
- package/dist/_cjs/actions/index.js +2 -1
- package/dist/_cjs/actions/index.js.map +1 -1
- package/dist/_cjs/actions/wallet/getUserWallet.js +1 -1
- package/dist/_cjs/actions/wallet/getUserWallet.js.map +1 -1
- package/dist/_cjs/actions/wallet/signingUtils.js +23 -3
- package/dist/_cjs/actions/wallet/signingUtils.js.map +1 -1
- package/dist/_cjs/adapters/viem.js +26 -9
- package/dist/_cjs/adapters/viem.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/rest.js.map +1 -1
- package/dist/_cjs/core/createZeroDevWalletCore.js +297 -168
- package/dist/_cjs/core/createZeroDevWalletCore.js.map +1 -1
- package/dist/_cjs/index.js +2 -1
- package/dist/_cjs/index.js.map +1 -1
- package/dist/_cjs/index.native.js +2 -1
- package/dist/_cjs/index.native.js.map +1 -1
- package/dist/_cjs/native/stampers/secureStore.js +94 -17
- package/dist/_cjs/native/stampers/secureStore.js.map +1 -1
- package/dist/_cjs/stampers/indexedDbStamper.js +34 -0
- package/dist/_cjs/stampers/indexedDbStamper.js.map +1 -1
- package/dist/_cjs/storage/manager.js +144 -17
- package/dist/_cjs/storage/manager.js.map +1 -1
- package/dist/_cjs/utils/buildClientSignature.js +4 -1
- package/dist/_cjs/utils/buildClientSignature.js.map +1 -1
- package/dist/_cjs/utils/derToRawSignature.js +12 -0
- package/dist/_cjs/utils/derToRawSignature.js.map +1 -1
- package/dist/_cjs/utils/exportPrivateKey.js +1 -1
- package/dist/_cjs/utils/exportPrivateKey.js.map +1 -1
- package/dist/_cjs/utils/utils.js +27 -6
- package/dist/_cjs/utils/utils.js.map +1 -1
- package/dist/_esm/actions/auth/getAuthenticators.js +3 -2
- package/dist/_esm/actions/auth/getAuthenticators.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/logout.js +21 -0
- package/dist/_esm/actions/auth/logout.js.map +1 -0
- package/dist/_esm/actions/index.js +1 -1
- package/dist/_esm/actions/index.js.map +1 -1
- package/dist/_esm/actions/wallet/getUserWallet.js +3 -1
- package/dist/_esm/actions/wallet/getUserWallet.js.map +1 -1
- package/dist/_esm/actions/wallet/signingUtils.js +24 -4
- package/dist/_esm/actions/wallet/signingUtils.js.map +1 -1
- package/dist/_esm/adapters/viem.js +27 -10
- package/dist/_esm/adapters/viem.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/rest.js.map +1 -1
- package/dist/_esm/core/createZeroDevWalletCore.js +320 -186
- package/dist/_esm/core/createZeroDevWalletCore.js.map +1 -1
- package/dist/_esm/index.js +1 -1
- package/dist/_esm/index.js.map +1 -1
- package/dist/_esm/index.native.js +1 -1
- package/dist/_esm/index.native.js.map +1 -1
- package/dist/_esm/native/stampers/secureStore.js +111 -18
- package/dist/_esm/native/stampers/secureStore.js.map +1 -1
- package/dist/_esm/stampers/indexedDbStamper.js +36 -0
- package/dist/_esm/stampers/indexedDbStamper.js.map +1 -1
- package/dist/_esm/storage/manager.js +150 -22
- package/dist/_esm/storage/manager.js.map +1 -1
- package/dist/_esm/utils/buildClientSignature.js +4 -1
- package/dist/_esm/utils/buildClientSignature.js.map +1 -1
- package/dist/_esm/utils/derToRawSignature.js +12 -0
- package/dist/_esm/utils/derToRawSignature.js.map +1 -1
- package/dist/_esm/utils/exportPrivateKey.js +1 -1
- package/dist/_esm/utils/exportPrivateKey.js.map +1 -1
- package/dist/_esm/utils/utils.js +27 -6
- package/dist/_esm/utils/utils.js.map +1 -1
- package/dist/_types/actions/auth/getAuthenticators.d.ts +19 -2
- package/dist/_types/actions/auth/getAuthenticators.d.ts.map +1 -1
- package/dist/_types/actions/auth/index.d.ts +2 -1
- package/dist/_types/actions/auth/index.d.ts.map +1 -1
- package/dist/_types/actions/auth/logout.d.ts +11 -0
- package/dist/_types/actions/auth/logout.d.ts.map +1 -0
- package/dist/_types/actions/index.d.ts +1 -1
- package/dist/_types/actions/index.d.ts.map +1 -1
- package/dist/_types/actions/wallet/getUserWallet.d.ts.map +1 -1
- package/dist/_types/actions/wallet/signingUtils.d.ts.map +1 -1
- package/dist/_types/adapters/viem.d.ts.map +1 -1
- package/dist/_types/client/decorators/client.d.ts +3 -1
- package/dist/_types/client/decorators/client.d.ts.map +1 -1
- package/dist/_types/client/transports/rest.d.ts +3 -3
- package/dist/_types/client/transports/rest.d.ts.map +1 -1
- package/dist/_types/core/createZeroDevWalletCore.d.ts +8 -1
- package/dist/_types/core/createZeroDevWalletCore.d.ts.map +1 -1
- package/dist/_types/index.d.ts +2 -2
- package/dist/_types/index.d.ts.map +1 -1
- package/dist/_types/index.native.d.ts +2 -2
- package/dist/_types/index.native.d.ts.map +1 -1
- package/dist/_types/native/stampers/secureStore.d.ts.map +1 -1
- package/dist/_types/stampers/indexedDbStamper.d.ts.map +1 -1
- package/dist/_types/stampers/types.d.ts +6 -0
- package/dist/_types/stampers/types.d.ts.map +1 -1
- package/dist/_types/storage/manager.d.ts +3 -0
- package/dist/_types/storage/manager.d.ts.map +1 -1
- package/dist/_types/types/session.d.ts +2 -0
- package/dist/_types/types/session.d.ts.map +1 -1
- package/dist/_types/utils/buildClientSignature.d.ts +1 -1
- package/dist/_types/utils/buildClientSignature.d.ts.map +1 -1
- package/dist/_types/utils/derToRawSignature.d.ts.map +1 -1
- package/dist/_types/utils/utils.d.ts +1 -1
- package/dist/_types/utils/utils.d.ts.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/actions/auth/getAuthenticators.ts +20 -2
- package/src/actions/auth/index.ts +6 -0
- package/src/actions/auth/logout.ts +35 -0
- package/src/actions/index.ts +4 -0
- package/src/actions/wallet/getUserWallet.ts +3 -1
- package/src/actions/wallet/signingUtils.ts +43 -4
- package/src/adapters/viem.ts +40 -12
- package/src/client/decorators/client.ts +7 -0
- package/src/client/transports/rest.ts +4 -4
- package/src/core/createZeroDevWalletCore.ts +393 -205
- package/src/index.native.ts +4 -0
- package/src/index.ts +4 -0
- package/src/native/stampers/secureStore.ts +119 -24
- package/src/stampers/indexedDbStamper.ts +47 -0
- package/src/stampers/types.ts +6 -0
- package/src/storage/manager.ts +205 -25
- package/src/types/session.ts +2 -0
- package/src/utils/buildClientSignature.ts +4 -2
- package/src/utils/derToRawSignature.ts +16 -2
- package/src/utils/exportPrivateKey.ts +1 -1
- package/src/utils/utils.ts +30 -9
|
@@ -7,11 +7,8 @@ import {
|
|
|
7
7
|
type ZeroDevWalletClient,
|
|
8
8
|
zeroDevWalletTransport,
|
|
9
9
|
} from '../client/index.js'
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
DEFAULT_SESSION_EXPIRATION_IN_SECONDS,
|
|
13
|
-
KMS_SERVER_URL,
|
|
14
|
-
} from '../constants.js'
|
|
10
|
+
import { DEFAULT_ORGANIZATION_ID, KMS_SERVER_URL } from '../constants.js'
|
|
11
|
+
import { RestRequestError } from '../errors/request.js'
|
|
15
12
|
import { createNoopPasskeyStamper } from '../stampers/noopPasskeyStamper.js'
|
|
16
13
|
import type { ApiKeyStamper, PasskeyStamper } from '../stampers/types.js'
|
|
17
14
|
import {
|
|
@@ -102,11 +99,20 @@ export interface ZeroDevWalletSDK {
|
|
|
102
99
|
sessionId?: string,
|
|
103
100
|
) => Promise<ZeroDevWalletSession | undefined>
|
|
104
101
|
|
|
105
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Revoke the active remote key, then erase local state. `force` still tries
|
|
104
|
+
* remote revocation first but permits local recovery if that attempt fails;
|
|
105
|
+
* use it only when accepting that the remote key may remain until expiry.
|
|
106
|
+
*/
|
|
107
|
+
logout: (options?: { force?: boolean }) => Promise<boolean>
|
|
106
108
|
|
|
107
109
|
toAccount: () => Promise<LocalAccount>
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
// The browser/native key vault is a single physical slot shared by SDK
|
|
113
|
+
// instances, so key transitions must be serialized across projects.
|
|
114
|
+
let keyTransitionTail = Promise.resolve()
|
|
115
|
+
|
|
110
116
|
export async function createZeroDevWalletCore(
|
|
111
117
|
config: ZeroDevWalletConfigCore,
|
|
112
118
|
): Promise<ZeroDevWalletSDK> {
|
|
@@ -137,12 +143,119 @@ export async function createZeroDevWalletCore(
|
|
|
137
143
|
fallback: DEFAULT_ORGANIZATION_ID,
|
|
138
144
|
})
|
|
139
145
|
|
|
146
|
+
let preparedOAuthPublicKey: string | undefined
|
|
147
|
+
|
|
148
|
+
const prepareFreshKeyRotation = async () => {
|
|
149
|
+
if (preparedOAuthPublicKey) {
|
|
150
|
+
await client.apiKeyStamper.discardKeyRotation()
|
|
151
|
+
preparedOAuthPublicKey = undefined
|
|
152
|
+
}
|
|
153
|
+
return client.apiKeyStamper.prepareKeyRotation()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const withKeyTransition = async <T>(transition: () => Promise<T>) => {
|
|
157
|
+
const result = keyTransitionTail.then(transition)
|
|
158
|
+
keyTransitionTail = result.then(
|
|
159
|
+
() => undefined,
|
|
160
|
+
() => undefined,
|
|
161
|
+
)
|
|
162
|
+
return result
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const normalizeKey = (key: string) => key.replace(/^0x/, '').toLowerCase()
|
|
166
|
+
|
|
167
|
+
const createReplacementSession = (
|
|
168
|
+
token: string,
|
|
169
|
+
expectedPublicKey: string,
|
|
170
|
+
idPrefix: 'indexedDb' | 'oauth' | 'otp',
|
|
171
|
+
sessionType?: SessionType,
|
|
172
|
+
): ZeroDevWalletSession => {
|
|
173
|
+
const parsed = parseSession(token)
|
|
174
|
+
if (
|
|
175
|
+
!parsed.publicKey ||
|
|
176
|
+
normalizeKey(parsed.publicKey) !== normalizeKey(expectedPublicKey)
|
|
177
|
+
) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
'Session public key does not match the locally prepared signing key',
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
const now = Date.now()
|
|
183
|
+
return {
|
|
184
|
+
id: `session_${idPrefix}_${now}`,
|
|
185
|
+
userId: parsed.userId,
|
|
186
|
+
organizationId: parsed.organizationId,
|
|
187
|
+
stamperType: 'apiKey',
|
|
188
|
+
sessionType: sessionType ?? parsed.sessionType ?? SessionType.READ_WRITE,
|
|
189
|
+
token,
|
|
190
|
+
publicKey: expectedPublicKey,
|
|
191
|
+
expiry: parsed.expiry,
|
|
192
|
+
createdAt: now,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const commitReplacementSession = async (
|
|
197
|
+
session: ZeroDevWalletSession,
|
|
198
|
+
targetPublicKey: string,
|
|
199
|
+
) => {
|
|
200
|
+
await sessionStorageManager.stageSessionTransition(session, targetPublicKey)
|
|
201
|
+
try {
|
|
202
|
+
await client.apiKeyStamper.commitKeyRotation()
|
|
203
|
+
} catch (error) {
|
|
204
|
+
await client.apiKeyStamper.discardKeyRotation()
|
|
205
|
+
const activePublicKey = await client.apiKeyStamper.getPublicKey()
|
|
206
|
+
const recovered =
|
|
207
|
+
await sessionStorageManager.recoverSessionTransition(activePublicKey)
|
|
208
|
+
if (
|
|
209
|
+
!activePublicKey ||
|
|
210
|
+
normalizeKey(activePublicKey) !== normalizeKey(targetPublicKey) ||
|
|
211
|
+
!recovered
|
|
212
|
+
) {
|
|
213
|
+
throw error
|
|
214
|
+
}
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const committed = await sessionStorageManager.commitSessionTransition()
|
|
220
|
+
if (!committed) {
|
|
221
|
+
throw new Error('Session transition journal disappeared before commit')
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
const recovered = await sessionStorageManager.recoverSessionTransition(
|
|
225
|
+
await client.apiKeyStamper.getPublicKey(),
|
|
226
|
+
)
|
|
227
|
+
if (!recovered) throw error
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
await withKeyTransition(async () => {
|
|
232
|
+
const activePublicKey = await client.apiKeyStamper.getPublicKey()
|
|
233
|
+
await sessionStorageManager.recoverSessionTransition(activePublicKey)
|
|
234
|
+
const restoredSession = await sessionStorageManager.getActiveSession()
|
|
235
|
+
if (restoredSession) {
|
|
236
|
+
try {
|
|
237
|
+
const restoredPublicKey = parseSession(restoredSession.token).publicKey
|
|
238
|
+
if (
|
|
239
|
+
!activePublicKey ||
|
|
240
|
+
!restoredPublicKey ||
|
|
241
|
+
normalizeKey(activePublicKey) !== normalizeKey(restoredPublicKey)
|
|
242
|
+
) {
|
|
243
|
+
await sessionStorageManager.clearAllSessions()
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
await sessionStorageManager.clearAllSessions()
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
|
|
140
251
|
return {
|
|
141
252
|
client,
|
|
142
253
|
async getPublicKey() {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
254
|
+
return withKeyTransition(async () => {
|
|
255
|
+
if (preparedOAuthPublicKey) return preparedOAuthPublicKey
|
|
256
|
+
preparedOAuthPublicKey = await client.apiKeyStamper.prepareKeyRotation()
|
|
257
|
+
return preparedOAuthPublicKey
|
|
258
|
+
})
|
|
146
259
|
},
|
|
147
260
|
|
|
148
261
|
async getSession() {
|
|
@@ -159,87 +272,113 @@ export async function createZeroDevWalletCore(
|
|
|
159
272
|
},
|
|
160
273
|
|
|
161
274
|
async switchSession(sessionId: string) {
|
|
162
|
-
await sessionStorageManager.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
275
|
+
const activeSessionKey = await sessionStorageManager.getActiveSessionKey()
|
|
276
|
+
if (sessionId !== activeSessionKey) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
'Session switching is not supported with a single active key vault',
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
return sessionStorageManager.getActiveSession()
|
|
166
282
|
},
|
|
167
283
|
|
|
168
284
|
async clearSession(sessionId: string) {
|
|
285
|
+
if ((await sessionStorageManager.getActiveSessionKey()) === sessionId) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
'Refusing to clear the active session locally; use logout() to revoke its remote key first.',
|
|
288
|
+
)
|
|
289
|
+
}
|
|
169
290
|
await sessionStorageManager.clearSession(sessionId)
|
|
170
291
|
},
|
|
171
292
|
|
|
172
293
|
async clearAllSessions() {
|
|
294
|
+
if (await sessionStorageManager.getActiveSession()) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
'Refusing to clear the active session locally; use logout() to revoke its remote key first.',
|
|
297
|
+
)
|
|
298
|
+
}
|
|
173
299
|
await sessionStorageManager.clearAllSessions()
|
|
174
300
|
},
|
|
175
301
|
|
|
176
302
|
async refreshSession(sessionId?: string) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const compressedPublicKeyHex =
|
|
185
|
-
await client.apiKeyStamper.prepareKeyRotation()
|
|
186
|
-
const data = await client.loginWithStamp({
|
|
187
|
-
targetPublicKey: compressedPublicKeyHex,
|
|
188
|
-
projectId,
|
|
189
|
-
// Stamp-login is signed against the Turnkey parent org; the backend
|
|
190
|
-
// resolves the sub-org from the stamped credential. Signing the
|
|
191
|
-
// sub-org here makes the relayed payload's org mismatch the
|
|
192
|
-
// signature → Turnkey SIGNATURE_INVALID.
|
|
193
|
-
organizationId: await resolveOrganizationId(),
|
|
194
|
-
stampWith: 'apiKey',
|
|
195
|
-
})
|
|
196
|
-
await client.apiKeyStamper.commitKeyRotation()
|
|
197
|
-
const parsedSession = parseSession(data.session)
|
|
198
|
-
const session: ZeroDevWalletSession = {
|
|
199
|
-
id: `session_indexedDb_${Date.now()}`,
|
|
200
|
-
userId: parsedSession.userId,
|
|
201
|
-
organizationId: parsedSession.organizationId,
|
|
202
|
-
stamperType: 'apiKey',
|
|
203
|
-
sessionType: SessionType.READ_WRITE,
|
|
204
|
-
token: data.session,
|
|
205
|
-
expiry: parsedSession.expiry,
|
|
206
|
-
createdAt: Date.now(),
|
|
303
|
+
return withKeyTransition(async () => {
|
|
304
|
+
const activeSessionKey =
|
|
305
|
+
await sessionStorageManager.getActiveSessionKey()
|
|
306
|
+
if (sessionId && sessionId !== activeSessionKey) {
|
|
307
|
+
throw new Error(
|
|
308
|
+
'Refreshing an inactive session is not supported with a single active key vault',
|
|
309
|
+
)
|
|
207
310
|
}
|
|
208
|
-
await sessionStorageManager.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
311
|
+
const activeSession = await sessionStorageManager.getActiveSession()
|
|
312
|
+
if (!activeSession) {
|
|
313
|
+
throw new Error('No active session')
|
|
314
|
+
}
|
|
315
|
+
if (activeSession.stamperType !== 'apiKey') {
|
|
316
|
+
throw new Error('Invalid session type')
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const targetPublicKey = await prepareFreshKeyRotation()
|
|
320
|
+
try {
|
|
321
|
+
const data = await client.loginWithStamp({
|
|
322
|
+
targetPublicKey,
|
|
323
|
+
projectId,
|
|
324
|
+
// Stamp-login is signed against the Turnkey parent org; the backend
|
|
325
|
+
// resolves the sub-org from the stamped credential. Signing the
|
|
326
|
+
// sub-org here makes the relayed payload's org mismatch the
|
|
327
|
+
// signature → Turnkey SIGNATURE_INVALID.
|
|
328
|
+
organizationId: await resolveOrganizationId(),
|
|
329
|
+
stampWith: 'apiKey',
|
|
330
|
+
})
|
|
331
|
+
const session = createReplacementSession(
|
|
332
|
+
data.session,
|
|
333
|
+
targetPublicKey,
|
|
334
|
+
'indexedDb',
|
|
335
|
+
SessionType.READ_WRITE,
|
|
336
|
+
)
|
|
337
|
+
await commitReplacementSession(session, targetPublicKey)
|
|
338
|
+
return session
|
|
339
|
+
} catch (error) {
|
|
340
|
+
await client.apiKeyStamper.discardKeyRotation()
|
|
341
|
+
throw error
|
|
342
|
+
}
|
|
343
|
+
})
|
|
213
344
|
},
|
|
214
345
|
|
|
215
346
|
// [TODO] refactor to smaller utils/actions
|
|
216
347
|
async auth(params: AuthParams) {
|
|
217
348
|
switch (params.type) {
|
|
218
349
|
case 'oauth': {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
350
|
+
return withKeyTransition(async () => {
|
|
351
|
+
const targetPublicKey =
|
|
352
|
+
preparedOAuthPublicKey ??
|
|
353
|
+
(await client.apiKeyStamper.prepareKeyRotation())
|
|
354
|
+
preparedOAuthPublicKey = undefined
|
|
355
|
+
try {
|
|
356
|
+
const popSignature = await client.apiKeyStamper.signPending(
|
|
357
|
+
params.sessionId,
|
|
358
|
+
)
|
|
359
|
+
const data = await client.authenticateWithOAuth({
|
|
360
|
+
provider: params.provider,
|
|
361
|
+
projectId,
|
|
362
|
+
sessionId: params.sessionId,
|
|
363
|
+
popSignature,
|
|
364
|
+
})
|
|
226
365
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
366
|
+
if (!data.session) {
|
|
367
|
+
await client.apiKeyStamper.discardKeyRotation()
|
|
368
|
+
return data
|
|
369
|
+
}
|
|
370
|
+
const session = createReplacementSession(
|
|
371
|
+
data.session,
|
|
372
|
+
targetPublicKey,
|
|
373
|
+
'oauth',
|
|
374
|
+
)
|
|
375
|
+
await commitReplacementSession(session, targetPublicKey)
|
|
376
|
+
return data
|
|
377
|
+
} catch (error) {
|
|
378
|
+
await client.apiKeyStamper.discardKeyRotation()
|
|
379
|
+
throw error
|
|
239
380
|
}
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
return data
|
|
381
|
+
})
|
|
243
382
|
}
|
|
244
383
|
case 'passkey': {
|
|
245
384
|
const { type } = params
|
|
@@ -248,83 +387,80 @@ export async function createZeroDevWalletCore(
|
|
|
248
387
|
'mode' in params &&
|
|
249
388
|
params.mode === 'register'
|
|
250
389
|
) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
390
|
+
return withKeyTransition(async () => {
|
|
391
|
+
if (await sessionStorageManager.getActiveSession()) {
|
|
392
|
+
throw new Error(
|
|
393
|
+
'Passkey registration requires an unauthenticated wallet; logout before registering a new wallet.',
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
const registrationPublicKey = await prepareFreshKeyRotation()
|
|
397
|
+
try {
|
|
398
|
+
const name = `ZeroDevWallet-${humanReadableDateTime()}`
|
|
399
|
+
const { attestation, encodedChallenge } =
|
|
400
|
+
await passkeyStamper.register({
|
|
401
|
+
rp: { id: rpId, name: 'ZeroDev Wallet' },
|
|
402
|
+
userName: name,
|
|
403
|
+
})
|
|
404
|
+
const data = await client.registerWithPasskey({
|
|
405
|
+
attestation,
|
|
406
|
+
challenge: encodedChallenge,
|
|
407
|
+
projectId,
|
|
408
|
+
encodedPublicKey: registrationPublicKey,
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
// Registration proves the temporary key exists remotely. Make
|
|
412
|
+
// it active so it can authorize the durable replacement key.
|
|
413
|
+
await client.apiKeyStamper.commitKeyRotation()
|
|
414
|
+
const targetPublicKey = await prepareFreshKeyRotation()
|
|
415
|
+
const loginData = await client.loginWithStamp({
|
|
416
|
+
projectId,
|
|
417
|
+
targetPublicKey,
|
|
418
|
+
// Sign against the parent org (see refreshSession note) — the
|
|
419
|
+
// backend derives the sub-org from the stamped credential.
|
|
420
|
+
organizationId: await resolveOrganizationId(),
|
|
421
|
+
})
|
|
422
|
+
const session = createReplacementSession(
|
|
423
|
+
loginData.session,
|
|
424
|
+
targetPublicKey,
|
|
425
|
+
'indexedDb',
|
|
426
|
+
)
|
|
427
|
+
await commitReplacementSession(session, targetPublicKey)
|
|
428
|
+
return data
|
|
429
|
+
} catch (error) {
|
|
430
|
+
await client.apiKeyStamper.discardKeyRotation()
|
|
431
|
+
throw error
|
|
432
|
+
}
|
|
276
433
|
})
|
|
277
|
-
await client.apiKeyStamper.commitKeyRotation()
|
|
278
|
-
const parsedSession = parseSession(loginData.session)
|
|
279
|
-
const session: ZeroDevWalletSession = {
|
|
280
|
-
id: `session_indexedDb_${Date.now()}`,
|
|
281
|
-
stamperType: 'apiKey',
|
|
282
|
-
createdAt: Date.now(),
|
|
283
|
-
sessionType: SessionType.READ_WRITE,
|
|
284
|
-
userId: parsedSession.userId,
|
|
285
|
-
organizationId: parsedSession.organizationId,
|
|
286
|
-
expiry:
|
|
287
|
-
Date.now() +
|
|
288
|
-
Number(DEFAULT_SESSION_EXPIRATION_IN_SECONDS) * 1000,
|
|
289
|
-
token: loginData.session,
|
|
290
|
-
}
|
|
291
|
-
await sessionStorageManager.storeSession(session, session.id)
|
|
292
|
-
return data
|
|
293
434
|
}
|
|
294
435
|
if (
|
|
295
436
|
type === 'passkey' &&
|
|
296
437
|
'mode' in params &&
|
|
297
438
|
params.mode === 'login'
|
|
298
439
|
) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
440
|
+
return withKeyTransition(async () => {
|
|
441
|
+
const targetPublicKey = await prepareFreshKeyRotation()
|
|
442
|
+
try {
|
|
443
|
+
const loginData = await client.loginWithStamp({
|
|
444
|
+
targetPublicKey,
|
|
445
|
+
projectId,
|
|
446
|
+
// Sign against the parent org, not the user's sub-org (see the
|
|
447
|
+
// refreshSession note). The backend derives the sub-org from the
|
|
448
|
+
// stamped passkey credential.
|
|
449
|
+
organizationId: await resolveOrganizationId(),
|
|
450
|
+
stampWith: 'passkey',
|
|
451
|
+
})
|
|
452
|
+
const session = createReplacementSession(
|
|
453
|
+
loginData.session,
|
|
454
|
+
targetPublicKey,
|
|
455
|
+
'indexedDb',
|
|
456
|
+
)
|
|
457
|
+
await commitReplacementSession(session, targetPublicKey)
|
|
458
|
+
return loginData
|
|
459
|
+
} catch (error) {
|
|
460
|
+
await client.apiKeyStamper.discardKeyRotation()
|
|
461
|
+
throw error
|
|
462
|
+
}
|
|
312
463
|
})
|
|
313
|
-
const parsedSession = parseSession(loginData.session)
|
|
314
|
-
const session: ZeroDevWalletSession = {
|
|
315
|
-
id: `session_indexedDb_${Date.now()}`,
|
|
316
|
-
stamperType: 'apiKey',
|
|
317
|
-
createdAt: Date.now(),
|
|
318
|
-
sessionType: SessionType.READ_WRITE,
|
|
319
|
-
userId: parsedSession.userId,
|
|
320
|
-
organizationId: parsedSession.organizationId,
|
|
321
|
-
expiry:
|
|
322
|
-
Date.now() +
|
|
323
|
-
Number(DEFAULT_SESSION_EXPIRATION_IN_SECONDS) * 1000,
|
|
324
|
-
token: loginData.session,
|
|
325
|
-
}
|
|
326
|
-
await sessionStorageManager.storeSession(session, session.id)
|
|
327
|
-
return loginData
|
|
328
464
|
}
|
|
329
465
|
throw new Error('Passkey authentication requires passkey parameter')
|
|
330
466
|
}
|
|
@@ -369,69 +505,64 @@ export async function createZeroDevWalletCore(
|
|
|
369
505
|
}
|
|
370
506
|
|
|
371
507
|
if (otpParams.mode === 'verifyOtp') {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
token: data.session,
|
|
429
|
-
expiry: parsedSession.expiry,
|
|
430
|
-
createdAt: Date.now(),
|
|
508
|
+
return withKeyTransition(async () => {
|
|
509
|
+
const { otpId, otpCode, otpEncryptionTargetBundle } = otpParams
|
|
510
|
+
const targetPublicKey = await prepareFreshKeyRotation()
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
// HPKE-seal the OTP attempt to the enclave's per-session target
|
|
514
|
+
// key. The auth proxy never sees the plaintext OTP code.
|
|
515
|
+
const encryptedOtpBundle = await encryptOtpAttempt({
|
|
516
|
+
otpCode,
|
|
517
|
+
publicKey: targetPublicKey,
|
|
518
|
+
encryptionTargetBundle: otpEncryptionTargetBundle,
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
if (!cachedAuthProxyConfigId) {
|
|
522
|
+
const { authProxyConfigId } =
|
|
523
|
+
await client.getAuthProxyConfigId()
|
|
524
|
+
cachedAuthProxyConfigId = authProxyConfigId
|
|
525
|
+
}
|
|
526
|
+
const authProxyClient = createAuthProxyClient({
|
|
527
|
+
authProxyConfigId: cachedAuthProxyConfigId,
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
const { verificationToken } = await authProxyClient.verifyOtp({
|
|
531
|
+
otpId,
|
|
532
|
+
encryptedOtpBundle,
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
const clientSignature = await buildClientSignature({
|
|
536
|
+
verificationToken,
|
|
537
|
+
publicKey: targetPublicKey,
|
|
538
|
+
stamper: {
|
|
539
|
+
stamp: (payload) =>
|
|
540
|
+
client.apiKeyStamper.stampPending(payload),
|
|
541
|
+
},
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
const data = await client.loginWithOTP({
|
|
545
|
+
verificationToken,
|
|
546
|
+
clientSignature,
|
|
547
|
+
projectId,
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
if (!data.session) {
|
|
551
|
+
await client.apiKeyStamper.discardKeyRotation()
|
|
552
|
+
return data
|
|
553
|
+
}
|
|
554
|
+
const session = createReplacementSession(
|
|
555
|
+
data.session,
|
|
556
|
+
targetPublicKey,
|
|
557
|
+
'otp',
|
|
558
|
+
)
|
|
559
|
+
await commitReplacementSession(session, targetPublicKey)
|
|
560
|
+
return data
|
|
561
|
+
} catch (error) {
|
|
562
|
+
await client.apiKeyStamper.discardKeyRotation()
|
|
563
|
+
throw error
|
|
431
564
|
}
|
|
432
|
-
|
|
433
|
-
}
|
|
434
|
-
return data
|
|
565
|
+
})
|
|
435
566
|
}
|
|
436
567
|
|
|
437
568
|
throw new Error('OTP authentication requires mode parameter')
|
|
@@ -441,10 +572,67 @@ export async function createZeroDevWalletCore(
|
|
|
441
572
|
}
|
|
442
573
|
},
|
|
443
574
|
|
|
444
|
-
async logout() {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
575
|
+
async logout(options) {
|
|
576
|
+
return withKeyTransition(async () => {
|
|
577
|
+
const session = await sessionStorageManager.getActiveSession()
|
|
578
|
+
if (session) {
|
|
579
|
+
const publicKey = await client.apiKeyStamper.getPublicKey()
|
|
580
|
+
if (publicKey) {
|
|
581
|
+
try {
|
|
582
|
+
const authenticators = await client.getAuthenticators({
|
|
583
|
+
subOrganizationId: session.organizationId,
|
|
584
|
+
projectId,
|
|
585
|
+
token: session.token,
|
|
586
|
+
})
|
|
587
|
+
const sessionKey = authenticators.sessionKeys?.find((key) => {
|
|
588
|
+
const candidate = key.ApiKey ?? key.apiKey
|
|
589
|
+
return (
|
|
590
|
+
candidate !== undefined &&
|
|
591
|
+
normalizeKey(candidate) === normalizeKey(publicKey)
|
|
592
|
+
)
|
|
593
|
+
})
|
|
594
|
+
const apiKeyId = sessionKey?.TurnkeyId ?? sessionKey?.turnkeyId
|
|
595
|
+
if (!apiKeyId) {
|
|
596
|
+
throw new Error(
|
|
597
|
+
'Current session key was not found in the authenticated key list; refusing to erase local credentials without confirmed remote revocation.',
|
|
598
|
+
)
|
|
599
|
+
}
|
|
600
|
+
await client.logout({
|
|
601
|
+
projectId,
|
|
602
|
+
organizationId: session.organizationId,
|
|
603
|
+
userId: session.userId,
|
|
604
|
+
apiKeyId,
|
|
605
|
+
})
|
|
606
|
+
} catch (error) {
|
|
607
|
+
// Only a confirmed rejection (401) or an explicit `force` clears
|
|
608
|
+
// local credentials below. Any other failure (403, 5xx, network)
|
|
609
|
+
// is ambiguous — erasing the only key on an inconclusive signal
|
|
610
|
+
// could lock the user out unrecoverably — so preserve it and
|
|
611
|
+
// return without throwing, so callers (e.g. a logout button)
|
|
612
|
+
// don't crash. Pass `logout({ force: true })` to clear locally
|
|
613
|
+
// regardless when the caller knows the remote is gone.
|
|
614
|
+
const credentialIsRejected =
|
|
615
|
+
error instanceof RestRequestError && error.status === 401
|
|
616
|
+
if (!options?.force && !credentialIsRejected) return false
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
preparedOAuthPublicKey = undefined
|
|
622
|
+
let cleanupError: unknown
|
|
623
|
+
try {
|
|
624
|
+
await client.apiKeyStamper.clear()
|
|
625
|
+
} catch (error) {
|
|
626
|
+
cleanupError = error
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
await sessionStorageManager.clearAllSessions()
|
|
630
|
+
} catch (error) {
|
|
631
|
+
cleanupError ??= error
|
|
632
|
+
}
|
|
633
|
+
if (cleanupError) throw cleanupError
|
|
634
|
+
return true
|
|
635
|
+
})
|
|
448
636
|
},
|
|
449
637
|
|
|
450
638
|
async toAccount(): Promise<LocalAccount> {
|