passkey-kit 0.12.1 → 0.13.1

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 (172) hide show
  1. package/README.md +399 -227
  2. package/{types → dist}/base.d.ts +1 -0
  3. package/dist/base.d.ts.map +1 -0
  4. package/{src/base.ts → dist/base.js} +8 -10
  5. package/dist/base.js.map +1 -0
  6. package/dist/base64url.d.ts +18 -0
  7. package/dist/base64url.d.ts.map +1 -0
  8. package/dist/base64url.js +18 -0
  9. package/dist/base64url.js.map +1 -0
  10. package/dist/constants.d.ts +87 -0
  11. package/dist/constants.d.ts.map +1 -0
  12. package/dist/constants.js +111 -0
  13. package/dist/constants.js.map +1 -0
  14. package/dist/contract-errors.d.ts +69 -0
  15. package/dist/contract-errors.d.ts.map +1 -0
  16. package/dist/contract-errors.js +129 -0
  17. package/dist/contract-errors.js.map +1 -0
  18. package/dist/errors.d.ts +163 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +244 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/events.d.ts +94 -0
  23. package/dist/events.d.ts.map +1 -0
  24. package/dist/events.js +92 -0
  25. package/dist/events.js.map +1 -0
  26. package/dist/index.d.ts +22 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +36 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/indexer/codec.d.ts +18 -0
  31. package/dist/indexer/codec.d.ts.map +1 -0
  32. package/dist/indexer/codec.js +32 -0
  33. package/dist/indexer/codec.js.map +1 -0
  34. package/dist/indexer/index.d.ts +25 -0
  35. package/dist/indexer/index.d.ts.map +1 -0
  36. package/dist/indexer/index.js +34 -0
  37. package/dist/indexer/index.js.map +1 -0
  38. package/dist/indexer/mercury.d.ts +88 -0
  39. package/dist/indexer/mercury.d.ts.map +1 -0
  40. package/dist/indexer/mercury.js +237 -0
  41. package/dist/indexer/mercury.js.map +1 -0
  42. package/dist/indexer/stellar-indexer.d.ts +115 -0
  43. package/dist/indexer/stellar-indexer.d.ts.map +1 -0
  44. package/dist/indexer/stellar-indexer.js +279 -0
  45. package/dist/indexer/stellar-indexer.js.map +1 -0
  46. package/dist/indexer/types.d.ts +73 -0
  47. package/dist/indexer/types.d.ts.map +1 -0
  48. package/dist/indexer/types.js +9 -0
  49. package/dist/indexer/types.js.map +1 -0
  50. package/dist/kit/auth-payload.d.ts +89 -0
  51. package/dist/kit/auth-payload.d.ts.map +1 -0
  52. package/dist/kit/auth-payload.js +203 -0
  53. package/dist/kit/auth-payload.js.map +1 -0
  54. package/dist/kit/deploy-ops.d.ts +45 -0
  55. package/dist/kit/deploy-ops.d.ts.map +1 -0
  56. package/dist/kit/deploy-ops.js +69 -0
  57. package/dist/kit/deploy-ops.js.map +1 -0
  58. package/dist/kit/tx-ops.d.ts +92 -0
  59. package/dist/kit/tx-ops.d.ts.map +1 -0
  60. package/dist/kit/tx-ops.js +113 -0
  61. package/dist/kit/tx-ops.js.map +1 -0
  62. package/dist/kit/wallet-ops.d.ts +59 -0
  63. package/dist/kit/wallet-ops.d.ts.map +1 -0
  64. package/dist/kit/wallet-ops.js +122 -0
  65. package/dist/kit/wallet-ops.js.map +1 -0
  66. package/dist/kit/webauthn-ops.d.ts +55 -0
  67. package/dist/kit/webauthn-ops.d.ts.map +1 -0
  68. package/dist/kit/webauthn-ops.js +79 -0
  69. package/dist/kit/webauthn-ops.js.map +1 -0
  70. package/dist/kit.d.ts +128 -0
  71. package/dist/kit.d.ts.map +1 -0
  72. package/dist/kit.js +276 -0
  73. package/dist/kit.js.map +1 -0
  74. package/dist/managers/credential-manager.d.ts +39 -0
  75. package/dist/managers/credential-manager.d.ts.map +1 -0
  76. package/dist/managers/credential-manager.js +47 -0
  77. package/dist/managers/credential-manager.js.map +1 -0
  78. package/dist/managers/index.d.ts +7 -0
  79. package/dist/managers/index.d.ts.map +1 -0
  80. package/dist/managers/index.js +7 -0
  81. package/dist/managers/index.js.map +1 -0
  82. package/dist/managers/signer-manager.d.ts +51 -0
  83. package/dist/managers/signer-manager.d.ts.map +1 -0
  84. package/dist/managers/signer-manager.js +92 -0
  85. package/dist/managers/signer-manager.js.map +1 -0
  86. package/dist/managers/submission-manager.d.ts +52 -0
  87. package/dist/managers/submission-manager.d.ts.map +1 -0
  88. package/dist/managers/submission-manager.js +77 -0
  89. package/dist/managers/submission-manager.js.map +1 -0
  90. package/dist/relayer.d.ts +61 -0
  91. package/dist/relayer.d.ts.map +1 -0
  92. package/dist/relayer.js +119 -0
  93. package/dist/relayer.js.map +1 -0
  94. package/dist/rpc-data.d.ts +35 -0
  95. package/dist/rpc-data.d.ts.map +1 -0
  96. package/dist/rpc-data.js +49 -0
  97. package/dist/rpc-data.js.map +1 -0
  98. package/dist/sac.d.ts +31 -0
  99. package/dist/sac.d.ts.map +1 -0
  100. package/dist/sac.js +47 -0
  101. package/dist/sac.js.map +1 -0
  102. package/dist/server.d.ts +79 -0
  103. package/dist/server.d.ts.map +1 -0
  104. package/dist/server.js +152 -0
  105. package/dist/server.js.map +1 -0
  106. package/dist/signers.d.ts +83 -0
  107. package/dist/signers.d.ts.map +1 -0
  108. package/dist/signers.js +124 -0
  109. package/dist/signers.js.map +1 -0
  110. package/dist/storage/index.d.ts +13 -0
  111. package/dist/storage/index.d.ts.map +1 -0
  112. package/dist/storage/index.js +12 -0
  113. package/dist/storage/index.js.map +1 -0
  114. package/dist/storage/indexeddb.d.ts +26 -0
  115. package/dist/storage/indexeddb.d.ts.map +1 -0
  116. package/dist/storage/indexeddb.js +100 -0
  117. package/dist/storage/indexeddb.js.map +1 -0
  118. package/dist/storage/localStorage.d.ts +22 -0
  119. package/dist/storage/localStorage.d.ts.map +1 -0
  120. package/dist/storage/localStorage.js +86 -0
  121. package/dist/storage/localStorage.js.map +1 -0
  122. package/dist/storage/memory.d.ts +18 -0
  123. package/dist/storage/memory.d.ts.map +1 -0
  124. package/dist/storage/memory.js +41 -0
  125. package/dist/storage/memory.js.map +1 -0
  126. package/dist/types.d.ts +136 -0
  127. package/dist/types.d.ts.map +1 -0
  128. package/dist/types.js +39 -0
  129. package/dist/types.js.map +1 -0
  130. package/dist/utils.d.ts +52 -0
  131. package/dist/utils.d.ts.map +1 -0
  132. package/dist/utils.js +153 -0
  133. package/dist/utils.js.map +1 -0
  134. package/dist/validation.d.ts +40 -0
  135. package/dist/validation.d.ts.map +1 -0
  136. package/dist/validation.js +73 -0
  137. package/dist/validation.js.map +1 -0
  138. package/dist/version.d.ts +7 -0
  139. package/dist/version.d.ts.map +1 -0
  140. package/dist/version.js +9 -0
  141. package/dist/version.js.map +1 -0
  142. package/package.json +45 -18
  143. package/.claude/settings.local.json +0 -20
  144. package/.cursorrules +0 -170
  145. package/.gitattributes +0 -2
  146. package/.github/workflows/release.yml +0 -21
  147. package/.vscode/settings.json +0 -6
  148. package/PROPOSAL.md +0 -397
  149. package/cheatsheet.txt +0 -18
  150. package/clone-js-sdk.sh +0 -21
  151. package/packages/passkey-kit-sdk/README.md +0 -54
  152. package/packages/passkey-kit-sdk/package.json +0 -21
  153. package/packages/passkey-kit-sdk/src/index.ts +0 -171
  154. package/packages/passkey-kit-sdk/tsconfig.json +0 -98
  155. package/packages/passkey-kit-sdk/types/index.d.ts +0 -190
  156. package/packages/sac-sdk/README.md +0 -54
  157. package/packages/sac-sdk/package.json +0 -21
  158. package/packages/sac-sdk/src/index.ts +0 -586
  159. package/packages/sac-sdk/tsconfig.json +0 -98
  160. package/packages/sac-sdk/types/index.d.ts +0 -532
  161. package/pnpm-workspace.yaml +0 -2
  162. package/src/index.ts +0 -5
  163. package/src/kit.ts +0 -823
  164. package/src/sac.ts +0 -29
  165. package/src/server.ts +0 -179
  166. package/src/types.ts +0 -32
  167. package/tsconfig.json +0 -28
  168. package/types/index.d.ts +0 -5
  169. package/types/kit.d.ts +0 -95
  170. package/types/sac.d.ts +0 -13
  171. package/types/server.d.ts +0 -29
  172. package/types/types.d.ts +0 -22
