@coordination-network/identity 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.
Files changed (39) hide show
  1. package/DERIVATION.md +313 -0
  2. package/LICENSE +21 -0
  3. package/README.md +258 -0
  4. package/dist/index.cjs +957 -0
  5. package/dist/index.js +893 -0
  6. package/package.json +57 -0
  7. package/src/account.ts +61 -0
  8. package/src/auth.ts +20 -0
  9. package/src/binding.ts +298 -0
  10. package/src/bytes.ts +142 -0
  11. package/src/confirmation-key.ts +50 -0
  12. package/src/constants.ts +40 -0
  13. package/src/derive.ts +41 -0
  14. package/src/device-secret.ts +24 -0
  15. package/src/eddsa-peer.ts +32 -0
  16. package/src/eddsa.ts +37 -0
  17. package/src/errors.ts +76 -0
  18. package/src/hash-to-scalar.ts +35 -0
  19. package/src/index.ts +53 -0
  20. package/src/keypair.ts +38 -0
  21. package/src/network-id.ts +26 -0
  22. package/src/stealthAddresses/assertCompressedPublicKey.ts +25 -0
  23. package/src/stealthAddresses/buildAnnouncement.ts +37 -0
  24. package/src/stealthAddresses/checkStealthAddress.ts +37 -0
  25. package/src/stealthAddresses/computeSharedSecret.ts +16 -0
  26. package/src/stealthAddresses/decodeMetaStealthAddress.ts +27 -0
  27. package/src/stealthAddresses/deriveStealthKeys.ts +30 -0
  28. package/src/stealthAddresses/encodeMetaStealthAddress.ts +19 -0
  29. package/src/stealthAddresses/encodeMetaStealthAddressUri.ts +8 -0
  30. package/src/stealthAddresses/generateEphemeralKey.ts +12 -0
  31. package/src/stealthAddresses/generateStealthAddress.ts +27 -0
  32. package/src/stealthAddresses/generateStealthPrivateKey.ts +29 -0
  33. package/src/stealthAddresses/getStealthPublicKey.ts +21 -0
  34. package/src/stealthAddresses/getViewTag.ts +7 -0
  35. package/src/stealthAddresses/metaStealthAddressUri.ts +7 -0
  36. package/src/stealthAddresses/publicKeyToAddress.ts +17 -0
  37. package/src/stealthAddresses/validateMetaStealthAddress.ts +19 -0
  38. package/src/types.ts +51 -0
  39. package/types/index.d.ts +499 -0
