@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.
Files changed (79) hide show
  1. package/.organigram-docs-cache/passkey-wallet/api-extractor.json +35 -0
  2. package/.organigram-docs-cache/passkey-wallet/api-model/passkey-wallet.api.json +3166 -0
  3. package/.organigram-docs-cache/passkey-wallet/entry/index.d.ts +9 -0
  4. package/.organigram-docs-cache/passkey-wallet/tmp/node-compile-cache/v24.12.0-arm64-cf738c9d-501/05732508 +0 -0
  5. package/.organigram-docs-cache/passkey-wallet/tmp/node-compile-cache/v24.12.0-arm64-cf738c9d-501/166b6dbf +0 -0
  6. package/.organigram-docs-cache/passkey-wallet/tmp/node-compile-cache/v24.12.0-arm64-cf738c9d-501/41c20e23 +0 -0
  7. package/.organigram-docs-cache/passkey-wallet/tmp/node-compile-cache/v24.12.0-arm64-cf738c9d-501/7111ea58 +0 -0
  8. package/.organigram-docs-cache/passkey-wallet/tmp/tsx-501/17844-1822dfbab9819e9f6a93485fe3da5dbf3745311c +1 -0
  9. package/.organigram-docs-cache/passkey-wallet/tmp/tsx-501/17844-45a65cfdc7a50c53817c1dad3954ae3d2d2c35fb +1 -0
  10. package/.organigram-docs-cache/passkey-wallet/tmp/tsx-501/17844-fee5290b05398fe9e3b878deefd22383744f1526 +1 -0
  11. package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/createorganigrampasskeywalletinput.yml +17 -0
  12. package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/identitypasskeycapabilities.yml +16 -0
  13. package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/organigrampasskeycapabilities.yml +18 -0
  14. package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/organigrampasskeyprovider.yml +16 -0
  15. package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/organigrampasskeyprovideractions.yml +20 -0
  16. package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeyprfunavailableerror.yml +28 -0
  17. package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeyproviderevent.yml +11 -0
  18. package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeyproviderlistener.yml +11 -0
  19. package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeyregistrationresult.yml +15 -0
  20. package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeyvaultdecrypterror.yml +28 -0
  21. package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeyvaultenvelopedata.yml +18 -0
  22. package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeyvaultenvelopeinput.yml +19 -0
  23. package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeywalletapiclient.yml +33 -0
  24. package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeywalletcapabilities.yml +11 -0
  25. package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeywalletlogincapabilities.yml +14 -0
  26. package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeywalletregistrationcapabilities.yml +18 -0
  27. package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeywalletvaultpayload.yml +18 -0
  28. package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/unlockedpasskeywallet.yml +21 -0
  29. package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet.yml +870 -0
  30. package/.organigram-docs-cache/passkey-wallet/yaml/toc.yml +43 -0
  31. package/.turbo/turbo-build.log +6 -0
  32. package/.turbo/turbo-docs$colon$generate.log +118 -0
  33. package/.turbo/turbo-lint.log +13 -0
  34. package/.turbo/turbo-test.log +152 -0
  35. package/.turbo/turbo-typecheck.log +5 -0
  36. package/CHANGELOG.md +8 -0
  37. package/LICENSE.md +21 -0
  38. package/README.md +427 -0
  39. package/__mocks__/organigram-js.ts +19 -0
  40. package/__mocks__/wagmi.ts +1 -0
  41. package/dist/crypto.d.ts +39 -0
  42. package/dist/crypto.js +95 -0
  43. package/dist/eip1193.d.ts +24 -0
  44. package/dist/eip1193.js +103 -0
  45. package/dist/errors.d.ts +7 -0
  46. package/dist/errors.js +13 -0
  47. package/dist/index.d.ts +4 -0
  48. package/dist/index.js +4 -0
  49. package/dist/rainbowkit.d.ts +14 -0
  50. package/dist/rainbowkit.js +128 -0
  51. package/dist/types.d.ts +45 -0
  52. package/dist/types.js +17 -0
  53. package/dist/vault.d.ts +10 -0
  54. package/dist/vault.js +41 -0
  55. package/dist/wallet.d.ts +15 -0
  56. package/dist/wallet.js +30 -0
  57. package/dist/webauthn-client.d.ts +73 -0
  58. package/dist/webauthn-client.js +234 -0
  59. package/dist/webauthn-server.d.ts +62 -0
  60. package/dist/webauthn-server.js +133 -0
  61. package/jest.config.ts +32 -0
  62. package/package.json +61 -0
  63. package/src/crypto.test.ts +69 -0
  64. package/src/crypto.ts +193 -0
  65. package/src/eip1193.ts +169 -0
  66. package/src/errors.ts +16 -0
  67. package/src/index.ts +4 -0
  68. package/src/rainbowkit.test.ts +42 -0
  69. package/src/rainbowkit.ts +171 -0
  70. package/src/types.ts +82 -0
  71. package/src/vault.test.ts +23 -0
  72. package/src/vault.ts +77 -0
  73. package/src/wallet.ts +61 -0
  74. package/src/webauthn-client.test.ts +103 -0
  75. package/src/webauthn-client.ts +430 -0
  76. package/src/webauthn-server.test.ts +43 -0
  77. package/src/webauthn-server.ts +258 -0
  78. package/tsconfig.build.json +15 -0
  79. 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,4 @@
1
+ export * from './crypto'
2
+ export * from './types'
3
+ export * from './vault'
4
+ export * from './wallet'
@@ -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
+ })