@accesly/react 1.0.0-pre.0 → 1.0.0-pre.2

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
@@ -1,4 +1,4 @@
1
- import { Environment, CognitoConfig, AuthClient, SessionStorage, DeviceStore, TelemetrySink, TokenManager, AccesslyEndpoints, AuthStatus, CredentialRecord } from '@accesly/core';
1
+ import { Environment, CognitoConfig, AuthClient, SessionStorage, DeviceStore, TelemetrySink, TokenManager, AccesslyEndpoints, AuthStatus, CredentialRecord, EncryptedEnvelope } from '@accesly/core';
2
2
  import * as react from 'react';
3
3
  import { ReactNode } from 'react';
4
4
 
@@ -127,6 +127,22 @@ interface CreateWalletInput {
127
127
  readonly credentialId?: Uint8Array;
128
128
  /** Optional. See `credentialId`. */
129
129
  readonly prfSalt?: Uint8Array;
130
+ /**
131
+ * Password de Cognito en plano (`Uint8Array` codificado UTF-8).
132
+ *
133
+ * Recovery v2 (Fase 1, 2026-06-15): si se provee, el SDK deriva
134
+ * `recoveryKey = PBKDF2(password, recoverySalt, 600k)` y la usa para
135
+ * cifrar F3 antes de enviarlo al backend, en vez de usar
136
+ * `encryptionKeys[2]`. El backend almacena F3 cifrado con esa key —
137
+ * SOLO descifrable client-side con el mismo password.
138
+ *
139
+ * Sin esta prop el wallet se crea pero NO podrá recuperarse vía OTP
140
+ * (F3 quedará cifrado con `encryptionKeys[2]`, igual que en 0.x).
141
+ *
142
+ * El caller es responsable de zeroizar este buffer tras `createWallet`
143
+ * (el SDK no lo retiene en memoria).
144
+ */
145
+ readonly cognitoPassword?: Uint8Array;
130
146
  }
