@agent-native/core 0.18.1 → 0.19.1

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 (168) hide show
  1. package/README.md +1 -11
  2. package/dist/a2a/caller-auth.d.ts +1 -0
  3. package/dist/a2a/caller-auth.d.ts.map +1 -1
  4. package/dist/a2a/caller-auth.js +1 -1
  5. package/dist/a2a/caller-auth.js.map +1 -1
  6. package/dist/a2a/client.d.ts +7 -0
  7. package/dist/a2a/client.d.ts.map +1 -1
  8. package/dist/a2a/client.js +3 -0
  9. package/dist/a2a/client.js.map +1 -1
  10. package/dist/agent/production-agent.d.ts +1 -1
  11. package/dist/agent/production-agent.d.ts.map +1 -1
  12. package/dist/agent/production-agent.js +34 -2
  13. package/dist/agent/production-agent.js.map +1 -1
  14. package/dist/cli/code-agent-executor.d.ts.map +1 -1
  15. package/dist/cli/code-agent-executor.js +47 -256
  16. package/dist/cli/code-agent-executor.js.map +1 -1
  17. package/dist/cli/connect.d.ts +94 -0
  18. package/dist/cli/connect.d.ts.map +1 -0
  19. package/dist/cli/connect.js +443 -0
  20. package/dist/cli/connect.js.map +1 -0
  21. package/dist/cli/index.js +16 -0
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/cli/mcp-config-writers.d.ts +71 -0
  24. package/dist/cli/mcp-config-writers.d.ts.map +1 -0
  25. package/dist/cli/mcp-config-writers.js +210 -0
  26. package/dist/cli/mcp-config-writers.js.map +1 -0
  27. package/dist/client/AgentPanel.d.ts +3 -1
  28. package/dist/client/AgentPanel.d.ts.map +1 -1
  29. package/dist/client/AgentPanel.js +4 -4
  30. package/dist/client/AgentPanel.js.map +1 -1
  31. package/dist/client/AssistantChat.d.ts +3 -0
  32. package/dist/client/AssistantChat.d.ts.map +1 -1
  33. package/dist/client/AssistantChat.js +22 -66
  34. package/dist/client/AssistantChat.js.map +1 -1
  35. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  36. package/dist/client/MultiTabAssistantChat.js +4 -1
  37. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  38. package/dist/client/composer/PromptComposer.d.ts +6 -1
  39. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  40. package/dist/client/composer/PromptComposer.js +5 -4
  41. package/dist/client/composer/PromptComposer.js.map +1 -1
  42. package/dist/client/composer/TiptapComposer.d.ts +6 -1
  43. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  44. package/dist/client/composer/TiptapComposer.js +20 -10
  45. package/dist/client/composer/TiptapComposer.js.map +1 -1
  46. package/dist/client/conversation/AgentConversation.d.ts +18 -0
  47. package/dist/client/conversation/AgentConversation.d.ts.map +1 -0
  48. package/dist/client/conversation/AgentConversation.js +94 -0
  49. package/dist/client/conversation/AgentConversation.js.map +1 -0
  50. package/dist/client/conversation/AgentConversation.spec.d.ts +2 -0
  51. package/dist/client/conversation/AgentConversation.spec.d.ts.map +1 -0
  52. package/dist/client/conversation/AgentConversation.spec.js +69 -0
  53. package/dist/client/conversation/AgentConversation.spec.js.map +1 -0
  54. package/dist/client/conversation/index.d.ts +4 -0
  55. package/dist/client/conversation/index.d.ts.map +1 -0
  56. package/dist/client/conversation/index.js +3 -0
  57. package/dist/client/conversation/index.js.map +1 -0
  58. package/dist/client/conversation/types.d.ts +54 -0
  59. package/dist/client/conversation/types.d.ts.map +1 -0
  60. package/dist/client/conversation/types.js +2 -0
  61. package/dist/client/conversation/types.js.map +1 -0
  62. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts +15 -0
  63. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts.map +1 -0
  64. package/dist/client/conversation/use-near-bottom-autoscroll.js +66 -0
  65. package/dist/client/conversation/use-near-bottom-autoscroll.js.map +1 -0
  66. package/dist/client/dynamic-suggestions.d.ts +43 -0
  67. package/dist/client/dynamic-suggestions.d.ts.map +1 -0
  68. package/dist/client/dynamic-suggestions.js +344 -0
  69. package/dist/client/dynamic-suggestions.js.map +1 -0
  70. package/dist/client/index.d.ts +2 -0
  71. package/dist/client/index.d.ts.map +1 -1
  72. package/dist/client/index.js +2 -0
  73. package/dist/client/index.js.map +1 -1
  74. package/dist/client/resources/ResourceTree.d.ts.map +1 -1
  75. package/dist/client/resources/ResourceTree.js +2 -2
  76. package/dist/client/resources/ResourceTree.js.map +1 -1
  77. package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
  78. package/dist/client/resources/ResourcesPanel.js +4 -28
  79. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  80. package/dist/client/settings/SettingsPanel.js +2 -2
  81. package/dist/client/settings/SettingsPanel.js.map +1 -1
  82. package/dist/code-agents/index.d.ts +1 -0
  83. package/dist/code-agents/index.d.ts.map +1 -1
  84. package/dist/code-agents/index.js +1 -0
  85. package/dist/code-agents/index.js.map +1 -1
  86. package/dist/code-agents/transcript-normalizer.d.ts +50 -0
  87. package/dist/code-agents/transcript-normalizer.d.ts.map +1 -0
  88. package/dist/code-agents/transcript-normalizer.js +356 -0
  89. package/dist/code-agents/transcript-normalizer.js.map +1 -0
  90. package/dist/coding-tools/index.d.ts +31 -0
  91. package/dist/coding-tools/index.d.ts.map +1 -0
  92. package/dist/coding-tools/index.js +411 -0
  93. package/dist/coding-tools/index.js.map +1 -0
  94. package/dist/extensions/schema.d.ts +1 -1
  95. package/dist/mcp/build-server.d.ts.map +1 -1
  96. package/dist/mcp/build-server.js +30 -0
  97. package/dist/mcp/build-server.js.map +1 -1
  98. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  99. package/dist/mcp/builtin-tools.js +85 -26
  100. package/dist/mcp/builtin-tools.js.map +1 -1
  101. package/dist/mcp/connect-route.d.ts +43 -0
  102. package/dist/mcp/connect-route.d.ts.map +1 -0
  103. package/dist/mcp/connect-route.js +744 -0
  104. package/dist/mcp/connect-route.js.map +1 -0
  105. package/dist/mcp/connect-store.d.ts +132 -0
  106. package/dist/mcp/connect-store.d.ts.map +1 -0
  107. package/dist/mcp/connect-store.js +434 -0
  108. package/dist/mcp/connect-store.js.map +1 -0
  109. package/dist/mcp/org-directory.d.ts +83 -0
  110. package/dist/mcp/org-directory.d.ts.map +1 -0
  111. package/dist/mcp/org-directory.js +201 -0
  112. package/dist/mcp/org-directory.js.map +1 -0
  113. package/dist/mcp/server.d.ts +38 -1
  114. package/dist/mcp/server.d.ts.map +1 -1
  115. package/dist/mcp/server.js +208 -77
  116. package/dist/mcp/server.js.map +1 -1
  117. package/dist/scripts/dev/index.d.ts +6 -4
  118. package/dist/scripts/dev/index.d.ts.map +1 -1
  119. package/dist/scripts/dev/index.js +28 -13
  120. package/dist/scripts/dev/index.js.map +1 -1
  121. package/dist/server/agent-chat-plugin.d.ts +6 -6
  122. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  123. package/dist/server/agent-chat-plugin.js +32 -32
  124. package/dist/server/agent-chat-plugin.js.map +1 -1
  125. package/dist/server/agent-teams.js +2 -2
  126. package/dist/server/agent-teams.js.map +1 -1
  127. package/dist/server/agents-bundle.d.ts +3 -3
  128. package/dist/server/agents-bundle.js +5 -5
  129. package/dist/server/agents-bundle.js.map +1 -1
  130. package/dist/server/auth.d.ts +17 -0
  131. package/dist/server/auth.d.ts.map +1 -1
  132. package/dist/server/auth.js +149 -33
  133. package/dist/server/auth.js.map +1 -1
  134. package/dist/server/better-auth-instance.d.ts +43 -0
  135. package/dist/server/better-auth-instance.d.ts.map +1 -1
  136. package/dist/server/better-auth-instance.js +25 -0
  137. package/dist/server/better-auth-instance.js.map +1 -1
  138. package/dist/server/core-routes-plugin.d.ts +12 -0
  139. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  140. package/dist/server/core-routes-plugin.js +42 -0
  141. package/dist/server/core-routes-plugin.js.map +1 -1
  142. package/dist/server/identity-sso-store.d.ts +86 -0
  143. package/dist/server/identity-sso-store.d.ts.map +1 -0
  144. package/dist/server/identity-sso-store.js +243 -0
  145. package/dist/server/identity-sso-store.js.map +1 -0
  146. package/dist/server/identity-sso.d.ts +78 -0
  147. package/dist/server/identity-sso.d.ts.map +1 -0
  148. package/dist/server/identity-sso.js +425 -0
  149. package/dist/server/identity-sso.js.map +1 -0
  150. package/dist/server/index.d.ts +1 -0
  151. package/dist/server/index.d.ts.map +1 -1
  152. package/dist/server/index.js +1 -0
  153. package/dist/server/index.js.map +1 -1
  154. package/dist/server/onboarding-html.d.ts.map +1 -1
  155. package/dist/server/onboarding-html.js +2 -1
  156. package/dist/server/onboarding-html.js.map +1 -1
  157. package/dist/server/sentry.d.ts.map +1 -1
  158. package/dist/server/sentry.js +17 -2
  159. package/dist/server/sentry.js.map +1 -1
  160. package/dist/sharing/schema.d.ts +1 -1
  161. package/docs/content/client.md +15 -0
  162. package/docs/content/code-agents-ui.md +25 -4
  163. package/docs/content/cross-app-sso.md +118 -0
  164. package/docs/content/drop-in-agent.md +3 -1
  165. package/docs/content/external-agents.md +130 -51
  166. package/docs/content/frames.md +1 -1
  167. package/docs/content/migration-workbench.md +6 -1
  168. package/package.json +2 -1
