@agent-native/core 0.149.1 → 0.149.2

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.
Files changed (115) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +2 -0
  3. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +150 -46
  4. package/corpus/templates/analytics/app/global.css +1 -1
  5. package/corpus/templates/analytics/app/i18n-data.ts +37 -0
  6. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +1 -1
  7. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -0
  8. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +2 -0
  9. package/corpus/templates/analytics/changelog/2026-08-10-dashboard-tables-can-be-copied-and-sorted-by-multiple-column.md +6 -0
  10. package/corpus/templates/analytics/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  11. package/corpus/templates/assets/app/global.css +1 -1
  12. package/corpus/templates/assets/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  13. package/corpus/templates/brain/app/global.css +1 -1
  14. package/corpus/templates/brain/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  15. package/corpus/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  16. package/corpus/templates/clips/changelog/2026-08-10-clips-desktop-sign-in-now-starts-with-magic-links-and-google.md +6 -0
  17. package/corpus/templates/clips/chrome-extension/src/overlay.css +1 -0
  18. package/corpus/templates/clips/desktop/src/app.tsx +274 -137
  19. package/corpus/templates/clips/desktop/src/styles.css +35 -0
  20. package/corpus/templates/content/app/global.css +1 -0
  21. package/corpus/templates/crm/app/global.css +1 -1
  22. package/corpus/templates/crm/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  23. package/corpus/templates/dispatch/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  24. package/corpus/templates/factory/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  25. package/corpus/templates/forms/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  26. package/corpus/templates/plan/app/global.css +1 -0
  27. package/corpus/templates/plan/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  28. package/corpus/templates/plan/server/ui-plan-html.ts +4 -1
  29. package/corpus/templates/slides/AGENTS.md +11 -10
  30. package/corpus/templates/slides/actions/add-deck.ts +2 -0
  31. package/corpus/templates/slides/actions/add-slide.ts +15 -1
  32. package/corpus/templates/slides/actions/create-deck.ts +18 -6
  33. package/corpus/templates/slides/actions/get-deck.ts +29 -1
  34. package/corpus/templates/slides/actions/patch-deck.ts +122 -9
  35. package/corpus/templates/slides/actions/save-deck.ts +24 -1
  36. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +134 -618
  37. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +69 -60
  38. package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +1 -1
  39. package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +13 -1
  40. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +56 -174
  41. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +239 -116
  42. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +3 -3
  43. package/corpus/templates/slides/app/components/layout/AgentWorkIndicator.tsx +1 -1
  44. package/corpus/templates/slides/app/components/layout/Layout.tsx +34 -35
  45. package/corpus/templates/slides/app/components/layout/layout-route-policy.ts +2 -5
  46. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +45 -40
  47. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +63 -16
  48. package/corpus/templates/slides/app/hooks/use-generating-slide-preview.ts +25 -3
  49. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  50. package/corpus/templates/slides/app/lib/create-deck-generation.ts +5 -4
  51. package/corpus/templates/slides/app/lib/recent-references.ts +23 -1
  52. package/corpus/templates/slides/app/lib/slide-animation-elements.ts +41 -3
  53. package/corpus/templates/slides/app/pages/DeckEditor.tsx +4 -27
  54. package/corpus/templates/slides/app/pages/Index.tsx +240 -89
  55. package/corpus/templates/slides/changelog/2026-08-10-ai-editing-avatar-badge.md +6 -0
  56. package/corpus/templates/slides/changelog/2026-08-10-generated-decks-keep-a-clear-human-readable-title.md +6 -0
  57. package/corpus/templates/slides/changelog/2026-08-10-new-deck-creation-now-shows-progress-opens-the-editor-direct.md +6 -0
  58. package/corpus/templates/slides/changelog/2026-08-10-presentation-reveals-now-follow-valid-targets.md +6 -0
  59. package/corpus/templates/slides/changelog/2026-08-10-slides-editing-controls-are-consolidated-into-one-focused-me.md +6 -0
  60. package/corpus/templates/slides/changelog/2026-08-10-slides-now-verify-and-restyle-every-imported-slide-in-one-pa.md +6 -0
  61. package/corpus/templates/slides/changelog/2026-08-10-slides-only-suggests-a-google-connection-for-relevant-reques.md +6 -0
  62. package/corpus/templates/slides/changelog/2026-08-10-the-google-slides-import-option-is-now-labeled-slides.md +6 -0
  63. package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
  64. package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -3
  65. package/corpus/templates/slides/shared/api.ts +2 -1
  66. package/corpus/templates/slides/shared/deck-title.ts +135 -0
  67. package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +4 -1
  68. package/dist/agent/engine/ai-sdk-engine.js +1 -1
  69. package/dist/agent/production-agent.d.ts +21 -1
  70. package/dist/agent/production-agent.js +129 -10
  71. package/dist/agent/tool-approval-store.d.ts +18 -0
  72. package/dist/agent/tool-approval-store.js +122 -0
  73. package/dist/agent/types.d.ts +1 -1
  74. package/dist/cli/code-agent-executor.js +11 -2
  75. package/dist/client/AgentPanel.js +31 -6
  76. package/dist/client/AssistantChat.d.ts +3 -2
  77. package/dist/client/AssistantChat.js +7 -20
  78. package/dist/client/agent-chat/index.d.ts +1 -1
  79. package/dist/client/agent-chat-adapter.js +11 -0
  80. package/dist/client/agent-chat.d.ts +1 -1
  81. package/dist/client/chat/markdown-renderer.js +14 -2
  82. package/dist/client/chat/message-components.d.ts +6 -2
  83. package/dist/client/chat/message-components.js +25 -47
  84. package/dist/client/chat/tool-call-display.d.ts +3 -5
  85. package/dist/client/chat/tool-call-display.js +7 -212
  86. package/dist/client/chat-first/apps-rail.js +3 -3
  87. package/dist/client/chat-first/browser-pane.js +4 -1
  88. package/dist/client/chat-first/chat-history.d.ts +3 -1
  89. package/dist/client/chat-first/chat-history.js +2 -2
  90. package/dist/client/chat-first/surface-tabs.d.ts +1 -1
  91. package/dist/client/chat-first/surface-tabs.js +52 -41
  92. package/dist/client/chat-first/types.d.ts +3 -0
  93. package/dist/client/chat-first.d.ts +10 -0
  94. package/dist/client/chat-first.js +37 -2
  95. package/dist/client/components/ui/message-scroller.js +4 -1
  96. package/dist/client/i18n.js +3 -1
  97. package/dist/client/resources/McpConnectionSuggestion.js +9 -1
  98. package/dist/client/sse-event-processor.js +10 -0
  99. package/dist/client/use-chat-models.js +156 -80
  100. package/dist/collab/struct-routes.d.ts +1 -1
  101. package/dist/integrations/webhook-handler.js +1 -1
  102. package/dist/observability/routes.d.ts +1 -1
  103. package/dist/provider-api/actions/custom-provider-registration.d.ts +1 -1
  104. package/dist/resources/handlers.d.ts +1 -1
  105. package/dist/secrets/routes.d.ts +9 -9
  106. package/dist/server/auth.js +262 -31
  107. package/dist/server/onboarding-html.js +56 -9
  108. package/dist/shared/reasoning-effort.d.ts +8 -8
  109. package/dist/shared/reasoning-effort.js +10 -10
  110. package/dist/shared/streaming-text-smoothing.js +41 -11
  111. package/dist/styles/agent-native.css +19 -32
  112. package/dist/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  113. package/docs/content/template-chat.mdx +1 -1
  114. package/package.json +2 -2
  115. package/src/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
