passkey-kit 0.0.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.
- package/.gitattributes +2 -0
- package/README.md +16 -0
- package/demo/.vscode/extensions.json +3 -0
- package/demo/README.md +47 -0
- package/demo/index.html +13 -0
- package/demo/package.json +22 -0
- package/demo/pnpm-lock.yaml +1253 -0
- package/demo/public/vite.svg +1 -0
- package/demo/src/App.svelte +169 -0
- package/demo/src/app.css +14 -0
- package/demo/src/lib/account.ts +81 -0
- package/demo/src/lib/common.ts +10 -0
- package/demo/src/lib/utils.ts +6 -0
- package/demo/src/main.ts +15 -0
- package/demo/src/vite-env.d.ts +17 -0
- package/demo/svelte.config.js +7 -0
- package/demo/tsconfig.json +22 -0
- package/demo/tsconfig.node.json +10 -0
- package/demo/vite.config.ts +7 -0
- package/esbuild.js +20 -0
- package/package.json +26 -0
- package/passkey-kit/index.d.ts +46 -0
- package/passkey-kit/index.js +135 -0
- package/passkey-kit/index.js.map +7 -0
- package/src/index.ts +473 -0
- package/src/passkey-factory-sdk/README.md +54 -0
- package/src/passkey-factory-sdk/package-lock.json +357 -0
- package/src/passkey-factory-sdk/package.json +18 -0
- package/src/passkey-factory-sdk/src/index.ts +169 -0
- package/src/passkey-factory-sdk/tsconfig.json +98 -0
- package/src/passkey-kit-sdk/README.md +54 -0
- package/src/passkey-kit-sdk/package-lock.json +357 -0
- package/src/passkey-kit-sdk/package.json +18 -0
- package/src/passkey-kit-sdk/src/index.ts +183 -0
- package/src/passkey-kit-sdk/tsconfig.json +98 -0
- package/tsconfig.json +30 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { Client as PasskeyClient } from 'passkey-kit-sdk'
|
|
2
|
+
import { Client as FactoryClient, networks } from 'passkey-factory-sdk'
|
|
3
|
+
import { Address, Networks, StrKey, hash, xdr, Transaction, Horizon, FeeBumpTransaction, SorobanRpc, Operation, scValToNative, TransactionBuilder } from '@stellar/stellar-sdk'
|
|
4
|
+
import { bufToBigint, bigintToBuf } from 'bigint-conversion'
|
|
5
|
+
import base64url from 'base64url'
|
|
6
|
+
import { startRegistration, startAuthentication } from "@simplewebauthn/browser"
|
|
7
|
+
import { decode } from 'cbor-x/decode'
|
|
8
|
+
import type { AuthenticationResponseJSON, RegistrationResponseJSON } from '@simplewebauthn/types';
|
|
9
|
+
import { Buffer } from 'buffer'
|
|
10
|
+
|
|
11
|
+
/* TODO
|
|
12
|
+
- Clean up these params and the interface as a whole
|
|
13
|
+
Might put wallet activities and maybe factory as well into the root of the class vs buried inside this.wallet and this.factory
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export class PasskeyAccount {
|
|
17
|
+
public id: string | undefined
|
|
18
|
+
public sudo: string | undefined
|
|
19
|
+
public wallet: PasskeyClient | undefined
|
|
20
|
+
public factory: FactoryClient
|
|
21
|
+
public sequencePublicKey: string
|
|
22
|
+
public networkPassphrase: Networks
|
|
23
|
+
public horizonUrl: string
|
|
24
|
+
public horizon: Horizon.Server
|
|
25
|
+
public rpcUrl: string
|
|
26
|
+
public rpc: SorobanRpc.Server
|
|
27
|
+
public feeBumpUrl: string
|
|
28
|
+
public feeBumpJwt: string
|
|
29
|
+
public factoryContractId: string = networks.testnet.contractId
|
|
30
|
+
|
|
31
|
+
constructor(options: {
|
|
32
|
+
sequencePublicKey: string,
|
|
33
|
+
networkPassphrase: Networks,
|
|
34
|
+
horizonUrl: string,
|
|
35
|
+
rpcUrl: string,
|
|
36
|
+
feeBumpUrl: string,
|
|
37
|
+
feeBumpJwt: string,
|
|
38
|
+
factoryContractId?: string,
|
|
39
|
+
}) {
|
|
40
|
+
const {
|
|
41
|
+
sequencePublicKey,
|
|
42
|
+
networkPassphrase,
|
|
43
|
+
horizonUrl,
|
|
44
|
+
rpcUrl,
|
|
45
|
+
feeBumpUrl,
|
|
46
|
+
feeBumpJwt,
|
|
47
|
+
factoryContractId
|
|
48
|
+
} = options
|
|
49
|
+
|
|
50
|
+
this.sequencePublicKey = sequencePublicKey
|
|
51
|
+
this.networkPassphrase = networkPassphrase
|
|
52
|
+
this.horizonUrl = horizonUrl
|
|
53
|
+
this.horizon = new Horizon.Server(horizonUrl)
|
|
54
|
+
this.rpcUrl = rpcUrl
|
|
55
|
+
this.rpc = new SorobanRpc.Server(rpcUrl)
|
|
56
|
+
this.feeBumpUrl = feeBumpUrl
|
|
57
|
+
this.feeBumpJwt = feeBumpJwt
|
|
58
|
+
|
|
59
|
+
if (factoryContractId)
|
|
60
|
+
this.factoryContractId = factoryContractId
|
|
61
|
+
|
|
62
|
+
this.factory = new FactoryClient({
|
|
63
|
+
publicKey: sequencePublicKey,
|
|
64
|
+
contractId: this.factoryContractId,
|
|
65
|
+
networkPassphrase,
|
|
66
|
+
rpcUrl
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public async createWallet(name: string, user: string) {
|
|
71
|
+
const { keyId, publicKey } = await this.createKey(name, user)
|
|
72
|
+
|
|
73
|
+
const { result, built } = await this.factory.deploy({
|
|
74
|
+
id: keyId,
|
|
75
|
+
pk: publicKey!
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const contractId = result.unwrap() as string
|
|
79
|
+
|
|
80
|
+
this.wallet = new PasskeyClient({
|
|
81
|
+
publicKey: this.sequencePublicKey,
|
|
82
|
+
contractId,
|
|
83
|
+
networkPassphrase: this.networkPassphrase,
|
|
84
|
+
rpcUrl: this.rpcUrl
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
contractId,
|
|
89
|
+
xdr: built!.toXDR() as string
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public async createKey(name: string, user: string) {
|
|
94
|
+
const startRegistrationResponse = await startRegistration({
|
|
95
|
+
challenge: base64url("sorobanisbest"),
|
|
96
|
+
rp: {
|
|
97
|
+
// id: undefined,
|
|
98
|
+
name,
|
|
99
|
+
},
|
|
100
|
+
user: {
|
|
101
|
+
id: base64url(user),
|
|
102
|
+
name: user,
|
|
103
|
+
displayName: user,
|
|
104
|
+
},
|
|
105
|
+
authenticatorSelection: {
|
|
106
|
+
requireResidentKey: false,
|
|
107
|
+
residentKey: "preferred",
|
|
108
|
+
userVerification: "discouraged",
|
|
109
|
+
},
|
|
110
|
+
pubKeyCredParams: [{ alg: -7, type: "public-key" }],
|
|
111
|
+
attestation: "none",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!this.id) {
|
|
115
|
+
this.id = startRegistrationResponse.id
|
|
116
|
+
|
|
117
|
+
if (!this.sudo)
|
|
118
|
+
this.sudo = startRegistrationResponse.id
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return this.getKey(startRegistrationResponse)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public async connectWallet() {
|
|
125
|
+
const startAuthenticationResponse = await startAuthentication({
|
|
126
|
+
challenge: base64url("sorobanisbest"),
|
|
127
|
+
// rpId: undefined,
|
|
128
|
+
userVerification: "discouraged",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (!this.id)
|
|
132
|
+
this.id = startAuthenticationResponse.id
|
|
133
|
+
|
|
134
|
+
const { keyId, publicKey } = await this.getKey(startAuthenticationResponse)
|
|
135
|
+
|
|
136
|
+
// NOTE might not need this for derivation as all signers are stored in the factory and we can use that lookup as both primary and secondary
|
|
137
|
+
let contractId = StrKey.encodeContract(hash(xdr.HashIdPreimage.envelopeTypeContractId(
|
|
138
|
+
new xdr.HashIdPreimageContractId({
|
|
139
|
+
networkId: hash(Buffer.from(this.networkPassphrase, 'utf-8')),
|
|
140
|
+
contractIdPreimage: xdr.ContractIdPreimage.contractIdPreimageFromAddress(
|
|
141
|
+
new xdr.ContractIdPreimageFromAddress({
|
|
142
|
+
address: Address.fromString(this.factoryContractId).toScAddress(),
|
|
143
|
+
salt: hash(keyId),
|
|
144
|
+
})
|
|
145
|
+
)
|
|
146
|
+
})
|
|
147
|
+
).toXDR()));
|
|
148
|
+
|
|
149
|
+
// attempt passkey id derivation
|
|
150
|
+
try {
|
|
151
|
+
await this.rpc.getContractData(contractId, xdr.ScVal.scvLedgerKeyContractInstance())
|
|
152
|
+
}
|
|
153
|
+
// if that fails look up from the factory mapper
|
|
154
|
+
catch {
|
|
155
|
+
const { val } = await this.rpc.getContractData(this.factoryContractId, xdr.ScVal.scvBytes(keyId))
|
|
156
|
+
contractId = scValToNative(val.contractData().val())
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this.wallet = new PasskeyClient({
|
|
160
|
+
publicKey: this.sequencePublicKey,
|
|
161
|
+
contractId,
|
|
162
|
+
networkPassphrase: this.networkPassphrase,
|
|
163
|
+
rpcUrl: this.rpcUrl
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
// get and set the sudo signer
|
|
167
|
+
await this.getData()
|
|
168
|
+
|
|
169
|
+
return contractId
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
public async sign(
|
|
173
|
+
txn: Transaction | string,
|
|
174
|
+
options?: {
|
|
175
|
+
id?: 'any' | 'sudo' | string | Uint8Array
|
|
176
|
+
ttl?: number
|
|
177
|
+
}
|
|
178
|
+
) {
|
|
179
|
+
// Default mirrors DEFAULT_TIMEOUT (currently 5 minutes) https://github.com/stellar/js-stellar-sdk/blob/master/src/contract/utils.ts#L7
|
|
180
|
+
let { id, ttl = 60 } = options || {}
|
|
181
|
+
|
|
182
|
+
// hack to ensure we don't stack fees when simulating and assembling multiple times
|
|
183
|
+
txn = TransactionBuilder.cloneFrom(new Transaction(
|
|
184
|
+
typeof txn === 'string'
|
|
185
|
+
? txn
|
|
186
|
+
: txn.toXDR(),
|
|
187
|
+
this.networkPassphrase
|
|
188
|
+
), { fee: '0' }).build()
|
|
189
|
+
|
|
190
|
+
// NOTE hard coded to sign only Soroban transactions and only and always the first auth
|
|
191
|
+
const op = txn.operations[0] as Operation.InvokeHostFunction
|
|
192
|
+
const auth = op.auth![0]
|
|
193
|
+
const lastLedger = await this.rpc.getLatestLedger().then(({ sequence }) => sequence)
|
|
194
|
+
const authHash = hash(
|
|
195
|
+
xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
|
|
196
|
+
new xdr.HashIdPreimageSorobanAuthorization({
|
|
197
|
+
networkId: hash(Buffer.from(this.networkPassphrase, 'utf-8')),
|
|
198
|
+
nonce: auth.credentials().address().nonce(),
|
|
199
|
+
signatureExpirationLedger: lastLedger + ttl,
|
|
200
|
+
invocation: auth.rootInvocation()
|
|
201
|
+
})
|
|
202
|
+
).toXDR()
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
const authenticationResponse = await startAuthentication(
|
|
206
|
+
id === 'any' || (id === 'sudo' && !this.sudo)
|
|
207
|
+
? {
|
|
208
|
+
challenge: base64url(authHash),
|
|
209
|
+
// rpId: undefined,
|
|
210
|
+
userVerification: "discouraged",
|
|
211
|
+
}
|
|
212
|
+
: {
|
|
213
|
+
challenge: base64url(authHash),
|
|
214
|
+
// rpId: undefined,
|
|
215
|
+
allowCredentials: [
|
|
216
|
+
{
|
|
217
|
+
id: id === 'sudo'
|
|
218
|
+
? this.sudo!
|
|
219
|
+
: id instanceof Uint8Array
|
|
220
|
+
? base64url(id)
|
|
221
|
+
: id || this.id!,
|
|
222
|
+
type: "public-key",
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
userVerification: "discouraged",
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// set sudo if this is a sudo request
|
|
230
|
+
if (id === 'sudo')
|
|
231
|
+
this.sudo = authenticationResponse.id
|
|
232
|
+
|
|
233
|
+
// reset this.id to be the most recently used passkey
|
|
234
|
+
this.id = authenticationResponse.id
|
|
235
|
+
|
|
236
|
+
const signatureRaw = base64url.toBuffer(authenticationResponse.response.signature);
|
|
237
|
+
const signature = this.convertEcdsaSignatureAsnToCompact(signatureRaw);
|
|
238
|
+
const creds = auth.credentials().address();
|
|
239
|
+
|
|
240
|
+
creds.signatureExpirationLedger(lastLedger + ttl)
|
|
241
|
+
creds.signature(xdr.ScVal.scvMap([
|
|
242
|
+
new xdr.ScMapEntry({
|
|
243
|
+
key: xdr.ScVal.scvSymbol('authenticator_data'),
|
|
244
|
+
val: xdr.ScVal.scvBytes(base64url.toBuffer(authenticationResponse.response.authenticatorData)),
|
|
245
|
+
}),
|
|
246
|
+
new xdr.ScMapEntry({
|
|
247
|
+
key: xdr.ScVal.scvSymbol('client_data_json'),
|
|
248
|
+
val: xdr.ScVal.scvBytes(base64url.toBuffer(authenticationResponse.response.clientDataJSON)),
|
|
249
|
+
}),
|
|
250
|
+
new xdr.ScMapEntry({
|
|
251
|
+
key: xdr.ScVal.scvSymbol('id'),
|
|
252
|
+
val: xdr.ScVal.scvBytes(base64url.toBuffer(authenticationResponse.id)),
|
|
253
|
+
}),
|
|
254
|
+
new xdr.ScMapEntry({
|
|
255
|
+
key: xdr.ScVal.scvSymbol('signature'),
|
|
256
|
+
val: xdr.ScVal.scvBytes(signature),
|
|
257
|
+
}),
|
|
258
|
+
]))
|
|
259
|
+
|
|
260
|
+
const sim = await this.rpc.simulateTransaction(txn)
|
|
261
|
+
|
|
262
|
+
if (
|
|
263
|
+
SorobanRpc.Api.isSimulationError(sim)
|
|
264
|
+
|| SorobanRpc.Api.isSimulationRestore(sim) // TODO handle restore flow
|
|
265
|
+
) throw sim
|
|
266
|
+
|
|
267
|
+
return SorobanRpc.assembleTransaction(txn, sim).build().toXDR()
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
public async send(txn: Transaction, fee: number = 10_000) {
|
|
271
|
+
const data = new FormData();
|
|
272
|
+
|
|
273
|
+
data.set('xdr', txn.toXDR());
|
|
274
|
+
data.set('fee', fee.toString());
|
|
275
|
+
|
|
276
|
+
const bumptxn = await fetch(this.feeBumpUrl, {
|
|
277
|
+
method: 'POST',
|
|
278
|
+
headers: {
|
|
279
|
+
authorization: `Bearer ${this.feeBumpJwt}`,
|
|
280
|
+
},
|
|
281
|
+
body: data
|
|
282
|
+
}).then(async (res) => {
|
|
283
|
+
if (res.ok)
|
|
284
|
+
return res.text()
|
|
285
|
+
else throw await res.json()
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
return this.horizon.submitTransaction(new FeeBumpTransaction(bumptxn, this.networkPassphrase))
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
public async getData() {
|
|
292
|
+
const data: Map<string, any> = new Map()
|
|
293
|
+
|
|
294
|
+
const { val } = await this.rpc.getContractData(
|
|
295
|
+
this.wallet!.options.contractId,
|
|
296
|
+
xdr.ScVal.scvLedgerKeyContractInstance(),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
val.contractData()
|
|
300
|
+
.val()
|
|
301
|
+
.instance()
|
|
302
|
+
.storage()
|
|
303
|
+
?.forEach((entry) => {
|
|
304
|
+
data.set(
|
|
305
|
+
scValToNative(entry.key()),
|
|
306
|
+
scValToNative(entry.val()),
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
this.sudo = base64url(data.get('sudo_sig'))
|
|
311
|
+
|
|
312
|
+
return data
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* TODO
|
|
316
|
+
- Add a getKeyInfo action to get info about a specific passkey
|
|
317
|
+
Specifically looking for name, type, etc. data so a user could grok what signer mapped to what passkey
|
|
318
|
+
@Later
|
|
319
|
+
*/
|
|
320
|
+
|
|
321
|
+
private async getKey(value: RegistrationResponseJSON | AuthenticationResponseJSON) {
|
|
322
|
+
let publicKey: Buffer | undefined
|
|
323
|
+
|
|
324
|
+
if (isAuthentication(value)) {
|
|
325
|
+
const { publicKeyObject } = this.getPublicKeyObject(value.response.attestationObject);
|
|
326
|
+
|
|
327
|
+
publicKey = Buffer.from([
|
|
328
|
+
4, // (0x04 prefix) https://en.bitcoin.it/wiki/Elliptic_Curve_Digital_Signature_Algorithm
|
|
329
|
+
...publicKeyObject.get('-2')!,
|
|
330
|
+
...publicKeyObject.get('-3')!
|
|
331
|
+
])
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
keyId: base64url.toBuffer(value.id),
|
|
336
|
+
publicKey
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private getPublicKeyObject(attestationObject: string) {
|
|
341
|
+
const { authData } = decode(base64url.toBuffer(attestationObject));
|
|
342
|
+
const authDataUint8Array = new Uint8Array(authData);
|
|
343
|
+
const authDataView = new DataView(authDataUint8Array.buffer, 0, authDataUint8Array.length);
|
|
344
|
+
|
|
345
|
+
let offset = 0;
|
|
346
|
+
|
|
347
|
+
// RP ID Hash (32 bytes)
|
|
348
|
+
const rpIdHash = authData.slice(offset, offset + 32);
|
|
349
|
+
offset += 32;
|
|
350
|
+
|
|
351
|
+
// Flags (1 byte)
|
|
352
|
+
const flags = authDataView.getUint8(offset);
|
|
353
|
+
offset += 1;
|
|
354
|
+
|
|
355
|
+
// Sign Count (4 bytes, big endian)
|
|
356
|
+
const signCount = authDataView.getUint32(offset, false);
|
|
357
|
+
offset += 4;
|
|
358
|
+
|
|
359
|
+
// Attested Credential Data, if present
|
|
360
|
+
if (flags & 0x40) { // Checking the AT flag
|
|
361
|
+
// AAGUID (16 bytes)
|
|
362
|
+
const aaguid = authData.slice(offset, offset + 16);
|
|
363
|
+
offset += 16;
|
|
364
|
+
|
|
365
|
+
// Credential ID Length (2 bytes, big endian)
|
|
366
|
+
const credIdLength = authDataView.getUint16(offset, false);
|
|
367
|
+
offset += 2;
|
|
368
|
+
|
|
369
|
+
// Credential ID (variable length)
|
|
370
|
+
const credentialId = authData.slice(offset, offset + credIdLength);
|
|
371
|
+
offset += credIdLength;
|
|
372
|
+
|
|
373
|
+
// Credential Public Key - (77 bytes...I hope)
|
|
374
|
+
const credentialPublicKey = authData.slice(offset, offset + 77);
|
|
375
|
+
offset += 77;
|
|
376
|
+
|
|
377
|
+
// Any leftover bytes. I found some when using a YubiKey
|
|
378
|
+
const theRest = authData.slice(offset);
|
|
379
|
+
|
|
380
|
+
// Decode the credential public key to COSE
|
|
381
|
+
const publicKeyObject = new Map<string, any>(Object.entries(decode(credentialPublicKey)));
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
rpIdHash,
|
|
385
|
+
flags,
|
|
386
|
+
signCount,
|
|
387
|
+
aaguid,
|
|
388
|
+
credIdLength,
|
|
389
|
+
credentialId,
|
|
390
|
+
credentialPublicKey,
|
|
391
|
+
theRest,
|
|
392
|
+
publicKeyObject
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
throw new Error("Attested credential data not present in the flags.");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private convertEcdsaSignatureAsnToCompact(sig: Buffer) {
|
|
400
|
+
// Define the order of the curve secp256k1
|
|
401
|
+
// https://github.com/RustCrypto/elliptic-curves/blob/master/p256/src/lib.rs#L72
|
|
402
|
+
const q = Buffer.from('ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551', 'hex')
|
|
403
|
+
|
|
404
|
+
// ASN Sequence
|
|
405
|
+
let offset = 0;
|
|
406
|
+
if (sig[offset] != 0x30) {
|
|
407
|
+
throw "signature is not a sequence";
|
|
408
|
+
}
|
|
409
|
+
offset += 1;
|
|
410
|
+
|
|
411
|
+
// ASN Sequence Byte Length
|
|
412
|
+
offset += 1;
|
|
413
|
+
|
|
414
|
+
// ASN Integer (R)
|
|
415
|
+
if (sig[offset] != 0x02) {
|
|
416
|
+
throw "first element in sequence is not an integer";
|
|
417
|
+
}
|
|
418
|
+
offset += 1;
|
|
419
|
+
|
|
420
|
+
// ASN Integer (R) Byte Length
|
|
421
|
+
const rLen = sig[offset];
|
|
422
|
+
offset += 1;
|
|
423
|
+
|
|
424
|
+
// ASN Integer (R) Byte Value
|
|
425
|
+
if (rLen >= 33) {
|
|
426
|
+
if (rLen != 33 || sig[offset] != 0x00) {
|
|
427
|
+
throw "can only handle larger than 32 byte R's that are len 33 and lead with zero";
|
|
428
|
+
}
|
|
429
|
+
offset += 1;
|
|
430
|
+
}
|
|
431
|
+
const r = sig.slice(offset, offset + 32);
|
|
432
|
+
offset += 32;
|
|
433
|
+
|
|
434
|
+
// ASN Integer (S)
|
|
435
|
+
if (sig[offset] != 0x02) {
|
|
436
|
+
throw "second element in sequence is not an integer";
|
|
437
|
+
}
|
|
438
|
+
offset += 1;
|
|
439
|
+
|
|
440
|
+
// ASN Integer (S) Byte Length
|
|
441
|
+
const sLen = sig[offset];
|
|
442
|
+
offset += 1;
|
|
443
|
+
|
|
444
|
+
// ASN Integer (S) Byte Value
|
|
445
|
+
if (sLen >= 33) {
|
|
446
|
+
if (sLen != 33 || sig[offset] != 0x00) {
|
|
447
|
+
throw "can only handle larger than 32 byte R's that are len 33 and lead with zero";
|
|
448
|
+
}
|
|
449
|
+
offset += 1;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const s = sig.slice(offset, offset + 32);
|
|
453
|
+
|
|
454
|
+
offset += 32;
|
|
455
|
+
|
|
456
|
+
let signature64: Buffer
|
|
457
|
+
|
|
458
|
+
// Force low S range
|
|
459
|
+
// https://github.com/stellar/stellar-protocol/discussions/1435#discussioncomment-8809175
|
|
460
|
+
// https://discord.com/channels/897514728459468821/1233048618571927693
|
|
461
|
+
if (bufToBigint(s) > ((bufToBigint(q) - BigInt(1)) / BigInt(2))) {
|
|
462
|
+
signature64 = Buffer.from([...r, ...Buffer.from(bigintToBuf(bufToBigint(q) - bufToBigint(s), true) as ArrayBuffer)]);
|
|
463
|
+
} else {
|
|
464
|
+
signature64 = Buffer.from([...r, ...s]);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return signature64;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function isAuthentication(value: any): value is RegistrationResponseJSON {
|
|
472
|
+
return value?.response?.attestationObject;
|
|
473
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# passkey-factory-sdk JS
|
|
2
|
+
|
|
3
|
+
JS library for interacting with [Soroban](https://soroban.stellar.org/) smart contract `passkey-factory-sdk` via Soroban RPC.
|
|
4
|
+
|
|
5
|
+
This library was automatically generated by Soroban CLI using a command similar to:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
soroban contract bindings ts \
|
|
9
|
+
--rpc-url https://soroban-testnet.stellar.org \
|
|
10
|
+
--network-passphrase "Test SDF Network ; September 2015" \
|
|
11
|
+
--contract-id CDSGG7BSWYWQMTY5KTZVDTC34ZESMZ75ZGSTTVHS4NIKCF4PM3GDJ4G3 \
|
|
12
|
+
--output-dir ./path/to/passkey-factory-sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The network passphrase and contract ID are exported from [index.ts](./src/index.ts) in the `networks` constant. If you are the one who generated this library and you know that this contract is also deployed to other networks, feel free to update `networks` with other valid options. This will help your contract consumers use this library more easily.
|
|
16
|
+
|
|
17
|
+
# To publish or not to publish
|
|
18
|
+
|
|
19
|
+
This library is suitable for publishing to NPM. You can publish it to NPM using the `npm publish` command.
|
|
20
|
+
|
|
21
|
+
But you don't need to publish this library to NPM to use it. You can add it to your project's `package.json` using a file path:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"passkey-factory-sdk": "./path/to/this/folder"
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
However, we've actually encountered [frustration](https://github.com/stellar/soroban-example-dapp/pull/117#discussion_r1232873560) using local libraries with NPM in this way. Though it seems a bit messy, we suggest generating the library directly to your `node_modules` folder automatically after each install by using a `postinstall` script. We've had the least trouble with this approach. NPM will automatically remove what it sees as erroneous directories during the `install` step, and then regenerate them when it gets to your `postinstall` step, which will keep the library up-to-date with your contract.
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
"scripts": {
|
|
33
|
+
"postinstall": "soroban contract bindings ts --rpc-url https://soroban-testnet.stellar.org --network-passphrase \"Test SDF Network ; September 2015\" --id CDSGG7BSWYWQMTY5KTZVDTC34ZESMZ75ZGSTTVHS4NIKCF4PM3GDJ4G3 --name passkey-factory-sdk"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Obviously you need to adjust the above command based on the actual command you used to generate the library.
|
|
38
|
+
|
|
39
|
+
# Use it
|
|
40
|
+
|
|
41
|
+
Now that you have your library up-to-date and added to your project, you can import it in a file and see inline documentation for all of its exported methods:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
import { Contract, networks } from "passkey-factory-sdk"
|
|
45
|
+
|
|
46
|
+
const contract = new Contract({
|
|
47
|
+
...networks.futurenet, // for example; check which networks this library exports
|
|
48
|
+
rpcUrl: '...', // use your own, or find one for testing at https://soroban.stellar.org/docs/reference/rpc#public-rpc-providers
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
contract.|
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
As long as your editor is configured to show JavaScript/TypeScript documentation, you can pause your typing at that `|` to get a list of all exports and inline-documentation for each. It exports a separate [async](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) function for each method in the smart contract, with documentation for each generated from the comments the contract's author included in the original source code.
|