@@ -47,6 +47,10 @@ import { extractOAuthStateAppId } from "../shared/oauth-state.js";
47
47
  import { isValidWorkspaceAppIdFormat } from "../shared/workspace-app-id.js";
48
48
  import { normalizeWorkspaceAppAudience, workspaceAppAudienceFromEnv, workspaceAppRouteAccessFromEnv, } from "../shared/workspace-app-audience.js";
49
49
  import { BUILDER_CONNECT_OWNER_COOKIE, BUILDER_CONNECT_PARAM, BUILDER_STATE_PARAM, verifyBuilderCallbackStateAndGetOwner, verifyBuilderConnectTokenAndGetOwner, } from "./builder-browser.js";
50
+ // Pure env-read feature switch from a leaf module (no dependency back on
51
+ // auth.ts), so the guard and the SSO route handler share one validator and
52
+ // can never disagree about whether federated SSO is enabled.
53
+ import { isIdentitySsoEnabled } from "./identity-sso-store.js";
50
54
  /**
51
55
  * Get the configured session max age. Desktop SSO broker writes from
52
56
  * OAuth flows read this so expiration stays consistent with the cookie.
@@ -310,6 +314,39 @@ export function getConfiguredLoginHtml(event) {
310
314
  const rawPath = queryStart >= 0 ? url.slice(0, queryStart) : url;
311
315
  return config.getLoginHtml?.(event, rawPath) ?? config.loginHtml ?? null;
312
316
  }
317
+ /**
318
+ * True only when the request originates from the local machine — the raw
319
+ * socket peer is `127.0.0.0/8`, `::1`, or the IPv4-mapped `::ffff:127.0.0.1`
320
+ * (an optional IPv6 zone id like `fe80::1%en0` is stripped first).
321
+ *
322
+ * `getRequestIP(event)` is called WITHOUT `{ xForwardedFor: true }`, so it
323
+ * returns the real connection peer and never an attacker-controlled
324
+ * `X-Forwarded-For` value — a remote client cannot spoof its way past this.
325
+ * Used to scope local-only conveniences (the desktop SSO broker and the dev
326
+ * auto-account) so a directly network-reachable dev server never exposes
327
+ * them to a remote visitor. NOTE: a reverse proxy / tunnel that connects to
328
+ * the dev server over localhost still appears as loopback, so this is a
329
+ * necessary but not sufficient gate — callers pair it with NODE_ENV and,
330
+ * for the dev account, a throwaway per-DB password.
331
+ */
332
+ export function isLoopbackAddress(ip) {
333
+ // Strip an optional IPv6 zone id (e.g. "fe80::1%en0") before comparing.
334
+ const normalised = (ip ?? "").split("%")[0];
335
+ return (normalised === "127.0.0.1" ||
336
+ normalised === "::1" ||
337
+ normalised === "::ffff:127.0.0.1" ||
338
+ normalised.startsWith("127."));
339
+ }
340
+ function isLoopbackRequest(event) {
341
+ let ip;
342
+ try {
343
+ ip = getRequestIP(event) ?? undefined;
344
+ }
345
+ catch {
346
+ ip = undefined;
347
+ }
348
+ return isLoopbackAddress(ip);
349
+ }
313
350
  /**
314
351
  * Read the desktop-SSO broker file, but only if the request is plausibly
315
352
  * from the Electron desktop app *and* coming from the local machine.
@@ -329,21 +366,7 @@ async function readDesktopSsoSafely(event) {
329
366
  return null;
330
367
  if (!isElectronRequest(event))
331
368
  return null;
332
- // Loopback-only: 127.0.0.1, ::1, and the IPv4-mapped form.
333
- let ip;
334
- try {
335
- ip = getRequestIP(event) ?? undefined;
336
- }
337
- catch {
338
- ip = undefined;
339
- }
340
- // Strip an optional zone id (e.g. "fe80::1%en0") before comparing.
341
- const normalised = (ip ?? "").split("%")[0];
342
- const isLoopback = normalised === "127.0.0.1" ||
343
- normalised === "::1" ||
344
- normalised === "::ffff:127.0.0.1" ||
345
- normalised.startsWith("127.");
346
- if (!isLoopback)
369
+ if (!isLoopbackRequest(event))
347
370
  return null;
348
371
  return await readDesktopSso();
349
372
  }
@@ -452,7 +475,7 @@ const EXPECTED_AUTH_FAILURE_PATTERNS = [
452
475
  /already\s+(exists|registered|in\s+use)/i,
453
476
  /not\s+verified/i,
454
477
  ];
455
- function isExpectedAuthFailure(error) {
478
+ export function isExpectedAuthFailure(error) {
456
479
  const msg = error?.message;
457
480
  if (typeof msg !== "string")
458
481
  return false;
@@ -912,6 +935,43 @@ function createAuthGuardFn() {
912
935
  if (p === "/_agent-native/mcp") {
913
936
  return;
914
937
  }
938
+ // MCP connect — frictionless external-agent connection. Like /open
939
+ // above, the connect *page* resolves the browser session itself and
940
+ // serves its own login form when unauthenticated (so the post-login
941
+ // reload returns to the same URL, carrying the device user_code in the
942
+ // query). The two unauthenticated device endpoints below are the CLI's
943
+ // OAuth-style polling pair: `device/start` (mint a device+user code) and
944
+ // `device/poll` (exchange an approved code for the token) — both must be
945
+ // reachable without a browser session because the CLI has none. They are
946
+ // protected by short-TTL, single-use, crypto-random codes + a creation
947
+ // rate-limit, not cookies.
948
+ //
949
+ // Everything that MINTS or MUTATES on behalf of the user — `/token`,
950
+ // `/device/authorize`, `/tokens`, `/tokens/revoke` — is intentionally
951
+ // NOT bypassed: the guard's default 401-for-/_agent-native/* is the
952
+ // correct gate for them. Those are POSTed by the in-page fetch, which
953
+ // carries the session cookie, so the guard (which only 401s when there
954
+ // is no session) lets the authenticated same-origin request through and
955
+ // the handler then re-checks the session itself (defense in depth).
956
+ if (p === "/_agent-native/mcp/connect" ||
957
+ p === "/_agent-native/mcp/connect/device/start" ||
958
+ p === "/_agent-native/mcp/connect/device/poll") {
959
+ return;
960
+ }
961
+ // Cross-app SSO ("Sign in with Agent-Native") — CLIENT side. Both the
962
+ // `/login` entry point and the `/callback` (hit by a user who is, by
963
+ // definition, NOT yet signed in to THIS app) must bypass the blanket
964
+ // 401-for-/_agent-native/*: they resolve / mint the browser session
965
+ // themselves and verify a signature-bound, single-use, CSRF-stated
966
+ // hub token — not a cookie. This bypass is GATED on the opt-in env var
967
+ // so an unset `AGENT_NATIVE_IDENTITY_HUB_URL` is a true no-op (the
968
+ // guard's behaviour is byte-for-byte unchanged when SSO is off). The
969
+ // handler itself 404s when disabled as defence in depth.
970
+ if (isIdentitySsoEnabled() &&
971
+ (p === "/_agent-native/identity/login" ||
972
+ p === "/_agent-native/identity/callback")) {
973
+ return;
974
+ }
915
975
  // Internal processor endpoint for the A2A async-mode fanout. Mirrors the
916
976
  // integration webhook fanout: when `message/send` is called with
917
977
  // `async: true`, the JSON-RPC handler enqueues to a2a_tasks and self-
@@ -1031,7 +1091,8 @@ function createAuthGuardFn() {
1031
1091
  // validator (a bare `dev@local` has no TLD and is rejected as INVALID_EMAIL,
1032
1092
  // which silently broke the zero-setup auto-sign-in on every fresh dev DB).
1033
1093
  const AUTO_DEV_ACCOUNT_EMAIL = "dev@local.test";
1034
- const AUTO_DEV_ACCOUNT_PASSWORD = "local-dev-account";
1094
+ // No fixed password: maybeAutoCreateDevSession mints a random one per DB
1095
+ // and prints it to the console once (see there).
1035
1096
  // Pre-fix local dev DBs may already contain a `dev@local` user. Treat that
1036
1097
  // legacy address as the dev account too, so the "any real users?" check
1037
1098
  // below doesn't mistake the old auto-account for a real signup (which would
@@ -1054,19 +1115,31 @@ const LEGACY_AUTO_DEV_ACCOUNT_EMAIL = "dev@local";
1054
1115
  * leaves the user on the regular sign-in form; without this guard the
1055
1116
  * post-logout reload would silently re-create the session.
1056
1117
  *
1057
- * The fixed password is intentional: it means a developer who signs
1058
- * out can sign back in with `dev@local.test` / `local-dev-account`
1059
- * from the regular login form. To get the auto-flow back, drop the
1060
- * user row or wipe the local DB. Set
1061
- * `AGENT_NATIVE_DISABLE_AUTO_DEV_ACCOUNT=1` to opt out entirely
1062
- * (useful for tests that exercise the unauthenticated branch). This
1063
- * is local-only the helper is gated on NODE_ENV.
1118
+ * Hardening (this is a convenience, not an auth bypass — it uses the
1119
+ * real Better Auth sign-up/sign-in, but a known-credential local account
1120
+ * is still worth not shipping):
1121
+ * - **Loopback only.** Gated on `isLoopbackRequest`, so a tunnelled /
1122
+ * reverse-proxied / misconfigured-non-prod dev server never auto-signs
1123
+ * in a directly-remote visitor (mirrors the desktop SSO broker).
1124
+ * - **Random per-DB password.** The account password is freshly
1125
+ * generated on creation and printed to the server console exactly
1126
+ * once — there is no source-code-known credential. After logout the
1127
+ * auto-flow won't refire (dev row exists), so signing back in uses
1128
+ * that printed password; lost it ⇒ drop the row or wipe the local DB.
1129
+ * - **NODE_ENV.** Still gated on development/test.
1130
+ *
1131
+ * Set `AGENT_NATIVE_DISABLE_AUTO_DEV_ACCOUNT=1` to opt out entirely
1132
+ * (useful for tests that exercise the unauthenticated branch).
1064
1133
  */
