decane-connect-kit 2.8.0 → 2.9.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
@@ -357,6 +357,26 @@ interface DecaneConnect {
357
357
  * yourself. Throws NewDeviceError when this device isn't eligible.
358
358
  */
359
359
  signInWithPasskey(): Promise<ConnectResult>;
360
+ /** Whether THIS device's share is wrapped by a passkey rather than a PIN. */
361
+ hasPasskey(): Promise<boolean>;
362
+ /**
363
+ * Add a passkey to this device after the fact, re-wrapping its device share
364
+ * under it.
365
+ *
366
+ * For the device that fell back to a PIN because no passkey was reachable at
367
+ * the time — a password manager that was not signed in, a browser without
368
+ * WebAuthn PRF, a prompt the user dismissed. Without this the fallback is
369
+ * permanent: once a PIN-wrapped share exists, nothing in the normal unlock
370
+ * path ever offers a passkey again, however available they later become.
371
+ *
372
+ * Prompts for the current PIN (through `promptPin`) to unwrap the share, then
373
+ * asks for the new credential. Idempotent when a passkey is already in place.
374
+ * Requires a live session; throws `WebAuthnNotSupportedError` where passkeys
375
+ * are unavailable, and `NewDeviceError` when this device holds no share yet
376
+ * (that case wants `connect()`, which provisions one and registers a passkey
377
+ * on the way).
378
+ */
379
+ addPasskey(): Promise<void>;
360
380
  sessionExpiresAt(): number | null;
361
381
  signMessage(req: SignMessageRequest): Promise<string>;
362
382
  sendTransaction(req: SendTransactionRequest): Promise<string>;
@@ -477,7 +497,8 @@ interface WrappedShareMeta {
477
497
  * after a page reload.
478
498
  *
479
499
  * Every field is optional because providers differ in what they release, and a
480
- * user can withhold most of it. Email sign-in returns an empty profile.
500
+ * user can withhold most of it. Email sign-in returns a profile carrying just
501
+ * the `email` the user signed in with; phone sign-in returns no profile.
481
502
  */
482
503
  interface AuthProfile {
483
504
  name?: string;
@@ -1205,6 +1226,7 @@ declare function useSocialAuth(): {
1205
1226
  isCreatingWallet: boolean;
1206
1227
  needsReconnect: boolean;
1207
1228
  googleLoading: boolean;
1229
+ kingschatLoading: boolean;
1208
1230
  emailLoading: boolean;
1209
1231
  phoneLoading: boolean;
1210
1232
  tokenLoading: boolean;
@@ -1213,6 +1235,7 @@ declare function useSocialAuth(): {
1213
1235
  error: string | null;
1214
1236
  clearError: () => void;
1215
1237
  signInWithGoogle: () => Promise<void>;
1238
+ signInWithKingsChat: () => Promise<void>;
1216
1239
  sendEmailCode: (email: string) => Promise<{
1217
1240
  codeRequired: true;
1218
1241
  }>;
@@ -1282,6 +1305,8 @@ declare function useSocialWallet(): {
1282
1305
  chainId?: number;
1283
1306
  }) => Eip1193Provider;
1284
1307
  getAccessToken: () => string | null;
1308
+ hasPasskey: () => Promise<boolean>;
1309
+ addPasskey: () => Promise<void>;
1285
1310
  rotateShares: (opts: {
1286
1311
  password: string;
1287
1312
  passwordHint?: string;
package/dist/index.d.ts CHANGED
@@ -357,6 +357,26 @@ interface DecaneConnect {
357
357
  * yourself. Throws NewDeviceError when this device isn't eligible.
358
358
  */
359
359
  signInWithPasskey(): Promise<ConnectResult>;
360
+ /** Whether THIS device's share is wrapped by a passkey rather than a PIN. */
361
+ hasPasskey(): Promise<boolean>;
362
+ /**
363
+ * Add a passkey to this device after the fact, re-wrapping its device share
364
+ * under it.
365
+ *
366
+ * For the device that fell back to a PIN because no passkey was reachable at
367
+ * the time — a password manager that was not signed in, a browser without
368
+ * WebAuthn PRF, a prompt the user dismissed. Without this the fallback is
369
+ * permanent: once a PIN-wrapped share exists, nothing in the normal unlock
370
+ * path ever offers a passkey again, however available they later become.
371
+ *
372
+ * Prompts for the current PIN (through `promptPin`) to unwrap the share, then
373
+ * asks for the new credential. Idempotent when a passkey is already in place.
374
+ * Requires a live session; throws `WebAuthnNotSupportedError` where passkeys
375
+ * are unavailable, and `NewDeviceError` when this device holds no share yet
376
+ * (that case wants `connect()`, which provisions one and registers a passkey
377
+ * on the way).
378
+ */
379
+ addPasskey(): Promise<void>;
360
380
  sessionExpiresAt(): number | null;
361
381
  signMessage(req: SignMessageRequest): Promise<string>;
362
382
  sendTransaction(req: SendTransactionRequest): Promise<string>;
@@ -477,7 +497,8 @@ interface WrappedShareMeta {
477
497
  * after a page reload.
478
498
  *
479
499
  * Every field is optional because providers differ in what they release, and a
480
- * user can withhold most of it. Email sign-in returns an empty profile.
500
+ * user can withhold most of it. Email sign-in returns a profile carrying just
501
+ * the `email` the user signed in with; phone sign-in returns no profile.
481
502
  */
482
503
  interface AuthProfile {
483
504
  name?: string;
@@ -1205,6 +1226,7 @@ declare function useSocialAuth(): {
1205
1226
  isCreatingWallet: boolean;
1206
1227
  needsReconnect: boolean;
1207
1228
  googleLoading: boolean;
1229
+ kingschatLoading: boolean;
1208
1230
  emailLoading: boolean;
1209
1231
  phoneLoading: boolean;
1210
1232
  tokenLoading: boolean;
@@ -1213,6 +1235,7 @@ declare function useSocialAuth(): {
1213
1235
  error: string | null;
1214
1236
  clearError: () => void;
1215
1237
  signInWithGoogle: () => Promise<void>;
1238
+ signInWithKingsChat: () => Promise<void>;
1216
1239
  sendEmailCode: (email: string) => Promise<{
1217
1240
  codeRequired: true;
1218
1241
  }>;
@@ -1282,6 +1305,8 @@ declare function useSocialWallet(): {
1282
1305
  chainId?: number;
1283
1306
  }) => Eip1193Provider;
1284
1307
  getAccessToken: () => string | null;
1308
+ hasPasskey: () => Promise<boolean>;
1309
+ addPasskey: () => Promise<void>;
1285
1310
  rotateShares: (opts: {
1286
1311
  password: string;
1287
1312
  passwordHint?: string;
package/dist/index.js CHANGED
@@ -3618,20 +3618,17 @@ async function createDecaneConnect(config) {
3618
3618
  const pin = await config.promptPin();
3619
3619
  return deriveKeyFromPin(pin, stored.pinSalt);
3620
3620
  }
3621
- async function registerPasskeyAndBackup(userId, jwt, deviceShare) {
3622
- if (!isPasskeySupported()) return;
3621
+ async function registerPasskeyForDevice(userId, jwt, deviceShare) {
3622
+ if (!isPasskeySupported()) throw new WebAuthnNotSupportedError();
3623
3623
  const prfSalt = generateRandomBytes(32);
3624
- let credentialId;
3625
- let prfOutput;
3626
- try {
3627
- const result = await registerWebAuthnCredential({ api, jwt, rpId, rpName, prfSalt });
3628
- credentialId = result.credentialId;
3629
- prfOutput = result.prfOutput;
3630
- } catch (e) {
3631
- console.warn("decane-connect-kit: passkey provisioning after recovery failed; user can set this up later.", e);
3632
- return;
3633
- }
3634
- if (!prfOutput) return;
3624
+ const { credentialId, prfOutput } = await registerWebAuthnCredential({
3625
+ api,
3626
+ jwt,
3627
+ rpId,
3628
+ rpName,
3629
+ prfSalt
3630
+ });
3631
+ if (!prfOutput) throw new WebAuthnNotSupportedError();
3635
3632
  const wrappingKey = await importAesKey(prfOutput);
3636
3633
  const iv = generateRandomBytes(12);
3637
3634
  const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
@@ -3656,6 +3653,17 @@ async function createDecaneConnect(config) {
3656
3653
  console.warn("decane-connect-kit: wrapped share backup upload failed; user can retry from settings.", e);
3657
3654
  }
3658
3655
  }
3656
+ async function registerPasskeyAndBackup(userId, jwt, deviceShare) {
3657
+ if (!isPasskeySupported()) return;
3658
+ try {
3659
+ await registerPasskeyForDevice(userId, jwt, deviceShare);
3660
+ } catch (e) {
3661
+ console.warn(
3662
+ "decane-connect-kit: passkey provisioning failed; the user can set one up later with addPasskey().",
3663
+ e
3664
+ );
3665
+ }
3666
+ }
3659
3667
  async function firstTimeSetup(userId, jwt) {
3660
3668
  prewarmAttestation(teeUrl);
3661
3669
  let unlockMethod;
@@ -4105,6 +4113,34 @@ async function createDecaneConnect(config) {
4105
4113
  if (!candidate) throw new NewDeviceError();
4106
4114
  return passkeyLogin(candidate);
4107
4115
  },
4116
+ async hasPasskey() {
4117
+ if (!auth) return false;
4118
+ const stored = await storage.getShare(auth.userId);
4119
+ return stored?.unlockMethod === "passkey";
4120
+ },
4121
+ async addPasskey() {
4122
+ if (!auth) throw new ReconnectRequiredError();
4123
+ if (!isPasskeySupported()) throw new WebAuthnNotSupportedError();
4124
+ const stored = await storage.getShare(auth.userId);
4125
+ if (!stored) throw new NewDeviceError();
4126
+ if (stored.unlockMethod === "passkey") return;
4127
+ let deviceShare;
4128
+ try {
4129
+ deviceShare = await aesGcmDecrypt(
4130
+ await getWrappingKey(stored),
4131
+ stored.wrappedDeviceShare,
4132
+ stored.iv
4133
+ );
4134
+ } catch (e) {
4135
+ if (e instanceof DecaneConnectError) throw e;
4136
+ throw new ShareCorruptedError();
4137
+ }
4138
+ try {
4139
+ await registerPasskeyForDevice(auth.userId, auth.jwt, deviceShare);
4140
+ } finally {
4141
+ zeroize(deviceShare);
4142
+ }
4143
+ },
4108
4144
  async unlock() {
4109
4145
  if (!auth) {
4110
4146
  const candidate = await passkeyLoginCandidate();
@@ -6871,6 +6907,16 @@ function DecaneKit({ config, children }) {
6871
6907
  if (!sdk) throw new Error("Social wallet not connected");
6872
6908
  return sdk.exportPortableBackup(opts);
6873
6909
  }, []);
6910
+ const socialHasPasskey = useCallback3(async () => {
6911
+ const sdk = socialRef.current;
6912
+ if (!sdk) throw new Error("Social wallet not connected");
6913
+ return sdk.hasPasskey();
6914
+ }, []);
6915
+ const socialAddPasskey = useCallback3(async () => {
6916
+ const sdk = socialRef.current;
6917
+ if (!sdk) throw new Error("Social wallet not connected");
6918
+ return sdk.addPasskey();
6919
+ }, []);
6874
6920
  const socialHasRecoveryShare = useCallback3(async () => {
6875
6921
  const sdk = socialRef.current;
6876
6922
  if (!sdk) throw new Error("Social wallet not connected");
@@ -6932,6 +6978,8 @@ function DecaneKit({ config, children }) {
6932
6978
  socialRotateShares,
6933
6979
  socialExportPortableBackup,
6934
6980
  socialHasRecoveryShare,
6981
+ socialHasPasskey,
6982
+ socialAddPasskey,
6935
6983
  socialShouldPromptRecovery,
6936
6984
  socialDismissRecoveryPrompt,
6937
6985
  socialModalOpen,
@@ -7010,6 +7058,7 @@ function useSocialAuth() {
7010
7058
  const {
7011
7059
  socialAuth,
7012
7060
  connectWithGoogle,
7061
+ connectWithKingsChat,
7013
7062
  connectWithEmail,
7014
7063
  verifyEmailCode: verifyEmailCode2,
7015
7064
  connectWithPhone,
@@ -7022,6 +7071,7 @@ function useSocialAuth() {
7022
7071
  socialDisconnect
7023
7072
  } = useDecaneContext();
7024
7073
  const [googleLoading, setGoogleLoading] = useState4(false);
7074
+ const [kingschatLoading, setKingschatLoading] = useState4(false);
7025
7075
  const [emailLoading, setEmailLoading] = useState4(false);
7026
7076
  const [phoneLoading, setPhoneLoading] = useState4(false);
7027
7077
  const [tokenLoading, setTokenLoading] = useState4(false);
@@ -7037,6 +7087,18 @@ function useSocialAuth() {
7037
7087
  setGoogleLoading(false);
7038
7088
  }
7039
7089
  }, [connectWithGoogle]);
7090
+ const signInWithKingsChat = useCallback4(async () => {
7091
+ setKingschatLoading(true);
7092
+ setError(null);
7093
+ try {
7094
+ await connectWithKingsChat();
7095
+ } catch (e) {
7096
+ setError(e.message);
7097
+ throw e;
7098
+ } finally {
7099
+ setKingschatLoading(false);
7100
+ }
7101
+ }, [connectWithKingsChat]);
7040
7102
  const sendEmailCode = useCallback4(async (email) => {
7041
7103
  setEmailLoading(true);
7042
7104
  setError(null);
@@ -7156,6 +7218,7 @@ function useSocialAuth() {
7156
7218
  // this into a real reconnect (signInWithGoogle / sendEmailCode), not unlock().
7157
7219
  needsReconnect: socialAuth.needsReconnect,
7158
7220
  googleLoading,
7221
+ kingschatLoading,
7159
7222
  emailLoading,
7160
7223
  phoneLoading,
7161
7224
  tokenLoading,
@@ -7164,6 +7227,7 @@ function useSocialAuth() {
7164
7227
  error,
7165
7228
  clearError: () => setError(null),
7166
7229
  signInWithGoogle,
7230
+ signInWithKingsChat,
7167
7231
  sendEmailCode,
7168
7232
  confirmEmailCode,
7169
7233
  sendPhoneCode,
@@ -7195,6 +7259,8 @@ function useSocialWallet() {
7195
7259
  socialRotateShares,
7196
7260
  socialExportPortableBackup,
7197
7261
  socialHasRecoveryShare,
7262
+ socialHasPasskey,
7263
+ socialAddPasskey,
7198
7264
  socialShouldPromptRecovery,
7199
7265
  socialDismissRecoveryPrompt,
7200
7266
  openSocialModal,
@@ -7282,6 +7348,16 @@ function useSocialWallet() {
7282
7348
  (opts) => run(() => socialExportPortableBackup(opts)),
7283
7349
  [socialExportPortableBackup]
7284
7350
  );
7351
+ const hasPasskey = useCallback4(() => run(() => socialHasPasskey()), [socialHasPasskey]);
7352
+ const addPasskey = useCallback4(async () => {
7353
+ setError(null);
7354
+ try {
7355
+ await socialAddPasskey();
7356
+ } catch (e) {
7357
+ setError(e.message);
7358
+ throw e;
7359
+ }
7360
+ }, [socialAddPasskey]);
7285
7361
  const hasRecoveryShare = useCallback4(
7286
7362
  () => run(() => socialHasRecoveryShare()),
7287
7363
  [socialHasRecoveryShare]
@@ -7335,6 +7411,9 @@ function useSocialWallet() {
7335
7411
  // The Decane JWT — authenticate the user to your own backend (bundler proxy,
7336
7412
  // etc.); verify server-side against backend.decane.app/.well-known/jwks.json.
7337
7413
  getAccessToken: socialGetAccessToken,
7414
+ // Passkey management for THIS device
7415
+ hasPasskey,
7416
+ addPasskey,
7338
7417
  // Recovery (2-of-3 Shamir share set)
7339
7418
  rotateShares,
7340
7419
  exportPortableBackup,