@zerodev/wallet-core 0.0.3 → 0.0.4
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/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/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/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/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
package/src/index.native.ts
CHANGED
|
@@ -27,11 +27,14 @@ export type {
|
|
|
27
27
|
GetWhoamiReturnType,
|
|
28
28
|
LoginWithOTPParameters,
|
|
29
29
|
LoginWithOTPReturnType,
|
|
30
|
+
LogoutParameters,
|
|
31
|
+
LogoutReturnType,
|
|
30
32
|
OAuthAuthenticator,
|
|
31
33
|
OtpContact,
|
|
32
34
|
PasskeyAuthenticator,
|
|
33
35
|
RegisterWithOTPParameters,
|
|
34
36
|
RegisterWithOTPReturnType,
|
|
37
|
+
SessionKeyAuthenticator,
|
|
35
38
|
Sign7702AuthorizationParameters,
|
|
36
39
|
Sign7702AuthorizationReturnType,
|
|
37
40
|
SignMessageParameters,
|
|
@@ -50,6 +53,7 @@ export {
|
|
|
50
53
|
getUserWallet,
|
|
51
54
|
getWhoami,
|
|
52
55
|
loginWithOTP,
|
|
56
|
+
logout,
|
|
53
57
|
registerWithOTP,
|
|
54
58
|
sign7702Authorization,
|
|
55
59
|
signMessage,
|
package/src/index.ts
CHANGED
|
@@ -24,11 +24,14 @@ export type {
|
|
|
24
24
|
GetWhoamiReturnType,
|
|
25
25
|
LoginWithOTPParameters,
|
|
26
26
|
LoginWithOTPReturnType,
|
|
27
|
+
LogoutParameters,
|
|
28
|
+
LogoutReturnType,
|
|
27
29
|
OAuthAuthenticator,
|
|
28
30
|
OtpContact,
|
|
29
31
|
PasskeyAuthenticator,
|
|
30
32
|
RegisterWithOTPParameters,
|
|
31
33
|
RegisterWithOTPReturnType,
|
|
34
|
+
SessionKeyAuthenticator,
|
|
32
35
|
Sign7702AuthorizationParameters,
|
|
33
36
|
Sign7702AuthorizationReturnType,
|
|
34
37
|
SignMessageParameters,
|
|
@@ -51,6 +54,7 @@ export {
|
|
|
51
54
|
getUserWallet,
|
|
52
55
|
getWhoami,
|
|
53
56
|
loginWithOTP,
|
|
57
|
+
logout,
|
|
54
58
|
registerWithOTP,
|
|
55
59
|
sign7702Authorization,
|
|
56
60
|
signMessage,
|
|
@@ -1,22 +1,78 @@
|
|
|
1
1
|
import { ApiKeyStamper, SignatureFormat } from '@turnkey/api-key-stamper'
|
|
2
|
-
import { generateP256KeyPair } from '@turnkey/crypto'
|
|
2
|
+
import { generateP256KeyPair, verifyStampSignature } from '@turnkey/crypto'
|
|
3
3
|
import * as SecureStore from 'expo-secure-store'
|
|
4
4
|
import type { ApiKeyStamper as ZDApiKeyStamper } from '../../stampers/types.js'
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
const
|
|
6
|
+
const KEY_PAIR = 'zerodev.keyPair'
|
|
7
|
+
const LEGACY_PUBLIC_KEY = 'zerodev.publicKey'
|
|
8
|
+
const LEGACY_PRIVATE_KEY = 'zerodev.privateKey'
|
|
9
|
+
|
|
10
|
+
type StoredKeyPair = { publicKey: string; privateKey: string }
|
|
11
|
+
const KEY_PAIR_CHECK = 'ZeroDev Wallet key-pair ownership check'
|
|
12
|
+
|
|
13
|
+
function parseKeyPair(value: string | null): StoredKeyPair | null {
|
|
14
|
+
if (!value) return null
|
|
15
|
+
try {
|
|
16
|
+
const pair = JSON.parse(value) as Partial<StoredKeyPair>
|
|
17
|
+
if (
|
|
18
|
+
typeof pair.publicKey !== 'string' ||
|
|
19
|
+
!pair.publicKey ||
|
|
20
|
+
typeof pair.privateKey !== 'string' ||
|
|
21
|
+
!pair.privateKey
|
|
22
|
+
) {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
return pair as StoredKeyPair
|
|
26
|
+
} catch {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
}
|
|
8
30
|
|
|
9
31
|
class SecureStoreStamperInner {
|
|
10
32
|
private publicKeyHex: string | null = null
|
|
11
33
|
|
|
12
34
|
async init(): Promise<void> {
|
|
13
|
-
const
|
|
14
|
-
|
|
35
|
+
const storedPair = parseKeyPair(await SecureStore.getItemAsync(KEY_PAIR))
|
|
36
|
+
if (storedPair && (await this.isUsable(storedPair))) {
|
|
37
|
+
this.publicKeyHex = storedPair.publicKey
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const publicKey = await SecureStore.getItemAsync(LEGACY_PUBLIC_KEY)
|
|
42
|
+
const privateKey = await SecureStore.getItemAsync(LEGACY_PRIVATE_KEY)
|
|
43
|
+
if (
|
|
44
|
+
publicKey &&
|
|
45
|
+
privateKey &&
|
|
46
|
+
(await this.isUsable({ publicKey, privateKey }))
|
|
47
|
+
) {
|
|
48
|
+
await this.persistKeyPair({ publicKey, privateKey })
|
|
49
|
+
await Promise.all([
|
|
50
|
+
SecureStore.deleteItemAsync(LEGACY_PUBLIC_KEY),
|
|
51
|
+
SecureStore.deleteItemAsync(LEGACY_PRIVATE_KEY),
|
|
52
|
+
])
|
|
53
|
+
return
|
|
54
|
+
}
|
|
15
55
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
56
|
+
// No usable stored credential — missing, corrupt, or mismatched (e.g. a
|
|
57
|
+
// broken key written before this validation existed). Mint a fresh pair
|
|
58
|
+
// rather than throwing: a key that fails the ownership check can't produce
|
|
59
|
+
// a valid stamp anyway, and a thrown init would brick the app — it could
|
|
60
|
+
// neither log out nor reset the stamper (both need a constructed SDK). The
|
|
61
|
+
// user simply re-authenticates from a clean state. Only validation failures
|
|
62
|
+
// land here; SecureStore I/O errors still propagate (we never discard a key
|
|
63
|
+
// just because a read/write transiently failed).
|
|
64
|
+
await this.resetKeyPair()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// True if the stored pair passes the ownership check. Isolates validation
|
|
68
|
+
// (pure crypto) from I/O so init() can distinguish an unusable key (reset)
|
|
69
|
+
// from a transient SecureStore failure (propagate).
|
|
70
|
+
private async isUsable(pair: StoredKeyPair): Promise<boolean> {
|
|
71
|
+
try {
|
|
72
|
+
await this.validateKeyPair(pair)
|
|
73
|
+
return true
|
|
74
|
+
} catch {
|
|
75
|
+
return false
|
|
20
76
|
}
|
|
21
77
|
}
|
|
22
78
|
|
|
@@ -24,20 +80,36 @@ class SecureStoreStamperInner {
|
|
|
24
80
|
return this.publicKeyHex
|
|
25
81
|
}
|
|
26
82
|
|
|
27
|
-
async resetKeyPair(externalKeyPair?: {
|
|
28
|
-
publicKey: string
|
|
29
|
-
privateKey: string
|
|
30
|
-
}): Promise<void> {
|
|
31
|
-
await this.clear()
|
|
32
|
-
|
|
83
|
+
async resetKeyPair(externalKeyPair?: StoredKeyPair): Promise<void> {
|
|
33
84
|
const pair = externalKeyPair ?? generateP256KeyPair()
|
|
85
|
+
await this.persistKeyPair(pair)
|
|
86
|
+
}
|
|
34
87
|
|
|
35
|
-
|
|
36
|
-
await
|
|
37
|
-
|
|
88
|
+
private async persistKeyPair(pair: StoredKeyPair): Promise<void> {
|
|
89
|
+
await this.validateKeyPair(pair)
|
|
90
|
+
// SecureStore replaces one value atomically. Update memory only after the
|
|
91
|
+
// durable write succeeds, so a failed rotation preserves the live pair.
|
|
92
|
+
await SecureStore.setItemAsync(KEY_PAIR, JSON.stringify(pair))
|
|
38
93
|
this.publicKeyHex = pair.publicKey
|
|
39
94
|
}
|
|
40
95
|
|
|
96
|
+
private async validateKeyPair(pair: StoredKeyPair): Promise<void> {
|
|
97
|
+
try {
|
|
98
|
+
const signature = await new ApiKeyStamper({
|
|
99
|
+
apiPublicKey: pair.publicKey,
|
|
100
|
+
apiPrivateKey: pair.privateKey,
|
|
101
|
+
}).sign(KEY_PAIR_CHECK, SignatureFormat.Der)
|
|
102
|
+
if (
|
|
103
|
+
await verifyStampSignature(pair.publicKey, signature, KEY_PAIR_CHECK)
|
|
104
|
+
) {
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// Normalize invalid encoding and mismatched-key failures below.
|
|
109
|
+
}
|
|
110
|
+
throw new Error('Stored public and private keys do not match.')
|
|
111
|
+
}
|
|
112
|
+
|
|
41
113
|
private async getTurnkeyApiKeyStamper(): Promise<ApiKeyStamper> {
|
|
42
114
|
if (!this.publicKeyHex) {
|
|
43
115
|
throw new Error(
|
|
@@ -45,14 +117,14 @@ class SecureStoreStamperInner {
|
|
|
45
117
|
)
|
|
46
118
|
}
|
|
47
119
|
|
|
48
|
-
const
|
|
49
|
-
if (!
|
|
50
|
-
throw new Error('No
|
|
120
|
+
const pair = parseKeyPair(await SecureStore.getItemAsync(KEY_PAIR))
|
|
121
|
+
if (!pair || pair.publicKey !== this.publicKeyHex) {
|
|
122
|
+
throw new Error('No matching key pair found in secure store.')
|
|
51
123
|
}
|
|
52
124
|
|
|
53
125
|
return new ApiKeyStamper({
|
|
54
126
|
apiPublicKey: this.publicKeyHex,
|
|
55
|
-
apiPrivateKey: privateKey,
|
|
127
|
+
apiPrivateKey: pair.privateKey,
|
|
56
128
|
})
|
|
57
129
|
}
|
|
58
130
|
|
|
@@ -70,8 +142,13 @@ class SecureStoreStamperInner {
|
|
|
70
142
|
}
|
|
71
143
|
|
|
72
144
|
async clear(): Promise<void> {
|
|
73
|
-
|
|
74
|
-
|
|
145
|
+
// Remove legacy recovery sources first. If this fails, keep the atomic
|
|
146
|
+
// record authoritative so a later init cannot resurrect a revoked key.
|
|
147
|
+
await Promise.all([
|
|
148
|
+
SecureStore.deleteItemAsync(LEGACY_PUBLIC_KEY),
|
|
149
|
+
SecureStore.deleteItemAsync(LEGACY_PRIVATE_KEY),
|
|
150
|
+
])
|
|
151
|
+
await SecureStore.deleteItemAsync(KEY_PAIR)
|
|
75
152
|
this.publicKeyHex = null
|
|
76
153
|
}
|
|
77
154
|
}
|
|
@@ -117,6 +194,7 @@ export async function createSecureStoreStamper(): Promise<ZDApiKeyStamper> {
|
|
|
117
194
|
return inner.sign(payload)
|
|
118
195
|
},
|
|
119
196
|
async clear() {
|
|
197
|
+
pendingKeyPair = null
|
|
120
198
|
await inner.clear()
|
|
121
199
|
},
|
|
122
200
|
async resetKeyPair() {
|
|
@@ -128,6 +206,20 @@ export async function createSecureStoreStamper(): Promise<ZDApiKeyStamper> {
|
|
|
128
206
|
pendingKeyPair = keyPair
|
|
129
207
|
return keyPair.publicKey
|
|
130
208
|
},
|
|
209
|
+
async stampPending(payload: string) {
|
|
210
|
+
if (!pendingKeyPair) throw new Error('No pending key rotation')
|
|
211
|
+
return new ApiKeyStamper({
|
|
212
|
+
apiPublicKey: pendingKeyPair.publicKey,
|
|
213
|
+
apiPrivateKey: pendingKeyPair.privateKey,
|
|
214
|
+
}).stamp(payload)
|
|
215
|
+
},
|
|
216
|
+
async signPending(payload: string) {
|
|
217
|
+
if (!pendingKeyPair) throw new Error('No pending key rotation')
|
|
218
|
+
return new ApiKeyStamper({
|
|
219
|
+
apiPublicKey: pendingKeyPair.publicKey,
|
|
220
|
+
apiPrivateKey: pendingKeyPair.privateKey,
|
|
221
|
+
}).sign(payload, SignatureFormat.Der)
|
|
222
|
+
},
|
|
131
223
|
async commitKeyRotation() {
|
|
132
224
|
if (!pendingKeyPair) {
|
|
133
225
|
throw new Error('No pending key rotation to commit')
|
|
@@ -135,5 +227,8 @@ export async function createSecureStoreStamper(): Promise<ZDApiKeyStamper> {
|
|
|
135
227
|
await inner.resetKeyPair(pendingKeyPair)
|
|
136
228
|
pendingKeyPair = null
|
|
137
229
|
},
|
|
230
|
+
async discardKeyRotation() {
|
|
231
|
+
pendingKeyPair = null
|
|
232
|
+
},
|
|
138
233
|
}
|
|
139
234
|
}
|
|
@@ -1,13 +1,44 @@
|
|
|
1
|
+
import { p256 } from '@noble/curves/nist.js'
|
|
1
2
|
import { IndexedDbStamper as TurnkeyIndexedDbStamper } from '@turnkey/indexed-db-stamper'
|
|
2
3
|
import { generateCompressedPublicKeyFromKeyPair } from '../utils/utils.js'
|
|
3
4
|
import type { ApiKeyStamper } from './types.js'
|
|
4
5
|
|
|
6
|
+
async function signWithKeyPair(
|
|
7
|
+
keyPair: CryptoKeyPair,
|
|
8
|
+
payload: string,
|
|
9
|
+
): Promise<string> {
|
|
10
|
+
const rawSignature = new Uint8Array(
|
|
11
|
+
await crypto.subtle.sign(
|
|
12
|
+
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
13
|
+
keyPair.privateKey,
|
|
14
|
+
new TextEncoder().encode(payload),
|
|
15
|
+
),
|
|
16
|
+
)
|
|
17
|
+
return p256.Signature.fromBytes(rawSignature, 'compact').toHex('der')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function encodeStamp(publicKey: string, signature: string): string {
|
|
21
|
+
const json = JSON.stringify({
|
|
22
|
+
publicKey,
|
|
23
|
+
scheme: 'SIGNATURE_SCHEME_TK_API_P256',
|
|
24
|
+
signature,
|
|
25
|
+
})
|
|
26
|
+
return btoa(json).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
|
|
27
|
+
}
|
|
28
|
+
|
|
5
29
|
export async function createIndexedDbStamper(): Promise<ApiKeyStamper> {
|
|
6
30
|
const inner = new TurnkeyIndexedDbStamper()
|
|
7
31
|
await inner.init()
|
|
8
32
|
|
|
9
33
|
let pendingKeyPair: CryptoKeyPair | null = null
|
|
10
34
|
|
|
35
|
+
// A rotation must be prepared before it can be stamped/signed with; centralize
|
|
36
|
+
// the guard so the pending-key paths don't each repeat it.
|
|
37
|
+
const requirePending = (): CryptoKeyPair => {
|
|
38
|
+
if (!pendingKeyPair) throw new Error('No pending key rotation')
|
|
39
|
+
return pendingKeyPair
|
|
40
|
+
}
|
|
41
|
+
|
|
11
42
|
return {
|
|
12
43
|
async getPublicKey() {
|
|
13
44
|
return await inner.getPublicKey()
|
|
@@ -19,6 +50,7 @@ export async function createIndexedDbStamper(): Promise<ApiKeyStamper> {
|
|
|
19
50
|
return await inner.sign(payload)
|
|
20
51
|
},
|
|
21
52
|
async clear() {
|
|
53
|
+
pendingKeyPair = null
|
|
22
54
|
await inner.clear()
|
|
23
55
|
},
|
|
24
56
|
async resetKeyPair() {
|
|
@@ -34,6 +66,18 @@ export async function createIndexedDbStamper(): Promise<ApiKeyStamper> {
|
|
|
34
66
|
pendingKeyPair = keyPair
|
|
35
67
|
return await generateCompressedPublicKeyFromKeyPair(keyPair)
|
|
36
68
|
},
|
|
69
|
+
async stampPending(payload: string) {
|
|
70
|
+
const keyPair = requirePending()
|
|
71
|
+
const publicKey = await generateCompressedPublicKeyFromKeyPair(keyPair)
|
|
72
|
+
const signature = await signWithKeyPair(keyPair, payload)
|
|
73
|
+
return {
|
|
74
|
+
stampHeaderName: 'X-Stamp',
|
|
75
|
+
stampHeaderValue: encodeStamp(publicKey, signature),
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
async signPending(payload: string) {
|
|
79
|
+
return signWithKeyPair(requirePending(), payload)
|
|
80
|
+
},
|
|
37
81
|
async commitKeyRotation() {
|
|
38
82
|
if (!pendingKeyPair) {
|
|
39
83
|
throw new Error('No pending key rotation to commit')
|
|
@@ -41,5 +85,8 @@ export async function createIndexedDbStamper(): Promise<ApiKeyStamper> {
|
|
|
41
85
|
await inner.resetKeyPair(pendingKeyPair)
|
|
42
86
|
pendingKeyPair = null
|
|
43
87
|
},
|
|
88
|
+
async discardKeyRotation() {
|
|
89
|
+
pendingKeyPair = null
|
|
90
|
+
},
|
|
44
91
|
}
|
|
45
92
|
}
|
package/src/stampers/types.ts
CHANGED
|
@@ -37,8 +37,14 @@ export type ApiKeyStamper = Stamper & {
|
|
|
37
37
|
resetKeyPair: () => Promise<void>
|
|
38
38
|
/** Generate a new key pair internally, return its compressed public key, but keep the OLD key active for stamp(). */
|
|
39
39
|
prepareKeyRotation: () => Promise<string>
|
|
40
|
+
/** Stamp with the prepared key without making it active. */
|
|
41
|
+
stampPending: (payload: string) => Promise<Stamp>
|
|
42
|
+
/** Sign with the prepared key without making it active. */
|
|
43
|
+
signPending: (payload: string) => Promise<string>
|
|
40
44
|
/** Promote the pending key to active. Call after the server accepts the new key. */
|
|
41
45
|
commitKeyRotation: () => Promise<void>
|
|
46
|
+
/** Forget the prepared key while keeping the active key untouched. */
|
|
47
|
+
discardKeyRotation: () => Promise<void>
|
|
42
48
|
/**
|
|
43
49
|
* Sign `payload` with the currently active key. Returns a hex-encoded
|
|
44
50
|
* ECDSA-P256 / SHA-256 signature in ASN.1 DER form.
|
package/src/storage/manager.ts
CHANGED
|
@@ -20,31 +20,114 @@ export type StorageManager = {
|
|
|
20
20
|
setActiveSession(sessionKey: string): Promise<void>
|
|
21
21
|
clearSession(sessionKey: string): Promise<void>
|
|
22
22
|
clearAllSessions(): Promise<void>
|
|
23
|
+
stageSessionTransition(
|
|
24
|
+
sessionData: ZeroDevWalletSession,
|
|
25
|
+
publicKey: string,
|
|
26
|
+
): Promise<void>
|
|
27
|
+
commitSessionTransition(): Promise<ZeroDevWalletSession | undefined>
|
|
28
|
+
recoverSessionTransition(
|
|
29
|
+
activePublicKey: string | null,
|
|
30
|
+
): Promise<ZeroDevWalletSession | undefined>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type SessionTransition = {
|
|
34
|
+
sessionData: ZeroDevWalletSession
|
|
35
|
+
publicKey: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Process-wide serialization queue for storage mutations. Every mutating op
|
|
39
|
+
// runs through `withMutation`, which chains onto this tail so read-modify-write
|
|
40
|
+
// sequences over the shared active-session pointer and session list can't
|
|
41
|
+
// interleave and corrupt each other. Module-level (not per-manager) so
|
|
42
|
+
// concurrent managers over the same storage still serialize; the tail swallows
|
|
43
|
+
// each outcome so one failed mutation never jams the queue.
|
|
44
|
+
let mutationTail = Promise.resolve()
|
|
45
|
+
|
|
46
|
+
function isStoredSession(value: unknown): value is ZeroDevWalletSession {
|
|
47
|
+
if (!value || typeof value !== 'object') return false
|
|
48
|
+
const session = value as Partial<ZeroDevWalletSession>
|
|
49
|
+
return (
|
|
50
|
+
typeof session.id === 'string' &&
|
|
51
|
+
typeof session.userId === 'string' &&
|
|
52
|
+
typeof session.organizationId === 'string' &&
|
|
53
|
+
(session.stamperType === 'apiKey' || session.stamperType === 'passkey') &&
|
|
54
|
+
typeof session.token === 'string' &&
|
|
55
|
+
session.token.length > 0 &&
|
|
56
|
+
typeof session.expiry === 'number' &&
|
|
57
|
+
Number.isFinite(session.expiry) &&
|
|
58
|
+
session.expiry > 0 &&
|
|
59
|
+
typeof session.createdAt === 'number' &&
|
|
60
|
+
Number.isFinite(session.createdAt)
|
|
61
|
+
)
|
|
23
62
|
}
|
|
24
63
|
|
|
25
64
|
export function createStorageManager(adapter: StorageAdapter): StorageManager {
|
|
26
65
|
const ACTIVE_SESSION_KEY = '@zerodev/active_session'
|
|
27
66
|
const ALL_SESSIONS_KEY = '@zerodev/sessions'
|
|
67
|
+
const SESSION_TRANSITION_KEY = '@zerodev/session_transition'
|
|
28
68
|
|
|
29
|
-
const
|
|
69
|
+
const withMutation = async <T>(mutation: () => Promise<T>): Promise<T> => {
|
|
70
|
+
const result = mutationTail.then(mutation)
|
|
71
|
+
mutationTail = result.then(
|
|
72
|
+
() => undefined,
|
|
73
|
+
() => undefined,
|
|
74
|
+
)
|
|
75
|
+
return result
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const parseSessionKeys = (value: string | null): string[] | undefined => {
|
|
79
|
+
if (!value) return []
|
|
80
|
+
try {
|
|
81
|
+
const parsed: unknown = JSON.parse(value)
|
|
82
|
+
if (
|
|
83
|
+
!Array.isArray(parsed) ||
|
|
84
|
+
!parsed.every((key): key is string => typeof key === 'string')
|
|
85
|
+
) {
|
|
86
|
+
return undefined
|
|
87
|
+
}
|
|
88
|
+
return parsed
|
|
89
|
+
} catch {
|
|
90
|
+
return undefined
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const restoreItem = async (key: string, value: string | null) => {
|
|
95
|
+
if (value === null) await adapter.removeItem(key)
|
|
96
|
+
else await adapter.setItem(key, value)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const storeSessionUnlocked = async (
|
|
30
100
|
sessionData: ZeroDevWalletSession,
|
|
31
101
|
sessionKey: string,
|
|
32
102
|
): Promise<void> => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
103
|
+
const recordKey = sessionKey
|
|
104
|
+
const [previousRecord, previousIndex, previousActive] = await Promise.all([
|
|
105
|
+
adapter.getItem(recordKey),
|
|
106
|
+
adapter.getItem(ALL_SESSIONS_KEY),
|
|
107
|
+
adapter.getItem(ACTIVE_SESSION_KEY),
|
|
108
|
+
])
|
|
109
|
+
try {
|
|
110
|
+
await adapter.setItem(recordKey, JSON.stringify(sessionData))
|
|
111
|
+
const sessions = parseSessionKeys(previousIndex) ?? []
|
|
112
|
+
if (!sessions.includes(sessionKey)) sessions.push(sessionKey)
|
|
41
113
|
await adapter.setItem(ALL_SESSIONS_KEY, JSON.stringify(sessions))
|
|
114
|
+
await adapter.setItem(ACTIVE_SESSION_KEY, sessionKey)
|
|
115
|
+
} catch (error) {
|
|
116
|
+
await Promise.allSettled([
|
|
117
|
+
restoreItem(recordKey, previousRecord),
|
|
118
|
+
restoreItem(ALL_SESSIONS_KEY, previousIndex),
|
|
119
|
+
restoreItem(ACTIVE_SESSION_KEY, previousActive),
|
|
120
|
+
])
|
|
121
|
+
throw error
|
|
42
122
|
}
|
|
43
|
-
|
|
44
|
-
// Set as active session
|
|
45
|
-
await adapter.setItem(ACTIVE_SESSION_KEY, sessionKey)
|
|
46
123
|
}
|
|
47
124
|
|
|
125
|
+
const storeSession = async (
|
|
126
|
+
sessionData: ZeroDevWalletSession,
|
|
127
|
+
sessionKey: string,
|
|
128
|
+
): Promise<void> =>
|
|
129
|
+
withMutation(() => storeSessionUnlocked(sessionData, sessionKey))
|
|
130
|
+
|
|
48
131
|
const getActiveSession = async (): Promise<
|
|
49
132
|
ZeroDevWalletSession | undefined
|
|
50
133
|
> => {
|
|
@@ -66,10 +149,14 @@ export function createStorageManager(adapter: StorageAdapter): StorageManager {
|
|
|
66
149
|
if (!sessionStr) return undefined
|
|
67
150
|
|
|
68
151
|
try {
|
|
69
|
-
const session:
|
|
152
|
+
const session: unknown = JSON.parse(sessionStr)
|
|
153
|
+
if (!isStoredSession(session)) {
|
|
154
|
+
await clearSession(sessionKey)
|
|
155
|
+
return undefined
|
|
156
|
+
}
|
|
70
157
|
|
|
71
158
|
// Check if session is expired
|
|
72
|
-
if (
|
|
159
|
+
if (normalizeTimestamp(session.expiry) < Date.now()) {
|
|
73
160
|
await clearSession(sessionKey)
|
|
74
161
|
return undefined
|
|
75
162
|
}
|
|
@@ -82,13 +169,17 @@ export function createStorageManager(adapter: StorageAdapter): StorageManager {
|
|
|
82
169
|
}
|
|
83
170
|
}
|
|
84
171
|
|
|
85
|
-
const
|
|
172
|
+
const listSessionKeysUnlocked = async (): Promise<string[]> => {
|
|
86
173
|
const sessionsStr = await adapter.getItem(ALL_SESSIONS_KEY)
|
|
87
|
-
const
|
|
174
|
+
const parsedKeys = parseSessionKeys(sessionsStr)
|
|
175
|
+
if (!parsedKeys) {
|
|
176
|
+
await adapter.removeItem(ALL_SESSIONS_KEY)
|
|
177
|
+
return []
|
|
178
|
+
}
|
|
88
179
|
|
|
89
180
|
// Clean up any keys that don't have corresponding sessions
|
|
90
181
|
const validKeys: string[] = []
|
|
91
|
-
for (const key of
|
|
182
|
+
for (const key of parsedKeys) {
|
|
92
183
|
const exists = await adapter.getItem(key)
|
|
93
184
|
if (exists) {
|
|
94
185
|
validKeys.push(key)
|
|
@@ -96,13 +187,16 @@ export function createStorageManager(adapter: StorageAdapter): StorageManager {
|
|
|
96
187
|
}
|
|
97
188
|
|
|
98
189
|
// Update the list if we found invalid keys
|
|
99
|
-
if (validKeys.length !==
|
|
190
|
+
if (validKeys.length !== parsedKeys.length) {
|
|
100
191
|
await adapter.setItem(ALL_SESSIONS_KEY, JSON.stringify(validKeys))
|
|
101
192
|
}
|
|
102
193
|
|
|
103
194
|
return validKeys
|
|
104
195
|
}
|
|
105
196
|
|
|
197
|
+
const listSessionKeys = async (): Promise<string[]> =>
|
|
198
|
+
withMutation(listSessionKeysUnlocked)
|
|
199
|
+
|
|
106
200
|
const listSessions = async (): Promise<ZeroDevWalletSession[]> => {
|
|
107
201
|
const sessionKeys = await listSessionKeys()
|
|
108
202
|
const sessions: ZeroDevWalletSession[] = []
|
|
@@ -124,15 +218,17 @@ export function createStorageManager(adapter: StorageAdapter): StorageManager {
|
|
|
124
218
|
throw new Error(`Session not found: ${sessionKey}`)
|
|
125
219
|
}
|
|
126
220
|
|
|
127
|
-
await
|
|
221
|
+
await withMutation(async () => {
|
|
222
|
+
await adapter.setItem(ACTIVE_SESSION_KEY, sessionKey)
|
|
223
|
+
})
|
|
128
224
|
}
|
|
129
225
|
|
|
130
|
-
const
|
|
226
|
+
const clearSessionUnlocked = async (sessionKey: string): Promise<void> => {
|
|
131
227
|
// Remove the session data
|
|
132
228
|
await adapter.removeItem(sessionKey)
|
|
133
229
|
|
|
134
230
|
// Remove from sessions list
|
|
135
|
-
const sessions = await
|
|
231
|
+
const sessions = await listSessionKeysUnlocked()
|
|
136
232
|
const updated = sessions.filter((k) => k !== sessionKey)
|
|
137
233
|
await adapter.setItem(ALL_SESSIONS_KEY, JSON.stringify(updated))
|
|
138
234
|
|
|
@@ -143,19 +239,100 @@ export function createStorageManager(adapter: StorageAdapter): StorageManager {
|
|
|
143
239
|
}
|
|
144
240
|
}
|
|
145
241
|
|
|
146
|
-
const
|
|
147
|
-
|
|
242
|
+
const clearSession = async (sessionKey: string): Promise<void> =>
|
|
243
|
+
withMutation(() => clearSessionUnlocked(sessionKey))
|
|
244
|
+
|
|
245
|
+
const clearAllSessions = async (): Promise<void> =>
|
|
246
|
+
withMutation(async () => {
|
|
247
|
+
await clearAllSessionsUnlocked()
|
|
248
|
+
await adapter.removeItem(SESSION_TRANSITION_KEY)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
async function clearAllSessionsUnlocked(): Promise<void> {
|
|
252
|
+
const sessions = await listSessionKeysUnlocked()
|
|
148
253
|
|
|
149
|
-
// Remove all session data
|
|
150
254
|
for (const key of sessions) {
|
|
151
255
|
await adapter.removeItem(key)
|
|
152
256
|
}
|
|
153
257
|
|
|
154
|
-
// Clear the metadata
|
|
155
258
|
await adapter.removeItem(ALL_SESSIONS_KEY)
|
|
156
259
|
await adapter.removeItem(ACTIVE_SESSION_KEY)
|
|
157
260
|
}
|
|
158
261
|
|
|
262
|
+
const parseTransition = (value: string | null): SessionTransition | null => {
|
|
263
|
+
if (!value) return null
|
|
264
|
+
try {
|
|
265
|
+
const parsed = JSON.parse(value) as Partial<SessionTransition>
|
|
266
|
+
if (
|
|
267
|
+
!isStoredSession(parsed.sessionData) ||
|
|
268
|
+
typeof parsed.publicKey !== 'string' ||
|
|
269
|
+
!parsed.publicKey
|
|
270
|
+
) {
|
|
271
|
+
return null
|
|
272
|
+
}
|
|
273
|
+
return parsed as SessionTransition
|
|
274
|
+
} catch {
|
|
275
|
+
return null
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const stageSessionTransition = async (
|
|
280
|
+
sessionData: ZeroDevWalletSession,
|
|
281
|
+
publicKey: string,
|
|
282
|
+
): Promise<void> =>
|
|
283
|
+
withMutation(async () => {
|
|
284
|
+
await adapter.setItem(
|
|
285
|
+
SESSION_TRANSITION_KEY,
|
|
286
|
+
JSON.stringify({ sessionData, publicKey }),
|
|
287
|
+
)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
const commitSessionTransition = async (): Promise<
|
|
291
|
+
ZeroDevWalletSession | undefined
|
|
292
|
+
> =>
|
|
293
|
+
withMutation(async () => {
|
|
294
|
+
const transition = parseTransition(
|
|
295
|
+
await adapter.getItem(SESSION_TRANSITION_KEY),
|
|
296
|
+
)
|
|
297
|
+
if (!transition) {
|
|
298
|
+
await adapter.removeItem(SESSION_TRANSITION_KEY)
|
|
299
|
+
return undefined
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
await clearAllSessionsUnlocked()
|
|
303
|
+
await storeSessionUnlocked(
|
|
304
|
+
transition.sessionData,
|
|
305
|
+
transition.sessionData.id,
|
|
306
|
+
)
|
|
307
|
+
await adapter.removeItem(SESSION_TRANSITION_KEY)
|
|
308
|
+
return transition.sessionData
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
const normalizeKey = (key: string) => key.replace(/^0x/, '').toLowerCase()
|
|
312
|
+
|
|
313
|
+
const recoverSessionTransition = async (
|
|
314
|
+
activePublicKey: string | null,
|
|
315
|
+
): Promise<ZeroDevWalletSession | undefined> => {
|
|
316
|
+
const transition = parseTransition(
|
|
317
|
+
await adapter.getItem(SESSION_TRANSITION_KEY),
|
|
318
|
+
)
|
|
319
|
+
if (!transition) {
|
|
320
|
+
if (await adapter.getItem(SESSION_TRANSITION_KEY)) {
|
|
321
|
+
await adapter.removeItem(SESSION_TRANSITION_KEY)
|
|
322
|
+
}
|
|
323
|
+
return undefined
|
|
324
|
+
}
|
|
325
|
+
if (!activePublicKey) {
|
|
326
|
+
await adapter.removeItem(SESSION_TRANSITION_KEY)
|
|
327
|
+
return undefined
|
|
328
|
+
}
|
|
329
|
+
if (normalizeKey(activePublicKey) !== normalizeKey(transition.publicKey)) {
|
|
330
|
+
await adapter.removeItem(SESSION_TRANSITION_KEY)
|
|
331
|
+
return undefined
|
|
332
|
+
}
|
|
333
|
+
return commitSessionTransition()
|
|
334
|
+
}
|
|
335
|
+
|
|
159
336
|
return {
|
|
160
337
|
storeSession,
|
|
161
338
|
getActiveSession,
|
|
@@ -166,5 +343,8 @@ export function createStorageManager(adapter: StorageAdapter): StorageManager {
|
|
|
166
343
|
setActiveSession,
|
|
167
344
|
clearSession,
|
|
168
345
|
clearAllSessions,
|
|
346
|
+
stageSessionTransition,
|
|
347
|
+
commitSessionTransition,
|
|
348
|
+
recoverSessionTransition,
|
|
169
349
|
}
|
|
170
350
|
}
|
package/src/types/session.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type BuildClientSignatureParams = {
|
|
|
7
7
|
/** The compressed public key hex */
|
|
8
8
|
publicKey: string
|
|
9
9
|
/** The API key stamper for signing */
|
|
10
|
-
stamper: ApiKeyStamper
|
|
10
|
+
stamper: Pick<ApiKeyStamper, 'stamp'>
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -64,7 +64,9 @@ function extractTokenIdFromJwt(jwt: string): string {
|
|
|
64
64
|
throw new Error('Invalid JWT format')
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
const
|
|
67
|
+
const encodedPayload = parts[1]
|
|
68
|
+
if (!encodedPayload) throw new Error('Invalid JWT format')
|
|
69
|
+
const payload = JSON.parse(base64UrlDecode(encodedPayload))
|
|
68
70
|
if (!payload.id) {
|
|
69
71
|
throw new Error('JWT payload missing id field')
|
|
70
72
|
}
|