@organigram/passkey-wallet 0.1.6
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/.organigram-docs-cache/passkey-wallet/api-extractor.json +35 -0
- package/.organigram-docs-cache/passkey-wallet/api-model/passkey-wallet.api.json +3166 -0
- package/.organigram-docs-cache/passkey-wallet/entry/index.d.ts +9 -0
- package/.organigram-docs-cache/passkey-wallet/tmp/node-compile-cache/v24.12.0-arm64-cf738c9d-501/05732508 +0 -0
- package/.organigram-docs-cache/passkey-wallet/tmp/node-compile-cache/v24.12.0-arm64-cf738c9d-501/166b6dbf +0 -0
- package/.organigram-docs-cache/passkey-wallet/tmp/node-compile-cache/v24.12.0-arm64-cf738c9d-501/41c20e23 +0 -0
- package/.organigram-docs-cache/passkey-wallet/tmp/node-compile-cache/v24.12.0-arm64-cf738c9d-501/7111ea58 +0 -0
- package/.organigram-docs-cache/passkey-wallet/tmp/tsx-501/17844-1822dfbab9819e9f6a93485fe3da5dbf3745311c +1 -0
- package/.organigram-docs-cache/passkey-wallet/tmp/tsx-501/17844-45a65cfdc7a50c53817c1dad3954ae3d2d2c35fb +1 -0
- package/.organigram-docs-cache/passkey-wallet/tmp/tsx-501/17844-fee5290b05398fe9e3b878deefd22383744f1526 +1 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/createorganigrampasskeywalletinput.yml +17 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/identitypasskeycapabilities.yml +16 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/organigrampasskeycapabilities.yml +18 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/organigrampasskeyprovider.yml +16 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/organigrampasskeyprovideractions.yml +20 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeyprfunavailableerror.yml +28 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeyproviderevent.yml +11 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeyproviderlistener.yml +11 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeyregistrationresult.yml +15 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeyvaultdecrypterror.yml +28 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeyvaultenvelopedata.yml +18 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeyvaultenvelopeinput.yml +19 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeywalletapiclient.yml +33 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeywalletcapabilities.yml +11 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeywalletlogincapabilities.yml +14 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeywalletregistrationcapabilities.yml +18 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeywalletvaultpayload.yml +18 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/unlockedpasskeywallet.yml +21 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet.yml +870 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/toc.yml +43 -0
- package/.turbo/turbo-build.log +6 -0
- package/.turbo/turbo-docs$colon$generate.log +118 -0
- package/.turbo/turbo-lint.log +13 -0
- package/.turbo/turbo-test.log +152 -0
- package/.turbo/turbo-typecheck.log +5 -0
- package/CHANGELOG.md +8 -0
- package/LICENSE.md +21 -0
- package/README.md +427 -0
- package/__mocks__/organigram-js.ts +19 -0
- package/__mocks__/wagmi.ts +1 -0
- package/dist/crypto.d.ts +39 -0
- package/dist/crypto.js +95 -0
- package/dist/eip1193.d.ts +24 -0
- package/dist/eip1193.js +103 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +13 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/rainbowkit.d.ts +14 -0
- package/dist/rainbowkit.js +128 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.js +17 -0
- package/dist/vault.d.ts +10 -0
- package/dist/vault.js +41 -0
- package/dist/wallet.d.ts +15 -0
- package/dist/wallet.js +30 -0
- package/dist/webauthn-client.d.ts +73 -0
- package/dist/webauthn-client.js +234 -0
- package/dist/webauthn-server.d.ts +62 -0
- package/dist/webauthn-server.js +133 -0
- package/jest.config.ts +32 -0
- package/package.json +61 -0
- package/src/crypto.test.ts +69 -0
- package/src/crypto.ts +193 -0
- package/src/eip1193.ts +169 -0
- package/src/errors.ts +16 -0
- package/src/index.ts +4 -0
- package/src/rainbowkit.test.ts +42 -0
- package/src/rainbowkit.ts +171 -0
- package/src/types.ts +82 -0
- package/src/vault.test.ts +23 -0
- package/src/vault.ts +77 -0
- package/src/wallet.ts +61 -0
- package/src/webauthn-client.test.ts +103 -0
- package/src/webauthn-client.ts +430 -0
- package/src/webauthn-server.test.ts +43 -0
- package/src/webauthn-server.ts +258 -0
- package/tsconfig.build.json +15 -0
- package/tsconfig.json +27 -0
package/src/crypto.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
export const passkeyVaultAlgorithm = 'AES-GCM-HKDF-SHA-256'
|
|
2
|
+
export const passkeyVaultKeyVersion = 1
|
|
3
|
+
|
|
4
|
+
export type PasskeyVaultEnvelopeData = {
|
|
5
|
+
algorithm: typeof passkeyVaultAlgorithm
|
|
6
|
+
keyVersion: typeof passkeyVaultKeyVersion
|
|
7
|
+
ciphertext: string
|
|
8
|
+
salt: string
|
|
9
|
+
nonce: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type DerivePasskeyVaultKeyInput = {
|
|
13
|
+
prfOutput: Uint8Array
|
|
14
|
+
salt: Uint8Array
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type EncryptPasskeyVaultSecretInput = {
|
|
18
|
+
plaintext: string
|
|
19
|
+
key: CryptoKey
|
|
20
|
+
salt: Uint8Array
|
|
21
|
+
nonce?: Uint8Array
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type DecryptPasskeyVaultSecretInput = {
|
|
25
|
+
envelope: PasskeyVaultEnvelopeData
|
|
26
|
+
key: CryptoKey
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type PrfSupportCredential = {
|
|
30
|
+
getClientExtensionResults: () => {
|
|
31
|
+
prf?: {
|
|
32
|
+
enabled?: boolean
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const textEncoder = new TextEncoder()
|
|
38
|
+
const textDecoder = new TextDecoder()
|
|
39
|
+
|
|
40
|
+
const getCrypto = (): Crypto => {
|
|
41
|
+
if (globalThis.crypto == null) {
|
|
42
|
+
throw new Error('WebCrypto is not available in this environment.')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return globalThis.crypto
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const toArrayBuffer = (bytes: Uint8Array): ArrayBuffer =>
|
|
49
|
+
bytes.buffer.slice(
|
|
50
|
+
bytes.byteOffset,
|
|
51
|
+
bytes.byteOffset + bytes.byteLength
|
|
52
|
+
) as ArrayBuffer
|
|
53
|
+
|
|
54
|
+
export const bytesToBase64Url = (bytes: Uint8Array): string =>
|
|
55
|
+
bytesToBase64(bytes)
|
|
56
|
+
.replaceAll('+', '-')
|
|
57
|
+
.replaceAll('/', '_')
|
|
58
|
+
.replaceAll('=', '')
|
|
59
|
+
|
|
60
|
+
export const base64UrlToBytes = (value: string): Uint8Array => {
|
|
61
|
+
const normalized = value.replaceAll('-', '+').replaceAll('_', '/')
|
|
62
|
+
const padded = normalized.padEnd(
|
|
63
|
+
normalized.length + ((4 - (normalized.length % 4)) % 4),
|
|
64
|
+
'='
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return base64ToBytes(padded)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const bytesToBase64 = (bytes: Uint8Array): string => {
|
|
71
|
+
if (typeof Buffer !== 'undefined') {
|
|
72
|
+
return Buffer.from(bytes).toString('base64')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let binary = ''
|
|
76
|
+
bytes.forEach(byte => {
|
|
77
|
+
binary += String.fromCharCode(byte)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return btoa(binary)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const base64ToBytes = (value: string): Uint8Array => {
|
|
84
|
+
if (typeof Buffer !== 'undefined') {
|
|
85
|
+
return new Uint8Array(Buffer.from(value, 'base64'))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const binary = atob(value)
|
|
89
|
+
const bytes = new Uint8Array(binary.length)
|
|
90
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
91
|
+
bytes[index] = binary.charCodeAt(index)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return bytes
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const derivePasskeyVaultKey = async ({
|
|
98
|
+
prfOutput,
|
|
99
|
+
salt
|
|
100
|
+
}: DerivePasskeyVaultKeyInput): Promise<CryptoKey> => {
|
|
101
|
+
if (prfOutput.byteLength !== 32) {
|
|
102
|
+
throw new Error('Passkey PRF output must be 32 bytes.')
|
|
103
|
+
}
|
|
104
|
+
if (salt.byteLength !== 32) {
|
|
105
|
+
throw new Error('Passkey vault salt must be 32 bytes.')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const crypto = getCrypto()
|
|
109
|
+
const baseKey = await crypto.subtle.importKey(
|
|
110
|
+
'raw',
|
|
111
|
+
toArrayBuffer(prfOutput),
|
|
112
|
+
'HKDF',
|
|
113
|
+
false,
|
|
114
|
+
['deriveKey']
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return await crypto.subtle.deriveKey(
|
|
118
|
+
{
|
|
119
|
+
name: 'HKDF',
|
|
120
|
+
hash: 'SHA-256',
|
|
121
|
+
salt: toArrayBuffer(salt),
|
|
122
|
+
info: textEncoder.encode('organigram-passkey-wallet-v1')
|
|
123
|
+
},
|
|
124
|
+
baseKey,
|
|
125
|
+
{
|
|
126
|
+
name: 'AES-GCM',
|
|
127
|
+
length: 256
|
|
128
|
+
},
|
|
129
|
+
false,
|
|
130
|
+
['encrypt', 'decrypt']
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const encryptPasskeyVaultSecret = async ({
|
|
135
|
+
plaintext,
|
|
136
|
+
key,
|
|
137
|
+
salt,
|
|
138
|
+
nonce = getCrypto().getRandomValues(new Uint8Array(12))
|
|
139
|
+
}: EncryptPasskeyVaultSecretInput): Promise<PasskeyVaultEnvelopeData> => {
|
|
140
|
+
if (plaintext.trim() === '') {
|
|
141
|
+
throw new Error('Passkey wallet vault plaintext cannot be empty.')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const ciphertext = await getCrypto().subtle.encrypt(
|
|
145
|
+
{
|
|
146
|
+
name: 'AES-GCM',
|
|
147
|
+
iv: toArrayBuffer(nonce)
|
|
148
|
+
},
|
|
149
|
+
key,
|
|
150
|
+
textEncoder.encode(plaintext)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
algorithm: passkeyVaultAlgorithm,
|
|
155
|
+
keyVersion: passkeyVaultKeyVersion,
|
|
156
|
+
ciphertext: bytesToBase64Url(new Uint8Array(ciphertext)),
|
|
157
|
+
salt: bytesToBase64Url(salt),
|
|
158
|
+
nonce: bytesToBase64Url(nonce)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const decryptPasskeyVaultSecret = async ({
|
|
163
|
+
envelope,
|
|
164
|
+
key
|
|
165
|
+
}: DecryptPasskeyVaultSecretInput): Promise<string> => {
|
|
166
|
+
try {
|
|
167
|
+
if (envelope.algorithm !== passkeyVaultAlgorithm) {
|
|
168
|
+
throw new Error('Unsupported passkey wallet vault algorithm.')
|
|
169
|
+
}
|
|
170
|
+
if (envelope.keyVersion !== passkeyVaultKeyVersion) {
|
|
171
|
+
throw new Error('Unsupported passkey wallet vault key version.')
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const plaintext = await getCrypto().subtle.decrypt(
|
|
175
|
+
{
|
|
176
|
+
name: 'AES-GCM',
|
|
177
|
+
iv: toArrayBuffer(base64UrlToBytes(envelope.nonce))
|
|
178
|
+
},
|
|
179
|
+
key,
|
|
180
|
+
toArrayBuffer(base64UrlToBytes(envelope.ciphertext))
|
|
181
|
+
)
|
|
182
|
+
return textDecoder.decode(plaintext)
|
|
183
|
+
} catch (error) {
|
|
184
|
+
throw new Error('Unable to decrypt passkey wallet.', { cause: error })
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const isPasskeyPrfSupported = async ({
|
|
189
|
+
credential
|
|
190
|
+
}: {
|
|
191
|
+
credential: PrfSupportCredential | null
|
|
192
|
+
}): Promise<boolean> =>
|
|
193
|
+
credential?.getClientExtensionResults().prf?.enabled === true
|
package/src/eip1193.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { Chain, EIP1193RequestFn, Hex } from 'viem'
|
|
2
|
+
import {
|
|
3
|
+
createWalletClient,
|
|
4
|
+
getAddress,
|
|
5
|
+
hexToString,
|
|
6
|
+
http,
|
|
7
|
+
numberToHex
|
|
8
|
+
} from 'viem'
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
PasskeyProviderEvent,
|
|
12
|
+
PasskeyProviderListener,
|
|
13
|
+
PasskeyRegistrationResult,
|
|
14
|
+
UnlockedPasskeyWallet
|
|
15
|
+
} from './types'
|
|
16
|
+
|
|
17
|
+
export type OrganigramPasskeyProvider = {
|
|
18
|
+
request: EIP1193RequestFn
|
|
19
|
+
on: (event: PasskeyProviderEvent, listener: PasskeyProviderListener) => void
|
|
20
|
+
removeListener: (
|
|
21
|
+
event: PasskeyProviderEvent,
|
|
22
|
+
listener: PasskeyProviderListener
|
|
23
|
+
) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type OrganigramPasskeyProviderActions = {
|
|
27
|
+
registerAdditionalPasskeyCredential: (input: {
|
|
28
|
+
wallet: UnlockedPasskeyWallet
|
|
29
|
+
name?: string
|
|
30
|
+
}) => Promise<PasskeyRegistrationResult>
|
|
31
|
+
exportPasskeyWalletRecoveryPhrase: (input: {
|
|
32
|
+
expectedAddress: `0x${string}`
|
|
33
|
+
}) => Promise<string>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const normalizePersonalSignMessage = (message: unknown): string => {
|
|
37
|
+
if (typeof message !== 'string') {
|
|
38
|
+
throw new Error('Passkey wallet expected a string message.')
|
|
39
|
+
}
|
|
40
|
+
if (message.startsWith('0x')) {
|
|
41
|
+
return hexToString(message as Hex)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return message
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const createPasskeyWalletProvider = ({
|
|
48
|
+
wallet,
|
|
49
|
+
chain,
|
|
50
|
+
rpcUrl,
|
|
51
|
+
switchChain,
|
|
52
|
+
actions
|
|
53
|
+
}: {
|
|
54
|
+
wallet: UnlockedPasskeyWallet
|
|
55
|
+
chain: Chain
|
|
56
|
+
rpcUrl: string
|
|
57
|
+
switchChain: (chainId: number) => Chain
|
|
58
|
+
actions: OrganigramPasskeyProviderActions
|
|
59
|
+
}): OrganigramPasskeyProvider => {
|
|
60
|
+
const listeners = new Map<PasskeyProviderEvent, Set<PasskeyProviderListener>>()
|
|
61
|
+
let currentChain = chain
|
|
62
|
+
|
|
63
|
+
const emit = (event: PasskeyProviderEvent, ...args: unknown[]): void => {
|
|
64
|
+
listeners.get(event)?.forEach(listener => {
|
|
65
|
+
listener(...args)
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const getWalletClient = () =>
|
|
70
|
+
createWalletClient({
|
|
71
|
+
account: wallet.account,
|
|
72
|
+
chain: currentChain,
|
|
73
|
+
transport: http(rpcUrl)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const request = (async ({ method, params }) => {
|
|
77
|
+
const requestParams = Array.isArray(params) ? params : []
|
|
78
|
+
|
|
79
|
+
switch (method) {
|
|
80
|
+
case 'eth_requestAccounts':
|
|
81
|
+
case 'eth_accounts':
|
|
82
|
+
return [wallet.address]
|
|
83
|
+
case 'eth_chainId':
|
|
84
|
+
return numberToHex(currentChain.id)
|
|
85
|
+
case 'wallet_switchEthereumChain': {
|
|
86
|
+
const chainId = Number.parseInt(
|
|
87
|
+
String((requestParams[0] as { chainId?: string })?.chainId),
|
|
88
|
+
16
|
|
89
|
+
)
|
|
90
|
+
currentChain = switchChain(chainId)
|
|
91
|
+
emit('chainChanged', numberToHex(currentChain.id))
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
case 'personal_sign':
|
|
95
|
+
return await wallet.account.signMessage({
|
|
96
|
+
message: normalizePersonalSignMessage(requestParams[0])
|
|
97
|
+
})
|
|
98
|
+
case 'eth_sign':
|
|
99
|
+
return await wallet.account.signMessage({
|
|
100
|
+
message: normalizePersonalSignMessage(requestParams[1])
|
|
101
|
+
})
|
|
102
|
+
case 'eth_signTypedData':
|
|
103
|
+
case 'eth_signTypedData_v3':
|
|
104
|
+
case 'eth_signTypedData_v4': {
|
|
105
|
+
const typedData =
|
|
106
|
+
typeof requestParams[1] === 'string'
|
|
107
|
+
? JSON.parse(requestParams[1])
|
|
108
|
+
: requestParams[1]
|
|
109
|
+
return await wallet.account.signTypedData(typedData)
|
|
110
|
+
}
|
|
111
|
+
case 'eth_sendTransaction':
|
|
112
|
+
return await getWalletClient().sendTransaction(requestParams[0] as never)
|
|
113
|
+
case 'organigram_addPasskey': {
|
|
114
|
+
return await actions.registerAdditionalPasskeyCredential({
|
|
115
|
+
wallet,
|
|
116
|
+
name:
|
|
117
|
+
typeof requestParams[0] === 'object' &&
|
|
118
|
+
requestParams[0] != null &&
|
|
119
|
+
'name' in requestParams[0]
|
|
120
|
+
? String((requestParams[0] as { name?: unknown }).name ?? '')
|
|
121
|
+
: undefined
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
case 'organigram_exportRecoveryPhrase':
|
|
125
|
+
return await actions.exportPasskeyWalletRecoveryPhrase({
|
|
126
|
+
expectedAddress: wallet.address
|
|
127
|
+
})
|
|
128
|
+
default: {
|
|
129
|
+
const response = await fetch(rpcUrl, {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: {
|
|
132
|
+
'Content-Type': 'application/json'
|
|
133
|
+
},
|
|
134
|
+
body: JSON.stringify({
|
|
135
|
+
jsonrpc: '2.0',
|
|
136
|
+
id: 1,
|
|
137
|
+
method,
|
|
138
|
+
params: requestParams
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
const body = (await response.json()) as {
|
|
142
|
+
result?: unknown
|
|
143
|
+
error?: { message?: string }
|
|
144
|
+
}
|
|
145
|
+
if (body.error != null) {
|
|
146
|
+
throw new Error(body.error.message ?? 'RPC request failed.')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return body.result
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}) as EIP1193RequestFn
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
request,
|
|
156
|
+
on: (event, listener) => {
|
|
157
|
+
const eventListeners = listeners.get(event) ?? new Set()
|
|
158
|
+
eventListeners.add(listener)
|
|
159
|
+
listeners.set(event, eventListeners)
|
|
160
|
+
},
|
|
161
|
+
removeListener: (event, listener) => {
|
|
162
|
+
listeners.get(event)?.delete(listener)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const normalizePasskeyWalletAddress = (
|
|
168
|
+
address: `0x${string}`
|
|
169
|
+
): `0x${string}` => getAddress(address)
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const passkeyPrfUnavailableErrorMessage =
|
|
2
|
+
"This password manager is not compatible with Organigram's passkey wallet. Please use a different password manager to unlock your wallet."
|
|
3
|
+
|
|
4
|
+
export class PasskeyPrfUnavailableError extends Error {
|
|
5
|
+
constructor(message = passkeyPrfUnavailableErrorMessage) {
|
|
6
|
+
super(message)
|
|
7
|
+
this.name = 'PasskeyPrfUnavailableError'
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class PasskeyVaultDecryptError extends Error {
|
|
12
|
+
constructor(message = 'Unable to decrypt passkey wallet.') {
|
|
13
|
+
super(message)
|
|
14
|
+
this.name = 'PasskeyVaultDecryptError'
|
|
15
|
+
}
|
|
16
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createOrganigramPasskeyWallet,
|
|
3
|
+
isOrganigramPasskeyConnector,
|
|
4
|
+
organigramPasskeyWalletId
|
|
5
|
+
} from './rainbowkit'
|
|
6
|
+
import { readFileSync } from 'node:fs'
|
|
7
|
+
import { resolve } from 'node:path'
|
|
8
|
+
|
|
9
|
+
const rainbowkitPath = resolve('src/rainbowkit.ts')
|
|
10
|
+
const eip1193Path = resolve('src/eip1193.ts')
|
|
11
|
+
|
|
12
|
+
describe('Organigram passkey RainbowKit wallet', () => {
|
|
13
|
+
it('builds the Organigram passkey wallet descriptor', () => {
|
|
14
|
+
const wallet = createOrganigramPasskeyWallet({
|
|
15
|
+
unlockOrCreatePasskeyWallet: async () => {
|
|
16
|
+
throw new Error('not used')
|
|
17
|
+
},
|
|
18
|
+
registerAdditionalPasskeyCredential: async () => {
|
|
19
|
+
throw new Error('not used')
|
|
20
|
+
},
|
|
21
|
+
exportPasskeyWalletRecoveryPhrase: async () => {
|
|
22
|
+
throw new Error('not used')
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
expect(wallet.id).toBe(organigramPasskeyWalletId)
|
|
27
|
+
expect(wallet.name).toBe('Passkey Wallet')
|
|
28
|
+
expect(wallet.shortName).toBe('Passkey')
|
|
29
|
+
expect(isOrganigramPasskeyConnector({ id: wallet.id })).toBe(true)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('exposes a local Wagmi connector backed by the Organigram passkey provider', () => {
|
|
33
|
+
expect(readFileSync(rainbowkitPath, 'utf8')).toMatch(/createConnector/)
|
|
34
|
+
expect(readFileSync(rainbowkitPath, 'utf8')).toMatch(
|
|
35
|
+
/createPasskeyWalletProvider/
|
|
36
|
+
)
|
|
37
|
+
expect(readFileSync(eip1193Path, 'utf8')).toMatch(/organigram_addPasskey/)
|
|
38
|
+
expect(readFileSync(eip1193Path, 'utf8')).not.toMatch(
|
|
39
|
+
/organigram_exportIpfsPrivateKey/
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { Wallet, WalletDetailsParams } from '@rainbow-me/rainbowkit'
|
|
2
|
+
import { getAddress } from 'viem'
|
|
3
|
+
import type { Chain } from 'viem'
|
|
4
|
+
import { createConnector } from 'wagmi'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createPasskeyWalletProvider,
|
|
8
|
+
type OrganigramPasskeyProvider,
|
|
9
|
+
type OrganigramPasskeyProviderActions
|
|
10
|
+
} from './eip1193'
|
|
11
|
+
import {
|
|
12
|
+
organigramPasskeyWalletIcon,
|
|
13
|
+
organigramPasskeyWalletId,
|
|
14
|
+
type OrganigramPasskeyCapabilities,
|
|
15
|
+
type UnlockedPasskeyWallet
|
|
16
|
+
} from './types'
|
|
17
|
+
|
|
18
|
+
export { organigramPasskeyWalletIcon, organigramPasskeyWalletId } from './types'
|
|
19
|
+
|
|
20
|
+
const walletName = 'Passkey Wallet'
|
|
21
|
+
|
|
22
|
+
const passkeyConnectorIds = new Set([organigramPasskeyWalletId])
|
|
23
|
+
|
|
24
|
+
export const isOrganigramPasskeyConnector = ({ id }: { id: string }): boolean =>
|
|
25
|
+
passkeyConnectorIds.has(id)
|
|
26
|
+
|
|
27
|
+
export type CreateOrganigramPasskeyWalletInput =
|
|
28
|
+
OrganigramPasskeyProviderActions & {
|
|
29
|
+
unlockOrCreatePasskeyWallet: (input: {
|
|
30
|
+
capabilities: OrganigramPasskeyCapabilities
|
|
31
|
+
targetChainId: number
|
|
32
|
+
}) => Promise<UnlockedPasskeyWallet>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const createOrganigramPasskeyWallet = ({
|
|
36
|
+
unlockOrCreatePasskeyWallet,
|
|
37
|
+
registerAdditionalPasskeyCredential,
|
|
38
|
+
exportPasskeyWalletRecoveryPhrase
|
|
39
|
+
}: CreateOrganigramPasskeyWalletInput): Wallet => ({
|
|
40
|
+
id: organigramPasskeyWalletId,
|
|
41
|
+
name: walletName,
|
|
42
|
+
shortName: 'Passkey',
|
|
43
|
+
rdns: 'ai.organigram.passkey',
|
|
44
|
+
iconUrl: organigramPasskeyWalletIcon,
|
|
45
|
+
iconAccent: '#00C2A8',
|
|
46
|
+
iconBackground: '#18272B',
|
|
47
|
+
installed: true,
|
|
48
|
+
createConnector: (walletDetails: WalletDetailsParams) =>
|
|
49
|
+
createConnector(config => {
|
|
50
|
+
let provider: OrganigramPasskeyProvider | null = null
|
|
51
|
+
let wallet: UnlockedPasskeyWallet | null = null
|
|
52
|
+
let activeChain = config.chains[0]
|
|
53
|
+
|
|
54
|
+
const resolveChain = (chainId?: number): Chain => {
|
|
55
|
+
const chain =
|
|
56
|
+
chainId == null
|
|
57
|
+
? activeChain
|
|
58
|
+
: config.chains.find(candidate => candidate.id === chainId)
|
|
59
|
+
if (chain == null) {
|
|
60
|
+
throw new Error('Unsupported network for Passkey Wallet.')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return chain
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const getRpcUrl = (chain: Chain): string =>
|
|
67
|
+
chain.rpcUrls.default.http[0] ?? chain.rpcUrls.public?.http[0] ?? ''
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
...walletDetails,
|
|
71
|
+
id: organigramPasskeyWalletId,
|
|
72
|
+
name: walletName,
|
|
73
|
+
type: 'passkey-wallet',
|
|
74
|
+
async connect(parameters): Promise<any> {
|
|
75
|
+
activeChain = resolveChain(parameters?.chainId)
|
|
76
|
+
const unlockedWallet = await unlockOrCreatePasskeyWallet({
|
|
77
|
+
capabilities:
|
|
78
|
+
(parameters as { capabilities?: OrganigramPasskeyCapabilities })
|
|
79
|
+
?.capabilities ?? {},
|
|
80
|
+
targetChainId: activeChain.id
|
|
81
|
+
})
|
|
82
|
+
wallet = {
|
|
83
|
+
...unlockedWallet,
|
|
84
|
+
address: getAddress(unlockedWallet.address)
|
|
85
|
+
}
|
|
86
|
+
provider = createPasskeyWalletProvider({
|
|
87
|
+
wallet,
|
|
88
|
+
chain: activeChain,
|
|
89
|
+
rpcUrl: getRpcUrl(activeChain),
|
|
90
|
+
switchChain: chainId => {
|
|
91
|
+
activeChain = resolveChain(chainId)
|
|
92
|
+
config.emitter.emit('change', {
|
|
93
|
+
chainId: activeChain.id
|
|
94
|
+
})
|
|
95
|
+
return activeChain
|
|
96
|
+
},
|
|
97
|
+
actions: {
|
|
98
|
+
registerAdditionalPasskeyCredential,
|
|
99
|
+
exportPasskeyWalletRecoveryPhrase
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
config.emitter.emit('connect', {
|
|
103
|
+
accounts: [wallet.address],
|
|
104
|
+
chainId: activeChain.id
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
if (parameters?.withCapabilities === true) {
|
|
108
|
+
return {
|
|
109
|
+
accounts: [
|
|
110
|
+
{
|
|
111
|
+
address: wallet.address,
|
|
112
|
+
capabilities: {}
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
chainId: activeChain.id
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
accounts: [wallet.address],
|
|
121
|
+
chainId: activeChain.id
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
async disconnect() {
|
|
125
|
+
wallet = null
|
|
126
|
+
provider = null
|
|
127
|
+
config.emitter.emit('disconnect')
|
|
128
|
+
},
|
|
129
|
+
async getAccounts() {
|
|
130
|
+
return wallet == null ? [] : [wallet.address]
|
|
131
|
+
},
|
|
132
|
+
async getChainId() {
|
|
133
|
+
return activeChain.id
|
|
134
|
+
},
|
|
135
|
+
async getProvider() {
|
|
136
|
+
if (provider == null) {
|
|
137
|
+
throw new Error('Passkey Wallet is locked.')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return provider
|
|
141
|
+
},
|
|
142
|
+
async isAuthorized() {
|
|
143
|
+
return wallet != null && wallet.expiresAt > Date.now()
|
|
144
|
+
},
|
|
145
|
+
async switchChain({ chainId }) {
|
|
146
|
+
activeChain = resolveChain(chainId)
|
|
147
|
+
config.emitter.emit('change', {
|
|
148
|
+
chainId
|
|
149
|
+
})
|
|
150
|
+
return activeChain
|
|
151
|
+
},
|
|
152
|
+
onAccountsChanged(accounts) {
|
|
153
|
+
config.emitter.emit('change', {
|
|
154
|
+
accounts: accounts.map(account =>
|
|
155
|
+
getAddress(account as `0x${string}`)
|
|
156
|
+
)
|
|
157
|
+
})
|
|
158
|
+
},
|
|
159
|
+
onChainChanged(chainId) {
|
|
160
|
+
config.emitter.emit('change', {
|
|
161
|
+
chainId: Number.parseInt(chainId, 16)
|
|
162
|
+
})
|
|
163
|
+
},
|
|
164
|
+
onDisconnect() {
|
|
165
|
+
wallet = null
|
|
166
|
+
provider = null
|
|
167
|
+
config.emitter.emit('disconnect')
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
})
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { HDAccount } from 'viem/accounts'
|
|
2
|
+
|
|
3
|
+
export const organigramPasskeyWalletId = 'organigram-passkeys'
|
|
4
|
+
export const organigramPasskeyWalletIcon = '/png/logo-gradient.png'
|
|
5
|
+
|
|
6
|
+
export type OrganigramPasskeyCapabilities = {
|
|
7
|
+
method?: 'login' | 'register'
|
|
8
|
+
name?: string
|
|
9
|
+
identity?: {
|
|
10
|
+
email: string
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type PasskeyWalletLoginCapabilities = {
|
|
15
|
+
method: 'login'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type PasskeyWalletRegistrationCapabilities = {
|
|
19
|
+
method: 'register'
|
|
20
|
+
name: string
|
|
21
|
+
identity?: {
|
|
22
|
+
email: string
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type PasskeyWalletCapabilities =
|
|
27
|
+
| PasskeyWalletLoginCapabilities
|
|
28
|
+
| PasskeyWalletRegistrationCapabilities
|
|
29
|
+
|
|
30
|
+
export type IdentityPasskeyCapabilities =
|
|
31
|
+
PasskeyWalletRegistrationCapabilities & {
|
|
32
|
+
identity: {
|
|
33
|
+
email: string
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const normalizePasskeyIdentityEmail = (email: string): string =>
|
|
38
|
+
email.trim().toLowerCase()
|
|
39
|
+
|
|
40
|
+
export const buildIdentityPasskeyCapabilities = (
|
|
41
|
+
email: string
|
|
42
|
+
): IdentityPasskeyCapabilities => {
|
|
43
|
+
const normalizedEmail = normalizePasskeyIdentityEmail(email)
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
method: 'register',
|
|
47
|
+
name: normalizedEmail,
|
|
48
|
+
identity: {
|
|
49
|
+
email: normalizedEmail
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const buildPasskeyWalletCapabilities =
|
|
55
|
+
(): PasskeyWalletCapabilities => ({
|
|
56
|
+
method: 'login'
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
export const buildEmailPasskeyCapabilities = buildIdentityPasskeyCapabilities
|
|
60
|
+
|
|
61
|
+
export type UnlockedPasskeyWallet = {
|
|
62
|
+
address: `0x${string}`
|
|
63
|
+
account: HDAccount
|
|
64
|
+
recoveryPhrase: string
|
|
65
|
+
userEncryptionPrivateKey: JsonWebKey
|
|
66
|
+
userEncryptionPublicKey: JsonWebKey
|
|
67
|
+
userEncryptionKeyVersion: number
|
|
68
|
+
credentialId: string
|
|
69
|
+
expiresAt: number
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type PasskeyRegistrationResult = {
|
|
73
|
+
address: `0x${string}`
|
|
74
|
+
credentialId: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type PasskeyProviderEvent =
|
|
78
|
+
| 'accountsChanged'
|
|
79
|
+
| 'chainChanged'
|
|
80
|
+
| 'disconnect'
|
|
81
|
+
|
|
82
|
+
export type PasskeyProviderListener = (...args: unknown[]) => void
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parsePasskeyWalletVaultPayload,
|
|
3
|
+
serializePasskeyWalletVaultPayload
|
|
4
|
+
} from './vault'
|
|
5
|
+
|
|
6
|
+
describe('passkey wallet vault payloads', () => {
|
|
7
|
+
it('serializes and parses mnemonic-backed vault payloads', () => {
|
|
8
|
+
const serialized = serializePasskeyWalletVaultPayload({
|
|
9
|
+
recoveryPhrase: 'test test test test test test test test test test test junk',
|
|
10
|
+
userEncryptionPrivateKey: { kty: 'EC' },
|
|
11
|
+
userEncryptionPublicKey: { kty: 'EC', x: 'x' },
|
|
12
|
+
userEncryptionKeyVersion: 42
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
expect(parsePasskeyWalletVaultPayload(serialized)).toMatchObject({
|
|
16
|
+
version: 1,
|
|
17
|
+
recoveryPhrase: 'test test test test test test test test test test test junk',
|
|
18
|
+
userEncryptionPrivateKey: { kty: 'EC' },
|
|
19
|
+
userEncryptionPublicKey: { kty: 'EC', x: 'x' },
|
|
20
|
+
userEncryptionKeyVersion: 42
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
})
|