package/dist/index.js ADDED
@@ -0,0 +1,893 @@
1
+ import { bytesToHex as bytesToHex$1, hexToBytes as hexToBytes$1, utf8ToBytes, concatBytes } from '@noble/hashes/utils';
2
+ import { keccak_256 } from '@noble/hashes/sha3';
3
+ import { secp256k1 } from '@noble/curves/secp256k1';
4
+ import { bytesToNumberBE, numberToBytesBE } from '@noble/curves/utils';
5
+ import { deriveSecretScalar, derivePublicKey } from '@zk-kit/eddsa-poseidon';
6
+ import { poseidon2 } from 'poseidon-lite/poseidon2';
7
+ import { hashToCurve, p256 } from '@noble/curves/p256';
8
+ import { sha256 } from '@noble/hashes/sha2';
9
+
10
+ /**
11
+ * Normative constants for the `v1` key derivation scheme. Changing any of
12
+ * these changes every derived key and `networkId`, so they MUST NOT change
13
+ * without a version bump (new `KEY_DERIVATION_VERSION` and domain strings).
14
+ */
15
+ const KEY_DERIVATION_VERSION = "v1";
16
+ /** Fixed domain-separation message signed by {@link signDerivationMessage}. */
17
+ const FIXED_DOMAIN_MSG = "coordination-network/identity/v1";
18
+ /** Domain-separation tag for deriving the account keypair from a device secret. */
19
+ const DOMAIN_ACCOUNT = "coordination-network/identity/v1/account";
20
+ /** Domain-separation tag for deriving the auth keypair. */
21
+ const DOMAIN_AUTH = "coordination-network/identity/v1/auth";
22
+ /** Domain-separation tag for deriving the Baby Jubjub EdDSA seed (the "fifth key") from the hardware ECDH shared secret. */
23
+ const DOMAIN_EDDSA = "coordination-network/identity/v1/eddsa";
24
+ /** Domain-separation message hashed to the fixed NUMS P-256 peer point `Q` for the fifth-key ECDH (`eddsa-peer.ts`). */
25
+ const DOMAIN_EDDSA_PEER = "coordination-network/identity/v1/eddsa/peer";
26
+ /**
27
+ * Byte length of `nonce` and `networkId` in the binding messages — fixed at
28
+ * 32 so the 64- vs 96-byte messages are injective.
29
+ */
30
+ const BINDING_NONCE_LENGTH = 32;
31
+ /** SEC1-compressed secp256k1 public key length. */
32
+ const COMPRESSED_PUBLIC_KEY_LENGTH = 33;
33
+ /** ERC-6538 scheme 1 meta-stealth address: compressed spending ‖ viewing public keys. */
34
+ const META_STEALTH_ADDRESS_LENGTH = 66;
35
+ /** ERC-5564 offchain display prefix — never stored onchain. */
36
+ const META_STEALTH_URI_PREFIX = "st:eth:";
37
+ /** ERC-5564 scheme id for secp256k1 with view tags. */
38
+ const ERC_5564_SCHEME_ID = 1;
39
+
40
+ /**
41
+ * Base error class for all errors thrown by this package, discriminated by
42
+ * `code` so callers can branch without `instanceof` across bundle boundaries.
43
+ */
44
+ class KeygenError extends Error {
45
+ constructor(code, message) {
46
+ super(message);
47
+ this.name = "KeygenError";
48
+ this.code = code;
49
+ }
50
+ }
51
+ /** Thrown when a device secret does not satisfy the exactly-32-bytes contract. */
52
+ class InvalidDeviceSecretError extends KeygenError {
53
+ constructor(message = "Invalid device secret: expected a 32-byte Uint8Array or a 0x-prefixed 64-character hex string") {
54
+ super("INVALID_DEVICE_SECRET", message);
55
+ this.name = "InvalidDeviceSecretError";
56
+ }
57
+ }
58
+ /** Thrown when key derivation fails for a reason unrelated to input shape. */
59
+ class DerivationFailureError extends KeygenError {
60
+ constructor(message = "Key derivation failed") {
61
+ super("DERIVATION_FAILURE", message);
62
+ this.name = "DerivationFailureError";
63
+ }
64
+ }
65
+ /**
66
+ * Thrown for malformed credential-binding inputs; a well-formed signature
67
+ * that merely fails to verify returns `false` instead.
68
+ */
69
+ class InvalidBindingInputError extends KeygenError {
70
+ constructor(message = "Invalid credential-binding input") {
71
+ super("INVALID_BINDING_INPUT", message);
72
+ this.name = "InvalidBindingInputError";
73
+ }
74
+ }
75
+ /** Thrown when a meta-stealth address fails to decode (bad prefix, length, hex, or curve point). */
76
+ class InvalidMetaStealthAddressError extends KeygenError {
77
+ constructor(message = "Invalid meta-stealth address") {
78
+ super("INVALID_META_STEALTH_ADDRESS", message);
79
+ this.name = "InvalidMetaStealthAddressError";
80
+ }
81
+ }
82
+ /** Thrown when a hardware ECDH shared secret does not satisfy the exactly-32-bytes contract. */
83
+ class InvalidEcdhSecretError extends KeygenError {
84
+ constructor(message = "Invalid ECDH shared secret: expected a 32-byte Uint8Array or a 0x-prefixed 64-character hex string") {
85
+ super("INVALID_ECDH_SECRET", message);
86
+ this.name = "InvalidEcdhSecretError";
87
+ }
88
+ }
89
+ /** Thrown when a public key is not the correct length */
90
+ class InvalidPublicKeyLengthError extends KeygenError {
91
+ constructor(message = "Invalid public key length. Expected a 65-byte uncompressed SEC1 public key") {
92
+ super("INVALID_PUBLIC_KEY_LENGTH", message);
93
+ this.name = "InvalidPublicKeyLengthError";
94
+ }
95
+ }
96
+
97
+ const HEX_RE = /^[0-9a-fA-F]*$/;
98
+ function hexToBytes(hex) {
99
+ if (typeof hex !== "string" || !hex.startsWith("0x")) {
100
+ throw new TypeError("hexToBytes: expected a string with a 0x prefix");
101
+ }
102
+ const body = hex.slice(2);
103
+ if (body.length % 2 !== 0) {
104
+ throw new TypeError("hexToBytes: expected an even number of hex characters");
105
+ }
106
+ if (!HEX_RE.test(body)) {
107
+ throw new TypeError("hexToBytes: expected only hex characters after the 0x prefix");
108
+ }
109
+ return hexToBytes$1(body);
110
+ }
111
+ function bytesToHex(bytes) {
112
+ return `0x${bytesToHex$1(bytes)}`;
113
+ }
114
+ const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
115
+ /** Unpadded base64url (RFC 4648 §5) */
116
+ function bytesToBase64url(bytes) {
117
+ let out = "";
118
+ let i = 0;
119
+ for (; i + 2 < bytes.length; i += 3) {
120
+ const chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
121
+ out +=
122
+ BASE64URL_ALPHABET[(chunk >> 18) & 63] +
123
+ BASE64URL_ALPHABET[(chunk >> 12) & 63] +
124
+ BASE64URL_ALPHABET[(chunk >> 6) & 63] +
125
+ BASE64URL_ALPHABET[chunk & 63];
126
+ }
127
+ const remaining = bytes.length - i;
128
+ if (remaining === 1) {
129
+ const chunk = bytes[i] << 16;
130
+ out += BASE64URL_ALPHABET[(chunk >> 18) & 63] + BASE64URL_ALPHABET[(chunk >> 12) & 63];
131
+ }
132
+ else if (remaining === 2) {
133
+ const chunk = (bytes[i] << 16) | (bytes[i + 1] << 8);
134
+ out +=
135
+ BASE64URL_ALPHABET[(chunk >> 18) & 63] +
136
+ BASE64URL_ALPHABET[(chunk >> 12) & 63] +
137
+ BASE64URL_ALPHABET[(chunk >> 6) & 63];
138
+ }
139
+ return out;
140
+ }
141
+ /** Decodes unpadded base64url (RFC 4648 §5). */
142
+ function base64urlToBytes(encoded) {
143
+ if (!/^[A-Za-z0-9_-]*$/.test(encoded) || encoded.length % 4 === 1) {
144
+ throw new TypeError("base64urlToBytes: expected unpadded base64url");
145
+ }
146
+ const output = new Uint8Array(Math.floor((encoded.length * 6) / 8));
147
+ let buffer = 0;
148
+ let bits = 0;
149
+ let offset = 0;
150
+ for (const character of encoded) {
151
+ buffer = (buffer << 6) | BASE64URL_ALPHABET.indexOf(character);
152
+ bits += 6;
153
+ if (bits >= 8) {
154
+ bits -= 8;
155
+ output[offset++] = (buffer >> bits) & 0xff;
156
+ buffer &= (1 << bits) - 1;
157
+ }
158
+ }
159
+ if (buffer !== 0)
160
+ throw new TypeError("base64urlToBytes: non-canonical trailing bits");
161
+ return output;
162
+ }
163
+ function toBytes(value) {
164
+ return value instanceof Uint8Array ? value : hexToBytes(value);
165
+ }
166
+ /** Prepends the SEC1 `0x04` prefix to a 64-byte raw `X || Y` pair (secp256k1 or P-256). */
167
+ function prependUncompressedPrefix(coordinates) {
168
+ const out = new Uint8Array(65);
169
+ out[0] = 0x04;
170
+ out.set(coordinates, 1);
171
+ return out;
172
+ }
173
+ /**
174
+ * Normalizes a device secret to a defensive copy of exactly 32 bytes.
175
+ * Accepts a 32-byte `Uint8Array` or a `0x`-prefixed 64-hex-char string;
176
+ * anything else throws {@link InvalidDeviceSecretError}.
177
+ */
178
+ function normalizeDeviceSecret(secret) {
179
+ if (secret instanceof Uint8Array) {
180
+ if (secret.length !== 32) {
181
+ throw new InvalidDeviceSecretError();
182
+ }
183
+ return secret.slice();
184
+ }
185
+ if (typeof secret === "string") {
186
+ if (!secret.startsWith("0x") || secret.length !== 66 || !HEX_RE.test(secret.slice(2))) {
187
+ throw new InvalidDeviceSecretError();
188
+ }
189
+ return hexToBytes(secret);
190
+ }
191
+ throw new InvalidDeviceSecretError();
192
+ }
193
+ /**
194
+ * Normalizes a hardware ECDH shared secret to a defensive copy of exactly 32
195
+ * bytes. Same exactly-32-bytes contract as {@link normalizeDeviceSecret};
196
+ * rethrown as {@link InvalidEcdhSecretError} so callers don't conflate the
197
+ * two secrets by error code.
198
+ */
199
+ function normalizeEcdhSharedSecret(secret) {
200
+ try {
201
+ return normalizeDeviceSecret(secret);
202
+ }
203
+ catch (error) {
204
+ if (error instanceof InvalidDeviceSecretError) {
205
+ throw new InvalidEcdhSecretError();
206
+ }
207
+ throw error;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Generates a fresh 32-byte device secret from the platform CSPRNG
213
+ * (`globalThis.crypto.getRandomValues`). Throws if no secure RNG is available
214
+ * (e.g. React Native without the `react-native-get-random-values` polyfill).
215
+ */
216
+ function generateDeviceSecret() {
217
+ const cryptoObj = globalThis.crypto;
218
+ if (typeof cryptoObj?.getRandomValues !== "function") {
219
+ throw new KeygenError("DERIVATION_FAILURE", "generateDeviceSecret: no secure random source (globalThis.crypto.getRandomValues) available");
220
+ }
221
+ const bytes = new Uint8Array(32);
222
+ cryptoObj.getRandomValues(bytes);
223
+ return bytesToHex(bytes);
224
+ }
225
+
226
+ const N = secp256k1.CURVE.n;
227
+ /**
228
+ * Tests whether `scalar` is a valid secp256k1 private key: exactly 32 bytes
229
+ * encoding an integer in `[1, n)`.
230
+ */
231
+ function isValidScalar(scalar) {
232
+ if (scalar.length !== 32) {
233
+ return false;
234
+ }
235
+ const value = BigInt(bytesToHex(scalar));
236
+ return value >= 1n && value < N;
237
+ }
238
+ /**
239
+ * Expands a 32-byte private scalar into a full {@link Secp256k1Keypair}, with
240
+ * all fields as lowercase `0x` hex.
241
+ *
242
+ * @throws {DerivationFailureError} if `scalar` is not in `[1, n)`
243
+ */
244
+ function scalarToKeypair(scalar) {
245
+ if (!isValidScalar(scalar)) {
246
+ throw new DerivationFailureError("Derived scalar is not a valid secp256k1 private key in [1, n)");
247
+ }
248
+ return {
249
+ privateKey: bytesToHex(scalar),
250
+ publicKey: bytesToHex(secp256k1.getPublicKey(scalar, true)),
251
+ uncompressedPublicKey: bytesToHex(secp256k1.getPublicKey(scalar, false))
252
+ };
253
+ }
254
+
255
+ /** {@link hashToScalar} with an injectable hash, letting tests drive the reject/retry branch. */
256
+ function hashToScalarWith(hashFn, tag, input) {
257
+ const tagBytes = utf8ToBytes(tag);
258
+ for (let counter = 0; counter <= 255; counter += 1) {
259
+ const candidate = hashFn(concatBytes(tagBytes, input, Uint8Array.of(counter)));
260
+ if (isValidScalar(candidate)) {
261
+ return candidate;
262
+ }
263
+ }
264
+ throw new DerivationFailureError("hashToScalar: exhausted all 256 counter values without a valid scalar");
265
+ }
266
+ /**
267
+ * Deterministically derives a secp256k1 private scalar from a domain tag and
268
+ * input bytes.
269
+ *
270
+ * NORMATIVE: for `counter = 0..255`, return the first
271
+ * `keccak256(utf8(tag) || input || [counter])` whose big-endian value lies in
272
+ * `[1, n)`. Out-of-range candidates have probability ~`2^-128`, so exhaustion
273
+ * is unreachable.
274
+ */
275
+ function hashToScalar(tag, input) {
276
+ return hashToScalarWith(keccak_256, tag, input);
277
+ }
278
+
279
+ /**
280
+ * Derives the root secp256k1 account keypair from a 32-byte device secret.
281
+ *
282
+ * NORMATIVE: `accountPrivKey = hashToScalar(DOMAIN_ACCOUNT, deviceSecret)`.
283
+ */
284
+ function deriveAccountKeypair(deviceSecret) {
285
+ const secret = normalizeDeviceSecret(deviceSecret);
286
+ const scalar = hashToScalar(DOMAIN_ACCOUNT, secret);
287
+ return scalarToKeypair(scalar);
288
+ }
289
+ /**
290
+ * Normalizes a 64-byte compact ECDSA signature (`r || s`) to raw bytes.
291
+ *
292
+ * @internal
293
+ */
294
+ function normalizeSignature64(sig64) {
295
+ const bytes = toBytes(sig64);
296
+ if (bytes.length !== 64) {
297
+ throw new TypeError("normalizeSignature64: expected a 64-byte compact signature (r || s)");
298
+ }
299
+ return bytes;
300
+ }
301
+ /**
302
+ * EIP-191 personal-sign digest:
303
+ * `keccak256(utf8("\x19Ethereum Signed Message:\n" + decimalByteLength) || message)`.
304
+ *
305
+ * @internal
306
+ */
307
+ function eip191Digest(message) {
308
+ const prefix = utf8ToBytes(`\x19Ethereum Signed Message:\n${String(message.length)}`);
309
+ return keccak_256(concatBytes(prefix, message));
310
+ }
311
+ /**
312
+ * Signs the fixed derivation message with the account private key.
313
+ *
314
+ * NORMATIVE: the EIP-191 digest of {@link FIXED_DOMAIN_MSG}, signed with
315
+ * RFC-6979 deterministic ECDSA (`lowS: true`, no `extraEntropy`), returned as
316
+ * the 64-byte compact `r || s` encoding.
317
+ */
318
+ function signDerivationMessage(accountPrivateKey) {
319
+ const privateKey = toBytes(accountPrivateKey);
320
+ const digest = eip191Digest(utf8ToBytes(FIXED_DOMAIN_MSG));
321
+ const signature = secp256k1.sign(digest, privateKey, { lowS: true });
322
+ return bytesToHex(signature.toCompactRawBytes());
323
+ }
324
+
325
+ /**
326
+ * Derives the spending and viewing secp256k1 keypairs from the 64-byte
327
+ * compact derivation-message signature.
328
+ *
329
+ * NORMATIVE (FluidKey `generateKeysFromSignature` semantics): with
330
+ * `r = sig[0..32]` and `s = sig[32..64]`, `spendingPrivKey = keccak256(r)`
331
+ * and `viewingPrivKey = keccak256(s)`. No reject/retry — the hash output is
332
+ * used directly and validated to be in `[1, n)` by {@link scalarToKeypair}.
333
+ *
334
+ * `sig64` is unauthenticated — prefer {@link deriveKeys} (see `DERIVATION.md`).
335
+ */
336
+ function deriveStealthKeys(sig64) {
337
+ const signature = normalizeSignature64(sig64);
338
+ const r = signature.slice(0, 32);
339
+ const s = signature.slice(32, 64);
340
+ return {
341
+ spending: scalarToKeypair(keccak_256(r)),
342
+ viewing: scalarToKeypair(keccak_256(s))
343
+ };
344
+ }
345
+
346
+ /**
347
+ * The ERC-5564 hashed shared secret: keccak256 of the 33-byte compressed
348
+ * ECDH point.
349
+ *
350
+ * The sender can pass (ephemeralPrivateKey, viewingPublicKey).
351
+ * The recipient can pass (viewingPrivateKey, ephemeralPublicKey).
352
+ */
353
+ function computeSharedSecret({ privateKey, publicKey }) {
354
+ const sharedSecret = secp256k1.getSharedSecret(privateKey.slice(2), publicKey.slice(2), true);
355
+ return keccak_256(sharedSecret);
356
+ }
357
+
358
+ /**
359
+ * First byte of the hashed shared secret, published in announcements so
360
+ * scanners can skip 255/256 non-matching shared secrets.
361
+ */
362
+ function getViewTag(hashedSharedSecret) {
363
+ return hashedSharedSecret[0];
364
+ }
365
+
366
+ /**
367
+ * Generates stealth public key. ERC-5564 scheme 1: P_stealth = P_spend + (s_h mod n)·G,
368
+ * returned as a 65-byte uncompressed public key.
369
+ */
370
+ function getStealthPublicKey({ spendingPublicKey, hashedSharedSecret }) {
371
+ const sharedSecretScalar = bytesToNumberBE(hashedSharedSecret) % secp256k1.Point.CURVE().n;
372
+ return secp256k1.Point.fromHex(spendingPublicKey.slice(2))
373
+ .add(secp256k1.Point.BASE.multiply(sharedSecretScalar))
374
+ .toBytes(false);
375
+ }
376
+
377
+ /**
378
+ * Derive Ethereum address from public key: keccak256 of the 64-byte X || Y
379
+ * coordinates (65 bytes without 0x04 prefix stripped), then last 20 bytes.
380
+ */
381
+ function publicKeyToAddress(publicKey) {
382
+ if (publicKey.length !== 65) {
383
+ throw new InvalidPublicKeyLengthError();
384
+ }
385
+ const publicKeyHash = keccak_256(publicKey.slice(1));
386
+ return `0x${bytesToHex(publicKeyHash).slice(-40)}`;
387
+ }
388
+
389
+ /**
390
+ * A fresh CSPRNG keypair for one load. MUST be single-use — reusing an
391
+ * ephemeral key derives the same stealth address twice.
392
+ */
393
+ function generateEphemeralKey() {
394
+ return scalarToKeypair(hexToBytes(generateDeviceSecret()));
395
+ }
396
+
397
+ /**
398
+ * Derives a fresh stealth address from the
399
+ * recipient's spending and viewing public keys.
400
+ */
401
+ function generateStealthAddress({ viewingPublicKey, spendingPublicKey, ephemeralPrivateKey }) {
402
+ const hashedSharedSecret = computeSharedSecret({
403
+ privateKey: ephemeralPrivateKey,
404
+ publicKey: viewingPublicKey
405
+ });
406
+ const stealthPublicKey = getStealthPublicKey({ spendingPublicKey, hashedSharedSecret });
407
+ return publicKeyToAddress(stealthPublicKey);
408
+ }
409
+
410
+ /**
411
+ * Recovers the private key controlling a stealth address,
412
+ * (p_spend + s_h) mod n.
413
+ */
414
+ function generateStealthPrivateKey({ ephemeralPublicKey, viewingPrivateKey, spendingPrivateKey }) {
415
+ const secretHash = computeSharedSecret({
416
+ privateKey: viewingPrivateKey,
417
+ publicKey: ephemeralPublicKey
418
+ });
419
+ const privateKey = (bytesToNumberBE(hexToBytes(spendingPrivateKey)) + bytesToNumberBE(secretHash)) % secp256k1.Point.CURVE().n;
420
+ return bytesToHex(numberToBytesBE(privateKey, 32));
421
+ }
422
+
423
+ /**
424
+ * Checks whether an announcement is addressed to a recipient: filters by
425
+ * view-tag first, then full address recompute.
426
+ */
427
+ function checkStealthAddress({ stealthAddress, ephemeralPublicKey, spendingPublicKey, viewingPrivateKey, viewTag }) {
428
+ const hashedSharedSecret = computeSharedSecret({
429
+ privateKey: viewingPrivateKey,
430
+ publicKey: ephemeralPublicKey
431
+ });
432
+ if (viewTag !== getViewTag(hashedSharedSecret)) {
433
+ return false;
434
+ }
435
+ const stealthPublicKey = getStealthPublicKey({ spendingPublicKey, hashedSharedSecret });
436
+ const expectedStealthAddress = publicKeyToAddress(stealthPublicKey);
437
+ return expectedStealthAddress.toLowerCase() === stealthAddress.toLowerCase();
438
+ }
439
+
440
+ /** Checked per key, not on the concatenation: 32 + 34 bytes hits the right total but splits wrong. */
441
+ function assertCompressedPublicKey(publicKey, label) {
442
+ if (!/^0x[0-9a-fA-F]+$/.test(publicKey) || (publicKey.length - 2) / 2 !== COMPRESSED_PUBLIC_KEY_LENGTH) {
443
+ throw new InvalidMetaStealthAddressError(
444
+ // Never echo the value: a 32-byte private key passed here by mistake lands on this branch.
445
+ `${label} public key must be ${COMPRESSED_PUBLIC_KEY_LENGTH} bytes of SEC1-compressed hex`);
446
+ }
447
+ const prefix = publicKey.slice(2, 4);
448
+ if (prefix !== "02" && prefix !== "03") {
449
+ throw new InvalidMetaStealthAddressError(`${label} public key must start with 0x02 or 0x03, got 0x${prefix}`);
450
+ }
451
+ try {
452
+ secp256k1.Point.fromHex(publicKey.slice(2));
453
+ }
454
+ catch {
455
+ throw new InvalidMetaStealthAddressError(`${label} public key is not a point on secp256k1`);
456
+ }
457
+ }
458
+
459
+ /** Validates 66-byte meta stealth address: shape and curve checks */
460
+ function validateMetaStealthAddress(metaStealthAddress) {
461
+ if (!metaStealthAddress.startsWith("0x")) {
462
+ return false;
463
+ }
464
+ const cleanedMetaStealthAddress = metaStealthAddress.slice(2);
465
+ try {
466
+ assertCompressedPublicKey(`0x${cleanedMetaStealthAddress.slice(0, 66)}`, "spending");
467
+ assertCompressedPublicKey(`0x${cleanedMetaStealthAddress.slice(66)}`, "viewing");
468
+ }
469
+ catch {
470
+ return false;
471
+ }
472
+ return true;
473
+ }
474
+
475
+ /**
476
+ * The 66-byte compressed spending || viewing concatenation the onchain registry stores. Throws
477
+ * InvalidMetaStealthAddressError, naming the offending key, so it can be used before registering.
478
+ */
479
+ function encodeMetaStealthAddress({ spendingPublicKey, viewingPublicKey }) {
480
+ assertCompressedPublicKey(spendingPublicKey, "spending");
481
+ assertCompressedPublicKey(viewingPublicKey, "viewing");
482
+ return `${spendingPublicKey}${viewingPublicKey.slice(2)}`;
483
+ }
484
+
485
+ /** Takes the encoded address, so it also formats one read back from a registry. */
486
+ function metaStealthAddressUri(metaStealthAddress) {
487
+ return `${META_STEALTH_URI_PREFIX}${metaStealthAddress}`;
488
+ }
489
+
490
+ /** The st:eth:0x… display form (ERC-5564 URI). Not stored onchain. */
491
+ function encodeMetaStealthAddressUri(publicKeys) {
492
+ return metaStealthAddressUri(encodeMetaStealthAddress(publicKeys));
493
+ }
494
+
495
+ /**
496
+ * Splits a 66-byte meta-stealth address (raw hex or `st:eth:` prefixed) into its
497
+ * spending and viewing public keys.
498
+ */
499
+ function decodeMetaStealthAddress(metaStealthAddress) {
500
+ const hexMetaStealthAddress = metaStealthAddress.startsWith(META_STEALTH_URI_PREFIX)
501
+ ? metaStealthAddress.slice(META_STEALTH_URI_PREFIX.length)
502
+ : metaStealthAddress;
503
+ if (!validateMetaStealthAddress(hexMetaStealthAddress)) {
504
+ throw new InvalidMetaStealthAddressError();
505
+ }
506
+ const cleanedMetaStealthAddress = hexMetaStealthAddress.slice(2);
507
+ return {
508
+ spendingPublicKey: `0x${cleanedMetaStealthAddress.slice(0, 66)}`,
509
+ viewingPublicKey: `0x${cleanedMetaStealthAddress.slice(66)}`
510
+ };
511
+ }
512
+
513
+ /**
514
+ * Builds an ERC-5564 announcement payload for a fresh stealth address,
515
+ * generates ephemeral key internally. The view tag is the
516
+ * first and only metadata byte.
517
+ */
518
+ function buildAnnouncement({ metaStealthAddress }) {
519
+ const { spendingPublicKey, viewingPublicKey } = decodeMetaStealthAddress(metaStealthAddress);
520
+ const ephemeralKey = generateEphemeralKey();
521
+ const hashedSharedSecret = computeSharedSecret({
522
+ privateKey: ephemeralKey.privateKey,
523
+ publicKey: viewingPublicKey
524
+ });
525
+ const stealthPublicKey = getStealthPublicKey({ spendingPublicKey, hashedSharedSecret });
526
+ return {
527
+ schemeId: ERC_5564_SCHEME_ID,
528
+ stealthAddress: publicKeyToAddress(stealthPublicKey),
529
+ ephemeralPublicKey: ephemeralKey.publicKey,
530
+ metadata: bytesToHex(Uint8Array.of(getViewTag(hashedSharedSecret)))
531
+ };
532
+ }
533
+
534
+ /**
535
+ * Derives the secp256k1 auth keypair from the 64-byte compact
536
+ * derivation-message signature.
537
+ *
538
+ * NORMATIVE: `authPrivKey = hashToScalar(DOMAIN_AUTH, sig64)`.
539
+ *
540
+ * `sig64` is unauthenticated — prefer {@link deriveKeys} (see `DERIVATION.md`).
541
+ */
542
+ function deriveAuthKeypair(sig64) {
543
+ const signature = normalizeSignature64(sig64);
544
+ const scalar = hashToScalar(DOMAIN_AUTH, signature);
545
+ return scalarToKeypair(scalar);
546
+ }
547
+
548
+ /** Baby Jubjub EdDSA "fifth key" derivation — see `DERIVATION.md` §12. */
549
+ /**
550
+ * Derives the Baby Jubjub EdDSA keypair and Semaphore-v4 identity commitment
551
+ * (the "fifth key"), rooted in a hardware ECDH shared secret.
552
+ *
553
+ * NORMATIVE (`DERIVATION.md` §12): `ecdhSharedSecret = X(hwPriv · Q)` for the
554
+ * fixed NUMS peer point `Q` (`eddsa-peer.ts`), computed natively by the
555
+ * caller — the library never talks to the Keystore, it only takes the raw
556
+ * 32-byte result. `seed = keccak256(utf8(DOMAIN_EDDSA) || ecdhSharedSecret)`,
557
+ * fed to `@zk-kit/eddsa-poseidon` as raw bytes (MUST NOT be the hex string,
558
+ * see §12.4); `commitment = poseidon2([x, y])`.
559
+ *
560
+ * @throws {InvalidEcdhSecretError} if `ecdhSharedSecret` is not exactly 32 bytes
561
+ */
562
+ function deriveEddsaKeypair(ecdhSharedSecret) {
563
+ const secret = normalizeEcdhSharedSecret(ecdhSharedSecret);
564
+ const seed = keccak_256(concatBytes(utf8ToBytes(DOMAIN_EDDSA), secret));
565
+ const secretScalar = deriveSecretScalar(seed);
566
+ const publicKey = derivePublicKey(seed);
567
+ const commitment = poseidon2(publicKey);
568
+ return {
569
+ seed: bytesToHex(seed),
570
+ secretScalar,
571
+ publicKey,
572
+ commitment
573
+ };
574
+ }
575
+
576
+ /** Fixed NUMS P-256 peer point `Q` for the fifth-key ECDH — see `DERIVATION.md` §12. */
577
+ /**
578
+ * NORMATIVE: `Q = hashToCurve(utf8(DOMAIN_EDDSA_PEER))` (RFC 9380
579
+ * `P256_XMD:SHA-256_SSWU_RO_`, suite-default DST). Nobody knows `dlog(Q)`,
580
+ * so `hwPriv · Q` is computable only by the device that holds `hwPriv`.
581
+ * 65-byte SEC1 uncompressed encoding, `0x04`-prefixed.
582
+ */
583
+ const EDDSA_PEER_POINT_HEX = "0x045db1cabf31539848bf802ad49a94ad8f8e3d52e75bb53aa4552d7cf836eba054213badca0cf4ba6daf8c146d3415d9ae1905440781dc5f08e6e946a471af99e9";
584
+ /** Fresh copy of {@link EDDSA_PEER_POINT_HEX} as raw bytes. */
585
+ function eddsaPeerPointBytes() {
586
+ return hexToBytes(EDDSA_PEER_POINT_HEX);
587
+ }
588
+ /**
589
+ * Reproduces `Q` from its RFC 9380 provenance, independent of the pinned
590
+ * constant — the evidence that {@link EDDSA_PEER_POINT_HEX} is a NUMS point.
591
+ *
592
+ * `hashToCurve` returns an `H2CPoint`, which lacks `toRawBytes`; round-trip
593
+ * through `ProjectivePoint.fromAffine` to get the SEC1 encoding.
594
+ */
595
+ function computeEddsaPeerPoint() {
596
+ const point = hashToCurve(utf8ToBytes(DOMAIN_EDDSA_PEER));
597
+ return p256.ProjectivePoint.fromAffine(point.toAffine()).toRawBytes(false);
598
+ }
599
+
600
+ /**
601
+ * Computes the bytes32 `networkId` from an uncompressed secp256k1 public key
602
+ * (65-byte SEC1 `0x04`-prefixed or 64-byte raw `X || Y`).
603
+ *
604
+ * NORMATIVE: `networkId = keccak256(X || Y)` — the SEC1 prefix is stripped.
605
+ * The last 20 bytes are the account's Ethereum address.
606
+ */
607
+ function computeNetworkId(uncompressedPublicKey) {
608
+ const bytes = toBytes(uncompressedPublicKey);
609
+ let coordinates;
610
+ if (bytes.length === 65 && bytes[0] === 0x04) {
611
+ coordinates = bytes.slice(1);
612
+ }
613
+ else if (bytes.length === 64) {
614
+ coordinates = bytes;
615
+ }
616
+ else {
617
+ throw new TypeError("computeNetworkId: expected a 65-byte 0x04-prefixed or 64-byte raw uncompressed public key");
618
+ }
619
+ return bytesToHex(keccak_256(coordinates));
620
+ }
621
+
622
+ /**
623
+ * Derives the full `v1` device identity — `networkId` plus the four-key
624
+ * all-secp256k1 model — from a single device secret.
625
+ *
626
+ * Pipeline (NORMATIVE):
627
+ * 1. `account = deriveAccountKeypair(deviceSecret)`
628
+ * 2. `networkId = computeNetworkId(account.uncompressedPublicKey)`
629
+ * 3. `sig = signDerivationMessage(account.privateKey)` (EIP-191, deterministic)
630
+ * 4. `{ spending, viewing } = deriveStealthKeys(sig)` (FluidKey semantics)
631
+ * 5. `auth = deriveAuthKeypair(sig)`
632
+ */
633
+ function deriveKeys(deviceSecret) {
634
+ const account = deriveAccountKeypair(deviceSecret);
635
+ const networkId = computeNetworkId(account.uncompressedPublicKey);
636
+ const signature = signDerivationMessage(account.privateKey);
637
+ const { spending, viewing } = deriveStealthKeys(signature);
638
+ const auth = deriveAuthKeypair(signature);
639
+ return {
640
+ version: KEY_DERIVATION_VERSION,
641
+ networkId,
642
+ account,
643
+ spending,
644
+ viewing,
645
+ auth
646
+ };
647
+ }
648
+ /** Derives only the `networkId` — equal to `deriveKeys(deviceSecret).networkId`, minus the signature work. */
649
+ function deriveNetworkId(deviceSecret) {
650
+ const account = deriveAccountKeypair(deviceSecret);
651
+ return computeNetworkId(account.uncompressedPublicKey);
652
+ }
653
+
654
+ /**
655
+ * Normalizes a P-256 `confirmationKey` to its raw 64-byte `X || Y`
656
+ * coordinates, validating that the point is on the curve. Accepts a 65-byte
657
+ * SEC1 uncompressed, 33-byte compressed, or 64-byte raw `X || Y` encoding.
658
+ */
659
+ function normalizeConfirmationKey(confirmationKey) {
660
+ let bytes;
661
+ try {
662
+ bytes = toBytes(confirmationKey);
663
+ }
664
+ catch {
665
+ throw new InvalidBindingInputError("normalizeConfirmationKey: expected raw bytes or a well-formed 0x-prefixed hex string");
666
+ }
667
+ if (bytes.length !== 33 && bytes.length !== 64 && bytes.length !== 65) {
668
+ throw new InvalidBindingInputError("normalizeConfirmationKey: expected a 65-byte uncompressed, 33-byte compressed, or 64-byte raw P-256 public key");
669
+ }
670
+ const sec1Bytes = bytes.length === 64 ? prependUncompressedPrefix(bytes) : bytes;
671
+ try {
672
+ return p256.ProjectivePoint.fromHex(sec1Bytes).toRawBytes(false).slice(1);
673
+ }
674
+ catch {
675
+ throw new InvalidBindingInputError("normalizeConfirmationKey: not a valid on-curve P-256 public key");
676
+ }
677
+ }
678
+ /**
679
+ * Computes the `confirmationKeyHash` embedded in the account binding message.
680
+ *
681
+ * NORMATIVE: the RFC 7638 JWK thumbprint of the P-256 confirmation key, i.e.
682
+ * `sha256(utf8('{"crv":"P-256","kty":"EC","x":<base64url X>,"y":<base64url Y>}'))`
683
+ */
684
+ function computeConfirmationKeyHash(confirmationKey) {
685
+ const coordinates = normalizeConfirmationKey(confirmationKey);
686
+ const x = bytesToBase64url(coordinates.slice(0, 32));
687
+ const y = bytesToBase64url(coordinates.slice(32));
688
+ return bytesToHex(sha256(`{"crv":"P-256","kty":"EC","x":"${x}","y":"${y}"}`));
689
+ }
690
+
691
+ /**
692
+ * Credential dual proof-of-possession binding — see `DERIVATION.md` §11.
693
+ *
694
+ * NORMATIVE layouts:
695
+ * ```
696
+ * deviceBindingMessage = nonce(32) || networkId(32) || holderKeyCommitment(32) // credential issuance
697
+ * accountBindingMessage = nonce(32) || confirmationKeyHash(32) || networkId(32) // 96B, signed by A (secp256k1)
698
+ * ```
699
+ *
700
+ * NORMATIVE MUST: the device signs a locally-derived `networkId`, never one
701
+ * supplied by the issuance request. Every function here except the two
702
+ * low-level builders and {@link verifyDeviceBindingSignature} enforces this
703
+ * by deriving `networkId` internally rather than accepting it as a parameter.
704
+ */
705
+ function toBindingBytes(value, label) {
706
+ try {
707
+ return toBytes(value);
708
+ }
709
+ catch {
710
+ throw new InvalidBindingInputError(`${label}: expected raw bytes or a well-formed 0x-prefixed hex string`);
711
+ }
712
+ }
713
+ function decodeAndValidateNonceAndNetworkId(nonce, networkId) {
714
+ const nonceBytes = toBindingBytes(nonce, "nonce");
715
+ const networkIdBytes = toBindingBytes(networkId, "networkId");
716
+ if (nonceBytes.length !== BINDING_NONCE_LENGTH) {
717
+ throw new InvalidBindingInputError(`nonce: expected exactly ${BINDING_NONCE_LENGTH} bytes`);
718
+ }
719
+ if (networkIdBytes.length !== BINDING_NONCE_LENGTH) {
720
+ throw new InvalidBindingInputError(`networkId: expected exactly ${BINDING_NONCE_LENGTH} bytes`);
721
+ }
722
+ return { nonceBytes, networkIdBytes };
723
+ }
724
+ /**
725
+ * Normalizes an `accountPublicKey` (65-byte uncompressed, 33-byte compressed,
726
+ * or 64-byte raw `X || Y`) to its 65-byte SEC1 uncompressed encoding,
727
+ * validating that it is on the secp256k1 curve.
728
+ */
729
+ function normalizeAccountPublicKey(accountPublicKey) {
730
+ const bytes = toBindingBytes(accountPublicKey, "accountPublicKey");
731
+ if (bytes.length !== 33 && bytes.length !== 64 && bytes.length !== 65) {
732
+ throw new InvalidBindingInputError("accountPublicKey: expected a 65-byte uncompressed, 33-byte compressed, or 64-byte raw secp256k1 public key");
733
+ }
734
+ const sec1Bytes = bytes.length === 64 ? prependUncompressedPrefix(bytes) : bytes;
735
+ try {
736
+ return secp256k1.ProjectivePoint.fromHex(sec1Bytes).toRawBytes(false);
737
+ }
738
+ catch {
739
+ throw new InvalidBindingInputError("accountPublicKey: not a valid on-curve secp256k1 public key");
740
+ }
741
+ }
742
+ function normalizeAccountSignature(signature) {
743
+ try {
744
+ return normalizeSignature64(signature);
745
+ }
746
+ catch {
747
+ throw new InvalidBindingInputError("accountSignature: expected a 64-byte compact secp256k1 signature (r || s)");
748
+ }
749
+ }
750
+ /**
751
+ * Parses a P-256 signature in either accepted platform encoding: 64-byte raw
752
+ * `r || s` (WebCrypto) or DER (Secure Enclave / Android Keystore). The DER
753
+ * `0x30` tag is checked before length because a genuine DER signature can
754
+ * itself be exactly 64 bytes.
755
+ */
756
+ function parseDeviceSignature(signature) {
757
+ const bytes = toBindingBytes(signature, "deviceSignature");
758
+ if (bytes[0] === 0x30) {
759
+ try {
760
+ return p256.Signature.fromDER(bytes);
761
+ }
762
+ catch {
763
+ // not valid DER; try compact
764
+ }
765
+ }
766
+ if (bytes.length === 64) {
767
+ try {
768
+ return p256.Signature.fromCompact(bytes);
769
+ }
770
+ catch {
771
+ // not valid compact either; reject below
772
+ }
773
+ }
774
+ throw new InvalidBindingInputError("deviceSignature: expected a 64-byte compact (r || s) or DER-encoded P-256 signature");
775
+ }
776
+ /**
777
+ * Builds `nonce(32) || networkId(32)`, optionally followed by a 32-byte holder commitment.
778
+ *
779
+ * Low-level, spec-literal builder — prefer {@link buildBindingMessages},
780
+ * which enforces the locally-derived-`networkId` MUST structurally.
781
+ */
782
+ function buildDeviceBindingMessage(nonce, networkId, holderKeyCommitment) {
783
+ const { nonceBytes, networkIdBytes } = decodeAndValidateNonceAndNetworkId(nonce, networkId);
784
+ if (holderKeyCommitment === undefined)
785
+ return concatBytes(nonceBytes, networkIdBytes);
786
+ const holderBytes = toBindingBytes(holderKeyCommitment, "holderKeyCommitment");
787
+ if (holderBytes.length !== 32)
788
+ throw new InvalidBindingInputError("holderKeyCommitment: expected exactly 32 bytes");
789
+ return concatBytes(nonceBytes, networkIdBytes, holderBytes);
790
+ }
791
+ /**
792
+ * Builds the 96-byte `accountBindingMessage`:
793
+ * `nonce(32) || confirmationKeyHash(32) || networkId(32)`.
794
+ *
795
+ * Low-level, spec-literal builder — see {@link buildBindingMessages} for the
796
+ * wallet-side entry point.
797
+ */
798
+ function buildAccountBindingMessage(nonce, confirmationKey, networkId) {
799
+ const { nonceBytes, networkIdBytes } = decodeAndValidateNonceAndNetworkId(nonce, networkId);
800
+ const confirmationKeyHash = toBytes(computeConfirmationKeyHash(confirmationKey));
801
+ return concatBytes(nonceBytes, confirmationKeyHash, networkIdBytes);
802
+ }
803
+ /**
804
+ * Wallet-side entry point: builds both binding messages with `networkId`
805
+ * derived internally from the account's own public key, so a caller cannot
806
+ * supply a `networkId` from elsewhere even by mistake.
807
+ */
808
+ function buildBindingMessages(nonce, accountUncompressedPublicKey, confirmationKey, holderKeyCommitment) {
809
+ let networkId;
810
+ try {
811
+ networkId = computeNetworkId(accountUncompressedPublicKey);
812
+ }
813
+ catch {
814
+ throw new InvalidBindingInputError("accountUncompressedPublicKey: expected a 65-byte 0x04-prefixed or 64-byte raw uncompressed secp256k1 public key");
815
+ }
816
+ const deviceBindingMessage = buildDeviceBindingMessage(nonce, networkId, holderKeyCommitment);
817
+ const accountBindingMessage = buildAccountBindingMessage(nonce, confirmationKey, networkId);
818
+ return { networkId, deviceBindingMessage, accountBindingMessage };
819
+ }
820
+ /**
821
+ * Produces `sig_A`: the EIP-191 signature over the `accountBindingMessage`,
822
+ * with `networkId` derived internally from the account private key. Same
823
+ * signing posture as {@link signDerivationMessage} (RFC-6979 deterministic,
824
+ * `lowS: true`, no `extraEntropy`, 64-byte compact `r || s`).
825
+ */
826
+ function signAccountBinding(accountPrivateKey, nonce, confirmationKey) {
827
+ const privateKeyBytes = toBytes(accountPrivateKey);
828
+ const uncompressedPublicKey = secp256k1.getPublicKey(privateKeyBytes, false);
829
+ const networkId = computeNetworkId(uncompressedPublicKey);
830
+ const message = buildAccountBindingMessage(nonce, confirmationKey, networkId);
831
+ const digest = eip191Digest(message);
832
+ const signature = secp256k1.sign(digest, privateKeyBytes, { lowS: true });
833
+ return { signature: bytesToHex(signature.toCompactRawBytes()), networkId };
834
+ }
835
+ /**
836
+ * Issuer-side verification of `sig_A`, with `networkId` derived internally
837
+ * from `accountPublicKey` — never accepted as a parameter.
838
+ *
839
+ * Malformed inputs throw {@link InvalidBindingInputError}; a well-formed
840
+ * signature that fails to verify returns `false`.
841
+ */
842
+ function verifyAccountBindingSignature({ nonce, confirmationKey, accountPublicKey, signature }) {
843
+ const publicKeyBytes = normalizeAccountPublicKey(accountPublicKey);
844
+ const networkId = computeNetworkId(publicKeyBytes);
845
+ const message = buildAccountBindingMessage(nonce, confirmationKey, networkId);
846
+ const digest = eip191Digest(message);
847
+ const signatureBytes = normalizeAccountSignature(signature);
848
+ return secp256k1.verify(signatureBytes, digest, publicKeyBytes, { lowS: true });
849
+ }
850
+ /**
851
+ * Issuer-side verification of `sig_B` (hardware P-256) over the
852
+ * `deviceBindingMessage`. NORMATIVE MUST: `networkId` — not derivable here,
853
+ * since `sig_B` is independent of the account key — must be the device's
854
+ * locally-derived value; prefer {@link verifyCredentialBinding}.
855
+ *
856
+ * `prehash: true` (signers hash SHA-256 internally); `lowS: false` (hardware
857
+ * signatures aren't s-normalized — `DERIVATION.md` §11.4).
858
+ */
859
+ function verifyDeviceBindingSignature({ nonce, networkId, confirmationKey, holderKeyCommitment, signature }) {
860
+ const message = buildDeviceBindingMessage(nonce, networkId, holderKeyCommitment);
861
+ const publicKeyBytes = prependUncompressedPrefix(normalizeConfirmationKey(confirmationKey));
862
+ const parsedSignature = parseDeviceSignature(signature);
863
+ return p256.verify(parsedSignature.toCompactRawBytes(), message, publicKeyBytes, {
864
+ prehash: true,
865
+ lowS: false
866
+ });
867
+ }
868
+ /**
869
+ * The full issuer gate: one `networkId` derived from `accountPublicKey` gates
870
+ * both `sig_A` and `sig_B`, so a device signature from one session can't pair
871
+ * with an unrelated account signature in either direction. `networkId` is
872
+ * returned even when `valid` is false, for issuer-side logging/rate-limiting.
873
+ */
874
+ function verifyCredentialBinding({ nonce, accountPublicKey, confirmationKey, holderKeyCommitment, accountSignature, deviceSignature }) {
875
+ const publicKeyBytes = normalizeAccountPublicKey(accountPublicKey);
876
+ const networkId = computeNetworkId(publicKeyBytes);
877
+ const accountValid = verifyAccountBindingSignature({
878
+ nonce,
879
+ confirmationKey,
880
+ accountPublicKey: publicKeyBytes,
881
+ signature: accountSignature
882
+ });
883
+ const deviceValid = verifyDeviceBindingSignature({
884
+ nonce,
885
+ networkId,
886
+ confirmationKey,
887
+ holderKeyCommitment,
888
+ signature: deviceSignature
889
+ });
890
+ return { valid: accountValid && deviceValid, networkId };
891
+ }
892
+
893
+ export { BINDING_NONCE_LENGTH, COMPRESSED_PUBLIC_KEY_LENGTH, DOMAIN_ACCOUNT, DOMAIN_AUTH, DOMAIN_EDDSA, DOMAIN_EDDSA_PEER, DerivationFailureError, EDDSA_PEER_POINT_HEX, ERC_5564_SCHEME_ID, FIXED_DOMAIN_MSG, InvalidBindingInputError, InvalidDeviceSecretError, InvalidEcdhSecretError, InvalidMetaStealthAddressError, InvalidPublicKeyLengthError, KEY_DERIVATION_VERSION, KeygenError, META_STEALTH_ADDRESS_LENGTH, META_STEALTH_URI_PREFIX, base64urlToBytes, buildAccountBindingMessage, buildAnnouncement, buildBindingMessages, buildDeviceBindingMessage, bytesToBase64url, bytesToHex, checkStealthAddress, computeConfirmationKeyHash, computeEddsaPeerPoint, computeNetworkId, computeSharedSecret, decodeMetaStealthAddress, deriveAccountKeypair, deriveAuthKeypair, deriveEddsaKeypair, deriveKeys, deriveNetworkId, deriveStealthKeys, eddsaPeerPointBytes, encodeMetaStealthAddress, encodeMetaStealthAddressUri, generateDeviceSecret, generateEphemeralKey, generateStealthAddress, generateStealthPrivateKey, getStealthPublicKey, getViewTag, hashToScalar, hashToScalarWith, hexToBytes, isValidScalar, metaStealthAddressUri, normalizeConfirmationKey, normalizeDeviceSecret, normalizeEcdhSharedSecret, publicKeyToAddress, scalarToKeypair, signAccountBinding, signDerivationMessage, validateMetaStealthAddress, verifyAccountBindingSignature, verifyCredentialBinding, verifyDeviceBindingSignature };