@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/README.md ADDED
@@ -0,0 +1,427 @@
1
+ # Organigram Passkey Wallet
2
+
3
+ **The ultimate web3 experience: unlock wallets and sign transactions with passkeys in a
4
+ breeze.**
5
+
6
+ Organigram Passkey Wallet is a lightweight, headless, EIP-1193 wallet powered by
7
+ WebAuthn passkeys. It gives users the day-to-day experience they expect from
8
+ modern passwordless apps while preserving the core property that matters in
9
+ web3: fully and safely owning a standard Ethereum address.
10
+
11
+ No cumbersome smart account deployment. No per-network account ceremony. No
12
+ custodial key service. No protocol lock-in. Just a standard EOA, encrypted
13
+ client-side, unlocked with rotative passkeys, and exposed through a wallet interface the
14
+ Ethereum ecosystem already understands.
15
+
16
+ ```sh
17
+ pnpm add @organigram/passkey-wallet
18
+ ```
19
+
20
+ ## Web3 identity, solved
21
+
22
+ Most web3 onboarding still asks users to understand browser extensions, seed
23
+ phrases, hardware devices, account abstraction, gas sponsorship, recovery
24
+ contracts, network-specific deployments, signer permissions... before they have
25
+ even used the product. Organigram Passkey Wallet takes a simpler path.
26
+
27
+ The user creates a wallet in the browser. The wallet is a normal Ethereum EOA
28
+ derived from a recovery phrase. That recovery phrase is encrypted locally with a
29
+ key derived from a WebAuthn passkey PRF output, then stored only as an encrypted
30
+ vault envelope. When the user returns, their passkey unlocks the vault and the
31
+ EOA signs messages or transactions like any other wallet.
32
+
33
+ The result is a wallet experience that feels native to the web:
34
+
35
+ - passkeys for everyday unlocks;
36
+ - standard EIP-1193 semantics for dapps;
37
+ - one address across every EVM network;
38
+ - multiple backup passkeys for the same address;
39
+ - seed phrase export for disaster recovery;
40
+ - no onchain deployment required;
41
+ - no offchain signing relay or hosted signer to trust.
42
+
43
+ ## Highlights
44
+
45
+ ### Unlock and sign with passkeys
46
+
47
+ Users unlock the wallet with a WebAuthn passkey. Once unlocked, the wallet signs
48
+ messages, typed data, and transactions with the underlying EOA.
49
+
50
+ ### Fully-fledged EIP-1193 wallet
51
+
52
+ The wallet exposes the same interface dapps already expect from injected wallets,
53
+ WalletConnect wallets, and RainbowKit connectors. It supports account
54
+ requests, chain switching, message signing, typed data signing, and transaction
55
+ submission like any other standard wallet.
56
+
57
+ ### Multiple backup keys, one address
58
+
59
+ Users can add additional passkeys for the same address. A backup phone, laptop,
60
+ password manager, or security key can unlock the same encrypted wallet vault.
61
+
62
+ ### No smart-account deployment burden
63
+
64
+ The address is a standard EOA, so it exists everywhere EVM accounts exist. There
65
+ is no need to deploy account contracts across networks before the user can start.
66
+
67
+ ### Seed phrase recovery
68
+
69
+ Passkeys optimize daily use. Seed phrase export preserves portability and gives
70
+ users a disaster recovery path if every passkey is lost.
71
+
72
+ ### Headless and open source
73
+
74
+ The package provides wallet, WebAuthn, vault, provider, and RainbowKit building
75
+ blocks. You own the product surface. No hosted signer, no mandatory backend
76
+ service, no strings attached.
77
+
78
+ ## What You Get
79
+
80
+ Passkey Wallet is built for production UX:
81
+
82
+ - users unlock and sign with passkeys;
83
+ - backup passkeys can be added to the same address;
84
+ - compromised or obsolete passkeys can be rotated out by the host app;
85
+ - recovery phrases can be exported for disaster recovery;
86
+ - the same EOA works on every supported EVM chain without deploying anything;
87
+ - the package is headless, so product teams own the UI;
88
+ - all sensitive wallet material is encrypted client-side before persistence.
89
+
90
+ It is also intentionally boring where it should be boring: the address is a
91
+ standard EOA, the dapp integration is EIP-1193, the RainbowKit integration is a
92
+ wallet descriptor, and the WebAuthn server helpers are explicit about
93
+ registration, authentication, challenges, origins, RP IDs, counters, and vault
94
+ envelopes.
95
+
96
+ ## Architecture
97
+
98
+ ```txt
99
+ Browser
100
+ WebAuthn passkey with PRF
101
+ |
102
+ v
103
+ HKDF-derived vault key
104
+ |
105
+ v
106
+ AES-GCM encrypted wallet vault envelope
107
+ |
108
+ v
109
+ Recovery phrase -> EOA -> EIP-1193 provider
110
+
111
+ Server
112
+ WebAuthn challenges
113
+ Credential public keys
114
+ Credential counters
115
+ Encrypted vault envelopes
116
+ ```
117
+
118
+ In practice, the flow is:
119
+
120
+ 1. Generate a recovery phrase in the browser.
121
+ 2. Derive a standard Ethereum EOA from that phrase.
122
+ 3. Create a WebAuthn passkey with `residentKey: "required"`,
123
+ `userVerification: "required"`, and the `prf` extension.
124
+ 4. Derive a vault encryption key from the PRF output and a random salt.
125
+ 5. Encrypt the wallet vault with AES-GCM.
126
+ 6. Store only the credential metadata and encrypted vault envelope server-side.
127
+ 7. On unlock, verify WebAuthn, recover the PRF output, decrypt the envelope, and
128
+ expose the EOA through EIP-1193.
129
+
130
+ The private wallet material is generated and decrypted in the browser. The server
131
+ stores only WebAuthn verification material and encrypted vault envelopes. The
132
+ server never needs the seed phrase, the private key, or the decrypted user vault.
133
+
134
+ The passkey unlocks the local vault. The EOA signs Ethereum messages and
135
+ transactions. Those are separate responsibilities, which keeps the integration
136
+ simple and compatible with existing web3 infrastructure.
137
+
138
+ ## Package Exports
139
+
140
+ ```ts
141
+ import {
142
+ buildPasskeyWalletCapabilities,
143
+ buildEmailPasskeyCapabilities
144
+ } from '@organigram/passkey-wallet'
145
+
146
+ import {
147
+ createFetchPasskeyWalletApiClient,
148
+ unlockOrCreatePasskeyWallet,
149
+ registerAdditionalPasskeyCredential,
150
+ exportPasskeyWalletRecoveryPhrase
151
+ } from '@organigram/passkey-wallet/webauthn-client'
152
+
153
+ import {
154
+ createPasskeyRegistrationOptions,
155
+ createPasskeyAuthenticationOptions,
156
+ verifyPasskeyRegistration,
157
+ verifyPasskeyAuthentication
158
+ } from '@organigram/passkey-wallet/webauthn-server'
159
+
160
+ import {
161
+ createOrganigramPasskeyWallet,
162
+ isOrganigramPasskeyConnector
163
+ } from '@organigram/passkey-wallet/rainbowkit'
164
+ ```
165
+
166
+ The package is split by runtime boundary:
167
+
168
+ - `@organigram/passkey-wallet` exposes shared wallet, vault, and capability
169
+ types.
170
+ - `@organigram/passkey-wallet/webauthn-client` runs in the browser and performs
171
+ WebAuthn ceremonies, PRF extraction, vault encryption, vault decryption, and
172
+ wallet unlock.
173
+ - `@organigram/passkey-wallet/webauthn-server` runs on the server and verifies
174
+ WebAuthn registration/authentication responses.
175
+ - `@organigram/passkey-wallet/eip1193` exposes the provider implementation.
176
+ - `@organigram/passkey-wallet/rainbowkit` exposes a RainbowKit wallet adapter.
177
+
178
+ ## Quick Start
179
+
180
+ Create a browser API client:
181
+
182
+ ```ts
183
+ import {
184
+ createFetchPasskeyWalletApiClient,
185
+ unlockOrCreatePasskeyWallet
186
+ } from '@organigram/passkey-wallet/webauthn-client'
187
+ import { buildPasskeyWalletCapabilities } from '@organigram/passkey-wallet'
188
+
189
+ const api = createFetchPasskeyWalletApiClient('/api/auth/passkey')
190
+
191
+ const wallet = await unlockOrCreatePasskeyWallet({
192
+ api,
193
+ capabilities: buildPasskeyWalletCapabilities(),
194
+ targetChainId: 1
195
+ })
196
+
197
+ const signature = await wallet.account.signMessage({
198
+ message: 'gm'
199
+ })
200
+ ```
201
+
202
+ That is the headless primitive. Most apps will wrap it in the RainbowKit adapter
203
+ below so the passkey wallet appears next to other connection methods.
204
+
205
+ ## RainbowKit Integration
206
+
207
+ ```ts
208
+ import { connectorsForWallets } from '@rainbow-me/rainbowkit'
209
+ import { createConfig, http } from 'wagmi'
210
+ import { mainnet, sepolia } from 'wagmi/chains'
211
+ import { createOrganigramPasskeyWallet } from '@organigram/passkey-wallet/rainbowkit'
212
+ import {
213
+ createFetchPasskeyWalletApiClient,
214
+ unlockOrCreatePasskeyWallet,
215
+ registerAdditionalPasskeyCredential,
216
+ exportPasskeyWalletRecoveryPhrase
217
+ } from '@organigram/passkey-wallet/webauthn-client'
218
+
219
+ const api = createFetchPasskeyWalletApiClient('/api/auth/passkey')
220
+
221
+ const passkeyWallet = createOrganigramPasskeyWallet({
222
+ unlockOrCreatePasskeyWallet: async input =>
223
+ await unlockOrCreatePasskeyWallet({
224
+ api,
225
+ capabilities: input.capabilities,
226
+ targetChainId: input.targetChainId
227
+ }),
228
+ registerAdditionalPasskeyCredential: async input =>
229
+ await registerAdditionalPasskeyCredential({
230
+ api,
231
+ wallet: input.wallet,
232
+ name: input.name
233
+ }),
234
+ exportPasskeyWalletRecoveryPhrase: async input =>
235
+ await exportPasskeyWalletRecoveryPhrase({
236
+ api,
237
+ expectedAddress: input.expectedAddress
238
+ })
239
+ })
240
+
241
+ export const wagmiConfig = createConfig({
242
+ chains: [mainnet, sepolia],
243
+ connectors: connectorsForWallets(
244
+ [
245
+ {
246
+ groupName: 'Recommended',
247
+ wallets: [() => passkeyWallet]
248
+ }
249
+ ],
250
+ {
251
+ appName: 'Your App',
252
+ projectId: 'walletconnect-project-id'
253
+ }
254
+ ),
255
+ transports: {
256
+ [mainnet.id]: http(),
257
+ [sepolia.id]: http()
258
+ }
259
+ })
260
+ ```
261
+
262
+ From the dapp's perspective, this behaves like a wallet. Calling
263
+ `personal_sign`, `eth_signTypedData_v4`, or `eth_sendTransaction` goes through
264
+ the unlocked EOA account behind the passkey vault.
265
+
266
+ ## Server Responsibilities
267
+
268
+ This package does not hide persistence behind a hosted service. Your app owns the
269
+ server boundary.
270
+
271
+ At minimum, store:
272
+
273
+ - short-lived WebAuthn challenges;
274
+ - credential IDs;
275
+ - credential public keys;
276
+ - credential transports;
277
+ - signature counters;
278
+ - the wallet address linked to each credential;
279
+ - encrypted vault envelopes for each credential.
280
+
281
+ Typical endpoints:
282
+
283
+ ```txt
284
+ POST /api/auth/passkey/register/options
285
+ POST /api/auth/passkey/register/verify
286
+ POST /api/auth/passkey/unlock/options
287
+ POST /api/auth/passkey/unlock/verify
288
+ ```
289
+
290
+ The server helpers generate WebAuthn options and verify responses, but your app
291
+ decides how to persist credentials, consume challenges, enforce origin/RP ID
292
+ rules, and link credentials to users.
293
+
294
+ ## Suggested Data Model
295
+
296
+ The package does not require a specific database, but a production integration
297
+ usually needs records shaped like this:
298
+
299
+ ```ts
300
+ type PasskeyCredential = {
301
+ credentialId: string
302
+ userAddress: `0x${string}`
303
+ publicKey: string
304
+ transports: string[]
305
+ signCount: number
306
+ createdAt: Date
307
+ updatedAt: Date
308
+ }
309
+
310
+ type PasskeyVaultEnvelope = {
311
+ credentialId: string
312
+ userAddress: `0x${string}`
313
+ encryptedMnemonic: string
314
+ salt: string
315
+ nonce: string
316
+ algorithm: string
317
+ keyVersion: number
318
+ createdAt: Date
319
+ }
320
+
321
+ type PasskeyChallenge = {
322
+ challenge: string
323
+ type: 'registration' | 'authentication'
324
+ expiresAt: Date
325
+ consumedAt?: Date
326
+ }
327
+ ```
328
+
329
+ Use short challenge TTLs, consume challenges exactly once, and enforce uniqueness
330
+ for credential IDs.
331
+
332
+ ## Backup Keys And Rotation
333
+
334
+ A passkey wallet should never depend on a single authenticator.
335
+
336
+ Organigram Passkey Wallet supports adding another passkey to the same wallet by
337
+ re-encrypting the same vault payload for a new credential. The address does not
338
+ change. The user can keep using the same EOA while adding a laptop passkey, a
339
+ phone passkey, a password manager passkey, or a hardware security key.
340
+
341
+ Rotation is the same idea in reverse: remove obsolete credentials only when
342
+ another recovery method remains. The package gives you the wallet and envelope
343
+ building blocks; your product should decide the UX policy for deletion,
344
+ confirmation, and minimum backup requirements.
345
+
346
+ ## Recovery
347
+
348
+ Passkeys make daily wallet use simple, but users still need an escape hatch.
349
+
350
+ Organigram Passkey Wallet keeps the wallet portable by deriving the EOA from a
351
+ standard recovery phrase. If every passkey is lost, the user can recover with the
352
+ exported seed phrase in any compatible wallet. That is the difference between
353
+ passwordless convenience and platform lock-in.
354
+
355
+ The recovery phrase should always be exported before the user loses all passkeys.
356
+ The package provides a helper to export the recovery phrase after a successful
357
+ WebAuthn ceremony, but cannot do so after the access to the vault is lost.
358
+
359
+ ## Security Model
360
+
361
+ Organigram Passkey Wallet is non-custodial by design.
362
+
363
+ The browser creates the wallet. The browser encrypts the vault. The browser
364
+ decrypts the vault after a successful WebAuthn ceremony. The server stores
365
+ encrypted envelopes and verifies that WebAuthn responses are valid for the
366
+ expected challenge, origin, RP ID, and credential public key.
367
+
368
+ This model assumes:
369
+
370
+ - WebAuthn PRF support is required for the passkey vault flow;
371
+ - the host app serves the wallet UI from a trustworthy origin;
372
+ - the server correctly expires and consumes challenges;
373
+ - the server verifies credential counters and user verification;
374
+ - recovery phrase export is treated as a sensitive operation;
375
+ - backup passkey deletion never leaves the user without a recovery method;
376
+ - a compromised browser session can access unlocked wallet material while it is
377
+ in memory.
378
+
379
+ The package avoids custodial recovery, hosted signing, and smart-account
380
+ dependency by keeping the encrypted EOA vault offchain and user-portable.
381
+
382
+ ## Browser Compatibility
383
+
384
+ The passkey vault depends on the WebAuthn `prf` extension. Browsers or password
385
+ managers that do not return PRF output cannot unlock the encrypted vault.
386
+
387
+ Apps should present a clear fallback when PRF is unavailable:
388
+
389
+ - retry with another passkey provider;
390
+ - use a platform authenticator that supports PRF;
391
+ - connect an external crypto wallet instead.
392
+
393
+ ## What This Is Not
394
+
395
+ Organigram Passkey Wallet is not a smart account framework. It does not require
396
+ 4337 infrastructure, paymasters, bundlers, module registries, or per-chain
397
+ deployments.
398
+
399
+ It is not a custodial wallet. The server does not receive the recovery phrase or
400
+ private key in clear text.
401
+
402
+ It is not a hosted wallet service. The package is open source and headless; the
403
+ host app owns persistence, authentication policy, UI, and deployment.
404
+
405
+ It is not a replacement for hardware wallets in high-assurance custody flows. It
406
+ is designed for the safest mainstream web UX possible while staying portable and
407
+ EVM-compatible.
408
+
409
+ ## Philosophy
410
+
411
+ The safest web3 UX is the one users can actually complete.
412
+
413
+ Organigram Passkey Wallet keeps the cryptographic model understandable:
414
+
415
+ - the wallet is an EOA;
416
+ - the unlock method is a passkey;
417
+ - the backup is another passkey;
418
+ - the disaster recovery path is a seed phrase;
419
+ - the dapp interface is EIP-1193;
420
+ - the server stores encrypted envelopes, not keys.
421
+
422
+ That is the whole point: the most frictionless, simplest, and safest UX possible
423
+ for web3, without strings attached.
424
+
425
+ ## License
426
+
427
+ MIT
@@ -0,0 +1,19 @@
1
+ export const createEncryptionKeyVersion = (): number => 1
2
+
3
+ export const generateUserEncryptionKeyPair = async (): Promise<CryptoKeyPair> =>
4
+ await crypto.subtle.generateKey(
5
+ {
6
+ name: 'ECDH',
7
+ namedCurve: 'P-256'
8
+ },
9
+ true,
10
+ ['deriveKey']
11
+ )
12
+
13
+ export const exportUserPrivateKey = async (
14
+ keyPair: CryptoKeyPair
15
+ ): Promise<JsonWebKey> => await crypto.subtle.exportKey('jwk', keyPair.privateKey)
16
+
17
+ export const exportUserPublicKey = async (
18
+ keyPair: CryptoKeyPair
19
+ ): Promise<JsonWebKey> => await crypto.subtle.exportKey('jwk', keyPair.publicKey)
@@ -0,0 +1 @@
1
+ export const createConnector = (factory: unknown): unknown => factory
@@ -0,0 +1,39 @@
1
+ export declare const passkeyVaultAlgorithm = "AES-GCM-HKDF-SHA-256";
2
+ export declare const passkeyVaultKeyVersion = 1;
3
+ export type PasskeyVaultEnvelopeData = {
4
+ algorithm: typeof passkeyVaultAlgorithm;
5
+ keyVersion: typeof passkeyVaultKeyVersion;
6
+ ciphertext: string;
7
+ salt: string;
8
+ nonce: string;
9
+ };
10
+ type DerivePasskeyVaultKeyInput = {
11
+ prfOutput: Uint8Array;
12
+ salt: Uint8Array;
13
+ };
14
+ type EncryptPasskeyVaultSecretInput = {
15
+ plaintext: string;
16
+ key: CryptoKey;
17
+ salt: Uint8Array;
18
+ nonce?: Uint8Array;
19
+ };
20
+ type DecryptPasskeyVaultSecretInput = {
21
+ envelope: PasskeyVaultEnvelopeData;
22
+ key: CryptoKey;
23
+ };
24
+ type PrfSupportCredential = {
25
+ getClientExtensionResults: () => {
26
+ prf?: {
27
+ enabled?: boolean;
28
+ };
29
+ };
30
+ };
31
+ export declare const bytesToBase64Url: (bytes: Uint8Array) => string;
32
+ export declare const base64UrlToBytes: (value: string) => Uint8Array;
33
+ export declare const derivePasskeyVaultKey: ({ prfOutput, salt }: DerivePasskeyVaultKeyInput) => Promise<CryptoKey>;
34
+ export declare const encryptPasskeyVaultSecret: ({ plaintext, key, salt, nonce }: EncryptPasskeyVaultSecretInput) => Promise<PasskeyVaultEnvelopeData>;
35
+ export declare const decryptPasskeyVaultSecret: ({ envelope, key }: DecryptPasskeyVaultSecretInput) => Promise<string>;
36
+ export declare const isPasskeyPrfSupported: ({ credential }: {
37
+ credential: PrfSupportCredential | null;
38
+ }) => Promise<boolean>;
39
+ export {};
package/dist/crypto.js ADDED
@@ -0,0 +1,95 @@
1
+ export const passkeyVaultAlgorithm = 'AES-GCM-HKDF-SHA-256';
2
+ export const passkeyVaultKeyVersion = 1;
3
+ const textEncoder = new TextEncoder();
4
+ const textDecoder = new TextDecoder();
5
+ const getCrypto = () => {
6
+ if (globalThis.crypto == null) {
7
+ throw new Error('WebCrypto is not available in this environment.');
8
+ }
9
+ return globalThis.crypto;
10
+ };
11
+ const toArrayBuffer = (bytes) => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
12
+ export const bytesToBase64Url = (bytes) => bytesToBase64(bytes)
13
+ .replaceAll('+', '-')
14
+ .replaceAll('/', '_')
15
+ .replaceAll('=', '');
16
+ export const base64UrlToBytes = (value) => {
17
+ const normalized = value.replaceAll('-', '+').replaceAll('_', '/');
18
+ const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), '=');
19
+ return base64ToBytes(padded);
20
+ };
21
+ const bytesToBase64 = (bytes) => {
22
+ if (typeof Buffer !== 'undefined') {
23
+ return Buffer.from(bytes).toString('base64');
24
+ }
25
+ let binary = '';
26
+ bytes.forEach(byte => {
27
+ binary += String.fromCharCode(byte);
28
+ });
29
+ return btoa(binary);
30
+ };
31
+ const base64ToBytes = (value) => {
32
+ if (typeof Buffer !== 'undefined') {
33
+ return new Uint8Array(Buffer.from(value, 'base64'));
34
+ }
35
+ const binary = atob(value);
36
+ const bytes = new Uint8Array(binary.length);
37
+ for (let index = 0; index < binary.length; index += 1) {
38
+ bytes[index] = binary.charCodeAt(index);
39
+ }
40
+ return bytes;
41
+ };
42
+ export const derivePasskeyVaultKey = async ({ prfOutput, salt }) => {
43
+ if (prfOutput.byteLength !== 32) {
44
+ throw new Error('Passkey PRF output must be 32 bytes.');
45
+ }
46
+ if (salt.byteLength !== 32) {
47
+ throw new Error('Passkey vault salt must be 32 bytes.');
48
+ }
49
+ const crypto = getCrypto();
50
+ const baseKey = await crypto.subtle.importKey('raw', toArrayBuffer(prfOutput), 'HKDF', false, ['deriveKey']);
51
+ return await crypto.subtle.deriveKey({
52
+ name: 'HKDF',
53
+ hash: 'SHA-256',
54
+ salt: toArrayBuffer(salt),
55
+ info: textEncoder.encode('organigram-passkey-wallet-v1')
56
+ }, baseKey, {
57
+ name: 'AES-GCM',
58
+ length: 256
59
+ }, false, ['encrypt', 'decrypt']);
60
+ };
61
+ export const encryptPasskeyVaultSecret = async ({ plaintext, key, salt, nonce = getCrypto().getRandomValues(new Uint8Array(12)) }) => {
62
+ if (plaintext.trim() === '') {
63
+ throw new Error('Passkey wallet vault plaintext cannot be empty.');
64
+ }
65
+ const ciphertext = await getCrypto().subtle.encrypt({
66
+ name: 'AES-GCM',
67
+ iv: toArrayBuffer(nonce)
68
+ }, key, textEncoder.encode(plaintext));
69
+ return {
70
+ algorithm: passkeyVaultAlgorithm,
71
+ keyVersion: passkeyVaultKeyVersion,
72
+ ciphertext: bytesToBase64Url(new Uint8Array(ciphertext)),
73
+ salt: bytesToBase64Url(salt),
74
+ nonce: bytesToBase64Url(nonce)
75
+ };
76
+ };
77
+ export const decryptPasskeyVaultSecret = async ({ envelope, key }) => {
78
+ try {
79
+ if (envelope.algorithm !== passkeyVaultAlgorithm) {
80
+ throw new Error('Unsupported passkey wallet vault algorithm.');
81
+ }
82
+ if (envelope.keyVersion !== passkeyVaultKeyVersion) {
83
+ throw new Error('Unsupported passkey wallet vault key version.');
84
+ }
85
+ const plaintext = await getCrypto().subtle.decrypt({
86
+ name: 'AES-GCM',
87
+ iv: toArrayBuffer(base64UrlToBytes(envelope.nonce))
88
+ }, key, toArrayBuffer(base64UrlToBytes(envelope.ciphertext)));
89
+ return textDecoder.decode(plaintext);
90
+ }
91
+ catch (error) {
92
+ throw new Error('Unable to decrypt passkey wallet.', { cause: error });
93
+ }
94
+ };
95
+ export const isPasskeyPrfSupported = async ({ credential }) => credential?.getClientExtensionResults().prf?.enabled === true;
@@ -0,0 +1,24 @@
1
+ import type { Chain, EIP1193RequestFn } from 'viem';
2
+ import type { PasskeyProviderEvent, PasskeyProviderListener, PasskeyRegistrationResult, UnlockedPasskeyWallet } from './types';
3
+ export type OrganigramPasskeyProvider = {
4
+ request: EIP1193RequestFn;
5
+ on: (event: PasskeyProviderEvent, listener: PasskeyProviderListener) => void;
6
+ removeListener: (event: PasskeyProviderEvent, listener: PasskeyProviderListener) => void;
7
+ };
8
+ export type OrganigramPasskeyProviderActions = {
9
+ registerAdditionalPasskeyCredential: (input: {
10
+ wallet: UnlockedPasskeyWallet;
11
+ name?: string;
12
+ }) => Promise<PasskeyRegistrationResult>;
13
+ exportPasskeyWalletRecoveryPhrase: (input: {
14
+ expectedAddress: `0x${string}`;
15
+ }) => Promise<string>;
16
+ };
17
+ export declare const createPasskeyWalletProvider: ({ wallet, chain, rpcUrl, switchChain, actions }: {
18
+ wallet: UnlockedPasskeyWallet;
19
+ chain: Chain;
20
+ rpcUrl: string;
21
+ switchChain: (chainId: number) => Chain;
22
+ actions: OrganigramPasskeyProviderActions;
23
+ }) => OrganigramPasskeyProvider;
24
+ export declare const normalizePasskeyWalletAddress: (address: `0x${string}`) => `0x${string}`;