decane-connect-kit 2.17.0 → 2.19.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.cts CHANGED
@@ -592,7 +592,12 @@ declare class ApiClient {
592
592
  metadata?: Record<string, unknown>;
593
593
  }): Promise<TransactionRecord>;
594
594
  getTransactions(jwt: string, limit?: number): Promise<TransactionRecord[]>;
595
- getWebAuthnRegisterOptions(jwt: string, rpId: string, rpName: string): Promise<_simplewebauthn_browser.PublicKeyCredentialCreationOptionsJSON>;
595
+ /**
596
+ * `label` is what the user's password manager will show for this passkey.
597
+ * Without one they see the account's opaque id. Sent, used, and forgotten —
598
+ * the backend echoes it into the options and stores nothing.
599
+ */
600
+ getWebAuthnRegisterOptions(jwt: string, rpId: string, rpName: string, label?: string): Promise<_simplewebauthn_browser.PublicKeyCredentialCreationOptionsJSON>;
596
601
  postWebAuthnRegister(jwt: string, response: _simplewebauthn_browser.RegistrationResponseJSON, origin: string): Promise<void>;
597
602
  /**
598
603
  * Exchange a token from the developer's own auth system for a Decane
package/dist/index.d.ts CHANGED
@@ -592,7 +592,12 @@ declare class ApiClient {
592
592
  metadata?: Record<string, unknown>;
593
593
  }): Promise<TransactionRecord>;
594
594
  getTransactions(jwt: string, limit?: number): Promise<TransactionRecord[]>;
595
- getWebAuthnRegisterOptions(jwt: string, rpId: string, rpName: string): Promise<_simplewebauthn_browser.PublicKeyCredentialCreationOptionsJSON>;
595
+ /**
596
+ * `label` is what the user's password manager will show for this passkey.
597
+ * Without one they see the account's opaque id. Sent, used, and forgotten —
598
+ * the backend echoes it into the options and stores nothing.
599
+ */
600
+ getWebAuthnRegisterOptions(jwt: string, rpId: string, rpName: string, label?: string): Promise<_simplewebauthn_browser.PublicKeyCredentialCreationOptionsJSON>;
596
601
  postWebAuthnRegister(jwt: string, response: _simplewebauthn_browser.RegistrationResponseJSON, origin: string): Promise<void>;
597
602
  /**
598
603
  * Exchange a token from the developer's own auth system for a Decane
package/dist/index.js CHANGED
@@ -2085,8 +2085,14 @@ var ApiClient = class {
2085
2085
  return body.transactions;
2086
2086
  }
2087
2087
  // ── WebAuthn registration (additive to the PRF-only passkey flow) ─────────
2088
- async getWebAuthnRegisterOptions(jwt, rpId, rpName) {
2089
- return this._request(`/webauthn/register-options?rpId=${encodeURIComponent(rpId)}&rpName=${encodeURIComponent(rpName)}`, {
2088
+ /**
2089
+ * `label` is what the user's password manager will show for this passkey.
2090
+ * Without one they see the account's opaque id. Sent, used, and forgotten —
2091
+ * the backend echoes it into the options and stores nothing.
2092
+ */
2093
+ async getWebAuthnRegisterOptions(jwt, rpId, rpName, label) {
2094
+ const query = `rpId=${encodeURIComponent(rpId)}&rpName=${encodeURIComponent(rpName)}` + (label ? `&label=${encodeURIComponent(label)}` : "");
2095
+ return this._request(`/webauthn/register-options?${query}`, {
2090
2096
  headers: { Authorization: `Bearer ${jwt}` }
2091
2097
  });
2092
2098
  }
@@ -2970,7 +2976,13 @@ function bufFromBytes(u) {
2970
2976
  return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2971
2977
  }