package/src/kit.ts DELETED
@@ -1,823 +0,0 @@
1
- import { Client as PasskeyClient, type Signature, type SignerKey as SDKSignerKey, type SignerLimits as SDKSignerLimits } from 'passkey-kit-sdk'
2
- import * as StellarSdk from '@stellar/stellar-sdk'
3
- import { StrKey, hash, xdr, Keypair, Address, TransactionBuilder, Operation } from '@stellar/stellar-sdk'
4
- import { startRegistration, startAuthentication, type AuthenticationResponseJSON, type AuthenticatorAttestationResponseJSON, type AuthenticatorSelectionCriteria } from "@simplewebauthn/browser"
5
- import { Buffer } from 'buffer'
6
- import base64url from 'base64url'
7
- import type { SignerKey, SignerLimits, SignerStore } from './types'
8
- import { PasskeyBase } from './base'
9
- import { AssembledTransaction, basicNodeSigner, type AssembledTransactionOptions, type Spec as ContractSpec, type Tx } from '@stellar/stellar-sdk/contract'
10
- import type { Server } from '@stellar/stellar-sdk/rpc'
11
-
12
- type P27HashIdPreimage = typeof xdr.HashIdPreimage & {
13
- envelopeTypeSorobanAuthorizationWithAddress?: (value: unknown) => xdr.HashIdPreimage
14
- }
15
-
16
- type P27Xdr = typeof xdr & {
17
- HashIdPreimageSorobanAuthorizationWithAddress?: new (fields: {
18
- networkId: Buffer
19
- nonce: xdr.Int64
20
- signatureExpirationLedger: number
21
- invocation: xdr.SorobanAuthorizedInvocation
22
- address: xdr.ScAddress
23
- }) => unknown
24
- }
25
-
26
- type P27AuthSdk = typeof StellarSdk & {
27
- buildAuthorizationEntryPreimage?: (
28
- entry: xdr.SorobanAuthorizationEntry,
29
- validUntilLedgerSeq: number,
30
- networkPassphrase: string
31
- ) => xdr.HashIdPreimage
32
- }
33
-
34
- type PasskeyClientWithSpec = PasskeyClient & {
35
- spec: ContractSpec
36
- }
37
-
38
- export function getAddressCredentials(credentials: xdr.SorobanCredentials): xdr.SorobanAddressCredentials {
39
- const typedCredentials = credentials as unknown as {
40
- address: () => xdr.SorobanAddressCredentials
41
- addressV2?: () => xdr.SorobanAddressCredentials
42
- addressWithDelegates?: () => {
43
- addressCredentials: () => xdr.SorobanAddressCredentials
44
- }
45
- }
46
-
47
- switch (credentials.switch().name as string) {
48
- case 'sorobanCredentialsAddress':
49
- return typedCredentials.address()
50
- case 'sorobanCredentialsAddressV2':
51
- if (!typedCredentials.addressV2) {
52
- throw new Error(
53
- 'ADDRESS_V2 credentials require an SDK with Protocol 27 credential support'
54
- )
55
- }
56
- return typedCredentials.addressV2()
57
- case 'sorobanCredentialsAddressWithDelegates':
58
- if (!typedCredentials.addressWithDelegates) {
59
- throw new Error(
60
- 'ADDRESS_WITH_DELEGATES credentials require an SDK with Protocol 27 credential support'
61
- )
62
- }
63
- return typedCredentials.addressWithDelegates().addressCredentials()
64
- }
65
-
66
- throw new Error(`Soroban credentials do not contain address credentials: ${credentials.switch().name}`)
67
- }
68
-
69
- export function usesAddressBoundPayload(credentials: xdr.SorobanCredentials): boolean {
70
- const credentialType = credentials.switch().name as string
71
- return credentialType === 'sorobanCredentialsAddressV2'
72
- || credentialType === 'sorobanCredentialsAddressWithDelegates'
73
- }
74
-
75
- export function assertSignatureExpirationLedger(expiration: number): void {
76
- if (!Number.isInteger(expiration) || expiration < 0 || expiration > 0xffffffff) {
77
- throw new Error('Soroban signature expiration ledger must be a uint32 integer')
78
- }
79
- }
80
-
81
- export function buildSignaturePayload(
82
- networkPassphrase: string,
83
- entry: xdr.SorobanAuthorizationEntry,
84
- expiration: number
85
- ): Buffer {
86
- assertSignatureExpirationLedger(expiration)
87
-
88
- const credentials = getAddressCredentials(entry.credentials())
89
- const sdkAuth = StellarSdk as P27AuthSdk
90
-
91
- if (sdkAuth.buildAuthorizationEntryPreimage) {
92
- credentials.signatureExpirationLedger(expiration)
93
- return hash(sdkAuth.buildAuthorizationEntryPreimage(
94
- entry,
95
- expiration,
96
- networkPassphrase
97
- ).toXDR())
98
- }
99
-
100
- if (usesAddressBoundPayload(entry.credentials())) {
101
- const hashIdPreimage = xdr.HashIdPreimage as P27HashIdPreimage
102
- const WithAddressPreimage = (xdr as P27Xdr).HashIdPreimageSorobanAuthorizationWithAddress
103
-
104
- if (!hashIdPreimage.envelopeTypeSorobanAuthorizationWithAddress || !WithAddressPreimage) {
105
- throw new Error(
106
- 'Address-bound Soroban auth credentials require an SDK with Protocol 27 auth preimage support'
107
- )
108
- }
109
-
110
- credentials.signatureExpirationLedger(expiration)
111
- const preimage = hashIdPreimage.envelopeTypeSorobanAuthorizationWithAddress(
112
- new WithAddressPreimage({
113
- networkId: hash(Buffer.from(networkPassphrase)),
114
- nonce: credentials.nonce(),
115
- signatureExpirationLedger: expiration,
116
- invocation: entry.rootInvocation(),
117
- address: credentials.address(),
118
- })
119
- )
120
-
121
- return hash(preimage.toXDR())
122
- }
123
-
124
- credentials.signatureExpirationLedger(expiration)
125
- const preimage = xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
126
- new xdr.HashIdPreimageSorobanAuthorization({
127
- networkId: hash(Buffer.from(networkPassphrase)),
128
- nonce: credentials.nonce(),
129
- signatureExpirationLedger: expiration,
130
- invocation: entry.rootInvocation()
131
- })
132
- )
133
-
134
- return hash(preimage.toXDR())
135
- }
136
-
137
- // TODO we should allow setting the rpId in the constructor so we don't have to keep setting it for every call
138
- // e.g. if you want to sign with a smol.xyz key on next.smol.xyz key you have to set rpId over and over again
139
-
140
- export class PasskeyKit extends PasskeyBase {
141
- declare rpc: Server
142
- declare rpcUrl: string
143
-
144
- private walletKeypair: Keypair
145
- private walletPublicKey: string
146
- private walletWasmHash: string
147
- private timeoutInSeconds: number
148
- private WebAuthn: {
149
- startRegistration: typeof startRegistration,
150
- startAuthentication: typeof startAuthentication
151
- }
152
-
153
- public keyId: string | undefined
154
- public networkPassphrase: string
155
- public wallet: PasskeyClient | undefined
156
-
157
- private get walletSpec(): ContractSpec {
158
- return (this.wallet as PasskeyClientWithSpec).spec
159
- }
160
-
161
- constructor(options: {
162
- rpcUrl: string,
163
- networkPassphrase: string,
164
- walletWasmHash: string,
165
- timeoutInSeconds?: number,
166
- WebAuthn?: {
167
- startRegistration: typeof startRegistration,
168
- startAuthentication: typeof startAuthentication
169
- }
170
- }) {
171
- const { rpcUrl, networkPassphrase, walletWasmHash, WebAuthn } = options
172
-
173
- super(rpcUrl)
174
-
175
- this.networkPassphrase = networkPassphrase
176
- // this account exists as the seed source for deploying new wallets
177
- // not using the genesis wallet as on mainnet the account has no usable signers
178
- // there's a chance this isn't the best move and should instead be a constructor variable
179
- // alternatively when we create a new wallet we shouldn't inherit the source as the auth entry signer
180
- // Keypair.fromRawEd25519Seed(hash(Buffer.from(this.networkPassphrase)))
181
- this.walletKeypair = Keypair.fromRawEd25519Seed(hash(Buffer.from('kalepail')));
182
- this.walletPublicKey = this.walletKeypair.publicKey()
183
- this.walletWasmHash = walletWasmHash
184
- this.timeoutInSeconds = options.timeoutInSeconds || 30 // OpenZeppelin Relayer requires <= 30 second timeout so let's default to that
185
- this.WebAuthn = WebAuthn || { startRegistration, startAuthentication }
186
- }
187
-
188
- public async createWallet(
189
- app: string,
190
- user: string,
191
- settings?: {
192
- rpId?: string
193
- authenticatorSelection?: AuthenticatorSelectionCriteria
194
- }
195
- ) {
196
- const { rawResponse, keyId, keyIdBase64, publicKey } = await this.createKey(app, user, settings)
197
-
198
- const at = await PasskeyClient.deploy(
199
- {
200
- signer: {
201
- tag: 'Secp256r1',
202
- values: [
203
- keyId,
204
- publicKey,
205
- [undefined],
206
- [undefined],
207
- { tag: 'Persistent', values: undefined },
208
- ]
209
- }
210
- },
211
- {
212
- rpcUrl: this.rpcUrl,
213
- wasmHash: this.walletWasmHash,
214
- networkPassphrase: this.networkPassphrase,
215
- publicKey: this.walletPublicKey,
216
- salt: hash(keyId),
217
- timeoutInSeconds: this.timeoutInSeconds,
218
- }
219
- )
220
-
221
- const contractId = at.result.options.contractId
222
-
223
- this.wallet = new PasskeyClient({
224
- contractId,
225
- networkPassphrase: this.networkPassphrase,
226
- rpcUrl: this.rpcUrl
227
- })
228
-
229
- await at.sign({
230
- signTransaction: basicNodeSigner(this.walletKeypair, this.networkPassphrase).signTransaction
231
- })
232
-
233
- return {
234
- rawResponse,
235
- keyId,
236
- keyIdBase64,
237
- contractId,
238
- signedTx: at.signed!
239
- }
240
- }
241
-
242
- public async createKey(app: string, user: string, settings?: {
243
- rpId?: string
244
- authenticatorSelection?: AuthenticatorSelectionCriteria
245
- }) {
246
- const now = new Date()
247
- const displayName = `${user} — ${now.toLocaleString()}`
248
- const { rpId, authenticatorSelection = {
249
- residentKey: "preferred",
250
- userVerification: "preferred",
251
- } } = settings || {}
252
-
253
- // TODO discover the contract id before creating the key so we can use it in the key name
254
- // TODO it's possible for the creation to fail in which case we've created a passkey but it's not onchain.
255
- // In this case we should save the passkey info and retry uploading it async vs asking the user to create another passkey
256
- // This does introduce a storage dependency though so it likely needs to be a function with some logic for choosing how to store the passkey data
257
-
258
- const rawResponse = await this.WebAuthn.startRegistration({
259
- optionsJSON: {
260
- challenge: base64url("stellaristhebetterblockchain"),
261
- rp: {
262
- id: rpId,
263
- name: app,
264
- },
265
- user: {
266
- id: base64url(`${user}:${now.getTime()}:${Math.random()}`),
267
- name: displayName,
268
- displayName
269
- },
270
- authenticatorSelection,
271
- pubKeyCredParams: [{ alg: -7, type: "public-key" }],
272
- }
273
- });
274
- const { id, response } = rawResponse
275
-
276
- if (!this.keyId)
277
- this.keyId = id;
278
-
279
- return {
280
- rawResponse,
281
- keyId: base64url.toBuffer(id),
282
- keyIdBase64: id,
283
- publicKey: await this.getPublicKey(response),
284
- }
285
- }
286
-
287
- public async connectWallet(opts?: {
288
- rpId?: string,
289
- keyId?: string | Uint8Array,
290
- getContractId?: (keyId: string) => Promise<string | undefined>,
291
- // TEMP for backwards compatibility for when we seeded wallets from a factory address
292
- // Consider putting this somewhere else??
293
- walletPublicKey?: string
294
- }) {
295
- let { rpId, keyId, getContractId, walletPublicKey } = opts || {}
296
- let keyIdBuffer: Buffer
297
- let rawResponse: AuthenticationResponseJSON | undefined;
298
-
299
- if (!keyId) {
300
- rawResponse = await this.WebAuthn.startAuthentication({
301
- optionsJSON: {
302
- challenge: base64url("stellaristhebetterblockchain"),
303
- rpId,
304
- userVerification: "preferred",
305
- }
306
- });
307
-
308
- keyId = rawResponse.id
309
- }
310
-
311
- if (keyId instanceof Uint8Array) {
312
- keyIdBuffer = Buffer.from(keyId)
313
- keyId = base64url(keyIdBuffer)
314
- } else {
315
- keyIdBuffer = base64url.toBuffer(keyId)
316
- }
317
-
318
- if (!this.keyId)
319
- this.keyId = keyId
320
-
321
- // Check for the contractId on-chain as a derivation from the keyId. This is the easiest and "cheapest" check however it will only work for the initially deployed passkey if it was used as derivation
322
- let contractId: string | undefined = this.encodeContract(this.walletPublicKey, keyIdBuffer);
323
-
324
- // attempt passkey id derivation
325
- try {
326
- // TODO what is the error if the entry exists but is archived?
327
- await this.rpc.getContractData(contractId, xdr.ScVal.scvLedgerKeyContractInstance())
328
- }
329
- // if that fails look up from the `getContractId` function
330
- catch {
331
- contractId = getContractId && await getContractId(keyId)
332
- }
333
-
334
- ////
335
- // TEMP for backwards compatibility for when we seeded wallets from a factory address
336
- // Consider putting this in the constructor
337
- if (!contractId && walletPublicKey) {
338
- contractId = this.encodeContract(walletPublicKey, keyIdBuffer);
339
-
340
- try {
341
- await this.rpc.getContractData(contractId, xdr.ScVal.scvLedgerKeyContractInstance())
342
- } catch {
343
- contractId = undefined
344
- }
345
- }
346
- ////
347
-
348
- if (!contractId)
349
- throw new Error('Failed to connect wallet')
350
-
351
- this.wallet = new PasskeyClient({
352
- contractId,
353
- rpcUrl: this.rpcUrl,
354
- networkPassphrase: this.networkPassphrase,
355
- })
356
-
357
- return {
358
- rawResponse,
359
- keyId: keyIdBuffer,
360
- keyIdBase64: keyId,
361
- contractId
362
- }
363
- }
364
-
365
- public async signAuthEntry(
366
- entry: xdr.SorobanAuthorizationEntry,
367
- options?: {
368
- rpId?: string,
369
- keyId?: 'any' | string | Uint8Array
370
- keypair?: Keypair,
371
- policy?: string,
372
- expiration?: number
373
- }
374
- ) {
375
- let { rpId, keyId, keypair, policy, expiration } = options || {}
376
-
377
- if ([keyId, keypair, policy].filter((arg) => !!arg).length > 1)
378
- throw new Error('Exactly one of `options.keyId`, `options.keypair`, or `options.policy` must be provided.');
379
-
380
- const credentialType = entry.credentials().switch().name as string
381
- if (credentialType === 'sorobanCredentialsAddressWithDelegates') {
382
- throw new Error('ADDRESS_WITH_DELEGATES auth entries are not supported by passkey signing yet')
383
- }
384
-
385
- const credentials = getAddressCredentials(entry.credentials());
386
-
387
- if (!expiration) {
388
- expiration = credentials.signatureExpirationLedger()
389
-
390
- if (!expiration) {
391
- const { sequence } = await this.rpc.getLatestLedger()
392
- expiration = sequence + this.timeoutInSeconds / 5; // assumes 5 second ledger time
393
- expiration = Math.ceil(expiration)
394
- }
395
- }
396
-
397
- const payload = buildSignaturePayload(this.networkPassphrase, entry, expiration)
398
-
399
- // let signatures: Signatures
400
- let key: SDKSignerKey
401
- let val: Signature | undefined
402
-
403
- // const scSpecTypeDefSignatures = xdr.ScSpecTypeDef.scSpecTypeUdt(
404
- // new xdr.ScSpecTypeUdt({ name: "Signatures" }),
405
- // );
406
-
407
- // switch (credentials.signature().switch()) {
408
- // case xdr.ScValType.scvVoid():
409
- // signatures = [new Map()]
410
- // break;
411
- // default: {
412
- // signatures = this.wallet!.spec.scValToNative(credentials.signature(), scSpecTypeDefSignatures)
413
- // }
414
- // }
415
-
416
- // Sign with a policy
417
- if (policy) {
418
- key = {
419
- tag: "Policy",
420
- values: [policy]
421
- }
422
- val = {
423
- tag: "Policy",
424
- values: undefined,
425
- }
426
- }
427
-
428
- // Sign with the keypair as an ed25519 signer
429
- else if (keypair) {
430
- const signature = keypair.sign(payload);
431
-
432
- key = {
433
- tag: "Ed25519",
434
- values: [keypair.rawPublicKey()]
435
- }
436
- val = {
437
- tag: "Ed25519",
438
- values: [signature],
439
- }
440
- }
441
-
442
- // Default, use passkey
443
- else {
444
- const authenticationResponse = await this.WebAuthn.startAuthentication({
445
- optionsJSON: keyId === 'any'
446
- || (!keyId && !this.keyId)
447
- ? {
448
- challenge: base64url(payload),
449
- rpId,
450
- userVerification: "preferred",
451
- }
452
- : {
453
- challenge: base64url(payload),
454
- rpId,
455
- allowCredentials: [
456
- {
457
- id: keyId instanceof Uint8Array
458
- ? base64url(Buffer.from(keyId))
459
- : keyId || this.keyId!,
460
- type: "public-key",
461
- },
462
- ],
463
- userVerification: "preferred",
464
- }
465
- });
466
-
467
- key = {
468
- tag: "Secp256r1",
469
- values: [base64url.toBuffer(authenticationResponse.id)]
470
- }
471
- val = {
472
- tag: "Secp256r1",
473
- values: [
474
- {
475
- authenticator_data: base64url.toBuffer(
476
- authenticationResponse.response.authenticatorData,
477
- ),
478
- client_data_json: base64url.toBuffer(
479
- authenticationResponse.response.clientDataJSON,
480
- ),
481
- signature: this.compactSignature(
482
- base64url.toBuffer(authenticationResponse.response.signature)
483
- ),
484
- },
485
- ],
486
- }
487
- }
488
-
489
- const scKeyType = xdr.ScSpecTypeDef.scSpecTypeUdt(
490
- new xdr.ScSpecTypeUdt({ name: "SignerKey" }),
491
- );
492
- const scValType = xdr.ScSpecTypeDef.scSpecTypeUdt(
493
- new xdr.ScSpecTypeUdt({ name: "Signature" }),
494
- );
495
- const scKey = this.walletSpec.nativeToScVal(key, scKeyType);
496
- const scVal = val ? this.walletSpec.nativeToScVal(val, scValType) : xdr.ScVal.scvVoid();
497
- const scEntry = new xdr.ScMapEntry({
498
- key: scKey,
499
- val: scVal,
500
- })
501
-
502
- switch (credentials.signature().switch().name) {
503
- case 'scvVoid':
504
- credentials.signature(xdr.ScVal.scvVec([
505
- xdr.ScVal.scvMap([scEntry])
506
- ]))
507
- break;
508
- case 'scvVec':
509
- // Add the new signature to the existing map
510
- credentials.signature().vec()?.[0].map()?.push(scEntry)
511
-
512
- // Order the map by key
513
- // Not using Buffer.compare because Symbols are 9 bytes and unused bytes _append_ 0s vs prepending them, which is too bad
514
- credentials.signature().vec()?.[0].map()?.sort((a, b) => {
515
- return (
516
- a.key().vec()![0].sym() +
517
- a.key().vec()![1].toXDR().join('')
518
- ).localeCompare(
519
- b.key().vec()![0].sym() +
520
- b.key().vec()![1].toXDR().join('')
521
- )
522
- })
523
- break;
524
- default:
525
- throw new Error('Unsupported signature')
526
- }
527
-
528
- // Insert the new signature into the signatures Map
529
- // signatures[0].set(key, val)
530
-
531
- // Insert the new signatures Map into the credentials
532
- // credentials.signature(
533
- // this.wallet!.spec.nativeToScVal(signatures, scSpecTypeDefSignatures)
534
- // )
535
-
536
- // Order the signatures map
537
- // credentials.signature().vec()?.[0].map()?.sort((a, b) => {
538
- // return (
539
- // a.key().vec()![0].sym() +
540
- // a.key().vec()![1].toXDR().join('')
541
- // ).localeCompare(
542
- // b.key().vec()![0].sym() +
543
- // b.key().vec()![1].toXDR().join('')
544
- // )
545
- // })
546
-
547
- return entry
548
- }
549
-
550
- public async sign<T>(
551
- txn: AssembledTransaction<T> | Tx | string,
552
- options?: {
553
- rpId?: string,
554
- keyId?: 'any' | string | Uint8Array
555
- keypair?: Keypair,
556
- policy?: string,
557
- expiration?: number
558
- }
559
- ) {
560
- if (!(txn instanceof AssembledTransaction)) {
561
- try {
562
- txn = AssembledTransaction.fromXDR(this.wallet!.options, typeof txn === 'string' ? txn : txn.toXDR(), this.walletSpec);
563
- } catch {
564
- if (!(txn instanceof AssembledTransaction)) {
565
- const built = TransactionBuilder.fromXDR(typeof txn === 'string' ? txn : txn.toXDR(), this.networkPassphrase);
566
- const operation = built.operations[0] as Operation.InvokeHostFunction;
567
-
568
- txn = await AssembledTransaction.buildWithOp<T>(
569
- Operation.invokeHostFunction({ func: operation.func }),
570
- this.wallet!.options as AssembledTransactionOptions<T>
571
- );
572
- }
573
- }
574
- }
575
-
576
- await txn.signAuthEntries({
577
- address: this.wallet!.options.contractId,
578
- authorizeEntry: (entry) => {
579
- const clone = xdr.SorobanAuthorizationEntry.fromXDR(entry.toXDR())
580
- return this.signAuthEntry(clone, options)
581
- },
582
- })
583
-
584
- return txn
585
- }
586
-
587
- public addSecp256r1(keyId: string | Uint8Array, publicKey: string | Uint8Array, limits: SignerLimits, store: SignerStore, expiration?: number) {
588
- return this.secp256r1(keyId, publicKey, limits, store, 'add_signer', expiration)
589
- }
590
- public addEd25519(publicKey: string, limits: SignerLimits, store: SignerStore, expiration?: number) {
591
- return this.ed25519(publicKey, limits, store, 'add_signer', expiration)
592
- }
593
- public addPolicy(policy: string, limits: SignerLimits, store: SignerStore, expiration?: number) {
594
- return this.policy(policy, limits, store, 'add_signer', expiration)
595
- }
596
-
597
- public updateSecp256r1(keyId: string | Uint8Array, publicKey: string | Uint8Array, limits: SignerLimits, store: SignerStore, expiration?: number) {
598
- return this.secp256r1(keyId, publicKey, limits, store, 'update_signer', expiration)
599
- }
600
- public updateEd25519(publicKey: string, limits: SignerLimits, store: SignerStore, expiration?: number) {
601
- return this.ed25519(publicKey, limits, store, 'update_signer', expiration)
602
- }
603
- public updatePolicy(policy: string, limits: SignerLimits, store: SignerStore, expiration?: number) {
604
- return this.policy(policy, limits, store, 'update_signer', expiration)
605
- }
606
-
607
- public remove(signer: SignerKey) {
608
- return this.wallet!.remove_signer({
609
- signer_key: this.getSignerKey(signer)
610
- }, {
611
- timeoutInSeconds: this.timeoutInSeconds,
612
- });
613
- }
614
-
615
- private secp256r1(keyId: string | Uint8Array, publicKey: string | Uint8Array, limits: SignerLimits, store: SignerStore, fn: 'add_signer' | 'update_signer', expiration?: number) {
616
- keyId = typeof keyId === 'string' ? base64url.toBuffer(keyId) : keyId
617
- publicKey = typeof publicKey === 'string' ? base64url.toBuffer(publicKey) : publicKey
618
-
619
- return this.wallet![fn]({
620
- signer: {
621
- tag: "Secp256r1",
622
- values: [
623
- Buffer.from(keyId),
624
- Buffer.from(publicKey),
625
- [expiration],
626
- this.getSignerLimits(limits),
627
- { tag: store, values: undefined },
628
- ],
629
- },
630
- }, {
631
- timeoutInSeconds: this.timeoutInSeconds,
632
- });
633
- }
634
- private ed25519(publicKey: string, limits: SignerLimits, store: SignerStore, fn: 'add_signer' | 'update_signer', expiration?: number) {
635
- return this.wallet![fn]({
636
- signer: {
637
- tag: "Ed25519",
638
- values: [
639
- Keypair.fromPublicKey(publicKey).rawPublicKey(),
640
- [expiration],
641
- this.getSignerLimits(limits),
642
- { tag: store, values: undefined },
643
- ],
644
- },
645
- }, {
646
- timeoutInSeconds: this.timeoutInSeconds,
647
- });
648
- }
649
- private policy(policy: string, limits: SignerLimits, store: SignerStore, fn: 'add_signer' | 'update_signer', expiration?: number) {
650
- return this.wallet![fn]({
651
- signer: {
652
- tag: "Policy",
653
- values: [
654
- policy,
655
- [expiration],
656
- this.getSignerLimits(limits),
657
- { tag: store, values: undefined },
658
- ],
659
- },
660
- }, {
661
- timeoutInSeconds: this.timeoutInSeconds,
662
- });
663
- }
664
-
665
- /* LATER
666
- - Add a getKeyInfo action to get info about a specific passkey
667
- Specifically looking for name, type, etc. data so a user could grok what signer mapped to what passkey
668
- */
669
-
670
- private getSignerLimits(limits: SignerLimits) {
671
- if (!limits)
672
- return [undefined] as SDKSignerLimits
673
-
674
- const sdk_limits: SDKSignerLimits = [new Map()]
675
-
676
- for (const [contract, signer_keys] of limits.entries()) {
677
- let sdk_signer_keys: SDKSignerKey[] | undefined
678
-
679
- if (signer_keys?.length) {
680
- sdk_signer_keys = []
681
-
682
- for (const signer_key of signer_keys) {
683
- sdk_signer_keys.push(
684
- this.getSignerKey(signer_key)
685
- )
686
- }
687
- }
688
-
689
- sdk_limits[0]?.set(contract, sdk_signer_keys)
690
- }
691
-
692
- return sdk_limits
693
- }
694
-
695
- private getSignerKey({ key: tag, value }: SignerKey) {
696
- let signer_key: SDKSignerKey
697
-
698
- switch (tag) {
699
- case 'Policy':
700
- signer_key = {
701
- tag,
702
- values: [value]
703
- }
704
- break;
705
- case 'Ed25519':
706
- signer_key = {
707
- tag,
708
- values: [Keypair.fromPublicKey(value).rawPublicKey()]
709
- }
710
- break;
711
- case 'Secp256r1':
712
- signer_key = {
713
- tag,
714
- values: [base64url.toBuffer(value)]
715
- }
716
- break;
717
- }
718
-
719
- return signer_key
720
- }
721
-
722
- private encodeContract(walletPublicKey: string, keyIdBuffer: Buffer) {
723
- let contractId: string | undefined = StrKey.encodeContract(hash(xdr.HashIdPreimage.envelopeTypeContractId(
724
- new xdr.HashIdPreimageContractId({
725
- networkId: hash(Buffer.from(this.networkPassphrase)),
726
- contractIdPreimage: xdr.ContractIdPreimage.contractIdPreimageFromAddress(
727
- new xdr.ContractIdPreimageFromAddress({
728
- address: Address.fromString(walletPublicKey).toScAddress(),
729
- salt: hash(keyIdBuffer),
730
- })
731
- )
732
- })
733
- ).toXDR()));
734
-
735
- return contractId
736
- }
737
-
738
- private async getPublicKey(response: AuthenticatorAttestationResponseJSON) {
739
- let publicKey: Buffer | undefined
740
-
741
- if (response.publicKey) {
742
- publicKey = base64url.toBuffer(response.publicKey)
743
- publicKey = publicKey?.slice(publicKey.length - 65)
744
- }
745
-
746
- if (
747
- !publicKey
748
- || publicKey[0] !== 0x04
749
- || publicKey.length !== 65
750
- ) {
751
- let x: Buffer
752
- let y: Buffer
753
-
754
- if (response.authenticatorData) {
755
- const authenticatorData = base64url.toBuffer(response.authenticatorData)
756
- const credentialIdLength = (authenticatorData[53] << 8) | authenticatorData[54]
757
-
758
- x = authenticatorData.slice(65 + credentialIdLength, 97 + credentialIdLength)
759
- y = authenticatorData.slice(100 + credentialIdLength, 132 + credentialIdLength)
760
- } else {
761
- const attestationObject = base64url.toBuffer(response.attestationObject)
762
-
763
- let publicKeykPrefixSlice = Buffer.from([0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20])
764
- let startIndex = attestationObject.indexOf(publicKeykPrefixSlice)
765
- startIndex = startIndex + publicKeykPrefixSlice.length
766
-
767
- x = attestationObject.slice(startIndex, 32 + startIndex)
768
- y = attestationObject.slice(35 + startIndex, 67 + startIndex)
769
- }
770
-
771
- publicKey = Buffer.from([
772
- 0x04, // (0x04 prefix) https://en.bitcoin.it/wiki/Elliptic_Curve_Digital_Signature_Algorithm
773
- ...x,
774
- ...y
775
- ])
776
- }
777
-
778
- /* TODO
779
- - We're doing some pretty "smart" public key decoding stuff so we should verify the signature against this final public key before assuming it's safe to use and save on-chain
780
- Hmm...Given that `startRegistration` doesn't produce a signature, verifying we've got the correct public key isn't really possible
781
- - This probably needs to be an onchain check, even if just a simulation, just to ensure everything looks good before we get too far adding value etc.
782
- */
783
-
784
- return publicKey
785
- }
786
-
787
- private compactSignature(signature: Buffer) {
788
- // Decode the DER signature
789
- let offset = 2;
790
-
791
- const rLength = signature[offset + 1];
792
- const r = signature.slice(offset + 2, offset + 2 + rLength);
793
-
794
- offset += 2 + rLength;
795
-
796
- const sLength = signature[offset + 1];
797
- const s = signature.slice(offset + 2, offset + 2 + sLength);
798
-
799
- // Convert r and s to BigInt
800
- const rBigInt = BigInt('0x' + r.toString('hex'));
801
- let sBigInt = BigInt('0x' + s.toString('hex'));
802
-
803
- // Ensure s is in the low-S form
804
- // https://github.com/stellar/stellar-protocol/discussions/1435#discussioncomment-8809175
805
- // https://discord.com/channels/897514728459468821/1233048618571927693
806
- // Define the order of the curve secp256r1
807
- // https://github.com/RustCrypto/elliptic-curves/blob/master/p256/src/lib.rs#L72
808
- const n = BigInt('0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551');
809
- const halfN = n / 2n;
810
-
811
- if (sBigInt > halfN)
812
- sBigInt = n - sBigInt;
813
-
814
- // Convert back to buffers and ensure they are 32 bytes
815
- const rPadded = Buffer.from(rBigInt.toString(16).padStart(64, '0'), 'hex');
816
- const sLowS = Buffer.from(sBigInt.toString(16).padStart(64, '0'), 'hex');
817
-
818
- // Concatenate r and low-s
819
- const concatSignature = Buffer.concat([rPadded, sLowS]);
820
-
821
- return concatSignature;
822
- }
823
- }