@@ -63,7 +63,7 @@ import { resolveAuthCookieNamespace } from "./cookie-namespace.js";
63
63
  import { getAllowedCorsOrigin, readCorsAllowedOrigins, } from "./cors-origins.js";
64
64
  import { readDesktopSso, writeDesktopSso, clearDesktopSso, } from "./desktop-sso.js";
65
65
  import { resolveGoogleSignInCredentials } from "./google-oauth-credentials.js";
66
- import { isElectron as isElectronRequest, getAppBasePath, getAppUrl, getOrigin, encodeOAuthState, decodeOAuthState, createOAuthSession, oauthCallbackResponse, oauthErrorPage, resolveOAuthRedirectUri, isAllowedOAuthRedirectUri, } from "./google-oauth.js";
66
+ import { isElectron as isElectronRequest, getAppBasePath, getAppUrl, getOrigin, encodeOAuthState, decodeOAuthState, createOAuthSession, oauthCallbackResponse, oauthDesktopExchangePage, oauthErrorPage, resolveOAuthRedirectUri, isAllowedOAuthRedirectUri, } from "./google-oauth.js";
67
67
  // Pure env-read feature switch from a leaf module (no dependency back on
68
68
  // auth.ts), so the guard and the SSO route handler share one validator and
69
69
  // can never disagree about whether federated SSO is enabled.
@@ -1025,6 +1025,8 @@ function areGenericGoogleOAuthRoutesEnabled(app) {
1025
1025
  }
1026
1026
  const _desktopExchanges = new Map();
1027
1027
  const DESKTOP_EXCHANGE_ERROR_PREFIX = "__error__::";
