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.cjs CHANGED
@@ -2167,8 +2167,14 @@ var ApiClient = class {
2167
2167
  return body.transactions;
2168
2168
  }
2169
2169
  // ── WebAuthn registration (additive to the PRF-only passkey flow) ─────────
2170
- async getWebAuthnRegisterOptions(jwt, rpId, rpName) {
2171
- return this._request(`/webauthn/register-options?rpId=${encodeURIComponent(rpId)}&rpName=${encodeURIComponent(rpName)}`, {
2170
+ /**
2171
+ * `label` is what the user's password manager will show for this passkey.
2172
+ * Without one they see the account's opaque id. Sent, used, and forgotten —
2173
+ * the backend echoes it into the options and stores nothing.
2174
+ */
2175
+ async getWebAuthnRegisterOptions(jwt, rpId, rpName, label) {
2176
+ const query = `rpId=${encodeURIComponent(rpId)}&rpName=${encodeURIComponent(rpName)}` + (label ? `&label=${encodeURIComponent(label)}` : "");
2177
+ return this._request(`/webauthn/register-options?${query}`, {
2172
2178
  headers: { Authorization: `Bearer ${jwt}` }
2173
2179
  });
2174
2180
  }
@@ -3052,7 +3058,13 @@ function bufFromBytes(u) {
3052
3058
  return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
3053
3059
  }
3054
3060
  async function registerWebAuthnCredential(opts) {
3055
- const baseOptions = await opts.api.getWebAuthnRegisterOptions(opts.jwt, opts.rpId, opts.rpName);
3061
+ const baseOptions = await opts.api.getWebAuthnRegisterOptions(
3062
+ opts.jwt,
3063
+ opts.rpId,
3064
+ opts.rpName,
3065
+ opts.label
3066
+ );
3067
+ if (opts.allowDuplicate) delete baseOptions.excludeCredentials;
3056
3068
  const optionsJSON = {
3057
3069
  ...baseOptions,
3058
3070
  extensions: {
@@ -3061,11 +3073,7 @@ async function registerWebAuthnCredential(opts) {
3061
3073
  }
3062
3074
  };
3063
3075
  const { credentialId, prfOutput, response } = await registerPasskey({ optionsJSON });
3064
- try {
3065
- await opts.api.postWebAuthnRegister(opts.jwt, response, window.location.origin);
3066
- } catch (e) {
3067
- 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);
3068
- }
3076
+ await opts.api.postWebAuthnRegister(opts.jwt, response, window.location.origin);
3069
3077
  return { credentialId, prfOutput };
3070
3078
  }
3071
3079
 
@@ -3855,21 +3863,24 @@ async function createDecaneConnect(config) {
3855
3863
  const pin = await config.promptPin();
3856
3864
  return deriveKeyFromPin(pin, stored.pinSalt);
3857
3865
  }
3858
- async function registerPasskeyForDevice(userId, jwt, deviceShare) {
3866
+ async function registerPasskeyForDevice(userId, jwt, deviceShare, opts = {}) {
3859
3867
  if (!isPasskeySupported()) throw new WebAuthnNotSupportedError();
3860
3868
  const prfSalt = generateRandomBytes(32);
3861
3869
  let credentialId;
3862
3870
  let prfOutput;
3863
3871
  try {
3872
+ const label = (await storage.getShare(userId))?.profile?.email;
3864
3873
  ({ credentialId, prfOutput } = await registerWebAuthnCredential({
3865
3874
  api,
3866
3875
  jwt,
3867
3876
  rpId,
3868
3877
  rpName,
3869
- prfSalt
3878
+ prfSalt,
3879
+ ...label ? { label } : {},
3880
+ ...opts.allowDuplicate ? { allowDuplicate: true } : {}
3870
3881
  }));
3871
3882
  } catch (e) {
3872
- if (e?.name !== "InvalidStateError") throw e;
3883
+ if (e?.name !== "InvalidStateError" || opts.allowDuplicate) throw e;
3873
3884
  return adoptExistingPasskey(userId, jwt, deviceShare);
3874
3885
  }
3875
3886
  let key = prfOutput;
@@ -3921,7 +3932,12 @@ async function createDecaneConnect(config) {
3921
3932
  }
3922
3933
  async function adoptExistingPasskey(userId, jwt, deviceShare) {
3923
3934
  const options = await api.getWebAuthnLoginOptions(userId, rpId);
3924
- if (!options) throw new WebAuthnNotSupportedError();
3935
+ if (!options) {
3936
+ console.warn(
3937
+ "decane-connect-kit: this authenticator holds a credential the server does not know; registering a fresh one."
3938
+ );
3939
+ return registerPasskeyForDevice(userId, jwt, deviceShare, { allowDuplicate: true });
3940
+ }
3925
3941
  const prfSalt = generateRandomBytes(32);
3926
3942
  const { response, prfOutput } = await authenticateWithPasskey({
3927
3943
  optionsJSON: options,
@@ -3966,19 +3982,26 @@ async function createDecaneConnect(config) {
3966
3982
  );
3967
3983
  return;
3968
3984
  }
3969
- const { wrappingKey, tokenKey, meta } = await passwordSetup();
3970
- const iv = generateRandomBytes(12);
3971
- const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
3972
- await storage.putShare({
3973
- userId,
3974
- wrappedDeviceShare,
3975
- iv,
3976
- unlockMethod: "password",
3977
- passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
3978
- passwordIterations: meta.passwordIterations,
3979
- version: 1
3980
- });
3981
- await attachUnlockToken(userId, jwt, tokenKey);
3985
+ try {
3986
+ const { wrappingKey, tokenKey, meta } = await passwordSetup();
3987
+ const iv = generateRandomBytes(12);
3988
+ const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
3989
+ await storage.putShare({
3990
+ userId,
3991
+ wrappedDeviceShare,
3992
+ iv,
3993
+ unlockMethod: "password",
3994
+ passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
3995
+ passwordIterations: meta.passwordIterations,
3996
+ version: 1
3997
+ });
3998
+ await attachUnlockToken(userId, jwt, tokenKey);
3999
+ } catch (e) {
4000
+ console.warn(
4001
+ "decane-connect-kit: no unlock password was set, so this device stored no share. The session works; the next visit will provision again.",
4002
+ e
4003
+ );
4004
+ }
3982
4005
  }
3983
4006
  async function firstTimeSetup(userId, jwt) {
3984
4007
  prewarmAttestation(teeUrl);