@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
|
@@ -57,7 +57,9 @@ current factory graph, and executes only the explicit automation prompts that
|
|
|
57
57
|
are stored in the organization.
|
|
58
58
|
Use the Factory actions as the source of truth. When a user asks to
|
|
59
59
|
create or change a factory, first inspect the current graph, then propose a complete
|
|
60
|
-
versioned graph through save-factory-graph with source=ai
|
|
60
|
+
versioned graph through save-factory-graph with source=ai, the inspected graphVersion
|
|
61
|
+
as expectedGraphVersion, and a concise changeSummary. Never save a graph from a
|
|
62
|
+
stale read: refresh and re-propose when the action reports a version conflict.
|
|
61
63
|
Never hide a graph change in prose: the visual map and the saved graph must agree.
|
|
62
64
|
The graph is currently a reviewable blueprint, not the runtime router: automation
|
|
63
65
|
markdown resources are the runtime prompts, while enabled triage rules are evaluated
|
|
@@ -89,13 +91,15 @@ Builder, auto-approve, or auto-merge those items. Slack clear bugs use the
|
|
|
89
91
|
thread-preserving start-builder-for-item flow; GitHub and Sentry clear bugs use
|
|
90
92
|
the Builder agent-run flow. Slack repeat reports must be clustered by underlying
|
|
91
93
|
symptom, with one Builder thread for the cluster and 👀 on every grouped report.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
After classifying an item, call start-builder-for-item with clearBug true or
|
|
95
|
+
false and a short reason so a skip is recorded. Use /address-feedback for the
|
|
96
|
+
repository feedback workflow. For pull requests, follow review-prs: read the
|
|
97
|
+
complete diff and review evidence, verify current BuilderIO membership, preserve
|
|
98
|
+
the ultra-scary safety gate, and distinguish unknown or unresolved checks from
|
|
99
|
+
clean ones. Auto-merge also requires a verified Factory Builder run. When a user
|
|
100
|
+
says to do a review-gated item now, use the explicit approval action, which
|
|
101
|
+
records the approver and applies the rule's configured executor policy. Keep
|
|
102
|
+
Slack replies concise and link to the
|
|
99
103
|
Factory item when a review is needed. The scheduled builder-io-bot PR babysitter
|
|
100
104
|
posts its exact feedback-fix request through GitHub, persists a 20-minute quiet
|
|
101
105
|
window, and never approves or merges.`,
|
|
@@ -169,6 +169,8 @@ const FACTORY_DEFAULT_MODEL =
|
|
|
169
169
|
process.env.FACTORY_AUTOMATION_MODEL?.trim() || "gpt-5.6-luna";
|
|
170
170
|
const FACTORY_DEFAULT_MAX_ITERATIONS = 32;
|
|
171
171
|
const FACTORY_DEFAULT_MAX_RUN_INPUT_TOKENS = 1_000_000;
|
|
172
|
+
const SKIP_RECORD_GUARD =
|
|
173
|
+
"After classifying each processed item, call start-builder-for-item with clearBug true or false and a short evidence-grounded reason so the skip or dispatch is recorded.";
|
|
172
174
|
|
|
173
175
|
const AUTOMATION_SEEDS: AutomationSeed[] = [
|
|
174
176
|
{
|
|
@@ -224,6 +226,7 @@ the representative source, every related source, and the need to fix the
|
|
|
224
226
|
underlying boundary across the whole cluster. Never add the reaction or tag
|
|
225
227
|
Builder for owner-managed Clips, Design, or Content work, or for a non-bug
|
|
226
228
|
report.
|
|
229
|
+
After classifying each processed item, call start-builder-for-item with clearBug true or false and a short evidence-grounded reason so the skip or dispatch is recorded.
|
|
227
230
|
|
|
228
231
|
Keep each run bounded. Preserve action errors and do not claim a Builder reply,
|
|
229
232
|
PR, merge, or fix unless an action returned that state.
|
|
@@ -254,6 +257,7 @@ For each eligible clear bug, call start-builder-for-item with clearBug true,
|
|
|
254
257
|
an evidence-grounded reason, and clearErrorReport containing only the bounded
|
|
255
258
|
Sentry evidence. Builder should open a PR; do not claim it did so until the
|
|
256
259
|
run callback or PR observation confirms it.
|
|
260
|
+
After classifying each processed item, call start-builder-for-item with clearBug true or false and a short evidence-grounded reason so the skip or dispatch is recorded.
|
|
257
261
|
`,
|
|
258
262
|
},
|
|
259
263
|
{
|
|
@@ -281,6 +285,7 @@ For each eligible item call start-builder-for-item with clearBug true,
|
|
|
281
285
|
evidence-grounded reason, and the bounded issue body as clearErrorReport.
|
|
282
286
|
Preserve failures and never report a successful Builder run without its action
|
|
283
287
|
confirmation.
|
|
288
|
+
After classifying each processed item, call start-builder-for-item with clearBug true or false and a short evidence-grounded reason so the skip or dispatch is recorded.
|
|
284
289
|
`,
|
|
285
290
|
},
|
|
286
291
|
{
|
|
@@ -522,6 +527,14 @@ async function ensureOrganizationAutomations(
|
|
|
522
527
|
if (promptGuard && !repaired.includes(promptGuard)) {
|
|
523
528
|
repaired = `${repaired.trimEnd()}\n\n${promptGuard}\n`;
|
|
524
529
|
}
|
|
530
|
+
if (
|
|
531
|
+
(seed.name === "factory-slack-feedback" ||
|
|
532
|
+
seed.name === "factory-sentry-errors" ||
|
|
533
|
+
seed.name === "factory-github-issues") &&
|
|
534
|
+
!repaired.includes(SKIP_RECORD_GUARD)
|
|
535
|
+
) {
|
|
536
|
+
repaired = `${repaired.trimEnd()}\n\n${SKIP_RECORD_GUARD}\n`;
|
|
537
|
+
}
|
|
525
538
|
if (repaired === existing.content) return;
|
|
526
539
|
|
|
527
540
|
const updated = await resourcePutIfCurrent({
|
|
@@ -4,11 +4,13 @@ import {
|
|
|
4
4
|
addEyesReaction as writeEyesReaction,
|
|
5
5
|
getThread as readThread,
|
|
6
6
|
getTeamInfo as readTeamInfo,
|
|
7
|
+
getUserInfo as readUserInfo,
|
|
7
8
|
postThreadReply as writeThreadReply,
|
|
8
9
|
type ChannelHistoryResult,
|
|
9
10
|
type SlackPostMessageResult,
|
|
10
11
|
type SlackReactionResult,
|
|
11
12
|
type SlackTeamInfo,
|
|
13
|
+
type SlackUserInfo,
|
|
12
14
|
type SlackTokenResolver,
|
|
13
15
|
type ThreadRepliesResult,
|
|
14
16
|
type Workspace,
|
|
@@ -57,6 +59,9 @@ export function createSlackReader(identity: SlackReaderIdentity) {
|
|
|
57
59
|
getTeamInfo(workspace: Workspace): Promise<SlackTeamInfo> {
|
|
58
60
|
return readTeamInfo(workspace, tokenResolver);
|
|
59
61
|
},
|
|
62
|
+
getUserInfo(workspace: Workspace, userId: string): Promise<SlackUserInfo> {
|
|
63
|
+
return readUserInfo(workspace, userId, tokenResolver);
|
|
64
|
+
},
|
|
60
65
|
getThread(
|
|
61
66
|
workspace: Workspace,
|
|
62
67
|
channelId: string,
|
|
@@ -5,6 +5,7 @@ import { createSlackReader } from "./slack-client";
|
|
|
5
5
|
const SLACK_HISTORY_LIMIT = 100;
|
|
6
6
|
const MAX_HISTORY_PAGES = 5;
|
|
7
7
|
const MAX_SUMMARY_LENGTH = 500;
|
|
8
|
+
export const SLACK_USER_INFO_CONCURRENCY = 4;
|
|
8
9
|
|
|
9
10
|
export interface SlackPollInput {
|
|
10
11
|
workspace: Workspace;
|
|
@@ -37,17 +38,75 @@ function compactText(text: string): string {
|
|
|
37
38
|
: compact;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function messageLabel(message: SlackMessage): string {
|
|
41
|
+
function messageLabel(message: SlackMessage, resolvedLabel?: string): string {
|
|
41
42
|
if (message.bot_id || message.username) {
|
|
42
43
|
return `Slack bot ${message.username ?? message.bot_id ?? "unknown"}`;
|
|
43
44
|
}
|
|
44
|
-
return `Slack user ${message.user ?? "unknown"}`;
|
|
45
|
+
return `Slack user ${resolvedLabel ?? message.user ?? "unknown"}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function slackUserLabel(
|
|
49
|
+
user: { name: string | null; displayName: string | null } | null,
|
|
50
|
+
userId: string,
|
|
51
|
+
): string {
|
|
52
|
+
const displayName = user?.displayName?.trim();
|
|
53
|
+
if (displayName) return displayName;
|
|
54
|
+
const name = user?.name?.trim();
|
|
55
|
+
if (name) return name.startsWith("@") ? name : `@${name}`;
|
|
56
|
+
return userId;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function resolveUserLabels(
|
|
60
|
+
slack: ReturnType<typeof createSlackReader>,
|
|
61
|
+
workspace: Workspace,
|
|
62
|
+
messages: SlackMessage[],
|
|
63
|
+
): Promise<Map<string, string>> {
|
|
64
|
+
const userIds = [
|
|
65
|
+
...new Set(
|
|
66
|
+
messages
|
|
67
|
+
.filter((message) => !message.bot_id && !message.username)
|
|
68
|
+
.map((message) => message.user?.trim())
|
|
69
|
+
.filter((userId): userId is string => Boolean(userId)),
|
|
70
|
+
),
|
|
71
|
+
];
|
|
72
|
+
const labels = new Map<string, string>();
|
|
73
|
+
await mapWithConcurrency(
|
|
74
|
+
userIds,
|
|
75
|
+
SLACK_USER_INFO_CONCURRENCY,
|
|
76
|
+
async (userId) => {
|
|
77
|
+
try {
|
|
78
|
+
const user = await slack.getUserInfo(workspace, userId);
|
|
79
|
+
labels.set(userId, slackUserLabel(user, userId));
|
|
80
|
+
} catch {
|
|
81
|
+
labels.set(userId, userId);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
return labels;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function mapWithConcurrency<T>(
|
|
89
|
+
values: T[],
|
|
90
|
+
concurrency: number,
|
|
91
|
+
mapper: (value: T) => Promise<void>,
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
let nextIndex = 0;
|
|
94
|
+
const worker = async () => {
|
|
95
|
+
while (nextIndex < values.length) {
|
|
96
|
+
const index = nextIndex++;
|
|
97
|
+
await mapper(values[index]!);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
await Promise.all(
|
|
101
|
+
Array.from({ length: Math.min(concurrency, values.length) }, worker),
|
|
102
|
+
);
|
|
45
103
|
}
|
|
46
104
|
|
|
47
105
|
function toEnvelope(
|
|
48
106
|
channelId: string,
|
|
49
107
|
message: SlackMessage,
|
|
50
108
|
teamDomain?: string,
|
|
109
|
+
userLabels?: Map<string, string>,
|
|
51
110
|
): IngestionEnvelope {
|
|
52
111
|
const threadTs = message.thread_ts ?? message.ts;
|
|
53
112
|
const suppliedPermalink = (message as SlackMessage & { permalink?: string })
|
|
@@ -63,7 +122,10 @@ function toEnvelope(
|
|
|
63
122
|
externalId: `${channelId}:${threadTs}`,
|
|
64
123
|
receivedAt: new Date().toISOString(),
|
|
65
124
|
...(sourceUrl ? { sourceUrl } : {}),
|
|
66
|
-
title: messageLabel(
|
|
125
|
+
title: messageLabel(
|
|
126
|
+
message,
|
|
127
|
+
message.user ? userLabels?.get(message.user) : undefined,
|
|
128
|
+
),
|
|
67
129
|
summary: compactText(message.text),
|
|
68
130
|
channelId,
|
|
69
131
|
threadTs,
|
|
@@ -149,9 +211,15 @@ export async function pollSlackChannel({
|
|
|
149
211
|
{ value: priorTs, raw: priorLastSlackTs },
|
|
150
212
|
);
|
|
151
213
|
|
|
214
|
+
const userLabels = await resolveUserLabels(
|
|
215
|
+
slack,
|
|
216
|
+
workspace,
|
|
217
|
+
uniqueNewMessages.map(({ message }) => message),
|
|
218
|
+
);
|
|
219
|
+
|
|
152
220
|
return {
|
|
153
221
|
envelopes: uniqueNewMessages.map(({ message }) =>
|
|
154
|
-
toEnvelope(channelId, message, teamDomain),
|
|
222
|
+
toEnvelope(channelId, message, teamDomain, userLabels),
|
|
155
223
|
),
|
|
156
224
|
nextLastSlackTs: maxSeen.raw,
|
|
157
225
|
nextHistoryCursor,
|
|
@@ -70,7 +70,7 @@ async function fetchEmailList(
|
|
|
70
70
|
|
|
71
71
|
const gmailQuery = buildGmailEmailSearchQuery({ view, q: search });
|
|
72
72
|
const effectiveQuery =
|
|
73
|
-
view === "all" && !search ? "" : gmailQuery || "in:inbox
|
|
73
|
+
view === "all" && !search ? "" : gmailQuery || "in:inbox";
|
|
74
74
|
const { messages } = await listGmailMessages(
|
|
75
75
|
effectiveQuery,
|
|
76
76
|
50,
|
|
@@ -76,6 +76,25 @@ const ADD_ACCOUNT_POLL_ABORT_MS = Math.max(
|
|
|
76
76
|
ADD_ACCOUNT_POLL_INTERVAL_MS * 4,
|
|
77
77
|
);
|
|
78
78
|
|
|
79
|
+
function newDesktopOAuthVerifier(): string | null {
|
|
80
|
+
const cryptoApi = globalThis.crypto;
|
|
81
|
+
const randomUuid = cryptoApi?.randomUUID;
|
|
82
|
+
if (typeof randomUuid === "function") {
|
|
83
|
+
return `${randomUuid.call(cryptoApi)}${randomUuid.call(cryptoApi)}`;
|
|
84
|
+
}
|
|
85
|
+
if (typeof cryptoApi?.getRandomValues === "function") {
|
|
86
|
+
const bytes = new Uint8Array(32);
|
|
87
|
+
cryptoApi.getRandomValues(bytes);
|
|
88
|
+
let binary = "";
|
|
89
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
90
|
+
return btoa(binary)
|
|
91
|
+
.replace(/\+/g, "-")
|
|
92
|
+
.replace(/\//g, "_")
|
|
93
|
+
.replace(/=+$/, "");
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
79
98
|
interface GoogleConnectBannerProps {
|
|
80
99
|
variant?: "banner" | "hero";
|
|
81
100
|
}
|
|
@@ -128,20 +147,81 @@ export function GoogleConnectBanner({
|
|
|
128
147
|
const flowId =
|
|
129
148
|
crypto.randomUUID?.() ||
|
|
130
149
|
Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
150
|
+
const verifier = newDesktopOAuthVerifier();
|
|
151
|
+
if (!verifier) {
|
|
152
|
+
setDesktopAuthIssue({
|
|
153
|
+
code: "desktop_auth_start_failed",
|
|
154
|
+
message: t("mail.error.failedToConnect"),
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
131
158
|
const origin = window.location.origin;
|
|
132
159
|
const endpoint = addAccount
|
|
133
160
|
? "/_agent-native/google/add-account/auth-url"
|
|
134
161
|
: "/_agent-native/google/auth-url";
|
|
135
|
-
const
|
|
136
|
-
oauthRedirectUri("/_agent-native/google/callback"),
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
)
|
|
162
|
+
const params = new URLSearchParams({
|
|
163
|
+
redirect_uri: oauthRedirectUri("/_agent-native/google/callback"),
|
|
164
|
+
desktop: "1",
|
|
165
|
+
flow_id: flowId,
|
|
166
|
+
});
|
|
167
|
+
const popup = window.open("", "_blank");
|
|
168
|
+
if (!popup) {
|
|
169
|
+
setDesktopAuthIssue({
|
|
170
|
+
code: "popup_blocked",
|
|
171
|
+
message: t("mail.error.failedToConnect"),
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
let pollHandle: ReturnType<typeof setInterval> | null = null;
|
|
176
|
+
const stopPoll = () => {
|
|
177
|
+
if (!pollHandle) return;
|
|
178
|
+
clearInterval(pollHandle);
|
|
179
|
+
if (desktopPollRef.current === pollHandle) {
|
|
180
|
+
desktopPollRef.current = null;
|
|
181
|
+
}
|
|
182
|
+
pollHandle = null;
|
|
183
|
+
};
|
|
184
|
+
void fetch(`${origin}${agentNativePath(endpoint)}?${params.toString()}`, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
credentials: "include",
|
|
187
|
+
headers: { "X-Agent-Native-Desktop-Verifier": verifier },
|
|
188
|
+
})
|
|
189
|
+
.then(async (response) => {
|
|
190
|
+
let data: {
|
|
191
|
+
url?: unknown;
|
|
192
|
+
message?: unknown;
|
|
193
|
+
error?: unknown;
|
|
194
|
+
};
|
|
195
|
+
try {
|
|
196
|
+
data = (await response.json()) as typeof data;
|
|
197
|
+
} catch {
|
|
198
|
+
throw new Error(t("mail.googleConnect.connectionFailed"));
|
|
199
|
+
}
|
|
200
|
+
if (!response.ok || typeof data?.url !== "string") {
|
|
201
|
+
const message =
|
|
202
|
+
typeof data.message === "string"
|
|
203
|
+
? data.message
|
|
204
|
+
: typeof data.error === "string"
|
|
205
|
+
? data.error
|
|
206
|
+
: t("mail.googleConnect.connectionFailed");
|
|
207
|
+
throw new Error(message);
|
|
208
|
+
}
|
|
209
|
+
popup.location.href = data.url;
|
|
210
|
+
})
|
|
211
|
+
.catch((error) => {
|
|
212
|
+
stopPoll();
|
|
213
|
+
popup.close();
|
|
214
|
+
setDesktopAuthIssue({
|
|
215
|
+
code: "desktop_auth_start_failed",
|
|
216
|
+
message:
|
|
217
|
+
error instanceof Error
|
|
218
|
+
? error.message
|
|
219
|
+
: t("mail.googleConnect.connectionFailed"),
|
|
220
|
+
});
|
|
221
|
+
});
|
|
142
222
|
const start = Date.now();
|
|
143
223
|
if (desktopPollRef.current) clearInterval(desktopPollRef.current);
|
|
144
|
-
|
|
224
|
+
pollHandle = setInterval(async () => {
|
|
145
225
|
if (document.hidden || desktopPollInFlightRef.current) return;
|
|
146
226
|
desktopPollInFlightRef.current = true;
|
|
147
227
|
const controller = new AbortController();
|
|
@@ -153,18 +233,21 @@ export function GoogleConnectBanner({
|
|
|
153
233
|
try {
|
|
154
234
|
const res = await fetch(
|
|
155
235
|
agentNativePath(
|
|
156
|
-
`/_agent-native/auth/desktop-exchange?flow_id=${flowId}`,
|
|
236
|
+
`/_agent-native/auth/desktop-exchange?flow_id=${encodeURIComponent(flowId)}`,
|
|
157
237
|
),
|
|
158
|
-
{
|
|
238
|
+
{
|
|
239
|
+
headers: {
|
|
240
|
+
"X-Agent-Native-Desktop-Verifier": verifier,
|
|
241
|
+
},
|
|
242
|
+
signal: controller.signal,
|
|
243
|
+
},
|
|
159
244
|
);
|
|
160
245
|
const data = await res.json();
|
|
161
246
|
if (data?.error) {
|
|
162
|
-
|
|
163
|
-
desktopPollRef.current = null;
|
|
247
|
+
stopPoll();
|
|
164
248
|
setDesktopAuthIssue(data);
|
|
165
249
|
} else if (data?.token) {
|
|
166
|
-
|
|
167
|
-
desktopPollRef.current = null;
|
|
250
|
+
stopPoll();
|
|
168
251
|
await fetch(
|
|
169
252
|
agentNativePath(
|
|
170
253
|
`/_agent-native/auth/session?_session=${data.token}`,
|
|
@@ -176,13 +259,11 @@ export function GoogleConnectBanner({
|
|
|
176
259
|
);
|
|
177
260
|
window.location.reload();
|
|
178
261
|
} else if (Date.now() - start > 120_000) {
|
|
179
|
-
|
|
180
|
-
desktopPollRef.current = null;
|
|
262
|
+
stopPoll();
|
|
181
263
|
}
|
|
182
264
|
} catch {
|
|
183
265
|
if (Date.now() - start > 120_000) {
|
|
184
|
-
|
|
185
|
-
desktopPollRef.current = null;
|
|
266
|
+
stopPoll();
|
|
186
267
|
}
|
|
187
268
|
}
|
|
188
269
|
} finally {
|
|
@@ -190,6 +271,7 @@ export function GoogleConnectBanner({
|
|
|
190
271
|
desktopPollInFlightRef.current = false;
|
|
191
272
|
}
|
|
192
273
|
}, DESKTOP_POLL_INTERVAL_MS);
|
|
274
|
+
desktopPollRef.current = pollHandle;
|
|
193
275
|
}
|
|
194
276
|
|
|
195
277
|
const [authError, setAuthError] = useState<string | null>(null);
|
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
oauthCallbackResponse,
|
|
16
16
|
oauthDesktopExchangePage,
|
|
17
17
|
oauthErrorPage,
|
|
18
|
+
registerDesktopExchange,
|
|
19
|
+
prepareDesktopOAuthBrowserBinding,
|
|
20
|
+
matchesDesktopOAuthBrowserBinding,
|
|
18
21
|
safeReturnPath,
|
|
19
22
|
setDesktopExchange,
|
|
20
23
|
setDesktopExchangeError,
|
|
@@ -22,6 +25,8 @@ import {
|
|
|
22
25
|
import { getUserSetting, putUserSetting } from "@agent-native/core/settings";
|
|
23
26
|
import {
|
|
24
27
|
defineEventHandler,
|
|
28
|
+
getHeader,
|
|
29
|
+
getMethod,
|
|
25
30
|
getQuery,
|
|
26
31
|
setResponseStatus,
|
|
27
32
|
type H3Event,
|
|
@@ -64,17 +69,25 @@ function googleOAuthErrorPayload(
|
|
|
64
69
|
error?.name === "OAuthAccountOwnedByOtherUserError"
|
|
65
70
|
) {
|
|
66
71
|
const account = error.accountId || "This Google account";
|
|
72
|
+
const message = `${account} is connected to another login. Sign out, then sign in with ${account}.`;
|
|
67
73
|
return {
|
|
68
|
-
message
|
|
74
|
+
message,
|
|
69
75
|
code: "account_owner_mismatch",
|
|
70
76
|
accountId: error.accountId,
|
|
71
77
|
};
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
const msg = error?.message || "Unknown error";
|
|
81
|
+
const statusCode = Number(error?.statusCode || error?.status || 0);
|
|
75
82
|
const isPermission =
|
|
83
|
+
error?.oauthErrorCode === "access_denied" ||
|
|
84
|
+
error?.oauthErrorCode === "forbidden" ||
|
|
85
|
+
error?.oauthErrorCode === "insufficient_scope" ||
|
|
76
86
|
msg.includes("Insufficient Permission") ||
|
|
77
|
-
msg.includes("insufficient_scope")
|
|
87
|
+
msg.includes("insufficient_scope") ||
|
|
88
|
+
/insufficient authentication scopes/i.test(msg) ||
|
|
89
|
+
(statusCode === 403 &&
|
|
90
|
+
/\b(?:scope|permission|forbidden|access denied)\b/i.test(msg));
|
|
78
91
|
return {
|
|
79
92
|
message: isPermission
|
|
80
93
|
? "This account wasn't granted the required permissions. Make sure you check all the permission boxes on the consent screen. If the app is in testing mode, add this email as a test user in Google Cloud Console."
|
|
@@ -99,6 +112,7 @@ function googleOAuthErrorResponse(
|
|
|
99
112
|
export const getGoogleAuthUrl = defineEventHandler(async (event: H3Event) => {
|
|
100
113
|
try {
|
|
101
114
|
const q = getQuery(event);
|
|
115
|
+
const method = getMethod(event);
|
|
102
116
|
const redirectUri = resolveOAuthRedirectUri(event);
|
|
103
117
|
if (!redirectUri) {
|
|
104
118
|
setResponseStatus(event, 400);
|
|
@@ -113,6 +127,34 @@ export const getGoogleAuthUrl = defineEventHandler(async (event: H3Event) => {
|
|
|
113
127
|
const desktop =
|
|
114
128
|
isElectron(event) || q.desktop === "1" || q.desktop === "true";
|
|
115
129
|
const flowId = desktop ? (q.flow_id as string) || undefined : undefined;
|
|
130
|
+
if (method === "POST" && (!desktop || !flowId)) {
|
|
131
|
+
setResponseStatus(event, 400);
|
|
132
|
+
return { error: "Invalid desktop exchange challenge." };
|
|
133
|
+
}
|
|
134
|
+
let desktopVerifierHash: string | undefined;
|
|
135
|
+
let desktopBrowserBindingHash: string | undefined;
|
|
136
|
+
if (flowId) {
|
|
137
|
+
if (method !== "POST" || q.redirect !== undefined) {
|
|
138
|
+
setResponseStatus(event, 400);
|
|
139
|
+
return { error: "Invalid desktop exchange challenge." };
|
|
140
|
+
}
|
|
141
|
+
const verifier = getHeader(event, "x-agent-native-desktop-verifier");
|
|
142
|
+
if (!verifier || q.verifier !== undefined) {
|
|
143
|
+
setResponseStatus(event, 400);
|
|
144
|
+
return { error: "Invalid desktop exchange challenge." };
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
desktopBrowserBindingHash = prepareDesktopOAuthBrowserBinding(event);
|
|
148
|
+
desktopVerifierHash = await registerDesktopExchange(
|
|
149
|
+
flowId,
|
|
150
|
+
verifier,
|
|
151
|
+
desktopBrowserBindingHash,
|
|
152
|
+
);
|
|
153
|
+
} catch {
|
|
154
|
+
setResponseStatus(event, 400);
|
|
155
|
+
return { error: "Invalid desktop exchange challenge." };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
116
158
|
const requestedReturn =
|
|
117
159
|
typeof q.return === "string" ? safeReturnPath(q.return) : "/";
|
|
118
160
|
const returnUrl = requestedReturn !== "/" ? requestedReturn : undefined;
|
|
@@ -127,6 +169,8 @@ export const getGoogleAuthUrl = defineEventHandler(async (event: H3Event) => {
|
|
|
127
169
|
app: OAUTH_STATE_APP_ID,
|
|
128
170
|
returnUrl,
|
|
129
171
|
flowId,
|
|
172
|
+
desktopVerifierHash,
|
|
173
|
+
desktopBrowserBindingHash,
|
|
130
174
|
});
|
|
131
175
|
const url = await getAuthUrl(undefined, redirectUri, state, owner);
|
|
132
176
|
if (q.redirect === "1") {
|
|
@@ -159,22 +203,28 @@ export const handleGoogleCallback = defineEventHandler(
|
|
|
159
203
|
);
|
|
160
204
|
desktop = state.desktop ?? false;
|
|
161
205
|
flowId = state.flowId;
|
|
206
|
+
if (
|
|
207
|
+
flowId &&
|
|
208
|
+
(!state.desktopVerifierHash ||
|
|
209
|
+
!state.desktopBrowserBindingHash ||
|
|
210
|
+
!matchesDesktopOAuthBrowserBinding(
|
|
211
|
+
event,
|
|
212
|
+
state.desktopBrowserBindingHash,
|
|
213
|
+
))
|
|
214
|
+
) {
|
|
215
|
+
throw new Error("Desktop OAuth browser binding is invalid.");
|
|
216
|
+
}
|
|
162
217
|
|
|
163
218
|
// Handle Google authorization errors (e.g. user denied access, invalid client)
|
|
164
219
|
const googleError = query.error as string | undefined;
|
|
165
220
|
if (googleError) {
|
|
166
221
|
const errorDesc =
|
|
167
222
|
(query.error_description as string | undefined) || googleError;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
: `Connection failed: ${errorDesc}`;
|
|
174
|
-
return googleOAuthErrorResponse(event, new Error(userMessage), {
|
|
175
|
-
desktop,
|
|
176
|
-
flowId,
|
|
177
|
-
});
|
|
223
|
+
return googleOAuthErrorResponse(
|
|
224
|
+
event,
|
|
225
|
+
Object.assign(new Error(errorDesc), { oauthErrorCode: googleError }),
|
|
226
|
+
{ desktop, flowId },
|
|
227
|
+
);
|
|
178
228
|
}
|
|
179
229
|
|
|
180
230
|
const code = query.code as string;
|
|
@@ -183,7 +233,13 @@ export const handleGoogleCallback = defineEventHandler(
|
|
|
183
233
|
return { error: "Missing authorization code" };
|
|
184
234
|
}
|
|
185
235
|
|
|
186
|
-
const {
|
|
236
|
+
const {
|
|
237
|
+
redirectUri,
|
|
238
|
+
owner: stateOwner,
|
|
239
|
+
addAccount,
|
|
240
|
+
returnUrl,
|
|
241
|
+
desktopVerifierHash,
|
|
242
|
+
} = state;
|
|
187
243
|
|
|
188
244
|
// 1. Resolve owner (needs session context, before exchangeCode)
|
|
189
245
|
const { owner, hasProductionSession } = await resolveOAuthOwner(
|
|
@@ -249,7 +305,15 @@ export const handleGoogleCallback = defineEventHandler(
|
|
|
249
305
|
});
|
|
250
306
|
|
|
251
307
|
if (flowId && sessionToken) {
|
|
252
|
-
|
|
308
|
+
if (!desktopVerifierHash) {
|
|
309
|
+
throw new Error("Missing desktop exchange challenge.");
|
|
310
|
+
}
|
|
311
|
+
await setDesktopExchange(
|
|
312
|
+
flowId,
|
|
313
|
+
sessionToken,
|
|
314
|
+
email,
|
|
315
|
+
desktopVerifierHash,
|
|
316
|
+
);
|
|
253
317
|
}
|
|
254
318
|
|
|
255
319
|
// 4. Return platform-appropriate response
|
|
@@ -284,6 +348,7 @@ export const getGoogleAddAccountUrl = defineEventHandler(
|
|
|
284
348
|
}
|
|
285
349
|
try {
|
|
286
350
|
const q = getQuery(event);
|
|
351
|
+
const method = getMethod(event);
|
|
287
352
|
const redirectUri = resolveOAuthRedirectUri(event);
|
|
288
353
|
if (!redirectUri) {
|
|
289
354
|
setResponseStatus(event, 400);
|
|
@@ -295,6 +360,34 @@ export const getGoogleAddAccountUrl = defineEventHandler(
|
|
|
295
360
|
const desktop =
|
|
296
361
|
isElectron(event) || q.desktop === "1" || q.desktop === "true";
|
|
297
362
|
const flowId = desktop ? (q.flow_id as string) || undefined : undefined;
|
|
363
|
+
if (method === "POST" && (!desktop || !flowId)) {
|
|
364
|
+
setResponseStatus(event, 400);
|
|
365
|
+
return { error: "Invalid desktop exchange challenge." };
|
|
366
|
+
}
|
|
367
|
+
let desktopVerifierHash: string | undefined;
|
|
368
|
+
let desktopBrowserBindingHash: string | undefined;
|
|
369
|
+
if (flowId) {
|
|
370
|
+
if (method !== "POST" || q.redirect !== undefined) {
|
|
371
|
+
setResponseStatus(event, 400);
|
|
372
|
+
return { error: "Invalid desktop exchange challenge." };
|
|
373
|
+
}
|
|
374
|
+
const verifier = getHeader(event, "x-agent-native-desktop-verifier");
|
|
375
|
+
if (!verifier || q.verifier !== undefined) {
|
|
376
|
+
setResponseStatus(event, 400);
|
|
377
|
+
return { error: "Invalid desktop exchange challenge." };
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
desktopBrowserBindingHash = prepareDesktopOAuthBrowserBinding(event);
|
|
381
|
+
desktopVerifierHash = await registerDesktopExchange(
|
|
382
|
+
flowId,
|
|
383
|
+
verifier,
|
|
384
|
+
desktopBrowserBindingHash,
|
|
385
|
+
);
|
|
386
|
+
} catch {
|
|
387
|
+
setResponseStatus(event, 400);
|
|
388
|
+
return { error: "Invalid desktop exchange challenge." };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
298
391
|
const state = encodeOAuthState({
|
|
299
392
|
redirectUri,
|
|
300
393
|
owner: session.email,
|
|
@@ -302,6 +395,8 @@ export const getGoogleAddAccountUrl = defineEventHandler(
|
|
|
302
395
|
addAccount: true,
|
|
303
396
|
app: OAUTH_STATE_APP_ID,
|
|
304
397
|
flowId,
|
|
398
|
+
desktopVerifierHash,
|
|
399
|
+
desktopBrowserBindingHash,
|
|
305
400
|
});
|
|
306
401
|
const url = await getAuthUrl(
|
|
307
402
|
undefined,
|
|
@@ -333,22 +428,28 @@ export const handleGoogleAddAccountCallback = defineEventHandler(
|
|
|
333
428
|
);
|
|
334
429
|
desktop = state.desktop ?? false;
|
|
335
430
|
flowId = state.flowId;
|
|
431
|
+
if (
|
|
432
|
+
flowId &&
|
|
433
|
+
(!state.desktopVerifierHash ||
|
|
434
|
+
!state.desktopBrowserBindingHash ||
|
|
435
|
+
!matchesDesktopOAuthBrowserBinding(
|
|
436
|
+
event,
|
|
437
|
+
state.desktopBrowserBindingHash,
|
|
438
|
+
))
|
|
439
|
+
) {
|
|
440
|
+
throw new Error("Desktop OAuth browser binding is invalid.");
|
|
441
|
+
}
|
|
336
442
|
|
|
337
443
|
// Handle Google authorization errors (e.g. user denied access, invalid client)
|
|
338
444
|
const googleError = query.error as string | undefined;
|
|
339
445
|
if (googleError) {
|
|
340
446
|
const errorDesc =
|
|
341
447
|
(query.error_description as string | undefined) || googleError;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
: `Connection failed: ${errorDesc}`;
|
|
348
|
-
return googleOAuthErrorResponse(event, new Error(userMessage), {
|
|
349
|
-
desktop,
|
|
350
|
-
flowId,
|
|
351
|
-
});
|
|
448
|
+
return googleOAuthErrorResponse(
|
|
449
|
+
event,
|
|
450
|
+
Object.assign(new Error(errorDesc), { oauthErrorCode: googleError }),
|
|
451
|
+
{ desktop, flowId },
|
|
452
|
+
);
|
|
352
453
|
}
|
|
353
454
|
|
|
354
455
|
const { redirectUri, owner: stateOwner } = state;
|
|
@@ -6,8 +6,11 @@ import { isSelfAddressedThread } from "@shared/self-notes.js";
|
|
|
6
6
|
import type { EmailMessage } from "@shared/types.js";
|
|
7
7
|
|
|
8
8
|
export const VIEW_QUERIES: Record<string, string> = {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// Keep sent replies in a matching Gmail thread. The local thread filter
|
|
10
|
+
// below excludes sent-only threads while preserving received inbox threads
|
|
11
|
+
// whose latest message was sent by the user.
|
|
12
|
+
inbox: "in:inbox",
|
|
13
|
+
unread: "is:unread in:inbox",
|
|
11
14
|
starred: "is:starred",
|
|
12
15
|
sent: "in:sent",
|
|
13
16
|
drafts: "in:drafts",
|
package/corpus/templates/mail/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "../../../../_agent-native/google/add-account/auth-url.post.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "../../../_agent-native/google/auth-url.post.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getGoogleAddAccountUrl as default } from "../../../../handlers/google-auth.js";
|