2972
2978
  async function registerWebAuthnCredential(opts) {
2973
- const baseOptions = await opts.api.getWebAuthnRegisterOptions(opts.jwt, opts.rpId, opts.rpName);
2979
+ const baseOptions = await opts.api.getWebAuthnRegisterOptions(
2980
+ opts.jwt,
2981
+ opts.rpId,
2982
+ opts.rpName,
2983
+ opts.label
2984
+ );
2985
+ if (opts.allowDuplicate) delete baseOptions.excludeCredentials;
2974
2986
  const optionsJSON = {
2975
2987
  ...baseOptions,
2976
2988
  extensions: {
@@ -2979,11 +2991,7 @@ async function registerWebAuthnCredential(opts) {
2979
2991
  }
2980
2992
  };
2981
2993
  const { credentialId, prfOutput, response } = await registerPasskey({ optionsJSON });
2982
- try {
2983
- await opts.api.postWebAuthnRegister(opts.jwt, response, window.location.origin);
2984
- } catch (e) {
2985
- console.warn("decane-connect-kit: WebAuthn server-side registration failed; PRF-based unlock still works, but rotateShares()/exportPortableBackup() will not until this is retried.", e);
2986
- }
2994
+ await opts.api.postWebAuthnRegister(opts.jwt, response, window.location.origin);
2987
2995
  return { credentialId, prfOutput };
2988
2996
  }
2989
2997
 
@@ -3773,21 +3781,24 @@ async function createDecaneConnect(config) {
3773
3781
  const pin = await config.promptPin();
3774
3782
  return deriveKeyFromPin(pin, stored.pinSalt);
3775
3783
  }
3776
- async function registerPasskeyForDevice(userId, jwt, deviceShare) {
3784
+ async function registerPasskeyForDevice(userId, jwt, deviceShare, opts = {}) {
3777
3785
  if (!isPasskeySupported()) throw new WebAuthnNotSupportedError();
3778
3786
  const prfSalt = generateRandomBytes(32);
3779
3787
  let credentialId;
3780
3788
  let prfOutput;
3781
3789
  try {
3790
+ const label = (await storage.getShare(userId))?.profile?.email;
3782
3791
  ({ credentialId, prfOutput } = await registerWebAuthnCredential({
3783
3792
  api,
3784
3793
  jwt,
3785
3794
  rpId,
3786
3795
  rpName,
3787
- prfSalt
3796
+ prfSalt,
3797
+ ...label ? { label } : {},
3798
+ ...opts.allowDuplicate ? { allowDuplicate: true } : {}
3788
3799
  }));
3789
3800
  } catch (e) {
3790
- if (e?.name !== "InvalidStateError") throw e;
3801
+ if (e?.name !== "InvalidStateError" || opts.allowDuplicate) throw e;
3791
3802
  return adoptExistingPasskey(userId, jwt, deviceShare);
3792
3803
  }
3793
3804
  let key = prfOutput;
@@ -3839,7 +3850,12 @@ async function createDecaneConnect(config) {
3839
3850
  }
3840
3851
  async function adoptExistingPasskey(userId, jwt, deviceShare) {
3841
3852
  const options = await api.getWebAuthnLoginOptions(userId, rpId);
3842
- if (!options) throw new WebAuthnNotSupportedError();
3853
+ if (!options) {
3854
+ console.warn(
3855
+ "decane-connect-kit: this authenticator holds a credential the server does not know; registering a fresh one."
3856
+ );
3857
+ return registerPasskeyForDevice(userId, jwt, deviceShare, { allowDuplicate: true });
3858
+ }
3843
3859
  const prfSalt = generateRandomBytes(32);
3844
3860
  const { response, prfOutput } = await authenticateWithPasskey({
3845
3861
  optionsJSON: options,
@@ -3884,19 +3900,26 @@ async function createDecaneConnect(config) {
3884
3900
  );
3885
3901
  return;
3886
3902
  }
3887
- const { wrappingKey, tokenKey, meta } = await passwordSetup();
3888
- const iv = generateRandomBytes(12);
3889
- const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
3890
- await storage.putShare({
3891
- userId,
3892
- wrappedDeviceShare,
3893
- iv,
3894
- unlockMethod: "password",
3895
- passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
3896
- passwordIterations: meta.passwordIterations,
3897
- version: 1
3898
- });
3899
- await attachUnlockToken(userId, jwt, tokenKey);
3903
+ try {
3904
+ const { wrappingKey, tokenKey, meta } = await passwordSetup();
3905
+ const iv = generateRandomBytes(12);
3906
+ const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
3907
+ await storage.putShare({
3908
+ userId,
3909
+ wrappedDeviceShare,
3910
+ iv,
3911
+ unlockMethod: "password",
3912
+ passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
3913
+ passwordIterations: meta.passwordIterations,
3914
+ version: 1
3915
+ });
3916
+ await attachUnlockToken(userId, jwt, tokenKey);
3917
+ } catch (e) {
3918
+ console.warn(
3919
+ "decane-connect-kit: no unlock password was set, so this device stored no share. The session works; the next visit will provision again.",
3920
+ e
3921
+ );
3922
+ }
3900
3923
  }
3901
3924
  async function firstTimeSetup(userId, jwt) {
3902
3925
  prewarmAttestation(teeUrl);