1065
1134
  async function maybeAutoCreateDevSession(event, redirectTo) {
1066
1135
  if (!isDevEnvironment())
1067
1136
  return null;
1068
1137
  if (process.env.AGENT_NATIVE_DISABLE_AUTO_DEV_ACCOUNT === "1")
1069
1138
  return null;
1139
+ // Local machine only: never auto-sign-in a remote visitor, even if a
1140
+ // dev server is exposed (tunnel, reverse proxy, misconfigured NODE_ENV).
1141
+ if (!isLoopbackRequest(event))
1142
+ return null;
1070
1143
  try {
1071
1144
  const db = getDbExec();
1072
1145
  // Exclude BOTH the current and the legacy dev-account email so a
@@ -1097,14 +1170,21 @@ async function maybeAutoCreateDevSession(event, redirectTo) {
1097
1170
  const auth = await getBetterAuth();
1098
1171
  if (!auth)
1099
1172
  return null;
1100
- // Idempotent sign-up: succeeds on first run, throws an "already exists"
1101
- // failure on subsequent runs (which we swallow before falling through
1102
- // to the sign-in path below).
1173
+ // Random per-DB password there is no source-code-known credential
1174
+ // for this account. Printed once below so the developer can sign back
1175
+ // in after logout (the auto-flow won't refire while the dev row
1176
+ // exists).
1177
+ const devPassword = crypto.randomBytes(18).toString("base64url");
1178
+ // The dev account does not exist at this point (the devUsers check
1179
+ // above returned early otherwise). The "already exists" swallow only
1180
+ // matters under a rare concurrent first-hit race — in that case the
1181
+ // sign-in below fails the password check and we return null, leaving
1182
+ // the racing request that already won to keep the session.
1103
1183
  try {
1104
1184
  await auth.api.signUpEmail({
1105
1185
  body: {
1106
1186
  email: AUTO_DEV_ACCOUNT_EMAIL,
1107
- password: AUTO_DEV_ACCOUNT_PASSWORD,
1187
+ password: devPassword,
1108
1188
  name: "Dev",
1109
1189
  },
1110
1190
  });
@@ -1116,17 +1196,26 @@ async function maybeAutoCreateDevSession(event, redirectTo) {
1116
1196
  const result = await auth.api.signInEmail({
1117
1197
  body: {
1118
1198
  email: AUTO_DEV_ACCOUNT_EMAIL,
1119
- password: AUTO_DEV_ACCOUNT_PASSWORD,
1199
+ password: devPassword,
1120
1200
  },
1121
1201
  });
1122
1202
  if (!result?.token)
1123
1203
  return null;
1124
1204
  setFrameworkSessionCookie(event, result.token);
1125
1205
  await addSession(result.token, AUTO_DEV_ACCOUNT_EMAIL);
1126
- return new Response("", {
1127
- status: 302,
1128
- headers: { Location: redirectTo },
1129
- });
1206
+ // Print the throwaway credential exactly once so the developer can
1207
+ // sign back in manually after logout (auto-flow won't refire once the
1208
+ // dev row exists). Local console only — never Sentry.
1209
+ console.log(`\n[agent-native] Local dev auto-login ready.\n` +
1210
+ ` email: ${AUTO_DEV_ACCOUNT_EMAIL}\n` +
1211
+ ` password: ${devPassword}\n` +
1212
+ ` (random, this DB only — needed to sign back in after logout.\n` +
1213
+ ` Set AGENT_NATIVE_DISABLE_AUTO_DEV_ACCOUNT=1 to disable.)\n`);
1214
+ // Emit the session cookie ON the 302 itself. Returning a bare
1215
+ // `new Response(...)` here drops the cookie staged on event.node.res
1216
+ // (see redirectWithStagedCookies), so the developer would 302 to the
1217
+ // app and immediately bounce back to the login form.
1218
+ return redirectWithStagedCookies(event, redirectTo);
1130
1219
  }
1131
1220
  catch (e) {
1132
1221
  // Local-dev only — log to console for debugging, but don't surface
@@ -1279,6 +1368,33 @@ export function setFrameworkSessionCookie(event, token) {
1279
1368
  maxAge: sessionMaxAge,
1280
1369
  });
1281
1370
  }
1371
+ /**
1372
+ * Build a redirect `Response` that carries whatever `Set-Cookie` headers were
1373
+ * just staged on the event (e.g. by `setFrameworkSessionCookie`).
1374
+ *
1375
+ * h3 v2's `setCookie` appends the cookie onto `event.res.headers`. When a
1376
+ * handler returns a plain object/string, h3's `prepareResponse` merges those
1377
+ * staged headers into the synthesized response, so the cookie survives. But
1378
+ * when a handler returns a web `Response`, `prepareResponse` only merges the
1379
+ * staged headers if the Response is 2xx — its `!val.ok` early-return hands a
1380
+ * non-2xx Response (like a 302) straight back WITHOUT merging. A bare
1381
+ * `new Response("", { status: 302, headers: { Location } })` therefore 302s
1382
+ * the browser with no session cookie, so the zero-setup dev auto-sign-in
1383
+ * bounces straight back to the login form.
1384
+ *
1385
+ * Mirroring the staged cookies onto the redirect Response's own headers makes
1386
+ * them part of the Response that's returned as-is, so the 302 actually
1387
+ * carries the session cookie. (`event.res.headers` is also left intact for
1388
+ * any non-Response continuation path; h3 only skips the merge for the
1389
+ * Response branch, so there's no double-emit.)
1390
+ */
1391
+ function redirectWithStagedCookies(event, location, status = 302) {
1392
+ const headers = new Headers({ Location: location });
1393
+ const staged = event.res?.headers?.getSetCookie?.() ?? [];
1394
+ for (const cookie of staged)
1395
+ headers.append("set-cookie", cookie);
1396
+ return new Response("", { status, headers });
1397
+ }
1282
1398
  function isHttpsRequest(event) {
1283
1399
  try {
1284
1400
  const xfProto = getHeader(event, "x-forwarded-proto");