1028
+ const DESKTOP_MAGIC_LINK_CHALLENGE_PREFIX = "__magic-link-challenge__::";
1029
+ const DESKTOP_MAGIC_LINK_EXCHANGE_PREFIX = "__magic-link-exchange__::";
1028
1030
  const DESKTOP_AUTH_TOKEN_BODY_ORIGINS = new Set([
1029
1031
  "tauri://localhost",
1030
1032
  "http://tauri.localhost",
@@ -1033,6 +1035,90 @@ const DESKTOP_AUTH_TOKEN_BODY_ORIGINS = new Set([
1033
1035
  ]);
1034
1036
  // 5-minute TTL for exchange entries (short — single-use tokens).
1035
1037
  const DESKTOP_EXCHANGE_TTL_MS = 5 * 60 * 1000;
1038
+ function normalizeDesktopFlowId(value) {
1039
+ if (typeof value !== "string")
1040
+ return null;
1041
+ const flowId = value.trim();
1042
+ return /^[A-Za-z0-9_-]{1,128}$/.test(flowId) ? flowId : null;
1043
+ }
1044
+ function normalizeDesktopFlowVerifier(value) {
1045
+ if (typeof value !== "string")
1046
+ return null;
1047
+ const verifier = value.trim();
1048
+ return /^[A-Za-z0-9_-]{32,128}$/.test(verifier) ? verifier : null;
1049
+ }
1050
+ function desktopFlowVerifierHash(verifier) {
1051
+ return crypto.createHash("sha256").update(verifier).digest("base64url");
1052
+ }
1053
+ function matchesDesktopFlowVerifier(verifier, expectedHash) {
1054
+ const actual = Buffer.from(desktopFlowVerifierHash(verifier));
1055
+ const expected = Buffer.from(expectedHash);
1056
+ return (actual.length === expected.length &&
1057
+ crypto.timingSafeEqual(actual, expected));
1058
+ }
1059
+ function parseDesktopExchangeStoredEntry(packed) {
1060
+ if (packed.startsWith(DESKTOP_MAGIC_LINK_CHALLENGE_PREFIX)) {
1061
+ const verifierHash = packed.slice(DESKTOP_MAGIC_LINK_CHALLENGE_PREFIX.length);
1062
+ return verifierHash ? { challenge: true, verifierHash } : null;
1063
+ }
1064
+ if (packed.startsWith(DESKTOP_EXCHANGE_ERROR_PREFIX)) {
1065
+ const raw = packed.slice(DESKTOP_EXCHANGE_ERROR_PREFIX.length);
1066
+ return {
1067
+ error: JSON.parse(Buffer.from(raw, "base64url").toString()),
1068
+ };
1069
+ }
1070
+ const isMagicLinkExchange = packed.startsWith(DESKTOP_MAGIC_LINK_EXCHANGE_PREFIX);
1071
+ const encoded = isMagicLinkExchange
1072
+ ? packed.slice(DESKTOP_MAGIC_LINK_EXCHANGE_PREFIX.length)
1073
+ : packed;
1074
+ const verifierSeparator = isMagicLinkExchange ? encoded.indexOf("::") : -1;
1075
+ const verifierHash = verifierSeparator >= 0 ? encoded.slice(0, verifierSeparator) : undefined;
1076
+ const tokenAndEmail = verifierSeparator >= 0 ? encoded.slice(verifierSeparator + 2) : encoded;
1077
+ const sepIdx = tokenAndEmail.indexOf("::");
1078
+ if (sepIdx === -1)
1079
+ return null;
1080
+ return {
1081
+ token: tokenAndEmail.slice(0, sepIdx),
1082
+ email: tokenAndEmail.slice(sepIdx + 2),
1083
+ ...(verifierHash ? { verifierHash } : {}),
1084
+ };
1085
+ }
1086
+ function isDesktopMagicLinkCallbackPath(value) {
1087
+ try {
1088
+ return new URL(value, "http://agent-native.invalid").pathname.endsWith("/_agent-native/auth/magic-link/desktop-callback");
1089
+ }
1090
+ catch {
1091
+ // coercion-ok: malformed callback URLs are treated as non-desktop callbacks.
1092
+ return false;
1093
+ }
1094
+ }
1095
+ function withDesktopMagicLinkFlow(callbackURL, flow) {
1096
+ const url = new URL(callbackURL, "http://agent-native.invalid");
1097
+ url.searchParams.set("flow_id", flow.flowId);
1098
+ url.searchParams.set("verifier", flow.verifier);
1099
+ return `${url.pathname}${url.search}${url.hash}`;
1100
+ }
1101
+ async function persistDesktopMagicLinkChallenge(flowId, verifierHash) {
1102
+ await addSession(`dex:${flowId}`, `${DESKTOP_MAGIC_LINK_CHALLENGE_PREFIX}${verifierHash}`);
1103
+ }
1104
+ async function issueDesktopMagicLinkFlow() {
1105
+ const flowId = crypto.randomBytes(32).toString("base64url");
1106
+ const verifier = crypto.randomBytes(32).toString("base64url");
1107
+ const verifierHash = desktopFlowVerifierHash(verifier);
1108
+ _desktopExchanges.set(flowId, {
1109
+ challenge: true,
1110
+ verifierHash,
1111
+ expiresAt: Date.now() + DESKTOP_EXCHANGE_TTL_MS,
1112
+ });
1113
+ try {
1114
+ await persistDesktopMagicLinkChallenge(flowId, verifierHash);
1115
+ }
1116
+ catch (error) {
1117
+ _desktopExchanges.delete(flowId);
1118
+ throw error;
1119
+ }
1120
+ return { flowId, verifier };
1121
+ }
1036
1122
  export function setDesktopExchange(flowId, token, email) {
1037
1123
  _desktopExchanges.set(flowId, {
1038
1124
  token,
@@ -1059,9 +1145,12 @@ export function setDesktopExchangeError(flowId, error) {
1059
1145
  * Non-fatal — if the DB isn't ready yet the in-memory Map still works for
1060
1146
  * same-instance requests.
1061
1147
  */
1062
- async function persistDesktopExchangeToDB(flowId, token, email) {
1148
+ async function persistDesktopExchangeToDB(flowId, token, email, verifierHash) {
1063
1149
  try {
1064
- await addSession(`dex:${flowId}`, `${token}::${email}`);
1150
+ const packed = verifierHash
1151
+ ? `${DESKTOP_MAGIC_LINK_EXCHANGE_PREFIX}${verifierHash}::${token}::${email}`
1152
+ : `${token}::${email}`;
1153
+ await addSession(`dex:${flowId}`, packed);
1065
1154
  }
1066
1155
  catch {
1067
1156
  // non-fatal — in-memory Map is the primary path
@@ -1090,8 +1179,12 @@ async function consumeDesktopExchangeFromDB(flowId) {
1090
1179
  // redeemed with the session table's default 30-day TTL.
1091
1180
  const client = getDbExec();
1092
1181
  const { rows } = await client.execute({
1093
- sql: `DELETE FROM sessions WHERE token = ? AND created_at > ? RETURNING email`,
1094
- args: [`dex:${flowId}`, Date.now() - DESKTOP_EXCHANGE_TTL_MS],
1182
+ sql: `DELETE FROM sessions WHERE token = ? AND created_at > ? AND email NOT LIKE ? RETURNING email`,
1183
+ args: [
1184
+ `dex:${flowId}`,
1185
+ Date.now() - DESKTOP_EXCHANGE_TTL_MS,
1186
+ `${DESKTOP_MAGIC_LINK_CHALLENGE_PREFIX}%`,
1187
+ ],
1095
1188
  });
1096
1189
  forgetCachedSessionEmail(`dex:${flowId}`);
1097
1190
  if (rows.length === 0)
@@ -1099,21 +1192,72 @@ async function consumeDesktopExchangeFromDB(flowId) {
1099
1192
  const packed = (rows[0].email ?? rows[0][0]);
1100
1193
  if (!packed)
1101
1194
  return null;
1102
- if (packed.startsWith(DESKTOP_EXCHANGE_ERROR_PREFIX)) {
1103
- const raw = packed.slice(DESKTOP_EXCHANGE_ERROR_PREFIX.length);
1104
- return {
1105
- error: JSON.parse(Buffer.from(raw, "base64url").toString()),
1106
- };
1107
- }
1108
- const sepIdx = packed.indexOf("::");
1109
- if (sepIdx === -1)
1195
+ return parseDesktopExchangeStoredEntry(packed);
1196
+ }
1197
+ catch {
1198
+ // coercion-ok: a DB fallback outage leaves the exchange pending so polling can retry without consuming a token.
1199
+ return null;
1200
+ }
1201
+ }
1202
+ async function readDesktopMagicLinkChallengeFromDB(flowId) {
1203
+ try {
1204
+ const client = getDbExec();
1205
+ const { rows } = await client.execute({
1206
+ sql: `SELECT email FROM sessions WHERE token = ? AND created_at > ? LIMIT 1`,
1207
+ args: [`dex:${flowId}`, Date.now() - DESKTOP_EXCHANGE_TTL_MS],
1208
+ });
1209
+ if (rows.length === 0)
1210
+ return null;
1211
+ const packed = (rows[0].email ?? rows[0][0]);
1212
+ if (!packed)
1110
1213
  return null;
1111
- return { token: packed.slice(0, sepIdx), email: packed.slice(sepIdx + 2) };
1214
+ const entry = parseDesktopExchangeStoredEntry(packed);
1215
+ return entry && "challenge" in entry
1216
+ ? { verifierHash: entry.verifierHash }
1217
+ : null;
1112
1218
  }
1113
1219
  catch {
1114
1220
  return null;
1115
1221
  }
1116
1222
  }
1223
+ async function claimDesktopMagicLinkFlow(flowId, verifier, token, email) {
1224
+ const verifierHash = desktopFlowVerifierHash(verifier);
1225
+ const entry = _desktopExchanges.get(flowId);
1226
+ if (entry) {
1227
+ if (!("challenge" in entry) ||
1228
+ entry.expiresAt < Date.now() ||
1229
+ !matchesDesktopFlowVerifier(verifier, entry.verifierHash)) {
1230
+ return false;
1231
+ }
1232
+ _desktopExchanges.set(flowId, {
1233
+ token,
1234
+ email,
1235
+ verifierHash,
1236
+ expiresAt: Date.now() + DESKTOP_EXCHANGE_TTL_MS,
1237
+ });
1238
+ await persistDesktopExchangeToDB(flowId, token, email, verifierHash);
1239
+ return true;
1240
+ }
1241
+ try {
1242
+ const client = getDbExec();
1243
+ const packed = `${DESKTOP_MAGIC_LINK_EXCHANGE_PREFIX}${verifierHash}::${token}::${email}`;
1244
+ const { rows } = await client.execute({
1245
+ sql: `UPDATE sessions SET email = ?, created_at = ? WHERE token = ? AND created_at > ? AND email = ? RETURNING email`,
1246
+ args: [
1247
+ packed,
1248
+ Date.now(),
1249
+ `dex:${flowId}`,
1250
+ Date.now() - DESKTOP_EXCHANGE_TTL_MS,
1251
+ `${DESKTOP_MAGIC_LINK_CHALLENGE_PREFIX}${verifierHash}`,
1252
+ ],
1253
+ });
1254
+ return rows.length > 0;
1255
+ }
1256
+ catch {
1257
+ // coercion-ok: a failed DB claim must not issue a native session token.
1258
+ return false;
1259
+ }
1260
+ }
1117
1261
  setInterval(() => {
1118
1262
  const now = Date.now();
1119
1263
  for (const [k, v] of _desktopExchanges) {
@@ -2640,15 +2784,7 @@ async function mountBetterAuthRoutes(app, options) {
2640
2784
  emailDomain: email.split("@")[1] || "",
2641
2785
  });
2642
2786
  if (flowId && sessionToken) {
2643
- _desktopExchanges.set(flowId, {
2644
- token: sessionToken,
2645
- email,
2646
- expiresAt: Date.now() + DESKTOP_EXCHANGE_TTL_MS,
2647
- });
2648
- // Also persist to DB for cross-instance durability (Cloudflare
2649
- // Workers, multi-region). Fire-and-forget — in-memory Map is
2650
- // still the primary fast path for same-instance requests.
2651
- void persistDesktopExchangeToDB(flowId, sessionToken, email);
2787
+ setDesktopExchange(flowId, sessionToken, email);
2652
2788
  logGoogleOAuthDebug(event, "callback-exchange-stored", {
2653
2789
  flowId,
2654
2790
  desktop,
@@ -2687,13 +2823,23 @@ async function mountBetterAuthRoutes(app, options) {
2687
2823
  return { error: "Method not allowed" };
2688
2824
  }
2689
2825
  const query = getQuery(event);
2690
- const flowId = query.flow_id;
2826
+ const flowId = normalizeDesktopFlowId(query.flow_id);
2691
2827
  if (!flowId) {
2692
2828
  setResponseStatus(event, 400);
2693
2829
  return { error: "Missing flow_id" };
2694
2830
  }
2831
+ const verifier = normalizeDesktopFlowVerifier(getQuery(event).verifier);
2695
2832
  let entry = _desktopExchanges.get(flowId);
2696
2833
  if (!entry || entry.expiresAt < Date.now()) {
2834
+ const challenge = await readDesktopMagicLinkChallengeFromDB(flowId);
2835
+ if (challenge) {
2836
+ if (!verifier ||
2837
+ !matchesDesktopFlowVerifier(verifier, challenge.verifierHash)) {
2838
+ setResponseStatus(event, 403);
2839
+ return { error: "Invalid desktop exchange verifier." };
2840
+ }
2841
+ return { pending: true, flow: oauthDebugFlowId(flowId) };
2842
+ }
2697
2843
  // In-memory miss — fall back to the DB-persisted entry. This handles
2698
2844
  // cross-instance routing (Cloudflare Workers, multi-region) where the
2699
2845
  // OAuth callback and the polling request may hit different isolates.
@@ -2709,11 +2855,35 @@ async function mountBetterAuthRoutes(app, options) {
2709
2855
  entry =
2710
2856
  "error" in fromDb
2711
2857
  ? { error: fromDb.error, expiresAt: Date.now() + 1 }
2712
- : {
2713
- token: fromDb.token,
2714
- email: fromDb.email,
2715
- expiresAt: Date.now() + 1,
2716
- };
2858
+ : "challenge" in fromDb
2859
+ ? {
2860
+ challenge: true,
2861
+ verifierHash: fromDb.verifierHash,
2862
+ expiresAt: Date.now() + 1,
2863
+ }
2864
+ : {
2865
+ token: fromDb.token,
2866
+ email: fromDb.email,
2867
+ ...(fromDb.verifierHash
2868
+ ? { verifierHash: fromDb.verifierHash }
2869
+ : {}),
2870
+ expiresAt: Date.now() + 1,
2871
+ };
2872
+ }
2873
+ if ("challenge" in entry) {
2874
+ if (!verifier ||
2875
+ !matchesDesktopFlowVerifier(verifier, entry.verifierHash)) {
2876
+ setResponseStatus(event, 403);
2877
+ return { error: "Invalid desktop exchange verifier." };
2878
+ }
2879
+ return { pending: true, flow: oauthDebugFlowId(flowId) };
2880
+ }
2881
+ if ("token" in entry && entry.verifierHash) {
2882
+ if (!verifier ||
2883
+ !matchesDesktopFlowVerifier(verifier, entry.verifierHash)) {
2884
+ setResponseStatus(event, 403);
2885
+ return { error: "Invalid desktop exchange verifier." };
2886
+ }
2717
2887
  }
2718
2888
  _desktopExchanges.delete(flowId);
2719
2889
  // Also wipe the DB-persisted entry so it cannot be replayed via the
@@ -2735,6 +2905,13 @@ async function mountBetterAuthRoutes(app, options) {
2735
2905
  // still make a follow-up /auth/session?_session=... request, but the
2736
2906
  // OAuth handoff should not depend on that second request succeeding.
2737
2907
  setFrameworkSessionCookie(event, entry.token);
2908
+ if (isElectronRequest(event)) {
2909
+ await writeDesktopSso({
2910
+ email: entry.email,
2911
+ token: entry.token,
2912
+ expiresAt: Date.now() + sessionMaxAge * 1000,
2913
+ });
2914
+ }
2738
2915
  setResponseHeader(event, "Referrer-Policy", "no-referrer");
2739
2916
  logGoogleOAuthDebug(event, "exchange-success", {
2740
2917
  flowId,
@@ -2742,6 +2919,52 @@ async function mountBetterAuthRoutes(app, options) {
2742
2919
  });
2743
2920
  return { token: entry.token, email: entry.email };
2744
2921
  }));
2922
+ // Magic-link verification happens in the system browser, so native shells
2923
+ // need the verified browser session copied into the same one-time exchange
2924
+ // used by Google OAuth before they can establish their own WebView session.
2925
+ app.use("/_agent-native/auth/magic-link/desktop-callback", defineEventHandler(async (event) => {
2926
+ if (!isReadMethod(event)) {
2927
+ setResponseStatus(event, 405);
2928
+ return { error: "Method not allowed" };
2929
+ }
2930
+ const flowId = normalizeDesktopFlowId(getQuery(event).flow_id);
2931
+ const verifier = normalizeDesktopFlowVerifier(getQuery(event).verifier);
2932
+ if (!flowId) {
2933
+ setResponseStatus(event, 400);
2934
+ return { error: "Missing flow_id" };
2935
+ }
2936
+ if (!verifier) {
2937
+ return oauthErrorPage(AUTH_MAGIC_LINK_FALLBACK);
2938
+ }
2939
+ const session = await getSession(event);
2940
+ if (!session?.email || !session.token) {
2941
+ setDesktopExchangeError(flowId, {
2942
+ message: AUTH_MAGIC_LINK_FALLBACK,
2943
+ code: "callback_error",
2944
+ });
2945
+ return oauthErrorPage(AUTH_MAGIC_LINK_FALLBACK);
2946
+ }
2947
+ try {
2948
+ // Better Auth owns the browser cookie, while native clients resolve
2949
+ // the exchanged token through the framework session table.
2950
+ await addSession(session.token, session.email);
2951
+ }
2952
+ catch (error) {
2953
+ captureAuthError(error, {
2954
+ route: "magic-link",
2955
+ email: session.email,
2956
+ });
2957
+ setDesktopExchangeError(flowId, {
2958
+ message: AUTH_MAGIC_LINK_FALLBACK,
2959
+ code: "callback_error",
2960
+ });
2961
+ return oauthErrorPage(AUTH_MAGIC_LINK_FALLBACK);
2962
+ }
2963
+ if (!(await claimDesktopMagicLinkFlow(flowId, verifier, session.token, session.email))) {
2964
+ return oauthErrorPage(AUTH_MAGIC_LINK_FALLBACK);
2965
+ }
2966
+ return oauthDesktopExchangePage("Sign-in complete. You can return to the app.");
2967
+ }));
2745
2968
  // Initialize Better Auth. Forward `googleScopes` into the BetterAuthConfig
2746
2969
  // so the social provider requests the broader product scopes (Gmail,
2747
2970
  // Calendar, etc.) up front during the primary sign-in — eliminating the
@@ -3050,9 +3273,10 @@ async function mountBetterAuthRoutes(app, options) {
3050
3273
  const body = await readBody(event);
3051
3274
  const rawEmail = typeof body?.email === "string" ? body.email : "";
3052
3275
  const email = normalizeAuthEmail(rawEmail);
3053
- const callbackURL = typeof body?.callbackURL === "string"
3276
+ let callbackURL = typeof body?.callbackURL === "string"
3054
3277
  ? safeReturnPath(body.callbackURL)
3055
3278
  : "/";
3279
+ let desktopFlow;
3056
3280
  if (!email) {
3057
3281
  setResponseStatus(event, 400);
3058
3282
  return { error: VALID_AUTH_EMAIL_MESSAGE };
@@ -3062,6 +3286,10 @@ async function mountBetterAuthRoutes(app, options) {
3062
3286
  return { error: GOOGLE_AUTH_REQUIRED_MESSAGE };
3063
3287
  }
3064
3288
  try {
3289
+ if (isDesktopMagicLinkCallbackPath(callbackURL)) {
3290
+ desktopFlow = await issueDesktopMagicLinkFlow();
3291
+ callbackURL = withDesktopMagicLinkFlow(callbackURL, desktopFlow);
3292
+ }
3065
3293
  await auth.api.signInMagicLink({
3066
3294
  body: {
3067
3295
  email,
@@ -3070,7 +3298,10 @@ async function mountBetterAuthRoutes(app, options) {
3070
3298
  },
3071
3299
  headers: event.headers,
3072
3300
  });
3073
- return { ok: true };
3301
+ return {
3302
+ ok: true,
3303
+ ...(desktopFlow ?? {}),
3304
+ };
3074
3305
  }
3075
3306
  catch (e) {
3076
3307
  if (!isExpectedAuthFailure(e)) {
@@ -2965,6 +2965,7 @@ ${signInJourneyInlineScript()}
2965
2965
  }
2966
2966
  function __anFinishOAuthExchange(ret, flowId, sessionToken) {
2967
2967
  __anGoogleSignInInFlight = false;
2968
+ __anMagicLinkInFlight = false;
2968
2969
  if (__anIsBuilderPreview()) {
2969
2970
  if (sessionToken) {
2970
2971
  __anSetOAuthDebug('OAuth exchange redeemed; applying session bridge to embedded app', flowId);
@@ -3220,6 +3221,7 @@ ${identitySsoScript}
3220
3221
  var __anOAuthPollTimer = null;
3221
3222
  var __anOAuthPollCount = 0;
3222
3223
  var __anGoogleSignInInFlight = false;
3224
+ var __anMagicLinkInFlight = false;
3223
3225
  var __anGoogleRecoverBound = false;
3224
3226
  function __anNewOAuthFlowId() {
3225
3227
  try {
@@ -3253,15 +3255,26 @@ ${identitySsoScript}
3253
3255
  if (showDebugOverlay) debug.classList.add('show');
3254
3256
  }
3255
3257
  }
3256
- function __anShowOAuthError(err, btn, message) {
3258
+ function __anStopOAuthExchangePolling() {
3257
3259
  if (__anOAuthPollTimer) {
3258
3260
  clearInterval(__anOAuthPollTimer);
3259
3261
  __anOAuthPollTimer = null;
3260
3262
  }
3263
+ }
3264
+ function __anShowAuthExchangeError(err, btn, message, kind) {
3265
+ __anStopOAuthExchangePolling();
3261
3266
  err.textContent = message;
3262
- err.classList.add('show');
3267
+ err.classList.add('show', 'error');
3263
3268
  btn.disabled = false;
3264
- __anGoogleSignInInFlight = false;
3269
+ if (kind === 'magic-link') {
3270
+ __anMagicLinkInFlight = false;
3271
+ showMagicLinkForm();
3272
+ } else {
3273
+ __anGoogleSignInInFlight = false;
3274
+ }
3275
+ }
3276
+ function __anShowOAuthError(err, btn, message) {
3277
+ __anShowAuthExchangeError(err, btn, message, 'google');
3265
3278
  }
3266
3279
  function __anRecoverGoogleSignInAfterReturn() {
3267
3280
  // The user left for the Google sign-in window and came back. If the flow
@@ -3311,14 +3324,17 @@ ${identitySsoScript}
3311
3324
  __anShowOAuthError(err, btn, __anT('failedToConnect'));
3312
3325
  }
3313
3326
  }
3314
- function __anWaitForOAuthExchange(flowId, ret, btn, err) {
3327
+ function __anWaitForOAuthExchange(flowId, ret, btn, err, kind, verifier) {
3315
3328
  var started = Date.now();
3316
3329
  var timeoutMs = 5 * 60 * 1000;
3330
+ var isMagicLink = kind === 'magic-link';
3317
3331
  __anOAuthPollCount = 0;
3318
3332
  async function check() {
3319
3333
  __anOAuthPollCount++;
3320
3334
  try {
3321
- var res = await fetch(__anPath('/_agent-native/auth/desktop-exchange') + '?flow_id=' + encodeURIComponent(flowId), { credentials: 'include' });
3335
+ var exchangeParams = '?flow_id=' + encodeURIComponent(flowId);
3336
+ if (verifier) exchangeParams += '&verifier=' + encodeURIComponent(verifier);
3337
+ var res = await fetch(__anPath('/_agent-native/auth/desktop-exchange') + exchangeParams, { credentials: 'include' });
3322
3338
  var data = await res.json().catch(function() { return {}; });
3323
3339
  if (data && (data.email || data.token)) {
3324
3340
  if (__anOAuthPollTimer) clearInterval(__anOAuthPollTimer);
@@ -3328,7 +3344,12 @@ ${identitySsoScript}
3328
3344
  }
3329
3345
  if (data && data.error) {
3330
3346
  __anSetOAuthDebug('OAuth exchange returned an error: ' + (data.message || data.error), flowId);
3331
- __anShowOAuthError(err, btn, __anAuthErrorText(data, __anT('googleNotConfigured')));
3347
+ __anShowAuthExchangeError(
3348
+ err,
3349
+ btn,
3350
+ __anAuthErrorText(data, isMagicLink ? __anT('magicLinkFailed') : __anT('googleNotConfigured')),
3351
+ isMagicLink ? 'magic-link' : 'google',
3352
+ );
3332
3353
  return;
3333
3354
  }
3334
3355
  if (data && data.pending && (__anOAuthPollCount === 1 || __anOAuthPollCount % 5 === 0)) {
@@ -3340,10 +3361,17 @@ ${identitySsoScript}
3340
3361
  }
3341
3362
  }
3342
3363
  if (Date.now() - started > timeoutMs) {
3343
- __anShowOAuthError(err, btn, __anT('googleNeverFinished') + ' Flow ' + __anFlowDebugId(flowId) + '.');
3364
+ __anShowAuthExchangeError(
3365
+ err,
3366
+ btn,
3367
+ isMagicLink
3368
+ ? __anT('magicLinkFailed')
3369
+ : __anT('googleNeverFinished') + ' Flow ' + __anFlowDebugId(flowId) + '.',
3370
+ isMagicLink ? 'magic-link' : 'google',
3371
+ );
3344
3372
  }
3345
3373
  }
3346
- if (__anOAuthPollTimer) clearInterval(__anOAuthPollTimer);
3374
+ __anStopOAuthExchangePolling();
3347
3375
  __anOAuthPollTimer = setInterval(check, 1000);
3348
3376
  setTimeout(check, 500);
3349
3377
  }
@@ -3479,6 +3507,8 @@ ${googleOnly
3479
3507
  __anSetAuthView('magicLinkSent');
3480
3508
  }
3481
3509
  function showMagicLinkForm() {
3510
+ __anStopOAuthExchangePolling();
3511
+ __anMagicLinkInFlight = false;
3482
3512
  var form = document.getElementById('magic-link-form');
3483
3513
  if (!form) return;
3484
3514
  hideMagicLinkSuccess();
@@ -3851,6 +3881,7 @@ ${googleOnly
3851
3881
  var msg = document.getElementById('m-msg');
3852
3882
  var emailInput = document.getElementById('m-email');
3853
3883
  var email = __anNormalizeAuthEmail(emailInput && emailInput.value);
3884
+ var isDesktopMagicLink = __anIsAgentNativeDesktop();
3854
3885
  msg.classList.remove('show', 'error', 'success');
3855
3886
  if (!__anIsValidAuthEmail(email)) {
3856
3887
  __anShowEmailValidationError(emailInput, msg);
@@ -3858,12 +3889,18 @@ ${googleOnly
3858
3889
  }
3859
3890
  var originalLabel = btn.textContent;
3860
3891
  btn.disabled = true;
3892
+ __anMagicLinkInFlight = isDesktopMagicLink;
3861
3893
  btn.textContent = __anT('sending');
3862
3894
  try {
3863
3895
  var res = await fetch(__anPath('/_agent-native/auth/magic-link'), {
3864
3896
  method: 'POST',
3865
3897
  headers: { 'Content-Type': 'application/json' },
3866
- body: JSON.stringify({ email: email, callbackURL: __anResumeHref() }),
3898
+ body: JSON.stringify({
3899
+ email: email,
3900
+ callbackURL: isDesktopMagicLink
3901
+ ? __anPath('/_agent-native/auth/magic-link/desktop-callback')
3902
+ : __anResumeHref(),
3903
+ }),
3867
3904
  });
3868
3905
  var data = await res.json().catch(function(error) {
3869
3906
  console.warn('[auth] Could not parse magic-link response', error);
@@ -3873,13 +3910,23 @@ ${googleOnly
3873
3910
  btn.disabled = false;
3874
3911
  btn.textContent = originalLabel;
3875
3912
  showMagicLinkSuccess(email);
3913
+ if (isDesktopMagicLink) {
3914
+ var magicFlowId = data && typeof data.flowId === 'string' ? data.flowId : '';
3915
+ var magicVerifier = data && typeof data.verifier === 'string' ? data.verifier : '';
3916
+ if (!magicFlowId || !magicVerifier) {
3917
+ throw new Error('The sign-in flow was not initialized.');
3918
+ }
3919
+ __anWaitForOAuthExchange(magicFlowId, __anResumeHref(), btn, msg, 'magic-link', magicVerifier);
3920
+ }
3876
3921
  return;
3877
3922
  }
3923
+ __anMagicLinkInFlight = false;
3878
3924
  msg.textContent = __anAuthErrorText(data, __anT('magicLinkFailed'));
3879
3925
  msg.classList.add('show', 'error');
3880
3926
  btn.disabled = false;
3881
3927
  btn.textContent = originalLabel;
3882
3928
  } catch (err) {
3929
+ __anMagicLinkInFlight = false;
3883
3930
  msg.textContent = __anT('networkErrorDashRetry');
3884
3931
  msg.classList.add('show', 'error');
3885
3932
  btn.disabled = false;
@@ -1,9 +1,9 @@
1
1
  export declare const REASONING_EFFORTS: readonly ["auto", "none", "minimal", "low", "medium", "high", "xhigh", "max"];
2
2
  export type ReasoningEffort = (typeof REASONING_EFFORTS)[number];
3
3
  /**
4
- * Shared chat always chooses an explicit reasoning tier. Keep `auto` in the
4
+ * Shared chat always chooses an explicit effort tier. Keep `auto` in the
5
5
  * accepted type only so older persisted selections and external callers can
6
- * migrate cleanly; new UI and engine defaults resolve it to Medium.
6
+ * migrate cleanly; new UI and engine defaults resolve it to High.
7
7
  */
8
8
  export declare const DEFAULT_REASONING_EFFORT: ReasoningEffort;
9
9
  export declare const REASONING_EFFORT_LABELS: Record<ReasoningEffort, string>;
@@ -19,22 +19,22 @@ export declare function normalizeReasoningEffortForRequest(model: string | undef
19
19
  export declare function reasoningEffortLabel(effort: ReasoningEffort | undefined): string;
20
20
  /**
21
21
  * Resolve a user-facing selection for a model. Legacy `auto`, missing values,
22
- * and tiers unsupported by the newly selected model all become Medium.
23
- * Non-reasoning models still retain Medium in persisted chat state so moving
24
- * back to a reasoning model has a predictable default; their engines omit the
25
- * effort through `normalizeReasoningEffortForModel`.
22
+ * and tiers unsupported by the newly selected model all become High.
23
+ * Models without effort controls still retain High in persisted chat state so
24
+ * moving back to an effort-capable model has a predictable default; their
25
+ * engines omit the effort through `normalizeReasoningEffortForModel`.
26
26
  */
27
27
  export declare function resolveReasoningEffortSelection(model: string | undefined, effort: ReasoningEffort | undefined): ReasoningEffort;
28
28
  export declare function stepDownReasoningEffort(effort: ReasoningEffort | undefined): ReasoningEffort | undefined;
29
29
  export declare function isGPTReasoningModel(model: string): boolean;
30
30
  /**
31
31
  * Anthropic's adaptive-thinking API is only available on the newer Claude
32
- * model families. Claude Haiku 4.5 is reasoning-capable, but it still
32
+ * model families. Claude Haiku 4.5 supports effort, but it still
33
33
  * requires the legacy manual `budget_tokens` configuration.
34
34
  */
35
35
  export declare function supportsClaudeAdaptiveThinking(model: string | undefined): boolean;
36
36
  /**
37
- * Map the shared reasoning ladder to Anthropic's manual thinking budgets for
37
+ * Map the shared effort ladder to Anthropic's manual thinking budgets for
38
38
  * models that do not support adaptive thinking (currently Claude Haiku 4.5).
39
39
  */
40
40
  export declare function anthropicManualThinkingBudget(effort: ReasoningEffort): 1024 | 4096 | 8000 | 16000 | 32000;
@@ -9,11 +9,11 @@ export const REASONING_EFFORTS = [
9
9
  "max",
10
10
  ];
11
11
  /**
12
- * Shared chat always chooses an explicit reasoning tier. Keep `auto` in the
12
+ * Shared chat always chooses an explicit effort tier. Keep `auto` in the
13
13
  * accepted type only so older persisted selections and external callers can
14
- * migrate cleanly; new UI and engine defaults resolve it to Medium.
14
+ * migrate cleanly; new UI and engine defaults resolve it to High.
15
15
  */
16
- export const DEFAULT_REASONING_EFFORT = "medium";
16
+ export const DEFAULT_REASONING_EFFORT = "high";
17
17
  export const REASONING_EFFORT_LABELS = {
18
18
  auto: "Auto",
19
19
  none: "None",
@@ -91,10 +91,10 @@ export function reasoningEffortLabel(effort) {
91
91
  }
92
92
  /**
93
93
  * Resolve a user-facing selection for a model. Legacy `auto`, missing values,
94
- * and tiers unsupported by the newly selected model all become Medium.
95
- * Non-reasoning models still retain Medium in persisted chat state so moving
96
- * back to a reasoning model has a predictable default; their engines omit the
97
- * effort through `normalizeReasoningEffortForModel`.
94
+ * and tiers unsupported by the newly selected model all become High.
95
+ * Models without effort controls still retain High in persisted chat state so
96
+ * moving back to an effort-capable model has a predictable default; their
97
+ * engines omit the effort through `normalizeReasoningEffortForModel`.
98
98
  */
99
99
  export function resolveReasoningEffortSelection(model, effort) {
100
100
  const requested = !effort || effort === "auto" ? DEFAULT_REASONING_EFFORT : effort;
@@ -107,7 +107,7 @@ export function resolveReasoningEffortSelection(model, effort) {
107
107
  * One tier down from each effort, stopping at "minimal" — legacy `auto`,
108
108
  * "none", and "minimal" itself are left unchanged. Used by the
109
109
  * empty-final-response retry so a retried turn asks for meaningfully less
110
- * reasoning instead of repeating the exact request that came back empty.
110
+ * effort instead of repeating the exact request that came back empty.
111
111
  */
112
112
  const REASONING_EFFORT_STEP_DOWN = {
113
113
  max: "xhigh",
@@ -138,7 +138,7 @@ function isClaudeReasoningModel(model) {
138
138
  }
139
139
  /**
140
140
  * Anthropic's adaptive-thinking API is only available on the newer Claude
141
- * model families. Claude Haiku 4.5 is reasoning-capable, but it still
141
+ * model families. Claude Haiku 4.5 supports effort, but it still
142
142
  * requires the legacy manual `budget_tokens` configuration.
143
143
  */
144
144
  export function supportsClaudeAdaptiveThinking(model) {
@@ -153,7 +153,7 @@ export function supportsClaudeAdaptiveThinking(model) {
153
153
  return opusMatch ? parseInt(opusMatch[1], 10) >= 6 : false;
154
154
  }
155
155
  /**
156
- * Map the shared reasoning ladder to Anthropic's manual thinking budgets for
156
+ * Map the shared effort ladder to Anthropic's manual thinking budgets for
157
157
  * models that do not support adaptive thinking (currently Claude Haiku 4.5).
158
158
  */
159
159
  export function anthropicManualThinkingBudget(effort) {