@agent-native/core 0.164.4 → 0.164.12
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/calendar/.agents/skills/event-management/SKILL.md +84 -0
- package/corpus/templates/calendar/AGENTS.md +5 -0
- package/corpus/templates/calendar/actions/delete-event.ts +1 -1
- package/corpus/templates/calendar/actions/delete-events.ts +508 -0
- package/corpus/templates/calendar/actions/event-action-helpers.ts +1 -1
- package/corpus/templates/calendar/actions/list-events.ts +53 -0
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +36 -47
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +3 -0
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +18 -1
- package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +256 -38
- package/corpus/templates/calendar/app/hooks/use-google-auth.ts +43 -3
- package/corpus/templates/calendar/app/i18n-data.ts +120 -0
- package/corpus/templates/calendar/app/lib/event-form-utils.ts +113 -0
- package/corpus/templates/calendar/changelog/2026-08-18-the-agent-can-now-remove-many-meetings-at-once-such-as-every.md +6 -0
- package/corpus/templates/calendar/server/handlers/google-auth.ts +119 -3
- package/corpus/templates/calendar/server/lib/event-weekday.ts +112 -0
- package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
- package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/calendar/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
- package/corpus/templates/calendar/server/routes/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/clips/app/components/player/video-player.tsx +12 -0
- package/corpus/templates/clips/app/routes/_app.dictate.tsx +18 -4
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +114 -20
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +83 -78
- package/corpus/templates/clips/changelog/2026-08-19-clips-nightly-builds-are-available-for-trying-the-latest-upd.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-19-social-preview-video-frames.md +1 -0
- package/corpus/templates/clips/desktop/README.md +19 -4
- package/corpus/templates/clips/desktop/src/app.tsx +78 -16
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +3 -2
- package/corpus/templates/clips/desktop/src-tauri/tauri.conf.json +1 -0
- package/corpus/templates/clips/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +41 -3
- package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +22 -1
- package/corpus/templates/clips/server/routes/api/clips-updater.json.get.ts +41 -22
- package/corpus/templates/factory/AGENTS.md +3 -2
- package/corpus/templates/factory/actions/get-factory-graph-version.ts +69 -0
- package/corpus/templates/factory/actions/list-factory-automations.ts +4 -0
- package/corpus/templates/factory/actions/list-factory-graph-versions.ts +82 -0
- package/corpus/templates/factory/actions/restore-factory-graph-version.ts +163 -0
- package/corpus/templates/factory/actions/save-factory-graph.ts +93 -60
- package/corpus/templates/factory/actions/start-builder-for-item.ts +19 -17
- package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +100 -36
- package/corpus/templates/factory/app/components/factory/FactoryHistoryView.tsx +709 -0
- package/corpus/templates/factory/app/components/factory/FactoryInspector.tsx +62 -2
- package/corpus/templates/factory/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/factory/app/i18n/en-US.ts +47 -0
- package/corpus/templates/factory/app/routes/factory.tsx +324 -61
- package/corpus/templates/factory/server/connectors/slack.ts +44 -2
- package/corpus/templates/factory/server/plugins/agent-chat.ts +12 -8
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +13 -0
- package/corpus/templates/factory/server/triage/slack-client.ts +5 -0
- package/corpus/templates/factory/server/triage/slack-poller.ts +72 -4
- package/corpus/templates/mail/actions/view-screen.ts +1 -1
- package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +100 -18
- package/corpus/templates/mail/server/handlers/google-auth.ts +125 -24
- package/corpus/templates/mail/server/lib/gmail-query.ts +5 -2
- package/corpus/templates/mail/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
- package/corpus/templates/mail/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/mail/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
- package/corpus/templates/mail/server/routes/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/plan/app/pages/PlansPage.tsx +0 -12
- package/corpus/templates/plan/changelog/2026-08-19-plan-prompts-now-guide-you-to-connect-ai-before-sending-and-.md +6 -0
- package/corpus/templates/slides/actions/approve-deck-access-request.ts +234 -0
- package/corpus/templates/slides/actions/get-deck-access-status.ts +35 -2
- package/corpus/templates/slides/actions/request-deck-access.ts +263 -64
- package/corpus/templates/slides/app/components/editor/MissingDeckAccessPane.tsx +196 -0
- package/corpus/templates/slides/app/components/editor/RequestDeckAccessDialog.tsx +101 -0
- package/corpus/templates/slides/app/hooks/use-deck-access.ts +7 -4
- package/corpus/templates/slides/app/i18n/en-US.ts +29 -6
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +147 -169
- package/corpus/templates/slides/app/routes/access-request.approve.tsx +250 -0
- package/corpus/templates/slides/server/db/schema.ts +8 -0
- package/corpus/templates/slides/server/lib/access-request-email.ts +36 -0
- package/corpus/templates/slides/server/lib/emails.ts +26 -0
- package/corpus/templates/slides/server/plugins/agent-chat.ts +7 -0
- package/corpus/templates/slides/server/plugins/db.ts +50 -0
- package/corpus/templates/slides/shared/deck-access.ts +31 -0
- package/dist/a2a/task-store.d.ts +1 -0
- package/dist/a2a/task-store.js +1 -1
- package/dist/agent/observational-memory/store.d.ts +1 -0
- package/dist/agent/observational-memory/store.js +1 -1
- package/dist/agent/run-store.d.ts +1 -0
- package/dist/agent/run-store.js +1 -1
- package/dist/agent/tool-approval-store.d.ts +1 -0
- package/dist/agent/tool-approval-store.js +1 -1
- package/dist/application-state/store.d.ts +1 -0
- package/dist/application-state/store.js +1 -1
- package/dist/browser-sessions/store.d.ts +1 -0
- package/dist/browser-sessions/store.js +1 -1
- package/dist/checkpoints/store.d.ts +1 -0
- package/dist/checkpoints/store.js +1 -1
- package/dist/cli/code-agent-schedules.d.ts +11 -0
- package/dist/cli/code-agent-schedules.js +44 -5
- package/dist/client/AgentPanel.d.ts +5 -1
- package/dist/client/AgentPanel.js +2 -2
- package/dist/client/AssistantChat.d.ts +5 -1
- package/dist/client/AssistantChat.js +40 -6
- package/dist/client/EnvironmentBadge.d.ts +7 -13
- package/dist/client/EnvironmentBadge.js +43 -92
- package/dist/client/agent-chat/index.d.ts +1 -0
- package/dist/client/agent-chat/index.js +1 -0
- package/dist/client/agent-chat-adapter.js +76 -50
- package/dist/client/app-providers.js +4 -4
- package/dist/client/chat/message-components.d.ts +4 -0
- package/dist/client/chat/message-components.js +28 -1
- package/dist/client/chat/run-recovery.js +26 -0
- package/dist/client/chat-first/apps-rail.d.ts +1 -0
- package/dist/client/chat-first/apps-rail.js +28 -2
- package/dist/client/sse-event-processor.js +13 -8
- package/dist/client/use-active-agent-chat-run.d.ts +2 -0
- package/dist/client/use-active-agent-chat-run.js +22 -0
- package/dist/coding-tools/sandbox/executions-store.d.ts +1 -0
- package/dist/coding-tools/sandbox/executions-store.js +1 -1
- package/dist/collab/awareness-store.d.ts +1 -0
- package/dist/collab/awareness-store.js +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/storage.d.ts +1 -0
- package/dist/collab/storage.js +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/client.js +3 -3
- package/dist/db/ddl-guard.js +8 -0
- package/dist/db/migration-runtime.d.ts +23 -0
- package/dist/db/migration-runtime.js +41 -0
- package/dist/db/migrations.d.ts +1 -2
- package/dist/db/migrations.js +2 -24
- package/dist/email-catalog/log.d.ts +1 -0
- package/dist/email-catalog/log.js +1 -1
- package/dist/guards/index.d.ts +1 -0
- package/dist/guards/index.js +1 -0
- package/dist/guards/release-schema-complete.d.ts +22 -0
- package/dist/guards/release-schema-complete.js +144 -0
- package/dist/integrations/a2a-continuations-store.d.ts +1 -0
- package/dist/integrations/a2a-continuations-store.js +1 -1
- package/dist/integrations/awaiting-input-store.d.ts +1 -0
- package/dist/integrations/awaiting-input-store.js +1 -1
- package/dist/integrations/config-store.d.ts +1 -0
- package/dist/integrations/config-store.js +1 -1
- package/dist/integrations/controls-store.d.ts +1 -0
- package/dist/integrations/controls-store.js +1 -1
- package/dist/integrations/identity-links-store.d.ts +1 -0
- package/dist/integrations/identity-links-store.js +1 -1
- package/dist/integrations/installations-store.d.ts +1 -0
- package/dist/integrations/installations-store.js +1 -1
- package/dist/integrations/remote-commands-store.d.ts +1 -0
- package/dist/integrations/remote-commands-store.js +1 -1
- package/dist/integrations/remote-devices-store.d.ts +1 -0
- package/dist/integrations/remote-devices-store.js +1 -1
- package/dist/integrations/remote-push-store.d.ts +1 -0
- package/dist/integrations/remote-push-store.js +1 -1
- package/dist/integrations/remote-run-events-store.d.ts +1 -0
- package/dist/integrations/remote-run-events-store.js +1 -1
- package/dist/integrations/scope-store.d.ts +1 -0
- package/dist/integrations/scope-store.js +1 -1
- package/dist/integrations/thread-mapping-store.d.ts +1 -0
- package/dist/integrations/thread-mapping-store.js +1 -1
- package/dist/integrations/usage-budget-store.d.ts +1 -0
- package/dist/integrations/usage-budget-store.js +1 -1
- package/dist/jobs/background-automation-runner.js +111 -9
- package/dist/jobs/run-history.d.ts +1 -0
- package/dist/jobs/run-history.js +1 -1
- package/dist/jobs/scheduler-health.d.ts +1 -0
- package/dist/jobs/scheduler-health.js +1 -1
- package/dist/mcp/approval-store.d.ts +1 -0
- package/dist/mcp/approval-store.js +1 -1
- package/dist/mcp/connect-store.d.ts +1 -0
- package/dist/mcp/connect-store.js +1 -1
- package/dist/mcp/oauth-store.d.ts +1 -0
- package/dist/mcp/oauth-store.js +1 -1
- package/dist/notifications/store.d.ts +1 -0
- package/dist/notifications/store.js +1 -1
- package/dist/oauth-tokens/store.d.ts +1 -0
- package/dist/oauth-tokens/store.js +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/progress/store.d.ts +1 -0
- package/dist/progress/store.js +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +5 -5
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/provider-api/corpus-jobs-store.d.ts +1 -0
- package/dist/provider-api/corpus-jobs-store.js +1 -1
- package/dist/provider-api/custom-registry.d.ts +1 -0
- package/dist/provider-api/custom-registry.js +1 -1
- package/dist/provider-api/quota-governor.d.ts +1 -0
- package/dist/provider-api/quota-governor.js +1 -1
- package/dist/provider-api/staged-datasets-store.d.ts +1 -0
- package/dist/provider-api/staged-datasets-store.js +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/resources/store.d.ts +1 -0
- package/dist/resources/store.js +1 -1
- package/dist/scripts/dev/index.js +15 -1
- package/dist/search/index.js +3 -0
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/secrets/storage.d.ts +1 -0
- package/dist/secrets/storage.js +1 -1
- package/dist/server/agent-chat-plugin.js +38 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-teams-run-queue.d.ts +1 -0
- package/dist/server/agent-teams-run-queue.js +1 -1
- package/dist/server/auth-marketing.js +3 -1
- package/dist/server/auth.d.ts +18 -1
- package/dist/server/auth.js +370 -154
- package/dist/server/embed-session.d.ts +1 -0
- package/dist/server/embed-session.js +1 -1
- package/dist/server/google-auth-plugin.js +3 -1
- package/dist/server/google-oauth.d.ts +10 -0
- package/dist/server/google-oauth.js +25 -1
- package/dist/server/identity-sso-store.d.ts +1 -0
- package/dist/server/identity-sso-store.js +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/onboarding-html.js +200 -31
- package/dist/server/poll.d.ts +2 -0
- package/dist/server/poll.js +19 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/release-migrations.js +5 -0
- package/dist/server/release-schema.d.ts +46 -0
- package/dist/server/release-schema.js +307 -0
- package/dist/settings/store.d.ts +1 -0
- package/dist/settings/store.js +1 -1
- package/dist/shared/environment-lanes.d.ts +28 -0
- package/dist/shared/environment-lanes.js +61 -0
- package/dist/sharing/actions/share-resource.js +22 -2
- package/dist/styles/agent-native.css +17 -15
- package/dist/triggers/routes.d.ts +11 -0
- package/dist/triggers/routes.js +10 -0
- package/dist/usage/alerts-store.d.ts +1 -0
- package/dist/usage/alerts-store.js +1 -1
- package/dist/usage/store.d.ts +1 -0
- package/dist/usage/store.js +1 -1
- package/docs/content/actions-access-control.mdx +136 -2
- package/docs/content/actions-agent-tools.mdx +101 -0
- package/docs/content/actions-defining.mdx +2 -2
- package/docs/content/audit-log.mdx +1 -1
- package/docs/content/authentication.mdx +1 -1
- package/docs/content/cloudflare.mdx +43 -0
- package/docs/content/creating-templates.mdx +1 -1
- package/docs/content/deploy-an-app.mdx +109 -0
- package/docs/content/deployment-environment-variables.mdx +101 -0
- package/docs/content/deployment.mdx +125 -792
- package/docs/content/docs-components.mdx +2 -2
- package/docs/content/environment-variables.mdx +41 -41
- package/docs/content/file-uploads.mdx +74 -40
- package/docs/content/harness-agents.mdx +2 -2
- package/docs/content/internationalization.mdx +61 -65
- package/docs/content/key-concepts.mdx +3 -3
- package/docs/content/multi-app-workspace.mdx +2 -2
- package/docs/content/netlify.mdx +47 -0
- package/docs/content/node-docker.mdx +138 -0
- package/docs/content/organizations-teams-permissions.mdx +5 -5
- package/docs/content/other-platforms.mdx +27 -0
- package/docs/content/ssr-caching.mdx +45 -0
- package/docs/content/template-assets-developers.mdx +2 -2
- package/docs/content/template-chat-developers.mdx +2 -2
- package/docs/content/template-content-developers.mdx +2 -2
- package/docs/content/{local-file-mode.mdx → template-content-local-files.mdx} +2 -1
- package/docs/content/template-content-sync.mdx +2 -2
- package/docs/content/toolkit-agent-ux.mdx +1 -1
- package/docs/content/toolkit-resources.mdx +2 -2
- package/docs/content/updating-ui-in-production.mdx +35 -0
- package/docs/content/vercel.mdx +38 -0
- package/docs/content/what-is-agent-native.mdx +1 -1
- package/docs/content/workspace-deployment.mdx +350 -0
- package/package.json +2 -2
- package/docs/content/agents.mdx +0 -9
- package/docs/content/database.mdx +0 -317
- package/docs/content/human-approval.mdx +0 -156
package/dist/server/auth.js
CHANGED
|
@@ -842,7 +842,7 @@ export function isExpectedAuthFailure(error) {
|
|
|
842
842
|
// ---------------------------------------------------------------------------
|
|
843
843
|
let _sessionInitPromise;
|
|
844
844
|
let sessionMaxAge = DEFAULT_MAX_AGE;
|
|
845
|
-
async function ensureSessionTable() {
|
|
845
|
+
export async function ensureSessionTable() {
|
|
846
846
|
if (!_sessionInitPromise) {
|
|
847
847
|
_sessionInitPromise = (async () => {
|
|
848
848
|
const client = getDbExec();
|
|
@@ -1115,6 +1115,7 @@ const _desktopExchanges = new Map();
|
|
|
1115
1115
|
const DESKTOP_EXCHANGE_ERROR_PREFIX = "__error__::";
|
|
1116
1116
|
const DESKTOP_MAGIC_LINK_CHALLENGE_PREFIX = "__magic-link-challenge__::";
|
|
1117
1117
|
const DESKTOP_MAGIC_LINK_EXCHANGE_PREFIX = "__magic-link-exchange__::";
|
|
1118
|
+
export const DESKTOP_OAUTH_BROWSER_BINDING_COOKIE = "an_desktop_oauth_binding";
|
|
1118
1119
|
const DESKTOP_AUTH_TOKEN_BODY_ORIGINS = new Set([
|
|
1119
1120
|
"tauri://localhost",
|
|
1120
1121
|
"http://tauri.localhost",
|
|
@@ -1138,6 +1139,12 @@ function normalizeDesktopFlowVerifier(value) {
|
|
|
1138
1139
|
function desktopFlowVerifierHash(verifier) {
|
|
1139
1140
|
return crypto.createHash("sha256").update(verifier).digest("base64url");
|
|
1140
1141
|
}
|
|
1142
|
+
function matchesDesktopHash(actualHash, expectedHash) {
|
|
1143
|
+
const actual = Buffer.from(actualHash);
|
|
1144
|
+
const expected = Buffer.from(expectedHash);
|
|
1145
|
+
return (actual.length === expected.length &&
|
|
1146
|
+
crypto.timingSafeEqual(actual, expected));
|
|
1147
|
+
}
|
|
1141
1148
|
function matchesDesktopFlowVerifier(verifier, expectedHash) {
|
|
1142
1149
|
const actual = Buffer.from(desktopFlowVerifierHash(verifier));
|
|
1143
1150
|
const expected = Buffer.from(expectedHash);
|
|
@@ -1145,31 +1152,63 @@ function matchesDesktopFlowVerifier(verifier, expectedHash) {
|
|
|
1145
1152
|
crypto.timingSafeEqual(actual, expected));
|
|
1146
1153
|
}
|
|
1147
1154
|
function parseDesktopExchangeStoredEntry(packed) {
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1155
|
+
try {
|
|
1156
|
+
if (packed.startsWith(DESKTOP_MAGIC_LINK_CHALLENGE_PREFIX)) {
|
|
1157
|
+
const raw = packed.slice(DESKTOP_MAGIC_LINK_CHALLENGE_PREFIX.length);
|
|
1158
|
+
const parts = raw.split("::");
|
|
1159
|
+
if (parts.length > 2)
|
|
1160
|
+
return null;
|
|
1161
|
+
const [verifierHash, browserBindingHash] = parts;
|
|
1162
|
+
if (!isValidDesktopFlowVerifierHash(verifierHash))
|
|
1163
|
+
return null;
|
|
1164
|
+
if (browserBindingHash !== undefined &&
|
|
1165
|
+
!isValidDesktopFlowVerifierHash(browserBindingHash)) {
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
return {
|
|
1169
|
+
challenge: true,
|
|
1170
|
+
verifierHash,
|
|
1171
|
+
...(browserBindingHash ? { browserBindingHash } : {}),
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
if (packed.startsWith(DESKTOP_EXCHANGE_ERROR_PREFIX)) {
|
|
1175
|
+
const raw = packed.slice(DESKTOP_EXCHANGE_ERROR_PREFIX.length);
|
|
1176
|
+
const parsed = JSON.parse(Buffer.from(raw, "base64url").toString());
|
|
1177
|
+
if (!parsed ||
|
|
1178
|
+
typeof parsed !== "object" ||
|
|
1179
|
+
typeof parsed.message !== "string") {
|
|
1180
|
+
return null;
|
|
1181
|
+
}
|
|
1182
|
+
return { error: parsed };
|
|
1183
|
+
}
|
|
1184
|
+
const isMagicLinkExchange = packed.startsWith(DESKTOP_MAGIC_LINK_EXCHANGE_PREFIX);
|
|
1185
|
+
const encoded = isMagicLinkExchange
|
|
1186
|
+
? packed.slice(DESKTOP_MAGIC_LINK_EXCHANGE_PREFIX.length)
|
|
1187
|
+
: packed;
|
|
1188
|
+
const verifierSeparator = isMagicLinkExchange ? encoded.indexOf("::") : -1;
|
|
1189
|
+
const verifierHash = verifierSeparator >= 0 ? encoded.slice(0, verifierSeparator) : undefined;
|
|
1190
|
+
if (verifierHash !== undefined &&
|
|
1191
|
+
!isValidDesktopFlowVerifierHash(verifierHash)) {
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
const tokenAndEmail = verifierSeparator >= 0 ? encoded.slice(verifierSeparator + 2) : encoded;
|
|
1195
|
+
const sepIdx = tokenAndEmail.indexOf("::");
|
|
1196
|
+
if (sepIdx <= 0 || sepIdx === tokenAndEmail.length - 2)
|
|
1197
|
+
return null;
|
|
1198
|
+
const token = tokenAndEmail.slice(0, sepIdx);
|
|
1199
|
+
const email = tokenAndEmail.slice(sepIdx + 2);
|
|
1200
|
+
if (!token || !email)
|
|
1201
|
+
return null;
|
|
1154
1202
|
return {
|
|
1155
|
-
|
|
1203
|
+
token,
|
|
1204
|
+
email,
|
|
1205
|
+
...(verifierHash ? { verifierHash } : {}),
|
|
1156
1206
|
};
|
|
1157
1207
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
? packed.slice(DESKTOP_MAGIC_LINK_EXCHANGE_PREFIX.length)
|
|
1161
|
-
: packed;
|
|
1162
|
-
const verifierSeparator = isMagicLinkExchange ? encoded.indexOf("::") : -1;
|
|
1163
|
-
const verifierHash = verifierSeparator >= 0 ? encoded.slice(0, verifierSeparator) : undefined;
|
|
1164
|
-
const tokenAndEmail = verifierSeparator >= 0 ? encoded.slice(verifierSeparator + 2) : encoded;
|
|
1165
|
-
const sepIdx = tokenAndEmail.indexOf("::");
|
|
1166
|
-
if (sepIdx === -1)
|
|
1208
|
+
catch {
|
|
1209
|
+
// coercion-ok: malformed desktop exchange payloads are rejected as absent.
|
|
1167
1210
|
return null;
|
|
1168
|
-
|
|
1169
|
-
token: tokenAndEmail.slice(0, sepIdx),
|
|
1170
|
-
email: tokenAndEmail.slice(sepIdx + 2),
|
|
1171
|
-
...(verifierHash ? { verifierHash } : {}),
|
|
1172
|
-
};
|
|
1211
|
+
}
|
|
1173
1212
|
}
|
|
1174
1213
|
function isDesktopMagicLinkCallbackPath(value) {
|
|
1175
1214
|
try {
|
|
@@ -1186,20 +1225,49 @@ function withDesktopMagicLinkFlow(callbackURL, flow) {
|
|
|
1186
1225
|
url.searchParams.set("verifier", flow.verifier);
|
|
1187
1226
|
return `${url.pathname}${url.search}${url.hash}`;
|
|
1188
1227
|
}
|
|
1189
|
-
async function persistDesktopMagicLinkChallenge(flowId, verifierHash) {
|
|
1190
|
-
await addSession(`dex:${flowId}`, `${DESKTOP_MAGIC_LINK_CHALLENGE_PREFIX}${verifierHash}`);
|
|
1228
|
+
async function persistDesktopMagicLinkChallenge(flowId, verifierHash, browserBindingHash) {
|
|
1229
|
+
await addSession(`dex:${flowId}`, `${DESKTOP_MAGIC_LINK_CHALLENGE_PREFIX}${verifierHash}${browserBindingHash ? `::${browserBindingHash}` : ""}`);
|
|
1230
|
+
}
|
|
1231
|
+
function isValidDesktopFlowVerifierHash(value) {
|
|
1232
|
+
return typeof value === "string" && /^[A-Za-z0-9_-]{43}$/.test(value);
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Create or reuse the browser-local binding for a desktop Google OAuth flow.
|
|
1236
|
+
* The raw value stays in an HttpOnly cookie; only its hash travels in signed
|
|
1237
|
+
* OAuth state and the exchange challenge. This prevents a caller from
|
|
1238
|
+
* initiating a flow and handing its authorization URL to another browser.
|
|
1239
|
+
*/
|
|
1240
|
+
export function prepareDesktopOAuthBrowserBinding(event) {
|
|
1241
|
+
let binding = getCookie(event, DESKTOP_OAUTH_BROWSER_BINDING_COOKIE);
|
|
1242
|
+
if (!binding || !/^[A-Za-z0-9_-]{43}$/.test(binding)) {
|
|
1243
|
+
binding = crypto.randomBytes(32).toString("base64url");
|
|
1244
|
+
setCookie(event, DESKTOP_OAUTH_BROWSER_BINDING_COOKIE, binding, {
|
|
1245
|
+
...crossSiteCookieAttrs(event),
|
|
1246
|
+
httpOnly: true,
|
|
1247
|
+
path: "/",
|
|
1248
|
+
maxAge: Math.floor(DESKTOP_EXCHANGE_TTL_MS / 1_000),
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
return desktopFlowVerifierHash(binding);
|
|
1252
|
+
}
|
|
1253
|
+
/** Verify that a desktop OAuth callback is returning to its initiating browser. */
|
|
1254
|
+
export function matchesDesktopOAuthBrowserBinding(event, expectedHash) {
|
|
1255
|
+
const binding = getCookie(event, DESKTOP_OAUTH_BROWSER_BINDING_COOKIE);
|
|
1256
|
+
return Boolean(binding &&
|
|
1257
|
+
isValidDesktopFlowVerifierHash(expectedHash) &&
|
|
1258
|
+
matchesDesktopFlowVerifier(binding, expectedHash));
|
|
1191
1259
|
}
|
|
1192
1260
|
async function issueDesktopMagicLinkFlow() {
|
|
1193
1261
|
const flowId = crypto.randomBytes(32).toString("base64url");
|
|
1194
1262
|
const verifier = crypto.randomBytes(32).toString("base64url");
|
|
1195
1263
|
const verifierHash = desktopFlowVerifierHash(verifier);
|
|
1196
|
-
_desktopExchanges.set(flowId, {
|
|
1197
|
-
challenge: true,
|
|
1198
|
-
verifierHash,
|
|
1199
|
-
expiresAt: Date.now() + DESKTOP_EXCHANGE_TTL_MS,
|
|
1200
|
-
});
|
|
1201
1264
|
try {
|
|
1202
1265
|
await persistDesktopMagicLinkChallenge(flowId, verifierHash);
|
|
1266
|
+
_desktopExchanges.set(flowId, {
|
|
1267
|
+
challenge: true,
|
|
1268
|
+
verifierHash,
|
|
1269
|
+
expiresAt: Date.now() + DESKTOP_EXCHANGE_TTL_MS,
|
|
1270
|
+
});
|
|
1203
1271
|
}
|
|
1204
1272
|
catch (error) {
|
|
1205
1273
|
_desktopExchanges.delete(flowId);
|
|
@@ -1207,16 +1275,88 @@ async function issueDesktopMagicLinkFlow() {
|
|
|
1207
1275
|
}
|
|
1208
1276
|
return { flowId, verifier };
|
|
1209
1277
|
}
|
|
1210
|
-
export function setDesktopExchange(flowId, token, email) {
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
//
|
|
1217
|
-
//
|
|
1218
|
-
|
|
1219
|
-
|
|
1278
|
+
export async function setDesktopExchange(flowId, token, email, verifierHash) {
|
|
1279
|
+
if (!isValidDesktopFlowVerifierHash(verifierHash)) {
|
|
1280
|
+
throw new Error("Invalid desktop exchange challenge.");
|
|
1281
|
+
}
|
|
1282
|
+
// Persist before publishing anything to the in-memory cache. The DB is the
|
|
1283
|
+
// durable source of truth for one-time tokens; publishing first would let a
|
|
1284
|
+
// same-instance poll succeed while a delayed write recreated a token after
|
|
1285
|
+
// it had already been consumed.
|
|
1286
|
+
await persistDesktopExchangeToDB(flowId, token, email, verifierHash);
|
|
1287
|
+
_desktopExchanges.delete(flowId);
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Register the client-held verifier for a desktop OAuth flow before Google
|
|
1291
|
+
* sees the request. Only the verifier hash is persisted; the initiating
|
|
1292
|
+
* browser keeps the raw verifier and presents it when polling the exchange.
|
|
1293
|
+
*/
|
|
1294
|
+
export async function registerDesktopExchange(flowId, verifier, browserBindingHash) {
|
|
1295
|
+
const normalizedFlowId = normalizeDesktopFlowId(flowId);
|
|
1296
|
+
const normalizedVerifier = normalizeDesktopFlowVerifier(verifier);
|
|
1297
|
+
if (!normalizedFlowId || !normalizedVerifier) {
|
|
1298
|
+
throw new Error("Invalid desktop exchange challenge.");
|
|
1299
|
+
}
|
|
1300
|
+
if (browserBindingHash !== undefined &&
|
|
1301
|
+
!isValidDesktopFlowVerifierHash(browserBindingHash)) {
|
|
1302
|
+
throw new Error("Invalid desktop exchange browser binding.");
|
|
1303
|
+
}
|
|
1304
|
+
const verifierHash = desktopFlowVerifierHash(normalizedVerifier);
|
|
1305
|
+
const current = _desktopExchanges.get(normalizedFlowId);
|
|
1306
|
+
if (current && current.expiresAt >= Date.now()) {
|
|
1307
|
+
if ("challenge" in current &&
|
|
1308
|
+
matchesDesktopFlowVerifier(normalizedVerifier, current.verifierHash)) {
|
|
1309
|
+
if (browserBindingHash !== undefined &&
|
|
1310
|
+
(!current.browserBindingHash ||
|
|
1311
|
+
!matchesDesktopHash(current.browserBindingHash, browserBindingHash))) {
|
|
1312
|
+
throw new Error("Desktop exchange flow is already in use.");
|
|
1313
|
+
}
|
|
1314
|
+
return current.verifierHash;
|
|
1315
|
+
}
|
|
1316
|
+
throw new Error("Desktop exchange flow is already in use.");
|
|
1317
|
+
}
|
|
1318
|
+
const stored = await readDesktopExchangeFromDB(normalizedFlowId);
|
|
1319
|
+
if (stored.status === "unavailable") {
|
|
1320
|
+
throw new Error("Desktop exchange storage is unavailable.");
|
|
1321
|
+
}
|
|
1322
|
+
if (stored.status === "malformed") {
|
|
1323
|
+
throw new Error("Desktop exchange storage is invalid.");
|
|
1324
|
+
}
|
|
1325
|
+
if (stored.status === "entry") {
|
|
1326
|
+
const storedEntry = stored.entry;
|
|
1327
|
+
if ("challenge" in storedEntry &&
|
|
1328
|
+
matchesDesktopFlowVerifier(normalizedVerifier, storedEntry.verifierHash)) {
|
|
1329
|
+
if (browserBindingHash !== undefined &&
|
|
1330
|
+
(!storedEntry.browserBindingHash ||
|
|
1331
|
+
!matchesDesktopHash(storedEntry.browserBindingHash, browserBindingHash))) {
|
|
1332
|
+
throw new Error("Desktop exchange flow is already in use.");
|
|
1333
|
+
}
|
|
1334
|
+
_desktopExchanges.set(normalizedFlowId, {
|
|
1335
|
+
challenge: true,
|
|
1336
|
+
verifierHash: storedEntry.verifierHash,
|
|
1337
|
+
...(storedEntry.browserBindingHash
|
|
1338
|
+
? { browserBindingHash: storedEntry.browserBindingHash }
|
|
1339
|
+
: {}),
|
|
1340
|
+
expiresAt: Date.now() + DESKTOP_EXCHANGE_TTL_MS,
|
|
1341
|
+
});
|
|
1342
|
+
return storedEntry.verifierHash;
|
|
1343
|
+
}
|
|
1344
|
+
throw new Error("Desktop exchange flow is already in use.");
|
|
1345
|
+
}
|
|
1346
|
+
try {
|
|
1347
|
+
await persistDesktopMagicLinkChallenge(normalizedFlowId, verifierHash, browserBindingHash);
|
|
1348
|
+
_desktopExchanges.set(normalizedFlowId, {
|
|
1349
|
+
challenge: true,
|
|
1350
|
+
verifierHash,
|
|
1351
|
+
...(browserBindingHash ? { browserBindingHash } : {}),
|
|
1352
|
+
expiresAt: Date.now() + DESKTOP_EXCHANGE_TTL_MS,
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
catch (error) {
|
|
1356
|
+
_desktopExchanges.delete(normalizedFlowId);
|
|
1357
|
+
throw error;
|
|
1358
|
+
}
|
|
1359
|
+
return verifierHash;
|
|
1220
1360
|
}
|
|
1221
1361
|
export function setDesktopExchangeError(flowId, error) {
|
|
1222
1362
|
_desktopExchanges.set(flowId, {
|
|
@@ -1230,19 +1370,15 @@ export function setDesktopExchangeError(flowId, error) {
|
|
|
1230
1370
|
* cross-instance routing (e.g. Cloudflare Workers). Stored under a synthetic
|
|
1231
1371
|
* token key "dex:{flowId}"; the `email` column packs both the real session
|
|
1232
1372
|
* token and the user email so they can be recovered in one query.
|
|
1233
|
-
*
|
|
1234
|
-
*
|
|
1373
|
+
* Fail closed when the durable exchange cannot be written. A token must not
|
|
1374
|
+
* be published only in memory because the callback and poll can land on
|
|
1375
|
+
* different instances.
|
|
1235
1376
|
*/
|
|
1236
1377
|
async function persistDesktopExchangeToDB(flowId, token, email, verifierHash) {
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
await addSession(`dex:${flowId}`, packed);
|
|
1242
|
-
}
|
|
1243
|
-
catch {
|
|
1244
|
-
// non-fatal — in-memory Map is the primary path
|
|
1245
|
-
}
|
|
1378
|
+
const packed = verifierHash
|
|
1379
|
+
? `${DESKTOP_MAGIC_LINK_EXCHANGE_PREFIX}${verifierHash}::${token}::${email}`
|
|
1380
|
+
: `${token}::${email}`;
|
|
1381
|
+
await addSession(`dex:${flowId}`, packed);
|
|
1246
1382
|
}
|
|
1247
1383
|
async function persistDesktopExchangeErrorToDB(flowId, error) {
|
|
1248
1384
|
try {
|
|
@@ -1253,41 +1389,35 @@ async function persistDesktopExchangeErrorToDB(flowId, error) {
|
|
|
1253
1389
|
// non-fatal — in-memory Map is the primary path
|
|
1254
1390
|
}
|
|
1255
1391
|
}
|
|
1256
|
-
/**
|
|
1257
|
-
|
|
1258
|
-
* Returns null if not found or already consumed.
|
|
1259
|
-
*/
|
|
1260
|
-
async function consumeDesktopExchangeFromDB(flowId) {
|
|
1392
|
+
/** Read the durable exchange without mutating it. */
|
|
1393
|
+
async function readDesktopExchangeFromDB(flowId) {
|
|
1261
1394
|
try {
|
|
1262
|
-
// Atomic DELETE...RETURNING prevents token replay: two concurrent polls
|
|
1263
|
-
// cannot both retrieve the token because only one DELETE will match the row.
|
|
1264
|
-
// SQLite ≥3.35 and PostgreSQL both support this syntax.
|
|
1265
|
-
// The created_at predicate enforces the 5-minute TTL so stale DB entries
|
|
1266
|
-
// (e.g. the desktop app never polled) are rejected rather than silently
|
|
1267
|
-
// redeemed with the session table's default 30-day TTL.
|
|
1268
1395
|
const client = getDbExec();
|
|
1269
1396
|
const { rows } = await client.execute({
|
|
1270
|
-
sql: `
|
|
1271
|
-
args: [
|
|
1272
|
-
`dex:${flowId}`,
|
|
1273
|
-
Date.now() - DESKTOP_EXCHANGE_TTL_MS,
|
|
1274
|
-
`${DESKTOP_MAGIC_LINK_CHALLENGE_PREFIX}%`,
|
|
1275
|
-
],
|
|
1397
|
+
sql: `SELECT email FROM sessions WHERE token = ? AND created_at > ? LIMIT 1`,
|
|
1398
|
+
args: [`dex:${flowId}`, Date.now() - DESKTOP_EXCHANGE_TTL_MS],
|
|
1276
1399
|
});
|
|
1277
|
-
forgetCachedSessionEmail(`dex:${flowId}`);
|
|
1278
1400
|
if (rows.length === 0)
|
|
1279
|
-
return
|
|
1401
|
+
return { status: "missing" };
|
|
1280
1402
|
const packed = (rows[0].email ?? rows[0][0]);
|
|
1281
|
-
if (
|
|
1282
|
-
return
|
|
1283
|
-
|
|
1403
|
+
if (packed === null)
|
|
1404
|
+
return { status: "malformed", packed };
|
|
1405
|
+
const entry = parseDesktopExchangeStoredEntry(packed);
|
|
1406
|
+
return entry
|
|
1407
|
+
? { status: "entry", entry, packed }
|
|
1408
|
+
: { status: "malformed", packed };
|
|
1284
1409
|
}
|
|
1285
1410
|
catch {
|
|
1286
|
-
|
|
1287
|
-
return null;
|
|
1411
|
+
return { status: "unavailable" };
|
|
1288
1412
|
}
|
|
1289
1413
|
}
|
|
1290
|
-
|
|
1414
|
+
/**
|
|
1415
|
+
* Parse and atomically consume a desktop exchange entry from the DB fallback.
|
|
1416
|
+
* The exact packed payload is part of the DELETE predicate so concurrent
|
|
1417
|
+
* pollers cannot both redeem a row, and malformed payloads remain available
|
|
1418
|
+
* for diagnosis instead of being deleted before parsing.
|
|
1419
|
+
*/
|
|
1420
|
+
async function consumeDesktopExchangeFromDB(flowId) {
|
|
1291
1421
|
try {
|
|
1292
1422
|
const client = getDbExec();
|
|
1293
1423
|
const { rows } = await client.execute({
|
|
@@ -1295,17 +1425,27 @@ async function readDesktopMagicLinkChallengeFromDB(flowId) {
|
|
|
1295
1425
|
args: [`dex:${flowId}`, Date.now() - DESKTOP_EXCHANGE_TTL_MS],
|
|
1296
1426
|
});
|
|
1297
1427
|
if (rows.length === 0)
|
|
1298
|
-
return
|
|
1428
|
+
return { status: "missing" };
|
|
1299
1429
|
const packed = (rows[0].email ?? rows[0][0]);
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1430
|
+
const entry = packed ? parseDesktopExchangeStoredEntry(packed) : null;
|
|
1431
|
+
if (!entry)
|
|
1432
|
+
return { status: "malformed", packed };
|
|
1433
|
+
// SQLite >=3.35 and PostgreSQL both support RETURNING. Matching the
|
|
1434
|
+
// payload makes the read/claim pair safe against a concurrent replacement
|
|
1435
|
+
// of the same flow id, while the single DELETE makes concurrent pollers
|
|
1436
|
+
// one-time consumers.
|
|
1437
|
+
const deleted = await client.execute({
|
|
1438
|
+
sql: `DELETE FROM sessions WHERE token = ? AND created_at > ? AND email = ? RETURNING email`,
|
|
1439
|
+
args: [`dex:${flowId}`, Date.now() - DESKTOP_EXCHANGE_TTL_MS, packed],
|
|
1440
|
+
});
|
|
1441
|
+
if (deleted.rows.length === 0)
|
|
1442
|
+
return { status: "missing" };
|
|
1443
|
+
forgetCachedSessionEmail(`dex:${flowId}`);
|
|
1444
|
+
return { status: "entry", entry };
|
|
1306
1445
|
}
|
|
1307
1446
|
catch {
|
|
1308
|
-
|
|
1447
|
+
// coercion-ok: a DB fallback outage leaves the exchange pending so polling can retry without consuming a token.
|
|
1448
|
+
return { status: "unavailable" };
|
|
1309
1449
|
}
|
|
1310
1450
|
}
|
|
1311
1451
|
async function claimDesktopMagicLinkFlow(flowId, verifier, token, email) {
|
|
@@ -1317,14 +1457,15 @@ async function claimDesktopMagicLinkFlow(flowId, verifier, token, email) {
|
|
|
1317
1457
|
!matchesDesktopFlowVerifier(verifier, entry.verifierHash)) {
|
|
1318
1458
|
return false;
|
|
1319
1459
|
}
|
|
1320
|
-
|
|
1321
|
-
token,
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1460
|
+
try {
|
|
1461
|
+
await persistDesktopExchangeToDB(flowId, token, email, verifierHash);
|
|
1462
|
+
_desktopExchanges.delete(flowId);
|
|
1463
|
+
return true;
|
|
1464
|
+
}
|
|
1465
|
+
catch {
|
|
1466
|
+
// coercion-ok: a failed persistence claim must not issue a native session token.
|
|
1467
|
+
return false;
|
|
1468
|
+
}
|
|
1328
1469
|
}
|
|
1329
1470
|
try {
|
|
1330
1471
|
const client = getDbExec();
|
|
@@ -1440,6 +1581,7 @@ function applyCorsHeaders(event, publicCorsPaths = [], requestPath) {
|
|
|
1440
1581
|
"X-Request-Source",
|
|
1441
1582
|
"X-Agent-Native-CSRF",
|
|
1442
1583
|
"X-User-Timezone",
|
|
1584
|
+
"X-Agent-Native-Desktop-Verifier",
|
|
1443
1585
|
EMBED_TARGET_HEADER,
|
|
1444
1586
|
].join(","));
|
|
1445
1587
|
return { hasOrigin: true, allowed: true };
|
|
@@ -2911,10 +3053,11 @@ async function mountBetterAuthRoutes(app, options) {
|
|
|
2911
3053
|
"https://www.googleapis.com/auth/userinfo.email",
|
|
2912
3054
|
"https://www.googleapis.com/auth/userinfo.profile",
|
|
2913
3055
|
].join(" ");
|
|
2914
|
-
app.use("/_agent-native/google/auth-url", defineEventHandler((event) => {
|
|
3056
|
+
app.use("/_agent-native/google/auth-url", defineEventHandler(async (event) => {
|
|
2915
3057
|
if (!areGenericGoogleOAuthRoutesEnabled(app))
|
|
2916
3058
|
return undefined;
|
|
2917
|
-
|
|
3059
|
+
const method = getMethod(event);
|
|
3060
|
+
if (method !== "GET" && method !== "POST") {
|
|
2918
3061
|
setResponseStatus(event, 405);
|
|
2919
3062
|
return { error: "Method not allowed" };
|
|
2920
3063
|
}
|
|
@@ -2931,7 +3074,37 @@ async function mountBetterAuthRoutes(app, options) {
|
|
|
2931
3074
|
const q = getQuery(event);
|
|
2932
3075
|
const desktop = isElectronRequest(event) || q.desktop === "1" || q.desktop === "true";
|
|
2933
3076
|
const mobile = q.mobile === "1" || q.mobile === "true";
|
|
2934
|
-
const flowId = desktop
|
|
3077
|
+
const flowId = desktop && typeof q.flow_id === "string"
|
|
3078
|
+
? normalizeDesktopFlowId(q.flow_id)
|
|
3079
|
+
: undefined;
|
|
3080
|
+
if (method === "POST" && (!desktop || !flowId)) {
|
|
3081
|
+
setResponseStatus(event, 400);
|
|
3082
|
+
return { error: "Invalid desktop exchange challenge." };
|
|
3083
|
+
}
|
|
3084
|
+
const requestedVerifier = desktop
|
|
3085
|
+
? getHeader(event, "x-agent-native-desktop-verifier")
|
|
3086
|
+
: undefined;
|
|
3087
|
+
let desktopVerifierHash;
|
|
3088
|
+
let desktopBrowserBindingHash;
|
|
3089
|
+
if (desktop && (q.flow_id !== undefined || q.verifier !== undefined)) {
|
|
3090
|
+
if (method !== "POST" ||
|
|
3091
|
+
!flowId ||
|
|
3092
|
+
!requestedVerifier ||
|
|
3093
|
+
q.verifier !== undefined ||
|
|
3094
|
+
q.redirect !== undefined) {
|
|
3095
|
+
setResponseStatus(event, 400);
|
|
3096
|
+
return { error: "Invalid desktop exchange challenge." };
|
|
3097
|
+
}
|
|
3098
|
+
try {
|
|
3099
|
+
desktopBrowserBindingHash =
|
|
3100
|
+
prepareDesktopOAuthBrowserBinding(event);
|
|
3101
|
+
desktopVerifierHash = await registerDesktopExchange(flowId, requestedVerifier, desktopBrowserBindingHash);
|
|
3102
|
+
}
|
|
3103
|
+
catch {
|
|
3104
|
+
setResponseStatus(event, 400);
|
|
3105
|
+
return { error: "Invalid desktop exchange challenge." };
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
2935
3108
|
// Validate the caller's return param up front and only embed it
|
|
2936
3109
|
// into the OAuth state when it normalises to a non-root path —
|
|
2937
3110
|
// skip embedding "/" (the default fallback) so the state stays
|
|
@@ -2953,7 +3126,9 @@ async function mountBetterAuthRoutes(app, options) {
|
|
|
2953
3126
|
addAccount: false,
|
|
2954
3127
|
app: getOAuthStateAppId(),
|
|
2955
3128
|
returnUrl,
|
|
2956
|
-
flowId,
|
|
3129
|
+
flowId: flowId ?? undefined,
|
|
3130
|
+
desktopVerifierHash,
|
|
3131
|
+
desktopBrowserBindingHash,
|
|
2957
3132
|
signupAttribution,
|
|
2958
3133
|
signupAnonymousId,
|
|
2959
3134
|
});
|
|
@@ -3009,7 +3184,7 @@ async function mountBetterAuthRoutes(app, options) {
|
|
|
3009
3184
|
try {
|
|
3010
3185
|
const query = getQuery(event);
|
|
3011
3186
|
const code = query.code;
|
|
3012
|
-
const { redirectUri, desktop, mobile, returnUrl, flowId, signupAttribution, signupAnonymousId, } = decodeOAuthState(query.state, getAppUrl(event, "/_agent-native/google/callback"));
|
|
3187
|
+
const { redirectUri, desktop, mobile, returnUrl, flowId, desktopVerifierHash, desktopBrowserBindingHash, signupAttribution, signupAnonymousId, } = decodeOAuthState(query.state, getAppUrl(event, "/_agent-native/google/callback"));
|
|
3013
3188
|
callbackFlowId = flowId;
|
|
3014
3189
|
callbackDesktop = desktop ?? false;
|
|
3015
3190
|
callbackMobile = mobile ?? false;
|
|
@@ -3066,6 +3241,13 @@ async function mountBetterAuthRoutes(app, options) {
|
|
|
3066
3241
|
});
|
|
3067
3242
|
return oauthErrorPage(msg);
|
|
3068
3243
|
}
|
|
3244
|
+
if (flowId) {
|
|
3245
|
+
if (!desktopVerifierHash ||
|
|
3246
|
+
!desktopBrowserBindingHash ||
|
|
3247
|
+
!matchesDesktopOAuthBrowserBinding(event, desktopBrowserBindingHash)) {
|
|
3248
|
+
throw new Error("Desktop OAuth browser binding is invalid.");
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3069
3251
|
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
|
|
3070
3252
|
method: "POST",
|
|
3071
3253
|
headers: {
|
|
@@ -3140,7 +3322,10 @@ async function mountBetterAuthRoutes(app, options) {
|
|
|
3140
3322
|
emailDomain: email.split("@")[1] || "",
|
|
3141
3323
|
});
|
|
3142
3324
|
if (flowId && sessionToken) {
|
|
3143
|
-
|
|
3325
|
+
if (!desktopVerifierHash) {
|
|
3326
|
+
throw new Error("Missing desktop exchange challenge.");
|
|
3327
|
+
}
|
|
3328
|
+
await setDesktopExchange(flowId, sessionToken, email, desktopVerifierHash);
|
|
3144
3329
|
logGoogleOAuthDebug(event, "callback-exchange-stored", {
|
|
3145
3330
|
flowId,
|
|
3146
3331
|
desktop,
|
|
@@ -3186,79 +3371,110 @@ async function mountBetterAuthRoutes(app, options) {
|
|
|
3186
3371
|
setResponseStatus(event, 400);
|
|
3187
3372
|
return { error: "Missing flow_id" };
|
|
3188
3373
|
}
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
// Don't log on the pending path — clients poll every second for up
|
|
3207
|
-
// to 5 minutes, so logging here floods telemetry. The auth-url,
|
|
3208
|
-
// callback-start, callback-session-created, exchange-success, and
|
|
3209
|
-
// exchange-error breadcrumbs already cover every meaningful state
|
|
3210
|
-
// transition.
|
|
3211
|
-
return { pending: true, flow: oauthDebugFlowId(flowId) };
|
|
3374
|
+
if (query.verifier !== undefined) {
|
|
3375
|
+
setResponseStatus(event, 400);
|
|
3376
|
+
return {
|
|
3377
|
+
error: "Desktop exchange verifier must use a request header.",
|
|
3378
|
+
};
|
|
3379
|
+
}
|
|
3380
|
+
const verifier = normalizeDesktopFlowVerifier(getHeader(event, "x-agent-native-desktop-verifier"));
|
|
3381
|
+
const cached = _desktopExchanges.get(flowId);
|
|
3382
|
+
if (cached && cached.expiresAt < Date.now()) {
|
|
3383
|
+
_desktopExchanges.delete(flowId);
|
|
3384
|
+
}
|
|
3385
|
+
const current = _desktopExchanges.get(flowId);
|
|
3386
|
+
if (current && "challenge" in current) {
|
|
3387
|
+
if (!verifier ||
|
|
3388
|
+
!matchesDesktopFlowVerifier(verifier, current.verifierHash)) {
|
|
3389
|
+
setResponseStatus(event, 403);
|
|
3390
|
+
return { error: "Invalid desktop exchange verifier." };
|
|
3212
3391
|
}
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3392
|
+
return { pending: true, flow: oauthDebugFlowId(flowId) };
|
|
3393
|
+
}
|
|
3394
|
+
if (current && "error" in current) {
|
|
3395
|
+
_desktopExchanges.delete(flowId);
|
|
3396
|
+
removeSession(`dex:${flowId}`).catch((err) => {
|
|
3397
|
+
console.warn("[auth] desktop-exchange DB cleanup failed:", describeDbError(err));
|
|
3398
|
+
});
|
|
3399
|
+
logGoogleOAuthDebug(event, "exchange-error", {
|
|
3400
|
+
flowId,
|
|
3401
|
+
message: current.error.message,
|
|
3402
|
+
code: current.error.code,
|
|
3403
|
+
});
|
|
3404
|
+
setResponseStatus(event, 400);
|
|
3405
|
+
return { error: current.error.message, ...current.error };
|
|
3406
|
+
}
|
|
3407
|
+
// The DB is authoritative for token exchanges. Read and validate before
|
|
3408
|
+
// the conditional delete so malformed rows remain available for
|
|
3409
|
+
// diagnosis and two instances cannot both redeem the same payload.
|
|
3410
|
+
const fromDb = await readDesktopExchangeFromDB(flowId);
|
|
3411
|
+
if (fromDb.status === "missing" || fromDb.status === "unavailable") {
|
|
3412
|
+
// Don't log on the pending path — clients poll every second for up
|
|
3413
|
+
// to 5 minutes, so logging here floods telemetry. The auth-url,
|
|
3414
|
+
// callback-start, callback-session-created, exchange-success, and
|
|
3415
|
+
// exchange-error breadcrumbs already cover every meaningful state
|
|
3416
|
+
// transition.
|
|
3417
|
+
return { pending: true, flow: oauthDebugFlowId(flowId) };
|
|
3418
|
+
}
|
|
3419
|
+
if (fromDb.status === "malformed") {
|
|
3420
|
+
setResponseStatus(event, 500);
|
|
3421
|
+
return {
|
|
3422
|
+
error: "Desktop exchange storage is invalid.",
|
|
3423
|
+
code: "exchange_storage_invalid",
|
|
3424
|
+
};
|
|
3425
|
+
}
|
|
3426
|
+
if ("challenge" in fromDb.entry) {
|
|
3232
3427
|
if (!verifier ||
|
|
3233
|
-
!matchesDesktopFlowVerifier(verifier, entry.verifierHash)) {
|
|
3428
|
+
!matchesDesktopFlowVerifier(verifier, fromDb.entry.verifierHash)) {
|
|
3234
3429
|
setResponseStatus(event, 403);
|
|
3235
3430
|
return { error: "Invalid desktop exchange verifier." };
|
|
3236
3431
|
}
|
|
3237
3432
|
return { pending: true, flow: oauthDebugFlowId(flowId) };
|
|
3238
3433
|
}
|
|
3239
|
-
if ("token" in entry &&
|
|
3434
|
+
if ("token" in fromDb.entry &&
|
|
3435
|
+
(!fromDb.entry.verifierHash ||
|
|
3436
|
+
!verifier ||
|
|
3437
|
+
!matchesDesktopFlowVerifier(verifier, fromDb.entry.verifierHash))) {
|
|
3438
|
+
setResponseStatus(event, 403);
|
|
3439
|
+
return { error: "Invalid desktop exchange verifier." };
|
|
3440
|
+
}
|
|
3441
|
+
const consumed = await consumeDesktopExchangeFromDB(flowId);
|
|
3442
|
+
if (consumed.status === "missing" || consumed.status === "unavailable") {
|
|
3443
|
+
return { pending: true, flow: oauthDebugFlowId(flowId) };
|
|
3444
|
+
}
|
|
3445
|
+
if (consumed.status === "malformed") {
|
|
3446
|
+
setResponseStatus(event, 500);
|
|
3447
|
+
return {
|
|
3448
|
+
error: "Desktop exchange storage is invalid.",
|
|
3449
|
+
code: "exchange_storage_invalid",
|
|
3450
|
+
};
|
|
3451
|
+
}
|
|
3452
|
+
if ("challenge" in consumed.entry) {
|
|
3240
3453
|
if (!verifier ||
|
|
3241
|
-
!matchesDesktopFlowVerifier(verifier, entry.verifierHash)) {
|
|
3454
|
+
!matchesDesktopFlowVerifier(verifier, consumed.entry.verifierHash)) {
|
|
3242
3455
|
setResponseStatus(event, 403);
|
|
3243
3456
|
return { error: "Invalid desktop exchange verifier." };
|
|
3244
3457
|
}
|
|
3458
|
+
return { pending: true, flow: oauthDebugFlowId(flowId) };
|
|
3245
3459
|
}
|
|
3246
|
-
|
|
3247
|
-
// Also wipe the DB-persisted entry so it cannot be replayed via the
|
|
3248
|
-
// DB fallback path after in-memory consumption. Best-effort: a dropped
|
|
3249
|
-
// Neon WebSocket rejects with a raw ErrorEvent, and a floating
|
|
3250
|
-
// rejection here surfaces as an unhandled promise rejection.
|
|
3251
|
-
removeSession(`dex:${flowId}`).catch((err) => {
|
|
3252
|
-
console.warn("[auth] desktop-exchange DB cleanup failed:", describeDbError(err));
|
|
3253
|
-
});
|
|
3254
|
-
if ("error" in entry) {
|
|
3460
|
+
if ("error" in consumed.entry) {
|
|
3255
3461
|
logGoogleOAuthDebug(event, "exchange-error", {
|
|
3256
3462
|
flowId,
|
|
3257
|
-
message: entry.error.message,
|
|
3258
|
-
code: entry.error.code,
|
|
3463
|
+
message: consumed.entry.error.message,
|
|
3464
|
+
code: consumed.entry.error.code,
|
|
3259
3465
|
});
|
|
3260
3466
|
setResponseStatus(event, 400);
|
|
3261
|
-
return {
|
|
3467
|
+
return {
|
|
3468
|
+
error: consumed.entry.error.message,
|
|
3469
|
+
...consumed.entry.error,
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
const entry = consumed.entry;
|
|
3473
|
+
if (!verifier ||
|
|
3474
|
+
!entry.verifierHash ||
|
|
3475
|
+
!matchesDesktopFlowVerifier(verifier, entry.verifierHash)) {
|
|
3476
|
+
setResponseStatus(event, 403);
|
|
3477
|
+
return { error: "Invalid desktop exchange verifier." };
|
|
3262
3478
|
}
|
|
3263
3479
|
// Make the exchange itself establish the app session. Older clients
|
|
3264
3480
|
// still make a follow-up /auth/session?_session=... request, but the
|