decane-connect-kit 2.18.0 → 2.20.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
@@ -2028,6 +2028,37 @@ var EventEmitter = class {
2028
2028
  }
2029
2029
  };
2030
2030
 
2031
+ // src/social/auth/redirectResult.ts
2032
+ function readRedirectOutcome(search) {
2033
+ const params = new URLSearchParams(search);
2034
+ const error = params.get("decane_error");
2035
+ if (error) return { kind: "error", error };
2036
+ const jwt = params.get("decane_jwt");
2037
+ if (!jwt) return { kind: "none" };
2038
+ const name = params.get("decane_name");
2039
+ const email = params.get("decane_email");
2040
+ const picture = params.get("decane_picture");
2041
+ const profile = {
2042
+ ...name ? { name } : {},
2043
+ ...email ? { email } : {},
2044
+ ...picture ? { picture } : {}
2045
+ };
2046
+ return {
2047
+ kind: "result",
2048
+ result: {
2049
+ jwt,
2050
+ userId: params.get("decane_user_id") ?? "",
2051
+ isNewUser: params.get("decane_is_new_user") === "true",
2052
+ hasShare: params.get("decane_has_share") === "true",
2053
+ // Omitted entirely when the provider released nothing, so callers can
2054
+ // test `result.profile` rather than an object that is always present but
2055
+ // sometimes empty. X never sends an email, so this is the normal case
2056
+ // there rather than an edge one.
2057
+ ...Object.keys(profile).length ? { profile } : {}
2058
+ }
2059
+ };
2060
+ }
2061
+
2031
2062
  // src/social/api.ts
2032
2063
  init_errors();
