decane-connect-kit 2.29.3 → 2.31.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.js CHANGED
@@ -20,6 +20,7 @@ __export(errors_exports, {
20
20
  InvalidApiKeyError: () => InvalidApiKeyError,
21
21
  InvalidPinError: () => InvalidPinError,
22
22
  InvalidRecoveryFileError: () => InvalidRecoveryFileError,
23
+ LoginInUseError: () => LoginInUseError,
23
24
  NetworkError: () => NetworkError,
24
25
  NewDeviceError: () => NewDeviceError,
25
26
  OriginNotAllowedError: () => OriginNotAllowedError,
@@ -42,7 +43,7 @@ __export(errors_exports, {
42
43
  WrongPasswordError: () => WrongPasswordError,
43
44
  WrongUnlockPasswordError: () => WrongUnlockPasswordError
44
45
  });
45
- var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, ProvisioningTimeoutError, NewDeviceError, StaleDeviceShareError, PromptPinNotConfiguredError, PromptPasswordNotConfiguredError, WrongUnlockPasswordError, InvalidAccessTokenError, UnlockTokenRejectedError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError, WalletExistsError, WalletEnrolmentError, ProvisioningRateLimitedError, EnclaveHttpError;
46
+ var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, ProvisioningTimeoutError, NewDeviceError, StaleDeviceShareError, PromptPinNotConfiguredError, PromptPasswordNotConfiguredError, WrongUnlockPasswordError, InvalidAccessTokenError, UnlockTokenRejectedError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError, LoginInUseError, WalletExistsError, WalletEnrolmentError, ProvisioningRateLimitedError, EnclaveHttpError;
46
47
  var init_errors = __esm({
47
48
  "src/social/errors.ts"() {
48
49
  "use strict";
@@ -191,6 +192,11 @@ var init_errors = __esm({
191
192
  super("EXPORT_NOT_AUTHORIZED", "Export requires a fresh security check", true);
192
193
  }
193
194
  };
195
+ LoginInUseError = class extends DecaneConnectError {
196
+ constructor(message = "That login already opens another account. It was not linked.") {
197
+ super("LOGIN_IN_USE", message);
198
+ }
199
+ };
194
200
  WalletExistsError = class extends DecaneConnectError {
195
201
  constructor() {
196
202
  super("WALLET_EXISTS", "This account already has a wallet; opening it instead.", true);
@@ -2031,7 +2037,9 @@ var ERROR_MAP = {
2031
2037
  // generic failure message.
2032
2038
  UNAUTHORIZED: ReconnectRequiredError,
2033
2039
  // A restorable wallet already exists: the client provisions instead of creating.
2034
- WALLET_EXISTS: WalletExistsError
2040
+ WALLET_EXISTS: WalletExistsError,
2041
+ // linkWithToken(): the login already opens another account.
2042
+ LOGIN_IN_USE: LoginInUseError
2035
2043
  };
2036
2044
  var REQUEST_TIMEOUT_MS = 3e4;
2037
2045
  var ApiClient = class {
@@ -2098,9 +2106,19 @@ var ApiClient = class {
2098
2106
  return this._request("/auth/kingschat/config", { method: "GET" });
2099
2107
  }
2100
2108
  /**
2101
- * Exchange a KingsChat access token for a Decane session. The backend spends
2102
- * the token against KingsChat's own API to establish the identity — it is
2103
- * never trusted on its face, and never stored.
2109
+ * Exchange a KingsChat authorization code for a Decane session. The backend
2110
+ * trades it with KingsChat for an access token, verifies that token (it must
2111
+ * have been issued to this project's KingsChat app), and never stores it.
2112
+ */
2113
+ async authKingsChatCode(code) {
2114
+ return this._request("/auth/kingschat/code", {
2115
+ method: "POST",
2116
+ body: JSON.stringify({ code })
2117
+ });
2118
+ }
2119
+ /**
2120
+ * Exchange a KingsChat access token the app already holds for a Decane
2121
+ * session. Verified the same way as a code's token, and never stored.
2104
2122
  */
2105
2123
  async authKingsChat(accessToken) {
2106
2124
  return this._request("/auth/kingschat", {
@@ -2200,6 +2218,23 @@ var ApiClient = class {
2200
2218
  body: JSON.stringify(providerId ? { token, providerId } : { token })
2201
2219
  });
2202
2220
  }
2221
+ /** Attach a login from the project's own provider to the signed-in account. */
2222
+ async linkExternal(jwt, token, providerId) {
2223
+ return this._request("/auth/link/external", {
2224
+ method: "POST",
2225
+ headers: { Authorization: `Bearer ${jwt}` },
2226
+ body: JSON.stringify(providerId ? { token, providerId } : { token })
2227
+ });
2228
+ }
2229
+ async listLinkedLogins(jwt) {
2230
+ return this._request("/auth/link/external", { method: "GET", headers: { Authorization: `Bearer ${jwt}` } });
2231
+ }
2232
+ async unlinkLogin(jwt, id) {
2233
+ await this._request(`/auth/link/external/${encodeURIComponent(id)}`, {
2234
+ method: "DELETE",
2235
+ headers: { Authorization: `Bearer ${jwt}` }
2236
+ });
2237
+ }
2203
2238
  /**
2204
2239
  * Returns null when this user has no server-registered passkey, which is the
2205
2240
  * expected answer for wallets created before passkey sign-in existed or for
@@ -3263,75 +3298,47 @@ async function verifyPhoneCode(api, phone, code) {
3263
3298
 
3264
3299
  // src/social/auth/kingschat.ts
3265
3300
  init_errors();
3266
- var KINGSCHAT_AUTH_ORIGIN = "https://accounts.kingsch.at";
3267
- function readTokens(data) {
3268
- if (!data || typeof data !== "object") return null;
3269
- const d = data;
3270
- const accessToken = d.accessToken ?? d.access_token;
3271
- if (typeof accessToken !== "string" || !accessToken) return null;
3272
- const refreshToken = d.refreshToken ?? d.refresh_token;
3273
- const expires = d.expiresInMillis ?? d.expires_in_millis;
3274
- return {
3275
- accessToken,
3276
- refreshToken: typeof refreshToken === "string" ? refreshToken : void 0,
3277
- expiresInMillis: typeof expires === "number" ? expires : void 0
3278
- };
3301
+ var KINGSCHAT_LOGIN_URL = "https://accounts.kingschat.online/log-in";
3302
+ var STATE_KEY = "decane:kingschat_state";
3303
+ function randomState() {
3304
+ const bytes = new Uint8Array(16);
3305
+ crypto.getRandomValues(bytes);
3306
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
3307
+ }
3308
+ function storage2() {
3309
+ try {
3310
+ return window.sessionStorage ?? null;
3311
+ } catch {
3312
+ return null;
3313
+ }
3279
3314
  }
3280
- function buildAuthUrl(clientId, scopes) {
3281
- const url = new URL(KINGSCHAT_AUTH_ORIGIN);
3282
- url.searchParams.set("client_id", clientId);
3283
- url.searchParams.set("scopes", JSON.stringify(scopes));
3284
- url.searchParams.set("redirect_uri", window.location.origin);
3285
- url.searchParams.set("post_message", "1");
3315
+ function beginKingsChatLogin(clientId, opts = {}) {
3316
+ const store = storage2();
3317
+ if (!store) throw new Error("KingsChat sign-in needs sessionStorage, which this browser is blocking");
3318
+ const state = randomState();
3319
+ store.setItem(STATE_KEY, state);
3320
+ const url = new URL(KINGSCHAT_LOGIN_URL);
3321
+ url.searchParams.set("clientId", clientId);
3322
+ url.searchParams.set("origin", state);
3323
+ if (opts.forceLogin) url.searchParams.set("forceLogin", "true");
3286
3324
  return url.toString();
3287
3325
  }
3288
- function loginWithKingsChatPopup(opts) {
3289
- const scopes = opts.scopes ?? ["profile"];
3290
- const popup = window.open(
3291
- buildAuthUrl(opts.clientId, scopes),
3292
- "decane-kingschat",
3293
- "width=480,height=720,menubar=no,toolbar=no"
3326
+ function readKingsChatReturn(search) {
3327
+ const params = new URLSearchParams(search);
3328
+ const code = params.get("code");
3329
+ const origin = params.get("origin");
3330
+ if (!code || !origin) return null;
3331
+ const expected = storage2()?.getItem(STATE_KEY);
3332
+ if (!expected || expected !== origin) return null;
3333
+ return { code };
3334
+ }
3335
+ function clearKingsChatState() {
3336
+ storage2()?.removeItem(STATE_KEY);
3337
+ }
3338
+ async function loginWithKingsChatPopup(_opts) {
3339
+ throw new CallSequenceError(
3340
+ "loginWithKingsChatPopup was removed in 2.31.0: KingsChat retired its popup login. Use connectWithKingsChat(), which redirects to KingsChat and finishes on your registered Redirect URL."
3294
3341
  );
3295
- if (!popup) {
3296
- throw new CallSequenceError(
3297
- "The KingsChat sign-in popup was blocked. Call connectWithKingsChat() directly from a click handler."
3298
- );
3299
- }
3300
- return new Promise((resolve, reject) => {
3301
- let settled = false;
3302
- const finish = (fn) => {
3303
- if (settled) return;
3304
- settled = true;
3305
- window.removeEventListener("message", onMessage);
3306
- clearInterval(closedPoll);
3307
- clearTimeout(timeout);
3308
- try {
3309
- popup.close();
3310
- } catch {
3311
- }
3312
- fn();
3313
- };
3314
- const onMessage = (event) => {
3315
- if (event.origin !== KINGSCHAT_AUTH_ORIGIN) return;
3316
- const data = event.data;
3317
- if (data && typeof data === "object" && data.error) {
3318
- finish(() => reject(new CallSequenceError(`KingsChat sign-in failed: ${String(data.error)}`)));
3319
- return;
3320
- }
3321
- const tokens2 = readTokens(event.data);
3322
- if (tokens2) finish(() => resolve(tokens2));
3323
- };
3324
- window.addEventListener("message", onMessage);
3325
- const closedPoll = setInterval(() => {
3326
- if (popup.closed) finish(() => reject(new UserCancelledError()));
3327
- }, 400);
3328
- const timeout = setTimeout(
3329
- () => finish(
3330
- () => reject(new CallSequenceError("KingsChat sign-in timed out before returning a token."))
3331
- ),
3332
- opts.timeoutMs ?? 5 * 6e4
3333
- );
3334
- });
3335
3342
  }
3336
3343
 
3337
3344
  // src/social/signers/evm.ts
@@ -3936,7 +3943,7 @@ async function createDecaneConnect(config) {
3936
3943
  const teeUrl = config.teeUrl ?? DEFAULT_TEE_URL;
3937
3944
  const events = new EventEmitter();
3938
3945
  const api = new ApiClient(baseUrl, config.apiKey, config.appId);
3939
- const storage2 = new Storage();
3946
+ const storage3 = new Storage();
3940
3947
  const session = new SessionManager(
3941
3948
  events,
3942
3949
  durationMs,
@@ -4126,7 +4133,7 @@ async function createDecaneConnect(config) {
4126
4133
  }
4127
4134
  async function attachUnlockToken(userId, jwt, tokenKey) {
4128
4135
  try {
4129
- const stored = await storage2.getShare(userId);
4136
+ const stored = await storage3.getShare(userId);
4130
4137
  if (!stored) return;
4131
4138
  const { token } = await api.issueUnlockToken(jwt);
4132
4139
  const unlockTokenIv = generateRandomBytes(12);
@@ -4135,13 +4142,13 @@ async function createDecaneConnect(config) {
4135
4142
  new TextEncoder().encode(token),
4136
4143
  unlockTokenIv
4137
4144
  );
4138
- await storage2.putShare({ ...stored, wrappedUnlockToken, unlockTokenIv });
4145
+ await storage3.putShare({ ...stored, wrappedUnlockToken, unlockTokenIv });
4139
4146
  } catch (e) {
4140
4147
  console.warn("decane-connect-kit: could not store an unlock token for this device.", e);
4141
4148
  }
4142
4149
  }
4143
4150
  async function persistRotatedDeviceShare(userId, deviceShare) {
4144
- const stored = await storage2.getShare(userId);
4151
+ const stored = await storage3.getShare(userId);
4145
4152
  if (!stored) {
4146
4153
  throw new CallSequenceError(
4147
4154
  "Share set was rotated but this device has no stored share to update. Recover the wallet on this device before rotating."
@@ -4150,21 +4157,21 @@ async function createDecaneConnect(config) {
4150
4157
  const wrappingKey = await getWrappingKey(stored);
4151
4158
  const iv = generateRandomBytes(12);
4152
4159
  const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
4153
- await storage2.putShare({ ...stored, wrappedDeviceShare, iv });
4160
+ await storage3.putShare({ ...stored, wrappedDeviceShare, iv });
4154
4161
  }
4155
4162
  function hasPasswordUnlock(stored) {
4156
4163
  return !!stored && stored.unlockMethod === "password" && !!stored.passwordSalt && !!stored.wrappedUnlockToken && !!stored.unlockTokenIv;
4157
4164
  }
4158
4165
  async function passwordDeviceCandidate() {
4159
4166
  if (persisted?.userId) {
4160
- const stored = await storage2.getShare(persisted.userId);
4167
+ const stored = await storage3.getShare(persisted.userId);
4161
4168
  return hasPasswordUnlock(stored) ? stored : null;
4162
4169
  }
4163
4170
  if (lastUserId) {
4164
- const stored = await storage2.getShare(lastUserId);
4171
+ const stored = await storage3.getShare(lastUserId);
4165
4172
  return hasPasswordUnlock(stored) ? stored : null;
4166
4173
  }
4167
- const candidates = (await storage2.listShares()).filter(hasPasswordUnlock);
4174
+ const candidates = (await storage3.listShares()).filter(hasPasswordUnlock);
4168
4175
  return candidates.length === 1 ? candidates[0] : null;
4169
4176
  }
4170
4177
  async function getWrappingKey(stored) {
@@ -4192,7 +4199,7 @@ async function createDecaneConnect(config) {
4192
4199
  let credentialId;
4193
4200
  let prfOutput;
4194
4201
  try {
4195
- const label = (await storage2.getShare(userId))?.profile?.email;
4202
+ const label = (await storage3.getShare(userId))?.profile?.email;
4196
4203
  ({ credentialId, prfOutput } = await registerWebAuthnCredential({
4197
4204
  api,
4198
4205
  jwt,
@@ -4224,8 +4231,8 @@ async function createDecaneConnect(config) {
4224
4231
  const wrappingKey = await importAesKey(prfOutput);
4225
4232
  const iv = generateRandomBytes(12);
4226
4233
  const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
4227
- const existing = await storage2.getShare(userId);
4228
- await storage2.putShare({
4234
+ const existing = await storage3.getShare(userId);
4235
+ await storage3.putShare({
4229
4236
  ...existing,
4230
4237
  userId,
4231
4238
  wrappedDeviceShare,
@@ -4309,7 +4316,7 @@ async function createDecaneConnect(config) {
4309
4316
  const { wrappingKey, tokenKey, meta } = await passwordSetup();
4310
4317
  const iv = generateRandomBytes(12);
4311
4318
  const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
4312
- await storage2.putShare({
4319
+ await storage3.putShare({
4313
4320
  userId,
4314
4321
  wrappedDeviceShare,
4315
4322
  iv,
@@ -4383,7 +4390,7 @@ async function createDecaneConnect(config) {
4383
4390
  shareSet.device,
4384
4391
  iv
4385
4392
  );
4386
- await storage2.putShare({
4393
+ await storage3.putShare({
4387
4394
  userId,
4388
4395
  wrappedDeviceShare,
4389
4396
  iv,
@@ -4423,7 +4430,7 @@ async function createDecaneConnect(config) {
4423
4430
  zeroize(shareSet.server);
4424
4431
  zeroize(shareSet.device);
4425
4432
  zeroize(shareSet.recovery);
4426
- if (wrappingKey) await storage2.deleteShare(userId).catch(() => {
4433
+ if (wrappingKey) await storage3.deleteShare(userId).catch(() => {
4427
4434
  });
4428
4435
  events.emit("wallet-unlocking");
4429
4436
  return returningUserUnlock(userId, jwt);
@@ -4436,7 +4443,7 @@ async function createDecaneConnect(config) {
4436
4443
  } catch (e) {
4437
4444
  zeroize(shareSet.device);
4438
4445
  zeroize(shareSet.recovery);
4439
- if (wrappingKey) await storage2.deleteShare(userId).catch(() => {
4446
+ if (wrappingKey) await storage3.deleteShare(userId).catch(() => {
4440
4447
  });
4441
4448
  throw e;
4442
4449
  }
@@ -4512,7 +4519,7 @@ async function createDecaneConnect(config) {
4512
4519
  async function returningUserUnlock(userId, jwt) {
4513
4520
  const rawAddresses = await api.getAddresses(jwt);
4514
4521
  const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? { evm: rawAddresses.evm, solana: rawAddresses.solana, tron: rawAddresses.tron ?? evmToTronAddress2(rawAddresses.evm) } : null;
4515
- const stored = await storage2.getShare(userId);
4522
+ const stored = await storage3.getShare(userId);
4516
4523
  let deviceShare;
4517
4524
  let joinedViaPasskeySync = false;
4518
4525
  let migrating = false;
@@ -4564,7 +4571,7 @@ async function createDecaneConnect(config) {
4564
4571
  rpId,
4565
4572
  teeUrl,
4566
4573
  api,
4567
- storage: storage2,
4574
+ storage: storage3,
4568
4575
  onProgress: config.onRecoveryProgress,
4569
4576
  promptForRecoveryFile: config.promptForRecoveryFile,
4570
4577
  onNewRecoveryShare: async (recoveryShare, shareSetVersion) => {
@@ -4578,7 +4585,7 @@ async function createDecaneConnect(config) {
4578
4585
  });
4579
4586
  deviceShare = recovered.deviceShare;
4580
4587
  joinedViaPasskeySync = recovered.flow === "passkey-sync";
4581
- if (!identityTier && !await storage2.getShare(userId) && isPasskeySupported()) {
4588
+ if (!identityTier && !await storage3.getShare(userId) && isPasskeySupported()) {
4582
4589
  await registerPasskeyAndBackup(userId, jwt, deviceShare);
4583
4590
  }
4584
4591
  } else {
@@ -4607,7 +4614,7 @@ async function createDecaneConnect(config) {
4607
4614
  } catch (e) {
4608
4615
  if (e instanceof StaleDeviceShareError) {
4609
4616
  zeroize(deviceShare);
4610
- await storage2.deleteShare(userId);
4617
+ await storage3.deleteShare(userId);
4611
4618
  if (stored) return returningUserUnlock(userId, jwt);
4612
4619
  } else {
4613
4620
  zeroize(deviceShare);
@@ -4635,7 +4642,7 @@ async function createDecaneConnect(config) {
4635
4642
  const result = await tee.rotateShares(jwt);
4636
4643
  try {
4637
4644
  await persistRotatedDeviceShare(userId, result.deviceShare);
4638
- const refreshed = await storage2.getShare(userId);
4645
+ const refreshed = await storage3.getShare(userId);
4639
4646
  if (refreshed?.unlockMethod === "passkey" && refreshed.credentialId && refreshed.prfSalt) {
4640
4647
  await api.putRecoveryBackup(jwt, {
4641
4648
  wrappedDeviceShare: refreshed.wrappedDeviceShare,
@@ -4668,13 +4675,13 @@ async function createDecaneConnect(config) {
4668
4675
  }
4669
4676
  async function passkeyLoginCandidate() {
4670
4677
  if (!persisted?.userId || !isPasskeySupported()) return null;
4671
- return usablePasskeyShare(await storage2.getShare(persisted.userId));
4678
+ return usablePasskeyShare(await storage3.getShare(persisted.userId));
4672
4679
  }
4673
4680
  async function passkeyDeviceCandidate() {
4674
4681
  if (!isPasskeySupported()) return null;
4675
4682
  if (persisted?.userId) return passkeyLoginCandidate();
4676
- if (lastUserId) return usablePasskeyShare(await storage2.getShare(lastUserId));
4677
- const candidates = (await storage2.listShares()).map(usablePasskeyShare).filter(Boolean);
4683
+ if (lastUserId) return usablePasskeyShare(await storage3.getShare(lastUserId));
4684
+ const candidates = (await storage3.listShares()).map(usablePasskeyShare).filter(Boolean);
4678
4685
  return candidates.length === 1 ? candidates[0] : null;
4679
4686
  }
4680
4687
  async function canOfferPasskey() {
@@ -4682,15 +4689,15 @@ async function createDecaneConnect(config) {
4682
4689
  if (await passkeyDeviceCandidate()) return true;
4683
4690
  const known = persisted?.userId ?? lastUserId;
4684
4691
  if (!known) return false;
4685
- return await storage2.getShare(known) == null;
4692
+ return await storage3.getShare(known) == null;
4686
4693
  }
4687
4694
  async function rememberProfile(userId, profile) {
4688
4695
  const incoming = Object.fromEntries(Object.entries(profile).filter(([, v]) => v));
4689
4696
  if (!Object.keys(incoming).length) return;
4690
4697
  try {
4691
- const stored = await storage2.getShare(userId);
4698
+ const stored = await storage3.getShare(userId);
4692
4699
  if (!stored) return;
4693
- await storage2.putShare({ ...stored, profile: { ...stored.profile, ...incoming } });
4700
+ await storage3.putShare({ ...stored, profile: { ...stored.profile, ...incoming } });
4694
4701
  } catch {
4695
4702
  }
4696
4703
  }
@@ -4714,7 +4721,7 @@ async function createDecaneConnect(config) {
4714
4721
  savePersisted(PERSIST_KEY, persisted);
4715
4722
  events.emit("connected");
4716
4723
  persistLiveSession();
4717
- const stored = await storage2.getShare(userId);
4724
+ const stored = await storage3.getShare(userId);
4718
4725
  return {
4719
4726
  addresses,
4720
4727
  isNewUser: false,
@@ -4774,6 +4781,11 @@ async function createDecaneConnect(config) {
4774
4781
  return new Promise(() => {
4775
4782
  });
4776
4783
  }
4784
+ async function finishKingsChatReturn(code) {
4785
+ clearKingsChatState();
4786
+ window.history.replaceState({}, "", window.location.pathname);
4787
+ return handleAuthResult(await api.authKingsChatCode(code));
4788
+ }
4777
4789
  async function handleAuthResult(result) {
4778
4790
  let addresses;
4779
4791
  if (!result.hasShare) {
@@ -4836,6 +4848,8 @@ async function createDecaneConnect(config) {
4836
4848
  * this never starts a flow.
4837
4849
  */
4838
4850
  async completeRedirectSignIn() {
4851
+ const pending = readKingsChatReturn(window.location.search);
4852
+ if (pending) return finishKingsChatReturn(pending.code);
4839
4853
  const outcome = readRedirectOutcome(window.location.search);
4840
4854
  if (outcome.kind === "none") {
4841
4855
  throw new CallSequenceError("No sign-in result in the URL to complete");
@@ -4854,12 +4868,12 @@ async function createDecaneConnect(config) {
4854
4868
  return handleAuthResult(await api.authGoogleToken(idToken));
4855
4869
  },
4856
4870
  async connectWithKingsChat() {
4871
+ const pending = readKingsChatReturn(window.location.search);
4872
+ if (pending) return finishKingsChatReturn(pending.code);
4857
4873
  const { clientId } = await api.getKingsChatConfig();
4858
- const tokens2 = await loginWithKingsChatPopup({
4859
- clientId,
4860
- scopes: config.kingsChatScopes
4874
+ window.location.href = beginKingsChatLogin(clientId);
4875
+ return new Promise(() => {
4861
4876
  });
4862
- return client.connectWithKingsChatToken(tokens2.accessToken);
4863
4877
  },
4864
4878
  async connectWithKingsChatToken(accessToken) {
4865
4879
  if (!accessToken) {
@@ -4873,6 +4887,20 @@ async function createDecaneConnect(config) {
4873
4887
  const result = await api.authExternal(token, opts?.providerId);
4874
4888
  return handleAuthResult(result);
4875
4889
  },
4890
+ async linkWithToken(token, opts) {
4891
+ if (!token) throw new CallSequenceError("linkWithToken requires a token");
4892
+ if (!auth) throw new ReconnectRequiredError();
4893
+ const result = await api.linkExternal(auth.jwt, token, opts?.providerId);
4894
+ return result.id ? { alreadyLinked: result.alreadyLinked, id: result.id } : { alreadyLinked: result.alreadyLinked };
4895
+ },
4896
+ async listLinkedLogins() {
4897
+ if (!auth) throw new ReconnectRequiredError();
4898
+ return (await api.listLinkedLogins(auth.jwt)).logins;
4899
+ },
4900
+ async unlinkLogin(id) {
4901
+ if (!auth) throw new ReconnectRequiredError();
4902
+ await api.unlinkLogin(auth.jwt, id);
4903
+ },
4876
4904
  async connectWithAccessToken(token) {
4877
4905
  const claims = decodeAccessToken(token);
4878
4906
  if (!claims) throw new InvalidAccessTokenError("not a Decane access token");
@@ -4887,7 +4915,7 @@ async function createDecaneConnect(config) {
4887
4915
  events.emit("connected");
4888
4916
  persistLiveSession();
4889
4917
  scheduleAccessTokenExpiry(token, claims.expiresAt);
4890
- const stored = await storage2.getShare(claims.userId);
4918
+ const stored = await storage3.getShare(claims.userId);
4891
4919
  return {
4892
4920
  addresses,
4893
4921
  isNewUser: false,
@@ -4923,7 +4951,7 @@ async function createDecaneConnect(config) {
4923
4951
  const candidate = await passkeyDeviceCandidate();
4924
4952
  if (candidate) return passkeyLogin(candidate);
4925
4953
  const known = persisted?.userId ?? lastUserId;
4926
- if (isPasskeySupported() && known && await storage2.getShare(known) == null) {
4954
+ if (isPasskeySupported() && known && await storage3.getShare(known) == null) {
4927
4955
  return passkeyLoginWithoutShare(known);
4928
4956
  }
4929
4957
  throw new NewDeviceError();
@@ -4942,7 +4970,7 @@ async function createDecaneConnect(config) {
4942
4970
  async setUnlockPassword(password) {
4943
4971
  if (!auth) throw new ReconnectRequiredError();
4944
4972
  validateUnlockPassword(password);
4945
- const stored = await storage2.getShare(auth.userId);
4973
+ const stored = await storage3.getShare(auth.userId);
4946
4974
  if (!stored) throw new NewDeviceError();
4947
4975
  const deviceShare = await aesGcmDecrypt(
4948
4976
  await getWrappingKey(stored),
@@ -4961,7 +4989,7 @@ async function createDecaneConnect(config) {
4961
4989
  );
4962
4990
  const iv = generateRandomBytes(12);
4963
4991
  const wrappedDeviceShare = await aesGcmEncrypt(shareKey, deviceShare, iv);
4964
- await storage2.putShare({
4992
+ await storage3.putShare({
4965
4993
  ...stored,
4966
4994
  unlockMethod: "password",
4967
4995
  wrappedDeviceShare,
@@ -5041,9 +5069,9 @@ async function createDecaneConnect(config) {
5041
5069
  async removeUnlockPassword() {
5042
5070
  if (!auth) throw new ReconnectRequiredError();
5043
5071
  await api.revokeUnlockTokens(auth.jwt);
5044
- const stored = await storage2.getShare(auth.userId);
5072
+ const stored = await storage3.getShare(auth.userId);
5045
5073
  if (!stored || stored.unlockMethod !== "password") return;
5046
- await storage2.putShare({
5074
+ await storage3.putShare({
5047
5075
  ...stored,
5048
5076
  wrappedUnlockToken: void 0,
5049
5077
  unlockTokenIv: void 0
@@ -5051,7 +5079,7 @@ async function createDecaneConnect(config) {
5051
5079
  },
5052
5080
  async hasPasskey() {
5053
5081
  if (!auth) return false;
5054
- const stored = await storage2.getShare(auth.userId);
5082
+ const stored = await storage3.getShare(auth.userId);
5055
5083
  return stored?.unlockMethod === "passkey";
5056
5084
  },
5057
5085
  async addPasskey() {
@@ -5062,7 +5090,7 @@ async function createDecaneConnect(config) {
5062
5090
  }
5063
5091
  if (!auth) throw new ReconnectRequiredError();
5064
5092
  if (!isPasskeySupported()) throw new WebAuthnNotSupportedError();
5065
- const stored = await storage2.getShare(auth.userId);
5093
+ const stored = await storage3.getShare(auth.userId);
5066
5094
  if (stored?.unlockMethod === "passkey") return;
5067
5095
  if (!stored) {
5068
5096
  if (!serverAssistedRecovery) throw new NewDeviceError();
@@ -5095,12 +5123,12 @@ async function createDecaneConnect(config) {
5095
5123
  },
5096
5124
  async deviceProtected() {
5097
5125
  if (!auth) return false;
5098
- return await storage2.getShare(auth.userId) !== null;
5126
+ return await storage3.getShare(auth.userId) !== null;
5099
5127
  },
5100
5128
  async protectDevice() {
5101
5129
  if (identityTier) return false;
5102
5130
  if (!auth) throw new ReconnectRequiredError();
5103
- if (await storage2.getShare(auth.userId)) return true;
5131
+ if (await storage3.getShare(auth.userId)) return true;
5104
5132
  if (!serverAssistedRecovery) throw new NewDeviceError();
5105
5133
  const tee = new TeeClient(teeUrl);
5106
5134
  const provisioned = await tee.provisionDeviceShare(auth.jwt);
@@ -5110,7 +5138,7 @@ async function createDecaneConnect(config) {
5110
5138
  } finally {
5111
5139
  zeroize(provisioned);
5112
5140
  }
5113
- return await storage2.getShare(auth.userId) !== null;
5141
+ return await storage3.getShare(auth.userId) !== null;
5114
5142
  },
5115
5143
  async unlock() {
5116
5144
  if (!auth) {
@@ -6073,7 +6101,6 @@ function SignInView({
6073
6101
  await onKingsChatSignIn();
6074
6102
  } catch (e) {
6075
6103
  setError(e.message);
6076
- } finally {
6077
6104
  setKingsChatLoading(false);
6078
6105
  }
6079
6106
  }
@@ -6209,7 +6236,7 @@ function SignInView({
6209
6236
  value: phone,
6210
6237
  onChange: (e) => setPhone(e.target.value)
6211
6238
  }
6212
- ), /* @__PURE__ */ React2.createElement("button", { type: "submit", className: "dcm-submit", disabled: busy || !phone.trim() }, phoneLoading ? "Sending\u2026" : "Submit")), authMethods.includes("x") && onXSignIn && /* @__PURE__ */ React2.createElement("button", { type: "button", className: "dcm-method", onClick: handleX, disabled: busy }, /* @__PURE__ */ React2.createElement(XLogo, { size: 16 }), /* @__PURE__ */ React2.createElement("span", null, xLoading ? "Redirecting to X\u2026" : "X")), authMethods.includes("kingschat") && onKingsChatSignIn && /* @__PURE__ */ React2.createElement("button", { type: "button", className: "dcm-method", onClick: handleKingsChat, disabled: busy }, /* @__PURE__ */ React2.createElement(ChatIcon, { size: 18 }), /* @__PURE__ */ React2.createElement("span", null, kingsChatLoading ? "Waiting for KingsChat\u2026" : "KingsChat")), customAuthLabel && onCustomAuthSignIn && /* @__PURE__ */ React2.createElement("button", { type: "button", className: "dcm-method", onClick: handleCustomAuth, disabled: busy }, /* @__PURE__ */ React2.createElement(UserIcon, { size: 18 }), /* @__PURE__ */ React2.createElement("span", null, customLoading ? "Connecting\u2026" : customAuthLabel))), extra, errorLine, canUsePasskey && onPasskeySignIn && /* @__PURE__ */ React2.createElement("button", { type: "button", className: "dcm-link", onClick: () => {
6239
+ ), /* @__PURE__ */ React2.createElement("button", { type: "submit", className: "dcm-submit", disabled: busy || !phone.trim() }, phoneLoading ? "Sending\u2026" : "Submit")), authMethods.includes("x") && onXSignIn && /* @__PURE__ */ React2.createElement("button", { type: "button", className: "dcm-method", onClick: handleX, disabled: busy }, /* @__PURE__ */ React2.createElement(XLogo, { size: 16 }), /* @__PURE__ */ React2.createElement("span", null, xLoading ? "Redirecting to X\u2026" : "X")), authMethods.includes("kingschat") && onKingsChatSignIn && /* @__PURE__ */ React2.createElement("button", { type: "button", className: "dcm-method", onClick: handleKingsChat, disabled: busy }, /* @__PURE__ */ React2.createElement(ChatIcon, { size: 18 }), /* @__PURE__ */ React2.createElement("span", null, kingsChatLoading ? "Redirecting to KingsChat\u2026" : "KingsChat")), customAuthLabel && onCustomAuthSignIn && /* @__PURE__ */ React2.createElement("button", { type: "button", className: "dcm-method", onClick: handleCustomAuth, disabled: busy }, /* @__PURE__ */ React2.createElement(UserIcon, { size: 18 }), /* @__PURE__ */ React2.createElement("span", null, customLoading ? "Connecting\u2026" : customAuthLabel))), extra, errorLine, canUsePasskey && onPasskeySignIn && /* @__PURE__ */ React2.createElement("button", { type: "button", className: "dcm-link", onClick: () => {
6213
6240
  setError("");
6214
6241
  setShowOtherMethods(false);
6215
6242
  } }, "Use my passkey instead"), /* @__PURE__ */ React2.createElement(ProtectedBy, null));
@@ -6873,7 +6900,7 @@ function DecaneKit({ config, children }) {
6873
6900
  sdk.canSignInWithPasskey().then((can) => setSocialAuth((p) => ({ ...p, canUsePasskey: can }))).catch(() => {
6874
6901
  });
6875
6902
  const params = new URLSearchParams(window.location.search);
6876
- if (params.has("decane_jwt") || params.has("decane_error")) {
6903
+ if (params.has("decane_jwt") || params.has("decane_error") || readKingsChatReturn(window.location.search)) {
6877
6904
  sdk.completeRedirectSignIn().then(applyResult).catch((e) => {
6878
6905
  setSocialAuth((p) => p.phase ? { ...p, phase: null } : p);
6879
6906
  console.error(e);
@@ -6961,6 +6988,21 @@ function DecaneKit({ config, children }) {
6961
6988
  throw e;
6962
6989
  }
6963
6990
  }, [clearPhase]);
6991
+ const linkWithToken = useCallback3(async (token, opts) => {
6992
+ const sdk = socialRef.current;
6993
+ if (!sdk) throw new Error("Social sign-in not configured");
6994
+ return sdk.linkWithToken(token, opts);
6995
+ }, []);
6996
+ const listLinkedLogins = useCallback3(async () => {
6997
+ const sdk = socialRef.current;
6998
+ if (!sdk) throw new Error("Social sign-in not configured");
6999
+ return sdk.listLinkedLogins();
7000
+ }, []);
7001
+ const unlinkLogin = useCallback3(async (id) => {
7002
+ const sdk = socialRef.current;
7003
+ if (!sdk) throw new Error("Social sign-in not configured");
7004
+ await sdk.unlinkLogin(id);
7005
+ }, []);
6964
7006
  const connectWithToken = useCallback3(async (token, opts) => {
6965
7007
  const sdk = socialRef.current;
6966
7008
  if (!sdk) throw new Error("Social sign-in not configured");
@@ -7178,6 +7220,9 @@ function DecaneKit({ config, children }) {
7178
7220
  verifyPhoneCode: verifyPhoneCode2,
7179
7221
  connectWithToken,
7180
7222
  connectWithAccessToken,
7223
+ linkWithToken,
7224
+ listLinkedLogins,
7225
+ unlinkLogin,
7181
7226
  customAuthLabel: customAuthConfig?.label,
7182
7227
  connectWithCustomAuth: customAuthConfig ? connectWithCustomAuth : void 0,
7183
7228
  socialUnlock,
@@ -7294,6 +7339,9 @@ function useSocialAuth() {
7294
7339
  verifyPhoneCode: verifyPhoneCode2,
7295
7340
  connectWithToken,
7296
7341
  connectWithAccessToken,
7342
+ linkWithToken,
7343
+ listLinkedLogins,
7344
+ unlinkLogin,
7297
7345
  customAuthLabel,
7298
7346
  connectWithCustomAuth,
7299
7347
  signInWithPasskey: ctxSignInWithPasskey,
@@ -7497,6 +7545,11 @@ function useSocialAuth() {
7497
7545
  confirmPhoneCode,
7498
7546
  signInWithToken,
7499
7547
  signInWithAccessToken,
7548
+ // Linked logins: while signed in, attach a login from your own auth (a
7549
+ // phone number your backend verified, say) so it opens this same wallet.
7550
+ linkWithToken,
7551
+ listLinkedLogins,
7552
+ unlinkLogin,
7500
7553
  signInWithCustomAuth: connectWithCustomAuth ? signInWithCustomAuth : void 0,
7501
7554
  signInWithPasskey,
7502
7555
  unlock,
@@ -8050,6 +8103,7 @@ export {
8050
8103
  InvalidApiKeyError,
8051
8104
  InvalidPinError,
8052
8105
  InvalidRecoveryFileError,
8106
+ LoginInUseError,
8053
8107
  MIN_PASSWORD_LENGTH,
8054
8108
  NetworkError,
8055
8109
  NewDeviceError,