131
147
  interface CreatedWalletInfo {
132
148
  readonly walletAddress: string;
@@ -409,11 +425,90 @@ interface YieldNamespace {
409
425
  declare class NotImplementedYetError extends Error {
410
426
  constructor(namespace: string, method: string);
411
427
  }
428
+ /**
429
+ * Recovery v2 — OTP por email + password de Cognito (Fase 1, 2026-06-15).
430
+ *
431
+ * Flujo desde la UI:
432
+ * 1. `recovery.requestOtp({ email })` → manda el OTP por SES.
433
+ * 2. `recovery.verifyOtp({ email, code })` → devuelve `recoveryJwt`.
434
+ * 3. `recovery.finalize({ email, password, recoveryJwt })` orquesta todo:
435
+ * - GET /fragments/3 con el JWT
436
+ * - Deriva recoveryKey con el password + recoverySalt del backend
437
+ * - Decifra F3
438
+ * - Decifra F2 (vía session key ECDH)
439
+ * - Combina F2+F3 → seed ed25519 reconstruida
440
+ * - Genera new passkey + new Shamir split (F1', F2', F3')
441
+ * - Re-cifra F3' con la misma recoveryKey + nuevo salt
442
+ * - Firma la tx `rotate_signer` localmente
443
+ * - POST /recovery/finalize con todo
444
+ * - Persiste new CredentialRecord local
445
+ * - Zero-iza la seed
446
+ */
447
+ interface ReconstructedSeed {
448
+ /** 32-byte ed25519 seed reconstruida vía Shamir(F2_recovery + F3). CALLER ZEROIZE. */
449
+ readonly privateSeed: Uint8Array;
450
+ /** 32-byte ed25519 public key derivada. */
451
+ readonly publicKey: Uint8Array;
452
+ /** 32-byte recoveryKey derivada del password — útil para re-cifrar F2'/F3' nuevos. */
453
+ readonly recoveryKey: Uint8Array;
454
+ /** Base64 32-byte salt que vino del backend. */
455
+ readonly recoverySalt: string;
456
+ }
457
+ interface RecoveryNamespace {
458
+ /** Pide OTP. Backend rate-limita; el caller debe respetar `cooldownSeconds`. */
459
+ requestOtp(input: {
460
+ email: string;
461
+ }): Promise<{
462
+ cooldownSeconds: number;
463
+ expiresInSeconds: number;
464
+ }>;
465
+ /** Verifica OTP. Devuelve `recoveryJwt` con TTL 5min. */
466
+ verifyOtp(input: {
467
+ email: string;
468
+ code: string;
469
+ }): Promise<{
470
+ recoveryJwt: string;
471
+ expiresAt: number;
472
+ }>;
473
+ /**
474
+ * Descarga `/fragments/3`, descifra F2_recovery + F3 con la `recoveryKey`
475
+ * derivada del password y reconstruye la seed via Shamir.
476
+ *
477
+ * El caller DEBE zero-izar `result.privateSeed` y `result.recoveryKey`
478
+ * tras firmar la rotación + cifrar las nuevas F1'/F2'/F3'.
479
+ *
480
+ * El caller también es responsable de zeroizar `cognitoPassword` después.
481
+ */
482
+ reconstructSeed(input: {
483
+ cognitoPassword: Uint8Array;
484
+ recoveryJwt: string;
485
+ }): Promise<ReconstructedSeed>;
486
+ /**
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.
489
+ */
490
+ submitFinalize(input: {
491
+ recoveryJwt: string;
492
+ unsignedXdr: string;
493
+ newSecp256r1Pubkey: string;
494
+ newFragmentF1Encrypted: EncryptedEnvelope;
495
+ newFragmentF2Encrypted: EncryptedEnvelope;
496
+ newFragmentF2Recovery: EncryptedEnvelope;
497
+ newFragmentF3Encrypted: EncryptedEnvelope;
498
+ newRecoverySalt: string;
499
+ newEmailCommitment: string;
500
+ }): Promise<{
501
+ walletAddress: string;
502
+ txHash: string;
503
+ status: string;
504
+ }>;
505
+ }
412
506
  interface AcceslyHook {
413
507
  readonly auth: AuthNamespace;
414
508
  readonly wallet: WalletNamespace;
415
509
  readonly tx: TxNamespace;
416
510
  readonly kyc: KycNamespace;
511
+ readonly recovery: RecoveryNamespace;
417
512
  readonly session: SessionNamespace;
418
513
  readonly settings: SettingsNamespace;
419
514
  readonly yieldOps: YieldNamespace;
@@ -446,4 +541,4 @@ declare function useAccesly(): AcceslyHook;
446
541
  */
447
542
  declare const REACT_ADAPTER_VERSION = "0.0.0";
448
543
 
449
- export { AcceslyContext, type AcceslyContextValue, type AcceslyHook, AcceslyProvider, type AcceslyProviderProps, type AuthNamespace, type CreateWalletInput, type CreatedWalletInfo, ENVIRONMENT_DEFAULTS, type EnsureWalletResult, type EnvironmentDefaults, type KycNamespace, NotImplementedYetError, REACT_ADAPTER_VERSION, type RemoteWalletInfo, type RetryDeployResult, type SendXlmInput, type SendXlmResult, type SessionNamespace, type SettingsNamespace, type TxNamespace, type WalletNamespace, type WalletStatus, type YieldNamespace, useAccesly };
544
+ export { AcceslyContext, type AcceslyContextValue, type AcceslyHook, AcceslyProvider, type AcceslyProviderProps, type AuthNamespace, type CreateWalletInput, type CreatedWalletInfo, ENVIRONMENT_DEFAULTS, type EnsureWalletResult, type EnvironmentDefaults, type KycNamespace, NotImplementedYetError, REACT_ADAPTER_VERSION, type RecoveryNamespace, type RemoteWalletInfo, type RetryDeployResult, type SendXlmInput, type SendXlmResult, type SessionNamespace, type SettingsNamespace, type TxNamespace, type WalletNamespace, type WalletStatus, type YieldNamespace, useAccesly };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { CognitoAuthClient, InMemorySessionStorage, InMemoryDeviceStore, TokenManager, AccesslyApiClient, AccesslyEndpoints, normalizeSecp256r1Pubkey, createWallet, computeSmartAccountAddress, signTransaction, generateX25519Keypair, unwrapSessionFragment2, reconstructFromPlainAndEncrypted, signSorobanAuthEntry, AccesslyApiError } from '@accesly/core';
1
+ import { CognitoAuthClient, InMemorySessionStorage, InMemoryDeviceStore, TokenManager, AccesslyApiClient, AccesslyEndpoints, normalizeSecp256r1Pubkey, createWallet, generateRecoverySalt, decryptAesGcm, deriveRecoveryKey, encryptAesGcm, emailHashBytes, computeSmartAccountAddress, signTransaction, generateX25519Keypair, unwrapSessionFragment2, reconstructFromPlainAndEncrypted, signSorobanAuthEntry, reconstructKey, AccesslyApiError } from '@accesly/core';
2
2
  import { createContext, useMemo, useState, useRef, useEffect, useContext } from 'react';
3
3
  import { jsx } from 'react/jsx-runtime';
4
4
 
@@ -189,7 +189,10 @@ function useAccesly() {
189
189
  emailCommitment: hexFromBytes(params.emailCommitment),
190
190
  secp256r1Pubkey: hexFromBytes(params.secp256r1Pubkey),
191
191
  fragmentF2: encodeFragmentToWire(params.fragmentF2),
192
- fragmentF3: encodeFragmentToWire(params.fragmentF3)
192
+ fragmentF3: encodeFragmentToWire(params.fragmentF3),
193
+ ...params.fragmentF2Recovery ? { fragmentF2Recovery: encodeFragmentToWire(params.fragmentF2Recovery) } : {},
194
+ ...params.emailHash ? { emailHash: params.emailHash } : {},
195
+ ...params.recoverySalt ? { recoverySalt: params.recoverySalt } : {}
193
196
  });
194
197
  return res.walletAddress;
195
198
  };
@@ -270,6 +273,28 @@ function useAccesly() {
270
273
  emailSalt: input.emailSalt,
271
274
  encryptionKeys: input.encryptionKeys
272
275
  });