2033
2064
  var ERROR_MAP = {
@@ -2079,6 +2110,14 @@ var ApiClient = class {
2079
2110
  async getGoogleInitUrl() {
2080
2111
  return this._request("/auth/google/init", { method: "GET" });
2081
2112
  }
2113
+ /**
2114
+ * The X consent URL. Same contract as Google's: the backend holds the client
2115
+ * secret and the PKCE verifier, and answers 503 X_NOT_CONFIGURED when the
2116
+ * deployment has no X credentials.
2117
+ */
2118
+ async getXInitUrl() {
2119
+ return this._request("/auth/x/init", { method: "GET" });
2120
+ }
2082
2121
  /**
2083
2122
  * Exchange a Google ID token the app already holds for a Decane session.
2084
2123
  * Resolves to the SAME identity as the redirect flow, so one Google account
@@ -2167,8 +2206,14 @@ var ApiClient = class {
2167
2206
  return body.transactions;
2168
2207
  }
2169
2208
  // ── 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)}`, {
2209
+ /**
2210
+ * `label` is what the user's password manager will show for this passkey.
2211
+ * Without one they see the account's opaque id. Sent, used, and forgotten —
2212
+ * the backend echoes it into the options and stores nothing.
2213
+ */
2214
+ async getWebAuthnRegisterOptions(jwt, rpId, rpName, label) {
2215
+ const query = `rpId=${encodeURIComponent(rpId)}&rpName=${encodeURIComponent(rpName)}` + (label ? `&label=${encodeURIComponent(label)}` : "");
2216
+ return this._request(`/webauthn/register-options?${query}`, {
2172
2217
  headers: { Authorization: `Bearer ${jwt}` }
2173
2218
  });
2174
2219
  }
@@ -3052,7 +3097,12 @@ function bufFromBytes(u) {
3052
3097
  return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
3053
3098
  }
3054
3099
  async function registerWebAuthnCredential(opts) {
3055
- const baseOptions = await opts.api.getWebAuthnRegisterOptions(opts.jwt, opts.rpId, opts.rpName);
3100
+ const baseOptions = await opts.api.getWebAuthnRegisterOptions(
3101
+ opts.jwt,
3102
+ opts.rpId,
3103
+ opts.rpName,
3104
+ opts.label
3105
+ );
3056
3106
  if (opts.allowDuplicate) delete baseOptions.excludeCredentials;
3057
3107
  const optionsJSON = {
3058
3108
  ...baseOptions,
@@ -3858,12 +3908,14 @@ async function createDecaneConnect(config) {
3858
3908
  let credentialId;
3859
3909
  let prfOutput;
3860
3910
  try {
3911
+ const label = (await storage.getShare(userId))?.profile?.email;
3861
3912
  ({ credentialId, prfOutput } = await registerWebAuthnCredential({
3862
3913
  api,
3863
3914
  jwt,
3864
3915
  rpId,
3865
3916
  rpName,
3866
3917
  prfSalt,
3918
+ ...label ? { label } : {},
3867
3919
  ...opts.allowDuplicate ? { allowDuplicate: true } : {}
3868
3920
  }));
3869
3921
  } catch (e) {
@@ -4365,6 +4417,21 @@ async function createDecaneConnect(config) {
4365
4417
  ...stored.profile ? { profile: stored.profile } : {}
4366
4418
  };
4367
4419
  }
4420
+ async function connectViaRedirect(label, getInitUrl) {
4421
+ const outcome = readRedirectOutcome(window.location.search);
4422
+ if (outcome.kind === "error") {
4423
+ window.history.replaceState({}, "", window.location.pathname);
4424
+ throw new CallSequenceError(`${label} sign-in failed: ${outcome.error}`);
4425
+ }
4426
+ if (outcome.kind === "result") {
4427
+ window.history.replaceState({}, "", window.location.pathname);
4428
+ return handleAuthResult(outcome.result);
4429
+ }
4430
+ const { url } = await getInitUrl();
4431
+ window.location.href = url;
4432
+ return new Promise(() => {
4433
+ });
4434
+ }
4368
4435
  async function handleAuthResult(result) {
4369
4436
  let addresses;
4370
4437
  if (!result.hasShare) {
@@ -4411,39 +4478,33 @@ async function createDecaneConnect(config) {
4411
4478
  throw new CallSequenceError(`Unknown auth method: ${method}`);
4412
4479
  },
4413
4480
  async connectWithGoogle() {
4414
- const params = new URLSearchParams(window.location.search);
4415
- const decaneJwt = params.get("decane_jwt");
4416
- const decaneError = params.get("decane_error");
4417
- if (decaneError) {
4418
- window.history.replaceState({}, "", window.location.pathname);
4419
- throw new CallSequenceError(`Google sign-in failed: ${decaneError}`);
4420
- }
4421
- if (decaneJwt) {
4422
- const name = params.get("decane_name");
4423
- const email = params.get("decane_email");
4424
- const picture = params.get("decane_picture");
4425
- const profile = {
4426
- ...name ? { name } : {},
4427
- ...email ? { email } : {},
4428
- ...picture ? { picture } : {}
4429
- };
4430
- const result = {
4431
- jwt: decaneJwt,
4432
- userId: params.get("decane_user_id") ?? "",
4433
- isNewUser: params.get("decane_is_new_user") === "true",
4434
- hasShare: params.get("decane_has_share") === "true",
4435
- // Omitted entirely when Google released nothing, so callers can test
4436
- // `result.profile` rather than an object that is always present but
4437
- // sometimes empty.
4438
- ...Object.keys(profile).length ? { profile } : {}
4439
- };
4440
- window.history.replaceState({}, "", window.location.pathname);
4441
- return handleAuthResult(result);
4481
+ return connectViaRedirect("Google", () => api.getGoogleInitUrl());
4482
+ },
4483
+ /**
4484
+ * Finish a redirect sign-in that is already sitting in the URL, whichever
4485
+ * provider started it.
4486
+ *
4487
+ * Google and X bounce back with the same decane_* params, so the page that
4488
+ * receives them cannot tell which one ran — and should not have to. Calling
4489
+ * connectWithGoogle() here would work by accident and then report an X
4490
+ * failure as "Google sign-in failed".
4491
+ *
4492
+ * Throws if there is nothing to finish: unlike the connectWith* methods,
4493
+ * this never starts a flow.
4494
+ */
4495
+ async completeRedirectSignIn() {
4496
+ const outcome = readRedirectOutcome(window.location.search);
4497
+ if (outcome.kind === "none") {
4498
+ throw new CallSequenceError("No sign-in result in the URL to complete");
4442
4499
  }
4443
- const { url } = await api.getGoogleInitUrl();
4444
- window.location.href = url;
4445
- return new Promise(() => {
4446
- });
4500
+ window.history.replaceState({}, "", window.location.pathname);
4501
+ if (outcome.kind === "error") {
4502
+ throw new CallSequenceError(`Sign-in failed: ${outcome.error}`);
4503
+ }
4504
+ return handleAuthResult(outcome.result);
4505
+ },
4506
+ async connectWithX() {
4507
+ return connectViaRedirect("X", () => api.getXInitUrl());
4447
4508
  },
4448
4509
  async connectWithGoogleToken(idToken) {
4449
4510
  if (!idToken) throw new CallSequenceError("connectWithGoogleToken requires an ID token");
@@ -5267,6 +5328,20 @@ function ChainIconStack({ support, size = 16 }) {
5267
5328
  /* @__PURE__ */ import_react.default.createElement(ChainCircleIcon, { chain, size })
5268
5329
  )));
5269
5330
  }
5331
+ function XLogo({ size = 14 }) {
5332
+ return /* @__PURE__ */ import_react.default.createElement(
5333
+ "svg",
5334
+ {
5335
+ width: size,
5336
+ height: size,
5337
+ viewBox: "0 0 24 24",
5338
+ fill: "currentColor",
5339
+ "aria-hidden": "true",
5340
+ focusable: "false"
5341
+ },
5342
+ /* @__PURE__ */ import_react.default.createElement("path", { d: "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" })
5343
+ );
5344
+ }
5270
5345
 
5271
5346
  // src/ui/WalletSelector.tsx
5272
5347
  var FONT_URL = "https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap";
@@ -5408,6 +5483,7 @@ function WalletSelector({
5408
5483
  socialAuthMethods = ["google", "email"],
5409
5484
  onGoogleSignIn,
5410
5485
  onKingsChatSignIn,
5486
+ onXSignIn,
5411
5487
  onEmailStart,
5412
5488
  onEmailVerify,
5413
5489
  onPhoneStart,
@@ -5502,6 +5578,17 @@ function WalletSelector({
5502
5578
  setSocialLoad(false);
5503
5579
  }
5504
5580
  }
5581
+ async function handleXClick() {
5582
+ if (!onXSignIn) return;
5583
+ setSocialLoad(true);
5584
+ setSocialErr("");
5585
+ try {
5586
+ await onXSignIn();
5587
+ } catch (e) {
5588
+ setSocialErr(e.message);
5589
+ setSocialLoad(false);
5590
+ }
5591
+ }
5505
5592
  async function handleCustomAuthClick() {
5506
5593
  if (!onCustomAuthSignIn) return;
5507
5594
  setSocialLoad(true);
@@ -5983,6 +6070,38 @@ function WalletSelector({
5983
6070
  },
5984
6071
  /* @__PURE__ */ import_react2.default.createElement("svg", { width: "18", height: "18", viewBox: "0 0 18 18", fill: "none", "aria-hidden": "true" }, /* @__PURE__ */ import_react2.default.createElement("circle", { cx: "9", cy: "9", r: "9", fill: "var(--dc-accent)" }), /* @__PURE__ */ import_react2.default.createElement("path", { d: "M5.5 4.5h1.9v3.6l3.1-3.6h2.3l-3.3 3.8 3.5 5.2h-2.3l-2.5-3.8-.8.9v2.9H5.5V4.5z", fill: "var(--dc-accent-text)" })),
5985
6072
  socialLoad ? "Waiting for KingsChat\u2026" : "Continue with KingsChat"
6073
+ ), !passkeyFirst && socialAuthMethods.includes("x") && onXSignIn && /* @__PURE__ */ import_react2.default.createElement(
6074
+ "button",
6075
+ {
6076
+ onClick: handleXClick,
6077
+ disabled: socialLoad,
6078
+ style: {
6079
+ display: "flex",
6080
+ alignItems: "center",
6081
+ justifyContent: "center",
6082
+ gap: 10,
6083
+ width: "100%",
6084
+ padding: "11px 16px",
6085
+ background: "var(--dc-surface)",
6086
+ border: "1px solid var(--dc-border)",
6087
+ borderRadius: 10,
6088
+ cursor: socialLoad ? "not-allowed" : "pointer",
6089
+ fontSize: 14,
6090
+ fontWeight: 600,
6091
+ color: "var(--dc-text)",
6092
+ fontFamily: "inherit",
6093
+ transition: "border-color 0.12s, background 0.12s",
6094
+ opacity: socialLoad ? 0.6 : 1
6095
+ },
6096
+ onMouseEnter: (e) => {
6097
+ if (!socialLoad) e.currentTarget.style.borderColor = "var(--dc-border-accent)";
6098
+ },
6099
+ onMouseLeave: (e) => {
6100
+ e.currentTarget.style.borderColor = "var(--dc-border)";
6101
+ }
6102
+ },
6103
+ /* @__PURE__ */ import_react2.default.createElement(XLogo, { size: 16 }),
6104
+ socialLoad ? "Redirecting\u2026" : "Continue with X"
5986
6105
  ), !passkeyFirst && socialAuthMethods.includes("google") && socialAuthMethods.includes("email") && /* @__PURE__ */ import_react2.default.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ import_react2.default.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } }), /* @__PURE__ */ import_react2.default.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)", whiteSpace: "nowrap" } }, "or continue with email"), /* @__PURE__ */ import_react2.default.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } })), !passkeyFirst && socialAuthMethods.includes("email") && (emailStep === "input" ? /* @__PURE__ */ import_react2.default.createElement("form", { onSubmit: handleEmailStart, style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ import_react2.default.createElement(
5987
6106
  "input",
5988
6107
  {
@@ -6350,6 +6469,7 @@ function AddressRow({
6350
6469
  function SignInView({
6351
6470
  onGoogleSignIn,
6352
6471
  onKingsChatSignIn,
6472
+ onXSignIn,
6353
6473
  onEmailStart,
6354
6474
  onEmailVerify,
6355
6475
  onPhoneStart,
@@ -6364,6 +6484,7 @@ function SignInView({
6364
6484
  }) {
6365
6485
  const [googleLoading, setGoogleLoading] = (0, import_react3.useState)(false);
6366
6486
  const [kingsChatLoading, setKingsChatLoading] = (0, import_react3.useState)(false);
6487
+ const [xLoading, setXLoading] = (0, import_react3.useState)(false);
6367
6488
  const [email, setEmail] = (0, import_react3.useState)("");
6368
6489
  const [code, setCode] = (0, import_react3.useState)("");
6369
6490
  const [emailSent, setEmailSent] = (0, import_react3.useState)(false);
@@ -6414,6 +6535,17 @@ function SignInView({
6414
6535
  setKingsChatLoading(false);
6415
6536
  }
6416
6537
  }
6538
+ async function handleX() {
6539
+ if (!onXSignIn) return;
6540
+ setError("");
6541
+ setXLoading(true);
6542
+ try {
6543
+ await onXSignIn();
6544
+ } catch (e) {
6545
+ setError(e.message);
6546
+ setXLoading(false);
6547
+ }
6548
+ }
6417
6549
  async function handleEmailStart(e) {
6418
6550
  e.preventDefault();
6419
6551
  setError("");
@@ -6572,6 +6704,32 @@ function SignInView({
6572
6704
  }
6573
6705
  },
6574
6706
  kingsChatLoading ? "Waiting for KingsChat\u2026" : "Continue with KingsChat"
6707
+ ), !passkeyFirst && authMethods.includes("x") && onXSignIn && /* @__PURE__ */ import_react3.default.createElement(
6708
+ "button",
6709
+ {
6710
+ onClick: handleX,
6711
+ disabled: xLoading,
6712
+ style: {
6713
+ width: "100%",
6714
+ padding: "10px 0",
6715
+ marginBottom: 10,
6716
+ background: "var(--dc-surface)",
6717
+ color: "var(--dc-text)",
6718
+ border: "1px solid var(--dc-border)",
6719
+ borderRadius: 10,
6720
+ fontSize: 13,
6721
+ fontWeight: 700,
6722
+ cursor: xLoading ? "not-allowed" : "pointer",
6723
+ opacity: xLoading ? 0.7 : 1,
6724
+ fontFamily: "inherit",
6725
+ display: "flex",
6726
+ alignItems: "center",
6727
+ justifyContent: "center",
6728
+ gap: 8
6729
+ }
6730
+ },
6731
+ /* @__PURE__ */ import_react3.default.createElement(XLogo, null),
6732
+ xLoading ? "Redirecting\u2026" : "Continue with X"
6575
6733
  ), !passkeyFirst && authMethods.includes("email") && /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, authMethods.includes("google") && /* @__PURE__ */ import_react3.default.createElement("div", { style: { textAlign: "center", color: "var(--dc-text-muted)", fontSize: 12, margin: "10px 0" } }, "\u2014 or \u2014"), !emailSent ? /* @__PURE__ */ import_react3.default.createElement("form", { onSubmit: handleEmailStart, style: { display: "flex", gap: 8 } }, /* @__PURE__ */ import_react3.default.createElement(
6576
6734
  "input",
6577
6735
  {
@@ -7227,7 +7385,7 @@ function DecaneKit({ config, children }) {
7227
7385
  });
7228
7386
  const params = new URLSearchParams(window.location.search);
7229
7387
  if (params.has("decane_jwt") || params.has("decane_error")) {
7230
- sdk.connectWithGoogle().then(applyResult).catch((e) => {
7388
+ sdk.completeRedirectSignIn().then(applyResult).catch((e) => {
7231
7389
  setSocialAuth((p) => p.phase ? { ...p, phase: null } : p);
7232
7390
  console.error(e);
7233
7391
  });
@@ -7268,6 +7426,11 @@ function DecaneKit({ config, children }) {
7268
7426
  if (!sdk) throw new Error("Social sign-in not configured");
7269
7427
  await sdk.connectWithGoogle();
7270
7428
  }, []);
7429
+ const connectWithX = (0, import_react5.useCallback)(async () => {
7430
+ const sdk = socialRef.current;
7431
+ if (!sdk) throw new Error("Social sign-in not configured");
7432
+ await sdk.connectWithX();
7433
+ }, []);
7271
7434
  const connectWithKingsChat = (0, import_react5.useCallback)(async () => {
7272
7435
  const sdk = socialRef.current;
7273
7436
  if (!sdk) throw new Error("Social sign-in not configured");
@@ -7488,6 +7651,7 @@ function DecaneKit({ config, children }) {
7488
7651
  signInWithPasskey,
7489
7652
  connectWithGoogle,
7490
7653
  connectWithKingsChat,
7654
+ connectWithX,
7491
7655
  connectWithEmail,
7492
7656
  verifyEmailCode: verifyEmailCode2,
7493
7657
  connectWithPhone,
@@ -7539,6 +7703,7 @@ function DecaneKit({ config, children }) {
7539
7703
  socialAuthMethods: config?.social?.authMethods ?? ["google", "email"],
7540
7704
  onGoogleSignIn: connectWithGoogle,
7541
7705
  onKingsChatSignIn: connectWithKingsChat,
7706
+ onXSignIn: connectWithX,
7542
7707
  onEmailStart: connectWithEmail,
7543
7708
  onEmailVerify: verifyEmailCode2,
7544
7709
  onPhoneStart: connectWithPhone,
@@ -7563,6 +7728,7 @@ function DecaneKit({ config, children }) {
7563
7728
  onCustomAuthSignIn: customAuthConfig ? connectWithCustomAuth : void 0,
7564
7729
  onGoogleSignIn: connectWithGoogle,
7565
7730
  onKingsChatSignIn: connectWithKingsChat,
7731
+ onXSignIn: connectWithX,
7566
7732
  onEmailStart: connectWithEmail,
7567
7733
  onEmailVerify: verifyEmailCode2,
7568
7734
  onPhoneStart: connectWithPhone,
@@ -7597,6 +7763,7 @@ function useSocialAuth() {
7597
7763
  socialAuth,
7598
7764
  connectWithGoogle,
7599
7765
  connectWithKingsChat,
7766
+ connectWithX,
7600
7767
  connectWithEmail,
7601
7768
  verifyEmailCode: verifyEmailCode2,
7602
7769
  connectWithPhone,
@@ -7610,6 +7777,7 @@ function useSocialAuth() {
7610
7777
  } = useDecaneContext();
7611
7778
  const [googleLoading, setGoogleLoading] = (0, import_react6.useState)(false);
7612
7779
  const [kingschatLoading, setKingschatLoading] = (0, import_react6.useState)(false);
7780
+ const [xLoading, setXLoading] = (0, import_react6.useState)(false);
7613
7781
  const [emailLoading, setEmailLoading] = (0, import_react6.useState)(false);
7614
7782
  const [phoneLoading, setPhoneLoading] = (0, import_react6.useState)(false);
7615
7783
  const [tokenLoading, setTokenLoading] = (0, import_react6.useState)(false);
@@ -7625,6 +7793,16 @@ function useSocialAuth() {
7625
7793
  setGoogleLoading(false);
7626
7794
  }
7627
7795
  }, [connectWithGoogle]);
7796
+ const signInWithX = (0, import_react6.useCallback)(async () => {
7797
+ setXLoading(true);
7798
+ setError(null);
7799
+ try {
7800
+ await connectWithX();
7801
+ } catch (e) {
7802
+ setError(e.message);
7803
+ setXLoading(false);
7804
+ }
7805
+ }, [connectWithX]);
7628
7806
  const signInWithKingsChat = (0, import_react6.useCallback)(async () => {
7629
7807
  setKingschatLoading(true);
7630
7808
  setError(null);
@@ -7762,6 +7940,7 @@ function useSocialAuth() {
7762
7940
  needsReconnect: socialAuth.needsReconnect,
7763
7941
  googleLoading,
7764
7942
  kingschatLoading,
7943
+ xLoading,
7765
7944
  emailLoading,
7766
7945
  phoneLoading,
7767
7946
  tokenLoading,
@@ -7771,6 +7950,7 @@ function useSocialAuth() {
7771
7950
  clearError: () => setError(null),
7772
7951
  signInWithGoogle,
7773
7952
  signInWithKingsChat,
7953
+ signInWithX,
7774
7954
  sendEmailCode,
7775
7955
  confirmEmailCode,
7776
7956
  sendPhoneCode,