@accesly/react 1.0.0-pre.2 → 1.0.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/.tsbuildinfo +1 -1
- package/dist/index.cjs +95 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +80 -2
- package/dist/index.d.ts +80 -2
- package/dist/index.js +95 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -454,6 +454,78 @@ interface ReconstructedSeed {
|
|
|
454
454
|
/** Base64 32-byte salt que vino del backend. */
|
|
455
455
|
readonly recoverySalt: string;
|
|
456
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Input para `recovery.finalize(...)` — orquestador end-to-end de la rotación
|
|
459
|
+
* de signers tras una recuperación por OTP.
|
|
460
|
+
*
|
|
461
|
+
* El caller debe pre-cumplir 2 pasos UI:
|
|
462
|
+
* a) `reconstructSeed(...)` — devolvió `privateSeed` (la VIEJA, va a firmar
|
|
463
|
+
* la auth entry) + `publicKey` (la VIEJA, no se rota acá).
|
|
464
|
+
* b) WebAuthn `navigator.credentials.create()` con PRF extension — devolvió
|
|
465
|
+
* `credentialId`, `secp256r1Pubkey`, y un output PRF que el caller
|
|
466
|
+
* derivó en 2 keys `newF1Key` + `newF2Key` (HKDF típicamente).
|
|
467
|
+
*
|
|
468
|
+
* El SDK hace todo lo demás client-side:
|
|
469
|
+
* 1. Genera fresh ed25519 seed (la NUEVA) + Shamir 2-of-3 split.
|
|
470
|
+
* 2. Cifra F1' con `newF1Key`, F2' con `newF2Key`, F3' con `recoveryKey`.
|
|
471
|
+
* 3. Cifra una segunda copia de F2' con `recoveryKey` (F2_recovery) para
|
|
472
|
+
* que la siguiente recovery siga siendo posible.
|
|
473
|
+
* 4. POST /recovery/simulate-rotate-signer → backend simula + devuelve material.
|
|
474
|
+
* 5. Firma la auth entry con la SEED VIEJA contra la regla `admin-cfg`.
|
|
475
|
+
* 6. POST /recovery/finalize con auth entry firmada + new fragments.
|
|
476
|
+
* 7. Persiste el nuevo `CredentialRecord` en `DeviceStore` (sustituye el viejo).
|
|
477
|
+
* 8. Zeroiza todas las llaves intermedias (la seed vieja la entrega el caller,
|
|
478
|
+
* su lifecycle es responsabilidad del caller).
|
|
479
|
+
*/
|
|
480
|
+
interface FinalizeRecoveryInput {
|
|
481
|
+
/** Email del usuario (case-insensitive, se normaliza). */
|
|
482
|
+
readonly email: string;
|
|
483
|
+
/**
|
|
484
|
+
* Password de Cognito (UTF-8 bytes). El SDK la usa SOLO para derivar el
|
|
485
|
+
* `newRecoveryKey` con `PBKDF2(password, newRecoverySalt, 600k)`. Caller
|
|
486
|
+
* debe zeroizar tras la llamada.
|
|
487
|
+
*/
|
|
488
|
+
readonly cognitoPassword: Uint8Array;
|
|
489
|
+
/** Token KMS-HMAC que devolvió `verifyOtp()` — TTL 5min. */
|
|
490
|
+
readonly recoveryJwt: string;
|
|
491
|
+
/**
|
|
492
|
+
* La seed VIEJA reconstruida por `reconstructSeed()`. Se usa SOLO para
|
|
493
|
+
* firmar la `SorobanAuthorizationEntry` de `rotate_signer` (la regla
|
|
494
|
+
* `admin-cfg` valida con el ed25519 pubkey actual del Smart Account, que
|
|
495
|
+
* es el del owner viejo). Tras firmar, el SDK la zeroiza internamente.
|
|
496
|
+
*
|
|
497
|
+
* IMPORTANTE: pasa `seedResult.privateSeed` tal cual te lo dio
|
|
498
|
+
* `reconstructSeed()`. Si ya lo zeroizaste, falla.
|
|
499
|
+
*/
|
|
500
|
+
readonly oldReconstructedSeed: Uint8Array;
|
|
501
|
+
/**
|
|
502
|
+
* La pubkey VIEJA (32 bytes) — derivable de `oldReconstructedSeed`, pero
|
|
503
|
+
* la pedimos explícita para sanity-check y para empaquetar dentro del
|
|
504
|
+
* `Signer::External(verifier, pubkey)` del AuthPayload.
|
|
505
|
+
*/
|
|
506
|
+
readonly oldOwnerPubkey: Uint8Array;
|
|
507
|
+
/** Credential ID del nuevo passkey (de `navigator.credentials.create`). */
|
|
508
|
+
readonly newCredentialId: Uint8Array;
|
|
509
|
+
/** Pubkey secp256r1 uncompressed (65 bytes) del nuevo passkey. */
|
|
510
|
+
readonly newSecp256r1Pubkey: Uint8Array;
|
|
511
|
+
/** Salt PRF del nuevo passkey (32 bytes random). */
|
|
512
|
+
readonly newPrfSalt: Uint8Array;
|
|
513
|
+
/** AES-256 key derivada del PRF output para cifrar F1'. */
|
|
514
|
+
readonly newF1Key: Uint8Array;
|
|
515
|
+
/** AES-256 key derivada del PRF output para cifrar F2' (PRF-bound, sign path). */
|
|
516
|
+
readonly newF2Key: Uint8Array;
|
|
517
|
+
/** Salt aleatorio (32 bytes) para el nuevo emailCommitment. */
|
|
518
|
+
readonly newEmailSalt: Uint8Array;
|
|
519
|
+
}
|
|
520
|
+
interface FinalizeRecoveryResult {
|
|
521
|
+
readonly walletAddress: string;
|
|
522
|
+
readonly txHash: string;
|
|
523
|
+
readonly status: string;
|
|
524
|
+
/** Pubkey ed25519 NUEVA (32 bytes). Útil para UI confirmación. */
|
|
525
|
+
readonly newPublicKey: Uint8Array;
|
|
526
|
+
/** Link al explorer. */
|
|
527
|
+
readonly explorerUrl: string;
|
|
528
|
+
}
|
|
457
529
|
interface RecoveryNamespace {
|
|
458
530
|
/** Pide OTP. Backend rate-limita; el caller debe respetar `cooldownSeconds`. */
|
|
459
531
|
requestOtp(input: {
|
|
@@ -484,12 +556,18 @@ interface RecoveryNamespace {
|
|
|
484
556
|
recoveryJwt: string;
|
|
485
557
|
}): Promise<ReconstructedSeed>;
|
|
486
558
|
/**
|
|
487
|
-
*
|
|
488
|
-
* `
|
|
559
|
+
* Orquestador completo de la rotación de signers para Recovery v2.
|
|
560
|
+
* Ver `FinalizeRecoveryInput` para los pre-requisitos.
|
|
561
|
+
*/
|
|
562
|
+
finalize(input: FinalizeRecoveryInput): Promise<FinalizeRecoveryResult>;
|
|
563
|
+
/**
|
|
564
|
+
* Bajo nivel: submitea la rotación al backend tras que el caller haya
|
|
565
|
+
* armado el body manualmente. `finalize(...)` es el wrapper recomendado.
|
|
489
566
|
*/
|
|
490
567
|
submitFinalize(input: {
|
|
491
568
|
recoveryJwt: string;
|
|
492
569
|
unsignedXdr: string;
|
|
570
|
+
signedAuthEntryXdr: string;
|
|
493
571
|
newSecp256r1Pubkey: string;
|
|
494
572
|
newFragmentF1Encrypted: EncryptedEnvelope;
|
|
495
573
|
newFragmentF2Encrypted: EncryptedEnvelope;
|
package/dist/index.js
CHANGED
|
@@ -572,9 +572,103 @@ function useAccesly() {
|
|
|
572
572
|
recoverySalt: frag.recoverySalt
|
|
573
573
|
};
|
|
574
574
|
},
|
|
575
|
+
async finalize(input) {
|
|
576
|
+
const networkPassphrase = stellarConfig.networkPassphrase;
|
|
577
|
+
const verifierAddress = stellarConfig.ed25519VerifierAddress;
|
|
578
|
+
const explorerBase = networkPassphrase === "Public Global Stellar Network ; September 2015" ? "https://stellar.expert/explorer/public/tx/" : "https://stellar.expert/explorer/testnet/tx/";
|
|
579
|
+
if (input.oldReconstructedSeed.length !== 32) {
|
|
580
|
+
throw new Error(
|
|
581
|
+
`recovery.finalize: oldReconstructedSeed must be 32 bytes, got ${input.oldReconstructedSeed.length}`
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
if (input.oldOwnerPubkey.length !== 32) {
|
|
585
|
+
throw new Error(
|
|
586
|
+
`recovery.finalize: oldOwnerPubkey must be 32 bytes, got ${input.oldOwnerPubkey.length}`
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
if (input.newSecp256r1Pubkey.length !== 65) {
|
|
590
|
+
throw new Error(
|
|
591
|
+
`recovery.finalize: newSecp256r1Pubkey must be 65 bytes (uncompressed), got ${input.newSecp256r1Pubkey.length}`
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
const newRecoverySalt = generateRecoverySalt();
|
|
595
|
+
const newRecoveryKey = deriveRecoveryKey({
|
|
596
|
+
password: input.cognitoPassword,
|
|
597
|
+
salt: newRecoverySalt
|
|
598
|
+
});
|
|
599
|
+
const newRecoverySaltBase64 = base64FromBytes(newRecoverySalt);
|
|
600
|
+
const created = createWallet({
|
|
601
|
+
emailBytes: new TextEncoder().encode(input.email),
|
|
602
|
+
emailSalt: input.newEmailSalt,
|
|
603
|
+
encryptionKeys: [input.newF1Key, input.newF2Key, newRecoveryKey]
|
|
604
|
+
});
|
|
605
|
+
const f2PlainShare = decryptAesGcm(created.encryptedFragments[1], input.newF2Key);
|
|
606
|
+
let newFragmentF2Recovery;
|
|
607
|
+
try {
|
|
608
|
+
newFragmentF2Recovery = encryptAesGcm(f2PlainShare, newRecoveryKey);
|
|
609
|
+
} finally {
|
|
610
|
+
for (let i = 0; i < f2PlainShare.length; i += 1) f2PlainShare[i] = 0;
|
|
611
|
+
}
|
|
612
|
+
const newSecp256r1Canonical = normalizeSecp256r1Pubkey(input.newSecp256r1Pubkey);
|
|
613
|
+
const newOwnerHex = hexFromBytes(created.publicKey);
|
|
614
|
+
const newSecpHex = hexFromBytes(newSecp256r1Canonical);
|
|
615
|
+
const newEmailCommitHex = hexFromBytes(created.emailCommitment);
|
|
616
|
+
const sim = await ctx.endpoints.simulateRotateSigner(input.recoveryJwt, {
|
|
617
|
+
newOwnerEd25519Pubkey: newOwnerHex,
|
|
618
|
+
newSecp256r1Pubkey: newSecpHex,
|
|
619
|
+
newEmailCommitment: newEmailCommitHex
|
|
620
|
+
});
|
|
621
|
+
const { signedAuthEntryXdr } = await signSorobanAuthEntry({
|
|
622
|
+
signaturePayloadHashBase64: sim.signaturePayloadHashBase64,
|
|
623
|
+
contextRuleIds: [...sim.contextRuleIds],
|
|
624
|
+
placeholderAuthEntryXdr: sim.placeholderAuthEntryXdr,
|
|
625
|
+
ed25519Seed: input.oldReconstructedSeed,
|
|
626
|
+
ed25519VerifierAddress: verifierAddress,
|
|
627
|
+
ownerPubkey: input.oldOwnerPubkey
|
|
628
|
+
});
|
|
629
|
+
let finalize;
|
|
630
|
+
try {
|
|
631
|
+
finalize = await ctx.endpoints.finalizeRecovery(input.recoveryJwt, {
|
|
632
|
+
unsignedXdr: sim.unsignedXdr,
|
|
633
|
+
signedAuthEntryXdr,
|
|
634
|
+
newSecp256r1Pubkey: newSecpHex,
|
|
635
|
+
newFragmentF1Encrypted: encodeFragmentToWire(created.encryptedFragments[0]),
|
|
636
|
+
newFragmentF2Encrypted: encodeFragmentToWire(created.encryptedFragments[1]),
|
|
637
|
+
newFragmentF2Recovery: encodeFragmentToWire(newFragmentF2Recovery),
|
|
638
|
+
newFragmentF3Encrypted: encodeFragmentToWire(created.encryptedFragments[2]),
|
|
639
|
+
newRecoverySalt: newRecoverySaltBase64,
|
|
640
|
+
newEmailCommitment: newEmailCommitHex
|
|
641
|
+
});
|
|
642
|
+
} finally {
|
|
643
|
+
for (let i = 0; i < newRecoveryKey.length; i += 1) newRecoveryKey[i] = 0;
|
|
644
|
+
}
|
|
645
|
+
await ctx.deviceStore.saveCredential({
|
|
646
|
+
username: input.email,
|
|
647
|
+
credentialId: input.newCredentialId,
|
|
648
|
+
secp256r1Pubkey: newSecp256r1Canonical,
|
|
649
|
+
fragmentF1Encrypted: created.encryptedFragments[0],
|
|
650
|
+
fragmentF2Encrypted: created.encryptedFragments[1],
|
|
651
|
+
fragmentF3Encrypted: created.encryptedFragments[2],
|
|
652
|
+
publicKey: created.publicKey,
|
|
653
|
+
emailCommitment: created.emailCommitment,
|
|
654
|
+
prfSalt: input.newPrfSalt,
|
|
655
|
+
fallbackKeyMaterial: new Uint8Array(0),
|
|
656
|
+
walletAddress: finalize.walletAddress,
|
|
657
|
+
onChain: true,
|
|
658
|
+
createdAt: Date.now()
|
|
659
|
+
});
|
|
660
|
+
return {
|
|
661
|
+
walletAddress: finalize.walletAddress,
|
|
662
|
+
txHash: finalize.txHash,
|
|
663
|
+
status: finalize.status,
|
|
664
|
+
newPublicKey: created.publicKey,
|
|
665
|
+
explorerUrl: `${explorerBase}${finalize.txHash}`
|
|
666
|
+
};
|
|
667
|
+
},
|
|
575
668
|
async submitFinalize(input) {
|
|
576
669
|
return ctx.endpoints.finalizeRecovery(input.recoveryJwt, {
|
|
577
670
|
unsignedXdr: input.unsignedXdr,
|
|
671
|
+
signedAuthEntryXdr: input.signedAuthEntryXdr,
|
|
578
672
|
newSecp256r1Pubkey: input.newSecp256r1Pubkey,
|
|
579
673
|
newFragmentF1Encrypted: encodeFragmentToWire(input.newFragmentF1Encrypted),
|
|
580
674
|
newFragmentF2Encrypted: encodeFragmentToWire(input.newFragmentF2Encrypted),
|
|
@@ -585,7 +679,7 @@ function useAccesly() {
|
|
|
585
679
|
});
|
|
586
680
|
}
|
|
587
681
|
}),
|
|
588
|
-
[ctx]
|
|
682
|
+
[ctx, stellarConfig, hexFromBytes]
|
|
589
683
|
);
|
|
590
684
|
const session = useMemo(
|
|
591
685
|
() => ({
|