276
+ let fragmentF3ToSend = created.encryptedFragments[2];
277
+ let fragmentF2Recovery;
278
+ let recoverySaltBase64;
279
+ if (input.cognitoPassword) {
280
+ const recoverySalt = generateRecoverySalt();
281
+ const f2Plain = decryptAesGcm(created.encryptedFragments[1], input.encryptionKeys[1]);
282
+ const f3Plain = decryptAesGcm(created.encryptedFragments[2], input.encryptionKeys[2]);
283
+ const recoveryKey = deriveRecoveryKey({
284
+ password: input.cognitoPassword,
285
+ salt: recoverySalt
286
+ });
287
+ try {
288
+ fragmentF2Recovery = encryptAesGcm(f2Plain, recoveryKey);
289
+ fragmentF3ToSend = encryptAesGcm(f3Plain, recoveryKey);
290
+ } finally {
291
+ for (let i = 0; i < recoveryKey.length; i += 1) recoveryKey[i] = 0;
292
+ for (let i = 0; i < f2Plain.length; i += 1) f2Plain[i] = 0;
293
+ for (let i = 0; i < f3Plain.length; i += 1) f3Plain[i] = 0;
294
+ }
295
+ recoverySaltBase64 = base64FromBytes(recoverySalt);
296
+ }
297
+ const emailHashHex = hexFromBytes(emailHashBytes(input.email));
273
298
  const predictedAddress = await computeSmartAccountAddress({
274
299
  ownerPubkey: created.publicKey,
275
300
  deployerAddress: stellarConfig.deployerAddress,
@@ -283,7 +308,7 @@ function useAccesly() {
283
308
  secp256r1Pubkey: secp256r1Canonical,
284
309
  fragmentF1Encrypted: created.encryptedFragments[0],
285
310
  fragmentF2Encrypted: created.encryptedFragments[1],
286
- fragmentF3Encrypted: created.encryptedFragments[2],
311
+ fragmentF3Encrypted: fragmentF3ToSend,
287
312
  publicKey: created.publicKey,
288
313
  emailCommitment: created.emailCommitment,
289
314
  prfSalt: input.prfSalt,
@@ -302,7 +327,10 @@ function useAccesly() {
302
327
  emailCommitment: created.emailCommitment,
303
328
  secp256r1Pubkey: secp256r1Canonical,
304
329
  fragmentF2: created.encryptedFragments[1],
305
- fragmentF3: created.encryptedFragments[2]
330
+ fragmentF3: fragmentF3ToSend,
331
+ emailHash: emailHashHex,
332
+ ...fragmentF2Recovery ? { fragmentF2Recovery } : {},
333
+ ...recoverySaltBase64 ? { recoverySalt: recoverySaltBase64 } : {}
306
334
  });
307
335
  } catch (err) {
308
336
  if (!isSorobanDeployPendingError(err)) throw err;
@@ -503,6 +531,62 @@ function useAccesly() {
503
531
  }),
504
532
  [ctx]
505
533
  );
