@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
@@ -0,0 +1,234 @@
1
+ import { startAuthentication, startRegistration } from '@simplewebauthn/browser';
2
+ import { base64UrlToBytes, decryptPasskeyVaultSecret, derivePasskeyVaultKey, encryptPasskeyVaultSecret } from './crypto';
3
+ import { PasskeyPrfUnavailableError } from './errors';
4
+ import { createNewPasskeyWalletVault, createUnlockedPasskeyWallet } from './wallet';
5
+ import { parsePasskeyWalletVaultPayload, serializePasskeyWalletVaultPayload } from './vault';
6
+ export { parsePasskeyWalletVaultPayload, serializePasskeyWalletVaultPayload };
7
+ const readJson = async (response) => {
8
+ const body = (await response.json().catch(() => null));
9
+ if (!response.ok) {
10
+ throw new Error(body?.error ?? `Request failed with status ${response.status}`);
11
+ }
12
+ return body;
13
+ };
14
+ const postJson = async (path, body) => {
15
+ const response = await fetch(path, {
16
+ method: 'POST',
17
+ headers: {
18
+ 'Content-Type': 'application/json'
19
+ },
20
+ body: JSON.stringify(body)
21
+ });
22
+ return await readJson(response);
23
+ };
24
+ export const createFetchPasskeyWalletApiClient = (basePath = '/api/auth/passkey') => ({
25
+ registerOptions: async (input) => await postJson(`${basePath}/register/options`, input),
26
+ registerVerify: async (input) => await postJson(`${basePath}/register/verify`, input),
27
+ unlockOptions: async () => await postJson(`${basePath}/unlock/options`, {}),
28
+ unlockVerify: async (input) => await postJson(`${basePath}/unlock/verify`, input)
29
+ });
30
+ const hydrateSerializedPrfValue = (value) => {
31
+ if (value == null)
32
+ return value;
33
+ if (typeof value === 'string')
34
+ return base64UrlToBytes(value);
35
+ if (value instanceof ArrayBuffer)
36
+ return new Uint8Array(value);
37
+ if (ArrayBuffer.isView(value)) {
38
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
39
+ }
40
+ if (Array.isArray(value))
41
+ return new Uint8Array(value);
42
+ if (typeof value === 'object') {
43
+ const entries = Object.entries(value);
44
+ if (entries.every(([key, entryValue]) => /^\d+$/.test(key) && typeof entryValue === 'number')) {
45
+ return new Uint8Array(entries
46
+ .sort(([left], [right]) => Number(left) - Number(right))
47
+ .map(([, entryValue]) => entryValue));
48
+ }
49
+ }
50
+ return value;
51
+ };
52
+ const hydratePrfValueSet = (valueSet) => {
53
+ if (valueSet == null)
54
+ return valueSet;
55
+ return {
56
+ ...valueSet,
57
+ first: hydrateSerializedPrfValue(valueSet.first),
58
+ ...(valueSet.second != null
59
+ ? {
60
+ second: hydrateSerializedPrfValue(valueSet.second)
61
+ }
62
+ : {})
63
+ };
64
+ };
65
+ export const hydratePasskeyPrfOptions = (options) => {
66
+ const extensions = options.extensions;
67
+ const prf = extensions?.prf;
68
+ if (prf == null)
69
+ return options;
70
+ return {
71
+ ...options,
72
+ extensions: {
73
+ ...extensions,
74
+ prf: {
75
+ ...prf,
76
+ ...(prf.eval != null ? { eval: hydratePrfValueSet(prf.eval) } : {}),
77
+ ...(prf.evalByCredential != null
78
+ ? {
79
+ evalByCredential: Object.fromEntries(Object.entries(prf.evalByCredential).map(([credentialId, valueSet]) => [
80
+ credentialId,
81
+ hydratePrfValueSet(valueSet)
82
+ ]))
83
+ }
84
+ : {})
85
+ }
86
+ }
87
+ };
88
+ };
89
+ const getPasskeyPrfOutput = (response) => {
90
+ const first = response.clientExtensionResults?.prf?.results?.first;
91
+ if (first == null || first === '') {
92
+ throw new PasskeyPrfUnavailableError();
93
+ }
94
+ return typeof first === 'string'
95
+ ? base64UrlToBytes(first)
96
+ : new Uint8Array(first);
97
+ };
98
+ export async function registerPasskeyCredentialEnvelope({ api, address, vaultPayload, email, name }) {
99
+ const { options } = await api.registerOptions({
100
+ address,
101
+ email,
102
+ name
103
+ });
104
+ const registrationResponse = await startRegistration({
105
+ optionsJSON: hydratePasskeyPrfOptions(options)
106
+ });
107
+ const prfOutput = getPasskeyPrfOutput(registrationResponse);
108
+ const salt = crypto.getRandomValues(new Uint8Array(32));
109
+ const key = await derivePasskeyVaultKey({
110
+ prfOutput,
111
+ salt
112
+ });
113
+ const envelope = await encryptPasskeyVaultSecret({
114
+ plaintext: serializePasskeyWalletVaultPayload(vaultPayload),
115
+ key,
116
+ salt
117
+ });
118
+ return await api.registerVerify({
119
+ response: registrationResponse,
120
+ envelope: {
121
+ address,
122
+ encryptedMnemonic: envelope.ciphertext,
123
+ salt: envelope.salt,
124
+ nonce: envelope.nonce,
125
+ algorithm: envelope.algorithm,
126
+ keyVersion: envelope.keyVersion
127
+ }
128
+ });
129
+ }
130
+ export const registerPasskeyWallet = async ({ api, capabilities }) => {
131
+ const { address, vaultPayload } = await createNewPasskeyWalletVault({
132
+ capabilities
133
+ });
134
+ const credential = await registerPasskeyCredentialEnvelope({
135
+ api,
136
+ address,
137
+ vaultPayload,
138
+ email: capabilities.identity?.email ?? null,
139
+ name: capabilities.name ?? capabilities.identity?.email ?? null
140
+ });
141
+ return createUnlockedPasskeyWallet({
142
+ address: credential.address,
143
+ credentialId: credential.credentialId,
144
+ vaultPayload
145
+ });
146
+ };
147
+ export const unlockPasskeyWallet = async ({ api }) => {
148
+ const { options, hasCredentials } = await api.unlockOptions();
149
+ if (!hasCredentials)
150
+ return null;
151
+ const authenticationResponse = await startAuthentication({
152
+ optionsJSON: hydratePasskeyPrfOptions(options)
153
+ });
154
+ const prfOutput = getPasskeyPrfOutput(authenticationResponse);
155
+ const result = await api.unlockVerify({
156
+ response: authenticationResponse
157
+ });
158
+ const key = await derivePasskeyVaultKey({
159
+ prfOutput,
160
+ salt: base64UrlToBytes(result.envelope.salt)
161
+ });
162
+ const vaultPlaintext = await decryptPasskeyVaultSecret({
163
+ envelope: result.envelope,
164
+ key
165
+ });
166
+ const vaultPayload = parsePasskeyWalletVaultPayload(vaultPlaintext);
167
+ const wallet = createUnlockedPasskeyWallet({
168
+ address: result.address,
169
+ credentialId: result.credentialId,
170
+ vaultPayload
171
+ });
172
+ if (wallet.account.address.toLowerCase() !== result.address.toLowerCase()) {
173
+ throw new Error('Passkey wallet envelope does not match its address.');
174
+ }
175
+ return wallet;
176
+ };
177
+ export const exportPasskeyWalletRecoveryPhrase = async ({ api, expectedAddress }) => {
178
+ const wallet = await unlockPasskeyWallet({ api });
179
+ if (wallet == null) {
180
+ throw new Error('No passkey wallet is available to export.');
181
+ }
182
+ if (wallet.address.toLowerCase() !== expectedAddress.toLowerCase()) {
183
+ throw new Error('Selected passkey does not unlock this wallet.');
184
+ }
185
+ return wallet.recoveryPhrase;
186
+ };
187
+ export const registerAdditionalPasskeyCredential = async ({ api, wallet, name }) => await registerPasskeyCredentialEnvelope({
188
+ api,
189
+ address: wallet.address,
190
+ vaultPayload: {
191
+ version: 1,
192
+ recoveryPhrase: wallet.recoveryPhrase,
193
+ userEncryptionPrivateKey: wallet.userEncryptionPrivateKey,
194
+ userEncryptionPublicKey: wallet.userEncryptionPublicKey,
195
+ userEncryptionKeyVersion: wallet.userEncryptionKeyVersion
196
+ },
197
+ name: name ?? 'Backup passkey'
198
+ });
199
+ export const isPasskeyCredentialUnavailableError = (error) => {
200
+ if (typeof error !== 'object' || error == null)
201
+ return false;
202
+ const { name, message } = error;
203
+ const normalizedName = typeof name === 'string' ? name.toLowerCase() : '';
204
+ const normalizedMessage = typeof message === 'string' ? message.toLowerCase() : '';
205
+ return (normalizedName === 'notallowederror' ||
206
+ normalizedName === 'invalidstateerror' ||
207
+ normalizedMessage.includes('no credentials') ||
208
+ normalizedMessage.includes('not allowed') ||
209
+ normalizedMessage.includes('could not be completed'));
210
+ };
211
+ export const unlockOrCreatePasskeyWallet = async ({ api, capabilities }) => {
212
+ if (typeof window === 'undefined' || window.PublicKeyCredential == null) {
213
+ throw new Error('Passkeys are not available in this browser.');
214
+ }
215
+ if (capabilities.method !== 'register') {
216
+ try {
217
+ const wallet = await unlockPasskeyWallet({ api });
218
+ if (wallet != null)
219
+ return wallet;
220
+ }
221
+ catch (error) {
222
+ if (!isPasskeyCredentialUnavailableError(error)) {
223
+ throw error;
224
+ }
225
+ }
226
+ }
227
+ return await registerPasskeyWallet({
228
+ api,
229
+ capabilities: {
230
+ ...capabilities,
231
+ method: 'register'
232
+ }
233
+ });
234
+ };
@@ -0,0 +1,62 @@
1
+ import { verifyAuthenticationResponse, verifyRegistrationResponse } from '@simplewebauthn/server';
2
+ import { type PasskeyVaultEnvelopeData } from './crypto';
3
+ export declare const passkeyChallengeTtlMs: number;
4
+ export type PasskeyVaultEnvelopeInput = {
5
+ address: string;
6
+ encryptedMnemonic: string;
7
+ salt: string;
8
+ nonce: string;
9
+ algorithm: string;
10
+ keyVersion: number;
11
+ };
12
+ export declare const validatePasskeyVaultEnvelopeInput: (input: PasskeyVaultEnvelopeInput) => PasskeyVaultEnvelopeInput & {
13
+ address: `0x${string}`;
14
+ };
15
+ export declare const createPasskeyChallengeExpiry: () => Date;
16
+ export declare const getWebAuthnClientDataChallenge: (response: unknown) => string;
17
+ export declare const createPasskeyRegistrationOptions: ({ rpId, userAddress, email }: {
18
+ rpId: string;
19
+ userAddress?: string | null;
20
+ email?: string | null;
21
+ }) => Promise<import("@simplewebauthn/server").PublicKeyCredentialCreationOptionsJSON>;
22
+ type VerifiedPasskeyRegistrationResponse = Awaited<ReturnType<typeof verifyRegistrationResponse>> & {
23
+ registrationInfo: NonNullable<Awaited<ReturnType<typeof verifyRegistrationResponse>>['registrationInfo']>;
24
+ };
25
+ export declare const verifyPasskeyRegistration: ({ response, expectedChallenge, expectedOrigin, expectedRpId, envelope }: {
26
+ response: unknown;
27
+ expectedChallenge: string;
28
+ expectedOrigin: string;
29
+ expectedRpId: string;
30
+ envelope: PasskeyVaultEnvelopeInput;
31
+ }) => Promise<{
32
+ verification: VerifiedPasskeyRegistrationResponse;
33
+ vaultEnvelope: PasskeyVaultEnvelopeInput & {
34
+ address: `0x${string}`;
35
+ };
36
+ }>;
37
+ export declare const createPasskeyAuthenticationOptions: ({ rpId, credentials }: {
38
+ rpId: string;
39
+ credentials?: Array<{
40
+ credentialId: string;
41
+ transports: string[];
42
+ }>;
43
+ }) => Promise<import("@simplewebauthn/server").PublicKeyCredentialRequestOptionsJSON>;
44
+ export declare const verifyPasskeyAuthentication: ({ response, expectedChallenge, expectedOrigin, expectedRpId, credential }: {
45
+ response: unknown;
46
+ expectedChallenge: string;
47
+ expectedOrigin: string;
48
+ expectedRpId: string;
49
+ credential: {
50
+ credentialId: string;
51
+ publicKey: string;
52
+ signCount: number;
53
+ };
54
+ }) => Promise<Awaited<ReturnType<typeof verifyAuthenticationResponse>>>;
55
+ export declare const toPasskeyVaultEnvelopeData: ({ encryptedMnemonic, salt, nonce, algorithm, keyVersion }: {
56
+ encryptedMnemonic: string;
57
+ salt: string;
58
+ nonce: string;
59
+ algorithm: string;
60
+ keyVersion: number;
61
+ }) => PasskeyVaultEnvelopeData;
62
+ export {};
@@ -0,0 +1,133 @@
1
+ import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse } from '@simplewebauthn/server';
2
+ import { getAddress, isAddress } from 'viem';
3
+ import { base64UrlToBytes, passkeyVaultAlgorithm, passkeyVaultKeyVersion } from './crypto';
4
+ export const passkeyChallengeTtlMs = 5 * 60 * 1000;
5
+ export const validatePasskeyVaultEnvelopeInput = (input) => {
6
+ if (!isAddress(input.address)) {
7
+ throw new Error('Passkey vault envelope requires a valid wallet address.');
8
+ }
9
+ if (input.encryptedMnemonic.trim() === '') {
10
+ throw new Error('Passkey vault envelope requires encrypted recovery phrase data.');
11
+ }
12
+ if (input.salt.trim() === '') {
13
+ throw new Error('Passkey vault envelope requires a salt.');
14
+ }
15
+ if (input.nonce.trim() === '') {
16
+ throw new Error('Passkey vault envelope requires a nonce.');
17
+ }
18
+ if (input.algorithm !== passkeyVaultAlgorithm) {
19
+ throw new Error('Passkey vault envelope algorithm is not supported.');
20
+ }
21
+ if (input.keyVersion !== passkeyVaultKeyVersion) {
22
+ throw new Error('Passkey vault envelope key version is not supported.');
23
+ }
24
+ return {
25
+ ...input,
26
+ address: getAddress(input.address)
27
+ };
28
+ };
29
+ export const createPasskeyChallengeExpiry = () => new Date(Date.now() + passkeyChallengeTtlMs);
30
+ export const getWebAuthnClientDataChallenge = (response) => {
31
+ const clientDataJSON = response?.response?.clientDataJSON;
32
+ if (typeof clientDataJSON !== 'string' || clientDataJSON === '') {
33
+ throw new Error('WebAuthn response is missing client data.');
34
+ }
35
+ const clientData = JSON.parse(new TextDecoder().decode(base64UrlToBytes(clientDataJSON)));
36
+ if (typeof clientData.challenge !== 'string' || clientData.challenge === '') {
37
+ throw new Error('WebAuthn response is missing its challenge.');
38
+ }
39
+ return clientData.challenge;
40
+ };
41
+ export const createPasskeyRegistrationOptions = async ({ rpId, userAddress, email }) => await generateRegistrationOptions({
42
+ rpName: 'Organigram',
43
+ rpID: rpId,
44
+ userName: email ?? userAddress ?? 'Organigram passkey wallet',
45
+ userDisplayName: email ?? userAddress ?? 'Organigram passkey wallet',
46
+ attestationType: 'none',
47
+ authenticatorSelection: {
48
+ residentKey: 'required',
49
+ userVerification: 'required'
50
+ },
51
+ extensions: {
52
+ prf: {
53
+ eval: {
54
+ first: new Uint8Array(32)
55
+ }
56
+ }
57
+ }
58
+ });
59
+ export const verifyPasskeyRegistration = async ({ response, expectedChallenge, expectedOrigin, expectedRpId, envelope }) => {
60
+ const vaultEnvelope = validatePasskeyVaultEnvelopeInput(envelope);
61
+ const verification = await verifyRegistrationResponse({
62
+ response: response,
63
+ expectedChallenge,
64
+ expectedOrigin,
65
+ expectedRPID: expectedRpId,
66
+ requireUserVerification: true
67
+ });
68
+ if (!verification.verified || verification.registrationInfo == null) {
69
+ throw new Error('Passkey registration was not verified.');
70
+ }
71
+ return {
72
+ verification: verification,
73
+ vaultEnvelope
74
+ };
75
+ };
76
+ export const createPasskeyAuthenticationOptions = async ({ rpId, credentials = [] }) => {
77
+ const isDiscoverableAuthentication = credentials.length === 0;
78
+ return await generateAuthenticationOptions({
79
+ rpID: rpId,
80
+ userVerification: 'required',
81
+ ...(!isDiscoverableAuthentication
82
+ ? {
83
+ allowCredentials: credentials.map(credential => ({
84
+ id: credential.credentialId,
85
+ transports: credential.transports
86
+ }))
87
+ }
88
+ : {}),
89
+ extensions: {
90
+ prf: isDiscoverableAuthentication
91
+ ? {
92
+ eval: {
93
+ first: new Uint8Array(32)
94
+ }
95
+ }
96
+ : {
97
+ evalByCredential: Object.fromEntries(credentials.map(credential => [
98
+ credential.credentialId,
99
+ {
100
+ first: new Uint8Array(32)
101
+ }
102
+ ]))
103
+ }
104
+ }
105
+ });
106
+ };
107
+ export const verifyPasskeyAuthentication = async ({ response, expectedChallenge, expectedOrigin, expectedRpId, credential }) => await verifyAuthenticationResponse({
108
+ response: response,
109
+ expectedChallenge,
110
+ expectedOrigin,
111
+ expectedRPID: expectedRpId,
112
+ credential: {
113
+ id: credential.credentialId,
114
+ publicKey: Buffer.from(credential.publicKey, 'base64url'),
115
+ counter: credential.signCount
116
+ },
117
+ requireUserVerification: true
118
+ });
119
+ export const toPasskeyVaultEnvelopeData = ({ encryptedMnemonic, salt, nonce, algorithm, keyVersion }) => {
120
+ if (algorithm !== passkeyVaultAlgorithm) {
121
+ throw new Error('Passkey vault envelope algorithm is not supported.');
122
+ }
123
+ if (keyVersion !== passkeyVaultKeyVersion) {
124
+ throw new Error('Passkey vault envelope key version is not supported.');
125
+ }
126
+ return {
127
+ algorithm,
128
+ keyVersion,
129
+ ciphertext: encryptedMnemonic,
130
+ salt,
131
+ nonce
132
+ };
133
+ };
package/jest.config.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { Config } from 'jest'
2
+
3
+ const config: Config = {
4
+ preset: 'ts-jest/presets/default-esm',
5
+ testEnvironment: 'node',
6
+ watchman: false,
7
+ extensionsToTreatAsEsm: ['.ts'],
8
+ moduleNameMapper: {
9
+ '^(\\.{1,2}/.*)\\.js$': '$1',
10
+ '^@organigram/js$': '<rootDir>/__mocks__/organigram-js.ts',
11
+ '^wagmi$': '<rootDir>/__mocks__/wagmi.ts'
12
+ },
13
+ transform: {
14
+ '^.+\\.tsx?$': [
15
+ 'ts-jest',
16
+ {
17
+ tsconfig: {
18
+ module: 'ESNext',
19
+ moduleResolution: 'bundler',
20
+ target: 'ESNext',
21
+ resolveJsonModule: true,
22
+ esModuleInterop: true,
23
+ types: ['jest', 'node']
24
+ },
25
+ diagnostics: false,
26
+ useESM: true
27
+ }
28
+ ]
29
+ }
30
+ }
31
+
32
+ export default config
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@organigram/passkey-wallet",
3
+ "version": "0.1.6",
4
+ "description": "Organigram Passkey Wallet.",
5
+ "author": "Organigram (https://organigram.ai)",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "ssh://git@github.com:organigram/organigram.git",
10
+ "directory": "packages/passkey-wallet"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": "./dist/index.js",
19
+ "./crypto": "./dist/crypto.js",
20
+ "./vault": "./dist/vault.js",
21
+ "./webauthn-client": "./dist/webauthn-client.js",
22
+ "./webauthn-server": "./dist/webauthn-server.js",
23
+ "./wallet": "./dist/wallet.js",
24
+ "./eip1193": "./dist/eip1193.js",
25
+ "./rainbowkit": "./dist/rainbowkit.js",
26
+ "./errors": "./dist/errors.js"
27
+ },
28
+ "type": "module",
29
+ "peerDependencies": {
30
+ "@organigram/js": "workspace:^",
31
+ "@rainbow-me/rainbowkit": "^2.2.8",
32
+ "@simplewebauthn/browser": "^13.3.0",
33
+ "@simplewebauthn/server": "^13.3.2",
34
+ "viem": "^2.18.2",
35
+ "wagmi": "^2.16.3 || ^3.0.0"
36
+ },
37
+ "dependencies": {
38
+ "@organigram/js": "^0.1.6",
39
+ "@simplewebauthn/browser": "^13.3.0",
40
+ "@simplewebauthn/server": "^13.3.2",
41
+ "viem": "2.55.2"
42
+ },
43
+ "devDependencies": {
44
+ "@rainbow-me/rainbowkit": "^2.2.8",
45
+ "@tsconfig/recommended": "^1.0.7",
46
+ "@types/jest": "^29.5.12",
47
+ "@types/node": "^20.19.37",
48
+ "jest": "^29.7.0",
49
+ "ts-jest": "^29.2.3",
50
+ "typescript": "^5.5.4",
51
+ "wagmi": "^2.16.3"
52
+ },
53
+ "scripts": {
54
+ "docs:generate": "env-cmd --silent tsx ../docs/scripts/generatePackageTypeScriptReference.ts . passkey-wallet",
55
+ "build": "tsc -p tsconfig.build.json --pretty",
56
+ "test": "jest",
57
+ "typecheck": "tsc --noEmit --incremental false",
58
+ "lint": "eslint ."
59
+ },
60
+ "gitHead": "a2bf9b898c0c5f0dbcf80f2b9f705d5200519bb5"
61
+ }
@@ -0,0 +1,69 @@
1
+ import {
2
+ decryptPasskeyVaultSecret,
3
+ derivePasskeyVaultKey,
4
+ encryptPasskeyVaultSecret,
5
+ isPasskeyPrfSupported
6
+ } from './crypto'
7
+
8
+ describe('passkey vault crypto', () => {
9
+ it('encrypts and decrypts vault plaintext with a PRF-derived key', async () => {
10
+ const salt = new Uint8Array(32).fill(2)
11
+ const nonce = new Uint8Array(12).fill(3)
12
+ const key = await derivePasskeyVaultKey({
13
+ prfOutput: new Uint8Array(32).fill(1),
14
+ salt
15
+ })
16
+
17
+ const envelope = await encryptPasskeyVaultSecret({
18
+ plaintext: 'secret vault payload',
19
+ key,
20
+ salt,
21
+ nonce
22
+ })
23
+
24
+ await expect(
25
+ decryptPasskeyVaultSecret({ envelope, key })
26
+ ).resolves.toBe('secret vault payload')
27
+ })
28
+
29
+ it('rejects vault envelopes decrypted with the wrong passkey output', async () => {
30
+ const salt = new Uint8Array(32).fill(13)
31
+ const key = await derivePasskeyVaultKey({
32
+ prfOutput: new Uint8Array(32).fill(1),
33
+ salt
34
+ })
35
+ const wrongKey = await derivePasskeyVaultKey({
36
+ prfOutput: new Uint8Array(32).fill(2),
37
+ salt
38
+ })
39
+ const envelope = await encryptPasskeyVaultSecret({
40
+ plaintext: 'secret vault payload',
41
+ key,
42
+ salt
43
+ })
44
+
45
+ await expect(
46
+ decryptPasskeyVaultSecret({ envelope, key: wrongKey })
47
+ ).rejects.toThrow(/Unable to decrypt passkey wallet/)
48
+ })
49
+
50
+ it('detects PRF support from client extension results', async () => {
51
+ await expect(isPasskeyPrfSupported({ credential: null })).resolves.toBe(
52
+ false
53
+ )
54
+ await expect(
55
+ isPasskeyPrfSupported({
56
+ credential: {
57
+ getClientExtensionResults: () => ({ prf: { enabled: true } })
58
+ }
59
+ })
60
+ ).resolves.toBe(true)
61
+ await expect(
62
+ isPasskeyPrfSupported({
63
+ credential: {
64
+ getClientExtensionResults: () => ({ prf: { enabled: false } })
65
+ }
66
+ })
67
+ ).resolves.toBe(false)
68
+ })
69
+ })