@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/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
- * Envía la rotación al backend tras que el caller haya construido la tx
488
- * `rotate_signer` y firmado el auth entry localmente.
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
  () => ({