@agent-native/core 0.151.2 → 0.152.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/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/analytics/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/analytics/actions/bigquery-query.ts +21 -0
- package/corpus/templates/analytics/app/components/ConnectionTestStatus.tsx +65 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +3 -3
- package/corpus/templates/analytics/app/pages/DataSources.tsx +15 -37
- package/corpus/templates/analytics/app/root.tsx +0 -1
- package/corpus/templates/analytics/changelog/2026-08-11-analytics-custom-blocks-keep-loading-with-legacy-bigquery-action.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-11-analytics-routes-company-knowledge-and-slack-context-questio.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/analytics/public/manifest.json +0 -1
- package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +5 -0
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +5 -0
- package/corpus/templates/assets/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/assets/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/assets/app/root.tsx +0 -1
- package/corpus/templates/assets/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/assets/public/manifest.json +0 -1
- package/corpus/templates/brain/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/brain/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/brain/app/root.tsx +0 -1
- package/corpus/templates/brain/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/brain/public/manifest.json +0 -1
- package/corpus/templates/calendar/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/calendar/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +0 -53
- package/corpus/templates/calendar/app/i18n-data.ts +0 -80
- package/corpus/templates/calendar/app/root.tsx +0 -1
- package/corpus/templates/calendar/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-11-google-calendar-connection-buttons-now-open-the-sign-in-flow.md +6 -0
- package/corpus/templates/calendar/public/manifest.json +0 -1
- package/corpus/templates/calendar/server/plugins/auth.ts +0 -10
- package/corpus/templates/chat/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/chat/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/chat/app/root.tsx +0 -1
- package/corpus/templates/chat/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/chat/public/manifest.json +0 -1
- package/corpus/templates/clips/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +1 -1
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +6 -0
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +20 -3
- package/corpus/templates/clips/app/i18n/en-US.ts +4 -4
- package/corpus/templates/clips/app/root.tsx +0 -1
- package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +1 -3
- package/corpus/templates/clips/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-11-clips-desktop-settings-are-now-easier-to-scan-with-a-wider-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-11-google-calendar-connections-from-meetings-now-open-directly.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-11-recording-viewers-can-comment-and-react.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-11-system-audio-recording-now-lives-inside-the-microphone-menu-.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-11-timestamped-comments-stay-on-the-video-for-three-seconds-wh.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +773 -1001
- package/corpus/templates/clips/desktop/src/components/MediaDeviceRow.tsx +15 -27
- package/corpus/templates/clips/desktop/src/styles.css +364 -5
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +1 -1
- package/corpus/templates/clips/public/manifest.json +0 -1
- package/corpus/templates/content/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/content/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/content/AGENTS.md +3 -0
- package/corpus/templates/content/actions/resolve-content-landing.ts +120 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +9 -0
- package/corpus/templates/content/app/i18n-data.ts +58 -0
- package/corpus/templates/content/app/lib/content-landing.ts +22 -0
- package/corpus/templates/content/app/root.tsx +0 -1
- package/corpus/templates/content/app/routes/_app._index.tsx +36 -26
- package/corpus/templates/content/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/content/changelog/2026-08-11-content-opens-your-last-page-on-return-and-gives-new-users-a.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.workspace.session-restore.md +7 -1
- package/corpus/templates/content/parity/matrix.md +1 -0
- package/corpus/templates/content/parity/matrix.ts +22 -0
- package/corpus/templates/content/public/manifest.json +0 -1
- package/corpus/templates/content/shared/content-landing.ts +17 -0
- package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/crm/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/crm/app/root.tsx +0 -1
- package/corpus/templates/crm/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/crm/public/manifest.json +0 -1
- package/corpus/templates/design/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/design/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/design/app/i18n/en-US.ts +5 -0
- package/corpus/templates/design/app/root.tsx +0 -1
- package/corpus/templates/design/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/design/changelog/2026-08-11-design-sharing-now-identifies-viewers-who-can-add-review-com.md +6 -0
- package/corpus/templates/design/public/manifest.json +0 -1
- package/corpus/templates/dispatch/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/dispatch/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/dispatch/AGENTS.md +9 -0
- package/corpus/templates/dispatch/actions/list-dispatch-usage-metrics.ts +5 -19
- package/corpus/templates/dispatch/actions/view-screen.ts +10 -3
- package/corpus/templates/dispatch/app/root.tsx +0 -1
- package/corpus/templates/dispatch/app/routes/settings.tsx +13 -1
- package/corpus/templates/dispatch/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-11-clicking-the-dispatch-logo-returns-to-the-overview-page.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-11-dispatch-no-longer-links-to-itself-from-the-app-switcher.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-11-dispatch-settings-now-link-directly-to-admin.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-11-dispatch-shows-usage-trends-recent-prompts-and-an-agent-revi.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-11-human-friendly-app-names.md +6 -0
- package/corpus/templates/dispatch/public/manifest.json +0 -1
- package/corpus/templates/dispatch/server/lib/identity-sso.ts +432 -158
- package/corpus/templates/dispatch/server/plugins/identity-sso.ts +204 -123
- package/corpus/templates/dispatch/server/plugins/setup-dispatch.ts +1 -0
- package/corpus/templates/factory/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/factory/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/factory/app/root.tsx +0 -1
- package/corpus/templates/factory/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/factory/public/manifest.json +0 -1
- package/corpus/templates/forms/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/forms/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/forms/app/root.tsx +0 -1
- package/corpus/templates/forms/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/forms/public/manifest.json +0 -1
- package/corpus/templates/macros/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/macros/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/mail/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/mail/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/mail/app/root.tsx +0 -1
- package/corpus/templates/mail/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/mail/changelog/2026-08-11-google-sign-in-now-opens-the-gmail-connection-flow-directly.md +6 -0
- package/corpus/templates/mail/public/manifest.json +0 -1
- package/corpus/templates/mail/server/plugins/auth.ts +0 -10
- package/corpus/templates/plan/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/plan/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/plan/app/i18n/en-US.ts +2 -0
- package/corpus/templates/plan/app/pages/PlansPage.tsx +6 -0
- package/corpus/templates/plan/app/root.tsx +0 -1
- package/corpus/templates/plan/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/plan/changelog/2026-08-11-plan-sharing-now-identifies-viewers-who-can-add-comments.md +6 -0
- package/corpus/templates/plan/public/manifest.json +0 -1
- package/corpus/templates/slides/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/slides/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +2 -0
- package/corpus/templates/slides/app/root.tsx +0 -1
- package/corpus/templates/slides/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-share-dialog-uses-commenter-terminology.md +6 -0
- package/corpus/templates/slides/public/manifest.json +0 -1
- package/corpus/templates/tasks/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/tasks/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/tasks/app/root.tsx +0 -1
- package/corpus/templates/tasks/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/tasks/public/manifest.json +0 -1
- package/dist/agent/production-agent.js +1 -22
- package/dist/agent/run-manager.d.ts +7 -0
- package/dist/agent/run-manager.js +46 -3
- package/dist/agent/tool-approval-migrations.d.ts +40 -0
- package/dist/agent/tool-approval-migrations.js +51 -0
- package/dist/agent/tool-approval-store.js +5 -20
- package/dist/cli/templates-meta.js +2 -2
- package/dist/client/AgentPanel.js +10 -1
- package/dist/client/AssistantChat.d.ts +3 -0
- package/dist/client/AssistantChat.js +148 -140
- package/dist/client/agent-page/AgentTabsPage.js +2 -2
- package/dist/client/chat/attachment-adapters.js +8 -2
- package/dist/client/chat/tool-call-display.d.ts +1 -0
- package/dist/client/chat/tool-call-display.js +3 -1
- package/dist/client/chat-first.js +11 -5
- package/dist/client/hooks/index.d.ts +1 -1
- package/dist/client/hooks/index.js +1 -1
- package/dist/client/org/OrgSwitcher.d.ts +3 -1
- package/dist/client/org/OrgSwitcher.js +22 -16
- package/dist/client/org/WorkspaceNotice.js +3 -1
- package/dist/client/resources/index.d.ts +1 -1
- package/dist/client/resources/index.js +1 -1
- package/dist/client/resources/use-mcp-servers.d.ts +1 -0
- package/dist/client/resources/use-mcp-servers.js +14 -0
- package/dist/client/settings/AccountSettingsCard.js +8 -4
- package/dist/client/settings/SettingsPanel.js +46 -13
- package/dist/client/settings/SettingsTabsPage.js +8 -3
- package/dist/client/sharing/ShareButton.d.ts +7 -1
- package/dist/client/sharing/ShareButton.js +14 -7
- package/dist/client/sharing/ShareDialog.js +5 -1
- package/dist/client/sharing/useShareButtonController.d.ts +4 -0
- package/dist/client/sharing/useShareButtonController.js +15 -0
- package/dist/client/sharing/useShareDialogController.d.ts +7 -0
- package/dist/client/sharing/useShareDialogController.js +14 -0
- package/dist/client/sse-event-processor.js +37 -0
- package/dist/client/use-session.d.ts +2 -0
- package/dist/client/use-session.js +69 -5
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.js +1 -1
- package/dist/file-upload/pre-upload-attachments.d.ts +3 -4
- package/dist/file-upload/pre-upload-attachments.js +65 -17
- package/dist/ingestion/index.d.ts +1 -0
- package/dist/ingestion/index.js +1 -0
- package/dist/ingestion/office.d.ts +3 -3
- package/dist/ingestion/office.js +24 -2
- package/dist/ingestion/spreadsheet.d.ts +41 -0
- package/dist/ingestion/spreadsheet.js +172 -0
- package/dist/integrations/adapters/slack.js +7 -3
- package/dist/localization/default-messages.d.ts +3 -0
- package/dist/localization/default-messages.js +3 -0
- package/dist/mcp/embed-app.js +20 -9
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +6 -6
- package/dist/oauth-tokens/migrations.d.ts +8 -0
- package/dist/oauth-tokens/migrations.js +36 -0
- package/dist/org/workspace-url.d.ts +8 -0
- package/dist/org/workspace-url.js +26 -2
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/agent-chat-plugin.js +16 -8
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.d.ts +0 -11
- package/dist/server/auth.js +37 -20
- package/dist/server/better-auth-migrations.js +23 -0
- package/dist/server/email-template.d.ts +5 -0
- package/dist/server/email-template.js +9 -0
- package/dist/server/embed-route.js +17 -5
- package/dist/server/identity-sso-store.d.ts +40 -66
- package/dist/server/identity-sso-store.js +187 -150
- package/dist/server/identity-sso.d.ts +17 -73
- package/dist/server/identity-sso.js +294 -297
- package/dist/server/onboarding-html.d.ts +0 -12
- package/dist/server/onboarding-html.js +46 -269
- package/dist/server/release-migrations.js +8 -0
- package/dist/shared/index.d.ts +1 -1
- package/dist/shared/index.js +1 -1
- package/dist/shared/workspace-app-id.d.ts +2 -0
- package/dist/shared/workspace-app-id.js +10 -0
- package/dist/sharing/actions/share-resource.d.ts +1 -0
- package/dist/sharing/actions/share-resource.js +20 -5
- package/dist/templates/chat/.agents/skills/sharing/SKILL.md +1 -1
- package/dist/templates/chat/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/dist/templates/chat/app/root.tsx +0 -1
- package/dist/templates/chat/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/dist/templates/chat/public/manifest.json +0 -1
- package/dist/templates/default/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/dist/templates/default/app/root.tsx +0 -1
- package/dist/templates/default/public/manifest.json +0 -1
- package/dist/templates/headless/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +22 -11
- package/dist/templates/workspace-core/.agents/skills/sharing/SKILL.md +1 -1
- package/dist/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/docs/content/cross-app-sso.mdx +85 -27
- package/docs/content/template-mail.mdx +0 -6
- package/package.json +4 -3
- package/src/templates/chat/.agents/skills/sharing/SKILL.md +1 -1
- package/src/templates/chat/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/src/templates/chat/app/root.tsx +0 -1
- package/src/templates/chat/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/src/templates/chat/public/manifest.json +0 -1
- package/src/templates/default/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/src/templates/default/app/root.tsx +0 -1
- package/src/templates/default/public/manifest.json +0 -1
- package/src/templates/headless/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +22 -11
- package/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +1 -1
- package/src/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/calendar/changelog/2026-06-23-added-a-heads-up-explaining-google-s-app-not-verified-screen.md +0 -6
- package/corpus/templates/clips/app/components/meetings/google-oauth-identity-notice.tsx +0 -51
- package/corpus/templates/clips/changelog/2026-07-30-google-calendar-warning.md +0 -6
|
@@ -1,151 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cross-app SSO ("Sign in with Agent-Native") — the CLIENT side.
|
|
3
3
|
*
|
|
4
|
-
* Each hosted
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Each hosted app has its own Better Auth store. Dispatch is the identity
|
|
5
|
+
* authority, but the browser only ever carries a short-lived, one-time
|
|
6
|
+
* authorization code. The client keeps the PKCE verifier in an HttpOnly,
|
|
7
|
+
* callback-scoped cookie and redeems the code server-to-server. Only that
|
|
8
|
+
* server-to-server response may contain the signed identity assertion.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
* `AGENT_NATIVE_IDENTITY_HUB_URL
|
|
11
|
-
* canonical Dispatch authority on canonical
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* - UNSET → normal browser requests remain protected and the login page
|
|
15
|
-
* renders no SSO button. Packaged Canary requests on canonical app origins
|
|
16
|
-
* may reach the flow, but Dispatch refuses them unless its rollout flag
|
|
17
|
-
* allows the user.
|
|
18
|
-
* - SET (e.g. `https://dispatch.agent-native.com`) → two routes mount:
|
|
19
|
-
* GET /_agent-native/identity/login
|
|
20
|
-
* 302 → `<HUB>/_agent-native/identity/authorize?app=<id>
|
|
21
|
-
* &redirect_uri=<thisOrigin>/_agent-native/identity/callback
|
|
22
|
-
* &state=<single-use CSRF state>`
|
|
23
|
-
* GET /_agent-native/identity/callback?token=<jwt>&state=<state>
|
|
24
|
-
* Verifies the hub-issued identity JWT (HS256 over the SHARED A2A
|
|
25
|
-
* secret — the exact verify path A2A / MCP `verifyAuth` use), checks
|
|
26
|
-
* `scope:"identity"`, `exp`, single-use CSRF `state`, and (best
|
|
27
|
-
* effort) `jti` replay, then JIT-links the verified email into this
|
|
28
|
-
* app's local Better Auth store and mints a normal framework session
|
|
29
|
-
* the SAME way the Google OAuth callback does.
|
|
30
|
-
*
|
|
31
|
-
* Crypto reuse: the hub signs with `jose.SignJWT(...).sign(A2A_SECRET)` (the
|
|
32
|
-
* existing `signA2AToken` builder). We verify with the identical
|
|
33
|
-
* `jose.jwtVerify(token, A2A_SECRET)` call `mcp/build-server.ts#verifyAuth`
|
|
34
|
-
* uses — no new crypto, no new keys.
|
|
35
|
-
*
|
|
36
|
-
* Session reuse: a NEW email is created via `auth.api.signUpEmail` — the
|
|
37
|
-
* exact Better Auth signup path `maybeAutoCreateDevSession` already uses, so
|
|
38
|
-
* the adapter creates the `user` (+ adapter-managed credential `account`)
|
|
39
|
-
* row schema-correctly and the normal `databaseHooks.user.create.after`
|
|
40
|
-
* (org auto-join, analytics) fires. The framework session is then minted via
|
|
41
|
-
* `createOAuthSession` — the literal Google-OAuth session-mint path
|
|
42
|
-
* (`addSession` + `setFrameworkSessionCookie`). An EXISTING email is never
|
|
43
|
-
* mutated: we only ADD an inert federated-provider `account` row (if absent)
|
|
44
|
-
* and mint the same framework session. Removing the env returns the app to
|
|
45
|
-
* its prior auth with no residue.
|
|
10
|
+
* Direct browser federation remains opt-in through
|
|
11
|
+
* `AGENT_NATIVE_IDENTITY_HUB_URL`. The packaged Desktop Canary may use the
|
|
12
|
+
* canonical Dispatch authority on canonical app origins, as established by
|
|
13
|
+
* the merged feature-flagged Desktop work. This module does not change the
|
|
14
|
+
* Desktop implementation or any ordinary auth surface.
|
|
46
15
|
*/
|
|
47
|
-
import { randomBytes } from "node:crypto";
|
|
48
|
-
import { getHeader, getMethod } from "h3";
|
|
16
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
17
|
+
import { deleteCookie, getCookie, getHeader, getMethod, setCookie } from "h3";
|
|
49
18
|
import * as jose from "jose";
|
|
50
19
|
import { GOOGLE_AUTH_REQUIRED_MESSAGE, isGoogleSignInRequiredForEmail, } from "../org/auth-policy.js";
|
|
51
20
|
import { SIGN_IN_ENTRY_PATH } from "../shared/sign-in-journey.js";
|
|
52
21
|
import { getAppName } from "./app-name.js";
|
|
53
|
-
import { getSession,
|
|
22
|
+
import { getSession, isExpectedAuthFailure, safeReturnPath } from "./auth.js";
|
|
54
23
|
import { getBetterAuth, getBetterAuthInternalAdapter, } from "./better-auth-instance.js";
|
|
55
24
|
import { createOAuthSession, getOrigin } from "./google-oauth.js";
|
|
56
|
-
import {
|
|
57
|
-
export { getIdentityHubUrl,
|
|
58
|
-
/**
|
|
59
|
-
* The provider id recorded on the additive `account` row we link for an
|
|
60
|
-
* EXISTING local user. Must match the value the Dispatch authority agent
|
|
61
|
-
* expects to interoperate with — documented in the report so the two sides
|
|
62
|
-
* stay in sync. Inert when this provider is unused, so removing the env var
|
|
63
|
-
* leaves no behavioural residue.
|
|
64
|
-
*/
|
|
25
|
+
import { consumeSsoState, createSsoState, getIdentityHubUrl, identitySsoLoginButtonHtml, isCanonicalAgentNativeAppRequest, isDesktopSsoCanaryUserAgent, isIdentitySsoEnabled, isJtiReplayed, SSO_STATE_TTL_MS, } from "./identity-sso-store.js";
|
|
26
|
+
export { getIdentityHubUrl, identitySsoLoginButtonHtml, isIdentitySsoEnabled };
|
|
65
27
|
export const IDENTITY_SSO_PROVIDER_ID = "agent-native";
|
|
66
|
-
/**
|
|
67
|
-
* The JWT `scope` claim the hub MUST set on the identity token. The callback
|
|
68
|
-
* rejects any token whose `scope` is not exactly this value, so an A2A
|
|
69
|
-
* delegation JWT (no scope, or `scope:"mcp-connect"`) can never be replayed
|
|
70
|
-
* as an identity assertion.
|
|
71
|
-
*/
|
|
72
28
|
export const IDENTITY_SSO_SCOPE = "identity";
|
|
73
29
|
export const IDENTITY_SSO_DESKTOP_COMPLETE_PATH = "/_agent-native/identity/desktop-complete";
|
|
30
|
+
export const IDENTITY_SSO_CALLBACK_PATH = "/_agent-native/identity/callback";
|
|
31
|
+
export const IDENTITY_SSO_TOKEN_PATH = "/_agent-native/identity/token";
|
|
74
32
|
const DESKTOP_COMPLETION_NONCE = /^[A-Za-z0-9_-]{32,128}$/;
|
|
33
|
+
const STATE_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
34
|
+
const CODE = /^[A-Za-z0-9_-]{43}$/;
|
|
35
|
+
const CODE_VERIFIER = /^[A-Za-z0-9._~-]{43,128}$/;
|
|
75
36
|
const DESKTOP_IDENTITY_HUB_URL = "https://dispatch.agent-native.com";
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
* Direct web federation stays explicitly configured through
|
|
80
|
-
* `AGENT_NATIVE_IDENTITY_HUB_URL`. The packaged Desktop SSO Canary may use the
|
|
81
|
-
* canonical Dispatch authority without per-app deployment configuration, but
|
|
82
|
-
* only for canonical HTTPS Agent Native app origins. Dispatch's default-off
|
|
83
|
-
* feature flag remains the authoritative rollout gate.
|
|
84
|
-
*/
|
|
85
|
-
export function resolveIdentityHubUrl(event) {
|
|
86
|
-
const configured = getIdentityHubUrl();
|
|
87
|
-
if (configured)
|
|
88
|
-
return configured;
|
|
89
|
-
if (!isDesktopSsoCanaryUserAgent(getHeader(event, "user-agent"))) {
|
|
90
|
-
return undefined;
|
|
91
|
-
}
|
|
92
|
-
try {
|
|
93
|
-
if (!isCanonicalAgentNativeAppRequest(getHeader(event, "host"), getHeader(event, "x-forwarded-proto"))) {
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
96
|
-
return DESKTOP_IDENTITY_HUB_URL;
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
return undefined;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* A stable id for THIS app, sent to the hub as `?app=` so the authority can
|
|
104
|
-
* record / display which app requested sign-in. Best-effort, non-secret,
|
|
105
|
-
* never trusted for identity. Falls back to the request host.
|
|
106
|
-
*/
|
|
107
|
-
function resolveAppId(event) {
|
|
108
|
-
const configured = process.env.AGENT_NATIVE_APP_ID?.trim() ||
|
|
109
|
-
process.env.AGENT_NATIVE_WORKSPACE_APP_ID?.trim();
|
|
110
|
-
if (configured)
|
|
111
|
-
return configured;
|
|
112
|
-
const name = getAppName();
|
|
113
|
-
if (name && name !== "app")
|
|
114
|
-
return name;
|
|
115
|
-
try {
|
|
116
|
-
const origin = getOrigin(event);
|
|
117
|
-
return new URL(origin).hostname.split(".")[0] || "app";
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
return "app";
|
|
121
|
-
}
|
|
122
|
-
}
|
|
37
|
+
const SSO_VERIFIER_COOKIE_PREFIX = "agent_native_sso_verifier_";
|
|
38
|
+
const MAX_ASSERTION_AGE_SECONDS = 5 * 60;
|
|
39
|
+
const LOCALHOST_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
123
40
|
function html(body, status = 200) {
|
|
124
41
|
return new Response(body, {
|
|
125
42
|
status,
|
|
126
|
-
headers: {
|
|
43
|
+
headers: {
|
|
44
|
+
"Cache-Control": "no-store",
|
|
45
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
46
|
+
"Referrer-Policy": "no-referrer",
|
|
47
|
+
},
|
|
127
48
|
});
|
|
128
49
|
}
|
|
129
50
|
function redirect(event, location) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
// session cookie and the user would finish "Sign in with Agent-Native"
|
|
136
|
-
// still logged out. This mirrors the framework's `redirectWithStagedCookies`
|
|
137
|
-
// (auth.ts) exactly; it is a no-op when nothing is staged.
|
|
138
|
-
const headers = new Headers({ Location: location });
|
|
51
|
+
const headers = new Headers({
|
|
52
|
+
"Cache-Control": "no-store",
|
|
53
|
+
Location: location,
|
|
54
|
+
"Referrer-Policy": "no-referrer",
|
|
55
|
+
});
|
|
139
56
|
const staged = event.res?.headers?.getSetCookie?.() ?? [];
|
|
140
57
|
for (const cookie of staged)
|
|
141
58
|
headers.append("set-cookie", cookie);
|
|
142
59
|
return new Response("", { status: 302, headers });
|
|
143
60
|
}
|
|
144
|
-
/**
|
|
145
|
-
* Minimal self-contained error page (same inline-HTML approach as the auth /
|
|
146
|
-
* connect pages). Used when the federated round-trip fails so the user gets
|
|
147
|
-
* an actionable message instead of a raw 4xx. `message` is plain text.
|
|
148
|
-
*/
|
|
149
61
|
function errorPage(message, loginPath) {
|
|
150
62
|
const safe = message
|
|
151
63
|
.replace(/&/g, "&")
|
|
@@ -166,47 +78,200 @@ function errorPage(message, loginPath) {
|
|
|
166
78
|
`</head><body><div class="card"><h1>Could not sign you in</h1>` +
|
|
167
79
|
`<p>${safe}</p><a href="${safeHref}">Back to sign in</a></div></body></html>`, 400);
|
|
168
80
|
}
|
|
169
|
-
/**
|
|
170
|
-
* Create a strong, random, NEVER-exposed credential for a JIT-created SSO
|
|
171
|
-
* user. It is used once by Better Auth's signup path and is intentionally
|
|
172
|
-
* unavailable to every login surface, including later SSO retries.
|
|
173
|
-
*/
|
|
174
81
|
function createUnusableSsoCredential() {
|
|
175
82
|
return `an-sso_${randomBytes(32).toString("base64url")}`;
|
|
176
83
|
}
|
|
84
|
+
function normalizeAuthority(raw) {
|
|
85
|
+
try {
|
|
86
|
+
const url = new URL(raw);
|
|
87
|
+
if ((url.protocol !== "https:" &&
|
|
88
|
+
!(url.protocol === "http:" && LOCALHOST_HOSTS.has(url.hostname))) ||
|
|
89
|
+
url.username ||
|
|
90
|
+
url.password ||
|
|
91
|
+
url.search ||
|
|
92
|
+
url.hash) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return `${url.protocol}//${url.host}${url.pathname}`.replace(/\/+$/, "");
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
void error;
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function resolveAppId(event) {
|
|
103
|
+
const configured = process.env.AGENT_NATIVE_APP_ID?.trim() ||
|
|
104
|
+
process.env.AGENT_NATIVE_WORKSPACE_APP_ID?.trim();
|
|
105
|
+
if (configured)
|
|
106
|
+
return configured;
|
|
107
|
+
const name = getAppName();
|
|
108
|
+
if (name && name !== "app")
|
|
109
|
+
return name;
|
|
110
|
+
try {
|
|
111
|
+
return new URL(getOrigin(event)).hostname.split(".")[0] || "app";
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return "app";
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function resolveClientId(appId) {
|
|
118
|
+
return process.env.AGENT_NATIVE_SSO_CLIENT_ID?.trim() || appId;
|
|
119
|
+
}
|
|
120
|
+
function verifierCookieName(state) {
|
|
121
|
+
return `${SSO_VERIFIER_COOKIE_PREFIX}${state}`;
|
|
122
|
+
}
|
|
123
|
+
function createPkceVerifier() {
|
|
124
|
+
return randomBytes(48).toString("base64url");
|
|
125
|
+
}
|
|
126
|
+
function createPkceChallenge(verifier) {
|
|
127
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
128
|
+
}
|
|
129
|
+
function requestUrl(event) {
|
|
130
|
+
return event.node?.req?.url ?? event.path ?? "/";
|
|
131
|
+
}
|
|
132
|
+
function setPkceVerifierCookie(event, state, verifier, secure) {
|
|
133
|
+
setCookie(event, verifierCookieName(state), verifier, {
|
|
134
|
+
httpOnly: true,
|
|
135
|
+
maxAge: Math.floor(SSO_STATE_TTL_MS / 1_000),
|
|
136
|
+
path: IDENTITY_SSO_CALLBACK_PATH,
|
|
137
|
+
sameSite: "lax",
|
|
138
|
+
secure,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function clearPkceVerifierCookie(event, state) {
|
|
142
|
+
deleteCookie(event, verifierCookieName(state), {
|
|
143
|
+
path: IDENTITY_SSO_CALLBACK_PATH,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function resolveClientBinding(event, hub) {
|
|
147
|
+
const appId = resolveAppId(event);
|
|
148
|
+
const clientId = resolveClientId(appId);
|
|
149
|
+
const redirectUri = `${getOrigin(event)}${IDENTITY_SSO_CALLBACK_PATH}`;
|
|
150
|
+
const authority = normalizeAuthority(hub);
|
|
151
|
+
if (!authority || !appId || !clientId || !redirectUri)
|
|
152
|
+
return null;
|
|
153
|
+
return { appId, clientId, redirectUri, authority };
|
|
154
|
+
}
|
|
177
155
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* - EXISTING email → the local `user` / `session` / existing `account`
|
|
182
|
-
* rows are NEVER read-modify-written. We only ADD (if absent) one
|
|
183
|
-
* federated-provider `account` row via Better Auth's OWN
|
|
184
|
-
* `internalAdapter.linkAccount` — so id, timestamps, and schema stay
|
|
185
|
-
* adapter-correct. The row is inert (no template path reads
|
|
186
|
-
* `provider_id = "agent-native"`), so removing the env var leaves zero
|
|
187
|
-
* behavioural residue.
|
|
188
|
-
* - NEW email → created via the SAME `auth.api.signUpEmail` path the app
|
|
189
|
-
* already uses (`maybeAutoCreateDevSession` uses the identical call), so
|
|
190
|
-
* the adapter creates the `user` (+ a schema-correct credential
|
|
191
|
-
* `account`) and `databaseHooks.user.create.after` (org auto-join,
|
|
192
|
-
* analytics) fires exactly as for a normal first-time signup. Idempotent
|
|
193
|
-
* under a concurrent create (the "already exists" failure is swallowed).
|
|
194
|
-
*
|
|
195
|
-
* Returns nothing — success is implied by not throwing. Account-link
|
|
196
|
-
* failures for an existing user are swallowed (the verified email already
|
|
197
|
-
* authenticated them; the link row is bookkeeping and must never block the
|
|
198
|
-
* session).
|
|
156
|
+
* The packaged Desktop SSO Canary may use Dispatch without each canonical app
|
|
157
|
+
* deploying the direct-web hub env. Ordinary browsers and self-hosted apps
|
|
158
|
+
* remain strictly env-gated.
|
|
199
159
|
*/
|
|
160
|
+
export function resolveIdentityHubUrl(event) {
|
|
161
|
+
const configured = getIdentityHubUrl();
|
|
162
|
+
if (configured)
|
|
163
|
+
return configured;
|
|
164
|
+
if (!isDesktopSsoCanaryUserAgent(getHeader(event, "user-agent"))) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
if (!isCanonicalAgentNativeAppRequest(getHeader(event, "host"), getHeader(event, "x-forwarded-proto"))) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
return DESKTOP_IDENTITY_HUB_URL;
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
void error;
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** Verify only the server-to-server assertion returned by Dispatch /token. */
|
|
179
|
+
async function verifyIdentityAssertion(assertion, binding) {
|
|
180
|
+
const secret = process.env.A2A_SECRET;
|
|
181
|
+
if (!secret || !assertion)
|
|
182
|
+
return null;
|
|
183
|
+
try {
|
|
184
|
+
const { payload } = await jose.jwtVerify(assertion, new TextEncoder().encode(secret), { audience: binding.redirectUri });
|
|
185
|
+
if (payload.scope !== IDENTITY_SSO_SCOPE)
|
|
186
|
+
return null;
|
|
187
|
+
if (payload.identity_client_id !== binding.clientId)
|
|
188
|
+
return null;
|
|
189
|
+
if (payload.redirect_uri !== binding.redirectUri)
|
|
190
|
+
return null;
|
|
191
|
+
if (payload.identity_authority !== binding.authority)
|
|
192
|
+
return null;
|
|
193
|
+
const issuer = typeof payload.iss === "string" ? normalizeAuthority(payload.iss) : null;
|
|
194
|
+
if (issuer !== binding.authority)
|
|
195
|
+
return null;
|
|
196
|
+
const email = typeof payload.email === "string" && payload.email.includes("@")
|
|
197
|
+
? payload.email.trim().toLowerCase()
|
|
198
|
+
: null;
|
|
199
|
+
if (!email)
|
|
200
|
+
return null;
|
|
201
|
+
const iat = typeof payload.iat === "number" ? payload.iat : null;
|
|
202
|
+
if (iat == null ||
|
|
203
|
+
iat > Date.now() / 1_000 + 60 ||
|
|
204
|
+
Date.now() / 1_000 - iat > MAX_ASSERTION_AGE_SECONDS) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
const jti = typeof payload.jti === "string" ? payload.jti : "";
|
|
208
|
+
if (!jti)
|
|
209
|
+
return null;
|
|
210
|
+
return {
|
|
211
|
+
email,
|
|
212
|
+
name: typeof payload.name === "string" && payload.name.trim()
|
|
213
|
+
? payload.name.trim()
|
|
214
|
+
: "",
|
|
215
|
+
orgDomain: typeof payload.org_domain === "string" && payload.org_domain
|
|
216
|
+
? payload.org_domain
|
|
217
|
+
: undefined,
|
|
218
|
+
sub: typeof payload.sub === "string" && payload.sub ? payload.sub : email,
|
|
219
|
+
jti,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
void error;
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async function exchangeIdentityCode(hub, binding, input) {
|
|
228
|
+
if (!CODE.test(input.code) || !STATE_PATTERN.test(input.state))
|
|
229
|
+
return null;
|
|
230
|
+
if (!CODE_VERIFIER.test(input.codeVerifier))
|
|
231
|
+
return null;
|
|
232
|
+
const tokenEndpoint = `${hub}${IDENTITY_SSO_TOKEN_PATH}`;
|
|
233
|
+
try {
|
|
234
|
+
const response = await fetch(tokenEndpoint, {
|
|
235
|
+
method: "POST",
|
|
236
|
+
headers: {
|
|
237
|
+
"content-type": "application/json",
|
|
238
|
+
accept: "application/json",
|
|
239
|
+
},
|
|
240
|
+
body: JSON.stringify({
|
|
241
|
+
grant_type: "authorization_code",
|
|
242
|
+
code: input.code,
|
|
243
|
+
state: input.state,
|
|
244
|
+
app_id: binding.appId,
|
|
245
|
+
client_id: binding.clientId,
|
|
246
|
+
redirect_uri: binding.redirectUri,
|
|
247
|
+
code_verifier: input.codeVerifier,
|
|
248
|
+
}),
|
|
249
|
+
redirect: "error",
|
|
250
|
+
});
|
|
251
|
+
if (!response.ok)
|
|
252
|
+
return null;
|
|
253
|
+
const body = (await response.json().catch((error) => {
|
|
254
|
+
void error;
|
|
255
|
+
return null;
|
|
256
|
+
}));
|
|
257
|
+
return typeof body?.assertion === "string" ? body.assertion : null;
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
void error;
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
200
264
|
async function jitLinkIdentity(identity) {
|
|
201
265
|
const adapter = await getBetterAuthInternalAdapter();
|
|
202
|
-
// Look up the local user via Better Auth's own adapter (read-only).
|
|
203
266
|
let existing = adapter
|
|
204
267
|
? await adapter
|
|
205
268
|
.findUserByEmail(identity.email, { includeAccounts: true })
|
|
206
|
-
.catch(() =>
|
|
269
|
+
.catch((error) => {
|
|
270
|
+
void error;
|
|
271
|
+
return null;
|
|
272
|
+
})
|
|
207
273
|
: null;
|
|
208
274
|
if (!existing) {
|
|
209
|
-
// No local user → create via the SAME signup path the app already uses.
|
|
210
275
|
const auth = await getBetterAuth();
|
|
211
276
|
try {
|
|
212
277
|
await auth.api.signUpEmail({
|
|
@@ -217,24 +282,23 @@ async function jitLinkIdentity(identity) {
|
|
|
217
282
|
},
|
|
218
283
|
});
|
|
219
284
|
}
|
|
220
|
-
catch (
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (!isExpectedAuthFailure(e))
|
|
224
|
-
throw e;
|
|
285
|
+
catch (error) {
|
|
286
|
+
if (!isExpectedAuthFailure(error))
|
|
287
|
+
throw error;
|
|
225
288
|
}
|
|
226
289
|
if (adapter) {
|
|
227
290
|
existing = await adapter
|
|
228
291
|
.findUserByEmail(identity.email, { includeAccounts: true })
|
|
229
|
-
.catch(() =>
|
|
292
|
+
.catch((error) => {
|
|
293
|
+
void error;
|
|
294
|
+
return null;
|
|
295
|
+
});
|
|
230
296
|
}
|
|
231
297
|
}
|
|
232
|
-
// ADD the inert federated-provider link iff a local user resolved and the
|
|
233
|
-
// link is absent. Better Auth's `linkAccount` is the additive, schema-
|
|
234
|
-
// correct API — we never UPDATE/DELETE/RENAME any identity row.
|
|
235
298
|
if (adapter && existing?.user?.id) {
|
|
236
299
|
const accountId = identity.sub || identity.email;
|
|
237
|
-
const alreadyLinked = (existing.accounts ?? []).some((
|
|
300
|
+
const alreadyLinked = (existing.accounts ?? []).some((account) => account.providerId === IDENTITY_SSO_PROVIDER_ID &&
|
|
301
|
+
account.accountId === accountId);
|
|
238
302
|
if (!alreadyLinked) {
|
|
239
303
|
try {
|
|
240
304
|
await adapter.linkAccount({
|
|
@@ -243,183 +307,121 @@ async function jitLinkIdentity(identity) {
|
|
|
243
307
|
accountId,
|
|
244
308
|
});
|
|
245
309
|
}
|
|
246
|
-
catch {
|
|
247
|
-
|
|
310
|
+
catch (error) {
|
|
311
|
+
void error;
|
|
312
|
+
// The verified email already authenticates the user. This inert,
|
|
313
|
+
// additive bookkeeping link must never block session creation.
|
|
248
314
|
}
|
|
249
315
|
}
|
|
250
316
|
}
|
|
251
317
|
}
|
|
252
|
-
|
|
253
|
-
* Verify the hub-issued identity JWT using the EXACT same path A2A / MCP use:
|
|
254
|
-
* `jose.jwtVerify(token, A2A_SECRET)`. `jwtVerify` enforces `exp`
|
|
255
|
-
* automatically. We additionally require:
|
|
256
|
-
* - `scope === "identity"` (so an A2A delegation token can't be replayed)
|
|
257
|
-
* - `aud` is THIS app's callback URL (so a token minted for one app cannot
|
|
258
|
-
* be replayed against another app's callback with a fresh state)
|
|
259
|
-
* - a non-empty `email` claim (the join key — comes ONLY from the verified
|
|
260
|
-
* token, never a query param)
|
|
261
|
-
* - issued no more than `MAX_TOKEN_AGE_SECONDS` ago (belt-and-braces on top
|
|
262
|
-
* of `exp` in case the hub mints long-lived tokens)
|
|
263
|
-
*
|
|
264
|
-
* Returns the verified identity, or `null` for ANY failure (bad signature,
|
|
265
|
-
* expired, wrong scope, missing email, malformed). The caller maps `null` to
|
|
266
|
-
* a generic error — it never leaks which check failed.
|
|
267
|
-
*/
|
|
268
|
-
async function verifyIdentityToken(token, expectedAudience) {
|
|
269
|
-
const secret = process.env.A2A_SECRET;
|
|
270
|
-
if (!secret || !token)
|
|
271
|
-
return null;
|
|
318
|
+
function safeRequestReturnPath(event) {
|
|
272
319
|
try {
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
return null;
|
|
276
|
-
const aud = payload.aud;
|
|
277
|
-
const audienceMatches = Array.isArray(aud)
|
|
278
|
-
? aud.includes(expectedAudience)
|
|
279
|
-
: aud === expectedAudience;
|
|
280
|
-
if (!audienceMatches)
|
|
281
|
-
return null;
|
|
282
|
-
if (typeof payload.redirect_uri === "string" &&
|
|
283
|
-
payload.redirect_uri !== expectedAudience) {
|
|
284
|
-
return null;
|
|
285
|
-
}
|
|
286
|
-
const email = typeof payload.email === "string" && payload.email.includes("@")
|
|
287
|
-
? payload.email.trim().toLowerCase()
|
|
288
|
-
: null;
|
|
289
|
-
if (!email)
|
|
290
|
-
return null;
|
|
291
|
-
const iat = typeof payload.iat === "number" ? payload.iat : undefined;
|
|
292
|
-
if (iat !== undefined && Date.now() / 1000 - iat > MAX_TOKEN_AGE_SECONDS) {
|
|
293
|
-
return null;
|
|
294
|
-
}
|
|
295
|
-
const sub = typeof payload.sub === "string" && payload.sub ? payload.sub : email;
|
|
296
|
-
return {
|
|
297
|
-
email,
|
|
298
|
-
name: typeof payload.name === "string" && payload.name.trim()
|
|
299
|
-
? payload.name.trim()
|
|
300
|
-
: "",
|
|
301
|
-
orgDomain: typeof payload.org_domain === "string" && payload.org_domain
|
|
302
|
-
? payload.org_domain
|
|
303
|
-
: undefined,
|
|
304
|
-
sub,
|
|
305
|
-
jti: typeof payload.jti === "string" && payload.jti
|
|
306
|
-
? payload.jti
|
|
307
|
-
: undefined,
|
|
308
|
-
};
|
|
320
|
+
const url = new URL(requestUrl(event), "http://an.invalid");
|
|
321
|
+
return safeReturnPath(url.searchParams.get("return"));
|
|
309
322
|
}
|
|
310
323
|
catch {
|
|
311
|
-
|
|
312
|
-
return null;
|
|
324
|
+
return "/";
|
|
313
325
|
}
|
|
314
326
|
}
|
|
315
|
-
// ---------------------------------------------------------------------------
|
|
316
|
-
// Route handler — single entry point; the core-routes-plugin dispatches the
|
|
317
|
-
// subpath, mirroring `handleMcpConnect`.
|
|
318
|
-
// ---------------------------------------------------------------------------
|
|
319
|
-
/**
|
|
320
|
-
* Handle a `/_agent-native/identity/*` request. `subpath` is the part after
|
|
321
|
-
* `/identity` (e.g. `/login`, `/callback`). Returns a 404 Response whenever
|
|
322
|
-
* the feature is disabled so an unset env var is a true no-op even if the
|
|
323
|
-
* route somehow gets mounted.
|
|
324
|
-
*/
|
|
325
327
|
export async function handleIdentitySso(event, subpath) {
|
|
326
328
|
const hub = resolveIdentityHubUrl(event);
|
|
327
|
-
if (!hub)
|
|
329
|
+
if (!hub)
|
|
328
330
|
return new Response("Not found", { status: 404 });
|
|
329
|
-
}
|
|
330
331
|
const method = getMethod(event);
|
|
331
332
|
const sub = ("/" + subpath.replace(/^\/+/, "").replace(/\/+$/, "")).replace(/^\/$/, "");
|
|
332
|
-
const origin = getOrigin(event);
|
|
333
333
|
const loginPath = SIGN_IN_ENTRY_PATH;
|
|
334
|
-
// ---- GET /login → 302 to the hub authorize endpoint ------------------
|
|
335
334
|
if (sub === "/login") {
|
|
336
335
|
if (method !== "GET" && method !== "HEAD") {
|
|
337
336
|
return new Response("Method not allowed", { status: 405 });
|
|
338
337
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
catch {
|
|
347
|
-
returnPath = "/";
|
|
348
|
-
}
|
|
349
|
-
if (existing?.email) {
|
|
338
|
+
const existing = await getSession(event).catch((error) => {
|
|
339
|
+
void error;
|
|
340
|
+
return null;
|
|
341
|
+
});
|
|
342
|
+
const returnPath = safeRequestReturnPath(event);
|
|
343
|
+
if (existing?.email)
|
|
350
344
|
return redirect(event, returnPath);
|
|
351
|
-
|
|
345
|
+
const binding = resolveClientBinding(event, hub);
|
|
346
|
+
if (!binding)
|
|
347
|
+
return errorPage("Federated sign-in is not configured.", loginPath);
|
|
348
|
+
const verifier = createPkceVerifier();
|
|
349
|
+
const challenge = createPkceChallenge(verifier);
|
|
352
350
|
let state;
|
|
353
351
|
try {
|
|
354
|
-
state = await createSsoState(
|
|
352
|
+
state = await createSsoState({
|
|
353
|
+
returnPath: returnPath === "/" ? null : returnPath,
|
|
354
|
+
...binding,
|
|
355
|
+
codeChallenge: challenge,
|
|
356
|
+
});
|
|
355
357
|
}
|
|
356
|
-
catch (
|
|
357
|
-
if (
|
|
358
|
+
catch (error) {
|
|
359
|
+
if (error?.message === "RATE_LIMITED") {
|
|
358
360
|
return errorPage("Too many sign-in attempts. Please wait a moment and try again.", loginPath);
|
|
359
361
|
}
|
|
360
362
|
return errorPage("Could not start federated sign-in. Please try again.", loginPath);
|
|
361
363
|
}
|
|
362
|
-
|
|
364
|
+
setPkceVerifierCookie(event, state, verifier, binding.redirectUri.startsWith("https://"));
|
|
363
365
|
const authorizeUrl = `${hub}/_agent-native/identity/authorize` +
|
|
364
|
-
`?
|
|
365
|
-
`&
|
|
366
|
-
`&
|
|
366
|
+
`?response_type=code` +
|
|
367
|
+
`&app=${encodeURIComponent(binding.appId)}` +
|
|
368
|
+
`&client_id=${encodeURIComponent(binding.clientId)}` +
|
|
369
|
+
`&redirect_uri=${encodeURIComponent(binding.redirectUri)}` +
|
|
370
|
+
`&state=${encodeURIComponent(state)}` +
|
|
371
|
+
`&code_challenge=${encodeURIComponent(challenge)}` +
|
|
372
|
+
`&code_challenge_method=S256`;
|
|
367
373
|
return redirect(event, authorizeUrl);
|
|
368
374
|
}
|
|
369
|
-
// ---- GET /callback → verify token, JIT-link, mint session ------------
|
|
370
375
|
if (sub === "/callback") {
|
|
371
376
|
if (method !== "GET" && method !== "HEAD") {
|
|
372
377
|
return new Response("Method not allowed", { status: 405 });
|
|
373
378
|
}
|
|
374
|
-
let
|
|
375
|
-
let
|
|
379
|
+
let code = "";
|
|
380
|
+
let state = "";
|
|
376
381
|
try {
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
stateParam = u.searchParams.get("state") || "";
|
|
382
|
+
const url = new URL(requestUrl(event), "http://an.invalid");
|
|
383
|
+
code = url.searchParams.get("code") || "";
|
|
384
|
+
state = url.searchParams.get("state") || "";
|
|
381
385
|
}
|
|
382
386
|
catch {
|
|
383
387
|
return errorPage("Malformed sign-in response.", loginPath);
|
|
384
388
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
+
if (!STATE_PATTERN.test(state) || !CODE.test(code)) {
|
|
390
|
+
return errorPage("Malformed sign-in response.", loginPath);
|
|
391
|
+
}
|
|
392
|
+
const binding = resolveClientBinding(event, hub);
|
|
393
|
+
const verifier = getCookie(event, verifierCookieName(state));
|
|
394
|
+
clearPkceVerifierCookie(event, state);
|
|
395
|
+
if (!binding || !verifier) {
|
|
396
|
+
return errorPage("Your sign-in session expired or was already used. Please try again.", loginPath);
|
|
397
|
+
}
|
|
398
|
+
const stateResult = await consumeSsoState(state, {
|
|
399
|
+
...binding,
|
|
400
|
+
codeChallenge: createPkceChallenge(verifier),
|
|
401
|
+
});
|
|
389
402
|
if (!stateResult.ok) {
|
|
390
403
|
return errorPage("Your sign-in session expired or was already used. Please try again.", loginPath);
|
|
391
404
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
405
|
+
const assertion = await exchangeIdentityCode(hub, binding, {
|
|
406
|
+
code,
|
|
407
|
+
state,
|
|
408
|
+
codeVerifier: verifier,
|
|
409
|
+
});
|
|
410
|
+
const identity = assertion
|
|
411
|
+
? await verifyIdentityAssertion(assertion, binding)
|
|
412
|
+
: null;
|
|
413
|
+
if (!identity || (await isJtiReplayed(identity.jti))) {
|
|
397
414
|
return errorPage("We could not verify the sign-in response. Please try again.", loginPath);
|
|
398
415
|
}
|
|
399
|
-
// Replay guard (best-effort, defence in depth on top of single-use
|
|
400
|
-
// state): reject a token whose jti we've already accepted.
|
|
401
|
-
if (await isJtiReplayed(identity.jti)) {
|
|
402
|
-
return errorPage("This sign-in link was already used. Please try again.", loginPath);
|
|
403
|
-
}
|
|
404
|
-
// Identity-hub federation is a separate provider from Google. Do not let
|
|
405
|
-
// it mint a legacy session that bypasses an org's Google-only policy.
|
|
406
416
|
if (await isGoogleSignInRequiredForEmail(identity.email)) {
|
|
407
417
|
return errorPage(GOOGLE_AUTH_REQUIRED_MESSAGE, loginPath);
|
|
408
418
|
}
|
|
409
|
-
// JIT link STRICTLY by verified email — additive only. Existing users
|
|
410
|
-
// are never mutated; new users are created via the app's own signup
|
|
411
|
-
// path; an inert federated `account` link is added via Better Auth's
|
|
412
|
-
// own adapter API. A failure here must not leave the user signed out
|
|
413
|
-
// mid-flow, so surface a retryable error rather than a half state.
|
|
414
419
|
try {
|
|
415
420
|
await jitLinkIdentity(identity);
|
|
416
421
|
}
|
|
417
422
|
catch {
|
|
418
423
|
return errorPage("Could not finish linking your account. Please try again.", loginPath);
|
|
419
424
|
}
|
|
420
|
-
// Mint a normal framework session EXACTLY the way the Google OAuth
|
|
421
|
-
// callback does (`createOAuthSession` → addSession + framework cookie).
|
|
422
|
-
// `hasProductionSession: false` so a fresh session cookie is always set.
|
|
423
425
|
try {
|
|
424
426
|
await createOAuthSession(event, identity.email, {
|
|
425
427
|
hasProductionSession: false,
|
|
@@ -428,9 +430,7 @@ export async function handleIdentitySso(event, subpath) {
|
|
|
428
430
|
catch {
|
|
429
431
|
return errorPage("Signed in, but could not start your session. Please try again.", loginPath);
|
|
430
432
|
}
|
|
431
|
-
|
|
432
|
-
const dest = safeReturnPath(stateResult.returnPath);
|
|
433
|
-
return redirect(event, dest);
|
|
433
|
+
return redirect(event, safeReturnPath(stateResult.returnPath));
|
|
434
434
|
}
|
|
435
435
|
if (sub === "/desktop-complete") {
|
|
436
436
|
if (method !== "GET" && method !== "HEAD") {
|
|
@@ -438,8 +438,8 @@ export async function handleIdentitySso(event, subpath) {
|
|
|
438
438
|
}
|
|
439
439
|
let nonce = "";
|
|
440
440
|
try {
|
|
441
|
-
|
|
442
|
-
|
|
441
|
+
nonce =
|
|
442
|
+
new URL(requestUrl(event), "http://an.invalid").searchParams.get("nonce") || "";
|
|
443
443
|
}
|
|
444
444
|
catch {
|
|
445
445
|
return new Response("Invalid completion request", { status: 400 });
|
|
@@ -447,7 +447,10 @@ export async function handleIdentitySso(event, subpath) {
|
|
|
447
447
|
if (!DESKTOP_COMPLETION_NONCE.test(nonce)) {
|
|
448
448
|
return new Response("Invalid completion request", { status: 400 });
|
|
449
449
|
}
|
|
450
|
-
const current = await getSession(event).catch(() =>
|
|
450
|
+
const current = await getSession(event).catch((error) => {
|
|
451
|
+
void error;
|
|
452
|
+
return null;
|
|
453
|
+
});
|
|
451
454
|
if (!current?.email) {
|
|
452
455
|
return new Response("Authentication required", { status: 401 });
|
|
453
456
|
}
|
|
@@ -456,21 +459,15 @@ export async function handleIdentitySso(event, subpath) {
|
|
|
456
459
|
"<title>Signed in</title></head><body>Signed in. You can close this window.</body></html>", {
|
|
457
460
|
status: 200,
|
|
458
461
|
headers: {
|
|
459
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
460
462
|
"Cache-Control": "no-store",
|
|
461
463
|
"Content-Security-Policy": "default-src 'none'; style-src 'none'",
|
|
464
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
462
465
|
"Referrer-Policy": "no-referrer",
|
|
463
466
|
},
|
|
464
467
|
});
|
|
465
468
|
}
|
|
466
469
|
return new Response("Not found", { status: 404 });
|
|
467
470
|
}
|
|
468
|
-
/**
|
|
469
|
-
* Whether the given (already base-path-stripped) request path is one of the
|
|
470
|
-
* two SSO routes that must bypass the blanket auth guard. The handler makes
|
|
471
|
-
* the request-scoped enablement decision and fails closed with 404; no other
|
|
472
|
-
* identity subpath bypasses authentication.
|
|
473
|
-
*/
|
|
474
471
|
export function isIdentitySsoBypassPath(p) {
|
|
475
472
|
if (!isIdentitySsoEnabled())
|
|
476
473
|
return false;
|