534
+ const recovery = useMemo(
535
+ () => ({
536
+ async requestOtp(input) {
537
+ return ctx.endpoints.requestRecoveryOtp(input);
538
+ },
539
+ async verifyOtp(input) {
540
+ return ctx.endpoints.verifyRecoveryOtp(input);
541
+ },
542
+ async reconstructSeed(input) {
543
+ const frag = await ctx.endpoints.getFragment3(input.recoveryJwt);
544
+ if (!frag.fragmentF2Recovery) {
545
+ throw new Error(
546
+ "recovery.reconstructSeed: la wallet fue creada antes de Fase 1 y no tiene F2 cipher-bound a recoveryKey. No es recuperable v\xEDa OTP."
547
+ );
548
+ }
549
+ const recoverySalt = base64ToBytes(frag.recoverySalt);
550
+ const recoveryKey = deriveRecoveryKey({
551
+ password: input.cognitoPassword,
552
+ salt: recoverySalt
553
+ });
554
+ const f2Envelope = {
555
+ ciphertext: base64ToBytes(frag.fragmentF2Recovery.ciphertext),
556
+ nonce: base64ToBytes(frag.fragmentF2Recovery.nonce)
557
+ };
558
+ const f3Envelope = {
559
+ ciphertext: base64ToBytes(frag.fragmentF3Encrypted.ciphertext),
560
+ nonce: base64ToBytes(frag.fragmentF3Encrypted.nonce)
561
+ };
562
+ const seedResult = reconstructKey({
563
+ fragments: [
564
+ { envelope: f2Envelope, key: recoveryKey },
565
+ { envelope: f3Envelope, key: recoveryKey }
566
+ ]
567
+ });
568
+ return {
569
+ privateSeed: seedResult.privateSeed,
570
+ publicKey: seedResult.publicKey,
571
+ recoveryKey,
572
+ recoverySalt: frag.recoverySalt
573
+ };
574
+ },
575
+ async submitFinalize(input) {
576
+ return ctx.endpoints.finalizeRecovery(input.recoveryJwt, {
577
+ unsignedXdr: input.unsignedXdr,
578
+ newSecp256r1Pubkey: input.newSecp256r1Pubkey,
579
+ newFragmentF1Encrypted: encodeFragmentToWire(input.newFragmentF1Encrypted),
580
+ newFragmentF2Encrypted: encodeFragmentToWire(input.newFragmentF2Encrypted),
581
+ newFragmentF2Recovery: encodeFragmentToWire(input.newFragmentF2Recovery),
582
+ newFragmentF3Encrypted: encodeFragmentToWire(input.newFragmentF3Encrypted),
583
+ newRecoverySalt: input.newRecoverySalt,
584
+ newEmailCommitment: input.newEmailCommitment
585
+ });
586
+ }
587
+ }),
588
+ [ctx]
589
+ );
506
590
  const session = useMemo(
507
591
  () => ({
508
592
  async create() {
@@ -545,7 +629,7 @@ function useAccesly() {
545
629
  }),
546
630
  []
547
631
  );
548
- return { auth, wallet, tx, kyc, session, settings, yieldOps, _internal: ctx };
632
+ return { auth, wallet, tx, kyc, recovery, session, settings, yieldOps, _internal: ctx };
549
633
  }
550
634
  function coderHelpers() {
551
635
  function hexFromBytes(bytes) {