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.cjs CHANGED
@@ -42,6 +42,7 @@ __export(errors_exports, {
42
42
  InvalidApiKeyError: () => InvalidApiKeyError,
43
43
  InvalidPinError: () => InvalidPinError,
44
44
  InvalidRecoveryFileError: () => InvalidRecoveryFileError,
45
+ LoginInUseError: () => LoginInUseError,
45
46
  NetworkError: () => NetworkError,
46
47
  NewDeviceError: () => NewDeviceError,
47
48
  OriginNotAllowedError: () => OriginNotAllowedError,
@@ -64,7 +65,7 @@ __export(errors_exports, {
64
65
  WrongPasswordError: () => WrongPasswordError,
65
66
  WrongUnlockPasswordError: () => WrongUnlockPasswordError
66
67
  });
67
- 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;
68
+ 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;
68
69
  var init_errors = __esm({
69
70
  "src/social/errors.ts"() {
70
71
  "use strict";
@@ -213,6 +214,11 @@ var init_errors = __esm({
213
214
  super("EXPORT_NOT_AUTHORIZED", "Export requires a fresh security check", true);
214
215
  }
215
216
  };
217
+ LoginInUseError = class extends DecaneConnectError {
218
+ constructor(message = "That login already opens another account. It was not linked.") {
219
+ super("LOGIN_IN_USE", message);
220
+ }
221
+ };
216
222
  WalletExistsError = class extends DecaneConnectError {
217
223
  constructor() {
218
224
  super("WALLET_EXISTS", "This account already has a wallet; opening it instead.", true);
@@ -266,6 +272,7 @@ __export(index_exports, {
266
272
  InvalidApiKeyError: () => InvalidApiKeyError,
267
273
  InvalidPinError: () => InvalidPinError,
268
274
  InvalidRecoveryFileError: () => InvalidRecoveryFileError,
275
+ LoginInUseError: () => LoginInUseError,
269
276
  MIN_PASSWORD_LENGTH: () => MIN_PASSWORD_LENGTH,
270
277
  NetworkError: () => NetworkError,
271
278
  NewDeviceError: () => NewDeviceError,
@@ -2118,7 +2125,9 @@ var ERROR_MAP = {
2118
2125
  // generic failure message.
2119
2126
  UNAUTHORIZED: ReconnectRequiredError,
2120
2127
  // A restorable wallet already exists: the client provisions instead of creating.
2121
- WALLET_EXISTS: WalletExistsError
2128
+ WALLET_EXISTS: WalletExistsError,
2129
+ // linkWithToken(): the login already opens another account.
2130
+ LOGIN_IN_USE: LoginInUseError
2122
2131
  };
2123
2132
  var REQUEST_TIMEOUT_MS = 3e4;
2124
2133
  var ApiClient = class {
@@ -2185,9 +2194,19 @@ var ApiClient = class {
2185
2194
  return this._request("/auth/kingschat/config", { method: "GET" });
2186
2195
  }
2187
2196
  /**
2188
- * Exchange a KingsChat access token for a Decane session. The backend spends
2189
- * the token against KingsChat's own API to establish the identity — it is
2190
- * never trusted on its face, and never stored.
2197
+ * Exchange a KingsChat authorization code for a Decane session. The backend
2198
+ * trades it with KingsChat for an access token, verifies that token (it must
2199
+ * have been issued to this project's KingsChat app), and never stores it.
2200
+ */
2201
+ async authKingsChatCode(code) {
2202
+ return this._request("/auth/kingschat/code", {
2203
+ method: "POST",
2204
+ body: JSON.stringify({ code })
2205
+ });
2206
+ }
2207
+ /**
2208
+ * Exchange a KingsChat access token the app already holds for a Decane
2209
+ * session. Verified the same way as a code's token, and never stored.
2191
2210
  */
2192
2211
  async authKingsChat(accessToken) {
2193
2212
  return this._request("/auth/kingschat", {
@@ -2287,6 +2306,23 @@ var ApiClient = class {
2287
2306
  body: JSON.stringify(providerId ? { token, providerId } : { token })
2288
2307
  });
2289
2308
  }
2309
+ /** Attach a login from the project's own provider to the signed-in account. */
2310
+ async linkExternal(jwt, token, providerId) {
2311
+ return this._request("/auth/link/external", {
2312
+ method: "POST",
2313
+ headers: { Authorization: `Bearer ${jwt}` },
2314
+ body: JSON.stringify(providerId ? { token, providerId } : { token })
2315
+ });
2316
+ }
2317
+ async listLinkedLogins(jwt) {
2318
+ return this._request("/auth/link/external", { method: "GET", headers: { Authorization: `Bearer ${jwt}` } });
2319
+ }
2320
+ async unlinkLogin(jwt, id) {
2321
+ await this._request(`/auth/link/external/${encodeURIComponent(id)}`, {
2322
+ method: "DELETE",
2323
+ headers: { Authorization: `Bearer ${jwt}` }
2324
+ });
2325
+ }
2290
2326
  /**
2291
2327
  * Returns null when this user has no server-registered passkey, which is the
2292
2328
  * expected answer for wallets created before passkey sign-in existed or for
@@ -3345,75 +3381,47 @@ async function verifyPhoneCode(api, phone, code) {
3345
3381
 
3346
3382
  // src/social/auth/kingschat.ts
3347
3383
  init_errors();
3348
- var KINGSCHAT_AUTH_ORIGIN = "https://accounts.kingsch.at";
3349
- function readTokens(data) {
3350
- if (!data || typeof data !== "object") return null;
3351
- const d = data;
3352
- const accessToken = d.accessToken ?? d.access_token;
3353
- if (typeof accessToken !== "string" || !accessToken) return null;
3354
- const refreshToken = d.refreshToken ?? d.refresh_token;
3355
- const expires = d.expiresInMillis ?? d.expires_in_millis;
3356
- return {
3357
- accessToken,
3358
- refreshToken: typeof refreshToken === "string" ? refreshToken : void 0,
3359
- expiresInMillis: typeof expires === "number" ? expires : void 0
3360
- };
3384
+ var KINGSCHAT_LOGIN_URL = "https://accounts.kingschat.online/log-in";
3385
+ var STATE_KEY = "decane:kingschat_state";
3386
+ function randomState() {
3387
+ const bytes = new Uint8Array(16);
3388
+ crypto.getRandomValues(bytes);
3389
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
3390
+ }
3391
+ function storage2() {
3392
+ try {
3393
+ return window.sessionStorage ?? null;
3394
+ } catch {
3395
+ return null;
3396
+ }
3361
3397
  }
3362
- function buildAuthUrl(clientId, scopes) {
3363
- const url = new URL(KINGSCHAT_AUTH_ORIGIN);
3364
- url.searchParams.set("client_id", clientId);
3365
- url.searchParams.set("scopes", JSON.stringify(scopes));
3366
- url.searchParams.set("redirect_uri", window.location.origin);
3367
- url.searchParams.set("post_message", "1");
3398
+ function beginKingsChatLogin(clientId, opts = {}) {
3399
+ const store = storage2();
3400
+ if (!store) throw new Error("KingsChat sign-in needs sessionStorage, which this browser is blocking");
3401
+ const state = randomState();
3402
+ store.setItem(STATE_KEY, state);
3403
+ const url = new URL(KINGSCHAT_LOGIN_URL);
3404
+ url.searchParams.set("clientId", clientId);
3405
+ url.searchParams.set("origin", state);
3406
+ if (opts.forceLogin) url.searchParams.set("forceLogin", "true");
3368
3407
  return url.toString();
3369
3408
  }
3370
- function loginWithKingsChatPopup(opts) {
3371
- const scopes = opts.scopes ?? ["profile"];
3372
- const popup = window.open(
3373
- buildAuthUrl(opts.clientId, scopes),
3374
- "decane-kingschat",
3375
- "width=480,height=720,menubar=no,toolbar=no"
3409
+ function readKingsChatReturn(search) {
3410
+ const params = new URLSearchParams(search);
3411
+ const code = params.get("code");
3412
+ const origin = params.get("origin");
3413
+ if (!code || !origin) return null;
3414
+ const expected = storage2()?.getItem(STATE_KEY);
3415
+ if (!expected || expected !== origin) return null;
3416
+ return { code };
3417
+ }
3418
+ function clearKingsChatState() {
3419
+ storage2()?.removeItem(STATE_KEY);
3420
+ }
3421
+ async function loginWithKingsChatPopup(_opts) {
3422
+ throw new CallSequenceError(
3423
+ "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."
3376
3424
  );
3377
- if (!popup) {
3378
- throw new CallSequenceError(
3379
- "The KingsChat sign-in popup was blocked. Call connectWithKingsChat() directly from a click handler."
3380
- );
3381
- }
3382
- return new Promise((resolve, reject) => {
3383
- let settled = false;
3384
- const finish = (fn) => {
3385
- if (settled) return;
3386
- settled = true;
3387
- window.removeEventListener("message", onMessage);
3388
- clearInterval(closedPoll);
3389
- clearTimeout(timeout);
3390
- try {
3391
- popup.close();
3392
- } catch {
3393
- }
3394
- fn();
3395
- };
3396
- const onMessage = (event) => {
3397
- if (event.origin !== KINGSCHAT_AUTH_ORIGIN) return;
3398
- const data = event.data;
3399
- if (data && typeof data === "object" && data.error) {
3400
- finish(() => reject(new CallSequenceError(`KingsChat sign-in failed: ${String(data.error)}`)));
3401
- return;
3402
- }
3403
- const tokens2 = readTokens(event.data);
3404
- if (tokens2) finish(() => resolve(tokens2));
3405
- };
3406
- window.addEventListener("message", onMessage);
3407
- const closedPoll = setInterval(() => {
3408
- if (popup.closed) finish(() => reject(new UserCancelledError()));
3409
- }, 400);
3410
- const timeout = setTimeout(
3411
- () => finish(
3412
- () => reject(new CallSequenceError("KingsChat sign-in timed out before returning a token."))
3413
- ),
3414
- opts.timeoutMs ?? 5 * 6e4
3415
- );
3416
- });
3417
3425
  }
3418
3426
 
3419
3427
  // src/social/signers/evm.ts
@@ -4018,7 +4026,7 @@ async function createDecaneConnect(config) {
4018
4026
  const teeUrl = config.teeUrl ?? DEFAULT_TEE_URL;
4019
4027
  const events = new EventEmitter();
4020
4028
  const api = new ApiClient(baseUrl, config.apiKey, config.appId);
4021
- const storage2 = new Storage();
4029
+ const storage3 = new Storage();
4022
4030
  const session = new SessionManager(
4023
4031
  events,
4024
4032
  durationMs,
@@ -4208,7 +4216,7 @@ async function createDecaneConnect(config) {
4208
4216
  }
4209
4217
  async function attachUnlockToken(userId, jwt, tokenKey) {
4210
4218
  try {
4211
- const stored = await storage2.getShare(userId);
4219
+ const stored = await storage3.getShare(userId);
4212
4220
  if (!stored) return;
4213
4221
  const { token } = await api.issueUnlockToken(jwt);
4214
4222
  const unlockTokenIv = generateRandomBytes(12);
@@ -4217,13 +4225,13 @@ async function createDecaneConnect(config) {
4217
4225
  new TextEncoder().encode(token),
4218
4226
  unlockTokenIv
4219
4227
  );
4220
- await storage2.putShare({ ...stored, wrappedUnlockToken, unlockTokenIv });
4228
+ await storage3.putShare({ ...stored, wrappedUnlockToken, unlockTokenIv });
4221
4229
  } catch (e) {
4222
4230
  console.warn("decane-connect-kit: could not store an unlock token for this device.", e);
4223
4231
  }
4224
4232
  }
4225
4233
  async function persistRotatedDeviceShare(userId, deviceShare) {
4226
- const stored = await storage2.getShare(userId);
4234
+ const stored = await storage3.getShare(userId);
4227
4235
  if (!stored) {
4228
4236
  throw new CallSequenceError(
4229
4237
  "Share set was rotated but this device has no stored share to update. Recover the wallet on this device before rotating."
@@ -4232,21 +4240,21 @@ async function createDecaneConnect(config) {
4232
4240
  const wrappingKey = await getWrappingKey(stored);
4233
4241
  const iv = generateRandomBytes(12);
4234
4242
  const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
4235
- await storage2.putShare({ ...stored, wrappedDeviceShare, iv });
4243
+ await storage3.putShare({ ...stored, wrappedDeviceShare, iv });
4236
4244
  }
4237
4245
  function hasPasswordUnlock(stored) {
4238
4246
  return !!stored && stored.unlockMethod === "password" && !!stored.passwordSalt && !!stored.wrappedUnlockToken && !!stored.unlockTokenIv;
4239
4247
  }
4240
4248
  async function passwordDeviceCandidate() {
4241
4249
  if (persisted?.userId) {
4242
- const stored = await storage2.getShare(persisted.userId);
4250
+ const stored = await storage3.getShare(persisted.userId);
4243
4251
  return hasPasswordUnlock(stored) ? stored : null;
4244
4252
  }
4245
4253
  if (lastUserId) {
4246
- const stored = await storage2.getShare(lastUserId);
4254
+ const stored = await storage3.getShare(lastUserId);
4247
4255
  return hasPasswordUnlock(stored) ? stored : null;
4248
4256
  }
4249
- const candidates = (await storage2.listShares()).filter(hasPasswordUnlock);
4257
+ const candidates = (await storage3.listShares()).filter(hasPasswordUnlock);
4250
4258
  return candidates.length === 1 ? candidates[0] : null;
4251
4259
  }
4252
4260
  async function getWrappingKey(stored) {
@@ -4274,7 +4282,7 @@ async function createDecaneConnect(config) {
4274
4282
  let credentialId;
4275
4283
  let prfOutput;
4276
4284
  try {
4277
- const label = (await storage2.getShare(userId))?.profile?.email;
4285
+ const label = (await storage3.getShare(userId))?.profile?.email;
4278
4286
  ({ credentialId, prfOutput } = await registerWebAuthnCredential({
4279
4287
  api,
4280
4288
  jwt,
@@ -4306,8 +4314,8 @@ async function createDecaneConnect(config) {
4306
4314
  const wrappingKey = await importAesKey(prfOutput);
4307
4315
  const iv = generateRandomBytes(12);
4308
4316
  const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
4309
- const existing = await storage2.getShare(userId);
4310
- await storage2.putShare({
4317
+ const existing = await storage3.getShare(userId);
4318
+ await storage3.putShare({
4311
4319
  ...existing,
4312
4320
  userId,
4313
4321
  wrappedDeviceShare,
@@ -4391,7 +4399,7 @@ async function createDecaneConnect(config) {
4391
4399
  const { wrappingKey, tokenKey, meta } = await passwordSetup();
4392
4400
  const iv = generateRandomBytes(12);
4393
4401
  const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
4394
- await storage2.putShare({
4402
+ await storage3.putShare({
4395
4403
  userId,
4396
4404
  wrappedDeviceShare,
4397
4405
  iv,
@@ -4465,7 +4473,7 @@ async function createDecaneConnect(config) {
4465
4473
  shareSet.device,
4466
4474
  iv
4467
4475
  );
4468
- await storage2.putShare({
4476
+ await storage3.putShare({
4469
4477
  userId,
4470
4478
  wrappedDeviceShare,
4471
4479
  iv,
@@ -4505,7 +4513,7 @@ async function createDecaneConnect(config) {
4505
4513
  zeroize(shareSet.server);
4506
4514
  zeroize(shareSet.device);
4507
4515
  zeroize(shareSet.recovery);
4508
- if (wrappingKey) await storage2.deleteShare(userId).catch(() => {
4516
+ if (wrappingKey) await storage3.deleteShare(userId).catch(() => {
4509
4517
  });
4510
4518
  events.emit("wallet-unlocking");
4511
4519
  return returningUserUnlock(userId, jwt);
@@ -4518,7 +4526,7 @@ async function createDecaneConnect(config) {
4518
4526
  } catch (e) {
4519
4527
  zeroize(shareSet.device);
4520
4528
  zeroize(shareSet.recovery);
4521
- if (wrappingKey) await storage2.deleteShare(userId).catch(() => {
4529
+ if (wrappingKey) await storage3.deleteShare(userId).catch(() => {
4522
4530
  });
4523
4531
  throw e;
4524
4532
  }
@@ -4594,7 +4602,7 @@ async function createDecaneConnect(config) {
4594
4602
  async function returningUserUnlock(userId, jwt) {
4595
4603
  const rawAddresses = await api.getAddresses(jwt);
4596
4604
  const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? { evm: rawAddresses.evm, solana: rawAddresses.solana, tron: rawAddresses.tron ?? evmToTronAddress2(rawAddresses.evm) } : null;
4597
- const stored = await storage2.getShare(userId);
4605
+ const stored = await storage3.getShare(userId);
4598
4606
  let deviceShare;
4599
4607
  let joinedViaPasskeySync = false;
4600
4608
  let migrating = false;
@@ -4646,7 +4654,7 @@ async function createDecaneConnect(config) {
4646
4654
  rpId,
4647
4655
  teeUrl,
4648
4656
  api,
4649
- storage: storage2,
4657
+ storage: storage3,
4650
4658
  onProgress: config.onRecoveryProgress,
4651
4659
  promptForRecoveryFile: config.promptForRecoveryFile,
4652
4660
  onNewRecoveryShare: async (recoveryShare, shareSetVersion) => {
@@ -4660,7 +4668,7 @@ async function createDecaneConnect(config) {
4660
4668
  });
4661
4669
  deviceShare = recovered.deviceShare;
4662
4670
  joinedViaPasskeySync = recovered.flow === "passkey-sync";
4663
- if (!identityTier && !await storage2.getShare(userId) && isPasskeySupported()) {
4671
+ if (!identityTier && !await storage3.getShare(userId) && isPasskeySupported()) {
4664
4672
  await registerPasskeyAndBackup(userId, jwt, deviceShare);
4665
4673
  }
4666
4674
  } else {
@@ -4689,7 +4697,7 @@ async function createDecaneConnect(config) {
4689
4697
  } catch (e) {
4690
4698
  if (e instanceof StaleDeviceShareError) {
4691
4699
  zeroize(deviceShare);
4692
- await storage2.deleteShare(userId);
4700
+ await storage3.deleteShare(userId);
4693
4701
  if (stored) return returningUserUnlock(userId, jwt);
4694
4702
  } else {
4695
4703
  zeroize(deviceShare);
@@ -4717,7 +4725,7 @@ async function createDecaneConnect(config) {
4717
4725
  const result = await tee.rotateShares(jwt);
4718
4726
  try {
4719
4727
  await persistRotatedDeviceShare(userId, result.deviceShare);
4720
- const refreshed = await storage2.getShare(userId);
4728
+ const refreshed = await storage3.getShare(userId);
4721
4729
  if (refreshed?.unlockMethod === "passkey" && refreshed.credentialId && refreshed.prfSalt) {
4722
4730
  await api.putRecoveryBackup(jwt, {
4723
4731
  wrappedDeviceShare: refreshed.wrappedDeviceShare,
@@ -4750,13 +4758,13 @@ async function createDecaneConnect(config) {
4750
4758
  }
4751
4759
  async function passkeyLoginCandidate() {
4752
4760
  if (!persisted?.userId || !isPasskeySupported()) return null;
4753
- return usablePasskeyShare(await storage2.getShare(persisted.userId));
4761
+ return usablePasskeyShare(await storage3.getShare(persisted.userId));
4754
4762
  }
4755
4763
  async function passkeyDeviceCandidate() {
4756
4764
  if (!isPasskeySupported()) return null;
4757
4765
  if (persisted?.userId) return passkeyLoginCandidate();
4758
- if (lastUserId) return usablePasskeyShare(await storage2.getShare(lastUserId));
4759
- const candidates = (await storage2.listShares()).map(usablePasskeyShare).filter(Boolean);
4766
+ if (lastUserId) return usablePasskeyShare(await storage3.getShare(lastUserId));
4767
+ const candidates = (await storage3.listShares()).map(usablePasskeyShare).filter(Boolean);
4760
4768
  return candidates.length === 1 ? candidates[0] : null;
4761
4769
  }
4762
4770
  async function canOfferPasskey() {
@@ -4764,15 +4772,15 @@ async function createDecaneConnect(config) {
4764
4772
  if (await passkeyDeviceCandidate()) return true;
4765
4773
  const known = persisted?.userId ?? lastUserId;
4766
4774
  if (!known) return false;
4767
- return await storage2.getShare(known) == null;
4775
+ return await storage3.getShare(known) == null;
4768
4776
  }
4769
4777
  async function rememberProfile(userId, profile) {
4770
4778
  const incoming = Object.fromEntries(Object.entries(profile).filter(([, v]) => v));
4771
4779
  if (!Object.keys(incoming).length) return;
4772
4780
  try {
4773
- const stored = await storage2.getShare(userId);
4781
+ const stored = await storage3.getShare(userId);
4774
4782
  if (!stored) return;
4775
- await storage2.putShare({ ...stored, profile: { ...stored.profile, ...incoming } });
4783
+ await storage3.putShare({ ...stored, profile: { ...stored.profile, ...incoming } });
4776
4784
  } catch {
4777
4785
  }
4778
4786
  }
@@ -4796,7 +4804,7 @@ async function createDecaneConnect(config) {
4796
4804
  savePersisted(PERSIST_KEY, persisted);
4797
4805
  events.emit("connected");
4798
4806
  persistLiveSession();
4799
- const stored = await storage2.getShare(userId);
4807
+ const stored = await storage3.getShare(userId);
4800
4808
  return {
4801
4809
  addresses,
4802
4810
  isNewUser: false,
@@ -4856,6 +4864,11 @@ async function createDecaneConnect(config) {
4856
4864
  return new Promise(() => {
4857
4865
  });
4858
4866
  }
4867
+ async function finishKingsChatReturn(code) {
4868
+ clearKingsChatState();
4869
+ window.history.replaceState({}, "", window.location.pathname);
4870
+ return handleAuthResult(await api.authKingsChatCode(code));
4871
+ }
4859
4872
  async function handleAuthResult(result) {
4860
4873
  let addresses;
4861
4874
  if (!result.hasShare) {
@@ -4918,6 +4931,8 @@ async function createDecaneConnect(config) {
4918
4931
  * this never starts a flow.
4919
4932
  */
4920
4933
  async completeRedirectSignIn() {
4934
+ const pending = readKingsChatReturn(window.location.search);
4935
+ if (pending) return finishKingsChatReturn(pending.code);
4921
4936
  const outcome = readRedirectOutcome(window.location.search);
4922
4937
  if (outcome.kind === "none") {
4923
4938
  throw new CallSequenceError("No sign-in result in the URL to complete");
@@ -4936,12 +4951,12 @@ async function createDecaneConnect(config) {
4936
4951
  return handleAuthResult(await api.authGoogleToken(idToken));
4937
4952
  },
4938
4953
  async connectWithKingsChat() {
4954
+ const pending = readKingsChatReturn(window.location.search);
4955
+ if (pending) return finishKingsChatReturn(pending.code);
4939
4956
  const { clientId } = await api.getKingsChatConfig();
4940
- const tokens2 = await loginWithKingsChatPopup({
4941
- clientId,
4942
- scopes: config.kingsChatScopes
4957
+ window.location.href = beginKingsChatLogin(clientId);
4958
+ return new Promise(() => {
4943
4959
  });
4944
- return client.connectWithKingsChatToken(tokens2.accessToken);
4945
4960
  },
4946
4961
  async connectWithKingsChatToken(accessToken) {
4947
4962
  if (!accessToken) {
@@ -4955,6 +4970,20 @@ async function createDecaneConnect(config) {
4955
4970
  const result = await api.authExternal(token, opts?.providerId);
4956
4971
  return handleAuthResult(result);
4957
4972
  },
4973
+ async linkWithToken(token, opts) {
4974
+ if (!token) throw new CallSequenceError("linkWithToken requires a token");
4975
+ if (!auth) throw new ReconnectRequiredError();
4976
+ const result = await api.linkExternal(auth.jwt, token, opts?.providerId);
4977
+ return result.id ? { alreadyLinked: result.alreadyLinked, id: result.id } : { alreadyLinked: result.alreadyLinked };
4978
+ },
4979
+ async listLinkedLogins() {
4980
+ if (!auth) throw new ReconnectRequiredError();
4981
+ return (await api.listLinkedLogins(auth.jwt)).logins;
4982
+ },
4983
+ async unlinkLogin(id) {
4984
+ if (!auth) throw new ReconnectRequiredError();
4985
+ await api.unlinkLogin(auth.jwt, id);
4986
+ },
4958
4987
  async connectWithAccessToken(token) {
4959
4988
  const claims = decodeAccessToken(token);
4960
4989
  if (!claims) throw new InvalidAccessTokenError("not a Decane access token");
@@ -4969,7 +4998,7 @@ async function createDecaneConnect(config) {
4969
4998
  events.emit("connected");
4970
4999
  persistLiveSession();
4971
5000
  scheduleAccessTokenExpiry(token, claims.expiresAt);
4972
- const stored = await storage2.getShare(claims.userId);
5001
+ const stored = await storage3.getShare(claims.userId);
4973
5002
  return {
4974
5003
  addresses,
4975
5004
  isNewUser: false,
@@ -5005,7 +5034,7 @@ async function createDecaneConnect(config) {
5005
5034
  const candidate = await passkeyDeviceCandidate();
5006
5035
  if (candidate) return passkeyLogin(candidate);
5007
5036
  const known = persisted?.userId ?? lastUserId;
5008
- if (isPasskeySupported() && known && await storage2.getShare(known) == null) {
5037
+ if (isPasskeySupported() && known && await storage3.getShare(known) == null) {
5009
5038
  return passkeyLoginWithoutShare(known);
5010
5039
  }
5011
5040
  throw new NewDeviceError();
@@ -5024,7 +5053,7 @@ async function createDecaneConnect(config) {
5024
5053
  async setUnlockPassword(password) {
5025
5054
  if (!auth) throw new ReconnectRequiredError();
5026
5055
  validateUnlockPassword(password);
5027
- const stored = await storage2.getShare(auth.userId);
5056
+ const stored = await storage3.getShare(auth.userId);
5028
5057
  if (!stored) throw new NewDeviceError();
5029
5058
  const deviceShare = await aesGcmDecrypt(
5030
5059
  await getWrappingKey(stored),
@@ -5043,7 +5072,7 @@ async function createDecaneConnect(config) {
5043
5072
  );
5044
5073
  const iv = generateRandomBytes(12);
5045
5074
  const wrappedDeviceShare = await aesGcmEncrypt(shareKey, deviceShare, iv);
5046
- await storage2.putShare({
5075
+ await storage3.putShare({
5047
5076
  ...stored,
5048
5077
  unlockMethod: "password",
5049
5078
  wrappedDeviceShare,
@@ -5123,9 +5152,9 @@ async function createDecaneConnect(config) {
5123
5152
  async removeUnlockPassword() {
5124
5153
  if (!auth) throw new ReconnectRequiredError();
5125
5154
  await api.revokeUnlockTokens(auth.jwt);
5126
- const stored = await storage2.getShare(auth.userId);
5155
+ const stored = await storage3.getShare(auth.userId);
5127
5156
  if (!stored || stored.unlockMethod !== "password") return;
5128
- await storage2.putShare({
5157
+ await storage3.putShare({
5129
5158
  ...stored,
5130
5159
  wrappedUnlockToken: void 0,
5131
5160
  unlockTokenIv: void 0
@@ -5133,7 +5162,7 @@ async function createDecaneConnect(config) {
5133
5162
  },
5134
5163
  async hasPasskey() {
5135
5164
  if (!auth) return false;
5136
- const stored = await storage2.getShare(auth.userId);
5165
+ const stored = await storage3.getShare(auth.userId);
5137
5166
  return stored?.unlockMethod === "passkey";
5138
5167
  },
5139
5168
  async addPasskey() {
@@ -5144,7 +5173,7 @@ async function createDecaneConnect(config) {
5144
5173
  }
5145
5174
  if (!auth) throw new ReconnectRequiredError();
5146
5175
  if (!isPasskeySupported()) throw new WebAuthnNotSupportedError();
5147
- const stored = await storage2.getShare(auth.userId);
5176
+ const stored = await storage3.getShare(auth.userId);
5148
5177
  if (stored?.unlockMethod === "passkey") return;
5149
5178
  if (!stored) {
5150
5179
  if (!serverAssistedRecovery) throw new NewDeviceError();
@@ -5177,12 +5206,12 @@ async function createDecaneConnect(config) {
5177
5206
  },
5178
5207
  async deviceProtected() {
5179
5208
  if (!auth) return false;
5180
- return await storage2.getShare(auth.userId) !== null;
5209
+ return await storage3.getShare(auth.userId) !== null;
5181
5210
  },
5182
5211
  async protectDevice() {
5183
5212
  if (identityTier) return false;
5184
5213
  if (!auth) throw new ReconnectRequiredError();
5185
- if (await storage2.getShare(auth.userId)) return true;
5214
+ if (await storage3.getShare(auth.userId)) return true;
5186
5215
  if (!serverAssistedRecovery) throw new NewDeviceError();
5187
5216
  const tee = new TeeClient(teeUrl);
5188
5217
  const provisioned = await tee.provisionDeviceShare(auth.jwt);
@@ -5192,7 +5221,7 @@ async function createDecaneConnect(config) {
5192
5221
  } finally {
5193
5222
  zeroize(provisioned);
5194
5223
  }
5195
- return await storage2.getShare(auth.userId) !== null;
5224
+ return await storage3.getShare(auth.userId) !== null;
5196
5225
  },
5197
5226
  async unlock() {
5198
5227
  if (!auth) {
@@ -6155,7 +6184,6 @@ function SignInView({
6155
6184
  await onKingsChatSignIn();
6156
6185
  } catch (e) {
6157
6186
  setError(e.message);
6158
- } finally {
6159
6187
  setKingsChatLoading(false);
6160
6188
  }
6161
6189
  }
@@ -6291,7 +6319,7 @@ function SignInView({
6291
6319
  value: phone,
6292
6320
  onChange: (e) => setPhone(e.target.value)
6293
6321
  }
6294
- ), /* @__PURE__ */ import_react2.default.createElement("button", { type: "submit", className: "dcm-submit", disabled: busy || !phone.trim() }, phoneLoading ? "Sending\u2026" : "Submit")), authMethods.includes("x") && onXSignIn && /* @__PURE__ */ import_react2.default.createElement("button", { type: "button", className: "dcm-method", onClick: handleX, disabled: busy }, /* @__PURE__ */ import_react2.default.createElement(XLogo, { size: 16 }), /* @__PURE__ */ import_react2.default.createElement("span", null, xLoading ? "Redirecting to X\u2026" : "X")), authMethods.includes("kingschat") && onKingsChatSignIn && /* @__PURE__ */ import_react2.default.createElement("button", { type: "button", className: "dcm-method", onClick: handleKingsChat, disabled: busy }, /* @__PURE__ */ import_react2.default.createElement(ChatIcon, { size: 18 }), /* @__PURE__ */ import_react2.default.createElement("span", null, kingsChatLoading ? "Waiting for KingsChat\u2026" : "KingsChat")), customAuthLabel && onCustomAuthSignIn && /* @__PURE__ */ import_react2.default.createElement("button", { type: "button", className: "dcm-method", onClick: handleCustomAuth, disabled: busy }, /* @__PURE__ */ import_react2.default.createElement(UserIcon, { size: 18 }), /* @__PURE__ */ import_react2.default.createElement("span", null, customLoading ? "Connecting\u2026" : customAuthLabel))), extra, errorLine, canUsePasskey && onPasskeySignIn && /* @__PURE__ */ import_react2.default.createElement("button", { type: "button", className: "dcm-link", onClick: () => {
6322
+ ), /* @__PURE__ */ import_react2.default.createElement("button", { type: "submit", className: "dcm-submit", disabled: busy || !phone.trim() }, phoneLoading ? "Sending\u2026" : "Submit")), authMethods.includes("x") && onXSignIn && /* @__PURE__ */ import_react2.default.createElement("button", { type: "button", className: "dcm-method", onClick: handleX, disabled: busy }, /* @__PURE__ */ import_react2.default.createElement(XLogo, { size: 16 }), /* @__PURE__ */ import_react2.default.createElement("span", null, xLoading ? "Redirecting to X\u2026" : "X")), authMethods.includes("kingschat") && onKingsChatSignIn && /* @__PURE__ */ import_react2.default.createElement("button", { type: "button", className: "dcm-method", onClick: handleKingsChat, disabled: busy }, /* @__PURE__ */ import_react2.default.createElement(ChatIcon, { size: 18 }), /* @__PURE__ */ import_react2.default.createElement("span", null, kingsChatLoading ? "Redirecting to KingsChat\u2026" : "KingsChat")), customAuthLabel && onCustomAuthSignIn && /* @__PURE__ */ import_react2.default.createElement("button", { type: "button", className: "dcm-method", onClick: handleCustomAuth, disabled: busy }, /* @__PURE__ */ import_react2.default.createElement(UserIcon, { size: 18 }), /* @__PURE__ */ import_react2.default.createElement("span", null, customLoading ? "Connecting\u2026" : customAuthLabel))), extra, errorLine, canUsePasskey && onPasskeySignIn && /* @__PURE__ */ import_react2.default.createElement("button", { type: "button", className: "dcm-link", onClick: () => {
6295
6323
  setError("");
6296
6324
  setShowOtherMethods(false);
6297
6325
  } }, "Use my passkey instead"), /* @__PURE__ */ import_react2.default.createElement(ProtectedBy, null));
@@ -6948,7 +6976,7 @@ function DecaneKit({ config, children }) {
6948
6976
  sdk.canSignInWithPasskey().then((can) => setSocialAuth((p) => ({ ...p, canUsePasskey: can }))).catch(() => {
6949
6977
  });
6950
6978
  const params = new URLSearchParams(window.location.search);
6951
- if (params.has("decane_jwt") || params.has("decane_error")) {
6979
+ if (params.has("decane_jwt") || params.has("decane_error") || readKingsChatReturn(window.location.search)) {
6952
6980
  sdk.completeRedirectSignIn().then(applyResult).catch((e) => {
6953
6981
  setSocialAuth((p) => p.phase ? { ...p, phase: null } : p);
6954
6982
  console.error(e);
@@ -7036,6 +7064,21 @@ function DecaneKit({ config, children }) {
7036
7064
  throw e;
7037
7065
  }
7038
7066
  }, [clearPhase]);
7067
+ const linkWithToken = (0, import_react5.useCallback)(async (token, opts) => {
7068
+ const sdk = socialRef.current;
7069
+ if (!sdk) throw new Error("Social sign-in not configured");
7070
+ return sdk.linkWithToken(token, opts);
7071
+ }, []);
7072
+ const listLinkedLogins = (0, import_react5.useCallback)(async () => {
7073
+ const sdk = socialRef.current;
7074
+ if (!sdk) throw new Error("Social sign-in not configured");
7075
+ return sdk.listLinkedLogins();
7076
+ }, []);
7077
+ const unlinkLogin = (0, import_react5.useCallback)(async (id) => {
7078
+ const sdk = socialRef.current;
7079
+ if (!sdk) throw new Error("Social sign-in not configured");
7080
+ await sdk.unlinkLogin(id);
7081
+ }, []);
7039
7082
  const connectWithToken = (0, import_react5.useCallback)(async (token, opts) => {
7040
7083
  const sdk = socialRef.current;
7041
7084
  if (!sdk) throw new Error("Social sign-in not configured");
@@ -7253,6 +7296,9 @@ function DecaneKit({ config, children }) {
7253
7296
  verifyPhoneCode: verifyPhoneCode2,
7254
7297
  connectWithToken,
7255
7298
  connectWithAccessToken,
7299
+ linkWithToken,
7300
+ listLinkedLogins,
7301
+ unlinkLogin,
7256
7302
  customAuthLabel: customAuthConfig?.label,
7257
7303
  connectWithCustomAuth: customAuthConfig ? connectWithCustomAuth : void 0,
7258
7304
  socialUnlock,
@@ -7369,6 +7415,9 @@ function useSocialAuth() {
7369
7415
  verifyPhoneCode: verifyPhoneCode2,
7370
7416
  connectWithToken,
7371
7417
  connectWithAccessToken,
7418
+ linkWithToken,
7419
+ listLinkedLogins,
7420
+ unlinkLogin,
7372
7421
  customAuthLabel,
7373
7422
  connectWithCustomAuth,
7374
7423
  signInWithPasskey: ctxSignInWithPasskey,
@@ -7572,6 +7621,11 @@ function useSocialAuth() {
7572
7621
  confirmPhoneCode,
7573
7622
  signInWithToken,
7574
7623
  signInWithAccessToken,
7624
+ // Linked logins: while signed in, attach a login from your own auth (a
7625
+ // phone number your backend verified, say) so it opens this same wallet.
7626
+ linkWithToken,
7627
+ listLinkedLogins,
7628
+ unlinkLogin,
7575
7629
  signInWithCustomAuth: connectWithCustomAuth ? signInWithCustomAuth : void 0,
7576
7630
  signInWithPasskey,
7577
7631
  unlock,
@@ -8126,6 +8180,7 @@ function useSignTypedData() {
8126
8180
  InvalidApiKeyError,
8127
8181
  InvalidPinError,
8128
8182
  InvalidRecoveryFileError,
8183
+ LoginInUseError,
8129
8184
  MIN_PASSWORD_LENGTH,
8130
8185
  NetworkError,
8131
8186
  NewDeviceError,