@soropass/core 0.1.0
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/dist/checkAuth-C2AcXqFe.d.cts +110 -0
- package/dist/checkAuth-C2AcXqFe.d.ts +110 -0
- package/dist/chunk-AHFGTIBY.js +36 -0
- package/dist/chunk-AHFGTIBY.js.map +1 -0
- package/dist/chunk-AMYFJQAC.js +29 -0
- package/dist/chunk-AMYFJQAC.js.map +1 -0
- package/dist/chunk-BIRF5I6I.js +265 -0
- package/dist/chunk-BIRF5I6I.js.map +1 -0
- package/dist/chunk-JNJ2FY45.js +209 -0
- package/dist/chunk-JNJ2FY45.js.map +1 -0
- package/dist/chunk-LB4HOTLV.js +135 -0
- package/dist/chunk-LB4HOTLV.js.map +1 -0
- package/dist/chunk-LM2AZXKZ.js +108 -0
- package/dist/chunk-LM2AZXKZ.js.map +1 -0
- package/dist/chunk-LUDXP56M.js +3 -0
- package/dist/chunk-LUDXP56M.js.map +1 -0
- package/dist/chunk-MMB4ZIW3.js +48 -0
- package/dist/chunk-MMB4ZIW3.js.map +1 -0
- package/dist/chunk-MOLZIRIH.js +22 -0
- package/dist/chunk-MOLZIRIH.js.map +1 -0
- package/dist/chunk-OGSUGATA.js +3 -0
- package/dist/chunk-OGSUGATA.js.map +1 -0
- package/dist/chunk-TPBBZNPP.js +30 -0
- package/dist/chunk-TPBBZNPP.js.map +1 -0
- package/dist/chunk-UQKNPO44.js +32 -0
- package/dist/chunk-UQKNPO44.js.map +1 -0
- package/dist/chunk-UV5HORMF.js +175 -0
- package/dist/chunk-UV5HORMF.js.map +1 -0
- package/dist/chunk-W75OQKHR.js +133 -0
- package/dist/chunk-W75OQKHR.js.map +1 -0
- package/dist/connect.cjs +166 -0
- package/dist/connect.cjs.map +1 -0
- package/dist/connect.d.cts +24 -0
- package/dist/connect.d.ts +24 -0
- package/dist/connect.js +6 -0
- package/dist/connect.js.map +1 -0
- package/dist/create.cjs +483 -0
- package/dist/create.cjs.map +1 -0
- package/dist/create.d.cts +106 -0
- package/dist/create.d.ts +106 -0
- package/dist/create.js +8 -0
- package/dist/create.js.map +1 -0
- package/dist/index.cjs +1542 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +197 -0
- package/dist/index.d.ts +197 -0
- package/dist/index.js +354 -0
- package/dist/index.js.map +1 -0
- package/dist/recover-0wbOXDjR.d.cts +23 -0
- package/dist/recover-COnsxhYk.d.ts +23 -0
- package/dist/recover.cjs +802 -0
- package/dist/recover.cjs.map +1 -0
- package/dist/recover.d.cts +49 -0
- package/dist/recover.d.ts +49 -0
- package/dist/recover.js +10 -0
- package/dist/recover.js.map +1 -0
- package/dist/sign-Cfg0NM0T.d.cts +73 -0
- package/dist/sign-Cfg0NM0T.d.ts +73 -0
- package/dist/sign.cjs +883 -0
- package/dist/sign.cjs.map +1 -0
- package/dist/sign.d.cts +143 -0
- package/dist/sign.d.ts +143 -0
- package/dist/sign.js +8 -0
- package/dist/sign.js.map +1 -0
- package/dist/submit-BDKADQq0.d.cts +112 -0
- package/dist/submit-BO0Nh49I.d.ts +112 -0
- package/dist/testing.cjs +955 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +86 -0
- package/dist/testing.d.ts +86 -0
- package/dist/testing.js +235 -0
- package/dist/testing.js.map +1 -0
- package/dist/types--WNl-Sda.d.cts +125 -0
- package/dist/types--WNl-Sda.d.ts +125 -0
- package/dist/types-dMRWEEo0.d.cts +73 -0
- package/dist/types-lW2h8Uxn.d.ts +73 -0
- package/dist/types.cjs +44 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.cts +5 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +117 -0
package/dist/testing.cjs
ADDED
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var nist = require('@noble/curves/nist');
|
|
4
|
+
var sha256 = require('@noble/hashes/sha256');
|
|
5
|
+
var stellarSdk = require('@stellar/stellar-sdk');
|
|
6
|
+
|
|
7
|
+
// src/errors.ts
|
|
8
|
+
var KitError = class _KitError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
name = "KitError";
|
|
11
|
+
constructor(code, message, options) {
|
|
12
|
+
super(message ?? code, options);
|
|
13
|
+
this.code = code;
|
|
14
|
+
Object.setPrototypeOf(this, _KitError.prototype);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var ALG_ES256 = -7;
|
|
18
|
+
function assertES256(alg) {
|
|
19
|
+
if (alg !== ALG_ES256) {
|
|
20
|
+
throw new KitError("ES256_NOT_SUPPORTED", `COSE alg ${alg} is not ES256 (-7)`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function assertUserActivation(activation) {
|
|
24
|
+
const ua = activation ?? globalThis.navigator?.userActivation;
|
|
25
|
+
if (ua && !ua.isActive) {
|
|
26
|
+
throw new KitError(
|
|
27
|
+
"USER_CANCELLED",
|
|
28
|
+
"WebAuthn ceremony must be invoked inside a user activation (click/tap)"
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/internal/encoding.ts
|
|
34
|
+
function bytesToBase64Url(bytes) {
|
|
35
|
+
let binary = "";
|
|
36
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
37
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
38
|
+
}
|
|
39
|
+
function base64UrlToBytes(input) {
|
|
40
|
+
const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
41
|
+
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
42
|
+
const binary = atob(padded);
|
|
43
|
+
const out = new Uint8Array(new ArrayBuffer(binary.length));
|
|
44
|
+
for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function utf8ToBytes(text) {
|
|
48
|
+
return new TextEncoder().encode(text);
|
|
49
|
+
}
|
|
50
|
+
function bytesToUtf8(bytes) {
|
|
51
|
+
return new TextDecoder().decode(bytes);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/webauthn/createOptions.ts
|
|
55
|
+
function buildCreateOptions(input) {
|
|
56
|
+
const residentKey = input.residentKey ?? "required";
|
|
57
|
+
return {
|
|
58
|
+
rp: { id: input.rpId, name: input.rpName },
|
|
59
|
+
user: {
|
|
60
|
+
id: bytesToBase64Url(input.userId ?? utf8ToBytes(input.userName)),
|
|
61
|
+
name: input.userName,
|
|
62
|
+
displayName: input.userDisplayName ?? input.userName
|
|
63
|
+
},
|
|
64
|
+
challenge: bytesToBase64Url(input.challenge),
|
|
65
|
+
pubKeyCredParams: [{ type: "public-key", alg: -7 }],
|
|
66
|
+
authenticatorSelection: {
|
|
67
|
+
residentKey,
|
|
68
|
+
requireResidentKey: residentKey === "required",
|
|
69
|
+
userVerification: input.userVerification ?? "preferred"
|
|
70
|
+
},
|
|
71
|
+
attestation: "none",
|
|
72
|
+
timeout: input.timeout ?? 6e4
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/internal/bytes.ts
|
|
77
|
+
function concatBytes(...arrays) {
|
|
78
|
+
const total = arrays.reduce((n, a) => n + a.length, 0);
|
|
79
|
+
const out = new Uint8Array(total);
|
|
80
|
+
let offset = 0;
|
|
81
|
+
for (const a of arrays) {
|
|
82
|
+
out.set(a, offset);
|
|
83
|
+
offset += a.length;
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
function equalBytes(a, b) {
|
|
88
|
+
if (a.length !== b.length) return false;
|
|
89
|
+
let diff = 0;
|
|
90
|
+
for (let i = 0; i < a.length; i += 1) diff |= (a[i] ?? 0) ^ (b[i] ?? 0);
|
|
91
|
+
return diff === 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/internal/cbor.ts
|
|
95
|
+
function byteAt(bytes, index) {
|
|
96
|
+
const value = bytes[index];
|
|
97
|
+
if (value === void 0) throw new Error(`CBOR: unexpected end of input at byte ${index}`);
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
var MAX_DEPTH = 16;
|
|
101
|
+
function decodeCbor(bytes, offset = 0, depth = 0) {
|
|
102
|
+
if (depth > MAX_DEPTH) throw new Error("CBOR: maximum nesting depth exceeded");
|
|
103
|
+
const first = byteAt(bytes, offset);
|
|
104
|
+
const major = first >> 5;
|
|
105
|
+
const info = first & 31;
|
|
106
|
+
let pos = offset + 1;
|
|
107
|
+
let argument;
|
|
108
|
+
if (info < 24) {
|
|
109
|
+
argument = info;
|
|
110
|
+
} else if (info === 24) {
|
|
111
|
+
argument = byteAt(bytes, pos);
|
|
112
|
+
pos += 1;
|
|
113
|
+
} else if (info === 25) {
|
|
114
|
+
argument = byteAt(bytes, pos) << 8 | byteAt(bytes, pos + 1);
|
|
115
|
+
pos += 2;
|
|
116
|
+
} else if (info === 26) {
|
|
117
|
+
argument = byteAt(bytes, pos) * 16777216 + (byteAt(bytes, pos + 1) << 16) + (byteAt(bytes, pos + 2) << 8) + byteAt(bytes, pos + 3);
|
|
118
|
+
pos += 4;
|
|
119
|
+
} else {
|
|
120
|
+
throw new Error(`CBOR: unsupported length encoding (additional info ${info})`);
|
|
121
|
+
}
|
|
122
|
+
switch (major) {
|
|
123
|
+
case 0:
|
|
124
|
+
return { value: argument, length: pos - offset };
|
|
125
|
+
case 1:
|
|
126
|
+
return { value: -1 - argument, length: pos - offset };
|
|
127
|
+
case 2: {
|
|
128
|
+
const end = pos + argument;
|
|
129
|
+
if (end > bytes.length) throw new Error("CBOR: byte string exceeds input");
|
|
130
|
+
return { value: bytes.slice(pos, end), length: end - offset };
|
|
131
|
+
}
|
|
132
|
+
case 3: {
|
|
133
|
+
const end = pos + argument;
|
|
134
|
+
if (end > bytes.length) throw new Error("CBOR: text string exceeds input");
|
|
135
|
+
return { value: new TextDecoder().decode(bytes.slice(pos, end)), length: end - offset };
|
|
136
|
+
}
|
|
137
|
+
case 4: {
|
|
138
|
+
const items = [];
|
|
139
|
+
let cursor = pos;
|
|
140
|
+
for (let i = 0; i < argument; i += 1) {
|
|
141
|
+
const item = decodeCbor(bytes, cursor, depth + 1);
|
|
142
|
+
items.push(item.value);
|
|
143
|
+
cursor += item.length;
|
|
144
|
+
}
|
|
145
|
+
return { value: items, length: cursor - offset };
|
|
146
|
+
}
|
|
147
|
+
case 5: {
|
|
148
|
+
const map = /* @__PURE__ */ new Map();
|
|
149
|
+
let cursor = pos;
|
|
150
|
+
for (let i = 0; i < argument; i += 1) {
|
|
151
|
+
const key = decodeCbor(bytes, cursor, depth + 1);
|
|
152
|
+
cursor += key.length;
|
|
153
|
+
const val = decodeCbor(bytes, cursor, depth + 1);
|
|
154
|
+
cursor += val.length;
|
|
155
|
+
if (typeof key.value !== "number" && typeof key.value !== "string") {
|
|
156
|
+
throw new Error("CBOR: unsupported map key type");
|
|
157
|
+
}
|
|
158
|
+
if (map.has(key.value)) {
|
|
159
|
+
throw new Error("CBOR: duplicate map key (non-canonical)");
|
|
160
|
+
}
|
|
161
|
+
map.set(key.value, val.value);
|
|
162
|
+
}
|
|
163
|
+
return { value: map, length: cursor - offset };
|
|
164
|
+
}
|
|
165
|
+
default:
|
|
166
|
+
throw new Error(`CBOR: unsupported major type ${major}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function decodeCborFirst(bytes) {
|
|
170
|
+
return decodeCbor(bytes, 0).value;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/webauthn/authData.ts
|
|
174
|
+
var MIN_LENGTH = 37;
|
|
175
|
+
function parseAuthenticatorData(authData) {
|
|
176
|
+
if (authData.length < MIN_LENGTH) {
|
|
177
|
+
throw new KitError(
|
|
178
|
+
"UNSUPPORTED_AUTHENTICATOR",
|
|
179
|
+
`authenticatorData is ${authData.length} bytes, expected at least ${MIN_LENGTH}`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const view = new DataView(authData.buffer, authData.byteOffset, authData.byteLength);
|
|
183
|
+
const rpIdHash = authData.slice(0, 32);
|
|
184
|
+
const bits = authData[32] ?? 0;
|
|
185
|
+
const flags = {
|
|
186
|
+
up: (bits & 1) !== 0,
|
|
187
|
+
uv: (bits & 4) !== 0,
|
|
188
|
+
be: (bits & 8) !== 0,
|
|
189
|
+
bs: (bits & 16) !== 0,
|
|
190
|
+
at: (bits & 64) !== 0,
|
|
191
|
+
ed: (bits & 128) !== 0,
|
|
192
|
+
bits
|
|
193
|
+
};
|
|
194
|
+
const signCount = view.getUint32(33, false);
|
|
195
|
+
const result = { rpIdHash, flags, signCount };
|
|
196
|
+
if (flags.at) {
|
|
197
|
+
if (authData.length < 55) {
|
|
198
|
+
throw new KitError(
|
|
199
|
+
"UNSUPPORTED_AUTHENTICATOR",
|
|
200
|
+
"authenticatorData truncated in attested data"
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
result.aaguid = authData.slice(37, 53);
|
|
204
|
+
const credIdLength = view.getUint16(53, false);
|
|
205
|
+
const credIdEnd = 55 + credIdLength;
|
|
206
|
+
if (authData.length < credIdEnd) {
|
|
207
|
+
throw new KitError(
|
|
208
|
+
"UNSUPPORTED_AUTHENTICATOR",
|
|
209
|
+
"authenticatorData truncated in credentialId"
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
result.credentialId = authData.slice(55, credIdEnd);
|
|
213
|
+
const rest = authData.slice(credIdEnd);
|
|
214
|
+
const { length } = decodeCbor(rest, 0);
|
|
215
|
+
result.credentialPublicKey = rest.slice(0, length);
|
|
216
|
+
}
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
function verifyRpIdHash(authData, rpId) {
|
|
220
|
+
const actual = parseAuthenticatorData(authData).rpIdHash;
|
|
221
|
+
const expected = sha256.sha256(utf8ToBytes(rpId));
|
|
222
|
+
if (!equalBytes(actual, expected)) {
|
|
223
|
+
throw new KitError(
|
|
224
|
+
"RP_ID_MISMATCH",
|
|
225
|
+
`authenticatorData rpIdHash does not match SHA-256("${rpId}")`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/webauthn/publicKey.ts
|
|
231
|
+
var COSE_KTY = 1;
|
|
232
|
+
var COSE_ALG = 3;
|
|
233
|
+
var COSE_CRV = -1;
|
|
234
|
+
var COSE_X = -2;
|
|
235
|
+
var COSE_Y = -3;
|
|
236
|
+
var KTY_EC2 = 2;
|
|
237
|
+
var CRV_P256 = 1;
|
|
238
|
+
function coseKeyToSec1(coseKey) {
|
|
239
|
+
let decoded;
|
|
240
|
+
try {
|
|
241
|
+
decoded = decodeCborFirst(coseKey);
|
|
242
|
+
} catch (cause) {
|
|
243
|
+
throw new KitError("INVALID_PUBLIC_KEY", "failed to CBOR-decode COSE key", { cause });
|
|
244
|
+
}
|
|
245
|
+
if (!(decoded instanceof Map)) {
|
|
246
|
+
throw new KitError("INVALID_PUBLIC_KEY", "COSE key is not a CBOR map");
|
|
247
|
+
}
|
|
248
|
+
const map = decoded;
|
|
249
|
+
const alg = map.get(COSE_ALG);
|
|
250
|
+
if (typeof alg !== "number") {
|
|
251
|
+
throw new KitError("ES256_NOT_SUPPORTED", `COSE alg ${String(alg)} is not ES256 (-7)`);
|
|
252
|
+
}
|
|
253
|
+
assertES256(alg);
|
|
254
|
+
if (map.get(COSE_KTY) !== KTY_EC2) {
|
|
255
|
+
throw new KitError(
|
|
256
|
+
"INVALID_PUBLIC_KEY",
|
|
257
|
+
`COSE kty ${String(map.get(COSE_KTY))} is not EC2 (2)`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
if (map.get(COSE_CRV) !== CRV_P256) {
|
|
261
|
+
throw new KitError(
|
|
262
|
+
"INVALID_PUBLIC_KEY",
|
|
263
|
+
`COSE crv ${String(map.get(COSE_CRV))} is not P-256 (1)`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const x = map.get(COSE_X);
|
|
267
|
+
const y = map.get(COSE_Y);
|
|
268
|
+
if (!(x instanceof Uint8Array) || x.length !== 32) {
|
|
269
|
+
throw new KitError("INVALID_PUBLIC_KEY", "COSE key X coordinate is not 32 bytes");
|
|
270
|
+
}
|
|
271
|
+
if (!(y instanceof Uint8Array) || y.length !== 32) {
|
|
272
|
+
throw new KitError("INVALID_PUBLIC_KEY", "COSE key Y coordinate is not 32 bytes");
|
|
273
|
+
}
|
|
274
|
+
const sec1 = concatBytes(new Uint8Array([4]), x, y);
|
|
275
|
+
try {
|
|
276
|
+
nist.p256.Point.fromHex(sec1);
|
|
277
|
+
} catch (cause) {
|
|
278
|
+
throw new KitError("INVALID_PUBLIC_KEY", "public key is not a valid P-256 point", { cause });
|
|
279
|
+
}
|
|
280
|
+
return sec1;
|
|
281
|
+
}
|
|
282
|
+
function extractPublicKeyFromAuthData(authData) {
|
|
283
|
+
const parsed = parseAuthenticatorData(authData);
|
|
284
|
+
if (!parsed.credentialPublicKey) {
|
|
285
|
+
throw new KitError(
|
|
286
|
+
"INVALID_PUBLIC_KEY",
|
|
287
|
+
"authenticatorData has no attested credential public key"
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
return coseKeyToSec1(parsed.credentialPublicKey);
|
|
291
|
+
}
|
|
292
|
+
function extractPublicKeyFromAttestationObject(attestationObject) {
|
|
293
|
+
let decoded;
|
|
294
|
+
try {
|
|
295
|
+
decoded = decodeCborFirst(attestationObject);
|
|
296
|
+
} catch (cause) {
|
|
297
|
+
throw new KitError("INVALID_PUBLIC_KEY", "failed to CBOR-decode attestationObject", { cause });
|
|
298
|
+
}
|
|
299
|
+
if (!(decoded instanceof Map)) {
|
|
300
|
+
throw new KitError("INVALID_PUBLIC_KEY", "attestationObject is not a CBOR map");
|
|
301
|
+
}
|
|
302
|
+
const authData = decoded.get("authData");
|
|
303
|
+
if (!(authData instanceof Uint8Array)) {
|
|
304
|
+
throw new KitError("INVALID_PUBLIC_KEY", "attestationObject has no authData byte string");
|
|
305
|
+
}
|
|
306
|
+
return extractPublicKeyFromAuthData(authData);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/ceremonies/random.ts
|
|
310
|
+
function randomBytes(length) {
|
|
311
|
+
const webcrypto = globalThis.crypto;
|
|
312
|
+
if (typeof webcrypto?.getRandomValues !== "function") {
|
|
313
|
+
throw new KitError(
|
|
314
|
+
"UNSUPPORTED_AUTHENTICATOR",
|
|
315
|
+
"no secure random source (globalThis.crypto.getRandomValues) is available in this environment"
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
const out = new Uint8Array(new ArrayBuffer(length));
|
|
319
|
+
webcrypto.getRandomValues(out);
|
|
320
|
+
return out;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/ceremonies/browserClient.ts
|
|
324
|
+
function asKitError(error) {
|
|
325
|
+
if (error instanceof KitError) return error;
|
|
326
|
+
const name = typeof error === "object" && error !== null && "name" in error ? error.name : void 0;
|
|
327
|
+
if (name === "NotAllowedError") {
|
|
328
|
+
return new KitError("USER_CANCELLED", "The passkey prompt was dismissed.", { cause: error });
|
|
329
|
+
}
|
|
330
|
+
const message = error instanceof Error ? error.message : "WebAuthn request failed";
|
|
331
|
+
const detail = typeof name === "string" ? `${name}: ${message}` : message;
|
|
332
|
+
return new KitError("UNSUPPORTED_AUTHENTICATOR", detail, { cause: error });
|
|
333
|
+
}
|
|
334
|
+
function browserWebAuthnClient() {
|
|
335
|
+
const credentials = globalThis.navigator?.credentials;
|
|
336
|
+
if (!credentials) {
|
|
337
|
+
throw new KitError("UNSUPPORTED_AUTHENTICATOR", "navigator.credentials is unavailable");
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
async create(options) {
|
|
341
|
+
const publicKey = {
|
|
342
|
+
challenge: base64UrlToBytes(options.challenge),
|
|
343
|
+
rp: options.rp,
|
|
344
|
+
user: {
|
|
345
|
+
id: base64UrlToBytes(options.user.id),
|
|
346
|
+
name: options.user.name,
|
|
347
|
+
displayName: options.user.displayName
|
|
348
|
+
},
|
|
349
|
+
pubKeyCredParams: options.pubKeyCredParams,
|
|
350
|
+
authenticatorSelection: options.authenticatorSelection,
|
|
351
|
+
attestation: options.attestation,
|
|
352
|
+
timeout: options.timeout
|
|
353
|
+
};
|
|
354
|
+
let credential;
|
|
355
|
+
try {
|
|
356
|
+
credential = await credentials.create({ publicKey });
|
|
357
|
+
} catch (error) {
|
|
358
|
+
throw asKitError(error);
|
|
359
|
+
}
|
|
360
|
+
if (!credential) throw new KitError("USER_CANCELLED", "credential creation returned null");
|
|
361
|
+
const response = credential.response;
|
|
362
|
+
return {
|
|
363
|
+
id: credential.id,
|
|
364
|
+
rawId: new Uint8Array(credential.rawId),
|
|
365
|
+
attestationObject: new Uint8Array(response.attestationObject)
|
|
366
|
+
};
|
|
367
|
+
},
|
|
368
|
+
async get(options) {
|
|
369
|
+
const publicKey = {
|
|
370
|
+
rpId: options.rpId,
|
|
371
|
+
challenge: options.challenge ? new Uint8Array(options.challenge) : randomBytes(32),
|
|
372
|
+
allowCredentials: (options.allowCredentials ?? []).map((id) => ({
|
|
373
|
+
type: "public-key",
|
|
374
|
+
id: base64UrlToBytes(id)
|
|
375
|
+
})),
|
|
376
|
+
userVerification: options.userVerification
|
|
377
|
+
};
|
|
378
|
+
const request = { publicKey };
|
|
379
|
+
if (options.mediation) request.mediation = options.mediation;
|
|
380
|
+
let credential;
|
|
381
|
+
try {
|
|
382
|
+
credential = await credentials.get(request);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
throw asKitError(error);
|
|
385
|
+
}
|
|
386
|
+
if (!credential) throw new KitError("USER_CANCELLED", "assertion returned null");
|
|
387
|
+
const response = credential.response;
|
|
388
|
+
return {
|
|
389
|
+
id: credential.id,
|
|
390
|
+
rawId: new Uint8Array(credential.rawId),
|
|
391
|
+
authenticatorData: new Uint8Array(response.authenticatorData),
|
|
392
|
+
clientDataJSON: new Uint8Array(response.clientDataJSON),
|
|
393
|
+
signature: new Uint8Array(response.signature)
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function defaultCredentialStorage() {
|
|
399
|
+
const key = (rpId) => `stellar-passkey:${rpId}`;
|
|
400
|
+
const ls = globalThis.localStorage;
|
|
401
|
+
if (ls) {
|
|
402
|
+
return {
|
|
403
|
+
get: (rpId) => ls.getItem(key(rpId)),
|
|
404
|
+
set: (rpId, credentialId) => {
|
|
405
|
+
ls.setItem(key(rpId), credentialId);
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
const memory = /* @__PURE__ */ new Map();
|
|
410
|
+
return {
|
|
411
|
+
get: (rpId) => memory.get(rpId) ?? null,
|
|
412
|
+
set: (rpId, credentialId) => {
|
|
413
|
+
memory.set(rpId, credentialId);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/ceremonies/create.ts
|
|
419
|
+
async function registerPasskey(options) {
|
|
420
|
+
assertUserActivation(options.userActivation);
|
|
421
|
+
const webauthn = options.webauthn ?? browserWebAuthnClient();
|
|
422
|
+
const creationOptions = buildCreateOptions({
|
|
423
|
+
rpId: options.rpId,
|
|
424
|
+
rpName: options.rpName,
|
|
425
|
+
userName: options.userName,
|
|
426
|
+
userId: options.userId,
|
|
427
|
+
challenge: options.challenge ?? randomBytes(32),
|
|
428
|
+
residentKey: options.residentKey,
|
|
429
|
+
userVerification: options.userVerification
|
|
430
|
+
});
|
|
431
|
+
const credential = await webauthn.create(creationOptions);
|
|
432
|
+
const publicKey = extractPublicKeyFromAttestationObject(credential.attestationObject);
|
|
433
|
+
return { credentialId: credential.id, publicKey };
|
|
434
|
+
}
|
|
435
|
+
async function createPasskey(options) {
|
|
436
|
+
const storage = options.storage ?? defaultCredentialStorage();
|
|
437
|
+
const { credentialId, publicKey } = await registerPasskey(options);
|
|
438
|
+
const { contractId } = await options.deployer.deploy({ publicKey, credentialId });
|
|
439
|
+
storage.set(options.rpId, credentialId);
|
|
440
|
+
return { contractId, credentialId, publicKey };
|
|
441
|
+
}
|
|
442
|
+
async function connect(options) {
|
|
443
|
+
const storage = options.storage ?? defaultCredentialStorage();
|
|
444
|
+
const credentialId = storage.get(options.rpId);
|
|
445
|
+
if (!credentialId) return null;
|
|
446
|
+
const accounts = await options.indexer.resolveByCredential(credentialId);
|
|
447
|
+
const contractId = accounts[0]?.contractId;
|
|
448
|
+
return contractId ? { contractId, credentialId } : null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/ceremonies/recover.ts
|
|
452
|
+
async function recover(options) {
|
|
453
|
+
assertUserActivation(options.userActivation);
|
|
454
|
+
const webauthn = options.webauthn ?? browserWebAuthnClient();
|
|
455
|
+
const assertion = await webauthn.get({
|
|
456
|
+
rpId: options.rpId,
|
|
457
|
+
allowCredentials: [],
|
|
458
|
+
challenge: options.challenge ?? randomBytes(32)
|
|
459
|
+
});
|
|
460
|
+
const accounts = await options.indexer.resolveByCredential(assertion.id);
|
|
461
|
+
return accounts.map((account) => ({
|
|
462
|
+
contractId: account.contractId,
|
|
463
|
+
credentialId: assertion.id
|
|
464
|
+
}));
|
|
465
|
+
}
|
|
466
|
+
var MAX_DER_LENGTH = 72;
|
|
467
|
+
function derToCompact(der) {
|
|
468
|
+
if (der.length > MAX_DER_LENGTH) {
|
|
469
|
+
throw new KitError(
|
|
470
|
+
"INVALID_SIGNATURE_DER",
|
|
471
|
+
`DER signature is ${der.length} bytes, exceeds max ${MAX_DER_LENGTH}`
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
return nist.p256.Signature.fromDER(der).toCompactRawBytes();
|
|
476
|
+
} catch (cause) {
|
|
477
|
+
throw new KitError("INVALID_SIGNATURE_DER", "failed to parse DER signature", { cause });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function assertCompactLength(compactSignature) {
|
|
481
|
+
if (compactSignature.length !== 64) {
|
|
482
|
+
throw new KitError(
|
|
483
|
+
"INVALID_SIGNATURE_DER",
|
|
484
|
+
`compact signature must be 64 bytes, got ${compactSignature.length}`
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function normalizeLowS(compactSignature) {
|
|
489
|
+
assertCompactLength(compactSignature);
|
|
490
|
+
return nist.p256.Signature.fromCompact(compactSignature).normalizeS().toCompactRawBytes();
|
|
491
|
+
}
|
|
492
|
+
function derToCompactLowS(der) {
|
|
493
|
+
return normalizeLowS(derToCompact(der));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/webauthn/clientData.ts
|
|
497
|
+
function parseClientDataJSON(clientDataJSON) {
|
|
498
|
+
let raw;
|
|
499
|
+
try {
|
|
500
|
+
raw = JSON.parse(bytesToUtf8(clientDataJSON));
|
|
501
|
+
} catch (cause) {
|
|
502
|
+
throw new KitError("UNSUPPORTED_AUTHENTICATOR", "clientDataJSON is not valid JSON", { cause });
|
|
503
|
+
}
|
|
504
|
+
if (typeof raw !== "object" || raw === null) {
|
|
505
|
+
throw new KitError("UNSUPPORTED_AUTHENTICATOR", "clientDataJSON is not a JSON object");
|
|
506
|
+
}
|
|
507
|
+
const obj = raw;
|
|
508
|
+
const { type, challenge, origin, crossOrigin } = obj;
|
|
509
|
+
if (typeof type !== "string" || typeof challenge !== "string" || typeof origin !== "string") {
|
|
510
|
+
throw new KitError("UNSUPPORTED_AUTHENTICATOR", "clientDataJSON is missing required fields");
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
type,
|
|
514
|
+
challenge,
|
|
515
|
+
origin,
|
|
516
|
+
crossOrigin: typeof crossOrigin === "boolean" ? crossOrigin : void 0
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function verifyClientDataJSON(clientDataJSON, expected) {
|
|
520
|
+
const data = parseClientDataJSON(clientDataJSON);
|
|
521
|
+
if (data.type !== "webauthn.get") {
|
|
522
|
+
throw new KitError(
|
|
523
|
+
"UNSUPPORTED_AUTHENTICATOR",
|
|
524
|
+
`clientDataJSON.type "${data.type}" is not "webauthn.get"`
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
const allowed = Array.isArray(expected.origin) ? expected.origin : [expected.origin];
|
|
528
|
+
if (!allowed.includes(data.origin)) {
|
|
529
|
+
throw new KitError("ORIGIN_MISMATCH", `origin "${data.origin}" not in [${allowed.join(", ")}]`);
|
|
530
|
+
}
|
|
531
|
+
if (data.challenge !== expected.challenge) {
|
|
532
|
+
throw new KitError("CHALLENGE_MISMATCH", "clientDataJSON challenge does not match expected");
|
|
533
|
+
}
|
|
534
|
+
return data;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/webauthn/payload.ts
|
|
538
|
+
function reconstructSignedPayload(input) {
|
|
539
|
+
const clientDataHash = sha256.sha256(input.clientDataJSON);
|
|
540
|
+
return sha256.sha256(concatBytes(input.authenticatorData, clientDataHash));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/webauthn/verify.ts
|
|
544
|
+
function verifyAssertionSignature(input) {
|
|
545
|
+
const payload = reconstructSignedPayload(input);
|
|
546
|
+
const compact = input.signature.length === 64 ? input.signature : derToCompact(input.signature);
|
|
547
|
+
return nist.p256.verify(compact, payload, input.publicKey, { lowS: false, format: "compact" });
|
|
548
|
+
}
|
|
549
|
+
function authEntryChallengeBytes(entry, networkPassphrase) {
|
|
550
|
+
const credentials = entry.credentials();
|
|
551
|
+
if (credentials.switch().name !== "sorobanCredentialsAddress") {
|
|
552
|
+
throw new KitError("CONTRACT_AUTH_FAILED", "auth entry has no address credentials to sign");
|
|
553
|
+
}
|
|
554
|
+
const address = credentials.address();
|
|
555
|
+
const preimage = stellarSdk.xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
|
|
556
|
+
new stellarSdk.xdr.HashIdPreimageSorobanAuthorization({
|
|
557
|
+
networkId: Buffer.from(stellarSdk.hash(Buffer.from(utf8ToBytes(networkPassphrase)))),
|
|
558
|
+
nonce: address.nonce(),
|
|
559
|
+
signatureExpirationLedger: address.signatureExpirationLedger(),
|
|
560
|
+
invocation: entry.rootInvocation()
|
|
561
|
+
})
|
|
562
|
+
);
|
|
563
|
+
return new Uint8Array(stellarSdk.hash(preimage.toXDR()));
|
|
564
|
+
}
|
|
565
|
+
function authEntryChallenge(entry, networkPassphrase) {
|
|
566
|
+
return bytesToBase64Url(authEntryChallengeBytes(entry, networkPassphrase));
|
|
567
|
+
}
|
|
568
|
+
function applyAssertionToEntry(entry, assertion) {
|
|
569
|
+
entry.credentials().address().signature(buildSignatureScVal(assertion));
|
|
570
|
+
return entry;
|
|
571
|
+
}
|
|
572
|
+
function buildSignatureScVal(assertion) {
|
|
573
|
+
return stellarSdk.xdr.ScVal.scvMap([
|
|
574
|
+
bytesField("authenticator_data", assertion.authenticatorData),
|
|
575
|
+
bytesField("client_data_json", assertion.clientDataJSON),
|
|
576
|
+
bytesField("signature", assertion.signature)
|
|
577
|
+
]);
|
|
578
|
+
}
|
|
579
|
+
function bytesField(name, bytes) {
|
|
580
|
+
return new stellarSdk.xdr.ScMapEntry({
|
|
581
|
+
key: stellarSdk.xdr.ScVal.scvSymbol(name),
|
|
582
|
+
val: stellarSdk.xdr.ScVal.scvBytes(Buffer.from(bytes))
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
function buildSignerKeyScVal(credentialId) {
|
|
586
|
+
return stellarSdk.xdr.ScVal.scvVec([
|
|
587
|
+
stellarSdk.xdr.ScVal.scvSymbol("Secp256r1"),
|
|
588
|
+
stellarSdk.xdr.ScVal.scvBytes(Buffer.from(credentialId))
|
|
589
|
+
]);
|
|
590
|
+
}
|
|
591
|
+
function buildSmartWalletSignatureVariant(assertion) {
|
|
592
|
+
return stellarSdk.xdr.ScVal.scvVec([stellarSdk.xdr.ScVal.scvSymbol("Secp256r1"), buildSignatureScVal(assertion)]);
|
|
593
|
+
}
|
|
594
|
+
function decomposeSignerKey(key) {
|
|
595
|
+
const vec = key.vec();
|
|
596
|
+
if (!vec || vec.length < 2) {
|
|
597
|
+
throw new KitError("CONTRACT_AUTH_FAILED", "malformed SignerKey (expected an enum vec)");
|
|
598
|
+
}
|
|
599
|
+
const variant = vec[0].sym().toString();
|
|
600
|
+
const el = vec[1];
|
|
601
|
+
const payload = el.switch().name === "scvBytes" ? new Uint8Array(el.bytes()) : new Uint8Array(el.toXDR());
|
|
602
|
+
return { variant, payload };
|
|
603
|
+
}
|
|
604
|
+
function compareBytes(a, b) {
|
|
605
|
+
const n = Math.min(a.length, b.length);
|
|
606
|
+
for (let i = 0; i < n; i++) {
|
|
607
|
+
if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
|
|
608
|
+
}
|
|
609
|
+
return a.length - b.length;
|
|
610
|
+
}
|
|
611
|
+
function compareSignerKeyScVal(a, b) {
|
|
612
|
+
const ka = decomposeSignerKey(a);
|
|
613
|
+
const kb = decomposeSignerKey(b);
|
|
614
|
+
if (ka.variant !== kb.variant) return ka.variant < kb.variant ? -1 : 1;
|
|
615
|
+
return compareBytes(ka.payload, kb.payload);
|
|
616
|
+
}
|
|
617
|
+
function applyAssertionToSmartWalletEntry(entry, assertion) {
|
|
618
|
+
const credentials = entry.credentials();
|
|
619
|
+
if (credentials.switch().name !== "sorobanCredentialsAddress") {
|
|
620
|
+
throw new KitError("CONTRACT_AUTH_FAILED", "auth entry has no address credentials to sign");
|
|
621
|
+
}
|
|
622
|
+
const key = buildSignerKeyScVal(assertion.credentialId);
|
|
623
|
+
const newEntry = new stellarSdk.xdr.ScMapEntry({ key, val: buildSmartWalletSignatureVariant(assertion) });
|
|
624
|
+
const current = credentials.address().signature();
|
|
625
|
+
let mapEntries;
|
|
626
|
+
switch (current.switch().name) {
|
|
627
|
+
case "scvVoid":
|
|
628
|
+
mapEntries = [newEntry];
|
|
629
|
+
break;
|
|
630
|
+
case "scvVec": {
|
|
631
|
+
const existing = current.vec()?.[0]?.map() ?? [];
|
|
632
|
+
mapEntries = existing.filter((e) => compareSignerKeyScVal(e.key(), key) !== 0);
|
|
633
|
+
mapEntries.push(newEntry);
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
default:
|
|
637
|
+
throw new KitError(
|
|
638
|
+
"CONTRACT_AUTH_FAILED",
|
|
639
|
+
"unexpected existing signature shape for a smart-wallet entry"
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
mapEntries.sort((a, b) => compareSignerKeyScVal(a.key(), b.key()));
|
|
643
|
+
credentials.address().signature(stellarSdk.xdr.ScVal.scvVec([stellarSdk.xdr.ScVal.scvMap(mapEntries)]));
|
|
644
|
+
return entry;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/soroban/sign.ts
|
|
648
|
+
function preflightAssertion(assertion, challenge, verify) {
|
|
649
|
+
let clientData;
|
|
650
|
+
if (verify.origin !== void 0) {
|
|
651
|
+
clientData = verifyClientDataJSON(assertion.clientDataJSON, {
|
|
652
|
+
origin: verify.origin,
|
|
653
|
+
challenge
|
|
654
|
+
});
|
|
655
|
+
} else {
|
|
656
|
+
clientData = parseClientDataJSON(assertion.clientDataJSON);
|
|
657
|
+
if (clientData.type !== "webauthn.get") {
|
|
658
|
+
throw new KitError(
|
|
659
|
+
"UNSUPPORTED_AUTHENTICATOR",
|
|
660
|
+
`clientDataJSON.type "${clientData.type}" is not "webauthn.get"`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
if (clientData.challenge !== challenge) {
|
|
664
|
+
throw new KitError("CHALLENGE_MISMATCH", "assertion challenge does not match the auth entry");
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (clientData.crossOrigin === true && !verify.allowCrossOrigin) {
|
|
668
|
+
throw new KitError(
|
|
669
|
+
"ORIGIN_MISMATCH",
|
|
670
|
+
"assertion was made in a cross-origin context (crossOrigin=true)"
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
const parsed = parseAuthenticatorData(assertion.authenticatorData);
|
|
674
|
+
if (!parsed.flags.up) {
|
|
675
|
+
throw new KitError(
|
|
676
|
+
"UNSUPPORTED_AUTHENTICATOR",
|
|
677
|
+
"assertion is missing the User-Present (UP) flag"
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
if (verify.requireUserVerification && !parsed.flags.uv) {
|
|
681
|
+
throw new KitError(
|
|
682
|
+
"UNSUPPORTED_AUTHENTICATOR",
|
|
683
|
+
"assertion is missing the required User-Verified (UV) flag"
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
if (verify.rpId !== void 0) {
|
|
687
|
+
verifyRpIdHash(assertion.authenticatorData, verify.rpId);
|
|
688
|
+
}
|
|
689
|
+
if (verify.publicKey !== void 0) {
|
|
690
|
+
const ok = verifyAssertionSignature({
|
|
691
|
+
publicKey: verify.publicKey,
|
|
692
|
+
authenticatorData: assertion.authenticatorData,
|
|
693
|
+
clientDataJSON: assertion.clientDataJSON,
|
|
694
|
+
signature: assertion.signature
|
|
695
|
+
});
|
|
696
|
+
if (!ok) {
|
|
697
|
+
throw new KitError(
|
|
698
|
+
"CONTRACT_AUTH_FAILED",
|
|
699
|
+
"assertion signature failed pre-flight verification"
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
async function signEntryInPlace(entry, options) {
|
|
705
|
+
if (options.signatureExpirationLedger !== void 0) {
|
|
706
|
+
const creds = entry.credentials();
|
|
707
|
+
if (creds.switch().name === "sorobanCredentialsAddress") {
|
|
708
|
+
creds.address().signatureExpirationLedger(options.signatureExpirationLedger);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const challenge = authEntryChallenge(entry, options.networkPassphrase);
|
|
712
|
+
const assertion = await options.sign(challenge);
|
|
713
|
+
if (options.verify) preflightAssertion(assertion, challenge, options.verify);
|
|
714
|
+
const signature = assertion.signature.length === 64 ? assertion.signature : derToCompactLowS(assertion.signature);
|
|
715
|
+
const normalized = {
|
|
716
|
+
credentialId: assertion.credentialId,
|
|
717
|
+
authenticatorData: assertion.authenticatorData,
|
|
718
|
+
clientDataJSON: assertion.clientDataJSON,
|
|
719
|
+
signature
|
|
720
|
+
};
|
|
721
|
+
if (options.target === "smart-wallet") {
|
|
722
|
+
applyAssertionToSmartWalletEntry(entry, normalized);
|
|
723
|
+
} else {
|
|
724
|
+
applyAssertionToEntry(entry, normalized);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
async function signAuthEntry(entryXdr, options) {
|
|
728
|
+
const entry = stellarSdk.xdr.SorobanAuthorizationEntry.fromXDR(entryXdr, "base64");
|
|
729
|
+
await signEntryInPlace(entry, options);
|
|
730
|
+
return entry.toXDR("base64");
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// src/internal/cborEncode.ts
|
|
734
|
+
function head(major, arg) {
|
|
735
|
+
const m = major << 5;
|
|
736
|
+
if (arg < 24) return [m | arg];
|
|
737
|
+
if (arg < 256) return [m | 24, arg];
|
|
738
|
+
if (arg < 65536) return [m | 25, arg >> 8 & 255, arg & 255];
|
|
739
|
+
return [m | 26, arg >>> 24 & 255, arg >> 16 & 255, arg >> 8 & 255, arg & 255];
|
|
740
|
+
}
|
|
741
|
+
function write(value, out) {
|
|
742
|
+
if (typeof value === "number" && Number.isInteger(value)) {
|
|
743
|
+
if (value >= 0) out.push(...head(0, value));
|
|
744
|
+
else out.push(...head(1, -1 - value));
|
|
745
|
+
} else if (value instanceof Uint8Array) {
|
|
746
|
+
out.push(...head(2, value.length));
|
|
747
|
+
for (const b of value) out.push(b);
|
|
748
|
+
} else if (typeof value === "string") {
|
|
749
|
+
const bytes = utf8ToBytes(value);
|
|
750
|
+
out.push(...head(3, bytes.length));
|
|
751
|
+
for (const b of bytes) out.push(b);
|
|
752
|
+
} else if (Array.isArray(value)) {
|
|
753
|
+
out.push(...head(4, value.length));
|
|
754
|
+
for (const item of value) write(item, out);
|
|
755
|
+
} else if (value instanceof Map) {
|
|
756
|
+
out.push(...head(5, value.size));
|
|
757
|
+
for (const [k, v] of value) {
|
|
758
|
+
write(k, out);
|
|
759
|
+
write(v, out);
|
|
760
|
+
}
|
|
761
|
+
} else {
|
|
762
|
+
throw new Error("CBOR encode: unsupported value");
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
function encodeCbor(value) {
|
|
766
|
+
const out = [];
|
|
767
|
+
write(value, out);
|
|
768
|
+
return Uint8Array.from(out);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// src/testing/mockAuthenticator.ts
|
|
772
|
+
function deriveScalar(seed) {
|
|
773
|
+
const n = nist.p256.CURVE.n;
|
|
774
|
+
let scalar = BigInt("0x" + bytesToHexLocal(sha256.sha256(utf8ToBytes("priv:" + seed)))) % n;
|
|
775
|
+
if (scalar === 0n) scalar = 1n;
|
|
776
|
+
const hex = scalar.toString(16).padStart(64, "0");
|
|
777
|
+
return Uint8Array.from((hex.match(/../g) ?? []).map((h) => parseInt(h, 16)));
|
|
778
|
+
}
|
|
779
|
+
function bytesToHexLocal(bytes) {
|
|
780
|
+
let hex = "";
|
|
781
|
+
for (const b of bytes) hex += b.toString(16).padStart(2, "0");
|
|
782
|
+
return hex;
|
|
783
|
+
}
|
|
784
|
+
function mockAuthenticator(options) {
|
|
785
|
+
const seed = options.seed ?? "mock-authenticator";
|
|
786
|
+
const origin = options.origin ?? `https://${options.rpId}`;
|
|
787
|
+
const privateKey = deriveScalar(seed);
|
|
788
|
+
const publicKey = nist.p256.getPublicKey(privateKey, false);
|
|
789
|
+
const x = publicKey.slice(1, 33);
|
|
790
|
+
const y = publicKey.slice(33, 65);
|
|
791
|
+
const credIdBytes = sha256.sha256(utf8ToBytes("cred:" + seed)).slice(0, 16);
|
|
792
|
+
const credentialId = bytesToBase64Url(credIdBytes);
|
|
793
|
+
const rpIdHash = sha256.sha256(utf8ToBytes(options.rpId));
|
|
794
|
+
const coseKey = encodeCbor(
|
|
795
|
+
/* @__PURE__ */ new Map([
|
|
796
|
+
[1, 2],
|
|
797
|
+
// kty EC2
|
|
798
|
+
[3, -7],
|
|
799
|
+
// alg ES256
|
|
800
|
+
[-1, 1],
|
|
801
|
+
// crv P-256
|
|
802
|
+
[-2, x],
|
|
803
|
+
[-3, y]
|
|
804
|
+
])
|
|
805
|
+
);
|
|
806
|
+
const attestedAuthData = concatBytes(
|
|
807
|
+
rpIdHash,
|
|
808
|
+
new Uint8Array([69]),
|
|
809
|
+
// UP | UV | AT
|
|
810
|
+
new Uint8Array([0, 0, 0, 0]),
|
|
811
|
+
// signCount
|
|
812
|
+
new Uint8Array(16),
|
|
813
|
+
// aaguid
|
|
814
|
+
new Uint8Array([0, credIdBytes.length]),
|
|
815
|
+
credIdBytes,
|
|
816
|
+
coseKey
|
|
817
|
+
);
|
|
818
|
+
const attestationObject = encodeCbor(
|
|
819
|
+
/* @__PURE__ */ new Map([
|
|
820
|
+
["fmt", "none"],
|
|
821
|
+
["attStmt", /* @__PURE__ */ new Map()],
|
|
822
|
+
["authData", attestedAuthData]
|
|
823
|
+
])
|
|
824
|
+
);
|
|
825
|
+
function signAssertion(challengeB64) {
|
|
826
|
+
const authenticatorData = concatBytes(
|
|
827
|
+
rpIdHash,
|
|
828
|
+
new Uint8Array([5]),
|
|
829
|
+
new Uint8Array([0, 0, 0, 1])
|
|
830
|
+
);
|
|
831
|
+
const clientDataJSON = utf8ToBytes(
|
|
832
|
+
JSON.stringify({ type: "webauthn.get", challenge: challengeB64, origin })
|
|
833
|
+
);
|
|
834
|
+
const payload = sha256.sha256(concatBytes(authenticatorData, sha256.sha256(clientDataJSON)));
|
|
835
|
+
const sig = nist.p256.sign(payload, privateKey);
|
|
836
|
+
const s = options.forceHighS ? nist.p256.CURVE.n - sig.s : sig.s;
|
|
837
|
+
const signatureDer = new nist.p256.Signature(sig.r, s).toDERRawBytes();
|
|
838
|
+
return { authenticatorData, clientDataJSON, signatureDer };
|
|
839
|
+
}
|
|
840
|
+
return {
|
|
841
|
+
publicKey,
|
|
842
|
+
credentialId,
|
|
843
|
+
privateKey,
|
|
844
|
+
create() {
|
|
845
|
+
return Promise.resolve({ id: credentialId, rawId: credIdBytes, attestationObject });
|
|
846
|
+
},
|
|
847
|
+
get(getOptions) {
|
|
848
|
+
const challengeB64 = getOptions.challenge ? bytesToBase64Url(getOptions.challenge) : "mock";
|
|
849
|
+
const a = signAssertion(challengeB64);
|
|
850
|
+
return Promise.resolve({
|
|
851
|
+
id: credentialId,
|
|
852
|
+
rawId: credIdBytes,
|
|
853
|
+
authenticatorData: a.authenticatorData,
|
|
854
|
+
clientDataJSON: a.clientDataJSON,
|
|
855
|
+
signature: a.signatureDer
|
|
856
|
+
});
|
|
857
|
+
},
|
|
858
|
+
sign(challenge) {
|
|
859
|
+
const a = signAssertion(challenge);
|
|
860
|
+
return {
|
|
861
|
+
authenticatorData: a.authenticatorData,
|
|
862
|
+
clientDataJSON: a.clientDataJSON,
|
|
863
|
+
signature: a.signatureDer,
|
|
864
|
+
credentialId: credIdBytes
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
function createInMemoryBackend() {
|
|
870
|
+
const registry = /* @__PURE__ */ new Map();
|
|
871
|
+
let nonce = 0;
|
|
872
|
+
return {
|
|
873
|
+
registry,
|
|
874
|
+
deployer: {
|
|
875
|
+
deploy({ publicKey, credentialId }) {
|
|
876
|
+
const contractId = stellarSdk.StrKey.encodeContract(
|
|
877
|
+
Buffer.from(sha256.sha256(utf8ToBytes("account:" + credentialId)))
|
|
878
|
+
);
|
|
879
|
+
registry.set(credentialId, { contractId, publicKey });
|
|
880
|
+
return Promise.resolve({ contractId, txHash: `mock-deploy-${String(nonce++)}` });
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
indexer: {
|
|
884
|
+
resolveByCredential(credentialId) {
|
|
885
|
+
const account = registry.get(credentialId);
|
|
886
|
+
return Promise.resolve(account ? [{ contractId: account.contractId }] : []);
|
|
887
|
+
}
|
|
888
|
+
},
|
|
889
|
+
submission: {
|
|
890
|
+
send: () => Promise.resolve({ status: "SUCCESS", hash: "mock-tx" })
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// src/testing/kit.ts
|
|
896
|
+
var TESTNET = "Test SDF Network ; September 2015";
|
|
897
|
+
function inMemoryStorage() {
|
|
898
|
+
const map = /* @__PURE__ */ new Map();
|
|
899
|
+
return { get: (rpId) => map.get(rpId) ?? null, set: (rpId, id) => void map.set(rpId, id) };
|
|
900
|
+
}
|
|
901
|
+
function createPasskeyKit(options) {
|
|
902
|
+
const networkPassphrase = options.networkPassphrase ?? TESTNET;
|
|
903
|
+
const rpName = options.rpName ?? options.rpId;
|
|
904
|
+
let webauthn;
|
|
905
|
+
let deployer;
|
|
906
|
+
let indexer;
|
|
907
|
+
let storage;
|
|
908
|
+
let signer;
|
|
909
|
+
if (options.mode === "mock") {
|
|
910
|
+
const auth = mockAuthenticator({
|
|
911
|
+
rpId: options.rpId,
|
|
912
|
+
seed: options.seed,
|
|
913
|
+
forceHighS: options.forceHighS
|
|
914
|
+
});
|
|
915
|
+
const backend = createInMemoryBackend();
|
|
916
|
+
webauthn = auth;
|
|
917
|
+
deployer = backend.deployer;
|
|
918
|
+
indexer = backend.indexer;
|
|
919
|
+
signer = auth.sign;
|
|
920
|
+
storage = inMemoryStorage();
|
|
921
|
+
} else {
|
|
922
|
+
if (!options.webauthn || !options.deployer || !options.indexer || !options.signer) {
|
|
923
|
+
throw new Error("live mode requires webauthn, deployer, indexer and signer");
|
|
924
|
+
}
|
|
925
|
+
webauthn = options.webauthn;
|
|
926
|
+
deployer = options.deployer;
|
|
927
|
+
indexer = options.indexer;
|
|
928
|
+
signer = options.signer;
|
|
929
|
+
storage = options.storage ?? inMemoryStorage();
|
|
930
|
+
}
|
|
931
|
+
return {
|
|
932
|
+
mode: options.mode,
|
|
933
|
+
rpId: options.rpId,
|
|
934
|
+
createPasskey: (input) => createPasskey({
|
|
935
|
+
rpId: options.rpId,
|
|
936
|
+
rpName,
|
|
937
|
+
userName: input?.userName ?? "user",
|
|
938
|
+
deployer,
|
|
939
|
+
webauthn,
|
|
940
|
+
storage
|
|
941
|
+
}),
|
|
942
|
+
connect: () => connect({
|
|
943
|
+
rpId: options.rpId,
|
|
944
|
+
indexer,
|
|
945
|
+
storage}),
|
|
946
|
+
recover: () => recover({ rpId: options.rpId, indexer, webauthn }),
|
|
947
|
+
signAuthEntry: (entryXdr) => signAuthEntry(entryXdr, { networkPassphrase, sign: signer })
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
exports.createInMemoryBackend = createInMemoryBackend;
|
|
952
|
+
exports.createPasskeyKit = createPasskeyKit;
|
|
953
|
+
exports.mockAuthenticator = mockAuthenticator;
|
|
954
|
+
//# sourceMappingURL=testing.cjs.map
|
|
955
|
+
//# sourceMappingURL=testing.cjs.map
|